From 7ba8d8b8c9facb7f1ca673fb80e14d5d78eb7cff Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Sat, 20 Jun 2026 20:56:47 -0700 Subject: [PATCH] Fix sync-freeze: backpressure, prune protection, eviction direction, bridge-repair + PoS header guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync-freeze patch (original): - Backpressure ceiling HEADER_FRONT_MAX_AHEAD=8000 - PruneHeaders protects live sync window (nProtectFloor) - Hard-cap eviction from highest-height first - Bridge-repair getheaders from connected tip via PathReachesChain Additional fix: - Skip PoW check on PoS headers (nonce=0) in AddHeaderNode Block 1026 is PoS but within the 0-9000 PoW range — old code rejected valid PoS headers and severed the chain at height 1025 Verified: from-zero no-snapshot sync reached block 17k+ past the old 15k freeze zone. 132/132 unit tests pass. --- src/syncmanager.cpp | 113 ++++++++++++++++++++++++++++++++++++++------ src/syncmanager.h | 1 + 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/src/syncmanager.cpp b/src/syncmanager.cpp index f06837b..d64411f 100644 --- a/src/syncmanager.cpp +++ b/src/syncmanager.cpp @@ -31,6 +31,11 @@ static const size_t HEADER_REDUNDANT_PEER_THRESHOLD = 4; static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 60 * 1000000; static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 5 * 1000000; static const int64_t HEADER_SYNC_TTL_MICROS = 15 * 60 * 1000000; +// Backpressure ceiling: how far (in blocks) the header front is allowed to +// run ahead of the connected chain tip before we stop fetching MORE headers +// and let the block planner catch up. Comfortly below MAX_HEADER_SYNC_CACHE +// so the cache never overflows in normal from-zero sync. +static const int HEADER_FRONT_MAX_AHEAD = 8000; std::map mapHeaders; uint256 hashBestHeader = 0; @@ -124,12 +129,23 @@ void CSyncManager::PruneHeaders() { const int64_t nNow = GetTime() * 1000000; + // Never evict headers in the live sync window. Fix 2's backpressure caps the + // header front at nBestHeight + HEADER_FRONT_MAX_AHEAD, so protecting that + // whole span means the entire in-flight header chain is safe: no mid-chain + // hole can form between the connected tip and the front during normal sync. + // Evicting any of these would sever GetDownloadPath() from the connected + // chain and freeze sync. (The hard cap below is the memory safety valve; + // Fix 3's bridge-repair is the backstop for restart/reorg edge cases.) + const int nProtectFloor = nBestHeight + HEADER_FRONT_MAX_AHEAD; + + // TTL pass: evict aged headers, but only ABOVE the protected floor. if (mapHeaders.size() > MAX_HEADER_SYNC_CACHE / 2) { unsigned int nEvicted = 0; for (std::map::iterator it = mapHeaders.begin(); it != mapHeaders.end(); ) { - if (nNow - it->second.nInsertTime >= HEADER_SYNC_TTL_MICROS) + if (it->second.nHeight > nProtectFloor && + nNow - it->second.nInsertTime >= HEADER_SYNC_TTL_MICROS) { it = mapHeaders.erase(it); ++nEvicted; @@ -139,25 +155,37 @@ void CSyncManager::PruneHeaders() } if (nEvicted > 0) { - printf("IBD-DIAG: TTL-evicted %u stale sync headers, %u remain\n", - nEvicted, (unsigned int)mapHeaders.size()); + printf("IBD-DIAG: TTL-evicted %u stale sync headers above floor %d, %u remain\n", + nEvicted, nProtectFloor, (unsigned int)mapHeaders.size()); RecomputeBestHeader(); } } + // Hard cap (last-resort safety valve): evict the HIGHEST-height headers + // first — the ones furthest ahead of the tip — never the bridge zone. + // Lowering the sync target temporarily is fine; we re-extend it once blocks + // catch up. Losing a bridge header is not fine: it stalls forever. if (mapHeaders.size() > MAX_HEADER_SYNC_CACHE) { - printf("IBD-DIAG: sync header cache exceeded %u entries, evicting oldest\n", MAX_HEADER_SYNC_CACHE); - while (mapHeaders.size() > MAX_HEADER_SYNC_CACHE * 3 / 4) - { - std::map::iterator oldest = mapHeaders.begin(); - for (std::map::iterator it = mapHeaders.begin(); it != mapHeaders.end(); ++it) - { - if (it->second.nInsertTime < oldest->second.nInsertTime) - oldest = it; - } - mapHeaders.erase(oldest); - } + printf("IBD-DIAG: sync header cache exceeded %u entries, evicting from the front (floor=%d)\n", + MAX_HEADER_SYNC_CACHE, nProtectFloor); + + std::vector::iterator> vEvictable; + for (std::map::iterator it = mapHeaders.begin(); it != mapHeaders.end(); ++it) + if (it->second.nHeight > nProtectFloor) + vEvictable.push_back(it); + + std::sort(vEvictable.begin(), vEvictable.end(), + [](const std::map::iterator& a, + const std::map::iterator& b) { + return a->second.nHeight > b->second.nHeight; + }); + + const size_t nTarget = (size_t)MAX_HEADER_SYNC_CACHE * 3 / 4; + size_t i = 0; + while (mapHeaders.size() > nTarget && i < vEvictable.size()) + mapHeaders.erase(vEvictable[i++]); + RecomputeBestHeader(); } } @@ -191,7 +219,11 @@ bool CSyncManager::AddHeaderNode(const CBlock& header, const uint256& hashHeader } const int nHeight = nPrevHeight + 1; - if (nHeight <= CUTOFF_POW_BLOCK && !CheckProofOfWork(hashHeader, header.nBits)) + // Only check PoW on actual PoW headers (nNonce != 0). + // Triangles is a hybrid PoW/PoS coin — PoS blocks (nonce=0) can appear + // even within the 0-9000 PoW range. Checking PoW on a PoS header + // rejects valid blocks and severs the header chain during IBD. + if (nHeight <= CUTOFF_POW_BLOCK && header.nNonce != 0 && !CheckProofOfWork(hashHeader, header.nBits)) { printf("IBD-DIAG: header PoW FAILED at height %d hash=%s nBits=%08x prevHash=%s\n", nHeight, hashHeader.ToString().substr(0,20).c_str(), header.nBits, @@ -235,6 +267,20 @@ std::vector CSyncManager::GetDownloadPath(uint256 hashTip) const return vPath; } +// A download path is "anchored" when its lowest header's parent is a block we +// already have in the active chain (mapBlockIndex). If it isn't, a bridging +// header was lost and requesting these blocks would only create orphans — +// Tick() rebuilds the bridge with a getheaders anchored at pindexBest. +bool CSyncManager::PathReachesChain(const std::vector& vPath) const +{ + if (vPath.empty()) + return true; // nothing queued == nothing to bridge + std::map::const_iterator mi = mapHeaders.find(vPath.front()); + if (mi == mapHeaders.end()) + return false; + return mapBlockIndex.count(mi->second.header.hashPrevBlock) != 0; +} + unsigned int CSyncManager::CountInFlight() const { const int64_t nNow = GetTime() * 1000000; @@ -291,6 +337,13 @@ void CSyncManager::ContinueHeaders(CNode* pfrom, const uint256& hashTip) if (!pfrom || hashTip == 0) return; + // Backpressure: don't extend the header front when it is already far ahead + // of the connected block tip. Otherwise headers race past the block planner + // and the bridge headers age out / get evicted before their blocks arrive. + if (hashBestHeader != 0 && + GetPlannerHeight() - nBestHeight > HEADER_FRONT_MAX_AHEAD) + return; + std::vector vHave; uint256 hashWalk = hashTip; int nStep = 1; @@ -326,6 +379,12 @@ bool CSyncManager::RequestRefill(CNode* pfrom, uint256 hashTip, int64_t nMinInte nNowSec - pfrom->nLastIbdHeaderRequest < nMinIntervalSeconds) return false; + // Backpressure: stop pulling new headers once the front is far enough ahead + // of the connected tip; let block download drain first (see ContinueHeaders). + if (hashBestHeader != 0 && + GetPlannerHeight() - nBestHeight > HEADER_FRONT_MAX_AHEAD) + return false; + uint256 hashLocatorTip = hashTip; if (hashLocatorTip == 0 || (!mapBlockIndex.count(hashLocatorTip) && !mapHeaders.count(hashLocatorTip))) @@ -386,6 +445,15 @@ unsigned int CSyncManager::QueueBlocksParallel(unsigned int nWindow) if (vPath.empty()) return 0; + // If the path doesn't connect back to the active chain, requesting these + // blocks just fills the orphan pool. Bail and let Tick() repair the bridge. + if (!PathReachesChain(vPath)) + { + printf("IBD-DIAG: download path not anchored to chain (front=%s) — deferring to bridge repair\n", + vPath.front().ToString().substr(0,20).c_str()); + return 0; + } + std::vector vEligiblePeers; { LOCK(cs_vNodes); @@ -645,6 +713,21 @@ void CSyncManager::Tick(CNode* pto, int nHighestInvWalk, const uint256& hashHigh if (nNowSec - pto->nLastIbdHeaderRequest >= nMinInterval) RequestRefill(pto, hashBestHeader, nMinInterval, "heartbeat"); + // Bridge repair: we have a best header, but the download path can't reach + // the connected chain — a linking header was lost (TTL/eviction/hole). Ask + // this peer for headers with a locator anchored at the REAL chain tip so it + // resends the headers directly above pindexBest and re-links the path. + if (hashBestHeader != 0 && pindexBest && + !PathReachesChain(GetDownloadPath(hashBestHeader)) && + nNowSec - pto->nLastIbdHeaderRequest >= HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS) + { + pto->pindexLastGetHeadersBegin = NULL; + pto->PushGetHeaders(pindexBest, uint256(0)); + pto->nLastIbdHeaderRequest = nNowSec; + printf("IBD-DIAG: bridge-repair getheaders from connected tip height=%d peer=%s\n", + pindexBest->nHeight, pto->addr.ToString().c_str()); + } + if (nNowSec - nLastBlockPlannerControl >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS && hashBestHeader != 0 && nPlannerDepth > 0) diff --git a/src/syncmanager.h b/src/syncmanager.h index 03cb837..a012526 100644 --- a/src/syncmanager.h +++ b/src/syncmanager.h @@ -50,6 +50,7 @@ private: void PruneHeaders(); bool AddHeaderNode(const CBlock& header, const uint256& hashHeader); std::vector GetDownloadPath(uint256 hashTip) const; + bool PathReachesChain(const std::vector& vPath) const; void ContinueHeaders(CNode* pfrom, const uint256& hashTip); };