diff --git a/.changelog/unreleased/breaking-changes/1030-remove-chrono.md b/.changelog/unreleased/breaking-changes/1030-remove-chrono.md new file mode 100644 index 000000000..7d8385ee5 --- /dev/null +++ b/.changelog/unreleased/breaking-changes/1030-remove-chrono.md @@ -0,0 +1,11 @@ +- `[tendermint]` Reform `tendermint::Time` + ([#1030](https://github.com/informalsystems/tendermint-rs/issues/1030)): + * The struct content is made private. + * The range of acceptable values is restricted to years 1-9999 + (as reckoned in UTC). + * Removed conversions from/to `chrono::DateTime`. + * Changes in error variants: removed `TimestampOverflow`, replaced with + `TimestampNanosOutOfRange`; removed `ChronoParse`, replaced with `TimeParse`. +- `[tendermint-rpc]` Use `OffsetDateTime` and `Date` types provided by the `time` crate + in query operands instead of their `chrono` counterparts. + ([#1030](https://github.com/informalsystems/tendermint-rs/issues/1030)) diff --git a/.changelog/unreleased/improvements/1030-new-time-api.md b/.changelog/unreleased/improvements/1030-new-time-api.md new file mode 100644 index 000000000..a9c269f0d --- /dev/null +++ b/.changelog/unreleased/improvements/1030-new-time-api.md @@ -0,0 +1,11 @@ +- Remove dependencies on the `chrono` crate. + ([#1030](https://github.com/informalsystems/tendermint-rs/issues/1030)) +- `[tendermint]` Improve `tendermint::Time` + ([#1030](https://github.com/informalsystems/tendermint-rs/issues/1030)): + * Restrict the validity range of `Time` to dates with years in the range + 1-9999, to match the specification of protobuf message `Timestamp`. + Add an `ErrorDetail` variant `DateOutOfRange` to report when this + restriction is not met. + * Added a conversion to, and a fallible conversion from, + `OffsetDateTime` of the `time` crate. + * Added `Time` methods `checked_add` and `checked_sub`. diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 12021a25c..e31a9d536 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -41,7 +41,6 @@ tendermint = { version = "0.23.0", path = "../tendermint", default-features = fa tendermint-rpc = { version = "0.23.0", path = "../rpc", default-features = false } contracts = { version = "0.4.0", default-features = false } -chrono = { version = "0.4", default-features = false, features = ["clock"] } crossbeam-channel = { version = "0.4.2", default-features = false } derive_more = { version = "0.99.5", default-features = false, features = ["display"] } futures = { version = "0.3.4", default-features = false } @@ -50,6 +49,7 @@ serde_cbor = { version = "0.11.1", default-features = false, features = ["alloc" serde_derive = { version = "1.0.106", default-features = false } sled = { version = "0.34.3", optional = true, default-features = false } static_assertions = { version = "1.1.0", default-features = false } +time = { version = "0.3", default-features = false, features = ["std"] } tokio = { version = "1.0", default-features = false, features = ["rt"], optional = true } flex-error = { version = "0.4.4", default-features = false } diff --git a/light-client/src/components/clock.rs b/light-client/src/components/clock.rs index c25869302..b1dd3ff0e 100644 --- a/light-client/src/components/clock.rs +++ b/light-client/src/components/clock.rs @@ -1,7 +1,8 @@ //! Provides an interface and a default implementation of the `Clock` component use crate::types::Time; -use chrono::Utc; +use std::convert::TryInto; +use time::OffsetDateTime; /// Abstracts over the current time. pub trait Clock: Send + Sync { @@ -14,6 +15,8 @@ pub trait Clock: Send + Sync { pub struct SystemClock; impl Clock for SystemClock { fn now(&self) -> Time { - Time(Utc::now()) + OffsetDateTime::now_utc() + .try_into() + .expect("system clock produces invalid time") } } diff --git a/light-client/src/predicates.rs b/light-client/src/predicates.rs index 605bd1806..8b984c4dc 100644 --- a/light-client/src/predicates.rs +++ b/light-client/src/predicates.rs @@ -205,10 +205,11 @@ pub trait VerificationPredicates: Send + Sync { #[cfg(test)] mod tests { - use chrono::Utc; - use std::ops::Sub; + use std::convert::TryInto; use std::time::Duration; - use tendermint::Time; + use tendermint::block::CommitSig; + use tendermint::validator::Set; + use time::OffsetDateTime; use tendermint_testgen::{ light_block::{LightBlock as TestgenLightBlock, TmLightBlock}, @@ -224,8 +225,6 @@ mod tests { Hasher, ProdCommitValidator, ProdHasher, ProdVotingPowerCalculator, VotingPowerTally, }; use crate::types::{LightBlock, TrustThreshold}; - use tendermint::block::CommitSig; - use tendermint::validator::Set; impl From for LightBlock { fn from(lb: TmLightBlock) -> Self { @@ -294,7 +293,7 @@ mod tests { // 1. ensure valid header verifies let mut trusting_period = Duration::new(1000, 0); - let now = Time(Utc::now()); + let now = OffsetDateTime::now_utc().try_into().unwrap(); let result_ok = vp.is_within_trust_period(header.time, trusting_period, now); assert!(result_ok.is_ok()); @@ -322,13 +321,15 @@ mod tests { let vp = ProdPredicates::default(); let one_second = Duration::new(1, 0); + let now = OffsetDateTime::now_utc().try_into().unwrap(); + // 1. ensure valid header verifies - let result_ok = vp.is_header_from_past(header.time, one_second, Time(Utc::now())); + let result_ok = vp.is_header_from_past(header.time, one_second, now); assert!(result_ok.is_ok()); // 2. ensure it fails if header is from a future time - let now = Time(Utc::now()).sub(one_second * 15).unwrap(); + let now = now.checked_sub(one_second * 15).unwrap(); let result_err = vp.is_header_from_past(header.time, one_second, now); match result_err { diff --git a/light-client/tests/model_based.rs b/light-client/tests/model_based.rs index c658dc21f..136567d9e 100644 --- a/light-client/tests/model_based.rs +++ b/light-client/tests/model_based.rs @@ -1,11 +1,10 @@ #[cfg(feature = "mbt")] mod mbt { - use chrono::Utc; use rand::Rng; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Error; - use std::convert::TryFrom; + use std::convert::{TryFrom, TryInto}; use std::str::FromStr; use std::time::Duration; use tendermint::validator::Set; @@ -20,6 +19,7 @@ mod mbt { apalache::*, jsonatr::*, light_block::TmLightBlock, validator::generate_validators, Command, Generator, LightBlock as TestgenLightBlock, TestEnv, Tester, Validator, Vote, }; + use time::OffsetDateTime; fn testgen_to_lb(tm_lb: TmLightBlock) -> LightBlock { LightBlock { @@ -180,7 +180,8 @@ mod mbt { impl SingleStepTestFuzzer for HeaderTimeFuzzer { fn fuzz_input(input: &mut BlockVerdict) -> (String, LiteVerdict) { let mut rng = rand::thread_rng(); - let secs = tendermint::Time(Utc::now()) + let now: Time = OffsetDateTime::now_utc().try_into().unwrap(); + let secs = now .duration_since(tendermint::Time::unix_epoch()) .unwrap() .as_secs(); diff --git a/pbt-gen/Cargo.toml b/pbt-gen/Cargo.toml index 7481be786..8a86378bd 100644 --- a/pbt-gen/Cargo.toml +++ b/pbt-gen/Cargo.toml @@ -17,8 +17,10 @@ description = """ [features] default = ["time"] -time = ["chrono"] [dependencies] -chrono = { version = "0.4", default-features = false, features = ["serde"], optional = true} +time = { version = "0.3.5", default-features = false, optional = true } proptest = { version = "0.10.1", default-features = false, features = ["std"] } + +[dev-dependencies] +time = { version = "0.3.5", features = ["macros"] } diff --git a/pbt-gen/src/time.rs b/pbt-gen/src/time.rs index 567ce6140..2046df0ba 100644 --- a/pbt-gen/src/time.rs +++ b/pbt-gen/src/time.rs @@ -3,107 +3,169 @@ use std::convert::TryInto; -use chrono::{DateTime, NaiveDate, TimeZone, Timelike, Utc}; use proptest::prelude::*; +use time::format_description::well_known::Rfc3339; +use time::macros::{datetime, offset}; +use time::{Date, OffsetDateTime, UtcOffset}; /// Any higher, and we're at seconds pub const MAX_NANO_SECS: u32 = 999_999_999u32; -/// The most distant time in the past for which chrono produces correct -/// times from [Utc.timestamp](chrono::Utc.timestamp). -/// -/// See . +/// The most distant time in the past for which `time` produces correct +/// times with [`OffsetDateTime::from_unix_timestamp`]. /// /// ``` /// use tendermint_pbt_gen as pbt_gen; +/// use time::OffsetDateTime; +/// +/// let timestamp = pbt_gen::time::min_time().unix_timestamp_nanos(); +/// assert!(OffsetDateTime::from_unix_timestamp_nanos(timestamp).is_ok()); +/// assert!(OffsetDateTime::from_unix_timestamp_nanos(timestamp - 1).is_err()); /// -/// assert_eq!(pbt_gen::time::min_time().to_string(), "1653-02-10 06:13:21 UTC".to_string()); /// ``` -pub fn min_time() -> DateTime { - Utc.timestamp(-9999999999, 0) +pub fn min_time() -> OffsetDateTime { + Date::MIN.midnight().assume_utc() } -/// The most distant time in the future for which chrono produces correct -/// times from [Utc.timestamp](chrono::Utc.timestamp). -/// -/// See . +/// The most distant time in the future for which `time` produces correct +/// times with [`OffsetDateTime::from_unix_timestamp`]. /// /// ``` /// use tendermint_pbt_gen as pbt_gen; +/// use time::OffsetDateTime; /// -/// assert_eq!(pbt_gen::time::max_time().to_string(), "5138-11-16 09:46:39 UTC".to_string()); +/// let timestamp = pbt_gen::time::max_time().unix_timestamp_nanos(); +/// assert!(OffsetDateTime::from_unix_timestamp_nanos(timestamp).is_ok()); +/// assert!(OffsetDateTime::from_unix_timestamp_nanos(timestamp + 1).is_err()); /// ``` -pub fn max_time() -> DateTime { - Utc.timestamp(99999999999, 0) -} - -fn num_days_in_month(year: i32, month: u32) -> u32 { - // Using chrono, we get the duration beteween this month and the next, - // then count the number of days in that duration. See - // https://stackoverflow.com/a/58188385/1187277 - let given_month = NaiveDate::from_ymd(year, month, 1); - let next_month = NaiveDate::from_ymd( - if month == 12 { year + 1 } else { year }, - if month == 12 { 1 } else { month + 1 }, - 1, - ); - next_month - .signed_duration_since(given_month) - .num_days() - .try_into() +pub fn max_time() -> OffsetDateTime { + Date::MAX + .with_hms_nano(23, 59, 59, MAX_NANO_SECS) .unwrap() + .assume_utc() +} + +/// The most distant time in the past that has a valid representation in +/// Google's well-known [`Timestamp`] protobuf message format. +/// +/// [`Timestamp`]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp +/// +pub const fn min_protobuf_time() -> OffsetDateTime { + datetime!(0001-01-01 00:00:00 UTC) +} + +/// The most distant time in the future that has a valid representation in +/// Google's well-known [`Timestamp`] protobuf message format. +/// +/// [`Timestamp`]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp +/// +pub const fn max_protobuf_time() -> OffsetDateTime { + datetime!(9999-12-31 23:59:59.999999999 UTC) +} + +fn num_days_in_month(year: i32, month: u8) -> u8 { + let month = month.try_into().unwrap(); + time::util::days_in_year_month(year, month) } prop_compose! { - /// An abitrary [chrono::DateTime] that is between the given `min` - /// and `max`. + /// An abitrary [`OffsetDateTime`], offset in UTC, + /// that is between the given `min` and `max`. /// /// # Examples /// /// ``` - /// use chrono::{TimeZone, Utc}; + /// use time::macros::datetime; /// use tendermint_pbt_gen as pbt_gen; /// use proptest::prelude::*; /// /// proptest!{ /// fn rosa_luxemburg_and_octavia_butler_were_not_alive_at_the_same_time( /// time_in_luxemburgs_lifespan in pbt_gen::time::arb_datetime_in_range( - /// Utc.ymd(1871, 3, 5).and_hms(0,0,0), // DOB - /// Utc.ymd(1919, 1, 15).and_hms(0,0,0), // DOD + /// datetime!(1871-03-05 00:00 UTC), // DOB + /// datetime!(1919-01-15 00:00 UTC), // DOD /// ), /// time_in_butlers_lifespan in pbt_gen::time::arb_datetime_in_range( - /// Utc.ymd(1947, 6, 22).and_hms(0,0,0), // DOB - /// Utc.ymd(2006, 2, 24).and_hms(0,0,0), // DOD + /// datetime!(1947-06-22 00:00 UTC), // DOB + /// datetime!(2006-02-24 00:00 UTC), // DOD /// ), /// ) { /// prop_assert!(time_in_luxemburgs_lifespan != time_in_butlers_lifespan) /// } /// } /// ``` - pub fn arb_datetime_in_range(min: DateTime, max: DateTime)( - secs in min.timestamp()..max.timestamp() - )( - // min mano secods is only relevant if we happen to hit the minimum - // seconds on the nose. - nano in (if secs == min.timestamp() { min.nanosecond() } else { 0 })..MAX_NANO_SECS, - // Make secs in scope - secs in Just(secs), - ) -> DateTime { - println!(">> Secs {:?}", secs); - Utc.timestamp(secs, nano) + pub fn arb_datetime_in_range(min: OffsetDateTime, max: OffsetDateTime)( + nanos in min.unix_timestamp_nanos()..max.unix_timestamp_nanos() + ) -> OffsetDateTime { + OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap() } } prop_compose! { - /// An abitrary [chrono::DateTime] (between [min_time] and [max_time]). + /// An abitrary [`OffsetDateTime`], offset in UTC (between [min_time] and [max_time]). pub fn arb_datetime() ( d in arb_datetime_in_range(min_time(), max_time()) - ) -> DateTime { + ) -> OffsetDateTime { d } } +prop_compose! { + /// An abitrary [`OffsetDateTime`] ((between [min_time] and [max_time])), + /// with an arbitrary time zone offset from UTC. + pub fn arb_datetime_with_offset() + ( + d in arb_datetime_in_range(min_time(), max_time()), + off in arb_utc_offset(), + ) -> OffsetDateTime { + d.to_offset(off) + } +} + +prop_compose! { + /// An abitrary [`OffsetDateTime`], offset in UTC, that can be represented + /// as an RFC 3339 timestamp. Values with year 0 are further excluded + /// due to the validity requirements on + /// Google's well-known [`Timestamp`] protobuf message format. + /// + /// [`Timestamp`]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp + pub fn arb_protobuf_safe_datetime() + ( + d in arb_datetime_in_range( + min_protobuf_time(), + max_protobuf_time(), + ) + ) -> OffsetDateTime { + d + } +} + +prop_compose! { + fn arb_utc_offset_hms() + ( + h in 0..=23i8, + m in 0..=59i8, + s in 0..=59i8, + ) -> UtcOffset { + UtcOffset::from_hms(h, m, s).unwrap() + } +} + +prop_compose! { + /// An abitrary [`UtcOffset`]. + pub fn arb_utc_offset() + ( + off in prop_oneof![ + Just(offset!(UTC)), + arb_utc_offset_hms(), + arb_utc_offset_hms().prop_map(|off| -off), + ] + ) -> UtcOffset { + off + } +} + // The following components of the timestamp follow // Section 5.6 of RFC3339: https://tools.ietf.org/html/rfc3339#ref-ABNF. @@ -147,16 +209,16 @@ prop_compose! { } prop_compose! { - fn arb_rfc3339_day_of_year_and_month(year: i32, month: u32) + fn arb_rfc3339_day_of_year_and_month(year: i32, month: u8) ( d in 1..num_days_in_month(year, month) - ) -> u32 { + ) -> u8 { d } } prop_compose! { - fn arb_rfc3339_full_date()(year in 0..9999i32, month in 1..12u32) + fn arb_rfc3339_full_date()(year in 0..9999i32, month in 1..12u8) ( day in arb_rfc3339_day_of_year_and_month(year, month), year in Just(year), @@ -180,3 +242,16 @@ prop_compose! { format!("{:}T{:}", date, time) } } + +/// Like `[arb_rfc3339_timestamp]`, but restricted to produce timestamps +/// that have a valid representation in +/// Google's well-known [`Timestamp`] protobuf message format. +/// +/// [`Timestamp`]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp +/// +pub fn arb_protobuf_safe_rfc3339_timestamp() -> impl Strategy { + arb_rfc3339_timestamp().prop_filter("timestamp out of protobuf range", |ts| { + let t = OffsetDateTime::parse(ts, &Rfc3339).unwrap(); + (min_protobuf_time()..=max_protobuf_time()).contains(&t) + }) +} diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 3b836564e..d3a6db8a7 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -25,7 +25,7 @@ serde_bytes = { version = "0.11", default-features = false, features = ["alloc"] subtle-encoding = { version = "0.5", default-features = false, features = ["hex", "base64", "alloc"] } num-traits = { version = "0.2", default-features = false } num-derive = { version = "0.3", default-features = false } -chrono = { version = "0.4", default-features = false, features = ["serde", "alloc"] } +time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } flex-error = { version = "0.4.4", default-features = false } [dev-dependencies] diff --git a/proto/src/chrono.rs b/proto/src/chrono.rs deleted file mode 100644 index 3b969f0f0..000000000 --- a/proto/src/chrono.rs +++ /dev/null @@ -1,53 +0,0 @@ -use core::convert::TryInto; - -use chrono::{DateTime, Duration, TimeZone, Utc}; - -use crate::google::protobuf as pb; - -impl From> for pb::Timestamp { - fn from(dt: DateTime) -> pb::Timestamp { - pb::Timestamp { - seconds: dt.timestamp(), - // This can exceed 1_000_000_000 in the case of a leap second, but - // even with a leap second it should be under 2_147_483_647. - nanos: dt - .timestamp_subsec_nanos() - .try_into() - .expect("timestamp_subsec_nanos bigger than i32::MAX"), - } - } -} - -impl From for DateTime { - fn from(ts: pb::Timestamp) -> DateTime { - Utc.timestamp(ts.seconds, ts.nanos as u32) - } -} - -// Note: we convert a protobuf::Duration into a chrono::Duration, not a -// std::time::Duration, because std::time::Durations are unsigned, but the -// protobuf duration is signed. - -impl From for pb::Duration { - fn from(d: Duration) -> pb::Duration { - // chrono's Duration stores the fractional part as `nanos: i32` - // internally but doesn't provide a way to access it, only a way to get - // the *total* number of nanoseconds. so we have to do this cool and fun - // hoop-jumping maneuver - let seconds = d.num_seconds(); - let nanos = (d - Duration::seconds(seconds)) - .num_nanoseconds() - .expect("we computed the fractional part, so there's no overflow") - .try_into() - .expect("the fractional part fits in i32"); - - pb::Duration { seconds, nanos } - } -} - -impl From for Duration { - fn from(d: pb::Duration) -> Duration { - // there's no constructor that supplies both at once - Duration::seconds(d.seconds) + Duration::nanoseconds(d.nanos as i64) - } -} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index f306f87ab..018ea6216 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -20,7 +20,6 @@ pub mod google { } } -mod chrono; mod error; #[allow(warnings)] mod tendermint; diff --git a/proto/src/serializers/timestamp.rs b/proto/src/serializers/timestamp.rs index 8843c17ef..6904bc9fc 100644 --- a/proto/src/serializers/timestamp.rs +++ b/proto/src/serializers/timestamp.rs @@ -1,10 +1,15 @@ //! Serialize/deserialize Timestamp type from and into string: -use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use crate::google::protobuf::Timestamp; use crate::prelude::*; -use chrono::{DateTime, LocalResult, TimeZone, Utc}; + +use core::fmt; +use serde::de::Error as _; use serde::ser::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use time::format_description::well_known::Rfc3339 as Rfc3339Format; +use time::macros::offset; +use time::OffsetDateTime; /// Helper struct to serialize and deserialize Timestamp into an RFC3339-compatible string /// This is required because the serde `with` attribute is only available to fields of a struct but @@ -30,12 +35,16 @@ where D: Deserializer<'de>, { let value_string = String::deserialize(deserializer)?; - let value_datetime = DateTime::parse_from_rfc3339(value_string.as_str()) - .map_err(|e| D::Error::custom(format!("{}", e)))?; - Ok(Timestamp { - seconds: value_datetime.timestamp(), - nanos: value_datetime.timestamp_subsec_nanos() as i32, - }) + let t = OffsetDateTime::parse(&value_string, &Rfc3339Format).map_err(D::Error::custom)?; + let t = t.to_offset(offset!(UTC)); + if !matches!(t.year(), 1..=9999) { + return Err(D::Error::custom("date is out of range")); + } + let seconds = t.unix_timestamp(); + // Safe to convert to i32 because .nanosecond() + // is guaranteed to return a value in 0..1_000_000_000 range. + let nanos = t.nanosecond() as i32; + Ok(Timestamp { seconds, nanos }) } /// Serialize from Timestamp into string @@ -43,51 +52,79 @@ pub fn serialize(value: &Timestamp, serializer: S) -> Result where S: Serializer, { - if value.nanos < 0 { + if value.nanos < 0 || value.nanos > 999_999_999 { return Err(S::Error::custom("invalid nanoseconds in time")); } - match Utc.timestamp_opt(value.seconds, value.nanos as u32) { - LocalResult::None => Err(S::Error::custom("invalid time")), - LocalResult::Single(t) => Ok(as_rfc3339_nanos(&t)), - LocalResult::Ambiguous(_, _) => Err(S::Error::custom("ambiguous time")), - }? - .serialize(serializer) + let total_nanos = value.seconds as i128 * 1_000_000_000 + value.nanos as i128; + let datetime = OffsetDateTime::from_unix_timestamp_nanos(total_nanos) + .map_err(|_| S::Error::custom("invalid time"))?; + to_rfc3339_nanos(datetime).serialize(serializer) } -/// Serialization helper for converting a `DateTime` object to a string. +/// Serialization helper for converting an [`OffsetDateTime`] object to a string. /// /// This reproduces the behavior of Go's `time.RFC3339Nano` format, /// ie. a RFC3339 date-time with left-padded subsecond digits without /// trailing zeros and no trailing dot. -pub fn as_rfc3339_nanos(t: &DateTime) -> String { - use chrono::format::{Fixed, Item, Numeric::*, Pad::Zero}; - - const PREFIX: &[Item<'_>] = &[ - Item::Numeric(Year, Zero), - Item::Literal("-"), - Item::Numeric(Month, Zero), - Item::Literal("-"), - Item::Numeric(Day, Zero), - Item::Literal("T"), - Item::Numeric(Hour, Zero), - Item::Literal(":"), - Item::Numeric(Minute, Zero), - Item::Literal(":"), - Item::Numeric(Second, Zero), - ]; - - const NANOS: &[Item<'_>] = &[Item::Fixed(Fixed::Nanosecond)]; +pub fn to_rfc3339_nanos(t: OffsetDateTime) -> String { + // Can't use OffsetDateTime::format because the feature enabling it + // currently requires std (https://github.com/time-rs/time/issues/400) - // Format as RFC339 without nanoseconds nor timezone marker - let prefix = t.format_with_items(PREFIX.iter()); + // Preallocate enough string capacity to fit the shortest possible form, + // yyyy-mm-ddThh:mm:ssZ + let mut buf = String::with_capacity(20); - // Format nanoseconds with dot, leading zeros, and variable number of trailing zeros - let nanos = t.format_with_items(NANOS.iter()).to_string(); + fmt_as_rfc3339_nanos(t, &mut buf).unwrap(); - // Trim trailing zeros and remove leftover dot if any - let nanos_trimmed = nanos.trim_end_matches('0').trim_end_matches('.'); + buf +} - format!("{}{}Z", prefix, nanos_trimmed) +/// Helper for formatting an [`OffsetDateTime`] value. +/// +/// This function can be used to efficiently format date-time values +/// in [`Display`] or [`Debug`] implementations. +/// +/// The format reproduces Go's `time.RFC3339Nano` format, +/// ie. a RFC3339 date-time with left-padded subsecond digits without +/// trailing zeros and no trailing dot. +/// +/// [`Display`]: core::fmt::Display +/// [`Debug`]: core::fmt::Debug +/// +pub fn fmt_as_rfc3339_nanos(t: OffsetDateTime, f: &mut impl fmt::Write) -> fmt::Result { + let t = t.to_offset(offset!(UTC)); + let nanos = t.nanosecond(); + if nanos == 0 { + write!( + f, + "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z", + year = t.year(), + month = t.month() as u8, + day = t.day(), + hour = t.hour(), + minute = t.minute(), + second = t.second(), + ) + } else { + let mut secfrac = nanos; + let mut secfrac_width = 9; + while secfrac % 10 == 0 { + secfrac /= 10; + secfrac_width -= 1; + } + write!( + f, + "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{secfrac:0sfw$}Z", + year = t.year(), + month = t.month() as u8, + day = t.day(), + hour = t.hour(), + minute = t.minute(), + second = t.second(), + secfrac = secfrac, + sfw = secfrac_width, + ) + } } #[allow(warnings)] diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 753b983ce..0aa860675 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -62,7 +62,6 @@ websocket-client = [ [dependencies] bytes = { version = "1.0", default-features = false } -chrono = { version = "0.4", default-features = false } getrandom = { version = "0.1", default-features = false } peg = { version = "0.7.0", default-features = false } pin-project = { version = "1.0.1", default-features = false } @@ -73,6 +72,7 @@ tendermint-config = { version = "0.23.0", path = "../config", default-features = tendermint = { version = "0.23.0", default-features = false, path = "../tendermint" } tendermint-proto = { version = "0.23.0", default-features = false, path = "../proto" } thiserror = { version = "1", default-features = false } +time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } uuid = { version = "0.8", default-features = false } subtle-encoding = { version = "0.5", default-features = false, features = ["bech32-preview"] } url = { version = "2.2", default-features = false } diff --git a/rpc/src/endpoint/consensus_state.rs b/rpc/src/endpoint/consensus_state.rs index 91b36f8a7..90edb2d7c 100644 --- a/rpc/src/endpoint/consensus_state.rs +++ b/rpc/src/endpoint/consensus_state.rs @@ -299,7 +299,7 @@ impl fmt::Display for VoteSummary { self.vote_type, self.block_id_hash_fingerprint, self.signature_fingerprint, - self.timestamp.as_rfc3339(), + self.timestamp, ) } } diff --git a/rpc/src/query.rs b/rpc/src/query.rs index 7a4de90e3..82b7da87e 100644 --- a/rpc/src/query.rs +++ b/rpc/src/query.rs @@ -6,9 +6,12 @@ use crate::prelude::*; use crate::Error; -use chrono::{Date, DateTime, FixedOffset, NaiveDate, Utc}; use core::fmt; use core::str::FromStr; +use tendermint_proto::serializers::timestamp; +use time::format_description::well_known::Rfc3339; +use time::macros::{format_description, offset}; +use time::{Date, OffsetDateTime}; /// A structured query for use in interacting with the Tendermint RPC event /// subscription system. @@ -271,16 +274,16 @@ peg::parser! { rule datetime_op() -> Operand = "TIME" __ dt:datetime() {? - DateTime::parse_from_rfc3339(dt) - .map(|dt| Operand::DateTime(dt.with_timezone(&Utc))) + OffsetDateTime::parse(dt, &Rfc3339) + .map(|dt| Operand::DateTime(dt.to_offset(offset!(UTC)))) .map_err(|_| "failed to parse as RFC3339-compatible date/time") } rule date_op() -> Operand = "DATE" __ dt:date() {? - let naive_date = NaiveDate::parse_from_str(dt, "%Y-%m-%d") + let date = Date::parse(dt, format_description!("[year]-[month]-[day]")) .map_err(|_| "failed to parse as RFC3339-compatible date")?; - Ok(Operand::Date(Date::from_utc(naive_date, Utc))) + Ok(Operand::Date(date)) } rule float_op() -> Operand @@ -465,8 +468,8 @@ pub enum Operand { Signed(i64), Unsigned(u64), Float(f64), - Date(Date), - DateTime(DateTime), + Date(Date), + DateTime(OffsetDateTime), } impl fmt::Display for Operand { @@ -476,12 +479,24 @@ impl fmt::Display for Operand { Operand::Signed(i) => write!(f, "{}", i), Operand::Unsigned(u) => write!(f, "{}", u), Operand::Float(h) => write!(f, "{}", h), - Operand::Date(d) => write!(f, "DATE {}", d.format("%Y-%m-%d").to_string()), - Operand::DateTime(dt) => write!(f, "TIME {}", dt.to_rfc3339()), + Operand::Date(d) => { + write!(f, "DATE ")?; + fmt_date(*d, f)?; + Ok(()) + } + Operand::DateTime(dt) => { + write!(f, "TIME ")?; + timestamp::fmt_as_rfc3339_nanos(*dt, f)?; + Ok(()) + } } } } +fn fmt_date(d: Date, mut f: impl fmt::Write) -> fmt::Result { + write!(f, "{:04}-{:02}-{:02}", d.year(), d.month() as u8, d.day()) +} + impl From for Operand { fn from(source: String) -> Self { Operand::String(source) @@ -566,21 +581,15 @@ impl From for Operand { } } -impl From> for Operand { - fn from(source: Date) -> Self { +impl From for Operand { + fn from(source: Date) -> Self { Operand::Date(source) } } -impl From> for Operand { - fn from(source: DateTime) -> Self { - Operand::DateTime(source) - } -} - -impl From> for Operand { - fn from(source: DateTime) -> Self { - Operand::DateTime(source.into()) +impl From for Operand { + fn from(source: OffsetDateTime) -> Self { + Operand::DateTime(source.to_offset(offset!(UTC))) } } @@ -599,7 +608,7 @@ fn escape(s: &str) -> String { #[cfg(test)] mod test { use super::*; - use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + use time::macros::{date, datetime}; #[test] fn empty_query() { @@ -657,21 +666,15 @@ mod test { #[test] fn date_condition() { - let query = Query::eq( - "some_date", - Date::from_utc(NaiveDate::from_ymd(2020, 9, 24), Utc), - ); + let query = Query::eq("some_date", date!(2020 - 09 - 24)); assert_eq!("some_date = DATE 2020-09-24", query.to_string()); } #[test] fn date_time_condition() { - let query = Query::eq( - "some_date_time", - DateTime::parse_from_rfc3339("2020-09-24T10:17:23-04:00").unwrap(), - ); + let query = Query::eq("some_date_time", datetime!(2020-09-24 10:17:23 -04:00)); assert_eq!( - "some_date_time = TIME 2020-09-24T14:17:23+00:00", + "some_date_time = TIME 2020-09-24T14:17:23Z", query.to_string() ); } @@ -793,7 +796,7 @@ mod test { query.conditions, vec![Condition::Lte( "some.date".to_owned(), - Operand::Date(Date::from_utc(NaiveDate::from_ymd(2022, 2, 3), Utc)) + Operand::Date(date!(2022 - 2 - 3)) )] ); } @@ -808,13 +811,7 @@ mod test { query.conditions, vec![Condition::Eq( "some.datetime".to_owned(), - Operand::DateTime(DateTime::from_utc( - NaiveDateTime::new( - NaiveDate::from_ymd(2021, 2, 26), - NaiveTime::from_hms_nano(17, 5, 2, 149500000) - ), - Utc - )) + Operand::DateTime(datetime!(2021-2-26 17:05:02.149500000 UTC)) )] ) } diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 6ee3f5f14..ceb207ae9 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -35,7 +35,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] async-trait = { version = "0.1", default-features = false } bytes = { version = "1.0", default-features = false, features = ["serde"] } -chrono = { version = "0.4.19", default-features = false, features = ["serde"] } ed25519 = { version = "1.3", default-features = false } ed25519-dalek = { version = "1", default-features = false, features = ["u64_backend"] } futures = { version = "0.3", default-features = false } @@ -52,6 +51,7 @@ signature = { version = "1.2", default-features = false } subtle = { version = "2", default-features = false } subtle-encoding = { version = "0.5", default-features = false, features = ["bech32-preview"] } tendermint-proto = { version = "0.23.0", default-features = false, path = "../proto" } +time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } zeroize = { version = "1.1", default-features = false, features = ["zeroize_derive", "alloc"] } flex-error = { version = "0.4.4", default-features = false } k256 = { version = "0.9", optional = true, default-features = false, features = ["ecdsa", "sha256"] } diff --git a/tendermint/src/abci/request/init_chain.rs b/tendermint/src/abci/request/init_chain.rs index bf2a697b5..61d6a8b18 100644 --- a/tendermint/src/abci/request/init_chain.rs +++ b/tendermint/src/abci/request/init_chain.rs @@ -1,7 +1,6 @@ use bytes::Bytes; -use chrono::{DateTime, Utc}; -use crate::{block, consensus, prelude::*}; +use crate::{block, consensus, prelude::*, Time}; use super::super::types::ValidatorUpdate; @@ -11,7 +10,7 @@ use super::super::types::ValidatorUpdate; #[derive(Clone, PartialEq, Eq, Debug)] pub struct InitChain { /// The genesis time. - pub time: DateTime, + pub time: Time, /// The ID of the blockchain. pub chain_id: String, /// Initial consensus-critical parameters. diff --git a/tendermint/src/abci/types.rs b/tendermint/src/abci/types.rs index 710342130..7e1ca5e6e 100644 --- a/tendermint/src/abci/types.rs +++ b/tendermint/src/abci/types.rs @@ -8,9 +8,8 @@ use core::convert::{TryFrom, TryInto}; use bytes::Bytes; -use chrono::{DateTime, Utc}; -use crate::{block, prelude::*, vote, Error, PublicKey}; +use crate::{block, prelude::*, vote, Error, PublicKey, Time}; /// A validator address with voting power. /// @@ -80,7 +79,7 @@ pub struct Evidence { /// The height when the offense occurred. pub height: block::Height, /// The corresponding time when the offense occurred. - pub time: DateTime, + pub time: Time, /// Total voting power of the validator set at `height`. /// /// This is included in case the ABCI application does not store historical @@ -247,7 +246,10 @@ impl TryFrom for Evidence { .ok_or_else(Error::missing_validator)? .try_into()?, height: evidence.height.try_into()?, - time: evidence.time.ok_or_else(Error::missing_timestamp)?.into(), + time: evidence + .time + .ok_or_else(Error::missing_timestamp)? + .try_into()?, total_voting_power: evidence.total_voting_power.try_into()?, }) } diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs index 62eed778e..a6be0a514 100644 --- a/tendermint/src/error.rs +++ b/tendermint/src/error.rs @@ -33,9 +33,9 @@ define_error! { { detail: String } |e| { format_args!("protocol error: {}", e.detail) }, - // When the oldtime feature is disabled, the chrono::oldtime::OutOfRangeError - // type is private and cannot be referred: - // https://github.com/chronotope/chrono/pull/541 + DateOutOfRange + |_| { format_args!("date out of range") }, + DurationOutOfRange |_| { format_args!("duration value out of range") }, @@ -80,9 +80,8 @@ define_error! { [ DisplayOnly ] |_| { format_args!("integer overflow") }, - TimestampOverflow - [ DisplayOnly ] - |_| { format_args!("timestamp overflow") }, + TimestampNanosOutOfRange + |_| { format_args!("timestamp nanosecond component is out of range") }, TimestampConversion |_| { format_args!("timestamp conversion error") }, @@ -200,9 +199,9 @@ define_error! { { account: account::Id } |e| { format_args!("proposer with address '{0}' no found in validator set", e.account) }, - ChronoParse - [ DisplayOnly ] - |_| { format_args!("chrono parse error") }, + TimeParse + [ DisplayOnly ] + |_| { format_args!("time parsing error") }, SubtleEncoding [ DisplayOnly ] diff --git a/tendermint/src/proposal.rs b/tendermint/src/proposal.rs index 4f266a9b9..f3637f407 100644 --- a/tendermint/src/proposal.rs +++ b/tendermint/src/proposal.rs @@ -123,13 +123,14 @@ mod tests { use crate::proposal::SignProposalRequest; use crate::test::dummy_signature; use crate::{proposal::Type, Proposal}; - use chrono::{DateTime, Utc}; + use core::convert::TryInto; use core::str::FromStr; use tendermint_proto::Protobuf; + use time::macros::datetime; #[test] fn test_serialization() { - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let dt = datetime!(2018-02-11 07:09:22.765 UTC); let proposal = Proposal { msg_type: Type::Proposal, height: Height::from(12345_u32), @@ -151,7 +152,7 @@ mod tests { ) .unwrap(), }), - timestamp: Some(dt.into()), + timestamp: Some(dt.try_into().unwrap()), signature: Some(dummy_signature()), }; @@ -215,7 +216,7 @@ mod tests { #[test] // Test proposal encoding with a malformed block ID which is considered null in Go. fn test_encoding_with_empty_block_id() { - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let dt = datetime!(2018-02-11 07:09:22.765 UTC); let proposal = Proposal { msg_type: Type::Proposal, height: Height::from(12345_u32), @@ -233,7 +234,7 @@ mod tests { ) .unwrap(), }), - timestamp: Some(dt.into()), + timestamp: Some(dt.try_into().unwrap()), signature: Some(dummy_signature()), }; @@ -294,12 +295,12 @@ mod tests { #[test] fn test_deserialization() { - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let dt = datetime!(2018-02-11 07:09:22.765 UTC); let proposal = Proposal { msg_type: Type::Proposal, height: Height::from(12345_u32), round: Round::from(23456_u16), - timestamp: Some(dt.into()), + timestamp: Some(dt.try_into().unwrap()), pol_round: None, block_id: Some(BlockId { diff --git a/tendermint/src/serializers/time.rs b/tendermint/src/serializers/time.rs index 940a9e6b1..ae5240448 100644 --- a/tendermint/src/serializers/time.rs +++ b/tendermint/src/serializers/time.rs @@ -11,7 +11,7 @@ pub fn serialize(value: &Time, serializer: S) -> Result where S: Serializer, { - value.as_rfc3339().serialize(serializer) + value.to_rfc3339().serialize(serializer) } /// Deserialize `String` into `Time` diff --git a/tendermint/src/time.rs b/tendermint/src/time.rs index 726ef7023..ac53be2c8 100644 --- a/tendermint/src/time.rs +++ b/tendermint/src/time.rs @@ -1,6 +1,5 @@ //! Timestamps used by Tendermint blockchains -use chrono::{DateTime, LocalResult, TimeZone, Utc}; use serde::{Deserialize, Serialize}; use crate::prelude::*; @@ -12,14 +11,32 @@ use core::time::Duration; use tendermint_proto::google::protobuf::Timestamp; use tendermint_proto::serializers::timestamp; use tendermint_proto::Protobuf; +use time::format_description::well_known::Rfc3339; +use time::macros::{datetime, offset}; +use time::{OffsetDateTime, PrimitiveDateTime}; use crate::error::Error; /// Tendermint timestamps -/// +/// +/// A `Time` value is guaranteed to represent a valid `Timestamp` as defined +/// by Google's well-known protobuf type [specification]. Conversions and +/// operations that would result in exceeding `Timestamp`'s validity +/// range return an error or `None`. +/// +/// The string serialization format for `Time` is defined as an RFC 3339 +/// compliant string with the optional subsecond fraction part having +/// up to 9 digits and no trailing zeros, and the UTC offset denoted by Z. +/// This reproduces the behavior of Go's `time.RFC3339Nano` format. +/// +/// [specification]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp +/// +// For memory efficiency, the inner member is `PrimitiveDateTime`, with assumed +// UTC offset. The `assume_utc` method is used to get the operational +// `OffsetDateTime` value. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(try_from = "Timestamp")] -pub struct Time(pub DateTime); +#[serde(try_from = "Timestamp", into = "Timestamp")] +pub struct Time(PrimitiveDateTime); impl Protobuf for Time {} @@ -27,35 +44,49 @@ impl TryFrom for Time { type Error = Error; fn try_from(value: Timestamp) -> Result { - let nanos = value.nanos.try_into().map_err(Error::timestamp_overflow)?; - Time::from_unix_timestamp(value.seconds, nanos) + let nanos = value + .nanos + .try_into() + .map_err(|_| Error::timestamp_nanos_out_of_range())?; + Self::from_unix_timestamp(value.seconds, nanos) } } impl From