From 7b58dcef19b6639bbd20d7d700b7d29c4c853988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:26:53 -0300 Subject: [PATCH 1/4] feat(blockchain): cap attestations packed per built block Add --max-attestations-per-block (default 3) to bound how many distinct AttestationData entries a proposer packs into a block it builds. This is a proposer-side self-limit only: the consensus cap for accepting peers' blocks stays at MAX_ATTESTATIONS_DATA, and the configured value is clamped to it so we never build a block that peers would reject. Group the new knob with the existing enable_proposer_aggregation flag into a ProposerConfig struct, since build_block otherwise exceeds clippy's argument limit. --- bin/ethlambda/src/cli.rs | 10 ++ bin/ethlambda/src/main.rs | 1 + crates/blockchain/src/block_builder.rs | 189 +++++++++++++++++++++++-- crates/blockchain/src/lib.rs | 10 ++ crates/blockchain/src/store.rs | 8 +- 5 files changed, 206 insertions(+), 12 deletions(-) diff --git a/bin/ethlambda/src/cli.rs b/bin/ethlambda/src/cli.rs index c0f09ddf..534cacf3 100644 --- a/bin/ethlambda/src/cli.rs +++ b/bin/ethlambda/src/cli.rs @@ -97,4 +97,14 @@ pub(crate) struct CliOptions { /// coverage. #[arg(long, default_value = "false")] pub(crate) enable_proposer_aggregation: bool, + /// Maximum number of distinct attestations to pack when building a block. + /// + /// Bounds how many distinct `AttestationData` entries the proposer includes + /// in a block it builds. This is a proposer-side self-limit only: it does + /// NOT change the consensus cap for accepting blocks from peers, which + /// stays at `MAX_ATTESTATIONS_DATA`. Values above `MAX_ATTESTATIONS_DATA` + /// are clamped to it, since a block carrying more would be rejected by + /// `on_block`. + #[arg(long, default_value = "3")] + pub(crate) max_attestations_per_block: usize, } diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 591cdcfd..f0cde2ec 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -216,6 +216,7 @@ async fn main() -> eyre::Result<()> { attestation_committee_count, !options.disable_duty_sync_gate, options.enable_proposer_aggregation, + options.max_attestations_per_block, ); // Note: SwarmConfig.is_aggregator is intentionally a plain bool, not the diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index e52b3fff..532447c0 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -43,6 +43,21 @@ pub struct PostBlockCheckpoints { pub finalized: Checkpoint, } +/// Proposer-side block-building policy, seeded from the CLI at spawn. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ProposerConfig { + /// How the proposer collapses same-`AttestationData` proofs (a block may + /// carry at most one entry per data). When true, they are merged via + /// recursive single-message aggregation into a union-coverage proof + /// (leanSpec #510); when false, only the single best-coverage proof per + /// data is kept, skipping the leanVM work. + pub enable_proposer_aggregation: bool, + /// Maximum number of distinct attestations to pack into a built block. + /// Proposer-side self-limit only; clamped to `MAX_ATTESTATIONS_DATA` during + /// selection so the block never exceeds the cap `on_block` enforces. + pub max_attestations_per_block: usize, +} + /// Build a valid block on top of this state. /// /// Selects attestations via `select_attestations`, collapses entries sharing @@ -62,6 +77,11 @@ pub struct PostBlockCheckpoints { /// /// Either way the output has one entry per `AttestationData` and the /// attestation-to-proof correspondence stays 1:1. +/// +/// `config.max_attestations_per_block` bounds how many distinct +/// `AttestationData` entries are packed (a proposer-side self-limit). It is +/// clamped to `MAX_ATTESTATIONS_DATA` so the block never exceeds the cap +/// `on_block` enforces on incoming blocks. pub(crate) fn build_block( head_state: &State, slot: u64, @@ -69,7 +89,7 @@ pub(crate) fn build_block( parent_root: H256, known_block_roots: &HashSet, aggregated_payloads: &HashMap)>, - enable_proposer_aggregation: bool, + config: ProposerConfig, ) -> Result<(Block, Vec, PostBlockCheckpoints), StoreError> { info!(slot, proposer_index, "Building block"); @@ -80,6 +100,7 @@ pub(crate) fn build_block( parent_root, known_block_roots, aggregated_payloads, + config.max_attestations_per_block, ); metrics::observe_block_proposal_phase("select_payloads", select_start.elapsed()); @@ -93,7 +114,7 @@ pub(crate) fn build_block( // work and keep only the single best-coverage proof per data. Both paths // log the entry / unique-entry counts they already compute. let compact_start = Instant::now(); - let compacted = if enable_proposer_aggregation { + let compacted = if config.enable_proposer_aggregation { compact_attestations(selected, head_state, slot)? } else { keep_best_proof_per_data(selected, slot) @@ -143,15 +164,16 @@ pub(crate) fn build_block( /// finalization are projected incrementally so dependent attestations become /// eligible on the next round without re-running the STF. /// -/// Stops at `MAX_ATTESTATIONS_DATA` distinct data entries or when no -/// remaining candidate has a positive score. Within-entry proof selection is -/// delegated to `extend_proofs_greedily`. +/// Stops at `max_attestations_per_block` distinct data entries (clamped to +/// `MAX_ATTESTATIONS_DATA`) or when no remaining candidate has a positive +/// score. Within-entry proof selection is delegated to `extend_proofs_greedily`. fn select_attestations( head_state: &State, slot: u64, parent_root: H256, known_block_roots: &HashSet, aggregated_payloads: &HashMap)>, + max_attestations_per_block: usize, ) -> Vec<(AggregatedAttestation, SingleMessageAggregate)> { let mut selected: Vec<(AggregatedAttestation, SingleMessageAggregate)> = Vec::new(); if aggregated_payloads.is_empty() { @@ -186,7 +208,10 @@ fn select_attestations( }; let mut processed_data_roots: HashSet = HashSet::new(); - for _round in 0..MAX_ATTESTATIONS_DATA { + // A block may carry at most `MAX_ATTESTATIONS_DATA` distinct entries + // (`on_block` rejects more), so the proposer-side limit never exceeds it. + let max_rounds = max_attestations_per_block.min(MAX_ATTESTATIONS_DATA); + for _round in 0..max_rounds { let Some((data_root, score, new_voters)) = pick_best_candidate(&chain, &processed_data_roots, &projected) else { @@ -991,7 +1016,10 @@ mod tests { parent_root, &known_block_roots, &aggregated_payloads, - true, + ProposerConfig { + enable_proposer_aggregation: true, + max_attestations_per_block: MAX_ATTESTATIONS_DATA, + }, ) .expect("build_block should succeed"); @@ -1028,6 +1056,138 @@ mod tests { ); } + /// A proposer-side `max_attestations_per_block` below `MAX_ATTESTATIONS_DATA` + /// must cap how many distinct `AttestationData` entries the built block + /// carries. The pool holds more selectable entries than the limit, so the + /// limit (not candidate exhaustion) is the binding constraint: building the + /// same pool at `MAX_ATTESTATIONS_DATA` packs strictly more. + #[test] + fn build_block_respects_configured_attestation_limit() { + use ethlambda_types::{ + block::BlockHeader, + state::{ChainConfig, JustificationValidators, JustifiedSlots}, + }; + use libssz_types::SszList; + + const NUM_VALIDATORS: usize = 50; + const NUM_PAYLOAD_ENTRIES: usize = 10; + const CONFIGURED_LIMIT: usize = 3; + + const HEAD_SLOT: u64 = 51; + const TARGET_SLOT: u64 = 5; + + let validators: Vec<_> = (0..NUM_VALIDATORS) + .map(|i| ethlambda_types::state::Validator { + attestation_pubkey: [i as u8; 52], + proposal_pubkey: [i as u8; 52], + index: i as u64, + }) + .collect(); + + let hashes: Vec = (0..HEAD_SLOT).map(|i| H256([(i + 1) as u8; 32])).collect(); + let historical_block_hashes = SszList::try_from(hashes.clone()).unwrap(); + + let head_header = BlockHeader { + slot: HEAD_SLOT, + proposer_index: 0, + parent_root: H256::ZERO, + state_root: H256::ZERO, + body_root: BlockBody::default().hash_tree_root(), + }; + + let head_state = State { + config: ChainConfig { genesis_time: 1000 }, + slot: HEAD_SLOT, + latest_block_header: head_header, + latest_justified: Checkpoint::default(), + latest_finalized: Checkpoint::default(), + historical_block_hashes, + justified_slots: JustifiedSlots::new(), + validators: SszList::try_from(validators).unwrap(), + justifications_roots: Default::default(), + justifications_validators: JustificationValidators::new(), + }; + + let mut header_for_root = head_state.latest_block_header.clone(); + header_for_root.state_root = head_state.hash_tree_root(); + let parent_root = header_for_root.hash_tree_root(); + + let slot = HEAD_SLOT + 1; + let proposer_index = slot % NUM_VALIDATORS as u64; + + // Common source / target / head so every payload passes the chain-match + // filter; distinct attestation slots give distinct data_roots, and one + // fresh validator per entry keeps each candidate scoring (adds a voter). + let source = Checkpoint { + root: hashes[0], + slot: 0, + }; + let target = Checkpoint { + root: hashes[TARGET_SLOT as usize], + slot: TARGET_SLOT, + }; + let head = Checkpoint { + root: hashes[0], + slot: 0, + }; + + let mut known_block_roots = HashSet::new(); + known_block_roots.insert(parent_root); + known_block_roots.insert(hashes[0]); + + let mut aggregated_payloads: HashMap)> = + HashMap::new(); + for i in 0..NUM_PAYLOAD_ENTRIES { + let att_data = AttestationData { + slot: (i + 1) as u64, + head, + target, + source, + }; + let data_root = att_data.hash_tree_root(); + + let mut bits = AggregationBits::with_length(NUM_VALIDATORS).unwrap(); + bits.set(i % NUM_VALIDATORS, true).unwrap(); + let proof_data = SszList::try_from(vec![0xABu8; 8]).expect("proof fits in ByteListMiB"); + let proof = SingleMessageAggregate::new(bits, proof_data); + + aggregated_payloads.insert(data_root, (att_data, vec![proof])); + } + + let build = |limit: usize| { + build_block( + &head_state, + slot, + proposer_index, + parent_root, + &known_block_roots, + &aggregated_payloads, + ProposerConfig { + enable_proposer_aggregation: false, + max_attestations_per_block: limit, + }, + ) + .expect("build_block should succeed") + .0 + .body + .attestations + .len() + }; + + let limited = build(CONFIGURED_LIMIT); + let unlimited = build(MAX_ATTESTATIONS_DATA); + + assert!( + (1..=CONFIGURED_LIMIT).contains(&limited), + "configured limit should cap attestations to {CONFIGURED_LIMIT}: got {limited}" + ); + assert!( + unlimited > CONFIGURED_LIMIT, + "the pool must offer more than {CONFIGURED_LIMIT} selectable entries so the limit \ + is the binding constraint: MAX_ATTESTATIONS_DATA build packed {unlimited}" + ); + } + /// With proposer aggregation disabled, `build_block` must still emit at /// most one entry per `AttestationData` (`on_block` rejects duplicates), /// keeping the single best-coverage proof and dropping the rest rather than @@ -1128,7 +1288,10 @@ mod tests { parent_root, &known_block_roots, &aggregated_payloads, - false, + ProposerConfig { + enable_proposer_aggregation: false, + max_attestations_per_block: MAX_ATTESTATIONS_DATA, + }, ) .expect("build_block should succeed"); @@ -1264,7 +1427,10 @@ mod tests { parent_root, &known_block_roots, &aggregated_payloads, - true, + ProposerConfig { + enable_proposer_aggregation: true, + max_attestations_per_block: MAX_ATTESTATIONS_DATA, + }, ) .expect("build_block should succeed"); @@ -1397,7 +1563,10 @@ mod tests { parent_root, &known_block_roots, &aggregated_payloads, - true, + ProposerConfig { + enable_proposer_aggregation: true, + max_attestations_per_block: MAX_ATTESTATIONS_DATA, + }, ) .expect("build_block should succeed"); diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 89a1a1d5..92d83d6d 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -83,6 +83,7 @@ impl BlockChain { attestation_committee_count: u64, gate_duties: bool, enable_proposer_aggregation: bool, + max_attestations_per_block: usize, ) -> BlockChain { metrics::set_is_aggregator(aggregator.is_enabled()); metrics::set_node_sync_status(metrics::SyncStatus::Idle); @@ -108,6 +109,7 @@ impl BlockChain { last_tick_instant: None, attestation_committee_count, enable_proposer_aggregation, + max_attestations_per_block, pre_merge_coverage: None, sync_status: SyncStatusTracker::new(gate_duties), } @@ -177,6 +179,13 @@ pub struct BlockChainServer { /// `--enable-proposer-aggregation` flag at spawn. enable_proposer_aggregation: bool, + /// Maximum number of distinct attestations the proposer packs into a block + /// it builds. Proposer-side self-limit only; it does not affect the cap for + /// accepting peers' blocks (`MAX_ATTESTATIONS_DATA`). Clamped to + /// `MAX_ATTESTATIONS_DATA` during selection. Seeded from the CLI + /// `--max-attestations-per-block` flag at spawn. + max_attestations_per_block: usize, + /// Pre-merge `new_payloads` snapshot for the attestation aggregate coverage /// report. Captured at the end-of-slot promote (interval 4), read at the /// next slot boundary. Owned solely by the actor and only touched from the @@ -511,6 +520,7 @@ impl BlockChainServer { slot, validator_id, self.enable_proposer_aggregation, + self.max_attestations_per_block, ) .inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to build block")) else { diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 4ac2a428..daefff4e 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -19,7 +19,7 @@ use tracing::{info, trace, warn}; use crate::{ GOSSIP_DISPARITY_INTERVALS, INTERVALS_PER_SLOT, MAX_ATTESTATIONS_DATA, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, - block_builder::{PostBlockCheckpoints, build_block}, + block_builder::{PostBlockCheckpoints, ProposerConfig, build_block}, metrics, }; @@ -790,6 +790,7 @@ pub fn produce_block_with_signatures( slot: u64, validator_index: u64, enable_proposer_aggregation: bool, + max_attestations_per_block: usize, ) -> Result<(Block, Vec, PostBlockCheckpoints), StoreError> { // Get parent block and state to build upon let head_root = get_proposal_head(store, slot); @@ -823,7 +824,10 @@ pub fn produce_block_with_signatures( head_root, &known_block_roots, &aggregated_payloads, - enable_proposer_aggregation, + ProposerConfig { + enable_proposer_aggregation, + max_attestations_per_block, + }, )? }; From 8a6ae05b71064bc9ebb1d5012adc568f2989fe6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:46:28 -0300 Subject: [PATCH 2/4] refactor(blockchain): store ProposerConfig on the actor Hold the proposer policy as a single ProposerConfig field on BlockChainServer and thread it through produce_block_with_signatures, instead of carrying enable_proposer_aggregation and max_attestations_per_block as loose fields reassembled at the call site. spawn stays the CLI-to-actor seam and keeps taking the raw primitives. ProposerConfig becomes pub since the public produce_block_with_signatures now takes it. --- crates/blockchain/src/block_builder.rs | 2 +- crates/blockchain/src/lib.rs | 30 ++++++++++---------------- crates/blockchain/src/store.rs | 8 ++----- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index 532447c0..919a7719 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -45,7 +45,7 @@ pub struct PostBlockCheckpoints { /// Proposer-side block-building policy, seeded from the CLI at spawn. #[derive(Debug, Clone, Copy)] -pub(crate) struct ProposerConfig { +pub struct ProposerConfig { /// How the proposer collapses same-`AttestationData` proofs (a block may /// carry at most one entry per data). When true, they are merged via /// recursive single-message aggregation into a union-coverage proof diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 92d83d6d..9f0d92ed 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -26,6 +26,7 @@ use spawned_concurrency::tasks::{Actor, ActorRef, ActorStart, Context, Handler, use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, trace, warn}; +use crate::block_builder::ProposerConfig; use crate::store::StoreError; pub mod aggregation; @@ -108,8 +109,10 @@ impl BlockChain { current_aggregation: None, last_tick_instant: None, attestation_committee_count, - enable_proposer_aggregation, - max_attestations_per_block, + proposer_config: ProposerConfig { + enable_proposer_aggregation, + max_attestations_per_block, + }, pre_merge_coverage: None, sync_status: SyncStatusTracker::new(gate_duties), } @@ -170,21 +173,11 @@ pub struct BlockChainServer { /// attestation aggregate coverage emission. attestation_committee_count: u64, - /// How the proposer collapses same-data attestations during block building - /// (a block may carry at most one entry per `AttestationData`). When true, - /// same-data proofs are merged via recursive single-message aggregation - /// into a union-coverage proof (leanSpec #510); when false (the default), - /// only the single best-coverage proof per data is kept, skipping the - /// per-data leanVM aggregation. Seeded from the CLI - /// `--enable-proposer-aggregation` flag at spawn. - enable_proposer_aggregation: bool, - - /// Maximum number of distinct attestations the proposer packs into a block - /// it builds. Proposer-side self-limit only; it does not affect the cap for - /// accepting peers' blocks (`MAX_ATTESTATIONS_DATA`). Clamped to - /// `MAX_ATTESTATIONS_DATA` during selection. Seeded from the CLI - /// `--max-attestations-per-block` flag at spawn. - max_attestations_per_block: usize, + /// Proposer-side block-building policy (how same-data attestations are + /// collapsed, and how many distinct attestations to pack). Seeded from the + /// CLI `--enable-proposer-aggregation` and `--max-attestations-per-block` + /// flags at spawn and read when this node proposes a block. + proposer_config: ProposerConfig, /// Pre-merge `new_payloads` snapshot for the attestation aggregate coverage /// report. Captured at the end-of-slot promote (interval 4), read at the @@ -519,8 +512,7 @@ impl BlockChainServer { &mut self.store, slot, validator_id, - self.enable_proposer_aggregation, - self.max_attestations_per_block, + self.proposer_config, ) .inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to build block")) else { diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index daefff4e..20f7cf5c 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -789,8 +789,7 @@ pub fn produce_block_with_signatures( store: &mut Store, slot: u64, validator_index: u64, - enable_proposer_aggregation: bool, - max_attestations_per_block: usize, + config: ProposerConfig, ) -> Result<(Block, Vec, PostBlockCheckpoints), StoreError> { // Get parent block and state to build upon let head_root = get_proposal_head(store, slot); @@ -824,10 +823,7 @@ pub fn produce_block_with_signatures( head_root, &known_block_roots, &aggregated_payloads, - ProposerConfig { - enable_proposer_aggregation, - max_attestations_per_block, - }, + config, )? }; From 51dfb60bce321185aa05205b98c868f4ac4630af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:54:28 -0300 Subject: [PATCH 3/4] docs: simplify comment --- crates/blockchain/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 9f0d92ed..2295ceda 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -173,10 +173,7 @@ pub struct BlockChainServer { /// attestation aggregate coverage emission. attestation_committee_count: u64, - /// Proposer-side block-building policy (how same-data attestations are - /// collapsed, and how many distinct attestations to pack). Seeded from the - /// CLI `--enable-proposer-aggregation` and `--max-attestations-per-block` - /// flags at spawn and read when this node proposes a block. + /// Proposer-side block-building policy proposer_config: ProposerConfig, /// Pre-merge `new_payloads` snapshot for the attestation aggregate coverage From fe516f426cf1bd5be73867417f383d1545499416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:28:41 -0300 Subject: [PATCH 4/4] refactor(blockchain): pass ProposerConfig into spawn Now that ProposerConfig is public, BlockChain::spawn takes it directly instead of the two raw primitives, and main.rs assembles it from the CLI options. Drops spawn to six arguments and keeps the proposer policy as one object end to end. --- bin/ethlambda/src/main.rs | 7 +++++-- crates/blockchain/src/lib.rs | 8 ++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index f0cde2ec..914c6c86 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -34,6 +34,7 @@ use tokio_util::sync::CancellationToken; use clap::Parser; use cli::CliOptions; use ethlambda_blockchain::MILLISECONDS_PER_SLOT; +use ethlambda_blockchain::block_builder::ProposerConfig; use ethlambda_blockchain::key_manager::ValidatorKeyPair; use ethlambda_network_api::{InitBlockChain, InitP2P, ToBlockChainToP2PRef, ToP2PToBlockChainRef}; use ethlambda_p2p::{Bootnode, P2P, PeerId, SwarmConfig, build_swarm, parse_enrs}; @@ -215,8 +216,10 @@ async fn main() -> eyre::Result<()> { aggregator.clone(), attestation_committee_count, !options.disable_duty_sync_gate, - options.enable_proposer_aggregation, - options.max_attestations_per_block, + ProposerConfig { + enable_proposer_aggregation: options.enable_proposer_aggregation, + max_attestations_per_block: options.max_attestations_per_block, + }, ); // Note: SwarmConfig.is_aggregator is intentionally a plain bool, not the diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 2295ceda..1d8195cd 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -83,8 +83,7 @@ impl BlockChain { aggregator: AggregatorController, attestation_committee_count: u64, gate_duties: bool, - enable_proposer_aggregation: bool, - max_attestations_per_block: usize, + proposer_config: ProposerConfig, ) -> BlockChain { metrics::set_is_aggregator(aggregator.is_enabled()); metrics::set_node_sync_status(metrics::SyncStatus::Idle); @@ -109,10 +108,7 @@ impl BlockChain { current_aggregation: None, last_tick_instant: None, attestation_committee_count, - proposer_config: ProposerConfig { - enable_proposer_aggregation, - max_attestations_per_block, - }, + proposer_config, pre_merge_coverage: None, sync_status: SyncStatusTracker::new(gate_duties), }