From 0f53a6ef630fd1f8b110c1c28990e030ce2e5a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:11:12 -0300 Subject: [PATCH 1/4] feat: add state transition histogram metrics Add Group 7 metrics from the leanMetrics specification: - lean_state_transition_time_seconds: Duration of entire state transition - lean_state_transition_slots_processing_time_seconds: Duration to process slots - lean_state_transition_block_processing_time_seconds: Duration to process a block - lean_state_transition_attestations_processing_time_seconds: Duration to process attestations Each function is instrumented with std::time::Instant to measure execution time and record to the appropriate histogram. --- crates/blockchain/state_transition/src/lib.rs | 12 ++++ .../state_transition/src/metrics.rs | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index 742ff67..bb7ca46 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -29,6 +29,8 @@ pub enum Error { /// /// Similar to the spec's `State.state_transition`: https://github.com/leanEthereum/leanSpec/blob/bf0f606a75095cf1853529bc770516b1464d9716/src/lean_spec/subspecs/containers/state/state.py#L569 pub fn state_transition(state: &mut State, block: &Block) -> Result<(), Error> { + let start = std::time::Instant::now(); + process_slots(state, block.slot)?; process_block(state, block)?; @@ -51,11 +53,14 @@ pub fn state_transition(state: &mut State, block: &Block) -> Result<(), Error> { computed: computed_state_root, }); } + metrics::observe_state_transition_time(start.elapsed().as_secs_f64()); Ok(()) } /// Advance the state through empty slots up to, but not including, target_slot. pub fn process_slots(state: &mut State, target_slot: u64) -> Result<(), Error> { + let start = std::time::Instant::now(); + if state.slot >= target_slot { return Err(Error::StateSlotIsNewer { target_slot, @@ -69,13 +74,18 @@ pub fn process_slots(state: &mut State, target_slot: u64) -> Result<(), Error> { let slots_processed = target_slot - state.slot; state.slot = target_slot; metrics::inc_slots_processed(slots_processed); + metrics::observe_slots_processing_time(start.elapsed().as_secs_f64()); Ok(()) } /// Apply full block processing including header and body. pub fn process_block(state: &mut State, block: &Block) -> Result<(), Error> { + let start = std::time::Instant::now(); + process_block_header(state, block)?; process_attestations(state, &block.body.attestations)?; + + metrics::observe_block_processing_time(start.elapsed().as_secs_f64()); Ok(()) } @@ -184,6 +194,7 @@ fn process_attestations( state: &mut State, attestations: &AggregatedAttestations, ) -> Result<(), Error> { + let start = std::time::Instant::now(); let validator_count = state.validators.len(); let mut attestations_processed: u64 = 0; let mut justifications: HashMap> = state @@ -338,6 +349,7 @@ fn process_attestations( .expect("justifications_roots limit exceeded"); state.justifications_validators = justifications_validators; metrics::inc_attestations_processed(attestations_processed); + metrics::observe_attestations_processing_time(start.elapsed().as_secs_f64()); Ok(()) } diff --git a/crates/blockchain/state_transition/src/metrics.rs b/crates/blockchain/state_transition/src/metrics.rs index 59785f5..7859ea2 100644 --- a/crates/blockchain/state_transition/src/metrics.rs +++ b/crates/blockchain/state_transition/src/metrics.rs @@ -44,3 +44,62 @@ pub fn inc_attestations_processed(count: u64) { pub fn inc_finalizations(result: &str) { LEAN_FINALIZATIONS_TOTAL.with_label_values(&[result]).inc(); } + +static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { + prometheus::register_histogram!( + "lean_state_transition_time_seconds", + "Duration of the entire state transition", + vec![0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0] + ) + .unwrap() +}); + +static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = + LazyLock::new(|| { + prometheus::register_histogram!( + "lean_state_transition_slots_processing_time_seconds", + "Duration to process slots", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + +static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock = + LazyLock::new(|| { + prometheus::register_histogram!( + "lean_state_transition_block_processing_time_seconds", + "Duration to process a block in state transition", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + +static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock = + LazyLock::new(|| { + prometheus::register_histogram!( + "lean_state_transition_attestations_processing_time_seconds", + "Duration to process attestations", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + +/// Record state transition time in seconds. +pub fn observe_state_transition_time(duration_secs: f64) { + LEAN_STATE_TRANSITION_TIME_SECONDS.observe(duration_secs); +} + +/// Record slots processing time in seconds. +pub fn observe_slots_processing_time(duration_secs: f64) { + LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS.observe(duration_secs); +} + +/// Record block processing time in seconds. +pub fn observe_block_processing_time(duration_secs: f64) { + LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS.observe(duration_secs); +} + +/// Record attestations processing time in seconds. +pub fn observe_attestations_processing_time(duration_secs: f64) { + LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS.observe(duration_secs); +} From a6152fa466d9fdd0d0ab11650100a8764ceed5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:04:30 -0300 Subject: [PATCH 2/4] refactor: use guard pattern --- crates/blockchain/state_transition/src/lib.rs | 12 ++-- .../state_transition/src/metrics.rs | 58 +++++++++++++------ 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index bb7ca46..6e5991f 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -29,7 +29,7 @@ pub enum Error { /// /// Similar to the spec's `State.state_transition`: https://github.com/leanEthereum/leanSpec/blob/bf0f606a75095cf1853529bc770516b1464d9716/src/lean_spec/subspecs/containers/state/state.py#L569 pub fn state_transition(state: &mut State, block: &Block) -> Result<(), Error> { - let start = std::time::Instant::now(); + let _timing = metrics::time_state_transition(); process_slots(state, block.slot)?; process_block(state, block)?; @@ -53,13 +53,12 @@ pub fn state_transition(state: &mut State, block: &Block) -> Result<(), Error> { computed: computed_state_root, }); } - metrics::observe_state_transition_time(start.elapsed().as_secs_f64()); Ok(()) } /// Advance the state through empty slots up to, but not including, target_slot. pub fn process_slots(state: &mut State, target_slot: u64) -> Result<(), Error> { - let start = std::time::Instant::now(); + let _timing = metrics::time_slots_processing(); if state.slot >= target_slot { return Err(Error::StateSlotIsNewer { @@ -74,18 +73,16 @@ pub fn process_slots(state: &mut State, target_slot: u64) -> Result<(), Error> { let slots_processed = target_slot - state.slot; state.slot = target_slot; metrics::inc_slots_processed(slots_processed); - metrics::observe_slots_processing_time(start.elapsed().as_secs_f64()); Ok(()) } /// Apply full block processing including header and body. pub fn process_block(state: &mut State, block: &Block) -> Result<(), Error> { - let start = std::time::Instant::now(); + let _timing = metrics::time_block_processing(); process_block_header(state, block)?; process_attestations(state, &block.body.attestations)?; - metrics::observe_block_processing_time(start.elapsed().as_secs_f64()); Ok(()) } @@ -194,7 +191,7 @@ fn process_attestations( state: &mut State, attestations: &AggregatedAttestations, ) -> Result<(), Error> { - let start = std::time::Instant::now(); + let _timing = metrics::time_attestations_processing(); let validator_count = state.validators.len(); let mut attestations_processed: u64 = 0; let mut justifications: HashMap> = state @@ -349,7 +346,6 @@ fn process_attestations( .expect("justifications_roots limit exceeded"); state.justifications_validators = justifications_validators; metrics::inc_attestations_processed(attestations_processed); - metrics::observe_attestations_processing_time(start.elapsed().as_secs_f64()); Ok(()) } diff --git a/crates/blockchain/state_transition/src/metrics.rs b/crates/blockchain/state_transition/src/metrics.rs index 7859ea2..3e3c8fb 100644 --- a/crates/blockchain/state_transition/src/metrics.rs +++ b/crates/blockchain/state_transition/src/metrics.rs @@ -1,8 +1,11 @@ //! Prometheus metrics for state transition. use std::sync::LazyLock; +use std::time::Instant; -use prometheus::{IntCounter, IntCounterVec, register_int_counter, register_int_counter_vec}; +use prometheus::{ + Histogram, IntCounter, IntCounterVec, register_int_counter, register_int_counter_vec, +}; static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter!( @@ -45,7 +48,7 @@ pub fn inc_finalizations(result: &str) { LEAN_FINALIZATIONS_TOTAL.with_label_values(&[result]).inc(); } -static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { +static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { prometheus::register_histogram!( "lean_state_transition_time_seconds", "Duration of the entire state transition", @@ -54,7 +57,7 @@ static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = Laz .unwrap() }); -static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = +static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { prometheus::register_histogram!( "lean_state_transition_slots_processing_time_seconds", @@ -64,7 +67,7 @@ static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = +static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { prometheus::register_histogram!( "lean_state_transition_block_processing_time_seconds", @@ -74,7 +77,7 @@ static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock = +static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { prometheus::register_histogram!( "lean_state_transition_attestations_processing_time_seconds", @@ -84,22 +87,43 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock Self { + Self { + histogram, + start: Instant::now(), + } + } } -/// Record block processing time in seconds. -pub fn observe_block_processing_time(duration_secs: f64) { - LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS.observe(duration_secs); +impl Drop for TimingGuard { + fn drop(&mut self) { + self.histogram.observe(self.start.elapsed().as_secs_f64()); + } } -/// Record attestations processing time in seconds. -pub fn observe_attestations_processing_time(duration_secs: f64) { - LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS.observe(duration_secs); +/// Start timing state transition. Records duration when the guard is dropped. +pub fn time_state_transition() -> TimingGuard { + TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS) +} + +/// Start timing slots processing. Records duration when the guard is dropped. +pub fn time_slots_processing() -> TimingGuard { + TimingGuard::new(&LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS) +} + +/// Start timing block processing. Records duration when the guard is dropped. +pub fn time_block_processing() -> TimingGuard { + TimingGuard::new(&LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS) +} + +/// Start timing attestations processing. Records duration when the guard is dropped. +pub fn time_attestations_processing() -> TimingGuard { + TimingGuard::new(&LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS) } From fe4c568b7eb24c13b8ed16c2ffa294df5ceb6870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:05:07 -0300 Subject: [PATCH 3/4] feat: register failed finalization --- crates/blockchain/state_transition/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index 6e5991f..2c9b4f2 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -316,6 +316,8 @@ fn process_attestations( let slot = root_to_slot[root]; slot > state.latest_finalized.slot }); + } else { + metrics::inc_finalizations("error"); } } } From 79a1a8b53de694c8f6b5c34e4882137adb2862da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:14:24 -0300 Subject: [PATCH 4/4] docs: update metrics checklist --- docs/metrics.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index 3ab6024..5bfbd1e 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -48,12 +48,12 @@ The exposed metrics follow [the leanMetrics specification](https://github.com/le | `lean_latest_justified_slot` | Gauge | Latest justified slot | On state transition | | | ✅ | | `lean_latest_finalized_slot` | Gauge | Latest finalized slot | On state transition | | | ✅ | | `lean_finalizations_total` | Counter | Total number of finalization attempts | On finalization attempt | result=success,error | | ✅ | -|`lean_state_transition_time_seconds`| Histogram | Time to process state transition | On state transition | | 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4 | □ | +|`lean_state_transition_time_seconds`| Histogram | Time to process state transition | On state transition | | 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4 | ✅ | |`lean_state_transition_slots_processed_total`| Counter | Total number of processed slots | On state transition process slots | | | ✅ | -|`lean_state_transition_slots_processing_time_seconds`| Histogram | Time taken to process slots | On state transition process slots | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | -|`lean_state_transition_block_processing_time_seconds`| Histogram | Time taken to process block | On state transition process block | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +|`lean_state_transition_slots_processing_time_seconds`| Histogram | Time taken to process slots | On state transition process slots | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | ✅ | +|`lean_state_transition_block_processing_time_seconds`| Histogram | Time taken to process block | On state transition process block | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | ✅ | |`lean_state_transition_attestations_processed_total`| Counter | Total number of processed attestations | On state transition process attestations | | | ✅ | -|`lean_state_transition_attestations_processing_time_seconds`| Histogram | Time taken to process attestations | On state transition process attestations | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | □ | +|`lean_state_transition_attestations_processing_time_seconds`| Histogram | Time taken to process attestations | On state transition process attestations | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 | ✅ | ## Validator Metrics