Skip to main content

tsoracle_core/
epoch.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//! Leader epoch — opaque monotonic identifier chosen by the consensus driver.
25//!
26//! The width is `u128` so a driver can encode its full leadership identity
27//! without truncation. The paxos driver packs an OmniPaxos `Ballot`'s
28//! `(config_id: u32, n: u32, pid: u64)` — 128 bits — losslessly; raft-style
29//! drivers use the low bits for a `u64` term. The fence compares epochs by
30//! equality and the client's leader cache orders them, so the encoding a
31//! driver chooses must be both injective and monotonic.
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct Epoch(pub u128);
36
37impl Epoch {
38    pub const ZERO: Epoch = Epoch(0);
39
40    /// Split into `(hi, lo)` 64-bit halves for the two-`uint64` wire form.
41    /// `hi` is the more significant half, so lexicographic `(hi, lo)`
42    /// comparison equals numeric `u128` comparison.
43    #[must_use]
44    pub fn to_wire(self) -> (u64, u64) {
45        ((self.0 >> 64) as u64, self.0 as u64)
46    }
47
48    /// Reassemble from the `(hi, lo)` halves produced by [`Self::to_wire`].
49    #[must_use]
50    pub fn from_wire(hi: u64, lo: u64) -> Self {
51        Epoch((u128::from(hi) << 64) | u128::from(lo))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use proptest::prelude::*;
59
60    #[test]
61    fn default_is_zero() {
62        assert_eq!(Epoch::default(), Epoch::ZERO);
63    }
64
65    #[test]
66    fn ordering() {
67        assert!(Epoch(1) < Epoch(2));
68        assert!(Epoch::ZERO < Epoch(1));
69    }
70
71    #[test]
72    fn wire_round_trip_spans_the_64_bit_boundary() {
73        for epoch in [
74            Epoch::ZERO,
75            Epoch(1),
76            Epoch(u128::from(u64::MAX)),
77            Epoch(u128::from(u64::MAX) + 1),
78            Epoch(u128::MAX),
79        ] {
80            let (hi, lo) = epoch.to_wire();
81            assert_eq!(Epoch::from_wire(hi, lo), epoch);
82        }
83    }
84
85    #[test]
86    fn wire_halves_preserve_numeric_order() {
87        // A value one above u64::MAX (hi=1, lo=0) must outrank the largest
88        // value that fits in the low half alone (hi=0, lo=u64::MAX).
89        let just_over = Epoch::from_wire(1, 0);
90        let max_low = Epoch::from_wire(0, u64::MAX);
91        assert!(just_over > max_low);
92    }
93
94    proptest! {
95        // from_wire is the exact inverse of to_wire across the full u128 domain.
96        #[test]
97        fn wire_round_trip(raw in any::<u128>()) {
98            let epoch = Epoch(raw);
99            let (hi, lo) = epoch.to_wire();
100            prop_assert_eq!(Epoch::from_wire(hi, lo), epoch);
101        }
102
103        // Lexicographic (hi, lo) order equals numeric Epoch order — the
104        // property the client's monotone-forward leader cache relies on.
105        #[test]
106        fn wire_halves_order_matches_numeric(a in any::<u128>(), b in any::<u128>()) {
107            let (ea, eb) = (Epoch(a), Epoch(b));
108            prop_assert_eq!(ea.to_wire().cmp(&eb.to_wire()), ea.cmp(&eb));
109        }
110    }
111}