Skip to content

Commit f24771a

Browse files
committed
relax child-with-unconfirmed-parents rule
This rule was originally introduced along with a very early proposal for package relay as a way to verify that the "correct" child-with-unconfirmed-parents package was provided for a transaction, where correctness was defined as all of the transactions unconfirmed parents. However, we are not planning to introduce a protocol where peers would be asked to send these packages. This rule has downsides: if a transaction has multiple parents but only 1 that requires package CPFP to be accepted, the current rule prevents us from accepting that package. Even if the other parents are already in mempool, the p2p logic will only submit the 1p1c package, which fails this check. See the test in p2p_1p1c_network.py
1 parent 73e754b commit f24771a

File tree

7 files changed

+32
-81
lines changed

7 files changed

+32
-81
lines changed

doc/policy/packages.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ Graph (a directed edge exists between a transaction that spends the output of an
88
For every transaction `t` in a **topologically sorted** package, if any of its parents are present
99
in the package, they appear somewhere in the list before `t`.
1010

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.
11+
A **child-with-parents** package is a topologically sorted package that consists of exactly one child and at least one
12+
of its unconfirmed parents. Not all unconfirmed parents need to be present but no other transactions may be present; the
13+
parent of a parent should not be in this package (unless this "grandparent" is also a direct parent of the child).
1614

1715
## Package Mempool Acceptance Rules
1816

@@ -73,7 +71,7 @@ The following rules are enforced for all packages:
7371
The following rules are only enforced for packages to be submitted to the mempool (not
7472
enforced for test accepts):
7573

76-
* Packages must be child-with-unconfirmed-parents packages. This also means packages must contain at
74+
* Packages must be child-with-parents packages. This also means packages must contain at
7775
least 1 transaction. (#31096)
7876

7977
- *Rationale*: This allows for fee-bumping by CPFP. Allowing multiple parents makes it possible

src/policy/packages.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ static_assert(MAX_PACKAGE_WEIGHT >= MAX_STANDARD_TX_WEIGHT);
2626

2727
// If a package is to be evaluated, it must be at least as large as the mempool's ancestor/descendant limits,
2828
// otherwise transactions that would be individually accepted may be rejected in a package erroneously.
29-
// Since a submitted package must be child-with-unconfirmed-parents (all of the transactions are an ancestor
29+
// Since a submitted package must be child-with-parents (all of the transactions are a parent
3030
// of the child), package limits are ultimately bounded by mempool package limits. Ensure that the
3131
// defaults reflect this constraint.
3232
static_assert(DEFAULT_DESCENDANT_LIMIT >= MAX_PACKAGE_COUNT);

src/test/txpackage_tests.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -441,20 +441,6 @@ BOOST_AUTO_TEST_CASE(package_submission_tests)
441441
BOOST_CHECK_EQUAL(result_quit_early.m_state.GetResult(), PackageValidationResult::PCKG_TX);
442442
}
443443

444-
// Child with missing parent.
445-
mtx_child.vin.emplace_back(COutPoint(package_unrelated[0]->GetHash(), 0));
446-
Package package_missing_parent;
447-
package_missing_parent.push_back(tx_parent);
448-
package_missing_parent.push_back(MakeTransactionRef(mtx_child));
449-
{
450-
const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,
451-
package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{});
452-
BOOST_CHECK(result_missing_parent.m_state.IsInvalid());
453-
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_POLICY);
454-
BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "package-not-child-with-unconfirmed-parents");
455-
BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size);
456-
}
457-
458444
// Submit package with parent + child.
459445
{
460446
const auto submit_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool,

src/validation.cpp

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ class MemPoolAccept
522522
};
523523
}
524524

525-
/** Parameters for child-with-unconfirmed-parents package validation. */
525+
/** Parameters for child-with-parents package validation. */
526526
static ATMPArgs PackageChildWithParents(const CChainParams& chainparams, int64_t accept_time,
527527
std::vector<COutPoint>& coins_to_uncache, const std::optional<CFeeRate>& client_maxfeerate) {
528528
return ATMPArgs{/* m_chainparams */ chainparams,
@@ -1694,7 +1694,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
16941694

16951695
// There are two topologies we are able to handle through this function:
16961696
// (1) A single transaction
1697-
// (2) A child-with-unconfirmed-parents package.
1697+
// (2) A child-with-parents package.
16981698
// Check that the package is well-formed. If it isn't, we won't try to validate any of the
16991699
// transactions and thus won't return any MempoolAcceptResults, just a package-wide error.
17001700

@@ -1703,49 +1703,11 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
17031703
return PackageMempoolAcceptResult(package_state_quit_early, {});
17041704
}
17051705

1706-
if (package.size() > 1) {
1706+
if (package.size() > 1 && !IsChildWithParents(package)) {
17071707
// All transactions in the package must be a parent of the last transaction. This is just an
17081708
// opportunity for us to fail fast on a context-free check without taking the mempool lock.
1709-
if (!IsChildWithParents(package)) {
1710-
package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents");
1711-
return PackageMempoolAcceptResult(package_state_quit_early, {});
1712-
}
1713-
1714-
// IsChildWithParents() guarantees the package is > 1 transactions.
1715-
assert(package.size() > 1);
1716-
// The package must be 1 child with all of its unconfirmed parents. The package is expected to
1717-
// be sorted, so the last transaction is the child.
1718-
const auto& child = package.back();
1719-
std::unordered_set<uint256, SaltedTxidHasher> unconfirmed_parent_txids;
1720-
std::transform(package.cbegin(), package.cend() - 1,
1721-
std::inserter(unconfirmed_parent_txids, unconfirmed_parent_txids.end()),
1722-
[](const auto& tx) { return tx->GetHash(); });
1723-
1724-
// All child inputs must refer to a preceding package transaction or a confirmed UTXO. The only
1725-
// way to verify this is to look up the child's inputs in our current coins view (not including
1726-
// mempool), and enforce that all parents not present in the package be available at chain tip.
1727-
// Since this check can bring new coins into the coins cache, keep track of these coins and
1728-
// uncache them if we don't end up submitting this package to the mempool.
1729-
const CCoinsViewCache& coins_tip_cache = m_active_chainstate.CoinsTip();
1730-
for (const auto& input : child->vin) {
1731-
if (!coins_tip_cache.HaveCoinInCache(input.prevout)) {
1732-
args.m_coins_to_uncache.push_back(input.prevout);
1733-
}
1734-
}
1735-
// Using the MemPoolAccept m_view cache allows us to look up these same coins faster later.
1736-
// This should be connecting directly to CoinsTip, not to m_viewmempool, because we specifically
1737-
// require inputs to be confirmed if they aren't in the package.
1738-
m_view.SetBackend(m_active_chainstate.CoinsTip());
1739-
const auto package_or_confirmed = [this, &unconfirmed_parent_txids](const auto& input) {
1740-
return unconfirmed_parent_txids.count(input.prevout.hash) > 0 || m_view.HaveCoin(input.prevout);
1741-
};
1742-
if (!std::all_of(child->vin.cbegin(), child->vin.cend(), package_or_confirmed)) {
1743-
package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-unconfirmed-parents");
1744-
return PackageMempoolAcceptResult(package_state_quit_early, {});
1745-
}
1746-
// Protect against bugs where we pull more inputs from disk that miss being added to
1747-
// coins_to_uncache. The backend will be connected again when needed in PreChecks.
1748-
m_view.SetBackend(m_dummy);
1709+
package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents");
1710+
return PackageMempoolAcceptResult(package_state_quit_early, {});
17491711
}
17501712

17511713
LOCK(m_pool.cs);

test/functional/p2p_1p1c_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def create_packages(self):
101101
package_hex_2, parent_2, child_2 = self.create_basic_1p1c(self.wallet_nonsegwit)
102102

103103
# 3: 2-parent-1-child package. Both parents are above mempool min feerate. No package submission happens.
104-
# We require packages to be child-with-unconfirmed-parents and only allow 1-parent-1-child packages.
104+
# We require packages to be child-with-parents and only allow 1-parent-1-child packages.
105105
package_hex_3, parent_31, _parent_32, child_3 = self.create_package_2p1c(self.wallet)
106106

107107
# 4: parent + child package where the child spends 2 different outputs from the parent.

test/functional/p2p_opportunistic_1p1c.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ def set_test_params(self):
7878
"-maxmempool=5",
7979
]]
8080

81-
def create_tx_below_mempoolminfee(self, wallet):
81+
def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None):
8282
"""Create a 1-input 1sat/vB transaction using a confirmed UTXO. Decrement and use
8383
self.sequence so that subsequent calls to this function result in unique transactions."""
8484

8585
self.sequence -= 1
8686
assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], FEERATE_1SAT_VB)
8787

88-
return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, confirmed_only=True)
88+
return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, utxo_to_spend=utxo_to_spend, confirmed_only=True)
8989

9090
@cleanup
9191
def test_basic_child_then_parent(self):
@@ -353,11 +353,14 @@ def test_multiple_parents(self):
353353

354354
@cleanup
355355
def test_other_parent_in_mempool(self):
356-
self.log.info("Check opportunistic 1p1c fails if child already has another parent in mempool")
356+
self.log.info("Check opportunistic 1p1c works even if child already has another parent in mempool")
357357
node = self.nodes[0]
358358

359+
# Grandparent will enter mempool by itself
360+
grandparent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
361+
359362
# This parent needs CPFP
360-
parent_low = self.create_tx_below_mempoolminfee(self.wallet)
363+
parent_low = self.create_tx_below_mempoolminfee(self.wallet, utxo_to_spend=grandparent_high["new_utxo"])
361364
# This parent does not need CPFP and can be submitted alone ahead of time
362365
parent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
363366
child = self.wallet.create_self_transfer_multi(
@@ -367,27 +370,27 @@ def test_other_parent_in_mempool(self):
367370

368371
peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
369372

370-
# 1. Send first parent which will be accepted.
373+
# 1. Send grandparent which is accepted
374+
peer_sender.send_and_ping(msg_tx(grandparent_high["tx"]))
375+
assert grandparent_high["txid"] in node.getrawmempool()
376+
377+
# 2. Send first parent which is accepted.
371378
peer_sender.send_and_ping(msg_tx(parent_high["tx"]))
372379
assert parent_high["txid"] in node.getrawmempool()
373380

374-
# 2. Send child.
381+
# 3. Send child which is handled as an orphan.
375382
peer_sender.send_and_ping(msg_tx(child["tx"]))
376383

377-
# 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents
384+
# 4. Node requests parent_low.
378385
parent_low_txid_int = int(parent_low["txid"], 16)
379386
peer_sender.wait_for_getdata([parent_low_txid_int])
380387
peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
381388

382389
node_mempool = node.getrawmempool()
390+
assert grandparent_high["txid"] in node_mempool
383391
assert parent_high["txid"] in node_mempool
384-
assert parent_low["txid"] not in node_mempool
385-
assert child["txid"] not in node_mempool
386-
387-
# Same error if submitted through submitpackage without parent_high
388-
package_hex_missing_parent = [parent_low["hex"], child["hex"]]
389-
result_missing_parent = node.submitpackage(package_hex_missing_parent)
390-
assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents")
392+
assert parent_low["txid"] in node_mempool
393+
assert child["txid"] in node_mempool
391394

392395
def create_small_orphan(self):
393396
"""Create small orphan transaction"""

test/functional/rpc_packages.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,11 @@ def test_conflicting(self):
271271
assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []})
272272
assert tx_child["txid"] not in node.getrawmempool()
273273

274-
# ... and without the in-mempool ancestor tx1 included in the call
274+
# without the in-mempool ancestor tx1 included in the call, tx2 can be submitted, but
275+
# tx_child is missing an input.
275276
submitres = node.submitpackage([tx2["hex"], tx_child["hex"]])
276-
assert_equal(submitres, {'package_msg': 'package-not-child-with-unconfirmed-parents', 'tx-results': {}, 'replaced-transactions': []})
277+
assert_equal(submitres["tx-results"][tx_child["wtxid"]], {"txid": tx_child["txid"], "error": "bad-txns-inputs-missingorspent"})
278+
assert tx2["txid"] in node.getrawmempool()
277279

278280
# Regardless of error type, the child can never enter the mempool
279281
assert tx_child["txid"] not in node.getrawmempool()

0 commit comments

Comments
 (0)