Skip to content

Commit 42cae39

Browse files
committed
test: add feature_assumeutxo functional test
Most ideas for test improvements (TODOs) provided by Russ Yanofsky.
1 parent 0f64bac commit 42cae39

File tree

4 files changed

+258
-0
lines changed

4 files changed

+258
-0
lines changed

src/kernel/chainparams.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,13 @@ class CRegTestParams : public CChainParams
484484
.nChainTx = 110,
485485
.blockhash = uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c")
486486
},
487+
{
488+
// For use by test/functional/feature_assumeutxo.py
489+
.height = 299,
490+
.hash_serialized = AssumeutxoHash{uint256S("0xef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0")},
491+
.nChainTx = 300,
492+
.blockhash = uint256S("0x7e0517ef3ea6ecbed9117858e42eedc8eb39e8698a38dcbd1b3962a283233f4c")
493+
},
487494
};
488495

489496
chainTxData = ChainTxData{

test/functional/feature_assumeutxo.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2021 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, a means of quickly bootstrapping a node using
6+
a serialized version of the UTXO set at a certain height, which corresponds
7+
to a hash that has been compiled into bitcoind.
8+
9+
The assumeutxo value generated and used here is committed to in
10+
`CRegTestParams::m_assumeutxo_data` in `src/chainparams.cpp`.
11+
12+
## Possible test improvements
13+
14+
- TODO: test submitting a transaction and verifying it appears in mempool
15+
- TODO: test what happens with -reindex and -reindex-chainstate before the
16+
snapshot is validated, and make sure it's deleted successfully.
17+
18+
Interesting test cases could be loading an assumeutxo snapshot file with:
19+
20+
- TODO: An invalid hash
21+
- TODO: Valid hash but invalid snapshot file (bad coin height or truncated file or
22+
bad other serialization)
23+
- TODO: Valid snapshot file, but referencing an unknown block
24+
- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be
25+
invalid, or has an invalid parent
26+
- TODO: Valid snapshot file and snapshot block, but the block is not on the
27+
most-work chain
28+
29+
Interesting starting states could be loading a snapshot when the current chain tip is:
30+
31+
- TODO: An ancestor of snapshot block
32+
- TODO: Not an ancestor of the snapshot block but has less work
33+
- TODO: The snapshot block
34+
- TODO: A descendant of the snapshot block
35+
- TODO: Not an ancestor or a descendant of the snapshot block and has more work
36+
37+
"""
38+
from test_framework.test_framework import BitcoinTestFramework
39+
from test_framework.util import assert_equal, wait_until_helper
40+
41+
START_HEIGHT = 199
42+
SNAPSHOT_BASE_HEIGHT = 299
43+
FINAL_HEIGHT = 399
44+
COMPLETE_IDX = {'synced': True, 'best_block_height': FINAL_HEIGHT}
45+
46+
47+
class AssumeutxoTest(BitcoinTestFramework):
48+
49+
def set_test_params(self):
50+
"""Use the pregenerated, deterministic chain up to height 199."""
51+
self.num_nodes = 3
52+
self.rpc_timeout = 120
53+
self.extra_args = [
54+
[],
55+
["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"],
56+
["-txindex=1", "-blockfilterindex=1", "-coinstatsindex=1"],
57+
]
58+
59+
def setup_network(self):
60+
"""Start with the nodes disconnected so that one can generate a snapshot
61+
including blocks the other hasn't yet seen."""
62+
self.add_nodes(3)
63+
self.start_nodes(extra_args=self.extra_args)
64+
65+
def run_test(self):
66+
"""
67+
Bring up two (disconnected) nodes, mine some new blocks on the first,
68+
and generate a UTXO snapshot.
69+
70+
Load the snapshot into the second, ensure it syncs to tip and completes
71+
background validation when connected to the first.
72+
"""
73+
n0 = self.nodes[0]
74+
n1 = self.nodes[1]
75+
n2 = self.nodes[2]
76+
77+
# Mock time for a deterministic chain
78+
for n in self.nodes:
79+
n.setmocktime(n.getblockheader(n.getbestblockhash())['time'])
80+
81+
self.sync_blocks()
82+
83+
def no_sync():
84+
pass
85+
86+
# Generate a series of blocks that `n0` will have in the snapshot,
87+
# but that n1 doesn't yet see. In order for the snapshot to activate,
88+
# though, we have to ferry over the new headers to n1 so that it
89+
# isn't waiting forever to see the header of the snapshot's base block
90+
# while disconnected from n0.
91+
for i in range(100):
92+
self.generate(n0, nblocks=1, sync_fun=no_sync)
93+
newblock = n0.getblock(n0.getbestblockhash(), 0)
94+
95+
# make n1 aware of the new header, but don't give it the block.
96+
n1.submitheader(newblock)
97+
n2.submitheader(newblock)
98+
99+
# Ensure everyone is seeing the same headers.
100+
for n in self.nodes:
101+
assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT)
102+
103+
self.log.info("-- Testing assumeutxo + some indexes + pruning")
104+
105+
assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT)
106+
assert_equal(n1.getblockcount(), START_HEIGHT)
107+
108+
self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
109+
dump_output = n0.dumptxoutset('utxos.dat')
110+
111+
assert_equal(
112+
dump_output['txoutset_hash'],
113+
'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0')
114+
assert_equal(dump_output['nchaintx'], 300)
115+
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
116+
117+
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
118+
# will allow us to test n1's sync-to-tip on top of a snapshot.
119+
self.generate(n0, nblocks=100, sync_fun=no_sync)
120+
121+
assert_equal(n0.getblockcount(), FINAL_HEIGHT)
122+
assert_equal(n1.getblockcount(), START_HEIGHT)
123+
124+
assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)
125+
126+
self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
127+
loaded = n1.loadtxoutset(dump_output['path'])
128+
assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
129+
assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
130+
131+
monitor = n1.getchainstates()
132+
assert_equal(monitor['normal']['blocks'], START_HEIGHT)
133+
assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT)
134+
assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash'])
135+
136+
assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
137+
138+
PAUSE_HEIGHT = FINAL_HEIGHT - 40
139+
140+
self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT)
141+
self.restart_node(1, extra_args=[
142+
f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]])
143+
144+
# Finally connect the nodes and let them sync.
145+
self.connect_nodes(0, 1)
146+
147+
n1.wait_until_stopped(timeout=5)
148+
149+
self.log.info("Checking that blocks are segmented on disk")
150+
assert self.has_blockfile(n1, "00000"), "normal blockfile missing"
151+
assert self.has_blockfile(n1, "00001"), "assumed blockfile missing"
152+
assert not self.has_blockfile(n1, "00002"), "too many blockfiles"
153+
154+
self.log.info("Restarted node before snapshot validation completed, reloading...")
155+
self.restart_node(1, extra_args=self.extra_args[1])
156+
self.connect_nodes(0, 1)
157+
158+
self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})")
159+
wait_until_helper(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT)
160+
self.sync_blocks(nodes=(n0, n1))
161+
162+
self.log.info("Ensuring background validation completes")
163+
# N.B.: the `snapshot` key disappears once the background validation is complete.
164+
wait_until_helper(lambda: not n1.getchainstates().get('snapshot'))
165+
166+
# Ensure indexes have synced.
167+
completed_idx_state = {
168+
'basic block filter index': COMPLETE_IDX,
169+
'coinstatsindex': COMPLETE_IDX,
170+
}
171+
self.wait_until(lambda: n1.getindexinfo() == completed_idx_state)
172+
173+
174+
for i in (0, 1):
175+
n = self.nodes[i]
176+
self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes")
177+
self.restart_node(i, extra_args=self.extra_args[i])
178+
179+
assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT)
180+
181+
assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT)
182+
assert_equal(n.getchainstates().get('snapshot'), None)
183+
184+
if i != 0:
185+
# Ensure indexes have synced for the assumeutxo node
186+
self.wait_until(lambda: n.getindexinfo() == completed_idx_state)
187+
188+
189+
# Node 2: all indexes + reindex
190+
# -----------------------------
191+
192+
self.log.info("-- Testing all indexes + reindex")
193+
assert_equal(n2.getblockcount(), START_HEIGHT)
194+
195+
self.log.info(f"Loading snapshot into third node from {dump_output['path']}")
196+
loaded = n2.loadtxoutset(dump_output['path'])
197+
assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
198+
assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
199+
200+
monitor = n2.getchainstates()
201+
assert_equal(monitor['normal']['blocks'], START_HEIGHT)
202+
assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT)
203+
assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash'])
204+
205+
self.connect_nodes(0, 2)
206+
wait_until_helper(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT)
207+
self.sync_blocks()
208+
209+
self.log.info("Ensuring background validation completes")
210+
wait_until_helper(lambda: not n2.getchainstates().get('snapshot'))
211+
212+
completed_idx_state = {
213+
'basic block filter index': COMPLETE_IDX,
214+
'coinstatsindex': COMPLETE_IDX,
215+
'txindex': COMPLETE_IDX,
216+
}
217+
self.wait_until(lambda: n2.getindexinfo() == completed_idx_state)
218+
219+
for i in (0, 2):
220+
n = self.nodes[i]
221+
self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes")
222+
self.restart_node(i, extra_args=self.extra_args[i])
223+
224+
assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT)
225+
226+
assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT)
227+
assert_equal(n.getchainstates().get('snapshot'), None)
228+
229+
if i != 0:
230+
# Ensure indexes have synced for the assumeutxo node
231+
self.wait_until(lambda: n.getindexinfo() == completed_idx_state)
232+
233+
self.log.info("Test -reindex-chainstate of an assumeutxo-synced node")
234+
self.restart_node(2, extra_args=[
235+
'-reindex-chainstate=1', *self.extra_args[2]])
236+
assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT)
237+
wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT)
238+
239+
self.log.info("Test -reindex of an assumeutxo-synced node")
240+
self.restart_node(2, extra_args=['-reindex=1', *self.extra_args[2]])
241+
self.connect_nodes(0, 2)
242+
wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT)
243+
244+
245+
if __name__ == '__main__':
246+
AssumeutxoTest().main()

test/functional/test_framework/test_framework.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,3 +979,7 @@ def is_sqlite_compiled(self):
979979
def is_bdb_compiled(self):
980980
"""Checks whether the wallet module was compiled with BDB support."""
981981
return self.config["components"].getboolean("USE_BDB")
982+
983+
def has_blockfile(self, node, filenum: str):
984+
blocksdir = os.path.join(node.datadir, self.chain, 'blocks', '')
985+
return os.path.isfile(os.path.join(blocksdir, f"blk{filenum}.dat"))

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@
324324
'wallet_coinbase_category.py --descriptors',
325325
'feature_filelock.py',
326326
'feature_loadblock.py',
327+
'feature_assumeutxo.py',
327328
'p2p_dos_header_tree.py',
328329
'p2p_add_connections.py',
329330
'feature_bind_port_discover.py',

0 commit comments

Comments
 (0)