v5.9.14 + pitfall #61 guard: initialize pindexFinalized on startup
ROOT CAUSE of the 2026-06-16 minority-fork reorg: The v5.9.14 getheaders handler serves headers from pindexGenesisBlock when a fork peer sends a locator that doesn't match our main chain. The intent was to help fork peers learn the canonical chain. The bug: this allows the fork peer to feed us THEIR short chain back via getheaders, and we accept it because: 1. pindexFinalized is NULL on a fresh restart (the auto-checkpoint code in ActivateBestChain() at main.cpp:2459 only sets it when !IsInitialBlockDownload(), but a synced daemon restarting with a chain tip > 24h stale is considered IBD by the time check at main.cpp:1331). 2. With pindexFinalized = NULL, the reorg guard at main.cpp:2198 short-circuits: 'if (pindexFinalized && pfork->nHeight < ...)'. 3. The fork peer's 3,755-block chain gets accepted, overwriting our healthy 2,206,004-block chain. THE FIX (two parts): A. init.cpp:1083-1113 — after LoadBlockIndex(), call Checkpoints::GetLastCheckpoint(mapBlockIndex) to initialize pindexFinalized from the hardcoded checkpoint (block 2,205,000). This is a no-op on the first ~10 seconds of the daemon's life (during the initial IBD walk), but as soon as we sync past block 2,205,000, the checkpoint is in mapBlockIndex and pindexFinalized is set for the daemon's entire lifetime. B. main.cpp:4473-4496 — in the getheaders fork-detection branch, serve from pindexFinalized->pnext (the block after our last finalized checkpoint) instead of pindexGenesisBlock. This is the safe equivalent of the v5.9.14 'serve from genesis' logic — the peer learns our canonical chain from the most recently finalized point forward, and their short fork gets rejected at the reorg guard in Reorganize() because the fork point is below pindexFinalized. Combined: a fork peer can no longer drag us below block 2,205,000 because (a) pindexFinalized is always set on a synced restart, and (b) the reorg guard now sees a non-NULL pindexFinalized and rejects any fork below it. Tested manually by simulating a restart with the v5.9.14 binary on a chain that had been reorged to a minority fork; the new build refuses the reorg and prints 'STARTUP-CHECKPOINT: pindexFinalized set to block 2205000' on startup, then 'getheaders: fork detected from peer ... serving headers from finalized block 2205000' on the first fork peer's getheaders request.
This commit is contained in:
@@ -1081,6 +1081,37 @@ bool AppInit2()
|
||||
if (!LoadBlockIndex())
|
||||
return InitError(_("Error loading blkindex.dat"));
|
||||
|
||||
// triangles fix (pitfall #61): initialize pindexFinalized from the
|
||||
// hardcoded checkpoint on startup, BEFORE the daemon opens any peer
|
||||
// connections or processes any block messages.
|
||||
//
|
||||
// Without this, pindexFinalized stays NULL on a fresh restart even when
|
||||
// we have 2.2M blocks on disk, because the auto-checkpoint code in
|
||||
// ActivateBestChain() at main.cpp:2459 only sets it when
|
||||
// !IsInitialBlockDownload(). If the chain tip is more than 24h stale
|
||||
// (which happens on every restart with a synced chain), IsInitialBlockDownload()
|
||||
// returns true and pindexFinalized never gets set.
|
||||
//
|
||||
// The downstream reorg guard at main.cpp:2198 short-circuits when
|
||||
// pindexFinalized is NULL, which allowed a 3,755-block minority fork
|
||||
// to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading
|
||||
// the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on
|
||||
// startup means the reorg guard is always active whenever the
|
||||
// checkpointed block is in our local mapBlockIndex.
|
||||
{
|
||||
CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||
if (pCheckpoint && pCheckpoint != pindexFinalized)
|
||||
{
|
||||
pindexFinalized = pCheckpoint;
|
||||
printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n",
|
||||
pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
}
|
||||
else if (!pCheckpoint)
|
||||
{
|
||||
printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n");
|
||||
}
|
||||
}
|
||||
|
||||
// If the block index is empty but blk0001.dat exists (bootstrap download),
|
||||
// fast-import: build the index directly from the block file without re-writing
|
||||
// data. Batches LevelDB commits every 200K blocks for speed.
|
||||
|
||||
+19
-2
@@ -4471,11 +4471,28 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
// backwards walk if pnext is null for any other reason (this
|
||||
// happens when LoadBlockIndex() didn't fully heal pnext links,
|
||||
// or the chain was bootstrapped from a snapshot).
|
||||
//
|
||||
// pitfall #61 guard: if pindexFinalized is set (from the startup
|
||||
// hardcoded-checkpoint init in init.cpp), serve from there instead
|
||||
// of genesis. This prevents a fork peer from feeding us their
|
||||
// short chain back via getheaders — the peer only learns our
|
||||
// canonical chain from the finalized point forward, and their
|
||||
// conflicting fork gets rejected at the reorg check in
|
||||
// Reorganize() because the fork point is below pindexFinalized.
|
||||
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
||||
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
||||
{
|
||||
printf("WARNING: peer getheaders locator has no common blocks — serving headers from genesis (peer may be on a fork)\n");
|
||||
pindex = pindexGenesisBlock;
|
||||
if (pindexFinalized && pindexFinalized->pnext)
|
||||
{
|
||||
printf("getheaders: fork detected from peer %s, serving headers from finalized block %d (not genesis) — pitfall #61 guard\n",
|
||||
pfrom->addr.ToString().c_str(), pindexFinalized->nHeight);
|
||||
pindex = pindexFinalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("WARNING: peer getheaders locator has no common blocks — serving headers from genesis (peer may be on a fork)\n");
|
||||
pindex = pindexGenesisBlock;
|
||||
}
|
||||
}
|
||||
|
||||
if (pindex)
|
||||
|
||||
Reference in New Issue
Block a user