Skip to content
Closed
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
5 changes: 2 additions & 3 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2747,7 +2747,7 @@ impl EthApi {
pub fn evm_set_time(&self, timestamp: u64) -> Result<u64> {
node_info!("evm_setTime");
let now = self.backend.time().current_call_timestamp();
self.backend.time().reset(timestamp);
self.backend.time().set_time(timestamp)?;

// number of seconds between the given timestamp and the current time.
let offset = timestamp.saturating_sub(now);
Expand All @@ -2768,8 +2768,7 @@ impl EthApi {
/// Handler for RPC call: `anvil_setBlockTimestampInterval`
pub fn evm_set_block_timestamp_interval(&self, seconds: u64) -> Result<()> {
node_info!("anvil_setBlockTimestampInterval");
self.backend.time().set_block_timestamp_interval(seconds);
Ok(())
self.backend.time().set_block_timestamp_interval(seconds)
}

/// Sets an interval for the block timestamp
Expand Down
191 changes: 154 additions & 37 deletions crates/anvil/src/eth/backend/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use std::{sync::Arc, time::Duration};

// Bound development time jumps to year 9999. Mining may continue past this bound,
// but RPC-controlled offsets must not exhaust u64 or chrono's printable date range.
pub(super) const MAX_BEACON_TIMESTAMP: u64 = 253_402_300_799;

/// Returns the `Utc` datetime for the given seconds since unix epoch
pub fn utc_from_secs(secs: u64) -> DateTime<Utc> {
DateTime::from_timestamp(secs as i64, 0).unwrap()
Expand Down Expand Up @@ -53,21 +57,60 @@ impl TimeManager {
/// `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.
/// Pending settings that are invalid on the grid are dropped: an interval that is not a
/// positive multiple of the slot duration, and a next block timestamp that is not a slot after
/// the previous block's timestamp.
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(),
));
if let Some(slots) = slots {
if slots.seconds_per_slot == 0
|| slots.genesis_time > MAX_BEACON_TIMESTAMP
|| slots.seconds_per_slot > MAX_BEACON_TIMESTAMP
|| *self.last_timestamp.read() > MAX_BEACON_TIMESTAMP
{
return Err(BlockchainError::TimestampError(
"Beacon schedule must have a positive duration and fit the supported date range".to_string(),
));
}
let last_timestamp = *self.last_timestamp.read();
self.interval.write().take_if(|interval| !slots.is_interval(*interval));
self.next_exact_timestamp
.write()
.take_if(|next| *next <= last_timestamp || !slots.contains(*next));
}
trace!(target: "time", "set beacon slots {:?}", slots);
*self.beacon_slots.write() = slots;
Ok(())
}

/// Sets the clock to `timestamp`.
///
/// Without Beacon slots this is [`Self::reset`]. With Beacon slots `timestamp` must be a slot
/// at or after the previous block's timestamp, and only the clock moves: the previous block's
/// timestamp is canonical chain state and is never lowered. Like `reset`, this clears the next
/// block timestamp.
pub fn set_time(&self, timestamp: u64) -> Result<(), BlockchainError> {
let slots = *self.beacon_slots.read();
let Some(slots) = slots else {
self.reset(timestamp);
return Ok(());
};
let last_timestamp = *self.last_timestamp.read();
if timestamp < last_timestamp
|| timestamp > MAX_BEACON_TIMESTAMP
|| !slots.contains(timestamp)
{
return Err(BlockchainError::TimestampError(format!(
"{timestamp} is not a Beacon slot at or after previous block's timestamp {last_timestamp}"
)));
}
let current = duration_since_unix_epoch().as_secs() as i128;
*self.offset.write() = (timestamp as i128) - current;
self.next_exact_timestamp.write().take();
Ok(())
}

pub fn offset(&self) -> i128 {
*self.offset.read()
}
Expand All @@ -89,10 +132,21 @@ impl TimeManager {
}

/// Sets the exact timestamp to use in the next block
/// Fails if it's before (or at the same time) the last timestamp
/// Fails if it's before the last timestamp, or with Beacon slots, if it's not a slot after the
/// last timestamp
pub fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), BlockchainError> {
trace!(target: "time", "override next timestamp {}", timestamp);
if timestamp < *self.last_timestamp.read() {
let last_timestamp = *self.last_timestamp.read();
if let Some(slots) = *self.beacon_slots.read() {
if timestamp <= last_timestamp
|| timestamp > MAX_BEACON_TIMESTAMP
|| !slots.contains(timestamp)
{
return Err(BlockchainError::TimestampError(format!(
"{timestamp} is not a Beacon slot after previous block's timestamp {last_timestamp}"
)));
}
} else if timestamp < last_timestamp {
return Err(BlockchainError::TimestampError(format!(
"{timestamp} is lower than previous block's timestamp"
)));
Expand All @@ -104,10 +158,20 @@ impl TimeManager {
/// Sets an interval to use when computing the next timestamp
///
/// If an interval already exists, this will update the interval, otherwise a new interval will
/// be set starting with the current timestamp.
pub fn set_block_timestamp_interval(&self, interval: u64) {
/// be set starting with the current timestamp. With Beacon slots the interval must be a
/// positive multiple of the slot duration.
pub fn set_block_timestamp_interval(&self, interval: u64) -> Result<(), BlockchainError> {
trace!(target: "time", "set interval {}", interval);
if let Some(slots) = *self.beacon_slots.read()
&& !slots.is_interval(interval)
{
return Err(BlockchainError::TimestampError(format!(
"interval {interval} is not a positive multiple of the {}s Beacon slot duration",
slots.seconds_per_slot
)));
}
self.interval.write().replace(interval);
Ok(())
}

/// Removes the interval if it exists
Expand All @@ -126,16 +190,22 @@ impl TimeManager {
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() {
(next, true)
} else if let Some(interval) = *self.interval.read() {
(last_timestamp.saturating_add(interval), false)
} else {
(current.saturating_add(self.offset()) as u64, false)
};
let (mut next_timestamp, update_offset) = if let Some(next) =
*self.next_exact_timestamp.read()
{
(next, true)
} else if let Some(interval) = *self.interval.read() {
(last_timestamp.saturating_add(interval), false)
} else {
let clock = current.saturating_add(self.offset());
// Clamp instead of wrapping so slot rounding sees the intended clock.
let clock =
if slots.is_some() { clock.clamp(0, MAX_BEACON_TIMESTAMP as i128) } else { clock };
(clock as u64, false)
};
if let Some(slots) = slots {
next_timestamp = slots.next_after(last_timestamp, next_timestamp);
next_timestamp =
slots.next_after(last_timestamp, next_timestamp.min(MAX_BEACON_TIMESTAMP));
} else if next_timestamp < last_timestamp {
// Ensures that the timestamp is always increasing
next_timestamp = last_timestamp + 1;
Expand Down Expand Up @@ -172,7 +242,22 @@ struct BeaconSlots {
}

impl BeaconSlots {
/// Returns true if `timestamp` is a slot timestamp
fn contains(&self, timestamp: u64) -> bool {
timestamp
.checked_sub(self.genesis_time)
.is_some_and(|elapsed| elapsed.is_multiple_of(self.seconds_per_slot))
}

/// Returns true if `interval` advances from one slot to a later one
fn is_interval(&self, interval: u64) -> bool {
interval != 0
&& interval <= MAX_BEACON_TIMESTAMP
&& interval.is_multiple_of(self.seconds_per_slot)
}

/// Uses the slot containing the clock, or the next slot if the parent already occupies it.
/// Inputs are bounded before reaching here; normal mining cannot exhaust the integer range.
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 =
Expand Down Expand Up @@ -212,48 +297,80 @@ mod tests {
}

#[test]
fn beacon_next_block_timestamp_uses_its_slot() {
fn beacon_next_block_timestamp_must_be_later_slot() {
let time = beacon(TIP);
assert!(time.set_next_block_timestamp(1253).is_err());
assert!(time.set_next_block_timestamp(TIP).is_err());
assert!(time.set_next_block_timestamp(1228).is_err());
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() {
fn beacon_interval_must_be_positive_slot_multiple() {
let time = beacon(TIP);
time.set_block_timestamp_interval(24);
assert!(time.set_block_timestamp_interval(5).is_err());
assert!(time.set_block_timestamp_interval(0).is_err());
time.set_block_timestamp_interval(24).unwrap();
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);
}

#[test]
fn beacon_set_time_cannot_move_behind_tip() {
let time = beacon(TIP);
time.set_next_block_timestamp(1300).unwrap();
assert_eq!(time.next_timestamp(), 1300);

assert!(time.set_time(1288).is_err());
assert!(time.set_time(1301).is_err());
time.set_time(1300).unwrap();
// The mined tip stays canonical, so the next block lands in the following slot.
assert!(time.set_next_block_timestamp(1300).is_err());
assert_eq!(time.current_call_timestamp(), 1312);
assert_eq!(time.next_timestamp(), 1312);
}

#[test]
fn beacon_reset_clears_one_shot_and_keeps_interval() {
let time = beacon(TIP);
time.set_block_timestamp_interval(24);
time.set_block_timestamp_interval(24).unwrap();
time.set_next_block_timestamp(1300).unwrap();
time.reset(TIP);
assert_eq!(time.next_timestamp(), 1264);
}

#[test]
fn beacon_slots_require_positive_duration() {
fn beacon_activation_drops_incompatible_pending_state() {
let time = TimeManager::new(TIP);
assert!(time.set_beacon_slots(Some((GENESIS, 0))).is_err());
time.set_block_timestamp_interval(30).unwrap();
time.set_next_block_timestamp(1253).unwrap();
assert_eq!(time.next_timestamp(), 1253);
time.set_beacon_slots(Some((GENESIS, SLOT))).unwrap();
assert_eq!(time.next_timestamp(), 1252);
assert!(time.set_beacon_slots(Some((GENESIS, 0))).is_err());
}

#[test]
fn beacon_extreme_time_jump_leaves_room_to_continue_mining() {
let time = beacon(TIP);
time.increase_time(u64::MAX);
let first = time.next_timestamp();
assert!(first <= MAX_BEACON_TIMESTAMP);
assert_eq!((first - GENESIS) % SLOT, 0);
assert_eq!(time.next_timestamp(), first + SLOT);
assert!(time.set_next_block_timestamp(u64::MAX).is_err());
assert!(time.set_time(u64::MAX).is_err());
assert!(time.set_block_timestamp_interval(u64::MAX / SLOT * SLOT).is_err());
}

#[test]
fn beacon_clock_uses_containing_slot_after_elapsed_seconds() {
let time = beacon(TIP);
time.set_time(1300).unwrap();
time.increase_time(1);
assert_eq!(time.next_timestamp(), 1300);
let time = beacon(TIP);
time.increase_time(5);
time.increase_time(12);
Expand All @@ -266,10 +383,10 @@ mod tests {
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);
time.set_block_timestamp_interval(0).unwrap();
time.set_block_timestamp_interval(5).unwrap();
assert_eq!(time.next_timestamp(), 1258);
time.reset(100);
time.set_time(100).unwrap();
assert_eq!(time.next_timestamp(), 105);
}

Expand All @@ -279,8 +396,8 @@ mod tests {
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);
time.set_block_timestamp_interval(5).unwrap();
time.set_time(100).unwrap();
assert_eq!(time.next_timestamp(), 105);
}
}
Loading