0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-02 09:46:52 -05:00

Merge bitcoin/bitcoin#26646: validation, bugfix: provide more info in *MempoolAcceptResult

264f9ef17f [validation] return MempoolAcceptResult for every tx on PCKG_TX failure (glozow)
dae81e01e8 [refactor] rename variables in AcceptPackage for clarity (glozow)
da484bc738 [doc] release note effective-feerate and effective-includes RPC results (glozow)
5eab397b98 [validation] remove PackageMempoolAcceptResult::m_package_feerate (glozow)
601bac88cb [rpc] return effective-includes in testmempoolaccept and submitpackage (glozow)
1691eaa818 [rpc] return effective-feerate in testmempoolaccept and submitpackage (glozow)
d6c7b78ef2 [validation] return wtxids of other transactions whose fees were used (glozow)
1605886380 [validation] return effective feerate from mempool validation (glozow)
5d35b4a7de [test] package validation quits early due to non-policy, non-missing-inputs failure (glozow)
be2e4d94e5 [validation] when quitting early in AcceptPackage, set package_state and tx result (glozow)

Pull request description:

  This PR fixes a bug and improves the mempool accept interface to return information more predictably.

  Bug: In package validation, we first try the transactions individually (see doc/policy/packages.md for more explanation) and, if they all failed for missing inputs and policy-related (i.e. fee) reasons, we'll try package validation. Otherwise, we'll just "quit early" since, for example, if a transaction had an invalid signature, adding a child will not help make it valid. Currently, when we quit early, we're not setting the `package_state` to be invalid, so the caller might think it succeeded. Also, we're returning no results - it makes more sense to return the individual transaction failure. Thanks instagibbs for catching https://github.com/bitcoin/bitcoin/pull/25038#discussion_r1013293248!

  Also, make the package results interface generally more useful/predictable:
  - Always return the feerate at which a transaction was considered for `CheckFeeRate` in `MempoolAcceptResult::m_effective_feerate` when it was successful. This can replace the current `PackageMempoolAcceptResult::m_package_feerate`, which only sometimes exists.
  - Always provide an entry for every transaction in `PackageMempoolAcceptResult::m_tx_results` when the error is `PCKG_TX`.

ACKs for top commit:
  instagibbs:
    reACK 264f9ef17f
  achow101:
    ACK 264f9ef17f
  naumenkogs:
    reACK 264f9ef17f

Tree-SHA512: ce7fd9927a80030317cc6157822596e85a540feff5dbf5eea7c62da2eb50c917cdddc9da1e2ff62cc18b98b27d360151811546bd9d498859679a04bbee090837
This commit is contained in:
glozow 2023-01-11 13:04:13 +00:00
commit 26002570ab
No known key found for this signature in database
GPG key ID: BA03F4DBE0C63FB4
8 changed files with 266 additions and 123 deletions

8
release-notes-26646.md Normal file
View file

@ -0,0 +1,8 @@
JSON-RPC
--------
The `testmempoolaccept` RPC now returns 2 additional results within the "fees" result:
"effective-feerate" is the feerate including fees and sizes of transactions validated together if
package validation was used, and also includes any modified fees from prioritisetransaction. The
"effective-includes" result lists the wtxids of transactions whose modified fees and sizes were used
in the effective-feerate (#26646).

View file

@ -126,6 +126,10 @@ static RPCHelpMan testmempoolaccept()
{RPCResult::Type::OBJ, "fees", /*optional=*/true, "Transaction fees (only present if 'allowed' is true)",
{
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
{RPCResult::Type::STR_AMOUNT, "effective-feerate", /*optional=*/false, "the effective feerate in " + CURRENCY_UNIT + " per KvB. May differ from the base feerate if, for example, there are modified fees from prioritisetransaction or a package feerate was used."},
{RPCResult::Type::ARR, "effective-includes", /*optional=*/false, "transactions whose fees and vsizes are included in effective-feerate.",
{RPCResult{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"},
}},
}},
{RPCResult::Type::STR, "reject-reason", /*optional=*/true, "Rejection string (only present when 'allowed' is false)"},
}},
@ -217,6 +221,12 @@ static RPCHelpMan testmempoolaccept()
result_inner.pushKV("vsize", virtual_size);
UniValue fees(UniValue::VOBJ);
fees.pushKV("base", ValueFromAmount(fee));
fees.pushKV("effective-feerate", ValueFromAmount(tx_result.m_effective_feerate.value().GetFeePerK()));
UniValue effective_includes_res(UniValue::VARR);
for (const auto& wtxid : tx_result.m_wtxids_fee_calculations.value()) {
effective_includes_res.push_back(wtxid.ToString());
}
fees.pushKV("effective-includes", effective_includes_res);
result_inner.pushKV("fees", fees);
}
} else {
@ -768,10 +778,13 @@ static RPCHelpMan submitpackage()
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
{RPCResult::Type::STR_AMOUNT, "effective-feerate", /*optional=*/true, "if the transaction was not already in the mempool, the effective feerate in " + CURRENCY_UNIT + " per KvB. For example, the package feerate and/or feerate with modified fees from prioritisetransaction."},
{RPCResult::Type::ARR, "effective-includes", /*optional=*/true, "if effective-feerate is provided, the wtxids of the transactions whose fees and vsizes are included in effective-feerate.",
{{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"},
}},
}},
}}
}},
{RPCResult::Type::STR_AMOUNT, "package-feerate", /*optional=*/true, "package feerate used for feerate checks in " + CURRENCY_UNIT + " per KvB. Excludes transactions which were deduplicated or accepted individually."},
{RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
{
{RPCResult::Type::STR_HEX, "", "The transaction id"},
@ -856,6 +869,7 @@ static RPCHelpMan submitpackage()
CHECK_NONFATAL(it != package_result.m_tx_results.end());
UniValue result_inner{UniValue::VOBJ};
result_inner.pushKV("txid", tx->GetHash().GetHex());
const auto& tx_result = it->second;
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
}
@ -864,6 +878,17 @@ static RPCHelpMan submitpackage()
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
UniValue fees(UniValue::VOBJ);
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
// Effective feerate is not provided for MEMPOOL_ENTRY transactions even
// though modified fees is known, because it is unknown whether package
// feerate was used when it was originally submitted.
fees.pushKV("effective-feerate", ValueFromAmount(tx_result.m_effective_feerate.value().GetFeePerK()));
UniValue effective_includes_res(UniValue::VARR);
for (const auto& wtxid : tx_result.m_wtxids_fee_calculations.value()) {
effective_includes_res.push_back(wtxid.ToString());
}
fees.pushKV("effective-includes", effective_includes_res);
}
result_inner.pushKV("fees", fees);
if (it->second.m_replaced_transactions.has_value()) {
for (const auto& ptx : it->second.m_replaced_transactions.value()) {
@ -874,9 +899,6 @@ static RPCHelpMan submitpackage()
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
}
rpc_result.pushKV("tx-results", tx_result_map);
if (package_result.m_package_feerate.has_value()) {
rpc_result.pushKV("package-feerate", ValueFromAmount(package_result.m_package_feerate.value().GetFeePerK()));
}
UniValue replaced_list(UniValue::VARR);
for (const uint256& hash : replaced_txids) replaced_list.push_back(hash.ToString());
rpc_result.pushKV("replaced-transactions", replaced_list);

View file

@ -90,17 +90,21 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup)
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->GetWitnessHash());
auto it_child = result_parent_child.m_tx_results.find(tx_child->GetWitnessHash());
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(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 != 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(result_parent_child.m_package_feerate.has_value());
BOOST_CHECK(result_parent_child.m_package_feerate.value() ==
CFeeRate(2 * COIN, GetVirtualTransactionSize(*tx_parent) + GetVirtualTransactionSize(*tx_child)));
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);
@ -109,10 +113,10 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup)
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->GetWitnessHash());
BOOST_CHECK(it_giant_tx != result_single_large.m_tx_results.end());
BOOST_CHECK_EQUAL(it_giant_tx->second.m_state.GetRejectReason(), "tx-size");
BOOST_CHECK(result_single_large.m_package_feerate == std::nullopt);
// Check that mempool size hasn't changed.
BOOST_CHECK_EQUAL(m_node.mempool->size(), initialPoolSize);
@ -233,7 +237,6 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
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);
BOOST_CHECK(result_unrelated_submit.m_package_feerate == std::nullopt);
// Parent and Child (and Grandchild) Package
Package package_parent_child;
@ -275,7 +278,30 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
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);
BOOST_CHECK(result_3gen_submit.m_package_feerate == std::nullopt);
}
// 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.push_back(std::vector<unsigned char>(1));
CMutableTransaction mtx_parent_invalid{mtx_parent};
mtx_parent_invalid.vin[0].scriptWitness = bad_witness;
CTransactionRef tx_parent_invalid = MakeTransactionRef(mtx_parent_invalid);
auto result_quit_early = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
{tx_parent_invalid, tx_child}, /*test_accept=*/ false);
BOOST_CHECK(result_quit_early.m_state.IsInvalid());
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->GetWitnessHash());
auto it_child = result_quit_early.m_tx_results.find(tx_child->GetWitnessHash());
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_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");
}
// Child with missing parent.
@ -290,8 +316,6 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "package-not-child-with-unconfirmed-parents");
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
BOOST_CHECK(result_missing_parent.m_package_feerate == std::nullopt);
}
// Submit package with parent + child.
@ -301,20 +325,23 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
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 != 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(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);
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_parent->GetHash())));
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_child->GetHash())));
// Since both transactions have high feerates, they each passed validation individually.
// Package validation was unnecessary, so there is no package feerate.
BOOST_CHECK(submit_parent_child.m_package_feerate == std::nullopt);
}
// Already-in-mempool transactions should be detected and de-duplicated.
@ -323,6 +350,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
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->GetWitnessHash());
auto it_child_deduped = submit_deduped.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent_deduped != submit_deduped.m_tx_results.end());
@ -335,8 +363,6 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_parent->GetHash())));
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_child->GetHash())));
BOOST_CHECK(submit_deduped.m_package_feerate == std::nullopt);
}
}
@ -399,6 +425,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
{ptx_parent, ptx_child1}, /*test_accept=*/false);
BOOST_CHECK_MESSAGE(submit_witness1.m_state.IsValid(),
"Package validation unexpectedly failed: " << submit_witness1.m_state.GetRejectReason());
BOOST_CHECK_EQUAL(submit_witness1.m_tx_results.size(), 2);
auto it_parent1 = submit_witness1.m_tx_results.find(ptx_parent->GetWitnessHash());
auto it_child1 = submit_witness1.m_tx_results.find(ptx_child1->GetWitnessHash());
BOOST_CHECK(it_parent1 != submit_witness1.m_tx_results.end());
@ -412,13 +439,11 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(ptx_child1->GetHash())));
// Child2 would have been validated individually.
BOOST_CHECK(submit_witness1.m_package_feerate == std::nullopt);
const auto submit_witness2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
{ptx_parent, ptx_child2}, /*test_accept=*/false);
BOOST_CHECK(submit_witness2.m_package_feerate == std::nullopt);
BOOST_CHECK_MESSAGE(submit_witness2.m_state.IsValid(),
"Package validation unexpectedly failed: " << submit_witness2.m_state.GetRejectReason());
BOOST_CHECK_EQUAL(submit_witness2.m_tx_results.size(), 2);
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 != submit_witness2.m_tx_results.end());
@ -436,11 +461,11 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
{ptx_parent, ptx_child1}, /*test_accept=*/false);
BOOST_CHECK_MESSAGE(submit_segwit_dedup.m_state.IsValid(),
"Package validation unexpectedly failed: " << submit_segwit_dedup.m_state.GetRejectReason());
BOOST_CHECK_EQUAL(submit_segwit_dedup.m_tx_results.size(), 2);
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);
BOOST_CHECK(submit_witness2.m_package_feerate == std::nullopt);
}
// Try submitting Package1{child2, grandchild} where child2 is same-txid-different-witness as
@ -465,6 +490,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
{ptx_child2, ptx_grandchild}, /*test_accept=*/false);
BOOST_CHECK_MESSAGE(submit_spend_ignored.m_state.IsValid(),
"Package validation unexpectedly failed: " << submit_spend_ignored.m_state.GetRejectReason());
BOOST_CHECK_EQUAL(submit_spend_ignored.m_tx_results.size(), 2);
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 != submit_spend_ignored.m_tx_results.end());
@ -475,9 +501,6 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(ptx_child2->GetHash())));
BOOST_CHECK(!m_node.mempool->exists(GenTxid::Wtxid(ptx_child2->GetWitnessHash())));
BOOST_CHECK(m_node.mempool->exists(GenTxid::Wtxid(ptx_grandchild->GetWitnessHash())));
// Since child2 is ignored, grandchild would be validated individually.
BOOST_CHECK(submit_spend_ignored.m_package_feerate == std::nullopt);
}
// A package Package{parent1, parent2, parent3, child} where the parents are a mixture of
@ -568,6 +591,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
{
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->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());
@ -590,11 +614,12 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup)
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(ptx_mixed_child->GetHash())));
// package feerate should include parent3 and child. It should not include parent1 or parent2_v1.
BOOST_CHECK(mixed_result.m_package_feerate.has_value());
const CFeeRate expected_feerate(1 * COIN, GetVirtualTransactionSize(*ptx_parent3) + GetVirtualTransactionSize(*ptx_mixed_child));
BOOST_CHECK_MESSAGE(mixed_result.m_package_feerate.value() == expected_feerate,
strprintf("Expected package feerate %s, got %s", expected_feerate.ToString(),
mixed_result.m_package_feerate.value().ToString()));
BOOST_CHECK(it_parent3->second.m_effective_feerate.value() == expected_feerate);
BOOST_CHECK(it_child->second.m_effective_feerate.value() == expected_feerate);
std::vector<uint256> 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);
}
}
@ -638,16 +663,12 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
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);
BOOST_CHECK_EQUAL(submit_cpfp_deprio.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_MESSAGE(submit_cpfp_deprio.m_state.IsInvalid(),
"Package validation unexpectedly succeeded: " << submit_cpfp_deprio.m_state.GetRejectReason());
BOOST_CHECK(submit_cpfp_deprio.m_tx_results.empty());
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
const CFeeRate expected_feerate(0, GetVirtualTransactionSize(*tx_parent) + GetVirtualTransactionSize(*tx_child));
BOOST_CHECK(submit_cpfp_deprio.m_package_feerate.has_value());
BOOST_CHECK(submit_cpfp_deprio.m_package_feerate.value() == CFeeRate{0});
BOOST_CHECK_MESSAGE(submit_cpfp_deprio.m_package_feerate.value() == expected_feerate,
strprintf("Expected package feerate %s, got %s", expected_feerate.ToString(),
submit_cpfp_deprio.m_package_feerate.value().ToString()));
}
// Clear the prioritisation of the parent transaction.
@ -662,6 +683,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
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->GetWitnessHash());
auto it_child = submit_cpfp.m_tx_results.find(tx_child->GetWitnessHash());
BOOST_CHECK(it_parent != submit_cpfp.m_tx_results.end());
@ -677,11 +699,12 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
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<uint256> 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);
BOOST_CHECK(submit_cpfp.m_package_feerate.has_value());
BOOST_CHECK_MESSAGE(submit_cpfp.m_package_feerate.value() == expected_feerate,
strprintf("Expected package feerate %s, got %s", expected_feerate.ToString(),
submit_cpfp.m_package_feerate.value().ToString()));
}
// Just because we allow low-fee parents doesn't mean we allow low-feerate packages.
@ -716,10 +739,6 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
const CFeeRate expected_feerate(200,
GetVirtualTransactionSize(*tx_parent_cheap) + GetVirtualTransactionSize(*tx_child_cheap));
BOOST_CHECK(expected_feerate.GetFeePerK() < 1000);
BOOST_CHECK(submit_package_too_low.m_package_feerate.has_value());
BOOST_CHECK_MESSAGE(submit_package_too_low.m_package_feerate.value() == expected_feerate,
strprintf("Expected package feerate %s, got %s", expected_feerate.ToString(),
submit_package_too_low.m_package_feerate.value().ToString()));
}
// Package feerate includes the modified fees of the transactions.
@ -734,10 +753,20 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
"Package validation unexpectedly failed" << submit_prioritised_package.m_state.GetRejectReason());
const CFeeRate expected_feerate(1 * COIN + 200,
GetVirtualTransactionSize(*tx_parent_cheap) + GetVirtualTransactionSize(*tx_child_cheap));
BOOST_CHECK(submit_prioritised_package.m_package_feerate.has_value());
BOOST_CHECK_MESSAGE(submit_prioritised_package.m_package_feerate.value() == expected_feerate,
strprintf("Expected package feerate %s, got %s", expected_feerate.ToString(),
submit_prioritised_package.m_package_feerate.value().ToString()));
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 != 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() == 0);
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() == 200);
BOOST_CHECK(it_child->second.m_effective_feerate.value() == expected_feerate);
std::vector<uint256> 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);
}
// Package feerate is calculated without topology in mind; it's just aggregating fees and sizes.
@ -770,10 +799,6 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
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.
// The package feerate is just the child's feerate, which is 0sat/vb.
BOOST_CHECK(submit_rich_parent.m_package_feerate.has_value());
BOOST_CHECK_MESSAGE(submit_rich_parent.m_package_feerate.value() == CFeeRate(),
"expected 0, got " << submit_rich_parent.m_package_feerate.value().ToString());
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetRejectReason(), "package-fee-too-low");
@ -783,6 +808,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
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(m_node.mempool->size(), expected_pool_size);
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_parent_rich->GetHash())));

View file

@ -583,6 +583,11 @@ private:
/** Total virtual size of all transactions being replaced. */
size_t m_conflicting_size{0};
/** If we're doing package validation (i.e. m_package_feerates=true), the "effective"
* package feerate of this transaction is the total fees divided by the total size of
* transactions (which may include its ancestors and/or descendants). */
CFeeRate m_package_feerate{0};
const CTransactionRef& m_ptx;
/** Txid. */
const uint256& m_hash;
@ -1144,12 +1149,21 @@ bool MemPoolAccept::SubmitPackage(const ATMPArgs& args, std::vector<Workspace>&
// make sure we haven't exceeded max mempool size.
LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip());
std::vector<uint256> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
// Find the wtxids of the transactions that made it into the mempool. Allow partial submission,
// but don't report success unless they all made it into the mempool.
for (Workspace& ws : workspaces) {
const auto effective_feerate = args.m_package_feerates ? ws.m_package_feerate :
CFeeRate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<uint256>({ws.m_ptx->GetWitnessHash()});
if (m_pool.exists(GenTxid::Wtxid(ws.m_ptx->GetWitnessHash()))) {
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees));
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, effective_feerate_wtxids));
GetMainSignals().TransactionAddedToMempool(ws.m_ptx, m_pool.GetAndIncrementSequence());
} else {
all_submitted = false;
@ -1177,16 +1191,20 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
if (!ConsensusScriptChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
const CFeeRate effective_feerate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const std::vector<uint256> single_wtxid{ws.m_ptx->GetWitnessHash()};
// Tx was accepted, but not added
if (args.m_test_accept) {
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees);
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, single_wtxid);
}
if (!Finalize(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
GetMainSignals().TransactionAddedToMempool(ptx, m_pool.GetAndIncrementSequence());
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees);
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees,
effective_feerate, single_wtxid);
}
PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::vector<CTransactionRef>& txns, ATMPArgs& args)
@ -1233,7 +1251,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
if (args.m_package_feerates &&
!CheckFeeRate(m_total_vsize, m_total_modified_fees, placeholder_state)) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-fee-too-low");
return PackageMempoolAcceptResult(package_state, package_feerate, {});
return PackageMempoolAcceptResult(package_state, {});
}
// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
@ -1241,51 +1259,60 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
// transactions, but this exemption is not extended to packages in CheckPackageLimits().
std::string err_string;
if (txns.size() > 1 && !PackageMempoolChecks(txns, package_state)) {
return PackageMempoolAcceptResult(package_state, package_feerate, std::move(results));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
std::vector<uint256> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
for (Workspace& ws : workspaces) {
ws.m_package_feerate = package_feerate;
if (!PolicyScriptChecks(args, ws)) {
// Exit early to avoid doing pointless work. Update the failed tx result; the rest are unfinished.
package_state.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state));
return PackageMempoolAcceptResult(package_state, package_feerate, std::move(results));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
if (args.m_test_accept) {
// When test_accept=true, transactions that pass PolicyScriptChecks are valid because there are
// no further mempool checks (passing PolicyScriptChecks implies passing ConsensusScriptChecks).
const auto effective_feerate = args.m_package_feerates ? ws.m_package_feerate :
CFeeRate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<uint256>{ws.m_ptx->GetWitnessHash()};
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions),
ws.m_vsize, ws.m_base_fees));
ws.m_vsize, ws.m_base_fees, effective_feerate,
effective_feerate_wtxids));
}
}
if (args.m_test_accept) return PackageMempoolAcceptResult(package_state, package_feerate, std::move(results));
if (args.m_test_accept) return PackageMempoolAcceptResult(package_state, std::move(results));
if (!SubmitPackage(args, workspaces, package_state, results)) {
// PackageValidationState filled in by SubmitPackage().
return PackageMempoolAcceptResult(package_state, package_feerate, std::move(results));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
return PackageMempoolAcceptResult(package_state, package_feerate, std::move(results));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package, ATMPArgs& args)
{
AssertLockHeld(cs_main);
PackageValidationState package_state;
// Used if returning a PackageMempoolAcceptResult directly from this function.
PackageValidationState package_state_quit_early;
// Check that the package is well-formed. If it isn't, we won't try to validate any of the
// transactions and thus won't return any MempoolAcceptResults, just a package-wide error.
// Context-free package checks.
if (!CheckPackage(package, package_state)) return PackageMempoolAcceptResult(package_state, {});
if (!CheckPackage(package, package_state_quit_early)) return PackageMempoolAcceptResult(package_state_quit_early, {});
// All transactions in the package must be a parent of the last transaction. This is just an
// opportunity for us to fail fast on a context-free check without taking the mempool lock.
if (!IsChildWithParents(package)) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents");
return PackageMempoolAcceptResult(package_state, {});
package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents");
return PackageMempoolAcceptResult(package_state_quit_early, {});
}
// IsChildWithParents() guarantees the package is > 1 transactions.
@ -1317,15 +1344,16 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
return unconfirmed_parent_txids.count(input.prevout.hash) > 0 || m_view.HaveCoin(input.prevout);
};
if (!std::all_of(child->vin.cbegin(), child->vin.cend(), package_or_confirmed)) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-unconfirmed-parents");
return PackageMempoolAcceptResult(package_state, {});
package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-unconfirmed-parents");
return PackageMempoolAcceptResult(package_state_quit_early, {});
}
// Protect against bugs where we pull more inputs from disk that miss being added to
// coins_to_uncache. The backend will be connected again when needed in PreChecks.
m_view.SetBackend(m_dummy);
LOCK(m_pool.cs);
std::map<const uint256, const MempoolAcceptResult> results;
// Stores final results that won't change
std::map<const uint256, const MempoolAcceptResult> results_final;
// Node operators are free to set their mempool policies however they please, nodes may receive
// transactions in different orders, and malicious counterparties may try to take advantage of
// policy differences to pin or delay propagation of transactions. As such, it's possible for
@ -1335,8 +1363,13 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
// the new transactions. This ensures we don't double-count transaction counts and sizes when
// checking ancestor/descendant limits, or double-count transaction fees for fee-related policy.
ATMPArgs single_args = ATMPArgs::SingleInPackageAccept(args);
// Results from individual validation. "Nonfinal" because if a transaction fails by itself but
// succeeds later (i.e. when evaluated with a fee-bumping child), the result changes (though not
// reflected in this map). If a transaction fails more than once, we want to return the first
// result, when it was considered on its own. So changes will only be from invalid -> valid.
std::map<uint256, MempoolAcceptResult> individual_results_nonfinal;
bool quit_early{false};
std::vector<CTransactionRef> txns_new;
std::vector<CTransactionRef> txns_package_eval;
for (const auto& tx : package) {
const auto& wtxid = tx->GetWitnessHash();
const auto& txid = tx->GetHash();
@ -1347,7 +1380,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
// Exact transaction already exists in the mempool.
auto iter = m_pool.GetIter(txid);
assert(iter != std::nullopt);
results.emplace(wtxid, MempoolAcceptResult::MempoolTx(iter.value()->GetTxSize(), iter.value()->GetFee()));
results_final.emplace(wtxid, MempoolAcceptResult::MempoolTx(iter.value()->GetTxSize(), iter.value()->GetFee()));
} else if (m_pool.exists(GenTxid::Txid(txid))) {
// Transaction with the same non-witness data but different witness (same txid,
// different wtxid) already exists in the mempool.
@ -1359,7 +1392,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
auto iter = m_pool.GetIter(txid);
assert(iter != std::nullopt);
// Provide the wtxid of the mempool tx so that the caller can look it up in the mempool.
results.emplace(wtxid, MempoolAcceptResult::MempoolTxDifferentWitness(iter.value()->GetTx().GetWitnessHash()));
results_final.emplace(wtxid, MempoolAcceptResult::MempoolTxDifferentWitness(iter.value()->GetTx().GetWitnessHash()));
} else {
// Transaction does not already exist in the mempool.
// Try submitting the transaction on its own.
@ -1368,7 +1401,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
// The transaction succeeded on its own and is now in the mempool. Don't include it
// in package validation, because its fees should only be "used" once.
assert(m_pool.exists(GenTxid::Wtxid(wtxid)));
results.emplace(wtxid, single_res);
results_final.emplace(wtxid, single_res);
} else if (single_res.m_state.GetResult() != TxValidationResult::TX_MEMPOOL_POLICY &&
single_res.m_state.GetResult() != TxValidationResult::TX_MISSING_INPUTS) {
// Package validation policy only differs from individual policy in its evaluation
@ -1381,24 +1414,40 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
// future. Continue individually validating the rest of the transactions, because
// some of them may still be valid.
quit_early = true;
package_state_quit_early.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
individual_results_nonfinal.emplace(wtxid, single_res);
} else {
txns_new.push_back(tx);
individual_results_nonfinal.emplace(wtxid, single_res);
txns_package_eval.push_back(tx);
}
}
}
// Nothing to do if the entire package has already been submitted.
if (quit_early || txns_new.empty()) {
// No package feerate when no package validation was done.
return PackageMempoolAcceptResult(package_state, std::move(results));
// Quit early because package validation won't change the result or the entire package has
// already been submitted.
if (quit_early || txns_package_eval.empty()) {
for (const auto& [wtxid, mempoolaccept_res] : individual_results_nonfinal) {
Assume(results_final.emplace(wtxid, mempoolaccept_res).second);
Assume(mempoolaccept_res.m_result_type == MempoolAcceptResult::ResultType::INVALID);
}
return PackageMempoolAcceptResult(package_state_quit_early, std::move(results_final));
}
// Validate the (deduplicated) transactions as a package.
auto submission_result = AcceptMultipleTransactions(txns_new, args);
// Validate the (deduplicated) transactions as a package. Note that submission_result has its
// own PackageValidationState; package_state_quit_early is unused past this point.
auto submission_result = AcceptMultipleTransactions(txns_package_eval, args);
// Include already-in-mempool transaction results in the final result.
for (const auto& [wtxid, mempoolaccept_res] : results) {
submission_result.m_tx_results.emplace(wtxid, mempoolaccept_res);
for (const auto& [wtxid, mempoolaccept_res] : results_final) {
Assume(submission_result.m_tx_results.emplace(wtxid, mempoolaccept_res).second);
Assume(mempoolaccept_res.m_result_type != MempoolAcceptResult::ResultType::INVALID);
}
if (submission_result.m_state.GetResult() == PackageValidationResult::PCKG_TX) {
// Package validation failed because one or more transactions failed. Provide a result for
// each transaction; if AcceptMultipleTransactions() didn't return a result for a tx,
// include the previous individual failure reason.
submission_result.m_tx_results.insert(individual_results_nonfinal.cbegin(),
individual_results_nonfinal.cend());
Assume(submission_result.m_tx_results.size() == package.size());
}
if (submission_result.m_state.IsValid()) assert(submission_result.m_package_feerate.has_value());
return submission_result;
}

View file

@ -134,6 +134,19 @@ struct MempoolAcceptResult {
const std::optional<int64_t> m_vsize;
/** Raw base fees in satoshis. */
const std::optional<CAmount> m_base_fees;
/** The feerate at which this transaction was considered. This includes any 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_wtxids_fee_calculations below).
* Only present when m_result_type = ResultType::VALID.
*/
const std::optional<CFeeRate> m_effective_feerate;
/** Contains the wtxids of the transactions used for fee-related checks. Includes this
* transaction's wtxid 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<uint256>> m_wtxids_fee_calculations;
// The following field is only present when m_result_type = ResultType::DIFFERENT_WITNESS
/** The wtxid of the transaction in the mempool which has the same txid but different witness. */
@ -143,8 +156,13 @@ struct MempoolAcceptResult {
return MempoolAcceptResult(state);
}
static MempoolAcceptResult Success(std::list<CTransactionRef>&& replaced_txns, int64_t vsize, CAmount fees) {
return MempoolAcceptResult(std::move(replaced_txns), vsize, fees);
static MempoolAcceptResult Success(std::list<CTransactionRef>&& replaced_txns,
int64_t vsize,
CAmount fees,
CFeeRate effective_feerate,
const std::vector<uint256>& wtxids_fee_calculations) {
return MempoolAcceptResult(std::move(replaced_txns), vsize, fees,
effective_feerate, wtxids_fee_calculations);
}
static MempoolAcceptResult MempoolTx(int64_t vsize, CAmount fees) {
@ -164,9 +182,17 @@ private:
}
/** Constructor for success case */
explicit MempoolAcceptResult(std::list<CTransactionRef>&& replaced_txns, int64_t vsize, CAmount fees)
explicit MempoolAcceptResult(std::list<CTransactionRef>&& replaced_txns,
int64_t vsize,
CAmount fees,
CFeeRate effective_feerate,
const std::vector<uint256>& wtxids_fee_calculations)
: m_result_type(ResultType::VALID),
m_replaced_transactions(std::move(replaced_txns)), m_vsize{vsize}, m_base_fees(fees) {}
m_replaced_transactions(std::move(replaced_txns)),
m_vsize{vsize},
m_base_fees(fees),
m_effective_feerate(effective_feerate),
m_wtxids_fee_calculations(wtxids_fee_calculations) {}
/** Constructor for already-in-mempool case. It wouldn't replace any transactions. */
explicit MempoolAcceptResult(int64_t vsize, CAmount fees)
@ -190,10 +216,6 @@ struct PackageMempoolAcceptResult
* was a package-wide error (see result in m_state), m_tx_results will be empty.
*/
std::map<const uint256, const MempoolAcceptResult> m_tx_results;
/** Package feerate, defined as the aggregated modified fees divided by the total virtual size
* of all transactions in the package. May be unavailable if some inputs were not available or
* a transaction failure caused validation to terminate early. */
std::optional<CFeeRate> m_package_feerate;
explicit PackageMempoolAcceptResult(PackageValidationState state,
std::map<const uint256, const MempoolAcceptResult>&& results)
@ -201,7 +223,7 @@ struct PackageMempoolAcceptResult
explicit PackageMempoolAcceptResult(PackageValidationState state, CFeeRate feerate,
std::map<const uint256, const MempoolAcceptResult>&& results)
: m_state{state}, m_tx_results(std::move(results)), m_package_feerate{feerate} {}
: m_state{state}, m_tx_results(std::move(results)) {}
/** Constructor to create a PackageMempoolAcceptResult from a single MempoolAcceptResult */
explicit PackageMempoolAcceptResult(const uint256& wtxid, const MempoolAcceptResult& result)

View file

@ -58,7 +58,11 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
"""Wrapper to check result of testmempoolaccept on node_0's mempool"""
result_test = self.nodes[0].testmempoolaccept(*args, **kwargs)
for r in result_test:
r.pop('wtxid') # Skip check for now
# Skip these checks for now
r.pop('wtxid')
if "fees" in r:
r["fees"].pop("effective-feerate")
r["fees"].pop("effective-includes")
assert_equal(result_expected, result_test)
assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size) # Must not change mempool state

View file

@ -622,8 +622,10 @@ class SegWitTest(BitcoinTestFramework):
if not self.segwit_active:
# Just check mempool acceptance, but don't add the transaction to the mempool, since witness is disallowed
# in blocks and the tx is impossible to mine right now.
assert_equal(
self.nodes[0].testmempoolaccept([tx3.serialize_with_witness().hex()]),
testres3 = self.nodes[0].testmempoolaccept([tx3.serialize_with_witness().hex()])
testres3[0]["fees"].pop("effective-feerate")
testres3[0]["fees"].pop("effective-includes")
assert_equal(testres3,
[{
'txid': tx3.hash,
'wtxid': tx3.getwtxid(),
@ -639,8 +641,10 @@ class SegWitTest(BitcoinTestFramework):
tx3 = tx
tx3.vout = [tx3_out]
tx3.rehash()
assert_equal(
self.nodes[0].testmempoolaccept([tx3.serialize_with_witness().hex()]),
testres3_replaced = self.nodes[0].testmempoolaccept([tx3.serialize_with_witness().hex()])
testres3_replaced[0]["fees"].pop("effective-feerate")
testres3_replaced[0]["fees"].pop("effective-includes")
assert_equal(testres3_replaced,
[{
'txid': tx3.hash,
'wtxid': tx3.getwtxid(),

View file

@ -20,6 +20,7 @@ from test_framework.util import (
assert_raises_rpc_error,
)
from test_framework.wallet import (
COIN,
DEFAULT_FEE,
MiniWallet,
)
@ -239,11 +240,14 @@ class RPCPackagesTest(BitcoinTestFramework):
coin = self.wallet.get_utxo()
fee = Decimal("0.00125000")
replaceable_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = fee)
testres_replaceable = node.testmempoolaccept([replaceable_tx["hex"]])
assert_equal(testres_replaceable, [
{"txid": replaceable_tx["txid"], "wtxid": replaceable_tx["wtxid"],
"allowed": True, "vsize": replaceable_tx["tx"].get_vsize(), "fees": { "base": fee }}
])
testres_replaceable = node.testmempoolaccept([replaceable_tx["hex"]])[0]
assert_equal(testres_replaceable["txid"], replaceable_tx["txid"])
assert_equal(testres_replaceable["wtxid"], replaceable_tx["wtxid"])
assert testres_replaceable["allowed"]
assert_equal(testres_replaceable["vsize"], replaceable_tx["tx"].get_vsize())
assert_equal(testres_replaceable["fees"]["base"], fee)
assert_fee_amount(fee, replaceable_tx["tx"].get_vsize(), testres_replaceable["fees"]["effective-feerate"])
assert_equal(testres_replaceable["fees"]["effective-includes"], [replaceable_tx["wtxid"]])
# Replacement transaction is identical except has double the fee
replacement_tx = self.wallet.create_self_transfer(utxo_to_spend=coin, sequence=MAX_BIP125_RBF_SEQUENCE, fee = 2 * fee)
@ -287,11 +291,13 @@ class RPCPackagesTest(BitcoinTestFramework):
peer = node.add_p2p_connection(P2PTxInvStore())
package_txns = []
presubmitted_wtxids = set()
for _ in range(num_parents):
parent_tx = self.wallet.create_self_transfer(fee=DEFAULT_FEE)
package_txns.append(parent_tx)
if partial_submit and random.choice([True, False]):
node.sendrawtransaction(parent_tx["hex"])
presubmitted_wtxids.add(parent_tx["wtxid"])
child_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx["new_utxo"] for tx in package_txns], fee_per_output=10000) #DEFAULT_FEE
package_txns.append(child_tx)
@ -302,24 +308,19 @@ class RPCPackagesTest(BitcoinTestFramework):
for package_txn in package_txns:
tx = package_txn["tx"]
assert tx.getwtxid() in submitpackage_result["tx-results"]
tx_result = submitpackage_result["tx-results"][tx.getwtxid()]
assert_equal(tx_result, {
"txid": package_txn["txid"],
"vsize": tx.get_vsize(),
"fees": {
"base": DEFAULT_FEE,
}
})
wtxid = tx.getwtxid()
assert wtxid in submitpackage_result["tx-results"]
tx_result = submitpackage_result["tx-results"][wtxid]
assert_equal(tx_result["txid"], tx.rehash())
assert_equal(tx_result["vsize"], tx.get_vsize())
assert_equal(tx_result["fees"]["base"], DEFAULT_FEE)
if wtxid not in presubmitted_wtxids:
assert_fee_amount(DEFAULT_FEE, tx.get_vsize(), tx_result["fees"]["effective-feerate"])
assert_equal(tx_result["fees"]["effective-includes"], [wtxid])
# submitpackage result should be consistent with testmempoolaccept and getmempoolentry
self.assert_equal_package_results(node, testmempoolaccept_result, submitpackage_result)
# Package feerate is calculated for the remaining transactions after deduplication and
# individual submission. If only 0 or 1 transaction is left, e.g. because all transactions
# had high-feerates or were already in the mempool, no package feerate is provided.
# In this case, since all of the parents have high fees, each is accepted individually.
assert "package-feerate" not in submitpackage_result
# The node should announce each transaction. No guarantees for propagation.
peer.wait_for_broadcast([tx["tx"].getwtxid() for tx in package_txns])
self.generate(node, 1)
@ -328,8 +329,11 @@ class RPCPackagesTest(BitcoinTestFramework):
node = self.nodes[0]
peer = node.add_p2p_connection(P2PTxInvStore())
# Package with 2 parents and 1 child. One parent pays for itself using modified fees, and
# another has 0 fees but is bumped by child.
tx_poor = self.wallet.create_self_transfer(fee=0, fee_rate=0)
tx_rich = self.wallet.create_self_transfer(fee=DEFAULT_FEE)
tx_rich = self.wallet.create_self_transfer(fee=0, fee_rate=0)
node.prioritisetransaction(tx_rich["txid"], 0, int(DEFAULT_FEE * COIN))
package_txns = [tx_rich, tx_poor]
coins = [tx["new_utxo"] for tx in package_txns]
tx_child = self.wallet.create_self_transfer_multi(utxos_to_spend=coins, fee_per_output=10000) #DEFAULT_FEE
@ -340,14 +344,18 @@ class RPCPackagesTest(BitcoinTestFramework):
rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
child_result = submitpackage_result["tx-results"][tx_child["tx"].getwtxid()]
assert_equal(rich_parent_result["fees"]["base"], DEFAULT_FEE)
assert_equal(rich_parent_result["fees"]["base"], 0)
assert_equal(poor_parent_result["fees"]["base"], 0)
assert_equal(child_result["fees"]["base"], DEFAULT_FEE)
# Package feerate is calculated for the remaining transactions after deduplication and
# individual submission. Since this package had a 0-fee parent, package feerate must have
# been used and returned.
assert "package-feerate" in submitpackage_result
assert_fee_amount(DEFAULT_FEE, rich_parent_result["vsize"] + child_result["vsize"], submitpackage_result["package-feerate"])
# The "rich" parent does not require CPFP so its effective feerate.
assert_fee_amount(DEFAULT_FEE, tx_rich["tx"].get_vsize(), rich_parent_result["fees"]["effective-feerate"])
assert_equal(rich_parent_result["fees"]["effective-includes"], [tx_rich["wtxid"]])
# The "poor" parent and child's effective feerates are the same, composed of the child's fee
# divided by their combined vsize.
assert_fee_amount(DEFAULT_FEE, tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize(), poor_parent_result["fees"]["effective-feerate"])
assert_fee_amount(DEFAULT_FEE, tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize(), child_result["fees"]["effective-feerate"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], poor_parent_result["fees"]["effective-includes"])
assert_equal([tx_poor["wtxid"], tx_child["tx"].getwtxid()], child_result["fees"]["effective-includes"])
# The node will broadcast each transaction, still abiding by its peer's fee filter
peer.wait_for_broadcast([tx["tx"].getwtxid() for tx in package_txns])