mirror of https://github.com/oxen-io/oxen-core.git
355 lines
14 KiB
C++
355 lines
14 KiB
C++
// Copyright (c) 2014-2019, The Monero Project
|
|
// Copyright (c) 2018, The Loki Project
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without modification, are
|
|
// permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
|
// conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
|
// of conditions and the following disclaimer in the documentation and/or other
|
|
// materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
|
// used to endorse or promote products derived from this software without specific
|
|
// prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
//
|
|
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
|
|
|
#include "checkpoints.h"
|
|
|
|
#include "common/dns_utils.h"
|
|
#include "string_tools.h"
|
|
#include "storages/portable_storage_template_helper.h" // epee json include
|
|
#include "serialization/keyvalue_serialization.h"
|
|
#include "cryptonote_core/service_node_rules.h"
|
|
#include <vector>
|
|
#include "syncobj.h"
|
|
|
|
using namespace epee;
|
|
|
|
#include "common/loki_integration_test_hooks.h"
|
|
|
|
#undef LOKI_DEFAULT_LOG_CATEGORY
|
|
#define LOKI_DEFAULT_LOG_CATEGORY "checkpoints"
|
|
|
|
namespace cryptonote
|
|
{
|
|
/**
|
|
* @brief struct for loading a checkpoint from json
|
|
*/
|
|
struct t_hashline
|
|
{
|
|
uint64_t height; //!< the height of the checkpoint
|
|
std::string hash; //!< the hash for the checkpoint
|
|
BEGIN_KV_SERIALIZE_MAP()
|
|
KV_SERIALIZE(height)
|
|
KV_SERIALIZE(hash)
|
|
END_KV_SERIALIZE_MAP()
|
|
};
|
|
|
|
/**
|
|
* @brief struct for loading many checkpoints from json
|
|
*/
|
|
struct t_hash_json {
|
|
std::vector<t_hashline> hashlines; //!< the checkpoint lines from the file
|
|
BEGIN_KV_SERIALIZE_MAP()
|
|
KV_SERIALIZE(hashlines)
|
|
END_KV_SERIALIZE_MAP()
|
|
};
|
|
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::add_checkpoint(uint64_t height, const std::string& hash_str)
|
|
{
|
|
crypto::hash h = crypto::null_hash;
|
|
bool r = epee::string_tools::hex_to_pod(hash_str, h);
|
|
CHECK_AND_ASSERT_MES(r, false, "Failed to parse checkpoint hash string into binary representation!");
|
|
|
|
CRITICAL_REGION_LOCAL(m_lock);
|
|
if (m_points.count(height)) // return false if adding at a height we already have AND the hash is different
|
|
{
|
|
checkpoint_t const &checkpoint = m_points[height];
|
|
crypto::hash const &curr_hash = checkpoint.block_hash;
|
|
CHECK_AND_ASSERT_MES(h == curr_hash, false, "Checkpoint at given height already exists, and hash for new checkpoint was different!");
|
|
}
|
|
else
|
|
{
|
|
checkpoint_t checkpoint = {};
|
|
checkpoint.type = checkpoint_type::predefined_or_dns;
|
|
checkpoint.block_hash = h;
|
|
m_points[height] = checkpoint;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
static bool add_vote_if_unique(checkpoint_t &checkpoint, service_nodes::checkpoint_vote const &vote)
|
|
{
|
|
CHECK_AND_ASSERT_MES(checkpoint.block_hash == vote.block_hash,
|
|
false,
|
|
"Developer error: Add vote if unique should only be called when the vote hash and checkpoint hash match");
|
|
|
|
// TODO(doyle): Factor this out, a similar function exists in service node deregister
|
|
CHECK_AND_ASSERT_MES(vote.voters_quorum_index < service_nodes::QUORUM_SIZE,
|
|
false,
|
|
"Vote is indexing out of bounds");
|
|
|
|
const auto signature_it = std::find_if(checkpoint.signatures.begin(), checkpoint.signatures.end(), [&vote](service_nodes::voter_to_signature const &check) {
|
|
return vote.voters_quorum_index == check.quorum_index;
|
|
});
|
|
|
|
if (signature_it == checkpoint.signatures.end())
|
|
{
|
|
service_nodes::voter_to_signature new_voter_to_signature = {};
|
|
new_voter_to_signature.quorum_index = vote.voters_quorum_index;
|
|
new_voter_to_signature.signature = vote.signature;
|
|
checkpoint.signatures.push_back(new_voter_to_signature);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::add_checkpoint_vote(service_nodes::checkpoint_vote const &vote)
|
|
{
|
|
// TODO(doyle): Always accept votes. Later on, we should only accept votes up
|
|
// to a certain point, so that eventually once a certain number of blocks
|
|
// have transpired, we can go through all our "seen" votes and deregister
|
|
// anyone who has not participated in voting by that point.
|
|
|
|
#if 0
|
|
uint64_t newest_checkpoint_height = get_max_height();
|
|
if (vote.block_height < newest_checkpoint_height)
|
|
return true;
|
|
#endif
|
|
|
|
CRITICAL_REGION_LOCAL(m_lock);
|
|
std::array<int, service_nodes::QUORUM_SIZE> unique_vote_set = {};
|
|
auto pre_existing_checkpoint_it = m_points.find(vote.block_height);
|
|
if (pre_existing_checkpoint_it != m_points.end())
|
|
{
|
|
checkpoint_t &checkpoint = pre_existing_checkpoint_it->second;
|
|
if (checkpoint.type == checkpoint_type::predefined_or_dns)
|
|
return true;
|
|
|
|
if (checkpoint.block_hash == vote.block_hash)
|
|
{
|
|
bool added = add_vote_if_unique(checkpoint, vote);
|
|
return added;
|
|
}
|
|
|
|
for (service_nodes::voter_to_signature const &vote_to_sig : checkpoint.signatures)
|
|
{
|
|
if (vote_to_sig.quorum_index > unique_vote_set.size()) // TODO(doyle): Sanity check, remove once universal vote verifying function is written
|
|
return false;
|
|
++unique_vote_set[vote_to_sig.quorum_index];
|
|
}
|
|
|
|
}
|
|
|
|
// TODO(doyle): Double work. Factor into a generic vote checker as we already have one in service node deregister
|
|
std::vector<checkpoint_t> &candidate_checkpoints = m_staging_points[vote.block_height];
|
|
std::vector<checkpoint_t>::iterator curr_checkpoint = candidate_checkpoints.end();
|
|
for (auto it = candidate_checkpoints.begin(); it != candidate_checkpoints.end(); it++)
|
|
{
|
|
checkpoint_t const &checkpoint = *it;
|
|
if (checkpoint.block_hash == vote.block_hash)
|
|
curr_checkpoint = it;
|
|
|
|
for (service_nodes::voter_to_signature const &vote_to_sig : checkpoint.signatures)
|
|
{
|
|
if (vote_to_sig.quorum_index > unique_vote_set.size())
|
|
return false;
|
|
|
|
if (++unique_vote_set[vote_to_sig.quorum_index] > 1)
|
|
{
|
|
// NOTE: Voter is trying to vote twice
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (curr_checkpoint == candidate_checkpoints.end())
|
|
{
|
|
checkpoint_t new_checkpoint = {};
|
|
new_checkpoint.type = checkpoint_type::service_node;
|
|
new_checkpoint.block_hash = vote.block_hash;
|
|
candidate_checkpoints.push_back(new_checkpoint);
|
|
curr_checkpoint = (candidate_checkpoints.end() - 1);
|
|
}
|
|
|
|
if (add_vote_if_unique(*curr_checkpoint, vote))
|
|
{
|
|
if (curr_checkpoint->signatures.size() > service_nodes::MIN_VOTES_TO_CHECKPOINT) // TODO(doyle): Quorum SuperMajority variable
|
|
{
|
|
m_points[vote.block_height] = *curr_checkpoint;
|
|
candidate_checkpoints.erase(curr_checkpoint);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::is_in_checkpoint_zone(uint64_t height) const
|
|
{
|
|
return !m_points.empty() && (height <= (--m_points.end())->first);
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::check_block(uint64_t height, const crypto::hash& h, bool* is_a_checkpoint) const
|
|
{
|
|
auto it = m_points.find(height);
|
|
bool found = (it != m_points.end());
|
|
if (is_a_checkpoint) *is_a_checkpoint = found;
|
|
|
|
if(!found)
|
|
return true;
|
|
|
|
checkpoint_t const &checkpoint = it->second;
|
|
bool result = checkpoint.block_hash == h;
|
|
if (result) MINFO ("CHECKPOINT PASSED FOR HEIGHT " << height << " " << h);
|
|
else MWARNING("CHECKPOINT FAILED FOR HEIGHT " << height << ". EXPECTED HASH " << checkpoint.block_hash << "FETCHED HASH: " << h);
|
|
return result;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::is_alternative_block_allowed(uint64_t blockchain_height, uint64_t block_height) const
|
|
{
|
|
if (0 == block_height)
|
|
return false;
|
|
|
|
CRITICAL_REGION_LOCAL(m_lock);
|
|
std::map<uint64_t, checkpoint_t>::const_iterator it = m_points.upper_bound(blockchain_height);
|
|
if (it == m_points.begin()) // Is blockchain_height before the first checkpoint?
|
|
return true;
|
|
|
|
--it; // move the iterator to the first checkpoint that is <= my height
|
|
uint64_t sentinel_reorg_height = it->first;
|
|
if (it->second.type == checkpoint_type::service_node)
|
|
{
|
|
// NOTE: The current checkpoint is a service node checkpoint. Go back
|
|
// 1 checkpoint, which will either be another service node checkpoint or
|
|
// a predefined one.
|
|
|
|
// If it's a service node checkpoint, this is the 2nd newest checkpoint,
|
|
// so we can't reorg past that height. If it's predefined, that's ok as
|
|
// well, we can't reorg past that height so irrespective, always accept
|
|
// the height of this next checkpoint.
|
|
if (it == m_points.begin())
|
|
{
|
|
return true; // NOTE: Only one service node checkpoint recorded, we can override this checkpoint.
|
|
}
|
|
else
|
|
{
|
|
--it;
|
|
sentinel_reorg_height = it->first;
|
|
}
|
|
}
|
|
|
|
bool result = sentinel_reorg_height < block_height;
|
|
return result;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
uint64_t checkpoints::get_max_height() const
|
|
{
|
|
uint64_t result = 0;
|
|
if (m_points.size() > 0)
|
|
{
|
|
auto last_it = m_points.rbegin();
|
|
result = last_it->first;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool checkpoints::check_for_conflicts(const checkpoints& other) const
|
|
{
|
|
for (auto& pt : other.get_points())
|
|
{
|
|
if (m_points.count(pt.first))
|
|
{
|
|
checkpoint_t const &our_checkpoint = m_points.at(pt.first);
|
|
checkpoint_t const &their_checkpoint = pt.second;
|
|
CHECK_AND_ASSERT_MES(our_checkpoint.block_hash == their_checkpoint.block_hash, false, "Checkpoint at given height already exists, and hash for new checkpoint was different!");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool checkpoints::init_default_checkpoints(network_type nettype)
|
|
{
|
|
switch (nettype) {
|
|
case STAGENET:
|
|
break;
|
|
case TESTNET:
|
|
break;
|
|
case FAKECHAIN:
|
|
break;
|
|
case UNDEFINED:
|
|
break;
|
|
case MAINNET:
|
|
#if !defined(LOKI_ENABLE_INTEGRATION_TEST_HOOKS)
|
|
ADD_CHECKPOINT(0, "08ff156d993012b0bdf2816c4bee47c9bbc7930593b70ee02574edddf15ee933");
|
|
ADD_CHECKPOINT(1, "647997953a5ea9b5ab329c2291d4cbb08eed587c287e451eeeb2c79bab9b940f");
|
|
ADD_CHECKPOINT(10, "4a7cd8b9bff380d48d6f3533a5e0509f8589cc77d18218b3f7218846e77738fc");
|
|
ADD_CHECKPOINT(100, "01b8d33a50713ff837f8ad7146021b8e3060e0316b5e4afc407e46cdb50b6760");
|
|
ADD_CHECKPOINT(1000, "5e3b0a1f931885bc0ab1d6ecdc625816576feae29e2f9ac94c5ccdbedb1465ac");
|
|
ADD_CHECKPOINT(86535, "52b7c5a60b97bf1efbf0d63a0aa1a313e8f0abe4627eb354b0c5a73cb1f4391e");
|
|
ADD_CHECKPOINT(97407, "504af73abbaba85a14ddc16634658bf4dcc241dc288b1eaad09e216836b71023");
|
|
ADD_CHECKPOINT(98552, "2058d5c675bd91284f4996435593499c9ab84a5a0f569f57a86cde2e815e57da");
|
|
ADD_CHECKPOINT(144650,"a1ab207afc790675070ecd7aac874eb0691eb6349ea37c44f8f58697a5d6cbc4");
|
|
#endif
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool checkpoints::load_checkpoints_from_json(const std::string &json_hashfile_fullpath)
|
|
{
|
|
boost::system::error_code errcode;
|
|
if (! (boost::filesystem::exists(json_hashfile_fullpath, errcode)))
|
|
{
|
|
LOG_PRINT_L1("Blockchain checkpoints file not found");
|
|
return true;
|
|
}
|
|
|
|
LOG_PRINT_L1("Adding checkpoints from blockchain hashfile");
|
|
|
|
uint64_t prev_max_height = get_max_height();
|
|
LOG_PRINT_L1("Hard-coded max checkpoint height is " << prev_max_height);
|
|
t_hash_json hashes;
|
|
if (!epee::serialization::load_t_from_json_file(hashes, json_hashfile_fullpath))
|
|
{
|
|
MERROR("Error loading checkpoints from " << json_hashfile_fullpath);
|
|
return false;
|
|
}
|
|
for (std::vector<t_hashline>::const_iterator it = hashes.hashlines.begin(); it != hashes.hashlines.end(); )
|
|
{
|
|
uint64_t height;
|
|
height = it->height;
|
|
if (height <= prev_max_height) {
|
|
LOG_PRINT_L1("ignoring checkpoint height " << height);
|
|
} else {
|
|
std::string blockhash = it->hash;
|
|
LOG_PRINT_L1("Adding checkpoint height " << height << ", hash=" << blockhash);
|
|
ADD_CHECKPOINT(height, blockhash);
|
|
}
|
|
++it;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|