diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index f02d5736..abf92872 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -9,7 +9,10 @@ //! without re-running the STF. The final STF runs once after selection to //! seal `state_root`. -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::Instant, +}; use ethlambda_crypto::aggregate_proofs; use ethlambda_state_transition::{ @@ -54,6 +57,7 @@ pub(crate) fn build_block( ) -> Result<(Block, Vec, PostBlockCheckpoints), StoreError> { info!(slot, proposer_index, "Building block"); + let select_start = Instant::now(); let selected = select_attestations( head_state, slot, @@ -61,10 +65,15 @@ pub(crate) fn build_block( known_block_roots, aggregated_payloads, ); + metrics::observe_block_proposal_phase("select_payloads", select_start.elapsed()); + + let child_payloads_consumed = selected.len(); // Compact: merge proofs sharing the same AttestationData via recursive // aggregation so each AttestationData appears at most once (leanSpec #510). + let compact_start = Instant::now(); let compacted = compact_attestations(selected, head_state)?; + metrics::observe_block_proposal_phase("compact", compact_start.elapsed()); let (aggregated_attestations, aggregated_signatures): (Vec<_>, Vec<_>) = compacted.into_iter().unzip(); @@ -80,10 +89,19 @@ pub(crate) fn build_block( body: BlockBody { attestations }, }; let mut post_state = head_state.clone(); + // ethlambda runs the STF once after selection (it projects justification + // incrementally instead of re-running the STF per loop round), so this is + // a single `stf_simulate` observation per build. + let stf_start = Instant::now(); process_slots(&mut post_state, slot)?; process_block(&mut post_state, &final_block)?; + metrics::observe_block_proposal_phase("stf_simulate", stf_start.elapsed()); final_block.state_root = post_state.hash_tree_root(); + metrics::inc_block_proposal_child_payloads_consumed(child_payloads_consumed as u64); + metrics::observe_block_proposal_attestation_data_selected(final_block.body.attestations.len()); + metrics::observe_block_proposal_aggregates_selected(aggregated_signatures.len()); + let post_checkpoints = PostBlockCheckpoints { justified: post_state.latest_justified, finalized: post_state.latest_finalized, @@ -156,6 +174,7 @@ fn select_attestations( let (att_data, proofs) = &chain.aggregated_payloads[&data_root]; processed_data_roots.insert(data_root); + metrics::inc_block_proposal_attestation_builds(); let before = selected.len(); extend_proofs_greedily(proofs, &mut selected, att_data); diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index 259afb16..8a8cbcf3 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -23,6 +23,18 @@ pub const ATTESTATION_AGGREGATE_COVERAGE_SECTIONS: &[&str] = &[ /// locally-aggregated pre-merge (`timely`) payloads. pub const ATTESTATION_AGGREGATE_COVERAGE_DIFF_DIRECTIONS: &[&str] = &["block_only", "timely_only"]; +/// Phase labels for `lean_block_proposal_attestation_build_phase_seconds`. +/// +/// `select_payloads`: greedy per-`AttestationData` proof selection. +/// `compact`: recursive merge of proofs sharing the same `AttestationData`. +/// `stf_simulate`: the single candidate-block state transition that seals the +/// state root. Unlike leanSpec (which re-runs the STF inside a fixed-point +/// loop), ethlambda projects justification/finalization incrementally during +/// `select_payloads` and runs the STF exactly once, so its `stf_simulate` +/// timing is a single observation per build rather than one per loop round. +pub const BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES: &[&str] = + &["select_payloads", "compact", "stf_simulate"]; + // --- Gauges --- static LEAN_HEAD_SLOT: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -420,6 +432,62 @@ static LEAN_BLOCK_BUILDING_FAILURES_TOTAL: std::sync::LazyLock = register_int_counter!("lean_block_building_failures_total", "Failed block builds").unwrap() }); +// --- Block Proposal Attestation Selection (build_block fixed-point loop) --- + +static LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + register_histogram_vec!( + "lean_block_proposal_attestation_build_phase_seconds", + "Phase-level time in block-proposal attestation selection: select_payloads (greedy \ + per-AttestationData proof pick), compact (recursive merge of proofs per \ + AttestationData), stf_simulate (candidate block state transition).", + &["phase"], + vec![ + 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0 + ] + ) + .unwrap() + }); + +static LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + register_int_counter!( + "lean_block_proposal_attestation_builds_total", + "Attestations selected during block-proposal selection (one increment per \ + selection-loop round that picks an AttestationData)." + ) + .unwrap() + }); + +static LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + register_int_counter!( + "lean_block_proposal_child_payloads_consumed_total", + "Child aggregated payloads selected during greedy proof picking (before compaction)." + ) + .unwrap() + }); + +static LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + register_histogram!( + "lean_block_proposal_attestation_data_selected", + "Distinct AttestationData entries in the proposal block body", + vec![0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0] + ) + .unwrap() + }); + +static LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + register_histogram!( + "lean_block_proposal_aggregates_selected", + "Aggregated signature proofs in the proposal result after compaction", + vec![0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0] + ) + .unwrap() + }); + // --- Sync Status --- /// Node synchronization status. @@ -512,6 +580,12 @@ pub fn init() { std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_TIME_SECONDS); std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_SUCCESS_TOTAL); std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_FAILURES_TOTAL); + // Block proposal attestation selection + std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS); + std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL); + std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL); + std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED); + std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED); // Sync status std::sync::LazyLock::force(&LEAN_NODE_SYNC_STATUS); } @@ -739,6 +813,34 @@ pub fn inc_block_building_failures() { LEAN_BLOCK_BUILDING_FAILURES_TOTAL.inc(); } +/// Observe the duration of a block-proposal attestation-selection phase. +/// `phase` must be one of [`BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES`]. +pub fn observe_block_proposal_phase(phase: &str, elapsed: Duration) { + LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS + .with_label_values(&[phase]) + .observe(elapsed.as_secs_f64()); +} + +/// Increment the completed block-proposal attestation selection runs counter. +pub fn inc_block_proposal_attestation_builds() { + LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL.inc(); +} + +/// Increment the greedily-selected child payloads counter (before compaction). +pub fn inc_block_proposal_child_payloads_consumed(count: u64) { + LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL.inc_by(count); +} + +/// Observe the number of distinct `AttestationData` entries in the proposal block body. +pub fn observe_block_proposal_attestation_data_selected(count: usize) { + LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED.observe(count as f64); +} + +/// Observe the number of aggregated signature proofs in the proposal result after compaction. +pub fn observe_block_proposal_aggregates_selected(count: usize) { + LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED.observe(count as f64); +} + /// Set the node sync status. Sets the given status label to 1 and all others to 0. pub fn set_node_sync_status(status: SyncStatus) { let active = status.as_str(); diff --git a/docs/metrics.md b/docs/metrics.md index f9314e32..93e8de33 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -39,6 +39,11 @@ The exposed metrics follow [the leanMetrics specification](https://github.com/le | `lean_block_building_time_seconds` | Histogram | Time taken to build a block | On block production | | 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.75, 1 | ✅ | | `lean_block_building_success_total` | Counter | Successful block builds | On block production | | | ✅ | | `lean_block_building_failures_total` | Counter | Failed block builds (error building the block, signing the block root, or processing it locally) | On block production failure | | | ✅ | +| `lean_block_proposal_attestation_build_phase_seconds` | Histogram | Phase-level time in block-proposal attestation selection | On block production | phase=select_payloads,compact,stf_simulate | 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8 | ✅ | +| `lean_block_proposal_attestation_builds_total` | Counter | Attestations selected during block-proposal selection (one per selection-loop round that picks an `AttestationData`) | On each attestation selection | | | ✅ | +| `lean_block_proposal_child_payloads_consumed_total` | Counter | Child aggregated payloads selected during greedy proof picking (before compaction) | On block production | | | ✅ | +| `lean_block_proposal_attestation_data_selected` | Histogram | Distinct `AttestationData` entries in the proposal block body | On block production | | 0, 1, 2, 4, 8, 16, 32 | ✅ | +| `lean_block_proposal_aggregates_selected` | Histogram | Aggregated signature proofs in the proposal result after compaction | On block production | | 0, 1, 2, 4, 8, 16, 32, 64, 128 | ✅ | ## Fork-Choice Metrics