Skip to content

Commit 85115d7

Browse files
authored
Add lean_block_proposal_* metrics for block-proposal attestation selection (#414)
## 🗒️ Description / Motivation Ports the five `lean_block_proposal_*` observability metrics from [leanSpec PR #753](leanEthereum/leanSpec#753) into the ethlambda block builder. These metrics give cross-client visibility into the block-proposal attestation-selection path (`build_block`): how long each phase takes, how many proposal builds run, how many child payloads are greedily consumed, and how many distinct `AttestationData` / aggregated proofs end up in the proposed block. They align with [zeam #914](blockblaz/zeam#914 `getProposalAttestations` instrumentation and the leanSpec naming so the [leanMetrics](https://github.com/leanEthereum/leanMetrics) dashboards work across clients. ## What Changed **`crates/blockchain/src/metrics.rs`** — five new metrics registered with the existing `LazyLock` + `register_*!` pattern, a `BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES` label constant, registration in `init()`, public API functions, and a unit test: | Metric | Type | Buckets / Labels | |--------|------|------------------| | `lean_block_proposal_attestation_build_phase_seconds` | HistogramVec | `phase` = `select_payloads`, `compact`, `stf_simulate`; buckets `0.001…8` | | `lean_block_proposal_attestation_builds_total` | Counter | one per proposal attempt | | `lean_block_proposal_child_payloads_consumed_total` | Counter | greedily-picked proofs before compaction | | `lean_block_proposal_attestation_data_selected` | Histogram | buckets `0, 1, 2, 4, 8, 16, 32` | | `lean_block_proposal_aggregates_selected` | Histogram | buckets `0, 1, 2, 4, 8, 16, 32, 64, 128` | **`crates/blockchain/src/block_builder.rs`** — instruments `build_block`: times the `select_attestations`, `compact_attestations`, and STF (`process_slots` + `process_block`) phases, and emits the counters/histograms after a successful build. **`docs/metrics.md`** — documents all five in the Block Production Metrics table. ## Correctness / Behavior Guarantees - **No behavior change.** Only metric observations were added around existing logic; block contents, selection order, and the state-transition path are untouched. - **Architectural divergence from leanSpec, documented.** leanSpec re-runs the STF inside a fixed-point loop and observes `stf_simulate` per round. ethlambda projects justification/finalization incrementally during selection and runs the STF exactly **once** at the end, so its `stf_simulate` is a single observation per build. This is noted on the `BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES` doc comment, consistent with the upstream PR's own caveat that phase timings are not directly comparable across clients. - `attestation_data_selected` and `aggregates_selected` are observed from the post-compaction body (one merged proof per distinct `AttestationData`), matching the spec's intent. - Metrics are only emitted on a **successful** build; a build that errors out in the STF is already counted by the existing `lean_block_building_failures_total`. ## Tests Added / Run - New unit test `metrics::tests::block_proposal_attestation_build_metrics_are_usable` — verifies the phase metric registers and accepts every label in `BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES`, and that the companion counters/histograms are callable. Guards against drift between the label constant and the strings passed at the `build_block` call sites. - Existing `block_builder` tests (`build_block_*`, `compact_attestations_*`, `extend_proofs_greedily_*`) now exercise the new metric paths and pass unchanged. Commands run: - `make fmt` — clean - `cargo clippy -p ethlambda-blockchain --all-targets -- -D warnings` — clean - `cargo test -p ethlambda-blockchain --lib` — 23 passing ## Related Issues / PRs - Ports [leanEthereum/leanSpec#753](leanEthereum/leanSpec#753) - Related to [blockblaz/zeam#914](blockblaz/zeam#914) - Follows the metrics pattern from #406 (per-subnet attestation aggregate coverage) ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [ ] Ran `cargo test --workspace --release` — only `ethlambda-blockchain` lib suite run (23 passing); full workspace release run not yet executed
1 parent 161245f commit 85115d7

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

crates/blockchain/src/block_builder.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
//! without re-running the STF. The final STF runs once after selection to
1010
//! seal `state_root`.
1111
12-
use std::collections::{HashMap, HashSet};
12+
use std::{
13+
collections::{HashMap, HashSet},
14+
time::Instant,
15+
};
1316

1417
use ethlambda_crypto::aggregate_proofs;
1518
use ethlambda_state_transition::{
@@ -54,17 +57,23 @@ pub(crate) fn build_block(
5457
) -> Result<(Block, Vec<AggregatedSignatureProof>, PostBlockCheckpoints), StoreError> {
5558
info!(slot, proposer_index, "Building block");
5659

60+
let select_start = Instant::now();
5761
let selected = select_attestations(
5862
head_state,
5963
slot,
6064
parent_root,
6165
known_block_roots,
6266
aggregated_payloads,
6367
);
68+
metrics::observe_block_proposal_phase("select_payloads", select_start.elapsed());
69+
70+
let child_payloads_consumed = selected.len();
6471

6572
// Compact: merge proofs sharing the same AttestationData via recursive
6673
// aggregation so each AttestationData appears at most once (leanSpec #510).
74+
let compact_start = Instant::now();
6775
let compacted = compact_attestations(selected, head_state)?;
76+
metrics::observe_block_proposal_phase("compact", compact_start.elapsed());
6877

6978
let (aggregated_attestations, aggregated_signatures): (Vec<_>, Vec<_>) =
7079
compacted.into_iter().unzip();
@@ -80,10 +89,19 @@ pub(crate) fn build_block(
8089
body: BlockBody { attestations },
8190
};
8291
let mut post_state = head_state.clone();
92+
// ethlambda runs the STF once after selection (it projects justification
93+
// incrementally instead of re-running the STF per loop round), so this is
94+
// a single `stf_simulate` observation per build.
95+
let stf_start = Instant::now();
8396
process_slots(&mut post_state, slot)?;
8497
process_block(&mut post_state, &final_block)?;
98+
metrics::observe_block_proposal_phase("stf_simulate", stf_start.elapsed());
8599
final_block.state_root = post_state.hash_tree_root();
86100

101+
metrics::inc_block_proposal_child_payloads_consumed(child_payloads_consumed as u64);
102+
metrics::observe_block_proposal_attestation_data_selected(final_block.body.attestations.len());
103+
metrics::observe_block_proposal_aggregates_selected(aggregated_signatures.len());
104+
87105
let post_checkpoints = PostBlockCheckpoints {
88106
justified: post_state.latest_justified,
89107
finalized: post_state.latest_finalized,
@@ -156,6 +174,7 @@ fn select_attestations(
156174
let (att_data, proofs) = &chain.aggregated_payloads[&data_root];
157175

158176
processed_data_roots.insert(data_root);
177+
metrics::inc_block_proposal_attestation_builds();
159178

160179
let before = selected.len();
161180
extend_proofs_greedily(proofs, &mut selected, att_data);

crates/blockchain/src/metrics.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ pub const ATTESTATION_AGGREGATE_COVERAGE_SECTIONS: &[&str] = &[
2323
/// locally-aggregated pre-merge (`timely`) payloads.
2424
pub const ATTESTATION_AGGREGATE_COVERAGE_DIFF_DIRECTIONS: &[&str] = &["block_only", "timely_only"];
2525

26+
/// Phase labels for `lean_block_proposal_attestation_build_phase_seconds`.
27+
///
28+
/// `select_payloads`: greedy per-`AttestationData` proof selection.
29+
/// `compact`: recursive merge of proofs sharing the same `AttestationData`.
30+
/// `stf_simulate`: the single candidate-block state transition that seals the
31+
/// state root. Unlike leanSpec (which re-runs the STF inside a fixed-point
32+
/// loop), ethlambda projects justification/finalization incrementally during
33+
/// `select_payloads` and runs the STF exactly once, so its `stf_simulate`
34+
/// timing is a single observation per build rather than one per loop round.
35+
pub const BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES: &[&str] =
36+
&["select_payloads", "compact", "stf_simulate"];
37+
2638
// --- Gauges ---
2739

2840
static LEAN_HEAD_SLOT: std::sync::LazyLock<IntGauge> = std::sync::LazyLock::new(|| {
@@ -420,6 +432,62 @@ static LEAN_BLOCK_BUILDING_FAILURES_TOTAL: std::sync::LazyLock<IntCounter> =
420432
register_int_counter!("lean_block_building_failures_total", "Failed block builds").unwrap()
421433
});
422434

435+
// --- Block Proposal Attestation Selection (build_block fixed-point loop) ---
436+
437+
static LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS: std::sync::LazyLock<HistogramVec> =
438+
std::sync::LazyLock::new(|| {
439+
register_histogram_vec!(
440+
"lean_block_proposal_attestation_build_phase_seconds",
441+
"Phase-level time in block-proposal attestation selection: select_payloads (greedy \
442+
per-AttestationData proof pick), compact (recursive merge of proofs per \
443+
AttestationData), stf_simulate (candidate block state transition).",
444+
&["phase"],
445+
vec![
446+
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0
447+
]
448+
)
449+
.unwrap()
450+
});
451+
452+
static LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL: std::sync::LazyLock<IntCounter> =
453+
std::sync::LazyLock::new(|| {
454+
register_int_counter!(
455+
"lean_block_proposal_attestation_builds_total",
456+
"Attestations selected during block-proposal selection (one increment per \
457+
selection-loop round that picks an AttestationData)."
458+
)
459+
.unwrap()
460+
});
461+
462+
static LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL: std::sync::LazyLock<IntCounter> =
463+
std::sync::LazyLock::new(|| {
464+
register_int_counter!(
465+
"lean_block_proposal_child_payloads_consumed_total",
466+
"Child aggregated payloads selected during greedy proof picking (before compaction)."
467+
)
468+
.unwrap()
469+
});
470+
471+
static LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED: std::sync::LazyLock<Histogram> =
472+
std::sync::LazyLock::new(|| {
473+
register_histogram!(
474+
"lean_block_proposal_attestation_data_selected",
475+
"Distinct AttestationData entries in the proposal block body",
476+
vec![0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0]
477+
)
478+
.unwrap()
479+
});
480+
481+
static LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED: std::sync::LazyLock<Histogram> =
482+
std::sync::LazyLock::new(|| {
483+
register_histogram!(
484+
"lean_block_proposal_aggregates_selected",
485+
"Aggregated signature proofs in the proposal result after compaction",
486+
vec![0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0]
487+
)
488+
.unwrap()
489+
});
490+
423491
// --- Sync Status ---
424492

425493
/// Node synchronization status.
@@ -512,6 +580,12 @@ pub fn init() {
512580
std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_TIME_SECONDS);
513581
std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_SUCCESS_TOTAL);
514582
std::sync::LazyLock::force(&LEAN_BLOCK_BUILDING_FAILURES_TOTAL);
583+
// Block proposal attestation selection
584+
std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS);
585+
std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL);
586+
std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL);
587+
std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED);
588+
std::sync::LazyLock::force(&LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED);
515589
// Sync status
516590
std::sync::LazyLock::force(&LEAN_NODE_SYNC_STATUS);
517591
}
@@ -739,6 +813,34 @@ pub fn inc_block_building_failures() {
739813
LEAN_BLOCK_BUILDING_FAILURES_TOTAL.inc();
740814
}
741815

816+
/// Observe the duration of a block-proposal attestation-selection phase.
817+
/// `phase` must be one of [`BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASES`].
818+
pub fn observe_block_proposal_phase(phase: &str, elapsed: Duration) {
819+
LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILD_PHASE_SECONDS
820+
.with_label_values(&[phase])
821+
.observe(elapsed.as_secs_f64());
822+
}
823+
824+
/// Increment the completed block-proposal attestation selection runs counter.
825+
pub fn inc_block_proposal_attestation_builds() {
826+
LEAN_BLOCK_PROPOSAL_ATTESTATION_BUILDS_TOTAL.inc();
827+
}
828+
829+
/// Increment the greedily-selected child payloads counter (before compaction).
830+
pub fn inc_block_proposal_child_payloads_consumed(count: u64) {
831+
LEAN_BLOCK_PROPOSAL_CHILD_PAYLOADS_CONSUMED_TOTAL.inc_by(count);
832+
}
833+
834+
/// Observe the number of distinct `AttestationData` entries in the proposal block body.
835+
pub fn observe_block_proposal_attestation_data_selected(count: usize) {
836+
LEAN_BLOCK_PROPOSAL_ATTESTATION_DATA_SELECTED.observe(count as f64);
837+
}
838+
839+
/// Observe the number of aggregated signature proofs in the proposal result after compaction.
840+
pub fn observe_block_proposal_aggregates_selected(count: usize) {
841+
LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED.observe(count as f64);
842+
}
843+
742844
/// Set the node sync status. Sets the given status label to 1 and all others to 0.
743845
pub fn set_node_sync_status(status: SyncStatus) {
744846
let active = status.as_str();

docs/metrics.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ The exposed metrics follow [the leanMetrics specification](https://github.com/le
3939
| `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 ||
4040
| `lean_block_building_success_total` | Counter | Successful block builds | On block production | | ||
4141
| `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 | | ||
42+
| `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 ||
43+
| `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 | | ||
44+
| `lean_block_proposal_child_payloads_consumed_total` | Counter | Child aggregated payloads selected during greedy proof picking (before compaction) | On block production | | ||
45+
| `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 ||
46+
| `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 ||
4247

4348
## Fork-Choice Metrics
4449

0 commit comments

Comments
 (0)