perf: verify each block's RandomX solution once, not twice, during sync

RandomX PoW verification is ~84% of block-connect wall time during network IBD, and
CheckBlock was recomputing it TWICE per block: once in CheckBlockHeader and again in
hush_checkPOW (which has no CBlockIndex, so it cannot use the fRandomXVerified dedup
the parallel pre-verify pool relies on). Skip the redundant recompute inside
hush_checkPOW: CheckBlockHeader runs first in CheckBlock and rejects an invalid
solution before hush_checkPOW is reached, so the block is already verified once.
Equihash, PoW-target and notary checks in hush_checkPOW still run.

A scoped guard (ScopedRandomXSkip) SAVES and RESTORES the thread-local
fSkipRandomXValidation, so it neither clobbers the miner's own skip
(TestBlockValidity -> ConnectBlock re-entry, which would otherwise force the ~256MB
inline RandomX alloc the miner deliberately avoids) nor leaks the flag on an exception.

Measured on an isolated RandomX test chain: RandomX verifies per block 2.0 -> 1.03
(~40% faster network sync). The 2x behavior pre-exists in v1.0.2. Consensus-neutral:
RandomXPreVerify.ConsensusEquivalence gtest passes; each block is still verified
exactly once by CheckBlockHeader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:26:30 +02:00
parent 762e25294f
commit 4e67e687d7
3 changed files with 20 additions and 2 deletions

View File

@@ -5100,6 +5100,14 @@ bool CheckBlockHeader(int32_t *futureblockp,int32_t height,CBlockIndex *pindex,
int32_t hush_checkPOW(int32_t slowflag,CBlock *pblock,int32_t height);
// RAII: save+restore the thread-local RandomX-skip flag around the verify-once dedup in CheckBlock,
// so it can never clobber the miner's own fSkipRandomXValidation (TestBlockValidity -> ConnectBlock
// re-entry) nor leak TRUE on an exception thrown out of hush_checkPOW.
struct ScopedRandomXSkip {
bool prev;
ScopedRandomXSkip() : prev(GetSkipRandomXValidation()) { SetSkipRandomXValidation(true); }
~ScopedRandomXSkip() { SetSkipRandomXValidation(prev); }
};
bool CheckBlock(int32_t *futureblockp,int32_t height,CBlockIndex *pindex,const CBlock& block, CValidationState& state,
libzcash::ProofVerifier& verifier,
bool fCheckPOW, bool fCheckMerkleRoot)
@@ -5130,8 +5138,16 @@ bool CheckBlock(int32_t *futureblockp,int32_t height,CBlockIndex *pindex,const C
fprintf(stderr," failed hash ht.%d\n",height);
return state.DoS(50, error("CheckBlock: proof of work failed"),REJECT_INVALID, "high-hash");
}
if ( ASSETCHAINS_STAKED == 0 && hush_checkPOW(1,(CBlock *)&block,height) < 0 ) // checks Equihash
return state.DoS(100, error("CheckBlock: failed slow_checkPOW"),REJECT_INVALID, "failed-slow_checkPOW");
if ( ASSETCHAINS_STAKED == 0 ) {
// verify-once: CheckBlockHeader above already verified this block RandomX solution; skip the
// redundant recompute inside hush_checkPOW (the un-deduped 2nd verify, ~half the RandomX cost
// that dominates IBD). The scoped guard saves/restores the skip flag (never hardcodes false)
// so the miner's own skip is preserved and nothing leaks on throw. Equihash + PoW-target in
// hush_checkPOW still run.
ScopedRandomXSkip _rxskip;
if ( hush_checkPOW(1,(CBlock *)&block,height) < 0 )
return state.DoS(100, error("CheckBlock: failed slow_checkPOW"),REJECT_INVALID, "failed-slow_checkPOW");
}
}
// Check the merkle root.

View File

@@ -715,6 +715,7 @@ static int64_t nTimeRandomX = 0; // cumulative RandomX validation time (us), r
thread_local bool fSkipRandomXValidation = false;
void SetSkipRandomXValidation(bool skip) { fSkipRandomXValidation = skip; }
bool GetSkipRandomXValidation() { return fSkipRandomXValidation; }
CBlockIndex *hush_chainactive(int32_t height);

View File

@@ -97,6 +97,7 @@ void RandomXValidatorShutdown();
/** Set thread-local flag to skip RandomX validation (used by miner during TestBlockValidity) */
void SetSkipRandomXValidation(bool skip);
bool GetSkipRandomXValidation();
/** Return the RandomX key rotation interval in blocks */
int GetRandomXInterval();