2
2
# Copyright (c) 2014-2016 The Bitcoin Core developers
3
3
# Distributed under the MIT software license, see the accompanying
4
4
# 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
6
21
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 )
8
23
from decimal import Decimal
9
24
10
25
import collections
11
26
import enum
12
27
import itertools
13
- import functools
14
28
15
29
Call = enum .Enum ("Call" , "single multi" )
16
30
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
54
104
55
105
56
106
class ImportRescanTest (BitcoinTestFramework ):
@@ -60,96 +110,75 @@ def __init__(self):
60
110
61
111
def setup_network (self ):
62
112
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
+
63
117
self .nodes = start_nodes (self .num_nodes , self .options .tmpdir , extra_args )
64
118
for i in range (1 , self .num_nodes ):
65
119
connect_nodes (self .nodes [i ], 0 )
66
120
67
121
def run_test (self ):
68
122
# Create one transaction on node 0 with a unique amount and label for
69
123
# 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).
81
133
self .nodes [0 ].generate (1 )
82
134
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 )
83
138
sync_blocks (self .nodes )
84
139
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.
117
157
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 )
129
161
162
+ # Generate a block containing the new transactions.
130
163
self .nodes [0 ].generate (1 )
131
164
assert_equal (self .nodes [0 ].getrawmempool (), [])
132
165
sync_blocks (self .nodes )
133
166
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
153
182
154
183
155
184
if __name__ == "__main__" :
0 commit comments