From d6ba1aed4e42b7ba61bd6cdb18ea97c62a416d2b Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 3 Mar 2026 13:47:47 -0600 Subject: [PATCH] Fix RandomX validation exploit: verify nSolution contains valid RandomX hash - Add CheckRandomXSolution() to validate RandomX PoW in nSolution field - Add ASSETCHAINS_RANDOMX_VALIDATION activation height per chain (DRAGONX: 2838976, TUMIN: 1200, others: height 1) - Add CRandomXInput serializer for deterministic RandomX hash input - Fix CheckProofOfWork() to properly reject invalid PoW (was missing SMART_CHAIN_SYMBOL check, allowing bypass) - Call CheckRandomXSolution() in hush_checkPOW and CheckBlockHeader Without this fix, attackers could submit blocks with invalid RandomX hashes that passed validation, as CheckProofOfWork returned early during block loading and the nSolution field was never verified. --- .gitignore | 1 + src/hush_bitcoind.h | 5 ++ src/hush_defs.h | 1 + src/hush_globals.h | 1 + src/hush_utils.h | 12 ++++ src/main.cpp | 2 + src/pow.cpp | 136 +++++++++++++++++++++++++++++++++++++++++ src/pow.h | 12 ++++ src/primitives/block.h | 27 ++++++++ 9 files changed, 197 insertions(+) diff --git a/.gitignore b/.gitignore index f010d3da8..873d3596b 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,4 @@ REGTEST_7776 src/cc/librogue.so src/cc/games/prices src/cc/games/tetris +release-linux/ \ No newline at end of file diff --git a/src/hush_bitcoind.h b/src/hush_bitcoind.h index 218f09db7..424801ae0 100644 --- a/src/hush_bitcoind.h +++ b/src/hush_bitcoind.h @@ -1693,6 +1693,11 @@ int32_t hush_checkPOW(int32_t slowflag,CBlock *pblock,int32_t height) fprintf(stderr,"hush_checkPOW slowflag.%d ht.%d CheckEquihashSolution failed\n",slowflag,height); return(-1); } + if ( !CheckRandomXSolution(pblock, height) ) + { + fprintf(stderr,"hush_checkPOW slowflag.%d ht.%d CheckRandomXSolution failed\n",slowflag,height); + return(-1); + } hash = pblock->GetHash(); bnTarget.SetCompact(pblock->nBits,&fNegative,&fOverflow); bhash = UintToArith256(hash); diff --git a/src/hush_defs.h b/src/hush_defs.h index 510a6309a..6fd33a8f9 100644 --- a/src/hush_defs.h +++ b/src/hush_defs.h @@ -568,6 +568,7 @@ extern uint64_t ASSETCHAINS_SUPPLY, ASSETCHAINS_FOUNDERS_REWARD; extern int32_t ASSETCHAINS_LWMAPOS, ASSETCHAINS_SAPLING, ASSETCHAINS_OVERWINTER,ASSETCHAINS_BLOCKTIME; extern uint64_t ASSETCHAINS_TIMELOCKGTE; extern uint32_t ASSETCHAINS_ALGO,ASSETCHAINS_EQUIHASH,ASSETCHAINS_RANDOMX, HUSH_INITDONE; +extern int32_t ASSETCHAINS_RANDOMX_VALIDATION; extern int32_t HUSH_MININGTHREADS,HUSH_LONGESTCHAIN,ASSETCHAINS_SEED,IS_HUSH_NOTARY,USE_EXTERNAL_PUBKEY,HUSH_CHOSEN_ONE,HUSH_ON_DEMAND,HUSH_PASSPORT_INITDONE,ASSETCHAINS_STAKED,HUSH_NSPV; extern uint64_t ASSETCHAINS_COMMISSION, ASSETCHAINS_LASTERA,ASSETCHAINS_CBOPRET; extern uint64_t ASSETCHAINS_REWARD[ASSETCHAINS_MAX_ERAS+1], ASSETCHAINS_NOTARY_PAY[ASSETCHAINS_MAX_ERAS+1], ASSETCHAINS_TIMELOCKGTE, ASSETCHAINS_NONCEMASK[],ASSETCHAINS_NK[2]; diff --git a/src/hush_globals.h b/src/hush_globals.h index f7bbeb860..bfa1650b1 100644 --- a/src/hush_globals.h +++ b/src/hush_globals.h @@ -93,6 +93,7 @@ uint64_t ASSETCHAINS_NONCEMASK[] = {0xffff}; uint32_t ASSETCHAINS_NONCESHIFT[] = {32}; uint32_t ASSETCHAINS_HASHESPERROUND[] = {1}; uint32_t ASSETCHAINS_ALGO = _ASSETCHAINS_EQUIHASH; +int32_t ASSETCHAINS_RANDOMX_VALIDATION = -1; // activation height for RandomX validation (-1 = disabled) // min diff returned from GetNextWorkRequired needs to be added here for each algo, so they can work with ac_staked. uint32_t ASSETCHAINS_MINDIFF[] = {537857807}; int32_t ASSETCHAINS_LWMAPOS = 0; // percentage of blocks should be PoS diff --git a/src/hush_utils.h b/src/hush_utils.h index 1ca9db12d..23e230396 100644 --- a/src/hush_utils.h +++ b/src/hush_utils.h @@ -1902,6 +1902,18 @@ void hush_args(char *argv0) strncpy(SMART_CHAIN_SYMBOL,name.c_str(),sizeof(SMART_CHAIN_SYMBOL)-1); const bool ishush3 = strncmp(SMART_CHAIN_SYMBOL, "HUSH3",5) == 0 ? true : false; + // Set RandomX validation activation height per chain + if (ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX) { + if (strncmp(SMART_CHAIN_SYMBOL, "DRAGONX", 7) == 0) { + ASSETCHAINS_RANDOMX_VALIDATION = 2838976; // TBD: set to coordinated upgrade height + } else if (strncmp(SMART_CHAIN_SYMBOL, "TUMIN", 5) == 0) { + ASSETCHAINS_RANDOMX_VALIDATION = 1200; // TBD: set to coordinated upgrade height + } else { + ASSETCHAINS_RANDOMX_VALIDATION = 1; // all other RandomX HACs: enforce from height 1 + } + printf("ASSETCHAINS_RANDOMX_VALIDATION set to %d for %s\n", ASSETCHAINS_RANDOMX_VALIDATION, SMART_CHAIN_SYMBOL); + } + ASSETCHAINS_LASTERA = GetArg("-ac_eras", 1); if(ishush3) { ASSETCHAINS_LASTERA = 3; diff --git a/src/main.cpp b/src/main.cpp index 2ae864b3a..ea0045589 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4993,6 +4993,8 @@ bool CheckBlockHeader(int32_t *futureblockp,int32_t height,CBlockIndex *pindex, { if ( !CheckEquihashSolution(&blockhdr, Params()) ) return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"),REJECT_INVALID, "invalid-solution"); + if ( !CheckRandomXSolution(&blockhdr, height) ) + return state.DoS(100, error("CheckBlockHeader(): RandomX solution invalid"),REJECT_INVALID, "invalid-randomx-solution"); } // Check proof of work matches claimed amount /*hush_index2pubkey33(pubkey33,pindex,height); diff --git a/src/pow.cpp b/src/pow.cpp index 0ba332b07..c23ced293 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -28,6 +28,8 @@ #include "uint256.h" #include "util.h" #include "sodium.h" +#include "RandomX/src/randomx.h" +#include #ifdef ENABLE_RUST #include "librustzcash.h" @@ -683,6 +685,137 @@ bool CheckEquihashSolution(const CBlockHeader *pblock, const CChainParams& param return true; } +// Static objects for CheckRandomXSolution +static std::mutex cs_randomx_validator; +static randomx_cache *s_rxCache = nullptr; +static randomx_vm *s_rxVM = nullptr; +static std::string s_rxCurrentKey; // tracks current key to avoid re-init + +// Thread-local flag: skip CheckRandomXSolution when the miner is validating its own block +// The miner already computed the correct RandomX hash — re-verifying with a separate +// cache+VM would allocate ~256MB extra memory and can trigger the OOM killer. +thread_local bool fSkipRandomXValidation = false; + +void SetSkipRandomXValidation(bool skip) { fSkipRandomXValidation = skip; } + +CBlockIndex *hush_chainactive(int32_t height); + +bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height) +{ + // Only applies to RandomX chains + if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX) + return true; + + // Disabled if activation height is negative + if (ASSETCHAINS_RANDOMX_VALIDATION < 0) + return true; + + // Not yet at activation height + if (height < ASSETCHAINS_RANDOMX_VALIDATION) + return true; + + // Do not affect initial block loading + extern int32_t HUSH_LOADINGBLOCKS; + if (HUSH_LOADINGBLOCKS != 0) + return true; + + // Skip when miner is validating its own block via TestBlockValidity + if (fSkipRandomXValidation) + return true; + + // nSolution must be exactly RANDOMX_HASH_SIZE (32) bytes + if (pblock->nSolution.size() != RANDOMX_HASH_SIZE) { + return error("CheckRandomXSolution(): nSolution size %u != expected %d at height %d", + pblock->nSolution.size(), RANDOMX_HASH_SIZE, height); + } + + static int randomxInterval = GetRandomXInterval(); + static int randomxBlockLag = GetRandomXBlockLag(); + + // Determine the correct RandomX key for this height + char initialKey[82]; + snprintf(initialKey, 81, "%08x%s%08x", ASSETCHAINS_MAGIC, SMART_CHAIN_SYMBOL, ASSETCHAINS_RPCPORT); + + std::string rxKey; + if (height < randomxInterval + randomxBlockLag) { + // Use initial key derived from chain params + rxKey = std::string(initialKey, strlen(initialKey)); + } else { + // Use block hash at the key height + int keyHeight = ((height - randomxBlockLag) / randomxInterval) * randomxInterval; + CBlockIndex *pKeyIndex = hush_chainactive(keyHeight); + if (pKeyIndex == nullptr) { + return error("CheckRandomXSolution(): cannot get block index at key height %d for block %d", keyHeight, height); + } + uint256 blockKey = pKeyIndex->GetBlockHash(); + rxKey = std::string((const char*)&blockKey, sizeof(blockKey)); + } + + // Serialize the block header without nSolution (but with nNonce) as RandomX input + CRandomXInput rxInput(*pblock); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << rxInput; + + char computedHash[RANDOMX_HASH_SIZE]; + + { + std::lock_guard lock(cs_randomx_validator); + + // Initialize cache + VM if needed, or re-init if key changed + if (s_rxCache == nullptr) { + randomx_flags flags = randomx_get_flags(); + s_rxCache = randomx_alloc_cache(flags); + if (s_rxCache == nullptr) { + return error("CheckRandomXSolution(): failed to allocate RandomX cache"); + } + randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size()); + s_rxCurrentKey = rxKey; + s_rxVM = randomx_create_vm(flags, s_rxCache, nullptr); + if (s_rxVM == nullptr) { + randomx_release_cache(s_rxCache); + s_rxCache = nullptr; + return error("CheckRandomXSolution(): failed to create RandomX VM"); + } + } else if (s_rxCurrentKey != rxKey) { + randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size()); + s_rxCurrentKey = rxKey; + randomx_vm_set_cache(s_rxVM, s_rxCache); + } + + randomx_calculate_hash(s_rxVM, &ss[0], ss.size(), computedHash); + } + + // Compare computed hash against nSolution + if (memcmp(computedHash, pblock->nSolution.data(), RANDOMX_HASH_SIZE) != 0) { + // Debug: dump both hashes for diagnosis + std::string computedHex, solutionHex; + for (int i = 0; i < RANDOMX_HASH_SIZE; i++) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x", (uint8_t)computedHash[i]); + computedHex += buf; + snprintf(buf, sizeof(buf), "%02x", pblock->nSolution[i]); + solutionHex += buf; + } + fprintf(stderr, "CheckRandomXSolution(): HASH MISMATCH at height %d\n", height); + fprintf(stderr, " computed : %s\n", computedHex.c_str()); + fprintf(stderr, " nSolution: %s\n", solutionHex.c_str()); + fprintf(stderr, " rxKey size=%lu, input size=%lu, nNonce=%s\n", + rxKey.size(), ss.size(), pblock->nNonce.ToString().c_str()); + fprintf(stderr, " nSolution.size()=%lu, RANDOMX_HASH_SIZE=%d\n", + pblock->nSolution.size(), RANDOMX_HASH_SIZE); + // Also log to debug.log + LogPrintf("CheckRandomXSolution(): HASH MISMATCH at height %d\n", height); + LogPrintf(" computed : %s\n", computedHex); + LogPrintf(" nSolution: %s\n", solutionHex); + LogPrintf(" rxKey size=%lu, input size=%lu, nNonce=%s\n", + rxKey.size(), ss.size(), pblock->nNonce.ToString()); + return false; + } + + LogPrint("randomx", "CheckRandomXSolution(): valid at height %d\n", height); + return true; +} + int32_t hush_chosennotary(int32_t *notaryidp,int32_t height,uint8_t *pubkey33,uint32_t timestamp); int32_t hush_currentheight(); void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height); @@ -729,6 +862,8 @@ bool CheckProofOfWork(const CBlockHeader &blkHeader, uint8_t *pubkey33, int32_t if ( HUSH_LOADINGBLOCKS != 0 ) return true; + if ( SMART_CHAIN_SYMBOL[0] != 0 || height > 792000 ) + { if ( Params().NetworkIDString() != "regtest" ) { for (i=31; i>=0; i--) @@ -745,6 +880,7 @@ bool CheckProofOfWork(const CBlockHeader &blkHeader, uint8_t *pubkey33, int32_t fprintf(stderr," <- origpubkey\n"); } return false; + } } /*for (i=31; i>=0; i--) fprintf(stderr,"%02x",((uint8_t *)&hash)[i]); diff --git a/src/pow.h b/src/pow.h index 9a86bee64..6027c45f9 100644 --- a/src/pow.h +++ b/src/pow.h @@ -38,6 +38,18 @@ unsigned int CalculateNextWorkRequired(arith_uint256 bnAvg, /** Check whether the Equihash solution in a block header is valid */ bool CheckEquihashSolution(const CBlockHeader *pblock, const CChainParams&); +/** Check whether a block header contains a valid RandomX solution */ +bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height); + +/** Set thread-local flag to skip RandomX validation (used by miner during TestBlockValidity) */ +void SetSkipRandomXValidation(bool skip); + +/** Return the RandomX key rotation interval in blocks */ +int GetRandomXInterval(); + +/** Return the RandomX key change lag in blocks */ +int GetRandomXBlockLag(); + /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ bool CheckProofOfWork(const CBlockHeader &blkHeader, uint8_t *pubkey33, int32_t height, const Consensus::Params& params); CChainPower GetBlockProof(const CBlockIndex& block); diff --git a/src/primitives/block.h b/src/primitives/block.h index 245be24ed..bd0d429e8 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -237,6 +237,33 @@ public: } }; +/** + * Custom serializer for CBlockHeader that includes nNonce but omits nSolution, + * for use as deterministic input to RandomX hashing. + */ +class CRandomXInput : private CBlockHeader +{ +public: + CRandomXInput(const CBlockHeader &header) + { + CBlockHeader::SetNull(); + *((CBlockHeader*)this) = header; + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(this->nVersion); + READWRITE(hashPrevBlock); + READWRITE(hashMerkleRoot); + READWRITE(hashFinalSaplingRoot); + READWRITE(nTime); + READWRITE(nBits); + READWRITE(nNonce); + } +}; + /** Describes a place in the block chain to another node such that if the * other node doesn't have the same branch, it can find a recent common trunk.