From 9ec8d094952aed0d48202e709b7236866dd0b540 Mon Sep 17 00:00:00 2001 From: drcpu Date: Fri, 21 Jun 2024 23:31:13 +0000 Subject: [PATCH] feature(data_structures): switch block time when v2.0 activates --- config/src/config.rs | 8 +- config/src/defaults.rs | 4 +- data_structures/src/chain/mod.rs | 92 +++++++++++++++++----- data_structures/src/lib.rs | 31 ++++++-- data_structures/src/proto/versioning.rs | 14 +++- data_structures/src/transaction_factory.rs | 2 +- node/src/actors/chain_manager/mod.rs | 6 ++ node/src/actors/epoch_manager/mod.rs | 23 ++++-- node/src/actors/sessions_manager/actor.rs | 17 ++-- node/tests/actors/epoch_manager.rs | 66 +++++++++++++++- src/cli/mod.rs | 6 +- validations/src/validations.rs | 4 +- wallet/src/lib.rs | 20 ++++- wallet/src/repository/wallet/mod.rs | 8 +- 14 files changed, 245 insertions(+), 56 deletions(-) diff --git a/config/src/config.rs b/config/src/config.rs index d18e5e8cf..f558039f5 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -432,13 +432,13 @@ pub struct Tapi { /// Configuration related to protocol versions. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct Protocol { - pub v1_7: Option, - pub v1_8: Option, - pub v2_0: Option, + pub v1_7: Option<(Epoch, u16)>, + pub v1_8: Option<(Epoch, u16)>, + pub v2_0: Option<(Epoch, u16)>, } impl Protocol { - pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option), 3> { + pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option<(Epoch, u16)>), 3> { [ (ProtocolVersion::V1_7, self.v1_7), (ProtocolVersion::V1_8, self.v1_8), diff --git a/config/src/defaults.rs b/config/src/defaults.rs index 7ae8017ac..93bb42112 100644 --- a/config/src/defaults.rs +++ b/config/src/defaults.rs @@ -481,8 +481,8 @@ pub trait Defaults { 100 } - fn protocol_versions(&self) -> HashMap { - [(ProtocolVersion::V1_7, 0)].into_iter().collect() + fn protocol_versions(&self) -> HashMap { + [(ProtocolVersion::V1_7, (0, 45))].into_iter().collect() } } diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 24ee8e5ca..da4494ab8 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -4478,6 +4478,12 @@ pub struct EpochConstants { /// Period between checkpoints, in seconds pub checkpoints_period: u16, + + /// Timestamp of checkpoint (in seconds) when v2 started) + pub checkpoint_zero_timestamp_v2: i64, + + /// Period between checkpoints, in seconds, starting at v2 + pub checkpoints_period_v2: u16, } // This default is only used for tests @@ -4487,6 +4493,10 @@ impl Default for EpochConstants { checkpoint_zero_timestamp: 0, // This cannot be 0 because we would divide by zero checkpoints_period: 1, + // Variables for v2 + checkpoint_zero_timestamp_v2: i64::MAX, + // This cannot be 0 because we would divide by zero + checkpoints_period_v2: 1, } } } @@ -4494,39 +4504,85 @@ impl Default for EpochConstants { impl EpochConstants { /// Calculate the last checkpoint (current epoch) at the supplied timestamp pub fn epoch_at(&self, timestamp: i64) -> Result { - let zero = self.checkpoint_zero_timestamp; - let period = self.checkpoints_period; - let elapsed = timestamp - zero; + if timestamp >= self.checkpoint_zero_timestamp_v2 { + let epochs_pre_v2 = match Epoch::try_from( + self.checkpoint_zero_timestamp_v2 - self.checkpoint_zero_timestamp, + ) { + Ok(epoch) => epoch / Epoch::from(self.checkpoints_period), + Err(_) => { + return Err(EpochCalculationError::CheckpointZeroInTheFuture( + self.checkpoint_zero_timestamp, + )); + } + }; + let epochs_post_v2 = + match Epoch::try_from(timestamp - self.checkpoint_zero_timestamp_v2) { + Ok(epoch) => epoch / Epoch::from(self.checkpoints_period_v2), + Err(_) => { + return Err(EpochCalculationError::CheckpointZeroInTheFuture( + self.checkpoint_zero_timestamp, + )); + } + }; - Epoch::try_from(elapsed) - .map(|epoch| epoch / Epoch::from(period)) - .map_err(|_| EpochCalculationError::CheckpointZeroInTheFuture(zero)) + Ok(epochs_pre_v2 + epochs_post_v2) + } else { + Epoch::try_from(timestamp - self.checkpoint_zero_timestamp) + .map(|epoch| epoch / Epoch::from(self.checkpoints_period)) + .map_err(|_| { + EpochCalculationError::CheckpointZeroInTheFuture(self.checkpoint_zero_timestamp) + }) + } } /// Calculate the timestamp for a checkpoint (the start of an epoch) - pub fn epoch_timestamp(&self, epoch: Epoch) -> Result { - let zero = self.checkpoint_zero_timestamp; - let period = self.checkpoints_period; - - Epoch::from(period) + pub fn epoch_timestamp(&self, epoch: Epoch) -> Result<(i64, bool), EpochCalculationError> { + let epoch_timestamp = Epoch::from(self.checkpoints_period) .checked_mul(epoch) .filter(|&x| x <= Epoch::MAX as Epoch) .map(i64::from) - .and_then(|x| x.checked_add(zero)) - .ok_or(EpochCalculationError::Overflow) + .and_then(|x| x.checked_add(self.checkpoint_zero_timestamp)) + .ok_or(EpochCalculationError::Overflow); + + let epoch_timestamp = match epoch_timestamp { + Ok(timestamp) => timestamp, + Err(error) => { + return Err(error); + } + }; + + let mut in_v2 = false; + let timestamp = if epoch_timestamp >= self.checkpoint_zero_timestamp_v2 { + in_v2 = true; + + let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2 + - self.checkpoint_zero_timestamp) + / self.checkpoints_period as i64) as u32; + + self.checkpoint_zero_timestamp_v2 + + i64::from((epoch - epochs_pre_v2) * Epoch::from(self.checkpoints_period_v2)) + } else { + epoch_timestamp + }; + + Ok((timestamp, in_v2)) } /// Calculate the timestamp for when block mining should happen. pub fn block_mining_timestamp(&self, epoch: Epoch) -> Result { - let start = self.epoch_timestamp(epoch)?; + let (start, in_v2) = self.epoch_timestamp(epoch)?; // TODO: analyze when should nodes start mining a block // Start mining at the midpoint of the epoch - let seconds_before_next_epoch = self.checkpoints_period / 2; + let checkpoints_period = if in_v2 { + self.checkpoints_period_v2 + } else { + self.checkpoints_period + }; + + let seconds_before_next_epoch = checkpoints_period / 2; start - .checked_add(i64::from( - self.checkpoints_period - seconds_before_next_epoch, - )) + .checked_add(i64::from(checkpoints_period - seconds_before_next_epoch)) .ok_or(EpochCalculationError::Overflow) } } diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 3d0283d73..825fb5813 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -139,14 +139,18 @@ pub fn get_protocol_version(epoch: Option) -> ProtocolVersion { } /// Let the protocol versions controller know about the a protocol version, and its activation epoch. -pub fn register_protocol_version(protocol_version: ProtocolVersion, epoch: Epoch) { +pub fn register_protocol_version( + protocol_version: ProtocolVersion, + epoch: Epoch, + checkpoint_period: u16, +) { log::debug!( "Registering protocol version {protocol_version}, which enters into force at epoch {epoch}" ); // This unwrap is safe as long as the lock is not poisoned. // The lock can only become poisoned when a writer panics. let mut protocol_info = PROTOCOL.write().unwrap(); - protocol_info.register(epoch, protocol_version); + protocol_info.register(epoch, protocol_version, checkpoint_period); } /// Set the protocol version that we are running. @@ -163,6 +167,23 @@ pub fn refresh_protocol_version(current_epoch: Epoch) { set_protocol_version(current_version) } +pub fn get_protocol_version_activation_epoch(protocol_version: ProtocolVersion) -> Epoch { + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + let protocol = PROTOCOL.write().unwrap(); + protocol.all_versions.get_activation_epoch(protocol_version) +} + +pub fn get_protocol_version_period(protocol_version: ProtocolVersion) -> u16 { + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + let protocol = PROTOCOL.write().unwrap(); + match protocol.all_checkpoints_periods.get(&protocol_version) { + Some(period) => *period, + None => u16::MAX, + } +} + #[cfg(test)] mod tests { use super::*; @@ -183,9 +204,9 @@ mod tests { assert_eq!(version, ProtocolVersion::V1_7); // Register the different protocol versions - register_protocol_version(ProtocolVersion::V1_7, 100); - register_protocol_version(ProtocolVersion::V1_8, 200); - register_protocol_version(ProtocolVersion::V2_0, 300); + register_protocol_version(ProtocolVersion::V1_7, 100, 45); + register_protocol_version(ProtocolVersion::V1_8, 200, 45); + register_protocol_version(ProtocolVersion::V2_0, 300, 20); // The initial protocol version should be the default one let version = get_protocol_version(Some(0)); diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs index 46d8e6534..545e375a6 100644 --- a/data_structures/src/proto/versioning.rs +++ b/data_structures/src/proto/versioning.rs @@ -31,11 +31,14 @@ use crate::{ pub struct ProtocolInfo { pub current_version: ProtocolVersion, pub all_versions: VersionsMap, + pub all_checkpoints_periods: HashMap, } impl ProtocolInfo { - pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) { - self.all_versions.register(epoch, version) + pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion, checkpoint_period: u16) { + self.all_versions.register(epoch, version); + self.all_checkpoints_periods + .insert(version, checkpoint_period); } } @@ -60,6 +63,13 @@ impl VersionsMap { .copied() .unwrap_or_default() } + + pub fn get_activation_epoch(&self, version: ProtocolVersion) -> Epoch { + match self.efv.get(&version) { + Some(epoch) => *epoch, + None => Epoch::MAX, + } + } } /// An enumeration of different protocol versions. diff --git a/data_structures/src/transaction_factory.rs b/data_structures/src/transaction_factory.rs index f91de5c43..a6127f869 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -675,7 +675,7 @@ pub fn transaction_inputs_sum( })?; // Verify that commits are only accepted after the time lock expired - let epoch_timestamp = epoch_constants.epoch_timestamp(epoch)?; + let (epoch_timestamp, _) = epoch_constants.epoch_timestamp(epoch)?; let vt_time_lock = i64::try_from(vt_output.time_lock)?; if vt_time_lock > epoch_timestamp { return Err(TransactionError::TimeLock { diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index ff447574f..5f549e029 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -4072,6 +4072,8 @@ mod tests { chain_manager.epoch_constants = Some(EpochConstants { checkpoint_zero_timestamp: 0, checkpoints_period: 1_000, + checkpoint_zero_timestamp_v2: i64::MAX, + checkpoints_period_v2: 1, }); chain_manager.chain_state.chain_info = Some(ChainInfo { environment: Environment::default(), @@ -4113,10 +4115,12 @@ mod tests { Reputation(0), vrf_hash_1, false, + Power::from(0 as u64), block_2.hash(), Reputation(0), vrf_hash_2, false, + Power::from(0 as u64), &VrfSlots::new(vec![Hash::default()]), ProtocolVersion::V1_7, ), @@ -4199,6 +4203,8 @@ mod tests { chain_manager.epoch_constants = Some(EpochConstants { checkpoint_zero_timestamp: 0, checkpoints_period: 1_000, + checkpoint_zero_timestamp_v2: i64::MAX, + checkpoints_period_v2: 1, }); chain_manager.chain_state.chain_info = Some(ChainInfo { environment: Environment::default(), diff --git a/node/src/actors/epoch_manager/mod.rs b/node/src/actors/epoch_manager/mod.rs index e3c7ffdde..a87331f1d 100644 --- a/node/src/actors/epoch_manager/mod.rs +++ b/node/src/actors/epoch_manager/mod.rs @@ -7,6 +7,8 @@ use rand::Rng; use witnet_data_structures::{ chain::{Epoch, EpochConstants}, error::EpochCalculationError, + get_protocol_version_activation_epoch, get_protocol_version_period, + proto::versioning::ProtocolVersion, }; use witnet_util::timestamp::{ duration_between_timestamps, get_timestamp, get_timestamp_nanos, update_global_timestamp, @@ -85,15 +87,15 @@ impl EpochManager { pub fn set_checkpoint_zero_and_period( &mut self, checkpoint_zero_timestamp: i64, - mut checkpoints_period: u16, + checkpoints_period: u16, + checkpoint_zero_timestamp_v2: i64, + checkpoints_period_v2: u16, ) { - if checkpoints_period == 0 { - log::warn!("Setting the checkpoint period to the minimum value of 1 second"); - checkpoints_period = 1; - } self.constants = Some(EpochConstants { checkpoint_zero_timestamp, checkpoints_period, + checkpoint_zero_timestamp_v2, + checkpoints_period_v2, }); } /// Calculate the last checkpoint (current epoch) at the supplied timestamp @@ -113,7 +115,10 @@ impl EpochManager { pub fn epoch_timestamp(&self, epoch: Epoch) -> EpochResult { match &self.constants { // Calculate (period * epoch + zero) with overflow checks - Some(x) => Ok(x.epoch_timestamp(epoch)?), + Some(x) => { + let (timestamp, _) = x.epoch_timestamp(epoch)?; + Ok(timestamp) + } None => Err(EpochManagerError::UnknownEpochConstants), } } @@ -122,9 +127,15 @@ impl EpochManager { config_mngr::get() .into_actor(self) .and_then(|config, act, ctx| { + let checkpoint_zero_timestamp_v2 = + config.consensus_constants.checkpoint_zero_timestamp + + get_protocol_version_activation_epoch(ProtocolVersion::V2_0) as i64 + * config.consensus_constants.checkpoints_period as i64; act.set_checkpoint_zero_and_period( config.consensus_constants.checkpoint_zero_timestamp, config.consensus_constants.checkpoints_period, + checkpoint_zero_timestamp_v2, + get_protocol_version_period(ProtocolVersion::V2_0), ); log::info!( "Checkpoint zero timestamp: {}, checkpoints period: {}", diff --git a/node/src/actors/sessions_manager/actor.rs b/node/src/actors/sessions_manager/actor.rs index a23863846..062dfeb36 100644 --- a/node/src/actors/sessions_manager/actor.rs +++ b/node/src/actors/sessions_manager/actor.rs @@ -1,7 +1,10 @@ use super::SessionsManager; use crate::config_mngr; use actix::prelude::*; -use witnet_data_structures::chain::EpochConstants; +use witnet_data_structures::{ + chain::EpochConstants, get_protocol_version_activation_epoch, get_protocol_version_period, + proto::versioning::ProtocolVersion, +}; use witnet_util::timestamp::get_timestamp; @@ -39,16 +42,18 @@ impl Actor for SessionsManager { .set_range_limit(config.connections.reject_sybil_inbounds_range_limit); // Initialized epoch from config - let mut checkpoints_period = config.consensus_constants.checkpoints_period; + let checkpoints_period = config.consensus_constants.checkpoints_period; let checkpoint_zero_timestamp = config.consensus_constants.checkpoint_zero_timestamp; - if checkpoints_period == 0 { - log::warn!("Setting the checkpoint period to the minimum value of 1 second"); - checkpoints_period = 1; - } + let checkpoint_zero_timestamp_v2 = checkpoint_zero_timestamp + + get_protocol_version_activation_epoch(ProtocolVersion::V2_0) as i64 + * checkpoints_period as i64; + let checkpoints_period_v2 = get_protocol_version_period(ProtocolVersion::V2_0); let epoch_constants = EpochConstants { checkpoint_zero_timestamp, checkpoints_period, + checkpoint_zero_timestamp_v2, + checkpoints_period_v2, }; act.current_epoch = epoch_constants .epoch_at(get_timestamp()) diff --git a/node/tests/actors/epoch_manager.rs b/node/tests/actors/epoch_manager.rs index c3b1ac527..63f9566da 100644 --- a/node/tests/actors/epoch_manager.rs +++ b/node/tests/actors/epoch_manager.rs @@ -5,8 +5,15 @@ use witnet_node::actors::epoch_manager::{EpochManager, EpochManagerError}; fn epoch_zero_range() { let zero = 1000; let period = 90; + let zero_v2 = i64::MAX; + let period_v2 = 1; let mut em = EpochManager::default(); - em.set_checkpoint_zero_and_period(zero, u16::try_from(period).unwrap()); + em.set_checkpoint_zero_and_period( + zero, + u16::try_from(period).unwrap(), + zero_v2, + u16::try_from(period_v2).unwrap(), + ); // [1000, 1089] are in epoch 0 for now in zero..zero + period { @@ -28,8 +35,10 @@ fn epoch_zero_in_the_future() { let zero = 1000; let now = 999; let period = 90u16; + let zero_v2 = i64::MAX; + let period_v2 = 1u16; let mut em = EpochManager::default(); - em.set_checkpoint_zero_and_period(zero, period); + em.set_checkpoint_zero_and_period(zero, period, zero_v2, period_v2); assert_eq!( em.epoch_at(now), @@ -46,3 +55,56 @@ fn epoch_unknown() { Err(EpochManagerError::UnknownEpochConstants) ); } + +#[test] +fn epoch_v2() { + let zero = 1000; + let period = 50; + let zero_v2 = 2000; + let period_v2 = 25; + let mut em = EpochManager::default(); + em.set_checkpoint_zero_and_period( + zero, + u16::try_from(period).unwrap(), + zero_v2, + u16::try_from(period_v2).unwrap(), + ); + + // [1000, 1049] are in epoch 0 + for now in zero..zero + period { + assert_eq!(em.epoch_at(now), Ok(0), "Error at {}", now); + } + + // 1050 is the start of epoch 1 + assert_eq!( + em.epoch_at(zero + period), + Ok(1), + "Error at {}", + zero + period + ); + + // [1100, 1149] is part of period 2 + for now in zero + 2 * period..zero + 3 * period { + assert_eq!(em.epoch_at(now), Ok(2), "Error at {}", now); + } + + // [1950, 1050] are part of epoch 19 and more + for now in zero + 19 * period..zero + 21 * period { + if now < 2000 { + assert_eq!(em.epoch_at(now), Ok(19), "Error at {}", now); + } else if now < 2025 { + assert_eq!(em.epoch_at(now), Ok(20), "Error at {}", now); + } else { + assert_eq!(em.epoch_at(now), Ok(21), "Error at {}", now); + } + } + + // Epoch 1 to 20, block time of 50 seconds + assert_eq!(em.epoch_timestamp(0), Ok(1000), "Error at {}", 1000); + assert_eq!(em.epoch_timestamp(10), Ok(1500), "Error at {}", 1500); + assert_eq!(em.epoch_timestamp(20), Ok(2000), "Error at {}", 2000); + // Epoch 21 to 40, block time of 25 seconds + assert_eq!(em.epoch_timestamp(21), Ok(2025), "Error at {}", 2025); + assert_eq!(em.epoch_timestamp(30), Ok(2250), "Error at {}", 2250); + assert_eq!(em.epoch_timestamp(40), Ok(2500), "Error at {}", 2500); +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a48344572..1c76f8a3d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -58,9 +58,9 @@ pub fn exec( witnet_data_structures::set_environment(config.environment); log::debug!("{:#?}", config); - for (version, epoch) in config.protocol.iter() { - if let Some(epoch) = epoch { - register_protocol_version(version, epoch); + for (version, epoch_period) in config.protocol.iter() { + if let Some((epoch, period)) = epoch_period { + register_protocol_version(version, epoch, period); } } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 5d2961497..34bc16491 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -233,7 +233,7 @@ pub fn validate_commit_collateral( } // Verify that commits are only accepted after the time lock expired - let epoch_timestamp = epoch_constants.epoch_timestamp(epoch)?; + let (epoch_timestamp, _) = epoch_constants.epoch_timestamp(epoch)?; let vt_time_lock = i64::try_from(vt_output.time_lock)?; if vt_time_lock > epoch_timestamp { return Err(TransactionError::TimeLock { @@ -708,7 +708,7 @@ pub fn validate_commit_transaction( // commit time_lock was disabled in the first hard fork if !active_wips.wip_0008() { // Verify that commits are only accepted after the time lock expired - let epoch_timestamp = epoch_constants.epoch_timestamp(epoch)?; + let (epoch_timestamp, _) = epoch_constants.epoch_timestamp(epoch)?; let dr_time_lock = i64::try_from(dr_output.data_request.time_lock)?; if dr_time_lock > epoch_timestamp { return Err(TransactionError::TimeLock { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 698466166..6679b4b0c 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -21,7 +21,11 @@ use actix::prelude::*; use failure::Error; use witnet_config::config::Config; -use witnet_data_structures::chain::{CheckpointBeacon, EpochConstants}; +use witnet_data_structures::{ + chain::{CheckpointBeacon, EpochConstants}, + get_protocol_version_activation_epoch, get_protocol_version_period, + proto::versioning::ProtocolVersion, +}; use witnet_net::client::tcp::JsonRpcClient; use witnet_validations::witnessing::validate_witnessing_config; @@ -47,10 +51,20 @@ pub fn run(conf: Config) -> Result<(), Error> { let db_file_name = conf.wallet.db_file_name; let node_urls = conf.wallet.node_url; let rocksdb_opts = conf.rocksdb.to_rocksdb_options(); + + let checkpoints_period = conf.consensus_constants.checkpoints_period; + let checkpoint_zero_timestamp = conf.consensus_constants.checkpoint_zero_timestamp; + let checkpoint_zero_timestamp_v2 = checkpoint_zero_timestamp + + get_protocol_version_activation_epoch(ProtocolVersion::V2_0) as i64 + * checkpoints_period as i64; + let checkpoints_period_v2 = get_protocol_version_period(ProtocolVersion::V2_0); let epoch_constants = EpochConstants { - checkpoint_zero_timestamp: conf.consensus_constants.checkpoint_zero_timestamp, - checkpoints_period: conf.consensus_constants.checkpoints_period, + checkpoint_zero_timestamp, + checkpoints_period, + checkpoint_zero_timestamp_v2, + checkpoints_period_v2, }; + let genesis_hash = conf.consensus_constants.genesis_hash; let genesis_prev_hash = conf.consensus_constants.bootstrap_hash; diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 77bec5789..9e7d9fc55 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -2047,8 +2047,12 @@ where fn convert_block_epoch_to_timestamp(epoch_constants: EpochConstants, epoch: Epoch) -> u64 { // In case of error, return timestamp 0 - u64::try_from(epoch_constants.epoch_timestamp(epoch).unwrap_or(0)) - .expect("Epoch timestamp should return a positive value") + match epoch_constants.epoch_timestamp(epoch) { + Ok((timestamp, _)) => { + u64::try_from(timestamp).expect("Epoch timestamp should return a positive value") + } + Err(_) => 0, + } } // Extract inputs and output from a transaction