Skip to main content

tsoracle_core/
timestamp.rs

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