From b91ff9ee342aebd9506224eaff6a3c1fe000eb91 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 29 Apr 2021 16:13:15 +0200 Subject: [PATCH] Use proper Timestamp type to track time (#855) * Use proper Timestamp type to track time * Handle error when converting from u64 to Timestamp * Use more precise result when comparing the expiry between two timestamps * Move timestamp module to root crate * Fix import errors * Add basic tests for timestamp * Address review feedback * Update changelog --- CHANGELOG.md | 4 +- .../ics20_fungible_token_transfer/error.rs | 9 +- .../relay_application_logic/send_transfer.rs | 6 +- modules/src/ics02_client/client_consensus.rs | 9 +- modules/src/ics02_client/error.rs | 6 +- modules/src/ics04_channel/context.rs | 7 +- modules/src/ics04_channel/error.rs | 6 +- .../src/ics04_channel/handler/recv_packet.rs | 10 +- .../src/ics04_channel/handler/send_packet.rs | 9 +- modules/src/ics04_channel/handler/timeout.rs | 7 +- modules/src/ics04_channel/packet.rs | 12 +- modules/src/ics26_routing/handler.rs | 4 +- modules/src/lib.rs | 1 + modules/src/mock/client_state.rs | 7 +- modules/src/mock/context.rs | 9 +- modules/src/mock/header.rs | 6 +- modules/src/mock/host.rs | 3 +- modules/src/timestamp.rs | 174 ++++++++++++++++++ 18 files changed, 244 insertions(+), 45 deletions(-) create mode 100644 modules/src/timestamp.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d83efa30d..7b144595a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,10 @@ - [ibc] - Reinstated `ics23` dependency ([#854]) + - Use proper Timestamp type to track time ([#855]) - [ibc-relayer] - Change the default for client creation to allow governance recovery in case of expiration or misbehaviour. ([#785]) - + ### BUG FIXES - [ibc-relayer] @@ -24,6 +25,7 @@ [#811]: https://github.com/informalsystems/ibc-rs/issues/811 [#854]: https://github.com/informalsystems/ibc-rs/issues/854 [#851]: https://github.com/informalsystems/ibc-rs/issues/851 +[#855]: https://github.com/informalsystems/ibc-rs/issues/855 ## v0.2.0 diff --git a/modules/src/application/ics20_fungible_token_transfer/error.rs b/modules/src/application/ics20_fungible_token_transfer/error.rs index bcf4d4a262..c3555e381e 100644 --- a/modules/src/application/ics20_fungible_token_transfer/error.rs +++ b/modules/src/application/ics20_fungible_token_transfer/error.rs @@ -13,16 +13,19 @@ pub enum Kind { #[error("error raised by message handler")] HandlerRaisedError, - #[error("Sending sequence number not found for port {0} and channel {1}")] + #[error("sending sequence number not found for port {0} and channel {1}")] SequenceSendNotFound(PortId, ChannelId), - #[error("Missing channel for port_id {0} and channel_id {1} ")] + #[error("missing channel for port_id {0} and channel_id {1} ")] ChannelNotFound(PortId, ChannelId), #[error( - "Destination channel not found in the counterparty of port_id {0} and channel_id {1} " + "destination channel not found in the counterparty of port_id {0} and channel_id {1} " )] DestinationChannelNotFound(PortId, ChannelId), + + #[error("invalid packet timeout timestamp value")] + InvalidPacketTimestamp(u64), } impl Kind { diff --git a/modules/src/application/ics20_fungible_token_transfer/relay_application_logic/send_transfer.rs b/modules/src/application/ics20_fungible_token_transfer/relay_application_logic/send_transfer.rs index 2bb200ad9b..4b21abe5e0 100644 --- a/modules/src/application/ics20_fungible_token_transfer/relay_application_logic/send_transfer.rs +++ b/modules/src/application/ics20_fungible_token_transfer/relay_application_logic/send_transfer.rs @@ -5,6 +5,7 @@ use crate::handler::HandlerOutput; use crate::ics04_channel::handler::send_packet::send_packet; use crate::ics04_channel::packet::Packet; use crate::ics04_channel::packet::PacketResult; +use crate::timestamp::Timestamp; pub(crate) fn send_transfer( ctx: &Ctx, @@ -34,6 +35,9 @@ where Kind::SequenceSendNotFound(msg.source_port.clone(), msg.source_channel.clone()) })?; + let timeout_timestamp = Timestamp::from_nanoseconds(msg.timeout_timestamp) + .map_err(|_| Kind::InvalidPacketTimestamp(msg.timeout_timestamp))?; + //TODO: Application LOGIC. let packet = Packet { @@ -44,7 +48,7 @@ where destination_channel: destination_channel.clone(), data: vec![0], timeout_height: msg.timeout_height, - timeout_timestamp: msg.timeout_timestamp, + timeout_timestamp, }; let handler_output = diff --git a/modules/src/ics02_client/client_consensus.rs b/modules/src/ics02_client/client_consensus.rs index e645284d9a..70fb3eb99a 100644 --- a/modules/src/ics02_client/client_consensus.rs +++ b/modules/src/ics02_client/client_consensus.rs @@ -17,6 +17,7 @@ use crate::ics23_commitment::commitment::CommitmentRoot; use crate::ics24_host::identifier::ClientId; #[cfg(any(test, feature = "mocks"))] use crate::mock::client_state::MockConsensusState; +use crate::timestamp::Timestamp; pub const TENDERMINT_CONSENSUS_STATE_TYPE_URL: &str = "/ibc.lightclients.tendermint.v1.ConsensusState"; @@ -48,17 +49,15 @@ pub enum AnyConsensusState { } impl AnyConsensusState { - pub fn timestamp(&self) -> Result { + pub fn timestamp(&self) -> Timestamp { match self { Self::Tendermint(cs_state) => { let date: DateTime = cs_state.timestamp.into(); - let value = date.timestamp(); - u64::try_from(value) - .map_err(|_| Kind::NegativeConsensusStateTimestamp(value.to_string())) + Timestamp::from_datetime(date) } #[cfg(any(test, feature = "mocks"))] - Self::Mock(mock_state) => Ok(mock_state.timestamp()), + Self::Mock(mock_state) => mock_state.timestamp(), } } diff --git a/modules/src/ics02_client/error.rs b/modules/src/ics02_client/error.rs index 13627542a8..09a8d27574 100644 --- a/modules/src/ics02_client/error.rs +++ b/modules/src/ics02_client/error.rs @@ -32,9 +32,6 @@ pub enum Kind { #[error("implementation specific")] ImplementationSpecific, - #[error("Negative timestamp in consensus state {0}; timestamp must be a positive value")] - NegativeConsensusStateTimestamp(String), - #[error("header verification failed")] HeaderVerificationFailure, @@ -83,6 +80,9 @@ pub enum Kind { #[error("invalid proof for the upgraded consensus state")] InvalidUpgradeConsensusStateProof(Ics23Error), + #[error("invalid packet timeout timestamp value")] + InvalidPacketTimestamp, + #[error("mismatch between client and arguments types, expected: {0:?}")] ClientArgsTypeMismatch(ClientType), diff --git a/modules/src/ics04_channel/context.rs b/modules/src/ics04_channel/context.rs index 4493cbffd6..6b8432d60a 100644 --- a/modules/src/ics04_channel/context.rs +++ b/modules/src/ics04_channel/context.rs @@ -10,6 +10,7 @@ use crate::ics04_channel::handler::{ChannelIdState, ChannelResult}; use crate::ics04_channel::{error::Error, packet::Receipt}; use crate::ics05_port::capabilities::Capability; use crate::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; +use crate::timestamp::Timestamp; use crate::Height; use super::packet::{PacketResult, Sequence}; @@ -48,14 +49,14 @@ pub trait ChannelReader { fn get_packet_acknowledgement(&self, key: &(PortId, ChannelId, Sequence)) -> Option; - /// A hashing function for packet commitments + /// A hashing function for packet commitments fn hash(&self, value: String) -> String; /// Returns the current height of the local chain. fn host_height(&self) -> Height; /// Returns the current timestamp of the local chain. - fn host_timestamp(&self) -> u64; + fn host_timestamp(&self) -> Timestamp; /// Returns a counter on the number of channel ids have been created thus far. /// The value of this counter should increase only via method @@ -172,7 +173,7 @@ pub trait ChannelKeeper { fn store_packet_commitment( &mut self, key: (PortId, ChannelId, Sequence), - timestamp: u64, + timestamp: Timestamp, heigh: Height, data: Vec, ) -> Result<(), Error>; diff --git a/modules/src/ics04_channel/error.rs b/modules/src/ics04_channel/error.rs index 63925996e4..4b48b0438e 100644 --- a/modules/src/ics04_channel/error.rs +++ b/modules/src/ics04_channel/error.rs @@ -6,6 +6,7 @@ pub type Error = anomaly::Error; use super::packet::Sequence; use crate::ics04_channel::channel::State; use crate::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; +use crate::timestamp::Timestamp; use crate::{ics02_client, Height}; #[derive(Clone, Debug, Error, Eq, PartialEq)] @@ -126,11 +127,14 @@ pub enum Kind { PacketTimeoutHeightNotReached(Height, Height), #[error("Packet timeout timestamp {0} > chain timestamp {1}")] - PacketTimeoutTimestampNotReached(u64, u64), + PacketTimeoutTimestampNotReached(Timestamp, Timestamp), #[error("Receiving chain block timestamp >= packet timeout timestamp")] LowPacketTimestamp, + #[error("Invalid packet timeout timestamp value")] + InvalidPacketTimestamp, + #[error("Invalid timestamp in consensus state; timestamp must be a positive value")] ErrorInvalidConsensusState(ics02_client::error::Kind), diff --git a/modules/src/ics04_channel/handler/recv_packet.rs b/modules/src/ics04_channel/handler/recv_packet.rs index 50233fee7f..937878ff31 100644 --- a/modules/src/ics04_channel/handler/recv_packet.rs +++ b/modules/src/ics04_channel/handler/recv_packet.rs @@ -10,6 +10,7 @@ use crate::ics04_channel::handler::verify::verify_packet_recv_proofs; use crate::ics04_channel::msgs::recv_packet::MsgRecvPacket; use crate::ics04_channel::packet::{PacketResult, Receipt, Sequence}; use crate::ics24_host::identifier::{ChannelId, PortId}; +use crate::timestamp::Expiry; #[derive(Clone, Debug)] pub struct RecvPacketResult { @@ -78,7 +79,7 @@ pub fn process(ctx: &dyn ChannelReader, msg: MsgRecvPacket) -> HandlerResult, } @@ -77,12 +78,10 @@ pub fn send_packet(ctx: &dyn ChannelReader, packet: Packet) -> HandlerResult HandlerResult proof_timestamp { + if let Expiry::Expired = packet_timestamp.check_expiry(&proof_timestamp) { return Err( Kind::PacketTimeoutTimestampNotReached(packet_timestamp, proof_timestamp).into(), ); diff --git a/modules/src/ics04_channel/packet.rs b/modules/src/ics04_channel/packet.rs index c623b04b39..189dca9811 100644 --- a/modules/src/ics04_channel/packet.rs +++ b/modules/src/ics04_channel/packet.rs @@ -6,6 +6,7 @@ use ibc_proto::ibc::core::channel::v1::Packet as RawPacket; use crate::ics04_channel::error::Kind; use crate::ics24_host::identifier::{ChannelId, PortId}; +use crate::timestamp::Timestamp; use crate::Height; use super::handler::{ @@ -91,7 +92,7 @@ pub struct Packet { #[serde(serialize_with = "crate::serializers::ser_hex_upper")] pub data: Vec, pub timeout_height: Height, - pub timeout_timestamp: u64, + pub timeout_timestamp: Timestamp, } impl std::fmt::Debug for Packet { @@ -131,7 +132,7 @@ impl Default for Packet { destination_channel: Default::default(), data: vec![], timeout_height: Default::default(), - timeout_timestamp: 0, + timeout_timestamp: Default::default(), } } } @@ -156,6 +157,9 @@ impl TryFrom for Packet { return Err(Kind::ZeroPacketData.into()); } + let timeout_timestamp = Timestamp::from_nanoseconds(raw_pkt.timeout_timestamp) + .map_err(|_| Kind::InvalidPacketTimestamp)?; + Ok(Packet { sequence: Sequence::from(raw_pkt.sequence), source_port: raw_pkt @@ -176,7 +180,7 @@ impl TryFrom for Packet { .map_err(|e| Kind::IdentifierError.context(e))?, data: raw_pkt.data, timeout_height: packet_timeout_height, - timeout_timestamp: raw_pkt.timeout_timestamp, + timeout_timestamp, }) } } @@ -191,7 +195,7 @@ impl From for RawPacket { destination_channel: packet.destination_channel.to_string(), data: packet.data, timeout_height: Some(packet.timeout_height.into()), - timeout_timestamp: packet.timeout_timestamp, + timeout_timestamp: packet.timeout_timestamp.as_nanoseconds(), } } } diff --git a/modules/src/ics26_routing/handler.rs b/modules/src/ics26_routing/handler.rs index 44b5fcfacc..44d0c6c745 100644 --- a/modules/src/ics26_routing/handler.rs +++ b/modules/src/ics26_routing/handler.rs @@ -274,6 +274,7 @@ mod tests { use crate::mock::context::MockContext; use crate::mock::header::MockHeader; use crate::test_utils::get_dummy_account_id; + use crate::timestamp::Timestamp; use crate::Height; #[test] @@ -361,7 +362,8 @@ mod tests { MsgTimeoutOnClose::try_from(get_dummy_raw_msg_timeout_on_close(36, 5)).unwrap(); msg_to_on_close.packet.sequence = 2.into(); msg_to_on_close.packet.timeout_height = msg_transfer_two.timeout_height; - msg_to_on_close.packet.timeout_timestamp = msg_transfer_two.timeout_timestamp; + msg_to_on_close.packet.timeout_timestamp = + Timestamp::from_nanoseconds(msg_transfer_two.timeout_timestamp).unwrap(); let msg_recv_packet = MsgRecvPacket::try_from(get_dummy_raw_msg_recv_packet(35)).unwrap(); diff --git a/modules/src/lib.rs b/modules/src/lib.rs index da2b838fa5..085d93e334 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -33,6 +33,7 @@ pub mod macros; pub mod proofs; pub mod query; pub mod signer; +pub mod timestamp; pub mod tx_msg; pub mod ics02_client; diff --git a/modules/src/mock/client_state.rs b/modules/src/mock/client_state.rs index b3813a244c..0fcf86801b 100644 --- a/modules/src/mock/client_state.rs +++ b/modules/src/mock/client_state.rs @@ -15,6 +15,7 @@ use crate::ics02_client::error::Kind as ClientKind; use crate::ics23_commitment::commitment::CommitmentRoot; use crate::ics24_host::identifier::ChainId; use crate::mock::header::MockHeader; +use crate::timestamp::Timestamp; use crate::Height; /// A mock of an IBC client record as it is stored in a mock context. @@ -64,7 +65,7 @@ impl From for RawMockClientState { RawMockClientState { header: Some(ibc_proto::ibc::mock::Header { height: Some(value.0.height().into()), - timestamp: (value.0).timestamp, + timestamp: (value.0).timestamp.as_nanoseconds(), }), } } @@ -103,7 +104,7 @@ impl From for MockClientState { pub struct MockConsensusState(pub MockHeader); impl MockConsensusState { - pub fn timestamp(&self) -> u64 { + pub fn timestamp(&self) -> Timestamp { (self.0).timestamp } } @@ -127,7 +128,7 @@ impl From for RawMockConsensusState { RawMockConsensusState { header: Some(ibc_proto::ibc::mock::Header { height: Some(value.0.height().into()), - timestamp: (value.0).timestamp, + timestamp: (value.0).timestamp.as_nanoseconds(), }), } } diff --git a/modules/src/mock/context.rs b/modules/src/mock/context.rs index b0ec01cfa1..7ff4cd13dc 100644 --- a/modules/src/mock/context.rs +++ b/modules/src/mock/context.rs @@ -36,6 +36,7 @@ use crate::mock::client_state::{MockClientRecord, MockClientState, MockConsensus use crate::mock::header::MockHeader; use crate::mock::host::{HostBlock, HostType}; use crate::signer::Signer; +use crate::timestamp::Timestamp; use crate::Height; /// A context implementing the dependencies necessary for testing any IBC module. @@ -54,7 +55,7 @@ pub struct MockContext { latest_height: Height, /// Highest timestamp, i.e., of the most recent block in the history. - timestamp: u64, + timestamp: Timestamp, /// The chain of blocks underlying this context. A vector of size up to `max_history_size` /// blocks, ascending order by their height (latest block is on the last position). @@ -304,7 +305,7 @@ impl MockContext { } } - pub fn with_timestamp(self, timestamp: u64) -> Self { + pub fn with_timestamp(self, timestamp: Timestamp) -> Self { Self { timestamp, ..self } } @@ -517,7 +518,7 @@ impl ChannelReader for MockContext { self.latest_height } - fn host_timestamp(&self) -> u64 { + fn host_timestamp(&self) -> Timestamp { self.timestamp } @@ -530,7 +531,7 @@ impl ChannelKeeper for MockContext { fn store_packet_commitment( &mut self, key: (PortId, ChannelId, Sequence), - timeout_timestamp: u64, + timeout_timestamp: Timestamp, timeout_height: Height, data: Vec, ) -> Result<(), Ics4Error> { diff --git a/modules/src/mock/header.rs b/modules/src/mock/header.rs index e06ed4c4ec..b05c8a7cd8 100644 --- a/modules/src/mock/header.rs +++ b/modules/src/mock/header.rs @@ -11,12 +11,13 @@ use crate::ics02_client::error::{self, Error}; use crate::ics02_client::header::AnyHeader; use crate::ics02_client::header::Header; use crate::mock::client_state::MockConsensusState; +use crate::timestamp::Timestamp; use crate::Height; #[derive(Copy, Clone, Default, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct MockHeader { pub height: Height, - pub timestamp: u64, + pub timestamp: Timestamp, } impl Protobuf for MockHeader {} @@ -31,7 +32,8 @@ impl TryFrom for MockHeader { .ok_or_else(|| error::Kind::InvalidRawHeader.context("missing height in header"))? .try_into() .map_err(|e| error::Kind::InvalidRawHeader.context(e))?, - timestamp: raw.timestamp, + timestamp: Timestamp::from_nanoseconds(raw.timestamp) + .map_err(|_| error::Kind::InvalidPacketTimestamp)?, }) } } diff --git a/modules/src/mock/host.rs b/modules/src/mock/host.rs index 695fd9ad51..18f3fb2df0 100644 --- a/modules/src/mock/host.rs +++ b/modules/src/mock/host.rs @@ -12,6 +12,7 @@ use crate::ics07_tendermint::consensus_state::ConsensusState as TMConsensusState use crate::ics07_tendermint::header::Header as TMHeader; use crate::ics24_host::identifier::ChainId; use crate::mock::header::MockHeader; +use crate::timestamp::Timestamp; use crate::Height; /// Defines the different types of host chains that a mock context can emulate. @@ -50,7 +51,7 @@ impl HostBlock { match chain_type { HostType::Mock => HostBlock::Mock(MockHeader { height: Height::new(chain_id.version(), height), - timestamp: 1, + timestamp: Timestamp::from_nanoseconds(1).unwrap(), }), HostType::SyntheticTendermint => { HostBlock::SyntheticTendermint(Box::new(Self::generate_tm_block(chain_id, height))) diff --git a/modules/src/timestamp.rs b/modules/src/timestamp.rs new file mode 100644 index 0000000000..d0cd8c8a24 --- /dev/null +++ b/modules/src/timestamp.rs @@ -0,0 +1,174 @@ +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::fmt::Display; +use std::num::{ParseIntError, TryFromIntError}; +use std::str::FromStr; +use thiserror::Error; + +use chrono::{offset::Utc, DateTime, TimeZone}; + +/// A newtype wrapper over `Option>` to keep track of +/// IBC packet timeout. +/// +/// We use an explicit `Option` type to distinguish this when converting between +/// a `u64` value and a raw timestamp. In protocol buffer, the timestamp is +/// represented as a `u64` Unix timestamp in nanoseconds, with 0 representing the absence +/// of timestamp. +#[derive(PartialEq, Eq, Copy, Clone, Debug, Deserialize, Serialize, Hash)] +pub struct Timestamp { + time: Option>, +} + +/// The expiry result when comparing two timestamps. +/// - If either timestamp is invalid (0), the result is `InvalidTimestamp`. +/// - If the left timestamp is strictly after the right timestamp, the result is `Expired`. +/// - Otherwise, the result is `NotExpired`. +/// +/// User of this result may want to determine whether error should be raised, +/// when either of the timestamp being compared is invalid. +#[derive(PartialEq, Eq, Copy, Clone, Debug, Deserialize, Serialize, Hash)] +pub enum Expiry { + Expired, + NotExpired, + InvalidTimestamp, +} + +impl Timestamp { + /// When used in IBC, all raw timestamps are represented as u64 Unix timestamp in nanoseconds. + /// + /// A value of 0 indicates that the timestamp is not set, and result in the underlying + /// type being None. + /// + /// The underlying library [`chrono::DateTime`] allows conversion from nanoseconds only + /// from an `i64` value. In practice, `i64` still have sufficient precision for our purpose. + /// However we have to handle the case of `u64` overflowing in `i64`, to prevent + /// malicious packets from crashing the relayer. + pub fn from_nanoseconds(nanoseconds: u64) -> Result { + if nanoseconds == 0 { + Ok(Timestamp { time: None }) + } else { + let nanoseconds = nanoseconds.try_into()?; + Ok(Timestamp { + time: Some(Utc.timestamp_nanos(nanoseconds)), + }) + } + } + + /// Convert a `Timestamp` from [`chrono::DateTime`]. + pub fn from_datetime(time: DateTime) -> Timestamp { + Timestamp { time: Some(time) } + } + + /// Convert a `Timestamp` to `u64` value in nanoseconds. If no timestamp + /// is set, the result is 0. + pub fn as_nanoseconds(&self) -> u64 { + self.time + .map_or(0, |time| time.timestamp_nanos().try_into().unwrap()) + } + + /// Convert a `Timestamp` to an optional [`chrono::DateTime`] + pub fn as_datetime(&self) -> Option> { + self.time + } + + /// Checks whether the timestamp has expired when compared to the + /// `other` timestamp. Returns an [`Expiry`] result. + pub fn check_expiry(&self, other: &Timestamp) -> Expiry { + match (self.time, other.time) { + (Some(time1), Some(time2)) => { + if time1 > time2 { + Expiry::Expired + } else { + Expiry::NotExpired + } + } + _ => Expiry::InvalidTimestamp, + } + } +} + +impl Display for Timestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Timestamp({})", + self.time + .map_or("NoTimestamp".to_string(), |time| time.to_rfc3339()) + ) + } +} + +pub type ParseTimestampError = anomaly::Error; + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum ParseTimestampErrorKind { + #[error("Error parsing integer from string: {0}")] + ParseIntError(ParseIntError), + + #[error("Error converting from u64 to i64: {0}")] + TryFromIntError(TryFromIntError), +} + +impl FromStr for Timestamp { + type Err = ParseTimestampError; + + fn from_str(s: &str) -> Result { + let seconds = u64::from_str(s).map_err(ParseTimestampErrorKind::ParseIntError)?; + + Timestamp::from_nanoseconds(seconds) + .map_err(|err| ParseTimestampErrorKind::TryFromIntError(err).into()) + } +} + +impl Default for Timestamp { + fn default() -> Self { + Timestamp { time: None } + } +} + +#[cfg(test)] +mod tests { + use super::{Expiry, Timestamp}; + use std::convert::TryInto; + + #[test] + fn test_timestamp_comparisons() { + let nil_timestamp = Timestamp::from_nanoseconds(0).unwrap(); + assert_eq!(nil_timestamp.time, None); + assert_eq!(nil_timestamp.as_nanoseconds(), 0); + + let timestamp1 = Timestamp::from_nanoseconds(1).unwrap(); + assert_eq!(timestamp1.time.unwrap().timestamp(), 0); + assert_eq!(timestamp1.time.unwrap().timestamp_millis(), 0); + assert_eq!(timestamp1.time.unwrap().timestamp_nanos(), 1); + assert_eq!(timestamp1.as_nanoseconds(), 1); + + let timestamp2 = Timestamp::from_nanoseconds(1_000_000_000).unwrap(); + assert_eq!(timestamp2.time.unwrap().timestamp(), 1); + assert_eq!(timestamp2.time.unwrap().timestamp_millis(), 1_000); + assert_eq!(timestamp2.as_nanoseconds(), 1_000_000_000); + + assert_eq!(Timestamp::from_nanoseconds(u64::MAX).is_err(), true); + assert_eq!( + Timestamp::from_nanoseconds(i64::MAX.try_into().unwrap()).is_ok(), + true + ); + + assert_eq!(timestamp1.check_expiry(×tamp2), Expiry::NotExpired); + assert_eq!(timestamp1.check_expiry(×tamp1), Expiry::NotExpired); + assert_eq!(timestamp2.check_expiry(×tamp2), Expiry::NotExpired); + assert_eq!(timestamp2.check_expiry(×tamp1), Expiry::Expired); + assert_eq!( + timestamp1.check_expiry(&nil_timestamp), + Expiry::InvalidTimestamp + ); + assert_eq!( + nil_timestamp.check_expiry(×tamp2), + Expiry::InvalidTimestamp + ); + assert_eq!( + nil_timestamp.check_expiry(&nil_timestamp), + Expiry::InvalidTimestamp + ); + } +}