Skip to content
Merged
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
35 changes: 28 additions & 7 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use spawned_concurrency::error::ActorError;
use spawned_concurrency::protocol;
use spawned_concurrency::tasks::{Actor, ActorRef, ActorStart, Context, Handler, send_after};
use tokio_util::sync::CancellationToken;
use tracing::{error, info, trace, warn};
use tracing::{debug, error, info, trace, warn};

use crate::store::StoreError;

Expand Down Expand Up @@ -173,26 +173,47 @@ pub struct BlockChainServer {

impl BlockChainServer {
async fn on_tick(&mut self, timestamp_ms: u64, ctx: &Context<Self>) {
// Observe tick interval duration before any processing
if let Some(prev_instant) = self.last_tick_instant {
metrics::observe_tick_interval_duration(prev_instant.elapsed());
}
self.last_tick_instant = Some(Instant::now());

let genesis_time_ms = self.store.config().genesis_time * 1000;

// 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;

// Idempotency guard
//
// `slot`/`interval` come from the wall clock, but the tick cadence is driven
// by the monotonic clock (`tokio::sleep`). The wall clock can drift behind it
// inside VMs, so a tick scheduled for the next interval boundary can fire
// while the wall clock still reads the previous interval.
let tick_interval = time_since_genesis_ms / MILLISECONDS_PER_INTERVAL;
let store_time = self.store.time();

if store_time > 0 && tick_interval <= store_time {
debug!(
%slot,
%interval,
tick_interval,
store_time,
"Skipping already-processed tick"
);
return;
}

// Fail fast: a state with zero validators is invalid and would cause
// panics in proposer selection and attestation processing.
if self.store.head_state().validators.is_empty() {
error!("Head state has no validators, skipping tick");
return;
}

// Observe tick interval duration. Done after the idempotency guard so a
// skipped duplicate tick doesn't shorten the next real tick's sample.
if let Some(prev_instant) = self.last_tick_instant {
metrics::observe_tick_interval_duration(prev_instant.elapsed());
}
self.last_tick_instant = Some(Instant::now());

// Update current slot metric
metrics::update_current_slot(slot);

Expand Down
Loading