From d09ae2a27e8b9949afe972d66ccc54f0ec31013d Mon Sep 17 00:00:00 2001 From: rrruko Date: Wed, 18 Dec 2024 05:19:06 -0800 Subject: [PATCH 1/8] implement slot/epoch/time functions --- crates/amaru/src/lib.rs | 2 + crates/amaru/src/time.rs | 293 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 crates/amaru/src/time.rs diff --git a/crates/amaru/src/lib.rs b/crates/amaru/src/lib.rs index d313bf4..5754b84 100644 --- a/crates/amaru/src/lib.rs +++ b/crates/amaru/src/lib.rs @@ -19,3 +19,5 @@ pub mod ledger; /// /// A set of additional primitives around iterators. Not Amaru-specific so-to-speak. pub mod iter; + +pub mod time; diff --git a/crates/amaru/src/time.rs b/crates/amaru/src/time.rs new file mode 100644 index 0000000..c305a74 --- /dev/null +++ b/crates/amaru/src/time.rs @@ -0,0 +1,293 @@ +#[derive(PartialEq, Eq)] +pub struct Bound { + pub bound_time: u64, + pub bound_slot: u64, + pub bound_epoch: u64, +} + +#[derive(PartialEq, Eq)] +pub enum EraEnd { + Bounded(Bound), + Unbounded, +} + +#[derive(PartialEq, Eq)] +pub struct EraParams { + pub era_epoch_size: u64, + // Milliseconds + pub era_slot_length: u64, + // If the next era has not yet been announced then it is impossible for it + // to occur in the next `era_safe_zone` slots after the tip. But because a + // fork can only occur at an epoch boundary, in practice the safe zone + // will get rounded up to the end of that epoch. + pub era_safe_zone: u64, + pub era_genesis_window: u64, +} + +#[derive(PartialEq, Eq)] +pub struct Summary { + pub era_start: Bound, + pub era_end: EraEnd, + pub era_params: EraParams, +} + +// A complete history of eras that have taken place. +#[derive(PartialEq, Eq)] +pub struct EraHistory { + pub eras: Vec, +} + +#[derive(PartialEq, Eq)] +pub enum TimeHorizonError { + SlotIsPastTimeHorizon(u64), + InvalidEraHistory(u64), +} + +// The last era in the provided EraHistory must end at the time horizon for +// correct results. Returns number of seconds elapsed since the system start +// time. +pub fn slot_to_relative_time(eras: &EraHistory, slot: u64) -> Result { + for era in &eras.eras { + if era.era_start.bound_slot > slot { + return Err(TimeHorizonError::InvalidEraHistory(slot)) + } + let in_era = match &era.era_end { + EraEnd::Bounded(bound) => bound.bound_slot >= slot, + EraEnd::Unbounded => true, + }; + if in_era { + let slots_elapsed = slot - era.era_start.bound_slot; + let time_elapsed = era.era_params.era_slot_length * slots_elapsed; + let relative_time = era.era_start.bound_time + time_elapsed; + return Ok(relative_time) + } + } + return Err(TimeHorizonError::SlotIsPastTimeHorizon(slot)) +} + +pub fn slot_to_absolute_time(eras: &EraHistory, slot: u64, system_start: u64) -> Result { + slot_to_relative_time(eras, slot).map(|t| system_start + t) +} + +pub fn slot_to_epoch(eras: &EraHistory, slot: u64) -> Result { + for era in &eras.eras { + if era.era_start.bound_slot > slot { + return Err(TimeHorizonError::InvalidEraHistory(slot)) + } + let in_era = match &era.era_end { + EraEnd::Bounded(bound) => bound.bound_slot >= slot, + EraEnd::Unbounded => true, + }; + if in_era { + let slots_elapsed = slot - era.era_start.bound_slot; + let epochs_elapsed = slots_elapsed / era.era_params.era_epoch_size; + let epoch_number = era.era_start.bound_epoch + epochs_elapsed; + return Ok(epoch_number) + } + } + return Err(TimeHorizonError::SlotIsPastTimeHorizon(slot)) +} + +pub struct EpochBounds { + pub start_slot: u64, + pub end_slot: u64, +} + +pub fn epoch_bounds(eras: &EraHistory, epoch: u64) -> Result { + for era in &eras.eras { + if era.era_start.bound_epoch > epoch { + return Err(TimeHorizonError::InvalidEraHistory(epoch)) + } + let in_era = match &era.era_end { + // We can't answer queries about the upper bound epoch of the era because the bound is + // exclusive. + EraEnd::Bounded(bound) => bound.bound_epoch > epoch, + EraEnd::Unbounded => true, + }; + if in_era { + let epochs_elapsed = epoch - era.era_start.bound_epoch; + let offset = era.era_start.bound_slot; + let start_slot = offset + era.era_params.era_epoch_size * epochs_elapsed; + let end_slot = offset + era.era_params.era_epoch_size * (epochs_elapsed + 1); + return Ok(EpochBounds { + start_slot: start_slot, + end_slot: end_slot, + }) + } + } + return Err(TimeHorizonError::SlotIsPastTimeHorizon(epoch)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slot_to_time() { + let eras = EraHistory { + eras: vec![ + Summary { + era_start: Bound { + bound_time: 0, + bound_slot: 0, + bound_epoch: 0, + }, + era_end: EraEnd::Bounded(Bound { + bound_time: 86400000, + bound_slot: 86400, + bound_epoch: 1, + }), + era_params: EraParams { + era_epoch_size: 43200, + era_slot_length: 1000, + era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 + era_genesis_window: 0, + }, + }, + Summary { + era_start: Bound { + bound_time: 86400000, + bound_slot: 86400, + bound_epoch: 1, + }, + era_end: EraEnd::Bounded(Bound { + bound_time: 172800000, + bound_slot: 172800, + bound_epoch: 2, + }), + era_params: EraParams { + era_epoch_size: 43200, + era_slot_length: 1000, + era_safe_zone: 129600, + era_genesis_window: 0, + }, + }, + ], + }; + let t0 = slot_to_relative_time(&eras, 172801); + match t0 { + Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + assert_eq!(s, 172801); + } + _ => { + panic!("expected error"); + } + } + let t1 = slot_to_relative_time(&eras, 172800); + match t1 { + Ok(t) => { + assert_eq!(t, 172800000); + } + _ => { + panic!("expected no error"); + } + } + } + + #[test] + fn test_epoch_bounds() { + let eras = EraHistory { + eras: vec![ + Summary { + era_start: Bound { + bound_time: 0, + bound_slot: 0, + bound_epoch: 0, + }, + era_end: EraEnd::Bounded(Bound { + bound_time: 864000000, + bound_slot: 864000, + bound_epoch: 10, + }), + era_params: EraParams { + era_epoch_size: 86400, + era_slot_length: 1000, + era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 + era_genesis_window: 0, + }, + }, + ], + }; + let bounds0 = epoch_bounds(&eras, 1); + match bounds0 { + Ok(e) => { + assert_eq!(e.start_slot, 86400); + assert_eq!(e.end_slot, 172800); + } + _ => { + panic!("expected no error"); + } + } + let bounds1 = epoch_bounds(&eras, 10); + match bounds1 { + Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + assert_eq!(s, 10); + } + _ => { + panic!("expected error"); + } + } + } + + #[test] + fn test_slot_to_epoch() { + let eras = EraHistory { + eras: vec![ + Summary { + era_start: Bound { + bound_time: 0, + bound_slot: 0, + bound_epoch: 0, + }, + era_end: EraEnd::Bounded(Bound { + bound_time: 864000000, + bound_slot: 864000, + bound_epoch: 10, + }), + era_params: EraParams { + era_epoch_size: 86400, + era_slot_length: 1000, + era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 + era_genesis_window: 0, + }, + }, + ], + }; + let e0 = slot_to_epoch(&eras, 0); + match e0 { + Ok(e) => { + assert_eq!(e, 0); + } + _ => { + panic!("expected no error"); + } + } + let e1 = slot_to_epoch(&eras, 86399); + match e1 { + Ok(e) => { + assert_eq!(e, 0); + } + _ => { + panic!("expected no error"); + } + } + let e2 = slot_to_epoch(&eras, 864000); + match e2 { + Ok(e) => { + assert_eq!(e, 10); + } + _ => { + panic!("expected no error"); + } + } + let e3 = slot_to_epoch(&eras, 864001); + match e3 { + Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + assert_eq!(s, 864001); + } + _ => { + panic!("expected error"); + } + } + } +} From 2c5032d78a5ff9497183fd1a2b898afe81d3c6b8 Mon Sep 17 00:00:00 2001 From: rrruko Date: Wed, 18 Dec 2024 05:52:29 -0800 Subject: [PATCH 2/8] comments --- crates/amaru/src/time.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/amaru/src/time.rs b/crates/amaru/src/time.rs index c305a74..d35a1df 100644 --- a/crates/amaru/src/time.rs +++ b/crates/amaru/src/time.rs @@ -1,6 +1,6 @@ #[derive(PartialEq, Eq)] pub struct Bound { - pub bound_time: u64, + pub bound_time: u64, // Milliseconds pub bound_slot: u64, pub bound_epoch: u64, } @@ -14,8 +14,7 @@ pub enum EraEnd { #[derive(PartialEq, Eq)] pub struct EraParams { pub era_epoch_size: u64, - // Milliseconds - pub era_slot_length: u64, + pub era_slot_length: u64, // Milliseconds // If the next era has not yet been announced then it is impossible for it // to occur in the next `era_safe_zone` slots after the tip. But because a // fork can only occur at an epoch boundary, in practice the safe zone @@ -24,6 +23,8 @@ pub struct EraParams { pub era_genesis_window: u64, } +// The start is inclusive and the end is exclusive. In a valid EraHistory, the +// end of each era will equal the start of the next one. #[derive(PartialEq, Eq)] pub struct Summary { pub era_start: Bound, @@ -43,9 +44,9 @@ pub enum TimeHorizonError { InvalidEraHistory(u64), } -// The last era in the provided EraHistory must end at the time horizon for -// correct results. Returns number of seconds elapsed since the system start -// time. +// The last era in the provided EraHistory must end at the time horizon for accurate results. The +// horizon is the end of the epoch containing the end of the current era's safe zone relative to +// the current tip. Returns number of milliseconds elapsed since the system start time. pub fn slot_to_relative_time(eras: &EraHistory, slot: u64) -> Result { for era in &eras.eras { if era.era_start.bound_slot > slot { From b0a6acdce88f7d170d2bb7394a04b0eb354c0d08 Mon Sep 17 00:00:00 2001 From: rrruko Date: Wed, 18 Dec 2024 10:26:01 -0800 Subject: [PATCH 3/8] relative time to slot --- crates/amaru/src/time.rs | 167 +++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 85 deletions(-) diff --git a/crates/amaru/src/time.rs b/crates/amaru/src/time.rs index d35a1df..d1ec835 100644 --- a/crates/amaru/src/time.rs +++ b/crates/amaru/src/time.rs @@ -5,31 +5,25 @@ pub struct Bound { pub bound_epoch: u64, } -#[derive(PartialEq, Eq)] -pub enum EraEnd { - Bounded(Bound), - Unbounded, -} - #[derive(PartialEq, Eq)] pub struct EraParams { - pub era_epoch_size: u64, - pub era_slot_length: u64, // Milliseconds + pub epoch_size: u64, + pub slot_length: u64, // Milliseconds // If the next era has not yet been announced then it is impossible for it - // to occur in the next `era_safe_zone` slots after the tip. But because a + // to occur in the next `safe_zone` slots after the tip. But because a // fork can only occur at an epoch boundary, in practice the safe zone // will get rounded up to the end of that epoch. - pub era_safe_zone: u64, - pub era_genesis_window: u64, + pub safe_zone: u64, + pub genesis_window: u64, } // The start is inclusive and the end is exclusive. In a valid EraHistory, the // end of each era will equal the start of the next one. #[derive(PartialEq, Eq)] pub struct Summary { - pub era_start: Bound, - pub era_end: EraEnd, - pub era_params: EraParams, + pub start: Bound, + pub end: Bound, + pub params: EraParams, } // A complete history of eras that have taken place. @@ -40,8 +34,8 @@ pub struct EraHistory { #[derive(PartialEq, Eq)] pub enum TimeHorizonError { - SlotIsPastTimeHorizon(u64), - InvalidEraHistory(u64), + PastTimeHorizon, + InvalidEraHistory, } // The last era in the provided EraHistory must end at the time horizon for accurate results. The @@ -49,44 +43,51 @@ pub enum TimeHorizonError { // the current tip. Returns number of milliseconds elapsed since the system start time. pub fn slot_to_relative_time(eras: &EraHistory, slot: u64) -> Result { for era in &eras.eras { - if era.era_start.bound_slot > slot { - return Err(TimeHorizonError::InvalidEraHistory(slot)) + if era.start.bound_slot > slot { + return Err(TimeHorizonError::InvalidEraHistory) } - let in_era = match &era.era_end { - EraEnd::Bounded(bound) => bound.bound_slot >= slot, - EraEnd::Unbounded => true, - }; - if in_era { - let slots_elapsed = slot - era.era_start.bound_slot; - let time_elapsed = era.era_params.era_slot_length * slots_elapsed; - let relative_time = era.era_start.bound_time + time_elapsed; + if era.end.bound_slot >= slot { + let slots_elapsed = slot - era.start.bound_slot; + let time_elapsed = era.params.slot_length * slots_elapsed; + let relative_time = era.start.bound_time + time_elapsed; return Ok(relative_time) } } - return Err(TimeHorizonError::SlotIsPastTimeHorizon(slot)) + return Err(TimeHorizonError::PastTimeHorizon) } pub fn slot_to_absolute_time(eras: &EraHistory, slot: u64, system_start: u64) -> Result { slot_to_relative_time(eras, slot).map(|t| system_start + t) } +pub fn relative_time_to_slot(eras: &EraHistory, time: u64) -> Result { + for era in &eras.eras { + if era.start.bound_time > time { + return Err(TimeHorizonError::InvalidEraHistory) + } + if era.end.bound_time >= time { + let time_elapsed = time - era.start.bound_time; + let slots_elapsed = time_elapsed / era.params.slot_length; + let slot = era.start.bound_slot + slots_elapsed; + return Ok(slot) + } + } + return Err(TimeHorizonError::PastTimeHorizon) +} + pub fn slot_to_epoch(eras: &EraHistory, slot: u64) -> Result { for era in &eras.eras { - if era.era_start.bound_slot > slot { - return Err(TimeHorizonError::InvalidEraHistory(slot)) + if era.start.bound_slot > slot { + return Err(TimeHorizonError::InvalidEraHistory) } - let in_era = match &era.era_end { - EraEnd::Bounded(bound) => bound.bound_slot >= slot, - EraEnd::Unbounded => true, - }; - if in_era { - let slots_elapsed = slot - era.era_start.bound_slot; - let epochs_elapsed = slots_elapsed / era.era_params.era_epoch_size; - let epoch_number = era.era_start.bound_epoch + epochs_elapsed; + if era.end.bound_slot >= slot { + let slots_elapsed = slot - era.start.bound_slot; + let epochs_elapsed = slots_elapsed / era.params.epoch_size; + let epoch_number = era.start.bound_epoch + epochs_elapsed; return Ok(epoch_number) } } - return Err(TimeHorizonError::SlotIsPastTimeHorizon(slot)) + return Err(TimeHorizonError::PastTimeHorizon) } pub struct EpochBounds { @@ -96,27 +97,23 @@ pub struct EpochBounds { pub fn epoch_bounds(eras: &EraHistory, epoch: u64) -> Result { for era in &eras.eras { - if era.era_start.bound_epoch > epoch { - return Err(TimeHorizonError::InvalidEraHistory(epoch)) + if era.start.bound_epoch > epoch { + return Err(TimeHorizonError::InvalidEraHistory) } - let in_era = match &era.era_end { // We can't answer queries about the upper bound epoch of the era because the bound is // exclusive. - EraEnd::Bounded(bound) => bound.bound_epoch > epoch, - EraEnd::Unbounded => true, - }; - if in_era { - let epochs_elapsed = epoch - era.era_start.bound_epoch; - let offset = era.era_start.bound_slot; - let start_slot = offset + era.era_params.era_epoch_size * epochs_elapsed; - let end_slot = offset + era.era_params.era_epoch_size * (epochs_elapsed + 1); + if era.end.bound_epoch > epoch { + let epochs_elapsed = epoch - era.start.bound_epoch; + let offset = era.start.bound_slot; + let start_slot = offset + era.params.epoch_size * epochs_elapsed; + let end_slot = offset + era.params.epoch_size * (epochs_elapsed + 1); return Ok(EpochBounds { start_slot: start_slot, end_slot: end_slot, }) } } - return Err(TimeHorizonError::SlotIsPastTimeHorizon(epoch)); + return Err(TimeHorizonError::PastTimeHorizon); } #[cfg(test)] @@ -128,46 +125,46 @@ mod tests { let eras = EraHistory { eras: vec![ Summary { - era_start: Bound { + start: Bound { bound_time: 0, bound_slot: 0, bound_epoch: 0, }, - era_end: EraEnd::Bounded(Bound { + end: Bound { bound_time: 86400000, bound_slot: 86400, bound_epoch: 1, - }), - era_params: EraParams { - era_epoch_size: 43200, - era_slot_length: 1000, - era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 - era_genesis_window: 0, + }, + params: EraParams { + epoch_size: 86400, + slot_length: 1000, + safe_zone: 25920, // 3k/f where k=432 and f=0.05 + genesis_window: 25920, }, }, Summary { - era_start: Bound { + start: Bound { bound_time: 86400000, bound_slot: 86400, bound_epoch: 1, }, - era_end: EraEnd::Bounded(Bound { + end: Bound { bound_time: 172800000, bound_slot: 172800, bound_epoch: 2, - }), - era_params: EraParams { - era_epoch_size: 43200, - era_slot_length: 1000, - era_safe_zone: 129600, - era_genesis_window: 0, + }, + params: EraParams { + epoch_size: 86400, + slot_length: 1000, + safe_zone: 25920, + genesis_window: 25920, }, }, ], }; let t0 = slot_to_relative_time(&eras, 172801); match t0 { - Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + Err(TimeHorizonError::PastTimeHorizon) => { assert_eq!(s, 172801); } _ => { @@ -190,21 +187,21 @@ mod tests { let eras = EraHistory { eras: vec![ Summary { - era_start: Bound { + start: Bound { bound_time: 0, bound_slot: 0, bound_epoch: 0, }, - era_end: EraEnd::Bounded(Bound { + end: Bound { bound_time: 864000000, bound_slot: 864000, bound_epoch: 10, - }), - era_params: EraParams { - era_epoch_size: 86400, - era_slot_length: 1000, - era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 - era_genesis_window: 0, + }, + params: EraParams { + epoch_size: 86400, + slot_length: 1000, + safe_zone: 25920, + genesis_window: 25920, }, }, ], @@ -221,7 +218,7 @@ mod tests { } let bounds1 = epoch_bounds(&eras, 10); match bounds1 { - Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + Err(TimeHorizonError::PastTimeHorizon) => { assert_eq!(s, 10); } _ => { @@ -235,21 +232,21 @@ mod tests { let eras = EraHistory { eras: vec![ Summary { - era_start: Bound { + start: Bound { bound_time: 0, bound_slot: 0, bound_epoch: 0, }, - era_end: EraEnd::Bounded(Bound { + end: Bound { bound_time: 864000000, bound_slot: 864000, bound_epoch: 10, - }), - era_params: EraParams { - era_epoch_size: 86400, - era_slot_length: 1000, - era_safe_zone: 129600, // 3k/f where k=2160 and f=0.05 - era_genesis_window: 0, + }, + params: EraParams { + epoch_size: 86400, + slot_length: 1000, + safe_zone: 25920, + genesis_window: 25920, }, }, ], @@ -283,7 +280,7 @@ mod tests { } let e3 = slot_to_epoch(&eras, 864001); match e3 { - Err(TimeHorizonError::SlotIsPastTimeHorizon(s)) => { + Err(TimeHorizonError::PastTimeHorizon) => { assert_eq!(s, 864001); } _ => { From c55e2f70c28b3c5bdfd33f1e1a45d5f0d95f274d Mon Sep 17 00:00:00 2001 From: rrruko Date: Wed, 18 Dec 2024 10:28:56 -0800 Subject: [PATCH 4/8] remove unused record fields --- crates/amaru/src/time.rs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/crates/amaru/src/time.rs b/crates/amaru/src/time.rs index d1ec835..6e05e0b 100644 --- a/crates/amaru/src/time.rs +++ b/crates/amaru/src/time.rs @@ -9,12 +9,6 @@ pub struct Bound { pub struct EraParams { pub epoch_size: u64, pub slot_length: u64, // Milliseconds - // If the next era has not yet been announced then it is impossible for it - // to occur in the next `safe_zone` slots after the tip. But because a - // fork can only occur at an epoch boundary, in practice the safe zone - // will get rounded up to the end of that epoch. - pub safe_zone: u64, - pub genesis_window: u64, } // The start is inclusive and the end is exclusive. In a valid EraHistory, the @@ -100,8 +94,8 @@ pub fn epoch_bounds(eras: &EraHistory, epoch: u64) -> Result epoch { return Err(TimeHorizonError::InvalidEraHistory) } - // We can't answer queries about the upper bound epoch of the era because the bound is - // exclusive. + // We can't answer queries about the upper bound epoch of the era because the bound is + // exclusive. if era.end.bound_epoch > epoch { let epochs_elapsed = epoch - era.start.bound_epoch; let offset = era.start.bound_slot; @@ -138,8 +132,6 @@ mod tests { params: EraParams { epoch_size: 86400, slot_length: 1000, - safe_zone: 25920, // 3k/f where k=432 and f=0.05 - genesis_window: 25920, }, }, Summary { @@ -156,8 +148,6 @@ mod tests { params: EraParams { epoch_size: 86400, slot_length: 1000, - safe_zone: 25920, - genesis_window: 25920, }, }, ], @@ -165,7 +155,6 @@ mod tests { let t0 = slot_to_relative_time(&eras, 172801); match t0 { Err(TimeHorizonError::PastTimeHorizon) => { - assert_eq!(s, 172801); } _ => { panic!("expected error"); @@ -200,8 +189,6 @@ mod tests { params: EraParams { epoch_size: 86400, slot_length: 1000, - safe_zone: 25920, - genesis_window: 25920, }, }, ], @@ -219,7 +206,6 @@ mod tests { let bounds1 = epoch_bounds(&eras, 10); match bounds1 { Err(TimeHorizonError::PastTimeHorizon) => { - assert_eq!(s, 10); } _ => { panic!("expected error"); @@ -245,8 +231,6 @@ mod tests { params: EraParams { epoch_size: 86400, slot_length: 1000, - safe_zone: 25920, - genesis_window: 25920, }, }, ], @@ -281,7 +265,6 @@ mod tests { let e3 = slot_to_epoch(&eras, 864001); match e3 { Err(TimeHorizonError::PastTimeHorizon) => { - assert_eq!(s, 864001); } _ => { panic!("expected error"); From a12fed3eb7beb4446487296edf5827d42f6187af Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 20 Dec 2024 05:40:35 -0800 Subject: [PATCH 5/8] address pr feedback --- Cargo.lock | 4 + Cargo.toml | 2 +- crates/amaru/src/lib.rs | 2 - crates/amaru/src/time.rs | 274 --------------------------------------- crates/time/Cargo.toml | 12 ++ crates/time/src/lib.rs | 230 ++++++++++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 277 deletions(-) delete mode 100644 crates/amaru/src/time.rs create mode 100644 crates/time/Cargo.toml create mode 100644 crates/time/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4d5e286..a65220d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3190,6 +3190,10 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.1.0" + [[package]] name = "tinystr" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index d166a68..4da906d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/amaru"] +members = ["crates/amaru", "crates/time"] resolver = "2" [profile.dev.package] diff --git a/crates/amaru/src/lib.rs b/crates/amaru/src/lib.rs index 5754b84..d313bf4 100644 --- a/crates/amaru/src/lib.rs +++ b/crates/amaru/src/lib.rs @@ -19,5 +19,3 @@ pub mod ledger; /// /// A set of additional primitives around iterators. Not Amaru-specific so-to-speak. pub mod iter; - -pub mod time; diff --git a/crates/amaru/src/time.rs b/crates/amaru/src/time.rs deleted file mode 100644 index 6e05e0b..0000000 --- a/crates/amaru/src/time.rs +++ /dev/null @@ -1,274 +0,0 @@ -#[derive(PartialEq, Eq)] -pub struct Bound { - pub bound_time: u64, // Milliseconds - pub bound_slot: u64, - pub bound_epoch: u64, -} - -#[derive(PartialEq, Eq)] -pub struct EraParams { - pub epoch_size: u64, - pub slot_length: u64, // Milliseconds -} - -// The start is inclusive and the end is exclusive. In a valid EraHistory, the -// end of each era will equal the start of the next one. -#[derive(PartialEq, Eq)] -pub struct Summary { - pub start: Bound, - pub end: Bound, - pub params: EraParams, -} - -// A complete history of eras that have taken place. -#[derive(PartialEq, Eq)] -pub struct EraHistory { - pub eras: Vec, -} - -#[derive(PartialEq, Eq)] -pub enum TimeHorizonError { - PastTimeHorizon, - InvalidEraHistory, -} - -// The last era in the provided EraHistory must end at the time horizon for accurate results. The -// horizon is the end of the epoch containing the end of the current era's safe zone relative to -// the current tip. Returns number of milliseconds elapsed since the system start time. -pub fn slot_to_relative_time(eras: &EraHistory, slot: u64) -> Result { - for era in &eras.eras { - if era.start.bound_slot > slot { - return Err(TimeHorizonError::InvalidEraHistory) - } - if era.end.bound_slot >= slot { - let slots_elapsed = slot - era.start.bound_slot; - let time_elapsed = era.params.slot_length * slots_elapsed; - let relative_time = era.start.bound_time + time_elapsed; - return Ok(relative_time) - } - } - return Err(TimeHorizonError::PastTimeHorizon) -} - -pub fn slot_to_absolute_time(eras: &EraHistory, slot: u64, system_start: u64) -> Result { - slot_to_relative_time(eras, slot).map(|t| system_start + t) -} - -pub fn relative_time_to_slot(eras: &EraHistory, time: u64) -> Result { - for era in &eras.eras { - if era.start.bound_time > time { - return Err(TimeHorizonError::InvalidEraHistory) - } - if era.end.bound_time >= time { - let time_elapsed = time - era.start.bound_time; - let slots_elapsed = time_elapsed / era.params.slot_length; - let slot = era.start.bound_slot + slots_elapsed; - return Ok(slot) - } - } - return Err(TimeHorizonError::PastTimeHorizon) -} - -pub fn slot_to_epoch(eras: &EraHistory, slot: u64) -> Result { - for era in &eras.eras { - if era.start.bound_slot > slot { - return Err(TimeHorizonError::InvalidEraHistory) - } - if era.end.bound_slot >= slot { - let slots_elapsed = slot - era.start.bound_slot; - let epochs_elapsed = slots_elapsed / era.params.epoch_size; - let epoch_number = era.start.bound_epoch + epochs_elapsed; - return Ok(epoch_number) - } - } - return Err(TimeHorizonError::PastTimeHorizon) -} - -pub struct EpochBounds { - pub start_slot: u64, - pub end_slot: u64, -} - -pub fn epoch_bounds(eras: &EraHistory, epoch: u64) -> Result { - for era in &eras.eras { - if era.start.bound_epoch > epoch { - return Err(TimeHorizonError::InvalidEraHistory) - } - // We can't answer queries about the upper bound epoch of the era because the bound is - // exclusive. - if era.end.bound_epoch > epoch { - let epochs_elapsed = epoch - era.start.bound_epoch; - let offset = era.start.bound_slot; - let start_slot = offset + era.params.epoch_size * epochs_elapsed; - let end_slot = offset + era.params.epoch_size * (epochs_elapsed + 1); - return Ok(EpochBounds { - start_slot: start_slot, - end_slot: end_slot, - }) - } - } - return Err(TimeHorizonError::PastTimeHorizon); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_slot_to_time() { - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - bound_time: 0, - bound_slot: 0, - bound_epoch: 0, - }, - end: Bound { - bound_time: 86400000, - bound_slot: 86400, - bound_epoch: 1, - }, - params: EraParams { - epoch_size: 86400, - slot_length: 1000, - }, - }, - Summary { - start: Bound { - bound_time: 86400000, - bound_slot: 86400, - bound_epoch: 1, - }, - end: Bound { - bound_time: 172800000, - bound_slot: 172800, - bound_epoch: 2, - }, - params: EraParams { - epoch_size: 86400, - slot_length: 1000, - }, - }, - ], - }; - let t0 = slot_to_relative_time(&eras, 172801); - match t0 { - Err(TimeHorizonError::PastTimeHorizon) => { - } - _ => { - panic!("expected error"); - } - } - let t1 = slot_to_relative_time(&eras, 172800); - match t1 { - Ok(t) => { - assert_eq!(t, 172800000); - } - _ => { - panic!("expected no error"); - } - } - } - - #[test] - fn test_epoch_bounds() { - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - bound_time: 0, - bound_slot: 0, - bound_epoch: 0, - }, - end: Bound { - bound_time: 864000000, - bound_slot: 864000, - bound_epoch: 10, - }, - params: EraParams { - epoch_size: 86400, - slot_length: 1000, - }, - }, - ], - }; - let bounds0 = epoch_bounds(&eras, 1); - match bounds0 { - Ok(e) => { - assert_eq!(e.start_slot, 86400); - assert_eq!(e.end_slot, 172800); - } - _ => { - panic!("expected no error"); - } - } - let bounds1 = epoch_bounds(&eras, 10); - match bounds1 { - Err(TimeHorizonError::PastTimeHorizon) => { - } - _ => { - panic!("expected error"); - } - } - } - - #[test] - fn test_slot_to_epoch() { - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - bound_time: 0, - bound_slot: 0, - bound_epoch: 0, - }, - end: Bound { - bound_time: 864000000, - bound_slot: 864000, - bound_epoch: 10, - }, - params: EraParams { - epoch_size: 86400, - slot_length: 1000, - }, - }, - ], - }; - let e0 = slot_to_epoch(&eras, 0); - match e0 { - Ok(e) => { - assert_eq!(e, 0); - } - _ => { - panic!("expected no error"); - } - } - let e1 = slot_to_epoch(&eras, 86399); - match e1 { - Ok(e) => { - assert_eq!(e, 0); - } - _ => { - panic!("expected no error"); - } - } - let e2 = slot_to_epoch(&eras, 864000); - match e2 { - Ok(e) => { - assert_eq!(e, 10); - } - _ => { - panic!("expected no error"); - } - } - let e3 = slot_to_epoch(&eras, 864001); - match e3 { - Err(TimeHorizonError::PastTimeHorizon) => { - } - _ => { - panic!("expected error"); - } - } - } -} diff --git a/crates/time/Cargo.toml b/crates/time/Cargo.toml new file mode 100644 index 0000000..3bb25a8 --- /dev/null +++ b/crates/time/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "time" +version = "0.1.0" +edition = "2021" +description = "Cardano time arithmetic" +license = "Apache-2.0" +authors = ["Amaru Maintainers "] +repository = "https://github.com/pragma-org/amaru" +homepage = "https://github.com/pragma-org/amaru" +documentation = "https://docs.rs/amaru" +readme = "README.md" +rust-version = "1.81.0" diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs new file mode 100644 index 0000000..4cd31e2 --- /dev/null +++ b/crates/time/src/lib.rs @@ -0,0 +1,230 @@ +type Slot = u64; + +#[derive(Clone, PartialEq, Eq)] +pub struct Bound { + pub time: u64, // Milliseconds + pub slot: Slot, + pub epoch: u64, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct EraParams { + epoch_size: u64, + slot_length: u64, // Milliseconds +} + +impl EraParams { + pub fn new(epoch_size: u64, slot_length: u64) -> Option { + if epoch_size == 0 { + return None; + } + if slot_length == 0 { + return None; + } + Some(EraParams { + epoch_size, + slot_length, + }) + } +} + +// The start is inclusive and the end is exclusive. In a valid EraHistory, the +// end of each era will equal the start of the next one. +#[derive(Clone, PartialEq, Eq)] +pub struct Summary { + pub start: Bound, + pub end: Bound, + pub params: EraParams, +} + +// A complete history of eras that have taken place. +#[derive(PartialEq, Eq)] +pub struct EraHistory { + pub eras: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum TimeHorizonError { + PastTimeHorizon, + InvalidEraHistory, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EpochBounds { + pub start: Slot, + pub end: Slot, +} + +// The last era in the provided EraHistory must end at the time horizon for accurate results. The +// horizon is the end of the epoch containing the end of the current era's safe zone relative to +// the current tip. Returns number of milliseconds elapsed since the system start time. +impl EraHistory { + pub fn slot_to_relative_time(&self, slot: u64) -> Result { + for era in &self.eras { + if era.start.slot > slot { + return Err(TimeHorizonError::InvalidEraHistory) + } + if era.end.slot >= slot { + let slots_elapsed = slot - era.start.slot; + let time_elapsed = era.params.slot_length * slots_elapsed; + let relative_time = era.start.time + time_elapsed; + return Ok(relative_time) + } + } + return Err(TimeHorizonError::PastTimeHorizon) + } + + pub fn slot_to_absolute_time(&self, slot: u64, system_start: u64) -> Result { + self.slot_to_relative_time(slot).map(|t| system_start + t) + } + + pub fn relative_time_to_slot(&self, time: u64) -> Result { + for era in &self.eras { + if era.start.time > time { + return Err(TimeHorizonError::InvalidEraHistory) + } + if era.end.time >= time { + let time_elapsed = time - era.start.time; + let slots_elapsed = time_elapsed / era.params.slot_length; + let slot = era.start.slot + slots_elapsed; + return Ok(slot) + } + } + return Err(TimeHorizonError::PastTimeHorizon) + } + + pub fn slot_to_epoch(&self, slot: u64) -> Result { + for era in &self.eras { + if era.start.slot > slot { + return Err(TimeHorizonError::InvalidEraHistory) + } + if era.end.slot >= slot { + let slots_elapsed = slot - era.start.slot; + let epochs_elapsed = slots_elapsed / era.params.epoch_size; + let epoch_number = era.start.epoch + epochs_elapsed; + return Ok(epoch_number) + } + } + return Err(TimeHorizonError::PastTimeHorizon) + } + + pub fn epoch_bounds(&self, epoch: u64) -> Result { + for era in &self.eras { + if era.start.epoch > epoch { + return Err(TimeHorizonError::InvalidEraHistory) + } + // We can't answer queries about the upper bound epoch of the era because the bound is + // exclusive. + if era.end.epoch > epoch { + let epochs_elapsed = epoch - era.start.epoch; + let offset = era.start.slot; + let start = offset + era.params.epoch_size * epochs_elapsed; + let end = offset + era.params.epoch_size * (epochs_elapsed + 1); + return Ok(EpochBounds { + start: start, + end: end, + }) + } + } + return Err(TimeHorizonError::PastTimeHorizon); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slot_to_time() { + let params = EraParams::new(86400, 1000).unwrap(); + let eras = EraHistory { + eras: vec![ + Summary { + start: Bound { + time: 0, + slot: 0, + epoch: 0, + }, + end: Bound { + time: 86400000, + slot: 86400, + epoch: 1, + }, + params: params.clone(), + }, + Summary { + start: Bound { + time: 86400000, + slot: 86400, + epoch: 1, + }, + end: Bound { + time: 172800000, + slot: 172800, + epoch: 2, + }, + params: params.clone(), + }, + ], + }; + let t0 = eras.slot_to_relative_time(172801); + assert_eq!(t0, Err(TimeHorizonError::PastTimeHorizon)); + let t1 = eras.slot_to_relative_time(172800); + assert_eq!(t1, Ok(172800000)); + } + + #[test] + fn test_epoch_bounds() { + let params = EraParams::new(86400, 1000).unwrap(); + let eras = EraHistory { + eras: vec![ + Summary { + start: Bound { + time: 0, + slot: 0, + epoch: 0, + }, + end: Bound { + time: 864000000, + slot: 864000, + epoch: 10, + }, + params: params, + }, + ], + }; + assert_eq!(eras.epoch_bounds(1).unwrap().start, 86400); + assert_eq!(eras.epoch_bounds(1).unwrap().end, 172800); + assert_eq!(eras.epoch_bounds(10), Err(TimeHorizonError::PastTimeHorizon)); + } + + #[test] + fn test_slot_to_epoch() { + let params = EraParams::new(86400, 1000).unwrap(); + let eras = EraHistory { + eras: vec![ + Summary { + start: Bound { + time: 0, + slot: 0, + epoch: 0, + }, + end: Bound { + time: 864000000, + slot: 864000, + epoch: 10, + }, + params: params, + }, + ], + }; + let e0 = eras.slot_to_epoch(0); + assert_eq!(e0, Ok(0)); + let e1 = eras.slot_to_epoch(86399); + assert_eq!(e1, Ok(0)); + let e2 = eras.slot_to_epoch(864000); + assert_eq!(e2, Ok(10)); + let e3 = eras.slot_to_epoch(864001); + assert_eq!(e3, Err(TimeHorizonError::PastTimeHorizon)); + } +} From 53f1b3d2ca569462b5f39d2edda8c51a33af9a1f Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 20 Dec 2024 09:37:18 -0800 Subject: [PATCH 6/8] encoding --- Cargo.lock | 4 ++ crates/time/Cargo.toml | 4 ++ crates/time/src/lib.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a65220d..d8c456e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3193,6 +3193,10 @@ dependencies = [ [[package]] name = "time" version = "0.1.0" +dependencies = [ + "hex", + "minicbor", +] [[package]] name = "tinystr" diff --git a/crates/time/Cargo.toml b/crates/time/Cargo.toml index 3bb25a8..e50a8bf 100644 --- a/crates/time/Cargo.toml +++ b/crates/time/Cargo.toml @@ -10,3 +10,7 @@ homepage = "https://github.com/pragma-org/amaru" documentation = "https://docs.rs/amaru" readme = "README.md" rust-version = "1.81.0" + +[dependencies] +minicbor = { version = "0.25.1", features = ["std", "half", "derive"] } +hex = "0.4.3" diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs index 4cd31e2..f4cc1e1 100644 --- a/crates/time/src/lib.rs +++ b/crates/time/src/lib.rs @@ -1,3 +1,5 @@ +use minicbor::{Encode, Decode}; + type Slot = u64; #[derive(Clone, PartialEq, Eq)] @@ -7,6 +9,21 @@ pub struct Bound { pub epoch: u64, } +impl Encode for Bound { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.begin_array()?; + self.time.encode(e, ctx)?; + self.slot.encode(e, ctx)?; + self.epoch.encode(e, ctx)?; + e.end()?; + Ok(()) + } +} + #[derive(Clone, PartialEq, Eq)] pub struct EraParams { epoch_size: u64, @@ -28,6 +45,20 @@ impl EraParams { } } +impl Encode for EraParams { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.begin_array()?; + self.epoch_size.encode(e, ctx)?; + self.slot_length.encode(e, ctx)?; + e.end()?; + Ok(()) + } +} + // The start is inclusive and the end is exclusive. In a valid EraHistory, the // end of each era will equal the start of the next one. #[derive(Clone, PartialEq, Eq)] @@ -37,12 +68,42 @@ pub struct Summary { pub params: EraParams, } +impl Encode for Summary { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.begin_array()?; + self.start.encode(e, ctx)?; + self.end.encode(e, ctx)?; + self.params.encode(e, ctx)?; + e.end()?; + Ok(()) + } +} + // A complete history of eras that have taken place. #[derive(PartialEq, Eq)] pub struct EraHistory { pub eras: Vec, } +impl Encode for EraHistory { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.begin_array()?; + for s in &self.eras { + s.encode(e, ctx)?; + } + e.end()?; + Ok(()) + } +} + #[derive(Debug, PartialEq, Eq)] pub enum TimeHorizonError { PastTimeHorizon, @@ -133,6 +194,7 @@ impl EraHistory { #[cfg(test)] mod tests { use super::*; + use hex; #[test] fn test_slot_to_time() { @@ -227,4 +289,31 @@ mod tests { let e3 = eras.slot_to_epoch(864001); assert_eq!(e3, Err(TimeHorizonError::PastTimeHorizon)); } + + #[test] + fn test_encode_era_history() { + let params = EraParams::new(86400, 1000).unwrap(); + let eras = EraHistory { + eras: vec![ + Summary { + start: Bound { + time: 0, + slot: 0, + epoch: 0, + }, + end: Bound { + time: 864000000, + slot: 864000, + epoch: 10, + }, + params: params, + }, + ], + }; + let buffer = minicbor::to_vec(&eras).unwrap(); + assert_eq!( + hex::encode(buffer), + "9f9f9f000000ff9f1a337f98001a000d2f000aff9f1a000151801903e8ffffff" + ); + } } From eed128dc5ffaa79bd709cc50099395c2721a553a Mon Sep 17 00:00:00 2001 From: rrruko Date: Thu, 9 Jan 2025 02:26:34 -0800 Subject: [PATCH 7/8] address feedback --- crates/time/src/lib.rs | 267 ++++++++++++++++++++++++++--------------- 1 file changed, 170 insertions(+), 97 deletions(-) diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs index f4cc1e1..f971a89 100644 --- a/crates/time/src/lib.rs +++ b/crates/time/src/lib.rs @@ -1,10 +1,10 @@ -use minicbor::{Encode, Decode}; +use minicbor::{Encode, Decode, Decoder}; type Slot = u64; -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Bound { - pub time: u64, // Milliseconds + pub time_ms: u64, // Milliseconds pub slot: Slot, pub epoch: u64, } @@ -16,7 +16,7 @@ impl Encode for Bound { ctx: &mut C, ) -> Result<(), minicbor::encode::Error> { e.begin_array()?; - self.time.encode(e, ctx)?; + self.time_ms.encode(e, ctx)?; self.slot.encode(e, ctx)?; self.epoch.encode(e, ctx)?; e.end()?; @@ -24,22 +24,37 @@ impl Encode for Bound { } } -#[derive(Clone, PartialEq, Eq)] +impl<'b, C> Decode<'b, C> for Bound { + fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result { + let _ = d.array()?; + let time_ms = d.u64()?; + let slot = d.u64()?; + let epoch = d.u64()?; + let _ = d.skip()?; + Ok(Bound { + time_ms, + slot, + epoch, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub struct EraParams { - epoch_size: u64, + epoch_size_slots: u64, slot_length: u64, // Milliseconds } impl EraParams { - pub fn new(epoch_size: u64, slot_length: u64) -> Option { - if epoch_size == 0 { + pub fn new(epoch_size_slots: u64, slot_length: u64) -> Option { + if epoch_size_slots == 0 { return None; } if slot_length == 0 { return None; } Some(EraParams { - epoch_size, + epoch_size_slots, slot_length, }) } @@ -52,16 +67,29 @@ impl Encode for EraParams { ctx: &mut C, ) -> Result<(), minicbor::encode::Error> { e.begin_array()?; - self.epoch_size.encode(e, ctx)?; + self.epoch_size_slots.encode(e, ctx)?; self.slot_length.encode(e, ctx)?; e.end()?; Ok(()) } } +impl<'b, C> Decode<'b, C> for EraParams { + fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result { + let _ = d.array()?; + let epoch_size_slots = d.decode()?; + let slot_length = d.decode()?; + let _ = d.skip()?; + Ok(EraParams { + epoch_size_slots, + slot_length, + }) + } +} + // The start is inclusive and the end is exclusive. In a valid EraHistory, the // end of each era will equal the start of the next one. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Summary { pub start: Bound, pub end: Bound, @@ -83,8 +111,23 @@ impl Encode for Summary { } } +impl<'b, C> Decode<'b, C> for Summary { + fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result { + let _ = d.array()?; + let start = d.decode()?; + let end = d.decode()?; + let params = d.decode()?; + let _ = d.skip()?; + Ok(Summary { + start, + end, + params, + }) + } +} + // A complete history of eras that have taken place. -#[derive(PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub struct EraHistory { pub eras: Vec, } @@ -104,6 +147,19 @@ impl Encode for EraHistory { } } +impl<'b, C> Decode<'b, C> for EraHistory { + fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result { + let mut eras = vec![]; + let eras_iter: minicbor::decode::ArrayIter = d.array_iter()?; + for era in eras_iter { + eras.push(era?); + } + Ok(EraHistory { + eras, + }) + } +} + #[derive(Debug, PartialEq, Eq)] pub enum TimeHorizonError { PastTimeHorizon, @@ -128,7 +184,7 @@ impl EraHistory { if era.end.slot >= slot { let slots_elapsed = slot - era.start.slot; let time_elapsed = era.params.slot_length * slots_elapsed; - let relative_time = era.start.time + time_elapsed; + let relative_time = era.start.time_ms + time_elapsed; return Ok(relative_time) } } @@ -141,11 +197,11 @@ impl EraHistory { pub fn relative_time_to_slot(&self, time: u64) -> Result { for era in &self.eras { - if era.start.time > time { + if era.start.time_ms > time { return Err(TimeHorizonError::InvalidEraHistory) } - if era.end.time >= time { - let time_elapsed = time - era.start.time; + if era.end.time_ms >= time { + let time_elapsed = time - era.start.time_ms; let slots_elapsed = time_elapsed / era.params.slot_length; let slot = era.start.slot + slots_elapsed; return Ok(slot) @@ -161,7 +217,7 @@ impl EraHistory { } if era.end.slot >= slot { let slots_elapsed = slot - era.start.slot; - let epochs_elapsed = slots_elapsed / era.params.epoch_size; + let epochs_elapsed = slots_elapsed / era.params.epoch_size_slots; let epoch_number = era.start.epoch + epochs_elapsed; return Ok(epoch_number) } @@ -179,8 +235,8 @@ impl EraHistory { if era.end.epoch > epoch { let epochs_elapsed = epoch - era.start.epoch; let offset = era.start.slot; - let start = offset + era.params.epoch_size * epochs_elapsed; - let end = offset + era.params.epoch_size * (epochs_elapsed + 1); + let start = offset + era.params.epoch_size_slots * epochs_elapsed; + let end = offset + era.params.epoch_size_slots * (epochs_elapsed + 1); return Ok(EpochBounds { start: start, end: end, @@ -196,124 +252,141 @@ mod tests { use super::*; use hex; - #[test] - fn test_slot_to_time() { - let params = EraParams::new(86400, 1000).unwrap(); - let eras = EraHistory { + fn default_params() -> EraParams { + EraParams::new(86400, 1000).unwrap() + } + + fn one_era() -> EraHistory { + EraHistory { + eras: vec![ + Summary { + start: Bound { + time_ms: 0, + slot: 0, + epoch: 0, + }, + end: Bound { + time_ms: 864000000, + slot: 864000, + epoch: 10, + }, + params: default_params(), + }, + ], + } + } + + fn two_eras() -> EraHistory { + EraHistory { eras: vec![ Summary { start: Bound { - time: 0, + time_ms: 0, slot: 0, epoch: 0, }, end: Bound { - time: 86400000, + time_ms: 86400000, slot: 86400, epoch: 1, }, - params: params.clone(), + params: default_params(), }, Summary { start: Bound { - time: 86400000, + time_ms: 86400000, slot: 86400, epoch: 1, }, end: Bound { - time: 172800000, + time_ms: 172800000, slot: 172800, epoch: 2, }, - params: params.clone(), + params: default_params(), }, ], - }; + } + } + + #[test] + fn slot_to_time_example_1() { + let eras = two_eras(); + let t0 = eras.slot_to_relative_time(172800); + assert_eq!(t0, Ok(172800000)); + } + + fn slot_to_time_fails_after_time_horizon() { + let eras = two_eras(); let t0 = eras.slot_to_relative_time(172801); assert_eq!(t0, Err(TimeHorizonError::PastTimeHorizon)); - let t1 = eras.slot_to_relative_time(172800); - assert_eq!(t1, Ok(172800000)); } #[test] - fn test_epoch_bounds() { - let params = EraParams::new(86400, 1000).unwrap(); - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - time: 0, - slot: 0, - epoch: 0, - }, - end: Bound { - time: 864000000, - slot: 864000, - epoch: 10, - }, - params: params, - }, - ], - }; + fn epoch_bounds_example_1() { + let eras = one_era(); assert_eq!(eras.epoch_bounds(1).unwrap().start, 86400); + } + + #[test] + fn epoch_bounds_example_2() { + let eras = one_era(); assert_eq!(eras.epoch_bounds(1).unwrap().end, 172800); + } + + #[test] + fn epoch_bounds_fails_after_time_horizon() { + let eras = one_era(); assert_eq!(eras.epoch_bounds(10), Err(TimeHorizonError::PastTimeHorizon)); } #[test] - fn test_slot_to_epoch() { - let params = EraParams::new(86400, 1000).unwrap(); - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - time: 0, - slot: 0, - epoch: 0, - }, - end: Bound { - time: 864000000, - slot: 864000, - epoch: 10, - }, - params: params, - }, - ], - }; - let e0 = eras.slot_to_epoch(0); - assert_eq!(e0, Ok(0)); - let e1 = eras.slot_to_epoch(86399); - assert_eq!(e1, Ok(0)); - let e2 = eras.slot_to_epoch(864000); - assert_eq!(e2, Ok(10)); - let e3 = eras.slot_to_epoch(864001); - assert_eq!(e3, Err(TimeHorizonError::PastTimeHorizon)); + fn slot_to_epoch_example_1() { + let eras = one_era(); + let e = eras.slot_to_epoch(0); + assert_eq!(e, Ok(0)); } #[test] - fn test_encode_era_history() { - let params = EraParams::new(86400, 1000).unwrap(); - let eras = EraHistory { - eras: vec![ - Summary { - start: Bound { - time: 0, - slot: 0, - epoch: 0, - }, - end: Bound { - time: 864000000, - slot: 864000, - epoch: 10, - }, - params: params, - }, - ], - }; + fn slot_to_epoch_example_2() { + let eras = one_era(); + let e = eras.slot_to_epoch(86399); + assert_eq!(e, Ok(0)); + } + + #[test] + fn slot_to_epoch_example_3() { + let eras = one_era(); + let e = eras.slot_to_epoch(864000); + assert_eq!(e, Ok(10)); + } + + #[test] + fn slot_to_epoch_fails_after_time_horizon() { + let eras = one_era(); + let e = eras.slot_to_epoch(864001); + assert_eq!(e, Err(TimeHorizonError::PastTimeHorizon)); + } + + #[test] + fn encode_era_history() { + let eras = one_era(); let buffer = minicbor::to_vec(&eras).unwrap(); assert_eq!( hex::encode(buffer), "9f9f9f000000ff9f1a337f98001a000d2f000aff9f1a000151801903e8ffffff" ); } + + #[test] + fn roundtrip_era_history() { + let eras = one_era(); + let buffer = minicbor::to_vec(&eras).unwrap(); + assert_eq!( + hex::encode(&buffer), + "9f9f9f000000ff9f1a337f98001a000d2f000aff9f1a000151801903e8ffffff" + ); + let decoded = minicbor::decode(&buffer).unwrap(); + assert_eq!(eras, decoded); + } } From b56d39e028dd31de28ec6a39282cad06c2b6200e Mon Sep 17 00:00:00 2001 From: rrruko Date: Thu, 9 Jan 2025 11:08:45 -0800 Subject: [PATCH 8/8] quickcheck --- Cargo.lock | 34 +++++++++++++ crates/time/Cargo.toml | 4 ++ crates/time/src/lib.rs | 112 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 142 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8c456e..b162d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -977,6 +977,16 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "envpath" version = "0.0.1-beta.3" @@ -2590,6 +2600,28 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.37" @@ -3196,6 +3228,8 @@ version = "0.1.0" dependencies = [ "hex", "minicbor", + "quickcheck", + "quickcheck_macros", ] [[package]] diff --git a/crates/time/Cargo.toml b/crates/time/Cargo.toml index e50a8bf..d63ac93 100644 --- a/crates/time/Cargo.toml +++ b/crates/time/Cargo.toml @@ -12,5 +12,9 @@ readme = "README.md" rust-version = "1.81.0" [dependencies] +quickcheck = "1.0.3" minicbor = { version = "0.25.1", features = ["std", "half", "derive"] } hex = "0.4.3" + +[dev-dependencies] +quickcheck_macros = "1" diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs index f971a89..56734a8 100644 --- a/crates/time/src/lib.rs +++ b/crates/time/src/lib.rs @@ -1,4 +1,6 @@ use minicbor::{Encode, Decode, Decoder}; +use quickcheck::{Arbitrary, Gen}; +use std::cmp::{max, min}; type Slot = u64; @@ -39,6 +41,16 @@ impl<'b, C> Decode<'b, C> for Bound { } } +impl Arbitrary for Bound { + fn arbitrary(g: &mut Gen) -> Self { + Bound { + time_ms: u64::arbitrary(g), + slot: u64::arbitrary(g), + epoch: u64::arbitrary(g), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct EraParams { epoch_size_slots: u64, @@ -87,6 +99,17 @@ impl<'b, C> Decode<'b, C> for EraParams { } } +impl Arbitrary for EraParams { + fn arbitrary(g: &mut Gen) -> Self { + EraParams { + // An epoch can't be zero slots + epoch_size_slots: max(1, u16::arbitrary(g) as u64), + // A slot can't be zero milliseconds + slot_length: max(1, u16::arbitrary(g) as u64), + } + } +} + // The start is inclusive and the end is exclusive. In a valid EraHistory, the // end of each era will equal the start of the next one. #[derive(Clone, Debug, PartialEq, Eq)] @@ -126,8 +149,34 @@ impl<'b, C> Decode<'b, C> for Summary { } } +impl Arbitrary for Summary { + fn arbitrary(g: &mut Gen) -> Summary { + let b1 = u16::arbitrary(g) as u64; + let b2 = u16::arbitrary(g) as u64; + let params = EraParams::arbitrary(g); + let start = Bound { + time_ms: u16::arbitrary(g) as u64, + slot: u16::arbitrary(g) as u64, + epoch: min(b1, b2), + }; + let epochs_elapsed = max(b1, b2) - min(b1, b2); + let slots_elapsed = epochs_elapsed * params.epoch_size_slots; + let time_elapsed = slots_elapsed * params.slot_length; + let end = Bound { + time_ms: start.time_ms + time_elapsed, + slot: start.slot + slots_elapsed, + epoch: max(b1, b2), + }; + Summary { + start, + end, + params, + } + } +} + // A complete history of eras that have taken place. -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct EraHistory { pub eras: Vec, } @@ -160,6 +209,56 @@ impl<'b, C> Decode<'b, C> for EraHistory { } } +impl Arbitrary for EraHistory { + fn arbitrary(g: &mut Gen) -> EraHistory { + // Generate a number of boundaries corresponding to the generator size + let mut boundaries = vec![]; + boundaries.push(u16::arbitrary(g) as u64); + for i in 0..g.size() { + // An era can't be zero-length + boundaries.push(boundaries[i] + max(1, u16::arbitrary(g) as u64)); + } + let genesis = Bound { + time_ms: 0, + slot: 0, + epoch: 0, + }; + let genesis_era_params = EraParams::arbitrary(g); + + let mut prev_bound = genesis; + let mut prev_era_params = genesis_era_params; + + // For each boundary, compute the time and slot for that epoch based on the era params and + // construct a summary from the boundary pair + let mut summaries = vec![]; + for i in 1..g.size() { + let boundary = boundaries[i]; + let epochs_elapsed = boundary - prev_bound.epoch; + let slots_elapsed = epochs_elapsed * prev_era_params.epoch_size_slots; + let time_elapsed = slots_elapsed * prev_era_params.slot_length; + let new_bound = Bound { + time_ms: prev_bound.time_ms + time_elapsed, + slot: prev_bound.slot + slots_elapsed, + epoch: boundary, + }; + let new_era_params = EraParams::arbitrary(g); + + summaries.push(Summary { + start: prev_bound, + end: new_bound.clone(), + params: prev_era_params, + }); + + prev_era_params = new_era_params; + prev_bound = new_bound; + } + + EraHistory { + eras: summaries, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub enum TimeHorizonError { PastTimeHorizon, @@ -251,6 +350,8 @@ impl EraHistory { mod tests { use super::*; use hex; + #[macro_use(quickcheck)] + use quickcheck_macros::quickcheck; fn default_params() -> EraParams { EraParams::new(86400, 1000).unwrap() @@ -378,14 +479,9 @@ mod tests { ); } - #[test] - fn roundtrip_era_history() { - let eras = one_era(); + #[quickcheck] + fn roundtrip_era_history(eras: EraHistory) { let buffer = minicbor::to_vec(&eras).unwrap(); - assert_eq!( - hex::encode(&buffer), - "9f9f9f000000ff9f1a337f98001a000d2f000aff9f1a000151801903e8ffffff" - ); let decoded = minicbor::decode(&buffer).unwrap(); assert_eq!(eras, decoded); }