Skip to content

Commit

Permalink
Add a DateTime32 type for 32-bit serialized times (#2210)
Browse files Browse the repository at this point in the history
* Add a DateTime32 type for 32-bit serialized times
* Use DateTime32 for MetaAddr.last_seen
* Create and use a `DateTime32::now` method
  • Loading branch information
teor2345 authored May 31, 2021
1 parent a6e272b commit ebe1c9f
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 42 deletions.
2 changes: 2 additions & 0 deletions zebra-chain/src/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions zebra-chain/src/serialization/arbitrary.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -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`.
Expand Down
109 changes: 109 additions & 0 deletions zebra-chain/src/serialization/date_time.rs
Original file line number Diff line number Diff line change
@@ -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>()?,
})
}
}
8 changes: 4 additions & 4 deletions zebra-network/src/address_book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion zebra-network/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
39 changes: 16 additions & 23 deletions zebra-network/src/meta_addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -168,7 +166,7 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: DateTime32::now(),
last_connection_state: Responded,
}
}
Expand All @@ -178,7 +176,7 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: DateTime32::now(),
last_connection_state: AttemptPending,
}
}
Expand All @@ -189,7 +187,7 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: DateTime32::now(),
last_connection_state: NeverAttemptedAlternate,
}
}
Expand All @@ -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,
}
}
Expand All @@ -210,7 +208,7 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: DateTime32::now(),
last_connection_state: Failed,
}
}
Expand All @@ -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
}

Expand All @@ -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,
}
Expand Down Expand Up @@ -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(())
Expand All @@ -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()?;
Expand Down
11 changes: 4 additions & 7 deletions zebra-network/src/meta_addr/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
},
)
Expand Down
2 changes: 1 addition & 1 deletion zebra-network/src/meta_addr/tests/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions zebra-network/src/meta_addr/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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(),
};

Expand Down

0 comments on commit ebe1c9f

Please sign in to comment.