From 287a84a53385539d8891f6ffa7024415e58f35bb Mon Sep 17 00:00:00 2001 From: Capri Date: Tue, 15 Sep 2026 18:16:29 +0000 Subject: [PATCH] Normalize multi-algo chainwork and bootstrap RandomX difficulty --- src/chain.cpp | 71 ++++++++++++++++++++++++++++++++++- src/chain.h | 3 ++ src/chainparams.cpp | 4 ++ src/consensus/params.h | 1 + src/pow.cpp | 11 +++++- src/pow.h | 1 + src/test/pow_tests.cpp | 84 +++++++++++++++++++++++++++++++++++++++++- src/validation.cpp | 6 +-- 8 files changed, 173 insertions(+), 8 deletions(-) diff --git a/src/chain.cpp b/src/chain.cpp index 2797041..d5b3b94 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -174,10 +174,77 @@ arith_uint256 GetBlockProofBase(const CBlockIndex& block) arith_uint256 GetBlockProof(const CBlockIndex& block) { - // Vexta is SHA256D-only, so block proof is based only on this block's nBits. + // Raw proof derived directly from this block's nBits. return GetBlockProofBase(block); } +arith_uint256 GetBlockProof( + const CBlockIndex& block, + const Consensus::Params& params) +{ + // Preserve historical SHA256D chainwork exactly before multi-algo + // activation. + if (block.nHeight < params.randomXActivationHeight) { + return GetBlockProofBase(block); + } + + // Genesis has no parent state from which per-algo targets can be + // calculated. This path is only relevant if activation were configured + // at height zero. + if (block.pprev == nullptr) { + return GetBlockProofBase(block); + } + + const int blockAlgo = block.GetAlgo(); + if (blockAlgo != ALGO_SHA256D && blockAlgo != ALGO_RANDOMX) { + return 0; + } + + // Normalize heterogeneous PoW families using the geometric mean of the + // expected targets for all active algorithms, calculated from the same + // parent state. Competing SHA256D and RandomX children therefore receive + // the same chainwork increment regardless of their native difficulty + // scales. + arith_uint256 averageTarget(1); + + for (int algo = 0; algo < NUM_ALGOS_IMPL; ++algo) { + const unsigned int nBits = + GetNextWorkRequired(block.pprev, nullptr, params, algo); + + arith_uint256 target; + bool negative; + bool overflow; + target.SetCompact(nBits, &negative, &overflow); + + if (negative || + overflow || + target == 0 || + target > UintToArith256(params.powLimit)) { + return 0; + } + + const arith_uint256 root = target.ApproxNthRoot(NUM_ALGOS); + if (root == 0) { + return 0; + } + + averageTarget *= root; + } + + if (averageTarget == 0) { + return 0; + } + + arith_uint256 normalizedWork = + (~averageTarget / (averageTarget + 1)) + 1; + + // The normalized work value itself is the post-activation chainwork + // increment. It is derived only from consensus difficulty state and does + // not use a branch-specific activation anchor, so competing forks always + // use the same chainwork scale. + return normalizedWork; +} + int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& from, const CBlockIndex& tip, const Consensus::Params& params) { @@ -189,7 +256,7 @@ int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& fr r = from.nChainWork - to.nChainWork; sign = -1; } - r = r * arith_uint256(params.nPowTargetSpacing) / GetBlockProof(tip); + r = r * arith_uint256(params.nPowTargetSpacing) / GetBlockProof(tip, params); if (r.bits() > 63) { return sign * std::numeric_limits::max(); } diff --git a/src/chain.h b/src/chain.h index 6bd206c..fcffe9b 100644 --- a/src/chain.h +++ b/src/chain.h @@ -336,6 +336,9 @@ public: }; arith_uint256 GetBlockProof(const CBlockIndex& block); +arith_uint256 GetBlockProof( + const CBlockIndex& block, + const Consensus::Params& params); /** Return the time it would take to redo the work difference between from and to, assuming the current hashrate corresponds to the difficulty at tip, in seconds. */ int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& from, const CBlockIndex& tip, const Consensus::Params&); diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 90dbf42..c95f136 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -184,6 +184,7 @@ public: consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); + consensus.randomXInitialTarget = uint256S("000000359bc10000000000000000000000000000000000000000000000000000"); consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedLag = 64; @@ -229,6 +230,7 @@ public: consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); + consensus.randomXInitialTarget = consensus.powLimit; consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedLag = 64; @@ -386,6 +388,7 @@ public: consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256S("00000377ae000000000000000000000000000000000000000000000000000000"); + consensus.randomXInitialTarget = consensus.powLimit; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 27; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; @@ -470,6 +473,7 @@ public: consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); + consensus.randomXInitialTarget = consensus.powLimit; consensus.randomXSeedEpochLength = 64; consensus.randomXSeedLag = 8; diff --git a/src/consensus/params.h b/src/consensus/params.h index 749a13f..611f5ad 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -125,6 +125,7 @@ struct Params { int64_t asertHalfLife; int fastRiseActivationHeight; int randomXActivationHeight; + uint256 randomXInitialTarget; int randomXSeedEpochLength; int randomXSeedLag; diff --git a/src/pow.cpp b/src/pow.cpp index e475e2b..98a1172 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -69,6 +69,11 @@ unsigned int InitialDifficulty(const Consensus::Params& params) return PowLimit(params); } +unsigned int RandomXInitialDifficulty(const Consensus::Params& params) +{ + return UintToArith256(params.randomXInitialTarget).GetCompact(); +} + unsigned int CalculateASERT(const CBlockIndex* pindexLast, const Consensus::Params& params) { // Anchor ASERT to the last block before activation. @@ -205,7 +210,9 @@ static unsigned int CalculateMultiAlgoWorkRequired( GetLastBlockIndexForAlgoFast(pindexLast, algo); if (pindexPrevAlgo == nullptr) { - return InitialDifficulty(params); + return algo == ALGO_RANDOMX + ? RandomXInitialDifficulty(params) + : InitialDifficulty(params); } if (params.fPowNoRetargeting || params.fEasyPow) { @@ -318,7 +325,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead } if (algo == ALGO_RANDOMX) { - return InitialDifficulty(params); + return RandomXInitialDifficulty(params); } // Existing SHA256D difficulty logic below remains unchanged. diff --git a/src/pow.h b/src/pow.h index 000df63..4c99c4a 100644 --- a/src/pow.h +++ b/src/pow.h @@ -16,6 +16,7 @@ class CBlockIndex; class uint256; unsigned int InitialDifficulty(const Consensus::Params& params); +unsigned int RandomXInitialDifficulty(const Consensus::Params& params); unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params&, int algo); /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ diff --git a/src/test/pow_tests.cpp b/src/test/pow_tests.cpp index cb09c9b..3d25900 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -70,6 +70,88 @@ BOOST_AUTO_TEST_CASE(CheckProofOfWork_test_zero_target) BOOST_CHECK(!CheckProofOfWork(hash, nBits, consensus)); } + +BOOST_AUTO_TEST_CASE(MultiAlgo_chainwork_normalization_test) +{ + auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN); + auto consensus = chainParams->GetConsensus(); + + consensus.randomXActivationHeight = 40; + + std::vector blocks(40); + + arith_uint256 shaTarget = UintToArith256(consensus.powLimit); + shaTarget >>= 8; + const unsigned int shaBits = shaTarget.GetCompact(); + + const int64_t startTime = 1800000000; + + CBlockIndex* latest[NUM_ALGOS_IMPL]{}; + + for (int i = 0; i < 40; ++i) { + blocks[i].pprev = i ? &blocks[i - 1] : nullptr; + blocks[i].nHeight = i; + blocks[i].nTime = + startTime + i * consensus.nPowTargetSpacing; + blocks[i].nBits = shaBits; + blocks[i].nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + + for (int algo = 0; algo < NUM_ALGOS_IMPL; ++algo) { + blocks[i].lastAlgoBlocks[algo] = latest[algo]; + } + + latest[ALGO_SHA256D] = &blocks[i]; + } + + // Before multi-algo activation, consensus-aware proof must remain + // bit-for-bit identical to the historical SHA256D raw proof. + BOOST_CHECK( + GetBlockProof(blocks[39], consensus) == + GetBlockProof(blocks[39])); + + // Height 40 is the first multi-algo height. Build two alternative + // children of the same parent: one SHA256D and one RandomX. + CBlockIndex shaChild; + shaChild.pprev = &blocks[39]; + shaChild.nHeight = 40; + shaChild.nTime = + blocks[39].nTime + consensus.nPowTargetSpacing; + shaChild.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + shaChild.nBits = + GetNextWorkRequired( + &blocks[39], nullptr, consensus, ALGO_SHA256D); + + CBlockIndex randomXChild; + randomXChild.pprev = &blocks[39]; + randomXChild.nHeight = 40; + randomXChild.nTime = + blocks[39].nTime + consensus.nPowTargetSpacing; + randomXChild.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_RANDOMX; + randomXChild.nBits = + GetNextWorkRequired( + &blocks[39], nullptr, consensus, ALGO_RANDOMX); + + // The two algorithms deliberately have different raw targets here. + BOOST_REQUIRE_NE(shaChild.nBits, randomXChild.nBits); + + // Raw proof is therefore different. + BOOST_REQUIRE( + GetBlockProof(shaChild) != + GetBlockProof(randomXChild)); + + // But normalized chainwork for competing children of the same parent + // must be identical. Otherwise one PoW family could win chain selection + // merely because its numeric difficulty scale differs. + BOOST_CHECK( + GetBlockProof(shaChild, consensus) == + GetBlockProof(randomXChild, consensus)); + +} + + BOOST_AUTO_TEST_CASE(GetBlockProofEquivalentTime_test) { const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN); @@ -593,7 +675,7 @@ BOOST_AUTO_TEST_CASE(MultiAlgo_DAA_activation_boundary_test) BOOST_CHECK_EQUAL( firstRandomXBits, - InitialDifficulty(consensus)); + RandomXInitialDifficulty(consensus)); } diff --git a/src/validation.cpp b/src/validation.cpp index 3a72242..18a3020 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1337,7 +1337,7 @@ void CChainState::CheckForkWarningConditions() return; } - if (pindexBestInvalid && pindexBestInvalid->nChainWork > m_chain.Tip()->nChainWork + (GetBlockProof(*m_chain.Tip()) * 6)) { + if (pindexBestInvalid && pindexBestInvalid->nChainWork > m_chain.Tip()->nChainWork + (GetBlockProof(*m_chain.Tip(), m_params.GetConsensus()) * 6)) { LogPrintf("%s: Warning: Found invalid chain at least ~6 blocks longer than our best chain.\nChain state database corruption likely.\n", __func__); SetfLargeWorkInvalidChainFound(true); } else { @@ -3069,7 +3069,7 @@ CBlockIndex* BlockManager::AddToBlockIndex( } pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->nTime) : pindexNew->nTime); - pindexNew->nChainWork = (pindexNew->pprev ? pindexNew->pprev->nChainWork : 0) + GetBlockProof(*pindexNew); + pindexNew->nChainWork = (pindexNew->pprev ? pindexNew->pprev->nChainWork : 0) + GetBlockProof(*pindexNew, consensus_params); pindexNew->RaiseValidity(BLOCK_VALID_TREE); if (pindexBestHeader == nullptr || pindexBestHeader->nChainWork < pindexNew->nChainWork) pindexBestHeader = pindexNew; @@ -3959,7 +3959,7 @@ bool BlockManager::LoadBlockIndex( } nHeight = pindex-> nHeight; - pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex); + pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex, consensus_params); pindex->nTimeMax = (pindex->pprev ? std::max(pindex->pprev->nTimeMax, pindex->nTime) : pindex->nTime); // We can link the chain of blocks for which we've received transactions at some point. // Pruned nodes may have deleted the block.