diff --git a/CHANGES.md b/CHANGES.md index ca5c89284d8d723c18a8cdf022fed17c92bbb470..d89cef58ee37cb416cbacd5e37d6715e68b77835 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,13 @@ be documented here either. - The `--network` option now also accepts the name of a file containing the configuration for a custom network, or a URL from which such a file can be downloaded. + +- Fixed JSON encoding of timestamps before epoch (1970). + Pretty-printing and encoding of dates before epoch in human-readable form (as part + of a JSON value) that failed in the past will now succeed. Binary + form (used when nodes exchange data) was unaffected by the bug. This + may impact some RPC representations of timestamps. + ## Client diff --git a/src/lib_base/test/test_time.ml b/src/lib_base/test/test_time.ml index 76cdf30944bb8a3a63aa0fac02efa0f45c1c3505..78c381b6e18f648e1f29ddff8e0c8156155a8ce8 100644 --- a/src/lib_base/test/test_time.ml +++ b/src/lib_base/test/test_time.ml @@ -81,6 +81,19 @@ module Protocol = struct let j = Data_encoding.Json.construct encoding t in let tt = Data_encoding.Json.destruct encoding j in Crowbar.check_eq ~pp ~eq:equal t tt) + + let () = + Crowbar.add_test + ~name:"Base.Time.Protocol.to_notation roundtrip" + [Crowbar.range 1000] + (fun i -> + let close_to_epoch = add epoch (Int64.neg @@ Int64.of_int i) in + let s = to_notation close_to_epoch in + match of_notation s with + | None -> + Crowbar.fail "Failed to roundtrip notation" + | Some after_roundtrip -> + Crowbar.check_eq ~pp ~eq:equal close_to_epoch after_roundtrip) end module System = struct diff --git a/src/lib_base/time.ml b/src/lib_base/time.ml index 6189c34e324bac795b606f37583057f19222ddf9..8084098e808b80d385ae4c33e5daa751a4855b1e 100644 --- a/src/lib_base/time.ml +++ b/src/lib_base/time.ml @@ -43,6 +43,12 @@ module Protocol = struct let to_ptime t = let days = Int64.to_int (Int64.div t 86_400L) in let ps = Int64.mul (Int64.rem t 86_400L) 1_000_000_000_000L in + let (days, ps) = + if ps < 0L then + (* [Ptime.Span.of_d_ps] only accepts picoseconds in the range 0L-86_399_999_999_999_999L. Subtract a day and add a day's worth of picoseconds if need be. *) + (Int.pred days, Int64.(add ps (mul 86_400L 1_000_000_000_000L))) + else (days, ps) + in match Option.bind (Ptime.Span.of_d_ps (days, ps)) Ptime.of_span with | None -> invalid_arg "Time.Protocol.to_ptime" @@ -85,15 +91,15 @@ module Protocol = struct Data_encoding.Json.cannot_destruct "Time.Protocol.of_notation") string - let max_rfc3999 = of_ptime Ptime.max + let max_rfc3339 = of_ptime Ptime.max - let min_rfc3999 = of_ptime Ptime.min + let min_rfc3339 = of_ptime Ptime.min let as_string_encoding = let open Data_encoding in conv (fun i -> - if min_rfc3999 <= i && i <= max_rfc3999 then to_notation i + if min_rfc3339 <= i && i <= max_rfc3339 then to_notation i else Int64.to_string i) ( Json.wrap_error (* NOTE: this encoding is only used as a building block for a json diff --git a/src/lib_base/time.mli b/src/lib_base/time.mli index 287225d298c78cbd7a08cc6dbf494b663f8a5427..cd3c08da362ed5c6503c16a3096da265ad56ded2 100644 --- a/src/lib_base/time.mli +++ b/src/lib_base/time.mli @@ -41,13 +41,20 @@ protocol update changes the notion of time. - Protocol time and system time have different levels of precision. - Protocol time and system time have different end-of-times. Respectively - that's int64 end-of-time (some time in the year 292277026596) and rfc3339 + that's int64 end-of-time (some time in the year 292277026596) and RFC3339 end-of-time (end of the year 9999). + Note that while Protocol time has the int64 range, many of its functions + do not work outside of the RFC3339 range, namely: + - all [xx_notation_xx] functions (will be renamed and moved to an RFC + submodule later) + - [rfc_encoding] + - [pp_hum] + *) module Protocol : sig - (** {1:Protocol time} *) + (** {1 Protocol time} *) (** The out-of-protocol view of in-protocol timestamps. The precision of in-protocol timestamps are only precise to the second. @@ -70,27 +77,50 @@ module Protocol : sig [b] is later than [a]. *) val diff : t -> t -> int64 - (** Conversions to and from string representations. *) + (** {2 Conversions to and from string representations} *) + + (** Convert a string in the RFC3339 format (e.g., ["1970-01-01T00:00:00Z"]) into a protocol time. + Invalid RFC3339 notations will return [None]. + Note that years outside the 0000-9999 range are invalid RFC3339-wise. *) val of_notation : string -> t option + (** Convert a string in the RFC3339 format (e.g., ["1970-01-01T00:00:00Z"]) into a protocol time. + Invalid RFC3339 notations will raise [Invalid_argument]. + + Note that years outside the 0000-9999 range are invalid RFC3339-wise. *) val of_notation_exn : string -> t + (** Convert a protocol time into an RFC3339 notation (e.g., ["1970-01-01T00:00:00Z"]). + + Note that years outside the 0000-9999 range will raise [Invalid_argument] as they are invalid RFC3339-wise. *) val to_notation : t -> string (** Conversion to and from "number of seconds since epoch" representation. *) + (** Make a Protocol time from a number of seconds since the {!epoch}. *) val of_seconds : int64 -> t + (** Convert a Protocol time into a number of seconds since the {!epoch}. *) val to_seconds : t -> int64 - (** Serialization functions *) + (** {2 Serialization functions} *) + + (** Binary and JSON encoding. + + Binary is always encoding/decoding as [int64]. + JSON is more complex (for backward compatibility and user-friendliness): + - encoding uses the RFC3339 format (e.g., ["1970-01-01T00:00:00Z"]) if the year + is between 0000 and 9999 (RFC3339-compatible); otherwise it uses the [int64] format + - decoding tries to decode as an RFC3339 notation, and if it fails, it decodes as + [int64]. + *) val encoding : t Data_encoding.t - (* Note: the RFC has a different range than the internal representation. - Specifically, the RFC can only represent times from 0000-00-00 and - 9999-12-31 (inclusive). The rfc-encoding fails for dates outside of this + (** Note: the RFC has a different range than the internal representation. + Specifically, the RFC can only represent times between 0000-00-00 and + 9999-12-31 (inclusive). The RFC-encoding fails for dates outside of this range. For this reason, it is preferable to use {!encoding} and only resort to @@ -99,12 +129,12 @@ module Protocol : sig val rpc_arg : t RPC_arg.t - (** Pretty-printing functions *) + (** {2 Pretty-printing functions} *) - (* Pretty print as a number of seconds after epoch. *) + (** Pretty print as a number of seconds after epoch. *) val pp : Format.formatter -> t -> unit - (* Note: pretty-printing uses the rfc3999 for human-readability. As mentioned + (** Note: pretty-printing uses the RFC3339 for human-readability. As mentioned in the comment on {!rfc_encoding}, this representation fails when given dates too far in the future (after 9999-12-31) or the past (before 0000-00-00). @@ -114,7 +144,7 @@ module Protocol : sig end module System : sig - (** {1:System time} *) + (** {1 System time} *) (** A representation of timestamps. @@ -127,6 +157,7 @@ module System : sig type t = Ptime.t + (** Unix epoch is 1970-01-01 00:00:00.000000000000 UTC *) val epoch : t module Span : sig @@ -141,7 +172,7 @@ module System : sig span cannot be represented. *) val of_seconds_exn : float -> t - (** Serialization functions *) + (** {2 Serialization functions} *) val rpc_arg : t RPC_arg.t @@ -150,24 +181,43 @@ module System : sig val encoding : t Data_encoding.t end - (** Conversions to and from Protocol time. Note that converting system time to - protocol time truncates any subsecond precision. *) + (** {2 Conversions to and from Protocol time} *) + (** Note that converting system time to protocol time truncates any subsecond precision. *) + + (** Convert a Protocol time into a System time. + + Return [None] if the Protocol time is outside the RFC3339 range. *) val of_protocol_opt : Protocol.t -> t option + (** Convert a Protocol time into a System time. + + Raises [Invalid_argument] if the Protocol time is outside the RFC3339 range. *) val of_protocol_exn : Protocol.t -> t + (** Convert a System time into a Protocol time. + + Note that subseconds are truncated. *) val to_protocol : t -> Protocol.t - (** Conversions to and from string. It uses rfc3339. *) + (** {2 Conversions to and from string (using RFC3339)} *) + + (** Convert a string in the RFC3339 format (e.g., ["1970-01-01T00:00:00.000-00:00"]) into a system time. + Invalid RFC3339 notations will return [None]. + Note that years outside the 0000-9999 range are invalid RFC3339-wise. *) val of_notation_opt : string -> t option + (** Convert a string in the RFC3339 format (e.g., ["1970-01-01T00:00:00.000-00:00"]) into a system time. + Invalid RFC3339 notations will raise [Invalid_argument]. + + Note that years outside the 0000-9999 range are invalid RFC3339-wise. *) val of_notation_exn : string -> t + (** Convert a system time into an RFC3339 notation (e.g., ["1970-01-01T00:00:00.000-00:00"]). *) val to_notation : t -> string - (** Serialization. *) + (** {2 Serialization} *) val encoding : t Data_encoding.t @@ -175,11 +225,11 @@ module System : sig val rpc_arg : t RPC_arg.t - (** Pretty-printing *) + (** {2 Pretty-printing} *) val pp_hum : Format.formatter -> t -> unit - (** Timestamping data. *) + (** {2 Timestamping data} *) (** Data with an associated time stamp. *) type 'a stamped = {data : 'a; stamp : t} @@ -196,7 +246,7 @@ module System : sig timestamp), or [None] if both [a] and [b] are [None]. *) val recent : ('a * t) option -> ('a * t) option -> ('a * t) option - (** Helper modules *) + (** {2 Helper modules} *) val hash : t -> int