// Copyright (c) 2024-2026 The DragonX developers // Distributed under the GPLv3 software license, see the accompanying // file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html // // Consensus-equivalence test for the parallel RandomX pre-verification pool. The pool is purely an // optimization: a block's transient fRandomXVerified flag (set by CRandomXCheck on a real hash // match) only lets CheckBlockHeader SKIP the inline recompute. So for every block the pool's // outcome must equal the inline CheckRandomXSolution outcome — `(preVerified || inline) == inline`. // We exercise a valid solution, a corrupted solution, and confirm the pool never "succeeds" on a // block the inline check would reject. #include #include #include #include "arith_uint256.h" #include "chain.h" #include "chainparams.h" #include "pow.h" #include "primitives/block.h" #include "RandomX/src/randomx.h" #include "hush_defs.h" #include "util.h" #include #include extern int32_t HUSH_LOADINGBLOCKS; extern bool fCheckpointsEnabled; namespace { // Compute the correct RandomX solution for a header using a standalone reference light VM, via the // SAME key + input helpers the validator uses (so the bytes/key match exactly). void ReferenceRandomXHash(const CBlockHeader& hdr, const std::string& key, unsigned char out[RANDOMX_HASH_SIZE]) { std::vector in = GetRandomXInput(hdr); randomx_flags flags = randomx_get_flags(); randomx_cache* c = randomx_alloc_cache(flags); ASSERT_NE(c, nullptr); randomx_init_cache(c, key.data(), key.size()); randomx_vm* vm = randomx_create_vm(flags, c, nullptr); ASSERT_NE(vm, nullptr); randomx_calculate_hash(vm, in.data(), in.size(), out); randomx_destroy_vm(vm); randomx_release_cache(c); } } // namespace TEST(RandomXPreVerify, ConsensusEquivalence) { // Force RandomX validation to actually run at low heights in the test harness. uint32_t savedAlgo = ASSETCHAINS_ALGO, savedRx = ASSETCHAINS_RANDOMX; int32_t savedVal = ASSETCHAINS_RANDOMX_VALIDATION, savedLoad = HUSH_LOADINGBLOCKS; bool savedCkpt = fCheckpointsEnabled; ASSETCHAINS_RANDOMX = 2; // a distinct nonzero algo id ASSETCHAINS_ALGO = ASSETCHAINS_RANDOMX; ASSETCHAINS_RANDOMX_VALIDATION = 1; // enforce from height 1 HUSH_LOADINGBLOCKS = 0; // not in initial-load (else RandomX skipped) fCheckpointsEnabled = false; // avoid the below-checkpoint skip const int32_t height = 10; // < interval+lag -> the chain-params initial key (no chainActive needed) CBlockHeader hdr; hdr.nVersion = 4; hdr.hashPrevBlock = uint256S("0x0000000000000000000000000000000000000000000000000000000000000001"); hdr.hashMerkleRoot = uint256S("0x0000000000000000000000000000000000000000000000000000000000000002"); hdr.hashFinalSaplingRoot = uint256S("0x0000000000000000000000000000000000000000000000000000000000000003"); hdr.nTime = 1700000000; hdr.nBits = 0x200f0f0f; hdr.nNonce = uint256S("0x0000000000000000000000000000000000000000000000000000000000000004"); std::string key = GetRandomXKey(height); ASSERT_FALSE(key.empty()); unsigned char good[RANDOMX_HASH_SIZE]; ReferenceRandomXHash(hdr, key, good); // Run the pool path synchronously on this thread (CRandomXCheck creates its own thread_local VM). auto poolVerifies = [&](const CBlockHeader& h) -> bool { RandomXValidatorPrepareKey(key); // load the shared cache with this key bool slot = false; CRandomXCheck chk(key, GetRandomXInput(h), h.nSolution.data(), &slot); chk(); return slot; }; // Case 1 — valid solution: both inline and pool accept; equivalence holds. hdr.nSolution.assign(good, good + RANDOMX_HASH_SIZE); EXPECT_TRUE(CheckRandomXSolution(&hdr, height)); EXPECT_TRUE(poolVerifies(hdr)); EXPECT_EQ(poolVerifies(hdr) || CheckRandomXSolution(&hdr, height), CheckRandomXSolution(&hdr, height)); // Case 2 — corrupted solution: both reject; the pool must NOT set verified. { CBlockHeader bad = hdr; bad.nSolution[0] ^= 0xff; EXPECT_FALSE(CheckRandomXSolution(&bad, height)); EXPECT_FALSE(poolVerifies(bad)); EXPECT_EQ(poolVerifies(bad) || CheckRandomXSolution(&bad, height), CheckRandomXSolution(&bad, height)); } // Case 3 — a verified flag on the block lets CheckBlockHeader skip, but verified is only ever set // by a real hash match, so it can never mask an invalid block. (Pool returns false for the bad // block above, so its fRandomXVerified stays false and the inline path rejects it at connect.) ASSETCHAINS_ALGO = savedAlgo; ASSETCHAINS_RANDOMX = savedRx; ASSETCHAINS_RANDOMX_VALIDATION = savedVal; HUSH_LOADINGBLOCKS = savedLoad; fCheckpointsEnabled = savedCkpt; } // A/B: serial inline verification (single VM) vs the parallel pool (worker threads). Directly // measures the speedup the pool delivers. We don't care about validity here (mismatched solutions // still cost a full hash), only wall-clock. parallel must beat serial whenever >1 core is used. TEST(RandomXPreVerify, ParallelSpeedup) { uint32_t savedAlgo = ASSETCHAINS_ALGO, savedRx = ASSETCHAINS_RANDOMX; int32_t savedVal = ASSETCHAINS_RANDOMX_VALIDATION, savedLoad = HUSH_LOADINGBLOCKS; bool savedCkpt = fCheckpointsEnabled; ASSETCHAINS_RANDOMX = 2; ASSETCHAINS_ALGO = ASSETCHAINS_RANDOMX; ASSETCHAINS_RANDOMX_VALIDATION = 1; HUSH_LOADINGBLOCKS = 0; fCheckpointsEnabled = false; const int32_t height = 10; std::string key = GetRandomXKey(height); ASSERT_FALSE(key.empty()); ASSERT_TRUE(RandomXValidatorPrepareKey(key)); const int M = 16; // blocks to verify in the window std::vector hdrs(M); for (int i = 0; i < M; i++) { hdrs[i].nVersion = 4; hdrs[i].nTime = 1700000000 + i; hdrs[i].nBits = 0x200f0f0f; hdrs[i].nNonce = ArithToUint256(arith_uint256(i + 1)); // distinct inputs hdrs[i].nSolution.assign(RANDOMX_HASH_SIZE, 0); // arbitrary; we time the hash } // Serial baseline: inline single-VM verification (each call hashes, then mismatches -> false). int64_t t0 = GetTimeMicros(); for (int i = 0; i < M; i++) CheckRandomXSolution(&hdrs[i], height); int64_t serialUs = GetTimeMicros() - t0; // Parallel: spawn K-1 workers + the master (this thread) joining via Wait(). int K = std::min(8, std::max(2, (int)boost::thread::hardware_concurrency())); boost::thread_group workers; for (int i = 0; i < K - 1; i++) workers.create_thread(&ThreadRandomXVerify); std::unique_ptr slots(new bool[M]()); std::vector checks; checks.reserve(M); for (int i = 0; i < M; i++) checks.push_back(CRandomXCheck(key, GetRandomXInput(hdrs[i]), hdrs[i].nSolution.data(), &slots[i])); int64_t t1 = GetTimeMicros(); { CCheckQueueControl control(&rxCheckQueue); control.Add(checks); control.Wait(); } int64_t parallelUs = GetTimeMicros() - t1; workers.interrupt_all(); workers.join_all(); printf("[ RandomX A/B ] %d blocks: serial(1 VM)=%ldms, parallel(%d threads)=%ldms, speedup=%.1fx\n", M, (long)(serialUs / 1000), K, (long)(parallelUs / 1000), (double)serialUs / (double)std::max(1, parallelUs)); EXPECT_LT(parallelUs, serialUs); // parallel must be faster than serial on a multi-core box ASSETCHAINS_ALGO = savedAlgo; ASSETCHAINS_RANDOMX = savedRx; ASSETCHAINS_RANDOMX_VALIDATION = savedVal; HUSH_LOADINGBLOCKS = savedLoad; fCheckpointsEnabled = savedCkpt; }