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