diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 8be7c89f..e8d83dc4 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet, VecDeque}; +use std::fmt; use std::time::{Duration, Instant, SystemTime}; use ethlambda_network_api::{BlockChainToP2PRef, InitP2P}; @@ -59,6 +60,44 @@ pub use ethlambda_types::block::MAX_ATTESTATIONS_DATA; /// See: leanSpec PR #682. pub const GOSSIP_DISPARITY_INTERVALS: u64 = 1; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SlotInterval { + BlockPublication, + AttestationProduction, + Aggregation, + SafeTargetUpdate, + EndOfSlot, +} + +impl SlotInterval { + pub(crate) fn from_slot_index(index: u64) -> Self { + match index { + 0 => Self::BlockPublication, + 1 => Self::AttestationProduction, + 2 => Self::Aggregation, + 3 => Self::SafeTargetUpdate, + 4 => Self::EndOfSlot, + _ => unreachable!("slots only have 5 intervals"), + } + } + + fn as_slot_index(self) -> u64 { + match self { + Self::BlockPublication => 0, + Self::AttestationProduction => 1, + Self::Aggregation => 2, + Self::SafeTargetUpdate => 3, + Self::EndOfSlot => 4, + } + } +} + +impl fmt::Display for SlotInterval { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_slot_index().fmt(f) + } +} + /// Milliseconds until the next interval boundary, measured relative to genesis. fn ms_until_next_interval(now_ms: u64, genesis_time_ms: u64) -> u64 { // Before genesis: wait until genesis itself. @@ -192,7 +231,9 @@ impl BlockChainServer { // Calculate current slot and interval from milliseconds let time_since_genesis_ms = timestamp_ms.saturating_sub(genesis_time_ms); let slot = time_since_genesis_ms / MILLISECONDS_PER_SLOT; - let interval = (time_since_genesis_ms % MILLISECONDS_PER_SLOT) / MILLISECONDS_PER_INTERVAL; + let interval = SlotInterval::from_slot_index( + (time_since_genesis_ms % MILLISECONDS_PER_SLOT) / MILLISECONDS_PER_INTERVAL, + ); // Idempotency guard // @@ -252,7 +293,7 @@ impl BlockChainServer { // needs (those stragglers surface in the `late` section instead). Skip // empty snapshots so a missed round keeps the last set we saw. Pure // observability. - if interval == 4 + if interval == SlotInterval::EndOfSlot && let Some(snapshot) = coverage::snapshot_new_payloads(&self.store) { self.pre_merge_coverage = Some(snapshot); @@ -260,7 +301,7 @@ impl BlockChainServer { // Whether one of our validators proposes this slot. Drives the store's // interval-0 attestation acceptance. - let is_proposer = (interval == 0 && slot > 0) + let is_proposer = (interval == SlotInterval::BlockPublication && slot > 0) .then(|| self.get_our_proposer(slot)) .flatten() .is_some(); @@ -280,14 +321,14 @@ impl BlockChainServer { // advances the store to this slot's interval 0 before building (see // `propose_block`). The real interval-0 tick is then skipped by the // idempotency guard above, since the store clock is already here. - 0 => {} + SlotInterval::BlockPublication => {} // ==== interval 1 ==== // // Produce attestations at interval 1 (all validators including // proposer). Reuse the same snapshot so self-delivery decisions // match the rest of the tick. - 1 => { + SlotInterval::AttestationProduction => { // Emit the post-block coverage report for the previous slot. // Fired at interval 1 (not 0) so the block carrying `slot - 1`'s // votes — proposed at interval 0 of this slot — has typically @@ -309,7 +350,7 @@ impl BlockChainServer { } // ==== interval 2 ==== - 2 => { + SlotInterval::Aggregation => { if is_aggregator { coverage::emit_agg_start_new_coverage( &self.store, @@ -324,7 +365,7 @@ impl BlockChainServer { // ==== interval 3 ==== // // Safe-target update is handled inside `store::on_tick`. - 3 => {} + SlotInterval::SafeTargetUpdate => {} // ==== interval 4 ==== // @@ -335,7 +376,7 @@ impl BlockChainServer { // rather than stashing it for the interval-0 tick — keeps it robust: // `on_tick` skips the interval-0 tick whenever this build overruns // its interval. - 4 => { + SlotInterval::EndOfSlot => { let next_slot = slot + 1; let next_proposer = self .get_our_proposer(next_slot) @@ -345,8 +386,6 @@ impl BlockChainServer { self.propose_block(next_slot, validator_id).await; } } - - _ => {} } // Update safe target slot metric (updated by store.on_tick at interval 3) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 20f7cf5c..07e9e7c0 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -18,7 +18,7 @@ use tracing::{info, trace, warn}; use crate::{ GOSSIP_DISPARITY_INTERVALS, INTERVALS_PER_SLOT, MAX_ATTESTATIONS_DATA, - MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, + MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, SlotInterval, block_builder::{PostBlockCheckpoints, ProposerConfig, build_block}, metrics, }; @@ -278,7 +278,7 @@ pub fn on_tick(store: &mut Store, timestamp_ms: u64, has_proposal: bool) { .expect("set_time should succeed"); let slot = store.time() / INTERVALS_PER_SLOT; - let interval = store.time() % INTERVALS_PER_SLOT; + let interval = SlotInterval::from_slot_index(store.time() % INTERVALS_PER_SLOT); trace!(%slot, %interval, "processing tick"); @@ -292,27 +292,26 @@ pub fn on_tick(store: &mut Store, timestamp_ms: u64, has_proposal: bool) { // the actor's message loop stays unblocked during the expensive XMSS // proofs. See `BlockChainServer::start_aggregation_session` in `lib.rs`. match interval { - 0 => { + SlotInterval::BlockPublication => { // Start of slot - process attestations if proposal exists if should_signal_proposal { accept_new_attestations(store, false); } } - 1 => { + SlotInterval::AttestationProduction => { // Vote propagation — no action } - 2 => { + SlotInterval::Aggregation => { // Aggregation is driven by the actor (off-thread); nothing to do here. } - 3 => { + SlotInterval::SafeTargetUpdate => { // Update safe target for validators update_safe_target(store); } - 4 => { + SlotInterval::EndOfSlot => { // End of slot - accept accumulated attestations and log tree accept_new_attestations(store, true); } - _ => unreachable!("slots only have 5 intervals"), } } }