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