diff --git a/src/chain.cpp b/src/chain.cpp index d5b3b94..1710ae3 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -178,6 +178,54 @@ arith_uint256 GetBlockProof(const CBlockIndex& block) return GetBlockProofBase(block); } +static arith_uint256 ScaleMultiAlgoChainwork( + const arith_uint256& work, + uint32_t numerator, + uint32_t denominator) +{ + if (work == 0 || numerator == 0 || denominator == 0) { + return 0; + } + + if (numerator == denominator) { + return work; + } + + // Compute floor(work * numerator / denominator) without first + // multiplying the full 256-bit value. base_uint multiplication silently + // truncates on overflow, so split work into quotient and remainder: + // + // work = quotient * denominator + remainder + // + // Then: + // + // floor(work * numerator / denominator) + // = quotient * numerator + // + floor(remainder * numerator / denominator) + // + // remainder is smaller than the 32-bit denominator, so its product with + // the 32-bit numerator fits comfortably inside 256 bits. + arith_uint256 quotient = work / denominator; + arith_uint256 remainder = work - quotient * denominator; + + const arith_uint256 maxValue = ~arith_uint256(0); + + if (quotient > maxValue / numerator) { + return 0; + } + + quotient *= numerator; + + remainder *= numerator; + remainder /= denominator; + + if (quotient > maxValue - remainder) { + return 0; + } + + return quotient + remainder; +} + arith_uint256 GetBlockProof( const CBlockIndex& block, const Consensus::Params& params) @@ -238,11 +286,12 @@ arith_uint256 GetBlockProof( 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; + // Apply a fixed, branch-independent consensus scale. The default 1/1 + // preserves the normalized-work value exactly. + return ScaleMultiAlgoChainwork( + normalizedWork, + params.multiAlgoChainworkScaleNumerator, + params.multiAlgoChainworkScaleDenominator); } int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& from, diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 6b8a74e..6624ed2 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -185,6 +185,8 @@ public: consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); consensus.randomXInitialTarget = uint256S("000000359bc10000000000000000000000000000000000000000000000000000"); + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 1; consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedLag = 64; @@ -231,6 +233,8 @@ public: consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); consensus.randomXInitialTarget = consensus.powLimit; + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 1; consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedLag = 64; @@ -389,6 +393,8 @@ public: consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256S("00000377ae000000000000000000000000000000000000000000000000000000"); consensus.randomXInitialTarget = consensus.powLimit; + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 1; 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; @@ -474,6 +480,8 @@ public: consensus.fastRiseActivationHeight = 7000; consensus.randomXActivationHeight = std::numeric_limits::max(); consensus.randomXInitialTarget = consensus.powLimit; + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 1; consensus.randomXSeedEpochLength = 64; consensus.randomXSeedLag = 8; diff --git a/src/consensus/params.h b/src/consensus/params.h index 611f5ad..83e7db8 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -126,6 +126,14 @@ struct Params { int fastRiseActivationHeight; int randomXActivationHeight; uint256 randomXInitialTarget; + + // Fixed, branch-independent scaling applied to normalized + // multi-algo chainwork after RandomX activation. + // + // A value of 1/1 preserves the current normalized-work scale. + uint32_t multiAlgoChainworkScaleNumerator{1}; + uint32_t multiAlgoChainworkScaleDenominator{1}; + int randomXSeedEpochLength; int randomXSeedLag; diff --git a/src/test/pow_tests.cpp b/src/test/pow_tests.cpp index 35dec57..d45598c 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -153,6 +153,106 @@ BOOST_AUTO_TEST_CASE(MultiAlgo_chainwork_normalization_test) +BOOST_AUTO_TEST_CASE(MultiAlgo_chainwork_scale_test) +{ + auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN); + auto consensus = chainParams->GetConsensus(); + + consensus.randomXActivationHeight = 2; + + CBlockIndex genesisLike; + genesisLike.nHeight = 0; + genesisLike.nTime = 1800000000; + genesisLike.nBits = + UintToArith256(consensus.powLimit).GetCompact(); + genesisLike.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + + CBlockIndex parent; + parent.pprev = &genesisLike; + parent.nHeight = 1; + parent.nTime = + genesisLike.nTime + consensus.nPowTargetSpacing; + parent.nBits = + UintToArith256(consensus.powLimit).GetCompact(); + parent.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + parent.lastAlgoBlocks[ALGO_SHA256D] = &genesisLike; + + CBlockIndex child; + child.pprev = &parent; + child.nHeight = 2; + child.nTime = + parent.nTime + consensus.nPowTargetSpacing; + child.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + child.nBits = + GetNextWorkRequired( + &parent, + nullptr, + consensus, + ALGO_SHA256D); + + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 1; + const arith_uint256 unscaled = + GetBlockProof(child, consensus); + + BOOST_REQUIRE(unscaled > 0); + + consensus.multiAlgoChainworkScaleNumerator = 2; + consensus.multiAlgoChainworkScaleDenominator = 1; + BOOST_CHECK( + GetBlockProof(child, consensus) == + unscaled * 2); + + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 2; + BOOST_CHECK( + GetBlockProof(child, consensus) == + unscaled / 2); + + consensus.multiAlgoChainworkScaleNumerator = 1; + consensus.multiAlgoChainworkScaleDenominator = 0; + BOOST_CHECK( + GetBlockProof(child, consensus) == 0); + + consensus.multiAlgoChainworkScaleNumerator = 0; + consensus.multiAlgoChainworkScaleDenominator = 1; + BOOST_CHECK( + GetBlockProof(child, consensus) == 0); + + // Force the overflow guard using consensus state that produces an + // extremely large normalized work value. Post-activation chainwork is + // derived from expected per-algo targets, not from child.nBits. + auto overflowConsensus = consensus; + overflowConsensus.fPowNoRetargeting = true; + overflowConsensus.randomXActivationHeight = 2; + + arith_uint256 tinyTarget(1); + const unsigned int tinyBits = tinyTarget.GetCompact(); + overflowConsensus.randomXInitialTarget = + ArithToUint256(tinyTarget); + + CBlockIndex overflowParent = parent; + overflowParent.nBits = tinyBits; + overflowParent.nVersion = + BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D; + + CBlockIndex overflowChild = child; + overflowChild.pprev = &overflowParent; + overflowChild.nHeight = 2; + + overflowConsensus.multiAlgoChainworkScaleNumerator = + std::numeric_limits::max(); + overflowConsensus.multiAlgoChainworkScaleDenominator = 1; + + BOOST_CHECK( + GetBlockProof(overflowChild, overflowConsensus) == 0); +} + + + BOOST_AUTO_TEST_CASE(MultiAlgo_chainwork_branch_accumulation_test) { auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);