Skip to content

Commit 88edf2f

Browse files
authored
feat: add rest of fork choice metrics (#52)
* 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. * docs: update metrics checklist
1 parent 13827cc commit 88edf2f

3 files changed

Lines changed: 107 additions & 7 deletions

File tree

crates/blockchain/src/metrics.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,48 @@ pub fn set_node_start_time() {
8484
.as_secs();
8585
LEAN_NODE_START_TIME_SECONDS.set(timestamp as i64);
8686
}
87+
88+
/// Increment the valid attestations counter.
89+
pub fn inc_attestations_valid(source: &str) {
90+
static LEAN_ATTESTATIONS_VALID_TOTAL: std::sync::LazyLock<prometheus::IntCounterVec> =
91+
std::sync::LazyLock::new(|| {
92+
prometheus::register_int_counter_vec!(
93+
"lean_attestations_valid_total",
94+
"Count of valid attestations",
95+
&["source"]
96+
)
97+
.unwrap()
98+
});
99+
LEAN_ATTESTATIONS_VALID_TOTAL
100+
.with_label_values(&[source])
101+
.inc();
102+
}
103+
104+
/// Increment the invalid attestations counter.
105+
pub fn inc_attestations_invalid(source: &str) {
106+
static LEAN_ATTESTATIONS_INVALID_TOTAL: std::sync::LazyLock<prometheus::IntCounterVec> =
107+
std::sync::LazyLock::new(|| {
108+
prometheus::register_int_counter_vec!(
109+
"lean_attestations_invalid_total",
110+
"Count of invalid attestations",
111+
&["source"]
112+
)
113+
.unwrap()
114+
});
115+
LEAN_ATTESTATIONS_INVALID_TOTAL
116+
.with_label_values(&[source])
117+
.inc();
118+
}
119+
120+
/// Increment the fork choice reorgs counter.
121+
pub fn inc_fork_choice_reorgs() {
122+
static LEAN_FORK_CHOICE_REORGS_TOTAL: std::sync::LazyLock<prometheus::IntCounter> =
123+
std::sync::LazyLock::new(|| {
124+
prometheus::register_int_counter!(
125+
"lean_fork_choice_reorgs_total",
126+
"Count of fork choice reorganizations"
127+
)
128+
.unwrap()
129+
});
130+
LEAN_FORK_CHOICE_REORGS_TOTAL.inc();
131+
}

crates/blockchain/src/store.rs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use ethlambda_types::{
1616
};
1717
use tracing::{info, trace, warn};
1818

19-
use crate::SECONDS_PER_SLOT;
19+
use crate::{SECONDS_PER_SLOT, metrics};
2020

2121
const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3;
2222

@@ -175,13 +175,20 @@ impl Store {
175175
}
176176

177177
pub fn update_head(&mut self) {
178-
let head = ethlambda_fork_choice::compute_lmd_ghost_head(
178+
let old_head = self.head;
179+
let new_head = ethlambda_fork_choice::compute_lmd_ghost_head(
179180
self.latest_justified.root,
180181
&self.blocks,
181182
&self.latest_known_attestations,
182183
0,
183184
);
184-
self.head = head;
185+
186+
if is_reorg(old_head, new_head, &self.blocks) {
187+
metrics::inc_fork_choice_reorgs();
188+
info!(%old_head, %new_head, "Fork choice reorg detected");
189+
}
190+
191+
self.head = new_head;
185192
}
186193

187194
pub fn update_safe_target(&mut self) {
@@ -308,7 +315,10 @@ impl Store {
308315
validator_id,
309316
data: signed_attestation.message,
310317
};
311-
self.validate_attestation(&attestation)?;
318+
if let Err(err) = self.validate_attestation(&attestation) {
319+
metrics::inc_attestations_invalid("gossip");
320+
return Err(err);
321+
}
312322
let target = attestation.data.target;
313323
let target_state = self
314324
.states
@@ -339,6 +349,7 @@ impl Store {
339349
let signature = ValidatorSignature::from_bytes(&signed_attestation.signature)
340350
.map_err(|_| StoreError::SignatureDecodingFailed)?;
341351
self.gossip_signatures.insert(signature_key, signature);
352+
metrics::inc_attestations_valid("gossip");
342353
}
343354
Ok(())
344355
}
@@ -501,6 +512,9 @@ impl Store {
501512
// TODO: validate attestations before processing
502513
if let Err(err) = self.on_attestation(attestation, true) {
503514
warn!(%slot, %validator_id, %err, "Invalid attestation in block");
515+
metrics::inc_attestations_invalid("block");
516+
} else {
517+
metrics::inc_attestations_valid("block");
504518
}
505519
}
506520
}
@@ -716,6 +730,47 @@ impl Store {
716730
}
717731
}
718732

733+
/// Check if a head change represents a reorg.
734+
///
735+
/// A reorg occurs when the chains diverge - i.e., when walking back from the higher
736+
/// slot head to the lower slot head's slot, we don't arrive at the lower slot head.
737+
fn is_reorg(old_head: H256, new_head: H256, blocks: &HashMap<H256, Block>) -> bool {
738+
if new_head == old_head {
739+
return false;
740+
}
741+
742+
let Some(old_head_block) = blocks.get(&old_head) else {
743+
return false;
744+
};
745+
746+
let Some(new_head_block) = blocks.get(&new_head) else {
747+
return false;
748+
};
749+
750+
let old_slot = old_head_block.slot;
751+
let new_slot = new_head_block.slot;
752+
753+
// Determine which head has the higher slot and walk back from it
754+
let (mut current_root, target_slot, target_root) = if new_slot >= old_slot {
755+
(new_head, old_slot, old_head)
756+
} else {
757+
(old_head, new_slot, new_head)
758+
};
759+
760+
// Walk back through the chain until we reach the target slot
761+
while let Some(current_block) = blocks.get(&current_root) {
762+
if current_block.slot <= target_slot {
763+
// We've reached the target slot - check if we're at the target block
764+
return current_root != target_root;
765+
}
766+
current_root = current_block.parent_root;
767+
}
768+
769+
// Couldn't walk back far enough (missing blocks in chain)
770+
// Conservative: assume no reorg if we can't determine
771+
false
772+
}
773+
719774
/// Errors that can occur during Store operations.
720775
#[derive(Debug, thiserror::Error)]
721776
pub enum StoreError {

docs/metrics.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ The exposed metrics follow [the leanMetrics specification](https://github.com/le
3535
| `lean_current_slot` | Gauge | Current slot of the lean chain | On scrape | | | ✅(*) |
3636
| `lean_safe_target_slot` | Gauge | Safe target slot | On safe target update | | ||
3737
|`lean_fork_choice_block_processing_time_seconds`| Histogram | Time taken to process block | On fork choice process block | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 ||
38-
|`lean_attestations_valid_total`| Counter | Total number of valid attestations | On validate attestation | source=block,gossip | | |
39-
|`lean_attestations_invalid_total`| Counter | Total number of invalid attestations | On validate attestation | source=block,gossip | | |
38+
|`lean_attestations_valid_total`| Counter | Total number of valid attestations | On validate attestation | source=block,gossip | | |
39+
|`lean_attestations_invalid_total`| Counter | Total number of invalid attestations | On validate attestation | source=block,gossip | | |
4040
|`lean_attestation_validation_time_seconds`| Histogram | Time taken to validate attestation | On validate attestation | | 0.005, 0.01, 0.025, 0.05, 0.1, 1 ||
41-
| `lean_fork_choice_reorgs_total` | Counter | Total number of fork choice reorgs | On fork choice reorg | | | |
41+
| `lean_fork_choice_reorgs_total` | Counter | Total number of fork choice reorgs | On fork choice reorg | | | |
4242
| `lean_fork_choice_reorg_depth` | Histogram | Depth of fork choice reorgs (in blocks) | On fork choice reorg | | 1, 2, 3, 5, 7, 10, 20, 30, 50, 100 ||
4343

4444
## State Transition Metrics

0 commit comments

Comments
 (0)