Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ BITCOIN_TESTS =\
test/flatfile_tests.cpp \
test/fs_tests.cpp \
test/getarg_tests.cpp \
test/governance_superblock_tests.cpp \
test/governance_validators_tests.cpp \
test/coinjoin_inouts_tests.cpp \
test/coinjoin_dstxmanager_tests.cpp \
Expand Down
7 changes: 5 additions & 2 deletions src/governance/superblock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ bool CSuperblock::IsValid(const CChain& active_chain, const CTransaction& txNew,
return false;
}

int nVoutIndex = 0;
int nVoutIndex = -1;
for (int i = 0; i < nPayments; i++) {
CGovernancePayment payment;
if (!GetPayment(i, payment)) {
Expand All @@ -303,7 +303,10 @@ bool CSuperblock::IsValid(const CChain& active_chain, const CTransaction& txNew,

bool fPaymentMatch = false;

for (int j = nVoutIndex; j < nOutputs; j++) {
// Start past the previously matched output so each expected payment
// consumes a distinct vout (two adjacent payments with the same script
// and amount must match two separate outputs, not the same one twice).
for (int j = nVoutIndex + 1; j < nOutputs; j++) {
// Find superblock payment
fPaymentMatch = ((payment.script == txNew.vout[j].scriptPubKey) &&
(payment.nAmount == txNew.vout[j].nValue));
Expand Down
83 changes: 83 additions & 0 deletions src/test/governance_superblock_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2026 The Dash Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <chain.h>
#include <chainparams.h>
#include <consensus/amount.h>
#include <governance/superblock.h>
#include <key.h>
#include <primitives/transaction.h>
#include <pubkey.h>
#include <script/script.h>
#include <script/standard.h>
#include <uint256.h>

#include <test/util/setup_common.h>

#include <boost/test/unit_test.hpp>

#include <vector>

struct SuperblockRegtestSetup : public BasicTestingSetup {
SuperblockRegtestSetup() : BasicTestingSetup(CBaseChainParams::REGTEST) {}
};

BOOST_FIXTURE_TEST_SUITE(governance_superblock_tests, SuperblockRegtestSetup)

// Regression test for: CSuperblock::IsValid was matching expected payments
// against coinbase outputs using a forward scan that re-started at the
// previously matched index, which allowed two adjacent expected payments
// with identical scriptPubKey and amount to both match the same coinbase
// output. Each expected payment must consume a distinct output.
BOOST_AUTO_TEST_CASE(isvalid_duplicate_payments_require_distinct_outputs)
{
const auto& consensus = Params().GetConsensus();
const int nBlockHeight = consensus.nSuperblockStartBlock + consensus.nSuperblockCycle;
BOOST_REQUIRE(CSuperblock::IsValidBlockHeight(nBlockHeight));

CKey key;
key.MakeNewKey(/*fCompressed=*/true);
const CTxDestination dest{PKHash(key.GetPubKey())};
const CScript scriptPayee = GetScriptForDestination(dest);
const CAmount nPayAmount = 1 * COIN;

// Two identical expected payments (same script, same amount).
std::vector<CGovernancePayment> payments;
payments.emplace_back(dest, nPayAmount, /*proposalHash=*/uint256());
payments.emplace_back(dest, nPayAmount, /*proposalHash=*/uint256::ONE);
BOOST_REQUIRE(payments[0].IsValid());
BOOST_REQUIRE(payments[1].IsValid());

CSuperblock sb(nBlockHeight, payments);
BOOST_REQUIRE_EQUAL(sb.CountPayments(), 2);

const CScript scriptMinerOrMN = CScript() << OP_RETURN;
const CAmount blockReward = 500 * COIN;
CChain dummy_chain;

// Case 1 (regression): coinbase carries only ONE output matching the
// duplicate expected payment. With the buggy forward scan that restarted
// at the previously matched index, both expected payments would match the
// single matching vout and IsValid would (incorrectly) return true.
// After the fix, the second expected payment must find a distinct output
// and validation must fail.
{
CMutableTransaction txNew;
txNew.vout.emplace_back(blockReward - nPayAmount, scriptMinerOrMN);
txNew.vout.emplace_back(nPayAmount, scriptPayee); // single matching output
BOOST_CHECK(!sb.IsValid(dummy_chain, CTransaction(txNew), nBlockHeight, blockReward));
}

// Case 2: coinbase carries TWO outputs matching the duplicate expected
// payments. The fix must still accept this legitimate case.
{
CMutableTransaction txNew;
txNew.vout.emplace_back(blockReward - 2 * nPayAmount, scriptMinerOrMN);
txNew.vout.emplace_back(nPayAmount, scriptPayee);
txNew.vout.emplace_back(nPayAmount, scriptPayee);
BOOST_CHECK(sb.IsValid(dummy_chain, CTransaction(txNew), nBlockHeight, blockReward));
}
}

BOOST_AUTO_TEST_SUITE_END()
Loading