Skip to content

Commit 41544b8

Browse files
committed
Merge bitcoin/bitcoin#28984: Cluster size 2 package rbf
94ed4fb Add release note for size 2 package rbf (Greg Sanders) afd52d8 doc: update package RBF comment (Greg Sanders) 6e3c439 mempool: Improve logging of replaced transactions (Greg Sanders) d3466e4 CheckPackageMempoolAcceptResult: Check package rbf invariants (Greg Sanders) 316d7b6 Fuzz: pass mempool to CheckPackageMempoolAcceptResult (Greg Sanders) 4d15bcf [test] package rbf (glozow) dc21f61 [policy] package rbf (Suhas Daftuar) 5da3967 PackageV3Checks: Relax assumptions (Greg Sanders) Pull request description: Allows any 2 transaction package with no in-mempool ancestors to do package RBF when directly conflicting with other mempool clusters of size two or less. Proposed validation steps: 1) If the transaction package is of size 1, legacy rbf rules apply. 2) Otherwise the transaction package consists of a (parent, child) pair with no other in-mempool ancestors (or descendants, obviously), so it is also going to create a cluster of size 2. If larger, fail. 3) The package rbf may not evict more than 100 transactions from the mempool(bip125 rule 5) 4) The package is a single chunk 5) Every directly conflicted mempool transaction is connected to at most 1 other in-mempool transaction (ie the cluster size of the conflict is at most 2). 6) Diagram check: We ensure that the replacement is strictly superior, improving the mempool 7) The total fee of the package, minus the total fee of what is being evicted, is at least the minrelayfee * size of the package (equivalent to bip125 rule 3 and 4) Post-cluster mempool this will likely be expanded to general package rbf, but this is what we can safely support today. ACKs for top commit: achow101: ACK 94ed4fb glozow: reACK 94ed4fb via range-diff ismaelsadeeq: re-ACK 94ed4fb theStack: Code-review ACK 94ed4fb murchandamus: utACK 94ed4fb Tree-SHA512: 9bd383e695964f362f147482bbf73b1e77c4d792bda2e91d7f30d74b3540a09146a5528baf86854a113005581e8c75f04737302517b7d5124296bd7a151e3992
2 parents ddf2ebd + 94ed4fb commit 41544b8

File tree

9 files changed

+916
-34
lines changed

9 files changed

+916
-34
lines changed

doc/policy/packages.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,29 @@ The following rules are enforced for all packages:
3636
* Packages cannot have conflicting transactions, i.e. no two transactions in a package can spend
3737
the same inputs. Packages cannot have duplicate transactions. (#20833)
3838

39-
* No transaction in a package can conflict with a mempool transaction. Replace By Fee is
40-
currently disabled for packages. (#20833)
39+
* Only limited package replacements are currently considered. (#28984)
4140

42-
- Package RBF may be enabled in the future.
41+
- All direct conflicts must signal replacement (or have `-mempoolfullrbf=1` set).
42+
43+
- Packages are 1-parent-1-child, with no in-mempool ancestors of the package.
44+
45+
- All conflicting clusters(connected components of mempool transactions) must be clusters of up to size 2.
46+
47+
- No more than MAX_REPLACEMENT_CANDIDATES transactions can be replaced, analogous to
48+
regular [replacement rule](./mempool-replacements.md) 5).
49+
50+
- Replacements must pay more total total fees at the incremental relay fee (analogous to
51+
regular [replacement rules](./mempool-replacements.md) 3 and 4).
52+
53+
- Parent feerate must be lower than package feerate.
54+
55+
- Must improve [feerate diagram](https://delvingbitcoin.org/t/mempool-incentive-compatibility/553). (#29242)
56+
57+
- *Rationale*: Basic support for package RBF can be used by wallets
58+
by making chains of no longer than two, then directly conflicting
59+
those chains when needed. Combined with V3 transactions this can
60+
result in more robust fee bumping. More general package RBF may be
61+
enabled in the future.
4362

4463
* When packages are evaluated against ancestor/descendant limits, the union of all transactions'
4564
descendants and ancestors is considered. (#21800)

doc/release-28984.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
P2P and network changes
2+
-----------------------
3+
4+
- Limited package RBF is now enabled, where the proposed conflicting package would result in
5+
a connected component, aka cluster, of size 2 in the mempool. All clusters being conflicted
6+
against must be of size 2 or lower.

src/policy/v3_policy.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t v
9191
const auto parent_info = [&] {
9292
if (mempool_ancestors.size() > 0) {
9393
auto& mempool_parent = *mempool_ancestors.begin();
94-
Assume(mempool_parent->GetCountWithDescendants() == 1);
9594
return ParentInfo{mempool_parent->GetTx().GetHash(),
9695
mempool_parent->GetTx().GetWitnessHash(),
9796
mempool_parent->GetTx().version,
@@ -135,10 +134,7 @@ std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t v
135134
}
136135
}
137136

138-
// It shouldn't be possible to have any mempool siblings at this point. SingleV3Checks
139-
// catches mempool siblings and sibling eviction is not extended to packages. Also, if the package consists of connected transactions,
140-
// any tx having a mempool ancestor would mean the package exceeds ancestor limits.
141-
if (!Assume(!parent_info.m_has_mempool_descendant)) {
137+
if (parent_info.m_has_mempool_descendant) {
142138
return strprintf("tx %s (wtxid=%s) would exceed descendant count limit",
143139
parent_info.m_txid.ToString(), parent_info.m_wtxid.ToString());
144140
}

src/test/fuzz/package_eval.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
314314
// just use result_package.m_state here. This makes the expect_valid check meaningless, but
315315
// we can still verify that the contents of m_tx_results are consistent with m_state.
316316
const bool expect_valid{result_package.m_state.IsValid()};
317-
Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, nullptr));
317+
Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, &tx_pool));
318318
} else {
319319
// This is empty if it fails early checks, or "full" if transactions are looked at deeper
320320
Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty());

src/test/txpackage_tests.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <key_io.h>
77
#include <policy/packages.h>
88
#include <policy/policy.h>
9+
#include <policy/rbf.h>
910
#include <primitives/transaction.h>
1011
#include <script/script.h>
1112
#include <serialize.h>
@@ -938,4 +939,147 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
938939
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
939940
}
940941
}
942+
943+
BOOST_FIXTURE_TEST_CASE(package_rbf_tests, TestChain100Setup)
944+
{
945+
mineBlocks(5);
946+
LOCK(::cs_main);
947+
size_t expected_pool_size = m_node.mempool->size();
948+
CKey child_key{GenerateRandomKey()};
949+
CScript parent_spk = GetScriptForDestination(WitnessV0KeyHash(child_key.GetPubKey()));
950+
CKey grandchild_key{GenerateRandomKey()};
951+
CScript child_spk = GetScriptForDestination(WitnessV0KeyHash(grandchild_key.GetPubKey()));
952+
953+
const CAmount coinbase_value{50 * COIN};
954+
// Test that de-duplication works. This is not actually package rbf.
955+
{
956+
// 1 parent paying 200sat, 1 child paying 300sat
957+
Package package1;
958+
// 1 parent paying 200sat, 1 child paying 500sat
959+
Package package2;
960+
// Package1 and package2 have the same parent. The children conflict.
961+
auto mtx_parent = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
962+
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
963+
/*output_destination=*/parent_spk,
964+
/*output_amount=*/coinbase_value - low_fee_amt, /*submit=*/false);
965+
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
966+
package1.push_back(tx_parent);
967+
package2.push_back(tx_parent);
968+
969+
CTransactionRef tx_child_1 = MakeTransactionRef(CreateValidMempoolTransaction(tx_parent, 0, 101, child_key, child_spk, coinbase_value - low_fee_amt - 300, false));
970+
package1.push_back(tx_child_1);
971+
CTransactionRef tx_child_2 = MakeTransactionRef(CreateValidMempoolTransaction(tx_parent, 0, 101, child_key, child_spk, coinbase_value - low_fee_amt - 500, false));
972+
package2.push_back(tx_child_2);
973+
974+
LOCK(m_node.mempool->cs);
975+
const auto submit1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, /*test_accept=*/false, std::nullopt);
976+
if (auto err_1{CheckPackageMempoolAcceptResult(package1, submit1, /*expect_valid=*/true, m_node.mempool.get())}) {
977+
BOOST_ERROR(err_1.value());
978+
}
979+
980+
// Check precise ResultTypes and mempool size. We know it_parent_1 and it_child_1 exist from above call
981+
auto it_parent_1 = submit1.m_tx_results.find(tx_parent->GetWitnessHash());
982+
auto it_child_1 = submit1.m_tx_results.find(tx_child_1->GetWitnessHash());
983+
BOOST_CHECK_EQUAL(it_parent_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
984+
BOOST_CHECK_EQUAL(it_child_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
985+
expected_pool_size += 2;
986+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
987+
988+
const auto submit2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package2, /*test_accept=*/false, std::nullopt);
989+
if (auto err_2{CheckPackageMempoolAcceptResult(package2, submit2, /*expect_valid=*/true, m_node.mempool.get())}) {
990+
BOOST_ERROR(err_2.value());
991+
}
992+
993+
// Check precise ResultTypes and mempool size. We know it_parent_2 and it_child_2 exist from above call
994+
auto it_parent_2 = submit2.m_tx_results.find(tx_parent->GetWitnessHash());
995+
auto it_child_2 = submit2.m_tx_results.find(tx_child_2->GetWitnessHash());
996+
BOOST_CHECK_EQUAL(it_parent_2->second.m_result_type, MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
997+
BOOST_CHECK_EQUAL(it_child_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
998+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
999+
1000+
// child1 has been replaced
1001+
BOOST_CHECK(!m_node.mempool->exists(GenTxid::Txid(tx_child_1->GetHash())));
1002+
}
1003+
1004+
// Test package rbf.
1005+
{
1006+
CTransactionRef tx_parent_1 = MakeTransactionRef(CreateValidMempoolTransaction(
1007+
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
1008+
coinbaseKey, parent_spk, coinbase_value - 200, /*submit=*/false));
1009+
CTransactionRef tx_child_1 = MakeTransactionRef(CreateValidMempoolTransaction(
1010+
tx_parent_1, /*input_vout=*/0, /*input_height=*/101,
1011+
child_key, child_spk, coinbase_value - 400, /*submit=*/false));
1012+
1013+
CTransactionRef tx_parent_2 = MakeTransactionRef(CreateValidMempoolTransaction(
1014+
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
1015+
coinbaseKey, parent_spk, coinbase_value - 800, /*submit=*/false));
1016+
CTransactionRef tx_child_2 = MakeTransactionRef(CreateValidMempoolTransaction(
1017+
tx_parent_2, /*input_vout=*/0, /*input_height=*/101,
1018+
child_key, child_spk, coinbase_value - 800 - 200, /*submit=*/false));
1019+
1020+
CTransactionRef tx_parent_3 = MakeTransactionRef(CreateValidMempoolTransaction(
1021+
m_coinbase_txns[1], /*input_vout=*/0, /*input_height=*/0,
1022+
coinbaseKey, parent_spk, coinbase_value - 199, /*submit=*/false));
1023+
CTransactionRef tx_child_3 = MakeTransactionRef(CreateValidMempoolTransaction(
1024+
tx_parent_3, /*input_vout=*/0, /*input_height=*/101,
1025+
child_key, child_spk, coinbase_value - 199 - 1300, /*submit=*/false));
1026+
1027+
// In all packages, the parents conflict with each other
1028+
BOOST_CHECK(tx_parent_1->GetHash() != tx_parent_2->GetHash() && tx_parent_2->GetHash() != tx_parent_3->GetHash());
1029+
1030+
// 1 parent paying 200sat, 1 child paying 200sat.
1031+
Package package1{tx_parent_1, tx_child_1};
1032+
// 1 parent paying 800sat, 1 child paying 200sat.
1033+
Package package2{tx_parent_2, tx_child_2};
1034+
// 1 parent paying 199sat, 1 child paying 1300sat.
1035+
Package package3{tx_parent_3, tx_child_3};
1036+
1037+
const auto submit1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package1, false, std::nullopt);
1038+
if (auto err_1{CheckPackageMempoolAcceptResult(package1, submit1, /*expect_valid=*/true, m_node.mempool.get())}) {
1039+
BOOST_ERROR(err_1.value());
1040+
}
1041+
auto it_parent_1 = submit1.m_tx_results.find(tx_parent_1->GetWitnessHash());
1042+
auto it_child_1 = submit1.m_tx_results.find(tx_child_1->GetWitnessHash());
1043+
BOOST_CHECK_EQUAL(it_parent_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1044+
BOOST_CHECK_EQUAL(it_child_1->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1045+
expected_pool_size += 2;
1046+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
1047+
1048+
// This replacement is actually not package rbf; the parent carries enough fees
1049+
// to replace the entire package on its own.
1050+
const auto submit2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package2, false, std::nullopt);
1051+
if (auto err_2{CheckPackageMempoolAcceptResult(package2, submit2, /*expect_valid=*/true, m_node.mempool.get())}) {
1052+
BOOST_ERROR(err_2.value());
1053+
}
1054+
auto it_parent_2 = submit2.m_tx_results.find(tx_parent_2->GetWitnessHash());
1055+
auto it_child_2 = submit2.m_tx_results.find(tx_child_2->GetWitnessHash());
1056+
BOOST_CHECK_EQUAL(it_parent_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1057+
BOOST_CHECK_EQUAL(it_child_2->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1058+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
1059+
1060+
// Package RBF, in which the replacement transaction's child sponsors the fees to meet RBF feerate rules
1061+
const auto submit3 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package3, false, std::nullopt);
1062+
if (auto err_3{CheckPackageMempoolAcceptResult(package3, submit3, /*expect_valid=*/true, m_node.mempool.get())}) {
1063+
BOOST_ERROR(err_3.value());
1064+
}
1065+
auto it_parent_3 = submit3.m_tx_results.find(tx_parent_3->GetWitnessHash());
1066+
auto it_child_3 = submit3.m_tx_results.find(tx_child_3->GetWitnessHash());
1067+
BOOST_CHECK_EQUAL(it_parent_3->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1068+
BOOST_CHECK_EQUAL(it_child_3->second.m_result_type, MempoolAcceptResult::ResultType::VALID);
1069+
1070+
// package3 was considered as a package to replace both package2 transactions
1071+
BOOST_CHECK(it_parent_3->second.m_replaced_transactions.size() == 2);
1072+
BOOST_CHECK(it_child_3->second.m_replaced_transactions.empty());
1073+
1074+
std::vector<Wtxid> expected_package3_wtxids({tx_parent_3->GetWitnessHash(), tx_child_3->GetWitnessHash()});
1075+
const auto package3_total_vsize{GetVirtualTransactionSize(*tx_parent_3) + GetVirtualTransactionSize(*tx_child_3)};
1076+
BOOST_CHECK(it_parent_3->second.m_wtxids_fee_calculations.value() == expected_package3_wtxids);
1077+
BOOST_CHECK(it_child_3->second.m_wtxids_fee_calculations.value() == expected_package3_wtxids);
1078+
BOOST_CHECK_EQUAL(it_parent_3->second.m_effective_feerate.value().GetFee(package3_total_vsize), 199 + 1300);
1079+
BOOST_CHECK_EQUAL(it_child_3->second.m_effective_feerate.value().GetFee(package3_total_vsize), 199 + 1300);
1080+
1081+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
1082+
}
1083+
1084+
}
9411085
BOOST_AUTO_TEST_SUITE_END()

src/test/util/txmempool.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <chainparams.h>
88
#include <node/context.h>
99
#include <node/mempool_args.h>
10+
#include <policy/rbf.h>
1011
#include <policy/v3_policy.h>
1112
#include <txmempool.h>
1213
#include <util/check.h>
@@ -68,6 +69,28 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
6869
return strprintf("tx %s unexpectedly failed: %s", wtxid.ToString(), atmp_result.m_state.ToString());
6970
}
7071

72+
// Each subpackage is allowed MAX_REPLACEMENT_CANDIDATES replacements (only checking individually here)
73+
if (atmp_result.m_replaced_transactions.size() > MAX_REPLACEMENT_CANDIDATES) {
74+
return strprintf("tx %s result replaced too many transactions",
75+
wtxid.ToString());
76+
}
77+
78+
// Replacements can't happen for subpackages larger than 2
79+
if (!atmp_result.m_replaced_transactions.empty() &&
80+
atmp_result.m_wtxids_fee_calculations.has_value() && atmp_result.m_wtxids_fee_calculations.value().size() > 2) {
81+
return strprintf("tx %s was part of a too-large package RBF subpackage",
82+
wtxid.ToString());
83+
}
84+
85+
if (!atmp_result.m_replaced_transactions.empty() && mempool) {
86+
LOCK(mempool->cs);
87+
// If replacements occurred and it used 2 transactions, this is a package RBF and should result in a cluster of size 2
88+
if (atmp_result.m_wtxids_fee_calculations.has_value() && atmp_result.m_wtxids_fee_calculations.value().size() == 2) {
89+
const auto cluster = mempool->GatherClusters({tx->GetHash()});
90+
if (cluster.size() != 2) return strprintf("tx %s has too many ancestors or descendants for a package rbf", wtxid.ToString());
91+
}
92+
}
93+
7194
// m_vsize and m_base_fees should exist iff the result was VALID or MEMPOOL_ENTRY
7295
const bool mempool_entry{atmp_result.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY};
7396
if (atmp_result.m_base_fees.has_value() != (valid || mempool_entry)) {
@@ -108,6 +131,11 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
108131
return strprintf("wtxid %s should not be in mempool", wtxid.ToString());
109132
}
110133
}
134+
for (const auto& tx_ref : atmp_result.m_replaced_transactions) {
135+
if (mempool->exists(GenTxid::Txid(tx_ref->GetHash()))) {
136+
return strprintf("tx %s should not be in mempool as it was replaced", tx_ref->GetWitnessHash().ToString());
137+
}
138+
}
111139
}
112140
}
113141
return std::nullopt;

0 commit comments

Comments
 (0)