diff --git a/crates/anvil/src/eth/backend/time.rs b/crates/anvil/src/eth/backend/time.rs index 175f27924..b80fb0253 100644 --- a/crates/anvil/src/eth/backend/time.rs +++ b/crates/anvil/src/eth/backend/time.rs @@ -23,6 +23,8 @@ pub struct TimeManager { next_exact_timestamp: Arc>>, /// The interval to use when determining the next block's timestamp interval: Arc>>, + /// If set, block timestamps are restricted to this Ethereum Beacon slot grid + beacon_slots: Arc>>, } impl TimeManager { @@ -32,6 +34,7 @@ impl TimeManager { offset: Default::default(), next_exact_timestamp: Default::default(), interval: Default::default(), + beacon_slots: Default::default(), }; time_manager.reset(start_timestamp); time_manager @@ -46,6 +49,25 @@ impl TimeManager { self.next_exact_timestamp.write().take(); } + /// Restricts block timestamps to the Ethereum Beacon slot grid + /// `genesis_time + k * seconds_per_slot`, given as `(genesis_time, seconds_per_slot)`, or + /// lifts the restriction with `None`. + /// + /// Each block uses the slot containing its candidate timestamp, or the next slot if the + /// previous block already occupies it. + pub fn set_beacon_slots(&self, slots: Option<(u64, u64)>) -> Result<(), BlockchainError> { + let slots = slots + .map(|(genesis_time, seconds_per_slot)| BeaconSlots { genesis_time, seconds_per_slot }); + if slots.is_some_and(|slots| slots.seconds_per_slot == 0) { + return Err(BlockchainError::TimestampError( + "Beacon slot duration must be positive".to_string(), + )); + } + trace!(target: "time", "set beacon slots {:?}", slots); + *self.beacon_slots.write() = slots; + Ok(()) + } + pub fn offset(&self) -> i128 { *self.offset.read() } @@ -102,6 +124,7 @@ impl TimeManager { fn compute_next_timestamp(&self) -> (u64, Option) { let current = duration_since_unix_epoch().as_secs() as i128; let last_timestamp = *self.last_timestamp.read(); + let slots = *self.beacon_slots.read(); let (mut next_timestamp, update_offset) = if let Some(next) = *self.next_exact_timestamp.read() { @@ -111,8 +134,10 @@ impl TimeManager { } else { (current.saturating_add(self.offset()) as u64, false) }; - // Ensures that the timestamp is always increasing - if next_timestamp < last_timestamp { + if let Some(slots) = slots { + next_timestamp = slots.next_after(last_timestamp, next_timestamp); + } else if next_timestamp < last_timestamp { + // Ensures that the timestamp is always increasing next_timestamp = last_timestamp + 1; } let next_offset = update_offset.then_some((next_timestamp as i128) - current); @@ -138,6 +163,24 @@ impl TimeManager { } } +/// Ethereum Beacon slot grid: slot timestamps are `genesis_time + k * seconds_per_slot` +#[derive(Clone, Copy, Debug)] +struct BeaconSlots { + genesis_time: u64, + /// Always positive + seconds_per_slot: u64, +} + +impl BeaconSlots { + /// Uses the slot containing the clock, or the next slot if the parent already occupies it. + fn next_after(&self, last: u64, candidate: u64) -> u64 { + let clock_slot = candidate.saturating_sub(self.genesis_time) / self.seconds_per_slot; + let next_slot = + (last + 1).saturating_sub(self.genesis_time).div_ceil(self.seconds_per_slot); + self.genesis_time + clock_slot.max(next_slot) * self.seconds_per_slot + } +} + /// Returns the current duration since unix epoch. pub fn duration_since_unix_epoch() -> Duration { use std::time::SystemTime; @@ -145,3 +188,99 @@ pub fn duration_since_unix_epoch() -> Duration { now.duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_else(|err| panic!("Current time {now:?} is invalid: {err:?}")) } + +#[cfg(test)] +mod tests { + use super::*; + + const GENESIS: u64 = 1000; + const SLOT: u64 = 12; + const TIP: u64 = 1240; + + fn beacon(tip: u64) -> TimeManager { + let time = TimeManager::new(tip); + time.set_beacon_slots(Some((GENESIS, SLOT))).unwrap(); + time + } + + #[test] + fn beacon_implicit_timestamp_is_next_slot() { + let time = beacon(TIP); + assert_eq!(time.current_call_timestamp(), 1252); + assert_eq!(time.next_timestamp(), 1252); + assert_eq!(time.next_timestamp(), 1264); + } + + #[test] + fn beacon_next_block_timestamp_uses_its_slot() { + let time = beacon(TIP); + time.set_next_block_timestamp(1276).unwrap(); + assert_eq!(time.current_call_timestamp(), 1276); + assert_eq!(time.next_timestamp(), 1276); + time.set_next_block_timestamp(1290).unwrap(); + assert_eq!(time.next_timestamp(), 1288); + // The parent occupies 1288's slot. + time.set_next_block_timestamp(1288).unwrap(); + assert_eq!(time.next_timestamp(), 1300); + } + + #[test] + fn beacon_interval_advances_by_slots() { + let time = beacon(TIP); + time.set_block_timestamp_interval(24); + assert_eq!(time.current_call_timestamp(), 1264); + assert_eq!(time.next_timestamp(), 1264); + assert_eq!(time.next_timestamp(), 1288); + time.set_block_timestamp_interval(5); + assert_eq!(time.next_timestamp(), 1300); + } + + #[test] + fn beacon_reset_clears_one_shot_and_keeps_interval() { + let time = beacon(TIP); + time.set_block_timestamp_interval(24); + time.set_next_block_timestamp(1300).unwrap(); + time.reset(TIP); + assert_eq!(time.next_timestamp(), 1264); + } + + #[test] + fn beacon_slots_require_positive_duration() { + let time = TimeManager::new(TIP); + assert!(time.set_beacon_slots(Some((GENESIS, 0))).is_err()); + time.set_next_block_timestamp(1253).unwrap(); + assert_eq!(time.next_timestamp(), 1253); + } + + #[test] + fn beacon_clock_uses_containing_slot_after_elapsed_seconds() { + let time = beacon(TIP); + time.increase_time(5); + time.increase_time(12); + assert_eq!(time.next_timestamp(), 1252); + } + + #[test] + fn legacy_behavior_is_unchanged() { + let time = TimeManager::new(TIP); + time.set_next_block_timestamp(TIP).unwrap(); + time.set_next_block_timestamp(1253).unwrap(); + assert_eq!(time.next_timestamp(), 1253); + time.set_block_timestamp_interval(0); + time.set_block_timestamp_interval(5); + assert_eq!(time.next_timestamp(), 1258); + time.reset(100); + assert_eq!(time.next_timestamp(), 105); + } + + #[test] + fn clearing_beacon_slots_restores_legacy_behavior() { + let time = beacon(TIP); + time.set_beacon_slots(None).unwrap(); + time.set_next_block_timestamp(1253).unwrap(); + assert_eq!(time.next_timestamp(), 1253); + time.set_block_timestamp_interval(5); + time.reset(100); + assert_eq!(time.next_timestamp(), 105); + } +}