@@ -1820,81 +1820,94 @@ struct InvalidHeadSetup {
1820
1820
}
1821
1821
1822
1822
impl InvalidHeadSetup {
1823
+ /// This function aims to produce two things:
1824
+ ///
1825
+ /// 1. A chain where the only viable head block has an invalid execution payload.
1826
+ /// 2. A block (`fork_block`) which will become the head of the chain when
1827
+ /// it is imported.
1823
1828
async fn new ( ) -> InvalidHeadSetup {
1829
+ let slots_per_epoch = E :: slots_per_epoch ( ) ;
1824
1830
let mut rig = InvalidPayloadRig :: new ( ) . enable_attestations ( ) ;
1825
1831
rig. move_to_terminal_block ( ) ;
1826
1832
rig. import_block ( Payload :: Valid ) . await ; // Import a valid transition block.
1827
1833
1828
- // Import blocks until the first time the chain finalizes.
1834
+ // Import blocks until the first time the chain finalizes. This avoids
1835
+ // some edge-cases around genesis.
1829
1836
while rig. cached_head ( ) . finalized_checkpoint ( ) . epoch == 0 {
1830
1837
rig. import_block ( Payload :: Syncing ) . await ;
1831
1838
}
1832
1839
1833
- let slots_per_epoch = E :: slots_per_epoch ( ) ;
1834
- let start_slot = rig. cached_head ( ) . head_slot ( ) + 1 ;
1835
- let mut opt_fork_block = None ;
1836
-
1837
- assert_eq ! ( start_slot % slots_per_epoch, 1 ) ;
1838
- for i in 0 ..slots_per_epoch - 1 {
1839
- let slot = start_slot + i;
1840
- let slot_offset = slot. as_u64 ( ) % slots_per_epoch;
1841
-
1842
- rig. harness . set_current_slot ( slot) ;
1843
-
1844
- if slot_offset == slots_per_epoch - 1 {
1845
- // Optimistic head block right before epoch boundary.
1846
- let is_valid = Payload :: Syncing ;
1847
- rig. import_block_parametric ( is_valid, is_valid, Some ( slot) , |error| {
1848
- matches ! (
1849
- error,
1850
- BlockError :: ExecutionPayloadError (
1851
- ExecutionPayloadError :: RejectedByExecutionEngine { .. }
1852
- )
1853
- )
1854
- } )
1855
- . await ;
1856
- } else if 3 * slot_offset < 2 * slots_per_epoch {
1857
- // Valid block in previous epoch.
1858
- rig. import_block ( Payload :: Valid ) . await ;
1859
- } else if slot_offset == slots_per_epoch - 2 {
1860
- // Fork block one slot prior to invalid head, not applied immediately.
1861
- let parent_state = rig
1862
- . harness
1863
- . chain
1864
- . state_at_slot ( slot - 1 , StateSkipConfig :: WithStateRoots )
1865
- . unwrap ( ) ;
1866
- let ( fork_block_tuple, _) = rig. harness . make_block ( parent_state, slot) . await ;
1867
- opt_fork_block = Some ( fork_block_tuple. 0 ) ;
1868
- } else {
1869
- // Skipped slot.
1870
- } ;
1840
+ // Define a helper function.
1841
+ let chain = rig. harness . chain . clone ( ) ;
1842
+ let get_unrealized_justified_epoch = move || {
1843
+ chain
1844
+ . canonical_head
1845
+ . fork_choice_read_lock ( )
1846
+ . unrealized_justified_checkpoint ( )
1847
+ . epoch
1848
+ } ;
1849
+
1850
+ // Import more blocks until there is a new and higher unrealized
1851
+ // justified checkpoint.
1852
+ //
1853
+ // The result will be a single chain where the head block has a higher
1854
+ // unrealized justified checkpoint than all other blocks in the chain.
1855
+ let initial_unrealized_justified = get_unrealized_justified_epoch ( ) ;
1856
+ while get_unrealized_justified_epoch ( ) == initial_unrealized_justified {
1857
+ rig. import_block ( Payload :: Syncing ) . await ;
1871
1858
}
1872
1859
1860
+ // Create a forked block that competes with the head block. Both the
1861
+ // head block and this fork block will share the same parent.
1862
+ //
1863
+ // The fork block and head block will both have an unrealized justified
1864
+ // checkpoint at epoch `N` whilst their parent is at `N - 1`.
1865
+ let head_slot = rig. cached_head ( ) . head_slot ( ) ;
1866
+ let parent_slot = head_slot - 1 ;
1867
+ let fork_block_slot = head_slot + 1 ;
1868
+ let parent_state = rig
1869
+ . harness
1870
+ . chain
1871
+ . state_at_slot ( parent_slot, StateSkipConfig :: WithStateRoots )
1872
+ . unwrap ( ) ;
1873
+ let ( fork_block_tuple, _) = rig. harness . make_block ( parent_state, fork_block_slot) . await ;
1874
+ let fork_block = fork_block_tuple. 0 ;
1875
+
1873
1876
let invalid_head = rig. cached_head ( ) ;
1874
- assert_eq ! (
1875
- invalid_head. head_slot( ) % slots_per_epoch,
1876
- slots_per_epoch - 1
1877
- ) ;
1878
1877
1879
- // Advance clock to new epoch to realize the justification of soon-to-be-invalid head block.
1880
- rig. harness . set_current_slot ( invalid_head. head_slot ( ) + 1 ) ;
1878
+ // Advance the chain forward two epochs past the current head block.
1879
+ //
1880
+ // This ensures that `voting_source.epoch + 2 >= current_epoch` is
1881
+ // `false` in the `node_is_viable_for_head` function. In effect, this
1882
+ // ensures that no other block but the current head block is viable as a
1883
+ // head block.
1884
+ let invalid_head_epoch = invalid_head. head_slot ( ) . epoch ( slots_per_epoch) ;
1885
+ let new_wall_clock_epoch = invalid_head_epoch + 2 ;
1886
+ rig. harness
1887
+ . set_current_slot ( new_wall_clock_epoch. start_slot ( slots_per_epoch) ) ;
1881
1888
1882
1889
// Invalidate the head block.
1883
1890
rig. invalidate_manually ( invalid_head. head_block_root ( ) )
1884
1891
. await ;
1885
1892
1893
+ // Since our setup ensures that there is only a single, invalid block
1894
+ // that's viable for head (according to FFG filtering), setting the
1895
+ // head block as invalid should not result in another head being chosen.
1896
+ // Rather, it should fail to run fork choice and leave the invalid block as
1897
+ // the head.
1886
1898
assert ! ( rig
1887
1899
. canonical_head( )
1888
1900
. head_execution_status( )
1889
1901
. unwrap( )
1890
1902
. is_invalid( ) ) ;
1891
1903
1892
- // Finding a new head should fail since the only possible head is not valid.
1904
+ // Ensure that we're getting the correct error when trying to find a new
1905
+ // head.
1893
1906
rig. assert_get_head_error_contains ( "InvalidBestNode" ) ;
1894
1907
1895
1908
Self {
1896
1909
rig,
1897
- fork_block : opt_fork_block . unwrap ( ) ,
1910
+ fork_block,
1898
1911
invalid_head,
1899
1912
}
1900
1913
}
0 commit comments