#!/usr/bin/env python3
# Copyright (c) 2018-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
""" Test transaction time during old block rescanning
"""
import concurrent . futures
import time
from test_framework . authproxy import JSONRPCException
from test_framework . blocktools import COINBASE_MATURITY
from test_framework . test_framework import BitcoinTestFramework
from test_framework . util import (
assert_equal ,
assert_raises_rpc_error ,
set_node_times ,
)
from test_framework . wallet_util import (
get_generate_key ,
)
class TransactionTimeRescanTest ( BitcoinTestFramework ) :
def add_options ( self , parser ) :
self . add_wallet_options ( parser )
def set_test_params ( self ) :
self . setup_clean_chain = False
self . num_nodes = 3
self . extra_args = [ [ " -keypool=400 " ] ,
[ " -keypool=400 " ] ,
[ ]
]
def skip_test_if_missing_module ( self ) :
self . skip_if_no_wallet ( )
def run_test ( self ) :
self . log . info ( ' Prepare nodes and wallet ' )
minernode = self . nodes [ 0 ] # node used to mine BTC and create transactions
usernode = self . nodes [ 1 ] # user node with correct time
restorenode = self . nodes [ 2 ] # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp)
# time constant
cur_time = int ( time . time ( ) )
ten_days = 10 * 24 * 60 * 60
# synchronize nodes and time
self . sync_all ( )
set_node_times ( self . nodes , cur_time )
# prepare miner wallet
minernode . createwallet ( wallet_name = ' default ' )
miner_wallet = minernode . get_wallet_rpc ( ' default ' )
m1 = miner_wallet . getnewaddress ( )
# prepare the user wallet with 3 watch only addresses
wo1 = usernode . getnewaddress ( )
wo2 = usernode . getnewaddress ( )
wo3 = usernode . getnewaddress ( )
usernode . createwallet ( wallet_name = ' wo ' , disable_private_keys = True )
wo_wallet = usernode . get_wallet_rpc ( ' wo ' )
wo_wallet . importaddress ( wo1 )
wo_wallet . importaddress ( wo2 )
wo_wallet . importaddress ( wo3 )
self . log . info ( ' Start transactions ' )
# check blockcount
assert_equal ( minernode . getblockcount ( ) , 200 )
# generate some btc to create transactions and check blockcount
initial_mine = COINBASE_MATURITY + 1
self . generatetoaddress ( minernode , initial_mine , m1 )
assert_equal ( minernode . getblockcount ( ) , initial_mine + 200 )
# synchronize nodes and time
self . sync_all ( )
set_node_times ( self . nodes , cur_time + ten_days )
# send 10 btc to user's first watch-only address
self . log . info ( ' Send 10 btc to user ' )
miner_wallet . sendtoaddress ( wo1 , 10 )
# generate blocks and check blockcount
self . generatetoaddress ( minernode , COINBASE_MATURITY , m1 )
assert_equal ( minernode . getblockcount ( ) , initial_mine + 300 )
# synchronize nodes and time
self . sync_all ( )
set_node_times ( self . nodes , cur_time + ten_days + ten_days )
# send 5 btc to our second watch-only address
self . log . info ( ' Send 5 btc to user ' )
miner_wallet . sendtoaddress ( wo2 , 5 )
# generate blocks and check blockcount
self . generatetoaddress ( minernode , COINBASE_MATURITY , m1 )
assert_equal ( minernode . getblockcount ( ) , initial_mine + 400 )
# synchronize nodes and time
self . sync_all ( )
set_node_times ( self . nodes , cur_time + ten_days + ten_days + ten_days )
# send 1 btc to our third watch-only address
self . log . info ( ' Send 1 btc to user ' )
miner_wallet . sendtoaddress ( wo3 , 1 )
# generate more blocks and check blockcount
self . generatetoaddress ( minernode , COINBASE_MATURITY , m1 )
assert_equal ( minernode . getblockcount ( ) , initial_mine + 500 )
self . log . info ( ' Check user \' s final balance and transaction count ' )
assert_equal ( wo_wallet . getbalance ( ) , 16 )
assert_equal ( len ( wo_wallet . listtransactions ( ) ) , 3 )
self . log . info ( ' Check transaction times ' )
for tx in wo_wallet . listtransactions ( ) :
if tx [ ' address ' ] == wo1 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days )
elif tx [ ' address ' ] == wo2 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days + ten_days )
elif tx [ ' address ' ] == wo3 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days + ten_days + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days + ten_days + ten_days )
# restore user wallet without rescan
self . log . info ( ' Restore user wallet on another node without rescan ' )
restorenode . createwallet ( wallet_name = ' wo ' , disable_private_keys = True )
restorewo_wallet = restorenode . get_wallet_rpc ( ' wo ' )
# for descriptor wallets, the test framework maps the importaddress RPC to the
# importdescriptors RPC (with argument 'timestamp'='now'), which always rescans
# blocks of the past 2 hours, based on the current MTP timestamp; in order to avoid
# importing the last address (wo3), we advance the time further and generate 10 blocks
if self . options . descriptors :
set_node_times ( self . nodes , cur_time + ten_days + ten_days + ten_days + ten_days )
self . generatetoaddress ( minernode , 10 , m1 )
restorewo_wallet . importaddress ( wo1 , rescan = False )
restorewo_wallet . importaddress ( wo2 , rescan = False )
restorewo_wallet . importaddress ( wo3 , rescan = False )
# check user has 0 balance and no transactions
assert_equal ( restorewo_wallet . getbalance ( ) , 0 )
assert_equal ( len ( restorewo_wallet . listtransactions ( ) ) , 0 )
# proceed to rescan, first with an incomplete one, then with a full rescan
self . log . info ( ' Rescan last history part ' )
restorewo_wallet . rescanblockchain ( initial_mine + 350 )
self . log . info ( ' Rescan all history ' )
restorewo_wallet . rescanblockchain ( )
self . log . info ( ' Check user \' s final balance and transaction count after restoration ' )
assert_equal ( restorewo_wallet . getbalance ( ) , 16 )
assert_equal ( len ( restorewo_wallet . listtransactions ( ) ) , 3 )
self . log . info ( ' Check transaction times after restoration ' )
for tx in restorewo_wallet . listtransactions ( ) :
if tx [ ' address ' ] == wo1 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days )
elif tx [ ' address ' ] == wo2 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days + ten_days )
elif tx [ ' address ' ] == wo3 :
assert_equal ( tx [ ' blocktime ' ] , cur_time + ten_days + ten_days + ten_days )
assert_equal ( tx [ ' time ' ] , cur_time + ten_days + ten_days + ten_days )
self . log . info ( ' Test handling of invalid parameters for rescanblockchain ' )
assert_raises_rpc_error ( - 8 , " Invalid start_height " , restorewo_wallet . rescanblockchain , - 1 , 10 )
assert_raises_rpc_error ( - 8 , " Invalid stop_height " , restorewo_wallet . rescanblockchain , 1 , - 1 )
assert_raises_rpc_error ( - 8 , " stop_height must be greater than start_height " , restorewo_wallet . rescanblockchain , 20 , 10 )
self . log . info ( " Test `rescanblockchain` fails when wallet is encrypted and locked " )
usernode . createwallet ( wallet_name = " enc_wallet " , passphrase = " passphrase " )
enc_wallet = usernode . get_wallet_rpc ( " enc_wallet " )
assert_raises_rpc_error ( - 13 , " Error: Please enter the wallet passphrase with walletpassphrase first. " , enc_wallet . rescanblockchain )
if not self . options . descriptors :
self . log . info ( " Test rescanning an encrypted wallet " )
hd_seed = get_generate_key ( ) . privkey
usernode . createwallet ( wallet_name = " temp_wallet " , blank = True , descriptors = False )
temp_wallet = usernode . get_wallet_rpc ( " temp_wallet " )
temp_wallet . sethdseed ( seed = hd_seed )
for i in range ( 399 ) :
temp_wallet . getnewaddress ( )
self . generatetoaddress ( usernode , COINBASE_MATURITY + 1 , temp_wallet . getnewaddress ( ) )
self . generatetoaddress ( usernode , COINBASE_MATURITY + 1 , temp_wallet . getnewaddress ( ) )
minernode . createwallet ( " encrypted_wallet " , blank = True , passphrase = " passphrase " , descriptors = False )
encrypted_wallet = minernode . get_wallet_rpc ( " encrypted_wallet " )
encrypted_wallet . walletpassphrase ( " passphrase " , 99999 )
encrypted_wallet . sethdseed ( seed = hd_seed )
with concurrent . futures . ThreadPoolExecutor ( max_workers = 1 ) as thread :
with minernode . assert_debug_log ( expected_msgs = [ " Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks) " ] , timeout = 5 ) :
rescanning = thread . submit ( encrypted_wallet . rescanblockchain )
# set the passphrase timeout to 1 to test that the wallet remains unlocked during the rescan
minernode . cli ( " -rpcwallet=encrypted_wallet " ) . walletpassphrase ( " passphrase " , 1 )
try :
minernode . cli ( " -rpcwallet=encrypted_wallet " ) . walletlock ( )
except JSONRPCException as e :
assert e . error [ " code " ] == - 4 and " Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet. " in e . error [ " message " ]
try :
minernode . cli ( " -rpcwallet=encrypted_wallet " ) . walletpassphrasechange ( " passphrase " , " newpassphrase " )
except JSONRPCException as e :
assert e . error [ " code " ] == - 4 and " Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase. " in e . error [ " message " ]
assert_equal ( rescanning . result ( ) , { " start_height " : 0 , " stop_height " : 803 } )
assert_equal ( encrypted_wallet . getbalance ( ) , temp_wallet . getbalance ( ) )
if __name__ == ' __main__ ' :
TransactionTimeRescanTest ( __file__ ) . main ( )