From abd5ad90b842e3efdc4305ff5708034de1555290 Mon Sep 17 00:00:00 2001 From: Pablo Deymonnaz Date: Fri, 13 Mar 2026 17:21:20 -0300 Subject: [PATCH] Add subnet filtering for gossip signature storage The spec (store.py:385-392, 605-612) requires that gossip signatures are only stored if the attester is in the same attestation subnet as the local validator. With ATTESTATION_COMMITTEE_COUNT=1 this is always true (all validators are in subnet 0), so there is no behavioral change. This adds forward compatibility for when the committee count increases. Both gossip attestation and proposer signature storage paths now check subnet membership before inserting into the gossip signatures map. --- crates/blockchain/src/lib.rs | 6 +- crates/blockchain/src/store.rs | 57 ++++++++++++++----- .../blockchain/tests/signature_spectests.rs | 2 +- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index f6a6ac9b..22cb23fa 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -272,7 +272,8 @@ impl BlockChainServer { &mut self, signed_block: SignedBlockWithAttestation, ) -> Result<(), StoreError> { - store::on_block(&mut self.store, signed_block)?; + let validator_ids = self.key_manager.validator_ids(); + store::on_block(&mut self.store, signed_block, &validator_ids)?; metrics::update_head_slot(self.store.head_slot()); metrics::update_latest_justified_slot(self.store.latest_justified().slot); metrics::update_latest_finalized_slot(self.store.latest_finalized().slot); @@ -438,7 +439,8 @@ impl BlockChainServer { warn!("Received unaggregated attestation but node is not an aggregator"); return; } - let _ = store::on_gossip_attestation(&mut self.store, attestation) + let validator_ids = self.key_manager.validator_ids(); + let _ = store::on_gossip_attestation(&mut self.store, attestation, &validator_ids) .inspect_err(|err| warn!(%err, "Failed to process gossiped attestation")); } diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index e1c192c4..f2d008c9 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -26,6 +26,16 @@ use crate::{INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; +/// Number of attestation committees per slot. +/// With ATTESTATION_COMMITTEE_COUNT = 1, all validators are in subnet 0. +const ATTESTATION_COMMITTEE_COUNT: u64 = 1; + +/// Compute the attestation subnet ID for a validator. +#[allow(clippy::modulo_one)] +fn compute_subnet_id(validator_id: u64) -> u64 { + validator_id % ATTESTATION_COMMITTEE_COUNT +} + /// Accept new aggregated payloads, promoting them to known for fork choice. fn accept_new_attestations(store: &mut Store, log_tree: bool) { store.promote_new_aggregated_payloads(); @@ -357,6 +367,7 @@ pub fn on_tick( pub fn on_gossip_attestation( store: &mut Store, signed_attestation: SignedAttestation, + local_validator_ids: &[u64], ) -> Result<(), StoreError> { let validator_id = signed_attestation.validator_id; let attestation = Attestation { @@ -396,9 +407,16 @@ pub fn on_gossip_attestation( // Store attestation data by root (content-addressed, idempotent) store.insert_attestation_data_by_root(data_root, attestation.data.clone()); - // Store gossip signature for later aggregation at interval 2. - store.insert_gossip_signature(data_root, attestation.data.slot, validator_id, signature); - metrics::update_gossip_signatures(store.gossip_signatures_count()); + // Store gossip signature for later aggregation at interval 2, + // only if the attester is in the same subnet as one of our validators. + let attester_subnet = compute_subnet_id(validator_id); + let in_our_subnet = local_validator_ids + .iter() + .any(|&vid| compute_subnet_id(vid) == attester_subnet); + if in_our_subnet { + store.insert_gossip_signature(data_root, attestation.data.slot, validator_id, signature); + metrics::update_gossip_signatures(store.gossip_signatures_count()); + } metrics::inc_attestations_valid(); @@ -506,8 +524,9 @@ pub fn on_gossip_aggregated_attestation( pub fn on_block( store: &mut Store, signed_block: SignedBlockWithAttestation, + local_validator_ids: &[u64], ) -> Result<(), StoreError> { - on_block_core(store, signed_block, true) + on_block_core(store, signed_block, true, local_validator_ids) } /// Process a new block without signature verification. @@ -518,7 +537,7 @@ pub fn on_block_without_verification( store: &mut Store, signed_block: SignedBlockWithAttestation, ) -> Result<(), StoreError> { - on_block_core(store, signed_block, false) + on_block_core(store, signed_block, false, &[]) } /// Core block processing logic. @@ -529,6 +548,7 @@ fn on_block_core( store: &mut Store, signed_block: SignedBlockWithAttestation, verify: bool, + local_validator_ids: &[u64], ) -> Result<(), StoreError> { let _timing = metrics::time_fork_choice_block_processing(); @@ -633,16 +653,23 @@ fn on_block_core( }; store.insert_new_aggregated_payload((proposer_vid, proposer_data_root), payload); } else { - // Store the proposer's signature for potential future block building - let proposer_sig = - ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature) - .map_err(|_| StoreError::SignatureDecodingFailed)?; - store.insert_gossip_signature( - proposer_data_root, - proposer_attestation.data.slot, - proposer_vid, - proposer_sig, - ); + // Store the proposer's signature for potential future block building, + // only if the proposer is in the same subnet as one of our validators. + let proposer_subnet = compute_subnet_id(proposer_vid); + let in_our_subnet = local_validator_ids + .iter() + .any(|&vid| compute_subnet_id(vid) == proposer_subnet); + if in_our_subnet { + let proposer_sig = + ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature) + .map_err(|_| StoreError::SignatureDecodingFailed)?; + store.insert_gossip_signature( + proposer_data_root, + proposer_attestation.data.slot, + proposer_vid, + proposer_sig, + ); + } } info!(%slot, %block_root, %state_root, "Processed new block"); diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index 5d617e33..c35c9ebe 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -55,7 +55,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> { store::on_tick(&mut st, block_time_ms, true, false); // Process the block (this includes signature verification) - let result = store::on_block(&mut st, signed_block); + let result = store::on_block(&mut st, signed_block, &[]); // Step 3: Check that it succeeded or failed as expected match (result.is_ok(), test.expect_exception.as_ref()) {