Skip to content

Commit 4e104e2

Browse files
committed
Merge bitcoin/bitcoin#28838: test: add assumeutxo wallet test
997b9a7 test: add assumeutxo wallet test (Sjors Provoost) Pull request description: Extracted from #28616, this adds a (very) basic wallet test for assume utxo. It checks some circumstances where a backup can and can't be loaded. ACKs for top commit: maflcko: lgtm ACK 997b9a7 achow101: ACK 997b9a7 theStack: Code-review ACK 997b9a7 Tree-SHA512: 69474e56c6a46bb4f30fc54f8e5844766ac2a5f8226bb0b168d11ae1e3d4eae58570c1f1b4cc2b2f6f51b5d0e055bbe2bbd11684265215e01d4eb81ab4b7b0bb
2 parents 014f525 + 997b9a7 commit 4e104e2

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@
343343
'feature_filelock.py',
344344
'feature_loadblock.py',
345345
'feature_assumeutxo.py',
346+
'wallet_assumeutxo.py --descriptors',
346347
'p2p_dos_header_tree.py',
347348
'p2p_add_connections.py',
348349
'feature_bind_port_discover.py',

test/functional/wallet_assumeutxo.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2023-present 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+
"""Test for assumeutxo wallet related behavior.
6+
See feature_assumeutxo.py for background.
7+
8+
## Possible test improvements
9+
10+
- TODO: test import descriptors while background sync is in progress
11+
- TODO: test loading a wallet (backup) on a pruned node
12+
13+
"""
14+
from test_framework.test_framework import BitcoinTestFramework
15+
from test_framework.util import (
16+
assert_equal,
17+
assert_raises_rpc_error,
18+
)
19+
20+
START_HEIGHT = 199
21+
SNAPSHOT_BASE_HEIGHT = 299
22+
FINAL_HEIGHT = 399
23+
24+
25+
class AssumeutxoTest(BitcoinTestFramework):
26+
def skip_test_if_missing_module(self):
27+
self.skip_if_no_wallet()
28+
29+
def add_options(self, parser):
30+
self.add_wallet_options(parser, legacy=False)
31+
32+
def set_test_params(self):
33+
"""Use the pregenerated, deterministic chain up to height 199."""
34+
self.num_nodes = 2
35+
self.rpc_timeout = 120
36+
self.extra_args = [
37+
[],
38+
[],
39+
]
40+
41+
def setup_network(self):
42+
"""Start with the nodes disconnected so that one can generate a snapshot
43+
including blocks the other hasn't yet seen."""
44+
self.add_nodes(2)
45+
self.start_nodes(extra_args=self.extra_args)
46+
47+
def run_test(self):
48+
"""
49+
Bring up two (disconnected) nodes, mine some new blocks on the first,
50+
and generate a UTXO snapshot.
51+
52+
Load the snapshot into the second, ensure it syncs to tip and completes
53+
background validation when connected to the first.
54+
"""
55+
n0 = self.nodes[0]
56+
n1 = self.nodes[1]
57+
58+
# Mock time for a deterministic chain
59+
for n in self.nodes:
60+
n.setmocktime(n.getblockheader(n.getbestblockhash())['time'])
61+
62+
self.sync_blocks()
63+
64+
n0.createwallet('w')
65+
w = n0.get_wallet_rpc("w")
66+
67+
# Generate a series of blocks that `n0` will have in the snapshot,
68+
# but that n1 doesn't yet see. In order for the snapshot to activate,
69+
# though, we have to ferry over the new headers to n1 so that it
70+
# isn't waiting forever to see the header of the snapshot's base block
71+
# while disconnected from n0.
72+
for _ in range(100):
73+
self.generate(n0, nblocks=1, sync_fun=self.no_op)
74+
newblock = n0.getblock(n0.getbestblockhash(), 0)
75+
76+
# make n1 aware of the new header, but don't give it the block.
77+
n1.submitheader(newblock)
78+
79+
# Ensure everyone is seeing the same headers.
80+
for n in self.nodes:
81+
assert_equal(n.getblockchaininfo()[
82+
"headers"], SNAPSHOT_BASE_HEIGHT)
83+
84+
w.backupwallet("backup_w.dat")
85+
86+
self.log.info("-- Testing assumeutxo")
87+
88+
assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT)
89+
assert_equal(n1.getblockcount(), START_HEIGHT)
90+
91+
self.log.info(
92+
f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
93+
dump_output = n0.dumptxoutset('utxos.dat')
94+
95+
assert_equal(
96+
dump_output['txoutset_hash'],
97+
'61d9c2b29a2571a5fe285fe2d8554f91f93309666fc9b8223ee96338de25ff53')
98+
assert_equal(dump_output['nchaintx'], 300)
99+
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
100+
101+
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
102+
# will allow us to test n1's sync-to-tip on top of a snapshot.
103+
self.generate(n0, nblocks=100, sync_fun=self.no_op)
104+
105+
assert_equal(n0.getblockcount(), FINAL_HEIGHT)
106+
assert_equal(n1.getblockcount(), START_HEIGHT)
107+
108+
assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)
109+
110+
self.log.info(
111+
f"Loading snapshot into second node from {dump_output['path']}")
112+
loaded = n1.loadtxoutset(dump_output['path'])
113+
assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
114+
assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
115+
116+
normal, snapshot = n1.getchainstates()["chainstates"]
117+
assert_equal(normal['blocks'], START_HEIGHT)
118+
assert_equal(normal.get('snapshot_blockhash'), None)
119+
assert_equal(normal['validated'], True)
120+
assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT)
121+
assert_equal(snapshot['snapshot_blockhash'], dump_output['base_hash'])
122+
assert_equal(snapshot['validated'], False)
123+
124+
assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
125+
126+
self.log.info("Backup can't be loaded during background sync")
127+
assert_raises_rpc_error(-4, "Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height 299", n1.restorewallet, "w", "backup_w.dat")
128+
129+
PAUSE_HEIGHT = FINAL_HEIGHT - 40
130+
131+
self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT)
132+
self.restart_node(1, extra_args=[
133+
f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]])
134+
135+
# Finally connect the nodes and let them sync.
136+
#
137+
# Set `wait_for_connect=False` to avoid a race between performing connection
138+
# assertions and the -stopatheight tripping.
139+
self.connect_nodes(0, 1, wait_for_connect=False)
140+
141+
n1.wait_until_stopped(timeout=5)
142+
143+
self.log.info(
144+
"Restarted node before snapshot validation completed, reloading...")
145+
self.restart_node(1, extra_args=self.extra_args[1])
146+
147+
# TODO: inspect state of e.g. the wallet before reconnecting
148+
self.connect_nodes(0, 1)
149+
150+
self.log.info(
151+
f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})")
152+
self.wait_until(lambda: n1.getchainstates()[
153+
'chainstates'][-1]['blocks'] == FINAL_HEIGHT)
154+
self.sync_blocks(nodes=(n0, n1))
155+
156+
self.log.info("Ensuring background validation completes")
157+
self.wait_until(lambda: len(n1.getchainstates()['chainstates']) == 1)
158+
159+
self.log.info("Ensuring wallet can be restored from backup")
160+
n1.restorewallet("w", "backup_w.dat")
161+
162+
163+
if __name__ == '__main__':
164+
AssumeutxoTest().main()

0 commit comments

Comments
 (0)