p2p_1p1c_network.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 that 1p1c package submission allows a 1p1c package to propagate in a "network" of nodes. Send
   7  various packages from different nodes on a network in which some nodes have already received some of
   8  the transactions (and submitted them to mempool, kept them as orphans or rejected them as
   9  too-low-feerate transactions). The packages should be received and accepted by all nodes.
  10  """
  11  
  12  from decimal import Decimal
  13  from math import ceil
  14  
  15  from test_framework.mempool_util import (
  16      DEFAULT_MIN_RELAY_TX_FEE,
  17      fill_mempool,
  18  )
  19  from test_framework.messages import (
  20      COIN,
  21      msg_tx,
  22  )
  23  from test_framework.p2p import (
  24      P2PInterface,
  25  )
  26  from test_framework.test_framework import LimenkaTestFramework
  27  from test_framework.util import (
  28      assert_equal,
  29      assert_greater_than,
  30  )
  31  from test_framework.wallet import (
  32      MiniWallet,
  33      MiniWalletMode,
  34  )
  35  
  36  class PackageRelayTest(LimenkaTestFramework):
  37      def set_test_params(self):
  38          self.setup_clean_chain = True
  39          self.num_nodes = 4
  40          # hugely speeds up the test, as it involves multiple hops of tx relay.
  41          self.noban_tx_relay = True
  42          self.extra_args = [[
  43              "-datacarriersize=100000",
  44              "-maxmempool=5",
  45          ]] * self.num_nodes
  46          self.supports_cli = False
  47  
  48      def raise_network_minfee(self):
  49          fill_mempool(self, self.nodes[0])
  50  
  51          self.log.debug("Check that all nodes' mempool minimum feerates are above min relay feerate")
  52          for node in self.nodes:
  53              assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)
  54              assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)
  55  
  56          # Store mempoolminfee for dynamic feerate calculation
  57          self.mempoolminfee = self.nodes[0].getmempoolinfo()['mempoolminfee']
  58          self.log.info(f"mempoolminfee after fill_mempool: {self.mempoolminfee} BTC/kvB ({self.mempoolminfee * 100000:.4f} sat/vB)")
  59  
  60      def create_basic_1p1c(self, wallet):
  61          low_fee_parent = wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, confirmed_only=True)
  62          high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*Decimal(DEFAULT_MIN_RELAY_TX_FEE)/ COIN)
  63          package_hex_basic = [low_fee_parent["hex"], high_fee_child["hex"]]
  64          return package_hex_basic, low_fee_parent["tx"], high_fee_child["tx"]
  65  
  66      def create_package_2outs(self, wallet):
  67          # First create a tester tx to see the vsize, and then adjust the fees
  68          utxo_for_2outs = wallet.get_utxo(confirmed_only=True)
  69  
  70          low_fee_parent_2outs_tester = wallet.create_self_transfer_multi(
  71              utxos_to_spend=[utxo_for_2outs],
  72              num_outputs=2,
  73          )
  74  
  75          # Target 1sat/vB so the number of satoshis is equal to the vsize.
  76          # Round up. The goal is to be between min relay feerate and mempool min feerate.
  77          fee_2outs = ceil(low_fee_parent_2outs_tester["tx"].get_vsize() / 2)
  78  
  79          low_fee_parent_2outs = wallet.create_self_transfer_multi(
  80              utxos_to_spend=[utxo_for_2outs],
  81              num_outputs=2,
  82              fee_per_output=fee_2outs,
  83          )
  84  
  85          # Now create the child
  86          high_fee_child_2outs = wallet.create_self_transfer_multi(
  87              utxos_to_spend=low_fee_parent_2outs["new_utxos"][::-1],
  88              fee_per_output=fee_2outs*100,
  89          )
  90          return [low_fee_parent_2outs["hex"], high_fee_child_2outs["hex"]], low_fee_parent_2outs["tx"], high_fee_child_2outs["tx"]
  91  
  92      def create_package_2p1c(self, wallet):
  93          # Use dynamic feerates based on actual mempoolminfee to ensure parents are above eviction threshold
  94          # Set parent1 at 2x threshold, parent2 at 4x threshold (same relative ratio as before)
  95          parent1_feerate = self.mempoolminfee * 2
  96          parent2_feerate = self.mempoolminfee * 4
  97  
  98          self.log.info(f"Creating 2p1c package with parent1={parent1_feerate} BTC/kvB, parent2={parent2_feerate} BTC/kvB")
  99  
 100          parent1 = wallet.create_self_transfer(fee_rate=parent1_feerate, confirmed_only=True)
 101          parent2 = wallet.create_self_transfer(fee_rate=parent2_feerate, confirmed_only=True)
 102          child = wallet.create_self_transfer_multi(
 103              utxos_to_spend=[parent1["new_utxo"], parent2["new_utxo"]],
 104              fee_per_output=999*parent1["tx"].get_vsize(),
 105          )
 106          return [parent1["hex"], parent2["hex"], child["hex"]], parent1["tx"], parent2["tx"], child["tx"]
 107  
 108      def create_packages(self):
 109          # 1: Basic 1-parent-1-child package, parent 1sat/vB, child 999sat/vB
 110          package_hex_1, parent_1, child_1 = self.create_basic_1p1c(self.wallet)
 111  
 112          # 2: same as 1, parent's txid is the same as its wtxid.
 113          package_hex_2, parent_2, child_2 = self.create_basic_1p1c(self.wallet_nonsegwit)
 114  
 115          # 3: 2-parent-1-child package. Both parents are above mempool min feerate. No package submission happens.
 116          # We require packages to be child-with-unconfirmed-parents and only allow 1-parent-1-child packages.
 117          package_hex_3, parent_31, _parent_32, child_3 = self.create_package_2p1c(self.wallet)
 118  
 119          # 4: parent + child package where the child spends 2 different outputs from the parent.
 120          package_hex_4, parent_4, child_4 = self.create_package_2outs(self.wallet)
 121  
 122          # Assemble return results
 123          packages_to_submit = [package_hex_1, package_hex_2, package_hex_3, package_hex_4]
 124          # node0: sender
 125          # node1: pre-received the children (orphan)
 126          # node3: pre-received the parents (too low fee)
 127          # All nodes receive parent_31 ahead of time.
 128          txns_to_send = [
 129              [],
 130              [child_1, child_2, parent_31, child_3, child_4],
 131              [parent_31],
 132              [parent_1, parent_2, parent_31, parent_4]
 133          ]
 134  
 135          return packages_to_submit, txns_to_send
 136  
 137      def run_test(self):
 138          self.wallet = MiniWallet(self.nodes[1])
 139          self.wallet_nonsegwit = MiniWallet(self.nodes[2], mode=MiniWalletMode.RAW_P2PK)
 140          self.generate(self.wallet_nonsegwit, 10)
 141          self.generate(self.wallet, 120)
 142  
 143          self.log.info("Fill mempools with large transactions to raise mempool minimum feerates")
 144          self.raise_network_minfee()
 145  
 146          # Create the transactions.
 147          self.wallet.rescan_utxos(include_mempool=True)
 148          packages_to_submit, transactions_to_presend = self.create_packages()
 149  
 150          self.peers = [self.nodes[i].add_p2p_connection(P2PInterface()) for i in range(self.num_nodes)]
 151  
 152          self.log.info("Pre-send some transactions to nodes")
 153          for (i, peer) in enumerate(self.peers):
 154              for tx in transactions_to_presend[i]:
 155                  peer.send_and_ping(msg_tx(tx))
 156  
 157          # Disconnect python peers to clear outstanding orphan requests with them, avoiding timeouts.
 158          # We are only interested in the syncing behavior between real nodes.
 159          for i in range(self.num_nodes):
 160              self.nodes[i].disconnect_p2ps()
 161  
 162          self.log.info("Submit full packages to node0")
 163          for package_hex in packages_to_submit:
 164              submitpackage_result = self.nodes[0].submitpackage(package_hex)
 165              assert_equal(submitpackage_result["package_msg"], "success")
 166  
 167          self.log.info("Wait for mempools to sync")
 168          self.sync_mempools()
 169  
 170  
 171  if __name__ == '__main__':
 172      PackageRelayTest(__file__).main()
 173