From f24771af0581e8117bd638227469e37cc69c5103 Mon Sep 17 00:00:00 2001 From: glozow Date: Fri, 22 Nov 2024 07:17:36 -0500 Subject: [PATCH 1/4] 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 --- doc/policy/packages.md | 10 ++--- src/policy/packages.h | 2 +- src/test/txpackage_tests.cpp | 14 ------- src/validation.cpp | 48 +++-------------------- test/functional/p2p_1p1c_network.py | 2 +- test/functional/p2p_opportunistic_1p1c.py | 31 ++++++++------- test/functional/rpc_packages.py | 6 ++- 7 files changed, 32 insertions(+), 81 deletions(-) diff --git a/doc/policy/packages.md b/doc/policy/packages.md index febdbbf13ca..4c45470e24e 100644 --- a/doc/policy/packages.md +++ b/doc/policy/packages.md @@ -8,11 +8,9 @@ Graph (a directed edge exists between a transaction that spends the output of an For every transaction `t` in a **topologically sorted** package, if any of its parents are present in the package, they appear somewhere in the list before `t`. -A **child-with-unconfirmed-parents** package is a topologically sorted package that consists of -exactly one child and all of its unconfirmed parents (no other transactions may be present). -The last transaction in the package is the child, and its package can be canonically defined based -on the current state: each of its inputs must be available in the UTXO set as of the current chain -tip or some preceding transaction in the package. +A **child-with-parents** package is a topologically sorted package that consists of exactly one child and at least one +of its unconfirmed parents. Not all unconfirmed parents need to be present but no other transactions may be present; the +parent of a parent should not be in this package (unless this "grandparent" is also a direct parent of the child). ## Package Mempool Acceptance Rules @@ -73,7 +71,7 @@ The following rules are enforced for all packages: The following rules are only enforced for packages to be submitted to the mempool (not enforced for test accepts): -* Packages must be child-with-unconfirmed-parents packages. This also means packages must contain at +* Packages must be child-with-parents packages. This also means packages must contain at least 1 transaction. (#31096) - *Rationale*: This allows for fee-bumping by CPFP. Allowing multiple parents makes it possible diff --git a/src/policy/packages.h b/src/policy/packages.h index 4b2350edac7..5ed7f32a434 100644 --- a/src/policy/packages.h +++ b/src/policy/packages.h @@ -26,7 +26,7 @@ static_assert(MAX_PACKAGE_WEIGHT >= MAX_STANDARD_TX_WEIGHT); // If a package is to be evaluated, it must be at least as large as the mempool's ancestor/descendant limits, // otherwise transactions that would be individually accepted may be rejected in a package erroneously. -// Since a submitted package must be child-with-unconfirmed-parents (all of the transactions are an ancestor +// Since a submitted package must be child-with-parents (all of the transactions are a parent // of the child), package limits are ultimately bounded by mempool package limits. Ensure that the // defaults reflect this constraint. static_assert(DEFAULT_DESCENDANT_LIMIT >= MAX_PACKAGE_COUNT); diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp index 9b6d22dde32..e8976fcf426 100644 --- a/src/test/txpackage_tests.cpp +++ b/src/test/txpackage_tests.cpp @@ -441,20 +441,6 @@ BOOST_AUTO_TEST_CASE(package_submission_tests) BOOST_CHECK_EQUAL(result_quit_early.m_state.GetResult(), PackageValidationResult::PCKG_TX); } - // Child with missing parent. - mtx_child.vin.emplace_back(COutPoint(package_unrelated[0]->GetHash(), 0)); - Package package_missing_parent; - package_missing_parent.push_back(tx_parent); - package_missing_parent.push_back(MakeTransactionRef(mtx_child)); - { - const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{}); - BOOST_CHECK(result_missing_parent.m_state.IsInvalid()); - 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); - } - // Submit package with parent + child. { const auto submit_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, diff --git a/src/validation.cpp b/src/validation.cpp index 7992b857129..7a87ba48457 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -522,7 +522,7 @@ public: }; } - /** Parameters for child-with-unconfirmed-parents package validation. */ + /** Parameters for child-with-parents package validation. */ static ATMPArgs PackageChildWithParents(const CChainParams& chainparams, int64_t accept_time, std::vector& coins_to_uncache, const std::optional& client_maxfeerate) { return ATMPArgs{/* m_chainparams */ chainparams, @@ -1694,7 +1694,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package, // There are two topologies we are able to handle through this function: // (1) A single transaction - // (2) A child-with-unconfirmed-parents package. + // (2) A child-with-parents package. // 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. @@ -1703,49 +1703,11 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package, return PackageMempoolAcceptResult(package_state_quit_early, {}); } - if (package.size() > 1) { + if (package.size() > 1 && !IsChildWithParents(package)) { // 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_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents"); - return PackageMempoolAcceptResult(package_state_quit_early, {}); - } - - // IsChildWithParents() guarantees the package is > 1 transactions. - assert(package.size() > 1); - // The package must be 1 child with all of its unconfirmed parents. The package is expected to - // be sorted, so the last transaction is the child. - const auto& child = package.back(); - std::unordered_set unconfirmed_parent_txids; - std::transform(package.cbegin(), package.cend() - 1, - std::inserter(unconfirmed_parent_txids, unconfirmed_parent_txids.end()), - [](const auto& tx) { return tx->GetHash(); }); - - // All child inputs must refer to a preceding package transaction or a confirmed UTXO. The only - // way to verify this is to look up the child's inputs in our current coins view (not including - // mempool), and enforce that all parents not present in the package be available at chain tip. - // Since this check can bring new coins into the coins cache, keep track of these coins and - // uncache them if we don't end up submitting this package to the mempool. - const CCoinsViewCache& coins_tip_cache = m_active_chainstate.CoinsTip(); - for (const auto& input : child->vin) { - if (!coins_tip_cache.HaveCoinInCache(input.prevout)) { - args.m_coins_to_uncache.push_back(input.prevout); - } - } - // Using the MemPoolAccept m_view cache allows us to look up these same coins faster later. - // This should be connecting directly to CoinsTip, not to m_viewmempool, because we specifically - // require inputs to be confirmed if they aren't in the package. - m_view.SetBackend(m_active_chainstate.CoinsTip()); - const auto package_or_confirmed = [this, &unconfirmed_parent_txids](const auto& input) { - 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_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); + package_state_quit_early.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-child-with-parents"); + return PackageMempoolAcceptResult(package_state_quit_early, {}); } LOCK(m_pool.cs); diff --git a/test/functional/p2p_1p1c_network.py b/test/functional/p2p_1p1c_network.py index fbf242a06c8..4d0f11ea99b 100755 --- a/test/functional/p2p_1p1c_network.py +++ b/test/functional/p2p_1p1c_network.py @@ -101,7 +101,7 @@ class PackageRelayTest(BitcoinTestFramework): package_hex_2, parent_2, child_2 = self.create_basic_1p1c(self.wallet_nonsegwit) # 3: 2-parent-1-child package. Both parents are above mempool min feerate. No package submission happens. - # We require packages to be child-with-unconfirmed-parents and only allow 1-parent-1-child packages. + # We require packages to be child-with-parents and only allow 1-parent-1-child packages. package_hex_3, parent_31, _parent_32, child_3 = self.create_package_2p1c(self.wallet) # 4: parent + child package where the child spends 2 different outputs from the parent. diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py index e841cf08061..6652442c362 100755 --- a/test/functional/p2p_opportunistic_1p1c.py +++ b/test/functional/p2p_opportunistic_1p1c.py @@ -78,14 +78,14 @@ class PackageRelayTest(BitcoinTestFramework): "-maxmempool=5", ]] - def create_tx_below_mempoolminfee(self, wallet): + def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None): """Create a 1-input 1sat/vB transaction using a confirmed UTXO. Decrement and use self.sequence so that subsequent calls to this function result in unique transactions.""" self.sequence -= 1 assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], FEERATE_1SAT_VB) - return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, confirmed_only=True) + return wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB, sequence=self.sequence, utxo_to_spend=utxo_to_spend, confirmed_only=True) @cleanup def test_basic_child_then_parent(self): @@ -353,11 +353,14 @@ class PackageRelayTest(BitcoinTestFramework): @cleanup def test_other_parent_in_mempool(self): - self.log.info("Check opportunistic 1p1c fails if child already has another parent in mempool") + self.log.info("Check opportunistic 1p1c works even if child already has another parent in mempool") node = self.nodes[0] + # Grandparent will enter mempool by itself + grandparent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True) + # This parent needs CPFP - parent_low = self.create_tx_below_mempoolminfee(self.wallet) + parent_low = self.create_tx_below_mempoolminfee(self.wallet, utxo_to_spend=grandparent_high["new_utxo"]) # This parent does not need CPFP and can be submitted alone ahead of time parent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True) child = self.wallet.create_self_transfer_multi( @@ -367,27 +370,27 @@ class PackageRelayTest(BitcoinTestFramework): peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay") - # 1. Send first parent which will be accepted. + # 1. Send grandparent which is accepted + peer_sender.send_and_ping(msg_tx(grandparent_high["tx"])) + assert grandparent_high["txid"] in node.getrawmempool() + + # 2. Send first parent which is accepted. peer_sender.send_and_ping(msg_tx(parent_high["tx"])) assert parent_high["txid"] in node.getrawmempool() - # 2. Send child. + # 3. Send child which is handled as an orphan. peer_sender.send_and_ping(msg_tx(child["tx"])) - # 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents + # 4. Node requests parent_low. parent_low_txid_int = int(parent_low["txid"], 16) peer_sender.wait_for_getdata([parent_low_txid_int]) peer_sender.send_and_ping(msg_tx(parent_low["tx"])) node_mempool = node.getrawmempool() + assert grandparent_high["txid"] in node_mempool assert parent_high["txid"] in node_mempool - assert parent_low["txid"] not in node_mempool - assert child["txid"] not in node_mempool - - # Same error if submitted through submitpackage without parent_high - package_hex_missing_parent = [parent_low["hex"], child["hex"]] - result_missing_parent = node.submitpackage(package_hex_missing_parent) - assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents") + assert parent_low["txid"] in node_mempool + assert child["txid"] in node_mempool def create_small_orphan(self): """Create small orphan transaction""" diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 3947053c41e..efb08784f51 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -271,9 +271,11 @@ class RPCPackagesTest(BitcoinTestFramework): assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []}) assert tx_child["txid"] not in node.getrawmempool() - # ... and without the in-mempool ancestor tx1 included in the call + # without the in-mempool ancestor tx1 included in the call, tx2 can be submitted, but + # tx_child is missing an input. submitres = node.submitpackage([tx2["hex"], tx_child["hex"]]) - assert_equal(submitres, {'package_msg': 'package-not-child-with-unconfirmed-parents', 'tx-results': {}, 'replaced-transactions': []}) + assert_equal(submitres["tx-results"][tx_child["wtxid"]], {"txid": tx_child["txid"], "error": "bad-txns-inputs-missingorspent"}) + assert tx2["txid"] in node.getrawmempool() # Regardless of error type, the child can never enter the mempool assert tx_child["txid"] not in node.getrawmempool() From 525be56741cff5f89d596b8a0c44df00f2209bcb Mon Sep 17 00:00:00 2001 From: glozow Date: Tue, 10 Dec 2024 07:35:11 -0500 Subject: [PATCH 2/4] [unit test] package submission 2p1c with 1 parent missing --- src/test/txpackage_tests.cpp | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp index e8976fcf426..1f167586fe9 100644 --- a/src/test/txpackage_tests.cpp +++ b/src/test/txpackage_tests.cpp @@ -355,6 +355,9 @@ BOOST_AUTO_TEST_CASE(noncontextual_package_tests) BOOST_AUTO_TEST_CASE(package_submission_tests) { + // Mine blocks to mature coinbases. + mineBlocks(3); + MockMempoolMinFee(CFeeRate(5000)); LOCK(cs_main); unsigned int expected_pool_size = m_node.mempool->size(); CKey parent_key = GenerateRandomKey(); @@ -478,6 +481,60 @@ BOOST_AUTO_TEST_CASE(package_submission_tests) BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); } + + // In-mempool parent and child with missing parent. + { + auto tx_parent_1 = MakeTransactionRef(CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[1], /*input_vout=*/0, + /*input_height=*/0, /*input_signing_key=*/coinbaseKey, + /*output_destination=*/parent_locking_script, + /*output_amount=*/CAmount(50 * COIN - low_fee_amt), /*submit=*/false)); + auto tx_parent_2 = MakeTransactionRef(CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[2], /*input_vout=*/0, + /*input_height=*/0, /*input_signing_key=*/coinbaseKey, + /*output_destination=*/parent_locking_script, + /*output_amount=*/CAmount(50 * COIN - 800), /*submit=*/false)); + + auto tx_child_missing_parent = MakeTransactionRef(CreateValidMempoolTransaction({tx_parent_1, tx_parent_2}, + {{tx_parent_1->GetHash(), 0}, {tx_parent_2->GetHash(), 0}}, + /*input_height=*/0, {parent_key}, + {{49 * COIN, child_locking_script}}, /*submit=*/false)); + + Package package_missing_parent{tx_parent_1, tx_child_missing_parent}; + + const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, + package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{}); + if (auto err_missing_parent{CheckPackageMempoolAcceptResult(package_missing_parent, result_missing_parent, /*expect_valid=*/false, m_node.mempool.get())}) { + BOOST_ERROR(err_missing_parent.value()); + } else { + auto it_parent = result_missing_parent.m_tx_results.find(tx_parent_1->GetWitnessHash()); + auto it_child = result_missing_parent.m_tx_results.find(tx_child_missing_parent->GetWitnessHash()); + + BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_TX); + BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "transaction failed"); + + BOOST_CHECK_EQUAL(it_parent->second.m_state.GetResult(), TxValidationResult::TX_RECONSIDERABLE); + BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MISSING_INPUTS); + BOOST_CHECK_EQUAL(it_child->second.m_state.GetRejectReason(), "bad-txns-inputs-missingorspent"); + BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); + } + + // Submit parent2 ahead of time, should become ok. + Package package_just_parent2{tx_parent_2}; + expected_pool_size += 1; + const auto result_just_parent2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, + package_just_parent2, /*test_accept=*/false, /*client_maxfeerate=*/{}); + if (auto err_parent2{CheckPackageMempoolAcceptResult(package_just_parent2, result_just_parent2, /*expect_valid=*/true, m_node.mempool.get())}) { + BOOST_ERROR(err_parent2.value()); + } + BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); + + const auto result_parent_already_in = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, + package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{}); + expected_pool_size += 2; + if (auto err_parent_already_in{CheckPackageMempoolAcceptResult(package_missing_parent, result_parent_already_in, /*expect_valid=*/true, m_node.mempool.get())}) { + BOOST_ERROR(err_parent_already_in.value()); + } + BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); + } } // Tests for packages containing a single transaction From 12f48d5ed302e92a334dbe971c66df467d402655 Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Wed, 11 Jun 2025 09:29:42 -0400 Subject: [PATCH 3/4] test: add chained 1p1c propagation test --- test/functional/p2p_opportunistic_1p1c.py | 43 ++++++++++++++++++++++- test/functional/rpc_packages.py | 36 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py index 6652442c362..56ab394baaf 100755 --- a/test/functional/p2p_opportunistic_1p1c.py +++ b/test/functional/p2p_opportunistic_1p1c.py @@ -353,7 +353,7 @@ class PackageRelayTest(BitcoinTestFramework): @cleanup def test_other_parent_in_mempool(self): - self.log.info("Check opportunistic 1p1c works even if child already has another parent in mempool") + self.log.info("Check opportunistic 1p1c works when part of a 2p1c (child already has another parent in mempool)") node = self.nodes[0] # Grandparent will enter mempool by itself @@ -550,6 +550,46 @@ class PackageRelayTest(BitcoinTestFramework): assert orphan_tx.txid_hex in node.getrawmempool() assert_equal(node.getmempoolentry(orphan_tx.txid_hex)["ancestorcount"], 2) + @cleanup + def test_1p1c_on_1p1c(self): + self.log.info("Test that opportunistic 1p1c works when part of a 4-generation chain (1p1c chained from a 1p1c)") + node = self.nodes[0] + + # Prep 2 generations of 1p1c packages to be relayed + low_fee_great_grandparent = self.create_tx_below_mempoolminfee(self.wallet) + high_fee_grandparent = self.wallet.create_self_transfer(utxo_to_spend=low_fee_great_grandparent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB) + + low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet, utxo_to_spend=high_fee_grandparent["new_utxo"]) + high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB) + + peer_sender = node.add_p2p_connection(P2PInterface()) + + # The 1p1c that spends the confirmed utxo must be received first. Afterwards, the "younger" 1p1c can be received. + for package in [[low_fee_great_grandparent, high_fee_grandparent], [low_fee_parent, high_fee_child]]: + # Aliases + parent_relative, child_relative = package + + # 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input. + high_child_wtxid_int = child_relative["tx"].wtxid_int + peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)])) + peer_sender.wait_for_getdata([high_child_wtxid_int]) + peer_sender.send_and_ping(msg_tx(child_relative["tx"])) + + # 2. Node requests the missing parent by txid. + parent_txid_int = parent_relative["tx"].txid_int + peer_sender.wait_for_getdata([parent_txid_int]) + + # 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted. + peer_sender.send_and_ping(msg_tx(parent_relative["tx"])) + + # 4. All transactions should now be in mempool. + node_mempool = node.getrawmempool() + assert low_fee_great_grandparent["txid"] in node_mempool + assert high_fee_grandparent["txid"] in node_mempool + assert low_fee_parent["txid"] in node_mempool + assert high_fee_child["txid"] in node_mempool + assert_equal(node.getmempoolentry(low_fee_great_grandparent["txid"])["descendantcount"], 4) + def run_test(self): node = self.nodes[0] # To avoid creating transactions with the same txid (can happen if we set the same feerate @@ -583,6 +623,7 @@ class PackageRelayTest(BitcoinTestFramework): self.test_parent_consensus_failure() self.test_multiple_parents() self.test_other_parent_in_mempool() + self.test_1p1c_on_1p1c() self.test_orphanage_dos_large() self.test_orphanage_dos_many() diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index efb08784f51..d52fe33a847 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -82,6 +82,7 @@ class RPCPackagesTest(BitcoinTestFramework): self.independent_txns_testres_blank = [{ "txid": res["txid"], "wtxid": res["wtxid"]} for res in self.independent_txns_testres] + self.test_submitpackage_with_ancestors() self.test_independent(coin) self.test_chain() self.test_multiple_children() @@ -501,5 +502,40 @@ class RPCPackagesTest(BitcoinTestFramework): assert_equal(pkg_result["tx-results"][tx.wtxid_hex]["error"], "scriptpubkey") assert_equal(node.getrawmempool(), [chained_txns_burn[0]["txid"]]) + def test_submitpackage_with_ancestors(self): + self.log.info("Test that submitpackage can send a package that has in-mempool ancestors") + node = self.nodes[0] + peer = node.add_p2p_connection(P2PTxInvStore()) + + parent_tx = self.wallet.create_self_transfer() + child_tx = self.wallet.create_self_transfer(utxo_to_spend=parent_tx["new_utxo"]) + grandchild_tx = self.wallet.create_self_transfer(utxo_to_spend=child_tx["new_utxo"]) + ggrandchild_tx = self.wallet.create_self_transfer(utxo_to_spend=grandchild_tx["new_utxo"]) + + # Submitting them all together doesn't work, as the topology is not child-with-parents + assert_raises_rpc_error(-25, "package topology disallowed", node.submitpackage, [parent_tx["hex"], child_tx["hex"], grandchild_tx["hex"], ggrandchild_tx["hex"]]) + + # Submit older package and check acceptance + result_submit_older = node.submitpackage(package=[parent_tx["hex"], child_tx["hex"]]) + assert_equal(result_submit_older["package_msg"], "success") + mempool = node.getrawmempool() + assert parent_tx["txid"] in mempool + assert child_tx["txid"] in mempool + + # Submit younger package and check acceptance + result_submit_younger = node.submitpackage(package=[grandchild_tx["hex"], ggrandchild_tx["hex"]]) + assert_equal(result_submit_younger["package_msg"], "success") + mempool = node.getrawmempool() + + assert parent_tx["txid"] in mempool + assert child_tx["txid"] in mempool + assert grandchild_tx["txid"] in mempool + assert ggrandchild_tx["txid"] in mempool + + # The node should announce each transaction. + peer.wait_for_broadcast([tx["tx"].wtxid_hex for tx in [parent_tx, child_tx, grandchild_tx, ggrandchild_tx]]) + self.generate(node, 1) + + if __name__ == "__main__": RPCPackagesTest(__file__).main() From ea17a9423fb431a86d36927b02d3624f654fd867 Mon Sep 17 00:00:00 2001 From: glozow Date: Thu, 12 Jun 2025 16:58:20 -0400 Subject: [PATCH 4/4] [doc] release note for relaxing requirement of all unconfirmed parents present --- doc/release-notes-31385.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/release-notes-31385.md diff --git a/doc/release-notes-31385.md b/doc/release-notes-31385.md new file mode 100644 index 00000000000..2db108d1005 --- /dev/null +++ b/doc/release-notes-31385.md @@ -0,0 +1,14 @@ +RPC + +The `submitpackage` RPC, which allows submissions of child-with-parents +packages, no longer requires that all unconfirmed parents be present. The +package may contain other in-mempool ancestors as well. (#31385) + +P2P + +Opportunistic 1-parent-1-child package relay has been improved to handle +situations when the child already has unconfirmed parent(s) in the mempool. +This means that 1p1c packages can be accepted and propagate, even if they are +connected to broader topologies: multi-parent-1-child (where only 1 parent +requires fee-bumping), grandparent-parent-child (where only parent requires +fee-bumping) etc. (#31385)