9
9
The assumeutxo value generated and used here is committed to in
10
10
`CRegTestParams::m_assumeutxo_data` in `src/kernel/chainparams.cpp`.
11
11
"""
12
+ import time
12
13
from shutil import rmtree
13
14
14
15
from dataclasses import dataclass
15
16
from test_framework .blocktools import (
16
17
create_block ,
17
18
create_coinbase
18
19
)
19
- from test_framework .messages import tx_from_hex
20
+ from test_framework .messages import (
21
+ CBlockHeader ,
22
+ from_hex ,
23
+ msg_headers ,
24
+ tx_from_hex
25
+ )
26
+ from test_framework .p2p import (
27
+ P2PInterface ,
28
+ )
20
29
from test_framework .test_framework import BitcoinTestFramework
21
30
from test_framework .util import (
22
31
assert_approx ,
23
32
assert_equal ,
24
33
assert_raises_rpc_error ,
25
34
sha256sum_file ,
35
+ try_rpc ,
26
36
)
27
37
from test_framework .wallet import (
28
38
getnewdestination ,
@@ -248,6 +258,74 @@ def test_snapshot_not_on_most_work_chain(self, dump_output_path):
248
258
node1 .submitheader (main_block1 )
249
259
node1 .submitheader (main_block2 )
250
260
261
+ def test_sync_from_assumeutxo_node (self , snapshot ):
262
+ """
263
+ This test verifies that:
264
+ 1. An IBD node can sync headers from an AssumeUTXO node at any time.
265
+ 2. IBD nodes do not request historical blocks from AssumeUTXO nodes while they are syncing the background-chain.
266
+ 3. The assumeUTXO node dynamically adjusts the network services it offers according to its state.
267
+ 4. IBD nodes can fully sync from AssumeUTXO nodes after they finish the background-chain sync.
268
+ """
269
+ self .log .info ("Testing IBD-sync from assumeUTXO node" )
270
+ # Node2 starts clean and loads the snapshot.
271
+ # Node3 starts clean and seeks to sync-up from snapshot_node.
272
+ miner = self .nodes [0 ]
273
+ snapshot_node = self .nodes [2 ]
274
+ ibd_node = self .nodes [3 ]
275
+
276
+ # Start test fresh by cleaning up node directories
277
+ for node in (snapshot_node , ibd_node ):
278
+ self .stop_node (node .index )
279
+ rmtree (node .chain_path )
280
+ self .start_node (node .index , extra_args = self .extra_args [node .index ])
281
+
282
+ # Sync-up headers chain on snapshot_node to load snapshot
283
+ headers_provider_conn = snapshot_node .add_p2p_connection (P2PInterface ())
284
+ headers_provider_conn .wait_for_getheaders ()
285
+ msg = msg_headers ()
286
+ for block_num in range (1 , miner .getblockcount ()+ 1 ):
287
+ msg .headers .append (from_hex (CBlockHeader (), miner .getblockheader (miner .getblockhash (block_num ), verbose = False )))
288
+ headers_provider_conn .send_message (msg )
289
+
290
+ # Ensure headers arrived
291
+ default_value = {'status' : '' } # No status
292
+ headers_tip_hash = miner .getbestblockhash ()
293
+ self .wait_until (lambda : next (filter (lambda x : x ['hash' ] == headers_tip_hash , snapshot_node .getchaintips ()), default_value )['status' ] == "headers-only" )
294
+ snapshot_node .disconnect_p2ps ()
295
+
296
+ # Load snapshot
297
+ snapshot_node .loadtxoutset (snapshot ['path' ])
298
+
299
+ # Connect nodes and verify the ibd_node can sync-up the headers-chain from the snapshot_node
300
+ self .connect_nodes (ibd_node .index , snapshot_node .index )
301
+ snapshot_block_hash = snapshot ['base_hash' ]
302
+ self .wait_until (lambda : next (filter (lambda x : x ['hash' ] == snapshot_block_hash , ibd_node .getchaintips ()), default_value )['status' ] == "headers-only" )
303
+
304
+ # Once the headers-chain is synced, the ibd_node must avoid requesting historical blocks from the snapshot_node.
305
+ # If it does request such blocks, the snapshot_node will ignore requests it cannot fulfill, causing the ibd_node
306
+ # to stall. This stall could last for up to 10 min, ultimately resulting in an abrupt disconnection due to the
307
+ # ibd_node's perceived unresponsiveness.
308
+ time .sleep (3 ) # Sleep here because we can't detect when a node avoids requesting blocks from other peer.
309
+ assert_equal (len (ibd_node .getpeerinfo ()[0 ]['inflight' ]), 0 )
310
+
311
+ # Now disconnect nodes and finish background chain sync
312
+ self .disconnect_nodes (ibd_node .index , snapshot_node .index )
313
+ self .connect_nodes (snapshot_node .index , miner .index )
314
+ self .sync_blocks (nodes = (miner , snapshot_node ))
315
+ # Check the base snapshot block was stored and ensure node signals full-node service support
316
+ self .wait_until (lambda : not try_rpc (- 1 , "Block not found" , snapshot_node .getblock , snapshot_block_hash ))
317
+ assert 'NETWORK' in snapshot_node .getnetworkinfo ()['localservicesnames' ]
318
+
319
+ # Now the snapshot_node is sync, verify the ibd_node can sync from it
320
+ self .connect_nodes (snapshot_node .index , ibd_node .index )
321
+ assert 'NETWORK' in ibd_node .getpeerinfo ()[0 ]['servicesnames' ]
322
+ self .sync_blocks (nodes = (ibd_node , snapshot_node ))
323
+
324
+ def assert_only_network_limited_service (self , node ):
325
+ node_services = node .getnetworkinfo ()['localservicesnames' ]
326
+ assert 'NETWORK' not in node_services
327
+ assert 'NETWORK_LIMITED' in node_services
328
+
251
329
def run_test (self ):
252
330
"""
253
331
Bring up two (disconnected) nodes, mine some new blocks on the first,
@@ -381,13 +459,20 @@ def check_dump_output(output):
381
459
self .test_snapshot_block_invalidated (dump_output ['path' ])
382
460
self .test_snapshot_not_on_most_work_chain (dump_output ['path' ])
383
461
462
+ # Prune-node sanity check
463
+ assert 'NETWORK' not in n1 .getnetworkinfo ()['localservicesnames' ]
464
+
384
465
self .log .info (f"Loading snapshot into second node from { dump_output ['path' ]} " )
385
466
# This node's tip is on an ancestor block of the snapshot, which should
386
467
# be the normal case
387
468
loaded = n1 .loadtxoutset (dump_output ['path' ])
388
469
assert_equal (loaded ['coins_loaded' ], SNAPSHOT_BASE_HEIGHT )
389
470
assert_equal (loaded ['base_height' ], SNAPSHOT_BASE_HEIGHT )
390
471
472
+ self .log .info ("Confirm that local services remain unchanged" )
473
+ # Since n1 is a pruned node, the 'NETWORK' service flag must always be unset.
474
+ self .assert_only_network_limited_service (n1 )
475
+
391
476
self .log .info ("Check that UTXO-querying RPCs operate on snapshot chainstate" )
392
477
snapshot_hash = loaded ['tip_hash' ]
393
478
snapshot_num_coins = loaded ['coins_loaded' ]
@@ -491,6 +576,9 @@ def check_tx_counts(final: bool) -> None:
491
576
self .restart_node (1 , extra_args = [
492
577
f"-stopatheight={ PAUSE_HEIGHT } " , * self .extra_args [1 ]])
493
578
579
+ # Upon restart during snapshot tip sync, the node must remain in 'limited' mode.
580
+ self .assert_only_network_limited_service (n1 )
581
+
494
582
# Finally connect the nodes and let them sync.
495
583
#
496
584
# Set `wait_for_connect=False` to avoid a race between performing connection
@@ -507,6 +595,9 @@ def check_tx_counts(final: bool) -> None:
507
595
self .log .info ("Restarted node before snapshot validation completed, reloading..." )
508
596
self .restart_node (1 , extra_args = self .extra_args [1 ])
509
597
598
+ # Upon restart, the node must remain in 'limited' mode
599
+ self .assert_only_network_limited_service (n1 )
600
+
510
601
# Send snapshot block to n1 out of order. This makes the test less
511
602
# realistic because normally the snapshot block is one of the last
512
603
# blocks downloaded, but its useful to test because it triggers more
@@ -525,6 +616,10 @@ def check_tx_counts(final: bool) -> None:
525
616
self .log .info ("Ensuring background validation completes" )
526
617
self .wait_until (lambda : len (n1 .getchainstates ()['chainstates' ]) == 1 )
527
618
619
+ # Since n1 is a pruned node, it will not signal NODE_NETWORK after
620
+ # completing the background sync.
621
+ self .assert_only_network_limited_service (n1 )
622
+
528
623
# Ensure indexes have synced.
529
624
completed_idx_state = {
530
625
'basic block filter index' : COMPLETE_IDX ,
@@ -555,12 +650,18 @@ def check_tx_counts(final: bool) -> None:
555
650
556
651
self .log .info ("-- Testing all indexes + reindex" )
557
652
assert_equal (n2 .getblockcount (), START_HEIGHT )
653
+ assert 'NETWORK' in n2 .getnetworkinfo ()['localservicesnames' ] # sanity check
558
654
559
655
self .log .info (f"Loading snapshot into third node from { dump_output ['path' ]} " )
560
656
loaded = n2 .loadtxoutset (dump_output ['path' ])
561
657
assert_equal (loaded ['coins_loaded' ], SNAPSHOT_BASE_HEIGHT )
562
658
assert_equal (loaded ['base_height' ], SNAPSHOT_BASE_HEIGHT )
563
659
660
+ # Even though n2 is a full node, it will unset the 'NETWORK' service flag during snapshot loading.
661
+ # This indicates other peers that the node will temporarily not provide historical blocks.
662
+ self .log .info ("Check node2 updated the local services during snapshot load" )
663
+ self .assert_only_network_limited_service (n2 )
664
+
564
665
for reindex_arg in ['-reindex=1' , '-reindex-chainstate=1' ]:
565
666
self .log .info (f"Check that restarting with { reindex_arg } will delete the snapshot chainstate" )
566
667
self .restart_node (2 , extra_args = [reindex_arg , * self .extra_args [2 ]])
@@ -584,13 +685,21 @@ def check_tx_counts(final: bool) -> None:
584
685
msg = "Unable to load UTXO snapshot: Can't activate a snapshot-based chainstate more than once"
585
686
assert_raises_rpc_error (- 32603 , msg , n2 .loadtxoutset , dump_output ['path' ])
586
687
688
+ # Upon restart, the node must stay in 'limited' mode until the background
689
+ # chain sync completes.
690
+ self .restart_node (2 , extra_args = self .extra_args [2 ])
691
+ self .assert_only_network_limited_service (n2 )
692
+
587
693
self .connect_nodes (0 , 2 )
588
694
self .wait_until (lambda : n2 .getchainstates ()['chainstates' ][- 1 ]['blocks' ] == FINAL_HEIGHT )
589
695
self .sync_blocks (nodes = (n0 , n2 ))
590
696
591
697
self .log .info ("Ensuring background validation completes" )
592
698
self .wait_until (lambda : len (n2 .getchainstates ()['chainstates' ]) == 1 )
593
699
700
+ # Once background chain sync completes, the full node must start offering historical blocks again.
701
+ assert {'NETWORK' , 'NETWORK_LIMITED' }.issubset (n2 .getnetworkinfo ()['localservicesnames' ])
702
+
594
703
completed_idx_state = {
595
704
'basic block filter index' : COMPLETE_IDX ,
596
705
'coinstatsindex' : COMPLETE_IDX ,
@@ -625,6 +734,9 @@ def check_tx_counts(final: bool) -> None:
625
734
626
735
self .test_snapshot_in_a_divergent_chain (dump_output ['path' ])
627
736
737
+ # The following test cleans node2 and node3 chain directories.
738
+ self .test_sync_from_assumeutxo_node (snapshot = dump_output )
739
+
628
740
@dataclass
629
741
class Block :
630
742
hash : str
0 commit comments