Replace CTransaction::operator== with Equals that has options
What changed, and why it matters
This commit is a code cleanup: it replaces a simple transaction equality operator with a more flexible Equals() function that lets callers choose whether to compare signature and witness data. The change does not fix a known bug or vulnerability on its own. It is a refactor that makes future code easier to write correctly, especially in the wallet where transactions need to be compared ignoring signatures.
No immediate action required. Treat as routine refactor. Review future callers of Equals() to ensure EqualsOptions are chosen correctly, particularly anywhere comparing transactions for mempool, consensus, or wallet equivalence.
Security signals we found
Refactor of transaction equality semantics
Wallet IsEquivalentTo now uses explicit option to ignore scriptSig and witness data
Default Equals options preserve prior witness-hash-based equality behavior
No bounds, memory, cryptographic, or consensus bug visible in diff
Evidence from the diff
CTransaction::operator== previously compared only GetWitnessHash(). The commit introduces Equals(EqualsOptions) with flags to include/exclude scriptSig and scriptWitness comparison. Default options include both, preserving prior semantics for most callers. The wallet’s IsEquivalentTo() is simplified to use Equals() with both flags disabled, replacing a manual strip-and-compare pattern. No security bug is directly patched; the change is defensive/refactoring.
Changed components
src/primitives/transaction.hsrc/wallet/transaction.cppsrc/rpc/rawtransaction.cppsrc/ipc/test/fuzz/ipc.cppsrc/ipc/test/ipc_tests.cppsrc/test/fuzz/primitives_transaction.cppsrc/test/serialize_tests.cppsrc/test/transaction_tests.cppInspect captured patch +27 / −23
### src/ipc/test/fuzz/ipc.cpp
@@ -128,7 +128,7 @@ FUZZ_TARGET(ipc, .init = initialize_ipc)
const CMutableTransaction mutable_tx = ConsumeTransaction(fuzzed_data_provider, std::nullopt);
if (mutable_tx.vin.empty()) return;
const CTransactionRef tx = MakeTransactionRef(mutable_tx);
- assert(*ipc.m_client->passTransaction(tx) == *tx);
+ assert(ipc.m_client->passTransaction(tx)->Equals(*tx));
});
}
}
### src/ipc/test/ipc_tests.cpp
@@ -103,14 +103,14 @@ void IpcPipeTest()
mtx.vout.emplace_back(COIN, CScript());
CTransactionRef tx1{MakeTransactionRef(mtx)};
CTransactionRef tx2{foo->passTransaction(tx1)};
- BOOST_CHECK(*Assert(tx1) == *Assert(tx2));
+ BOOST_CHECK(Assert(tx1)->Equals(*Assert(tx2)));
std::vector<CTransactionRef> txs1;
txs1.push_back(tx1);
txs1.push_back(nullptr);
std::vector<CTransactionRef> txs2(foo->passTransactions(txs1));
BOOST_CHECK_EQUAL(txs2.size(), 2);
- BOOST_CHECK(*Assert(txs1[0]) == *Assert(txs2[0]));
+ BOOST_CHECK(Assert(txs1[0])->Equals(*Assert(txs2[0])));
BOOST_CHECK(!txs2[1]);
std::vector<char> vec1{'H', 'e', 'l', 'l', 'o'};
### src/primitives/transaction.h
@@ -12,6 +12,7 @@
#include <script/script.h>
#include <serialize.h>
+#include <algorithm>
#include <compare>
#include <cstddef>
#include <cstdint>
@@ -273,6 +274,11 @@ inline CAmount CalculateOutputValue(const TxType& tx)
return std::accumulate(tx.vout.cbegin(), tx.vout.cend(), CAmount{0}, [](CAmount sum, const auto& txout) { return sum + txout.nValue; });
}
+struct EqualsOptions {
+ bool include_script_sig{true};
+ bool include_witness_data{true};
+};
+
/** The basic transaction that is broadcasted on the network and contained in
* blocks. A transaction can contain multiple inputs and outputs.
@@ -343,9 +349,17 @@ class CTransaction
return (vin.size() == 1 && vin[0].prevout.IsNull());
}
- friend bool operator==(const CTransaction& a, const CTransaction& b)
+ bool Equals(const CTransaction& other, const EqualsOptions opts = {}) const
{
- return a.GetWitnessHash() == b.GetWitnessHash();
+ return nLockTime == other.nLockTime &&
+ version == other.version &&
+ vout == other.vout &&
+ std::ranges::equal(vin, other.vin, [&opts](const CTxIn& self, const CTxIn& other) {
+ return self.prevout == other.prevout &&
+ self.nSequence == other.nSequence &&
+ (opts.include_script_sig ? self.scriptSig == other.scriptSig : true) &&
+ (opts.include_witness_data ? self.scriptWitness.stack == other.scriptWitness.stack : true);
+ });
}
std::string ToString() const;
### src/rpc/rawtransaction.cpp
@@ -389,7 +389,7 @@ static RPCMethod getrawtransaction()
}
CTxUndo* undoTX {nullptr};
- auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return *t == *tx; });
+ auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return t->Equals(*tx); });
if (it != block.vtx.end()) {
// -1 as blockundo does not have coinbase tx
undoTX = &blockUndo.vtxundo.at(it - block.vtx.begin() - 1);
### src/test/fuzz/primitives_transaction.cpp
@@ -29,6 +29,6 @@ FUZZ_TARGET(primitives_transaction)
if (mutable_tx_1 && mutable_tx_2) {
const CTransaction tx_1{*mutable_tx_1};
const CTransaction tx_2{*mutable_tx_2};
- assert((tx_1 == tx_2) != (tx_1 != tx_2));
+ (void)tx_1.Equals(tx_2);
}
}
### src/test/serialize_tests.cpp
@@ -59,7 +59,7 @@ class CSerializeMethodsTestSingle
boolval == rhs.boolval &&
stringval == rhs.stringval &&
strcmp(charstrval, rhs.charstrval) == 0 &&
- *txval == *rhs.txval;
+ txval->Equals(*rhs.txval);
}
};
### src/test/transaction_tests.cpp
@@ -706,7 +706,7 @@ BOOST_AUTO_TEST_CASE(test_witness)
CheckWithFlag(output1, input1, SCRIPT_VERIFY_NONE, false);
CreateCreditAndSpend(keystore2, scriptMulti, output2, input2, false);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_NONE, false);
- BOOST_CHECK(*output1 == *output2);
+ BOOST_CHECK(output1->Equals(*output2));
UpdateInput(input1.vin[0], CombineSignatures(input1, input2, output1));
CheckWithFlag(output1, input1, STANDARD_SCRIPT_VERIFY_FLAGS, true);
@@ -717,7 +717,7 @@ BOOST_AUTO_TEST_CASE(test_witness)
CreateCreditAndSpend(keystore2, GetScriptForDestination(ScriptHash(scriptMulti)), output2, input2, false);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_NONE, true);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_P2SH, false);
- BOOST_CHECK(*output1 == *output2);
+ BOOST_CHECK(output1->Equals(*output2));
UpdateInput(input1.vin[0], CombineSignatures(input1, input2, output1));
CheckWithFlag(output1, input1, SCRIPT_VERIFY_P2SH, true);
CheckWithFlag(output1, input1, STANDARD_SCRIPT_VERIFY_FLAGS, true);
@@ -729,7 +729,7 @@ BOOST_AUTO_TEST_CASE(test_witness)
CreateCreditAndSpend(keystore2, destination_script_multi, output2, input2, false);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_NONE, true);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, false);
- BOOST_CHECK(*output1 == *output2);
+ BOOST_CHECK(output1->Equals(*output2));
UpdateInput(input1.vin[0], CombineSignatures(input1, input2, output1));
CheckWithFlag(output1, input1, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true);
CheckWithFlag(output1, input1, STANDARD_SCRIPT_VERIFY_FLAGS, true);
@@ -741,7 +741,7 @@ BOOST_AUTO_TEST_CASE(test_witness)
CreateCreditAndSpend(keystore2, GetScriptForDestination(ScriptHash(destination_script_multi)), output2, input2, false);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_P2SH, true);
CheckWithFlag(output2, input2, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, false);
- BOOST_CHECK(*output1 == *output2);
+ BOOST_CHECK(output1->Equals(*output2));
UpdateInput(input1.vin[0], CombineSignatures(input1, input2, output1));
CheckWithFlag(output1, input1, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true);
CheckWithFlag(output1, input1, STANDARD_SCRIPT_VERIFY_FLAGS, true);
### src/wallet/transaction.cpp
@@ -13,17 +13,7 @@ using interfaces::FoundBlock;
namespace wallet {
bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const
{
- CMutableTransaction tx1 {*this->GetTx()};
- CMutableTransaction tx2 {*_tx.GetTx()};
- for (auto& txin : tx1.vin) {
- txin.scriptSig = CScript();
- txin.scriptWitness.SetNull();
- }
- for (auto& txin : tx2.vin) {
- txin.scriptSig = CScript();
- txin.scriptWitness.SetNull();
- }
- return CTransaction(tx1) == CTransaction(tx2);
+ return GetTx()->Equals(*_tx.GetTx(), {.include_script_sig = false, .include_witness_data = false});
}
bool CWalletTx::InMempool() constWhy this scored 17/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.