Skip to content
Draft
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
143 changes: 141 additions & 2 deletions crates/anvil/src/eth/backend/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub struct TimeManager {
next_exact_timestamp: Arc<RwLock<Option<u64>>>,
/// The interval to use when determining the next block's timestamp
interval: Arc<RwLock<Option<u64>>>,
/// If set, block timestamps are restricted to this Ethereum Beacon slot grid
beacon_slots: Arc<RwLock<Option<BeaconSlots>>>,
}

impl TimeManager {
Expand All @@ -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
Expand All @@ -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()
}
Expand Down Expand Up @@ -102,6 +124,7 @@ impl TimeManager {
fn compute_next_timestamp(&self) -> (u64, Option<i128>) {
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() {
Expand All @@ -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);
Expand All @@ -138,10 +163,124 @@ 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;
let now = SystemTime::now();
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);
}
}
Loading