[grade=B] fix(staking): carve out caught-up nodes from IBD gate so chain can self-heal

IsStakingSafe() refused to stake whenever IsInitialBlockDownload() was
true, and IBD flips true whenever the chain tip is older than 24h. After
24h of no blocks, every node simultaneously refuses to stake and the
network deadlocks.

Narrow the gate: only refuse when IBD is true AND the local height is
behind the peer/checkpoint estimate. A node at the peer median clears
the gate and keeps staking through idle periods, so the chain can
restart itself. Genuinely-behind nodes still hold off.

Block validation, reorg rules, and checkpoint rules unchanged. The
existing -forcestaking bootstrap escape hatch still works on nodes
caught up to the checkpoint.

Codex verdict: urn:ump:6brctfzo5mrplzpyolstra5ula3hwtcy6t7bdd2iey552ozsoeiq
This commit is contained in:
Sami Ahmed
2026-07-31 20:02:15 -07:00
parent a23e601b6a
commit f69f08792a
+10 -2
View File
@@ -1650,8 +1650,16 @@ int GetNumBlocksOfPeers()
bool IsStakingSafe(const CWallet* pwallet, const std::vector<CNode*>& vNodesSnapshot)
{
// (1) Never stake during IBD.
if (IsInitialBlockDownload())
// (1) Never stake during IBD UNLESS we're caught up to peers. A node
// that is fully synced but idle (chain stalled >24h, so IBD flips
// true via the stale-tip heuristic) MUST keep staking so the network
// can self-heal. Without this carve-out, every node simultaneously
// refuses to stake after 24h of no blocks and the chain deadlocks.
//
// GetNumBlocksOfPeers() is the peer median height clamped to the
// checkpoint estimate, so this comparison is approximate: a node at
// the peer median clears it, a node behind does not.
if (IsInitialBlockDownload() && nBestHeight < GetNumBlocksOfPeers())
{
if (fDebug) printf("STAKING-GATE: refuse (IBD)\n");
return false;