diff --git a/src/consensus/validation.h b/src/consensus/validation.h --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -42,6 +42,11 @@ TX_MEMPOOL_POLICY, //! this node does not have a mempool so can't validate the transaction TX_NO_MEMPOOL, + //! fails some policy, but might be acceptable if submitted in a (different) + //! package + TX_RECONSIDERABLE, + //! transaction was not validated because package failed + TX_UNKNOWN, }; /** diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -2557,6 +2557,8 @@ case TxValidationResult::TX_CHILD_BEFORE_PARENT: case TxValidationResult::TX_MEMPOOL_POLICY: case TxValidationResult::TX_NO_MEMPOOL: + case TxValidationResult::TX_RECONSIDERABLE: + case TxValidationResult::TX_UNKNOWN: break; } if (message != "") { diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -55,6 +55,7 @@ util/setup_common.cpp util/str.cpp util/transaction_utils.cpp + util/txmempool.cpp util/validation.cpp util/wallet.cpp ) diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp --- a/src/test/txpackage_tests.cpp +++ b/src/test/txpackage_tests.cpp @@ -14,6 +14,7 @@ #include <test/util/random.h> #include <test/util/setup_common.h> +#include <test/util/txmempool.h> #include <boost/test/unit_test.hpp> @@ -99,59 +100,60 @@ /* output_destination */ child_locking_script, /* output_amount */ Amount(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, - {tx_parent, tx_child}, /* test_accept */ true); - BOOST_CHECK_MESSAGE(result_parent_child.m_state.IsValid(), - "Package validation unexpectedly failed: " - << result_parent_child.m_state.GetRejectReason()); - BOOST_CHECK(result_parent_child.m_tx_results.size() == 2); - auto it_parent = result_parent_child.m_tx_results.find(tx_parent->GetId()); - auto it_child = result_parent_child.m_tx_results.find(tx_child->GetId()); - BOOST_CHECK(it_parent != result_parent_child.m_tx_results.end()); - BOOST_CHECK_MESSAGE(it_parent->second.m_state.IsValid(), - "Package validation unexpectedly failed: " - << it_parent->second.m_state.GetRejectReason()); - BOOST_CHECK_EQUAL( - it_parent->second.m_effective_feerate.value().GetFeeCeiling( - GetVirtualTransactionSize(*tx_parent)), - COIN); - BOOST_CHECK_EQUAL(it_parent->second.m_txids_fee_calculations.value().size(), - 1); - BOOST_CHECK_EQUAL( - it_parent->second.m_txids_fee_calculations.value().front(), - tx_parent->GetId()); - BOOST_CHECK(it_child != result_parent_child.m_tx_results.end()); - BOOST_CHECK_MESSAGE(it_child->second.m_state.IsValid(), - "Package validation unexpectedly failed: " - << it_child->second.m_state.GetRejectReason()); - BOOST_CHECK_EQUAL( - it_child->second.m_effective_feerate.value().GetFeeCeiling( - GetVirtualTransactionSize(*tx_child)), - COIN); - BOOST_CHECK_EQUAL(it_child->second.m_txids_fee_calculations.value().size(), - 1); - BOOST_CHECK_EQUAL(it_child->second.m_txids_fee_calculations.value().front(), - tx_child->GetId()); + package_parent_child, /*test_accept=*/true); + 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->GetId()); + auto it_child = + result_parent_child.m_tx_results.find(tx_child->GetId()); + + BOOST_CHECK(it_parent->second.m_effective_feerate.value().GetFeeCeiling( + GetVirtualTransactionSize(*tx_parent)) == COIN); + BOOST_CHECK_EQUAL( + it_parent->second.m_txids_fee_calculations.value().size(), 1); + BOOST_CHECK_EQUAL( + it_parent->second.m_txids_fee_calculations.value().front(), + tx_parent->GetId()); + + BOOST_CHECK(it_child->second.m_effective_feerate.value().GetFeeCeiling( + GetVirtualTransactionSize(*tx_child)) == COIN); + BOOST_CHECK_EQUAL( + it_child->second.m_txids_fee_calculations.value().size(), 1); + BOOST_CHECK_EQUAL( + it_child->second.m_txids_fee_calculations.value().front(), + tx_child->GetId()); + } // 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) > MAX_PACKAGE_SIZE * 1000); + Package package_single_giant{giant_ptx}; auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - {giant_ptx}, /* test_accept */ true); - BOOST_CHECK(result_single_large.m_state.IsInvalid()); - BOOST_CHECK_EQUAL(result_single_large.m_state.GetResult(), - PackageValidationResult::PCKG_TX); - BOOST_CHECK_EQUAL(result_single_large.m_state.GetRejectReason(), - "transaction failed"); - BOOST_CHECK(result_single_large.m_tx_results.size() == 1); - auto it_giant_tx = - result_single_large.m_tx_results.find(giant_ptx->GetId()); - BOOST_CHECK(it_giant_tx != result_single_large.m_tx_results.end()); - BOOST_CHECK_EQUAL(it_giant_tx->second.m_state.GetRejectReason(), "tx-size"); + package_single_giant, /*test_accept=*/true); + 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->GetId()); + 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); @@ -285,6 +287,8 @@ auto result_unrelated_submit = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_unrelated, /*test_accept=*/false); + // 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); @@ -363,32 +367,34 @@ CTransactionRef tx_child_of_invalid_parent = MakeTransactionRef(mtx_child_of_invalid_parent); + Package package_invalid_parent{tx_parent_invalid, + tx_child_of_invalid_parent}; auto result_quit_early = WITH_LOCK( cs_main, return ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, - {tx_parent_invalid, tx_child_of_invalid_parent}, - /*test_accept=*/false)); - BOOST_CHECK(result_quit_early.m_state.IsInvalid()); + package_invalid_parent, /*test_accept=*/false)); + 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->GetId()); + auto it_child = result_quit_early.m_tx_results.find( + tx_child_of_invalid_parent->GetId()); + BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(), + TxValidationResult::TX_CONSENSUS); + BOOST_CHECK_EQUAL( + it_parent->second.m_state.GetRejectReason(), + "mandatory-script-verify-flag-failed (Script evaluated without " + "error but finished with a false/empty top stack element)"); + 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); - BOOST_CHECK(!result_quit_early.m_tx_results.empty()); - BOOST_CHECK_EQUAL(result_quit_early.m_tx_results.size(), 2); - auto it_parent = - result_quit_early.m_tx_results.find(tx_parent_invalid->GetId()); - auto it_child = result_quit_early.m_tx_results.find( - tx_child_of_invalid_parent->GetId()); - BOOST_CHECK(it_parent != result_quit_early.m_tx_results.end()); - BOOST_CHECK(it_child != result_quit_early.m_tx_results.end()); - BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(), - TxValidationResult::TX_CONSENSUS); - BOOST_CHECK_EQUAL( - it_parent->second.m_state.GetRejectReason(), - "mandatory-script-verify-flag-failed (Script evaluated without " - "error but finished with a false/empty top stack element)"); - 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"); } // Child with missing parent. @@ -435,8 +441,6 @@ BOOST_CHECK_EQUAL( it_parent->second.m_txids_fee_calculations.value().front(), tx_parent->GetId()); - BOOST_CHECK(it_child != submit_parent_child.m_tx_results.end()); - BOOST_CHECK(it_child->second.m_state.IsValid()); BOOST_CHECK(it_child->second.m_effective_feerate == CFeeRate(1 * COIN, GetVirtualTransactionSize(*tx_child))); BOOST_CHECK_EQUAL( @@ -446,8 +450,6 @@ tx_child->GetId()); BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); - BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId())); - BOOST_CHECK(m_node.mempool->exists(tx_child->GetId())); } // Already-in-mempool transactions should be detected and de-duplicated. @@ -456,27 +458,21 @@ const auto submit_deduped = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_parent_child, /*test_accept=*/false); - BOOST_CHECK_MESSAGE(submit_deduped.m_state.IsValid(), - "Package validation unexpectedly failed: " - << submit_deduped.m_state.GetRejectReason()); - BOOST_CHECK_EQUAL(submit_deduped.m_tx_results.size(), - package_parent_child.size()); - auto it_parent_deduped = - submit_deduped.m_tx_results.find(tx_parent->GetId()); - auto it_child_deduped = - submit_deduped.m_tx_results.find(tx_child->GetId()); - BOOST_CHECK(it_parent_deduped != submit_deduped.m_tx_results.end()); - BOOST_CHECK(it_parent_deduped->second.m_state.IsValid()); - BOOST_CHECK(it_parent_deduped->second.m_result_type == - MempoolAcceptResult::ResultType::MEMPOOL_ENTRY); - BOOST_CHECK(it_child_deduped != submit_deduped.m_tx_results.end()); - BOOST_CHECK(it_child_deduped->second.m_state.IsValid()); - BOOST_CHECK(it_child_deduped->second.m_result_type == - MempoolAcceptResult::ResultType::MEMPOOL_ENTRY); - + 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->GetId()); + auto it_child_deduped = + submit_deduped.m_tx_results.find(tx_child->GetId()); + 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); - BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId())); - BOOST_CHECK(m_node.mempool->exists(tx_child->GetId())); } } @@ -543,44 +539,41 @@ const auto mixed_result = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_mixed, false); - BOOST_CHECK_MESSAGE(mixed_result.m_state.IsValid(), - mixed_result.m_state.GetRejectReason()); - BOOST_CHECK_EQUAL(mixed_result.m_tx_results.size(), - package_mixed.size()); - auto it_parent1 = mixed_result.m_tx_results.find(ptx_parent1->GetId()); - auto it_parent2 = mixed_result.m_tx_results.find(ptx_parent2->GetId()); - auto it_child = - mixed_result.m_tx_results.find(ptx_mixed_child->GetId()); - BOOST_CHECK(it_parent1 != mixed_result.m_tx_results.end()); - BOOST_CHECK(it_parent2 != mixed_result.m_tx_results.end()); - BOOST_CHECK(it_child != mixed_result.m_tx_results.end()); - - BOOST_CHECK(it_parent1->second.m_result_type == - MempoolAcceptResult::ResultType::MEMPOOL_ENTRY); - BOOST_CHECK(it_parent2->second.m_result_type == - MempoolAcceptResult::ResultType::VALID); - BOOST_CHECK(it_child->second.m_result_type == - MempoolAcceptResult::ResultType::VALID); - - BOOST_CHECK(m_node.mempool->exists(ptx_parent1->GetId())); - BOOST_CHECK(m_node.mempool->exists(ptx_parent2->GetId())); - BOOST_CHECK(m_node.mempool->exists(ptx_mixed_child->GetId())); - - // package feerate should include parent2 and child. It should not - // include parent1. - const CFeeRate expected_feerate( - 1 * COIN, GetVirtualTransactionSize(*ptx_parent2) + - GetVirtualTransactionSize(*ptx_mixed_child)); - BOOST_CHECK(it_parent2->second.m_effective_feerate.value() == - expected_feerate); - BOOST_CHECK(it_child->second.m_effective_feerate.value() == - expected_feerate); - std::vector<TxId> expected_txids( - {ptx_parent2->GetId(), ptx_mixed_child->GetId()}); - BOOST_CHECK(it_parent2->second.m_txids_fee_calculations.value() == - expected_txids); - BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == - expected_txids); + 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->GetId()); + auto it_parent2 = + mixed_result.m_tx_results.find(ptx_parent2->GetId()); + auto it_child = + mixed_result.m_tx_results.find(ptx_mixed_child->GetId()); + + BOOST_CHECK(it_parent1->second.m_result_type == + MempoolAcceptResult::ResultType::MEMPOOL_ENTRY); + BOOST_CHECK(it_parent2->second.m_result_type == + MempoolAcceptResult::ResultType::VALID); + BOOST_CHECK(it_child->second.m_result_type == + MempoolAcceptResult::ResultType::VALID); + + // package feerate should include parent2 and child. It should not + // include parent1. + const CFeeRate expected_feerate( + 1 * COIN, GetVirtualTransactionSize(*ptx_parent2) + + GetVirtualTransactionSize(*ptx_mixed_child)); + BOOST_CHECK(it_parent2->second.m_effective_feerate.value() == + expected_feerate); + BOOST_CHECK(it_child->second.m_effective_feerate.value() == + expected_feerate); + std::vector<TxId> expected_txids( + {ptx_parent2->GetId(), ptx_mixed_child->GetId()}); + BOOST_CHECK(it_parent2->second.m_txids_fee_calculations.value() == + expected_txids); + BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == + expected_txids); + } } } @@ -630,26 +623,27 @@ const auto submit_cpfp_deprio = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_cpfp, /*test_accept=*/false); - - BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(), - PackageValidationResult::PCKG_TX); - BOOST_CHECK(submit_cpfp_deprio.m_state.IsInvalid()); - BOOST_CHECK_EQUAL( - submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId()) - ->second.m_state.GetResult(), - TxValidationResult::TX_MEMPOOL_POLICY); - BOOST_CHECK_EQUAL( - submit_cpfp_deprio.m_tx_results.find(tx_child->GetId()) - ->second.m_state.GetResult(), - TxValidationResult::TX_MISSING_INPUTS); - BOOST_CHECK(submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId()) - ->second.m_state.GetRejectReason() == - "min relay fee not met"); - - BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); - const CFeeRate expected_feerate( - Amount::zero(), GetVirtualTransactionSize(*tx_parent) + - GetVirtualTransactionSize(*tx_child)); + 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->GetId()) + ->second.m_state.GetResult(), + TxValidationResult::TX_MEMPOOL_POLICY); + BOOST_CHECK_EQUAL( + submit_cpfp_deprio.m_tx_results.find(tx_child->GetId()) + ->second.m_state.GetResult(), + TxValidationResult::TX_MISSING_INPUTS); + BOOST_CHECK( + submit_cpfp_deprio.m_tx_results.find(tx_parent->GetId()) + ->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. @@ -664,42 +658,39 @@ const auto submit_cpfp = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_cpfp, /*test_accept=*/false); + 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->GetId()); + auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetId()); + 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<TxId> expected_txids( + {tx_parent->GetId(), tx_child->GetId()}); + BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() == + expected_txids); + BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == + expected_txids); + BOOST_CHECK(expected_feerate.GetFeePerK() > 1000 * SATOSHI); + } expected_pool_size += 2; - BOOST_CHECK_MESSAGE(submit_cpfp.m_state.IsValid(), - "Package validation unexpectedly failed: " - << submit_cpfp.m_state.GetRejectReason()); - BOOST_CHECK_EQUAL(submit_cpfp.m_tx_results.size(), package_cpfp.size()); - auto it_parent = submit_cpfp.m_tx_results.find(tx_parent->GetId()); - auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetId()); - BOOST_CHECK(it_parent != submit_cpfp.m_tx_results.end()); - 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 != submit_cpfp.m_tx_results.end()); - BOOST_CHECK(it_child->second.m_result_type == - MempoolAcceptResult::ResultType::VALID); - BOOST_CHECK(it_child->second.m_base_fees.value() == COIN); - BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); - BOOST_CHECK(m_node.mempool->exists(tx_parent->GetId())); - BOOST_CHECK(m_node.mempool->exists(tx_child->GetId())); - - 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<TxId> expected_txids( - {tx_parent->GetId(), tx_child->GetId()}); - BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() == - expected_txids); - BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == - expected_txids); - BOOST_CHECK(expected_feerate.GetFeePerK() > 1000 * SATOSHI); } // Just because we allow low-fee parents doesn't mean we allow low-feerate @@ -729,26 +720,50 @@ /*submit=*/false); CTransactionRef tx_child_cheap = MakeTransactionRef(mtx_child_cheap); package_still_too_low.push_back(tx_child_cheap); - std::cout << GetVirtualTransactionSize(*tx_child_cheap) << std::endl; 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 with package-fee-too-low. + // Cheap package should fail for being too low fee. { - BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); const auto submit_package_too_low = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_still_too_low, /* test_accept */ false); - BOOST_CHECK_MESSAGE(submit_package_too_low.m_state.IsInvalid(), - "Package validation unexpectedly succeeded"); + 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->GetId()) + .m_state.GetResult(), + TxValidationResult::TX_RECONSIDERABLE); + BOOST_CHECK( + submit_package_too_low.m_tx_results.at(tx_parent_cheap->GetId()) + .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->GetId()) + .m_state.GetResult(), + TxValidationResult::TX_RECONSIDERABLE); + BOOST_CHECK( + submit_package_too_low.m_tx_results.at(tx_child_cheap->GetId()) + .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_POLICY); + PackageValidationResult::PCKG_TX); BOOST_CHECK_EQUAL(submit_package_too_low.m_state.GetRejectReason(), - "package-fee-too-low"); + "transaction failed"); BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); } @@ -762,39 +777,40 @@ const auto submit_prioritised_package = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_still_too_low, /*test_accept=*/false); + 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->GetId()); + auto it_child = submit_prioritised_package.m_tx_results.find( + tx_child_cheap->GetId()); + 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<TxId> expected_txids( + {tx_parent_cheap->GetId(), tx_child_cheap->GetId()}); + BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() == + expected_txids); + BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == + expected_txids); + } expected_pool_size += 2; - BOOST_CHECK_MESSAGE( - submit_prioritised_package.m_state.IsValid(), - "Package validation unexpectedly failed" - << submit_prioritised_package.m_state.GetRejectReason()); - 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->GetId()); - auto it_child = submit_prioritised_package.m_tx_results.find( - tx_child_cheap->GetId()); - BOOST_CHECK(it_parent != submit_prioritised_package.m_tx_results.end()); - 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 != submit_prioritised_package.m_tx_results.end()); - 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<TxId> expected_txids( - {tx_parent_cheap->GetId(), tx_child_cheap->GetId()}); - BOOST_CHECK(it_parent->second.m_txids_fee_calculations.value() == - expected_txids); - BOOST_CHECK(it_child->second.m_txids_fee_calculations.value() == - expected_txids); + BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); } // Package feerate is calculated without topology in mind; it's just @@ -828,43 +844,43 @@ const auto submit_rich_parent = ProcessNewPackage( m_node.chainman->ActiveChainstate(), *m_node.mempool, package_rich_parent, /* test_accept */ false); + 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->GetId()); + auto it_child = + submit_rich_parent.m_tx_results.find(tx_child_poor->GetId()); + 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_MESSAGE(submit_rich_parent.m_state.IsInvalid(), - "Package validation unexpectedly succeeded"); - - // The child would have been validated on its own and failed, then - // submitted as a "package" of 1. - 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->GetId()); - BOOST_CHECK(it_parent != submit_rich_parent.m_tx_results.end()); - BOOST_CHECK(it_parent->second.m_result_type == - MempoolAcceptResult::ResultType::VALID); - 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))); - auto it_child = - submit_rich_parent.m_tx_results.find(tx_child_poor->GetId()); - BOOST_CHECK(it_child != submit_rich_parent.m_tx_results.end()); - 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"); - BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); - BOOST_CHECK(m_node.mempool->exists(tx_parent_rich->GetId())); - BOOST_CHECK(!m_node.mempool->exists(tx_child_poor->GetId())); } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/txmempool.h b/src/test/util/txmempool.h new file mode 100644 --- /dev/null +++ b/src/test/util/txmempool.h @@ -0,0 +1,23 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_TEST_UTIL_TXMEMPOOL_H +#define BITCOIN_TEST_UTIL_TXMEMPOOL_H + +#include <policy/packages.h> +#include <txmempool.h> + +struct PackageMempoolAcceptResult; + +/** + * Check expected properties for every PackageMempoolAcceptResult, regardless of + * value. Returns a string if an error occurs with error populated, nullopt + * otherwise. If mempool is provided, checks that the expected transactions are + * in mempool (this should be set to nullptr for a test_accept). + */ +std::optional<std::string> +CheckPackageMempoolAcceptResult(const Package &txns, + const PackageMempoolAcceptResult &result, + bool expect_valid, const CTxMemPool *mempool); +#endif // BITCOIN_TEST_UTIL_TXMEMPOOL_H diff --git a/src/test/util/txmempool.cpp b/src/test/util/txmempool.cpp new file mode 100644 --- /dev/null +++ b/src/test/util/txmempool.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2022 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 <test/util/txmempool.h> + +#include <node/context.h> +#include <txmempool.h> +#include <validation.h> + +std::optional<std::string> +CheckPackageMempoolAcceptResult(const Package &txns, + const PackageMempoolAcceptResult &result, + bool expect_valid, const CTxMemPool *mempool) { + if (expect_valid) { + if (result.m_state.IsInvalid()) { + return strprintf("Package validation unexpectedly failed: %s", + result.m_state.ToString()); + } + } else { + if (result.m_state.IsValid()) { + return strprintf("Package validation unexpectedly succeeded. %s", + result.m_state.ToString()); + } + } + if (result.m_state.GetResult() != PackageValidationResult::PCKG_POLICY && + txns.size() != result.m_tx_results.size()) { + return strprintf("txns size %u does not match tx results size %u", + txns.size(), result.m_tx_results.size()); + } + for (const auto &tx : txns) { + const auto &txid = tx->GetId(); + if (result.m_tx_results.count(txid) == 0) { + return strprintf("result not found for tx %s", txid.ToString()); + } + + const auto &atmp_result = result.m_tx_results.at(txid); + const bool valid{atmp_result.m_result_type == + MempoolAcceptResult::ResultType::VALID}; + if (expect_valid && atmp_result.m_state.IsInvalid()) { + return strprintf("tx %s unexpectedly failed: %s", txid.ToString(), + atmp_result.m_state.ToString()); + } + + // m_vsize and m_base_fees should exist iff the result was VALID or + // MEMPOOL_ENTRY + const bool mempool_entry{ + atmp_result.m_result_type == + MempoolAcceptResult::ResultType::MEMPOOL_ENTRY}; + if (atmp_result.m_base_fees.has_value() != (valid || mempool_entry)) { + return strprintf("tx %s result should %shave m_base_fees", + txid.ToString(), + valid || mempool_entry ? "" : "not "); + } + if (atmp_result.m_vsize.has_value() != (valid || mempool_entry)) { + return strprintf("tx %s result should %shave m_vsize", + txid.ToString(), + valid || mempool_entry ? "" : "not "); + } + + // m_effective_feerate and m_txids_fee_calculations should exist iff the + // result was valid or if the failure was TX_RECONSIDERABLE + const bool valid_or_reconsiderable{ + atmp_result.m_result_type == + MempoolAcceptResult::ResultType::VALID || + atmp_result.m_state.GetResult() == + TxValidationResult::TX_RECONSIDERABLE}; + if (atmp_result.m_effective_feerate.has_value() != + valid_or_reconsiderable) { + return strprintf("tx %s result should %shave m_effective_feerate", + txid.ToString(), valid ? "" : "not "); + } + if (atmp_result.m_txids_fee_calculations.has_value() != + valid_or_reconsiderable) { + return strprintf("tx %s result should %shave m_effective_feerate", + txid.ToString(), valid ? "" : "not "); + } + + if (mempool) { + // The tx by txid should be in the mempool iff the result was not + // INVALID. + const bool txid_in_mempool{ + atmp_result.m_result_type != + MempoolAcceptResult::ResultType::INVALID}; + if (mempool->exists(tx->GetId()) != txid_in_mempool) { + return strprintf("tx %s should %sbe in mempool", + txid.ToString(), + txid_in_mempool ? "" : "not "); + } + } + } + return std::nullopt; +} diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -179,9 +179,30 @@ void PruneBlockFilesManual(Chainstate &active_chainstate, int nManualPruneHeight); +// clang-format off /** - * Validation result for a single transaction mempool acceptance. + * Validation result for a transaction evaluated by MemPoolAccept (single or + * package). + * Here are the expected fields and properties of a result depending on its + * ResultType, applicable to results returned from package evaluation: + *+--------------------------+-----------+---------------------------------+----------------+ + *| Field or property | VALID | INVALID | MEMPOOL_ENTRY | + *| | |---------------------------------| | + *| | | TX_RECONSIDERABLE | Other | | + *+--------------------------+-----------+-------------------+-------------+----------------+ + *| txid in mempool? | yes | no | no* | yes | + *| m_state | IsValid() | IsInvalid() | IsInvalid() | IsValid() | + *| m_replaced_transactions | yes | no | no | no | + *| m_vsize | yes | no | no | yes | + *| m_base_fees | yes | no | no | yes | + *| m_effective_feerate | yes | yes | no | no | + *| m_txids_fee_calculations | yes | yes | no | no | + *+--------------------------+-----------+-------------------+-------------+----------------+ + * (*) Individual transaction acceptance doesn't return MEMPOOL_ENTRY. It + * returns INVALID, with the error txn-already-in-mempool. In this case, the + * txid may be in the mempool for a TX_CONFLICT. */ +// clang-format on struct MempoolAcceptResult { /** Used to indicate the results of mempool validation. */ enum class ResultType { @@ -198,8 +219,6 @@ /** Contains information about why the transaction failed. */ const TxValidationState m_state; - // The following fields are only present when m_result_type = - // ResultType::VALID or MEMPOOL_ENTRY /** * Virtual size as used by the mempool, calculated using serialized size * and sigchecks. @@ -212,8 +231,7 @@ * fee delta added using prioritisetransaction (i.e. modified fees). If this * transaction was submitted as a package, this is the package feerate, * which may also include its descendants and/or ancestors - * (see m_txids_fee_calculations below). Only present when - * m_result_type = ResultType::VALID. + * (see m_txids_fee_calculations below). */ const std::optional<CFeeRate> m_effective_feerate; /** @@ -221,13 +239,20 @@ * Includes this transaction's txid and may include others if this * transaction was validated as part of a package. This is not necessarily * equivalent to the list of transactions passed to ProcessNewPackage(). - * Only present when m_result_type = ResultType::VALID. */ + */ const std::optional<std::vector<TxId>> m_txids_fee_calculations; static MempoolAcceptResult Failure(TxValidationState state) { return MempoolAcceptResult(state); } + static MempoolAcceptResult + FeeFailure(TxValidationState state, CFeeRate effective_feerate, + const std::vector<TxId> &txids_fee_calculations) { + return MempoolAcceptResult(state, effective_feerate, + txids_fee_calculations); + } + /** Constructor for success case */ static MempoolAcceptResult Success(int64_t vsize, Amount fees, CFeeRate effective_feerate, @@ -264,6 +289,14 @@ m_effective_feerate(effective_feerate), m_txids_fee_calculations(txids_fee_calculations) {} + /** Constructor for fee-related failure case */ + explicit MempoolAcceptResult( + TxValidationState state, CFeeRate effective_feerate, + const std::vector<TxId> &txids_fee_calculations) + : m_result_type(ResultType::INVALID), m_state(state), + m_effective_feerate(effective_feerate), + m_txids_fee_calculations(txids_fee_calculations) {} + /** Constructor for already-in-mempool case. */ explicit MempoolAcceptResult(int64_t vsize, Amount fees) : m_result_type(ResultType::MEMPOOL_ENTRY), m_vsize{vsize}, diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -529,7 +529,7 @@ if (mempoolRejectFee > Amount::zero() && package_fee < mempoolRejectFee) { return state.Invalid( - TxValidationResult::TX_MEMPOOL_POLICY, + TxValidationResult::TX_RECONSIDERABLE, "mempool min fee not met", strprintf("%d < %d", package_fee, mempoolRejectFee)); } @@ -538,7 +538,7 @@ // policy upgrade. if (package_fee < m_pool.m_min_relay_feerate.GetFee(package_size)) { return state.Invalid( - TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met", + TxValidationResult::TX_RECONSIDERABLE, "min relay fee not met", strprintf("%d < %d", package_fee, m_pool.m_min_relay_feerate.GetFee(package_size))); } @@ -726,6 +726,9 @@ if (!bypass_limits && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(ws.m_ptx->GetTotalSize())) { + // Even though this is a fee-related failure, this result is + // TX_MEMPOOL_POLICY, not TX_RECONSIDERABLE, because it cannot be + // bypassed using package validation. return state.Invalid( TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met", strprintf("%d < %d", ws.m_modified_fees, @@ -806,7 +809,9 @@ if (!args.m_package_submission && !bypass_limits) { m_pool.LimitSize(m_active_chainstate.CoinsTip()); if (!m_pool.exists(txid)) { - return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, + // The tx no longer meets our (new) mempool minimum feerate but + // could be reconsidered in a package. + return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "mempool full"); } } @@ -934,10 +939,19 @@ Workspace ws(ptx, GetNextBlockScriptFlags(tip, m_active_chainstate.m_chainman)); + const std::vector<TxId> single_txid{ws.m_ptx->GetId()}; + // Perform the inexpensive checks first and avoid hashing and signature // verification unless those checks pass, to mitigate CPU exhaustion // denial-of-service attacks. if (!PreChecks(args, ws)) { + if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) { + // Failed for fee reasons. Provide the effective feerate and which + // tx was included. + return MempoolAcceptResult::FeeFailure( + ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), + single_txid); + } return MempoolAcceptResult::Failure(ws.m_state); } @@ -968,7 +982,6 @@ const CFeeRate effective_feerate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)}; - const std::vector<TxId> single_txid{ws.m_ptx->GetId()}; // Tx was accepted, but not added if (args.m_test_accept) { return MempoolAcceptResult::Success(ws.m_vsize, ws.m_base_fees, @@ -976,7 +989,12 @@ } if (!Finalize(args, ws)) { - return MempoolAcceptResult::Failure(ws.m_state); + // The only possible failure reason is fee-related (mempool full). + // Failed for fee reasons. Provide the effective feerate and which txns + // were included. + Assume(ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE); + return MempoolAcceptResult::FeeFailure( + ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), single_txid); } GetMainSignals().TransactionAddedToMempool( @@ -1046,20 +1064,24 @@ workspaces.cbegin(), workspaces.cend(), Amount::zero(), [](Amount sum, auto &ws) { return sum + ws.m_modified_fees; }); const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize); - TxValidationState placeholder_state; - if (args.m_package_feerates && - !CheckFeeRate(m_total_size, m_total_vsize, m_total_modified_fees, - placeholder_state)) { - package_state.Invalid(PackageValidationResult::PCKG_POLICY, - "package-fee-too-low"); - return PackageMempoolAcceptResult(package_state, {}); - } - std::vector<TxId> all_package_txids; all_package_txids.reserve(workspaces.size()); std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_txids), [](const auto &ws) { return ws.m_ptx->GetId(); }); + TxValidationState placeholder_state; + if (args.m_package_feerates && + !CheckFeeRate(m_total_size, m_total_vsize, m_total_modified_fees, + placeholder_state)) { + package_state.Invalid(PackageValidationResult::PCKG_TX, + "transaction failed"); + return PackageMempoolAcceptResult( + package_state, {{workspaces.back().m_ptx->GetId(), + MempoolAcceptResult::FeeFailure( + placeholder_state, + CFeeRate(m_total_modified_fees, m_total_vsize), + all_package_txids)}}); + } for (Workspace &ws : workspaces) { ws.m_package_feerate = package_feerate; @@ -1214,7 +1236,7 @@ assert(m_pool.exists(txid)); results_final.emplace(txid, single_res); } else if (single_res.m_state.GetResult() != - TxValidationResult::TX_MEMPOOL_POLICY && + TxValidationResult::TX_RECONSIDERABLE && single_res.m_state.GetResult() != TxValidationResult::TX_MISSING_INPUTS) { // Package validation policy only differs from individual policy