Skip to content

Commit 71d9a7c

Browse files
aureleoulesryanofskykouloumos
committed
test: Wallet imports on pruned nodes
Co-authored-by: Ryan Ofsky <[email protected]> Co-authored-by: Andreas Kouloumos <[email protected]>
1 parent e6906fc commit 71d9a7c

File tree

4 files changed

+167
-18
lines changed

4 files changed

+167
-18
lines changed

test/functional/feature_pruning.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
"""
1111
import os
1212

13-
from test_framework.blocktools import create_coinbase
14-
from test_framework.messages import CBlock
13+
from test_framework.blocktools import (
14+
create_block,
15+
create_coinbase,
16+
)
1517
from test_framework.script import (
1618
CScript,
1719
OP_NOP,
@@ -48,21 +50,7 @@ def mine_large_blocks(node, n):
4850
previousblockhash = int(best_block["hash"], 16)
4951

5052
for _ in range(n):
51-
# Build the coinbase transaction (with large scriptPubKey)
52-
coinbase_tx = create_coinbase(height)
53-
coinbase_tx.vin[0].nSequence = 2 ** 32 - 1
54-
coinbase_tx.vout[0].scriptPubKey = big_script
55-
coinbase_tx.rehash()
56-
57-
# Build the block
58-
block = CBlock()
59-
block.nVersion = best_block["version"]
60-
block.hashPrevBlock = previousblockhash
61-
block.nTime = mine_large_blocks.nTime
62-
block.nBits = int('207fffff', 16)
63-
block.nNonce = 0
64-
block.vtx = [coinbase_tx]
65-
block.hashMerkleRoot = block.calc_merkle_root()
53+
block = create_block(hashprev=previousblockhash, ntime=mine_large_blocks.nTime, coinbase=create_coinbase(height, script_pubkey=big_script))
6654
block.solve()
6755

6856
# Submit to the node

test/functional/test_framework/blocktools.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def script_BIP34_coinbase_height(height):
120120
return CScript([CScriptNum(height)])
121121

122122

123-
def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0, nValue=50):
123+
def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_script=None, fees=0, nValue=50):
124124
"""Create a coinbase transaction.
125125
126126
If pubkey is passed in, the coinbase output will be a P2PK output;
@@ -138,6 +138,8 @@ def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0, nValu
138138
coinbaseoutput.nValue += fees
139139
if pubkey is not None:
140140
coinbaseoutput.scriptPubKey = key_to_p2pk_script(pubkey)
141+
elif script_pubkey is not None:
142+
coinbaseoutput.scriptPubKey = script_pubkey
141143
else:
142144
coinbaseoutput.scriptPubKey = CScript([OP_TRUE])
143145
coinbase.vout = [coinbaseoutput]

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
'feature_pruning.py',
8686
'feature_dbcrash.py',
8787
'feature_index_prune.py',
88+
'wallet_pruning.py --legacy-wallet',
8889
]
8990

9091
BASE_SCRIPTS = [

test/functional/wallet_pruning.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
"""Test wallet import on pruned node."""
7+
import os
8+
9+
from test_framework.util import assert_equal, assert_raises_rpc_error
10+
from test_framework.blocktools import (
11+
COINBASE_MATURITY,
12+
create_block
13+
)
14+
from test_framework.blocktools import create_coinbase
15+
from test_framework.test_framework import BitcoinTestFramework
16+
17+
from test_framework.script import (
18+
CScript,
19+
OP_RETURN,
20+
OP_TRUE,
21+
)
22+
23+
class WalletPruningTest(BitcoinTestFramework):
24+
def add_options(self, parser):
25+
self.add_wallet_options(parser, descriptors=False)
26+
27+
def set_test_params(self):
28+
self.setup_clean_chain = True
29+
self.num_nodes = 2
30+
self.wallet_names = []
31+
self.extra_args = [
32+
[], # node dedicated to mining
33+
['-prune=550'], # node dedicated to testing pruning
34+
]
35+
36+
def skip_test_if_missing_module(self):
37+
self.skip_if_no_wallet()
38+
self.skip_if_no_bdb()
39+
40+
def mine_large_blocks(self, node, n):
41+
# Get the block parameters for the first block
42+
best_block = node.getblock(node.getbestblockhash())
43+
height = int(best_block["height"]) + 1
44+
self.nTime = max(self.nTime, int(best_block["time"])) + 1
45+
previousblockhash = int(best_block["hash"], 16)
46+
big_script = CScript([OP_RETURN] + [OP_TRUE] * 950000)
47+
for _ in range(n):
48+
block = create_block(hashprev=previousblockhash, ntime=self.nTime, coinbase=create_coinbase(height, script_pubkey=big_script))
49+
block.solve()
50+
51+
# Submit to the node
52+
node.submitblock(block.serialize().hex())
53+
54+
previousblockhash = block.sha256
55+
height += 1
56+
57+
# Simulate 10 minutes of work time per block
58+
# Important for matching a timestamp with a block +- some window
59+
self.nTime += 600
60+
for n in self.nodes:
61+
if n.running:
62+
n.setmocktime(self.nTime) # Update node's time to accept future blocks
63+
self.sync_all()
64+
65+
def test_wallet_import_pruned(self, wallet_name):
66+
self.log.info("Make sure we can import wallet when pruned and required blocks are still available")
67+
68+
wallet_file = wallet_name + ".dat"
69+
wallet_birthheight = self.get_birthheight(wallet_file)
70+
71+
# Verify that the block at wallet's birthheight is available at the pruned node
72+
self.nodes[1].getblock(self.nodes[1].getblockhash(wallet_birthheight))
73+
74+
# Import wallet into pruned node
75+
self.nodes[1].createwallet(wallet_name="wallet_pruned", descriptors=False, load_on_startup=True)
76+
self.nodes[1].importwallet(os.path.join(self.nodes[0].datadir, wallet_file))
77+
78+
# Make sure that prune node's wallet correctly accounts for balances
79+
assert_equal(self.nodes[1].getbalance(), self.nodes[0].getbalance())
80+
81+
self.log.info("- Done")
82+
83+
def test_wallet_import_pruned_with_missing_blocks(self, wallet_name):
84+
self.log.info("Make sure we cannot import wallet when pruned and required blocks are not available")
85+
86+
wallet_file = wallet_name + ".dat"
87+
wallet_birthheight = self.get_birthheight(wallet_file)
88+
89+
# Verify that the block at wallet's birthheight is not available at the pruned node
90+
assert_raises_rpc_error(-1, "Block not available (pruned data)", self.nodes[1].getblock, self.nodes[1].getblockhash(wallet_birthheight))
91+
92+
# Make sure wallet cannot be imported because of missing blocks
93+
# This will try to rescan blocks `TIMESTAMP_WINDOW` (2h) before the wallet birthheight.
94+
# There are 6 blocks an hour, so 11 blocks (excluding birthheight).
95+
assert_raises_rpc_error(-4, f"Pruned blocks from height {wallet_birthheight - 11} required to import keys. Use RPC call getblockchaininfo to determine your pruned height.", self.nodes[1].importwallet, os.path.join(self.nodes[0].datadir, wallet_file))
96+
self.log.info("- Done")
97+
98+
def get_birthheight(self, wallet_file):
99+
"""Gets birthheight of a wallet on node0"""
100+
with open(os.path.join(self.nodes[0].datadir, wallet_file), 'r', encoding="utf8") as f:
101+
for line in f:
102+
if line.startswith('# * Best block at time of backup'):
103+
wallet_birthheight = int(line.split(' ')[9])
104+
return wallet_birthheight
105+
106+
def has_block(self, block_index):
107+
"""Checks if the pruned node has the specific blk0000*.dat file"""
108+
return os.path.isfile(os.path.join(self.nodes[1].datadir, self.chain, "blocks", f"blk{block_index:05}.dat"))
109+
110+
def create_wallet(self, wallet_name, *, unload=False):
111+
"""Creates and dumps a wallet on the non-pruned node0 to be later import by the pruned node"""
112+
self.nodes[0].createwallet(wallet_name=wallet_name, descriptors=False, load_on_startup=True)
113+
self.nodes[0].dumpwallet(os.path.join(self.nodes[0].datadir, wallet_name + ".dat"))
114+
if (unload):
115+
self.nodes[0].unloadwallet(wallet_name)
116+
117+
def run_test(self):
118+
self.nTime = 0
119+
self.log.info("Warning! This test requires ~1.3GB of disk space")
120+
121+
self.log.info("Generating a long chain of blocks...")
122+
123+
# A blk*.dat file is 128MB
124+
# Generate 250 light blocks
125+
self.generate(self.nodes[0], 250, sync_fun=self.no_op)
126+
# Generate 50MB worth of large blocks in the blk00000.dat file
127+
self.mine_large_blocks(self.nodes[0], 50)
128+
129+
# Create a wallet which birth's block is in the blk00000.dat file
130+
wallet_birthheight_1 = "wallet_birthheight_1"
131+
assert_equal(self.has_block(1), False)
132+
self.create_wallet(wallet_birthheight_1, unload=True)
133+
134+
# Generate enough large blocks to reach pruning disk limit
135+
# Not pruning yet because we are still below PruneAfterHeight
136+
self.mine_large_blocks(self.nodes[0], 600)
137+
self.log.info("- Long chain created")
138+
139+
# Create a wallet with birth height > wallet_birthheight_1
140+
wallet_birthheight_2 = "wallet_birthheight_2"
141+
self.create_wallet(wallet_birthheight_2)
142+
143+
# Fund wallet to later verify that importwallet correctly accounts for balances
144+
self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
145+
146+
# We've reached pruning storage & height limit but
147+
# pruning doesn't run until another chunk (blk*.dat file) is allocated.
148+
# That's why we are generating another 5 large blocks
149+
self.mine_large_blocks(self.nodes[0], 5)
150+
151+
# blk00000.dat file is now pruned from node1
152+
assert_equal(self.has_block(0), False)
153+
154+
self.test_wallet_import_pruned(wallet_birthheight_2)
155+
self.test_wallet_import_pruned_with_missing_blocks(wallet_birthheight_1)
156+
157+
if __name__ == '__main__':
158+
WalletPruningTest().main()

0 commit comments

Comments
 (0)