Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions src/test/bls_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,23 +636,6 @@ BOOST_AUTO_TEST_CASE(test_get_hash_consistency)
BOOST_CHECK(hash1 == hash2);
}

namespace {
struct TestChainV19Setup : public TestChainSetup {
TestChainV19Setup() :
TestChainSetup(494, CBaseChainParams::REGTEST,
{"-testactivationheight=v19@500", "-testactivationheight=v20@500", "-testactivationheight=mn_rr@500"})
{
const CScript coinbase_pk = GetScriptForRawPubKey(coinbaseKey.GetPubKey());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should you clean up any included header after removing this TestChainV19Setup?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch. Removed the now-unused <chainparams.h> include in b57f55f. The remaining boundary test still directly needs <deploymentstatus.h> and <script/standard.h>.

// Mine up to the block just before V19 activation
for (int i = 0; i < 5; ++i) {
CreateAndProcessBlock({}, coinbase_pk);
}
assert(DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(), Consensus::DEPLOYMENT_V19) &&
!DeploymentActiveAt(*m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(), Consensus::DEPLOYMENT_V19));
}
};
} // namespace

BOOST_AUTO_TEST_CASE(v19_boundary_validation_failure_restores_bls_scheme)
{
TestChainV19Setup setup;
Expand Down
124 changes: 68 additions & 56 deletions src/test/evo_deterministicmns_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <test/util/setup_common.h>

#include <chainlock/handler.h>
#include <chainparams.h>
#include <clientversion.h>
#include <consensus/merkle.h>
Expand All @@ -15,9 +16,11 @@
#include <evo/specialtx.h>
#include <evo/specialtxman.h>
#include <llmq/context.h>
#include <messagesigner.h>
#include <masternode/payments.h>
#include <mempool_args.h>
#include <messagesigner.h>
#include <netbase.h>
#include <node/miner.h>
#include <node/transaction.h>
#include <policy/policy.h>
#include <pow.h>
Expand Down Expand Up @@ -811,20 +814,53 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup)
return std::nullopt;
};

// Mine until a block's (pre-operator) masternode reward is even: only then does the 50%
// operator share equal the single owner share, so the owner and operator outputs -- both
// paying shared_script -- collide into two identical coinbase outputs. The reward is constant
// within a subsidy/reallocation epoch, so this can take up to a full epoch; intermediate
// blocks are processed to advance the chain.
auto mine_until_duplicate = [&]() -> std::pair<CBlock, CTxOut> {
for (int i = 0; i < 2000; ++i) {
CBlock good = setup.CreateBlock({}, coinbase_pk, chainman.ActiveChainstate());
if (auto dup = find_duplicate(good)) return {good, *dup};
BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared<CBlock>(good), /*force_processing=*/true, nullptr));
sync_dmn_tip();
// At this height the subsidy-only Evo masternode reward is odd. Derive the smallest fee that
// makes it even, then let BlockAssembler distribute that fee so the 50/50 owner and operator
// outputs collide immediately. A fresh fee transaction is needed for each sibling pair.
auto create_duplicate_block = [&]() -> std::pair<CBlock, CTxOut> {
const CBlockIndex* tip = tip_index();
const MnRewardEra era = GetMnRewardEraAfter(tip, chainman);
BOOST_REQUIRE(era == MnRewardEra::EvoReward);
const CAmount block_subsidy = GetBlockSubsidyInner(tip->nBits, tip->nHeight, consensus, /*fV20Active=*/true);
const CAmount platform_reward = PlatformShare(GetMasternodePayment(tip->nHeight + 1, block_subsidy, consensus, era));
auto payee_reward = [&](CAmount fee) {
return GetMasternodePayment(tip->nHeight + 1, block_subsidy + fee, consensus, era) - platform_reward;
};
BOOST_REQUIRE((payee_reward(/*fee=*/0) & 1) != 0);
CAmount fee{1};
while ((payee_reward(fee) & 1) != 0)
++fee;
BOOST_REQUIRE_EQUAL(fee, 1);

CMutableTransaction fee_tx;
WITH_LOCK(::cs_main,
FundTransaction(chainman.ActiveChain(), fee_tx, utxos, coinbase_pk, 1 * COIN, setup.coinbaseKey));
BOOST_REQUIRE(fee_tx.vout.back().nValue > fee);
fee_tx.vout.back().nValue -= fee;
SignTransaction(*setup.m_node.mempool, fee_tx, setup.coinbaseKey);

auto& mempool = *setup.m_node.mempool;
// Bypass fee policy while preserving the miner's InstantSend safety check.
constexpr int64_t ISLOCK_SAFETY_DELAY{601};
setup.m_node.clhandler->UpdateTxFirstSeenMap({fee_tx.GetHash()}, tip->GetMedianTimePast() - ISLOCK_SAFETY_DELAY);
{
LOCK2(::cs_main, mempool.cs);
mempool.addUnchecked(TestMemPoolEntryHelper{}.Fee(fee).SpendsCoinbase(true).FromTx(fee_tx));
}
BOOST_REQUIRE_MESSAGE(false, "expected owner/operator collision to yield a duplicate coinbase output");
return {}; // unreachable, BOOST_REQUIRE above aborts the test

node::BlockAssembler::Options options;
options.blockMinFeeRate = CFeeRate{0};
CBlock good = node::BlockAssembler(chainman.ActiveChainstate(), setup.m_node, &mempool, options)
.CreateNewBlock(coinbase_pk)
->block;
BOOST_REQUIRE(
std::ranges::any_of(good.vtx, [&](const CTransactionRef& tx) { return tx->GetHash() == fee_tx.GetHash(); }));
const auto dup = find_duplicate(good);
BOOST_REQUIRE(dup.has_value());
good.hashMerkleRoot = BlockMerkleRoot(good);
while (!CheckProofOfWork(good.GetHash(), good.nBits, consensus))
++good.nNonce;
return {good, *dup};
};

// Build the "merge cheat" sibling of `good`: drop one of the two identical outputs and fold
Expand Down Expand Up @@ -860,7 +896,7 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup)

// ---- Pre-v24: legacy existence-only matching ACCEPTS the merge cheat (masternode underpaid).
{
const auto [good, dup] = mine_until_duplicate();
const auto [good, dup] = create_duplicate_block();
// The collision must be reached while v24 is still pending, otherwise this phase would be
// testing post-v24 behaviour by accident.
BOOST_REQUIRE(!DeploymentActiveAfter(tip_index(), chainman, Consensus::DEPLOYMENT_V24));
Expand All @@ -880,7 +916,7 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup)
// ---- Post-v24: strict multiplicity matching REJECTS the same merge (two expected outputs, one
// distinct actual). Rejection is observed as the tip not advancing.
{
const auto [good, dup] = mine_until_duplicate();
const auto [good, dup] = create_duplicate_block();
const CBlock merged = build_merge_cheat(good, dup);
const uint256 tip_before = tip_hash();
chainman.ProcessNewBlock(std::make_shared<CBlock>(merged), /*force_processing=*/true, nullptr);
Expand Down Expand Up @@ -1506,9 +1542,16 @@ static void SmlCache(TestChainSetup& setup)

BOOST_AUTO_TEST_SUITE(evo_dip3_activation_tests)

// FuncDIP3Protx registers six masternodes in successive blocks. Height 109 is the lowest boundary
// that keeps two mature coinbases available for every 1000 DASH collateral; height 108 runs out on
// the sixth registration.
constexpr int DIP3_ACTIVATION_HEIGHT{109};

struct TestChainDIP3BeforeActivationSetup : public TestChainSetup {
TestChainDIP3BeforeActivationSetup() :
TestChainSetup(430)
TestChainSetup(DIP3_ACTIVATION_HEIGHT - 2, CBaseChainParams::REGTEST, {"-dip3params=109:500"},
/*coins_db_in_memory=*/true, /*block_tree_db_in_memory=*/true,
uint256S("0x40233e79ab24bc7c3e5686ac2b63915e15e1b1deecc3d0919f7ec32a9433fdfb"))
{
}
};
Expand All @@ -1521,39 +1564,13 @@ struct TestChainDIP3Setup : public TestChainDIP3BeforeActivationSetup {
}
};

struct TestChainV19BeforeActivationSetup : public TestChainSetup {
TestChainV19BeforeActivationSetup();
};

struct TestChainV19Setup : public TestChainV19BeforeActivationSetup {
TestChainV19Setup()
{
const CScript coinbase_pk = GetScriptForRawPubKey(coinbaseKey.GetPubKey());
// Activate V19
for (int i = 0; i < 5; ++i) {
CreateAndProcessBlock({}, coinbase_pk);
}
bool v19_just_activated{WITH_LOCK(::cs_main, return DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(), Consensus::DEPLOYMENT_V19) &&
!DeploymentActiveAt(*m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(), Consensus::DEPLOYMENT_V19))};
assert(v19_just_activated);
}
};

// 5 blocks earlier
TestChainV19BeforeActivationSetup::TestChainV19BeforeActivationSetup() :
TestChainSetup(494, CBaseChainParams::REGTEST, {"-testactivationheight=v19@500", "-testactivationheight=v20@500", "-testactivationheight=mn_rr@500"})
{
bool v19_active{WITH_LOCK(::cs_main, return DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(),
Consensus::DEPLOYMENT_V19))};
assert(!v19_active);
}

struct TestChainV24SignalBeforeV19Setup : public TestChainSetup {
TestChainV24SignalBeforeV19Setup() :
TestChainSetup(494, CBaseChainParams::REGTEST,
{"-testactivationheight=v19@500", "-testactivationheight=v20@500",
"-testactivationheight=mn_rr@500",
"-vbparams=v24:0:9999999999:0:500:400:300:5:0"})
"-testactivationheight=mn_rr@500", "-vbparams=v24:0:9999999999:510:1:1:1:5:0"},
/*coins_db_in_memory=*/true, /*block_tree_db_in_memory=*/true,
uint256S("0x083fa179797ea7e5893198ff1b6eab632526c2eeb6f0ca6c42fcac9cb9bad366"))
{
assert(WITH_LOCK(::cs_main, return !DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(),
Consensus::DEPLOYMENT_V19)));
Expand All @@ -1562,16 +1579,11 @@ struct TestChainV24SignalBeforeV19Setup : public TestChainSetup {
}
};

// v19/v20/mn_rr active at height 500, with v24 signalled but still PENDING (not yet locked in).
// The 500-block window means v24 locks in well after v19, so this fixture stops in the window
// where basic-BLS (v2) registrations are valid but strict multiplicity matching is off, letting
// a single test cross the v24 activation boundary.
struct TestChainV24PendingSetup : public TestChainSetup {
TestChainV24PendingSetup() :
TestChainSetup(494, CBaseChainParams::REGTEST,
{"-testactivationheight=v19@500", "-testactivationheight=v20@500",
"-testactivationheight=mn_rr@500",
"-vbparams=v24:0:9999999999:0:500:400:300:5:0"})
// Advance the shared v24 chain just far enough that v19/v20/mn_rr are active while v24 remains
// held in LOCKED_IN by its minimum activation height. The boundary test needs this short state:
// basic-BLS registrations are valid, but strict payment multiplicity is not active yet.
struct TestChainV24PendingSetup : public TestChainV24SignalBeforeV19Setup {
TestChainV24PendingSetup()
{
const CScript coinbase_pk = GetScriptForRawPubKey(coinbaseKey.GetPubKey());
auto& chainman = *Assert(m_node.chainman);
Expand Down
49 changes: 39 additions & 10 deletions src/test/util/setup_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,10 @@ TestChain100Setup::TestChain100Setup(
{
}

TestChainSetup::TestChainSetup(
int num_blocks,
const std::string& chain_name,
const std::vector<const char*>& extra_args,
const bool coins_db_in_memory,
const bool block_tree_db_in_memory)
: TestingSetup{chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory}
TestChainSetup::TestChainSetup(int num_blocks, const std::string& chain_name,
const std::vector<const char*>& extra_args, const bool coins_db_in_memory,
const bool block_tree_db_in_memory, const std::optional<uint256>& expected_tip_hash) :
TestingSetup{chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory}
{
SetMockTime(1598887952);
constexpr std::array<unsigned char, 32> vchKey = {
Expand Down Expand Up @@ -492,10 +489,42 @@ TestChainSetup::TestChainSetup(
{
LOCK(::cs_main);
auto hash = checkpoints.mapCheckpoints.find(num_blocks);
assert(
hash != checkpoints.mapCheckpoints.end() &&
m_node.chainman->ActiveChain().Tip()->GetBlockHash() == hash->second);
const uint256 actual_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't do new workarounds here; add a new checkpoint the the CCheckpointData checkpoints above

@thepastaclaw thepastaclaw Jul 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b57f55f. I removed the expected_tip_hash constructor workaround and moved each configuration-specific hash into the existing CCheckpointData checkpoints table.

assert(expected_tip_hash.has_value() || hash != checkpoints.mapCheckpoints.end());
const uint256& expected_hash = expected_tip_hash.has_value() ? *expected_tip_hash : hash->second;
assert(actual_hash == expected_hash);
}
}

namespace {
// This is the lowest activation height that leaves enough mature pre-mined coinbases for all
// consumers of the shared fixture. The DIP3 prerequisite is active before the v19 boundary work.
constexpr int V19_ACTIVATION_HEIGHT{109};
} // namespace

TestChainV19BeforeActivationSetup::TestChainV19BeforeActivationSetup() :
TestChainSetup{V19_ACTIVATION_HEIGHT - 6,
CBaseChainParams::REGTEST,
{"-dip3params=100:500", "-testactivationheight=v19@109", "-testactivationheight=v20@109",
"-testactivationheight=mn_rr@109"},
/*coins_db_in_memory=*/true,
/*block_tree_db_in_memory=*/true,
uint256S("0x13adad9565d0ca558f5675c50e3828f4354d26b64de044ebc88686056f30faab")}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add this checkpoint to the list of checkpoints ; inside CCheckpointData checkpoints

@thepastaclaw thepastaclaw Jul 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b57f55f. Height 103 is now registered in CCheckpointData checkpoints; the other hashes that used the same removed override are registered there as well.

{
assert(WITH_LOCK(::cs_main, return !DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(),
Consensus::DEPLOYMENT_V19)));
}

TestChainV19Setup::TestChainV19Setup()
{
const CScript coinbase_pk = GetScriptForRawPubKey(coinbaseKey.GetPubKey());
for (int i = 0; i < 5; ++i) {
CreateAndProcessBlock({}, coinbase_pk);
}
assert(WITH_LOCK(::cs_main, return DeploymentActiveAfter(m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(),
Consensus::DEPLOYMENT_V19) &&
!DeploymentActiveAt(*m_node.chainman->ActiveChain().Tip(), m_node.chainman->GetConsensus(),
Consensus::DEPLOYMENT_V19)));
}

void TestChainSetup::mineBlocks(int num_blocks)
Expand Down
21 changes: 16 additions & 5 deletions src/test/util/setup_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <util/vector.h>

#include <functional>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <vector>
Expand Down Expand Up @@ -143,11 +144,11 @@ class CScript;

struct TestChainSetup : public TestingSetup
{
TestChainSetup(int num_blocks,
const std::string& chain_name = CBaseChainParams::REGTEST,
const std::vector<const char*>& extra_args = {},
const bool coins_db_in_memory = true,
const bool block_tree_db_in_memory = true);
/** @param expected_tip_hash Configuration-specific checkpoint overriding the height default. */
TestChainSetup(int num_blocks, const std::string& chain_name = CBaseChainParams::REGTEST,
const std::vector<const char*>& extra_args = {}, const bool coins_db_in_memory = true,
const bool block_tree_db_in_memory = true,
const std::optional<uint256>& expected_tip_hash = std::nullopt);
~TestChainSetup();

/**
Expand Down Expand Up @@ -217,6 +218,16 @@ struct TestChainSetup : public TestingSetup
CKey coinbaseKey; // private/public key needed to spend coinbase transactions
};

/** Test chain stopped five blocks before v19 activation at height 109. */
struct TestChainV19BeforeActivationSetup : public TestChainSetup {
TestChainV19BeforeActivationSetup();
};

/** Test chain whose next block activates v19 at height 109. */
struct TestChainV19Setup : public TestChainV19BeforeActivationSetup {
TestChainV19Setup();
};

/**
* Testing fixture that pre-creates a 100-block REGTEST-mode block chain
*/
Expand Down
Loading