mining_prioritisetransaction.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2015-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 the prioritisetransaction mining RPC."""
6
7 from decimal import Decimal
8 import time
9
10 from test_framework.messages import (
11 COIN,
12 MAX_BLOCK_WEIGHT,
13 )
14 from test_framework.test_framework import LimenkaTestFramework
15 from test_framework.util import (
16 assert_equal,
17 assert_raises_rpc_error,
18 create_lots_of_big_transactions,
19 gen_return_txouts,
20 )
21 from test_framework.wallet import MiniWallet
22
23
24 class PrioritiseTransactionTest(LimenkaTestFramework):
25 def set_test_params(self):
26 self.num_nodes = 1
27 self.extra_args = [[
28 "-printpriority=1",
29 "-datacarriersize=100000",
30 ]] * self.num_nodes
31 self.supports_cli = False
32
33 def clear_prioritisation(self, node):
34 for txid, info in node.getprioritisedtransactions().items():
35 delta = info["fee_delta"]
36 node.prioritisetransaction(txid, 0, -delta)
37 assert_equal(node.getprioritisedtransactions(), {})
38
39 def test_replacement(self):
40 self.log.info("Test tx prioritisation stays after a tx is replaced")
41 conflicting_input = self.wallet.get_utxo()
42 tx_replacee = self.wallet.create_self_transfer(utxo_to_spend=conflicting_input, fee_rate=Decimal("0.0001"))
43 tx_replacement = self.wallet.create_self_transfer(utxo_to_spend=conflicting_input, fee_rate=Decimal("0.005"))
44 # Add 1 satoshi fee delta to replacee
45 self.nodes[0].prioritisetransaction(tx_replacee["txid"], 0, 100)
46 assert_equal(self.nodes[0].getprioritisedtransactions(), { tx_replacee["txid"] : { "fee_delta" : 100, "in_mempool" : False}})
47 self.nodes[0].sendrawtransaction(tx_replacee["hex"])
48 assert_equal(self.nodes[0].getprioritisedtransactions(), { tx_replacee["txid"] : { "fee_delta" : 100, "in_mempool" : True, "modified_fee": int(tx_replacee["fee"] * COIN + 100)}})
49 self.nodes[0].sendrawtransaction(tx_replacement["hex"])
50 assert tx_replacee["txid"] not in self.nodes[0].getrawmempool()
51 assert_equal(self.nodes[0].getprioritisedtransactions(), { tx_replacee["txid"] : { "fee_delta" : 100, "in_mempool" : False}})
52
53 # PrioritiseTransaction is additive
54 self.nodes[0].prioritisetransaction(tx_replacee["txid"], 0, COIN)
55 self.nodes[0].sendrawtransaction(tx_replacee["hex"])
56 assert_equal(self.nodes[0].getprioritisedtransactions(), { tx_replacee["txid"] : { "fee_delta" : COIN + 100, "in_mempool" : True, "modified_fee": int(tx_replacee["fee"] * COIN + COIN + 100)}})
57 self.generate(self.nodes[0], 1)
58 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
59
60 def test_diamond(self):
61 self.log.info("Test diamond-shape package with priority")
62 mock_time = int(time.time())
63 self.nodes[0].setmocktime(mock_time)
64
65 # tx_a
66 # / \
67 # / \
68 # tx_b tx_c
69 # \ /
70 # \ /
71 # tx_d
72
73 tx_o_a = self.wallet.send_self_transfer_multi(
74 from_node=self.nodes[0],
75 num_outputs=2,
76 )
77 txid_a = tx_o_a["txid"]
78
79 tx_o_b, tx_o_c = [self.wallet.send_self_transfer(
80 from_node=self.nodes[0],
81 utxo_to_spend=u,
82 ) for u in tx_o_a["new_utxos"]]
83 txid_b = tx_o_b["txid"]
84 txid_c = tx_o_c["txid"]
85
86 tx_o_d = self.wallet.send_self_transfer_multi(
87 from_node=self.nodes[0],
88 utxos_to_spend=[
89 self.wallet.get_utxo(txid=txid_b),
90 self.wallet.get_utxo(txid=txid_c),
91 ],
92 )
93 txid_d = tx_o_d["txid"]
94
95 self.log.info("Test priority while txs are in mempool")
96 raw_before = self.nodes[0].getrawmempool(verbose=True)
97 fee_delta_b = Decimal(9999) / COIN
98 fee_delta_c_1 = Decimal(-1234) / COIN
99 fee_delta_c_2 = Decimal(8888) / COIN
100 self.nodes[0].prioritisetransaction(txid=txid_b, fee_delta=int(fee_delta_b * COIN))
101 self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_1 * COIN))
102 self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_2 * COIN))
103 raw_before[txid_a]["fees"]["descendant"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
104 raw_before[txid_b]["fees"]["modified"] += fee_delta_b
105 raw_before[txid_b]["fees"]["ancestor"] += fee_delta_b
106 raw_before[txid_b]["fees"]["descendant"] += fee_delta_b
107 raw_before[txid_c]["fees"]["modified"] += fee_delta_c_1 + fee_delta_c_2
108 raw_before[txid_c]["fees"]["ancestor"] += fee_delta_c_1 + fee_delta_c_2
109 raw_before[txid_c]["fees"]["descendant"] += fee_delta_c_1 + fee_delta_c_2
110 raw_before[txid_d]["fees"]["ancestor"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
111 raw_after = self.nodes[0].getrawmempool(verbose=True)
112 assert_equal(raw_before[txid_a], raw_after[txid_a])
113 assert_equal(raw_before, raw_after)
114 assert_equal(self.nodes[0].getprioritisedtransactions(), {txid_b: {"fee_delta" : fee_delta_b*COIN, "in_mempool" : True, "modified_fee": int(fee_delta_b*COIN + COIN * tx_o_b["fee"])}, txid_c: {"fee_delta" : (fee_delta_c_1 + fee_delta_c_2)*COIN, "in_mempool" : True, "modified_fee": int((fee_delta_c_1 + fee_delta_c_2 ) * COIN + COIN * tx_o_c["fee"])}})
115 # Clear prioritisation, otherwise the transactions' fee deltas are persisted to mempool.dat and loaded again when the node
116 # is restarted at the end of this subtest. Deltas are removed when a transaction is mined, but only at that time. We do
117 # not check whether mapDeltas transactions were mined when loading from mempool.dat.
118 self.clear_prioritisation(node=self.nodes[0])
119
120 self.log.info("Test priority while txs are not in mempool")
121 self.restart_node(0, extra_args=["-nopersistmempool"])
122 self.nodes[0].setmocktime(mock_time)
123 assert_equal(self.nodes[0].getmempoolinfo()["size"], 0)
124 self.nodes[0].prioritisetransaction(txid=txid_b, fee_delta=int(fee_delta_b * COIN))
125 self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_1 * COIN))
126 self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_2 * COIN))
127 assert_equal(self.nodes[0].getprioritisedtransactions(), {txid_b: {"fee_delta" : fee_delta_b*COIN, "in_mempool" : False}, txid_c: {"fee_delta" : (fee_delta_c_1 + fee_delta_c_2)*COIN, "in_mempool" : False}})
128 for t in [tx_o_a["hex"], tx_o_b["hex"], tx_o_c["hex"], tx_o_d["hex"]]:
129 self.nodes[0].sendrawtransaction(t)
130 raw_after = self.nodes[0].getrawmempool(verbose=True)
131 assert_equal(raw_before[txid_a], raw_after[txid_a])
132 assert_equal(raw_before, raw_after)
133 assert_equal(self.nodes[0].getprioritisedtransactions(), {txid_b: {"fee_delta" : fee_delta_b*COIN, "in_mempool" : True, "modified_fee": int(fee_delta_b*COIN + COIN * tx_o_b["fee"])}, txid_c: {"fee_delta" : (fee_delta_c_1 + fee_delta_c_2)*COIN, "in_mempool" : True, "modified_fee": int((fee_delta_c_1 + fee_delta_c_2 ) * COIN + COIN * tx_o_c["fee"])}})
134
135 # Clear mempool
136 self.generate(self.nodes[0], 1)
137 # Prioritisation for transactions is automatically deleted after they are mined.
138 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
139
140 # Use default extra_args
141 self.restart_node(0)
142 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
143
144 def run_test(self):
145 self.wallet = MiniWallet(self.nodes[0])
146
147 # Test `prioritisetransaction` required parameters
148 assert_raises_rpc_error(-1, "prioritisetransaction", self.nodes[0].prioritisetransaction)
149
150 # Test `prioritisetransaction` invalid extra parameters
151 assert_raises_rpc_error(-1, "prioritisetransaction", self.nodes[0].prioritisetransaction, '', 0, 0, 0)
152
153 # Test `getprioritisedtransactions` invalid parameters
154 assert_raises_rpc_error(-1, "getprioritisedtransactions",
155 self.nodes[0].getprioritisedtransactions, True)
156
157 # Test `prioritisetransaction` invalid `txid`
158 assert_raises_rpc_error(-8, "txid must be of length 64 (not 3, for 'foo')", self.nodes[0].prioritisetransaction, txid='foo', fee_delta=0)
159 assert_raises_rpc_error(-8, "txid must be hexadecimal string (not 'Zd1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000')", self.nodes[0].prioritisetransaction, txid='Zd1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000', fee_delta=0)
160
161 # Test `prioritisetransaction` invalid `priority_delta`
162 txid = '1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000'
163 assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].prioritisetransaction, txid, 'foo', 0)
164
165 # Test `prioritisetransaction` invalid `fee_delta`
166 assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].prioritisetransaction, txid=txid, fee_delta='foo')
167
168 self.test_replacement()
169 self.test_diamond()
170
171 self.txouts = gen_return_txouts()
172 self.relayfee = self.nodes[0].getnetworkinfo()['relayfee']
173
174 utxo_count = 90
175 utxos = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], num_outputs=utxo_count)['new_utxos']
176 self.generate(self.wallet, 1)
177 assert_equal(len(self.nodes[0].getrawmempool()), 0)
178
179 base_fee = self.relayfee*100 # our transactions are smaller than 100kb
180 txids = []
181
182 # Create 3 batches of transactions at 3 different fee rate levels
183 range_size = utxo_count // 3
184 for i in range(3):
185 txids.append([])
186 start_range = i * range_size
187 end_range = start_range + range_size
188 txids[i] = create_lots_of_big_transactions(
189 self.wallet,
190 self.nodes[0],
191 (i+1) * base_fee,
192 end_range - start_range,
193 self.txouts,
194 utxos[start_range:end_range])
195
196 # Make sure that the size of each group of transactions exceeds
197 # MAX_BLOCK_WEIGHT // 4 -- otherwise the test needs to be revised to
198 # create more transactions.
199 mempool = self.nodes[0].getrawmempool(True)
200 sizes = [0, 0, 0]
201 for i in range(3):
202 for j in txids[i]:
203 assert j in mempool
204 sizes[i] += mempool[j]['vsize']
205 assert sizes[i] > MAX_BLOCK_WEIGHT // 4 # Fail => raise utxo_count
206
207 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
208 # add a fee delta to something in the cheapest bucket and make sure it gets mined
209 # also check that a different entry in the cheapest bucket is NOT mined
210 self.nodes[0].prioritisetransaction(txid=txids[0][0], fee_delta=int(3*base_fee*COIN))
211 assert_equal(self.nodes[0].getprioritisedtransactions(), {txids[0][0] : { "fee_delta" : 3*base_fee*COIN, "in_mempool" : True, "modified_fee": int(3*base_fee*COIN + COIN * 1 * base_fee)}})
212
213 # Priority disappears when prioritisetransaction is called with an inverse value...
214 self.nodes[0].prioritisetransaction(txid=txids[0][0], fee_delta=int(-3*base_fee*COIN))
215 assert txids[0][0] not in self.nodes[0].getprioritisedtransactions()
216 # ... and reappears when prioritisetransaction is called again.
217 self.nodes[0].prioritisetransaction(txid=txids[0][0], fee_delta=int(3*base_fee*COIN))
218 assert txids[0][0] in self.nodes[0].getprioritisedtransactions()
219
220 self.generate(self.nodes[0], 1)
221
222 mempool = self.nodes[0].getrawmempool()
223 self.log.info("Assert that prioritised transaction was mined")
224 assert txids[0][0] not in mempool
225 assert txids[0][1] in mempool
226
227 high_fee_tx = None
228 for x in txids[2]:
229 if x not in mempool:
230 high_fee_tx = x
231
232 # Something high-fee should have been mined!
233 assert high_fee_tx is not None
234
235 # Add a prioritisation before a tx is in the mempool (de-prioritising a
236 # high-fee transaction so that it's now low fee).
237 self.nodes[0].prioritisetransaction(txid=high_fee_tx, fee_delta=-int(2*base_fee*COIN))
238 assert_equal(self.nodes[0].getprioritisedtransactions()[high_fee_tx], { "fee_delta" : -2*base_fee*COIN, "in_mempool" : False})
239
240 # Add everything back to mempool
241 self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
242
243 # Check to make sure our high fee rate tx is back in the mempool
244 mempool = self.nodes[0].getrawmempool()
245 assert high_fee_tx in mempool
246
247 # Now verify the modified-high feerate transaction isn't mined before
248 # the other high fee transactions. Keep mining until our mempool has
249 # decreased by all the high fee size that we calculated above.
250 while (self.nodes[0].getmempoolinfo()['bytes'] > sizes[0] + sizes[1]):
251 self.generate(self.nodes[0], 1, sync_fun=self.no_op)
252
253 # High fee transaction should not have been mined, but other high fee rate
254 # transactions should have been.
255 mempool = self.nodes[0].getrawmempool()
256 self.log.info("Assert that de-prioritised transaction is still in mempool")
257 assert high_fee_tx in mempool
258 assert_equal(self.nodes[0].getprioritisedtransactions()[high_fee_tx], { "fee_delta" : -2*base_fee*COIN, "in_mempool" : True, "modified_fee": int(-2*base_fee*COIN + COIN * 3 * base_fee)})
259 for x in txids[2]:
260 if (x != high_fee_tx):
261 assert x not in mempool
262
263
264 self.log.info("Assert that 0 delta is never added to mapDeltas")
265 tx_id_zero_del = self.wallet.create_self_transfer()['txid']
266 self.nodes[0].prioritisetransaction(txid=tx_id_zero_del, fee_delta=0)
267 assert tx_id_zero_del not in self.nodes[0].getprioritisedtransactions()
268
269 # Create a free transaction. Should be rejected.
270 tx_res = self.wallet.create_self_transfer(fee_rate=0)
271 tx_hex = tx_res['hex']
272 tx_id = tx_res['txid']
273
274 # This will raise an exception due to min relay fee not being met
275 assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, tx_hex)
276 assert tx_id not in self.nodes[0].getrawmempool()
277
278 # This is a less than 1000-byte transaction, so just set the fee
279 # to be the minimum for a 1000-byte transaction and check that it is
280 # accepted.
281 self.nodes[0].prioritisetransaction(txid=tx_id, fee_delta=int(self.relayfee*COIN))
282 assert_equal(self.nodes[0].getprioritisedtransactions()[tx_id], { "fee_delta" : self.relayfee*COIN, "in_mempool" : False})
283
284 self.log.info("Assert that prioritised free transaction is accepted to mempool")
285 assert_equal(self.nodes[0].sendrawtransaction(tx_hex), tx_id)
286 assert tx_id in self.nodes[0].getrawmempool()
287 assert_equal(self.nodes[0].getprioritisedtransactions()[tx_id], { "fee_delta" : self.relayfee*COIN, "in_mempool" : True, "modified_fee": int(self.relayfee*COIN + COIN * tx_res["fee"])})
288
289 # Test that calling prioritisetransaction is sufficient to trigger
290 # getblocktemplate to (eventually) return a new block.
291 mock_time = int(time.time())
292 self.nodes[0].setmocktime(mock_time)
293 template = self.nodes[0].getblocktemplate({'rules': ['segwit']})
294 self.nodes[0].prioritisetransaction(txid=tx_id, fee_delta=-int(self.relayfee*COIN))
295
296 # Calling prioritisetransaction with the inverse amount should delete its prioritisation entry
297 assert tx_id not in self.nodes[0].getprioritisedtransactions()
298
299 self.nodes[0].setmocktime(mock_time+10)
300 new_template = self.nodes[0].getblocktemplate({'rules': ['segwit']})
301
302 assert template != new_template
303
304 if __name__ == '__main__':
305 PrioritiseTransactionTest(__file__).main()
306