diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 08beaf7d2599..e2f1782e5d42 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -142,6 +142,7 @@ BITCOIN_TESTS =\ test/llmq_snapshot_tests.cpp \ test/llmq_utils_tests.cpp \ test/logging_tests.cpp \ + test/masternode_payments_tests.cpp \ test/dbwrapper_tests.cpp \ test/validation_tests.cpp \ test/mempool_tests.cpp \ diff --git a/src/masternode/payments.cpp b/src/masternode/payments.cpp index f3f090e80942..030b67a64a30 100644 --- a/src/masternode/payments.cpp +++ b/src/masternode/payments.cpp @@ -21,6 +21,36 @@ #include #include +int FindUnmatchedMasternodePayment(const std::vector& expected, + const std::vector& actual, + bool strict_multiplicity) +{ + if (!strict_multiplicity) { + for (size_t i = 0; i < expected.size(); ++i) { + const auto& txout = expected[i]; + if (!std::ranges::any_of(actual, [&txout](const auto& txout2) { return txout == txout2; })) { + return static_cast(i); + } + } + return -1; + } + + std::vector consumed(actual.size(), false); + for (size_t i = 0; i < expected.size(); ++i) { + const auto& txout = expected[i]; + bool found = false; + for (size_t j = 0; j < actual.size(); ++j) { + if (!consumed[j] && actual[j] == txout) { + consumed[j] = true; + found = true; + break; + } + } + if (!found) return static_cast(i); + } + return -1; +} + CAmount PlatformShare(const CAmount reward) { const CAmount platformReward = reward * 375 / 1000; @@ -184,7 +214,7 @@ CAmount GetMasternodePayment(int nHeight, CAmount blockValue, const Consensus::P } [[nodiscard]] bool CMNPaymentsProcessor::IsTransactionValid(const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, - const CAmount feeReward, MnRewardEra era) + const CAmount feeReward, MnRewardEra era, bool strict_multiplicity) { const int nBlockHeight = pindexPrev == nullptr ? 0 : pindexPrev->nHeight + 1; if (!DeploymentDIP0003Enforced(nBlockHeight, m_consensus_params)) { @@ -198,19 +228,22 @@ CAmount GetMasternodePayment(int nHeight, CAmount blockValue, const Consensus::P return true; } - for (const auto& txout : voutMasternodePayments) { - bool found = std::ranges::any_of(txNew.vout, [&txout](const auto& txout2) { return txout == txout2; }); - if (!found) { - std::string str_payout; - if (CTxDestination dest; ExtractDestination(txout.scriptPubKey, dest)) { - str_payout = "address=" + EncodeDestination(dest); - } else { - str_payout = "scriptPubKey=" + HexStr(txout.scriptPubKey); - } - LogPrintf("CMNPaymentsProcessor::%s -- ERROR! Failed to find expected payee %s amount=%lld height=%d\n", - __func__, str_payout, txout.nValue, nBlockHeight); - return false; + // With strict multiplicity (v24 active, computed by the caller) each expected payment must be + // matched by a distinct coinbase output: duplicate expected outputs require duplicate coinbase + // outputs. Pre-v24 retains the legacy existence-only check to avoid tightening historical + // validation. + const int unmatched_idx = FindUnmatchedMasternodePayment(voutMasternodePayments, txNew.vout, strict_multiplicity); + if (unmatched_idx >= 0) { + const auto& txout = voutMasternodePayments[unmatched_idx]; + std::string str_payout; + if (CTxDestination dest; ExtractDestination(txout.scriptPubKey, dest)) { + str_payout = "address=" + EncodeDestination(dest); + } else { + str_payout = "scriptPubKey=" + HexStr(txout.scriptPubKey); } + LogPrintf("CMNPaymentsProcessor::%s -- ERROR! Failed to find expected payee %s amount=%lld height=%d\n", + __func__, str_payout, txout.nValue, nBlockHeight); + return false; } return true; } @@ -339,12 +372,12 @@ bool CMNPaymentsProcessor::IsBlockValueValid(const CChain& active_chain, const C return true; } -bool CMNPaymentsProcessor::IsBlockPayeeValid(const CChain& active_chain, const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, SuperBlockCheckType check_superblock) +bool CMNPaymentsProcessor::IsBlockPayeeValid(const CChain& active_chain, const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, bool strict_multiplicity, SuperBlockCheckType check_superblock) { const int nBlockHeight = pindexPrev == nullptr ? 0 : pindexPrev->nHeight + 1; // Check for correct masternode payment - if (IsTransactionValid(txNew, pindexPrev, blockSubsidy, feeReward, era)) { + if (IsTransactionValid(txNew, pindexPrev, blockSubsidy, feeReward, era, strict_multiplicity)) { LogPrint(BCLog::MNPAYMENTS, "CMNPaymentsProcessor::%s -- Valid masternode payment at height %d: %s", __func__, nBlockHeight, txNew.ToString()); /* Continued */ } else { LogPrintf("CMNPaymentsProcessor::%s -- ERROR! Invalid masternode payment detected at height %d: %s", __func__, nBlockHeight, txNew.ToString()); /* Continued */ diff --git a/src/masternode/payments.h b/src/masternode/payments.h index c1c06079aefa..661f8488a7a8 100644 --- a/src/masternode/payments.h +++ b/src/masternode/payments.h @@ -17,6 +17,25 @@ class CDeterministicMNManager; class CTransaction; class CTxOut; +/** + * Match the list of expected masternode payment outputs against the coinbase + * outputs. + * + * When @p strict_multiplicity is true, every expected output must be matched + * by a distinct actual output (multiplicity-correct matching): two identical + * expected outputs require two identical actual outputs. + * + * When false, the legacy behaviour is used where each expected output only + * has to appear at least once in the actual outputs. This is preserved for + * pre-v24 historical validation. + * + * @return -1 if every expected output is matched, otherwise the index in + * @p expected of the first output that could not be matched. + */ +int FindUnmatchedMasternodePayment(const std::vector& expected, + const std::vector& actual, + bool strict_multiplicity); + struct CMutableTransaction; namespace governance { @@ -65,7 +84,7 @@ class CMNPaymentsProcessor [[nodiscard]] bool GetMasternodeTxOuts(const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, std::vector& voutMasternodePaymentsRet); [[nodiscard]] bool IsTransactionValid(const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, - const CAmount feeReward, MnRewardEra era); + const CAmount feeReward, MnRewardEra era, bool strict_multiplicity); [[nodiscard]] bool IsOldBudgetBlockValueValid(const CBlock& block, const int nBlockHeight, const CAmount blockReward, std::string& strErrorRet); public: @@ -78,7 +97,7 @@ class CMNPaymentsProcessor } bool IsBlockValueValid(const CChain& active_chain, const CBlock& block, const CBlockIndex* pindexPrev, const CAmount blockReward, std::string& strErrorRet, SuperBlockCheckType check_superblock); - bool IsBlockPayeeValid(const CChain& active_chain, const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, SuperBlockCheckType check_superblock); + bool IsBlockPayeeValid(const CChain& active_chain, const CTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, bool strict_multiplicity, SuperBlockCheckType check_superblock); void FillBlockPayments(CMutableTransaction& txNew, const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward, MnRewardEra era, std::vector& voutMasternodePaymentsRet, std::vector& voutSuperblockPaymentsRet); }; diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index e8c40cf17d53..fa821d8a5674 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include