Merge sync-freeze-fix: resolves IBD freeze at 15k + PoS header rejection at 1026

From-zero sync test confirmed: chain advances past 15k freeze zone
to 17k+ with no stall. Build clean (149/149 Ninja targets).
132/132 unit tests pass.
This commit is contained in:
Sami Ahmed
2026-06-20 20:56:57 -07:00
2 changed files with 99 additions and 15 deletions
+98 -15
View File
@@ -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<uint256, CSyncManager::HeaderNode> 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<uint256, HeaderNode>::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<uint256, HeaderNode>::iterator oldest = mapHeaders.begin();
for (std::map<uint256, HeaderNode>::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<std::map<uint256, HeaderNode>::iterator> vEvictable;
for (std::map<uint256, HeaderNode>::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<uint256, HeaderNode>::iterator& a,
const std::map<uint256, HeaderNode>::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<uint256> 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<uint256>& vPath) const
{
if (vPath.empty())
return true; // nothing queued == nothing to bridge
std::map<uint256, HeaderNode>::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<uint256> 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<CNode*> 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)
+1
View File
@@ -50,6 +50,7 @@ private:
void PruneHeaders();
bool AddHeaderNode(const CBlock& header, const uint256& hashHeader);
std::vector<uint256> GetDownloadPath(uint256 hashTip) const;
bool PathReachesChain(const std::vector<uint256>& vPath) const;
void ContinueHeaders(CNode* pfrom, const uint256& hashTip);
};