Add a DateTime32 type for 32-bit serialized times (#2210)

* Add a DateTime32 type for 32-bit serialized times
* Use DateTime32 for MetaAddr.last_seen
* Create and use a `DateTime32::now` method
This commit is contained in:
teor 2021-05-31 12:52:34 +10:00 committed by GitHub
parent a6e272bf1c
commit ebe1c9f88e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 42 deletions

View File

@ -7,6 +7,7 @@
//! for reading and writing data (e.g., the Bitcoin variable-integer format). //! for reading and writing data (e.g., the Bitcoin variable-integer format).
mod constraint; mod constraint;
mod date_time;
mod error; mod error;
mod read_zcash; mod read_zcash;
mod write_zcash; mod write_zcash;
@ -21,6 +22,7 @@ pub mod sha256d;
pub mod arbitrary; pub mod arbitrary;
pub use constraint::AtLeastOne; pub use constraint::AtLeastOne;
pub use date_time::DateTime32;
pub use error::SerializationError; pub use error::SerializationError;
pub use read_zcash::ReadZcashExt; pub use read_zcash::ReadZcashExt;
pub use write_zcash::WriteZcashExt; pub use write_zcash::WriteZcashExt;

View File

@ -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 chrono::{TimeZone, Utc, MAX_DATETIME, MIN_DATETIME};
use proptest::{arbitrary::any, prelude::*}; use proptest::{arbitrary::any, prelude::*};
use std::net::SocketAddr; use std::net::SocketAddr;
impl Arbitrary for DateTime32 {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
any::<u32>().prop_map(Into::into).boxed()
}
type Strategy = BoxedStrategy<Self>;
}
/// Returns a strategy that produces an arbitrary [`chrono::DateTime<Utc>`], /// Returns a strategy that produces an arbitrary [`chrono::DateTime<Utc>`],
/// based on the full valid range of the type. /// based on the full valid range of the type.
/// ///
@ -38,8 +48,10 @@ pub fn datetime_full() -> impl Strategy<Value = chrono::DateTime<Utc>> {
/// ///
/// The Zcash protocol typically uses 4-byte seconds values, except for the /// The Zcash protocol typically uses 4-byte seconds values, except for the
/// [`Version`] message. /// [`Version`] message.
///
/// TODO: replace this strategy with `any::<DateTime32>()`.
pub fn datetime_u32() -> impl Strategy<Value = chrono::DateTime<Utc>> { pub fn datetime_u32() -> impl Strategy<Value = chrono::DateTime<Utc>> {
any::<u32>().prop_map(|secs| Utc.timestamp(secs.into(), 0)) any::<DateTime32>().prop_map(Into::into)
} }
/// Returns a random canonical Zebra `SocketAddr`. /// Returns a random canonical Zebra `SocketAddr`.

View File

@ -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<Utc> {
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::<Utc>::from(*self))
.finish()
}
}
impl From<u32> for DateTime32 {
fn from(value: u32) -> Self {
DateTime32 { timestamp: value }
}
}
impl From<&u32> for DateTime32 {
fn from(value: &u32) -> Self {
(*value).into()
}
}
impl From<DateTime32> for chrono::DateTime<Utc> {
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<Utc> {
fn from(value: &DateTime32) -> Self {
(*value).into()
}
}
impl TryFrom<chrono::DateTime<Utc>> 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<Utc>) -> Result<Self, Self::Error> {
Ok(Self {
timestamp: value.timestamp().try_into()?,
})
}
}
impl TryFrom<&chrono::DateTime<Utc>> 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<Utc>) -> Result<Self, Self::Error> {
(*value).try_into()
}
}
impl ZcashSerialize for DateTime32 {
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_u32::<LittleEndian>(self.timestamp)
}
}
impl ZcashDeserialize for DateTime32 {
fn zcash_deserialize<R: std::io::Read>(mut reader: R) -> Result<Self, SerializationError> {
Ok(DateTime32 {
timestamp: reader.read_u32::<LittleEndian>()?,
})
}
}

View File

@ -208,9 +208,9 @@ impl AddressBook {
/// [`constants::LIVE_PEER_DURATION`] ago, we know we should have /// [`constants::LIVE_PEER_DURATION`] ago, we know we should have
/// disconnected from it. Otherwise, we could potentially be connected to it. /// disconnected from it. Otherwise, we could potentially be connected to it.
fn liveness_cutoff_time() -> DateTime<Utc> { fn liveness_cutoff_time() -> DateTime<Utc> {
// chrono uses signed durations while stdlib uses unsigned durations Utc::now()
use chrono::Duration as CD; - chrono::Duration::from_std(constants::LIVE_PEER_DURATION)
Utc::now() - CD::from_std(constants::LIVE_PEER_DURATION).unwrap() .expect("unexpectedly large constant")
} }
/// Returns true if the given [`SocketAddr`] has recently sent us a message. /// 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 // NeverAttempted, Failed, and AttemptPending peers should never be live
Some(peer) => { Some(peer) => {
peer.last_connection_state == PeerAddrState::Responded peer.last_connection_state == PeerAddrState::Responded
&& peer.get_last_seen() > AddressBook::liveness_cutoff_time() && peer.get_last_seen().to_chrono() > AddressBook::liveness_cutoff_time()
} }
} }
} }

View File

@ -69,7 +69,7 @@ pub const GET_ADDR_FANOUT: usize = 3;
/// ///
/// Timestamp truncation prevents a peer from learning exactly when we received /// Timestamp truncation prevents a peer from learning exactly when we received
/// messages from each of our peers. /// 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. /// The User-Agent string provided by the node.
/// ///

View File

@ -2,17 +2,15 @@
use std::{ use std::{
cmp::{Ord, Ordering}, cmp::{Ord, Ordering},
convert::TryInto,
io::{Read, Write}, io::{Read, Write},
net::SocketAddr, net::SocketAddr,
}; };
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use chrono::{DateTime, TimeZone, Utc};
use zebra_chain::serialization::{ use zebra_chain::serialization::{
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize, DateTime32, ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt,
ZcashSerialize, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
}; };
use crate::protocol::{external::MAX_PROTOCOL_MESSAGE_LEN, types::PeerServices}; 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. /// The last time we interacted with this peer.
/// ///
/// See `get_last_seen` for details. /// See `get_last_seen` for details.
last_seen: DateTime<Utc>, last_seen: DateTime32,
/// The outcome of our most recent communication attempt with this peer. /// The outcome of our most recent communication attempt with this peer.
pub last_connection_state: PeerAddrState, pub last_connection_state: PeerAddrState,
@ -142,7 +140,7 @@ impl MetaAddr {
pub fn new_gossiped_meta_addr( pub fn new_gossiped_meta_addr(
addr: SocketAddr, addr: SocketAddr,
untrusted_services: PeerServices, untrusted_services: PeerServices,
untrusted_last_seen: DateTime<Utc>, untrusted_last_seen: DateTime32,
) -> MetaAddr { ) -> MetaAddr {
MetaAddr { MetaAddr {
addr, addr,
@ -168,7 +166,7 @@ impl MetaAddr {
MetaAddr { MetaAddr {
addr: *addr, addr: *addr,
services: *services, services: *services,
last_seen: Utc::now(), last_seen: DateTime32::now(),
last_connection_state: Responded, last_connection_state: Responded,
} }
} }
@ -178,7 +176,7 @@ impl MetaAddr {
MetaAddr { MetaAddr {
addr: *addr, addr: *addr,
services: *services, services: *services,
last_seen: Utc::now(), last_seen: DateTime32::now(),
last_connection_state: AttemptPending, last_connection_state: AttemptPending,
} }
} }
@ -189,7 +187,7 @@ impl MetaAddr {
MetaAddr { MetaAddr {
addr: *addr, addr: *addr,
services: *services, services: *services,
last_seen: Utc::now(), last_seen: DateTime32::now(),
last_connection_state: NeverAttemptedAlternate, last_connection_state: NeverAttemptedAlternate,
} }
} }
@ -200,7 +198,7 @@ impl MetaAddr {
addr: *addr, addr: *addr,
// TODO: create a "local services" constant // TODO: create a "local services" constant
services: PeerServices::NODE_NETWORK, services: PeerServices::NODE_NETWORK,
last_seen: Utc::now(), last_seen: DateTime32::now(),
last_connection_state: Responded, last_connection_state: Responded,
} }
} }
@ -210,7 +208,7 @@ impl MetaAddr {
MetaAddr { MetaAddr {
addr: *addr, addr: *addr,
services: *services, services: *services,
last_seen: Utc::now(), last_seen: DateTime32::now(),
last_connection_state: Failed, last_connection_state: Failed,
} }
} }
@ -236,7 +234,7 @@ impl MetaAddr {
/// ///
/// `last_seen` times from `NeverAttempted` peers may be invalid due to /// `last_seen` times from `NeverAttempted` peers may be invalid due to
/// clock skew, or buggy or malicious peers. /// clock skew, or buggy or malicious peers.
pub fn get_last_seen(&self) -> DateTime<Utc> { pub fn get_last_seen(&self) -> DateTime32 {
self.last_seen self.last_seen
} }
@ -258,13 +256,14 @@ impl MetaAddr {
/// Return a sanitized version of this `MetaAddr`, for sending to a remote peer. /// Return a sanitized version of this `MetaAddr`, for sending to a remote peer.
pub fn sanitize(&self) -> MetaAddr { pub fn sanitize(&self) -> MetaAddr {
let interval = crate::constants::TIMESTAMP_TRUNCATION_SECONDS; let interval = crate::constants::TIMESTAMP_TRUNCATION_SECONDS;
let ts = self.get_last_seen().timestamp(); let ts = self.last_seen.timestamp();
let last_seen = Utc.timestamp(ts - ts.rem_euclid(interval), 0); // This can't underflow, because `0 <= rem_euclid < ts`
let last_seen = ts - ts.rem_euclid(interval);
MetaAddr { MetaAddr {
addr: self.addr, addr: self.addr,
// deserialization also sanitizes services to known flags // deserialization also sanitizes services to known flags
services: self.services & PeerServices::all(), 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 // the state isn't sent to the remote peer, but sanitize it anyway
last_connection_state: NeverAttemptedGossiped, last_connection_state: NeverAttemptedGossiped,
} }
@ -328,12 +327,7 @@ impl Eq for MetaAddr {}
impl ZcashSerialize for MetaAddr { impl ZcashSerialize for MetaAddr {
fn zcash_serialize<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> { fn zcash_serialize<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_u32::<LittleEndian>( self.last_seen.zcash_serialize(&mut writer)?;
self.get_last_seen()
.timestamp()
.try_into()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
)?;
writer.write_u64::<LittleEndian>(self.services.bits())?; writer.write_u64::<LittleEndian>(self.services.bits())?;
writer.write_socket_addr(self.addr)?; writer.write_socket_addr(self.addr)?;
Ok(()) Ok(())
@ -342,8 +336,7 @@ impl ZcashSerialize for MetaAddr {
impl ZcashDeserialize for MetaAddr { impl ZcashDeserialize for MetaAddr {
fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> { fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
// This can't panic, because all u32 values are valid `Utc.timestamp`s let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?;
let untrusted_last_seen = Utc.timestamp(reader.read_u32::<LittleEndian>()?.into(), 0);
let untrusted_services = let untrusted_services =
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?); PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
let addr = reader.read_socket_addr()?; let addr = reader.read_socket_addr()?;

View File

@ -2,16 +2,14 @@ use proptest::{arbitrary::any, arbitrary::Arbitrary, prelude::*};
use super::{MetaAddr, PeerAddrState, PeerServices}; use super::{MetaAddr, PeerAddrState, PeerServices};
use zebra_chain::serialization::arbitrary::{canonical_socket_addr, datetime_u32}; use zebra_chain::serialization::{arbitrary::canonical_socket_addr, DateTime32};
use chrono::{TimeZone, Utc};
impl MetaAddr { impl MetaAddr {
pub fn gossiped_strategy() -> BoxedStrategy<Self> { pub fn gossiped_strategy() -> BoxedStrategy<Self> {
( (
canonical_socket_addr(), canonical_socket_addr(),
any::<PeerServices>(), any::<PeerServices>(),
datetime_u32(), any::<DateTime32>(),
) )
.prop_map(|(address, services, untrusted_last_seen)| { .prop_map(|(address, services, untrusted_last_seen)| {
MetaAddr::new_gossiped_meta_addr(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(), canonical_socket_addr(),
any::<PeerServices>(), any::<PeerServices>(),
any::<u32>(), any::<DateTime32>(),
any::<PeerAddrState>(), any::<PeerAddrState>(),
) )
.prop_map( .prop_map(
|(addr, services, last_seen, last_connection_state)| MetaAddr { |(addr, services, last_seen, last_connection_state)| MetaAddr {
addr, addr,
services, services,
// This can't panic, because all u32 values are valid `Utc.timestamp`s last_seen,
last_seen: Utc.timestamp(last_seen.into(), 0),
last_connection_state, last_connection_state,
}, },
) )

View File

@ -17,7 +17,7 @@ pub(crate) fn sanitize_avoids_leaks(original: &MetaAddr, sanitized: &MetaAddr) {
sanitized.get_last_seen().timestamp() % TIMESTAMP_TRUNCATION_SECONDS, sanitized.get_last_seen().timestamp() % TIMESTAMP_TRUNCATION_SECONDS,
0 0
); );
assert_eq!(sanitized.get_last_seen().timestamp_subsec_nanos(), 0);
// handle underflow and overflow by skipping the check // handle underflow and overflow by skipping the check
// the other check will ensure correctness // the other check will ensure correctness
let lowest_time = original let lowest_time = original

View File

@ -2,8 +2,6 @@
use super::{super::MetaAddr, check}; use super::{super::MetaAddr, check};
use chrono::{MAX_DATETIME, MIN_DATETIME};
/// Make sure that the sanitize function handles minimum and maximum times. /// Make sure that the sanitize function handles minimum and maximum times.
#[test] #[test]
fn sanitize_extremes() { fn sanitize_extremes() {
@ -12,14 +10,14 @@ fn sanitize_extremes() {
let min_time_entry = MetaAddr { let min_time_entry = MetaAddr {
addr: "127.0.0.1:8233".parse().unwrap(), addr: "127.0.0.1:8233".parse().unwrap(),
services: Default::default(), services: Default::default(),
last_seen: MIN_DATETIME, last_seen: u32::MIN.into(),
last_connection_state: Default::default(), last_connection_state: Default::default(),
}; };
let max_time_entry = MetaAddr { let max_time_entry = MetaAddr {
addr: "127.0.0.1:8233".parse().unwrap(), addr: "127.0.0.1:8233".parse().unwrap(),
services: Default::default(), services: Default::default(),
last_seen: MAX_DATETIME, last_seen: u32::MAX.into(),
last_connection_state: Default::default(), last_connection_state: Default::default(),
}; };