From 9e9291c329273dcd63f25cc80a080bf779fc0c05 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Wed, 1 Jul 2026 04:49:21 +0100 Subject: [PATCH] devnet cleanup on republish inbound gossip blocks --- lean_client/containers/src/state.rs | 25 ++++++++----------- lean_client/fork_choice/src/store.rs | 14 ++++++++++- .../fork_choice/tests/unit_tests/validator.rs | 19 +++++++++++--- lean_client/networking/src/network/service.rs | 2 +- lean_client/src/main.rs | 25 +++++++++++++++++-- 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 609f14e..318d08f 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -428,24 +428,19 @@ impl State { .start_timer() }); - // Each unique AttestationData must appear at most once per block. - // Mirrors leanSpec spec.py:1247-1252. Our own builder collapses - // duplicates upstream via `aggregate_by_data`, so this guards - // externally-built blocks. - ensure!( - !AggregatedAttestation::has_duplicate_data(attestations), - "Block contains duplicate AttestationData entries; \ - each AttestationData must appear at most once", - ); - // Cap distinct AttestationData entries per block. Mirrors leanSpec - // spec.py:1253-1256. With the duplicate check above, len == - // distinct-count, so this is equivalent to the spec's - // `len(att_data_set) <= MAX_ATTESTATIONS_DATA`. + // state_transition.py:205-216 — only distinct data is bounded; split + // aggregates sharing one data count once. Wire-level duplicate + // rejection lives at the on_block-equivalent in main.rs. + let distinct_data_count = attestations + .into_iter() + .map(|att| att.data.hash_tree_root()) + .collect::>() + .len(); ensure!( - (attestations.len_u64() as usize) <= MAX_ATTESTATIONS_DATA, + distinct_data_count <= MAX_ATTESTATIONS_DATA, "Block contains {} distinct AttestationData entries; maximum is {}", - attestations.len_u64(), + distinct_data_count, MAX_ATTESTATIONS_DATA, ); diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 2d3fa44..6951b9c 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -145,11 +145,23 @@ impl Store { let target_checkpoint = self.get_attestation_target(); + let head_state = self + .states + .get(&self.head) + .ok_or(anyhow!("head state is not known"))?; + let mut source = head_state.latest_justified.clone(); + if source.root == H256::zero() { + source = Checkpoint { + root: self.head, + slot: source.slot, + }; + } + Ok(AttestationData { slot, head: head_checkpoint, target: target_checkpoint, - source: self.latest_justified.clone(), + source, }) } diff --git a/lean_client/fork_choice/tests/unit_tests/validator.rs b/lean_client/fork_choice/tests/unit_tests/validator.rs index 7a964e3..97e5f37 100644 --- a/lean_client/fork_choice/tests/unit_tests/validator.rs +++ b/lean_client/fork_choice/tests/unit_tests/validator.rs @@ -598,19 +598,32 @@ fn test_validator_operations_invalid_parameters() { } #[test] -fn test_produce_attestation_data_uses_store_justified() { +fn test_produce_attestation_data_uses_head_state_justified() { let mut store = create_test_store(); - store.latest_justified = Checkpoint { + let head_state_justified = Checkpoint { + root: H256::from_slice(&[0xaa; 32]), + slot: Slot(3), + }; + let store_only_justified = Checkpoint { root: H256::from_slice(&[0xff; 32]), slot: Slot(5), }; + let mut new_head_state = store.states[&store.head].as_ref().clone(); + new_head_state.latest_justified = head_state_justified.clone(); + store + .states + .insert(store.head, std::sync::Arc::new(new_head_state)); + + store.latest_justified = store_only_justified.clone(); + let attestation_data = store .produce_attestation_data(Slot(1)) .expect("produce_attestation_data failed"); - assert_eq!(attestation_data.source, store.latest_justified); + assert_eq!(attestation_data.source, head_state_justified); + assert_ne!(attestation_data.source, store_only_justified); } fn produce_and_apply( diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 04d2ec9..a0a188d 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -645,7 +645,7 @@ where .send(ChainMessage::ProcessBlock { signed_block, is_trusted: false, - should_gossip: true, + should_gossip: false, cached_post_state: None, }) .await diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index edf2360..882c2be 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -5,8 +5,8 @@ static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; use anyhow::{Context as _, Result}; use clap::Parser; use containers::{ - Block, BlockBody, BlockHeader, Checkpoint, Config, MultiMessageAggregate, SignedBlock, Slot, - State, Status, Validator, + AggregatedAttestation, Block, BlockBody, BlockHeader, Checkpoint, Config, + MultiMessageAggregate, SignedBlock, Slot, State, Status, Validator, }; use dedicated_executor::DedicatedExecutor; use ethereum_types::H256; @@ -1317,6 +1317,16 @@ async fn main() -> Result<()> { continue; } + if AggregatedAttestation::has_duplicate_data( + &signed_block.block.body.attestations, + ) { + warn!( + block_root = %format!("0x{:x}", block_root), + "Rejecting verified block: duplicate AttestationData entries" + ); + continue; + } + // Skip if parent state was pruned by finalization while verify ran. // `apply_verified_block` retains finalized + buffer states only; if // finalization advanced past this block's parent during the verify @@ -1550,6 +1560,17 @@ async fn main() -> Result<()> { continue; } + if AggregatedAttestation::has_duplicate_data( + &signed_block.block.body.attestations, + ) { + warn!( + slot = block_slot.0, + block_root = %format!("0x{:x}", block_root), + "Rejecting block: duplicate AttestationData entries" + ); + continue; + } + info!( slot = block_slot.0, block_root = %format!("0x{:x}", block_root),