diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 4d9fbcb2f..02c513e11 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -2747,7 +2747,7 @@ impl EthApi { pub fn evm_set_time(&self, timestamp: u64) -> Result { 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); @@ -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 diff --git a/crates/anvil/src/eth/backend/time.rs b/crates/anvil/src/eth/backend/time.rs index b80fb0253..1a9146a30 100644 --- a/crates/anvil/src/eth/backend/time.rs +++ b/crates/anvil/src/eth/backend/time.rs @@ -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 { DateTime::from_timestamp(secs as i64, 0).unwrap() @@ -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() } @@ -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" ))); @@ -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 @@ -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; @@ -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 = @@ -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); @@ -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); } @@ -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); } }