Add symmetric SHA256D RandomX difficulty adjustment

This commit is contained in:
Capri 2026-09-15 11:23:25 +00:00
commit 89600821d1
Signed by: VextaProject
SSH key fingerprint: SHA256:739BZv8LvIDY2tR7lXLy/IQ6MaGTQtYcG9GNvCjmpVM
2 changed files with 289 additions and 10 deletions

View file

@ -192,22 +192,132 @@ static unsigned int ApplyFastRiseProtection(
return fastTarget.GetCompact();
}
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params, int algo)
static unsigned int CalculateMultiAlgoWorkRequired(
const CBlockIndex* pindexLast,
const Consensus::Params& params,
int algo)
{
if (algo == ALGO_RANDOMX) {
if (pindexLast == nullptr) {
return InitialDifficulty(params);
}
const CBlockIndex* pindexPrevAlgo =
GetLastBlockIndexForAlgoFast(pindexLast, ALGO_RANDOMX);
GetLastBlockIndexForAlgoFast(pindexLast, algo);
if (pindexPrevAlgo == nullptr) {
return InitialDifficulty(params);
}
// RandomX will get its own per-algo retarget logic here.
// For now, preserve the last RandomX difficulty.
if (params.fPowNoRetargeting || params.fEasyPow) {
return pindexPrevAlgo->nBits;
}
if (algo != ALGO_SHA256D) {
// With two algorithms and a 10-minute global block target, each
// algorithm is expected to appear approximately once every 20 minutes.
// Use 10 samples per algorithm => 20 global blocks.
const int64_t averagingBlocks =
params.difficultyAveragingWindow * NUM_ALGOS;
const CBlockIndex* pindexFirst = pindexLast;
for (int64_t i = 0; pindexFirst && i < averagingBlocks; ++i) {
pindexFirst = pindexFirst->pprev;
}
if (pindexFirst == nullptr) {
return pindexPrevAlgo->nBits;
}
const int64_t targetTimespan =
averagingBlocks * params.nPowTargetSpacing;
int64_t actualTimespan =
pindexLast->GetMedianTimePast() -
pindexFirst->GetMedianTimePast();
// Preserve the existing Vexta 1/4 damping.
actualTimespan =
targetTimespan + (actualTimespan - targetTimespan) / 4;
// Preserve the existing 92% / 116% adjustment bounds, scaled to
// the longer multi-algo averaging window.
const int64_t minTimespan =
targetTimespan * params.difficultyMinActualTimespan /
params.difficultyTargetTimespan;
const int64_t maxTimespan =
targetTimespan * params.difficultyMaxActualTimespan /
params.difficultyTargetTimespan;
if (actualTimespan < minTimespan) {
actualTimespan = minTimespan;
}
if (actualTimespan > maxTimespan) {
actualTimespan = maxTimespan;
}
arith_uint256 bnNew;
bnNew.SetCompact(pindexPrevAlgo->nBits);
// Global chain-speed adjustment.
bnNew *= actualTimespan;
bnNew /= targetTimespan;
// Local per-algo balancing.
//
// For two algorithms:
// ideal next algo: adjustment = 0
// same algo twice: adjustment = +1 => 4% harder
// algo skipped once: adjustment = -1 => 4% easier
const int nAdjustments =
pindexPrevAlgo->nHeight + NUM_ALGOS - 1 - pindexLast->nHeight;
static constexpr int64_t LOCAL_TARGET_ADJUSTMENT = 4;
if (nAdjustments > 0) {
for (int i = 0; i < nAdjustments; ++i) {
bnNew *= 100;
bnNew /= 100 + LOCAL_TARGET_ADJUSTMENT;
}
} else if (nAdjustments < 0) {
for (int i = 0; i < -nAdjustments; ++i) {
bnNew *= 100 + LOCAL_TARGET_ADJUSTMENT;
bnNew /= 100;
if (bnNew > UintToArith256(params.powLimit)) {
bnNew = UintToArith256(params.powLimit);
break;
}
}
}
if (bnNew == 0) {
bnNew = 1;
}
if (bnNew > UintToArith256(params.powLimit)) {
bnNew = UintToArith256(params.powLimit);
}
return bnNew.GetCompact();
}
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params, int algo)
{
if (algo != ALGO_SHA256D && algo != ALGO_RANDOMX) {
return InitialDifficulty(params);
}
// Before RandomX activation, preserve the existing SHA256D consensus
// exactly. RandomX remains unavailable through validation.
//
// From activation onward both algorithms use the same symmetric
// two-algorithm DAA.
if (pindexLast != nullptr &&
pindexLast->nHeight + 1 >= params.randomXActivationHeight) {
return CalculateMultiAlgoWorkRequired(pindexLast, params, algo);
}
if (algo == ALGO_RANDOMX) {
return InitialDifficulty(params);
}

View file

@ -532,6 +532,175 @@ BOOST_AUTO_TEST_CASE(FastRise_zero_and_negative_intervals_test)
BOOST_AUTO_TEST_CASE(MultiAlgo_DAA_activation_boundary_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
auto consensus = chainParams->GetConsensus();
consensus.randomXActivationHeight = 40;
std::vector<CBlockIndex> blocks(40);
arith_uint256 baseTarget = UintToArith256(consensus.powLimit);
baseTarget >>= 8;
const unsigned int baseBits = baseTarget.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 = baseBits;
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];
}
// Height 39 -> next block is height 40, exactly the multi-algo
// activation boundary.
//
// SHA256D is being requested directly after another SHA256D block,
// so the local balancing rule must make it exactly one 4% step harder.
const unsigned int firstMultiAlgoShaBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_SHA256D);
arith_uint256 compactBaseTarget;
compactBaseTarget.SetCompact(baseBits);
arith_uint256 expectedShaTarget = compactBaseTarget;
expectedShaTarget *= 100;
expectedShaTarget /= 104;
BOOST_CHECK_EQUAL(
firstMultiAlgoShaBits,
expectedShaTarget.GetCompact());
// No RandomX block exists yet. For now this deliberately documents
// the current fallback to InitialDifficulty(). This is NOT the final
// RandomX launch calibration and will be replaced by an explicit
// RandomX initial target before activation is enabled.
const unsigned int firstRandomXBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_RANDOMX);
BOOST_CHECK_EQUAL(
firstRandomXBits,
InitialDifficulty(consensus));
}
BOOST_AUTO_TEST_CASE(MultiAlgo_DAA_local_balance_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
auto consensus = chainParams->GetConsensus();
// Test-only activation. Mainnet RandomX remains disabled in chainparams.
consensus.randomXActivationHeight = 20;
std::vector<CBlockIndex> blocks(40);
arith_uint256 baseTarget = UintToArith256(consensus.powLimit);
baseTarget >>= 8;
const unsigned int baseBits = baseTarget.GetCompact();
const int64_t startTime = 1800000000;
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 = baseBits;
// Before activation the chain is SHA256D-only.
// From activation onward, build an ideal alternating sequence.
if (i >= consensus.randomXActivationHeight && (i & 1)) {
blocks[i].nVersion =
BLOCK_VERSION_DEFAULT | BLOCK_VERSION_RANDOMX;
} else {
blocks[i].nVersion =
BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D;
}
}
auto rebuildAlgoLinks = [&]() {
CBlockIndex* latest[NUM_ALGOS_IMPL]{};
for (auto& block : blocks) {
for (int algo = 0; algo < NUM_ALGOS_IMPL; ++algo) {
block.lastAlgoBlocks[algo] = latest[algo];
}
const int algo = block.GetAlgo();
BOOST_REQUIRE(algo >= 0);
BOOST_REQUIRE(algo < NUM_ALGOS_IMPL);
latest[algo] = &block;
}
};
rebuildAlgoLinks();
// Height 39 is RandomX. Therefore SHA256D is the ideal next algo.
// With perfect 10-minute global spacing its target must stay unchanged.
const unsigned int balancedShaBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_SHA256D);
BOOST_CHECK_EQUAL(balancedShaBits, baseBits);
// RandomX was just mined at height 39. Mining RandomX again immediately
// must make that algo 4% harder.
const unsigned int repeatedRandomXBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_RANDOMX);
arith_uint256 compactBaseTarget;
compactBaseTarget.SetCompact(baseBits);
arith_uint256 expectedRepeatedRandomXTarget = compactBaseTarget;
expectedRepeatedRandomXTarget *= 100;
expectedRepeatedRandomXTarget /= 104;
BOOST_CHECK_EQUAL(
repeatedRandomXBits,
expectedRepeatedRandomXTarget.GetCompact());
// Now simulate SHA256D also being mined at height 39, directly after
// SHA256D at height 38. SHA must become harder while the missing RandomX
// side becomes easier.
blocks[39].nVersion =
BLOCK_VERSION_DEFAULT | BLOCK_VERSION_SHA256D;
rebuildAlgoLinks();
const unsigned int repeatedShaBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_SHA256D);
const unsigned int missingRandomXBits =
GetNextWorkRequired(&blocks[39], nullptr, consensus, ALGO_RANDOMX);
arith_uint256 expectedRepeatedShaTarget = compactBaseTarget;
expectedRepeatedShaTarget *= 100;
expectedRepeatedShaTarget /= 104;
arith_uint256 expectedMissingRandomXTarget = compactBaseTarget;
expectedMissingRandomXTarget *= 104;
expectedMissingRandomXTarget /= 100;
BOOST_CHECK_EQUAL(
repeatedShaBits,
expectedRepeatedShaTarget.GetCompact());
BOOST_CHECK_EQUAL(
missingRandomXBits,
expectedMissingRandomXTarget.GetCompact());
}
BOOST_AUTO_TEST_CASE(RandomX_seed_height_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);