fix(pow): defer RandomX header check when key block not yet connected

Follow-on to the post-IBD header-PoW verification (b9fdc7981 + 7e9b2c661).
CheckRandomXSolution derives the RandomX key from the block at
keyHeight = ((height-lag)/interval)*interval, looked up on the ACTIVE chain
(hush_chainactive), so that block must be CONNECTED. When a post-IBD node's
block tip lags the header tip by more than ~one RandomX interval -- the normal
IBD tail, or any node catching up -- the key block is not connected yet, so
GetRandomXKey returns empty. The old code returned an error, making
CheckBlockHeader DoS(100)-ban the honest peer that sent a perfectly valid tip
header we simply could not verify yet.

Observed live: a node finishing a mainnet reindex banned the pool box + seeds
and stalled ~2000 blocks short of the tip. Fix: on an empty key, DEFER (return
true) instead of error -- the header is fully RandomX-verified at block-connect,
where the key block is always connected (blocks connect in order,
keyHeight <= height-lag < the connected tip). Flood protection is preserved for
synced nodes (key present -> real check) and bounded during catch-up by the
per-peer IBD header cap + nMinimumChainWork.

Validated on the live 3.14M-block chain: the affected node caught up the full
~2135-block gap to the tip with zero peer bans (was stalled + banned before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 18:54:35 -05:00
parent b5050d06c0
commit d2124a3038

View File

@@ -804,8 +804,20 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
// Derive the key (shared helper) and serialize the input (identical bytes to the pool path).
std::string rxKey = GetRandomXKey(height);
if (rxKey.empty())
return error("CheckRandomXSolution(): cannot derive RandomX key for height %d", height);
if (rxKey.empty()) {
// The RandomX key block (keyHeight = ((height-lag)/interval)*interval, looked up on the
// ACTIVE chain) is not yet connected. This happens ONLY at header-accept when headers run
// ahead of our connected block tip (the IBD tail / catch-up) -- block-connect always has it,
// since blocks connect in order and keyHeight <= height-lag < the connected tip. The header
// is NOT invalid; we simply cannot verify it YET. Defer to block-connect (which re-checks
// with the key present) rather than returning an error -- returning an error here makes
// CheckBlockHeader DoS(100)-ban the honest peer that sent a perfectly valid tip header we
// just can't check yet (observed live: a post-reindex node banned the whole fleet and stalled
// ~2000 blocks short of the tip). Flood protection is preserved for synced nodes (key present
// -> real RandomX check) and bounded during catch-up by the per-peer IBD header cap + nMinimumChainWork.
LogPrint("net", "CheckRandomXSolution: RandomX key block for height %d not yet connected; deferring verification to block-connect\n", height);
return true;
}
std::vector<unsigned char> ssInput = GetRandomXInput(*pblock);
char computedHash[RANDOMX_HASH_SIZE];