diff --git a/pallas-traverse/Cargo.toml b/pallas-traverse/Cargo.toml index 4ddaa09..18ac45f 100644 --- a/pallas-traverse/Cargo.toml +++ b/pallas-traverse/Cargo.toml @@ -18,5 +18,8 @@ pallas-codec = { version = "0.18.0", path = "../pallas-codec" } hex = "0.4.3" thiserror = "1.0.31" +# TODO: remove once GenesisValue moves into new genesis crate +serde = "1.0.155" + [features] unstable = [] diff --git a/pallas-traverse/src/header.rs b/pallas-traverse/src/header.rs index ac27994..0be5fbc 100644 --- a/pallas-traverse/src/header.rs +++ b/pallas-traverse/src/header.rs @@ -5,7 +5,7 @@ use pallas_codec::minicbor; use pallas_crypto::hash::{Hash, Hasher}; use pallas_primitives::{alonzo, babbage, byron}; -use crate::{time, Era, Error, MultiEraHeader, OriginalHash}; +use crate::{wellknown::GenesisValues, Era, Error, MultiEraHeader, OriginalHash}; impl<'b> MultiEraHeader<'b> { pub fn decode(tag: u8, subtag: Option, cbor: &'b [u8]) -> Result { @@ -56,15 +56,16 @@ impl<'b> MultiEraHeader<'b> { pub fn slot(&self) -> u64 { match self { - MultiEraHeader::EpochBoundary(x) => { - time::byron_epoch_slot_to_absolute(x.consensus_data.epoch_id, 0) - } MultiEraHeader::AlonzoCompatible(x) => x.header_body.slot, MultiEraHeader::Babbage(x) => x.header_body.slot, - MultiEraHeader::Byron(x) => time::byron_epoch_slot_to_absolute( - x.consensus_data.0.epoch, - x.consensus_data.0.slot, - ), + MultiEraHeader::EpochBoundary(x) => { + let genesis = GenesisValues::default(); + genesis.relative_slot_to_absolute(x.consensus_data.epoch_id, 0) + } + MultiEraHeader::Byron(x) => { + let genesis = GenesisValues::default(); + genesis.relative_slot_to_absolute(x.consensus_data.0.epoch, x.consensus_data.0.slot) + } } } diff --git a/pallas-traverse/src/lib.rs b/pallas-traverse/src/lib.rs index 5744144..9ad288d 100644 --- a/pallas-traverse/src/lib.rs +++ b/pallas-traverse/src/lib.rs @@ -30,6 +30,9 @@ pub mod tx; pub mod withdrawals; pub mod witnesses; +// TODO: move to genesis crate +pub mod wellknown; + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum Era { diff --git a/pallas-traverse/src/time.rs b/pallas-traverse/src/time.rs index 55cefa8..456bcb9 100644 --- a/pallas-traverse/src/time.rs +++ b/pallas-traverse/src/time.rs @@ -1,32 +1,230 @@ -// TODO: is it safe to hardcode these values? -const WELLKNOWN_SLOT_LENGTH: u64 = 20; // 20 secs -const WELLKNOWN_EPOCH_LENGTH: u64 = 5 * 24 * 60 * 60; // 5 days +use crate::{wellknown::GenesisValues, MultiEraBlock}; -pub fn byron_epoch_slot_to_absolute(epoch: u64, sub_epoch_slot: u64) -> u64 { - ((epoch * WELLKNOWN_EPOCH_LENGTH) / WELLKNOWN_SLOT_LENGTH) + sub_epoch_slot +pub type Epoch = u64; + +pub type Slot = u64; + +pub type SubSlot = u64; + +#[inline] +fn compute_linear_timestamp( + known_slot: u64, + known_time: u64, + slot_length: u64, + query_slot: u64, +) -> u64 { + known_time + (query_slot - known_slot) * slot_length +} + +#[inline] +fn compute_era_epoch(era_slot: Slot, era_slot_length: u64, era_epoch_length: u64) -> (Epoch, Slot) { + assert!( + era_epoch_length > 0, + "epoch length needs to be greater than zero" + ); + + let epoch = (era_slot * era_slot_length) / era_epoch_length; + let reminder = era_slot % era_epoch_length; + + (epoch, reminder) +} + +pub fn compute_absolute_slot_within_era( + sub_era_epoch: Epoch, + sub_epoch_slot: Slot, + era_epoch_length: u32, + era_slot_length: u32, +) -> u64 { + ((sub_era_epoch * era_epoch_length as u64) / era_slot_length as u64) + sub_epoch_slot +} + +impl GenesisValues { + pub fn shelley_start_epoch(&self) -> Epoch { + let (epoch, _) = compute_era_epoch( + self.shelley_known_slot, + self.byron_slot_length as u64, + self.byron_epoch_length as u64, + ); + + epoch + } + + pub fn slot_to_wallclock(&self, slot: u64) -> u64 { + if slot < self.shelley_known_slot { + compute_linear_timestamp( + self.byron_known_slot, + self.byron_known_time, + self.byron_slot_length as u64, + slot, + ) + } else { + compute_linear_timestamp( + self.shelley_known_slot, + self.shelley_known_time, + self.shelley_slot_length as u64, + slot, + ) + } + } + + pub fn absolute_slot_to_relative(&self, slot: u64) -> (u64, u64) { + if slot < self.shelley_known_slot { + compute_era_epoch( + slot, + self.byron_slot_length as u64, + self.byron_epoch_length as u64, + ) + } else { + let era_slot = slot - self.shelley_known_slot; + + let (era_epoch, reminder) = compute_era_epoch( + era_slot, + self.shelley_slot_length as u64, + self.shelley_epoch_length as u64, + ); + + (self.shelley_start_epoch() + era_epoch, reminder) + } + } + + pub fn relative_slot_to_absolute(&self, epoch: Epoch, slot: Slot) -> Slot { + let shelley_start_epoch = self.shelley_start_epoch(); + + if epoch < shelley_start_epoch { + compute_absolute_slot_within_era( + epoch, + slot, + self.byron_epoch_length, + self.byron_slot_length, + ) + } else { + let byron_slots = compute_absolute_slot_within_era( + shelley_start_epoch, + 0, + self.byron_epoch_length, + self.byron_slot_length, + ); + + let shelley_slots = compute_absolute_slot_within_era( + epoch - shelley_start_epoch, + slot, + self.shelley_epoch_length, + self.shelley_slot_length, + ); + + byron_slots + shelley_slots + } + } +} + +impl<'a> MultiEraBlock<'a> { + pub fn epoch(&self, genesis: &GenesisValues) -> (Epoch, SubSlot) { + match self { + MultiEraBlock::EpochBoundary(x) => (x.header.consensus_data.epoch_id, 0), + MultiEraBlock::Byron(x) => ( + x.header.consensus_data.0.epoch, + x.header.consensus_data.0.slot, + ), + MultiEraBlock::AlonzoCompatible(x, _) => { + genesis.absolute_slot_to_relative(x.header.header_body.slot) + } + MultiEraBlock::Babbage(x) => { + genesis.absolute_slot_to_relative(x.header.header_body.slot) + } + } + } + + /// Computes the unix timestamp for the slot of the tx + pub fn wallclock(&self, genesis: &GenesisValues) -> u64 { + let slot = self.slot(); + genesis.slot_to_wallclock(slot) + } } #[cfg(test)] mod tests { - use pallas_codec::minicbor; + use super::*; + use crate::MultiEraBlock; - use crate::{byron::Block, time::byron_epoch_slot_to_absolute}; + fn assert_slot_matches_timestamp( + genesis: &GenesisValues, + slot: u64, + expected_ts: u64, + expected_epoch: u64, + expected_epoch_slot: u64, + ) { + let wallclock = genesis.slot_to_wallclock(slot); + assert_eq!(wallclock, expected_ts); - type BlockWrapper = (u16, Block); + let (epoch, epoch_slot) = genesis.absolute_slot_to_relative(slot); + assert_eq!(epoch, expected_epoch); + assert_eq!(epoch_slot, expected_epoch_slot); + } #[test] - fn knwon_slot_matches() { + fn calc_matches_mainnet_values() { + let genesis = GenesisValues::mainnet(); + + // Byron start, value copied from: + // https://explorer.cardano.org/en/block?id=f0f7892b5c333cffc4b3c4344de48af4cc63f55e44936196f365a9ef2244134f + assert_slot_matches_timestamp(&genesis, 0, 1506203091, 0, 0); + + // Byron middle, value copied from: + // https://explorer.cardano.org/en/block?id=c1b57d58761af4dc3c6bdcb3542170cec6db3c81e551cd68012774d1c38129a3 + assert_slot_matches_timestamp(&genesis, 2160007, 1549403231, 100, 7); + + // Shelley start, value copied from: + // https://explorer.cardano.org/en/block?id=aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de + assert_slot_matches_timestamp(&genesis, 4492800, 1596059091, 208, 0); + + // Shelly middle, value copied from: + // https://explorer.cardano.org/en/block?id=ca60833847d0e70a1adfa6b7f485766003cf7d96d28d481c20d4390f91b76d68 + assert_slot_matches_timestamp(&genesis, 51580240, 1643146531, 316, 431440); + + // Shelly middle, value copied from: + // https://explorer.cardano.org/en/block?id=ec07c6f74f344062db5340480e5b364aac8bb40768d184c1b1491e05c5bec4c4 + assert_slot_matches_timestamp(&genesis, 54605026, 1646171317, 324, 226); + } + + #[test] + fn calc_matches_testnet_values() { + let genesis = GenesisValues::testnet(); + + // Byron origin, value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=8f8602837f7c6f8b8867dd1cbc1842cf51a27eaed2c70ef48325d00f8efb320f + assert_slot_matches_timestamp(&genesis, 0, 1564010416, 0, 0); + + // Byron start, value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=388a82f053603f3552717d61644a353188f2d5500f4c6354cc1ad27a36a7ea91 + assert_slot_matches_timestamp(&genesis, 1031, 1564031036, 0, 1031); + + // Byron middle, value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=66102c0b80e1eebc9cddf9cab43c1bf912e4f1963d6f3b8ff948952f8409e779 + assert_slot_matches_timestamp(&genesis, 561595, 1575242316, 25, 129595); + + // Shelley start, value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=02b1c561715da9e540411123a6135ee319b02f60b9a11a603d3305556c04329f + assert_slot_matches_timestamp(&genesis, 1598400, 1595967616, 74, 0); + + // Shelley middle, value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=26a1b5a649309c0c8dd48f3069d9adea5a27edf5171dfb941b708acaf2d76dcd + assert_slot_matches_timestamp(&genesis, 48783593, 1643152809, 183, 97193); + } + + #[test] + fn known_slot_matches() { // TODO: expand this test to include more test blocks - let block_idx = 1; let block_str = include_str!("../../test_data/byron1.block"); + let block_cbor = hex::decode(block_str).expect("invalid hex"); + let block = MultiEraBlock::decode(&block_cbor).expect("invalid cbor"); - let block_bytes = hex::decode(block_str).expect(&format!("bad block file {block_idx}")); - let (_, block): BlockWrapper = minicbor::decode(&block_bytes[..]) - .expect(&format!("error decoding cbor for file {block_idx}")); + let byron = block.as_byron().unwrap(); - let computed_slot = byron_epoch_slot_to_absolute( - block.header.consensus_data.0.epoch, - block.header.consensus_data.0.slot, + let genesis = GenesisValues::default(); + + let computed_slot = genesis.relative_slot_to_absolute( + byron.header.consensus_data.0.epoch, + byron.header.consensus_data.0.slot, ); assert_eq!(computed_slot, 4492794); diff --git a/pallas-upstream/src/wellknown.rs b/pallas-traverse/src/wellknown.rs similarity index 81% rename from pallas-upstream/src/wellknown.rs rename to pallas-traverse/src/wellknown.rs index 448c406..7e5f401 100644 --- a/pallas-upstream/src/wellknown.rs +++ b/pallas-traverse/src/wellknown.rs @@ -1,20 +1,20 @@ use serde::{Deserialize, Serialize}; -use pallas_miniprotocols::{ - MAINNET_MAGIC, - TESTNET_MAGIC, - // PREVIEW_MAGIC, PRE_PRODUCTION_MAGIC, -}; +/// Well-known magic for testnet +pub const TESTNET_MAGIC: u64 = 1097911063; -use crate::framework::*; +/// Well-known magic for mainnet +pub const MAINNET_MAGIC: u64 = 764824073; -// TODO: use from pallas once available -pub const PRE_PRODUCTION_MAGIC: u64 = 1; +/// Well-known magic for preview pub const PREVIEW_MAGIC: u64 = 2; +/// Well-known magic for pre-production +pub const PRE_PRODUCTION_MAGIC: u64 = 1; + /// Well-known information about specific networks #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct WellKnownChainInfo { +pub struct GenesisValues { pub magic: u64, pub byron_epoch_length: u32, pub byron_slot_length: u32, @@ -28,10 +28,10 @@ pub struct WellKnownChainInfo { pub shelley_known_time: u64, } -impl WellKnownChainInfo { +impl GenesisValues { /// Hardcoded values for mainnet pub fn mainnet() -> Self { - WellKnownChainInfo { + GenesisValues { magic: MAINNET_MAGIC, byron_epoch_length: 432000, byron_slot_length: 20, @@ -50,7 +50,7 @@ impl WellKnownChainInfo { /// Hardcoded values for testnet pub fn testnet() -> Self { - WellKnownChainInfo { + GenesisValues { magic: TESTNET_MAGIC, byron_epoch_length: 432000, byron_slot_length: 20, @@ -68,7 +68,7 @@ impl WellKnownChainInfo { } pub fn preview() -> Self { - WellKnownChainInfo { + GenesisValues { magic: PREVIEW_MAGIC, byron_epoch_length: 432000, byron_slot_length: 20, @@ -86,7 +86,7 @@ impl WellKnownChainInfo { /// Hardcoded values for the "pre-prod" testnet pub fn preprod() -> Self { - WellKnownChainInfo { + GenesisValues { magic: PRE_PRODUCTION_MAGIC, byron_epoch_length: 432000, byron_slot_length: 20, @@ -105,20 +105,18 @@ impl WellKnownChainInfo { /// Uses the value of the magic to return either mainnet or testnet /// hardcoded values. - pub fn try_from_magic(magic: u64) -> Result { + pub fn from_magic(magic: u64) -> Option { match magic { - MAINNET_MAGIC => Ok(Self::mainnet()), - TESTNET_MAGIC => Ok(Self::testnet()), - PREVIEW_MAGIC => Ok(Self::preview()), - PRE_PRODUCTION_MAGIC => Ok(Self::preprod()), - _ => Err(Error::message( - "can't infer well-known chain from specified magic", - )), + MAINNET_MAGIC => Some(Self::mainnet()), + TESTNET_MAGIC => Some(Self::testnet()), + PREVIEW_MAGIC => Some(Self::preview()), + PRE_PRODUCTION_MAGIC => Some(Self::preprod()), + _ => None, } } } -impl Default for WellKnownChainInfo { +impl Default for GenesisValues { fn default() -> Self { Self::mainnet() } diff --git a/pallas-upstream/src/api.rs b/pallas-upstream/src/api.rs index bc192a1..3f97f94 100644 --- a/pallas-upstream/src/api.rs +++ b/pallas-upstream/src/api.rs @@ -1,5 +1,3 @@ -pub use crate::wellknown as chains; - pub use crate::cursor; pub mod n2n { diff --git a/pallas-upstream/src/lib.rs b/pallas-upstream/src/lib.rs index 9f0cd36..b630243 100644 --- a/pallas-upstream/src/lib.rs +++ b/pallas-upstream/src/lib.rs @@ -4,7 +4,6 @@ pub(crate) mod framework; pub(crate) mod plexer; pub mod cursor; -pub mod wellknown; mod api;