Skip to content

Commit 22926a7

Browse files
committed
Improve block header signature handling (#8253)
Squashed commit of the following: commit 0cfde28 Author: Michael Sproul <[email protected]> Date: Tue Oct 21 17:15:47 2025 +1100 Use valid sidecars in SSE tests commit e181950 Author: Michael Sproul <[email protected]> Date: Tue Oct 21 16:56:31 2025 +1100 Set min blob count to 1 commit 697d2fe Author: Michael Sproul <[email protected]> Date: Tue Oct 21 15:37:20 2025 +1100 Add data column test commit a5ec707 Author: Michael Sproul <[email protected]> Date: Tue Oct 21 15:24:40 2025 +1100 Add test for RPC blobs
1 parent b1e4531 commit 22926a7

File tree

6 files changed

+307
-55
lines changed

6 files changed

+307
-55
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3564,7 +3564,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
35643564
.await
35653565
}
35663566

3567-
fn check_blobs_for_slashability<'a>(
3567+
fn check_blob_header_signature_and_slashability<'a>(
35683568
self: &Arc<Self>,
35693569
block_root: Hash256,
35703570
blobs: impl IntoIterator<Item = &'a BlobSidecar<T::EthSpec>>,
@@ -3575,17 +3575,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
35753575
.map(|b| b.signed_block_header.clone())
35763576
.unique()
35773577
{
3578-
if verify_header_signature::<T, BlockError>(self, &header).is_ok() {
3579-
slashable_cache
3580-
.observe_slashable(
3581-
header.message.slot,
3582-
header.message.proposer_index,
3583-
block_root,
3584-
)
3585-
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
3586-
if let Some(slasher) = self.slasher.as_ref() {
3587-
slasher.accept_block_header(header);
3588-
}
3578+
// Return an error if *any* header signature is invalid, we do not want to import this
3579+
// list of blobs into the DA checker. However, we will process any valid headers prior
3580+
// to the first invalid header in the slashable cache & slasher.
3581+
verify_header_signature::<T, BlockError>(self, &header)?;
3582+
3583+
slashable_cache
3584+
.observe_slashable(
3585+
header.message.slot,
3586+
header.message.proposer_index,
3587+
block_root,
3588+
)
3589+
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
3590+
if let Some(slasher) = self.slasher.as_ref() {
3591+
slasher.accept_block_header(header);
35893592
}
35903593
}
35913594
Ok(())
@@ -3599,7 +3602,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
35993602
block_root: Hash256,
36003603
blobs: FixedBlobSidecarList<T::EthSpec>,
36013604
) -> Result<AvailabilityProcessingStatus, BlockError> {
3602-
self.check_blobs_for_slashability(block_root, blobs.iter().flatten().map(Arc::as_ref))?;
3605+
self.check_blob_header_signature_and_slashability(
3606+
block_root,
3607+
blobs.iter().flatten().map(Arc::as_ref),
3608+
)?;
36033609
let availability = self
36043610
.data_availability_checker
36053611
.put_rpc_blobs(block_root, blobs)?;
@@ -3616,12 +3622,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
36163622
) -> Result<AvailabilityProcessingStatus, BlockError> {
36173623
let availability = match engine_get_blobs_output {
36183624
EngineGetBlobsOutput::Blobs(blobs) => {
3619-
self.check_blobs_for_slashability(block_root, blobs.iter().map(|b| b.as_blob()))?;
3625+
self.check_blob_header_signature_and_slashability(
3626+
block_root,
3627+
blobs.iter().map(|b| b.as_blob()),
3628+
)?;
36203629
self.data_availability_checker
36213630
.put_kzg_verified_blobs(block_root, blobs)?
36223631
}
36233632
EngineGetBlobsOutput::CustodyColumns(data_columns) => {
3624-
self.check_columns_for_slashability(
3633+
self.check_data_column_sidecar_header_signature_and_slashability(
36253634
block_root,
36263635
data_columns.iter().map(|c| c.as_data_column()),
36273636
)?;
@@ -3642,7 +3651,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
36423651
block_root: Hash256,
36433652
custody_columns: DataColumnSidecarList<T::EthSpec>,
36443653
) -> Result<AvailabilityProcessingStatus, BlockError> {
3645-
self.check_columns_for_slashability(
3654+
self.check_data_column_sidecar_header_signature_and_slashability(
36463655
block_root,
36473656
custody_columns.iter().map(|c| c.as_ref()),
36483657
)?;
@@ -3659,7 +3668,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
36593668
.await
36603669
}
36613670

3662-
fn check_columns_for_slashability<'a>(
3671+
fn check_data_column_sidecar_header_signature_and_slashability<'a>(
36633672
self: &Arc<Self>,
36643673
block_root: Hash256,
36653674
custody_columns: impl IntoIterator<Item = &'a DataColumnSidecar<T::EthSpec>>,
@@ -3673,17 +3682,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
36733682
.map(|c| c.signed_block_header.clone())
36743683
.unique()
36753684
{
3676-
if verify_header_signature::<T, BlockError>(self, &header).is_ok() {
3677-
slashable_cache
3678-
.observe_slashable(
3679-
header.message.slot,
3680-
header.message.proposer_index,
3681-
block_root,
3682-
)
3683-
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
3684-
if let Some(slasher) = self.slasher.as_ref() {
3685-
slasher.accept_block_header(header);
3686-
}
3685+
// Return an error if *any* header signature is invalid, we do not want to import this
3686+
// list of blobs into the DA checker. However, we will process any valid headers prior
3687+
// to the first invalid header in the slashable cache & slasher.
3688+
verify_header_signature::<T, BlockError>(self, &header)?;
3689+
3690+
slashable_cache
3691+
.observe_slashable(
3692+
header.message.slot,
3693+
header.message.proposer_index,
3694+
block_root,
3695+
)
3696+
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
3697+
if let Some(slasher) = self.slasher.as_ref() {
3698+
slasher.accept_block_header(header);
36873699
}
36883700
}
36893701
Ok(())

beacon_node/beacon_chain/src/test_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2437,7 +2437,7 @@ where
24372437
}
24382438

24392439
/// Builds an `RpcBlock` from a `SignedBeaconBlock` and `BlobsList`.
2440-
fn build_rpc_block_from_blobs(
2440+
pub fn build_rpc_block_from_blobs(
24412441
&self,
24422442
block_root: Hash256,
24432443
block: Arc<SignedBeaconBlock<E, FullPayload<E>>>,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#![cfg(not(debug_assertions))]
2+
3+
use beacon_chain::test_utils::{
4+
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec,
5+
};
6+
use beacon_chain::{
7+
AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer,
8+
block_verification_types::AsBlock,
9+
};
10+
use logging::create_test_tracing_subscriber;
11+
use std::sync::{Arc, LazyLock};
12+
use types::{blob_sidecar::FixedBlobSidecarList, *};
13+
14+
type E = MainnetEthSpec;
15+
16+
// Should ideally be divisible by 3.
17+
const VALIDATOR_COUNT: usize = 24;
18+
19+
/// A cached set of keys.
20+
static KEYPAIRS: LazyLock<Vec<Keypair>> =
21+
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));
22+
23+
fn get_harness(
24+
validator_count: usize,
25+
spec: Arc<ChainSpec>,
26+
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
27+
create_test_tracing_subscriber();
28+
let harness = BeaconChainHarness::builder(MainnetEthSpec)
29+
.spec(spec)
30+
.chain_config(ChainConfig {
31+
reconstruct_historic_states: true,
32+
..ChainConfig::default()
33+
})
34+
.keypairs(KEYPAIRS[0..validator_count].to_vec())
35+
.fresh_ephemeral_store()
36+
.mock_execution_layer()
37+
.build();
38+
39+
harness.advance_slot();
40+
41+
harness
42+
}
43+
44+
// Regression test for https://github.com/sigp/lighthouse/issues/7650
45+
#[tokio::test]
46+
async fn rpc_blobs_with_invalid_header_signature() {
47+
let spec = Arc::new(test_spec::<E>());
48+
49+
// Only run this test if blobs are enabled and columns are disabled.
50+
if spec.deneb_fork_epoch.is_none() || spec.is_fulu_scheduled() {
51+
return;
52+
}
53+
54+
let harness = get_harness(VALIDATOR_COUNT, spec);
55+
56+
let num_blocks = E::slots_per_epoch() as usize;
57+
58+
// Add some chain depth.
59+
harness
60+
.extend_chain(
61+
num_blocks,
62+
BlockStrategy::OnCanonicalHead,
63+
AttestationStrategy::AllValidators,
64+
)
65+
.await;
66+
67+
// Produce a block with blobs.
68+
harness.execution_block_generator().set_min_blob_count(1);
69+
let head_state = harness.get_current_state();
70+
let slot = head_state.slot() + 1;
71+
let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await;
72+
let (kzg_proofs, blobs) = opt_blobs.unwrap();
73+
assert!(!blobs.is_empty());
74+
let block_root = signed_block.canonical_root();
75+
76+
// Process the block without blobs so that it doesn't become available.
77+
harness.advance_slot();
78+
let rpc_block = harness
79+
.build_rpc_block_from_blobs(block_root, signed_block.clone(), None)
80+
.unwrap();
81+
let availability = harness
82+
.chain
83+
.process_block(
84+
block_root,
85+
rpc_block,
86+
NotifyExecutionLayer::Yes,
87+
BlockImportSource::RangeSync,
88+
|| Ok(()),
89+
)
90+
.await
91+
.unwrap();
92+
assert_eq!(
93+
availability,
94+
AvailabilityProcessingStatus::MissingComponents(slot, block_root)
95+
);
96+
97+
// Build blob sidecars with invalid signatures in the block header.
98+
let mut corrupt_block = (*signed_block).clone();
99+
*corrupt_block.signature_mut() = Signature::infinity().unwrap();
100+
101+
let max_len = harness
102+
.chain
103+
.spec
104+
.max_blobs_per_block(slot.epoch(E::slots_per_epoch())) as usize;
105+
let mut blob_sidecars = FixedBlobSidecarList::new(vec![None; max_len]);
106+
for (i, (kzg_proof, blob)) in kzg_proofs.into_iter().zip(blobs).enumerate() {
107+
let blob_sidecar = BlobSidecar::new(i, blob, &corrupt_block, kzg_proof).unwrap();
108+
blob_sidecars[i] = Some(Arc::new(blob_sidecar));
109+
}
110+
111+
let err = harness
112+
.chain
113+
.process_rpc_blobs(slot, block_root, blob_sidecars)
114+
.await
115+
.unwrap_err();
116+
assert!(matches!(
117+
err,
118+
BlockError::InvalidSignature(InvalidSignature::ProposerSignature)
119+
));
120+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#![cfg(not(debug_assertions))]
2+
3+
use beacon_chain::test_utils::{
4+
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
5+
generate_data_column_sidecars_from_block, test_spec,
6+
};
7+
use beacon_chain::{
8+
AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer,
9+
block_verification_types::AsBlock,
10+
};
11+
use logging::create_test_tracing_subscriber;
12+
use std::sync::{Arc, LazyLock};
13+
use types::*;
14+
15+
type E = MainnetEthSpec;
16+
17+
// Should ideally be divisible by 3.
18+
const VALIDATOR_COUNT: usize = 24;
19+
20+
/// A cached set of keys.
21+
static KEYPAIRS: LazyLock<Vec<Keypair>> =
22+
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));
23+
24+
fn get_harness(
25+
validator_count: usize,
26+
spec: Arc<ChainSpec>,
27+
supernode: bool,
28+
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
29+
create_test_tracing_subscriber();
30+
let harness = BeaconChainHarness::builder(MainnetEthSpec)
31+
.spec(spec)
32+
.chain_config(ChainConfig {
33+
reconstruct_historic_states: true,
34+
..ChainConfig::default()
35+
})
36+
.keypairs(KEYPAIRS[0..validator_count].to_vec())
37+
.import_all_data_columns(supernode)
38+
.fresh_ephemeral_store()
39+
.mock_execution_layer()
40+
.build();
41+
42+
harness.advance_slot();
43+
44+
harness
45+
}
46+
47+
// Regression test for https://github.com/sigp/lighthouse/issues/7650
48+
#[tokio::test]
49+
async fn rpc_columns_with_invalid_header_signature() {
50+
let spec = Arc::new(test_spec::<E>());
51+
52+
// Only run this test if columns are enabled.
53+
if !spec.is_fulu_scheduled() {
54+
return;
55+
}
56+
57+
let supernode = true;
58+
let harness = get_harness(VALIDATOR_COUNT, spec, supernode);
59+
60+
let num_blocks = E::slots_per_epoch() as usize;
61+
62+
// Add some chain depth.
63+
harness
64+
.extend_chain(
65+
num_blocks,
66+
BlockStrategy::OnCanonicalHead,
67+
AttestationStrategy::AllValidators,
68+
)
69+
.await;
70+
71+
// Produce a block with blobs.
72+
harness.execution_block_generator().set_min_blob_count(1);
73+
let head_state = harness.get_current_state();
74+
let slot = head_state.slot() + 1;
75+
let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await;
76+
let (_, blobs) = opt_blobs.unwrap();
77+
assert!(!blobs.is_empty());
78+
let block_root = signed_block.canonical_root();
79+
80+
// Process the block without blobs so that it doesn't become available.
81+
harness.advance_slot();
82+
let rpc_block = harness
83+
.build_rpc_block_from_blobs(block_root, signed_block.clone(), None)
84+
.unwrap();
85+
let availability = harness
86+
.chain
87+
.process_block(
88+
block_root,
89+
rpc_block,
90+
NotifyExecutionLayer::Yes,
91+
BlockImportSource::RangeSync,
92+
|| Ok(()),
93+
)
94+
.await
95+
.unwrap();
96+
assert_eq!(
97+
availability,
98+
AvailabilityProcessingStatus::MissingComponents(slot, block_root)
99+
);
100+
101+
// Build blob sidecars with invalid signatures in the block header.
102+
let mut corrupt_block = (*signed_block).clone();
103+
*corrupt_block.signature_mut() = Signature::infinity().unwrap();
104+
105+
let data_column_sidecars =
106+
generate_data_column_sidecars_from_block(&corrupt_block, &harness.chain.spec);
107+
108+
let err = harness
109+
.chain
110+
.process_rpc_custody_columns(data_column_sidecars)
111+
.await
112+
.unwrap_err();
113+
assert!(matches!(
114+
err,
115+
BlockError::InvalidSignature(InvalidSignature::ProposerSignature)
116+
));
117+
}

0 commit comments

Comments
 (0)