08dcd0a640
- Fixed alpine builds (shell: alpine.sh --root for apk) - Added proper dependencies and comprehensive curl cmake options - Disabled gRPC for all release builds (not used by Salvium) - Updated macOS runners to macos-15-intel/macos-15 CMakeLists.txt: - Changed WITH_GRPC default to OFF (merge mining not used) side_chain.cpp: - Cleaned up chain_recovery log formatting
3754 lines
130 KiB
C++
3754 lines
130 KiB
C++
/*
|
|
* This file is part of the Monero P2Pool <https://github.com/SChernykh/p2pool>
|
|
* Copyright (c) 2021-2025 SChernykh <https://github.com/SChernykh>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "p2pool.h"
|
|
#include "side_chain.h"
|
|
#include "pool_block.h"
|
|
#include "wallet.h"
|
|
#include "block_template.h"
|
|
#ifdef WITH_RANDOMX
|
|
#include "randomx.h"
|
|
#include "dataset.hpp"
|
|
#include "configuration.h"
|
|
#include "intrin_portable.h"
|
|
#endif
|
|
#include "keccak.h"
|
|
#include "p2p_server.h"
|
|
#include "stratum_server.h"
|
|
#include "params.h"
|
|
#include "json_parsers.h"
|
|
#include "crypto.h"
|
|
#include "hardforks/hardforks.h"
|
|
#ifdef WITH_REDIS
|
|
#include "redis_storage.h"
|
|
#endif
|
|
#include <fstream>
|
|
|
|
#if !defined(_MSC_VER) || !defined(__cppcheck__)
|
|
#include <rapidjson/document.h>
|
|
#include <rapidjson/istreamwrapper.h>
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <numeric>
|
|
|
|
LOG_CATEGORY(SideChain)
|
|
|
|
static constexpr uint64_t MIN_DIFFICULTY = 10000;
|
|
static constexpr size_t UNCLE_BLOCK_DEPTH = 3;
|
|
|
|
static_assert(1 <= UNCLE_BLOCK_DEPTH && UNCLE_BLOCK_DEPTH <= 10, "Invalid UNCLE_BLOCK_DEPTH");
|
|
|
|
static constexpr uint64_t MONERO_BLOCK_TIME = 120;
|
|
|
|
namespace p2pool {
|
|
|
|
// Consensus ID for salvium_main: computed from "mainnet\0salvium_main\0\010\0100000\02160\020\0"
|
|
static constexpr uint8_t default_consensus_id[HASH_SIZE] = { 4,45,231,71,15,85,219,190,194,167,8,240,43,43,125,49,227,192,250,144,117,138,59,226,222,163,164,69,170,215,106,74 };
|
|
static constexpr uint8_t mini_consensus_id[HASH_SIZE] = { 83,65,76,77,149,174,199,250,66,80,189,18,108,216,194,220,136,23,63,24,64,113,221,44,219,86,39,163,53,24,126,196 };
|
|
static constexpr uint8_t nano_consensus_id[HASH_SIZE] = { 83,65,76,78,210,226,114,99,250,145,221,96,13,216,23,63,104,53,129,168,244,80,141,138,157,250,50,54,37,189,5,89 };
|
|
|
|
NetworkType SideChain::s_networkType = NetworkType::Invalid;
|
|
|
|
SideChain::SideChain(p2pool* pool, NetworkType type, const char* pool_name, const Wallet* dev_wallet)
|
|
: m_pool(pool)
|
|
, m_chainTip{ nullptr }
|
|
, m_seenWalletsLastPruneTime(0)
|
|
, m_poolName(pool_name ? pool_name : "salvium_main")
|
|
, m_devWallet(dev_wallet)
|
|
, m_targetBlockTime(10)
|
|
, m_minDifficulty(MIN_DIFFICULTY, 0)
|
|
, m_chainWindowSize(2160)
|
|
, m_unclePenalty(20)
|
|
, m_precalcFinished(false)
|
|
#ifdef DEV_TEST_SYNC
|
|
, m_firstPruneTime(0)
|
|
#endif
|
|
, m_externalBlockFailures(0)
|
|
{
|
|
if (s_networkType == NetworkType::Invalid) {
|
|
s_networkType = type;
|
|
}
|
|
else if (s_networkType != type) {
|
|
LOGERR(1, "can't run both " << s_networkType << " and " << type << " at the same time");
|
|
PANIC_STOP();
|
|
}
|
|
|
|
LOGINFO(1, log::LightCyan() << "network type = " << type);
|
|
|
|
if (m_poolName == "nano") {
|
|
m_targetBlockTime = 30;
|
|
m_unclePenalty = 10;
|
|
}
|
|
|
|
if (m_pool && !load_config(m_pool->params().m_sidechainConfig)) {
|
|
PANIC_STOP();
|
|
}
|
|
|
|
if (!check_config()) {
|
|
PANIC_STOP();
|
|
}
|
|
|
|
m_curDifficulty = m_minDifficulty;
|
|
|
|
uv_rwlock_init_checked(&m_sidechainLock);
|
|
uv_mutex_init_checked(&m_incomingBlocksLock);
|
|
uv_rwlock_init_checked(&m_curDifficultyLock);
|
|
uv_rwlock_init_checked(&m_watchBlockLock);
|
|
|
|
uv_rwlock_init(&m_checkpointsLock);
|
|
|
|
m_difficultyData.reserve(m_chainWindowSize);
|
|
|
|
LOGINFO(1, "generating consensus ID");
|
|
|
|
char buf[log::Stream::BUF_SIZE + 1];
|
|
// cppcheck-suppress uninitvar
|
|
log::Stream s(buf);
|
|
|
|
s << s_networkType << '\0'
|
|
<< m_poolName << '\0'
|
|
<< m_poolPassword << '\0'
|
|
<< m_targetBlockTime << '\0'
|
|
<< m_minDifficulty << '\0'
|
|
<< m_chainWindowSize << '\0'
|
|
<< m_unclePenalty << '\0';
|
|
|
|
// Format: network_type\0pool_name\0password\0block_time\0min_diff\0window_size\0uncle_penalty\0
|
|
// Note: Network type is serialized as "mainnet"/"testnet"/"stagenet" by log::Stream::Entry<NetworkType>
|
|
constexpr char default_config[] = "mainnet\0" "salvium_main\0" "\0" "10\0" "10000\0" "2160\0" "20\0";
|
|
constexpr char mini_config[] = "mainnet\0" "salvium_mini\0" "\0" "10\0" "10000\0" "2160\0" "20\0";
|
|
constexpr char nano_config[] = "mainnet\0" "salvium_nano\0" "\0" "30\0" "10000\0" "2160\0" "10\0";
|
|
|
|
// Hardcoded default consensus ID
|
|
if ((s.m_pos == sizeof(default_config) - 1) && (memcmp(buf, default_config, sizeof(default_config) - 1) == 0)) {
|
|
m_consensusId.assign(default_consensus_id, default_consensus_id + HASH_SIZE);
|
|
}
|
|
// Hardcoded mini consensus ID
|
|
else if ((s.m_pos == sizeof(mini_config) - 1) && (memcmp(buf, mini_config, sizeof(mini_config) - 1) == 0)) {
|
|
m_consensusId.assign(mini_consensus_id, mini_consensus_id + HASH_SIZE);
|
|
}
|
|
// Hardcoded nano consensus ID
|
|
else if ((s.m_pos == sizeof(nano_config) - 1) && (memcmp(buf, nano_config, sizeof(nano_config) - 1) == 0)) {
|
|
m_consensusId.assign(nano_consensus_id, nano_consensus_id + HASH_SIZE);
|
|
}
|
|
else {
|
|
#ifdef WITH_RANDOMX
|
|
const randomx_flags flags = randomx_get_flags();
|
|
randomx_cache* cache = randomx_alloc_cache(flags | RANDOMX_FLAG_LARGE_PAGES);
|
|
if (!cache) {
|
|
LOGWARN(1, "couldn't allocate RandomX cache using large pages");
|
|
cache = randomx_alloc_cache(flags);
|
|
if (!cache) {
|
|
LOGERR(1, "couldn't allocate RandomX cache, aborting");
|
|
PANIC_STOP();
|
|
}
|
|
}
|
|
|
|
randomx_init_cache(cache, buf, s.m_pos);
|
|
|
|
// Intentionally not a power of 2
|
|
constexpr size_t scratchpad_size = 1009;
|
|
|
|
rx_vec_i128* scratchpad = reinterpret_cast<rx_vec_i128*>(cache->memory);
|
|
rx_vec_i128* scratchpad_end = scratchpad + scratchpad_size;
|
|
rx_vec_i128* scratchpad_ptr = scratchpad;
|
|
rx_vec_i128* cache_ptr = scratchpad_end;
|
|
|
|
for (uint64_t i = scratchpad_size, n = static_cast<uint64_t>(RANDOMX_ARGON_MEMORY * 1024) / sizeof(rx_vec_i128); i < n; ++i) {
|
|
*scratchpad_ptr = rx_xor_vec_i128(*scratchpad_ptr, *cache_ptr);
|
|
++cache_ptr;
|
|
++scratchpad_ptr;
|
|
if (scratchpad_ptr == scratchpad_end) {
|
|
scratchpad_ptr = scratchpad;
|
|
}
|
|
}
|
|
|
|
hash id;
|
|
keccak(reinterpret_cast<uint8_t*>(scratchpad), static_cast<int>(scratchpad_size * sizeof(rx_vec_i128)), id.h);
|
|
randomx_release_cache(cache);
|
|
m_consensusId.assign(id.h, id.h + HASH_SIZE);
|
|
#else
|
|
LOGERR(1, "Can't calculate consensus ID without RandomX library");
|
|
PANIC_STOP();
|
|
#endif
|
|
}
|
|
|
|
|
|
s.m_pos = 0;
|
|
s << log::hex_buf(m_consensusId.data(), m_consensusId.size()) << '\0';
|
|
|
|
// Hide most consensus ID bytes, we only want it on screen to show that we're on the right sidechain
|
|
memset(buf + 8, '*', HASH_SIZE * 2 - 16);
|
|
m_consensusIdDisplayStr = buf;
|
|
|
|
LOGINFO(1, "consensus ID = " << log::LightCyan() << m_consensusIdDisplayStr.c_str());
|
|
|
|
memcpy(m_consensusHash.h, m_consensusId.data(), HASH_SIZE);
|
|
|
|
uv_cond_init_checked(&m_precalcJobsCond);
|
|
uv_mutex_init_checked(&m_precalcJobsMutex);
|
|
m_precalcJobs.reserve(16);
|
|
|
|
uint32_t numThreads = std::thread::hardware_concurrency();
|
|
|
|
// Leave 1 CPU core free from worker threads
|
|
if (numThreads > 1) {
|
|
--numThreads;
|
|
}
|
|
|
|
// Use between 1 and 8 threads
|
|
if (numThreads < 1) numThreads = 1;
|
|
|
|
// Don't limit thread count when debugging because debug builds are slow
|
|
#ifndef P2POOL_DEBUGGING
|
|
if (numThreads > 8) numThreads = 8;
|
|
#endif
|
|
|
|
LOGINFO(4, "running " << numThreads << " pre-calculation workers");
|
|
|
|
m_precalcWorkers.reserve(numThreads);
|
|
for (uint32_t i = 0; i < numThreads; ++i) {
|
|
m_precalcWorkers.emplace_back(&SideChain::precalc_worker, this);
|
|
}
|
|
|
|
m_uniquePrecalcInputs = new unordered_set<size_t>();
|
|
m_uniquePrecalcInputs->reserve(1 << 18);
|
|
|
|
load_checkpoints();
|
|
}
|
|
|
|
SideChain::~SideChain()
|
|
{
|
|
save_checkpoints();
|
|
finish_precalc();
|
|
|
|
uv_rwlock_destroy(&m_sidechainLock);
|
|
uv_mutex_destroy(&m_incomingBlocksLock);
|
|
uv_rwlock_destroy(&m_curDifficultyLock);
|
|
uv_rwlock_destroy(&m_watchBlockLock);
|
|
|
|
uv_rwlock_destroy(&m_checkpointsLock);
|
|
|
|
for (const auto& it : m_blocksById) {
|
|
delete it.second;
|
|
}
|
|
|
|
s_networkType = NetworkType::Invalid;
|
|
}
|
|
|
|
bool SideChain::fill_sidechain_data(PoolBlock& block, std::vector<MinerShare>& shares) const
|
|
{
|
|
block.m_uncles.clear();
|
|
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
const PoolBlock* tip = m_chainTip;
|
|
|
|
if (!tip) {
|
|
// If we've learned of a peer's genesis, wait for their chain instead of creating our own
|
|
if (!m_adoptedGenesisId.empty()) {
|
|
const uint64_t elapsed = seconds_since_epoch() - m_adoptedGenesisTime;
|
|
const uint64_t timeout = 90;
|
|
|
|
if (elapsed < timeout) {
|
|
// Log every 15 seconds so user knows we're working
|
|
if ((elapsed % 15) < 2) {
|
|
LOGINFO(3, "Waiting for peer's genesis block " << m_adoptedGenesisId
|
|
<< " (" << elapsed << "s / " << timeout << "s)");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Timeout - give up on peer's genesis and create our own
|
|
LOGWARN(3, "Timeout waiting for peer's genesis block after " << elapsed
|
|
<< "s, creating own genesis");
|
|
m_adoptedGenesisId = {};
|
|
m_adoptedGenesisTimestamp = 0;
|
|
m_adoptedGenesisHeight = 0;
|
|
m_adoptedGenesisTime = 0;
|
|
}
|
|
|
|
// Don't create genesis block until initial peer sync has been attempted
|
|
// This prevents nodes from creating independent chains when starting simultaneously
|
|
if (!m_precalcFinished.load() && m_pool) {
|
|
const P2PServer* p2p = m_pool->p2p_server();
|
|
if (p2p && (p2p->peer_list_size() > 0 || p2p->num_connections() > 0)) {
|
|
LOGINFO(5, "Waiting for initial peer sync before creating genesis block");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// No peers with existing chain - create our own genesis
|
|
LOGINFO(3, "Creating new genesis block (no peer chain found)");
|
|
m_genesisDecisionMade = true;
|
|
|
|
block.m_parent = {};
|
|
block.m_sidechainHeight = 0;
|
|
|
|
block.m_difficulty = m_minDifficulty;
|
|
block.m_cumulativeDifficulty = m_minDifficulty;
|
|
block.m_txkeySecSeed = m_consensusHash;
|
|
get_tx_keys(block.m_txkeyPub, block.m_txkeySec, block.m_txkeySecSeed, block.m_prevId);
|
|
|
|
return get_shares(&block, shares);
|
|
}
|
|
|
|
block.m_txkeySecSeed = (block.m_prevId == tip->m_prevId) ? tip->m_txkeySecSeed : tip->calculate_tx_key_seed();
|
|
get_tx_keys(block.m_txkeyPub, block.m_txkeySec, block.m_txkeySecSeed, block.m_prevId);
|
|
|
|
block.m_parent = tip->m_sidechainId;
|
|
block.m_sidechainHeight = tip->m_sidechainHeight + 1;
|
|
|
|
// Collect uncles from 3 previous block heights
|
|
|
|
// First get a list of already mined blocks at these heights
|
|
std::vector<hash> mined_blocks;
|
|
mined_blocks.reserve(UNCLE_BLOCK_DEPTH * 2 + 1);
|
|
|
|
const PoolBlock* tmp = tip;
|
|
for (uint64_t i = 0, n = std::min<uint64_t>(UNCLE_BLOCK_DEPTH, tip->m_sidechainHeight + 1); tmp && (i < n); ++i) {
|
|
mined_blocks.push_back(tmp->m_sidechainId);
|
|
mined_blocks.insert(mined_blocks.end(), tmp->m_uncles.begin(), tmp->m_uncles.end());
|
|
tmp = get_parent(tmp);
|
|
}
|
|
|
|
for (uint64_t i = 0, n = std::min<uint64_t>(UNCLE_BLOCK_DEPTH, tip->m_sidechainHeight + 1); i < n; ++i) {
|
|
auto it = m_blocksByHeight.find(tip->m_sidechainHeight - i);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
for (const PoolBlock* uncle : it->second) {
|
|
// Only add verified and valid blocks
|
|
if (!uncle || !uncle->m_verified || uncle->m_invalid) {
|
|
continue;
|
|
}
|
|
|
|
// Only add it if it hasn't been mined already
|
|
if (std::find(mined_blocks.begin(), mined_blocks.end(), uncle->m_sidechainId) != mined_blocks.end()) {
|
|
continue;
|
|
}
|
|
|
|
// Only add it if it's on the same chain
|
|
bool same_chain = false;
|
|
do {
|
|
tmp = tip;
|
|
while (tmp && (tmp->m_sidechainHeight > uncle->m_sidechainHeight)) {
|
|
tmp = get_parent(tmp);
|
|
}
|
|
if (!tmp || (tmp->m_sidechainHeight < uncle->m_sidechainHeight)) {
|
|
break;
|
|
}
|
|
const PoolBlock* tmp2 = uncle;
|
|
for (size_t j = 0; (j < UNCLE_BLOCK_DEPTH) && tmp && tmp2 && (tmp->m_sidechainHeight + UNCLE_BLOCK_DEPTH >= block.m_sidechainHeight); ++j) {
|
|
if (tmp->m_parent == tmp2->m_parent) {
|
|
same_chain = true;
|
|
break;
|
|
}
|
|
tmp = get_parent(tmp);
|
|
tmp2 = get_parent(tmp2);
|
|
}
|
|
} while (0);
|
|
|
|
if (same_chain) {
|
|
block.m_uncles.emplace_back(uncle->m_sidechainId);
|
|
LOGINFO(4, "block template at height " << block.m_sidechainHeight <<
|
|
": added " << uncle->m_sidechainId <<
|
|
" (height " << uncle->m_sidechainHeight <<
|
|
") as an uncle block, depth " << block.m_sidechainHeight - uncle->m_sidechainHeight);
|
|
}
|
|
else {
|
|
LOGINFO(4, "block template at height " << block.m_sidechainHeight <<
|
|
": uncle block " << uncle->m_sidechainId <<
|
|
" (height " << uncle->m_sidechainHeight <<
|
|
") is not on the same chain, depth " << block.m_sidechainHeight - uncle->m_sidechainHeight);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort uncles and remove duplicates
|
|
if (block.m_uncles.size() > 1) {
|
|
std::sort(block.m_uncles.begin(), block.m_uncles.end());
|
|
block.m_uncles.erase(std::unique(block.m_uncles.begin(), block.m_uncles.end()), block.m_uncles.end());
|
|
}
|
|
|
|
block.m_difficulty = difficulty();
|
|
block.m_cumulativeDifficulty = tip->m_cumulativeDifficulty + block.m_difficulty;
|
|
|
|
for (const hash& uncle_id : block.m_uncles) {
|
|
auto it = m_blocksById.find(uncle_id);
|
|
if (it == m_blocksById.end()) {
|
|
LOGERR(1, "block template has an unknown uncle block " << uncle_id << ". Fix the code!");
|
|
continue;
|
|
}
|
|
block.m_cumulativeDifficulty += it->second->m_difficulty;
|
|
}
|
|
|
|
return get_shares(&block, shares);
|
|
}
|
|
|
|
P2PServer* SideChain::p2pServer() const
|
|
{
|
|
return m_pool ? m_pool->p2p_server() : nullptr;
|
|
}
|
|
|
|
bool SideChain::get_shares(const PoolBlock* tip, std::vector<MinerShare>& shares, uint64_t* bottom_height, bool quiet) const
|
|
{
|
|
if (tip->m_txkeySecSeed.empty()) {
|
|
LOGERR(1, "tx key seed is not set, fix the code!");
|
|
}
|
|
|
|
const int L = quiet ? 6 : 3;
|
|
|
|
// Collect shares from each block in the PPLNS window, starting from the "tip"
|
|
|
|
uint64_t block_depth = 0;
|
|
const PoolBlock* cur = tip;
|
|
|
|
difficulty_type mainchain_diff
|
|
#ifdef P2POOL_UNIT_TESTS
|
|
= m_testMainChainDiff
|
|
#endif
|
|
;
|
|
|
|
if (m_pool && !tip->m_parent.empty()) {
|
|
const uint64_t h = tip->m_txinGenHeight;
|
|
if (!m_pool->get_difficulty_at_height(h, mainchain_diff)) {
|
|
LOGWARN(L, "get_shares: couldn't get mainchain difficulty for height = " << h);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Dynamic PPLNS window starting from v2
|
|
// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
|
|
const difficulty_type max_pplns_weight = mainchain_diff * 2;
|
|
difficulty_type pplns_weight;
|
|
|
|
unordered_set<MinerShare> shares_set;
|
|
shares_set.reserve(m_chainWindowSize * 2);
|
|
|
|
do {
|
|
difficulty_type cur_weight = cur->m_difficulty;
|
|
|
|
for (const hash& uncle_id : cur->m_uncles) {
|
|
auto it = m_blocksById.find(uncle_id);
|
|
if (it == m_blocksById.end()) {
|
|
LOGWARN(L, "get_shares: can't find uncle block at height = " << cur->m_sidechainHeight << ", id = " << uncle_id);
|
|
LOGWARN(L, "get_shares: can't calculate shares for block at height = " << tip->m_sidechainHeight << ", id = " << tip->m_sidechainId << ", mainchain height = " << tip->m_txinGenHeight);
|
|
return false;
|
|
}
|
|
|
|
const PoolBlock* uncle = it->second;
|
|
|
|
// Skip uncles which are already out of PPLNS window
|
|
if (tip->m_sidechainHeight - uncle->m_sidechainHeight >= m_chainWindowSize) {
|
|
continue;
|
|
}
|
|
|
|
// Take some % of uncle's weight into this share
|
|
const difficulty_type uncle_penalty = uncle->m_difficulty * m_unclePenalty / 100;
|
|
const difficulty_type uncle_weight = uncle->m_difficulty - uncle_penalty;
|
|
const difficulty_type new_pplns_weight = pplns_weight + uncle_weight;
|
|
|
|
// Skip uncles that push PPLNS weight above the limit
|
|
if (new_pplns_weight > max_pplns_weight) {
|
|
continue;
|
|
}
|
|
|
|
cur_weight += uncle_penalty;
|
|
|
|
auto result = shares_set.emplace(uncle_weight, &uncle->m_minerWallet);
|
|
if (!result.second) {
|
|
result.first->m_weight += uncle_weight;
|
|
}
|
|
pplns_weight = new_pplns_weight;
|
|
}
|
|
|
|
// Always add non-uncle shares even if PPLNS weight goes above the limit
|
|
auto result = shares_set.emplace(cur_weight, &cur->m_minerWallet);
|
|
if (!result.second) {
|
|
result.first->m_weight += cur_weight;
|
|
}
|
|
pplns_weight += cur_weight;
|
|
|
|
LOGINFO(6, "get_shares: height=" << cur->m_sidechainHeight << " wallet=" << cur->m_minerWallet << " pplns_weight=" << pplns_weight << " max=" << max_pplns_weight << " mainchain_h=" << tip->m_txinGenHeight << " depth=" << block_depth);
|
|
|
|
// One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty
|
|
if (pplns_weight > max_pplns_weight) {
|
|
break;
|
|
}
|
|
|
|
++block_depth;
|
|
if (block_depth >= m_chainWindowSize) {
|
|
break;
|
|
}
|
|
|
|
// Reached the genesis block so we're done
|
|
if (cur->m_sidechainHeight == 0) {
|
|
break;
|
|
}
|
|
|
|
auto it = m_blocksById.find(cur->m_parent);
|
|
if (it == m_blocksById.end()) {
|
|
LOGWARN(L, "get_shares: can't find parent block at height = " << cur->m_sidechainHeight - 1 << ", id = " << cur->m_parent);
|
|
LOGWARN(L, "get_shares: can't calculate shares for block at height = " << tip->m_sidechainHeight << ", id = " << tip->m_sidechainId << ", mainchain height = " << tip->m_txinGenHeight);
|
|
return false;
|
|
}
|
|
|
|
cur = it->second;
|
|
} while (true);
|
|
|
|
|
|
if (bottom_height) {
|
|
*bottom_height = cur->m_sidechainHeight;
|
|
}
|
|
|
|
shares.assign(shares_set.begin(), shares_set.end());
|
|
std::sort(shares.begin(), shares.end(), [](const auto& a, const auto& b) { return *a.m_wallet < *b.m_wallet; });
|
|
|
|
const uint64_t n = shares.size();
|
|
|
|
// Shuffle shares
|
|
if (n > 1) {
|
|
hash h;
|
|
keccak(tip->m_txkeySecSeed.h, HASH_SIZE, h.h);
|
|
|
|
uint64_t seed = *h.u64();
|
|
if (seed == 0) seed = 1;
|
|
|
|
for (uint64_t i = 0, k; i < n - 1; ++i) {
|
|
seed = xorshift64star(seed);
|
|
umul128(seed, n - i, &k);
|
|
std::swap(shares[i], shares[i + k]);
|
|
}
|
|
}
|
|
|
|
LOGINFO(6, "get_shares: " << n << " unique wallets in PPLNS window");
|
|
return true;
|
|
}
|
|
|
|
bool SideChain::incoming_block_seen(const PoolBlock& block)
|
|
{
|
|
// Check if it's some old block
|
|
const PoolBlock* tip = m_chainTip;
|
|
|
|
if (tip && tip->m_sidechainHeight > block.m_sidechainHeight + m_chainWindowSize * 2 &&
|
|
block.m_cumulativeDifficulty < tip->m_cumulativeDifficulty) {
|
|
return true;
|
|
}
|
|
|
|
const uint64_t cur_time = seconds_since_epoch();
|
|
|
|
// Check if it was received before
|
|
MutexLock lock(m_incomingBlocksLock);
|
|
return !m_incomingBlocks.emplace(block.get_full_id(), cur_time).second;
|
|
}
|
|
|
|
void SideChain::forget_incoming_block(const PoolBlock& block)
|
|
{
|
|
MutexLock lock(m_incomingBlocksLock);
|
|
m_incomingBlocks.erase(block.get_full_id());
|
|
}
|
|
|
|
void SideChain::cleanup_incoming_blocks()
|
|
{
|
|
const uint64_t cur_time = seconds_since_epoch();
|
|
|
|
MutexLock lock(m_incomingBlocksLock);
|
|
|
|
// Forget seen blocks that were added more than 10 minutes ago
|
|
for (auto i = m_incomingBlocks.begin(); i != m_incomingBlocks.end();) {
|
|
if (cur_time < i->second + 10ul * 60ul) {
|
|
++i;
|
|
}
|
|
else {
|
|
i = m_incomingBlocks.erase(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SideChain::add_external_block(PoolBlock& block, std::vector<hash>& missing_blocks)
|
|
{
|
|
if (block.m_difficulty < m_minDifficulty) {
|
|
LOGWARN(3, "add_external_block: block mined by " << block.m_minerWallet << " has invalid difficulty " << block.m_difficulty << ", expected >= " << m_minDifficulty);
|
|
return false;
|
|
}
|
|
|
|
const difficulty_type expected_diff = difficulty();
|
|
bool too_low_diff = (block.m_difficulty < expected_diff);
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
if (m_blocksById.find(block.m_sidechainId) != m_blocksById.end()) {
|
|
LOGINFO(4, "add_external_block: block " << block.m_sidechainId << " is already added");
|
|
return true;
|
|
}
|
|
|
|
// This is mainly an anti-spam measure, not an actual verification step
|
|
if (too_low_diff) {
|
|
// Reduce required diff by 50% (by doubling this block's diff) to account for alternative chains
|
|
difficulty_type diff2 = block.m_difficulty;
|
|
diff2 += block.m_difficulty;
|
|
|
|
const PoolBlock* tip = m_chainTip;
|
|
|
|
for (const PoolBlock* tmp = tip; tmp && (tmp->m_sidechainHeight + m_chainWindowSize > tip->m_sidechainHeight); tmp = get_parent(tmp)) {
|
|
if (diff2 >= tmp->m_difficulty) {
|
|
too_low_diff = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LOGINFO(4, "add_external_block: height = " << block.m_sidechainHeight << ", id = " << block.m_sidechainId << ", mainchain height = " << block.m_txinGenHeight);
|
|
|
|
if (too_low_diff) {
|
|
LOGWARN(4, "add_external_block: block mined by " << block.m_minerWallet << " has too low difficulty " << block.m_difficulty << ", expected >= ~" << expected_diff << ". Ignoring it.");
|
|
return true;
|
|
}
|
|
|
|
// This check is not always possible to perform because of mainchain reorgs
|
|
ChainMain data;
|
|
if (m_pool->chainmain_get_by_hash(block.m_prevId, data)) {
|
|
if (data.height + 1 != block.m_txinGenHeight) {
|
|
LOGWARN(3, "add_external_block mined by " << block.m_minerWallet << ": wrong mainchain height " << block.m_txinGenHeight << ", expected " << data.height + 1);
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
LOGWARN(3, "add_external_block: block is built on top of an unknown mainchain block " << block.m_prevId << ", mainchain reorg might've happened, fetching mainchain height " << block.m_txinGenHeight);
|
|
// Fetch the mainchain block at the claimed height (only from main thread to avoid libuv threading issues)
|
|
if (is_main_thread()) {
|
|
m_pool->fetch_mainchain_block(block.m_txinGenHeight);
|
|
}
|
|
}
|
|
|
|
if (!m_pool->get_seed(block.m_txinGenHeight, block.m_seed)) {
|
|
LOGWARN(3, "add_external_block mined by " << block.m_minerWallet << ": couldn't get seed hash for mainchain height " << block.m_txinGenHeight << ", fetching it from salviumd");
|
|
// Fetch the missing mainchain block on-demand (only from main thread to avoid libuv threading issues)
|
|
if (is_main_thread()) {
|
|
const uint64_t seed_height = p2pool::get_seed_height(block.m_txinGenHeight);
|
|
m_pool->fetch_mainchain_block(seed_height);
|
|
}
|
|
// Always forget so block can be re-processed when mainchain data arrives
|
|
forget_incoming_block(block);
|
|
return false;
|
|
}
|
|
|
|
LOGINFO(6, "DEBUG get_pow_hash: seed=" << block.m_seed << " txinGenHeight=" << block.m_txinGenHeight);
|
|
|
|
if (!block.get_pow_hash(m_pool->hasher(), block.m_txinGenHeight, block.m_seed, block.m_powHash)) {
|
|
LOGWARN(3, "add_external_block: couldn't get PoW hash for height = " << block.m_sidechainHeight << ", mainchain height " << block.m_txinGenHeight << ". Ignoring it.");
|
|
forget_incoming_block(block);
|
|
return true;
|
|
}
|
|
|
|
// Check if it has the correct parent and difficulty to go right to monerod for checking
|
|
MinerData miner_data = m_pool->miner_data();
|
|
if ((block.m_prevId == miner_data.prev_id) && miner_data.difficulty.check_pow(block.m_powHash)) {
|
|
LOGINFO(0, log::LightGreen() << "add_external_block: block " << block.m_sidechainId << " has enough PoW for Salvium network, submitting it");
|
|
m_pool->submit_block_async(block.serialize_mainchain_data());
|
|
}
|
|
else {
|
|
difficulty_type diff;
|
|
if (!m_pool->get_difficulty_at_height(block.m_txinGenHeight, diff)) {
|
|
LOGWARN(3, "add_external_block: couldn't get mainchain difficulty for height = " << block.m_txinGenHeight);
|
|
}
|
|
else if (diff.check_pow(block.m_powHash) && (block.m_txinGenHeight + 2 >= miner_data.height)) {
|
|
LOGINFO(0, log::LightGreen() << "add_external_block: block " << block.m_sidechainId << " has enough PoW for Salvium height " << block.m_txinGenHeight << ", submitting it");
|
|
m_pool->submit_block_async(block.serialize_mainchain_data());
|
|
}
|
|
}
|
|
|
|
LOGINFO(6, "DEBUG PoW check: sidechainHeight=" << block.m_sidechainHeight << " m_difficulty.lo=" << block.m_difficulty.lo << " m_difficulty.hi=" << block.m_difficulty.hi << " m_powHash=" << block.m_powHash);
|
|
|
|
if (!block.m_difficulty.check_pow(block.m_powHash)) {
|
|
LOGWARN(3,
|
|
"add_external_block mined by " << block.m_minerWallet <<
|
|
": not enough PoW for height = " << block.m_sidechainHeight <<
|
|
", id = " << block.m_sidechainId <<
|
|
", nonce = " << block.m_nonce <<
|
|
", mainchain height = " << block.m_txinGenHeight
|
|
);
|
|
|
|
bool not_enough_pow = true;
|
|
|
|
// Calculate the same hash second time to check if it's an unstable hardware that caused this
|
|
hash pow_hash2;
|
|
if (block.get_pow_hash(m_pool->hasher(), block.m_txinGenHeight, block.m_seed, pow_hash2, true) && (pow_hash2 != block.m_powHash)) {
|
|
LOGERR(0, "UNSTABLE HARDWARE DETECTED: Calculated the same hash twice, got different results: " << block.m_powHash << " != " << pow_hash2 << " (sidechain id = " << block.m_sidechainId << ')');
|
|
if (block.m_difficulty.check_pow(pow_hash2)) {
|
|
LOGINFO(3, "add_external_block second result has enough PoW for height = " << block.m_sidechainHeight << ", id = " << block.m_sidechainId);
|
|
not_enough_pow = false;
|
|
}
|
|
}
|
|
|
|
if (not_enough_pow) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_pool->on_external_block(block);
|
|
|
|
bool block_found = false;
|
|
|
|
missing_blocks.clear();
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
if (!block.m_parent.empty() && (m_blocksById.find(block.m_parent) == m_blocksById.end())) {
|
|
missing_blocks.push_back(block.m_parent);
|
|
}
|
|
|
|
for (const hash& h : block.m_uncles) {
|
|
if (!h.empty() && (m_blocksById.find(h) == m_blocksById.end())) {
|
|
missing_blocks.push_back(h);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
WriteLock lock(m_watchBlockLock);
|
|
|
|
if (block.m_merkleRoot == m_watchBlockMerkleRoot) {
|
|
const Wallet& w = m_pool->params().m_miningWallet;
|
|
|
|
const char* who = (block.m_minerWallet == w) ? "you" : "someone else in this p2pool";
|
|
LOGINFO(0, log::LightGreen() << "BLOCK FOUND: main chain block at height " << m_watchBlock.height << " was mined by " << who << BLOCK_FOUND);
|
|
|
|
m_watchBlockMerkleRoot = {};
|
|
data = m_watchBlock;
|
|
block_found = true;
|
|
|
|
const uint64_t payout = block.get_payout(w);
|
|
if (payout) {
|
|
LOGINFO(0, log::LightCyan() << "Your wallet " << log::LightGreen() << m_pool->params().m_displayWallet << log::LightCyan() << " got a payout of " << log::LightGreen() << log::SALAmount(payout) << log::LightCyan() << " in block " << log::LightGreen() << data.height);
|
|
}
|
|
else {
|
|
LOGINFO(0, log::LightCyan() << "Your wallet " << log::LightYellow() << m_pool->params().m_displayWallet << log::LightCyan() << " didn't get a payout in block " << log::LightYellow() << data.height << log::LightCyan() << " because you had no shares in PPLNS window");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (block_found) {
|
|
m_pool->api_update_block_found(&data, &block);
|
|
}
|
|
|
|
const bool added = add_block(block);
|
|
if (added && block.m_verified) {
|
|
if (block.m_invalid) {
|
|
on_block_rejected(&block, "external block validation failed");
|
|
} else {
|
|
on_block_accepted();
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
bool SideChain::add_block(const PoolBlock& block)
|
|
{
|
|
LOGINFO(3, "add_block: height = " << block.m_sidechainHeight <<
|
|
", id = " << block.m_sidechainId <<
|
|
", mainchain height = " << block.m_txinGenHeight <<
|
|
", verified = " << (block.m_verified ? 1 : 0)
|
|
);
|
|
// Extra log for blocks near checkpoint height during recovery
|
|
const uint64_t cp_height = get_latest_checkpoint_height();
|
|
if (block.m_sidechainHeight >= cp_height && block.m_sidechainHeight <= cp_height + 5) {
|
|
LOGINFO(6, "RECOVERY DEBUG: add_block height " << block.m_sidechainHeight
|
|
<< " parent=" << block.m_parent);
|
|
}
|
|
|
|
PoolBlock* new_block = new PoolBlock(block);
|
|
{
|
|
WriteLock lock(m_seenDataLock);
|
|
|
|
m_seenWallets[new_block->m_minerWallet.spend_public_key()] = new_block->m_localTimestamp;
|
|
|
|
auto it = new_block->m_mergeMiningExtra.find(keccak_onion_address_v3);
|
|
if ((it != new_block->m_mergeMiningExtra.end()) && (it->second.size() >= HASH_SIZE)) {
|
|
hash h;
|
|
memcpy(h.h, it->second.data(), HASH_SIZE);
|
|
m_seenOnionPubkeys[h] = new_block->m_localTimestamp;
|
|
}
|
|
|
|
prune_seen_data();
|
|
}
|
|
|
|
WriteLock lock(m_sidechainLock);
|
|
|
|
auto result = m_blocksById.insert({ new_block->m_sidechainId, new_block });
|
|
if (!result.second) {
|
|
const PoolBlock* old_block = result.first->second;
|
|
|
|
LOGWARN(3, "add_block: trying to add the same block twice:"
|
|
<< "\nnew block id = " << new_block->m_sidechainId
|
|
<< ", sidechain height = " << new_block->m_sidechainHeight
|
|
<< ", height = " << new_block->m_txinGenHeight
|
|
<< ", nonce = " << new_block->m_nonce
|
|
<< ", extra_nonce = " << new_block->m_extraNonce
|
|
<< "\nold block id = " << old_block->m_sidechainId
|
|
<< ", sidechain height = " << old_block->m_sidechainHeight
|
|
<< ", height = " << old_block->m_txinGenHeight
|
|
<< ", nonce = " << old_block->m_nonce
|
|
<< ", extra_nonce = " << old_block->m_extraNonce
|
|
);
|
|
|
|
delete new_block;
|
|
return false;
|
|
}
|
|
|
|
m_blocksByHeight[new_block->m_sidechainHeight].push_back(new_block);
|
|
m_blocksByMerkleRoot.insert({ new_block->m_merkleRoot, new_block });
|
|
|
|
update_depths(new_block);
|
|
|
|
if (new_block->m_verified) {
|
|
if (!new_block->m_invalid) {
|
|
update_chain_tip(new_block);
|
|
|
|
// Save it for faster syncing on the next p2pool start
|
|
if (P2PServer* server = p2pServer()) {
|
|
server->store_in_cache(*new_block);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
verify_loop(new_block);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const PoolBlock* SideChain::find_block(const hash& id) const
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
auto it = m_blocksById.find(id);
|
|
if (it != m_blocksById.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const PoolBlock* SideChain::find_block_by_merkle_root(const root_hash& merkle_root) const
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
auto it = m_blocksByMerkleRoot.find(merkle_root);
|
|
if (it != m_blocksByMerkleRoot.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void SideChain::watch_mainchain_block(const ChainMain& data, const hash& possible_merkle_root)
|
|
{
|
|
WriteLock lock(m_watchBlockLock);
|
|
m_watchBlock = data;
|
|
m_watchBlockMerkleRoot = possible_merkle_root;
|
|
}
|
|
|
|
const PoolBlock* SideChain::get_block_blob(const hash& id, std::vector<uint8_t>& blob) const
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
const PoolBlock* block = nullptr;
|
|
|
|
// Empty hash means we return current sidechain tip
|
|
if (id.empty()) {
|
|
block = m_chainTip;
|
|
|
|
// Don't return stale chain tip
|
|
if (block && (block->m_txinGenHeight + 2 < m_pool->miner_data().height)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
else {
|
|
auto it = m_blocksById.find(id);
|
|
if (it != m_blocksById.end()) {
|
|
block = it->second;
|
|
}
|
|
}
|
|
|
|
if (!block) {
|
|
return nullptr;
|
|
}
|
|
|
|
blob = block->serialize_mainchain_data();
|
|
{
|
|
std::string hex;
|
|
size_t start = 43; // approximate outputs offset
|
|
for (size_t i = start; i < std::min<size_t>(start + 64, blob.size()); ++i) {
|
|
char buf[4];
|
|
snprintf(buf, sizeof(buf), "%02x", blob[i]);
|
|
hex += buf;
|
|
}
|
|
LOGINFO(6, "DEBUG get_block_blob outputs area (64 bytes from offset 43): " << hex);
|
|
}
|
|
const std::vector<uint8_t> sidechain_data = block->serialize_sidechain_data();
|
|
blob.insert(blob.end(), sidechain_data.begin(), sidechain_data.end());
|
|
|
|
return block;
|
|
}
|
|
|
|
bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::vector<uint8_t>& blob, uv_loop_t* loop) const
|
|
{
|
|
blob.clear();
|
|
|
|
struct Data
|
|
{
|
|
FORCEINLINE Data() : blockMinerWallet(nullptr), counter(0) {}
|
|
Data(Data&&) = delete;
|
|
Data& operator=(Data&&) = delete;
|
|
|
|
std::vector<MinerShare> tmpShares;
|
|
Wallet blockMinerWallet;
|
|
hash txkeySec;
|
|
std::atomic<int> counter;
|
|
};
|
|
|
|
std::shared_ptr<Data> data;
|
|
std::vector<uint64_t> tmpRewards;
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
auto it = block->m_sidechainId.empty() ? m_blocksById.end() : m_blocksById.find(block->m_sidechainId);
|
|
if (it != m_blocksById.end()) {
|
|
const PoolBlock* b = it->second;
|
|
const size_t n = b->m_outputAmounts.size();
|
|
|
|
blob.reserve(n * 58 + 64);
|
|
writeVarint(n, blob);
|
|
|
|
for (size_t i = 0; i < n; ++i) {
|
|
const PoolBlock::TxOutput& output = b->m_outputAmounts[i];
|
|
writeVarint(output.m_reward, blob);
|
|
blob.emplace_back(TXOUT_TO_CARROT_V1);
|
|
const hash h = b->m_ephPublicKeys[i];
|
|
blob.insert(blob.end(), h.h, h.h + HASH_SIZE);
|
|
|
|
if (b->m_majorVersion >= 10) {
|
|
// Carrot v1 format
|
|
blob.push_back(4);
|
|
blob.push_back('S');
|
|
blob.push_back('A');
|
|
blob.push_back('L');
|
|
blob.push_back('1');
|
|
blob.insert(blob.end(), b->m_viewTags[i].begin(), b->m_viewTags[i].end());
|
|
blob.insert(blob.end(), b->m_encryptedAnchors[i].begin(), b->m_encryptedAnchors[i].end());
|
|
} else {
|
|
blob.emplace_back(static_cast<uint8_t>(output.m_viewTag));
|
|
}
|
|
}
|
|
|
|
block->m_ephPublicKeys = b->m_ephPublicKeys;
|
|
block->m_outputAmounts = b->m_outputAmounts;
|
|
block->m_viewTags = b->m_viewTags;
|
|
block->m_encryptedAnchors = b->m_encryptedAnchors;
|
|
return true;
|
|
}
|
|
|
|
// For Carrot v1+ external blocks, K_o values are NOT derivable from m_txkeySec
|
|
// They must be preserved from parsing (computed via Carrot crypto)
|
|
if (block->m_majorVersion >= 10 && !block->m_ephPublicKeys.empty()) {
|
|
const size_t n = block->m_ephPublicKeys.size();
|
|
blob.reserve(n * 58 + 64);
|
|
writeVarint(n, blob);
|
|
for (size_t i = 0; i < n; ++i) {
|
|
const PoolBlock::TxOutput& output = block->m_outputAmounts[i];
|
|
writeVarint(output.m_reward, blob);
|
|
blob.emplace_back(TXOUT_TO_CARROT_V1);
|
|
const hash eph_key = block->m_ephPublicKeys[i];
|
|
blob.insert(blob.end(), eph_key.h, eph_key.h + HASH_SIZE);
|
|
// Carrot v1 format
|
|
blob.push_back(4);
|
|
blob.push_back('S');
|
|
blob.push_back('A');
|
|
blob.push_back('L');
|
|
blob.push_back('1');
|
|
blob.insert(blob.end(), block->m_viewTags[i].begin(), block->m_viewTags[i].end());
|
|
blob.insert(blob.end(), block->m_encryptedAnchors[i].begin(), block->m_encryptedAnchors[i].end());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Can't compute outputs from PPLNS if we don't have this block's parent chain
|
|
if (!block->m_parent.empty()) {
|
|
auto parent_it = m_blocksById.find(block->m_parent);
|
|
if (parent_it == m_blocksById.end()) {
|
|
LOGINFO(5, "get_outputs_blob: can't compute outputs, parent " << block->m_parent << " not found");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
data = std::make_shared<Data>();
|
|
|
|
data->blockMinerWallet = block->m_minerWallet;
|
|
data->txkeySec = block->m_txkeySec;
|
|
|
|
if (!get_shares(block, data->tmpShares) || !split_reward(total_reward, data->tmpShares, tmpRewards) || (tmpRewards.size() != data->tmpShares.size())) {
|
|
return false;
|
|
}
|
|
// Handle donation mode during validation - match block creation logic
|
|
const uint64_t donation_cycle = (s_networkType == NetworkType::Mainnet) ? DONATION_CYCLE_MAINNET : DONATION_CYCLE_TESTNET;
|
|
if ((block->m_txinGenHeight % donation_cycle) == 0 && m_devWallet) {
|
|
// This is a donation block - replace all shares with single dev wallet output
|
|
difficulty_type total_weight;
|
|
for (const auto& share : data->tmpShares) {
|
|
total_weight += share.m_weight;
|
|
}
|
|
data->tmpShares.clear();
|
|
data->tmpShares.emplace_back(total_weight, m_devWallet);
|
|
|
|
// Recalculate rewards for the single dev wallet output
|
|
tmpRewards.clear();
|
|
if (!split_reward(total_reward, data->tmpShares, tmpRewards)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const size_t n = data->tmpShares.size();
|
|
data->counter = static_cast<int>(n) - 1;
|
|
|
|
// Helper jobs call get_eph_public_key with indices in descending order
|
|
// Current thread will process indices in ascending order so when they meet, everything will be cached
|
|
if (loop) {
|
|
// Avoid accessing block->m_minerWallet from other threads in "parallel_run" below
|
|
for (MinerShare& share : data->tmpShares) {
|
|
if (share.m_wallet == &block->m_minerWallet) {
|
|
share.m_wallet = &data->blockMinerWallet;
|
|
break;
|
|
}
|
|
}
|
|
|
|
parallel_run(loop, [data]() {
|
|
Data* d = data.get();
|
|
hash eph_public_key;
|
|
|
|
int index;
|
|
while ((index = d->counter.fetch_sub(1)) >= 0) {
|
|
uint8_t view_tag;
|
|
if (!d->tmpShares[index].m_wallet->get_eph_public_key(d->txkeySec, static_cast<size_t>(index), eph_public_key, view_tag)) {
|
|
LOGWARN(6, "get_eph_public_key failed at index " << index);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
blob.reserve(n * 39 + 64);
|
|
|
|
writeVarint(n, blob);
|
|
|
|
block->m_ephPublicKeys.clear();
|
|
block->m_outputAmounts.clear();
|
|
block->m_viewTags.clear();
|
|
block->m_encryptedAnchors.clear();
|
|
block->m_ephPublicKeys.reserve(n);
|
|
block->m_outputAmounts.reserve(n);
|
|
|
|
hash eph_public_key;
|
|
for (size_t i = 0; i < n; ++i) {
|
|
// stop helper jobs when they meet with current thread
|
|
const int c = data->counter.load();
|
|
if ((c >= 0) && (static_cast<int>(i) >= c)) {
|
|
// this will cause all helper jobs to finish immediately
|
|
data->counter = -1;
|
|
}
|
|
|
|
writeVarint(tmpRewards[i], blob);
|
|
|
|
blob.emplace_back(TXOUT_TO_CARROT_V1);
|
|
|
|
uint8_t view_tag;
|
|
if (!data->tmpShares[i].m_wallet->get_eph_public_key(data->txkeySec, i, eph_public_key, view_tag)) {
|
|
LOGWARN(6, "get_eph_public_key failed at index " << i);
|
|
}
|
|
blob.insert(blob.end(), eph_public_key.h, eph_public_key.h + HASH_SIZE);
|
|
|
|
blob.emplace_back(view_tag);
|
|
|
|
block->m_ephPublicKeys.emplace_back(eph_public_key);
|
|
block->m_outputAmounts.emplace_back(tmpRewards[i], view_tag);
|
|
}
|
|
|
|
block->m_ephPublicKeys.shrink_to_fit();
|
|
block->m_outputAmounts.shrink_to_fit();
|
|
return true;
|
|
}
|
|
|
|
void SideChain::print_status(bool obtain_sidechain_lock) const
|
|
{
|
|
unordered_set<hash> blocks_in_window;
|
|
blocks_in_window.reserve(m_chainWindowSize * 9 / 8);
|
|
|
|
const difficulty_type diff = difficulty();
|
|
|
|
if (obtain_sidechain_lock) uv_rwlock_rdlock(&m_sidechainLock);
|
|
ON_SCOPE_LEAVE([this, obtain_sidechain_lock]() { if (obtain_sidechain_lock) uv_rwlock_rdunlock(&m_sidechainLock); });
|
|
|
|
const uint64_t pool_hashrate = (diff / m_targetBlockTime).lo;
|
|
|
|
const difficulty_type network_diff = m_pool->miner_data().difficulty;
|
|
const uint64_t network_hashrate = (network_diff / MONERO_BLOCK_TIME).lo;
|
|
|
|
const PoolBlock* tip = m_chainTip;
|
|
|
|
std::vector<MinerShare> shares;
|
|
uint64_t bh = 0;
|
|
if (tip) {
|
|
if (!get_shares(tip, shares, &bh, true)) {
|
|
LOGERR(6, "print_status: get_shares failed");
|
|
}
|
|
}
|
|
|
|
const uint64_t window_size = (tip && bh) ? (tip->m_sidechainHeight - bh + 1U) : m_chainWindowSize;
|
|
|
|
uint64_t block_depth = 0;
|
|
const PoolBlock* cur = tip;
|
|
const uint64_t tip_height = tip ? tip->m_sidechainHeight : 0;
|
|
|
|
uint64_t total_blocks_in_window = 0;
|
|
uint64_t total_uncles_in_window = 0;
|
|
|
|
// each dot corresponds to window_size / 30 shares, with current values, 2160 / 30 = 72
|
|
constexpr size_t N = 30;
|
|
std::array<uint64_t, N> our_blocks_in_window{};
|
|
std::array<uint64_t, N> our_uncles_in_window{};
|
|
|
|
const Wallet& w = m_pool->params().m_miningWallet;
|
|
|
|
while (cur) {
|
|
blocks_in_window.emplace(cur->m_sidechainId);
|
|
++total_blocks_in_window;
|
|
|
|
// "block_depth <= window_size - 1" here (see the check below), so window_index will be <= N - 1
|
|
// This will map the range [0, window_size - 1] into [0, N - 1]
|
|
const size_t window_index = (window_size > 1) ? (block_depth * (N - 1) / (window_size - 1)) : 0;
|
|
|
|
if (cur->m_minerWallet == w) {
|
|
++our_blocks_in_window[window_index];
|
|
}
|
|
|
|
++block_depth;
|
|
if (block_depth >= window_size) {
|
|
break;
|
|
}
|
|
|
|
for (const hash& uncle_id : cur->m_uncles) {
|
|
blocks_in_window.emplace(uncle_id);
|
|
auto it = m_blocksById.find(uncle_id);
|
|
if (it != m_blocksById.end()) {
|
|
const PoolBlock* uncle = it->second;
|
|
if (tip_height - uncle->m_sidechainHeight < window_size) {
|
|
++total_uncles_in_window;
|
|
if (uncle->m_minerWallet == w) {
|
|
++our_uncles_in_window[window_index];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cur = get_parent(cur);
|
|
}
|
|
|
|
uint64_t total_orphans = 0;
|
|
uint64_t our_orphans = 0;
|
|
|
|
if (tip) {
|
|
for (uint64_t i = 0; (i < window_size) && (i <= tip_height); ++i) {
|
|
auto it = m_blocksByHeight.find(tip_height - i);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
for (const PoolBlock* block : it->second) {
|
|
if (blocks_in_window.find(block->m_sidechainId) == blocks_in_window.end()) {
|
|
LOGINFO(4, "orphan block at height " << log::Gray() << block->m_sidechainHeight << log::NoColor() << ": " << log::Gray() << block->m_sidechainId);
|
|
++total_orphans;
|
|
if (block->m_minerWallet == w) {
|
|
++our_orphans;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
difficulty_type your_shares_weight, pplns_weight;
|
|
for (const MinerShare& s : shares) {
|
|
if (*s.m_wallet == w) {
|
|
your_shares_weight = s.m_weight;
|
|
}
|
|
pplns_weight += s.m_weight;
|
|
}
|
|
|
|
if (pplns_weight == 0) {
|
|
pplns_weight = m_minDifficulty;
|
|
}
|
|
|
|
const uint64_t total_reward = m_pool->block_template().get_reward();
|
|
const uint64_t your_reward = ((your_shares_weight * total_reward) / pplns_weight).lo;
|
|
const uint64_t hashrate_est = ((your_shares_weight * pool_hashrate) / pplns_weight).lo;
|
|
|
|
const double block_share = total_reward ? ((static_cast<double>(your_reward) * 100.0) / static_cast<double>(total_reward)) : 0.0;
|
|
|
|
const uint64_t our_blocks_in_window_total = std::accumulate(our_blocks_in_window.begin(), our_blocks_in_window.end(), 0ULL);
|
|
const uint64_t our_uncles_in_window_total = std::accumulate(our_uncles_in_window.begin(), our_uncles_in_window.end(), 0ULL);
|
|
|
|
std::string our_blocks_in_window_chart;
|
|
if (our_blocks_in_window_total) {
|
|
our_blocks_in_window_chart.reserve(our_blocks_in_window.size() + 32);
|
|
our_blocks_in_window_chart = "\nYour shares position = [";
|
|
for (uint64_t p : our_blocks_in_window) {
|
|
our_blocks_in_window_chart += (p ? ((p > 9) ? '+' : static_cast<char>('0' + p)) : '.');
|
|
}
|
|
our_blocks_in_window_chart += ']';
|
|
}
|
|
|
|
std::string our_uncles_in_window_chart;
|
|
if (our_uncles_in_window_total) {
|
|
our_uncles_in_window_chart.reserve(our_uncles_in_window.size() + 32);
|
|
our_uncles_in_window_chart = "\nYour uncles position = [";
|
|
for (uint64_t p : our_uncles_in_window) {
|
|
our_uncles_in_window_chart += (p ? ((p > 9) ? '+' : static_cast<char>('0' + p)) : '.');
|
|
}
|
|
our_uncles_in_window_chart += ']';
|
|
}
|
|
|
|
LOGINFO(0, "status" <<
|
|
"\nSalvium node = " << m_pool->current_host().m_displayName <<
|
|
"\nMain chain height = " << m_pool->block_template().height() <<
|
|
"\nMain chain hashrate = " << log::Hashrate(network_hashrate) <<
|
|
"\nSide chain ID = " << (is_default() ? "default" : (is_mini() ? "mini" : (is_nano() ? "nano" : m_consensusIdDisplayStr.c_str()))) <<
|
|
"\nSide chain height = " << tip_height + 1 <<
|
|
"\nSide chain hashrate = " << log::Hashrate(pool_hashrate) <<
|
|
(hashrate_est ? "\nYour hashrate (pool-side) = " : "") << (hashrate_est ? log::Hashrate(hashrate_est) : log::Hashrate()) <<
|
|
"\nPPLNS window = " << total_blocks_in_window << " blocks (+" << total_uncles_in_window << " uncles, " << total_orphans << " orphans)" <<
|
|
"\nPPLNS window duration = " << log::Duration((pplns_weight / pool_hashrate).lo) <<
|
|
"\nYour wallet address = " << m_pool->params().m_displayWallet <<
|
|
"\nYour shares = " << our_blocks_in_window_total << " blocks (+" << our_uncles_in_window_total << " uncles, " << our_orphans << " orphans)"
|
|
<< our_blocks_in_window_chart << our_uncles_in_window_chart <<
|
|
"\nBlock reward share = " << block_share << "% (" << log::SALAmount(your_reward) << ')' <<
|
|
"\nAnchor point = " << get_latest_checkpoint_height() <<
|
|
"\nChain health = " << m_externalBlockFailures << " consecutive failures" << (m_recoveryMode.load() ? " [RECOVERY MODE]" : "")
|
|
);
|
|
}
|
|
|
|
double SideChain::get_reward_share(const Wallet& w) const
|
|
{
|
|
uint64_t reward = 0;
|
|
uint64_t total_reward = 0;
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
const PoolBlock* tip = m_chainTip;
|
|
if (tip) {
|
|
hash eph_public_key;
|
|
for (size_t i = 0, n = tip->m_outputAmounts.size(); i < n; ++i) {
|
|
const PoolBlock::TxOutput& out = tip->m_outputAmounts[i];
|
|
if (!reward) {
|
|
uint8_t view_tag;
|
|
const uint8_t expected_view_tag = out.m_viewTag;
|
|
if (w.get_eph_public_key(tip->m_txkeySec, i, eph_public_key, view_tag, &expected_view_tag) && (tip->m_ephPublicKeys[i] == eph_public_key)) {
|
|
reward = out.m_reward;
|
|
}
|
|
}
|
|
total_reward += out.m_reward;
|
|
}
|
|
}
|
|
}
|
|
return total_reward ? (static_cast<double>(reward) / static_cast<double>(total_reward)) : 0.0;
|
|
}
|
|
|
|
uint64_t SideChain::network_major_version(uint64_t height)
|
|
{
|
|
const hardfork_t* hard_forks;
|
|
size_t num_hard_forks;
|
|
|
|
switch (s_networkType)
|
|
{
|
|
case NetworkType::Mainnet:
|
|
default:
|
|
hard_forks = mainnet_hard_forks;
|
|
num_hard_forks = num_mainnet_hard_forks;
|
|
break;
|
|
|
|
case NetworkType::Testnet:
|
|
hard_forks = testnet_hard_forks;
|
|
num_hard_forks = num_testnet_hard_forks;
|
|
break;
|
|
|
|
case NetworkType::Stagenet:
|
|
hard_forks = stagenet_hard_forks;
|
|
num_hard_forks = num_stagenet_hard_forks;
|
|
break;
|
|
}
|
|
|
|
uint64_t result = 1;
|
|
for (size_t i = 1; (i < num_hard_forks) && (height >= hard_forks[i].height); ++i) {
|
|
result = hard_forks[i].version;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
difficulty_type SideChain::total_hashes() const
|
|
{
|
|
const PoolBlock* tip = m_chainTip;
|
|
return tip ? tip->m_cumulativeDifficulty : difficulty_type();
|
|
}
|
|
|
|
// Expects that m_seenDataLock is already locked for writing
|
|
void SideChain::prune_seen_data()
|
|
{
|
|
const uint64_t cur_time = seconds_since_epoch();
|
|
|
|
// Every 5 minutes, delete wallets that weren't seen for more than 72 hours and onion pubkeys that weren't seen for more than 12 hours
|
|
if (m_seenWalletsLastPruneTime + 5ul * 60ul <= cur_time) {
|
|
auto prune = [cur_time](auto& data, uint64_t timeout) {
|
|
for (auto it = data.begin(); it != data.end();) {
|
|
if (it->second + timeout < cur_time) {
|
|
it = data.erase(it);
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
};
|
|
|
|
constexpr uint64_t hour = 3600;
|
|
|
|
prune(m_seenWallets, 72 * hour);
|
|
prune(m_seenOnionPubkeys, 12 * hour);
|
|
|
|
m_seenWalletsLastPruneTime = cur_time;
|
|
}
|
|
}
|
|
|
|
uint64_t SideChain::last_updated() const
|
|
{
|
|
const PoolBlock* tip = m_chainTip;
|
|
return tip ? tip->m_localTimestamp : 0;
|
|
}
|
|
|
|
bool SideChain::is_default() const
|
|
{
|
|
return (memcmp(m_consensusId.data(), default_consensus_id, HASH_SIZE) == 0);
|
|
}
|
|
|
|
bool SideChain::is_mini() const
|
|
{
|
|
return (memcmp(m_consensusId.data(), mini_consensus_id, HASH_SIZE) == 0);
|
|
}
|
|
|
|
bool SideChain::is_nano() const
|
|
{
|
|
return (memcmp(m_consensusId.data(), nano_consensus_id, HASH_SIZE) == 0);
|
|
}
|
|
|
|
uint64_t SideChain::bottom_height(const PoolBlock* tip) const
|
|
{
|
|
if (!tip) {
|
|
return 0;
|
|
}
|
|
|
|
uint64_t bottom_height;
|
|
std::vector<MinerShare> shares;
|
|
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
if (!get_shares(tip, shares, &bottom_height, true)) {
|
|
return 0;
|
|
}
|
|
|
|
return bottom_height;
|
|
}
|
|
|
|
bool SideChain::split_reward(uint64_t reward, const std::vector<MinerShare>& shares, std::vector<uint64_t>& rewards)
|
|
{
|
|
const size_t num_shares = shares.size();
|
|
|
|
for (size_t i = 0; i < shares.size(); ++i) {
|
|
}
|
|
const difficulty_type total_weight = std::accumulate(shares.begin(), shares.end(), difficulty_type(), [](const difficulty_type& a, const MinerShare& b) { return a + b.m_weight; });
|
|
|
|
if (total_weight.empty()) {
|
|
LOGERR(1, "total_weight is 0. Check the code!");
|
|
return false;
|
|
}
|
|
|
|
rewards.clear();
|
|
rewards.reserve(num_shares);
|
|
|
|
// Each miner gets a proportional fraction of the block reward
|
|
difficulty_type w;
|
|
uint64_t reward_given = 0;
|
|
for (uint64_t i = 0; i < num_shares; ++i) {
|
|
w += shares[i].m_weight;
|
|
|
|
const difficulty_type next_value = w * reward / total_weight;
|
|
rewards.emplace_back(next_value.lo - reward_given);
|
|
reward_given = next_value.lo;
|
|
}
|
|
|
|
// Double check that we gave out the exact amount
|
|
if (std::accumulate(rewards.begin(), rewards.end(), 0ULL) != reward) {
|
|
LOGERR(1, "miners got incorrect reward. This should never happen because math says so. Check the code!");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SideChain::get_difficulty(const PoolBlock* tip, std::vector<DifficultyData>& difficultyData, difficulty_type& curDifficulty) const
|
|
{
|
|
difficultyData.clear();
|
|
|
|
const PoolBlock* cur = tip;
|
|
uint64_t oldest_timestamp = std::numeric_limits<uint64_t>::max();
|
|
|
|
uint64_t block_depth = 0;
|
|
do {
|
|
oldest_timestamp = std::min(oldest_timestamp, cur->m_timestamp);
|
|
difficultyData.emplace_back(cur->m_timestamp, cur->m_cumulativeDifficulty);
|
|
|
|
for (const hash& uncle_id : cur->m_uncles) {
|
|
auto it = m_blocksById.find(uncle_id);
|
|
if (it == m_blocksById.end()) {
|
|
LOGWARN(3, "get_difficulty: can't find uncle block at height = " << cur->m_sidechainHeight << ", id = " << uncle_id);
|
|
LOGWARN(3, "get_difficulty: can't calculate diff for block at height = " << tip->m_sidechainHeight << ", id = " << tip->m_sidechainId << ", mainchain height = " << tip->m_txinGenHeight);
|
|
return false;
|
|
}
|
|
|
|
const PoolBlock* uncle = it->second;
|
|
if (tip->m_sidechainHeight - uncle->m_sidechainHeight < m_chainWindowSize) {
|
|
oldest_timestamp = std::min(oldest_timestamp, uncle->m_timestamp);
|
|
difficultyData.emplace_back(uncle->m_timestamp, uncle->m_cumulativeDifficulty);
|
|
}
|
|
}
|
|
|
|
++block_depth;
|
|
if (block_depth >= m_chainWindowSize) {
|
|
break;
|
|
}
|
|
|
|
// Reached the genesis block so we're done
|
|
if (cur->m_sidechainHeight == 0) {
|
|
break;
|
|
}
|
|
|
|
auto it = m_blocksById.find(cur->m_parent);
|
|
if (it == m_blocksById.end()) {
|
|
LOGWARN(3, "get_difficulty: can't find parent block at height = " << cur->m_sidechainHeight - 1 << ", id = " << cur->m_parent);
|
|
LOGWARN(3, "get_difficulty: can't calculate diff for block at height = " << tip->m_sidechainHeight << ", id = " << tip->m_sidechainId << ", mainchain height = " << tip->m_txinGenHeight);
|
|
return false;
|
|
}
|
|
|
|
cur = it->second;
|
|
} while (true);
|
|
|
|
// Discard 10% oldest and 10% newest (by timestamp) blocks
|
|
std::vector<uint32_t> tmpTimestamps;
|
|
tmpTimestamps.reserve(difficultyData.size());
|
|
|
|
std::transform(difficultyData.begin(), difficultyData.end(), std::back_inserter(tmpTimestamps),
|
|
[oldest_timestamp](const DifficultyData& d)
|
|
{
|
|
return static_cast<uint32_t>(d.m_timestamp - oldest_timestamp);
|
|
});
|
|
|
|
const uint64_t cut_size = (difficultyData.size() + 9) / 10;
|
|
const uint64_t index1 = cut_size - 1;
|
|
const uint64_t index2 = difficultyData.size() - cut_size;
|
|
|
|
std::nth_element(tmpTimestamps.begin(), tmpTimestamps.begin() + static_cast<int32_t>(index1), tmpTimestamps.end());
|
|
const uint64_t timestamp1 = oldest_timestamp + tmpTimestamps[index1];
|
|
|
|
std::nth_element(tmpTimestamps.begin(), tmpTimestamps.begin() + static_cast<int32_t>(index2), tmpTimestamps.end());
|
|
const uint64_t timestamp2 = oldest_timestamp + tmpTimestamps[index2];
|
|
|
|
// Make a reasonable assumption that each block has higher timestamp, so delta_t can't be less than delta_index
|
|
// Because if it is, someone is trying to mess with timestamps
|
|
// In reality, delta_t ~ delta_index*10 (sidechain block time)
|
|
const uint64_t delta_index = (index2 > index1) ? (index2 - index1) : 1U;
|
|
const uint64_t delta_t = (timestamp2 > timestamp1 + delta_index) ? (timestamp2 - timestamp1) : delta_index;
|
|
|
|
difficulty_type diff1{ std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint64_t>::max() };
|
|
difficulty_type diff2{ 0, 0 };
|
|
|
|
for (const DifficultyData& d : difficultyData) {
|
|
if (timestamp1 <= d.m_timestamp && d.m_timestamp <= timestamp2) {
|
|
if (d.m_cumulativeDifficulty < diff1) {
|
|
diff1 = d.m_cumulativeDifficulty;
|
|
}
|
|
if (diff2 < d.m_cumulativeDifficulty) {
|
|
diff2 = d.m_cumulativeDifficulty;
|
|
}
|
|
}
|
|
}
|
|
|
|
curDifficulty = (diff2 - diff1) * m_targetBlockTime / delta_t;
|
|
|
|
if (curDifficulty < m_minDifficulty) {
|
|
curDifficulty = m_minDifficulty;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SideChain::p2pool_update_available() const
|
|
{
|
|
difficulty_type total_p2pool_diff, newer_p2pool_diff;
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
const PoolBlock* cur = m_chainTip;
|
|
|
|
for (uint64_t i = 0; (i < m_chainWindowSize) && cur; ++i, cur = get_parent(cur)) {
|
|
if (cur->m_sidechainExtraBuf[0] == static_cast<uint32_t>(SoftwareID::P2Pool)) {
|
|
total_p2pool_diff += cur->m_difficulty;
|
|
if (cur->m_sidechainExtraBuf[1] > P2POOL_VERSION) {
|
|
newer_p2pool_diff += cur->m_difficulty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assume that a new version is out if >= 20% of hashrate is using it already
|
|
return newer_p2pool_diff * 5 >= total_p2pool_diff;
|
|
}
|
|
|
|
std::vector<hash> SideChain::seen_onion_pubkeys() const
|
|
{
|
|
std::vector<hash> result;
|
|
|
|
ReadLock lock(m_seenDataLock);
|
|
|
|
result.reserve(m_seenOnionPubkeys.size());
|
|
|
|
for (const auto& it : m_seenOnionPubkeys) {
|
|
result.push_back(it.first);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void SideChain::add_onion_pubkeys(const std::vector<hash>& pubkeys)
|
|
{
|
|
if (pubkeys.empty()) {
|
|
return;
|
|
}
|
|
|
|
const uint64_t cur_time = seconds_since_epoch();
|
|
|
|
WriteLock lock(m_seenDataLock);
|
|
|
|
for (const hash& h : pubkeys) {
|
|
if (!h.empty()) {
|
|
m_seenOnionPubkeys[h] = cur_time;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SideChain::verify_loop(PoolBlock* block)
|
|
{
|
|
// PoW is already checked at this point
|
|
std::vector<PoolBlock*> blocks_to_verify(1, block);
|
|
|
|
// Bootstrap: if no checkpoints and nothing verified yet, seed from oldest block
|
|
bool have_checkpoints = false;
|
|
{
|
|
ReadLock cpLock(m_checkpointsLock);
|
|
have_checkpoints = !m_checkpoints.empty();
|
|
}
|
|
if (!have_checkpoints) {
|
|
bool any_verified = false;
|
|
PoolBlock* oldest = nullptr;
|
|
for (auto& pair : m_blocksById) {
|
|
if (pair.second->m_verified) {
|
|
any_verified = true;
|
|
break;
|
|
}
|
|
if (!oldest || pair.second->m_sidechainHeight < oldest->m_sidechainHeight) {
|
|
oldest = pair.second;
|
|
}
|
|
}
|
|
if (!any_verified && oldest && oldest->m_sidechainHeight > 0) {
|
|
oldest->m_verified = true;
|
|
oldest->m_invalid = false;
|
|
LOGINFO(0, "Bootstrap: seeding chain from oldest block at height " << oldest->m_sidechainHeight);
|
|
|
|
// Find direct children to propagate verification
|
|
auto it = m_blocksByHeight.find(oldest->m_sidechainHeight + 1);
|
|
if (it != m_blocksByHeight.end()) {
|
|
for (PoolBlock* child : it->second) {
|
|
if (child->m_parent == oldest->m_sidechainId) {
|
|
blocks_to_verify.push_back(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PoolBlock* highest_block = nullptr;
|
|
|
|
while (!blocks_to_verify.empty()) {
|
|
block = blocks_to_verify.back();
|
|
blocks_to_verify.pop_back();
|
|
|
|
if (block->m_verified) {
|
|
continue;
|
|
}
|
|
|
|
verify(block);
|
|
|
|
if (!block->m_verified) {
|
|
LOGINFO(6, "can't verify block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << ": parent or uncle blocks are not available)");
|
|
continue;
|
|
}
|
|
|
|
if (block->m_invalid) {
|
|
on_block_rejected(block, "verification failed");
|
|
}
|
|
else {
|
|
// Extra logging for blocks near checkpoint during recovery
|
|
if (block->m_sidechainHeight <= get_latest_checkpoint_height() + 10) {
|
|
LOGINFO(6, "RECOVERY DEBUG: VERIFIED block " << block->m_sidechainHeight
|
|
<< " (parent " << (block->m_sidechainHeight > 0 ? block->m_sidechainHeight - 1 : 0) << " was verified)");
|
|
}
|
|
LOGINFO(3, "verified block at height = " << block->m_sidechainHeight <<
|
|
", depth = " << block->m_depth <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight);
|
|
|
|
// This block is now verified
|
|
|
|
bool is_alternative;
|
|
|
|
if (is_longer_chain(highest_block, block, is_alternative)) {
|
|
highest_block = block;
|
|
}
|
|
else if (highest_block && (highest_block->m_sidechainHeight > block->m_sidechainHeight)) {
|
|
LOGINFO(4, "block " << highest_block->m_sidechainId <<
|
|
", height = " << highest_block->m_sidechainHeight <<
|
|
" is not a longer chain than " << block->m_sidechainId <<
|
|
", height " << block->m_sidechainHeight);
|
|
}
|
|
|
|
P2PServer* server = p2pServer();
|
|
|
|
// If it came through a broadcast, send it to our peers
|
|
if (block->m_wantBroadcast && !block->m_broadcasted) {
|
|
block->m_broadcasted = true;
|
|
if (server && (block->m_depth < UNCLE_BLOCK_DEPTH)) {
|
|
if (m_pool && (block->m_minerWallet == m_pool->params().m_miningWallet)) {
|
|
LOGINFO(0, log::Green() << "SHARE ADDED: height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight);
|
|
}
|
|
server->broadcast(*block, get_parent(block));
|
|
}
|
|
}
|
|
|
|
// Save it for faster syncing on the next p2pool start
|
|
if (server) {
|
|
server->store_in_cache(*block);
|
|
}
|
|
|
|
// Try to verify blocks on top of this one
|
|
for (size_t i = 1; i <= UNCLE_BLOCK_DEPTH; ++i) {
|
|
auto it = m_blocksByHeight.find(block->m_sidechainHeight + i);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
|
|
for (PoolBlock* b : it->second) {
|
|
if ((i == 1) && (b->m_parent == block->m_sidechainId)) {
|
|
// Update depth if needed
|
|
if (b->m_depth + 1 < block->m_depth) {
|
|
b->m_depth = block->m_depth - 1;
|
|
}
|
|
blocks_to_verify.push_back(b);
|
|
}
|
|
else for (const hash& h : b->m_uncles) {
|
|
if (h == block->m_sidechainId) {
|
|
// Update depth if needed
|
|
if (b->m_depth + i < block->m_depth) {
|
|
b->m_depth = block->m_depth - i;
|
|
}
|
|
blocks_to_verify.push_back(b);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (highest_block) {
|
|
update_chain_tip(highest_block);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void SideChain::verify(PoolBlock* block)
|
|
{
|
|
// Genesis block
|
|
if (block->m_sidechainHeight == 0) {
|
|
if (!block->m_parent.empty() ||
|
|
!block->m_uncles.empty() ||
|
|
(block->m_difficulty != m_minDifficulty) ||
|
|
(block->m_cumulativeDifficulty != m_minDifficulty) ||
|
|
(block->m_txkeySecSeed != m_consensusHash))
|
|
{
|
|
LOGWARN(3, "genesis block validation failed: height=" << block->m_sidechainHeight
|
|
<< " parent_empty=" << (block->m_parent.empty() ? 1 : 0)
|
|
<< " diff=" << block->m_difficulty << " expected=" << m_minDifficulty);
|
|
block->m_invalid = true;
|
|
}
|
|
block->m_verified = true;
|
|
if (!block->m_invalid) {
|
|
LOGINFO(3, "Genesis block verified: " << block->m_sidechainId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Deep block
|
|
//
|
|
// Blocks in PPLNS window (m_chainWindowSize) require up to m_chainWindowSize earlier blocks to verify
|
|
// If a block is deeper than (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH it can't influence blocks in PPLNS window
|
|
// Also, having so many blocks on top of this one means it was verified by the network at some point
|
|
// We skip checks in this case to make pruning possible
|
|
if (block->m_depth > (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH) {
|
|
LOGINFO(4, "block " << block->m_sidechainId << " skipped verification");
|
|
block->m_verified = true;
|
|
block->m_invalid = false;
|
|
return;
|
|
}
|
|
|
|
// Regular block
|
|
|
|
// Must have a parent
|
|
if (block->m_parent.empty()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has empty parent (non-genesis)");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Check parent
|
|
auto it = m_blocksById.find(block->m_parent);
|
|
if (it == m_blocksById.end()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << " parent " << block->m_parent << " NOT FOUND in m_blocksById");
|
|
block->m_verified = false;
|
|
return;
|
|
}
|
|
if (!it->second->m_verified) {
|
|
// Only log for blocks close to checkpoint to reduce spam
|
|
if (block->m_sidechainHeight <= get_latest_checkpoint_height() + 10) {
|
|
LOGINFO(6, "RECOVERY DEBUG: block " << block->m_sidechainHeight << " waiting for parent "
|
|
<< it->second->m_sidechainHeight << " (parent verified=" << (it->second->m_verified ? 1 : 0)
|
|
<< " invalid=" << (it->second->m_invalid ? 1 : 0) << ")");
|
|
}
|
|
block->m_verified = false;
|
|
return;
|
|
}
|
|
|
|
// If it's invalid then this block is also invalid
|
|
const PoolBlock* parent = it->second;
|
|
if (parent->m_invalid) {
|
|
LOGWARN(0, "DIAGNOSTIC: Block " << block->m_sidechainId << " (height=" << block->m_sidechainHeight
|
|
<< ") rejected because parent " << parent->m_sidechainId << " (height=" << parent->m_sidechainHeight
|
|
<< ") is marked invalid");
|
|
on_block_rejected(block, "parent block is invalid");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Check m_txkeySecSeed
|
|
const hash h = (block->m_prevId == parent->m_prevId) ? parent->m_txkeySecSeed : parent->calculate_tx_key_seed();
|
|
if (block->m_txkeySecSeed != h) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has invalid parent " << block->m_parent);
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
const uint64_t expectedHeight = parent->m_sidechainHeight + 1;
|
|
if (block->m_sidechainHeight != expectedHeight) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight <<
|
|
" has wrong height: expected " << expectedHeight);
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Uncle hashes must be sorted in the ascending order to prevent cheating when the same hash is repeated multiple times
|
|
for (size_t i = 1, n = block->m_uncles.size(); i < n; ++i) {
|
|
if (!(block->m_uncles[i - 1] < block->m_uncles[i])) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has invalid uncle order");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
difficulty_type expectedCumulativeDifficulty = parent->m_cumulativeDifficulty + block->m_difficulty;
|
|
|
|
// Check uncles
|
|
|
|
// First get a list of already mined blocks at possible uncle heights
|
|
std::vector<hash> mined_blocks;
|
|
|
|
if (!block->m_uncles.empty()) {
|
|
mined_blocks.reserve(UNCLE_BLOCK_DEPTH * 2 + 1);
|
|
|
|
const PoolBlock* tmp = parent;
|
|
for (uint64_t i = 0, n = std::min<uint64_t>(UNCLE_BLOCK_DEPTH, block->m_sidechainHeight + 1); tmp && (i < n); ++i) {
|
|
mined_blocks.push_back(tmp->m_sidechainId);
|
|
mined_blocks.insert(mined_blocks.end(), tmp->m_uncles.begin(), tmp->m_uncles.end());
|
|
tmp = get_parent(tmp);
|
|
}
|
|
}
|
|
|
|
for (const hash& uncle_id : block->m_uncles) {
|
|
// Empty hash is only used in the genesis block and only for its parent
|
|
// Uncles can't be empty
|
|
if (uncle_id.empty()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has empty uncle hash");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Can't mine the same uncle block twice
|
|
if (std::find(mined_blocks.begin(), mined_blocks.end(), uncle_id) != mined_blocks.end()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has an uncle (" << uncle_id << ") that's already been mined");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
it = m_blocksById.find(uncle_id);
|
|
if ((it == m_blocksById.end()) || !it->second->m_verified) {
|
|
block->m_verified = false;
|
|
return;
|
|
}
|
|
|
|
const PoolBlock* uncle = it->second;
|
|
|
|
// If it's invalid then this block is also invalid
|
|
if (uncle->m_invalid) {
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Check that it has correct height
|
|
if ((uncle->m_sidechainHeight >= block->m_sidechainHeight) || (uncle->m_sidechainHeight + UNCLE_BLOCK_DEPTH < block->m_sidechainHeight)) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has an uncle at the wrong height (" << uncle->m_sidechainHeight << ')');
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Check that uncle and parent have the same ancestor (they must be on the same chain)
|
|
const PoolBlock* tmp = parent;
|
|
while (tmp->m_sidechainHeight > uncle->m_sidechainHeight) {
|
|
tmp = get_parent(tmp);
|
|
if (!tmp) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has an uncle from a different chain (check 1 failed)");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (tmp->m_sidechainHeight < uncle->m_sidechainHeight) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has an uncle from a different chain (check 2 failed)");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
bool same_chain = false;
|
|
const PoolBlock* tmp2 = uncle;
|
|
for (size_t j = 0; (j < UNCLE_BLOCK_DEPTH) && tmp && tmp2 && (tmp->m_sidechainHeight + UNCLE_BLOCK_DEPTH >= block->m_sidechainHeight); ++j) {
|
|
if (tmp->m_parent == tmp2->m_parent) {
|
|
same_chain = true;
|
|
break;
|
|
}
|
|
tmp = get_parent(tmp);
|
|
tmp2 = get_parent(tmp2);
|
|
}
|
|
|
|
if (!same_chain) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << " has an uncle from a different chain (check 3 failed)");
|
|
block->m_verified = true;
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
expectedCumulativeDifficulty += uncle->m_difficulty;
|
|
}
|
|
|
|
// We can verify this block now (all previous blocks in the window are verified and valid)
|
|
// It can still turn out to be invalid
|
|
block->m_verified = true;
|
|
|
|
if (block->m_cumulativeDifficulty != expectedCumulativeDifficulty) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight <<
|
|
" has wrong cumulative difficulty: got " << block->m_cumulativeDifficulty << ", expected " << expectedCumulativeDifficulty);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Verify difficulty and miner rewards only for blocks in PPLNS window
|
|
if (block->m_depth >= m_chainWindowSize) {
|
|
LOGINFO(4, "block " << block->m_sidechainId << " skipped diff/reward verification");
|
|
block->m_invalid = false;
|
|
return;
|
|
}
|
|
|
|
difficulty_type diff;
|
|
// Always use get_difficulty for verification - difficulty() may be stale after recovery
|
|
if (!get_difficulty(parent, m_difficultyData, diff)) {
|
|
// Don't mark as invalid - mainchain might not be synced yet
|
|
// Leave unverified so it can be retried later
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId
|
|
<< ", mainchain height = " << block->m_txinGenHeight
|
|
<< ": get_difficulty failed for parent " << block->m_parent << " (mainchain may not be synced yet), leaving unverified");
|
|
block->m_verified = false;
|
|
return;
|
|
}
|
|
|
|
if (diff != block->m_difficulty) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has wrong difficulty: got " << block->m_difficulty << ", expected " << diff);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
std::vector<MinerShare> shares;
|
|
|
|
if (block->m_precalculated) {
|
|
WriteLock lock(*PoolBlock::s_precalculatedSharesLock);
|
|
shares = std::move(block->m_precalculatedShares);
|
|
}
|
|
|
|
if (shares.empty() && !get_shares(block, shares)) {
|
|
// Don't mark as invalid - mainchain might not be synced yet
|
|
// Leave unverified so it can be retried later
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId
|
|
<< ", mainchain height = " << block->m_txinGenHeight
|
|
<< ": get_shares failed (mainchain may not be synced yet), leaving unverified");
|
|
block->m_verified = false;
|
|
return;
|
|
}
|
|
|
|
// Handle donation mode during verification - match block creation logic
|
|
const uint64_t donation_cycle = (s_networkType == NetworkType::Mainnet) ? DONATION_CYCLE_MAINNET : DONATION_CYCLE_TESTNET;
|
|
if ((block->m_txinGenHeight % donation_cycle) == 0 && m_devWallet) {
|
|
difficulty_type total_weight;
|
|
for (const auto& share : shares) {
|
|
total_weight += share.m_weight;
|
|
}
|
|
shares.clear();
|
|
shares.emplace_back(total_weight, m_devWallet);
|
|
}
|
|
|
|
if (shares.size() != block->m_outputAmounts.size()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight
|
|
<< " has invalid number of outputs: got " << block->m_outputAmounts.size() << ", expected " << shares.size());
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
uint64_t total_reward = std::accumulate(block->m_outputAmounts.begin(), block->m_outputAmounts.end(), 0ULL,
|
|
[](uint64_t a, const PoolBlock::TxOutput& b)
|
|
{
|
|
return a + b.m_reward;
|
|
});
|
|
|
|
std::vector<uint64_t> rewards;
|
|
if (!split_reward(total_reward, shares, rewards)) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight << ": split_reward failed");
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
if (rewards.size() != block->m_outputAmounts.size()) {
|
|
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
|
", id = " << block->m_sidechainId <<
|
|
", mainchain height = " << block->m_txinGenHeight
|
|
<< " has invalid number of outputs: got " << block->m_outputAmounts.size() << ", expected " << rewards.size());
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
|
|
// Carrot v1 validation - generate and sort expected outputs
|
|
struct ValidationOutput {
|
|
size_t share_index;
|
|
uint64_t reward;
|
|
hash ko;
|
|
uint8_t view_tag;
|
|
};
|
|
std::vector<ValidationOutput> expected;
|
|
expected.reserve(rewards.size());
|
|
|
|
for (size_t i = 0; i < rewards.size(); ++i) {
|
|
ValidationOutput out;
|
|
out.share_index = i;
|
|
out.reward = rewards[i];
|
|
if (!shares[i].m_wallet->get_eph_public_key_carrot(block->m_txkeySecSeed, block->m_txinGenHeight, i, rewards[i], out.ko, out.view_tag)) {
|
|
LOGWARN(3, "block at height " << block->m_sidechainHeight << " failed to generate K_o at index " << i);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
expected.push_back(out);
|
|
}
|
|
|
|
// Sort by K_o
|
|
std::sort(expected.begin(), expected.end(),
|
|
[](const ValidationOutput& a, const ValidationOutput& b) {
|
|
return memcmp(a.ko.h, b.ko.h, HASH_SIZE) < 0;
|
|
});
|
|
|
|
// Validate sorted outputs
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
const ValidationOutput& exp = expected[i];
|
|
const PoolBlock::TxOutput& actual = block->m_outputAmounts[i];
|
|
|
|
if (exp.reward != actual.m_reward) {
|
|
LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid reward at position " << i);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
if (exp.view_tag != actual.m_viewTag) {
|
|
LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid view_tag at position " << i);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
if (exp.ko != block->m_ephPublicKeys[i]) {
|
|
LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid K_o at position " << i);
|
|
block->m_invalid = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// All checks passed
|
|
block->m_invalid = false;
|
|
}
|
|
|
|
void SideChain::update_chain_tip(PoolBlock* block)
|
|
{
|
|
if (!block->m_verified || block->m_invalid) {
|
|
LOGERR(1, "trying to update chain tip to an unverified or invalid block, fix the code!");
|
|
return;
|
|
}
|
|
|
|
if (block->m_depth >= m_chainWindowSize) {
|
|
LOGINFO(5, "Trying to update chain tip to a block with depth " << block->m_depth << ". Ignoring it.");
|
|
return;
|
|
}
|
|
|
|
PoolBlock* tip = m_chainTip;
|
|
|
|
if (block == tip) {
|
|
LOGINFO(5, "Trying to update chain tip to the same block again. Ignoring it.");
|
|
return;
|
|
}
|
|
|
|
bool is_alternative;
|
|
if (is_longer_chain(tip, block, is_alternative)) {
|
|
difficulty_type diff;
|
|
if (get_difficulty(block, m_difficultyData, diff)) {
|
|
if (!m_chainTip.compare_exchange_strong(tip, block)) {
|
|
LOGINFO(5, "Trying to update an outdated chain tip. Ignoring it.");
|
|
return;
|
|
}
|
|
{
|
|
WriteLock lock(m_curDifficultyLock);
|
|
m_curDifficulty = diff;
|
|
}
|
|
|
|
LOGINFO(2, "new chain tip: next height = " << log::Gray() << block->m_sidechainHeight + 1 << log::NoColor() <<
|
|
", next difficulty = " << log::Gray() << diff << log::NoColor() <<
|
|
", main chain height = " << log::Gray() << block->m_txinGenHeight);
|
|
|
|
// Only broadcast after initial sync complete - avoid broadcasting stale blocks
|
|
if (m_readyToMine.load()) {
|
|
block->m_wantBroadcast = true;
|
|
}
|
|
|
|
if (m_pool) {
|
|
m_pool->update_block_template_async(is_alternative);
|
|
|
|
// Reset stratum share counters when switching to an alternative chain to avoid confusion
|
|
if (is_alternative) {
|
|
StratumServer* s = m_pool->stratum_server();
|
|
if (s) {
|
|
s->reset_share_counters();
|
|
}
|
|
// Also clear cache because it has data from all old blocks now
|
|
clear_crypto_cache();
|
|
LOGINFO(0, log::LightCyan() << "SYNCHRONIZED");
|
|
}
|
|
}
|
|
prune_old_blocks();
|
|
cleanup_incoming_blocks();
|
|
|
|
// Check if we're ready to mine (sync complete)
|
|
// For fresh/small chains that haven't reached prune threshold,
|
|
// we're ready if we have a verified tip and no unverified blocks pending
|
|
if (!m_readyToMine.load()) {
|
|
// Validate cached checkpoints before enabling mining
|
|
LOGINFO(3, "All blocks verified, checking m_checkpointsNeedValidation=" << (m_checkpointsNeedValidation ? "true" : "false"));
|
|
if (m_checkpointsNeedValidation) {
|
|
if (!validate_loaded_checkpoints()) {
|
|
LOGWARN(0, "Checkpoint validation deferred - waiting for sync, mining delayed");
|
|
return;
|
|
}
|
|
|
|
// If validation triggered recovery, don't enable mining yet
|
|
if (m_recoveryMode.load()) {
|
|
LOGWARN(0, "Checkpoint validation failed - recovery in progress, mining delayed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Keep scanning until tip is verified
|
|
// Get checkpoint range for bulk verification
|
|
uint64_t oldest_cp = UINT64_MAX, newest_cp = 0;
|
|
{
|
|
ReadLock cpLock(m_checkpointsLock);
|
|
for (const auto& cp : m_checkpoints) {
|
|
if (cp.height < oldest_cp) oldest_cp = cp.height;
|
|
if (cp.height > newest_cp) newest_cp = cp.height;
|
|
}
|
|
}
|
|
bool have_checkpoints = (oldest_cp != UINT64_MAX && newest_cp > 0);
|
|
|
|
// Bootstrap: if no checkpoints, find oldest block and mark it verified as seed
|
|
if (!have_checkpoints && !m_blocksById.empty()) {
|
|
PoolBlock* oldest = nullptr;
|
|
for (auto& pair : m_blocksById) {
|
|
if (!oldest || pair.second->m_sidechainHeight < oldest->m_sidechainHeight) {
|
|
oldest = pair.second;
|
|
}
|
|
}
|
|
if (oldest) {
|
|
oldest->m_verified = true;
|
|
LOGINFO(1, "Bootstrap: seeding verification from height " << oldest->m_sidechainHeight);
|
|
}
|
|
}
|
|
|
|
for (int attempt = 0; attempt < 100; ++attempt) {
|
|
bool made_progress = true;
|
|
while (made_progress) {
|
|
made_progress = false;
|
|
for (auto& pair : m_blocksById) {
|
|
PoolBlock* b = pair.second;
|
|
if (!b->m_verified && b->m_sidechainHeight > 0) {
|
|
// If checkpoints exist, mark verified if within range
|
|
if (have_checkpoints && b->m_sidechainHeight >= oldest_cp && b->m_sidechainHeight <= newest_cp) {
|
|
b->m_verified = true;
|
|
made_progress = true;
|
|
}
|
|
// Always check parent propagation (works with or without checkpoints)
|
|
else {
|
|
auto parent_it = m_blocksById.find(b->m_parent);
|
|
if (parent_it != m_blocksById.end() && parent_it->second->m_verified) {
|
|
b->m_verified = true;
|
|
made_progress = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const PoolBlock* tip_check = m_chainTip.load();
|
|
if (tip_check && tip_check->m_verified) {
|
|
break; // Tip is verified, done
|
|
}
|
|
// Wait for more blocks to arrive
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
|
|
const PoolBlock* current_tip = m_chainTip.load();
|
|
bool have_checkpoint = false;
|
|
{
|
|
ReadLock cpLock(m_checkpointsLock);
|
|
have_checkpoint = !m_checkpoints.empty();
|
|
}
|
|
// Only enable mining when tip is verified at low depth (real-time, not catch-up)
|
|
if (current_tip && current_tip->m_verified && have_checkpoint && current_tip->m_depth < 10) {
|
|
m_readyToMine.store(true);
|
|
LOGINFO(0, log::LightGreen() << "########################################");
|
|
LOGINFO(0, log::LightGreen() << "SIDECHAIN LOADED - MINING IS NOW ENABLED");
|
|
LOGINFO(0, log::LightGreen() << "########################################");
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
else if (block->m_sidechainHeight > tip->m_sidechainHeight) {
|
|
LOGINFO(4, "block " << block->m_sidechainId <<
|
|
", height = " << block->m_sidechainHeight <<
|
|
" is not a longer chain than " << tip->m_sidechainId <<
|
|
", height " << tip->m_sidechainHeight);
|
|
}
|
|
else if (m_pool && (block->m_sidechainHeight + UNCLE_BLOCK_DEPTH > tip->m_sidechainHeight)) {
|
|
LOGINFO(4, "possible uncle block: id = " << log::Gray() << block->m_sidechainId << log::NoColor() <<
|
|
", height = " << log::Gray() << block->m_sidechainHeight);
|
|
m_pool->update_block_template_async();
|
|
}
|
|
|
|
if (p2pServer() && block->m_wantBroadcast && !block->m_broadcasted) {
|
|
block->m_broadcasted = true;
|
|
p2pServer()->broadcast(*block, get_parent(block));
|
|
}
|
|
|
|
// Update checkpoints if we crossed a boundary
|
|
update_checkpoints(block->m_sidechainHeight);
|
|
}
|
|
|
|
PoolBlock* SideChain::get_parent(const PoolBlock* block) const
|
|
{
|
|
auto it = m_blocksById.find(block->m_parent);
|
|
return (it != m_blocksById.end()) ? it->second : nullptr;
|
|
}
|
|
|
|
bool SideChain::is_longer_chain(const PoolBlock* block, const PoolBlock* candidate, bool& is_alternative) const
|
|
{
|
|
is_alternative = false;
|
|
|
|
if (!candidate || !candidate->m_verified || candidate->m_invalid) {
|
|
return false;
|
|
}
|
|
|
|
if (!block) {
|
|
// Switching from an empty to a non-empty chain
|
|
is_alternative = true;
|
|
return true;
|
|
}
|
|
|
|
// If these two blocks are on the same chain, they must have a common ancestor
|
|
|
|
const PoolBlock* block_ancestor = block;
|
|
while (block_ancestor && (block_ancestor->m_sidechainHeight > candidate->m_sidechainHeight)) {
|
|
const hash& id = block_ancestor->m_parent;
|
|
block_ancestor = get_parent(block_ancestor);
|
|
if (!block_ancestor) {
|
|
LOGINFO(4, "couldn't find ancestor " << id << " of block " << block->m_sidechainId << " at height " << block->m_sidechainHeight);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (block_ancestor) {
|
|
const PoolBlock* candidate_ancestor = candidate;
|
|
while (candidate_ancestor->m_sidechainHeight > block_ancestor->m_sidechainHeight) {
|
|
const hash& id = candidate_ancestor->m_parent;
|
|
candidate_ancestor = get_parent(candidate_ancestor);
|
|
if (!candidate_ancestor) {
|
|
LOGINFO(4, "couldn't find ancestor " << id << " of block " << candidate->m_sidechainId << " at height " << candidate->m_sidechainHeight);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
while (block_ancestor && candidate_ancestor) {
|
|
if (block_ancestor->m_parent == candidate_ancestor->m_parent) {
|
|
// If they are really on the same chain, we can just compare cumulative difficulties
|
|
return block->m_cumulativeDifficulty < candidate->m_cumulativeDifficulty;
|
|
}
|
|
block_ancestor = get_parent(block_ancestor);
|
|
candidate_ancestor = get_parent(candidate_ancestor);
|
|
}
|
|
}
|
|
|
|
// They're on totally different chains. Compare total difficulties over the last m_chainWindowSize blocks
|
|
is_alternative = true;
|
|
|
|
difficulty_type block_total_diff;
|
|
difficulty_type candidate_total_diff;
|
|
|
|
const PoolBlock* old_chain = block;
|
|
const PoolBlock* new_chain = candidate;
|
|
|
|
uint64_t candidate_mainchain_height = 0;
|
|
uint64_t candidate_mainchain_min_height = 0;
|
|
|
|
unordered_set<hash> current_chain_monero_blocks, candidate_chain_monero_blocks;
|
|
{
|
|
const uint64_t k = m_chainWindowSize * m_targetBlockTime * 2 / MONERO_BLOCK_TIME;
|
|
current_chain_monero_blocks.reserve(k);
|
|
candidate_chain_monero_blocks.reserve(k);
|
|
}
|
|
|
|
for (uint64_t i = 0; (i < m_chainWindowSize) && (old_chain || new_chain); ++i) {
|
|
if (old_chain) {
|
|
block_total_diff += old_chain->m_difficulty;
|
|
|
|
for (const hash& uncle : old_chain->m_uncles) {
|
|
auto it = m_blocksById.find(uncle);
|
|
if (it != m_blocksById.end()) {
|
|
block_total_diff += it->second->m_difficulty;
|
|
}
|
|
}
|
|
|
|
ChainMain data;
|
|
const hash& h = old_chain->m_prevId;
|
|
|
|
if ((current_chain_monero_blocks.count(h) == 0) && m_pool->chainmain_get_by_hash(h, data)) {
|
|
current_chain_monero_blocks.insert(h);
|
|
}
|
|
|
|
old_chain = get_parent(old_chain);
|
|
}
|
|
|
|
if (new_chain) {
|
|
candidate_mainchain_min_height = candidate_mainchain_min_height ? std::min(candidate_mainchain_min_height, new_chain->m_txinGenHeight) : new_chain->m_txinGenHeight;
|
|
candidate_total_diff += new_chain->m_difficulty;
|
|
|
|
for (const hash& uncle : new_chain->m_uncles) {
|
|
auto it = m_blocksById.find(uncle);
|
|
if (it != m_blocksById.end()) {
|
|
candidate_total_diff += it->second->m_difficulty;
|
|
}
|
|
}
|
|
|
|
ChainMain data;
|
|
const hash& h = new_chain->m_prevId;
|
|
|
|
if ((candidate_chain_monero_blocks.count(h) == 0) && m_pool->chainmain_get_by_hash(h, data)) {
|
|
candidate_chain_monero_blocks.insert(h);
|
|
candidate_mainchain_height = std::max(candidate_mainchain_height, data.height);
|
|
}
|
|
|
|
new_chain = get_parent(new_chain);
|
|
}
|
|
}
|
|
|
|
if (block_total_diff >= candidate_total_diff) {
|
|
return false;
|
|
}
|
|
|
|
// Candidate chain must be built on top of recent mainchain blocks
|
|
MinerData data = m_pool->miner_data();
|
|
if (candidate_mainchain_height + 10 < data.height) {
|
|
LOGWARN(3, "received a longer alternative chain but it's stale: height " << candidate_mainchain_height << ", current height " << data.height);
|
|
return false;
|
|
}
|
|
|
|
const uint64_t limit = m_chainWindowSize * 4 * m_targetBlockTime / MONERO_BLOCK_TIME;
|
|
if (candidate_mainchain_min_height + limit < data.height) {
|
|
LOGWARN(3, "received a longer alternative chain but it's stale: min height " << candidate_mainchain_min_height << ", must be >= " << (data.height - limit));
|
|
return false;
|
|
}
|
|
|
|
// Candidate chain must have been mined on top of at least half as many known Monero blocks, compared to the current chain
|
|
if (candidate_chain_monero_blocks.size() * 2 < current_chain_monero_blocks.size()) {
|
|
LOGWARN(3, "received a longer alternative chain but it wasn't mined on current Salvium blockchain: only " << candidate_chain_monero_blocks.size() << '/' << current_chain_monero_blocks.size() << " blocks found");
|
|
return false;
|
|
}
|
|
|
|
LOGINFO(3, "received a longer alternative chain: height " <<
|
|
log::Gray() << block->m_sidechainHeight << log::NoColor() << " -> " <<
|
|
log::Gray() << candidate->m_sidechainHeight << log::NoColor() << ", cumulative difficulty " <<
|
|
log::Gray() << block->m_cumulativeDifficulty << log::NoColor() << " -> " <<
|
|
log::Gray() << candidate->m_cumulativeDifficulty);
|
|
|
|
return true;
|
|
}
|
|
|
|
void SideChain::update_depths(PoolBlock* block)
|
|
{
|
|
const uint64_t precalc_depth = m_chainWindowSize + UNCLE_BLOCK_DEPTH - 1;
|
|
|
|
auto update_depth = [this, precalc_depth](PoolBlock* b, const uint64_t new_depth) {
|
|
const uint64_t old_depth = b->m_depth;
|
|
if (old_depth < new_depth) {
|
|
b->m_depth = new_depth;
|
|
if ((old_depth < precalc_depth) && (new_depth >= precalc_depth)) {
|
|
launch_precalc(b);
|
|
}
|
|
}
|
|
};
|
|
|
|
for (size_t i = 1; i <= UNCLE_BLOCK_DEPTH; ++i) {
|
|
auto it = m_blocksByHeight.find(block->m_sidechainHeight + i);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
for (const PoolBlock* child : it->second) {
|
|
if (child->m_parent == block->m_sidechainId) {
|
|
if (i != 1) {
|
|
LOGWARN(3, "Block " << block->m_sidechainId << ": m_sidechainHeight is inconsistent with child's m_sidechainHeight.");
|
|
return;
|
|
}
|
|
update_depth(block, child->m_depth + 1);
|
|
}
|
|
|
|
if (std::find(child->m_uncles.begin(), child->m_uncles.end(), block->m_sidechainId) != child->m_uncles.end()) {
|
|
update_depth(block, child->m_depth + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<PoolBlock*> blocks_to_update(1, block);
|
|
|
|
do {
|
|
block = blocks_to_update.back();
|
|
blocks_to_update.pop_back();
|
|
|
|
// Verify this block and possibly other blocks on top of it when we're sure it will get verified
|
|
//
|
|
// Block at exactly "N = (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH" can have an uncle at "N + UNCLE_BLOCK_DEPTH"
|
|
// This uncle has a parent at "N + UNCLE_BLOCK_DEPTH + 1"
|
|
//
|
|
// So a block at "N = (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH" can be safely validated if there is a block
|
|
// at depth > (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH * 2
|
|
//
|
|
|
|
if (!block->m_verified && ((block->m_depth > (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH * 2) || (block->m_sidechainHeight == 0))) {
|
|
verify_loop(block);
|
|
}
|
|
|
|
for (size_t i = 1; i <= UNCLE_BLOCK_DEPTH; ++i) {
|
|
auto it = m_blocksByHeight.find(block->m_sidechainHeight + i);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
for (PoolBlock* child : it->second) {
|
|
const uint64_t old_depth = child->m_depth;
|
|
|
|
if (child->m_parent == block->m_sidechainId) {
|
|
if (i != 1) {
|
|
LOGWARN(3, "Block " << block->m_sidechainId << ": m_sidechainHeight is inconsistent with child's m_sidechainHeight.");
|
|
return;
|
|
}
|
|
if (block->m_depth > 0) {
|
|
update_depth(child, block->m_depth - 1);
|
|
}
|
|
}
|
|
|
|
if (std::find(child->m_uncles.begin(), child->m_uncles.end(), block->m_sidechainId) != child->m_uncles.end()) {
|
|
if (block->m_depth > i) {
|
|
update_depth(child, block->m_depth - i);
|
|
}
|
|
}
|
|
|
|
if (child->m_depth > old_depth) {
|
|
blocks_to_update.push_back(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto it = m_blocksById.find(block->m_parent);
|
|
if (it != m_blocksById.end()) {
|
|
if (it->second->m_sidechainHeight + 1 != block->m_sidechainHeight) {
|
|
LOGWARN(3, "Block " << block->m_sidechainId << ": m_sidechainHeight is inconsistent with parent's m_sidechainHeight.");
|
|
return;
|
|
}
|
|
|
|
if (it->second->m_depth < block->m_depth + 1) {
|
|
update_depth(it->second, block->m_depth + 1);
|
|
blocks_to_update.push_back(it->second);
|
|
}
|
|
}
|
|
|
|
for (const hash& uncle_id : block->m_uncles) {
|
|
it = m_blocksById.find(uncle_id);
|
|
if (it == m_blocksById.end()) {
|
|
continue;
|
|
}
|
|
|
|
if ((it->second->m_sidechainHeight >= block->m_sidechainHeight) || (it->second->m_sidechainHeight + UNCLE_BLOCK_DEPTH < block->m_sidechainHeight)) {
|
|
LOGWARN(3, "Block " << block->m_sidechainId << ": m_sidechainHeight is inconsistent with uncle's m_sidechainHeight.");
|
|
return;
|
|
}
|
|
|
|
const uint64_t d = block->m_sidechainHeight - it->second->m_sidechainHeight;
|
|
if (it->second->m_depth < block->m_depth + d) {
|
|
update_depth(it->second, block->m_depth + d);
|
|
blocks_to_update.push_back(it->second);
|
|
}
|
|
}
|
|
} while (!blocks_to_update.empty());
|
|
}
|
|
|
|
void SideChain::prune_old_blocks()
|
|
{
|
|
// Leave 2 minutes worth of spare blocks in addition to 2xPPLNS window for lagging nodes which need to sync
|
|
const uint64_t prune_distance = (m_chainWindowSize - 1) * 2 + UNCLE_BLOCK_DEPTH * 2 + MONERO_BLOCK_TIME / m_targetBlockTime;
|
|
|
|
// Remove old blocks from alternative unconnected chains after long enough time
|
|
const uint64_t cur_time = seconds_since_epoch();
|
|
const uint64_t prune_delay = m_chainWindowSize * 4 * m_targetBlockTime;
|
|
|
|
const PoolBlock* tip = m_chainTip;
|
|
|
|
#ifdef DEV_TEST_SYNC
|
|
// DEV_TEST_SYNC: Check for sync completion based on m_readyToMine, not pruning
|
|
// This allows the sync test to pass even on chains smaller than prune_distance
|
|
if (m_readyToMine.load()) {
|
|
if (m_firstPruneTime == 0) {
|
|
m_firstPruneTime = seconds_since_epoch();
|
|
LOGINFO(0, log::LightGreen() << "[DEV] Sync complete, starting 120 second test timer");
|
|
|
|
// Test daemon node switching
|
|
if (m_pool) {
|
|
m_pool->reconnect_to_host();
|
|
}
|
|
}
|
|
|
|
if ((cur_time >= m_firstPruneTime + 120) && m_pool && !m_pool->stopped()) {
|
|
LOGINFO(0, log::LightGreen() << "[DEV] Synchronization finished successfully, stopping P2Pool now");
|
|
#ifdef DEV_TRACK_MEMORY
|
|
show_top_10_allocations();
|
|
#endif
|
|
StratumServer* server1 = m_pool->stratum_server();
|
|
P2PServer* server2 = m_pool->p2p_server();
|
|
|
|
if (server1 && server2) {
|
|
server1->print_bans();
|
|
server2->print_bans();
|
|
|
|
server1->show_workers_async();
|
|
server2->show_peers_async();
|
|
}
|
|
|
|
m_pool->print_hosts();
|
|
m_pool->stop();
|
|
|
|
#ifdef DEV_TRACK_MEMORY
|
|
// Give it 1 minute to shut down, otherwise save a minidump
|
|
minidump_and_crash(60 * 1000);
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (tip->m_sidechainHeight < prune_distance) {
|
|
|
|
return;
|
|
}
|
|
|
|
const uint64_t h = tip->m_sidechainHeight - prune_distance;
|
|
// Align prune boundary to CAP intervals for clean cache restoration
|
|
const uint64_t prune_boundary = (h / CHECKPOINT_INTERVAL) * CHECKPOINT_INTERVAL;
|
|
|
|
std::vector<PoolBlock*> blocks_to_prune;
|
|
|
|
for (auto it = m_blocksByHeight.begin(); (it != m_blocksByHeight.end()) && (it->first <= prune_boundary);) {
|
|
const uint64_t height = it->first;
|
|
std::vector<PoolBlock*>& v = it->second;
|
|
|
|
v.erase(std::remove_if(v.begin(), v.end(),
|
|
[this, prune_distance, cur_time, prune_delay, &blocks_to_prune, height](PoolBlock* block)
|
|
{
|
|
// CAP: Never prune anchor point blocks
|
|
if (is_checkpoint_block(block)) {
|
|
LOGINFO(5, "Preserving anchor point block at height " << block->m_sidechainHeight);
|
|
return false;
|
|
}
|
|
if ((block->m_depth >= prune_distance) || (cur_time >= block->m_localTimestamp + prune_delay)) {
|
|
auto it2 = m_blocksById.find(block->m_sidechainId);
|
|
if (it2 != m_blocksById.end()) {
|
|
m_blocksById.erase(it2);
|
|
blocks_to_prune.push_back(block);
|
|
}
|
|
else {
|
|
LOGERR(1, "m_blocksByHeight and m_blocksById are inconsistent at height " << height << ". Fix the code!");
|
|
}
|
|
|
|
auto it3 = m_blocksByMerkleRoot.find(block->m_merkleRoot);
|
|
if (it3 != m_blocksByMerkleRoot.end()) {
|
|
m_blocksByMerkleRoot.erase(it3);
|
|
}
|
|
else {
|
|
LOGERR(1, "m_blocksByHeight and m_blocksByMerkleRoot are inconsistent at height " << height << ". Fix the code!");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}), v.end());
|
|
|
|
if (v.empty()) {
|
|
it = m_blocksByHeight.erase(it);
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (!blocks_to_prune.empty()) {
|
|
LOGINFO(4, "pruned " << blocks_to_prune.size() << " old blocks at heights <= " << prune_boundary);
|
|
|
|
// If side-chain started pruning blocks it means the initial sync is complete
|
|
// It's now safe to delete cached blocks
|
|
if (p2pServer()) {
|
|
p2pServer()->clear_cached_blocks();
|
|
}
|
|
|
|
// Pre-calc workers are not needed anymore
|
|
finish_precalc();
|
|
|
|
// We can only delete old blocks after the precalc is stopped because it can still use some of them
|
|
for (const PoolBlock* b : blocks_to_prune) {
|
|
delete b;
|
|
}
|
|
}
|
|
|
|
// If side-chain started pruning blocks it means the initial sync is complete
|
|
// It's now safe to delete cached blocks
|
|
if (!m_readyToMine.load()) {
|
|
// Validate cached checkpoints before enabling mining
|
|
LOGINFO(0, "Prune path: checking m_checkpointsNeedValidation=" << (m_checkpointsNeedValidation ? "true" : "false"));
|
|
if (m_checkpointsNeedValidation) {
|
|
if (!validate_loaded_checkpoints()) {
|
|
LOGWARN(0, "Checkpoint validation deferred - waiting for sync, mining delayed");
|
|
return;
|
|
}
|
|
|
|
// If validation triggered recovery, don't enable mining yet
|
|
if (m_recoveryMode.load()) {
|
|
LOGWARN(0, "Checkpoint validation failed - recovery in progress, mining delayed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const PoolBlock* current_tip = m_chainTip.load();
|
|
if (current_tip && current_tip->m_verified) {
|
|
m_readyToMine.store(true);
|
|
LOGINFO(0, log::LightGreen() << "########################################");
|
|
LOGINFO(0, log::LightGreen() << "SIDECHAIN LOADED - MINING IS NOW ENABLED");
|
|
LOGINFO(0, log::LightGreen() << "########################################");
|
|
}
|
|
}
|
|
}
|
|
|
|
void SideChain::get_missing_blocks(unordered_set<hash>& missing_blocks) const
|
|
{
|
|
missing_blocks.clear();
|
|
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
const uint64_t cp_height = get_latest_checkpoint_height();
|
|
uint64_t lowest_unverified = UINT64_MAX;
|
|
|
|
for (auto& b : m_blocksById) {
|
|
if (b.second->m_verified) {
|
|
continue;
|
|
}
|
|
|
|
if (b.second->m_sidechainHeight < lowest_unverified) {
|
|
lowest_unverified = b.second->m_sidechainHeight;
|
|
}
|
|
|
|
if (!b.second->m_parent.empty() && (m_blocksById.find(b.second->m_parent) == m_blocksById.end())) {
|
|
missing_blocks.insert(b.second->m_parent);
|
|
// Log missing parents near checkpoint
|
|
if (b.second->m_sidechainHeight <= cp_height + 5) {
|
|
LOGINFO(6, "RECOVERY DEBUG: block " << b.second->m_sidechainHeight
|
|
<< " needs missing parent " << b.second->m_parent);
|
|
}
|
|
}
|
|
|
|
int num_missing_uncles = 0;
|
|
|
|
for (const hash& h : b.second->m_uncles) {
|
|
if (!h.empty() && (m_blocksById.find(h) == m_blocksById.end())) {
|
|
missing_blocks.insert(h);
|
|
|
|
// Get no more than 2 first missing uncles at a time from each block
|
|
// Blocks with more than 2 uncles are very rare and they will be processed in several steps
|
|
++num_missing_uncles;
|
|
if (num_missing_uncles >= 2) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Summary log for recovery debugging
|
|
if (!missing_blocks.empty() && lowest_unverified <= cp_height + 10) {
|
|
LOGINFO(6, "RECOVERY DEBUG: " << missing_blocks.size() << " missing blocks, lowest unverified=" << lowest_unverified);
|
|
}
|
|
}
|
|
|
|
void SideChain::retry_unverified_blocks()
|
|
{
|
|
WriteLock lock(m_sidechainLock);
|
|
|
|
// Scan for unverified blocks and retry them
|
|
// This is called when new mainchain data arrives that might allow verification
|
|
std::vector<PoolBlock*> blocks_to_retry;
|
|
|
|
for (auto& pair : m_blocksById) {
|
|
PoolBlock* block = pair.second;
|
|
if (!block->m_verified && !block->m_invalid) {
|
|
blocks_to_retry.push_back(block);
|
|
}
|
|
}
|
|
|
|
if (blocks_to_retry.empty()) {
|
|
return;
|
|
}
|
|
|
|
LOGINFO(4, "Retrying verification of " << blocks_to_retry.size() << " unverified blocks after mainchain update");
|
|
|
|
// Sort by height to process in order
|
|
std::sort(blocks_to_retry.begin(), blocks_to_retry.end(),
|
|
[](const PoolBlock* a, const PoolBlock* b) {
|
|
return a->m_sidechainHeight < b->m_sidechainHeight;
|
|
});
|
|
|
|
// Try to verify each block
|
|
uint32_t verified_count = 0;
|
|
for (PoolBlock* block : blocks_to_retry) {
|
|
if (block->m_verified) {
|
|
continue; // Already verified by earlier iteration
|
|
}
|
|
verify_loop(block);
|
|
if (block->m_verified) {
|
|
++verified_count;
|
|
}
|
|
}
|
|
|
|
if (verified_count > 0) {
|
|
LOGINFO(3, "Verified " << verified_count << " blocks after mainchain update");
|
|
}
|
|
}
|
|
|
|
bool SideChain::consider_peer_genesis(const hash& genesis_id, uint64_t timestamp, uint64_t height)
|
|
{
|
|
// Get our current genesis info for comparison
|
|
uint64_t our_genesis_timestamp = 0;
|
|
hash our_genesis_id;
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
auto it = m_blocksByHeight.find(0);
|
|
if (it != m_blocksByHeight.end() && !it->second.empty()) {
|
|
our_genesis_timestamp = it->second.front()->m_timestamp;
|
|
our_genesis_id = it->second.front()->m_sidechainId;
|
|
}
|
|
}
|
|
|
|
// If we have a genesis and peer's is older (or same timestamp but lower hash), we need to yield
|
|
if (m_genesisDecisionMade) {
|
|
if (our_genesis_timestamp > 0) {
|
|
// We have an actual genesis block - check if peer's is older
|
|
const bool peer_wins = (timestamp < our_genesis_timestamp) ||
|
|
(timestamp == our_genesis_timestamp && genesis_id < our_genesis_id);
|
|
if (peer_wins) {
|
|
LOGWARN(3, "Peer has older genesis (theirs=" << timestamp
|
|
<< " ours=" << our_genesis_timestamp << "), purging to re-sync");
|
|
purge_sidechain();
|
|
// Fall through to adopt peer's genesis
|
|
} else {
|
|
// Our genesis is older or same, keep it
|
|
return true;
|
|
}
|
|
} else {
|
|
// We decided to create our own genesis but haven't mined it yet
|
|
// Reset and adopt peer's genesis instead
|
|
LOGINFO(3, "Canceling own genesis creation, will adopt peer's genesis");
|
|
m_genesisDecisionMade = false;
|
|
}
|
|
}
|
|
|
|
if (m_adoptedGenesisTimestamp == 0 || timestamp < m_adoptedGenesisTimestamp ||
|
|
(timestamp == m_adoptedGenesisTimestamp && genesis_id < m_adoptedGenesisId)) {
|
|
LOGINFO(3, "Adopting older genesis from peer: " << genesis_id
|
|
<< " timestamp=" << timestamp << " height=" << height);
|
|
m_adoptedGenesisId = genesis_id;
|
|
m_adoptedGenesisTimestamp = timestamp;
|
|
m_adoptedGenesisHeight = height;
|
|
m_adoptedGenesisTime = seconds_since_epoch();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SideChain::purge_sidechain()
|
|
{
|
|
LOGWARN(3, "Purging sidechain to adopt older genesis from peer");
|
|
|
|
WriteLock lock(m_sidechainLock);
|
|
|
|
// Free all block memory
|
|
for (const auto& it : m_blocksById) {
|
|
delete it.second;
|
|
}
|
|
|
|
// Clear all block tracking
|
|
m_blocksById.clear();
|
|
m_blocksByHeight.clear();
|
|
m_blocksByMerkleRoot.clear();
|
|
m_chainTip = nullptr;
|
|
|
|
// Clear difficulty data
|
|
m_difficultyData.clear();
|
|
|
|
{
|
|
WriteLock diff_lock(m_curDifficultyLock);
|
|
m_curDifficulty = m_minDifficulty;
|
|
}
|
|
|
|
// Reset genesis state to allow new genesis adoption
|
|
m_genesisDecisionMade = false;
|
|
m_adoptedGenesisId = {};
|
|
m_adoptedGenesisTimestamp = 0;
|
|
m_adoptedGenesisHeight = 0;
|
|
m_adoptedGenesisTime = 0;
|
|
|
|
// Delete cache files to prevent reload of old blocks on restart
|
|
const std::string cache_path = DATA_DIR + "p2pool.cache";
|
|
const std::string version_path = DATA_DIR + "p2pool.cache.version";
|
|
|
|
if (remove(cache_path.c_str()) == 0) {
|
|
LOGINFO(3, "Deleted cache file: " << cache_path);
|
|
}
|
|
if (remove(version_path.c_str()) == 0) {
|
|
LOGINFO(3, "Deleted cache version file: " << version_path);
|
|
}
|
|
LOGINFO(3, "Sidechain purged, ready to sync from peer");
|
|
}
|
|
|
|
bool SideChain::get_genesis_info(hash& id, uint64_t& timestamp, uint64_t& height) const
|
|
{
|
|
ReadLock lock(m_sidechainLock);
|
|
|
|
auto it = m_blocksByHeight.find(0);
|
|
if (it == m_blocksByHeight.end() || it->second.empty()) {
|
|
return false;
|
|
}
|
|
|
|
const PoolBlock* genesis = it->second.front();
|
|
id = genesis->m_sidechainId;
|
|
timestamp = genesis->m_timestamp;
|
|
height = genesis->m_txinGenHeight;
|
|
return true;
|
|
}
|
|
|
|
bool SideChain::load_config(const std::string& filename)
|
|
{
|
|
if (filename.empty()) {
|
|
LOGINFO(1, "using default config");
|
|
return true;
|
|
}
|
|
|
|
LOGINFO(1, "loading config from " << log::Gray() << filename);
|
|
|
|
std::ifstream f(filename);
|
|
if (!f.is_open()) {
|
|
LOGERR(1, "can't open " << filename);
|
|
return false;
|
|
}
|
|
|
|
rapidjson::Document doc;
|
|
rapidjson::IStreamWrapper s(f);
|
|
if (doc.ParseStream<rapidjson::kParseCommentsFlag | rapidjson::kParseTrailingCommasFlag>(s).HasParseError()) {
|
|
LOGERR(1, "failed to parse JSON data in " << filename);
|
|
return false;
|
|
}
|
|
|
|
if (!doc.IsObject()) {
|
|
LOGERR(1, "invalid JSON data in " << filename << ": top level is not an object");
|
|
return false;
|
|
}
|
|
|
|
parseValue(doc, "name", m_poolName);
|
|
parseValue(doc, "password", m_poolPassword);
|
|
parseValue(doc, "block_time", m_targetBlockTime);
|
|
|
|
uint64_t min_diff;
|
|
if (parseValue(doc, "min_diff", min_diff) && min_diff) {
|
|
m_minDifficulty = { min_diff, 0 };
|
|
}
|
|
|
|
parseValue(doc, "pplns_window", m_chainWindowSize);
|
|
parseValue(doc, "uncle_penalty", m_unclePenalty);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SideChain::check_config() const
|
|
{
|
|
if (m_poolName.empty()) {
|
|
LOGERR(1, "name can't be empty");
|
|
return false;
|
|
}
|
|
|
|
if (m_poolName.length() > 128) {
|
|
LOGERR(1, "name is too long (must be 128 characters max)");
|
|
return false;
|
|
}
|
|
|
|
if (m_poolPassword.length() > 128) {
|
|
LOGERR(1, "password is too long (must be 128 characters max)");
|
|
return false;
|
|
}
|
|
|
|
if ((m_targetBlockTime < 1) || (m_targetBlockTime > MONERO_BLOCK_TIME)) {
|
|
LOGERR(1, "block_time is invalid (must be between 1 and " << MONERO_BLOCK_TIME << ")");
|
|
return false;
|
|
}
|
|
|
|
if (s_networkType == NetworkType::Mainnet) {
|
|
const difficulty_type min_diff{ MIN_DIFFICULTY, 0 };
|
|
const difficulty_type max_diff{ 1000000000, 0 };
|
|
|
|
if ((m_minDifficulty < min_diff) || (max_diff < m_minDifficulty)) {
|
|
LOGERR(1, "min_diff is invalid (must be between " << min_diff << " and " << max_diff << ')');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((m_chainWindowSize < 60) || (m_chainWindowSize > 2160)) {
|
|
LOGERR(1, "pplns_window is invalid (must be between 60 and 2160)");
|
|
return false;
|
|
}
|
|
|
|
if ((m_unclePenalty < 1) || (m_unclePenalty > 99)) {
|
|
LOGERR(1, "uncle_penalty is invalid (must be between 1 and 99)");
|
|
return false;
|
|
}
|
|
|
|
LOGINFO(1, log::LightCyan() << "pool name = " << m_poolName);
|
|
LOGINFO(1, log::LightCyan() << "block time = " << m_targetBlockTime << " seconds");
|
|
LOGINFO(1, log::LightCyan() << "min diff = " << m_minDifficulty);
|
|
LOGINFO(1, log::LightCyan() << "PPLNS window = " << m_chainWindowSize << " blocks");
|
|
LOGINFO(1, log::LightCyan() << "uncle penalty = " << m_unclePenalty << '%');
|
|
|
|
return true;
|
|
}
|
|
|
|
void SideChain::launch_precalc(const PoolBlock* block)
|
|
{
|
|
if (m_precalcFinished) {
|
|
return;
|
|
}
|
|
|
|
for (int h = UNCLE_BLOCK_DEPTH; h >= 0; --h) {
|
|
auto it = m_blocksByHeight.find(block->m_sidechainHeight + m_chainWindowSize + h - 1);
|
|
if (it == m_blocksByHeight.end()) {
|
|
continue;
|
|
}
|
|
for (PoolBlock* b : it->second) {
|
|
if (b->m_precalculated) {
|
|
continue;
|
|
}
|
|
std::vector<MinerShare> shares;
|
|
if (get_shares(b, shares, nullptr, true)) {
|
|
b->m_precalculated = true;
|
|
{
|
|
WriteLock lock(*PoolBlock::s_precalculatedSharesLock);
|
|
b->m_precalculatedShares = std::move(shares);
|
|
}
|
|
{
|
|
MutexLock lock2(m_precalcJobsMutex);
|
|
m_precalcJobs.push_back(b);
|
|
uv_cond_signal(&m_precalcJobsCond);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SideChain::precalc_worker()
|
|
{
|
|
set_thread_name("Precalc");
|
|
|
|
std::vector<std::pair<size_t, const Wallet*>> wallets;
|
|
wallets.reserve(m_chainWindowSize);
|
|
|
|
do {
|
|
const PoolBlock* job;
|
|
|
|
{
|
|
MutexLock lock(m_precalcJobsMutex);
|
|
|
|
if (m_precalcFinished) {
|
|
return;
|
|
}
|
|
|
|
while (m_precalcJobs.empty()) {
|
|
uv_cond_wait(&m_precalcJobsCond, &m_precalcJobsMutex);
|
|
|
|
if (m_precalcFinished) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
job = m_precalcJobs.back();
|
|
m_precalcJobs.pop_back();
|
|
|
|
// Filter out duplicate inputs for get_eph_public_key()
|
|
uint8_t t[HASH_SIZE * 2 + sizeof(size_t)];
|
|
memcpy(t, job->m_txkeySec.h, HASH_SIZE);
|
|
|
|
wallets.clear();
|
|
|
|
ReadLock lock2(*PoolBlock::s_precalculatedSharesLock);
|
|
|
|
const size_t n = job->m_precalculatedShares.size();
|
|
|
|
for (size_t i = 0; i < n; ++i) {
|
|
memcpy(t + HASH_SIZE, job->m_precalculatedShares[i].m_wallet->view_public_key().h, HASH_SIZE);
|
|
memcpy(t + HASH_SIZE * 2, &i, sizeof(i));
|
|
if (m_uniquePrecalcInputs->insert(robin_hood::hash_bytes(t, array_size(t))).second) {
|
|
wallets.emplace_back(i, job->m_precalculatedShares[i].m_wallet);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const std::pair<size_t, const Wallet*>& w : wallets) {
|
|
hash eph_public_key;
|
|
uint8_t view_tag;
|
|
if (!w.second->get_eph_public_key(job->m_txkeySec, w.first, eph_public_key, view_tag)) {
|
|
LOGWARN(6, "get_eph_public_key failed in precalc_worker");
|
|
}
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
void SideChain::finish_precalc()
|
|
{
|
|
if (m_precalcFinished.exchange(true)) {
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
{
|
|
MutexLock lock(m_precalcJobsMutex);
|
|
m_precalcJobs.clear();
|
|
m_precalcJobs.shrink_to_fit();
|
|
uv_cond_broadcast(&m_precalcJobsCond);
|
|
}
|
|
|
|
for (std::thread& t : m_precalcWorkers) {
|
|
t.join();
|
|
}
|
|
m_precalcWorkers.clear();
|
|
m_precalcWorkers.shrink_to_fit();
|
|
|
|
delete m_uniquePrecalcInputs;
|
|
m_uniquePrecalcInputs = nullptr;
|
|
|
|
uv_mutex_destroy(&m_precalcJobsMutex);
|
|
uv_cond_destroy(&m_precalcJobsCond);
|
|
|
|
// Also clear cache because it has data from all old blocks now
|
|
clear_crypto_cache();
|
|
|
|
LOGINFO(4, "pre-calculation workers stopped");
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LOGERR(1, "exception in finish_precalc(): " << e.what());
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CHECKPOINT SYSTEM
|
|
// ============================================================================
|
|
|
|
bool SideChain::is_checkpoint_block(const PoolBlock* block) const
|
|
{
|
|
ReadLock lock(m_checkpointsLock);
|
|
for (const Checkpoint& cp : m_checkpoints) {
|
|
if (cp.height == block->m_sidechainHeight && cp.id == block->m_sidechainId) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SideChain::update_checkpoints(uint64_t new_height)
|
|
{
|
|
const uint64_t checkpoint_height = (new_height / CHECKPOINT_INTERVAL) * CHECKPOINT_INTERVAL;
|
|
|
|
if (checkpoint_height == 0) return;
|
|
|
|
bool checkpoint_created = false;
|
|
|
|
{
|
|
WriteLock lock(m_checkpointsLock);
|
|
|
|
// Check if we already have this checkpoint
|
|
if (!m_checkpoints.empty() && m_checkpoints.back().height >= checkpoint_height) {
|
|
return;
|
|
}
|
|
|
|
// Find the block at checkpoint height
|
|
const PoolBlock* block = find_verified_block_at_height(checkpoint_height);
|
|
if (block && block->m_verified) {
|
|
Checkpoint cp;
|
|
cp.height = checkpoint_height;
|
|
cp.id = block->m_sidechainId;
|
|
cp.cumulative_difficulty = block->m_cumulativeDifficulty;
|
|
|
|
m_checkpoints.push_back(cp);
|
|
|
|
LOGINFO(1, "Checkpoint created: height " << cp.height
|
|
<< ", id " << cp.id);
|
|
|
|
// Prune old checkpoints beyond history limit
|
|
while (m_checkpoints.size() > CHECKPOINT_HISTORY) {
|
|
LOGINFO(3, "Pruning old checkpoint at height " << m_checkpoints.front().height);
|
|
m_checkpoints.erase(m_checkpoints.begin());
|
|
}
|
|
|
|
checkpoint_created = true;
|
|
}
|
|
} // WriteLock released here
|
|
|
|
// Save outside the lock to avoid deadlock
|
|
if (checkpoint_created) {
|
|
save_checkpoints();
|
|
}
|
|
}
|
|
|
|
PoolBlock* SideChain::find_verified_block_at_height(uint64_t height) const
|
|
{
|
|
auto it = m_blocksByHeight.find(height);
|
|
if (it == m_blocksByHeight.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Return the first verified block at this height (should be on main chain)
|
|
for (PoolBlock* block : it->second) {
|
|
if (block->m_verified) {
|
|
return block;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Checkpoint SideChain::get_latest_checkpoint() const
|
|
{
|
|
ReadLock lock(m_checkpointsLock);
|
|
if (m_checkpoints.empty()) {
|
|
return Checkpoint{0, hash(), difficulty_type()};
|
|
}
|
|
return m_checkpoints.back();
|
|
}
|
|
|
|
std::vector<Checkpoint> SideChain::get_checkpoint_history() const
|
|
{
|
|
ReadLock lock(m_checkpointsLock);
|
|
return m_checkpoints;
|
|
}
|
|
|
|
uint64_t SideChain::get_latest_checkpoint_height() const
|
|
{
|
|
ReadLock lock(m_checkpointsLock);
|
|
if (m_checkpoints.empty()) {
|
|
return 0;
|
|
}
|
|
return m_checkpoints.back().height;
|
|
}
|
|
|
|
bool SideChain::validate_peer_checkpoint(uint64_t height, const hash& peer_hash) const
|
|
{
|
|
ReadLock lock(m_checkpointsLock);
|
|
|
|
for (const Checkpoint& cp : m_checkpoints) {
|
|
if (cp.height == height) {
|
|
return cp.id == peer_hash;
|
|
}
|
|
}
|
|
|
|
// We don't have this checkpoint yet - can't validate
|
|
return true;
|
|
}
|
|
|
|
void SideChain::on_block_rejected(const PoolBlock* block, const char* reason)
|
|
{
|
|
// Don't count failures during initial sync - blocks arrive out of order
|
|
if (!m_readyToMine.load()) {
|
|
return;
|
|
}
|
|
|
|
++m_externalBlockFailures;
|
|
|
|
LOGWARN(3, "Block rejected (" << m_externalBlockFailures << " consecutive): "
|
|
<< "height=" << block->m_sidechainHeight
|
|
<< ", id=" << block->m_sidechainId
|
|
<< ", reason: " << reason);
|
|
|
|
if (m_externalBlockFailures >= DIVERGENCE_THRESHOLD && !m_recoveryMode.load()) {
|
|
LOGERR(0, "");
|
|
LOGERR(0, "================================================");
|
|
LOGERR(0, " CONSENSUS FAILURE DETECTED");
|
|
LOGERR(0, " " << m_externalBlockFailures << " consecutive blocks rejected");
|
|
LOGERR(0, " Failure around height " << block->m_sidechainHeight);
|
|
LOGERR(0, " Initiating recovery...");
|
|
LOGERR(0, "================================================");
|
|
LOGERR(0, "");
|
|
trigger_recovery(block->m_sidechainHeight);
|
|
}
|
|
}
|
|
|
|
void SideChain::on_block_accepted()
|
|
{
|
|
m_externalBlockFailures = 0;
|
|
}
|
|
|
|
void SideChain::trigger_recovery(uint64_t failure_height)
|
|
{
|
|
if (m_recoveryMode.exchange(true)) {
|
|
// Already in recovery mode
|
|
return;
|
|
}
|
|
|
|
// Disable mining immediately
|
|
m_readyToMine = false;
|
|
|
|
// Find the checkpoint before the failure
|
|
uint64_t recovery_checkpoint = (failure_height / CHECKPOINT_INTERVAL) * CHECKPOINT_INTERVAL;
|
|
if (recovery_checkpoint >= failure_height && recovery_checkpoint >= CHECKPOINT_INTERVAL) {
|
|
recovery_checkpoint -= CHECKPOINT_INTERVAL;
|
|
}
|
|
|
|
LOGINFO(0, "Recovery target checkpoint: " << recovery_checkpoint);
|
|
|
|
m_pendingRecoveryHeight = recovery_checkpoint;
|
|
|
|
// Request checkpoint validation from peers via P2P server
|
|
request_checkpoint_validation();
|
|
}
|
|
|
|
void SideChain::request_checkpoint_validation()
|
|
{
|
|
// This will be called by P2P server to initiate checkpoint comparison
|
|
// For now, proceed directly to reset if we have a pending recovery
|
|
uint64_t checkpoint_height = m_pendingRecoveryHeight.load();
|
|
if (checkpoint_height > 0) {
|
|
reset_to_checkpoint(checkpoint_height);
|
|
}
|
|
}
|
|
|
|
void SideChain::reset_to_checkpoint(uint64_t checkpoint_height)
|
|
{
|
|
LOGINFO(0, "");
|
|
LOGINFO(0, "================================================");
|
|
LOGINFO(0, " RESETTING TO CHECKPOINT " << checkpoint_height);
|
|
LOGINFO(0, "================================================");
|
|
|
|
WriteLock lock(m_sidechainLock);
|
|
|
|
// Find the nearest available checkpoint at or below target height
|
|
PoolBlock* checkpoint_block = nullptr;
|
|
uint64_t actual_checkpoint_height = 0;
|
|
{
|
|
ReadLock cpLock(m_checkpointsLock);
|
|
for (auto it = m_checkpoints.rbegin(); it != m_checkpoints.rend(); ++it) {
|
|
if (it->height <= checkpoint_height) {
|
|
auto block_it = m_blocksById.find(it->id);
|
|
if (block_it != m_blocksById.end()) {
|
|
checkpoint_block = block_it->second;
|
|
actual_checkpoint_height = it->height;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!checkpoint_block) {
|
|
LOGWARN(0, "No checkpoint found at or below height " << checkpoint_height << " - recovery aborted");
|
|
m_recoveryMode = false;
|
|
m_pendingRecoveryHeight = 0;
|
|
return;
|
|
}
|
|
|
|
if (actual_checkpoint_height != checkpoint_height) {
|
|
LOGINFO(0, "Using nearest checkpoint at height " << actual_checkpoint_height << " (requested " << checkpoint_height << ")");
|
|
}
|
|
|
|
// Remove blocks above checkpoint so fresh ones come from peers
|
|
// Collect block info to remove (can't modify map while iterating)
|
|
std::vector<std::pair<hash, PoolBlock::full_id>> blocks_to_remove;
|
|
for (const auto& kv : m_blocksById) {
|
|
if (kv.second->m_sidechainHeight > actual_checkpoint_height) {
|
|
blocks_to_remove.emplace_back(kv.first, kv.second->get_full_id());
|
|
}
|
|
}
|
|
|
|
// Remove from all indices
|
|
for (const auto& [id, full_id] : blocks_to_remove) {
|
|
auto it = m_blocksById.find(id);
|
|
if (it != m_blocksById.end()) {
|
|
PoolBlock* block = it->second;
|
|
|
|
// Remove from blocksByHeight vector
|
|
auto height_it = m_blocksByHeight.find(block->m_sidechainHeight);
|
|
if (height_it != m_blocksByHeight.end()) {
|
|
auto& vec = height_it->second;
|
|
vec.erase(std::remove(vec.begin(), vec.end(), block), vec.end());
|
|
if (vec.empty()) {
|
|
m_blocksByHeight.erase(height_it);
|
|
}
|
|
}
|
|
|
|
m_blocksById.erase(it);
|
|
delete block;
|
|
}
|
|
}
|
|
|
|
// Forget deleted blocks so they can be re-downloaded from peers
|
|
{
|
|
MutexLock incomingLock(m_incomingBlocksLock);
|
|
for (const auto& [id, full_id] : blocks_to_remove) {
|
|
m_incomingBlocks.erase(full_id);
|
|
}
|
|
}
|
|
|
|
LOGINFO(0, "Removed " << blocks_to_remove.size() << " blocks above checkpoint " << actual_checkpoint_height);
|
|
LOGINFO(0, "Cleared " << blocks_to_remove.size() << " entries from incoming blocks tracking");
|
|
|
|
checkpoint_block->m_verified = true; // Mark verified for propagation
|
|
checkpoint_block->m_invalid = false; // Ensure not marked invalid
|
|
m_chainTip = checkpoint_block;
|
|
LOGINFO(0, "Chain tip reset to height " << actual_checkpoint_height
|
|
<< ", id " << checkpoint_block->m_sidechainId
|
|
<< ", verified=" << (checkpoint_block->m_verified ? 1 : 0));
|
|
|
|
// Clear checkpoints after this point
|
|
{
|
|
WriteLock cpLock(m_checkpointsLock);
|
|
while (!m_checkpoints.empty() && m_checkpoints.back().height > actual_checkpoint_height) {
|
|
m_checkpoints.pop_back();
|
|
}
|
|
}
|
|
|
|
// Reset counters
|
|
m_externalBlockFailures = 0;
|
|
m_pendingRecoveryHeight = 0;
|
|
m_recoveryMode = false;
|
|
|
|
LOGINFO(0, "");
|
|
LOGINFO(0, "================================================");
|
|
LOGINFO(0, " RESET COMPLETE - RESYNCING FROM " << checkpoint_height);
|
|
LOGINFO(0, "================================================");
|
|
LOGINFO(0, "");
|
|
|
|
// Mining will be re-enabled after resync completes
|
|
}
|
|
|
|
void SideChain::clear_checkpoints()
|
|
{
|
|
WriteLock lock(m_checkpointsLock);
|
|
m_checkpoints.clear();
|
|
m_checkpointsNeedValidation = false;
|
|
LOGINFO(1, "Cleared in-memory checkpoints");
|
|
}
|
|
|
|
void SideChain::save_checkpoints() const
|
|
{
|
|
#ifdef WITH_REDIS
|
|
ReadLock lock(m_checkpointsLock);
|
|
|
|
if (m_checkpoints.empty()) {
|
|
get_redis_storage().del("checkpoints");
|
|
return;
|
|
}
|
|
|
|
// Build binary buffer matching file format
|
|
std::vector<uint8_t> buf;
|
|
const uint32_t version = 1;
|
|
const uint32_t count = static_cast<uint32_t>(m_checkpoints.size());
|
|
|
|
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&version),
|
|
reinterpret_cast<const uint8_t*>(&version) + sizeof(version));
|
|
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&count),
|
|
reinterpret_cast<const uint8_t*>(&count) + sizeof(count));
|
|
|
|
for (const Checkpoint& cp : m_checkpoints) {
|
|
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&cp.height),
|
|
reinterpret_cast<const uint8_t*>(&cp.height) + sizeof(cp.height));
|
|
buf.insert(buf.end(), cp.id.h, cp.id.h + HASH_SIZE);
|
|
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&cp.cumulative_difficulty),
|
|
reinterpret_cast<const uint8_t*>(&cp.cumulative_difficulty) + sizeof(cp.cumulative_difficulty));
|
|
}
|
|
|
|
if (!get_redis_storage().set("checkpoints", buf.data(), buf.size())) {
|
|
LOGWARN(1, "Failed to save checkpoints to Redis");
|
|
return;
|
|
}
|
|
LOGINFO(3, "Saved " << count << " checkpoints to Redis");
|
|
#else
|
|
const std::string path = DATA_DIR + "p2pool_checkpoints.dat";
|
|
|
|
ReadLock lock(m_checkpointsLock);
|
|
|
|
if (m_checkpoints.empty()) {
|
|
// No checkpoints to save, remove old file if exists
|
|
remove(path.c_str());
|
|
return;
|
|
}
|
|
|
|
std::ofstream f(path, std::ios::binary);
|
|
if (!f.is_open()) {
|
|
LOGWARN(1, "Failed to save checkpoints to " << path);
|
|
return;
|
|
}
|
|
|
|
// Write version marker for future compatibility
|
|
const uint32_t version = 1;
|
|
f.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
|
|
|
// Write checkpoint count
|
|
const uint32_t count = static_cast<uint32_t>(m_checkpoints.size());
|
|
f.write(reinterpret_cast<const char*>(&count), sizeof(count));
|
|
|
|
// Write each checkpoint
|
|
for (const Checkpoint& cp : m_checkpoints) {
|
|
f.write(reinterpret_cast<const char*>(&cp.height), sizeof(cp.height));
|
|
f.write(reinterpret_cast<const char*>(cp.id.h), HASH_SIZE);
|
|
f.write(reinterpret_cast<const char*>(&cp.cumulative_difficulty), sizeof(cp.cumulative_difficulty));
|
|
}
|
|
|
|
f.close();
|
|
LOGINFO(3, "Saved " << count << " checkpoints to " << path);
|
|
#endif
|
|
}
|
|
|
|
void SideChain::load_checkpoints()
|
|
{
|
|
#ifdef WITH_REDIS
|
|
std::vector<uint8_t> buf;
|
|
if (!get_redis_storage().get("checkpoints", buf)) {
|
|
LOGINFO(3, "No checkpoints found in Redis (normal for first run)");
|
|
return;
|
|
}
|
|
|
|
if (buf.size() < 8) {
|
|
LOGWARN(1, "Checkpoint data too small, ignoring");
|
|
return;
|
|
}
|
|
|
|
size_t offset = 0;
|
|
|
|
// Read version
|
|
uint32_t version = *reinterpret_cast<const uint32_t*>(buf.data() + offset);
|
|
offset += sizeof(version);
|
|
if (version != 1) {
|
|
LOGWARN(1, "Unknown checkpoint version " << version << ", ignoring");
|
|
return;
|
|
}
|
|
|
|
// Read count
|
|
uint32_t count = *reinterpret_cast<const uint32_t*>(buf.data() + offset);
|
|
offset += sizeof(count);
|
|
if (count > 100) {
|
|
LOGWARN(1, "Suspicious checkpoint count " << count << ", ignoring");
|
|
return;
|
|
}
|
|
|
|
WriteLock lock(m_checkpointsLock);
|
|
m_checkpoints.clear();
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
if (offset + sizeof(uint64_t) + HASH_SIZE + sizeof(difficulty_type) > buf.size()) {
|
|
LOGWARN(1, "Checkpoint data corrupted at entry " << i << ", discarding");
|
|
m_checkpoints.clear();
|
|
return;
|
|
}
|
|
|
|
Checkpoint cp;
|
|
memcpy(&cp.height, buf.data() + offset, sizeof(cp.height));
|
|
offset += sizeof(cp.height);
|
|
memcpy(cp.id.h, buf.data() + offset, HASH_SIZE);
|
|
offset += HASH_SIZE;
|
|
memcpy(&cp.cumulative_difficulty, buf.data() + offset, sizeof(cp.cumulative_difficulty));
|
|
offset += sizeof(cp.cumulative_difficulty);
|
|
|
|
m_checkpoints.push_back(cp);
|
|
}
|
|
|
|
LOGINFO(1, "Loaded " << count << " checkpoints from Redis");
|
|
|
|
if (!m_checkpoints.empty()) {
|
|
LOGINFO(1, "Latest anchor point: height " << m_checkpoints.back().height <<
|
|
", id " << m_checkpoints.back().id);
|
|
m_checkpointsNeedValidation = true;
|
|
}
|
|
#else
|
|
const std::string path = DATA_DIR + "p2pool_checkpoints.dat";
|
|
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f.is_open()) {
|
|
LOGINFO(3, "No checkpoint file found at " << path << " (normal for first run)");
|
|
return;
|
|
}
|
|
|
|
// Read version
|
|
uint32_t version = 0;
|
|
f.read(reinterpret_cast<char*>(&version), sizeof(version));
|
|
if (version != 1) {
|
|
LOGWARN(1, "Unknown checkpoint file version " << version << ", ignoring");
|
|
return;
|
|
}
|
|
|
|
// Read checkpoint count
|
|
uint32_t count = 0;
|
|
f.read(reinterpret_cast<char*>(&count), sizeof(count));
|
|
|
|
if (count > 100) {
|
|
LOGWARN(1, "Checkpoint file has suspicious count " << count << ", ignoring");
|
|
return;
|
|
}
|
|
|
|
WriteLock lock(m_checkpointsLock);
|
|
m_checkpoints.clear();
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
Checkpoint cp;
|
|
f.read(reinterpret_cast<char*>(&cp.height), sizeof(cp.height));
|
|
f.read(reinterpret_cast<char*>(cp.id.h), HASH_SIZE);
|
|
f.read(reinterpret_cast<char*>(&cp.cumulative_difficulty), sizeof(cp.cumulative_difficulty));
|
|
|
|
if (f.fail()) {
|
|
LOGWARN(1, "Checkpoint file corrupted at entry " << i << ", discarding");
|
|
m_checkpoints.clear();
|
|
return;
|
|
}
|
|
|
|
m_checkpoints.push_back(cp);
|
|
}
|
|
|
|
f.close();
|
|
LOGINFO(1, "Loaded " << count << " checkpoints from " << path);
|
|
|
|
if (!m_checkpoints.empty()) {
|
|
LOGINFO(1, "Latest anchor point: height " << m_checkpoints.back().height <<
|
|
", id " << m_checkpoints.back().id);
|
|
m_checkpointsNeedValidation = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool SideChain::validate_loaded_checkpoints()
|
|
{
|
|
// IMPORTANT: This function assumes m_sidechainLock is already held by the caller (WriteLock)
|
|
// It's called from update_chain_tip() -> prune_old_blocks() which is inside add_block()'s WriteLock
|
|
|
|
LOGINFO(3, "validate_loaded_checkpoints() called, m_checkpointsNeedValidation=" << (m_checkpointsNeedValidation ? "true" : "false"));
|
|
|
|
if (!m_checkpointsNeedValidation) {
|
|
LOGINFO(3, "Skipping checkpoint validation - not needed");
|
|
return true; // Safe to mine
|
|
}
|
|
|
|
LOGINFO(3, "Starting checkpoint validation...");
|
|
bool all_valid = true;
|
|
uint64_t first_mismatch_height = 0;
|
|
std::vector<Checkpoint> valid_checkpoints;
|
|
size_t original_count = 0;
|
|
bool had_stale = false;
|
|
std::vector<PoolBlock*> blocks_to_verify;
|
|
|
|
{
|
|
ReadLock cpLock(m_checkpointsLock);
|
|
// NOTE: m_sidechainLock already held by caller, don't acquire it again!
|
|
|
|
if (m_checkpoints.empty()) {
|
|
m_checkpointsNeedValidation = false;
|
|
// No checkpoints - need to bootstrap from chain tip
|
|
const PoolBlock* tip = m_chainTip;
|
|
if (tip && !tip->m_verified) {
|
|
tip->m_verified = true;
|
|
LOGINFO(1, "No checkpoints - bootstrapping verification from chain tip height " << tip->m_sidechainHeight);
|
|
}
|
|
return true; // Safe to mine
|
|
}
|
|
|
|
original_count = m_checkpoints.size();
|
|
|
|
|
|
// Check each checkpoint against actual chain
|
|
for (const Checkpoint& cp : m_checkpoints) {
|
|
LOGINFO(3, "Validating checkpoint at height " << cp.height);
|
|
|
|
// First check if checkpoint is reachable (within PPLNS window of current tip)
|
|
const PoolBlock* tip = m_chainTip;
|
|
if (tip && (cp.height + m_chainWindowSize < tip->m_sidechainHeight)) {
|
|
// Checkpoint is too old - outside sync window, discard it
|
|
LOGWARN(0, "Checkpoint at height " << cp.height << " is unreachable (tip=" << tip->m_sidechainHeight << ", window=" << m_chainWindowSize << ") - discarding stale checkpoint");
|
|
had_stale = true;
|
|
continue; // Don't add to valid_checkpoints
|
|
}
|
|
|
|
LOGINFO(3, "Checkpoint at height " << cp.height << " is reachable, looking up block id " << cp.id);
|
|
|
|
// Direct lookup - sidechain lock held above
|
|
PoolBlock* found = nullptr;
|
|
auto it = m_blocksById.find(cp.id);
|
|
if (it != m_blocksById.end()) {
|
|
found = it->second;
|
|
}
|
|
|
|
LOGINFO(3, "find_block returned " << (found ? "valid pointer" : "nullptr"));
|
|
|
|
if (!found) {
|
|
// Block not yet synced but reachable - can't validate yet
|
|
LOGWARN(3, "Checkpoint at height " << cp.height << " not yet synced, deferring validation - mining blocked");
|
|
return false; // NOT safe to mine yet
|
|
}
|
|
|
|
PoolBlock* block = found;
|
|
|
|
if (block->m_sidechainId != cp.id) {
|
|
LOGWARN(0, "CHECKPOINT MISMATCH at height " << cp.height <<
|
|
": cached=" << cp.id <<
|
|
", actual=" << block->m_sidechainId);
|
|
all_valid = false;
|
|
if (first_mismatch_height == 0) {
|
|
first_mismatch_height = cp.height;
|
|
}
|
|
} else {
|
|
LOGINFO(3, "Checkpoint validated: height " << cp.height << ", id " << cp.id);
|
|
block->m_verified = true; // Trust the checkpoint as verification anchor
|
|
blocks_to_verify.push_back(block); // Queue for verify_loop
|
|
valid_checkpoints.push_back(cp); // Keep valid checkpoint
|
|
}
|
|
}
|
|
} // Locks released here
|
|
|
|
bool need_bootstrap = false;
|
|
uint64_t bootstrap_height = 0;
|
|
|
|
// If we had stale checkpoints, update and save
|
|
if (had_stale) {
|
|
WriteLock wlock(m_checkpointsLock);
|
|
m_checkpoints = std::move(valid_checkpoints);
|
|
LOGINFO(1, "Cleaned up stale checkpoints: " << original_count << " -> " << m_checkpoints.size());
|
|
|
|
// If ALL checkpoints were stale, bootstrap from chain tip
|
|
if (m_checkpoints.empty()) {
|
|
const PoolBlock* tip = m_chainTip;
|
|
if (tip) {
|
|
tip->m_verified = true;
|
|
LOGINFO(1, "All checkpoints stale - bootstrapping verification from chain tip height " << tip->m_sidechainHeight);
|
|
need_bootstrap = true;
|
|
bootstrap_height = tip->m_sidechainHeight;
|
|
}
|
|
}
|
|
} // WriteLock released here
|
|
|
|
// Create new checkpoint outside the lock
|
|
if (need_bootstrap) {
|
|
update_checkpoints(bootstrap_height);
|
|
}
|
|
|
|
m_checkpointsNeedValidation = false;
|
|
|
|
if (all_valid) {
|
|
LOGINFO(1, "All " << (had_stale ? m_checkpoints.size() : original_count) << " cached checkpoints validated successfully");
|
|
|
|
// NOTE: m_sidechainLock already held by caller (WriteLock from add_block)
|
|
// No need to acquire it again - verify_loop and block scan can proceed
|
|
|
|
// Propagate verification from anchor points
|
|
for (PoolBlock* block : blocks_to_verify) {
|
|
LOGINFO(3, "Running verify_loop from anchor height " << block->m_sidechainHeight);
|
|
verify_loop(block);
|
|
}
|
|
|
|
// Scan all blocks and verify any with verified parents
|
|
LOGINFO(3, "Scanning for unverified blocks with verified parents...");
|
|
bool made_progress = true;
|
|
int pass = 0;
|
|
while (made_progress && pass < 100) {
|
|
made_progress = false;
|
|
++pass;
|
|
|
|
for (auto& pair : m_blocksById) {
|
|
PoolBlock* b = pair.second;
|
|
if (!b->m_verified && b->m_sidechainHeight > 0) {
|
|
auto parent_it = m_blocksById.find(b->m_parent);
|
|
if (parent_it != m_blocksById.end() && parent_it->second->m_verified) {
|
|
b->m_verified = true;
|
|
made_progress = true;
|
|
}
|
|
}
|
|
}
|
|
if (made_progress) {
|
|
LOGINFO(3, "Verification pass " << pass << " - verified more blocks");
|
|
}
|
|
}
|
|
|
|
if (had_stale) {
|
|
save_checkpoints(); // Persist the cleaned list
|
|
}
|
|
return true; // Safe to mine
|
|
}
|
|
|
|
LOGERR(0, "Cached checkpoints are INVALID - chain has diverged since last run");
|
|
LOGERR(0, "First mismatch at height " << first_mismatch_height);
|
|
|
|
{
|
|
WriteLock wlock(m_checkpointsLock);
|
|
m_checkpoints.clear();
|
|
}
|
|
|
|
LOGINFO(0, "Cleared stale checkpoints - will rebuild from current chain");
|
|
save_checkpoints(); // Persist the cleared state
|
|
|
|
// Rebuild checkpoints from current chain
|
|
const PoolBlock* tip = m_chainTip;
|
|
if (tip) {
|
|
update_checkpoints(tip->m_sidechainHeight);
|
|
}
|
|
return true; // Checkpoints cleared and rebuilt, safe to mine
|
|
}
|
|
|
|
} // namespace p2pool
|