Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
May 31, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
102 changes: 102 additions & 0 deletions zebra-chain/src/serialization/date_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! 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 as_chrono(&self) -> chrono::DateTime<Utc> {
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
self.into()
}
}

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().as_chrono() > AddressBook::liveness_cutoff_time()
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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
48 changes: 26 additions & 22 deletions zebra-network/src/meta_addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use std::{
};

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use chrono::{DateTime, TimeZone, Utc};
use chrono::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 +130,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 +142,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 +168,9 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: Utc::now()
.try_into()
.expect("unexpected out of range DateTime"),
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
last_connection_state: Responded,
}
}
Expand All @@ -178,7 +180,9 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: Utc::now()
.try_into()
.expect("unexpected out of range DateTime"),
last_connection_state: AttemptPending,
}
}
Expand All @@ -189,7 +193,9 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: Utc::now()
.try_into()
.expect("unexpected out of range DateTime"),
last_connection_state: NeverAttemptedAlternate,
}
}
Expand All @@ -200,7 +206,9 @@ impl MetaAddr {
addr: *addr,
// TODO: create a "local services" constant
services: PeerServices::NODE_NETWORK,
last_seen: Utc::now(),
last_seen: Utc::now()
.try_into()
.expect("unexpected out of range DateTime"),
last_connection_state: Responded,
}
}
Expand All @@ -210,7 +218,9 @@ impl MetaAddr {
MetaAddr {
addr: *addr,
services: *services,
last_seen: Utc::now(),
last_seen: Utc::now()
.try_into()
.expect("unexpected out of range DateTime"),
last_connection_state: Failed,
}
}
Expand All @@ -236,7 +246,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 +268,13 @@ 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();
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 +338,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 +347,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