2021-06-17 16:09:38 -04:00
#!/usr/bin/env python3
2023-10-13 12:23:25 +02:00
# Copyright (c) 2021-present The Bitcoin Core developers
2021-06-17 16:09:38 -04:00
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
""" Test for assumeutxo, a means of quickly bootstrapping a node using
a serialized version of the UTXO set at a certain height , which corresponds
to a hash that has been compiled into bitcoind .
The assumeutxo value generated and used here is committed to in
` CRegTestParams : : m_assumeutxo_data ` in ` src / chainparams . cpp ` .
## Possible test improvements
- TODO : test submitting a transaction and verifying it appears in mempool
- TODO : test what happens with - reindex and - reindex - chainstate before the
snapshot is validated , and make sure it ' s deleted successfully.
Interesting test cases could be loading an assumeutxo snapshot file with :
2023-10-13 12:23:25 +02:00
- TODO : Valid hash but invalid snapshot file ( bad coin height or
2021-06-17 16:09:38 -04:00
bad other serialization )
- TODO : Valid snapshot file , but referencing a snapshot block that turns out to be
invalid , or has an invalid parent
- TODO : Valid snapshot file and snapshot block , but the block is not on the
most - work chain
Interesting starting states could be loading a snapshot when the current chain tip is :
- TODO : An ancestor of snapshot block
- TODO : Not an ancestor of the snapshot block but has less work
- TODO : The snapshot block
- TODO : A descendant of the snapshot block
- TODO : Not an ancestor or a descendant of the snapshot block and has more work
"""
from test_framework . test_framework import BitcoinTestFramework
2023-10-10 09:28:22 +02:00
from test_framework . util import (
assert_equal ,
assert_raises_rpc_error ,
)
2021-06-17 16:09:38 -04:00
START_HEIGHT = 199
SNAPSHOT_BASE_HEIGHT = 299
FINAL_HEIGHT = 399
COMPLETE_IDX = { ' synced ' : True , ' best_block_height ' : FINAL_HEIGHT }
class AssumeutxoTest ( BitcoinTestFramework ) :
def set_test_params ( self ) :
""" Use the pregenerated, deterministic chain up to height 199. """
self . num_nodes = 3
self . rpc_timeout = 120
self . extra_args = [
[ ] ,
[ " -fastprune " , " -prune=1 " , " -blockfilterindex=1 " , " -coinstatsindex=1 " ] ,
[ " -txindex=1 " , " -blockfilterindex=1 " , " -coinstatsindex=1 " ] ,
]
def setup_network ( self ) :
""" Start with the nodes disconnected so that one can generate a snapshot
including blocks the other hasn ' t yet seen. " " "
self . add_nodes ( 3 )
self . start_nodes ( extra_args = self . extra_args )
2023-10-10 09:28:22 +02:00
def test_invalid_snapshot_scenarios ( self , valid_snapshot_path ) :
self . log . info ( " Test different scenarios of loading invalid snapshot files " )
with open ( valid_snapshot_path , ' rb ' ) as f :
valid_snapshot_contents = f . read ( )
2023-10-12 11:21:08 +02:00
bad_snapshot_path = valid_snapshot_path + ' .mod '
2023-10-10 09:28:22 +02:00
2023-10-17 11:32:03 +02:00
def expected_error ( log_msg = " " , rpc_details = " " ) :
with self . nodes [ 1 ] . assert_debug_log ( [ log_msg ] ) :
assert_raises_rpc_error ( - 32603 , f " Unable to load UTXO snapshot { rpc_details } " , self . nodes [ 1 ] . loadtxoutset , bad_snapshot_path )
2023-10-12 11:21:08 +02:00
self . log . info ( " - snapshot file refering to a block that is not in the assumeutxo parameters " )
2023-10-17 16:57:07 +02:00
prev_block_hash = self . nodes [ 0 ] . getblockhash ( SNAPSHOT_BASE_HEIGHT - 1 )
bogus_block_hash = " 0 " * 64 # Represents any unknown block hash
for bad_block_hash in [ bogus_block_hash , prev_block_hash ] :
with open ( bad_snapshot_path , ' wb ' ) as f :
# block hash of the snapshot base is stored right at the start (first 32 bytes)
f . write ( bytes . fromhex ( bad_block_hash ) [ : : - 1 ] + valid_snapshot_contents [ 32 : ] )
2023-10-17 11:32:03 +02:00
error_details = f " , assumeutxo block hash in snapshot metadata not recognized ( { bad_block_hash } ) "
expected_error ( rpc_details = error_details )
2023-10-10 09:28:22 +02:00
2023-10-12 11:21:08 +02:00
self . log . info ( " - snapshot file with wrong number of coins " )
2023-10-13 12:23:25 +02:00
valid_num_coins = int . from_bytes ( valid_snapshot_contents [ 32 : 32 + 8 ] , " little " )
2023-10-12 11:21:08 +02:00
for off in [ - 1 , + 1 ] :
with open ( bad_snapshot_path , ' wb ' ) as f :
f . write ( valid_snapshot_contents [ : 32 ] )
2023-10-13 12:23:25 +02:00
f . write ( ( valid_num_coins + off ) . to_bytes ( 8 , " little " ) )
f . write ( valid_snapshot_contents [ 32 + 8 : ] )
2023-10-17 11:32:03 +02:00
expected_error ( log_msg = f " bad snapshot - coins left over after deserializing 298 coins " if off == - 1 else f " bad snapshot format or truncated snapshot after deserializing 299 coins " )
2023-10-12 11:21:08 +02:00
2023-10-13 12:23:25 +02:00
self . log . info ( " - snapshot file with wrong outpoint hash " )
with open ( bad_snapshot_path , " wb " ) as f :
f . write ( valid_snapshot_contents [ : ( 32 + 8 ) ] )
f . write ( b " \xff " * 32 )
f . write ( valid_snapshot_contents [ ( 32 + 8 + 32 ) : ] )
2023-10-17 11:32:03 +02:00
expected_error ( log_msg = " [snapshot] bad snapshot content hash: expected ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0, got 29926acf3ac81f908cf4f22515713ca541c08bb0f0ef1b2c3443a007134d69b8 " )
self . log . info ( " - snapshot file with wrong outpoint index " )
with open ( bad_snapshot_path , " wb " ) as f :
f . write ( valid_snapshot_contents [ : ( 32 + 8 + 32 ) ] )
new_index = 1 # The correct index is 0
f . write ( new_index . to_bytes ( 4 , " little " ) )
f . write ( valid_snapshot_contents [ ( 32 + 8 + 32 + 4 ) : ] )
expected_error ( log_msg = " [snapshot] bad snapshot content hash: expected ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0, got 798266c2e1f9a98fe5ce61f5951cbf47130743f3764cf3cbc254be129142cf9d " )
2023-10-13 12:23:25 +02:00
2021-06-17 16:09:38 -04:00
def run_test ( self ) :
"""
Bring up two ( disconnected ) nodes , mine some new blocks on the first ,
and generate a UTXO snapshot .
Load the snapshot into the second , ensure it syncs to tip and completes
background validation when connected to the first .
"""
n0 = self . nodes [ 0 ]
n1 = self . nodes [ 1 ]
n2 = self . nodes [ 2 ]
# Mock time for a deterministic chain
for n in self . nodes :
n . setmocktime ( n . getblockheader ( n . getbestblockhash ( ) ) [ ' time ' ] )
self . sync_blocks ( )
# Generate a series of blocks that `n0` will have in the snapshot,
# but that n1 doesn't yet see. In order for the snapshot to activate,
# though, we have to ferry over the new headers to n1 so that it
# isn't waiting forever to see the header of the snapshot's base block
# while disconnected from n0.
for i in range ( 100 ) :
2023-10-03 00:55:10 +02:00
self . generate ( n0 , nblocks = 1 , sync_fun = self . no_op )
2021-06-17 16:09:38 -04:00
newblock = n0 . getblock ( n0 . getbestblockhash ( ) , 0 )
# make n1 aware of the new header, but don't give it the block.
n1 . submitheader ( newblock )
n2 . submitheader ( newblock )
# Ensure everyone is seeing the same headers.
for n in self . nodes :
assert_equal ( n . getblockchaininfo ( ) [ " headers " ] , SNAPSHOT_BASE_HEIGHT )
self . log . info ( " -- Testing assumeutxo + some indexes + pruning " )
assert_equal ( n0 . getblockcount ( ) , SNAPSHOT_BASE_HEIGHT )
assert_equal ( n1 . getblockcount ( ) , START_HEIGHT )
self . log . info ( f " Creating a UTXO snapshot at height { SNAPSHOT_BASE_HEIGHT } " )
dump_output = n0 . dumptxoutset ( ' utxos.dat ' )
assert_equal (
dump_output [ ' txoutset_hash ' ] ,
' ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0 ' )
assert_equal ( dump_output [ ' nchaintx ' ] , 300 )
assert_equal ( n0 . getblockchaininfo ( ) [ " blocks " ] , SNAPSHOT_BASE_HEIGHT )
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
# will allow us to test n1's sync-to-tip on top of a snapshot.
2023-10-03 00:55:10 +02:00
self . generate ( n0 , nblocks = 100 , sync_fun = self . no_op )
2021-06-17 16:09:38 -04:00
assert_equal ( n0 . getblockcount ( ) , FINAL_HEIGHT )
assert_equal ( n1 . getblockcount ( ) , START_HEIGHT )
assert_equal ( n0 . getblockchaininfo ( ) [ " blocks " ] , FINAL_HEIGHT )
2023-10-10 09:28:22 +02:00
self . test_invalid_snapshot_scenarios ( dump_output [ ' path ' ] )
2021-06-17 16:09:38 -04:00
self . log . info ( f " Loading snapshot into second node from { dump_output [ ' path ' ] } " )
loaded = n1 . loadtxoutset ( dump_output [ ' path ' ] )
assert_equal ( loaded [ ' coins_loaded ' ] , SNAPSHOT_BASE_HEIGHT )
assert_equal ( loaded [ ' base_height ' ] , SNAPSHOT_BASE_HEIGHT )
2023-10-04 10:58:47 -04:00
normal , snapshot = n1 . getchainstates ( ) [ " chainstates " ]
assert_equal ( normal [ ' blocks ' ] , START_HEIGHT )
assert_equal ( normal . get ( ' snapshot_blockhash ' ) , None )
assert_equal ( normal [ ' validated ' ] , True )
assert_equal ( snapshot [ ' blocks ' ] , SNAPSHOT_BASE_HEIGHT )
assert_equal ( snapshot [ ' snapshot_blockhash ' ] , dump_output [ ' base_hash ' ] )
assert_equal ( snapshot [ ' validated ' ] , False )
2021-06-17 16:09:38 -04:00
assert_equal ( n1 . getblockchaininfo ( ) [ " blocks " ] , SNAPSHOT_BASE_HEIGHT )
PAUSE_HEIGHT = FINAL_HEIGHT - 40
self . log . info ( " Restarting node to stop at height %d " , PAUSE_HEIGHT )
self . restart_node ( 1 , extra_args = [
f " -stopatheight= { PAUSE_HEIGHT } " , * self . extra_args [ 1 ] ] )
# Finally connect the nodes and let them sync.
2023-10-04 11:05:27 -04:00
#
# Set `wait_for_connect=False` to avoid a race between performing connection
# assertions and the -stopatheight tripping.
self . connect_nodes ( 0 , 1 , wait_for_connect = False )
2021-06-17 16:09:38 -04:00
n1 . wait_until_stopped ( timeout = 5 )
self . log . info ( " Checking that blocks are segmented on disk " )
assert self . has_blockfile ( n1 , " 00000 " ) , " normal blockfile missing "
assert self . has_blockfile ( n1 , " 00001 " ) , " assumed blockfile missing "
assert not self . has_blockfile ( n1 , " 00002 " ) , " too many blockfiles "
self . log . info ( " Restarted node before snapshot validation completed, reloading... " )
self . restart_node ( 1 , extra_args = self . extra_args [ 1 ] )
self . connect_nodes ( 0 , 1 )
self . log . info ( f " Ensuring snapshot chain syncs to tip. ( { FINAL_HEIGHT } ) " )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : n1 . getchainstates ( ) [ ' chainstates ' ] [ - 1 ] [ ' blocks ' ] == FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
self . sync_blocks ( nodes = ( n0 , n1 ) )
self . log . info ( " Ensuring background validation completes " )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : len ( n1 . getchainstates ( ) [ ' chainstates ' ] ) == 1 )
2021-06-17 16:09:38 -04:00
# Ensure indexes have synced.
completed_idx_state = {
' basic block filter index ' : COMPLETE_IDX ,
' coinstatsindex ' : COMPLETE_IDX ,
}
self . wait_until ( lambda : n1 . getindexinfo ( ) == completed_idx_state )
for i in ( 0 , 1 ) :
n = self . nodes [ i ]
self . log . info ( f " Restarting node { i } to ensure (Check|Load)BlockIndex passes " )
self . restart_node ( i , extra_args = self . extra_args [ i ] )
assert_equal ( n . getblockchaininfo ( ) [ " blocks " ] , FINAL_HEIGHT )
2023-10-04 10:58:47 -04:00
chainstate , = n . getchainstates ( ) [ ' chainstates ' ]
assert_equal ( chainstate [ ' blocks ' ] , FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
if i != 0 :
# Ensure indexes have synced for the assumeutxo node
self . wait_until ( lambda : n . getindexinfo ( ) == completed_idx_state )
# Node 2: all indexes + reindex
# -----------------------------
self . log . info ( " -- Testing all indexes + reindex " )
assert_equal ( n2 . getblockcount ( ) , START_HEIGHT )
self . log . info ( f " Loading snapshot into third node from { dump_output [ ' path ' ] } " )
loaded = n2 . loadtxoutset ( dump_output [ ' path ' ] )
assert_equal ( loaded [ ' coins_loaded ' ] , SNAPSHOT_BASE_HEIGHT )
assert_equal ( loaded [ ' base_height ' ] , SNAPSHOT_BASE_HEIGHT )
2023-10-04 10:58:47 -04:00
normal , snapshot = n2 . getchainstates ( ) [ ' chainstates ' ]
assert_equal ( normal [ ' blocks ' ] , START_HEIGHT )
assert_equal ( normal . get ( ' snapshot_blockhash ' ) , None )
assert_equal ( normal [ ' validated ' ] , True )
assert_equal ( snapshot [ ' blocks ' ] , SNAPSHOT_BASE_HEIGHT )
assert_equal ( snapshot [ ' snapshot_blockhash ' ] , dump_output [ ' base_hash ' ] )
assert_equal ( snapshot [ ' validated ' ] , False )
2021-06-17 16:09:38 -04:00
self . connect_nodes ( 0 , 2 )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : n2 . getchainstates ( ) [ ' chainstates ' ] [ - 1 ] [ ' blocks ' ] == FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
self . sync_blocks ( )
self . log . info ( " Ensuring background validation completes " )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : len ( n2 . getchainstates ( ) [ ' chainstates ' ] ) == 1 )
2021-06-17 16:09:38 -04:00
completed_idx_state = {
' basic block filter index ' : COMPLETE_IDX ,
' coinstatsindex ' : COMPLETE_IDX ,
' txindex ' : COMPLETE_IDX ,
}
self . wait_until ( lambda : n2 . getindexinfo ( ) == completed_idx_state )
for i in ( 0 , 2 ) :
n = self . nodes [ i ]
self . log . info ( f " Restarting node { i } to ensure (Check|Load)BlockIndex passes " )
self . restart_node ( i , extra_args = self . extra_args [ i ] )
assert_equal ( n . getblockchaininfo ( ) [ " blocks " ] , FINAL_HEIGHT )
2023-10-04 10:58:47 -04:00
chainstate , = n . getchainstates ( ) [ ' chainstates ' ]
assert_equal ( chainstate [ ' blocks ' ] , FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
if i != 0 :
# Ensure indexes have synced for the assumeutxo node
self . wait_until ( lambda : n . getindexinfo ( ) == completed_idx_state )
self . log . info ( " Test -reindex-chainstate of an assumeutxo-synced node " )
self . restart_node ( 2 , extra_args = [
' -reindex-chainstate=1 ' , * self . extra_args [ 2 ] ] )
assert_equal ( n2 . getblockchaininfo ( ) [ " blocks " ] , FINAL_HEIGHT )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : n2 . getblockcount ( ) == FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
self . log . info ( " Test -reindex of an assumeutxo-synced node " )
self . restart_node ( 2 , extra_args = [ ' -reindex=1 ' , * self . extra_args [ 2 ] ] )
self . connect_nodes ( 0 , 2 )
2023-10-03 00:55:10 +02:00
self . wait_until ( lambda : n2 . getblockcount ( ) == FINAL_HEIGHT )
2021-06-17 16:09:38 -04:00
if __name__ == ' __main__ ' :
AssumeutxoTest ( ) . main ( )