From 5c001efa74854ddfccd0a0f0321b4706f6f64c2d Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 3 Aug 2026 13:49:13 -0500 Subject: [PATCH 1/2] refactor(utils): let FundTransaction return change to a separate script FundTransaction always paid the change back to the payout script, which only works when that script is spendable. A test funding a governance proposal fee has to burn the amount to an OP_RETURN, so the change needs its own destination. --- src/test/util/masternode.cpp | 8 +++++++- src/test/util/masternode.h | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/util/masternode.cpp b/src/test/util/masternode.cpp index a40ad701d641..0551591a2f94 100644 --- a/src/test/util/masternode.cpp +++ b/src/test/util/masternode.cpp @@ -66,6 +66,12 @@ SimpleUTXOMap BuildSimpleUtxoMap(const std::vector& txs) SimpleUTXOMap FundTransaction(const ChainstateManager& chainman, CMutableTransaction& tx, SimpleUTXOMap& utxos, const CScript& script_payout, CAmount amount) +{ + return FundTransaction(chainman, tx, utxos, script_payout, amount, /*script_change=*/script_payout); +} + +SimpleUTXOMap FundTransaction(const ChainstateManager& chainman, CMutableTransaction& tx, SimpleUTXOMap& utxos, + const CScript& script_payout, CAmount amount, const CScript& script_change) { CAmount change; auto inputs = WITH_LOCK(::cs_main, return SelectUTXOs(chainman.ActiveChain(), utxos, amount, change)); @@ -74,7 +80,7 @@ SimpleUTXOMap FundTransaction(const ChainstateManager& chainman, CMutableTransac } tx.vout.emplace_back(amount, script_payout); if (change != 0) { - tx.vout.emplace_back(change, script_payout); + tx.vout.emplace_back(change, script_change); } return inputs; } diff --git a/src/test/util/masternode.h b/src/test/util/masternode.h index 0a894e157a2c..63b715ae3d5a 100644 --- a/src/test/util/masternode.h +++ b/src/test/util/masternode.h @@ -20,6 +20,9 @@ using SimpleUTXOMap = std::map; SimpleUTXOMap BuildSimpleUtxoMap(const std::vector& txs); SimpleUTXOMap FundTransaction(const ChainstateManager& chainman, CMutableTransaction& tx, SimpleUTXOMap& utxos, const CScript& script_payout, CAmount amount); +//! Overload for payout scripts that cannot receive the change, e.g. an OP_RETURN burn. +SimpleUTXOMap FundTransaction(const ChainstateManager& chainman, CMutableTransaction& tx, SimpleUTXOMap& utxos, + const CScript& script_payout, CAmount amount, const CScript& script_change); void SignTransaction(CMutableTransaction& tx, const SimpleUTXOMap& coins, const CKey& coinbase_key); CMutableTransaction CreateProRegTx(const ChainstateManager& chainman, SimpleUTXOMap& utxos, int port, const CScript& script_payout, const CKey& coinbase_key, CKey& owner_key_ret, From 6a31b03f68746ca8e5a073fefcbd480619d76a14 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 17:07:34 -0500 Subject: [PATCH 2/2] test: cover the governance vote signature path with a chain-backed fixture The existing governance unit tests run on a fixture with no chain, so the tip masternode list is empty and every CGovernanceVote::IsValid() call short-circuits at GetMNByCollateral before any signature is verified. Nothing exercised CheckSignature, and nothing proved that a legitimately signed vote is accepted at all. Add a fixture that mines a regtest chain, registers a masternode via a real ProRegTx and keeps its voting (ECDSA) and operator (BLS) keys, so votes can be signed for real. On top of it: an orphan vote (parent object unknown) from a registered masternode is cached, requested and replayed onto the object once its fee collateral confirms and the proposal arrives; forged voting-key (ECDSA) and operator-key (BLS) signatures, unknown-masternode and future-dated votes are rejected with a peer penalty; and funding votes on a proposal are accepted only from the voting key while other signals accept the operator key. The orphan replay leaves the vote unindexed in cmapVoteToObject (so the inv relayed during replay cannot be served); the test pins that known gap so a future fix has to update it. Verified by mutation: inverting a masternode/signature gate in front of the orphan cache fails the orphan test, and making CheckSignature always succeed fails the rejection tests. --- src/Makefile.test.include | 1 + src/test/governance_vote_processing_tests.cpp | 384 ++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 src/test/governance_vote_processing_tests.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 44ef47bd7999..70443412e772 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -124,6 +124,7 @@ BITCOIN_TESTS =\ test/governance_inv_tests.cpp \ test/governance_superblock_tests.cpp \ test/governance_validators_tests.cpp \ + test/governance_vote_processing_tests.cpp \ test/governance_vote_sync_tests.cpp \ test/governance_vote_wire_tests.cpp \ test/coinjoin_inouts_tests.cpp \ diff --git a/src/test/governance_vote_processing_tests.cpp b/src/test/governance_vote_processing_tests.cpp new file mode 100644 index 000000000000..77e2705e8ae7 --- /dev/null +++ b/src/test/governance_vote_processing_tests.cpp @@ -0,0 +1,384 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include