diff --git a/Cargo.lock b/Cargo.lock index 895423b9a..e8b434118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,7 +233,7 @@ dependencies = [ "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "sloggers 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "slot_clock 0.1.0", + "slot_clock 0.2.0", "state_processing 0.1.0", "store 0.1.0", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -534,7 +534,7 @@ dependencies = [ "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "sloggers 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "slot_clock 0.1.0", + "slot_clock 0.2.0", "store 0.1.0", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3442,7 +3442,7 @@ dependencies = [ "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "slog-term 2.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "slot_clock 0.1.0", + "slot_clock 0.2.0", "state_processing 0.1.0", "store 0.1.0", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3866,10 +3866,11 @@ dependencies = [ [[package]] name = "slot_clock" -version = "0.1.0" +version = "0.2.0" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "lighthouse_metrics 0.1.0", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "types 0.1.0", ] @@ -4710,7 +4711,7 @@ dependencies = [ "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "slog-term 2.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "slot_clock 0.1.0", + "slot_clock 0.2.0", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/eth2/utils/slot_clock/Cargo.toml b/eth2/utils/slot_clock/Cargo.toml index e27395e42..81a7b57a9 100644 --- a/eth2/utils/slot_clock/Cargo.toml +++ b/eth2/utils/slot_clock/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slot_clock" -version = "0.1.0" +version = "0.2.0" authors = ["Paul Hauner "] edition = "2018" @@ -8,3 +8,4 @@ edition = "2018" types = { path = "../../types" } lazy_static = "1.4.0" lighthouse_metrics = { path = "../lighthouse_metrics" } +parking_lot = "0.9.0" diff --git a/eth2/utils/slot_clock/src/lib.rs b/eth2/utils/slot_clock/src/lib.rs index b85b5a72c..e9566a9a0 100644 --- a/eth2/utils/slot_clock/src/lib.rs +++ b/eth2/utils/slot_clock/src/lib.rs @@ -1,14 +1,15 @@ #[macro_use] extern crate lazy_static; +mod manual_slot_clock; mod metrics; mod system_time_slot_clock; -mod testing_slot_clock; use std::time::Duration; +pub use crate::manual_slot_clock::ManualSlotClock; +pub use crate::manual_slot_clock::ManualSlotClock as TestingSlotClock; pub use crate::system_time_slot_clock::SystemTimeSlotClock; -pub use crate::testing_slot_clock::TestingSlotClock; pub use metrics::scrape_for_metrics; pub use types::Slot; @@ -16,13 +17,21 @@ pub use types::Slot; /// /// The clock is not required to be monotonically increasing and may go backwards. pub trait SlotClock: Send + Sync + Sized { - /// Creates a new slot clock where the first slot is `genesis_slot`, genesis occured + /// Creates a new slot clock where the first slot is `genesis_slot`, genesis occurred /// `genesis_duration` after the `UNIX_EPOCH` and each slot is `slot_duration` apart. fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self; /// Returns the slot at this present time. fn now(&self) -> Option; + /// Returns the present time as a duration since the UNIX epoch. + /// + /// Returns `None` if the present time is before the UNIX epoch (unlikely). + fn now_duration(&self) -> Option; + + /// Returns the slot of the given duration since the UNIX epoch. + fn slot_of(&self, now: Duration) -> Option; + /// Returns the duration between slots fn slot_duration(&self) -> Duration; @@ -31,4 +40,18 @@ pub trait SlotClock: Send + Sync + Sized { /// Returns the duration until the first slot of the next epoch. fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option; + + /// Returns the first slot to be returned at the genesis time. + fn genesis_slot(&self) -> Slot; + + /// Returns the slot if the internal clock were advanced by `duration`. + fn now_with_future_tolerance(&self, tolerance: Duration) -> Option { + self.slot_of(self.now_duration()?.checked_add(tolerance)?) + } + + /// Returns the slot if the internal clock were reversed by `duration`. + fn now_with_past_tolerance(&self, tolerance: Duration) -> Option { + self.slot_of(self.now_duration()?.checked_sub(tolerance)?) + .or_else(|| Some(self.genesis_slot())) + } } diff --git a/eth2/utils/slot_clock/src/manual_slot_clock.rs b/eth2/utils/slot_clock/src/manual_slot_clock.rs new file mode 100644 index 000000000..fc0e27e3a --- /dev/null +++ b/eth2/utils/slot_clock/src/manual_slot_clock.rs @@ -0,0 +1,325 @@ +use super::SlotClock; +use parking_lot::RwLock; +use std::convert::TryInto; +use std::time::Duration; +use types::Slot; + +/// Determines the present slot based upon a manually-incremented UNIX timestamp. +pub struct ManualSlotClock { + genesis_slot: Slot, + /// Duration from UNIX epoch to genesis. + genesis_duration: Duration, + /// Duration from UNIX epoch to right now. + current_time: RwLock, + /// The length of each slot. + slot_duration: Duration, +} + +impl Clone for ManualSlotClock { + fn clone(&self) -> Self { + ManualSlotClock { + genesis_slot: self.genesis_slot.clone(), + genesis_duration: self.genesis_duration.clone(), + current_time: RwLock::new(self.current_time.read().clone()), + slot_duration: self.slot_duration.clone(), + } + } +} + +impl ManualSlotClock { + pub fn set_slot(&self, slot: u64) { + let slots_since_genesis = slot + .checked_sub(self.genesis_slot.as_u64()) + .expect("slot must be post-genesis") + .try_into() + .expect("slot must fit within a u32"); + *self.current_time.write() = + self.genesis_duration + self.slot_duration * slots_since_genesis; + } + + pub fn advance_slot(&self) { + self.set_slot(self.now().unwrap().as_u64() + 1) + } + + /// Returns the duration between UNIX epoch and the start of `slot`. + pub fn start_of(&self, slot: Slot) -> Option { + let slot = slot + .as_u64() + .checked_sub(self.genesis_slot.as_u64())? + .try_into() + .ok()?; + let unadjusted_slot_duration = self.slot_duration.checked_mul(slot)?; + + self.genesis_duration.checked_add(unadjusted_slot_duration) + } + + /// Returns the duration from `now` until the start of `slot`. + /// + /// Will return `None` if `now` is later than the start of `slot`. + pub fn duration_to_slot(&self, slot: Slot, now: Duration) -> Option { + self.start_of(slot)?.checked_sub(now) + } + + /// Returns the duration between `now` and the start of the next slot. + pub fn duration_to_next_slot_from(&self, now: Duration) -> Option { + if now < self.genesis_duration { + self.genesis_duration.checked_sub(now) + } else { + self.duration_to_slot(self.slot_of(now)? + 1, now) + } + } + + /// Returns the duration between `now` and the start of the next epoch. + pub fn duration_to_next_epoch_from( + &self, + now: Duration, + slots_per_epoch: u64, + ) -> Option { + if now < self.genesis_duration { + self.genesis_duration.checked_sub(now) + } else { + let next_epoch_start_slot = + (self.slot_of(now)?.epoch(slots_per_epoch) + 1).start_slot(slots_per_epoch); + + self.duration_to_slot(next_epoch_start_slot, now) + } + } +} + +impl SlotClock for ManualSlotClock { + fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self { + if slot_duration.as_millis() == 0 { + panic!("ManualSlotClock cannot have a < 1ms slot duration"); + } + + Self { + genesis_slot, + current_time: RwLock::new(genesis_duration.clone()), + genesis_duration, + slot_duration, + } + } + + fn now(&self) -> Option { + self.slot_of(*self.current_time.read()) + } + + fn now_duration(&self) -> Option { + Some(*self.current_time.read()) + } + + fn slot_of(&self, now: Duration) -> Option { + let genesis = self.genesis_duration; + + if now >= genesis { + let since_genesis = now + .checked_sub(genesis) + .expect("Control flow ensures now is greater than or equal to genesis"); + let slot = + Slot::from((since_genesis.as_millis() / self.slot_duration.as_millis()) as u64); + Some(slot + self.genesis_slot) + } else { + None + } + } + + fn duration_to_next_slot(&self) -> Option { + self.duration_to_next_slot_from(*self.current_time.read()) + } + + fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option { + self.duration_to_next_epoch_from(*self.current_time.read(), slots_per_epoch) + } + + fn slot_duration(&self) -> Duration { + self.slot_duration + } + + fn genesis_slot(&self) -> Slot { + self.genesis_slot + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slot_now() { + let clock = ManualSlotClock::new( + Slot::new(10), + Duration::from_secs(0), + Duration::from_secs(1), + ); + assert_eq!(clock.now(), Some(Slot::new(10))); + clock.set_slot(123); + assert_eq!(clock.now(), Some(Slot::new(123))); + } + + #[test] + fn start_of() { + // Genesis slot and genesis duration 0. + let clock = + ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + assert_eq!(clock.start_of(Slot::new(0)), Some(Duration::from_secs(0))); + assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(1))); + assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(2))); + + // Genesis slot 1 and genesis duration 10. + let clock = ManualSlotClock::new( + Slot::new(0), + Duration::from_secs(10), + Duration::from_secs(1), + ); + assert_eq!(clock.start_of(Slot::new(0)), Some(Duration::from_secs(10))); + assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(11))); + assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(12))); + + // Genesis slot 1 and genesis duration 0. + let clock = + ManualSlotClock::new(Slot::new(1), Duration::from_secs(0), Duration::from_secs(1)); + assert_eq!(clock.start_of(Slot::new(0)), None); + assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(0))); + assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(1))); + + // Genesis slot 1 and genesis duration 10. + let clock = ManualSlotClock::new( + Slot::new(1), + Duration::from_secs(10), + Duration::from_secs(1), + ); + assert_eq!(clock.start_of(Slot::new(0)), None); + assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(10))); + assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(11))); + } + + #[test] + fn test_duration_to_next_slot() { + let slot_duration = Duration::from_secs(1); + + // Genesis time is now. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + *clock.current_time.write() = Duration::from_secs(0); + assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(1))); + + // Genesis time is in the future. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(10), slot_duration); + *clock.current_time.write() = Duration::from_secs(0); + assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(10))); + + // Genesis time is in the past. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + *clock.current_time.write() = Duration::from_secs(10); + assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(1))); + } + + #[test] + fn test_duration_to_next_epoch() { + let slot_duration = Duration::from_secs(1); + let slots_per_epoch = 32; + + // Genesis time is now. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + *clock.current_time.write() = Duration::from_secs(0); + assert_eq!( + clock.duration_to_next_epoch(slots_per_epoch), + Some(Duration::from_secs(32)) + ); + + // Genesis time is in the future. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(10), slot_duration); + *clock.current_time.write() = Duration::from_secs(0); + assert_eq!( + clock.duration_to_next_epoch(slots_per_epoch), + Some(Duration::from_secs(10)) + ); + + // Genesis time is in the past. + let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + *clock.current_time.write() = Duration::from_secs(10); + assert_eq!( + clock.duration_to_next_epoch(slots_per_epoch), + Some(Duration::from_secs(22)) + ); + + // Genesis time is in the past. + let clock = ManualSlotClock::new( + Slot::new(0), + Duration::from_secs(0), + Duration::from_secs(12), + ); + *clock.current_time.write() = Duration::from_secs(72_333); + assert!(clock.duration_to_next_epoch(slots_per_epoch).is_some(),); + } + + #[test] + fn test_tolerance() { + let clock = ManualSlotClock::new( + Slot::new(0), + Duration::from_secs(10), + Duration::from_secs(1), + ); + + // Set clock to the 0'th slot. + *clock.current_time.write() = Duration::from_secs(10); + assert_eq!( + clock + .now_with_future_tolerance(Duration::from_secs(0)) + .unwrap(), + Slot::new(0), + "future tolerance of zero should return current slot" + ); + assert_eq!( + clock + .now_with_past_tolerance(Duration::from_secs(0)) + .unwrap(), + Slot::new(0), + "past tolerance of zero should return current slot" + ); + assert_eq!( + clock + .now_with_future_tolerance(Duration::from_millis(10)) + .unwrap(), + Slot::new(0), + "insignificant future tolerance should return current slot" + ); + assert_eq!( + clock + .now_with_past_tolerance(Duration::from_millis(10)) + .unwrap(), + Slot::new(0), + "past tolerance that precedes genesis should return genesis slot" + ); + + // Set clock to part-way through the 1st slot. + *clock.current_time.write() = Duration::from_millis(11_200); + assert_eq!( + clock + .now_with_future_tolerance(Duration::from_secs(0)) + .unwrap(), + Slot::new(1), + "future tolerance of zero should return current slot" + ); + assert_eq!( + clock + .now_with_past_tolerance(Duration::from_secs(0)) + .unwrap(), + Slot::new(1), + "past tolerance of zero should return current slot" + ); + assert_eq!( + clock + .now_with_future_tolerance(Duration::from_millis(800)) + .unwrap(), + Slot::new(2), + "significant future tolerance should return next slot" + ); + assert_eq!( + clock + .now_with_past_tolerance(Duration::from_millis(201)) + .unwrap(), + Slot::new(0), + "significant past tolerance should return previous slot" + ); + } +} diff --git a/eth2/utils/slot_clock/src/system_time_slot_clock.rs b/eth2/utils/slot_clock/src/system_time_slot_clock.rs index 5e4151528..81a5e5912 100644 --- a/eth2/utils/slot_clock/src/system_time_slot_clock.rs +++ b/eth2/utils/slot_clock/src/system_time_slot_clock.rs @@ -1,4 +1,4 @@ -use super::SlotClock; +use super::{ManualSlotClock, SlotClock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use types::Slot; @@ -7,95 +7,45 @@ pub use std::time::SystemTimeError; /// Determines the present slot based upon the present system time. #[derive(Clone)] pub struct SystemTimeSlotClock { - genesis_slot: Slot, - genesis_duration: Duration, - slot_duration: Duration, + clock: ManualSlotClock, } impl SlotClock for SystemTimeSlotClock { fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self { - if slot_duration.as_millis() == 0 { - panic!("SystemTimeSlotClock cannot have a < 1ms slot duration."); - } - Self { - genesis_slot, - genesis_duration, - slot_duration, + clock: ManualSlotClock::new(genesis_slot, genesis_duration, slot_duration), } } fn now(&self) -> Option { let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?; - let genesis = self.genesis_duration; + self.clock.slot_of(now) + } - if now >= genesis { - let since_genesis = now - .checked_sub(genesis) - .expect("Control flow ensures now is greater than or equal to genesis"); - let slot = - Slot::from((since_genesis.as_millis() / self.slot_duration.as_millis()) as u64); - Some(slot + self.genesis_slot) - } else { - None - } + fn now_duration(&self) -> Option { + SystemTime::now().duration_since(UNIX_EPOCH).ok() + } + + fn slot_of(&self, now: Duration) -> Option { + self.clock.slot_of(now) } fn duration_to_next_slot(&self) -> Option { let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?; - let genesis = self.genesis_duration; - - let slot_start = |slot: Slot| -> Duration { - let slot = slot.as_u64() as u32; - genesis + slot * self.slot_duration - }; - - if now >= genesis { - Some( - slot_start(self.now()? + 1) - .checked_sub(now) - .expect("The next slot cannot start before now"), - ) - } else { - Some( - genesis - .checked_sub(now) - .expect("Control flow ensures genesis is greater than or equal to now"), - ) - } + self.clock.duration_to_next_slot_from(now) } fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option { let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?; - let genesis = self.genesis_duration; - - let slot_start = |slot: Slot| -> Duration { - let slot = slot.as_u64() as u32; - genesis + slot * self.slot_duration - }; - - let epoch_start_slot = self - .now() - .map(|slot| slot.epoch(slots_per_epoch)) - .map(|epoch| (epoch + 1).start_slot(slots_per_epoch))?; - - if now >= genesis { - Some( - slot_start(epoch_start_slot) - .checked_sub(now) - .expect("The next epoch cannot start before now"), - ) - } else { - Some( - genesis - .checked_sub(now) - .expect("Control flow ensures genesis is greater than or equal to now"), - ) - } + self.clock.duration_to_next_epoch_from(now, slots_per_epoch) } fn slot_duration(&self) -> Duration { - self.slot_duration + self.clock.slot_duration() + } + + fn genesis_slot(&self) -> Slot { + self.clock.genesis_slot() } }