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}