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}