p2p_opportunistic_1p1c.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2024-present The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """
   6  Test opportunistic 1p1c package submission logic.
   7  """
   8  
   9  from decimal import Decimal
  10  import time
  11  from test_framework.mempool_util import (
  12      DEFAULT_MIN_RELAY_TX_FEE,
  13      fill_mempool,
  14  )
  15  from test_framework.messages import (
  16      CInv,
  17      COIN,
  18      CTxInWitness,
  19      MAX_BIP125_RBF_SEQUENCE,
  20      MSG_WTX,
  21      msg_inv,
  22      msg_tx,
  23      tx_from_hex,
  24  )
  25  from test_framework.p2p import (
  26      P2PInterface,
  27  )
  28  from test_framework.test_framework import LimenkaTestFramework
  29  from test_framework.util import (
  30      assert_equal,
  31      assert_greater_than,
  32  )
  33  from test_framework.wallet import (
  34      MiniWallet,
  35      MiniWalletMode,
  36  )
  37  
  38  # 1sat/vB feerate denominated in BTC/KvB
  39  FEERATE_1SAT_VB = Decimal("0.00001000")
  40  # Number of seconds to wait to ensure no getdata is received
  41  GETDATA_WAIT = 60
  42  
  43  def cleanup(func):
  44      def wrapper(self, *args, **kwargs):
  45          try:
  46              func(self, *args, **kwargs)
  47          finally:
  48              self.nodes[0].disconnect_p2ps()
  49              # Do not clear the node's mempool, as each test requires mempool min feerate > min
  50              # relay feerate. However, do check that this is the case.
  51              assert self.nodes[0].getmempoolinfo()["mempoolminfee"] > self.nodes[0].getnetworkinfo()["relayfee"]
  52              # Ensure we do not try to spend the same UTXOs in subsequent tests, as they will look like RBF attempts.
  53              self.wallet.rescan_utxos(include_mempool=True)
  54  
  55              # Resets if mocktime was used
  56              self.nodes[0].setmocktime(0)
  57      return wrapper
  58  
  59  class PackageRelayTest(LimenkaTestFramework):
  60      def set_test_params(self):
  61          self.setup_clean_chain = True
  62          self.num_nodes = 1
  63          self.extra_args = [[
  64              "-datacarriersize=100000",
  65              "-maxmempool=5",
  66          ]]
  67          self.supports_cli = False
  68  
  69      def create_tx_below_mempoolminfee(self, wallet):
  70          """Create a 1-input 0.1sat/vB transaction using a confirmed UTXO. Decrement and use
  71          self.sequence so that subsequent calls to this function result in unique transactions."""
  72  
  73          self.sequence -= 1
  74          assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)
  75  
  76          return wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, sequence=self.sequence, confirmed_only=True)
  77  
  78      @cleanup
  79      def test_basic_child_then_parent(self):
  80          node = self.nodes[0]
  81          self.log.info("Check that opportunistic 1p1c logic works when child is received before parent")
  82  
  83          low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
  84          high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
  85  
  86          peer_sender = node.add_p2p_connection(P2PInterface())
  87  
  88          # 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.
  89          high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
  90          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
  91          peer_sender.wait_for_getdata([high_child_wtxid_int])
  92          peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
  93  
  94          # 2. Node requests the missing parent by txid.
  95          parent_txid_int = int(low_fee_parent["txid"], 16)
  96          peer_sender.wait_for_getdata([parent_txid_int])
  97  
  98          # 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.
  99          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 100  
 101          # 4. Both transactions should now be in mempool.
 102          node_mempool = node.getrawmempool()
 103          assert low_fee_parent["txid"] in node_mempool
 104          assert high_fee_child["txid"] in node_mempool
 105  
 106          node.disconnect_p2ps()
 107  
 108      @cleanup
 109      def test_basic_parent_then_child(self, wallet):
 110          node = self.nodes[0]
 111          low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
 112          high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
 113  
 114          peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
 115          peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
 116  
 117          # 1. Parent is relayed first. It is too low feerate.
 118          parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
 119          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
 120          peer_sender.wait_for_getdata([parent_wtxid_int])
 121          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 122          assert low_fee_parent["txid"] not in node.getrawmempool()
 123  
 124          # Send again from peer_ignored, check that it is ignored
 125          peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
 126          assert "getdata" not in peer_ignored.last_message
 127  
 128          # 2. Child is relayed next. It is missing an input.
 129          high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
 130          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
 131          peer_sender.wait_for_getdata([high_child_wtxid_int])
 132          peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
 133  
 134          # 3. Node requests the missing parent by txid.
 135          # It should do so even if it has previously rejected that parent for being too low feerate.
 136          parent_txid_int = int(low_fee_parent["txid"], 16)
 137          peer_sender.wait_for_getdata([parent_txid_int])
 138  
 139          # 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted.
 140          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 141  
 142          # 5. Both transactions should now be in mempool.
 143          node_mempool = node.getrawmempool()
 144          assert low_fee_parent["txid"] in node_mempool
 145          assert high_fee_child["txid"] in node_mempool
 146  
 147      @cleanup
 148      def test_low_and_high_child(self, wallet):
 149          node = self.nodes[0]
 150          low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
 151          # This feerate is above mempoolminfee, but not enough to also bump the low feerate parent.
 152          feerate_just_above = node.getmempoolinfo()["mempoolminfee"]
 153          med_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=feerate_just_above)
 154          high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
 155  
 156          peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
 157          peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
 158  
 159          self.log.info("Check that tx caches low fee parent + low fee child package rejections")
 160  
 161          # 1. Send parent, rejected for being low feerate.
 162          parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
 163          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
 164          peer_sender.wait_for_getdata([parent_wtxid_int])
 165          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 166          assert low_fee_parent["txid"] not in node.getrawmempool()
 167  
 168          # Send again from peer_ignored, check that it is ignored
 169          peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
 170          assert "getdata" not in peer_ignored.last_message
 171  
 172          # 2. Send an (orphan) child that has a higher feerate, but not enough to bump the parent.
 173          med_child_wtxid_int = int(med_fee_child["tx"].getwtxid(), 16)
 174          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
 175          peer_sender.wait_for_getdata([med_child_wtxid_int])
 176          peer_sender.send_and_ping(msg_tx(med_fee_child["tx"]))
 177  
 178          # 3. Node requests the orphan's missing parent.
 179          parent_txid_int = int(low_fee_parent["txid"], 16)
 180          peer_sender.wait_for_getdata([parent_txid_int])
 181  
 182          # 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate.
 183          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 184  
 185          assert low_fee_parent["txid"] not in node.getrawmempool()
 186          assert med_fee_child["txid"] not in node.getrawmempool()
 187  
 188          # If peer_ignored announces the low feerate child, it should be ignored
 189          peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
 190          assert "getdata" not in peer_ignored.last_message
 191          # If either peer sends the parent again, package evaluation should not be attempted
 192          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 193          peer_ignored.send_and_ping(msg_tx(low_fee_parent["tx"]))
 194  
 195          assert low_fee_parent["txid"] not in node.getrawmempool()
 196          assert med_fee_child["txid"] not in node.getrawmempool()
 197  
 198          # 5. Send the high feerate (orphan) child
 199          high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
 200          peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
 201          peer_sender.wait_for_getdata([high_child_wtxid_int])
 202          peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
 203  
 204          # 6. Node requests the orphan's parent, even though it has already been rejected, both by
 205          # itself and with a child. This is necessary, otherwise high_fee_child can be censored.
 206          parent_txid_int = int(low_fee_parent["txid"], 16)
 207          peer_sender.wait_for_getdata([parent_txid_int])
 208  
 209          # 7. The low feerate parent + high feerate child are submitted as a package.
 210          peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 211  
 212          # 8. Both transactions should now be in mempool
 213          node_mempool = node.getrawmempool()
 214          assert low_fee_parent["txid"] in node_mempool
 215          assert high_fee_child["txid"] in node_mempool
 216          assert med_fee_child["txid"] not in node_mempool
 217  
 218      @cleanup
 219      def test_orphan_consensus_failure(self):
 220          self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")
 221          node = self.nodes[0]
 222          low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
 223          coin = low_fee_parent["new_utxo"]
 224          address = node.get_deterministic_priv_key().address
 225          # Create raw transaction spending the parent, but with no signature (a consensus error).
 226          hex_orphan_no_sig = node.createrawtransaction([{"txid": coin["txid"], "vout": coin["vout"]}], {address : coin["value"] - Decimal("0.0001")})
 227          tx_orphan_bad_wit = tx_from_hex(hex_orphan_no_sig)
 228          tx_orphan_bad_wit.wit.vtxinwit.append(CTxInWitness())
 229          tx_orphan_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
 230  
 231          bad_orphan_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0)
 232          parent_sender = node.add_p2p_connection(P2PInterface())
 233  
 234          # 1. Child is received first. It is missing an input.
 235          child_wtxid_int = int(tx_orphan_bad_wit.getwtxid(), 16)
 236          bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
 237          bad_orphan_sender.wait_for_getdata([child_wtxid_int])
 238          bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit))
 239  
 240          # 2. Node requests the missing parent by txid.
 241          parent_txid_int = int(low_fee_parent["txid"], 16)
 242          bad_orphan_sender.wait_for_getdata([parent_txid_int])
 243  
 244          # 3. A different peer relays the parent. Package is not evaluated because the transactions
 245          # were not sent from the same peer.
 246          parent_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 247  
 248          # 4. Transactions should not be in mempool.
 249          node_mempool = node.getrawmempool()
 250          assert low_fee_parent["txid"] not in node_mempool
 251          assert tx_orphan_bad_wit.rehash() not in node_mempool
 252  
 253          # 5. Have the other peer send the tx too, so that tx_orphan_bad_wit package is attempted.
 254          bad_orphan_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 255  
 256          # The bad orphan sender should not be disconnected.
 257          bad_orphan_sender.sync_with_ping()
 258  
 259          # The peer that didn't provide the orphan should not be disconnected.
 260          parent_sender.sync_with_ping()
 261  
 262      @cleanup
 263      def test_parent_consensus_failure(self):
 264          self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer")
 265          node = self.nodes[0]
 266          low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
 267          high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
 268  
 269          # Create invalid version of parent with a bad signature.
 270          tx_parent_bad_wit = tx_from_hex(low_fee_parent["hex"])
 271          tx_parent_bad_wit.wit.vtxinwit.append(CTxInWitness())
 272          tx_parent_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
 273  
 274          package_sender = node.add_p2p_connection(P2PInterface())
 275          fake_parent_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0)
 276  
 277          # 1. Child is received first. It is missing an input.
 278          child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
 279          package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
 280          package_sender.wait_for_getdata([child_wtxid_int])
 281          package_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
 282  
 283          # 2. Node requests the missing parent by txid.
 284          parent_txid_int = int(tx_parent_bad_wit.rehash(), 16)
 285          package_sender.wait_for_getdata([parent_txid_int])
 286  
 287          # 3. A different node relays the parent. The parent is first evaluated by itself and
 288          # rejected for being too low feerate. It is not evaluated as a package because the child was
 289          # sent from a different peer, so we don't find out that the child is consensus-invalid.
 290          fake_parent_sender.send_and_ping(msg_tx(tx_parent_bad_wit))
 291  
 292          # 4. Transactions should not be in mempool.
 293          node_mempool = node.getrawmempool()
 294          assert tx_parent_bad_wit.rehash() not in node_mempool
 295          assert high_fee_child["txid"] not in node_mempool
 296  
 297          self.log.info("Check that fake parent does not cause orphan to be deleted and real package can still be submitted")
 298          # 5. Child-sending should not have been punished and the orphan should remain in orphanage.
 299          # It can send the "real" parent transaction, and the package is accepted.
 300          parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
 301          package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
 302          package_sender.wait_for_getdata([parent_wtxid_int])
 303          package_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
 304  
 305          node_mempool = node.getrawmempool()
 306          assert low_fee_parent["txid"] in node_mempool
 307          assert high_fee_child["txid"] in node_mempool
 308  
 309      @cleanup
 310      def test_multiple_parents(self):
 311          self.log.info("Check that node does not request more than 1 previously-rejected low feerate parent")
 312  
 313          node = self.nodes[0]
 314          node.setmocktime(int(time.time()))
 315  
 316          # 2-parent-1-child package where both parents are below mempool min feerate
 317          parent_low_1 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
 318          parent_low_2 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
 319          child_bumping = self.wallet_nonsegwit.create_self_transfer_multi(
 320              utxos_to_spend=[parent_low_1["new_utxo"], parent_low_2["new_utxo"]],
 321              fee_per_output=999*parent_low_1["tx"].get_vsize(),
 322          )
 323  
 324          peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
 325  
 326          # 1. Send both parents. Each should be rejected for being too low feerate.
 327          # Send unsolicited so that we can later check that no "getdata" was ever received.
 328          peer_sender.send_and_ping(msg_tx(parent_low_1["tx"]))
 329          peer_sender.send_and_ping(msg_tx(parent_low_2["tx"]))
 330  
 331          # parent_low_1 and parent_low_2 are rejected for being low feerate.
 332          assert parent_low_1["txid"] not in node.getrawmempool()
 333          assert parent_low_2["txid"] not in node.getrawmempool()
 334  
 335          # 2. Send child.
 336          peer_sender.send_and_ping(msg_tx(child_bumping["tx"]))
 337  
 338          # 3. Node should not request any parents, as it should recognize that it will not accept
 339          # multi-parent-1-child packages.
 340          node.bumpmocktime(GETDATA_WAIT)
 341          peer_sender.sync_with_ping()
 342          assert "getdata" not in peer_sender.last_message
 343  
 344      @cleanup
 345      def test_other_parent_in_mempool(self):
 346          self.log.info("Check opportunistic 1p1c fails if child already has another parent in mempool")
 347          node = self.nodes[0]
 348  
 349          # This parent needs CPFP
 350          parent_low = self.create_tx_below_mempoolminfee(self.wallet)
 351          # This parent does not need CPFP and can be submitted alone ahead of time
 352          parent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
 353          child = self.wallet.create_self_transfer_multi(
 354              utxos_to_spend=[parent_high["new_utxo"], parent_low["new_utxo"]],
 355              fee_per_output=999*parent_low["tx"].get_vsize(),
 356          )
 357  
 358          peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
 359  
 360          # 1. Send first parent which will be accepted.
 361          peer_sender.send_and_ping(msg_tx(parent_high["tx"]))
 362          assert parent_high["txid"] in node.getrawmempool()
 363  
 364          # 2. Send child.
 365          peer_sender.send_and_ping(msg_tx(child["tx"]))
 366  
 367          # 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents
 368          parent_low_txid_int = int(parent_low["txid"], 16)
 369          peer_sender.wait_for_getdata([parent_low_txid_int])
 370          peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
 371  
 372          node_mempool = node.getrawmempool()
 373          assert parent_high["txid"] in node_mempool
 374          assert parent_low["txid"] not in node_mempool
 375          assert child["txid"] not in node_mempool
 376  
 377          # Same error if submitted through submitpackage without parent_high
 378          package_hex_missing_parent = [parent_low["hex"], child["hex"]]
 379          result_missing_parent = node.submitpackage(package_hex_missing_parent)
 380          assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents")
 381  
 382      def run_test(self):
 383          node = self.nodes[0]
 384          # To avoid creating transactions with the same txid (can happen if we set the same feerate
 385          # and reuse the same input as a previous transaction that wasn't successfully submitted),
 386          # we give each subtest a different nSequence for its transactions.
 387          self.sequence = MAX_BIP125_RBF_SEQUENCE
 388  
 389          self.wallet = MiniWallet(node)
 390          self.wallet_nonsegwit = MiniWallet(node, mode=MiniWalletMode.RAW_P2PK)
 391          self.generate(self.wallet_nonsegwit, 10)
 392          self.generate(self.wallet, 20)
 393  
 394          fill_mempool(self, node)
 395  
 396          self.log.info("Check opportunistic 1p1c logic when parent (txid != wtxid) is received before child")
 397          self.test_basic_parent_then_child(self.wallet)
 398  
 399          self.log.info("Check opportunistic 1p1c logic when parent (txid == wtxid) is received before child")
 400          self.test_basic_parent_then_child(self.wallet_nonsegwit)
 401  
 402          self.log.info("Check opportunistic 1p1c logic when child is received before parent")
 403          self.test_basic_child_then_parent()
 404  
 405          self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid != wtxid)")
 406          self.test_low_and_high_child(self.wallet)
 407  
 408          self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid == wtxid)")
 409          self.test_low_and_high_child(self.wallet_nonsegwit)
 410  
 411          self.test_orphan_consensus_failure()
 412          self.test_parent_consensus_failure()
 413          self.test_multiple_parents()
 414          self.test_other_parent_in_mempool()
 415  
 416  
 417  if __name__ == '__main__':
 418      PackageRelayTest(__file__).main()
 419