Skip to main content

tsoracle_core/
timestamp.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//
8//  Copyright (c) 2026 Prisma Risk
9//  Licensed under the Apache License, Version 2.0
10//  https://github.com/prisma-risk/tsoracle
11//
12
13// #[PerformanceCriticalPath]
14//! 64-bit packed timestamp: 46 high bits for `physical_ms`, 18 low bits for `logical`.
15
16const LOGICAL_BITS: u32 = 18;
17const LOGICAL_MASK: u64 = (1 << LOGICAL_BITS) - 1;
18pub const LOGICAL_MAX: u32 = (1 << LOGICAL_BITS) - 1;
19pub const PHYSICAL_MS_MAX: u64 = (1 << (64 - LOGICAL_BITS)) - 1;
20
21#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq)]
22pub enum TimestampError {
23    #[error("physical_ms {physical_ms} exceeds 46-bit maximum {max}")]
24    PhysicalMsOutOfRange { physical_ms: u64, max: u64 },
25    #[error("logical {logical} exceeds 18-bit maximum {max}")]
26    LogicalOutOfRange { logical: u32, max: u32 },
27}
28
29#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct Timestamp(pub u64);
32
33impl Timestamp {
34    pub const fn pack(physical_ms: u64, logical: u32) -> Self {
35        assert!(
36            physical_ms <= PHYSICAL_MS_MAX,
37            "physical_ms exceeds 46-bit timestamp field"
38        );
39        assert!(
40            logical <= LOGICAL_MAX,
41            "logical exceeds 18-bit timestamp field"
42        );
43        Timestamp((physical_ms << LOGICAL_BITS) | (logical as u64))
44    }
45
46    pub const fn try_pack(physical_ms: u64, logical: u32) -> Result<Self, TimestampError> {
47        if physical_ms > PHYSICAL_MS_MAX {
48            return Err(TimestampError::PhysicalMsOutOfRange {
49                physical_ms,
50                max: PHYSICAL_MS_MAX,
51            });
52        }
53        if logical > LOGICAL_MAX {
54            return Err(TimestampError::LogicalOutOfRange {
55                logical,
56                max: LOGICAL_MAX,
57            });
58        }
59        Ok(Timestamp((physical_ms << LOGICAL_BITS) | (logical as u64)))
60    }
61
62    pub const fn physical_ms(self) -> u64 {
63        self.0 >> LOGICAL_BITS
64    }
65    pub const fn logical(self) -> u32 {
66        (self.0 & LOGICAL_MASK) as u32
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn pack_unpack_roundtrip() {
76        let ts = Timestamp::pack(1_700_000_000_000, 12345);
77        assert_eq!(ts.physical_ms(), 1_700_000_000_000);
78        assert_eq!(ts.logical(), 12345);
79    }
80
81    #[test]
82    fn pack_zero() {
83        let ts = Timestamp::pack(0, 0);
84        assert_eq!(ts.0, 0);
85        assert_eq!(ts.physical_ms(), 0);
86        assert_eq!(ts.logical(), 0);
87    }
88
89    #[test]
90    fn pack_max_logical() {
91        let ts = Timestamp::pack(1000, LOGICAL_MAX);
92        assert_eq!(ts.physical_ms(), 1000);
93        assert_eq!(ts.logical(), LOGICAL_MAX);
94    }
95
96    #[test]
97    fn try_pack_rejects_out_of_range_physical_ms() {
98        assert!(matches!(
99            Timestamp::try_pack(PHYSICAL_MS_MAX + 1, 0),
100            Err(TimestampError::PhysicalMsOutOfRange { .. })
101        ));
102    }
103
104    #[test]
105    fn try_pack_rejects_out_of_range_logical() {
106        assert!(matches!(
107            Timestamp::try_pack(0, LOGICAL_MAX + 1),
108            Err(TimestampError::LogicalOutOfRange { .. })
109        ));
110    }
111
112    #[test]
113    #[should_panic(expected = "physical_ms exceeds 46-bit timestamp field")]
114    fn pack_panics_on_out_of_range_physical_ms() {
115        let _ = Timestamp::pack(PHYSICAL_MS_MAX + 1, 0);
116    }
117
118    #[test]
119    #[should_panic(expected = "logical exceeds 18-bit timestamp field")]
120    fn pack_panics_on_out_of_range_logical() {
121        let _ = Timestamp::pack(0, LOGICAL_MAX + 1);
122    }
123
124    #[test]
125    fn ordering_follows_packed_value() {
126        let earliest = Timestamp::pack(1000, 5);
127        let middle = Timestamp::pack(1000, 6);
128        let latest = Timestamp::pack(1001, 0);
129        assert!(earliest < middle);
130        assert!(middle < latest);
131        assert!(earliest < latest);
132    }
133
134    use proptest::prelude::*;
135
136    proptest! {
137        // Roundtrip: pack then unpack returns the original fields for any valid
138        // input in the 46-bit physical / 18-bit logical domain. A single failure
139        // here means the bit layout is wrong (mask, shift, or width).
140        #[test]
141        fn pack_unpack_roundtrip_property(
142            physical_ms in 0u64..=PHYSICAL_MS_MAX,
143            logical in 0u32..=LOGICAL_MAX,
144        ) {
145            let packed = Timestamp::pack(physical_ms, logical);
146            prop_assert_eq!(packed.physical_ms(), physical_ms);
147            prop_assert_eq!(packed.logical(), logical);
148        }
149
150        // Order preservation: the packed u64 ordering equals lexicographic
151        // ordering on (physical_ms, logical). This is the property that lets
152        // every consumer compare Timestamps directly without unpacking.
153        #[test]
154        fn ordering_matches_lexicographic_pair(
155            physical_a in 0u64..=PHYSICAL_MS_MAX,
156            logical_a in 0u32..=LOGICAL_MAX,
157            physical_b in 0u64..=PHYSICAL_MS_MAX,
158            logical_b in 0u32..=LOGICAL_MAX,
159        ) {
160            let timestamp_a = Timestamp::pack(physical_a, logical_a);
161            let timestamp_b = Timestamp::pack(physical_b, logical_b);
162            let pair_ordering = (physical_a, logical_a).cmp(&(physical_b, logical_b));
163            prop_assert_eq!(timestamp_a.cmp(&timestamp_b), pair_ordering);
164        }
165
166        // try_pack accepts exactly the values pack accepts and rejects everything
167        // else, with the right error variant. Catches drift between the two APIs.
168        #[test]
169        fn try_pack_accepts_iff_in_range(
170            physical_ms in 0u64..=u64::MAX >> 10,
171            logical in 0u32..=u32::MAX >> 10,
172        ) {
173            let result = Timestamp::try_pack(physical_ms, logical);
174            if physical_ms > PHYSICAL_MS_MAX {
175                let rejected_physical =
176                    matches!(result, Err(TimestampError::PhysicalMsOutOfRange { .. }));
177                prop_assert!(rejected_physical);
178            } else if logical > LOGICAL_MAX {
179                let rejected_logical =
180                    matches!(result, Err(TimestampError::LogicalOutOfRange { .. }));
181                prop_assert!(rejected_logical);
182            } else {
183                let timestamp = result.unwrap();
184                prop_assert_eq!(timestamp.physical_ms(), physical_ms);
185                prop_assert_eq!(timestamp.logical(), logical);
186            }
187        }
188    }
189}