Skip to content

Commit 9828f9a

Browse files
committed
Merge #9761: Use 2 hour grace period for key timestamps in importmulti rescans
e662af3 Use 2 hour grace period for key timestamps in importmulti rescans (Russell Yanofsky) 38d3e9e [qa] Extend import-rescan.py to test imports on pruned nodes. (Russell Yanofsky) c28583d [qa] Extend import-rescan.py to test specific key timestamps (Russell Yanofsky) 8be0866 [qa] Simplify import-rescan.py (Russell Yanofsky)
2 parents ad168ef + e662af3 commit 9828f9a

File tree

2 files changed

+145
-115
lines changed

2 files changed

+145
-115
lines changed

qa/rpc-tests/import-rescan.py

Lines changed: 142 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,105 @@
22
# Copyright (c) 2014-2016 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5-
5+
"""Test rescan behavior of importaddress, importpubkey, importprivkey, and
6+
importmulti RPCs with different types of keys and rescan options.
7+
8+
In the first part of the test, node 0 creates an address for each type of
9+
import RPC call and sends BTC to it. Then other nodes import the addresses,
10+
and the test makes listtransactions and getbalance calls to confirm that the
11+
importing node either did or did not execute rescans picking up the send
12+
transactions.
13+
14+
In the second part of the test, node 0 sends more BTC to each address, and the
15+
test makes more listtransactions and getbalance calls to confirm that the
16+
importing nodes pick up the new transactions regardless of whether rescans
17+
happened previously.
18+
"""
19+
20+
from test_framework.authproxy import JSONRPCException
621
from test_framework.test_framework import BitcoinTestFramework
7-
from test_framework.util import (start_nodes, connect_nodes, sync_blocks, assert_equal)
22+
from test_framework.util import (start_nodes, connect_nodes, sync_blocks, assert_equal, set_node_times)
823
from decimal import Decimal
924

1025
import collections
1126
import enum
1227
import itertools
13-
import functools
1428

1529
Call = enum.Enum("Call", "single multi")
1630
Data = enum.Enum("Data", "address pub priv")
17-
ImportNode = collections.namedtuple("ImportNode", "rescan")
18-
19-
20-
def call_import_rpc(call, data, address, scriptPubKey, pubkey, key, label, node, rescan):
21-
"""Helper that calls a wallet import RPC on a bitcoin node."""
22-
watchonly = data != Data.priv
23-
if call == Call.single:
24-
if data == Data.address:
25-
response = node.importaddress(address, label, rescan)
26-
elif data == Data.pub:
27-
response = node.importpubkey(pubkey, label, rescan)
28-
elif data == Data.priv:
29-
response = node.importprivkey(key, label, rescan)
30-
assert_equal(response, None)
31-
elif call == Call.multi:
32-
response = node.importmulti([{
33-
"scriptPubKey": {
34-
"address": address
35-
},
36-
"timestamp": "now",
37-
"pubkeys": [pubkey] if data == Data.pub else [],
38-
"keys": [key] if data == Data.priv else [],
39-
"label": label,
40-
"watchonly": watchonly
41-
}], {"rescan": rescan})
42-
assert_equal(response, [{"success": True}])
43-
return watchonly
44-
45-
46-
# List of RPCs that import a wallet key or address in various ways.
47-
IMPORT_RPCS = [functools.partial(call_import_rpc, call, data) for call, data in itertools.product(Call, Data)]
48-
49-
# List of bitcoind nodes that will import keys.
50-
IMPORT_NODES = [
51-
ImportNode(rescan=True),
52-
ImportNode(rescan=False),
53-
]
31+
Rescan = enum.Enum("Rescan", "no yes late_timestamp")
32+
33+
34+
class Variant(collections.namedtuple("Variant", "call data rescan prune")):
35+
"""Helper for importing one key and verifying scanned transactions."""
36+
37+
def do_import(self, timestamp):
38+
"""Call one key import RPC."""
39+
40+
if self.call == Call.single:
41+
if self.data == Data.address:
42+
response, error = try_rpc(self.node.importaddress, self.address["address"], self.label,
43+
self.rescan == Rescan.yes)
44+
elif self.data == Data.pub:
45+
response, error = try_rpc(self.node.importpubkey, self.address["pubkey"], self.label,
46+
self.rescan == Rescan.yes)
47+
elif self.data == Data.priv:
48+
response, error = try_rpc(self.node.importprivkey, self.key, self.label, self.rescan == Rescan.yes)
49+
assert_equal(response, None)
50+
assert_equal(error, {'message': 'Rescan is disabled in pruned mode',
51+
'code': -4} if self.expect_disabled else None)
52+
elif self.call == Call.multi:
53+
response = self.node.importmulti([{
54+
"scriptPubKey": {
55+
"address": self.address["address"]
56+
},
57+
"timestamp": timestamp + RESCAN_WINDOW + (1 if self.rescan == Rescan.late_timestamp else 0),
58+
"pubkeys": [self.address["pubkey"]] if self.data == Data.pub else [],
59+
"keys": [self.key] if self.data == Data.priv else [],
60+
"label": self.label,
61+
"watchonly": self.data != Data.priv
62+
}], {"rescan": self.rescan in (Rescan.yes, Rescan.late_timestamp)})
63+
assert_equal(response, [{"success": True}])
64+
65+
def check(self, txid=None, amount=None, confirmations=None):
66+
"""Verify that getbalance/listtransactions return expected values."""
67+
68+
balance = self.node.getbalance(self.label, 0, True)
69+
assert_equal(balance, self.expected_balance)
70+
71+
txs = self.node.listtransactions(self.label, 10000, 0, True)
72+
assert_equal(len(txs), self.expected_txs)
73+
74+
if txid is not None:
75+
tx, = [tx for tx in txs if tx["txid"] == txid]
76+
assert_equal(tx["account"], self.label)
77+
assert_equal(tx["address"], self.address["address"])
78+
assert_equal(tx["amount"], amount)
79+
assert_equal(tx["category"], "receive")
80+
assert_equal(tx["label"], self.label)
81+
assert_equal(tx["txid"], txid)
82+
assert_equal(tx["confirmations"], confirmations)
83+
assert_equal("trusted" not in tx, True)
84+
if self.data != Data.priv:
85+
assert_equal(tx["involvesWatchonly"], True)
86+
else:
87+
assert_equal("involvesWatchonly" not in tx, True)
88+
89+
90+
# List of Variants for each way a key or address could be imported.
91+
IMPORT_VARIANTS = [Variant(*variants) for variants in itertools.product(Call, Data, Rescan, (False, True))]
92+
93+
# List of nodes to import keys to. Half the nodes will have pruning disabled,
94+
# half will have it enabled. Different nodes will be used for imports that are
95+
# expected to cause rescans, and imports that are not expected to cause
96+
# rescans, in order to prevent rescans during later imports picking up
97+
# transactions associated with earlier imports. This makes it easier to keep
98+
# track of expected balances and transactions.
99+
ImportNode = collections.namedtuple("ImportNode", "prune rescan")
100+
IMPORT_NODES = [ImportNode(*fields) for fields in itertools.product((False, True), repeat=2)]
101+
102+
# Rescans start at the earliest block up to 2 hours before the key timestamp.
103+
RESCAN_WINDOW = 2 * 60 * 60
54104

55105

56106
class ImportRescanTest(BitcoinTestFramework):
@@ -60,96 +110,75 @@ def __init__(self):
60110

61111
def setup_network(self):
62112
extra_args = [["-debug=1"] for _ in range(self.num_nodes)]
113+
for i, import_node in enumerate(IMPORT_NODES, 1):
114+
if import_node.prune:
115+
extra_args[i] += ["-prune=1"]
116+
63117
self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args)
64118
for i in range(1, self.num_nodes):
65119
connect_nodes(self.nodes[i], 0)
66120

67121
def run_test(self):
68122
# Create one transaction on node 0 with a unique amount and label for
69123
# each possible type of wallet import RPC.
70-
import_rpc_variants = []
71-
for i, import_rpc in enumerate(IMPORT_RPCS):
72-
label = "label{}".format(i)
73-
addr = self.nodes[0].validateaddress(self.nodes[0].getnewaddress(label))
74-
key = self.nodes[0].dumpprivkey(addr["address"])
75-
amount = 24.9375 - i * .0625
76-
txid = self.nodes[0].sendtoaddress(addr["address"], amount)
77-
import_rpc = functools.partial(import_rpc, addr["address"], addr["scriptPubKey"], addr["pubkey"], key,
78-
label)
79-
import_rpc_variants.append((import_rpc, label, amount, txid, addr))
80-
124+
for i, variant in enumerate(IMPORT_VARIANTS):
125+
variant.label = "label {} {}".format(i, variant)
126+
variant.address = self.nodes[0].validateaddress(self.nodes[0].getnewaddress(variant.label))
127+
variant.key = self.nodes[0].dumpprivkey(variant.address["address"])
128+
variant.initial_amount = 25 - (i + 1) / 4.0
129+
variant.initial_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.initial_amount)
130+
131+
# Generate a block containing the initial transactions, then another
132+
# block further in the future (past the rescan window).
81133
self.nodes[0].generate(1)
82134
assert_equal(self.nodes[0].getrawmempool(), [])
135+
timestamp = self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"]
136+
set_node_times(self.nodes, timestamp + RESCAN_WINDOW + 1)
137+
self.nodes[0].generate(1)
83138
sync_blocks(self.nodes)
84139

85-
# For each importing node and variation of wallet import RPC, invoke
86-
# the RPC and check the results from getbalance and listtransactions.
87-
for node, import_node in zip(self.nodes[1:], IMPORT_NODES):
88-
for import_rpc, label, amount, txid, addr in import_rpc_variants:
89-
watchonly = import_rpc(node, import_node.rescan)
90-
91-
balance = node.getbalance(label, 0, True)
92-
if import_node.rescan:
93-
assert_equal(balance, amount)
94-
else:
95-
assert_equal(balance, 0)
96-
97-
txs = node.listtransactions(label, 10000, 0, True)
98-
if import_node.rescan:
99-
assert_equal(len(txs), 1)
100-
assert_equal(txs[0]["account"], label)
101-
assert_equal(txs[0]["address"], addr["address"])
102-
assert_equal(txs[0]["amount"], amount)
103-
assert_equal(txs[0]["category"], "receive")
104-
assert_equal(txs[0]["label"], label)
105-
assert_equal(txs[0]["txid"], txid)
106-
assert_equal(txs[0]["confirmations"], 1)
107-
assert_equal("trusted" not in txs[0], True)
108-
if watchonly:
109-
assert_equal(txs[0]["involvesWatchonly"], True)
110-
else:
111-
assert_equal("involvesWatchonly" not in txs[0], True)
112-
else:
113-
assert_equal(len(txs), 0)
114-
115-
# Create spends for all the imported addresses.
116-
spend_txids = []
140+
# For each variation of wallet key import, invoke the import RPC and
141+
# check the results from getbalance and listtransactions.
142+
for variant in IMPORT_VARIANTS:
143+
variant.expect_disabled = variant.rescan == Rescan.yes and variant.prune and variant.call == Call.single
144+
expect_rescan = variant.rescan == Rescan.yes and not variant.expect_disabled
145+
variant.node = self.nodes[1 + IMPORT_NODES.index(ImportNode(variant.prune, expect_rescan))]
146+
variant.do_import(timestamp)
147+
if expect_rescan:
148+
variant.expected_balance = variant.initial_amount
149+
variant.expected_txs = 1
150+
variant.check(variant.initial_txid, variant.initial_amount, 2)
151+
else:
152+
variant.expected_balance = 0
153+
variant.expected_txs = 0
154+
variant.check()
155+
156+
# Create new transactions sending to each address.
117157
fee = self.nodes[0].getnetworkinfo()["relayfee"]
118-
for import_rpc, label, amount, txid, addr in import_rpc_variants:
119-
raw_tx = self.nodes[0].getrawtransaction(txid)
120-
decoded_tx = self.nodes[0].decoderawtransaction(raw_tx)
121-
input_vout = next(out["n"] for out in decoded_tx["vout"]
122-
if out["scriptPubKey"]["addresses"] == [addr["address"]])
123-
inputs = [{"txid": txid, "vout": input_vout}]
124-
outputs = {self.nodes[0].getnewaddress(): Decimal(amount) - fee}
125-
raw_spend_tx = self.nodes[0].createrawtransaction(inputs, outputs)
126-
signed_spend_tx = self.nodes[0].signrawtransaction(raw_spend_tx)
127-
spend_txid = self.nodes[0].sendrawtransaction(signed_spend_tx["hex"])
128-
spend_txids.append(spend_txid)
158+
for i, variant in enumerate(IMPORT_VARIANTS):
159+
variant.sent_amount = 25 - (2 * i + 1) / 8.0
160+
variant.sent_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.sent_amount)
129161

162+
# Generate a block containing the new transactions.
130163
self.nodes[0].generate(1)
131164
assert_equal(self.nodes[0].getrawmempool(), [])
132165
sync_blocks(self.nodes)
133166

134-
# Check the results from getbalance and listtransactions after the spends.
135-
for node, import_node in zip(self.nodes[1:], IMPORT_NODES):
136-
txs = node.listtransactions("*", 10000, 0, True)
137-
for (import_rpc, label, amount, txid, addr), spend_txid in zip(import_rpc_variants, spend_txids):
138-
balance = node.getbalance(label, 0, True)
139-
spend_tx = [tx for tx in txs if tx["txid"] == spend_txid]
140-
if import_node.rescan:
141-
assert_equal(balance, amount)
142-
assert_equal(len(spend_tx), 1)
143-
assert_equal(spend_tx[0]["account"], "")
144-
assert_equal(spend_tx[0]["amount"] + spend_tx[0]["fee"], -amount)
145-
assert_equal(spend_tx[0]["category"], "send")
146-
assert_equal("label" not in spend_tx[0], True)
147-
assert_equal(spend_tx[0]["confirmations"], 1)
148-
assert_equal("trusted" not in spend_tx[0], True)
149-
assert_equal("involvesWatchonly" not in txs[0], True)
150-
else:
151-
assert_equal(balance, 0)
152-
assert_equal(spend_tx, [])
167+
# Check the latest results from getbalance and listtransactions.
168+
for variant in IMPORT_VARIANTS:
169+
if not variant.expect_disabled:
170+
variant.expected_balance += variant.sent_amount
171+
variant.expected_txs += 1
172+
variant.check(variant.sent_txid, variant.sent_amount, 1)
173+
else:
174+
variant.check()
175+
176+
177+
def try_rpc(func, *args, **kwargs):
178+
try:
179+
return func(*args, **kwargs), None
180+
except JSONRPCException as e:
181+
return None, e.error
153182

154183

155184
if __name__ == "__main__":

src/wallet/rpcdump.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,8 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
988988
" or the string \"now\" to substitute the current synced blockchain time. The timestamp of the oldest\n"
989989
" key will determine how far back blockchain rescans need to begin for missing wallet transactions.\n"
990990
" \"now\" can be specified to bypass scanning, for keys which are known to never have been used, and\n"
991-
" 0 can be specified to scan the entire blockchain.\n"
991+
" 0 can be specified to scan the entire blockchain. Blocks up to 2 hours before the earliest key\n"
992+
" creation time of all keys being imported by the importmulti call will be scanned.\n"
992993
" \"redeemscript\": \"<script>\" , (string, optional) Allowed only if the scriptPubKey is a P2SH address or a P2SH scriptPubKey\n"
993994
" \"pubkeys\": [\"<pubKey>\", ... ] , (array, optional) Array of strings giving pubkeys that must occur in the output or redeemscript\n"
994995
" \"keys\": [\"<key>\", ... ] , (array, optional) Array of strings giving private keys whose corresponding public keys must occur in the output or redeemscript\n"
@@ -1072,7 +1073,7 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
10721073
}
10731074

10741075
if (fRescan && fRunScan && requests.size() && nLowestTimestamp <= chainActive.Tip()->GetBlockTimeMax()) {
1075-
CBlockIndex* pindex = nLowestTimestamp > minimumTimestamp ? chainActive.FindEarliestAtLeast(nLowestTimestamp) : chainActive.Genesis();
1076+
CBlockIndex* pindex = nLowestTimestamp > minimumTimestamp ? chainActive.FindEarliestAtLeast(std::max<int64_t>(nLowestTimestamp - 7200, 0)) : chainActive.Genesis();
10761077

10771078
if (pindex) {
10781079
pwalletMain->ScanForWalletTransactions(pindex, true);

0 commit comments

Comments
 (0)