@@ -116,3 +116,96 @@ async fn rpc_columns_with_invalid_header_signature() {
116116 BlockError :: InvalidSignature ( InvalidSignature :: ProposerSignature )
117117 ) ) ;
118118}
119+
120+ // Regression test for verify_header_signature bug: it uses head_fork() which is wrong for fork blocks
121+ #[ tokio:: test]
122+ async fn verify_header_signature_fork_block_bug ( ) {
123+ // Create a spec with all forks enabled at genesis except Fulu which is at epoch 1
124+ // This allows us to easily create the scenario where the head is at Electra
125+ // but we're trying to verify a block from Fulu epoch
126+ let mut spec = test_spec :: < E > ( ) ;
127+
128+ // Only run this test for FORK_NAME=fulu.
129+ if !spec. is_fulu_scheduled ( ) || spec. is_gloas_scheduled ( ) {
130+ return ;
131+ }
132+
133+ spec. altair_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
134+ spec. bellatrix_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
135+ spec. capella_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
136+ spec. deneb_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
137+ spec. electra_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
138+ let fulu_fork_epoch = Epoch :: new ( 1 ) ;
139+ spec. fulu_fork_epoch = Some ( fulu_fork_epoch) ;
140+
141+ let spec = Arc :: new ( spec) ;
142+ let harness = get_harness ( VALIDATOR_COUNT , spec. clone ( ) , NodeCustodyType :: Supernode ) ;
143+ harness. execution_block_generator ( ) . set_min_blob_count ( 1 ) ;
144+
145+ // Add some blocks in epoch 0 (Electra)
146+ harness
147+ . extend_chain (
148+ E :: slots_per_epoch ( ) as usize - 1 ,
149+ BlockStrategy :: OnCanonicalHead ,
150+ AttestationStrategy :: AllValidators ,
151+ )
152+ . await ;
153+
154+ // Verify we're still in epoch 0 (Electra)
155+ let pre_fork_state = harness. get_current_state ( ) ;
156+ assert_eq ! ( pre_fork_state. current_epoch( ) , Epoch :: new( 0 ) ) ;
157+ assert ! ( matches!( pre_fork_state, BeaconState :: Electra ( _) ) ) ;
158+
159+ // Now produce a block at the first slot of epoch 1 (Fulu fork).
160+ // make_block will advance the state which will trigger the Electra->Fulu upgrade.
161+ let fork_slot = fulu_fork_epoch. start_slot ( E :: slots_per_epoch ( ) ) ;
162+ let ( ( signed_block, opt_blobs) , _state_root) =
163+ harness. make_block ( pre_fork_state. clone ( ) , fork_slot) . await ;
164+ let ( _, blobs) = opt_blobs. expect ( "Blobs should be present" ) ;
165+ assert ! ( !blobs. is_empty( ) , "Block should have blobs" ) ;
166+ let block_root = signed_block. canonical_root ( ) ;
167+
168+ // Process the block WITHOUT blobs to make it unavailable.
169+ // The block will be accepted but won't become the head because it's not fully available.
170+ // This keeps the head at the pre-fork state (Electra).
171+ harness. advance_slot ( ) ;
172+ let rpc_block = harness
173+ . build_rpc_block_from_blobs ( block_root, signed_block. clone ( ) , None )
174+ . expect ( "Should build RPC block" ) ;
175+ let availability = harness
176+ . chain
177+ . process_block (
178+ block_root,
179+ rpc_block,
180+ NotifyExecutionLayer :: Yes ,
181+ BlockImportSource :: RangeSync ,
182+ || Ok ( ( ) ) ,
183+ )
184+ . await
185+ . expect ( "Block should be processed" ) ;
186+ assert_eq ! (
187+ availability,
188+ AvailabilityProcessingStatus :: MissingComponents ( fork_slot, block_root) ,
189+ "Block should be pending availability"
190+ ) ;
191+
192+ // The head should still be in epoch 0 (Electra) because the fork block isn't available
193+ let current_head_state = harness. get_current_state ( ) ;
194+ assert_eq ! ( current_head_state. current_epoch( ) , Epoch :: new( 0 ) ) ;
195+ assert ! ( matches!( current_head_state, BeaconState :: Electra ( _) ) ) ;
196+
197+ // Now try to process columns for the fork block.
198+ // The bug: verify_header_signature previously used head_fork() which fetched the fork from
199+ // the head state (still Electra fork), but the block was signed with the Fulu fork version.
200+ // This caused an incorrect signature verification failure.
201+ let data_column_sidecars =
202+ generate_data_column_sidecars_from_block ( & signed_block, & harness. chain . spec ) ;
203+
204+ // Now that the bug is fixed, the block should import.
205+ let status = harness
206+ . chain
207+ . process_rpc_custody_columns ( data_column_sidecars)
208+ . await
209+ . unwrap ( ) ;
210+ assert_eq ! ( status, AvailabilityProcessingStatus :: Imported ( block_root) ) ;
211+ }
0 commit comments