mempool_package_limits.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2021-2022 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  """Test logic for limiting mempool and package ancestors/descendants."""
   6  from test_framework.blocktools import COINBASE_MATURITY
   7  from test_framework.test_framework import LimenkaTestFramework
   8  from test_framework.util import (
   9      assert_equal,
  10  )
  11  from test_framework.wallet import MiniWallet
  12  
  13  # Decorator to
  14  # 1) check that mempool is empty at the start of a subtest
  15  # 2) run the subtest, which may submit some transaction(s) to the mempool and
  16  #    create a list of hex transactions
  17  # 3) testmempoolaccept the package hex and check that it fails with the error
  18  #    "package-mempool-limits" for each tx
  19  # 4) after mining a block, clearing the pre-submitted transactions from mempool,
  20  #    check that submitting the created package succeeds
  21  def check_package_limits(func):
  22      def func_wrapper(self, *args, **kwargs):
  23          node = self.nodes[0]
  24          assert_equal(0, node.getmempoolinfo()["size"])
  25          package_hex = func(self, *args, **kwargs)
  26          testres_error_expected = node.testmempoolaccept(rawtxs=package_hex)
  27          assert_equal(len(testres_error_expected), len(package_hex))
  28          for txres in testres_error_expected:
  29              assert "package-mempool-limits" in txres["package-error"]
  30  
  31          # Clear mempool and check that the package passes now
  32          self.generate(node, 1)
  33          assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)])
  34  
  35      return func_wrapper
  36  
  37  
  38  class MempoolPackageLimitsTest(LimenkaTestFramework):
  39      def set_test_params(self):
  40          self.num_nodes = 1
  41          self.setup_clean_chain = True
  42  
  43      def run_test(self):
  44          self.wallet = MiniWallet(self.nodes[0])
  45          # Add enough mature utxos to the wallet so that all txs spend confirmed coins.
  46          self.generate(self.wallet, COINBASE_MATURITY + 35)
  47  
  48          self.test_chain_limits()
  49          self.test_desc_count_limits()
  50          self.test_desc_count_limits_2()
  51          self.test_anc_count_limits()
  52          self.test_anc_count_limits_2()
  53          self.test_anc_count_limits_bushy()
  54  
  55          # The node will accept (nonstandard) extra large OP_RETURN outputs
  56          self.restart_node(0, extra_args=["-datacarriersize=100000"])
  57          self.test_anc_size_limits()
  58          self.test_desc_size_limits()
  59  
  60      @check_package_limits
  61      def test_chain_limits_helper(self, mempool_count, package_count):
  62          node = self.nodes[0]
  63          chain_hex = []
  64  
  65          chaintip_utxo = self.wallet.send_self_transfer_chain(from_node=node, chain_length=mempool_count)[-1]["new_utxo"]
  66          # in-package transactions
  67          for _ in range(package_count):
  68              tx = self.wallet.create_self_transfer(utxo_to_spend=chaintip_utxo)
  69              chaintip_utxo = tx["new_utxo"]
  70              chain_hex.append(tx["hex"])
  71          return chain_hex
  72  
  73      def test_chain_limits(self):
  74          """Create chains from mempool and package transactions that are longer than 25,
  75          but only if both in-mempool and in-package transactions are considered together.
  76          This checks that both mempool and in-package transactions are taken into account when
  77          calculating ancestors/descendant limits.
  78          """
  79          self.log.info("Check that in-package ancestors count for mempool ancestor limits")
  80  
  81          # 24 transactions in the mempool and 2 in the package. The parent in the package has
  82          # 24 in-mempool ancestors and 1 in-package descendant. The child has 0 direct parents
  83          # in the mempool, but 25 in-mempool and in-package ancestors in total.
  84          self.test_chain_limits_helper(24, 2)
  85          # 2 transactions in the mempool and 24 in the package.
  86          self.test_chain_limits_helper(2, 24)
  87          # 13 transactions in the mempool and 13 in the package.
  88          self.test_chain_limits_helper(13, 13)
  89  
  90      @check_package_limits
  91      def test_desc_count_limits(self):
  92          """Create an 'A' shaped package with 24 transactions in the mempool and 2 in the package:
  93                      M1
  94                     ^  ^
  95                   M2a  M2b
  96                  .       .
  97                 .         .
  98                .           .
  99               M12a          ^
 100              ^              M13b
 101             ^                 ^
 102            Pa                  Pb
 103          The top ancestor in the package exceeds descendant limits but only if the in-mempool and in-package
 104          descendants are all considered together (24 including in-mempool descendants and 26 including both
 105          package transactions).
 106          """
 107          node = self.nodes[0]
 108          self.log.info("Check that in-mempool and in-package descendants are calculated properly in packages")
 109          # Top parent in mempool, M1
 110          m1_utxos = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2)['new_utxos']
 111  
 112          package_hex = []
 113          # Chain A (M2a... M12a)
 114          chain_a_tip_utxo = self.wallet.send_self_transfer_chain(from_node=node, chain_length=11, utxo_to_spend=m1_utxos[0])[-1]["new_utxo"]
 115          # Pa
 116          pa_hex = self.wallet.create_self_transfer(utxo_to_spend=chain_a_tip_utxo)["hex"]
 117          package_hex.append(pa_hex)
 118  
 119          # Chain B (M2b... M13b)
 120          chain_b_tip_utxo = self.wallet.send_self_transfer_chain(from_node=node, chain_length=12, utxo_to_spend=m1_utxos[1])[-1]["new_utxo"]
 121          # Pb
 122          pb_hex = self.wallet.create_self_transfer(utxo_to_spend=chain_b_tip_utxo)["hex"]
 123          package_hex.append(pb_hex)
 124  
 125          assert_equal(24, node.getmempoolinfo()["size"])
 126          assert_equal(2, len(package_hex))
 127          return package_hex
 128  
 129      @check_package_limits
 130      def test_desc_count_limits_2(self):
 131          """Create a Package with 24 transaction in mempool and 2 transaction in package:
 132                        M1
 133                       ^  ^
 134                     M2    ^
 135                     .      ^
 136                    .        ^
 137                   .          ^
 138                  M24          ^
 139                                ^
 140                                P1
 141                                ^
 142                                P2
 143          P1 has M1 as a mempool ancestor, P2 has no in-mempool ancestors, but when
 144          combined P2 has M1 as an ancestor and M1 exceeds descendant_limits(23 in-mempool
 145          descendants + 2 in-package descendants, a total of 26 including itself).
 146          """
 147  
 148          node = self.nodes[0]
 149          package_hex = []
 150          # M1
 151          m1_utxos = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2)['new_utxos']
 152  
 153          # Chain M2...M24
 154          self.wallet.send_self_transfer_chain(from_node=node, chain_length=23, utxo_to_spend=m1_utxos[0])[-1]["new_utxo"]
 155  
 156          # P1
 157          p1_tx = self.wallet.create_self_transfer(utxo_to_spend=m1_utxos[1])
 158          package_hex.append(p1_tx["hex"])
 159  
 160          # P2
 161          p2_tx = self.wallet.create_self_transfer(utxo_to_spend=p1_tx["new_utxo"])
 162          package_hex.append(p2_tx["hex"])
 163  
 164          assert_equal(24, node.getmempoolinfo()["size"])
 165          assert_equal(2, len(package_hex))
 166          return package_hex
 167  
 168      @check_package_limits
 169      def test_anc_count_limits(self):
 170          """Create a 'V' shaped chain with 24 transactions in the mempool and 3 in the package:
 171          M1a                    M1b
 172           ^                     ^
 173            M2a                M2b
 174             .                 .
 175              .               .
 176               .             .
 177               M12a        M12b
 178                 ^         ^
 179                  Pa     Pb
 180                   ^    ^
 181                     Pc
 182          The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool
 183          and in-package ancestors are all considered together.
 184          """
 185          node = self.nodes[0]
 186          package_hex = []
 187          pc_parent_utxos = []
 188  
 189          self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages")
 190  
 191          # Two chains of 13 transactions each
 192          for _ in range(2):
 193              chain_tip_utxo = self.wallet.send_self_transfer_chain(from_node=node, chain_length=12)[-1]["new_utxo"]
 194              # Save the 13th transaction for the package
 195              tx = self.wallet.create_self_transfer(utxo_to_spend=chain_tip_utxo)
 196              package_hex.append(tx["hex"])
 197              pc_parent_utxos.append(tx["new_utxo"])
 198  
 199          # Child Pc
 200          pc_hex = self.wallet.create_self_transfer_multi(utxos_to_spend=pc_parent_utxos)["hex"]
 201          package_hex.append(pc_hex)
 202  
 203          assert_equal(24, node.getmempoolinfo()["size"])
 204          assert_equal(3, len(package_hex))
 205          return package_hex
 206  
 207      @check_package_limits
 208      def test_anc_count_limits_2(self):
 209          """Create a 'Y' shaped chain with 24 transactions in the mempool and 2 in the package:
 210          M1a                M1b
 211           ^                ^
 212            M2a            M2b
 213             .            .
 214              .          .
 215               .        .
 216              M12a    M12b
 217                 ^    ^
 218                   Pc
 219                   ^
 220                   Pd
 221          The lowest descendant, Pd, exceeds ancestor limits, but only if the in-mempool
 222          and in-package ancestors are all considered together.
 223          """
 224          node = self.nodes[0]
 225          pc_parent_utxos = []
 226  
 227          self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages")
 228          # Two chains of 12 transactions each
 229          for _ in range(2):
 230              chaintip_utxo = self.wallet.send_self_transfer_chain(from_node=node, chain_length=12)[-1]["new_utxo"]
 231              # last 2 transactions will be the parents of Pc
 232              pc_parent_utxos.append(chaintip_utxo)
 233  
 234          # Child Pc
 235          pc_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=pc_parent_utxos)
 236  
 237          # Child Pd
 238          pd_tx = self.wallet.create_self_transfer(utxo_to_spend=pc_tx["new_utxos"][0])
 239  
 240          assert_equal(24, node.getmempoolinfo()["size"])
 241          return [pc_tx["hex"], pd_tx["hex"]]
 242  
 243      @check_package_limits
 244      def test_anc_count_limits_bushy(self):
 245          """Create a tree with 20 transactions in the mempool and 6 in the package:
 246          M1...M4 M5...M8 M9...M12 M13...M16 M17...M20
 247              ^      ^       ^        ^         ^             (each with 4 parents)
 248              P0     P1      P2      P3        P4
 249               ^     ^       ^       ^         ^              (5 parents)
 250                             PC
 251          Where M(4i+1)...M+(4i+4) are the parents of Pi and P0, P1, P2, P3, and P4 are the parents of PC.
 252          P0... P4 individually only have 4 parents each, and PC has no in-mempool parents. But
 253          combined, PC has 25 in-mempool and in-package parents.
 254          """
 255          node = self.nodes[0]
 256          package_hex = []
 257          pc_parent_utxos = []
 258          for _ in range(5): # Make package transactions P0 ... P4
 259              pc_grandparent_utxos = []
 260              for _ in range(4): # Make mempool transactions M(4i+1)...M(4i+4)
 261                  pc_grandparent_utxos.append(self.wallet.send_self_transfer(from_node=node)["new_utxo"])
 262              # Package transaction Pi
 263              pi_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=pc_grandparent_utxos)
 264              package_hex.append(pi_tx["hex"])
 265              pc_parent_utxos.append(pi_tx["new_utxos"][0])
 266          # Package transaction PC
 267          pc_hex = self.wallet.create_self_transfer_multi(utxos_to_spend=pc_parent_utxos)["hex"]
 268          package_hex.append(pc_hex)
 269  
 270          assert_equal(20, node.getmempoolinfo()["size"])
 271          assert_equal(6, len(package_hex))
 272          return package_hex
 273  
 274      @check_package_limits
 275      def test_anc_size_limits(self):
 276          """Test Case with 2 independent transactions in the mempool and a parent + child in the
 277          package, where the package parent is the child of both mempool transactions (30KvB each):
 278                A     B
 279                 ^   ^
 280                   C
 281                   ^
 282                   D
 283          The lowest descendant, D, exceeds ancestor size limits, but only if the in-mempool
 284          and in-package ancestors are all considered together.
 285          """
 286          node = self.nodes[0]
 287          parent_utxos = []
 288          target_vsize = 30_000
 289          high_fee = 10 * target_vsize  # 10 sats/vB
 290          self.log.info("Check that in-mempool and in-package ancestor size limits are calculated properly in packages")
 291          # Mempool transactions A and B
 292          for _ in range(2):
 293              bulked_tx = self.wallet.create_self_transfer(target_vsize=target_vsize)
 294              self.wallet.sendrawtransaction(from_node=node, tx_hex=bulked_tx["hex"])
 295              parent_utxos.append(bulked_tx["new_utxo"])
 296  
 297          # Package transaction C
 298          pc_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=high_fee, target_vsize=target_vsize)
 299  
 300          # Package transaction D
 301          pd_tx = self.wallet.create_self_transfer(utxo_to_spend=pc_tx["new_utxos"][0], target_vsize=target_vsize)
 302  
 303          assert_equal(2, node.getmempoolinfo()["size"])
 304          return [pc_tx["hex"], pd_tx["hex"]]
 305  
 306      @check_package_limits
 307      def test_desc_size_limits(self):
 308          """Create 3 mempool transactions and 2 package transactions (21KvB each):
 309                Ma
 310               ^ ^
 311              Mb  Mc
 312             ^     ^
 313            Pd      Pe
 314          The top ancestor in the package exceeds descendant size limits but only if the in-mempool
 315          and in-package descendants are all considered together.
 316          """
 317          node = self.nodes[0]
 318          target_vsize = 21_000
 319          high_fee = 10 * target_vsize  # 10 sats/vB
 320          self.log.info("Check that in-mempool and in-package descendant sizes are calculated properly in packages")
 321          # Top parent in mempool, Ma
 322          ma_tx = self.wallet.create_self_transfer_multi(num_outputs=2, fee_per_output=high_fee // 2, target_vsize=target_vsize)
 323          self.wallet.sendrawtransaction(from_node=node, tx_hex=ma_tx["hex"])
 324  
 325          package_hex = []
 326          for j in range(2): # Two legs (left and right)
 327              # Mempool transaction (Mb and Mc)
 328              mempool_tx = self.wallet.create_self_transfer(utxo_to_spend=ma_tx["new_utxos"][j], target_vsize=target_vsize)
 329              self.wallet.sendrawtransaction(from_node=node, tx_hex=mempool_tx["hex"])
 330  
 331              # Package transaction (Pd and Pe)
 332              package_tx = self.wallet.create_self_transfer(utxo_to_spend=mempool_tx["new_utxo"], target_vsize=target_vsize)
 333              package_hex.append(package_tx["hex"])
 334  
 335          assert_equal(3, node.getmempoolinfo()["size"])
 336          assert_equal(2, len(package_hex))
 337          return package_hex
 338  
 339  
 340  if __name__ == "__main__":
 341      MempoolPackageLimitsTest(__file__).main()
 342