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