2 Commits

Author SHA1 Message Date
5951ee118a fix(net): cap per-peer headers during IBD (header-flood DoS)
Audit #8. The HEADERS handler accepted unbounded headers per peer with no
cumulative cap; during IBD (fCheckPOW=0) a peer could flood cost-free PoW-less
headers into mapBlockIndex/leveldb (never selected -- nMinimumChainWork gates
that -- but still memory/disk growth). Add a per-peer nHeadersProcessed counter
in CNodeState; while IsInitialBlockDownload(), if one peer exceeds
2*max(pindexBestHeader height, checkpoint height) + 200000 headers,
Misbehaving(100) and drop it. The cap is ~2x the chain length, so honest sync
never approaches it; inert post-IBD (the RandomX header check handles forged
headers there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:54:35 -05:00
d2124a3038 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>
2026-07-14 18:54:35 -05:00
2 changed files with 38 additions and 2 deletions

View File

@@ -328,6 +328,8 @@ namespace {
bool fBulkHeaderSeen; bool fBulkHeaderSeen;
//! (server side) time (us) we last served a bulk stream to this peer, for flood throttling. //! (server side) time (us) we last served a bulk stream to this peer, for flood throttling.
int64_t nLastBulkServeTime; int64_t nLastBulkServeTime;
//! (#8 IBD header-flood cap) cumulative headers this peer made us process while in IBD.
int64_t nHeadersProcessed;
CNodeState() { CNodeState() {
fCurrentlyConnected = false; fCurrentlyConnected = false;
@@ -348,6 +350,7 @@ namespace {
nBulkHashStart.SetNull(); nBulkHashStart.SetNull();
fBulkHeaderSeen = false; fBulkHeaderSeen = false;
nLastBulkServeTime = 0; nLastBulkServeTime = 0;
nHeadersProcessed = 0;
} }
}; };
@@ -7888,6 +7891,27 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
} }
} }
// SECURITY (#8: IBD header-flood cap): bound how many headers a single peer can make us
// store while in IBD. Honest headers-first sync needs at most ~chain-length headers from a
// peer; one that floods far past 2x the known chain length is only trying to bloat
// mapBlockIndex/leveldb (such headers are never selected -- nMinimumChainWork gates that --
// but they still cost memory/disk). Cap per-peer and drop the peer. IBD-only: post-IBD the
// RandomX check in AcceptBlockHeader already makes forged headers fail RandomX and ban.
if (IsInitialBlockDownload()) {
CNodeState *hstate = State(pfrom->GetId());
if (hstate != NULL) {
hstate->nHeadersProcessed += (int64_t)nCount;
int knownH = std::max(pindexBestHeader ? (int)pindexBestHeader->GetHeight() : 0,
Checkpoints::GetTotalBlocksEstimate(chainparams.Checkpoints()));
int64_t headerCap = 2 * (int64_t)knownH + 200000;
if (hstate->nHeadersProcessed > headerCap) {
Misbehaving(pfrom->GetId(), 100);
return error("%s: peer=%d flooded %lld headers during IBD (cap %lld)", __func__,
pfrom->id, (long long)hstate->nHeadersProcessed, (long long)headerCap);
}
}
}
if (pindexLast) if (pindexLast)
UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash()); UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash());

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). // Derive the key (shared helper) and serialize the input (identical bytes to the pool path).
std::string rxKey = GetRandomXKey(height); std::string rxKey = GetRandomXKey(height);
if (rxKey.empty()) if (rxKey.empty()) {
return error("CheckRandomXSolution(): cannot derive RandomX key for height %d", height); // 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); std::vector<unsigned char> ssInput = GetRandomXInput(*pblock);
char computedHash[RANDOMX_HASH_SIZE]; char computedHash[RANDOMX_HASH_SIZE];