Normalize multi-algo chainwork and bootstrap RandomX difficulty

This commit is contained in:
Capri 2026-09-15 18:16:29 +00:00
commit 287a84a533
Signed by: VextaProject
SSH key fingerprint: SHA256:739BZv8LvIDY2tR7lXLy/IQ6MaGTQtYcG9GNvCjmpVM
8 changed files with 173 additions and 8 deletions

View file

@ -174,10 +174,77 @@ arith_uint256 GetBlockProofBase(const CBlockIndex& block)
arith_uint256 GetBlockProof(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); 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, int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& from,
const CBlockIndex& tip, const Consensus::Params& params) 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; r = from.nChainWork - to.nChainWork;
sign = -1; sign = -1;
} }
r = r * arith_uint256(params.nPowTargetSpacing) / GetBlockProof(tip); r = r * arith_uint256(params.nPowTargetSpacing) / GetBlockProof(tip, params);
if (r.bits() > 63) { if (r.bits() > 63) {
return sign * std::numeric_limits<int64_t>::max(); return sign * std::numeric_limits<int64_t>::max();
} }

View file

@ -336,6 +336,9 @@ public:
}; };
arith_uint256 GetBlockProof(const CBlockIndex& block); 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. */ /** 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&); int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& from, const CBlockIndex& tip, const Consensus::Params&);

View file

@ -184,6 +184,7 @@ public:
consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000; consensus.fastRiseActivationHeight = 7000;
consensus.randomXActivationHeight = std::numeric_limits<int>::max(); consensus.randomXActivationHeight = std::numeric_limits<int>::max();
consensus.randomXInitialTarget = uint256S("000000359bc10000000000000000000000000000000000000000000000000000");
consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedEpochLength = 2048;
consensus.randomXSeedLag = 64; consensus.randomXSeedLag = 64;
@ -229,6 +230,7 @@ public:
consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000; consensus.fastRiseActivationHeight = 7000;
consensus.randomXActivationHeight = std::numeric_limits<int>::max(); consensus.randomXActivationHeight = std::numeric_limits<int>::max();
consensus.randomXInitialTarget = consensus.powLimit;
consensus.randomXSeedEpochLength = 2048; consensus.randomXSeedEpochLength = 2048;
consensus.randomXSeedLag = 64; consensus.randomXSeedLag = 64;
@ -386,6 +388,7 @@ public:
consensus.MinBIP9WarningHeight = 0; consensus.MinBIP9WarningHeight = 0;
consensus.powLimit = uint256S("00000377ae000000000000000000000000000000000000000000000000000000"); consensus.powLimit = uint256S("00000377ae000000000000000000000000000000000000000000000000000000");
consensus.randomXInitialTarget = consensus.powLimit;
consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 27; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 27;
consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
@ -470,6 +473,7 @@ public:
consensus.asertHalfLife = 2 * 24 * 60 * 60; consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000; consensus.fastRiseActivationHeight = 7000;
consensus.randomXActivationHeight = std::numeric_limits<int>::max(); consensus.randomXActivationHeight = std::numeric_limits<int>::max();
consensus.randomXInitialTarget = consensus.powLimit;
consensus.randomXSeedEpochLength = 64; consensus.randomXSeedEpochLength = 64;
consensus.randomXSeedLag = 8; consensus.randomXSeedLag = 8;

View file

@ -125,6 +125,7 @@ struct Params {
int64_t asertHalfLife; int64_t asertHalfLife;
int fastRiseActivationHeight; int fastRiseActivationHeight;
int randomXActivationHeight; int randomXActivationHeight;
uint256 randomXInitialTarget;
int randomXSeedEpochLength; int randomXSeedEpochLength;
int randomXSeedLag; int randomXSeedLag;

View file

@ -69,6 +69,11 @@ unsigned int InitialDifficulty(const Consensus::Params& params)
return PowLimit(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) unsigned int CalculateASERT(const CBlockIndex* pindexLast, const Consensus::Params& params)
{ {
// Anchor ASERT to the last block before activation. // Anchor ASERT to the last block before activation.
@ -205,7 +210,9 @@ static unsigned int CalculateMultiAlgoWorkRequired(
GetLastBlockIndexForAlgoFast(pindexLast, algo); GetLastBlockIndexForAlgoFast(pindexLast, algo);
if (pindexPrevAlgo == nullptr) { if (pindexPrevAlgo == nullptr) {
return InitialDifficulty(params); return algo == ALGO_RANDOMX
? RandomXInitialDifficulty(params)
: InitialDifficulty(params);
} }
if (params.fPowNoRetargeting || params.fEasyPow) { if (params.fPowNoRetargeting || params.fEasyPow) {
@ -318,7 +325,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead
} }
if (algo == ALGO_RANDOMX) { if (algo == ALGO_RANDOMX) {
return InitialDifficulty(params); return RandomXInitialDifficulty(params);
} }
// Existing SHA256D difficulty logic below remains unchanged. // Existing SHA256D difficulty logic below remains unchanged.

View file

@ -16,6 +16,7 @@ class CBlockIndex;
class uint256; class uint256;
unsigned int InitialDifficulty(const Consensus::Params& params); 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); 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 */ /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */

View file

@ -70,6 +70,88 @@ BOOST_AUTO_TEST_CASE(CheckProofOfWork_test_zero_target)
BOOST_CHECK(!CheckProofOfWork(hash, nBits, consensus)); 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<CBlockIndex> 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) BOOST_AUTO_TEST_CASE(GetBlockProofEquivalentTime_test)
{ {
const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN); 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( BOOST_CHECK_EQUAL(
firstRandomXBits, firstRandomXBits,
InitialDifficulty(consensus)); RandomXInitialDifficulty(consensus));
} }

View file

@ -1337,7 +1337,7 @@ void CChainState::CheckForkWarningConditions()
return; 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__); LogPrintf("%s: Warning: Found invalid chain at least ~6 blocks longer than our best chain.\nChain state database corruption likely.\n", __func__);
SetfLargeWorkInvalidChainFound(true); SetfLargeWorkInvalidChainFound(true);
} else { } else {
@ -3069,7 +3069,7 @@ CBlockIndex* BlockManager::AddToBlockIndex(
} }
pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->nTime) : pindexNew->nTime); 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); pindexNew->RaiseValidity(BLOCK_VALID_TREE);
if (pindexBestHeader == nullptr || pindexBestHeader->nChainWork < pindexNew->nChainWork) if (pindexBestHeader == nullptr || pindexBestHeader->nChainWork < pindexNew->nChainWork)
pindexBestHeader = pindexNew; pindexBestHeader = pindexNew;
@ -3959,7 +3959,7 @@ bool BlockManager::LoadBlockIndex(
} }
nHeight = pindex-> nHeight; 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); 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. // We can link the chain of blocks for which we've received transactions at some point.
// Pruned nodes may have deleted the block. // Pruned nodes may have deleted the block.