Add CAP exchange protocol, Deadlock fix, Sync stuck fix with retry mechanism
This commit is contained in:
+803
-59
@@ -34,6 +34,7 @@
|
||||
#include "json_parsers.h"
|
||||
#include "crypto.h"
|
||||
#include "hardforks/hardforks.h"
|
||||
#include <fstream>
|
||||
|
||||
#if !defined(_MSC_VER) || !defined(__cppcheck__)
|
||||
#include <rapidjson/document.h>
|
||||
@@ -107,6 +108,8 @@ SideChain::SideChain(p2pool* pool, NetworkType type, const char* pool_name, cons
|
||||
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");
|
||||
@@ -220,10 +223,13 @@ SideChain::SideChain(p2pool* pool, NetworkType type, const char* pool_name, cons
|
||||
|
||||
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);
|
||||
@@ -231,6 +237,8 @@ SideChain::~SideChain()
|
||||
uv_rwlock_destroy(&m_curDifficultyLock);
|
||||
uv_rwlock_destroy(&m_watchBlockLock);
|
||||
|
||||
uv_rwlock_destroy(&m_checkpointsLock);
|
||||
|
||||
for (const auto& it : m_blocksById) {
|
||||
delete it.second;
|
||||
}
|
||||
@@ -727,24 +735,9 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector<hash>& missing_
|
||||
const bool added = add_block(block);
|
||||
if (added && block.m_verified) {
|
||||
if (block.m_invalid) {
|
||||
++m_externalBlockFailures;
|
||||
LOGWARN(3, "external block validation failed, failure count: " << m_externalBlockFailures << "/" << DIVERGENCE_THRESHOLD);
|
||||
if (m_externalBlockFailures >= DIVERGENCE_THRESHOLD) {
|
||||
LOGERR(0, log::LightRed() << "DIVERGENCE DETECTED: " << m_externalBlockFailures << " consecutive external block failures. Purging cache and restarting...");
|
||||
// Delete cache file
|
||||
const std::string cache_path = "p2pool.cache";
|
||||
remove(cache_path.c_str());
|
||||
// Signal shutdown - systemd will restart
|
||||
if (m_pool) {
|
||||
m_pool->stop();
|
||||
}
|
||||
}
|
||||
on_block_rejected(&block, "external block validation failed");
|
||||
} else {
|
||||
// Valid external block - reset counter
|
||||
if (m_externalBlockFailures > 0) {
|
||||
LOGINFO(3, "external block validated successfully, resetting failure counter from " << m_externalBlockFailures);
|
||||
m_externalBlockFailures = 0;
|
||||
}
|
||||
on_block_accepted();
|
||||
}
|
||||
}
|
||||
return added;
|
||||
@@ -1229,7 +1222,9 @@ void SideChain::print_status(bool obtain_sidechain_lock) const
|
||||
"\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::XMRAmount(your_reward) << ')'
|
||||
"\nBlock reward share = " << block_share << "% (" << log::XMRAmount(your_reward) << ')' <<
|
||||
"\nAnchor point = " << get_latest_checkpoint_height() <<
|
||||
"\nChain health = " << m_externalBlockFailures << " consecutive failures" << (m_recoveryMode.load() ? " [RECOVERY MODE]" : "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1548,9 +1543,44 @@ void SideChain::add_onion_pubkeys(const std::vector<hash>& pubkeys)
|
||||
|
||||
void SideChain::verify_loop(PoolBlock* block)
|
||||
{
|
||||
// PoW is already checked at this point
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PoolBlock*> blocks_to_verify(1, block);
|
||||
PoolBlock* highest_block = nullptr;
|
||||
|
||||
while (!blocks_to_verify.empty()) {
|
||||
@@ -1570,20 +1600,19 @@ void SideChain::verify_loop(PoolBlock* block)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block->m_invalid) {
|
||||
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
|
||||
", id = " << block->m_sidechainId <<
|
||||
", mainchain height = " << block->m_txinGenHeight << ", mined by " << block->m_minerWallet << " is invalid");
|
||||
}
|
||||
else {
|
||||
LOGINFO(3, "verified block at height = " << block->m_sidechainHeight <<
|
||||
", depth = " << block->m_depth <<
|
||||
", id = " << block->m_sidechainId <<
|
||||
", mainchain height = " << block->m_txinGenHeight);
|
||||
if (block->m_invalid) {
|
||||
on_block_rejected(block, "verification failed");
|
||||
}
|
||||
else {
|
||||
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
|
||||
// This block is now verified
|
||||
|
||||
bool is_alternative;
|
||||
|
||||
bool is_alternative;
|
||||
if (is_longer_chain(highest_block, block, is_alternative)) {
|
||||
highest_block = block;
|
||||
}
|
||||
@@ -1695,20 +1724,29 @@ void SideChain::verify(PoolBlock* block)
|
||||
}
|
||||
|
||||
// Check parent
|
||||
auto it = m_blocksById.find(block->m_parent);
|
||||
if ((it == m_blocksById.end()) || !it->second->m_verified) {
|
||||
block->m_verified = false;
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
LOGWARN(3, "block at height = " << block->m_sidechainHeight << " parent " << block->m_parent << " found but NOT VERIFIED (parent height = " << it->second->m_sidechainHeight << ")");
|
||||
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(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << ": get_shares failed");
|
||||
block->m_verified = true;
|
||||
block->m_invalid = true;
|
||||
return;
|
||||
}
|
||||
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();
|
||||
@@ -1879,8 +1917,12 @@ void SideChain::verify(PoolBlock* block)
|
||||
diff = difficulty();
|
||||
}
|
||||
else if (!get_difficulty(parent, m_difficultyData, diff)) {
|
||||
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);
|
||||
block->m_invalid = true;
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1898,10 +1940,14 @@ void SideChain::verify(PoolBlock* block)
|
||||
}
|
||||
|
||||
if (shares.empty() && !get_shares(block, shares)) {
|
||||
LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << ": get_shares failed");
|
||||
block->m_invalid = true;
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
@@ -2037,7 +2083,11 @@ void SideChain::update_chain_tip(PoolBlock* block)
|
||||
", next difficulty = " << log::Gray() << diff << log::NoColor() <<
|
||||
", main chain height = " << log::Gray() << block->m_txinGenHeight);
|
||||
|
||||
block->m_wantBroadcast = true;
|
||||
// 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);
|
||||
|
||||
@@ -2054,6 +2104,99 @@ void SideChain::update_chain_tip(PoolBlock* block)
|
||||
}
|
||||
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) {
|
||||
@@ -2072,6 +2215,9 @@ void SideChain::update_chain_tip(PoolBlock* block)
|
||||
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
|
||||
@@ -2351,21 +2497,29 @@ void SideChain::prune_old_blocks()
|
||||
const PoolBlock* tip = m_chainTip;
|
||||
|
||||
if (tip->m_sidechainHeight < prune_distance) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t h = tip->m_sidechainHeight - prune_distance;
|
||||
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 <= h);) {
|
||||
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)
|
||||
{
|
||||
if ((block->m_depth >= prune_distance) || (cur_time >= block->m_localTimestamp + prune_delay)) {
|
||||
[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);
|
||||
@@ -2397,7 +2551,7 @@ void SideChain::prune_old_blocks()
|
||||
}
|
||||
|
||||
if (!blocks_to_prune.empty()) {
|
||||
LOGINFO(4, "pruned " << blocks_to_prune.size() << " old blocks at heights <= " << h);
|
||||
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
|
||||
@@ -2451,10 +2605,28 @@ void SideChain::prune_old_blocks()
|
||||
// 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()) {
|
||||
m_readyToMine.store(true);
|
||||
LOGINFO(0, log::LightGreen() << "########################################");
|
||||
LOGINFO(0, log::LightGreen() << "SIDECHAIN LOADED - MINING IS NOW ENABLED");
|
||||
LOGINFO(0, log::LightGreen() << "########################################");
|
||||
// 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() << "########################################");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2490,6 +2662,50 @@ void SideChain::get_missing_blocks(unordered_set<hash>& missing_blocks) const
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -2821,4 +3037,532 @@ void SideChain::finish_precalc()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
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, "\n========================================");
|
||||
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, "========================================\n");
|
||||
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, "\n========================================");
|
||||
LOGINFO(0, "RESETTING TO CHECKPOINT " << checkpoint_height);
|
||||
LOGINFO(0, "========================================\n");
|
||||
|
||||
WriteLock lock(m_sidechainLock);
|
||||
|
||||
// SAFER APPROACH: Don't delete blocks - just mark them invalid and reset state
|
||||
// Normal pruning will clean them up later. This avoids complex threading issues.
|
||||
|
||||
size_t invalidated_count = 0;
|
||||
for (auto& kv : m_blocksById) {
|
||||
if (kv.second->m_sidechainHeight > checkpoint_height) {
|
||||
kv.second->m_invalid = true;
|
||||
kv.second->m_verified = true;
|
||||
++invalidated_count;
|
||||
}
|
||||
}
|
||||
|
||||
LOGINFO(0, "Marked " << invalidated_count << " blocks as invalid after checkpoint " << checkpoint_height);
|
||||
|
||||
// Reset chain tip to checkpoint block - find by checkpoint ID
|
||||
PoolBlock* checkpoint_block = nullptr;
|
||||
{
|
||||
ReadLock cpLock(m_checkpointsLock);
|
||||
for (const Checkpoint& cp : m_checkpoints) {
|
||||
if (cp.height == checkpoint_height) {
|
||||
auto it = m_blocksById.find(cp.id);
|
||||
if (it != m_blocksById.end()) {
|
||||
checkpoint_block = it->second;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checkpoint_block) {
|
||||
checkpoint_block->m_verified = true; // Mark verified for propagation
|
||||
m_chainTip = checkpoint_block;
|
||||
LOGINFO(0, "Chain tip reset to height " << checkpoint_height
|
||||
<< ", id " << checkpoint_block->m_sidechainId);
|
||||
} else {
|
||||
m_chainTip = nullptr;
|
||||
LOGWARN(0, "No block found at checkpoint height - chain tip set to null");
|
||||
}
|
||||
|
||||
// Clear checkpoints after this point
|
||||
{
|
||||
WriteLock cpLock(m_checkpointsLock);
|
||||
while (!m_checkpoints.empty() && m_checkpoints.back().height > checkpoint_height) {
|
||||
m_checkpoints.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset counters
|
||||
m_externalBlockFailures = 0;
|
||||
m_pendingRecoveryHeight = 0;
|
||||
m_recoveryMode = false;
|
||||
|
||||
LOGINFO(0, "\n========================================");
|
||||
LOGINFO(0, "RESET COMPLETE - RESYNCING FROM " << checkpoint_height);
|
||||
LOGINFO(0, "========================================\n");
|
||||
|
||||
// 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
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
void SideChain::load_checkpoints()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
PoolBlock* mutable_tip = const_cast<PoolBlock*>(tip);
|
||||
mutable_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
|
||||
const 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 = const_cast<PoolBlock*>(found);
|
||||
|
||||
if (!block) {
|
||||
// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
PoolBlock* mutable_tip = const_cast<PoolBlock*>(tip);
|
||||
mutable_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
|
||||
} else {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user