Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 {

@MegaRedHand MegaRedHand Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, we should give only current time in ms -> SlotInterval and current time in intervals -> SlotInterval constructors, so that callers don't have to compute the interval themselves. That way we centralize the logic in this enum

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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is unused, we should just remove it

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.
Expand Down Expand Up @@ -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,
);
Comment on lines +234 to +236

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, for example, we could do:

Suggested change
let interval = SlotInterval::from_slot_index(
(time_since_genesis_ms % MILLISECONDS_PER_SLOT) / MILLISECONDS_PER_INTERVAL,
);
let interval = SlotInterval::from_ms_since_genesis(time_since_genesis_ms);


// Idempotency guard
//
Expand Down Expand Up @@ -252,15 +293,15 @@ 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);
}

// 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();
Expand All @@ -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
Expand All @@ -309,7 +350,7 @@ impl BlockChainServer {
}

// ==== interval 2 ====
2 => {
SlotInterval::Aggregation => {
if is_aggregator {
coverage::emit_agg_start_new_coverage(
&self.store,
Expand All @@ -324,7 +365,7 @@ impl BlockChainServer {
// ==== interval 3 ====
//
// Safe-target update is handled inside `store::on_tick`.
3 => {}
SlotInterval::SafeTargetUpdate => {}

// ==== interval 4 ====
//
Expand All @@ -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)
Expand All @@ -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)
Expand Down
15 changes: 7 additions & 8 deletions crates/blockchain/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here:

Suggested change
let interval = SlotInterval::from_slot_index(store.time() % INTERVALS_PER_SLOT);
let interval = SlotInterval::from_intervals_since_genesis(store.time());


trace!(%slot, %interval, "processing tick");

Expand All @@ -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"),
}
}
}
Expand Down