diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 8be7c89f..32c2ac15 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -59,6 +59,32 @@ 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_ms_since_genesis(ms_since_genesis: u64) -> Self { + Self::from_intervals_since_genesis(ms_since_genesis / MILLISECONDS_PER_INTERVAL) + } + + pub(crate) fn from_intervals_since_genesis(intervals_since_genesis: u64) -> Self { + match intervals_since_genesis % INTERVALS_PER_SLOT { + 0 => Self::BlockPublication, + 1 => Self::AttestationProduction, + 2 => Self::Aggregation, + 3 => Self::SafeTargetUpdate, + 4 => Self::EndOfSlot, + _ => unreachable!("slots only have 5 intervals"), + } + } +} + /// 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 +218,7 @@ 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_ms_since_genesis(time_since_genesis_ms); // Idempotency guard // @@ -206,7 +232,7 @@ impl BlockChainServer { if store_time > 0 && tick_interval <= store_time { debug!( %slot, - %interval, + ?interval, tick_interval, store_time, "Skipping already-processed tick" @@ -252,7 +278,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 +286,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 +306,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 +335,7 @@ impl BlockChainServer { } // ==== interval 2 ==== - 2 => { + SlotInterval::Aggregation => { if is_aggregator { coverage::emit_agg_start_new_coverage( &self.store, @@ -324,7 +350,7 @@ impl BlockChainServer { // ==== interval 3 ==== // // Safe-target update is handled inside `store::on_tick`. - 3 => {} + SlotInterval::SafeTargetUpdate => {} // ==== interval 4 ==== // @@ -335,7 +361,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 +371,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..4b12bb3f 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,9 +278,9 @@ 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_intervals_since_genesis(store.time()); - trace!(%slot, %interval, "processing tick"); + trace!(%slot, ?interval, "processing tick"); // has_proposal is only signaled for the final tick (matching Python spec behavior) let is_final_tick = store.time() == time; @@ -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"), } } }