Skip to content

Commit 094c36d

Browse files
Use correct Fork in verify_header_signature (#8528)
Fix a bug in `verify_header_signature` which tripped up some Lighthouse nodes at the Fusaka fork. The bug was a latent bug in a function that has been present for a long time, but only used by slashers. With Fulu it entered the critical path of blob/column verification -- call stack: - `FetchBlobsBeaconAdapter::process_engine_blobs` - `BeaconChain::process_engine_blobs` - `BeaconChain::check_engine_blobs_availability_and_import` - `BeaconChain::check_blob_header_signature_and_slashability` - `verify_header_signature` Thanks @eserilev for quickly diagnosing the root cause. Change `verify_header_signature` to use `ChainSpec::fork_at_epoch` to compute the `Fork`, rather than using the head state's fork. At a fork boundary the head state's fork is stale and lacks the data for the new fork. Using `fork_at_epoch` ensures that we use the correct fork data and validate transition block's signature correctly. Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
1 parent ced49dd commit 094c36d

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,11 +2122,13 @@ pub fn verify_header_signature<T: BeaconChainTypes, Err: BlockBlobError>(
21222122
.get(header.message.proposer_index as usize)
21232123
.cloned()
21242124
.ok_or(Err::unknown_validator_error(header.message.proposer_index))?;
2125-
let head_fork = chain.canonical_head.cached_head().head_fork();
2125+
let fork = chain
2126+
.spec
2127+
.fork_at_epoch(header.message.slot.epoch(T::EthSpec::slots_per_epoch()));
21262128

21272129
if header.verify_signature::<T::EthSpec>(
21282130
&proposer_pubkey,
2129-
&head_fork,
2131+
&fork,
21302132
chain.genesis_validators_root,
21312133
&chain.spec,
21322134
) {

beacon_node/beacon_chain/tests/column_verification.rs

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

0 commit comments

Comments
 (0)