Skip to main content

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