Skip to content

Commit 216f4ca

Browse files
committed
Merge bitcoin/bitcoin#22674: validation: mempool validation and submission for packages of 1 child + parents
046e8ff [unit test] package submission (glozow) e12fafd [validation] de-duplicate package transactions already in mempool (glozow) 8310d94 [packages] add sanity checks for package vs mempool limits (glozow) be3ff15 [validation] full package accept + mempool submission (glozow) 144a290 [policy] require submitted packages to be child-with-unconfirmed-parents (glozow) d59ddc5 [packages/doc] define and document package rules (glozow) ba26169 [unit test] context-free package checks (glozow) 9b2fdca [packages] add static IsChildWithParents function (glozow) Pull request description: This is 1 chunk of [Package Mempool Accept](https://gist.github.com/glozow/dc4e9d5c5b14ade7cdfac40f43adb18a); it restricts packages to 1 child with its parents, doesn't allow conflicts, and doesn't have CPFP (yet). Future PRs (see #22290) will add RBF and CPFP within packages. ACKs for top commit: laanwj: Code review ACK 046e8ff Tree-SHA512: 37dbba37d527712f8efef71ee05c90a8308992615af35f5e0cfeafc60d859cc792737d125aac526e37742fe7683ac8c155ac24af562426213904333c01260c95
2 parents c09b41d + 046e8ff commit 216f4ca

File tree

9 files changed

+567
-21
lines changed

9 files changed

+567
-21
lines changed

doc/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ The Bitcoin repo's [root README](/README.md) contains relevant information on th
8383
- [Reduce Memory](reduce-memory.md)
8484
- [Reduce Traffic](reduce-traffic.md)
8585
- [Tor Support](tor.md)
86+
- [Transaction Relay Policy](policy/README.md)
8687
- [ZMQ](zmq.md)
8788

8889
License

doc/policy/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Transaction Relay Policy
2+
3+
Policy is a set of validation rules, in addition to consensus, enforced for unconfirmed
4+
transactions.
5+
6+
This documentation is not an exhaustive list of all policy rules.
7+
8+
- [Packages](packages.md)
9+
10+

doc/policy/packages.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Package Mempool Accept
2+
3+
## Definitions
4+
5+
A **package** is an ordered list of transactions, representable by a connected Directed Acyclic
6+
Graph (a directed edge exists between a transaction that spends the output of another transaction).
7+
8+
For every transaction `t` in a **topologically sorted** package, if any of its parents are present
9+
in the package, they appear somewhere in the list before `t`.
10+
11+
A **child-with-unconfirmed-parents** package is a topologically sorted package that consists of
12+
exactly one child and all of its unconfirmed parents (no other transactions may be present).
13+
The last transaction in the package is the child, and its package can be canonically defined based
14+
on the current state: each of its inputs must be available in the UTXO set as of the current chain
15+
tip or some preceding transaction in the package.
16+
17+
## Package Mempool Acceptance Rules
18+
19+
The following rules are enforced for all packages:
20+
21+
* Packages cannot exceed `MAX_PACKAGE_COUNT=25` count and `MAX_PACKAGE_SIZE=101KvB` total size
22+
(#20833)
23+
24+
- *Rationale*: This is already enforced as mempool ancestor/descendant limits. If
25+
transactions in a package are all related, exceeding this limit would mean that the package
26+
can either be split up or it wouldn't pass individual mempool policy.
27+
28+
- Note that, if these mempool limits change, package limits should be reconsidered. Users may
29+
also configure their mempool limits differently.
30+
31+
* Packages must be topologically sorted. (#20833)
32+
33+
* Packages cannot have conflicting transactions, i.e. no two transactions in a package can spend
34+
the same inputs. Packages cannot have duplicate transactions. (#20833)
35+
36+
* No transaction in a package can conflict with a mempool transaction. BIP125 Replace By Fee is
37+
currently disabled for packages. (#20833)
38+
39+
- Package RBF may be enabled in the future.
40+
41+
* When packages are evaluated against ancestor/descendant limits, the union of all transactions'
42+
descendants and ancestors is considered. (#21800)
43+
44+
- *Rationale*: This is essentially a "worst case" heuristic intended for packages that are
45+
heavily connected, i.e. some transaction in the package is the ancestor or descendant of all
46+
the other transactions.
47+
48+
The following rules are only enforced for packages to be submitted to the mempool (not enforced for
49+
test accepts):
50+
51+
* Packages must be child-with-unconfirmed-parents packages. This also means packages must contain at
52+
least 2 transactions. (#22674)
53+
54+
- *Rationale*: This allows for fee-bumping by CPFP. Allowing multiple parents makes it possible
55+
to fee-bump a batch of transactions. Restricting packages to a defined topology is easier to
56+
reason about and simplifies the validation logic greatly.
57+
58+
- Warning: Batched fee-bumping may be unsafe for some use cases. Users and application developers
59+
should take caution if utilizing multi-parent packages.

src/policy/packages.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,20 @@ bool CheckPackage(const Package& txns, PackageValidationState& state)
6060
}
6161
return true;
6262
}
63+
64+
bool IsChildWithParents(const Package& package)
65+
{
66+
assert(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;}));
67+
if (package.size() < 2) return false;
68+
69+
// The package is expected to be sorted, so the last transaction is the child.
70+
const auto& child = package.back();
71+
std::unordered_set<uint256, SaltedTxidHasher> input_txids;
72+
std::transform(child->vin.cbegin(), child->vin.cend(),
73+
std::inserter(input_txids, input_txids.end()),
74+
[](const auto& input) { return input.prevout.hash; });
75+
76+
// Every transaction must be a parent of the last transaction in the package.
77+
return std::all_of(package.cbegin(), package.cend() - 1,
78+
[&input_txids](const auto& ptx) { return input_txids.count(ptx->GetHash()) > 0; });
79+
}

src/policy/packages.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,10 @@ class PackageValidationState : public ValidationState<PackageValidationResult> {
4141
*/
4242
bool CheckPackage(const Package& txns, PackageValidationState& state);
4343

44+
/** Context-free check that a package is exactly one child and its parents; not all parents need to
45+
* be present, but the package must not contain any transactions that are not the child's parents.
46+
* It is expected to be sorted, which means the last transaction must be the child.
47+
*/
48+
bool IsChildWithParents(const Package& package);
49+
4450
#endif // BITCOIN_POLICY_PACKAGES_H

src/rpc/rawtransaction.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,8 @@ static RPCHelpMan testmempoolaccept()
10281028
continue;
10291029
}
10301030
const auto& tx_result = it->second;
1031+
// Package testmempoolaccept doesn't allow transactions to already be in the mempool.
1032+
CHECK_NONFATAL(tx_result.m_result_type != MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
10311033
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
10321034
const CAmount fee = tx_result.m_base_fees.value();
10331035
// Check that fee does not exceed maximum fee

src/test/txpackage_tests.cpp

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,217 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup)
114114
// Check that mempool size hasn't changed.
115115
BOOST_CHECK_EQUAL(m_node.mempool->size(), initialPoolSize);
116116
}
117+
118+
BOOST_FIXTURE_TEST_CASE(noncontextual_package_tests, TestChain100Setup)
119+
{
120+
// The signatures won't be verified so we can just use a placeholder
121+
CKey placeholder_key;
122+
placeholder_key.MakeNewKey(true);
123+
CScript spk = GetScriptForDestination(PKHash(placeholder_key.GetPubKey()));
124+
CKey placeholder_key_2;
125+
placeholder_key_2.MakeNewKey(true);
126+
CScript spk2 = GetScriptForDestination(PKHash(placeholder_key_2.GetPubKey()));
127+
128+
// Parent and Child Package
129+
{
130+
auto mtx_parent = CreateValidMempoolTransaction(m_coinbase_txns[0], 0, 0, coinbaseKey, spk,
131+
CAmount(49 * COIN), /* submit */ false);
132+
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
133+
134+
auto mtx_child = CreateValidMempoolTransaction(tx_parent, 0, 101, placeholder_key, spk2,
135+
CAmount(48 * COIN), /* submit */ false);
136+
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
137+
138+
PackageValidationState state;
139+
BOOST_CHECK(CheckPackage({tx_parent, tx_child}, state));
140+
BOOST_CHECK(!CheckPackage({tx_child, tx_parent}, state));
141+
BOOST_CHECK_EQUAL(state.GetResult(), PackageValidationResult::PCKG_POLICY);
142+
BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted");
143+
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
144+
}
145+
146+
// 24 Parents and 1 Child
147+
{
148+
Package package;
149+
CMutableTransaction child;
150+
for (int i{0}; i < 24; ++i) {
151+
auto parent = MakeTransactionRef(CreateValidMempoolTransaction(m_coinbase_txns[i + 1],
152+
0, 0, coinbaseKey, spk, CAmount(48 * COIN), false));
153+
package.emplace_back(parent);
154+
child.vin.push_back(CTxIn(COutPoint(parent->GetHash(), 0)));
155+
}
156+
child.vout.push_back(CTxOut(47 * COIN, spk2));
157+
158+
// The child must be in the package.
159+
BOOST_CHECK(!IsChildWithParents(package));
160+
161+
// The parents can be in any order.
162+
FastRandomContext rng;
163+
Shuffle(package.begin(), package.end(), rng);
164+
package.push_back(MakeTransactionRef(child));
165+
166+
PackageValidationState state;
167+
BOOST_CHECK(CheckPackage(package, state));
168+
BOOST_CHECK(IsChildWithParents(package));
169+
170+
package.erase(package.begin());
171+
BOOST_CHECK(IsChildWithParents(package));
172+
173+
// The package cannot have unrelated transactions.
174+
package.insert(package.begin(), m_coinbase_txns[0]);
175+
BOOST_CHECK(!IsChildWithParents(package));
176+
}
177+
178+
// 2 Parents and 1 Child where one parent depends on the other.
179+
{
180+
CMutableTransaction mtx_parent;
181+
mtx_parent.vin.push_back(CTxIn(COutPoint(m_coinbase_txns[0]->GetHash(), 0)));
182+
mtx_parent.vout.push_back(CTxOut(20 * COIN, spk));
183+
mtx_parent.vout.push_back(CTxOut(20 * COIN, spk2));
184+
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
185+
186+
CMutableTransaction mtx_parent_also_child;
187+
mtx_parent_also_child.vin.push_back(CTxIn(COutPoint(tx_parent->GetHash(), 0)));
188+
mtx_parent_also_child.vout.push_back(CTxOut(20 * COIN, spk));
189+
CTransactionRef tx_parent_also_child = MakeTransactionRef(mtx_parent_also_child);
190+
191+
CMutableTransaction mtx_child;
192+
mtx_child.vin.push_back(CTxIn(COutPoint(tx_parent->GetHash(), 1)));
193+
mtx_child.vin.push_back(CTxIn(COutPoint(tx_parent_also_child->GetHash(), 0)));
194+
mtx_child.vout.push_back(CTxOut(39 * COIN, spk));
195+
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
196+
197+
PackageValidationState state;
198+
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child}));
199+
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
200+
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child, tx_child}));
201+
// IsChildWithParents does not detect unsorted parents.
202+
BOOST_CHECK(IsChildWithParents({tx_parent_also_child, tx_parent, tx_child}));
203+
BOOST_CHECK(CheckPackage({tx_parent, tx_parent_also_child, tx_child}, state));
204+
BOOST_CHECK(!CheckPackage({tx_parent_also_child, tx_parent, tx_child}, state));
205+
BOOST_CHECK_EQUAL(state.GetResult(), PackageValidationResult::PCKG_POLICY);
206+
BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted");
207+
}
208+
}
209+
210+
BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
211+
{
212+
LOCK(cs_main);
213+
unsigned int expected_pool_size = m_node.mempool->size();
214+
CKey parent_key;
215+
parent_key.MakeNewKey(true);
216+
CScript parent_locking_script = GetScriptForDestination(PKHash(parent_key.GetPubKey()));
217+
218+
// Unrelated transactions are not allowed in package submission.
219+
Package package_unrelated;
220+
for (size_t i{0}; i < 10; ++i) {
221+
auto mtx = CreateValidMempoolTransaction(/* input_transaction */ m_coinbase_txns[i + 25], /* vout */ 0,
222+
/* input_height */ 0, /* input_signing_key */ coinbaseKey,
223+
/* output_destination */ parent_locking_script,
224+
/* output_amount */ CAmount(49 * COIN), /* submit */ false);
225+
package_unrelated.emplace_back(MakeTransactionRef(mtx));
226+
}
227+
auto result_unrelated_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
228+
package_unrelated, /* test_accept */ false);
229+
BOOST_CHECK(result_unrelated_submit.m_state.IsInvalid());
230+
BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
231+
BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetRejectReason(), "package-not-child-with-parents");
232+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
233+
234+
// Parent and Child (and Grandchild) Package
235+
Package package_parent_child;
236+
Package package_3gen;
237+
auto mtx_parent = CreateValidMempoolTransaction(/* input_transaction */ m_coinbase_txns[0], /* vout */ 0,
238+
/* input_height */ 0, /* input_signing_key */ coinbaseKey,
239+
/* output_destination */ parent_locking_script,
240+
/* output_amount */ CAmount(49 * COIN), /* submit */ false);
241+
CTransactionRef tx_parent = MakeTransactionRef(mtx_parent);
242+
package_parent_child.push_back(tx_parent);
243+
package_3gen.push_back(tx_parent);
244+
245+
CKey child_key;
246+
child_key.MakeNewKey(true);
247+
CScript child_locking_script = GetScriptForDestination(PKHash(child_key.GetPubKey()));
248+
auto mtx_child = CreateValidMempoolTransaction(/* input_transaction */ tx_parent, /* vout */ 0,
249+
/* input_height */ 101, /* input_signing_key */ parent_key,
250+
/* output_destination */ child_locking_script,
251+
/* output_amount */ CAmount(48 * COIN), /* submit */ false);
252+
CTransactionRef tx_child = MakeTransactionRef(mtx_child);
253+
package_parent_child.push_back(tx_child);
254+
package_3gen.push_back(tx_child);
255+
256+
CKey grandchild_key;
257+
grandchild_key.MakeNewKey(true);
258+
CScript grandchild_locking_script = GetScriptForDestination(PKHash(grandchild_key.GetPubKey()));
259+
auto mtx_grandchild = CreateValidMempoolTransaction(/* input_transaction */ tx_child, /* vout */ 0,
260+
/* input_height */ 101, /* input_signing_key */ child_key,
261+
/* output_destination */ grandchild_locking_script,
262+
/* output_amount */ CAmount(47 * COIN), /* submit */ false);
263+
CTransactionRef tx_grandchild = MakeTransactionRef(mtx_grandchild);
264+
package_3gen.push_back(tx_grandchild);
265+
266+
// 3 Generations is not allowed.
267+
{
268+
auto result_3gen_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
269+
package_3gen, /* test_accept */ false);
270+
BOOST_CHECK(result_3gen_submit.m_state.IsInvalid());
271+
BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
272+
BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetRejectReason(), "package-not-child-with-parents");
273+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
274+
}
275+
276+
// Child with missing parent.
277+
mtx_child.vin.push_back(CTxIn(COutPoint(package_unrelated[0]->GetHash(), 0)));
278+
Package package_missing_parent;
279+
package_missing_parent.push_back(tx_parent);
280+
package_missing_parent.push_back(MakeTransactionRef(mtx_child));
281+
{
282+
const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
283+
package_missing_parent, /* test_accept */ false);
284+
BOOST_CHECK(result_missing_parent.m_state.IsInvalid());
285+
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
286+
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "package-not-child-with-unconfirmed-parents");
287+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
288+
289+
}
290+
291+
// Submit package with parent + child.
292+
{
293+
const auto submit_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
294+
package_parent_child, /* test_accept */ false);
295+
expected_pool_size += 2;
296+
BOOST_CHECK_MESSAGE(submit_parent_child.m_state.IsValid(),
297+
"Package validation unexpectedly failed: " << submit_parent_child.m_state.GetRejectReason());
298+
auto it_parent = submit_parent_child.m_tx_results.find(tx_parent->GetWitnessHash());
299+
auto it_child = submit_parent_child.m_tx_results.find(tx_child->GetWitnessHash());
300+
BOOST_CHECK(it_parent != submit_parent_child.m_tx_results.end());
301+
BOOST_CHECK(it_parent->second.m_state.IsValid());
302+
BOOST_CHECK(it_child != submit_parent_child.m_tx_results.end());
303+
BOOST_CHECK(it_child->second.m_state.IsValid());
304+
305+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
306+
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_parent->GetHash())));
307+
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_child->GetHash())));
308+
}
309+
310+
// Already-in-mempool transactions should be detected and de-duplicated.
311+
{
312+
const auto submit_deduped = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
313+
package_parent_child, /* test_accept */ false);
314+
BOOST_CHECK_MESSAGE(submit_deduped.m_state.IsValid(),
315+
"Package validation unexpectedly failed: " << submit_deduped.m_state.GetRejectReason());
316+
auto it_parent_deduped = submit_deduped.m_tx_results.find(tx_parent->GetWitnessHash());
317+
auto it_child_deduped = submit_deduped.m_tx_results.find(tx_child->GetWitnessHash());
318+
BOOST_CHECK(it_parent_deduped != submit_deduped.m_tx_results.end());
319+
BOOST_CHECK(it_parent_deduped->second.m_state.IsValid());
320+
BOOST_CHECK(it_parent_deduped->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
321+
BOOST_CHECK(it_child_deduped != submit_deduped.m_tx_results.end());
322+
BOOST_CHECK(it_child_deduped->second.m_state.IsValid());
323+
BOOST_CHECK(it_child_deduped->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
324+
325+
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
326+
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_parent->GetHash())));
327+
BOOST_CHECK(m_node.mempool->exists(GenTxid::Txid(tx_child->GetHash())));
328+
}
329+
}
117330
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)