@@ -568,6 +568,8 @@ def test_persist_skips_state_when_post_state_missing(
568568 ) -> None :
569569 """No put_state when the store has no post-state for the block root."""
570570 db = RecordingSyncDatabase ()
571+ # The persisted head matches the mock genesis, as after a real genesis write.
572+ db .head_root = Bytes32 .zero ()
571573 service = create_mock_sync_service (
572574 peer_id ,
573575 database = cast (Database , db ),
@@ -612,6 +614,8 @@ def test_persist_writes_state_and_prunes_when_finalized_advanced(
612614 ) -> None :
613615 """Post-state indexing and pruning run when finalization is past genesis."""
614616 db = RecordingSyncDatabase ()
617+ # The persisted head matches the mock genesis, as after a real genesis write.
618+ db .head_root = Bytes32 .zero ()
615619 service = create_mock_sync_service (
616620 peer_id ,
617621 database = cast (Database , db ),
@@ -667,6 +671,99 @@ def test_persist_writes_state_and_prunes_when_finalized_advanced(
667671 ),
668672 ]
669673
674+ def test_persist_skips_slot_index_for_side_branch_block (
675+ self ,
676+ peer_id : PeerId ,
677+ ) -> None :
678+ """A block that does not move the head leaves the slot index untouched."""
679+ db = RecordingSyncDatabase ()
680+ db .head_root = Bytes32 .zero ()
681+ service = create_mock_sync_service (
682+ peer_id ,
683+ database = cast (Database , db ),
684+ )
685+ mock_store = cast (MockForkchoiceStore , service .store )
686+ mock_store .advance_head_on_block = False
687+ service .state = SyncState .SYNCING
688+ genesis_root = service .store .head
689+ block = make_signed_block (
690+ slot = Slot (1 ),
691+ proposer_index = ValidatorIndex (0 ),
692+ parent_root = genesis_root ,
693+ state_root = Bytes32 .zero (),
694+ )
695+ service .store = service .process_block (service .store , block )
696+
697+ inner = db .calls_inside_batch ()
698+ call_names = [call .name for call in inner ]
699+ assert "put_block_root_by_slot" not in call_names
700+ assert "delete_block_root_by_slot" not in call_names
701+ # The unchanged head pointer is still persisted with the block.
702+ empty : MappingProxyType [str , object ] = MappingProxyType ({})
703+ assert RecordedCall (name = "put_head_root" , args = (Bytes32 .zero (),), kwargs = empty ) in inner
704+
705+ def test_persist_reindexes_slot_index_on_reorg (
706+ self ,
707+ peer_id : PeerId ,
708+ ) -> None :
709+ """A head switch rewrites differing slots and deletes vacated ones."""
710+ db = RecordingSyncDatabase ()
711+ genesis_root = Bytes32 .zero ()
712+
713+ # Old branch genesis <- B1 (slot 1) <- B2 (slot 2) is the persisted canonical chain.
714+ b1 = make_signed_block (
715+ slot = Slot (1 ),
716+ proposer_index = ValidatorIndex (0 ),
717+ parent_root = genesis_root ,
718+ state_root = Bytes32 .zero (),
719+ )
720+ b1_root = hash_tree_root (b1 .block )
721+ b2 = make_signed_block (
722+ slot = Slot (2 ),
723+ proposer_index = ValidatorIndex (0 ),
724+ parent_root = b1_root ,
725+ state_root = Bytes32 .zero (),
726+ )
727+ b2_root = hash_tree_root (b2 .block )
728+ # Seed before service creation: the tracker reads the head at wiring time.
729+ db .head_root = b2_root
730+
731+ service = create_mock_sync_service (
732+ peer_id ,
733+ database = cast (Database , db ),
734+ )
735+ mock_store = cast (MockForkchoiceStore , service .store )
736+ service .state = SyncState .SYNCING
737+ mock_store .blocks [b1_root ] = b1 .block
738+ mock_store .blocks [b2_root ] = b2 .block
739+ mock_store .head = b2_root
740+
741+ # Importing C2 (slot 2, child of genesis) reorgs the head onto the new branch.
742+ c2 = make_signed_block (
743+ slot = Slot (2 ),
744+ proposer_index = ValidatorIndex (1 ),
745+ parent_root = genesis_root ,
746+ state_root = Bytes32 .zero (),
747+ )
748+ service .store = service .process_block (service .store , c2 )
749+ c2_root = hash_tree_root (c2 .block )
750+
751+ inner = db .calls_inside_batch ()
752+ empty : MappingProxyType [str , object ] = MappingProxyType ({})
753+ # Slot 2 is rewritten to the new branch; slot 1 has no canonical block anymore.
754+ assert (
755+ RecordedCall (name = "put_block_root_by_slot" , args = (Slot (2 ), c2_root ), kwargs = empty )
756+ in inner
757+ )
758+ assert (
759+ RecordedCall (name = "delete_block_root_by_slot" , args = (Slot (1 ),), kwargs = empty ) in inner
760+ )
761+ # The refilled slot is never deleted.
762+ assert (
763+ RecordedCall (name = "delete_block_root_by_slot" , args = (Slot (2 ),), kwargs = empty )
764+ not in inner
765+ )
766+
670767
671768class TestSignedBlockServing :
672769 """Tests for signed-block retention and the inbound serving lookups."""
0 commit comments