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:
parent
a6e272bf1c
commit
ebe1c9f88e
|
@ -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;
|
||||
|
|
|
@ -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::<u32>().prop_map(Into::into).boxed()
|
||||
}
|
||||
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
/// Returns a strategy that produces an arbitrary [`chrono::DateTime<Utc>`],
|
||||
/// 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
|
||||
/// [`Version`] message.
|
||||
///
|
||||
/// TODO: replace this strategy with `any::<DateTime32>()`.
|
||||
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`.
|
||||
|
|
|
@ -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>()?,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<Utc> {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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<Utc>,
|
||||
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<Utc>,
|
||||
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<Utc> {
|
||||
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<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
|
||||
writer.write_u32::<LittleEndian>(
|
||||
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::<LittleEndian>(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<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 = Utc.timestamp(reader.read_u32::<LittleEndian>()?.into(), 0);
|
||||
let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?;
|
||||
let untrusted_services =
|
||||
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
|
||||
let addr = reader.read_socket_addr()?;
|
||||
|
|
|
@ -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<Self> {
|
||||
(
|
||||
canonical_socket_addr(),
|
||||
any::<PeerServices>(),
|
||||
datetime_u32(),
|
||||
any::<DateTime32>(),
|
||||
)
|
||||
.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::<PeerServices>(),
|
||||
any::<u32>(),
|
||||
any::<DateTime32>(),
|
||||
any::<PeerAddrState>(),
|
||||
)
|
||||
.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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue