Merge bitcoin/bitcoin#36130: test: add tests in transaction_tests.cpp covering live mutants
What changed, and why it matters
This commit only adds new test cases to Bitcoin Core. It does not change any production consensus, validation, or networking code. The tests are designed to detect accidental future code changes (called 'mutants') that could break consensus rules around transaction finality, relative locktimes, and legacy signature operation counting. Because no real bug is being fixed and no live vulnerability is present, this is a defensive hardening change with no direct security impact on its own.
No immediate action required. Treat as routine test-coverage improvement. Reviewers may optionally verify that the new tests correctly exercise the intended consensus invariants and that they pass in CI.
Security signals we found
Adds regression tests for consensus-critical functions (IsFinalTx, CalculateSequenceLocks, GetLegacySigOpCount)
Tests target known mutation-sensitive branches in src/consensus/tx_verify.cpp
No changes to src/consensus/tx_verify.cpp or any other production code
Pull request description frames the change as test coverage for live mutants, not as a vulnerability fix
Evidence from the diff
The merge commit (0f206eed51e2d00aa78f709ecc427b484d04b4d5) adds three BOOST_AUTO_TEST_CASE units to src/test/transaction_tests.cpp: getlegacysigopcount_inaccurate_test, isfinaltx_sequences_test, and calculatesequencelocks_tx_version_test. They assert that (1) GetLegacySigOpCount counts a CHECKMULTISIG as MAX_PUBKEYS_PER_MULTISIG regardless of the preceding OP_N, (2) IsFinalTx returns false unless every input has nSequence == SEQUENCE_FINAL, and (3) CalculateSequenceLocks enforces BIP68 only for tx.version >= 2. These tests pin down existing consensus behavior in src/consensus/tx_verify.cpp so that mutation-testing-style edits would be caught by CI. The production code is untouched.
Changed components
src/test/transaction_tests.cppInspect captured patch +64 / −0
### src/test/transaction_tests.cpp
@@ -6,6 +6,7 @@
#include <test/data/tx_valid.json.h>
#include <test/util/setup_common.h>
+#include <chain.h>
#include <checkqueue.h>
#include <clientversion.h>
#include <consensus/amount.h>
@@ -1128,6 +1129,22 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
}
}
+BOOST_AUTO_TEST_CASE(getlegacysigopcount_inaccurate_test)
+{
+ // Legacy sigops are counted inaccurately in both the scriptSig and the
+ // scriptPubKey: a CHECKMULTISIG counts as MAX_PUBKEYS_PER_MULTISIG even when the
+ // preceding OP_N says it takes fewer keys. Counting it accurately would
+ // undercount, letting a block over the sigop limit through.
+ const CScript multisig{CScript() << OP_1 << OP_CHECKMULTISIG};
+
+ CMutableTransaction mtx;
+ mtx.vin.emplace_back(COutPoint{}, multisig);
+ BOOST_CHECK_EQUAL(GetLegacySigOpCount(CTransaction{mtx}), MAX_PUBKEYS_PER_MULTISIG);
+
+ mtx.vout.emplace_back(0, multisig);
+ BOOST_CHECK_EQUAL(GetLegacySigOpCount(CTransaction{mtx}), 2 * MAX_PUBKEYS_PER_MULTISIG);
+}
+
BOOST_AUTO_TEST_CASE(checktxinputs_invalid_transactions_test)
{
auto check_invalid{[](CAmount input_value, CAmount output_value, bool coinbase, int spend_height, TxValidationResult expected_result, std::string_view expected_reason) {
@@ -1167,6 +1184,53 @@ BOOST_AUTO_TEST_CASE(checktxinputs_invalid_transactions_test)
TxValidationResult::TX_PREMATURE_SPEND, /*expected_reason=*/"bad-txns-premature-spend-of-coinbase");
}
+BOOST_AUTO_TEST_CASE(isfinaltx_sequences_test)
+{
+ constexpr int height{100};
+
+ // Every transaction here has the same unsatisfied nLockTime, so only the
+ // sequences decide the outcome.
+ auto check_final{[](const std::vector<uint32_t>& sequences, bool expected_final) {
+ CMutableTransaction mtx;
+ mtx.nLockTime = height;
+ for (const uint32_t sequence : sequences) {
+ mtx.vin.emplace_back(COutPoint{}, CScript{}, sequence);
+ }
+
+ BOOST_CHECK_EQUAL(IsFinalTx(CTransaction{mtx}, /*nBlockHeight=*/height, /*nBlockTime=*/0), expected_final);
+ }};
+
+ check_final(/*sequences=*/{CTxIn::SEQUENCE_FINAL, CTxIn::SEQUENCE_FINAL}, /*expected_final=*/true);
+
+ // nLockTime is only ignored when every input is SEQUENCE_FINAL
+ check_final(/*sequences=*/{CTxIn::SEQUENCE_FINAL, CTxIn::MAX_SEQUENCE_NONFINAL}, /*expected_final=*/false);
+ check_final(/*sequences=*/{CTxIn::MAX_SEQUENCE_NONFINAL, CTxIn::SEQUENCE_FINAL}, /*expected_final=*/false);
+}
+
+BOOST_AUTO_TEST_CASE(calculatesequencelocks_tx_version_test)
+{
+ constexpr int coin_height{100};
+
+ // A single input with a height-based relative locktime of one block. Only the
+ // height branch is taken, so the block index is never dereferenced.
+ auto check_min_height{[](uint32_t version, int expected_min_height) {
+ CMutableTransaction mtx;
+ mtx.version = version;
+ mtx.vin.emplace_back(COutPoint{}, CScript{}, /*nSequenceIn=*/1);
+
+ std::vector<int> prev_heights{coin_height};
+ const CBlockIndex block{};
+ const auto lock_pair{CalculateSequenceLocks(CTransaction{mtx}, LOCKTIME_VERIFY_SEQUENCE, prev_heights, block)};
+ BOOST_CHECK_EQUAL(lock_pair.first, expected_min_height);
+ }};
+
+ // BIP68 only applies to versions 2 and up
+ check_min_height(/*version=*/0, /*expected_min_height=*/-1);
+ check_min_height(/*version=*/1, /*expected_min_height=*/-1);
+ check_min_height(/*version=*/2, /*expected_min_height=*/coin_height);
+ check_min_height(/*version=*/std::numeric_limits<uint32_t>::max(), /*expected_min_height=*/coin_height);
+}
+
BOOST_AUTO_TEST_CASE(getvalueout_out_of_range_throws)
{
CMutableTransaction mtx;Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.