mempool_packages.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-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 descendant package tracking code."""
   6  
   7  from decimal import Decimal
   8  
   9  from test_framework.messages import (
  10      DEFAULT_ANCESTOR_LIMIT,
  11      DEFAULT_DESCENDANT_LIMIT,
  12  )
  13  from test_framework.p2p import P2PTxInvStore
  14  from test_framework.test_framework import LimenkaTestFramework
  15  from test_framework.util import (
  16      assert_equal,
  17      assert_raises_rpc_error,
  18  )
  19  from test_framework.wallet import MiniWallet
  20  
  21  # custom limits for node1
  22  CUSTOM_ANCESTOR_LIMIT = 5
  23  CUSTOM_DESCENDANT_LIMIT = 10
  24  assert CUSTOM_DESCENDANT_LIMIT >= CUSTOM_ANCESTOR_LIMIT
  25  
  26  
  27  class MempoolPackagesTest(LimenkaTestFramework):
  28      def set_test_params(self):
  29          self.num_nodes = 2
  30          # whitelist peers to speed up tx relay / mempool sync
  31          self.noban_tx_relay = True
  32          self.extra_args = [
  33              [
  34              ],
  35              [
  36                  "-limitancestorcount={}".format(CUSTOM_ANCESTOR_LIMIT),
  37                  "-limitdescendantcount={}".format(CUSTOM_DESCENDANT_LIMIT),
  38              ],
  39          ]
  40  
  41      def run_test(self):
  42          self.wallet = MiniWallet(self.nodes[0])
  43          self.wallet.rescan_utxos()
  44  
  45          peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) # keep track of invs
  46  
  47          # DEFAULT_ANCESTOR_LIMIT transactions off a confirmed tx should be fine
  48          chain = self.wallet.create_self_transfer_chain(chain_length=DEFAULT_ANCESTOR_LIMIT)
  49          witness_chain = [t["wtxid"] for t in chain]
  50          ancestor_vsize = 0
  51          ancestor_fees = Decimal(0)
  52  
  53          for i, t in enumerate(chain):
  54              ancestor_vsize += t["tx"].get_vsize()
  55              ancestor_fees += t["fee"]
  56              self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=t["hex"])
  57  
  58          # Wait until mempool transactions have passed initial broadcast (sent inv and received getdata)
  59          # Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between
  60          peer_inv_store.wait_for_broadcast(witness_chain)
  61  
  62          # Check mempool has DEFAULT_ANCESTOR_LIMIT transactions in it, and descendant and ancestor
  63          # count and fees should look correct
  64          mempool = self.nodes[0].getrawmempool(True)
  65          assert_equal(len(mempool), DEFAULT_ANCESTOR_LIMIT)
  66          descendant_count = 1
  67          descendant_fees = 0
  68          descendant_vsize = 0
  69  
  70          assert_equal(ancestor_vsize, sum([mempool[tx]['vsize'] for tx in mempool]))
  71          ancestor_count = DEFAULT_ANCESTOR_LIMIT
  72          assert_equal(ancestor_fees, sum([mempool[tx]['fees']['base'] for tx in mempool]))
  73  
  74          # Adding one more transaction on to the chain should fail.
  75          next_hop = self.wallet.create_self_transfer(utxo_to_spend=chain[-1]["new_utxo"])["hex"]
  76          assert_raises_rpc_error(-26, "too-long-mempool-chain", lambda: self.nodes[0].sendrawtransaction(next_hop))
  77  
  78          descendants = []
  79          ancestors = [t["txid"] for t in chain]
  80          chain = [t["txid"] for t in chain]
  81          for x in reversed(chain):
  82              # Check that getmempoolentry is consistent with getrawmempool
  83              entry = self.nodes[0].getmempoolentry(x)
  84              assert_equal(entry, mempool[x])
  85  
  86              # Check that gettxspendingprevout is consistent with getrawmempool
  87              witnesstx = self.nodes[0].getrawtransaction(txid=x, verbose=True)
  88              for tx_in in witnesstx["vin"]:
  89                  spending_result = self.nodes[0].gettxspendingprevout([ {'txid' : tx_in["txid"], 'vout' : tx_in["vout"]} ])
  90                  assert_equal(spending_result, [ {'txid' : tx_in["txid"], 'vout' : tx_in["vout"], 'spendingtxid' : x} ])
  91  
  92              # Check that the descendant calculations are correct
  93              assert_equal(entry['descendantcount'], descendant_count)
  94              descendant_fees += entry['fees']['base']
  95              assert_equal(entry['fees']['modified'], entry['fees']['base'])
  96              assert_equal(entry['fees']['descendant'], descendant_fees)
  97              descendant_vsize += entry['vsize']
  98              assert_equal(entry['descendantsize'], descendant_vsize)
  99              descendant_count += 1
 100  
 101              # Check that ancestor calculations are correct
 102              assert_equal(entry['ancestorcount'], ancestor_count)
 103              assert_equal(entry['fees']['ancestor'], ancestor_fees)
 104              assert_equal(entry['ancestorsize'], ancestor_vsize)
 105              ancestor_vsize -= entry['vsize']
 106              ancestor_fees -= entry['fees']['base']
 107              ancestor_count -= 1
 108  
 109              # Check that parent/child list is correct
 110              assert_equal(entry['spentby'], descendants[-1:])
 111              assert_equal(entry['depends'], ancestors[-2:-1])
 112  
 113              # Check that getmempooldescendants is correct
 114              assert_equal(sorted(descendants), sorted(self.nodes[0].getmempooldescendants(x)))
 115  
 116              # Check getmempooldescendants verbose output is correct
 117              for descendant, dinfo in self.nodes[0].getmempooldescendants(x, True).items():
 118                  assert_equal(dinfo['depends'], [chain[chain.index(descendant)-1]])
 119                  if dinfo['descendantcount'] > 1:
 120                      assert_equal(dinfo['spentby'], [chain[chain.index(descendant)+1]])
 121                  else:
 122                      assert_equal(dinfo['spentby'], [])
 123              descendants.append(x)
 124  
 125              # Check that getmempoolancestors is correct
 126              ancestors.remove(x)
 127              assert_equal(sorted(ancestors), sorted(self.nodes[0].getmempoolancestors(x)))
 128  
 129              # Check that getmempoolancestors verbose output is correct
 130              for ancestor, ainfo in self.nodes[0].getmempoolancestors(x, True).items():
 131                  assert_equal(ainfo['spentby'], [chain[chain.index(ancestor)+1]])
 132                  if ainfo['ancestorcount'] > 1:
 133                      assert_equal(ainfo['depends'], [chain[chain.index(ancestor)-1]])
 134                  else:
 135                      assert_equal(ainfo['depends'], [])
 136  
 137  
 138          # Check that getmempoolancestors/getmempooldescendants correctly handle verbose=true
 139          v_ancestors = self.nodes[0].getmempoolancestors(chain[-1], True)
 140          assert_equal(len(v_ancestors), len(chain)-1)
 141          for x in v_ancestors.keys():
 142              assert_equal(mempool[x], v_ancestors[x])
 143          assert chain[-1] not in v_ancestors.keys()
 144  
 145          v_descendants = self.nodes[0].getmempooldescendants(chain[0], True)
 146          assert_equal(len(v_descendants), len(chain)-1)
 147          for x in v_descendants.keys():
 148              assert_equal(mempool[x], v_descendants[x])
 149          assert chain[0] not in v_descendants.keys()
 150  
 151          # Check that ancestor modified fees includes fee deltas from
 152          # prioritisetransaction
 153          self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=1000)
 154          ancestor_fees = 0
 155          for x in chain:
 156              entry = self.nodes[0].getmempoolentry(x)
 157              ancestor_fees += entry['fees']['base']
 158              assert_equal(entry['fees']['ancestor'], ancestor_fees + Decimal('0.00001'))
 159  
 160          # Undo the prioritisetransaction for later tests
 161          self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=-1000)
 162  
 163          # Check that descendant modified fees includes fee deltas from
 164          # prioritisetransaction
 165          self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=1000)
 166  
 167          descendant_fees = 0
 168          for x in reversed(chain):
 169              entry = self.nodes[0].getmempoolentry(x)
 170              descendant_fees += entry['fees']['base']
 171              assert_equal(entry['fees']['descendant'], descendant_fees + Decimal('0.00001'))
 172  
 173          # Check that prioritising a tx before it's added to the mempool works
 174          # First clear the mempool by mining a block.
 175          self.generate(self.nodes[0], 1)
 176          assert_equal(len(self.nodes[0].getrawmempool()), 0)
 177          # Prioritise a transaction that has been mined, then add it back to the
 178          # mempool by using invalidateblock.
 179          self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=2000)
 180          self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
 181          # Keep node1's tip synced with node0
 182          self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash())
 183  
 184          # Now check that the transaction is in the mempool, with the right modified fee
 185          descendant_fees = 0
 186          for x in reversed(chain):
 187              entry = self.nodes[0].getmempoolentry(x)
 188              descendant_fees += entry['fees']['base']
 189              if (x == chain[-1]):
 190                  assert_equal(entry['fees']['modified'], entry['fees']['base'] + Decimal("0.00002"))
 191              assert_equal(entry['fees']['descendant'], descendant_fees + Decimal("0.00002"))
 192  
 193          # Check that node1's mempool is as expected (-> custom ancestor limit)
 194          mempool0 = self.nodes[0].getrawmempool(False)
 195          mempool1 = self.nodes[1].getrawmempool(False)
 196          assert_equal(len(mempool1), CUSTOM_ANCESTOR_LIMIT)
 197          assert set(mempool1).issubset(set(mempool0))
 198          for tx in chain[:CUSTOM_ANCESTOR_LIMIT]:
 199              assert tx in mempool1
 200              entry0 = self.nodes[0].getmempoolentry(tx)
 201              entry1 = self.nodes[1].getmempoolentry(tx)
 202              assert not entry0['unbroadcast']
 203              assert not entry1['unbroadcast']
 204              assert_equal(entry1['fees']['base'], entry0['fees']['base'])
 205              assert_equal(entry1['vsize'], entry0['vsize'])
 206              assert_equal(entry1['depends'], entry0['depends'])
 207  
 208          # Now test descendant chain limits
 209  
 210          tx_children = []
 211          # First create one parent tx with 10 children
 212          tx_with_children = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], num_outputs=10)
 213          parent_transaction = tx_with_children["txid"]
 214          transaction_package = tx_with_children["new_utxos"]
 215  
 216          # Sign and send up to MAX_DESCENDANT transactions chained off the parent tx
 217          chain = [] # save sent txs for the purpose of checking node1's mempool later (see below)
 218          for _ in range(DEFAULT_DESCENDANT_LIMIT - 1):
 219              utxo = transaction_package.pop(0)
 220              new_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], num_outputs=10, utxos_to_spend=[utxo])
 221              txid = new_tx["txid"]
 222              chain.append(txid)
 223              if utxo['txid'] is parent_transaction:
 224                  tx_children.append(txid)
 225              transaction_package.extend(new_tx["new_utxos"])
 226  
 227          mempool = self.nodes[0].getrawmempool(True)
 228          assert_equal(mempool[parent_transaction]['descendantcount'], DEFAULT_DESCENDANT_LIMIT)
 229          assert_equal(sorted(mempool[parent_transaction]['spentby']), sorted(tx_children))
 230  
 231          for child in tx_children:
 232              assert_equal(mempool[child]['depends'], [parent_transaction])
 233  
 234          # Sending one more chained transaction will fail
 235          next_hop = self.wallet.create_self_transfer(utxo_to_spend=transaction_package.pop(0))["hex"]
 236          assert_raises_rpc_error(-26, "too-long-mempool-chain", lambda: self.nodes[0].sendrawtransaction(next_hop))
 237  
 238          # Check that node1's mempool is as expected, containing:
 239          # - txs from previous ancestor test (-> custom ancestor limit)
 240          # - parent tx for descendant test
 241          # - txs chained off parent tx (-> custom descendant limit)
 242          self.wait_until(lambda: len(self.nodes[1].getrawmempool()) ==
 243                                  CUSTOM_ANCESTOR_LIMIT + 1 + CUSTOM_DESCENDANT_LIMIT, timeout=10)
 244          mempool0 = self.nodes[0].getrawmempool(False)
 245          mempool1 = self.nodes[1].getrawmempool(False)
 246          assert set(mempool1).issubset(set(mempool0))
 247          assert parent_transaction in mempool1
 248          for tx in chain[:CUSTOM_DESCENDANT_LIMIT]:
 249              assert tx in mempool1
 250          for tx in chain[CUSTOM_DESCENDANT_LIMIT:]:
 251              assert tx not in mempool1
 252          for tx in mempool1:
 253              entry0 = self.nodes[0].getmempoolentry(tx)
 254              entry1 = self.nodes[1].getmempoolentry(tx)
 255              assert not entry0['unbroadcast']
 256              assert not entry1['unbroadcast']
 257              assert_equal(entry1['fees']['base'], entry0['fees']['base'])
 258              assert_equal(entry1['vsize'], entry0['vsize'])
 259              assert_equal(entry1['depends'], entry0['depends'])
 260          # Test reorg handling
 261          # First, the basics:
 262          self.generate(self.nodes[0], 1)
 263          self.nodes[1].invalidateblock(self.nodes[0].getbestblockhash())
 264          self.nodes[1].reconsiderblock(self.nodes[0].getbestblockhash())
 265  
 266          # Now test the case where node1 has a transaction T in its mempool that
 267          # depends on transactions A and B which are in a mined block, and the
 268          # block containing A and B is disconnected, AND B is not accepted back
 269          # into node1's mempool because its ancestor count is too high.
 270  
 271          # Create 8 transactions, like so:
 272          # Tx0 -> Tx1 (vout0)
 273          #   \--> Tx2 (vout1) -> Tx3 -> Tx4 -> Tx5 -> Tx6 -> Tx7
 274          #
 275          # Mine them in the next block, then generate a new tx8 that spends
 276          # Tx1 and Tx7, and add to node1's mempool, then disconnect the
 277          # last block.
 278  
 279          # Create tx0 with 2 outputs
 280          tx0 = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], num_outputs=2)
 281  
 282          # Create tx1
 283          tx1 = self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=tx0["new_utxos"][0])
 284  
 285          # Create tx2-7
 286          tx7 = self.wallet.send_self_transfer_chain(from_node=self.nodes[0], utxo_to_spend=tx0["new_utxos"][1], chain_length=6)[-1]
 287  
 288          # Mine these in a block
 289          self.generate(self.nodes[0], 1)
 290  
 291          # Now generate tx8, with a big fee
 292          self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[tx1["new_utxo"], tx7["new_utxo"]], fee_per_output=40000)
 293          self.sync_mempools()
 294  
 295          # Now try to disconnect the tip on each node...
 296          self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash())
 297          self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
 298          self.sync_blocks()
 299  
 300  if __name__ == '__main__':
 301      MempoolPackagesTest(__file__).main()
 302