From 34924ad4b37f2fccf45968d60db7b22edcc420b2 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Tue, 25 Jan 2022 20:32:21 -0300 Subject: [PATCH 1/2] fix: Slot to timestamp mapping matches public explorers --- src/mapper/mod.rs | 5 ++ src/mapper/prelude.rs | 62 +++++++++++++++++------ src/sources/n2c/run.rs | 10 +++- src/sources/n2n/run.rs | 16 +++++- src/utils/mod.rs | 2 + src/utils/time.rs | 108 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 src/utils/time.rs diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index acd50db8..8aa23438 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -1,3 +1,8 @@ +//! Maps block data into multiple events +//! +//! It uses a visitor pattern to crawl the nested data structures inside a block +//! and writes output events as they are found + mod cip25; mod collect; mod crawl; diff --git a/src/mapper/prelude.rs b/src/mapper/prelude.rs index cfcce938..087d52fb 100644 --- a/src/mapper/prelude.rs +++ b/src/mapper/prelude.rs @@ -1,17 +1,22 @@ use crate::{ framework::{Event, EventContext, EventData}, pipelining::StageSender, + utils::time::{NaiveConfig as TimeConfig, NaiveProvider as NaiveTime, TimeProvider}, }; use merge::Merge; use serde::Deserialize; -use pallas::ouroboros::network::handshake::{MAINNET_MAGIC, TESTNET_MAGIC}; +use pallas::ouroboros::network::{ + handshake::{MAINNET_MAGIC, TESTNET_MAGIC}, + machines::primitives::Point, +}; use serde::Serialize; use crate::Error; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ChainWellKnownInfo { + pub shelley_slot_length: u32, pub shelley_known_slot: u64, pub shelley_known_hash: String, pub shelley_known_time: u64, @@ -21,22 +26,48 @@ impl ChainWellKnownInfo { pub fn try_from_magic(magic: u64) -> Result { match magic { MAINNET_MAGIC => Ok(ChainWellKnownInfo { - shelley_known_slot: 4492799, + shelley_slot_length: 1, + shelley_known_slot: 4492800, shelley_known_hash: - "f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a457".to_string(), - shelley_known_time: 1596059071, + "aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de".to_string(), + shelley_known_time: 1596059091, }), TESTNET_MAGIC => Ok(ChainWellKnownInfo { - shelley_known_slot: 1598399, + shelley_slot_length: 1, + shelley_known_slot: 1598400, shelley_known_hash: - "7e16781b40ebf8b6da18f7b5e8ade855d6738095ef2f1c58c77e88b6e45997a4".to_string(), - shelley_known_time: 1595967596, + "02b1c561715da9e540411123a6135ee319b02f60b9a11a603d3305556c04329f".to_string(), + shelley_known_time: 1595967616, }), _ => Err("can't infer well-known chain infro from specified magic".into()), } } } +// HACK: to glue together legacy config with new time provider +impl From for TimeConfig { + fn from(other: ChainWellKnownInfo) -> Self { + TimeConfig { + slot_length: other.shelley_slot_length, + start_slot: other.shelley_known_slot, + start_timestamp: other.shelley_known_time, + } + } +} + +impl TryFrom for Point { + type Error = crate::Error; + + fn try_from(other: ChainWellKnownInfo) -> Result { + let out = Point( + other.shelley_known_slot, + hex::decode(other.shelley_known_hash)?, + ); + + Ok(out) + } +} + #[derive(Deserialize, Clone, Debug, Default)] pub struct Config { #[serde(default)] @@ -49,24 +80,24 @@ pub struct Config { pub include_transaction_end_events: bool, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub(crate) struct EventWriter { context: EventContext, output: StageSender, - chain_info: Option, + time_provider: Option, pub(crate) config: Config, } impl EventWriter { pub fn new( output: StageSender, - chain_info: Option, + well_known: Option, config: Config, ) -> Self { EventWriter { context: EventContext::default(), output, - chain_info, + time_provider: well_known.map(|x| NaiveTime::new(x.into())), config, } } @@ -98,14 +129,15 @@ impl EventWriter { EventWriter { context: extra_context, output: self.output.clone(), - chain_info: self.chain_info.clone(), + time_provider: self.time_provider.clone(), config: self.config.clone(), } } pub fn compute_timestamp(&self, slot: u64) -> Option { - self.chain_info - .as_ref() - .map(|info| info.shelley_known_time + (slot - info.shelley_known_slot)) + match &self.time_provider { + Some(provider) => provider.slot_to_wallclock(slot).ok(), + _ => None, + } } } diff --git a/src/sources/n2c/run.rs b/src/sources/n2c/run.rs index 3685b0a1..d409bda1 100644 --- a/src/sources/n2c/run.rs +++ b/src/sources/n2c/run.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use log::{error, info}; use pallas::{ @@ -43,9 +45,15 @@ impl BlockLike for Content { } } -#[derive(Debug)] struct ChainObserver(EventWriter); +// workaround to put a stop on excessive debug requirement coming from Pallas +impl Debug for ChainObserver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ChainObserver").finish() + } +} + impl ChainObserver { fn new(writer: EventWriter) -> Self { Self(writer) diff --git a/src/sources/n2n/run.rs b/src/sources/n2n/run.rs index e6dcad10..138fb124 100644 --- a/src/sources/n2n/run.rs +++ b/src/sources/n2n/run.rs @@ -1,4 +1,5 @@ use minicbor::data::Tag; +use std::fmt::Debug; use log::{info, warn}; @@ -55,9 +56,14 @@ impl BlockLike for Content { } } -#[derive(Debug)] struct Block2EventMapper(EventWriter); +impl Debug for Block2EventMapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Block2EventMapper").finish() + } +} + impl BlockObserver for Block2EventMapper { fn on_block_received(&self, body: Vec) -> Result<(), Error> { let maybe_block = alonzo::BlockWrapper::decode_fragment(&body[..]); @@ -80,12 +86,18 @@ impl BlockObserver for Block2EventMapper { } } -#[derive(Debug)] struct ChainObserver { block_requests: SyncSender, event_writer: EventWriter, } +// workaround to put a stop on excessive debug requirement coming from Pallas +impl Debug for ChainObserver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ChainObserver").finish() + } +} + impl Observer for ChainObserver { fn on_block(&self, cursor: &Option, _content: &Content) -> Result<(), Error> { info!("requesting block fetch for point {:?}", cursor); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 76d6753e..227064f8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,8 @@ use crate::framework::Error; pub mod throttle; +pub(crate) mod time; + pub(crate) trait SwallowResult { fn ok_or_warn(self, context: &'static str); } diff --git a/src/utils/time.rs b/src/utils/time.rs new file mode 100644 index 00000000..3dc7ae17 --- /dev/null +++ b/src/utils/time.rs @@ -0,0 +1,108 @@ +//! Blockchain time utils +//! +//! Common operations to deal with blockchain time and wallclock conversions + +use serde::Deserialize; + +use crate::Error; + +/// Abstraction available to stages to deal with blockchain time conversions +pub(crate) trait TimeProvider { + /// Maps between slots and wallclock + fn slot_to_wallclock(&self, slot: u64) -> Result; +} + +#[derive(Deserialize, Clone)] +pub struct NaiveConfig { + pub slot_length: u32, + pub start_slot: u64, + pub start_timestamp: u64, +} + +/// A naive, standalone implementation of a time provider +/// +/// This time provider doesn't require any external resources other than an +/// initial config. It works by applying simple slot => wallclock conversion +/// logic from a well-known configured point in the chain, assuming homogeneous +/// slot length from that point forward. +#[derive(Clone)] +pub(crate) struct NaiveProvider(NaiveConfig); + +impl NaiveProvider { + pub fn new(config: NaiveConfig) -> Self { + NaiveProvider(config) + } +} + +impl TimeProvider for NaiveProvider { + fn slot_to_wallclock(&self, slot: u64) -> Result { + let NaiveProvider(config) = self; + + if slot < config.start_slot { + return Err( + "naive time provider can't compute wallclock for slots after start_slot".into(), + ); + } + + let total_delta_secs = (slot - config.start_slot) * config.slot_length as u64; + + let out = config.start_timestamp + total_delta_secs; + + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn shelley_mainnet() -> NaiveConfig { + NaiveConfig { + slot_length: 1, + start_slot: 4492800, + start_timestamp: 1596059091, + } + } + + fn shelley_testnet() -> NaiveConfig { + NaiveConfig { + slot_length: 1, + start_slot: 1598400, + start_timestamp: 1595967616, + } + } + + fn assert_slot_matches_timestamp(provider: &NaiveProvider, slot: u64, ts: u64) { + let wallclock = provider + .slot_to_wallclock(slot) + .expect("unable to compute wallclock"); + + assert_eq!(wallclock, ts); + } + + #[test] + fn naive_provider_matches_mainnet_values() { + let provider = NaiveProvider::new(shelley_mainnet()); + + // value copied from: + // https://explorer.cardano.org/en/block?id=aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de + assert_slot_matches_timestamp(&provider, 4492800, 1596059091); + + // value copied from: + // https://explorer.cardano.org/en/block?id=ca60833847d0e70a1adfa6b7f485766003cf7d96d28d481c20d4390f91b76d68 + assert_slot_matches_timestamp(&provider, 51580240, 1643146531); + } + + #[test] + fn naive_provider_matches_testnet_values() { + let provider = NaiveProvider::new(shelley_testnet()); + + // value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=02b1c561715da9e540411123a6135ee319b02f60b9a11a603d3305556c04329f + assert_slot_matches_timestamp(&provider, 1598400, 1595967616); + + // value copied from: + // https://explorer.cardano-testnet.iohkdev.io/en/block?id=26a1b5a649309c0c8dd48f3069d9adea5a27edf5171dfb941b708acaf2d76dcd + assert_slot_matches_timestamp(&provider, 48783593, 1643152809); + } +} From 9760a52e497543fe21de5a52b42fcf3769e793ae Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Tue, 25 Jan 2022 20:36:16 -0300 Subject: [PATCH 2/2] Tidy up details --- src/mapper/mod.rs | 5 ----- src/utils/time.rs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index 8aa23438..acd50db8 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -1,8 +1,3 @@ -//! Maps block data into multiple events -//! -//! It uses a visitor pattern to crawl the nested data structures inside a block -//! and writes output events as they are found - mod cip25; mod collect; mod crawl; diff --git a/src/utils/time.rs b/src/utils/time.rs index 3dc7ae17..bb741f07 100644 --- a/src/utils/time.rs +++ b/src/utils/time.rs @@ -40,7 +40,7 @@ impl TimeProvider for NaiveProvider { if slot < config.start_slot { return Err( - "naive time provider can't compute wallclock for slots after start_slot".into(), + "naive time provider can't compute wallclock for slots prior to start_slot".into(), ); }