bitcoin/src/test/txpackage_tests.cpp
merge-script 7f295e1d9b
Merge bitcoin/bitcoin#34084: scripted-diff: [doc] Unify stale copyright headers
fa4cb13b52030c2e55c6bea170649ab69d75f758 test: [doc] Manually unify stale headers (MarcoFalke)
fa5f29774872d18febc0df38831a6e45f3de69cc scripted-diff: [doc] Unify stale copyright headers (MarcoFalke)

Pull request description:

  Historically, the upper year range in file headers was bumped manually
  or with a script.

  This has many issues:

  * The script is causing churn. See for example commit 306ccd4, or
    drive-by first-time contributions bumping them one-by-one. (A few from
    this year: https://github.com/bitcoin/bitcoin/pull/32008,
    https://github.com/bitcoin/bitcoin/pull/31642,
    https://github.com/bitcoin/bitcoin/pull/32963, ...)
  * Some, or likely most, upper year values were wrong. Reasons for
    incorrect dates could be code moves, cherry-picks, or simply bugs in
    the script.
  * The upper range is not needed for anything.
  * Anyone who wants to find the initial file creation date, or file
    history, can use `git log` or `git blame` to get more accurate
    results.
  * Many places are already using the `-present` suffix, with the meaning
    that the upper range is omitted.

  To fix all issues, this bumps the upper range of the copyright headers
  to `-present`.

  Further notes:

  * Obviously, the yearly 4-line bump commit for the build system (c.f.
    b537a2c02a9921235d1ecf8c3c7dc1836ec68131) is fine and will remain.
  * For new code, the date range can be fully omitted, as it is done
    already by some developers. Obviously, developers are free to pick
    whatever style they want. One can list the commits for each style.
  * For example, to list all commits that use `-present`:
    `git log --format='%an (%ae) [%h: %s]' -S 'present The Bitcoin'`.
  * Alternatively, to list all commits that use no range at all:
    `git log --format='%an (%ae) [%h: %s]' -S '(c) The Bitcoin'`.

  <!--
  * The lower range can be wrong as well, so it could be omitted as well,
    but this is left for a follow-up. A previous attempt was in
    https://github.com/bitcoin/bitcoin/pull/26817.

ACKs for top commit:
  l0rinc:
    ACK fa4cb13b52030c2e55c6bea170649ab69d75f758
  rkrux:
    re-ACK fa4cb13b52030c2e55c6bea170649ab69d75f758
  janb84:
    ACK fa4cb13b52030c2e55c6bea170649ab69d75f758

Tree-SHA512: e5132781bdc4417d1e2922809b27ef4cf0abb37ffb68c65aab8a5391d3c917b61a18928ec2ec2c75ef5184cb79a5b8c8290d63e949220dbeab3bd2c0dfbdc4c5
2025-12-19 16:56:02 +00:00

1239 lines
79 KiB
C++

// Copyright (c) 2021-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <consensus/validation.h>
#include <key_io.h>
#include <policy/packages.h>
#include <policy/policy.h>
#include <policy/rbf.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <serialize.h>
#include <streams.h>
#include <test/util/random.h>
#include <test/util/script.h>
#include <test/util/setup_common.h>
#include <util/strencodings.h>
#include <test/util/txmempool.h>
#include <validation.h>
#include <boost/test/unit_test.hpp>
using namespace util::hex_literals;
// A fee amount that is above 1sat/vB but below 5sat/vB for most transactions created within these
// unit tests.
static const CAmount low_fee_amt{200};
struct TxPackageTest : TestChain100Setup {
// Create placeholder transactions that have no meaning.
inline CTransactionRef create_placeholder_tx(size_t num_inputs, size_t num_outputs)
{
CMutableTransaction mtx = CMutableTransaction();
mtx.vin.resize(num_inputs);
mtx.vout.resize(num_outputs);
auto random_script = CScript() << ToByteVector(m_rng.rand256()) << ToByteVector(m_rng.rand256());
for (size_t i{0}; i < num_inputs; ++i) {
mtx.vin[i].prevout.hash = Txid::FromUint256(m_rng.rand256());
mtx.vin[i].prevout.n = 0;
mtx.vin[i].scriptSig = random_script;
}
for (size_t o{0}; o < num_outputs; ++o) {
mtx.vout[o].nValue = 1 * CENT;
mtx.vout[o].scriptPubKey = random_script;
}
return MakeTransactionRef(mtx);
}
}; // struct TxPackageTest
BOOST_FIXTURE_TEST_SUITE(txpackage_tests, TxPackageTest)
BOOST_AUTO_TEST_CASE(package_hash_tests)
{
// Random real segwit transaction
DataStream stream_1{
"02000000000101964b8aa63509579ca6086e6012eeaa4c2f4dd1e283da29b67c8eea38b3c6fd220000000000fdffffff0294c618000000000017a9145afbbb42f4e83312666d0697f9e66259912ecde38768fa2c0000000000160014897388a0889390fd0e153a22bb2cf9d8f019faf50247304402200547406380719f84d68cf4e96cc3e4a1688309ef475b150be2b471c70ea562aa02206d255f5acc40fd95981874d77201d2eb07883657ce1c796513f32b6079545cdf0121023ae77335cefcb5ab4c1dc1fb0d2acfece184e593727d7d5906c78e564c7c11d125cf0c00"_hex,
};
CTransaction tx_1(deserialize, TX_WITH_WITNESS, stream_1);
CTransactionRef ptx_1{MakeTransactionRef(tx_1)};
// Random real nonsegwit transaction
DataStream stream_2{
"01000000010b26e9b7735eb6aabdf358bab62f9816a21ba9ebdb719d5299e88607d722c190000000008b4830450220070aca44506c5cef3a16ed519d7c3c39f8aab192c4e1c90d065f37b8a4af6141022100a8e160b856c2d43d27d8fba71e5aef6405b8643ac4cb7cb3c462aced7f14711a0141046d11fee51b0e60666d5049a9101a72741df480b96ee26488a4d3466b95c9a40ac5eeef87e10a5cd336c19a84565f80fa6c547957b7700ff4dfbdefe76036c339ffffffff021bff3d11000000001976a91404943fdd508053c75000106d3bc6e2754dbcff1988ac2f15de00000000001976a914a266436d2965547608b9e15d9032a7b9d64fa43188ac00000000"_hex,
};
CTransaction tx_2(deserialize, TX_WITH_WITNESS, stream_2);
CTransactionRef ptx_2{MakeTransactionRef(tx_2)};
// Random real segwit transaction
DataStream stream_3{
"0200000000010177862801f77c2c068a70372b4c435ef8dd621291c36a64eb4dd491f02218f5324600000000fdffffff014a0100000000000022512035ea312034cfac01e956a269f3bf147f569c2fbb00180677421262da042290d803402be713325ff285e66b0380f53f2fae0d0fb4e16f378a440fed51ce835061437566729d4883bc917632f3cff474d6384bc8b989961a1d730d4a87ed38ad28bd337b20f1d658c6c138b1c312e072b4446f50f01ae0da03a42e6274f8788aae53416a7fac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800357b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a224342414c222c22616d74223a2236393639227d6821c1f1d658c6c138b1c312e072b4446f50f01ae0da03a42e6274f8788aae53416a7f00000000"_hex,
};
CTransaction tx_3(deserialize, TX_WITH_WITNESS, stream_3);
CTransactionRef ptx_3{MakeTransactionRef(tx_3)};
// It's easy to see that wtxids are sorted in lexicographical order:
constexpr Wtxid wtxid_1{"85cd1a31eb38f74ed5742ec9cb546712ab5aaf747de28a9168b53e846cbda17f"};
constexpr Wtxid wtxid_2{"b4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b"};
constexpr Wtxid wtxid_3{"e065bac15f62bb4e761d761db928ddee65a47296b2b776785abb912cdec474e3"};
BOOST_CHECK_EQUAL(tx_1.GetWitnessHash(), wtxid_1);
BOOST_CHECK_EQUAL(tx_2.GetWitnessHash(), wtxid_2);
BOOST_CHECK_EQUAL(tx_3.GetWitnessHash(), wtxid_3);
BOOST_CHECK(wtxid_1.GetHex() < wtxid_2.GetHex());
BOOST_CHECK(wtxid_2.GetHex() < wtxid_3.GetHex());
// The txids are not (we want to test that sorting and hashing use wtxid, not txid):
constexpr Txid txid_1{"bd0f71c1d5e50589063e134fad22053cdae5ab2320db5bf5e540198b0b5a4e69"};
constexpr Txid txid_2{"b4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b"};
constexpr Txid txid_3{"ee707be5201160e32c4fc715bec227d1aeea5940fb4295605e7373edce3b1a93"};
BOOST_CHECK_EQUAL(tx_1.GetHash(), txid_1);
BOOST_CHECK_EQUAL(tx_2.GetHash(), txid_2);
BOOST_CHECK_EQUAL(tx_3.GetHash(), txid_3);
BOOST_CHECK(txid_2.GetHex() < txid_1.GetHex());
BOOST_CHECK(txid_1.ToUint256() != wtxid_1.ToUint256());
BOOST_CHECK(txid_2.ToUint256() == wtxid_2.ToUint256());
BOOST_CHECK(txid_3.ToUint256() != wtxid_3.ToUint256());
// We are testing that both functions compare using GetHex() and not uint256.
// (in this pair of wtxids, hex string order != uint256 order)
BOOST_CHECK(wtxid_2 < wtxid_1);
// (in this pair of wtxids, hex string order == uint256 order)
BOOST_CHECK(wtxid_2 < wtxid_3);
// All permutations of the package containing ptx_1, ptx_2, ptx_3 have the same package hash
std::vector<CTransactionRef> package_123{ptx_1, ptx_2, ptx_3};
std::vector<CTransactionRef> package_132{ptx_1, ptx_3, ptx_2};
std::vector<CTransactionRef> package_231{ptx_2, ptx_3, ptx_1};
std::vector<CTransactionRef> package_213{ptx_2, ptx_1, ptx_3};
std::vector<CTransactionRef> package_312{ptx_3, ptx_1, ptx_2};
std::vector<CTransactionRef> package_321{ptx_3, ptx_2, ptx_1};
uint256 calculated_hash_123 = (HashWriter() << wtxid_1 << wtxid_2 << wtxid_3).GetSHA256();
uint256 hash_if_by_txid = (HashWriter() << wtxid_2 << wtxid_1 << wtxid_3).GetSHA256();
BOOST_CHECK(hash_if_by_txid != calculated_hash_123);
uint256 hash_if_use_txid = (HashWriter() << txid_2 << txid_1 << txid_3).GetSHA256();
BOOST_CHECK(hash_if_use_txid != calculated_hash_123);
uint256 hash_if_use_int_order = (HashWriter() << wtxid_2 << wtxid_1 << wtxid_3).GetSHA256();
BOOST_CHECK(hash_if_use_int_order != calculated_hash_123);
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_123));
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_132));
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_231));
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_213));
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_312));
BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_321));
}
BOOST_AUTO_TEST_CASE(package_sanitization_tests)
{
// Packages can't have more than 25 transactions.
Package package_too_many;
package_too_many.reserve(MAX_PACKAGE_COUNT + 1);
for (size_t i{0}; i < MAX_PACKAGE_COUNT + 1; ++i) {
package_too_many.emplace_back(create_placeholder_tx(1, 1));
}
PackageValidationState state_too_many;
BOOST_CHECK(!IsWellFormedPackage(package_too_many, state_too_many, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state_too_many.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_too_many.GetRejectReason(), "package-too-many-transactions");
// Packages can't have a total weight of more than 404'000WU.
CTransactionRef large_ptx = create_placeholder_tx(150, 150);
Package package_too_large;
auto size_large = GetTransactionWeight(*large_ptx);
size_t total_weight{0};
while (total_weight <= MAX_PACKAGE_WEIGHT) {
package_too_large.push_back(large_ptx);
total_weight += size_large;
}
BOOST_CHECK(package_too_large.size() <= MAX_PACKAGE_COUNT);
PackageValidationState state_too_large;
BOOST_CHECK(!IsWellFormedPackage(package_too_large, state_too_large, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state_too_large.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_too_large.GetRejectReason(), "package-too-large");
// Packages can't contain transactions with the same txid.
Package package_duplicate_txids_empty;
for (auto i{0}; i < 3; ++i) {
CMutableTransaction empty_tx;
package_duplicate_txids_empty.emplace_back(MakeTransactionRef(empty_tx));
}
PackageValidationState state_duplicates;
BOOST_CHECK(!IsWellFormedPackage(package_duplicate_txids_empty, state_duplicates, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state_duplicates.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_duplicates.GetRejectReason(), "package-contains-duplicates");
BOOST_CHECK(!IsConsistentPackage(package_duplicate_txids_empty));
// Packages can't have transactions spending the same prevout
CMutableTransaction tx_zero_1;
CMutableTransaction tx_zero_2;
COutPoint same_prevout{Txid::FromUint256(m_rng.rand256()), 0};
tx_zero_1.vin.emplace_back(same_prevout);
tx_zero_2.vin.emplace_back(same_prevout);
// Different vouts (not the same tx)
tx_zero_1.vout.emplace_back(CENT, P2WSH_OP_TRUE);
tx_zero_2.vout.emplace_back(2 * CENT, P2WSH_OP_TRUE);
Package package_conflicts{MakeTransactionRef(tx_zero_1), MakeTransactionRef(tx_zero_2)};
BOOST_CHECK(!IsConsistentPackage(package_conflicts));
// Transactions are considered sorted when they have no dependencies.
BOOST_CHECK(IsTopoSortedPackage(package_conflicts));
PackageValidationState state_conflicts;
BOOST_CHECK(!IsWellFormedPackage(package_conflicts, state_conflicts, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state_conflicts.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_conflicts.GetRejectReason(), "conflict-in-package");
// IsConsistentPackage only cares about conflicts between transactions, not about a transaction
// conflicting with itself (i.e. duplicate prevouts in vin).
CMutableTransaction dup_tx;
const COutPoint rand_prevout{Txid::FromUint256(m_rng.rand256()), 0};
dup_tx.vin.emplace_back(rand_prevout);
dup_tx.vin.emplace_back(rand_prevout);
Package package_with_dup_tx{MakeTransactionRef(dup_tx)};
BOOST_CHECK(IsConsistentPackage(package_with_dup_tx));
package_with_dup_tx.emplace_back(create_placeholder_tx(1, 1));
BOOST_CHECK(IsConsistentPackage(package_with_dup_tx));
}
BOOST_AUTO_TEST_CASE(package_validation_tests)
{
LOCK(cs_main);
unsigned int initialPoolSize = m_node.mempool->size();
// Parent and Child Package
CKey parent_key = GenerateRandomKey();
CScript parent_locking_script = GetScriptForDestination(PKHash(parent_key.GetPubKey()));
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
CKey child_key = GenerateRandomKey();
CScript child_locking_script = GetScriptForDestination(PKHash(child_key.GetPubKey()));
auto mtx_child = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/parent_key,
/*output_destination=*/child_locking_script,
/*output_amount=*/CAmount(48 * COIN), /*submit=*/false);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
Package package_parent_child{tx_parent, tx_child};
const auto result_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_parent_child, /*test_accept=*/true, /*client_maxfeerate=*/{});
if (auto err_parent_child{CheckPackageMempoolAcceptResult(package_parent_child, result_parent_child, /*expect_valid=*/true, nullptr)}) {
BOOST_ERROR(err_parent_child.value());
} else {
auto it_parent = result_parent_child.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child = result_parent_child.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent->second.m_effective_feerate.value().GetFee(GetVirtualTransactionSize(*tx_parent)) == COIN);
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().front(), tx_parent->GetWitnessHash());
BOOST_CHECK(it_child->second.m_effective_feerate.value().GetFee(GetVirtualTransactionSize(*tx_child)) == COIN);
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().front(), tx_child->GetWitnessHash());
}
// A single, giant transaction submitted through ProcessNewPackage fails on single tx policy.
CTransactionRef giant_ptx = create_placeholder_tx(999, 999);
BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) > DEFAULT_CLUSTER_SIZE_LIMIT_KVB * 1000);
Package package_single_giant{giant_ptx};
auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_single_giant, /*test_accept=*/true, /*client_maxfeerate=*/{});
if (auto err_single_large{CheckPackageMempoolAcceptResult(package_single_giant, result_single_large, /*expect_valid=*/false, nullptr)}) {
BOOST_ERROR(err_single_large.value());
} else {
BOOST_CHECK_EQUAL(result_single_large.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(result_single_large.m_state.GetRejectReason(), "transaction failed");
auto it_giant_tx = result_single_large.m_tx_results.find(giant_ptx->GetWitnessHash());
BOOST_CHECK_EQUAL(it_giant_tx->second.m_state.GetRejectReason(), "tx-size");
}
// Check that mempool size hasn't changed.
BOOST_CHECK_EQUAL(m_node.mempool->size(), initialPoolSize);
}
BOOST_AUTO_TEST_CASE(noncontextual_package_tests)
{
// The signatures won't be verified so we can just use a placeholder
CKey placeholder_key = GenerateRandomKey();
CScript spk = GetScriptForDestination(PKHash(placeholder_key.GetPubKey()));
CKey placeholder_key_2 = GenerateRandomKey();
CScript spk2 = GetScriptForDestination(PKHash(placeholder_key_2.GetPubKey()));
// Parent and Child Package
{
auto mtx_parent = CreateValidMempoolTransaction(m_coinbase_txns[0], 0, 0, coinbaseKey, spk,
CAmount(49 * COIN), /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
auto mtx_child = CreateValidMempoolTransaction(tx_parent, 0, 101, placeholder_key, spk2,
CAmount(48 * COIN), /*submit=*/false);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
PackageValidationState state;
BOOST_CHECK(IsWellFormedPackage({tx_parent, tx_child}, state, /*require_sorted=*/true));
BOOST_CHECK(!IsWellFormedPackage({tx_child, tx_parent}, state, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted");
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
BOOST_CHECK(IsChildWithParentsTree({tx_parent, tx_child}));
BOOST_CHECK(GetPackageHash({tx_parent}) != GetPackageHash({tx_child}));
BOOST_CHECK(GetPackageHash({tx_child, tx_child}) != GetPackageHash({tx_child}));
BOOST_CHECK(GetPackageHash({tx_child, tx_parent}) != GetPackageHash({tx_child, tx_child}));
BOOST_CHECK(!IsChildWithParents({}));
BOOST_CHECK(!IsChildWithParentsTree({}));
}
// 24 Parents and 1 Child
{
Package package;
CMutableTransaction child;
for (int i{0}; i < 24; ++i) {
auto parent = MakeTransactionRef(CreateValidMempoolTransaction(m_coinbase_txns[i + 1],
0, 0, coinbaseKey, spk, CAmount(48 * COIN), false));
package.emplace_back(parent);
child.vin.emplace_back(COutPoint(parent->GetHash(), 0));
}
child.vout.emplace_back(47 * COIN, spk2);
// The child must be in the package.
BOOST_CHECK(!IsChildWithParents(package));
// The parents can be in any order.
FastRandomContext rng;
std::shuffle(package.begin(), package.end(), rng);
package.push_back(MakeTransactionRef(child));
PackageValidationState state;
BOOST_CHECK(IsWellFormedPackage(package, state, /*require_sorted=*/true));
BOOST_CHECK(IsChildWithParents(package));
BOOST_CHECK(IsChildWithParentsTree(package));
package.erase(package.begin());
BOOST_CHECK(IsChildWithParents(package));
// The package cannot have unrelated transactions.
package.insert(package.begin(), m_coinbase_txns[0]);
BOOST_CHECK(!IsChildWithParents(package));
}
// 2 Parents and 1 Child where one parent depends on the other.
{
CMutableTransaction mtx_parent;
mtx_parent.vin.emplace_back(COutPoint(m_coinbase_txns[0]->GetHash(), 0));
mtx_parent.vout.emplace_back(20 * COIN, spk);
mtx_parent.vout.emplace_back(20 * COIN, spk2);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
CMutableTransaction mtx_parent_also_child;
mtx_parent_also_child.vin.emplace_back(COutPoint(tx_parent->GetHash(), 0));
mtx_parent_also_child.vout.emplace_back(20 * COIN, spk);
CTransactionRef tx_parent_also_child = MakeTransactionRef(mtx_parent_also_child);
CMutableTransaction mtx_child;
mtx_child.vin.emplace_back(COutPoint(tx_parent->GetHash(), 1));
mtx_child.vin.emplace_back(COutPoint(tx_parent_also_child->GetHash(), 0));
mtx_child.vout.emplace_back(39 * COIN, spk);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
PackageValidationState state;
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child}));
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child, tx_child}));
BOOST_CHECK(!IsChildWithParentsTree({tx_parent, tx_parent_also_child, tx_child}));
// IsChildWithParents does not detect unsorted parents.
BOOST_CHECK(IsChildWithParents({tx_parent_also_child, tx_parent, tx_child}));
BOOST_CHECK(IsWellFormedPackage({tx_parent, tx_parent_also_child, tx_child}, state, /*require_sorted=*/true));
BOOST_CHECK(!IsWellFormedPackage({tx_parent_also_child, tx_parent, tx_child}, state, /*require_sorted=*/true));
BOOST_CHECK_EQUAL(state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted");
}
}
BOOST_AUTO_TEST_CASE(package_submission_tests)
{
// Mine blocks to mature coinbases.
mineBlocks(3);
MockMempoolMinFee(CFeeRate(5000), *m_node.mempool);
LOCK(cs_main);
unsigned int expected_pool_size = m_node.mempool->size();
CKey parent_key = GenerateRandomKey();
CScript parent_locking_script = GetScriptForDestination(PKHash(parent_key.GetPubKey()));
// Unrelated transactions are not allowed in package submission.
Package package_unrelated;
for (size_t i{0}; i < 10; ++i) {
auto mtx = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[i + 25], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/false);
package_unrelated.emplace_back(MakeTransactionRef(mtx));
}
auto result_unrelated_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_unrelated, /*test_accept=*/false, /*client_maxfeerate=*/{});
// We don't expect m_tx_results for each transaction when basic sanity checks haven't passed.
BOOST_CHECK(result_unrelated_submit.m_state.IsInvalid());
BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetRejectReason(), "package-not-child-with-parents");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Parent and Child (and Grandchild) Package
Package package_parent_child;
Package package_3gen;
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
package_parent_child.push_back(tx_parent);
package_3gen.push_back(tx_parent);
CKey child_key = GenerateRandomKey();
CScript child_locking_script = GetScriptForDestination(PKHash(child_key.GetPubKey()));
auto mtx_child = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/parent_key,
/*output_destination=*/child_locking_script,
/*output_amount=*/CAmount(48 * COIN), /*submit=*/false);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
package_parent_child.push_back(tx_child);
package_3gen.push_back(tx_child);
CKey grandchild_key = GenerateRandomKey();
CScript grandchild_locking_script = GetScriptForDestination(PKHash(grandchild_key.GetPubKey()));
auto mtx_grandchild = CreateValidMempoolTransaction(/*input_transaction=*/tx_child, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/child_key,
/*output_destination=*/grandchild_locking_script,
/*output_amount=*/CAmount(47 * COIN), /*submit=*/false);
CTransactionRef tx_grandchild = MakeTransactionRef(mtx_grandchild);
package_3gen.push_back(tx_grandchild);
// 3 Generations is not allowed.
{
auto result_3gen_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_3gen, /*test_accept=*/false, /*client_maxfeerate=*/{});
BOOST_CHECK(result_3gen_submit.m_state.IsInvalid());
BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetRejectReason(), "package-not-child-with-parents");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Parent and child package where transactions are invalid for reasons other than fee and
// missing inputs, so the package validation isn't expected to happen.
{
CScriptWitness bad_witness;
bad_witness.stack.emplace_back(1);
CMutableTransaction mtx_parent_invalid{mtx_parent};
mtx_parent_invalid.vin[0].scriptWitness = bad_witness;
CTransactionRef tx_parent_invalid = MakeTransactionRef(mtx_parent_invalid);
Package package_invalid_parent{tx_parent_invalid, tx_child};
auto result_quit_early = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_invalid_parent, /*test_accept=*/ false, /*client_maxfeerate=*/{});
if (auto err_parent_invalid{CheckPackageMempoolAcceptResult(package_invalid_parent, result_quit_early, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_parent_invalid.value());
} else {
auto it_parent = result_quit_early.m_tx_results.find(tx_parent_invalid->GetWitnessHash());
auto it_child = result_quit_early.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(), TxValidationResult::TX_WITNESS_MUTATED);
BOOST_CHECK_EQUAL(it_parent->second.m_state.GetRejectReason(), "bad-witness-nonstandard");
BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MISSING_INPUTS);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(), "bad-txns-inputs-missingorspent");
}
BOOST_CHECK_EQUAL(result_quit_early.m_state.GetResult(), PackageValidationResult::PCKG_TX);
}
// Submit package with parent + child.
{
const auto submit_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_parent_child, /*test_accept=*/false, /*client_maxfeerate=*/{});
expected_pool_size += 2;
BOOST_CHECK_MESSAGE(submit_parent_child.m_state.IsValid(),
"Package validation unexpectedly failed: " << submit_parent_child.m_state.GetRejectReason());
BOOST_CHECK_EQUAL(submit_parent_child.m_tx_results.size(), package_parent_child.size());
auto it_parent = submit_parent_child.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child = submit_parent_child.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent != submit_parent_child.m_tx_results.end());
BOOST_CHECK(it_parent->second.m_state.IsValid());
BOOST_CHECK(it_parent->second.m_effective_feerate == CFeeRate(1 * COIN, GetVirtualTransactionSize(*tx_parent)));
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().front(), tx_parent->GetWitnessHash());
BOOST_CHECK(it_child->second.m_effective_feerate == CFeeRate(1 * COIN, GetVirtualTransactionSize(*tx_child)));
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().front(), tx_child->GetWitnessHash());
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Already-in-mempool transactions should be detected and de-duplicated.
{
const auto submit_deduped = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_parent_child, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_deduped{CheckPackageMempoolAcceptResult(package_parent_child, submit_deduped, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_deduped.value());
} else {
auto it_parent_deduped = submit_deduped.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child_deduped = submit_deduped.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent_deduped->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
BOOST_CHECK(it_child_deduped->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
}
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// In-mempool parent and child with missing parent.
{
auto tx_parent_1 = MakeTransactionRef(CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[1], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(50 * COIN - low_fee_amt), /*submit=*/false));
auto tx_parent_2 = MakeTransactionRef(CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[2], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(50 * COIN - 800), /*submit=*/false));
auto tx_child_missing_parent = MakeTransactionRef(CreateValidMempoolTransaction({tx_parent_1, tx_parent_2},
{{tx_parent_1->GetHash(), 0}, {tx_parent_2->GetHash(), 0}},
/*input_height=*/0, {parent_key},
{{49 * COIN, child_locking_script}}, /*submit=*/false));
Package package_missing_parent{tx_parent_1, tx_child_missing_parent};
const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_missing_parent{CheckPackageMempoolAcceptResult(package_missing_parent, result_missing_parent, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_missing_parent.value());
} else {
auto it_parent = result_missing_parent.m_tx_results.find(tx_parent_1->GetWitnessHash());
auto it_child = result_missing_parent.m_tx_results.find(tx_child_missing_parent->GetWitnessHash());
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "transaction failed");
BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(), TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MISSING_INPUTS);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(), "bad-txns-inputs-missingorspent");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Submit parent2 ahead of time, should become ok.
Package package_just_parent2{tx_parent_2};
expected_pool_size += 1;
const auto result_just_parent2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_just_parent2, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_parent2{CheckPackageMempoolAcceptResult(package_just_parent2, result_just_parent2, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_parent2.value());
}
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const auto result_parent_already_in = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{});
expected_pool_size += 2;
if (auto err_parent_already_in{CheckPackageMempoolAcceptResult(package_missing_parent, result_parent_already_in, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_parent_already_in.value());
}
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
}
// Tests for packages containing a single transaction
BOOST_AUTO_TEST_CASE(package_single_tx)
{
// Mine blocks to mature coinbases.
mineBlocks(3);
LOCK(cs_main);
auto expected_pool_size{m_node.mempool->size()};
const CAmount high_fee{1000};
// No unconfirmed parents
CKey single_key = GenerateRandomKey();
CScript single_locking_script = GetScriptForDestination(PKHash(single_key.GetPubKey()));
auto mtx_single = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/single_locking_script,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/false);
CTransactionRef tx_single = MakeTransactionRef(mtx_single);
Package package_tx_single{tx_single};
const auto result_single_tx = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_tx_single, /*test_accept=*/false, /*client_maxfeerate=*/{});
expected_pool_size += 1;
BOOST_CHECK_MESSAGE(result_single_tx.m_state.IsValid(),
"Package validation unexpectedly failed: " << result_single_tx.m_state.ToString());
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Parent and Child. Both submitted by themselves through the ProcessNewPackage interface.
CKey parent_key = GenerateRandomKey();
CScript parent_locking_script = GetScriptForDestination(WitnessV0KeyHash(parent_key.GetPubKey()));
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[1], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_locking_script,
/*output_amount=*/CAmount(50 * COIN) - high_fee, /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
Package package_just_parent{tx_parent};
const auto result_just_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_just_parent, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_parent_child{CheckPackageMempoolAcceptResult(package_just_parent, result_just_parent, /*expect_valid=*/true, nullptr)}) {
BOOST_ERROR(err_parent_child.value());
} else {
auto it_parent = result_just_parent.m_tx_results.find(tx_parent->GetWitnessHash());
BOOST_CHECK_MESSAGE(it_parent->second.m_state.IsValid(), it_parent->second.m_state.ToString());
BOOST_CHECK(it_parent->second.m_effective_feerate.value().GetFee(GetVirtualTransactionSize(*tx_parent)) == high_fee);
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_parent->second.m_wtxids_fee_calculations.value().front(), tx_parent->GetWitnessHash());
}
expected_pool_size += 1;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
CKey child_key = GenerateRandomKey();
CScript child_locking_script = GetScriptForDestination(WitnessV0KeyHash(child_key.GetPubKey()));
auto mtx_child = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/parent_key,
/*output_destination=*/child_locking_script,
/*output_amount=*/CAmount(50 * COIN) - 2 * high_fee, /*submit=*/false);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
Package package_just_child{tx_child};
const auto result_just_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_just_child, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_parent_child{CheckPackageMempoolAcceptResult(package_just_child, result_just_child, /*expect_valid=*/true, nullptr)}) {
BOOST_ERROR(err_parent_child.value());
} else {
auto it_child = result_just_child.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK_MESSAGE(it_child->second.m_state.IsValid(), it_child->second.m_state.ToString());
BOOST_CHECK(it_child->second.m_effective_feerate.value().GetFee(GetVirtualTransactionSize(*tx_child)) == high_fee);
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().size(), 1);
BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().front(), tx_child->GetWitnessHash());
}
expected_pool_size += 1;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Too-low fee to RBF tx_single
auto mtx_single_low_fee = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/single_locking_script,
/*output_amount=*/CAmount(49 * COIN - 1), /*submit=*/false);
CTransactionRef tx_single_low_fee = MakeTransactionRef(mtx_single_low_fee);
Package package_tx_single_low_fee{tx_single_low_fee};
const auto result_single_tx_low_fee = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_tx_single_low_fee, /*test_accept=*/false, /*client_maxfeerate=*/{});
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
BOOST_CHECK(!result_single_tx_low_fee.m_state.IsValid());
BOOST_CHECK_EQUAL(result_single_tx_low_fee.m_state.GetResult(), PackageValidationResult::PCKG_TX);
auto it_low_fee = result_single_tx_low_fee.m_tx_results.find(tx_single_low_fee->GetWitnessHash());
BOOST_CHECK_EQUAL(it_low_fee->second.m_state.GetResult(), TxValidationResult::TX_RECONSIDERABLE);
if (auto err_single{CheckPackageMempoolAcceptResult(package_tx_single_low_fee, result_single_tx_low_fee, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_single.value());
}
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Tests for packages containing transactions that have same-txid-different-witness equivalents in
// the mempool.
BOOST_AUTO_TEST_CASE(package_witness_swap_tests)
{
// Mine blocks to mature coinbases.
mineBlocks(5);
MockMempoolMinFee(CFeeRate(5000), *m_node.mempool);
LOCK(cs_main);
// Transactions with a same-txid-different-witness transaction in the mempool should be ignored,
// and the mempool entry's wtxid returned.
CScript witnessScript = CScript() << OP_DROP << OP_TRUE;
CScript scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessScript));
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/scriptPubKey,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/false);
CTransactionRef ptx_parent = MakeTransactionRef(mtx_parent);
// Make two children with the same txid but different witnesses.
CScriptWitness witness1;
witness1.stack.emplace_back(1);
witness1.stack.emplace_back(witnessScript.begin(), witnessScript.end());
CScriptWitness witness2(witness1);
witness2.stack.emplace_back(2);
witness2.stack.emplace_back(witnessScript.begin(), witnessScript.end());
CKey child_key = GenerateRandomKey();
CScript child_locking_script = GetScriptForDestination(WitnessV0KeyHash(child_key.GetPubKey()));
CMutableTransaction mtx_child1;
mtx_child1.version = 1;
mtx_child1.vin.resize(1);
mtx_child1.vin[0].prevout.hash = ptx_parent->GetHash();
mtx_child1.vin[0].prevout.n = 0;
mtx_child1.vin[0].scriptSig = CScript();
mtx_child1.vin[0].scriptWitness = witness1;
mtx_child1.vout.resize(1);
mtx_child1.vout[0].nValue = CAmount(48 * COIN);
mtx_child1.vout[0].scriptPubKey = child_locking_script;
CMutableTransaction mtx_child2{mtx_child1};
mtx_child2.vin[0].scriptWitness = witness2;
CTransactionRef ptx_child1 = MakeTransactionRef(mtx_child1);
CTransactionRef ptx_child2 = MakeTransactionRef(mtx_child2);
// child1 and child2 have the same txid
BOOST_CHECK_EQUAL(ptx_child1->GetHash(), ptx_child2->GetHash());
// child1 and child2 have different wtxids
BOOST_CHECK(ptx_child1->GetWitnessHash() != ptx_child2->GetWitnessHash());
// Check that they have different package hashes
BOOST_CHECK(GetPackageHash({ptx_parent, ptx_child1}) != GetPackageHash({ptx_parent, ptx_child2}));
// Try submitting Package1{parent, child1} and Package2{parent, child2} where the children are
// same-txid-different-witness.
{
Package package_parent_child1{ptx_parent, ptx_child1};
const auto submit_witness1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_parent_child1, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_witness1{CheckPackageMempoolAcceptResult(package_parent_child1, submit_witness1, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_witness1.value());
}
// Child2 would have been validated individually.
Package package_parent_child2{ptx_parent, ptx_child2};
const auto submit_witness2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_parent_child2, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_witness2{CheckPackageMempoolAcceptResult(package_parent_child2, submit_witness2, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_witness2.value());
} else {
auto it_parent2_deduped = submit_witness2.m_tx_results.find(ptx_parent->GetWitnessHash());
auto it_child2 = submit_witness2.m_tx_results.find(ptx_child2->GetWitnessHash());
BOOST_CHECK(it_parent2_deduped->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
BOOST_CHECK(it_child2->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
BOOST_CHECK_EQUAL(ptx_child1->GetWitnessHash(), it_child2->second.m_other_wtxid.value());
}
// Deduplication should work when wtxid != txid. Submit package with the already-in-mempool
// transactions again, which should not fail.
const auto submit_segwit_dedup = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_parent_child1, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_segwit_dedup{CheckPackageMempoolAcceptResult(package_parent_child1, submit_segwit_dedup, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_segwit_dedup.value());
} else {
auto it_parent_dup = submit_segwit_dedup.m_tx_results.find(ptx_parent->GetWitnessHash());
auto it_child_dup = submit_segwit_dedup.m_tx_results.find(ptx_child1->GetWitnessHash());
BOOST_CHECK(it_parent_dup->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
BOOST_CHECK(it_child_dup->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
}
}
// Try submitting Package1{child2, grandchild} where child2 is same-txid-different-witness as
// the in-mempool transaction, child1. Since child1 exists in the mempool and its outputs are
// available, child2 should be ignored and grandchild should be accepted.
//
// This tests a potential censorship vector in which an attacker broadcasts a competing package
// where a parent's witness is mutated. The honest package should be accepted despite the fact
// that we don't allow witness replacement.
CKey grandchild_key = GenerateRandomKey();
CScript grandchild_locking_script = GetScriptForDestination(WitnessV0KeyHash(grandchild_key.GetPubKey()));
auto mtx_grandchild = CreateValidMempoolTransaction(/*input_transaction=*/ptx_child2, /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/child_key,
/*output_destination=*/grandchild_locking_script,
/*output_amount=*/CAmount(47 * COIN), /*submit=*/false);
CTransactionRef ptx_grandchild = MakeTransactionRef(mtx_grandchild);
// Check that they have different package hashes
BOOST_CHECK(GetPackageHash({ptx_child1, ptx_grandchild}) != GetPackageHash({ptx_child2, ptx_grandchild}));
// We already submitted child1 above.
{
Package package_child2_grandchild{ptx_child2, ptx_grandchild};
const auto submit_spend_ignored = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_child2_grandchild, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_spend_ignored{CheckPackageMempoolAcceptResult(package_child2_grandchild, submit_spend_ignored, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_spend_ignored.value());
} else {
auto it_child2_ignored = submit_spend_ignored.m_tx_results.find(ptx_child2->GetWitnessHash());
auto it_grandchild = submit_spend_ignored.m_tx_results.find(ptx_grandchild->GetWitnessHash());
BOOST_CHECK(it_child2_ignored->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
BOOST_CHECK(it_grandchild->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
}
}
// A package Package{parent1, parent2, parent3, child} where the parents are a mixture of
// identical-tx-in-mempool, same-txid-different-witness-in-mempool, and new transactions.
Package package_mixed;
// Give all the parents anyone-can-spend scripts so we don't have to deal with signing the child.
CScript acs_script = CScript() << OP_TRUE;
CScript acs_spk = GetScriptForDestination(WitnessV0ScriptHash(acs_script));
CScriptWitness acs_witness;
acs_witness.stack.emplace_back(acs_script.begin(), acs_script.end());
// parent1 will already be in the mempool
auto mtx_parent1 = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[1], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/acs_spk,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/true);
CTransactionRef ptx_parent1 = MakeTransactionRef(mtx_parent1);
package_mixed.push_back(ptx_parent1);
// parent2 will have a same-txid-different-witness tx already in the mempool
CScript grandparent2_script = CScript() << OP_DROP << OP_TRUE;
CScript grandparent2_spk = GetScriptForDestination(WitnessV0ScriptHash(grandparent2_script));
CScriptWitness parent2_witness1;
parent2_witness1.stack.emplace_back(1);
parent2_witness1.stack.emplace_back(grandparent2_script.begin(), grandparent2_script.end());
CScriptWitness parent2_witness2;
parent2_witness2.stack.emplace_back(2);
parent2_witness2.stack.emplace_back(grandparent2_script.begin(), grandparent2_script.end());
// Create grandparent2 creating an output with multiple spending paths. Submit to mempool.
auto mtx_grandparent2 = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[2], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/grandparent2_spk,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/true);
CTransactionRef ptx_grandparent2 = MakeTransactionRef(mtx_grandparent2);
CMutableTransaction mtx_parent2_v1;
mtx_parent2_v1.version = 1;
mtx_parent2_v1.vin.resize(1);
mtx_parent2_v1.vin[0].prevout.hash = ptx_grandparent2->GetHash();
mtx_parent2_v1.vin[0].prevout.n = 0;
mtx_parent2_v1.vin[0].scriptSig = CScript();
mtx_parent2_v1.vin[0].scriptWitness = parent2_witness1;
mtx_parent2_v1.vout.resize(1);
mtx_parent2_v1.vout[0].nValue = CAmount(48 * COIN);
mtx_parent2_v1.vout[0].scriptPubKey = acs_spk;
CMutableTransaction mtx_parent2_v2{mtx_parent2_v1};
mtx_parent2_v2.vin[0].scriptWitness = parent2_witness2;
CTransactionRef ptx_parent2_v1 = MakeTransactionRef(mtx_parent2_v1);
CTransactionRef ptx_parent2_v2 = MakeTransactionRef(mtx_parent2_v2);
// Put parent2_v1 in the package, submit parent2_v2 to the mempool.
const MempoolAcceptResult parent2_v2_result = m_node.chainman->ProcessTransaction(ptx_parent2_v2);
BOOST_CHECK(parent2_v2_result.m_result_type == MempoolAcceptResult::ResultType::VALID);
package_mixed.push_back(ptx_parent2_v1);
// parent3 will be a new transaction. Put a low feerate to make it invalid on its own.
auto mtx_parent3 = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[3], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/acs_spk,
/*output_amount=*/CAmount(50 * COIN - low_fee_amt), /*submit=*/false);
CTransactionRef ptx_parent3 = MakeTransactionRef(mtx_parent3);
package_mixed.push_back(ptx_parent3);
BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*ptx_parent3)) > low_fee_amt);
BOOST_CHECK(m_node.mempool->m_opts.min_relay_feerate.GetFee(GetVirtualTransactionSize(*ptx_parent3)) <= low_fee_amt);
// child spends parent1, parent2, and parent3
CKey mixed_grandchild_key = GenerateRandomKey();
CScript mixed_child_spk = GetScriptForDestination(WitnessV0KeyHash(mixed_grandchild_key.GetPubKey()));
CMutableTransaction mtx_mixed_child;
mtx_mixed_child.vin.emplace_back(COutPoint(ptx_parent1->GetHash(), 0));
mtx_mixed_child.vin.emplace_back(COutPoint(ptx_parent2_v1->GetHash(), 0));
mtx_mixed_child.vin.emplace_back(COutPoint(ptx_parent3->GetHash(), 0));
mtx_mixed_child.vin[0].scriptWitness = acs_witness;
mtx_mixed_child.vin[1].scriptWitness = acs_witness;
mtx_mixed_child.vin[2].scriptWitness = acs_witness;
mtx_mixed_child.vout.emplace_back((48 + 49 + 50 - 1) * COIN, mixed_child_spk);
CTransactionRef ptx_mixed_child = MakeTransactionRef(mtx_mixed_child);
package_mixed.push_back(ptx_mixed_child);
// Submit package:
// parent1 should be ignored
// parent2_v1 should be ignored (and v2 wtxid returned)
// parent3 should be accepted
// child should be accepted
{
const auto mixed_result = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_mixed, false, /*client_maxfeerate=*/{});
if (auto err_mixed{CheckPackageMempoolAcceptResult(package_mixed, mixed_result, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_mixed.value());
} else {
auto it_parent1 = mixed_result.m_tx_results.find(ptx_parent1->GetWitnessHash());
auto it_parent2 = mixed_result.m_tx_results.find(ptx_parent2_v1->GetWitnessHash());
auto it_parent3 = mixed_result.m_tx_results.find(ptx_parent3->GetWitnessHash());
auto it_child = mixed_result.m_tx_results.find(ptx_mixed_child->GetWitnessHash());
BOOST_CHECK(it_parent1->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
BOOST_CHECK(it_parent2->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
BOOST_CHECK(it_parent3->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_child->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(ptx_parent2_v2->GetWitnessHash(), it_parent2->second.m_other_wtxid.value());
// package feerate should include parent3 and child. It should not include parent1 or parent2_v1.
const CFeeRate expected_feerate(1 * COIN, GetVirtualTransactionSize(*ptx_parent3) + GetVirtualTransactionSize(*ptx_mixed_child));
BOOST_CHECK(it_parent3->second.m_effective_feerate.value() == expected_feerate);
BOOST_CHECK(it_child->second.m_effective_feerate.value() == expected_feerate);
std::vector<Wtxid> expected_wtxids({ptx_parent3->GetWitnessHash(), ptx_mixed_child->GetWitnessHash()});
BOOST_CHECK(it_parent3->second.m_wtxids_fee_calculations.value() == expected_wtxids);
BOOST_CHECK(it_child->second.m_wtxids_fee_calculations.value() == expected_wtxids);
}
}
}
BOOST_AUTO_TEST_CASE(package_cpfp_tests)
{
mineBlocks(5);
MockMempoolMinFee(CFeeRate(5000), *m_node.mempool);
LOCK(::cs_main);
size_t expected_pool_size = m_node.mempool->size();
CKey child_key = GenerateRandomKey();
CScript parent_spk = GetScriptForDestination(WitnessV0KeyHash(child_key.GetPubKey()));
CKey grandchild_key = GenerateRandomKey();
CScript child_spk = GetScriptForDestination(WitnessV0KeyHash(grandchild_key.GetPubKey()));
// low-fee parent and high-fee child package
const CAmount coinbase_value{50 * COIN};
const CAmount parent_value{coinbase_value - low_fee_amt};
const CAmount child_value{parent_value - COIN};
Package package_cpfp;
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_spk,
/*output_amount=*/parent_value, /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
package_cpfp.push_back(tx_parent);
auto mtx_child = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/child_key,
/*output_destination=*/child_spk,
/*output_amount=*/child_value, /*submit=*/false);
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
package_cpfp.push_back(tx_child);
// Package feerate is calculated using modified fees, and prioritisetransaction accepts negative
// fee deltas. This should be taken into account. De-prioritise the parent transaction
// to bring the package feerate to 0.
m_node.mempool->PrioritiseTransaction(tx_parent->GetHash(), child_value - coinbase_value);
{
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const auto submit_cpfp_deprio = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_cpfp, /*test_accept=*/ false, /*client_maxfeerate=*/{});
if (auto err_cpfp_deprio{CheckPackageMempoolAcceptResult(package_cpfp, submit_cpfp_deprio, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_cpfp_deprio.value());
} else {
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetResult(),
TxValidationResult::TX_MEMPOOL_POLICY);
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_tx_results.find(tx_child->GetWitnessHash())->second.m_state.GetResult(),
TxValidationResult::TX_MISSING_INPUTS);
BOOST_CHECK(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetWitnessHash())->second.m_state.GetRejectReason() == "min relay fee not met");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
}
// Clear the prioritisation of the parent transaction.
WITH_LOCK(m_node.mempool->cs, m_node.mempool->ClearPrioritisation(tx_parent->GetHash()));
// Package CPFP: Even though the parent's feerate is below the mempool minimum feerate, the
// child pays enough for the package feerate to meet the threshold.
{
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const auto submit_cpfp = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_cpfp, /*test_accept=*/ false, /*client_maxfeerate=*/{});
if (auto err_cpfp{CheckPackageMempoolAcceptResult(package_cpfp, submit_cpfp, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_cpfp.value());
} else {
auto it_parent = submit_cpfp.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_parent->second.m_base_fees.value() == coinbase_value - parent_value);
BOOST_CHECK(it_child->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_child->second.m_base_fees.value() == COIN);
const CFeeRate expected_feerate(coinbase_value - child_value,
GetVirtualTransactionSize(*tx_parent) + GetVirtualTransactionSize(*tx_child));
BOOST_CHECK(it_parent->second.m_effective_feerate.value() == expected_feerate);
BOOST_CHECK(it_child->second.m_effective_feerate.value() == expected_feerate);
std::vector<Wtxid> expected_wtxids({tx_parent->GetWitnessHash(), tx_child->GetWitnessHash()});
BOOST_CHECK(it_parent->second.m_wtxids_fee_calculations.value() == expected_wtxids);
BOOST_CHECK(it_child->second.m_wtxids_fee_calculations.value() == expected_wtxids);
BOOST_CHECK(expected_feerate.GetFeePerK() > 1000);
}
expected_pool_size += 2;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Just because we allow low-fee parents doesn't mean we allow low-feerate packages.
// The mempool minimum feerate is 5sat/vB, but this package just pays 800 satoshis total.
// The child fees would be able to pay for itself, but isn't enough for the entire package.
Package package_still_too_low;
const CAmount parent_fee{200};
const CAmount child_fee{600};
auto mtx_parent_cheap = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[1], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_spk,
/*output_amount=*/coinbase_value - parent_fee, /*submit=*/false);
CTransactionRef tx_parent_cheap = MakeTransactionRef(mtx_parent_cheap);
package_still_too_low.push_back(tx_parent_cheap);
BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*tx_parent_cheap)) > parent_fee);
BOOST_CHECK(m_node.mempool->m_opts.min_relay_feerate.GetFee(GetVirtualTransactionSize(*tx_parent_cheap)) <= parent_fee);
auto mtx_child_cheap = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent_cheap, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/child_key,
/*output_destination=*/child_spk,
/*output_amount=*/coinbase_value - parent_fee - child_fee, /*submit=*/false);
CTransactionRef tx_child_cheap = MakeTransactionRef(mtx_child_cheap);
package_still_too_low.push_back(tx_child_cheap);
BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*tx_child_cheap)) <= child_fee);
BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*tx_parent_cheap) + GetVirtualTransactionSize(*tx_child_cheap)) > parent_fee + child_fee);
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Cheap package should fail for being too low fee.
{
const auto submit_package_too_low = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_still_too_low, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_package_too_low{CheckPackageMempoolAcceptResult(package_still_too_low, submit_package_too_low, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_package_too_low.value());
} else {
// Individual feerate of parent is too low.
BOOST_CHECK_EQUAL(submit_package_too_low.m_tx_results.at(tx_parent_cheap->GetWitnessHash()).m_state.GetResult(),
TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK(submit_package_too_low.m_tx_results.at(tx_parent_cheap->GetWitnessHash()).m_effective_feerate.value() ==
CFeeRate(parent_fee, GetVirtualTransactionSize(*tx_parent_cheap)));
// Package feerate of parent + child is too low.
BOOST_CHECK_EQUAL(submit_package_too_low.m_tx_results.at(tx_child_cheap->GetWitnessHash()).m_state.GetResult(),
TxValidationResult::TX_RECONSIDERABLE);
BOOST_CHECK(submit_package_too_low.m_tx_results.at(tx_child_cheap->GetWitnessHash()).m_effective_feerate.value() ==
CFeeRate(parent_fee + child_fee, GetVirtualTransactionSize(*tx_parent_cheap) + GetVirtualTransactionSize(*tx_child_cheap)));
}
BOOST_CHECK_EQUAL(submit_package_too_low.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(submit_package_too_low.m_state.GetRejectReason(), "transaction failed");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Package feerate includes the modified fees of the transactions.
// This means a child with its fee delta from prioritisetransaction can pay for a parent.
m_node.mempool->PrioritiseTransaction(tx_child_cheap->GetHash(), 1 * COIN);
// Now that the child's fees have "increased" by 1 BTC, the cheap package should succeed.
{
const auto submit_prioritised_package = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_still_too_low, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_prioritised{CheckPackageMempoolAcceptResult(package_still_too_low, submit_prioritised_package, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_prioritised.value());
} else {
const CFeeRate expected_feerate(1 * COIN + parent_fee + child_fee,
GetVirtualTransactionSize(*tx_parent_cheap) + GetVirtualTransactionSize(*tx_child_cheap));
BOOST_CHECK_EQUAL(submit_prioritised_package.m_tx_results.size(), package_still_too_low.size());
auto it_parent = submit_prioritised_package.m_tx_results.find(tx_parent_cheap->GetWitnessHash());
auto it_child = submit_prioritised_package.m_tx_results.find(tx_child_cheap->GetWitnessHash());
BOOST_CHECK(it_parent->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_parent->second.m_base_fees.value() == parent_fee);
BOOST_CHECK(it_parent->second.m_effective_feerate.value() == expected_feerate);
BOOST_CHECK(it_child->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_child->second.m_base_fees.value() == child_fee);
BOOST_CHECK(it_child->second.m_effective_feerate.value() == expected_feerate);
std::vector<Wtxid> expected_wtxids({tx_parent_cheap->GetWitnessHash(), tx_child_cheap->GetWitnessHash()});
BOOST_CHECK(it_parent->second.m_wtxids_fee_calculations.value() == expected_wtxids);
BOOST_CHECK(it_child->second.m_wtxids_fee_calculations.value() == expected_wtxids);
}
expected_pool_size += 2;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
// Package feerate is calculated without topology in mind; it's just aggregating fees and sizes.
// However, this should not allow parents to pay for children. Each transaction should be
// validated individually first, eliminating sufficient-feerate parents before they are unfairly
// included in the package feerate. It's also important that the low-fee child doesn't prevent
// the parent from being accepted.
Package package_rich_parent;
const CAmount high_parent_fee{1 * COIN};
auto mtx_parent_rich = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[2], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_spk,
/*output_amount=*/coinbase_value - high_parent_fee, /*submit=*/false);
CTransactionRef tx_parent_rich = MakeTransactionRef(mtx_parent_rich);
package_rich_parent.push_back(tx_parent_rich);
auto mtx_child_poor = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent_rich, /*input_vout=*/0,
/*input_height=*/101, /*input_signing_key=*/child_key,
/*output_destination=*/child_spk,
/*output_amount=*/coinbase_value - high_parent_fee, /*submit=*/false);
CTransactionRef tx_child_poor = MakeTransactionRef(mtx_child_poor);
package_rich_parent.push_back(tx_child_poor);
// Parent pays 1 BTC and child pays none. The parent should be accepted without the child.
{
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const auto submit_rich_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
package_rich_parent, /*test_accept=*/false, /*client_maxfeerate=*/{});
if (auto err_rich_parent{CheckPackageMempoolAcceptResult(package_rich_parent, submit_rich_parent, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_rich_parent.value());
} else {
// The child would have been validated on its own and failed.
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetRejectReason(), "transaction failed");
auto it_parent = submit_rich_parent.m_tx_results.find(tx_parent_rich->GetWitnessHash());
auto it_child = submit_rich_parent.m_tx_results.find(tx_child_poor->GetWitnessHash());
BOOST_CHECK(it_parent->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_child->second.m_result_type == MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK(it_parent->second.m_state.GetRejectReason() == "");
BOOST_CHECK_MESSAGE(it_parent->second.m_base_fees.value() == high_parent_fee,
strprintf("rich parent: expected fee %s, got %s", high_parent_fee, it_parent->second.m_base_fees.value()));
BOOST_CHECK(it_parent->second.m_effective_feerate == CFeeRate(high_parent_fee, GetVirtualTransactionSize(*tx_parent_rich)));
BOOST_CHECK_EQUAL(it_child->second.m_result_type, MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MEMPOOL_POLICY);
BOOST_CHECK(it_child->second.m_state.GetRejectReason() == "min relay fee not met");
}
expected_pool_size += 1;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
}
}
BOOST_AUTO_TEST_CASE(package_rbf_tests)
{
mineBlocks(5);
LOCK(::cs_main);
size_t expected_pool_size = m_node.mempool->size();
CKey child_key{GenerateRandomKey()};
CScript parent_spk = GetScriptForDestination(WitnessV0KeyHash(child_key.GetPubKey()));
CKey grandchild_key{GenerateRandomKey()};
CScript child_spk = GetScriptForDestination(WitnessV0KeyHash(grandchild_key.GetPubKey()));
const CAmount coinbase_value{50 * COIN};
// Test that de-duplication works. This is not actually package rbf.
{
// 1 parent paying 200sat, 1 child paying 300sat
Package package1;
// 1 parent paying 200sat, 1 child paying 500sat
Package package2;
// Package1 and package2 have the same parent. The children conflict.
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/parent_spk,
/*output_amount=*/coinbase_value - low_fee_amt, /*submit=*/false);
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
package1.push_back(tx_parent);
package2.push_back(tx_parent);
CTransactionRef tx_child_1 = MakeTransactionRef(CreateValidMempoolTransaction(tx_parent, 0, 101, child_key, child_spk, coinbase_value - low_fee_amt - 300, false));
package1.push_back(tx_child_1);
CTransactionRef tx_child_2 = MakeTransactionRef(CreateValidMempoolTransaction(tx_parent, 0, 101, child_key, child_spk, coinbase_value - low_fee_amt - 500, false));
package2.push_back(tx_child_2);
LOCK(m_node.mempool->cs);
const auto submit1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, /*test_accept=*/false, std::nullopt);
if (auto err_1{CheckPackageMempoolAcceptResult(package1, submit1, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_1.value());
}
// Check precise ResultTypes and mempool size. We know it_parent_1 and it_child_1 exist from above call
auto it_parent_1 = submit1.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child_1 = submit1.m_tx_results.find(tx_child_1->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(it_child_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
expected_pool_size += 2;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const auto submit2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package2, /*test_accept=*/false, std::nullopt);
if (auto err_2{CheckPackageMempoolAcceptResult(package2, submit2, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_2.value());
}
// Check precise ResultTypes and mempool size. We know it_parent_2 and it_child_2 exist from above call
auto it_parent_2 = submit2.m_tx_results.find(tx_parent->GetWitnessHash());
auto it_child_2 = submit2.m_tx_results.find(tx_child_2->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_2->second.m_result_type, MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
BOOST_CHECK_EQUAL(it_child_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// child1 has been replaced
BOOST_CHECK(!m_node.mempool->exists(tx_child_1->GetHash()));
}
// Test package rbf.
{
CTransactionRef tx_parent_1 = MakeTransactionRef(CreateValidMempoolTransaction(
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
coinbaseKey, parent_spk, coinbase_value - 200, /*submit=*/false));
CTransactionRef tx_child_1 = MakeTransactionRef(CreateValidMempoolTransaction(
tx_parent_1, /*input_vout=*/0, /*input_height=*/101,
child_key, child_spk, coinbase_value - 400, /*submit=*/false));
CTransactionRef tx_parent_2 = MakeTransactionRef(CreateValidMempoolTransaction(
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
coinbaseKey, parent_spk, coinbase_value - 800, /*submit=*/false));
CTransactionRef tx_child_2 = MakeTransactionRef(CreateValidMempoolTransaction(
tx_parent_2, /*input_vout=*/0, /*input_height=*/101,
child_key, child_spk, coinbase_value - 800 - 200, /*submit=*/false));
CTransactionRef tx_parent_3 = MakeTransactionRef(CreateValidMempoolTransaction(
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
coinbaseKey, parent_spk, coinbase_value - 199, /*submit=*/false));
CTransactionRef tx_child_3 = MakeTransactionRef(CreateValidMempoolTransaction(
tx_parent_3, /*input_vout=*/0, /*input_height=*/101,
child_key, child_spk, coinbase_value - 199 - 1300, /*submit=*/false));
// In all packages, the parents conflict with each other
BOOST_CHECK(tx_parent_1->GetHash() != tx_parent_2->GetHash() && tx_parent_2->GetHash() != tx_parent_3->GetHash());
// 1 parent paying 200sat, 1 child paying 200sat.
Package package1{tx_parent_1, tx_child_1};
// 1 parent paying 800sat, 1 child paying 200sat.
Package package2{tx_parent_2, tx_child_2};
// 1 parent paying 199sat, 1 child paying 1300sat.
Package package3{tx_parent_3, tx_child_3};
const auto submit1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, false, std::nullopt);
if (auto err_1{CheckPackageMempoolAcceptResult(package1, submit1, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_1.value());
}
auto it_parent_1 = submit1.m_tx_results.find(tx_parent_1->GetWitnessHash());
auto it_child_1 = submit1.m_tx_results.find(tx_child_1->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(it_child_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
expected_pool_size += 2;
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// This replacement is actually not package rbf; the parent carries enough fees
// to replace the entire package on its own.
const auto submit2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package2, false, std::nullopt);
if (auto err_2{CheckPackageMempoolAcceptResult(package2, submit2, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_2.value());
}
auto it_parent_2 = submit2.m_tx_results.find(tx_parent_2->GetWitnessHash());
auto it_child_2 = submit2.m_tx_results.find(tx_child_2->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(it_child_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Package RBF, in which the replacement transaction's child sponsors the fees to meet RBF feerate rules
const auto submit3 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package3, false, std::nullopt);
if (auto err_3{CheckPackageMempoolAcceptResult(package3, submit3, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_3.value());
}
auto it_parent_3 = submit3.m_tx_results.find(tx_parent_3->GetWitnessHash());
auto it_child_3 = submit3.m_tx_results.find(tx_child_3->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_3->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(it_child_3->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
// package3 was considered as a package to replace both package2 transactions
BOOST_CHECK(it_parent_3->second.m_replaced_transactions.size() == 2);
BOOST_CHECK(it_child_3->second.m_replaced_transactions.empty());
std::vector<Wtxid> expected_package3_wtxids({tx_parent_3->GetWitnessHash(), tx_child_3->GetWitnessHash()});
const auto package3_total_vsize{GetVirtualTransactionSize(*tx_parent_3) + GetVirtualTransactionSize(*tx_child_3)};
BOOST_CHECK(it_parent_3->second.m_wtxids_fee_calculations.value() == expected_package3_wtxids);
BOOST_CHECK(it_child_3->second.m_wtxids_fee_calculations.value() == expected_package3_wtxids);
BOOST_CHECK_EQUAL(it_parent_3->second.m_effective_feerate.value().GetFee(package3_total_vsize), 199 + 1300);
BOOST_CHECK_EQUAL(it_child_3->second.m_effective_feerate.value().GetFee(package3_total_vsize), 199 + 1300);
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
// Finally, check that we can prioritise tx_child_1 to get package1 into the mempool.
// It should not be possible to resubmit package1 and get it in without prioritisation.
const auto submit4 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, false, std::nullopt);
if (auto err_4{CheckPackageMempoolAcceptResult(package1, submit4, /*expect_valid=*/false, m_node.mempool.get())}) {
BOOST_ERROR(err_4.value());
}
m_node.mempool->PrioritiseTransaction(tx_child_1->GetHash(), 1363);
const auto submit5 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, false, std::nullopt);
if (auto err_5{CheckPackageMempoolAcceptResult(package1, submit5, /*expect_valid=*/true, m_node.mempool.get())}) {
BOOST_ERROR(err_5.value());
}
it_parent_1 = submit5.m_tx_results.find(tx_parent_1->GetWitnessHash());
it_child_1 = submit5.m_tx_results.find(tx_child_1->GetWitnessHash());
BOOST_CHECK_EQUAL(it_parent_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK_EQUAL(it_child_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
LOCK(m_node.mempool->cs);
BOOST_CHECK(m_node.mempool->GetIter(tx_parent_1->GetHash()).has_value());
BOOST_CHECK(m_node.mempool->GetIter(tx_child_1->GetHash()).has_value());
}
}
BOOST_AUTO_TEST_SUITE_END()