diff --git a/src/common.h b/src/common.h index 14f8a57..e795fdd 100644 --- a/src/common.h +++ b/src/common.h @@ -559,13 +559,14 @@ struct MinerData struct ChainMain { - FORCEINLINE ChainMain() : difficulty(), height(0), timestamp(0), reward(0), id() {} + FORCEINLINE ChainMain() : difficulty(), height(0), timestamp(0), reward(0), id(), prev_id() {} difficulty_type difficulty; uint64_t height; uint64_t timestamp; uint64_t reward; hash id; + hash prev_id; // Parent block hash - used for reorg detection }; enum class NetworkType { diff --git a/src/p2p_server.cpp b/src/p2p_server.cpp index 5b8ba3d..79b496e 100644 --- a/src/p2p_server.cpp +++ b/src/p2p_server.cpp @@ -3426,16 +3426,26 @@ bool P2PServer::P2PClient::on_checkpoint_response(const uint8_t* buf, uint32_t c } if (mismatch_count > 0) { - LOGWARN(2, "Checkpoint validation: " << mismatch_count << "/" << count - << " mismatches with peer " << static_cast(m_addrString)); + // Check if we're in a reorg grace period - if so, don't take drastic action + const bool in_grace_period = side_chain.is_in_reorg_grace_period(); - // If more than half the checkpoints mismatch, we may be on wrong chain - if (mismatch_count > count / 2) { - LOGERR(1, "DIVERGENCE DETECTED: More than 50% checkpoint mismatch with peer " - << static_cast(m_addrString) << " (" << mismatch_count << "/" << count << ")"); - // Trigger recovery if we have significantly different checkpoints - if (!peer_checkpoints.empty()) { - side_chain.trigger_recovery(peer_checkpoints[0].height); + if (in_grace_period) { + LOGINFO(2, "Checkpoint mismatch (" << mismatch_count << "/" << count + << ") with peer " << static_cast(m_addrString) + << " - IGNORED (mainchain reorg grace period active)"); + } + else { + LOGWARN(2, "Checkpoint validation: " << mismatch_count << "/" << count + << " mismatches with peer " << static_cast(m_addrString)); + + // If more than half the checkpoints mismatch, we may be on wrong chain + if (mismatch_count > count / 2) { + LOGERR(1, "DIVERGENCE DETECTED: More than 50% checkpoint mismatch with peer " + << static_cast(m_addrString) << " (" << mismatch_count << "/" << count << ")"); + // Trigger recovery if we have significantly different checkpoints + if (!peer_checkpoints.empty()) { + side_chain.trigger_recovery(peer_checkpoints[0].height); + } } } } else if (count > 0) { diff --git a/src/p2pool.cpp b/src/p2pool.cpp index 777b5ce..fdf33d1 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -627,9 +627,35 @@ void p2pool::handle_chain_main(ChainMain& data, const char* extra, const std::ve // These transactions were already mined, so remove them from mempool if any of them slipped through m_mempool->remove(tx_hashes_in_block); + // Check for mainchain reorg by comparing prev_id with stored block at height-1 + bool reorg_detected = false; + uint64_t reorg_height = 0; + hash old_block_hash; + { WriteLock lock(m_mainchainLock); + if (!data.prev_id.empty() && data.height > 0) { + auto prev_it = m_mainchainByHeight.find(data.height - 1); + if (prev_it != m_mainchainByHeight.end() && !prev_it->second.id.empty()) { + if (prev_it->second.id != data.prev_id) { + // REORG DETECTED - the parent block changed! + reorg_detected = true; + reorg_height = data.height - 1; + old_block_hash = prev_it->second.id; + + LOGWARN(0, log::LightYellow() << "MAINCHAIN REORG DETECTED at height " << data.height + << ": expected prev_id " << prev_it->second.id + << " but got " << data.prev_id); + + // Remove old hash from byHash map and update to new hash + m_mainchainByHash.erase(prev_it->second.id); + prev_it->second.id = data.prev_id; + m_mainchainByHash[data.prev_id] = prev_it->second; + } + } + } + ChainMain& c = m_mainchainByHeight[data.height]; c.height = data.height; c.timestamp = data.timestamp; @@ -640,6 +666,12 @@ void p2pool::handle_chain_main(ChainMain& data, const char* extra, const std::ve m_mainchainByHash[c.id] = c; } + + // Notify sidechain of reorg outside the lock to avoid deadlock + if (reorg_detected) { + side_chain().on_mainchain_reorg(reorg_height, old_block_hash); + } + update_median_timestamp(); root_hash merkle_root; diff --git a/src/side_chain.cpp b/src/side_chain.cpp index 4d0c1a9..4a32215 100644 --- a/src/side_chain.cpp +++ b/src/side_chain.cpp @@ -3843,4 +3843,160 @@ bool SideChain::validate_loaded_checkpoints() return true; // Checkpoints cleared and rebuilt, safe to mine } +void SideChain::on_mainchain_reorg(uint64_t split_height, const hash& old_block_hash) +{ + // Level 1: Important event - mainchain reorg detected + LOGINFO(1, log::LightYellow() << "MAINCHAIN REORG at height " << split_height + << " (old block " << old_block_hash << "), handling sidechain..."); + + // PAUSE MINING IMMEDIATELY - no point mining on potentially invalid state + // This prevents wasted work and ensures reorg handling takes priority + const bool was_mining = m_readyToMine.exchange(false); + if (was_mining) { + LOGINFO(2, "Mining paused for reorg handling"); + } + + // Set grace period - don't disconnect peers for checkpoint mismatches during this time + const uint64_t now = seconds_since_epoch(); + m_reorgGracePeriodEnd.store(now + REORG_GRACE_PERIOD_SECONDS); + m_lastReorgHeight.store(split_height); + + LOGINFO(3, "Grace period active for " << REORG_GRACE_PERIOD_SECONDS << " seconds"); + + uint32_t affected_count = 0; + bool tip_affected = false; + + // Wrap all reorg handling in try-catch to ensure mining resumes even on error + try { + // Phase 1: Find and mark ALL sidechain blocks that reference orphaned mainchain blocks + // Use chainmain_get_by_hash() to check if each block's mainchain reference is still valid + unordered_set invalid_block_ids; + + { + ReadLock lock(m_sidechainLock); + const PoolBlock* current_tip = m_chainTip.load(); + + for (auto& pair : m_blocksById) { + PoolBlock* block = pair.second; + + // Skip blocks already marked invalid + if (block->m_invalid) { + continue; + } + + // Check if this block's mainchain reference is still valid + ChainMain data; + if (!m_pool->chainmain_get_by_hash(block->m_prevId, data)) { + // This sidechain block references an orphaned mainchain block + LOGINFO(4, "Sidechain block " << block->m_sidechainId + << " at height " << block->m_sidechainHeight + << " references orphaned mainchain - marking invalid"); + + block->m_verified = false; + block->m_invalid = true; + invalid_block_ids.insert(block->m_sidechainId); + ++affected_count; + + // Check if the chain tip is affected + if (current_tip && block->m_sidechainId == current_tip->m_sidechainId) { + tip_affected = true; + } + } + } + } + + LOGINFO(3, "Phase 1: " << affected_count << " sidechain blocks invalidated"); + + // Phase 2: Clean checkpoints that reference invalid blocks + // Important: Use separate lock scopes to avoid deadlock + if (!invalid_block_ids.empty()) { + WriteLock cpLock(m_checkpointsLock); + size_t old_count = m_checkpoints.size(); + + m_checkpoints.erase( + std::remove_if(m_checkpoints.begin(), m_checkpoints.end(), + [&invalid_block_ids](const Checkpoint& cp) { + return invalid_block_ids.count(cp.id) > 0; + }), + m_checkpoints.end() + ); + + if (m_checkpoints.size() < old_count) { + LOGINFO(3, "Phase 2: Removed " << (old_count - m_checkpoints.size()) << " checkpoints"); + } + } + + // Phase 3: Reset chain tip if current tip was invalidated + if (tip_affected) { + LOGINFO(2, "Chain tip invalidated, finding new valid tip..."); + + ReadLock lock(m_sidechainLock); + PoolBlock* best = nullptr; + + for (auto& pair : m_blocksById) { + PoolBlock* block = pair.second; + if (block->m_verified && !block->m_invalid) { + if (!best || block->m_cumulativeDifficulty > best->m_cumulativeDifficulty) { + best = block; + } + } + } + + m_chainTip.store(best); + if (best) { + LOGINFO(2, "Chain tip reset to height " << best->m_sidechainHeight); + } else { + LOGWARN(1, "No valid chain tip after reorg - will sync from peers or create genesis"); + } + } + + // Phase 4: Rebuild checkpoints from valid chain + const PoolBlock* tip = m_chainTip.load(); + if (tip && affected_count > 0) { + LOGINFO(3, "Phase 4: Rebuilding checkpoints..."); + update_checkpoints(tip->m_sidechainHeight); + save_checkpoints(); + } + + // Phase 5: Trigger re-verification of any pending blocks + if (affected_count > 0) { + LOGINFO(3, "Phase 5: Re-verifying pending blocks..."); + retry_unverified_blocks(); + } + } + catch (const std::exception& e) { + LOGERR(0, "Exception during reorg handling: " << e.what()); + } + catch (...) { + LOGERR(0, "Unknown exception during reorg handling"); + } + + // ALWAYS RESUME MINING if it was active before + // Even without a valid tip, we need mining enabled to: + // 1. Create a new genesis block if all blocks were invalidated + // 2. Process new blocks from peers that might give us a valid tip + // Without this, a deep reorg could deadlock the entire network + if (was_mining) { + m_readyToMine.store(true); + LOGINFO(2, "Mining resumed"); + } + + // Level 1 summary: only if blocks were actually affected + if (affected_count > 0) { + LOGINFO(1, log::LightYellow() << "Reorg handled: " << affected_count << " blocks invalidated" + << (tip_affected ? ", tip reset" : "")); + } else { + LOGINFO(2, "Reorg handled: no sidechain blocks affected"); + } +} + +bool SideChain::is_in_reorg_grace_period() const +{ + const uint64_t grace_end = m_reorgGracePeriodEnd.load(); + if (grace_end == 0) { + return false; + } + return seconds_since_epoch() < grace_end; +} + } // namespace p2pool diff --git a/src/side_chain.h b/src/side_chain.h index e5bb82b..6057559 100644 --- a/src/side_chain.h +++ b/src/side_chain.h @@ -114,6 +114,11 @@ public: void check_and_run_deferred_recovery(); bool is_in_recovery() const { return m_recoveryMode.load(); } + // Mainchain reorg handling + void on_mainchain_reorg(uint64_t split_height, const hash& old_block_hash); + bool is_in_reorg_grace_period() const; + static constexpr uint64_t REORG_GRACE_PERIOD_SECONDS = 60; + [[nodiscard]] FORCEINLINE difficulty_type difficulty() const { ReadLock lock(m_curDifficultyLock); return m_curDifficulty; } [[nodiscard]] difficulty_type total_hashes() const; [[nodiscard]] uint64_t block_time() const { return m_targetBlockTime; } @@ -245,6 +250,10 @@ private: std::atomic m_pendingRecoveryHeight{0}; bool m_checkpointsNeedValidation{false}; + // Mainchain reorg tracking + std::atomic m_reorgGracePeriodEnd{0}; + std::atomic m_lastReorgHeight{0}; + hash m_consensusHash; void launch_precalc(const PoolBlock* block); diff --git a/src/zmq_reader.cpp b/src/zmq_reader.cpp index 63402c8..c71d2a1 100644 --- a/src/zmq_reader.cpp +++ b/src/zmq_reader.cpp @@ -573,6 +573,18 @@ void ZMQReader::parse(char* data, size_t size) continue; } + // Parse prev_id for reorg detection + std::string prev_id_str; + if (parseValue(*i, "prev_id", prev_id_str)) { + if (!from_hex(prev_id_str.c_str(), prev_id_str.length(), m_chainmainData.prev_id)) { + LOGWARN(1, "json-full-chain_main invalid prev_id, skipping it"); + continue; + } + } + else { + m_chainmainData.prev_id = hash(); + } + auto it = i->FindMember("miner_tx"); if ((it == i->MemberEnd()) || !it->value.IsObject()) { LOGWARN(1, "json-full-chain_main miner_tx not found, skipping it");