From 0a01ad8bbaa00696b58d7810234ac7e4b41bef69 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 5 Mar 2026 03:07:07 -0600 Subject: [PATCH] Fix nBits validation bypass and restore CheckProofOfWork rejection for HACs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two critical vulnerabilities allowed an attacker to flood the DragonX chain with minimum-difficulty blocks starting at height 2879907: 1. ContextualCheckBlockHeader only validated nBits for HUSH3 mainnet (gated behind `if (ishush3)`), never for HAC/smart chains. An attacker could submit blocks claiming any difficulty and the node accepted them. Add nBits validation for all non-HUSH3 smart chains, gated above daaForkHeight (default 450000) to maintain consensus with early chain history that was mined by a different binary. 2. The rebrand commit (85c8d7f7d) commented out the `return false` block in CheckProofOfWork that rejects blocks whose hash does not meet the claimed target. This made PoW validation a no-op — any hash passed. Restore the rejection block and add RANDOMX_VALIDATION height-gated logic so blocks after the activation height are always validated even during initial block loading. Vulnerability #1 was inherited from the upstream hush3 codebase. Vulnerability #2 was introduced by the DragonX rebrand. --- src/main.cpp | 20 ++++++++++++++++++++ src/pow.cpp | 14 ++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ea0045589..a045afce3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5140,6 +5140,26 @@ bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationState& sta } } + // Check Proof-of-Work difficulty for smart chains (HACs) + // Without this check, an attacker can submit blocks with arbitrary nBits + // (e.g., powLimit / diff=1) and they will be accepted, allowing the chain + // to be flooded with minimum-difficulty blocks. + // Only enforce above daaForkHeight to avoid consensus mismatch with early + // chain blocks that were mined by a different binary version. + if (!ishush3 && SMART_CHAIN_SYMBOL[0] != 0 && nHeight > daaForkHeight) { + unsigned int nNextWork = GetNextWorkRequired(pindexPrev, &block, consensusParams); + if (fDebug) { + LogPrintf("%s: HAC nbits height=%d expected=%lu actual=%lu\n", + __func__, nHeight, (unsigned long)nNextWork, (unsigned long)block.nBits); + } + if (block.nBits != nNextWork) { + return state.DoS(100, + error("%s: Incorrect diffbits for %s at height %d: expected %lu got %lu", + __func__, SMART_CHAIN_SYMBOL, nHeight, (unsigned long)nNextWork, (unsigned long)block.nBits), + REJECT_INVALID, "bad-diffbits"); + } + } + // Check timestamp against prev if (ASSETCHAINS_ADAPTIVEPOW <= 0 || nHeight < 30) { if (block.GetBlockTime() <= pindexPrev->GetMedianTimePast() ) diff --git a/src/pow.cpp b/src/pow.cpp index eef5d4ff6..309a0831a 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -863,10 +863,17 @@ bool CheckProofOfWork(const CBlockHeader &blkHeader, uint8_t *pubkey33, int32_t // Check proof of work matches claimed amount if ( UintToArith256(hash = blkHeader.GetHash()) > bnTarget ) { - if ( HUSH_LOADINGBLOCKS != 0 ) - return true; + // During initial block loading/sync, skip PoW validation for blocks + // before RandomX validation height. After activation, always validate + // to prevent injection of blocks with fake PoW. + if ( HUSH_LOADINGBLOCKS != 0 ) { + if (ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX && ASSETCHAINS_RANDOMX_VALIDATION > 0 && height >= ASSETCHAINS_RANDOMX_VALIDATION) { + // Fall through to reject the block — do NOT skip validation after activation + } else { + return true; + } + } - /* if ( SMART_CHAIN_SYMBOL[0] != 0 || height > 792000 ) { if ( Params().NetworkIDString() != "regtest" ) @@ -886,7 +893,6 @@ bool CheckProofOfWork(const CBlockHeader &blkHeader, uint8_t *pubkey33, int32_t } return false; } - */ } /*for (i=31; i>=0; i--) fprintf(stderr,"%02x",((uint8_t *)&hash)[i]);