diff --git a/zebra-chain/src/serialization.rs b/zebra-chain/src/serialization.rs index e384cfa6f1e..f383a064183 100644 --- a/zebra-chain/src/serialization.rs +++ b/zebra-chain/src/serialization.rs @@ -7,6 +7,7 @@ //! for reading and writing data (e.g., the Bitcoin variable-integer format). mod constraint; +mod date_time; mod error; mod read_zcash; mod write_zcash; @@ -21,6 +22,7 @@ pub mod sha256d; pub mod arbitrary; pub use constraint::AtLeastOne; +pub use date_time::DateTime32; pub use error::SerializationError; pub use read_zcash::ReadZcashExt; pub use write_zcash::WriteZcashExt; diff --git a/zebra-chain/src/serialization/arbitrary.rs b/zebra-chain/src/serialization/arbitrary.rs index d81beca0003..b005beb37ef 100644 --- a/zebra-chain/src/serialization/arbitrary.rs +++ b/zebra-chain/src/serialization/arbitrary.rs @@ -1,8 +1,18 @@ -use super::read_zcash::canonical_ip_addr; +use super::{read_zcash::canonical_ip_addr, DateTime32}; use chrono::{TimeZone, Utc, MAX_DATETIME, MIN_DATETIME}; use proptest::{arbitrary::any, prelude::*}; use std::net::SocketAddr; +impl Arbitrary for DateTime32 { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + any::().prop_map(Into::into).boxed() + } + + type Strategy = BoxedStrategy; +} + /// Returns a strategy that produces an arbitrary [`chrono::DateTime`], /// based on the full valid range of the type. /// @@ -38,8 +48,10 @@ pub fn datetime_full() -> impl Strategy> { /// /// The Zcash protocol typically uses 4-byte seconds values, except for the /// [`Version`] message. +/// +/// TODO: replace this strategy with `any::()`. pub fn datetime_u32() -> impl Strategy> { - any::().prop_map(|secs| Utc.timestamp(secs.into(), 0)) + any::().prop_map(Into::into) } /// Returns a random canonical Zebra `SocketAddr`. diff --git a/zebra-chain/src/serialization/date_time.rs b/zebra-chain/src/serialization/date_time.rs new file mode 100644 index 00000000000..a2883dce48e --- /dev/null +++ b/zebra-chain/src/serialization/date_time.rs @@ -0,0 +1,109 @@ +//! DateTime types with specific serialization invariants. + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use chrono::{TimeZone, Utc}; + +use std::{ + convert::{TryFrom, TryInto}, + fmt, + num::TryFromIntError, +}; + +use super::{SerializationError, ZcashDeserialize, ZcashSerialize}; + +/// A date and time, represented by a 32-bit number of seconds since the UNIX epoch. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct DateTime32 { + timestamp: u32, +} + +impl DateTime32 { + /// Returns the number of seconds since the UNIX epoch. + pub fn timestamp(&self) -> u32 { + self.timestamp + } + + /// Returns the equivalent [`chrono::DateTime`]. + pub fn to_chrono(self) -> chrono::DateTime { + self.into() + } + + /// Returns the current time + pub fn now() -> DateTime32 { + Utc::now() + .try_into() + .expect("unexpected out of range chrono::DateTime") + } +} + +impl fmt::Debug for DateTime32 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DateTime32") + .field("timestamp", &self.timestamp) + .field("calendar", &chrono::DateTime::::from(*self)) + .finish() + } +} + +impl From for DateTime32 { + fn from(value: u32) -> Self { + DateTime32 { timestamp: value } + } +} + +impl From<&u32> for DateTime32 { + fn from(value: &u32) -> Self { + (*value).into() + } +} + +impl From for chrono::DateTime { + fn from(value: DateTime32) -> Self { + // chrono::DateTime is guaranteed to hold 32-bit values + Utc.timestamp(value.timestamp.into(), 0) + } +} + +impl From<&DateTime32> for chrono::DateTime { + fn from(value: &DateTime32) -> Self { + (*value).into() + } +} + +impl TryFrom> for DateTime32 { + type Error = TryFromIntError; + + /// Convert from a [`chrono::DateTime`] to a [`DateTime32`], discarding any nanoseconds. + /// + /// Conversion fails if the number of seconds is outside the `u32` range. + fn try_from(value: chrono::DateTime) -> Result { + Ok(Self { + timestamp: value.timestamp().try_into()?, + }) + } +} + +impl TryFrom<&chrono::DateTime> for DateTime32 { + type Error = TryFromIntError; + + /// Convert from a [`chrono::DateTime`] to a [`DateTime32`], discarding any nanoseconds. + /// + /// Conversion fails if the number of seconds is outside the `u32` range. + fn try_from(value: &chrono::DateTime) -> Result { + (*value).try_into() + } +} + +impl ZcashSerialize for DateTime32 { + fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { + writer.write_u32::(self.timestamp) + } +} + +impl ZcashDeserialize for DateTime32 { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(DateTime32 { + timestamp: reader.read_u32::()?, + }) + } +} diff --git a/zebra-network/src/address_book.rs b/zebra-network/src/address_book.rs index 8fa4864a304..42cb41b32af 100644 --- a/zebra-network/src/address_book.rs +++ b/zebra-network/src/address_book.rs @@ -208,9 +208,9 @@ impl AddressBook { /// [`constants::LIVE_PEER_DURATION`] ago, we know we should have /// disconnected from it. Otherwise, we could potentially be connected to it. fn liveness_cutoff_time() -> DateTime { - // chrono uses signed durations while stdlib uses unsigned durations - use chrono::Duration as CD; - Utc::now() - CD::from_std(constants::LIVE_PEER_DURATION).unwrap() + Utc::now() + - chrono::Duration::from_std(constants::LIVE_PEER_DURATION) + .expect("unexpectedly large constant") } /// Returns true if the given [`SocketAddr`] has recently sent us a message. @@ -221,7 +221,7 @@ impl AddressBook { // NeverAttempted, Failed, and AttemptPending peers should never be live Some(peer) => { peer.last_connection_state == PeerAddrState::Responded - && peer.get_last_seen() > AddressBook::liveness_cutoff_time() + && peer.get_last_seen().to_chrono() > AddressBook::liveness_cutoff_time() } } } diff --git a/zebra-network/src/constants.rs b/zebra-network/src/constants.rs index affb2464973..4df3885980a 100644 --- a/zebra-network/src/constants.rs +++ b/zebra-network/src/constants.rs @@ -69,7 +69,7 @@ pub const GET_ADDR_FANOUT: usize = 3; /// /// Timestamp truncation prevents a peer from learning exactly when we received /// messages from each of our peers. -pub const TIMESTAMP_TRUNCATION_SECONDS: i64 = 30 * 60; +pub const TIMESTAMP_TRUNCATION_SECONDS: u32 = 30 * 60; /// The User-Agent string provided by the node. /// diff --git a/zebra-network/src/meta_addr.rs b/zebra-network/src/meta_addr.rs index 95b447acd27..06fe2a0c784 100644 --- a/zebra-network/src/meta_addr.rs +++ b/zebra-network/src/meta_addr.rs @@ -2,17 +2,15 @@ use std::{ cmp::{Ord, Ordering}, - convert::TryInto, io::{Read, Write}, net::SocketAddr, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use chrono::{DateTime, TimeZone, Utc}; use zebra_chain::serialization::{ - ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize, - ZcashSerialize, + DateTime32, ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, + ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, }; use crate::protocol::{external::MAX_PROTOCOL_MESSAGE_LEN, types::PeerServices}; @@ -130,7 +128,7 @@ pub struct MetaAddr { /// The last time we interacted with this peer. /// /// See `get_last_seen` for details. - last_seen: DateTime, + last_seen: DateTime32, /// The outcome of our most recent communication attempt with this peer. pub last_connection_state: PeerAddrState, @@ -142,7 +140,7 @@ impl MetaAddr { pub fn new_gossiped_meta_addr( addr: SocketAddr, untrusted_services: PeerServices, - untrusted_last_seen: DateTime, + untrusted_last_seen: DateTime32, ) -> MetaAddr { MetaAddr { addr, @@ -168,7 +166,7 @@ impl MetaAddr { MetaAddr { addr: *addr, services: *services, - last_seen: Utc::now(), + last_seen: DateTime32::now(), last_connection_state: Responded, } } @@ -178,7 +176,7 @@ impl MetaAddr { MetaAddr { addr: *addr, services: *services, - last_seen: Utc::now(), + last_seen: DateTime32::now(), last_connection_state: AttemptPending, } } @@ -189,7 +187,7 @@ impl MetaAddr { MetaAddr { addr: *addr, services: *services, - last_seen: Utc::now(), + last_seen: DateTime32::now(), last_connection_state: NeverAttemptedAlternate, } } @@ -200,7 +198,7 @@ impl MetaAddr { addr: *addr, // TODO: create a "local services" constant services: PeerServices::NODE_NETWORK, - last_seen: Utc::now(), + last_seen: DateTime32::now(), last_connection_state: Responded, } } @@ -210,7 +208,7 @@ impl MetaAddr { MetaAddr { addr: *addr, services: *services, - last_seen: Utc::now(), + last_seen: DateTime32::now(), last_connection_state: Failed, } } @@ -236,7 +234,7 @@ impl MetaAddr { /// /// `last_seen` times from `NeverAttempted` peers may be invalid due to /// clock skew, or buggy or malicious peers. - pub fn get_last_seen(&self) -> DateTime { + pub fn get_last_seen(&self) -> DateTime32 { self.last_seen } @@ -258,13 +256,14 @@ impl MetaAddr { /// Return a sanitized version of this `MetaAddr`, for sending to a remote peer. pub fn sanitize(&self) -> MetaAddr { let interval = crate::constants::TIMESTAMP_TRUNCATION_SECONDS; - let ts = self.get_last_seen().timestamp(); - let last_seen = Utc.timestamp(ts - ts.rem_euclid(interval), 0); + let ts = self.last_seen.timestamp(); + // This can't underflow, because `0 <= rem_euclid < ts` + let last_seen = ts - ts.rem_euclid(interval); MetaAddr { addr: self.addr, // deserialization also sanitizes services to known flags services: self.services & PeerServices::all(), - last_seen, + last_seen: last_seen.into(), // the state isn't sent to the remote peer, but sanitize it anyway last_connection_state: NeverAttemptedGossiped, } @@ -328,12 +327,7 @@ impl Eq for MetaAddr {} impl ZcashSerialize for MetaAddr { fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { - writer.write_u32::( - self.get_last_seen() - .timestamp() - .try_into() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?, - )?; + self.last_seen.zcash_serialize(&mut writer)?; writer.write_u64::(self.services.bits())?; writer.write_socket_addr(self.addr)?; Ok(()) @@ -342,8 +336,7 @@ impl ZcashSerialize for MetaAddr { impl ZcashDeserialize for MetaAddr { fn zcash_deserialize(mut reader: R) -> Result { - // This can't panic, because all u32 values are valid `Utc.timestamp`s - let untrusted_last_seen = Utc.timestamp(reader.read_u32::()?.into(), 0); + let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?; let untrusted_services = PeerServices::from_bits_truncate(reader.read_u64::()?); let addr = reader.read_socket_addr()?; diff --git a/zebra-network/src/meta_addr/arbitrary.rs b/zebra-network/src/meta_addr/arbitrary.rs index 8b43ea7ba75..5c4d2872337 100644 --- a/zebra-network/src/meta_addr/arbitrary.rs +++ b/zebra-network/src/meta_addr/arbitrary.rs @@ -2,16 +2,14 @@ use proptest::{arbitrary::any, arbitrary::Arbitrary, prelude::*}; use super::{MetaAddr, PeerAddrState, PeerServices}; -use zebra_chain::serialization::arbitrary::{canonical_socket_addr, datetime_u32}; - -use chrono::{TimeZone, Utc}; +use zebra_chain::serialization::{arbitrary::canonical_socket_addr, DateTime32}; impl MetaAddr { pub fn gossiped_strategy() -> BoxedStrategy { ( canonical_socket_addr(), any::(), - datetime_u32(), + any::(), ) .prop_map(|(address, services, untrusted_last_seen)| { MetaAddr::new_gossiped_meta_addr(address, services, untrusted_last_seen) @@ -27,15 +25,14 @@ impl Arbitrary for MetaAddr { ( canonical_socket_addr(), any::(), - any::(), + any::(), any::(), ) .prop_map( |(addr, services, last_seen, last_connection_state)| MetaAddr { addr, services, - // This can't panic, because all u32 values are valid `Utc.timestamp`s - last_seen: Utc.timestamp(last_seen.into(), 0), + last_seen, last_connection_state, }, ) diff --git a/zebra-network/src/meta_addr/tests/check.rs b/zebra-network/src/meta_addr/tests/check.rs index dbe5d3fd4fa..b6563c67480 100644 --- a/zebra-network/src/meta_addr/tests/check.rs +++ b/zebra-network/src/meta_addr/tests/check.rs @@ -17,7 +17,7 @@ pub(crate) fn sanitize_avoids_leaks(original: &MetaAddr, sanitized: &MetaAddr) { sanitized.get_last_seen().timestamp() % TIMESTAMP_TRUNCATION_SECONDS, 0 ); - assert_eq!(sanitized.get_last_seen().timestamp_subsec_nanos(), 0); + // handle underflow and overflow by skipping the check // the other check will ensure correctness let lowest_time = original diff --git a/zebra-network/src/meta_addr/tests/vectors.rs b/zebra-network/src/meta_addr/tests/vectors.rs index 828dd8ff6e8..ba0f842928a 100644 --- a/zebra-network/src/meta_addr/tests/vectors.rs +++ b/zebra-network/src/meta_addr/tests/vectors.rs @@ -2,8 +2,6 @@ use super::{super::MetaAddr, check}; -use chrono::{MAX_DATETIME, MIN_DATETIME}; - /// Make sure that the sanitize function handles minimum and maximum times. #[test] fn sanitize_extremes() { @@ -12,14 +10,14 @@ fn sanitize_extremes() { let min_time_entry = MetaAddr { addr: "127.0.0.1:8233".parse().unwrap(), services: Default::default(), - last_seen: MIN_DATETIME, + last_seen: u32::MIN.into(), last_connection_state: Default::default(), }; let max_time_entry = MetaAddr { addr: "127.0.0.1:8233".parse().unwrap(), services: Default::default(), - last_seen: MAX_DATETIME, + last_seen: u32::MAX.into(), last_connection_state: Default::default(), };