Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
63 changes: 48 additions & 15 deletions src/masternode/payments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@
#include <ranges>
#include <string>

int FindUnmatchedMasternodePayment(const std::vector<CTxOut>& expected,
const std::vector<CTxOut>& 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<int>(i);
}
}
return -1;
}

std::vector<bool> 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<int>(i);
}
return -1;
}

CAmount PlatformShare(const CAmount reward)
{
const CAmount platformReward = reward * 375 / 1000;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
}
Comment thread
thepastaclaw marked this conversation as resolved.
Expand Down Expand Up @@ -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)) {

@knst knst Jul 7, 2026

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.

consider changing strict_multiplicity == true to check_superblock == SuperBlockCheckType::DisallowDuplicates for CMNPaymentsProcessor::IsBlockPayeeValid to reduce amount of arguments.

They are semantically the same meaning.

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.

I think we need to keep these separate. check_superblock == DisallowDuplicates also encodes whether superblock validation is being run at all: NoCheck is used for chainlocked blocks / during sync, even when v24 is active. IsBlockPayeeValid() still validates the masternode payee before the later NoCheck return, so deriving strict multiplicity from check_superblock would disable the new v24 masternode-payment multiplicity rule in those skip-superblock cases. The extra bool is a little noisier, but it preserves the deployment-state distinction from the superblock-check mode.

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 */
Expand Down
23 changes: 21 additions & 2 deletions src/masternode/payments.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CTxOut>& expected,
const std::vector<CTxOut>& actual,
bool strict_multiplicity);

struct CMutableTransaction;

namespace governance {
Expand Down Expand Up @@ -65,7 +84,7 @@ class CMNPaymentsProcessor
[[nodiscard]] bool GetMasternodeTxOuts(const CBlockIndex* pindexPrev, const CAmount blockSubsidy, const CAmount feeReward,
MnRewardEra era, std::vector<CTxOut>& 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:
Expand All @@ -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<CTxOut>& voutMasternodePaymentsRet, std::vector<CTxOut>& voutSuperblockPaymentsRet);
};
Expand Down
Loading
Loading