Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement slot arithmetic #39

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions crates/amaru/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
274 changes: 274 additions & 0 deletions crates/amaru/src/time.rs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put that in another crate actually 🫡

Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
#[derive(PartialEq, Eq)]
pub struct Bound {
pub bound_time: u64, // Milliseconds
pub bound_slot: u64,
pub bound_epoch: u64,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub bound_time: u64, // Milliseconds
pub bound_slot: u64,
pub bound_epoch: u64,
pub time: u64, // Milliseconds
pub slot: u64,
pub epoch: u64,

}

#[derive(PartialEq, Eq)]
pub struct EraParams {
pub epoch_size: u64,
pub slot_length: u64, // Milliseconds
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that epoch_size and slot_length can't be null, it might be worth making those fields private and defining a smart constructor enforcing those invariants.


// 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i recall correctly, eras don't necessarily have an upper-bound (the last one in particular). At least, it used to be like that but I recall it was changed around Babbage. Not sure how anymore. To be confirmed 😅 ...

pub params: EraParams,
}

// A complete history of eras that have taken place.
#[derive(PartialEq, Eq)]
pub struct EraHistory {
pub eras: Vec<Summary>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we'll eventually have to persist this on disk; so defining CBOR (minicbor) encoding and decoding for it would be useful. JSON (via serde) is also arguably useful, though priority is on CBOR.


#[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<u64, TimeHorizonError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like all functions in this module are defined on &EraHistory, so perhaps consider making them methods of that type?

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<u64, TimeHorizonError> {
slot_to_relative_time(eras, slot).map(|t| system_start + t)
}

pub fn relative_time_to_slot(eras: &EraHistory, time: u64) -> Result<u64, TimeHorizonError> {
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<u64, TimeHorizonError> {
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub start_slot: u64,
pub end_slot: u64,
pub start: Slot,
pub end: Slot,

And define somewhere type Slot = u64, that better convey the intent IMO, even though it's semantically the same.

}

pub fn epoch_bounds(eras: &EraHistory, epoch: u64) -> Result<EpochBounds, TimeHorizonError> {
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");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
match t1 {
Ok(t) => {
assert_eq!(t, 172800000);
}
_ => {
panic!("expected no error");
}
}
assert_eq!(t1, Ok(172800000));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both more concise and will provide a better error message showing the actual unexpected value.

}

#[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");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let bounds1 = epoch_bounds(&eras, 10);
match bounds1 {
Err(TimeHorizonError::PastTimeHorizon) => {
}
_ => {
panic!("expected error");
}
}
assert_eq!(epoch_bounds(&eras, 10), Err(TimeHorizonError::PastTimeHorizon))

}

#[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");
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for all those 😅, pardon my laziness to suggest changes.

}