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 wallet import RPCs.
6 7 Test rescan behavior of importaddress, importpubkey, importprivkey, and
8 importmulti RPCs with different types of keys and rescan options.
9 10 In the first part of the test, node 0 creates an address for each type of
11 import RPC call and sends BTC to it. Then other nodes import the addresses,
12 and the test makes listtransactions and getbalance calls to confirm that the
13 importing node either did or did not execute rescans picking up the send
14 transactions.
15 16 In the second part of the test, node 0 sends more BTC to each address, and the
17 test makes more listtransactions and getbalance calls to confirm that the
18 importing nodes pick up the new transactions regardless of whether rescans
19 happened previously.
20 """
21 22 from test_framework.test_framework import LimenkaTestFramework
23 from test_framework.address import (
24 AddressType,
25 ADDRESS_BCRT1_UNSPENDABLE,
26 )
27 from test_framework.messages import COIN
28 from test_framework.util import (
29 assert_equal,
30 set_node_times,
31 )
32 33 import collections
34 from decimal import Decimal
35 import enum
36 import itertools
37 import random
38 39 Call = enum.Enum("Call", "single multiaddress multiscript")
40 Data = enum.Enum("Data", "address pub priv")
41 Rescan = enum.Enum("Rescan", "no yes late_timestamp")
42 43 44 class Variant(collections.namedtuple("Variant", "call data address_type rescan prune")):
45 """Helper for importing one key and verifying scanned transactions."""
46 def do_import(self, timestamp):
47 """Call one key import RPC."""
48 rescan = self.rescan == Rescan.yes
49 50 assert_equal(self.address["solvable"], True)
51 assert_equal(self.address["isscript"], self.address_type == AddressType.p2sh_segwit)
52 assert_equal(self.address["iswitness"], self.address_type == AddressType.bech32)
53 if self.address["isscript"]:
54 assert_equal(self.address["embedded"]["isscript"], False)
55 assert_equal(self.address["embedded"]["iswitness"], True)
56 57 if self.call == Call.single:
58 if self.data == Data.address:
59 response = self.node.importaddress(address=self.address["address"], label=self.label, rescan=rescan)
60 elif self.data == Data.pub:
61 response = self.node.importpubkey(pubkey=self.address["pubkey"], label=self.label, rescan=rescan)
62 elif self.data == Data.priv:
63 response = self.node.importprivkey(privkey=self.key, label=self.label, rescan=rescan)
64 assert_equal(response, None)
65 66 elif self.call in (Call.multiaddress, Call.multiscript):
67 request = {
68 "scriptPubKey": {
69 "address": self.address["address"]
70 } if self.call == Call.multiaddress else self.address["scriptPubKey"],
71 "timestamp": timestamp + TIMESTAMP_WINDOW + (1 if self.rescan == Rescan.late_timestamp else 0),
72 "pubkeys": [self.address["pubkey"]] if self.data == Data.pub else [],
73 "keys": [self.key] if self.data == Data.priv else [],
74 "label": self.label,
75 "watchonly": self.data != Data.priv
76 }
77 if self.address_type == AddressType.p2sh_segwit and self.data != Data.address:
78 # We need solving data when providing a pubkey or privkey as data
79 request.update({"redeemscript": self.address['embedded']['scriptPubKey']})
80 response = self.node.importmulti(
81 requests=[request],
82 rescan=self.rescan in (Rescan.yes, Rescan.late_timestamp),
83 )
84 assert_equal(response, [{"success": True}])
85 86 def check(self, txid=None, amount=None, confirmation_height=None):
87 """Verify that listtransactions/listreceivedbyaddress return expected values."""
88 89 txs = self.node.listtransactions(label=self.label, count=10000, include_watchonly=True)
90 current_height = self.node.getblockcount()
91 assert_equal(len(txs), self.expected_txs)
92 93 addresses = self.node.listreceivedbyaddress(minconf=0, include_watchonly=True, address_filter=self.address['address'])
94 95 if self.expected_txs:
96 assert_equal(len(addresses[0]["txids"]), self.expected_txs)
97 98 if txid is not None:
99 tx, = [tx for tx in txs if tx["txid"] == txid]
100 assert_equal(tx["label"], self.label)
101 assert_equal(tx["address"], self.address["address"])
102 assert_equal(tx["amount"], amount)
103 assert_equal(tx["category"], "receive")
104 assert_equal(tx["label"], self.label)
105 assert_equal(tx["txid"], txid)
106 107 # If no confirmation height is given, the tx is still in the
108 # mempool.
109 confirmations = (1 + current_height - confirmation_height) if confirmation_height else 0
110 assert_equal(tx["confirmations"], confirmations)
111 if confirmations:
112 assert "trusted" not in tx
113 114 address, = [ad for ad in addresses if txid in ad["txids"]]
115 assert_equal(address["address"], self.address["address"])
116 assert_equal(address["amount"], self.amount_received)
117 assert_equal(address["confirmations"], confirmations)
118 # Verify the transaction is correctly marked watchonly depending on
119 # whether the transaction pays to an imported public key or
120 # imported private key. The test setup ensures that transaction
121 # inputs will not be from watchonly keys (important because
122 # involvesWatchonly will be true if either the transaction output
123 # or inputs are watchonly).
124 if self.data != Data.priv:
125 assert_equal(address["involvesWatchonly"], True)
126 else:
127 assert_equal("involvesWatchonly" not in address, True)
128 129 130 # List of Variants for each way a key or address could be imported.
131 IMPORT_VARIANTS = [Variant(*variants) for variants in itertools.product(Call, Data, AddressType, Rescan, (False, True))]
132 133 # List of nodes to import keys to. Half the nodes will have pruning disabled,
134 # half will have it enabled. Different nodes will be used for imports that are
135 # expected to cause rescans, and imports that are not expected to cause
136 # rescans, in order to prevent rescans during later imports picking up
137 # transactions associated with earlier imports. This makes it easier to keep
138 # track of expected balances and transactions.
139 ImportNode = collections.namedtuple("ImportNode", "prune rescan")
140 IMPORT_NODES = [ImportNode(*fields) for fields in itertools.product((False, True), repeat=2)]
141 142 # Rescans start at the earliest block up to 2 hours before the key timestamp.
143 TIMESTAMP_WINDOW = 2 * 60 * 60
144 145 AMOUNT_DUST = 0.00000546
146 147 148 def get_rand_amount(min_amount=AMOUNT_DUST):
149 assert min_amount <= 1
150 r = random.uniform(min_amount, 1)
151 # note: min_amount can get rounded down here
152 return Decimal(str(round(r, 8)))
153 154 155 class ImportRescanTest(LimenkaTestFramework):
156 def add_options(self, parser):
157 self.add_wallet_options(parser, descriptors=False)
158 159 def set_test_params(self):
160 self.num_nodes = 2 + len(IMPORT_NODES)
161 self.supports_cli = False
162 self.rpc_timeout = 120
163 # whitelist peers to speed up tx relay / mempool sync
164 self.noban_tx_relay = True
165 166 def skip_test_if_missing_module(self):
167 self.skip_if_no_wallet()
168 169 def setup_network(self):
170 self.extra_args = [[] for _ in range(self.num_nodes)]
171 for i, import_node in enumerate(IMPORT_NODES, 2):
172 if import_node.prune:
173 self.extra_args[i] += ["-prune=1"]
174 175 self.add_nodes(self.num_nodes, extra_args=self.extra_args)
176 177 # Import keys with pruning disabled
178 self.start_nodes(extra_args=[[]] * self.num_nodes)
179 self.import_deterministic_coinbase_privkeys()
180 self.stop_nodes()
181 182 self.start_nodes()
183 for i in range(1, self.num_nodes):
184 self.connect_nodes(i, 0)
185 186 def run_test(self):
187 188 # Create one transaction on node 0 with a unique amount for
189 # each possible type of wallet import RPC.
190 last_variants = []
191 for i, variant in enumerate(IMPORT_VARIANTS):
192 if i % 10 == 0:
193 blockhash = self.generate(self.nodes[0], 1)[0]
194 conf_height = self.nodes[0].getblockcount()
195 timestamp = self.nodes[0].getblockheader(blockhash)["time"]
196 for var in last_variants:
197 var.confirmation_height = conf_height
198 var.timestamp = timestamp
199 last_variants.clear()
200 variant.label = "label {} {}".format(i, variant)
201 variant.address = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress(
202 label=variant.label,
203 address_type=variant.address_type.value,
204 ))
205 variant.key = self.nodes[1].dumpprivkey(variant.address["address"])
206 variant.initial_amount = get_rand_amount()
207 variant.initial_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.initial_amount)
208 last_variants.append(variant)
209 210 blockhash = self.generate(self.nodes[0], 1)[0]
211 conf_height = self.nodes[0].getblockcount()
212 timestamp = self.nodes[0].getblockheader(blockhash)["time"]
213 for var in last_variants:
214 var.confirmation_height = conf_height
215 var.timestamp = timestamp
216 last_variants.clear()
217 218 # Generate a block further in the future (past the rescan window).
219 assert_equal(self.nodes[0].getrawmempool(), [])
220 set_node_times(
221 self.nodes,
222 self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"] + TIMESTAMP_WINDOW + 1,
223 )
224 self.generate(self.nodes[0], 1)
225 226 # For each variation of wallet key import, invoke the import RPC and
227 # check the results from getbalance and listtransactions.
228 for variant in IMPORT_VARIANTS:
229 self.log.info('Run import for variant {}'.format(variant))
230 expect_rescan = variant.rescan == Rescan.yes
231 variant.node = self.nodes[2 + IMPORT_NODES.index(ImportNode(variant.prune, expect_rescan))]
232 variant.do_import(variant.timestamp)
233 if expect_rescan:
234 variant.amount_received = variant.initial_amount
235 variant.expected_txs = 1
236 variant.check(variant.initial_txid, variant.initial_amount, variant.confirmation_height)
237 else:
238 variant.amount_received = 0
239 variant.expected_txs = 0
240 variant.check()
241 242 # Create new transactions sending to each address.
243 for i, variant in enumerate(IMPORT_VARIANTS):
244 if i % 10 == 0:
245 blockhash = self.generate(self.nodes[0], 1)[0]
246 conf_height = self.nodes[0].getblockcount() + 1
247 variant.sent_amount = get_rand_amount()
248 variant.sent_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.sent_amount)
249 variant.confirmation_height = conf_height
250 self.generate(self.nodes[0], 1)
251 252 assert_equal(self.nodes[0].getrawmempool(), [])
253 self.sync_all()
254 255 # Check the latest results from getbalance and listtransactions.
256 for variant in IMPORT_VARIANTS:
257 self.log.info('Run check for variant {}'.format(variant))
258 variant.amount_received += variant.sent_amount
259 variant.expected_txs += 1
260 variant.check(variant.sent_txid, variant.sent_amount, variant.confirmation_height)
261 262 self.log.info('Test that the mempool is rescanned as well if the rescan parameter is set to true')
263 264 # The late timestamp and pruned variants are not necessary when testing mempool rescan
265 mempool_variants = [variant for variant in IMPORT_VARIANTS if variant.rescan != Rescan.late_timestamp and not variant.prune]
266 # No further blocks are mined so the timestamp will stay the same
267 timestamp = self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"]
268 269 # Create one transaction on node 0 with a unique amount for
270 # each possible type of wallet import RPC.
271 for i, variant in enumerate(mempool_variants):
272 variant.label = "mempool label {} {}".format(i, variant)
273 variant.address = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress(
274 label=variant.label,
275 address_type=variant.address_type.value,
276 ))
277 variant.key = self.nodes[1].dumpprivkey(variant.address["address"])
278 # Ensure output is large enough to pay for fees: conservatively assuming txsize of
279 # 500 vbytes and feerate of 20 sats/vbytes
280 variant.initial_amount = get_rand_amount(min_amount=((500 * 20 / COIN) + AMOUNT_DUST))
281 variant.initial_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.initial_amount)
282 variant.confirmation_height = 0
283 variant.timestamp = timestamp
284 285 # Mine a block so these parents are confirmed
286 assert_equal(len(self.nodes[0].getrawmempool()), len(mempool_variants))
287 self.sync_mempools()
288 block_to_disconnect = self.generate(self.nodes[0], 1)[0]
289 assert_equal(len(self.nodes[0].getrawmempool()), 0)
290 291 # For each variant, create an unconfirmed child transaction from initial_txid, sending all
292 # the funds to an unspendable address. Importantly, no change output is created so the
293 # transaction can't be recognized using its outputs. The wallet rescan needs to know the
294 # inputs of the transaction to detect it, so the parent must be processed before the child.
295 # An equivalent test for descriptors exists in wallet_rescan_unconfirmed.py.
296 unspent_txid_map = {txin["txid"] : txin for txin in self.nodes[1].listunspent()}
297 for variant in mempool_variants:
298 # Send full amount, subtracting fee from outputs, to ensure no change is created.
299 child = self.nodes[1].send(
300 add_to_wallet=False,
301 inputs=[unspent_txid_map[variant.initial_txid]],
302 outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}],
303 locktime=0,
304 subtract_fee_from_outputs=[0]
305 )
306 variant.child_txid = child["txid"]
307 variant.amount_received = 0
308 self.nodes[0].sendrawtransaction(child["hex"])
309 310 # Mempools should contain the child transactions for each variant.
311 assert_equal(len(self.nodes[0].getrawmempool()), len(mempool_variants))
312 self.sync_mempools()
313 314 # Mock a reorg so the parent transactions are added back to the mempool
315 for node in self.nodes:
316 node.invalidateblock(block_to_disconnect)
317 # Mempools should now contain the parent and child for each variant.
318 assert_equal(len(node.getrawmempool()), 2 * len(mempool_variants))
319 320 # For each variation of wallet key import, invoke the import RPC and
321 # check the results from getbalance and listtransactions.
322 for variant in mempool_variants:
323 self.log.info('Run import for mempool variant {}'.format(variant))
324 expect_rescan = variant.rescan == Rescan.yes
325 variant.node = self.nodes[2 + IMPORT_NODES.index(ImportNode(variant.prune, expect_rescan))]
326 variant.do_import(variant.timestamp)
327 if expect_rescan:
328 # Ensure both transactions were rescanned. This would raise a JSONRPCError if the
329 # transactions were not identified as belonging to the wallet.
330 assert_equal(variant.node.gettransaction(variant.initial_txid)['confirmations'], 0)
331 assert_equal(variant.node.gettransaction(variant.child_txid)['confirmations'], 0)
332 variant.amount_received = variant.initial_amount
333 variant.expected_txs = 1
334 variant.check(variant.initial_txid, variant.initial_amount, 0)
335 else:
336 variant.amount_received = 0
337 variant.expected_txs = 0
338 variant.check()
339 340 341 if __name__ == "__main__":
342 ImportRescanTest(__file__).main()
343