Add Fast-Rise difficulty protection

This commit is contained in:
Capri 2026-09-09 20:38:21 +00:00
commit f8a8daaae5
Signed by: VextaProject
SSH key fingerprint: SHA256:739BZv8LvIDY2tR7lXLy/IQ6MaGTQtYcG9GNvCjmpVM
4 changed files with 245 additions and 1 deletions

View file

@ -182,6 +182,7 @@ public:
consensus.difficultyMaxActualTimespan = consensus.difficultyTargetTimespan * 116 / 100;
consensus.asertActivationHeight = 6500;
consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000;
@ -223,6 +224,7 @@ public:
consensus.difficultyMaxActualTimespan = consensus.difficultyTargetTimespan * 116 / 100;
consensus.asertActivationHeight = 6500;
consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000;
@ -457,6 +459,7 @@ public:
consensus.difficultyMaxActualTimespan = consensus.difficultyTargetTimespan * 116 / 100;
consensus.asertActivationHeight = 6500;
consensus.asertHalfLife = 2 * 24 * 60 * 60;
consensus.fastRiseActivationHeight = 7000;
consensus.BIP65Height = 1351;

View file

@ -123,6 +123,7 @@ struct Params {
int64_t difficultyMaxActualTimespan;
int asertActivationHeight;
int64_t asertHalfLife;
int fastRiseActivationHeight;

View file

@ -84,6 +84,67 @@ unsigned int CalculateASERT(const CBlockIndex* pindexLast, const Consensus::Para
return target.GetCompact();
}
static bool ShouldTriggerFastRise(const CBlockIndex* pindexLast, const Consensus::Params& params)
{
// Trigger when at least 3 of the last 4 solve intervals are below
// 20% of the target spacing (120 seconds on Vexta).
// Zero or negative raw intervals also count as fast to prevent
// backward/equal timestamp manipulation from bypassing the trigger.
const int64_t fastThreshold = params.nPowTargetSpacing / 5;
int fastIntervals = 0;
const CBlockIndex* pindex = pindexLast;
for (int i = 0; i < 4; ++i) {
if (pindex == nullptr || pindex->pprev == nullptr) {
return false;
}
const int64_t solveTime =
pindex->GetBlockTime() - pindex->pprev->GetBlockTime();
if (solveTime < fastThreshold) {
++fastIntervals;
}
pindex = pindex->pprev;
}
return fastIntervals >= 3;
}
static unsigned int ApplyFastRiseProtection(
const CBlockIndex* pindexLast,
unsigned int asertBits,
const Consensus::Params& params)
{
if (pindexLast->nHeight + 1 < params.fastRiseActivationHeight) {
return asertBits;
}
if (!ShouldTriggerFastRise(pindexLast, params)) {
return asertBits;
}
arith_uint256 asertTarget;
asertTarget.SetCompact(asertBits);
arith_uint256 fastTarget;
fastTarget.SetCompact(pindexLast->nBits);
fastTarget >>= 1;
if (fastTarget == 0) {
fastTarget = 1;
}
// Never weaken standard ASERT. Under a Fast-Rise trigger, require
// at least one 2x difficulty step relative to the previous block.
if (asertTarget < fastTarget) {
return asertBits;
}
return fastTarget.GetCompact();
}
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params)
{
// Vexta is SHA256D-only.
@ -97,7 +158,8 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead
if (!params.fPowNoRetargeting &&
!params.fEasyPow &&
pindexLast->nHeight + 1 >= params.asertActivationHeight) {
return CalculateASERT(pindexLast, params);
const unsigned int asertBits = CalculateASERT(pindexLast, params);
return ApplyFastRiseProtection(pindexLast, asertBits, params);
}
const CBlockIndex* pindexFirst = pindexLast;

View file

@ -351,4 +351,182 @@ BOOST_AUTO_TEST_CASE(ASERT_reference_vector_run06_test)
}
}
BOOST_AUTO_TEST_CASE(FastRise_activation_and_trigger_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
auto consensus = chainParams->GetConsensus();
consensus.asertActivationHeight = 6500;
consensus.fastRiseActivationHeight = 7000;
std::vector<CBlockIndex> blocks(7001);
arith_uint256 baseTarget = UintToArith256(consensus.powLimit);
baseTarget >>= 8;
const unsigned int baseBits = baseTarget.GetCompact();
const int64_t startTime = 1800000000;
// Build an otherwise perfectly spaced chain.
for (int i = 0; i <= 7000; ++i) {
blocks[i].pprev = i ? &blocks[i - 1] : nullptr;
blocks[i].nHeight = i;
blocks[i].nTime = startTime + i * consensus.nPowTargetSpacing;
blocks[i].nBits = baseBits;
}
// Two fast intervals out of the last four must NOT trigger Fast-Rise.
blocks[6995].nTime = startTime + 6995 * consensus.nPowTargetSpacing;
blocks[6996].nTime = blocks[6995].nTime + 60;
blocks[6997].nTime = blocks[6996].nTime + 60;
blocks[6998].nTime = blocks[6997].nTime + 600;
blocks[6999].nTime = blocks[6998].nTime + 600;
auto baselineConsensus = consensus;
baselineConsensus.fastRiseActivationHeight = 1000000;
const unsigned int twoFastBaseline =
GetNextWorkRequired(&blocks[6999], nullptr, baselineConsensus);
const unsigned int twoFastResult =
GetNextWorkRequired(&blocks[6999], nullptr, consensus);
BOOST_CHECK_EQUAL(twoFastResult, twoFastBaseline);
// Three fast intervals out of the last four MUST trigger Fast-Rise.
blocks[6998].nTime = blocks[6997].nTime + 60;
blocks[6999].nTime = blocks[6998].nTime + 600;
const unsigned int threeFastBaseline =
GetNextWorkRequired(&blocks[6999], nullptr, baselineConsensus);
const unsigned int threeFastResult =
GetNextWorkRequired(&blocks[6999], nullptr, consensus);
arith_uint256 expectedFastTarget;
expectedFastTarget.SetCompact(blocks[6999].nBits);
expectedFastTarget >>= 1;
BOOST_CHECK_NE(threeFastResult, threeFastBaseline);
BOOST_CHECK_EQUAL(threeFastResult, expectedFastTarget.GetCompact());
// The same pattern before activation must still behave exactly like ASERT.
consensus.fastRiseActivationHeight = 7001;
const unsigned int preActivationResult =
GetNextWorkRequired(&blocks[6999], nullptr, consensus);
BOOST_CHECK_EQUAL(preActivationResult, threeFastBaseline);
}
BOOST_AUTO_TEST_CASE(FastRise_returns_to_ASERT_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
auto consensus = chainParams->GetConsensus();
consensus.asertActivationHeight = 6500;
consensus.fastRiseActivationHeight = 7000;
std::vector<CBlockIndex> blocks(7002);
arith_uint256 baseTarget = UintToArith256(consensus.powLimit);
baseTarget >>= 8;
const unsigned int baseBits = baseTarget.GetCompact();
const int64_t startTime = 1800000000;
for (int i = 0; i <= 7001; ++i) {
blocks[i].pprev = i ? &blocks[i - 1] : nullptr;
blocks[i].nHeight = i;
blocks[i].nTime = startTime + i * consensus.nPowTargetSpacing;
blocks[i].nBits = baseBits;
}
// Create a 3-of-4 fast pattern before block 7000.
blocks[6995].nTime = startTime + 6995 * consensus.nPowTargetSpacing;
blocks[6996].nTime = blocks[6995].nTime + 60;
blocks[6997].nTime = blocks[6996].nTime + 60;
blocks[6998].nTime = blocks[6997].nTime + 60;
blocks[6999].nTime = blocks[6998].nTime + 600;
const unsigned int triggeredBits =
GetNextWorkRequired(&blocks[6999], nullptr, consensus);
arith_uint256 triggeredTarget;
triggeredTarget.SetCompact(triggeredBits);
arith_uint256 expectedTriggeredTarget;
expectedTriggeredTarget.SetCompact(blocks[6999].nBits);
expectedTriggeredTarget >>= 1;
BOOST_CHECK_EQUAL(triggeredTarget.GetCompact(), expectedTriggeredTarget.GetCompact());
// Simulate block 7000 being mined at the Fast-Rise target.
blocks[7000].nBits = triggeredBits;
blocks[7000].nTime = blocks[6999].nTime + 600;
// The last four intervals are now 60, 60, 600, 600:
// only two are fast, so Fast-Rise must stop immediately.
auto baselineConsensus = consensus;
baselineConsensus.fastRiseActivationHeight = 1000000;
const unsigned int expectedASERT =
GetNextWorkRequired(&blocks[7000], nullptr, baselineConsensus);
const unsigned int actual =
GetNextWorkRequired(&blocks[7000], nullptr, consensus);
BOOST_CHECK_EQUAL(actual, expectedASERT);
}
BOOST_AUTO_TEST_CASE(FastRise_zero_and_negative_intervals_test)
{
auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
auto consensus = chainParams->GetConsensus();
consensus.asertActivationHeight = 6500;
consensus.fastRiseActivationHeight = 7000;
std::vector<CBlockIndex> blocks(7001);
arith_uint256 baseTarget = UintToArith256(consensus.powLimit);
baseTarget >>= 8;
const unsigned int baseBits = baseTarget.GetCompact();
const int64_t startTime = 1800000000;
for (int i = 0; i <= 7000; ++i) {
blocks[i].pprev = i ? &blocks[i - 1] : nullptr;
blocks[i].nHeight = i;
blocks[i].nTime = startTime + i * consensus.nPowTargetSpacing;
blocks[i].nBits = baseBits;
}
// Last four raw intervals:
// 60, 0, -30, 600
// Zero and negative intervals must count as fast.
blocks[6995].nTime = startTime + 6995 * consensus.nPowTargetSpacing;
blocks[6996].nTime = blocks[6995].nTime + 60;
blocks[6997].nTime = blocks[6996].nTime;
blocks[6998].nTime = blocks[6997].nTime - 30;
blocks[6999].nTime = blocks[6998].nTime + 600;
auto baselineConsensus = consensus;
baselineConsensus.fastRiseActivationHeight = 1000000;
const unsigned int baseline =
GetNextWorkRequired(&blocks[6999], nullptr, baselineConsensus);
const unsigned int actual =
GetNextWorkRequired(&blocks[6999], nullptr, consensus);
arith_uint256 expectedFastTarget;
expectedFastTarget.SetCompact(blocks[6999].nBits);
expectedFastTarget >>= 1;
BOOST_CHECK_NE(actual, baseline);
BOOST_CHECK_EQUAL(actual, expectedFastTarget.GetCompact());
}
BOOST_AUTO_TEST_SUITE_END()