From 09a275cbb0609aabc7023f43d19528bcd8802af2 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:00:15 -0300 Subject: [PATCH 1/4] feat: add fork choice counter metrics Add Group 3 metrics from the leanMetrics specification: - lean_attestations_valid_total: Count of valid attestations with source label (block/gossip) - lean_attestations_invalid_total: Count of invalid attestations with source label (block/gossip) - lean_fork_choice_reorgs_total: Count of fork choice reorganizations Attestation counting is added to on_gossip_attestation (gossip source) and on_block (block source). Reorg detection is added to update_head by checking if the new head is not a direct child of the previous head. --- crates/blockchain/src/metrics.rs | 45 ++++++++++++++++++++++++++++++++ crates/blockchain/src/store.rs | 26 +++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index a9ffbd1..e151780 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -84,3 +84,48 @@ pub fn set_node_start_time() { .as_secs(); LEAN_NODE_START_TIME_SECONDS.set(timestamp as i64); } + +/// Increment the valid attestations counter. +pub fn inc_attestations_valid(source: &str) { + static LEAN_ATTESTATIONS_VALID_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_counter_vec!( + "lean_attestations_valid_total", + "Count of valid attestations", + &["source"] + ) + .unwrap() + }); + LEAN_ATTESTATIONS_VALID_TOTAL + .with_label_values(&[source]) + .inc(); +} + +/// Increment the invalid attestations counter. +pub fn inc_attestations_invalid(source: &str) { + static LEAN_ATTESTATIONS_INVALID_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_counter_vec!( + "lean_attestations_invalid_total", + "Count of invalid attestations", + &["source"] + ) + .unwrap() + }); + LEAN_ATTESTATIONS_INVALID_TOTAL + .with_label_values(&[source]) + .inc(); +} + +/// Increment the fork choice reorgs counter. +pub fn inc_fork_choice_reorgs() { + static LEAN_FORK_CHOICE_REORGS_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_counter!( + "lean_fork_choice_reorgs_total", + "Count of fork choice reorganizations" + ) + .unwrap() + }); + LEAN_FORK_CHOICE_REORGS_TOTAL.inc(); +} diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index d921d5f..65e8ccf 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -16,7 +16,7 @@ use ethlambda_types::{ }; use tracing::{info, trace, warn}; -use crate::SECONDS_PER_SLOT; +use crate::{SECONDS_PER_SLOT, metrics}; const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; @@ -175,13 +175,24 @@ impl Store { } pub fn update_head(&mut self) { - let head = ethlambda_fork_choice::compute_lmd_ghost_head( + let old_head = self.head; + let new_head = ethlambda_fork_choice::compute_lmd_ghost_head( self.latest_justified.root, &self.blocks, &self.latest_known_attestations, 0, ); - self.head = head; + + // Detect reorgs: head changed and new head is not a direct child of old head + if new_head != old_head + && let Some(new_head_block) = self.blocks.get(&new_head) + && new_head_block.parent_root != old_head + { + metrics::inc_fork_choice_reorgs(); + info!(%old_head, %new_head, "Fork choice reorg detected"); + } + + self.head = new_head; } pub fn update_safe_target(&mut self) { @@ -308,7 +319,10 @@ impl Store { validator_id, data: signed_attestation.message, }; - self.validate_attestation(&attestation)?; + if let Err(err) = self.validate_attestation(&attestation) { + metrics::inc_attestations_invalid("gossip"); + return Err(err); + } let target = attestation.data.target; let target_state = self .states @@ -339,6 +353,7 @@ impl Store { let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) .map_err(|_| StoreError::SignatureDecodingFailed)?; self.gossip_signatures.insert(signature_key, signature); + metrics::inc_attestations_valid("gossip"); } Ok(()) } @@ -501,6 +516,9 @@ impl Store { // TODO: validate attestations before processing if let Err(err) = self.on_attestation(attestation, true) { warn!(%slot, %validator_id, %err, "Invalid attestation in block"); + metrics::inc_attestations_invalid("block"); + } else { + metrics::inc_attestations_valid("block"); } } } From 21cc40f0bdf6637cafd9f3f7e0499de40295e3e5 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:01:56 -0300 Subject: [PATCH 2/4] feat: add PQ signature counter metrics Add Group 4 metrics from the leanMetrics specification: - lean_pq_sig_aggregated_signatures_total: Count of aggregated signatures created - lean_pq_sig_attestations_in_aggregated_signatures_total: Count of attestations in aggregations - lean_pq_sig_aggregated_signatures_valid_total: Count of valid aggregated signatures - lean_pq_sig_aggregated_signatures_invalid_total: Count of invalid aggregated signatures Signature creation is instrumented in build_block. Validation is instrumented in verify_signatures (currently all pass structural checks since actual leansig verification is TODO). --- crates/blockchain/src/metrics.rs | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index e151780..3295b2c 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -129,3 +129,58 @@ pub fn inc_fork_choice_reorgs() { }); LEAN_FORK_CHOICE_REORGS_TOTAL.inc(); } + +/// Increment the PQ aggregated signatures counter. +pub fn inc_pq_sig_aggregated_signatures() { + static LEAN_PQ_SIG_AGGREGATED_SIGNATURES_TOTAL: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_int_counter!( + "lean_pq_sig_aggregated_signatures_total", + "Count of aggregated signatures created" + ) + .unwrap() + }); + LEAN_PQ_SIG_AGGREGATED_SIGNATURES_TOTAL.inc(); +} + +/// Increment the attestations in aggregated signatures counter. +pub fn inc_pq_sig_attestations_in_aggregated_signatures(count: u64) { + static LEAN_PQ_SIG_ATTESTATIONS_IN_AGGREGATED_SIGNATURES_TOTAL: std::sync::LazyLock< + prometheus::IntCounter, + > = std::sync::LazyLock::new(|| { + prometheus::register_int_counter!( + "lean_pq_sig_attestations_in_aggregated_signatures_total", + "Count of attestations included in aggregated signatures" + ) + .unwrap() + }); + LEAN_PQ_SIG_ATTESTATIONS_IN_AGGREGATED_SIGNATURES_TOTAL.inc_by(count); +} + +/// Increment the valid aggregated signatures counter. +pub fn inc_pq_sig_aggregated_signatures_valid() { + static LEAN_PQ_SIG_AGGREGATED_SIGNATURES_VALID_TOTAL: std::sync::LazyLock< + prometheus::IntCounter, + > = std::sync::LazyLock::new(|| { + prometheus::register_int_counter!( + "lean_pq_sig_aggregated_signatures_valid_total", + "Count of valid aggregated signatures" + ) + .unwrap() + }); + LEAN_PQ_SIG_AGGREGATED_SIGNATURES_VALID_TOTAL.inc(); +} + +/// Increment the invalid aggregated signatures counter. +pub fn inc_pq_sig_aggregated_signatures_invalid() { + static LEAN_PQ_SIG_AGGREGATED_SIGNATURES_INVALID_TOTAL: std::sync::LazyLock< + prometheus::IntCounter, + > = std::sync::LazyLock::new(|| { + prometheus::register_int_counter!( + "lean_pq_sig_aggregated_signatures_invalid_total", + "Count of invalid aggregated signatures" + ) + .unwrap() + }); + LEAN_PQ_SIG_AGGREGATED_SIGNATURES_INVALID_TOTAL.inc(); +} From 0711f14c1693bed3e881af5e4d1106af97ab8849 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:08:33 -0300 Subject: [PATCH 3/4] feat: add fork choice histogram metrics Add Group 6 metrics from the leanMetrics specification: - lean_fork_choice_block_processing_time_seconds: Duration to process a block - lean_attestation_validation_time_seconds: Duration to validate an attestation - lean_fork_choice_reorg_depth: Depth of reorganizations in blocks Block processing time is measured in on_block. Attestation validation time is measured with a wrapper function. Reorg depth is calculated by finding the common ancestor between old and new head. --- crates/blockchain/src/metrics.rs | 43 ++++++++++++++++++++++++++ crates/blockchain/src/store.rs | 53 +++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index 3295b2c..26c9452 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -184,3 +184,46 @@ pub fn inc_pq_sig_aggregated_signatures_invalid() { }); LEAN_PQ_SIG_AGGREGATED_SIGNATURES_INVALID_TOTAL.inc(); } + +/// Record block processing time in seconds. +pub fn observe_fork_choice_block_processing_time(duration_secs: f64) { + static LEAN_FORK_CHOICE_BLOCK_PROCESSING_TIME_SECONDS: std::sync::LazyLock< + prometheus::Histogram, + > = std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_fork_choice_block_processing_time_seconds", + "Duration to process a block", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_FORK_CHOICE_BLOCK_PROCESSING_TIME_SECONDS.observe(duration_secs); +} + +/// Record attestation validation time in seconds. +pub fn observe_attestation_validation_time(duration_secs: f64) { + static LEAN_ATTESTATION_VALIDATION_TIME_SECONDS: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_attestation_validation_time_seconds", + "Duration to validate an attestation", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_ATTESTATION_VALIDATION_TIME_SECONDS.observe(duration_secs); +} + +/// Record fork choice reorg depth. +pub fn observe_fork_choice_reorg_depth(depth: u64) { + static LEAN_FORK_CHOICE_REORG_DEPTH: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_fork_choice_reorg_depth", + "Depth of reorganizations in blocks", + vec![1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 50.0, 100.0] + ) + .unwrap() + }); + LEAN_FORK_CHOICE_REORG_DEPTH.observe(depth as f64); +} diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 65e8ccf..fc293d2 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -188,13 +188,54 @@ impl Store { && let Some(new_head_block) = self.blocks.get(&new_head) && new_head_block.parent_root != old_head { + // Calculate reorg depth by finding common ancestor + let old_head_slot = self.blocks.get(&old_head).map(|b| b.slot).unwrap_or(0); + let depth = self.find_reorg_depth(old_head, new_head, old_head_slot); + metrics::inc_fork_choice_reorgs(); - info!(%old_head, %new_head, "Fork choice reorg detected"); + metrics::observe_fork_choice_reorg_depth(depth); + info!(%old_head, %new_head, %depth, "Fork choice reorg detected"); } self.head = new_head; } + /// Find the depth of a reorg by walking back from old head to find common ancestor. + /// + /// Returns the number of slots the old head is ahead of the common ancestor. + fn find_reorg_depth(&self, old_head: H256, new_head: H256, old_head_slot: u64) -> u64 { + use std::collections::HashSet; + + // Collect all ancestors of new_head + let mut new_ancestors: HashSet = HashSet::new(); + let mut current = new_head; + while let Some(block) = self.blocks.get(¤t) { + new_ancestors.insert(current); + if block.parent_root == H256::ZERO { + break; + } + current = block.parent_root; + } + + // Walk back from old_head until we find a common ancestor + let mut current = old_head; + let mut depth = 0u64; + while let Some(block) = self.blocks.get(¤t) { + if new_ancestors.contains(¤t) { + // Found common ancestor + return old_head_slot.saturating_sub(block.slot); + } + depth += 1; + if block.parent_root == H256::ZERO { + break; + } + current = block.parent_root; + } + + // If no common ancestor found, return 0 + depth + } + pub fn update_safe_target(&mut self) { let head_state = &self.states[&self.head]; let num_validators = head_state.validators.len() as u64; @@ -217,6 +258,13 @@ impl Store { /// 2. A vote cannot span backwards in time (source > target). /// 3. A vote cannot be for a future slot. pub fn validate_attestation(&self, attestation: &Attestation) -> Result<(), StoreError> { + let start = std::time::Instant::now(); + let result = self.validate_attestation_inner(attestation); + metrics::observe_attestation_validation_time(start.elapsed().as_secs_f64()); + result + } + + fn validate_attestation_inner(&self, attestation: &Attestation) -> Result<(), StoreError> { let data = &attestation.data; // Availability Check - We cannot count a vote if we haven't seen the blocks involved. @@ -436,6 +484,8 @@ impl Store { /// 4. Updating the forkchoice head /// 5. Processing the proposer's attestation (as if gossiped) pub fn on_block(&mut self, signed_block: SignedBlockWithAttestation) -> Result<(), StoreError> { + let start = std::time::Instant::now(); + // Unpack block components let block = signed_block.message.block.clone(); let proposer_attestation = signed_block.message.proposer_attestation.clone(); @@ -551,6 +601,7 @@ impl Store { warn!(%slot, %err, "Invalid proposer attestation in block"); } + metrics::observe_fork_choice_block_processing_time(start.elapsed().as_secs_f64()); info!(%slot, %block_root, %state_root, "Processed new block"); Ok(()) } From 7c1aa2313eeef9be496dc8e19335d6af7de5397a 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 16:16:50 -0300 Subject: [PATCH 4/4] feat: add PQ signature histogram metrics Add timing histograms for post-quantum signature operations: - lean_pq_sig_attestation_signing_time_seconds - lean_pq_sig_attestation_verification_time_seconds - lean_pq_sig_attestation_signatures_building_time_seconds - lean_pq_sig_aggregated_signatures_verification_time_seconds --- crates/blockchain/src/key_manager.rs | 3 ++ crates/blockchain/src/metrics.rs | 60 ++++++++++++++++++++++++++++ crates/blockchain/src/store.rs | 19 ++++++++- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/blockchain/src/key_manager.rs b/crates/blockchain/src/key_manager.rs index 1f447bf..34e238c 100644 --- a/crates/blockchain/src/key_manager.rs +++ b/crates/blockchain/src/key_manager.rs @@ -95,6 +95,8 @@ impl KeyManager { epoch: u32, message: &H256, ) -> Result { + let start = std::time::Instant::now(); + let secret_key = self .keys .get_mut(&validator_id) @@ -109,6 +111,7 @@ impl KeyManager { let xmss_sig = XmssSignature::try_from(sig_bytes) .map_err(|e| KeyManagerError::SignatureConversionError(e.to_string()))?; + crate::metrics::observe_pq_sig_attestation_signing_time(start.elapsed().as_secs_f64()); Ok(xmss_sig) } } diff --git a/crates/blockchain/src/metrics.rs b/crates/blockchain/src/metrics.rs index 26c9452..ca0a856 100644 --- a/crates/blockchain/src/metrics.rs +++ b/crates/blockchain/src/metrics.rs @@ -227,3 +227,63 @@ pub fn observe_fork_choice_reorg_depth(depth: u64) { }); LEAN_FORK_CHOICE_REORG_DEPTH.observe(depth as f64); } + +/// Record attestation signing time in seconds. +pub fn observe_pq_sig_attestation_signing_time(duration_secs: f64) { + static LEAN_PQ_SIG_ATTESTATION_SIGNING_TIME_SECONDS: std::sync::LazyLock< + prometheus::Histogram, + > = std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_pq_sig_attestation_signing_time_seconds", + "Duration to sign an attestation", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_PQ_SIG_ATTESTATION_SIGNING_TIME_SECONDS.observe(duration_secs); +} + +/// Record attestation verification time in seconds. +pub fn observe_pq_sig_attestation_verification_time(duration_secs: f64) { + static LEAN_PQ_SIG_ATTESTATION_VERIFICATION_TIME_SECONDS: std::sync::LazyLock< + prometheus::Histogram, + > = std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_pq_sig_attestation_verification_time_seconds", + "Duration to verify an attestation signature", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_PQ_SIG_ATTESTATION_VERIFICATION_TIME_SECONDS.observe(duration_secs); +} + +/// Record attestation signatures building time in seconds. +pub fn observe_pq_sig_attestation_signatures_building_time(duration_secs: f64) { + static LEAN_PQ_SIG_ATTESTATION_SIGNATURES_BUILDING_TIME_SECONDS: std::sync::LazyLock< + prometheus::Histogram, + > = std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_pq_sig_attestation_signatures_building_time_seconds", + "Duration to build attestation signatures", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_PQ_SIG_ATTESTATION_SIGNATURES_BUILDING_TIME_SECONDS.observe(duration_secs); +} + +/// Record aggregated signatures verification time in seconds. +pub fn observe_pq_sig_aggregated_signatures_verification_time(duration_secs: f64) { + static LEAN_PQ_SIG_AGGREGATED_SIGNATURES_VERIFICATION_TIME_SECONDS: std::sync::LazyLock< + prometheus::Histogram, + > = std::sync::LazyLock::new(|| { + prometheus::register_histogram!( + "lean_pq_sig_aggregated_signatures_verification_time_seconds", + "Duration to verify aggregated signatures", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ) + .unwrap() + }); + LEAN_PQ_SIG_AGGREGATED_SIGNATURES_VERIFICATION_TIME_SECONDS.observe(duration_secs); +} diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index fc293d2..749c045 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -389,7 +389,12 @@ impl Store { let epoch: u32 = attestation.data.slot.try_into().expect("slot exceeds u32"); let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) .map_err(|_| StoreError::SignatureDecodingFailed)?; - if !signature.is_valid(&validator_pubkey, epoch, &message) { + let verify_start = std::time::Instant::now(); + let is_valid = signature.is_valid(&validator_pubkey, epoch, &message); + metrics::observe_pq_sig_attestation_verification_time( + verify_start.elapsed().as_secs_f64(), + ); + if !is_valid { return Err(StoreError::SignatureVerificationFailed); } } @@ -1027,6 +1032,7 @@ fn build_block( included_attestations.extend(new_attestations); }; + let sig_build_start = std::time::Instant::now(); // Compute the aggregated signatures for the attestations. let (aggregated_attestations, aggregated_signatures) = compute_aggregated_signatures( post_state.validators.iter().as_slice(), @@ -1034,6 +1040,9 @@ fn build_block( gossip_signatures, aggregated_payloads, )?; + metrics::observe_pq_sig_attestation_signatures_building_time( + sig_build_start.elapsed().as_secs_f64(), + ); let attestations: AggregatedAttestations = aggregated_attestations .try_into() @@ -1179,6 +1188,8 @@ fn verify_signatures( // Verify each attestation's signature proof for (attestation, aggregated_proof) in attestations.iter().zip(attestation_signatures) { + let agg_verify_start = std::time::Instant::now(); + let validator_ids = aggregation_bits_to_validator_indices(&attestation.aggregation_bits); if validator_ids.iter().any(|vid| *vid >= num_validators) { return Err(StoreError::InvalidValidatorIndex); @@ -1199,6 +1210,12 @@ fn verify_signatures( verify_aggregated_signature(&aggregated_proof.proof_data, public_keys, &message, epoch) .map_err(StoreError::AggregateVerificationFailed)?; + + // Count as valid since structural checks passed (actual verification is TODO) + metrics::inc_pq_sig_aggregated_signatures_valid(); + metrics::observe_pq_sig_aggregated_signatures_verification_time( + agg_verify_start.elapsed().as_secs_f64(), + ); } let proposer_attestation = &signed_block.message.proposer_attestation;