diff options
author | Ava Chow <github@achow101.com> | 2024-02-09 23:29:39 -0500 |
---|---|---|
committer | Ava Chow <github@achow101.com> | 2024-02-09 23:37:57 -0500 |
commit | 7143d4388407ab3d12005e55a02d5e8f334e4dc9 (patch) | |
tree | a377c5653b546f1eee5d4c02f074760e8a076bab /test | |
parent | 1d334d830f9423caef0a230dc62e4458a56d3bbe (diff) | |
parent | 29029df5c700e6940c712028303761d91ae15847 (diff) | |
download | bitcoin-7143d4388407ab3d12005e55a02d5e8f334e4dc9.tar.xz |
Merge bitcoin/bitcoin#28948: v3 transaction policy for anti-pinning
29029df5c700e6940c712028303761d91ae15847 [doc] v3 signaling in mempool-replacements.md (glozow)
e643ea795e4b6fea4a6bbb3d72870ee6a4c836b1 [fuzz] v3 transactions and sigop-adjusted vsize (glozow)
1fd16b5c62f54c7f4c60122acd65d852f63d1e8b [functional test] v3 transaction submission (glozow)
27c8786ba918a42c860e6a50eaee9fdf56d7c646 test framework: Add and use option for tx-version in MiniWallet methods (MarcoFalke)
9a1fea55b29fe025355b06b45e3d77d192acc635 [policy/validation] allow v3 transactions with certain restrictions (glozow)
eb8d5a2e7d939dd3ee683486e98702079e0dfcc0 [policy] add v3 policy rules (glozow)
9a29d470fbb62bbb27d517efeafe46ff03c25f54 [rpc] return full string for package_msg and package-error (glozow)
158623b8e0726dff7eae4288138f1710e727db9c [refactor] change Workspace::m_conflicts and adjacent funcs/structs to use Txid (glozow)
Pull request description:
See #27463 for overall package relay tracking.
Delving Bitcoin discussion thread: https://delvingbitcoin.org/t/v3-transaction-policy-for-anti-pinning/340
Delving Bitcoin discussion for LN usage: https://delvingbitcoin.org/t/lightning-transactions-with-v3-and-ephemeral-anchors/418
Rationale:
- There are various pinning problems with RBF and our general ancestor/descendant limits. These policies help mitigate many pinning attacks and make package RBF feasible (see #28984 which implements package RBF on top of this). I would focus the most here on Rule 3 pinning. [1][2]
- Switching to a cluster-based mempool (see #27677 and #28676) requires the removal of CPFP carve out, which applications depend on. V3 + package RBF + ephemeral anchors + 1-parent-1-child package relay provides an intermediate solution.
V3 policy is for "Priority Transactions." [3][4] It allows users to opt in to more restrictive topological limits for shared transactions, in exchange for the more robust fee-bumping abilities that offers. Even though we don't have cluster limits, we are able to treat these transactions as having as having a maximum cluster size of 2.
Immediate benefits:
- You can presign a transaction with 0 fees (not just 1sat/vB!) and add a fee-bump later.
- Rule 3 pinning is reduced by a significant amount, since the attacker can only attach a maximum of 1000vB to your shared transaction.
This also enables some other cool things (again see #27463 for overall roadmap):
- Ephemeral Anchors
- Package RBF for these 1-parent-1-child packages. That means e.g. a commitment tx + child can replace another commitment tx using the child's fees.
- We can transition to a "single anchor" universe without worrying about package limit pinning. So current users of CPFP carve out would have something else to use.
- We can switch to a cluster-based mempool [5] (#27677 #28676), which removes CPFP carve out [6].
[1]: Original mailing list post and discussion about RBF pinning problems https://gist.github.com/glozow/25d9662c52453bd08b4b4b1d3783b9ff, https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-January/019817.html
[2]: A FAQ is "we need this for cluster mempool, but is this still necessary afterwards?" There are some pinning issues that are fixed here and not fully fixed in cluster mempool, so we will still want this or something similar afterward.
[3]: Mailing list post for v3 https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-September/020937.html
[4]: Original PR #25038 also contains a lot of the discussion
[5]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393/7
[6]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393#the-cpfp-carveout-rule-can-no-longer-be-supported-12
ACKs for top commit:
sdaftuar:
ACK 29029df5c700e6940c712028303761d91ae15847
achow101:
ACK 29029df5c700e6940c712028303761d91ae15847
instagibbs:
ACK 29029df5c700e6940c712028303761d91ae15847 modulo that
Tree-SHA512: 9664b078890cfdca2a146439f8835c9d9ab483f43b30af8c7cd6962f09aa557fb1ce7689d5e130a2ec142235dbc8f21213881baa75241c5881660f9008d68450
Diffstat (limited to 'test')
-rwxr-xr-x | test/functional/feature_bip68_sequence.py | 6 | ||||
-rwxr-xr-x | test/functional/mempool_accept_v3.py | 418 | ||||
-rwxr-xr-x | test/functional/mempool_package_limits.py | 2 | ||||
-rwxr-xr-x | test/functional/mempool_sigoplimit.py | 3 | ||||
-rw-r--r-- | test/functional/test_framework/wallet.py | 20 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 |
6 files changed, 438 insertions, 12 deletions
diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py index 894afffc79..8768d4040d 100755 --- a/test/functional/feature_bip68_sequence.py +++ b/test/functional/feature_bip68_sequence.py @@ -408,10 +408,8 @@ class BIP68Test(BitcoinTestFramework): # Use self.nodes[1] to test that version 2 transactions are standard. def test_version2_relay(self): mini_wallet = MiniWallet(self.nodes[1]) - mini_wallet.rescan_utxos() - tx = mini_wallet.create_self_transfer()["tx"] - tx.nVersion = 2 - mini_wallet.sendrawtransaction(from_node=self.nodes[1], tx_hex=tx.serialize().hex()) + mini_wallet.send_self_transfer(from_node=self.nodes[1], version=2) + if __name__ == '__main__': BIP68Test().main() diff --git a/test/functional/mempool_accept_v3.py b/test/functional/mempool_accept_v3.py new file mode 100755 index 0000000000..ca599a9993 --- /dev/null +++ b/test/functional/mempool_accept_v3.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.messages import ( + MAX_BIP125_RBF_SEQUENCE, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_greater_than, + assert_greater_than_or_equal, + assert_raises_rpc_error, +) +from test_framework.wallet import ( + DEFAULT_FEE, + MiniWallet, +) + +def cleanup(extra_args=None): + def decorator(func): + def wrapper(self): + try: + if extra_args is not None: + self.restart_node(0, extra_args=extra_args) + func(self) + finally: + # Clear mempool again after test + self.generate(self.nodes[0], 1) + if extra_args is not None: + self.restart_node(0) + return wrapper + return decorator + +class MempoolAcceptV3(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [["-acceptnonstdtxn=1"]] + self.setup_clean_chain = True + + def check_mempool(self, txids): + """Assert exact contents of the node's mempool (by txid).""" + mempool_contents = self.nodes[0].getrawmempool() + assert_equal(len(txids), len(mempool_contents)) + assert all([txid in txids for txid in mempool_contents]) + + @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"]) + def test_v3_acceptance(self): + node = self.nodes[0] + self.log.info("Test a child of a v3 transaction cannot be more than 1000vB") + tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3) + self.check_mempool([tx_v3_parent_normal["txid"]]) + tx_v3_child_heavy = self.wallet.create_self_transfer( + utxo_to_spend=tx_v3_parent_normal["new_utxo"], + target_weight=4004, + version=3 + ) + assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), 1000) + expected_error_child_heavy = f"v3-rule-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big" + assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"]) + self.check_mempool([tx_v3_parent_normal["txid"]]) + # tx has no descendants + assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1) + + self.log.info("Test that, during replacements, only the new transaction counts for v3 descendant limit") + tx_v3_child_almost_heavy = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE, + utxo_to_spend=tx_v3_parent_normal["new_utxo"], + target_weight=3987, + version=3 + ) + assert_greater_than_or_equal(1000, tx_v3_child_almost_heavy["tx"].get_vsize()) + self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]]) + assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) + tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE * 2, + utxo_to_spend=tx_v3_parent_normal["new_utxo"], + target_weight=3500, + version=3 + ) + assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(), 1000) + self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]]) + assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) + + @cleanup(extra_args=["-acceptnonstdtxn=1"]) + def test_v3_replacement(self): + node = self.nodes[0] + self.log.info("Test v3 transactions may be replaced by v3 transactions") + utxo_v3_bip125 = self.wallet.get_utxo() + tx_v3_bip125 = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE, + utxo_to_spend=utxo_v3_bip125, + sequence=MAX_BIP125_RBF_SEQUENCE, + version=3 + ) + self.check_mempool([tx_v3_bip125["txid"]]) + + tx_v3_bip125_rbf = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE * 2, + utxo_to_spend=utxo_v3_bip125, + version=3 + ) + self.check_mempool([tx_v3_bip125_rbf["txid"]]) + + self.log.info("Test v3 transactions may be replaced by V2 transactions") + tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE * 3, + utxo_to_spend=utxo_v3_bip125, + version=2 + ) + self.check_mempool([tx_v3_bip125_rbf_v2["txid"]]) + + self.log.info("Test that replacements cannot cause violation of inherited v3") + utxo_v3_parent = self.wallet.get_utxo() + tx_v3_parent = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE, + utxo_to_spend=utxo_v3_parent, + version=3 + ) + tx_v3_child = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE, + utxo_to_spend=tx_v3_parent["new_utxo"], + version=3 + ) + self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) + + tx_v3_child_rbf_v2 = self.wallet.create_self_transfer( + fee_rate=DEFAULT_FEE * 2, + utxo_to_spend=tx_v3_parent["new_utxo"], + version=2 + ) + expected_error_v2_v3 = f"v3-rule-violation, non-v3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})" + assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"]) + self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) + + + @cleanup(extra_args=["-acceptnonstdtxn=1"]) + def test_v3_bip125(self): + node = self.nodes[0] + self.log.info("Test v3 transactions that don't signal BIP125 are replaceable") + assert_equal(node.getmempoolinfo()["fullrbf"], False) + utxo_v3_no_bip125 = self.wallet.get_utxo() + tx_v3_no_bip125 = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE, + utxo_to_spend=utxo_v3_no_bip125, + sequence=MAX_BIP125_RBF_SEQUENCE + 1, + version=3 + ) + + self.check_mempool([tx_v3_no_bip125["txid"]]) + assert not node.getmempoolentry(tx_v3_no_bip125["txid"])["bip125-replaceable"] + tx_v3_no_bip125_rbf = self.wallet.send_self_transfer( + from_node=node, + fee_rate=DEFAULT_FEE * 2, + utxo_to_spend=utxo_v3_no_bip125, + version=3 + ) + self.check_mempool([tx_v3_no_bip125_rbf["txid"]]) + + @cleanup(extra_args=["-datacarriersize=40000", "-acceptnonstdtxn=1"]) + def test_v3_reorg(self): + node = self.nodes[0] + self.log.info("Test that, during a reorg, v3 rules are not enforced") + tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2) + tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3) + tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3) + self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]]) + + block = self.generate(node, 1) + self.check_mempool([]) + tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2) + tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3) + tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_weight=5000, version=3) + assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], 1000) + self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) + node.invalidateblock(block[0]) + self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) + # This is needed because generate() will create the exact same block again. + node.reconsiderblock(block[0]) + + + @cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"]) + def test_nondefault_package_limits(self): + """ + Max standard tx size + v3 rules imply the ancestor/descendant rules (at their default + values), but those checks must not be skipped. Ensure both sets of checks are done by + changing the ancestor/descendant limit configurations. + """ + node = self.nodes[0] + self.log.info("Test that a decreased limitdescendantsize also applies to v3 child") + tx_v3_parent_large1 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3) + tx_v3_child_large1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large1["new_utxo"], version=3) + # Child is within v3 limits, but parent's descendant limit is exceeded + assert_greater_than(1000, tx_v3_child_large1["tx"].get_vsize()) + assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds descendant size limit for tx {tx_v3_parent_large1['txid']}", node.sendrawtransaction, tx_v3_child_large1["hex"]) + self.check_mempool([tx_v3_parent_large1["txid"]]) + assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1) + self.generate(node, 1) + + self.log.info("Test that a decreased limitancestorsize also applies to v3 parent") + self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"]) + tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3) + tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], version=3) + # Child is within v3 limits + assert_greater_than_or_equal(1000, tx_v3_child_large2["tx"].get_vsize()) + assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds ancestor size limit", node.sendrawtransaction, tx_v3_child_large2["hex"]) + self.check_mempool([tx_v3_parent_large2["txid"]]) + + @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"]) + def test_v3_ancestors_package(self): + self.log.info("Test that v3 ancestor limits are checked within the package") + node = self.nodes[0] + tx_v3_parent_normal = self.wallet.create_self_transfer( + fee_rate=0, + target_weight=4004, + version=3 + ) + tx_v3_parent_2_normal = self.wallet.create_self_transfer( + fee_rate=0, + target_weight=4004, + version=3 + ) + tx_v3_child_multiparent = self.wallet.create_self_transfer_multi( + utxos_to_spend=[tx_v3_parent_normal["new_utxo"], tx_v3_parent_2_normal["new_utxo"]], + fee_per_output=10000, + version=3 + ) + tx_v3_child_heavy = self.wallet.create_self_transfer_multi( + utxos_to_spend=[tx_v3_parent_normal["new_utxo"]], + target_weight=4004, + fee_per_output=10000, + version=3 + ) + + self.check_mempool([]) + result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]]) + assert_equal(result['package_msg'], f"v3-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors") + self.check_mempool([]) + + self.check_mempool([]) + result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]]) + # tx_v3_child_heavy is heavy based on weight, not sigops. + assert_equal(result['package_msg'], f"v3-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes") + self.check_mempool([]) + + tx_v3_parent = self.wallet.create_self_transfer(version=3) + tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3) + tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3) + result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]]) + assert all([txresult["package-error"] == f"v3-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors" for txresult in result]) + + @cleanup(extra_args=["-acceptnonstdtxn=1"]) + def test_v3_ancestors_package_and_mempool(self): + """ + A v3 transaction in a package cannot have 2 v3 parents. + Test that if we have a transaction graph A -> B -> C, where A, B, C are + all v3 transactions, that we cannot use submitpackage to get the + transactions all into the mempool. + + Verify, in particular, that if A is already in the mempool, then + submitpackage(B, C) will fail. + """ + node = self.nodes[0] + self.log.info("Test that v3 ancestor limits include transactions within the package and all in-mempool ancestors") + # This is our transaction "A": + tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3) + + # Verify that A is in the mempool + self.check_mempool([tx_in_mempool["txid"]]) + + # tx_0fee_parent is our transaction "B"; just create it. + tx_0fee_parent = self.wallet.create_self_transfer(utxo_to_spend=tx_in_mempool["new_utxo"], fee=0, fee_rate=0, version=3) + + # tx_child_violator is our transaction "C"; create it: + tx_child_violator = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"]], version=3) + + # submitpackage(B, C) should fail + result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]]) + assert_equal(result['package_msg'], f"v3-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors") + self.check_mempool([tx_in_mempool["txid"]]) + + @cleanup(extra_args=["-acceptnonstdtxn=1"]) + def test_mempool_sibling(self): + self.log.info("Test that v3 transaction cannot have mempool siblings") + node = self.nodes[0] + # Add a parent + child to mempool + tx_mempool_parent = self.wallet.send_self_transfer_multi( + from_node=node, + utxos_to_spend=[self.wallet.get_utxo()], + num_outputs=2, + version=3 + ) + tx_mempool_sibling = self.wallet.send_self_transfer( + from_node=node, + utxo_to_spend=tx_mempool_parent["new_utxos"][0], + version=3 + ) + self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]]) + + tx_has_mempool_sibling = self.wallet.create_self_transfer( + utxo_to_spend=tx_mempool_parent["new_utxos"][1], + version=3 + ) + expected_error_mempool_sibling = f"v3-rule-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit" + assert_raises_rpc_error(-26, expected_error_mempool_sibling, node.sendrawtransaction, tx_has_mempool_sibling["hex"]) + + tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_has_mempool_sibling["new_utxo"], version=3) + + # Also fails with another non-related transaction via testmempoolaccept + tx_unrelated = self.wallet.create_self_transfer(version=3) + result_test_unrelated = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_unrelated["hex"]]) + assert_equal(result_test_unrelated[0]["reject-reason"], "v3-rule-violation") + + result_test_1p1c = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]]) + assert_equal(result_test_1p1c[0]["reject-reason"], "v3-rule-violation") + + # Also fails with a child via submitpackage + result_submitpackage = node.submitpackage([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]]) + assert_equal(result_submitpackage["tx-results"][tx_has_mempool_sibling['wtxid']]['error'], expected_error_mempool_sibling) + + + @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"]) + def test_v3_package_inheritance(self): + self.log.info("Test that v3 inheritance is checked within package") + node = self.nodes[0] + tx_v3_parent = self.wallet.create_self_transfer( + fee_rate=0, + target_weight=4004, + version=3 + ) + tx_v2_child = self.wallet.create_self_transfer_multi( + utxos_to_spend=[tx_v3_parent["new_utxo"]], + fee_per_output=10000, + version=2 + ) + self.check_mempool([]) + result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]]) + assert_equal(result['package_msg'], f"v3-violation, non-v3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})") + self.check_mempool([]) + + @cleanup(extra_args=["-acceptnonstdtxn=1"]) + def test_v3_in_testmempoolaccept(self): + node = self.nodes[0] + + self.log.info("Test that v3 inheritance is accurately assessed in testmempoolaccept") + tx_v2 = self.wallet.create_self_transfer(version=2) + tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2) + tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3) + tx_v3 = self.wallet.create_self_transfer(version=3) + tx_v2_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=2) + tx_v3_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=3) + + # testmempoolaccept paths don't require child-with-parents topology. Ensure that topology + # assumptions aren't made in inheritance checks. + test_accept_v2_and_v3 = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"]]) + assert all([result["allowed"] for result in test_accept_v2_and_v3]) + + test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]]) + expected_error_v3_from_v2 = f"v3-violation, v3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-v3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})" + assert all([result["package-error"] == expected_error_v3_from_v2 for result in test_accept_v3_from_v2]) + + test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]]) + expected_error_v2_from_v3 = f"v3-violation, non-v3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from v3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})" + assert all([result["package-error"] == expected_error_v2_from_v3 for result in test_accept_v2_from_v3]) + + test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]]) + assert all([result["allowed"] for result in test_accept_pairs]) + + self.log.info("Test that descendant violations are caught in testmempoolaccept") + tx_v3_independent = self.wallet.create_self_transfer(version=3) + tx_v3_parent = self.wallet.create_self_transfer_multi(num_outputs=2, version=3) + tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3) + tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3) + test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]]) + expected_error_2children = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit" + assert all([result["package-error"] == expected_error_2children for result in test_accept_2children]) + + # Extra v3 transaction does not get incorrectly marked as extra descendant + test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]]) + assert all([result["allowed"] for result in test_accept_1child_with_exra]) + + # Extra v3 transaction does not make us ignore the extra descendant + test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]]) + expected_error_extra = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit" + assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_exra]) + # Same result if the parent is already in mempool + node.sendrawtransaction(tx_v3_parent["hex"]) + test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]]) + assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_in_mempool_parent]) + + def run_test(self): + self.log.info("Generate blocks to create UTXOs") + node = self.nodes[0] + self.wallet = MiniWallet(node) + self.generate(self.wallet, 110) + self.test_v3_acceptance() + self.test_v3_replacement() + self.test_v3_bip125() + self.test_v3_reorg() + self.test_nondefault_package_limits() + self.test_v3_ancestors_package() + self.test_v3_ancestors_package_and_mempool() + self.test_mempool_sibling() + self.test_v3_package_inheritance() + self.test_v3_in_testmempoolaccept() + + +if __name__ == "__main__": + MempoolAcceptV3().main() diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py index 81451bf2a5..2a64597511 100755 --- a/test/functional/mempool_package_limits.py +++ b/test/functional/mempool_package_limits.py @@ -29,7 +29,7 @@ def check_package_limits(func): testres_error_expected = node.testmempoolaccept(rawtxs=package_hex) assert_equal(len(testres_error_expected), len(package_hex)) for txres in testres_error_expected: - assert_equal(txres["package-error"], "package-mempool-limits") + assert "package-mempool-limits" in txres["package-error"] # Clear mempool and check that the package passes now self.generate(node, 1) diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py index 2e7850fb40..384423e5f5 100755 --- a/test/functional/mempool_sigoplimit.py +++ b/test/functional/mempool_sigoplimit.py @@ -165,7 +165,8 @@ class BytesPerSigOpTest(BitcoinTestFramework): # But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked # here, it would get further in validation and give too-long-mempool-chain error instead. packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()]) - assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"]) + expected_package_error = f"package-mempool-limits, package size {2*20*5000} exceeds ancestor size limit [limit: 101000]" + assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * 2) # When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()]) diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 53c8e1b0cc..470ed08ed4 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -286,11 +286,12 @@ class MiniWallet: utxos_to_spend: Optional[list[dict]] = None, num_outputs=1, amount_per_output=0, + version=2, locktime=0, sequence=0, fee_per_output=1000, target_weight=0, - confirmed_only=False + confirmed_only=False, ): """ Create and return a transaction that spends the given UTXOs and creates a @@ -313,6 +314,7 @@ class MiniWallet: tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)] tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)] + tx.nVersion = version tx.nLockTime = locktime self.sign_tx(tx) @@ -337,14 +339,15 @@ class MiniWallet: "tx": tx, } - def create_self_transfer(self, *, + def create_self_transfer( + self, + *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, - locktime=0, - sequence=0, target_weight=0, - confirmed_only=False + confirmed_only=False, + **kwargs, ): """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed.""" utxo_to_spend = utxo_to_spend or self.get_utxo(confirmed_only=confirmed_only) @@ -360,7 +363,12 @@ class MiniWallet: send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000)) # create tx - tx = self.create_self_transfer_multi(utxos_to_spend=[utxo_to_spend], locktime=locktime, sequence=sequence, amount_per_output=int(COIN * send_value), target_weight=target_weight) + tx = self.create_self_transfer_multi( + utxos_to_spend=[utxo_to_spend], + amount_per_output=int(COIN * send_value), + target_weight=target_weight, + **kwargs, + ) if not target_weight: assert_equal(tx["tx"].get_vsize(), vsize) tx["new_utxo"] = tx.pop("new_utxos")[0] diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d037ccf6dd..e438a60edc 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -266,6 +266,7 @@ BASE_SCRIPTS = [ 'p2p_v2_encrypted.py', 'p2p_v2_earlykeyresponse.py', 'example_test.py', + 'mempool_accept_v3.py', 'wallet_txn_doublespend.py --legacy-wallet', 'wallet_multisig_descriptor_psbt.py --descriptors', 'wallet_txn_doublespend.py --descriptors', |