mirror of https://github.com/bitcoin/bitcoin
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
10 KiB
227 lines
10 KiB
#!/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()
|