test: validate duplicate detection in CheckTransaction

The `CheckTransaction` validation function in https://github.com/bitcoin/bitcoin/blob/master/src/consensus/tx_check.cpp#L41-L45 relies on a correct ordering relation for detecting duplicate transaction inputs.

This update to the tests ensures that:
* Accurate detection of duplicates: Beyond trivial cases (e.g., two identical inputs), duplicates are detected correctly in more complex scenarios.
* Consistency across methods: Both sorted sets and hash-based sets behave identically when detecting duplicates for `COutPoint` and related values.
* Robust ordering and equality relations: The function maintains expected behavior for ordering and equality checks.

Using randomized testing with shuffled inputs (to avoid any remaining bias introduced), the enhanced test validates that `CheckTransaction` remains robust and reliable across various input configurations. It confirms identical behavior to a hashing-based duplicate detection mechanism, ensuring consistency and correctness.

To make sure the new branches in the follow-up commits will be covered, `basic_transaction_tests` was extended a randomized test one comparing against the old implementation (and also an alternative duplicate). The iterations and ranges were chosen such that every new branch is expected to be hit once.
This commit is contained in:
Lőrinc 2025-01-18 15:37:08 +01:00
parent c072498305
commit b07cdbe542
3 changed files with 217 additions and 9 deletions

View File

@ -406,20 +406,110 @@ BOOST_AUTO_TEST_CASE(tx_oversized)
}
}
BOOST_AUTO_TEST_CASE(basic_transaction_tests)
static CMutableTransaction CreateTransaction()
{
// Random real transaction (e2769b09e784f32f62ef849763d4f45b98e07ba658647343b915ff832b110436)
unsigned char ch[] = {0x01, 0x00, 0x00, 0x00, 0x01, 0x6b, 0xff, 0x7f, 0xcd, 0x4f, 0x85, 0x65, 0xef, 0x40, 0x6d, 0xd5, 0xd6, 0x3d, 0x4f, 0xf9, 0x4f, 0x31, 0x8f, 0xe8, 0x20, 0x27, 0xfd, 0x4d, 0xc4, 0x51, 0xb0, 0x44, 0x74, 0x01, 0x9f, 0x74, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x49, 0x30, 0x46, 0x02, 0x21, 0x00, 0xda, 0x0d, 0xc6, 0xae, 0xce, 0xfe, 0x1e, 0x06, 0xef, 0xdf, 0x05, 0x77, 0x37, 0x57, 0xde, 0xb1, 0x68, 0x82, 0x09, 0x30, 0xe3, 0xb0, 0xd0, 0x3f, 0x46, 0xf5, 0xfc, 0xf1, 0x50, 0xbf, 0x99, 0x0c, 0x02, 0x21, 0x00, 0xd2, 0x5b, 0x5c, 0x87, 0x04, 0x00, 0x76, 0xe4, 0xf2, 0x53, 0xf8, 0x26, 0x2e, 0x76, 0x3e, 0x2d, 0xd5, 0x1e, 0x7f, 0xf0, 0xbe, 0x15, 0x77, 0x27, 0xc4, 0xbc, 0x42, 0x80, 0x7f, 0x17, 0xbd, 0x39, 0x01, 0x41, 0x04, 0xe6, 0xc2, 0x6e, 0xf6, 0x7d, 0xc6, 0x10, 0xd2, 0xcd, 0x19, 0x24, 0x84, 0x78, 0x9a, 0x6c, 0xf9, 0xae, 0xa9, 0x93, 0x0b, 0x94, 0x4b, 0x7e, 0x2d, 0xb5, 0x34, 0x2b, 0x9d, 0x9e, 0x5b, 0x9f, 0xf7, 0x9a, 0xff, 0x9a, 0x2e, 0xe1, 0x97, 0x8d, 0xd7, 0xfd, 0x01, 0xdf, 0xc5, 0x22, 0xee, 0x02, 0x28, 0x3d, 0x3b, 0x06, 0xa9, 0xd0, 0x3a, 0xcf, 0x80, 0x96, 0x96, 0x8d, 0x7d, 0xbb, 0x0f, 0x91, 0x78, 0xff, 0xff, 0xff, 0xff, 0x02, 0x8b, 0xa7, 0x94, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x19, 0x76, 0xa9, 0x14, 0xba, 0xde, 0xec, 0xfd, 0xef, 0x05, 0x07, 0x24, 0x7f, 0xc8, 0xf7, 0x42, 0x41, 0xd7, 0x3b, 0xc0, 0x39, 0x97, 0x2d, 0x7b, 0x88, 0xac, 0x40, 0x94, 0xa8, 0x02, 0x00, 0x00, 0x00, 0x00, 0x19, 0x76, 0xa9, 0x14, 0xc1, 0x09, 0x32, 0x48, 0x3f, 0xec, 0x93, 0xed, 0x51, 0xf5, 0xfe, 0x95, 0xe7, 0x25, 0x59, 0xf2, 0xcc, 0x70, 0x43, 0xf9, 0x88, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<unsigned char> vch(ch, ch + sizeof(ch) -1);
DataStream stream(vch);
// Serialized random real transaction (e2769b09e784f32f62ef849763d4f45b98e07ba658647343b915ff832b110436)
static constexpr auto ser_tx{"01000000016bff7fcd4f8565ef406dd5d63d4ff94f318fe82027fd4dc451b04474019f74b4000000008c493046022100da0dc6aecefe1e06efdf05773757deb168820930e3b0d03f46f5fcf150bf990c022100d25b5c87040076e4f253f8262e763e2dd51e7ff0be157727c4bc42807f17bd39014104e6c26ef67dc610d2cd192484789a6cf9aea9930b944b7e2db5342b9d9e5b9ff79aff9a2ee1978dd7fd01dfc522ee02283d3b06a9d03acf8096968d7dbb0f9178ffffffff028ba7940e000000001976a914badeecfdef0507247fc8f74241d73bc039972d7b88ac4094a802000000001976a914c10932483fec93ed51f5fe95e72559f2cc7043f988ac0000000000"_hex};
CMutableTransaction tx;
stream >> TX_WITH_WITNESS(tx);
DataStream(ser_tx) >> TX_WITH_WITNESS(tx);
return tx;
}
BOOST_AUTO_TEST_CASE(transaction_duplicate_input_test)
{
auto tx{CreateTransaction()};
TxValidationState state;
BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), state) && state.IsValid(), "Simple deserialized transaction should be valid.");
// Check that duplicate txins fail
tx.vin.push_back(tx.vin[0]);
BOOST_CHECK_MESSAGE(!CheckTransaction(CTransaction(tx), state) || !state.IsValid(), "Transaction with duplicate txins should be invalid.");
// Add duplicate input
tx.vin.emplace_back(tx.vin[0]);
std::ranges::shuffle(tx.vin, m_rng);
BOOST_CHECK_MESSAGE(!CheckTransaction(CTransaction(tx), state) || !state.IsValid(), "Transaction with 2 duplicate txins should be invalid.");
// ... add a valid input for more complex check
tx.vin.emplace_back(COutPoint(Txid::FromUint256(uint256{1}), 1));
std::ranges::shuffle(tx.vin, m_rng);
BOOST_CHECK_MESSAGE(!CheckTransaction(CTransaction(tx), state) || !state.IsValid(), "Transaction with 3 inputs (2 valid, 1 duplicate) should be invalid.");
}
BOOST_AUTO_TEST_CASE(transaction_duplicate_detection_test)
{
// Randomized testing against hash- and tree-based duplicate check
auto reference_duplicate_check_hash{[](const std::vector<CTxIn>& vin) {
std::unordered_set<COutPoint, SaltedOutpointHasher> vInOutPoints;
for (const auto& txin : vin) {
if (!vInOutPoints.insert(txin.prevout).second) {
return false;
}
}
return true;
}};
auto reference_duplicate_check_tree{[](const std::vector<CTxIn>& vin) {
std::set<COutPoint> vInOutPoints;
for (const auto& txin : vin) {
if (!vInOutPoints.insert(txin.prevout).second) {
return false;
}
}
return true;
}};
std::vector<Txid> hashes;
std::vector<uint32_t> ns;
for (int i = 0; i < 10; ++i) {
hashes.emplace_back(Txid::FromUint256(m_rng.rand256()));
ns.emplace_back(m_rng.rand32());
}
auto tx{CreateTransaction()};
TxValidationState state;
for (int i{0}; i < 100; ++i) {
if (m_rng.randbool()) {
tx.vin.clear();
}
for (int j{0}, num_inputs{1 + m_rng.randrange(5)}; j < num_inputs; ++j) {
if (COutPoint outpoint(hashes[m_rng.randrange(hashes.size())], ns[m_rng.randrange(ns.size())]); !outpoint.IsNull()) {
tx.vin.emplace_back(outpoint);
}
}
std::ranges::shuffle(tx.vin, m_rng);
bool actual{CheckTransaction(CTransaction(tx), state)};
BOOST_CHECK_EQUAL(actual, reference_duplicate_check_hash(tx.vin));
BOOST_CHECK_EQUAL(actual, reference_duplicate_check_tree(tx.vin));
}
}
BOOST_AUTO_TEST_CASE(transaction_null_prevout_detection_test)
{
// Randomized testing against linear null prevout check
auto reference_null_prevout_check_hash{[](const std::vector<CTxIn>& vin) {
for (const auto& txin : vin) {
if (txin.prevout.IsNull()) {
return false;
}
}
return true;
}};
auto tx{CreateTransaction()};
TxValidationState state;
for (int i{0}; i < 100; ++i) {
if (m_rng.randbool()) {
tx.vin.clear();
}
for (int j{0}, num_inputs{1 + m_rng.randrange(5)}; j < num_inputs; ++j) {
switch (m_rng.randrange(5)) {
case 0: tx.vin.emplace_back(COutPoint()); break; // Null prevout
case 1: tx.vin.emplace_back(Txid::FromUint256(uint256::ZERO), m_rng.rand32()); break; // Null hash, random index
case 2: tx.vin.emplace_back(Txid::FromUint256(m_rng.rand256()), COutPoint::NULL_INDEX); break; // Random hash, Null index
default: tx.vin.emplace_back(Txid::FromUint256(m_rng.rand256()), m_rng.rand32()); // Random prevout
}
}
std::ranges::shuffle(tx.vin, m_rng);
BOOST_CHECK_EQUAL(CheckTransaction(CTransaction(tx), state), reference_null_prevout_check_hash(tx.vin));
}
}
BOOST_AUTO_TEST_CASE(test_Get)
@ -1048,4 +1138,116 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
CheckIsNotStandard(t, "dust");
}
BOOST_AUTO_TEST_CASE(test_uint256_sorting)
{
// Sorting
std::vector original{
uint256{1},
uint256{2},
uint256{3}
};
std::vector shuffled{original};
std::ranges::shuffle(shuffled, m_rng);
std::sort(shuffled.begin(), shuffled.end());
BOOST_CHECK_EQUAL_COLLECTIONS(original.begin(), original.end(), shuffled.begin(), shuffled.end());
// Operators
constexpr auto a{uint256{1}},
b{uint256{2}},
c{uint256{3}};
BOOST_CHECK(a == a);
BOOST_CHECK(a == uint256{1});
BOOST_CHECK(b == b);
BOOST_CHECK(c == c);
BOOST_CHECK(a != b);
BOOST_CHECK(a != uint256{10});
BOOST_CHECK(a != c);
BOOST_CHECK(b != c);
BOOST_CHECK(a < b);
BOOST_CHECK(a < uint256{10});
BOOST_CHECK(b < c);
BOOST_CHECK(a < c);
}
BOOST_AUTO_TEST_CASE(test_transaction_identifier_sorting)
{
std::vector original{
Txid::FromUint256(uint256{1}),
Txid::FromUint256(uint256{2}),
Txid::FromUint256(uint256{3})
};
std::vector shuffled{original};
std::ranges::shuffle(shuffled, m_rng);
std::sort(shuffled.begin(), shuffled.end());
BOOST_CHECK_EQUAL_COLLECTIONS(original.begin(), original.end(), shuffled.begin(), shuffled.end());
// Operators
const auto a(Txid::FromUint256(uint256{1})),
b(Txid::FromUint256(uint256{2})),
c(Txid::FromUint256(uint256{3}));
BOOST_CHECK(a == uint256{1});
BOOST_CHECK(a == a);
BOOST_CHECK(a == Txid::FromUint256(uint256{1}));
BOOST_CHECK(b == b);
BOOST_CHECK(c == c);
BOOST_CHECK(a != b);
BOOST_CHECK(a != Txid::FromUint256(uint256{10}));
BOOST_CHECK(a != c);
BOOST_CHECK(b != c);
BOOST_CHECK(a < b);
BOOST_CHECK(a < Txid::FromUint256(uint256{10}));
BOOST_CHECK(b < c);
BOOST_CHECK(a < c);
}
BOOST_AUTO_TEST_CASE(test_coutpoint_sorting)
{
// Sorting
std::vector original{
COutPoint(Txid::FromUint256(uint256{1}), 1),
COutPoint(Txid::FromUint256(uint256{1}), 2),
COutPoint(Txid::FromUint256(uint256{1}), 3),
COutPoint(Txid::FromUint256(uint256{2}), 1),
COutPoint(Txid::FromUint256(uint256{2}), 2),
COutPoint(Txid::FromUint256(uint256{2}), 3),
COutPoint(Txid::FromUint256(uint256{3}), 1),
COutPoint(Txid::FromUint256(uint256{3}), 2),
COutPoint(Txid::FromUint256(uint256{3}), 3)
};
std::vector shuffled{original};
std::ranges::shuffle(shuffled, m_rng);
std::sort(shuffled.begin(), shuffled.end());
BOOST_CHECK_EQUAL_COLLECTIONS(original.begin(), original.end(), shuffled.begin(), shuffled.end());
// Operators
const auto a{COutPoint(Txid::FromUint256(uint256{1}), 1)},
b{COutPoint(Txid::FromUint256(uint256{1}), 2)},
c{COutPoint(Txid::FromUint256(uint256{2}), 1)};
BOOST_CHECK(a == a);
BOOST_CHECK(a == COutPoint(Txid::FromUint256(uint256{1}), 1));
BOOST_CHECK(b == b);
BOOST_CHECK(c == c);
BOOST_CHECK(a != b);
BOOST_CHECK(a != COutPoint(Txid::FromUint256(uint256{1}), 10));
BOOST_CHECK(a != c);
BOOST_CHECK(b != c);
BOOST_CHECK(a < b);
BOOST_CHECK(a < COutPoint(Txid::FromUint256(uint256{1}), 10));
BOOST_CHECK(b < c);
BOOST_CHECK(a < c);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -614,3 +614,8 @@ std::ostream& operator<<(std::ostream& os, const uint256& num)
{
return os << num.ToString();
}
std::ostream& operator<<(std::ostream& os, const COutPoint& outpoint)
{
return os << outpoint.hash << ", " << outpoint.n;
}

View File

@ -285,6 +285,7 @@ inline std::ostream& operator<<(std::ostream& os, const std::optional<T>& v)
std::ostream& operator<<(std::ostream& os, const arith_uint256& num);
std::ostream& operator<<(std::ostream& os, const uint160& num);
std::ostream& operator<<(std::ostream& os, const uint256& num);
std::ostream& operator<<(std::ostream& os, const COutPoint& outpoint);
// @}
/**