Skip to main content

vortex_core/
seed.rs

1//! Hierarchical seed derivation for deterministic simulation.
2//!
3//! [`SeedTree`] provides a tree of deterministic sub-seeds: each subsystem
4//! (executor, fs, clock, alloc, net, process) gets its own independent but
5//! seed-reproducible random stream. Adding a new subsystem does **not** change
6//! the stream of any existing subsystem — this is the key invariant.
7//!
8//! Seeds are 128-bit for collision resistance across billions of simulation runs.
9
10use std::fmt;
11
12/// A 128-bit simulation seed.
13///
14/// Two identical `VortexSeed` values will always produce identical simulation
15/// behaviour. This is the top-level input to every Vortex simulation.
16#[derive(Clone, Copy, PartialEq, Eq, Hash)]
17pub struct VortexSeed {
18    hi: u64,
19    lo: u64,
20}
21
22impl VortexSeed {
23    /// Create a seed from two 64-bit halves.
24    pub const fn new(hi: u64, lo: u64) -> Self {
25        Self { hi, lo }
26    }
27
28    /// Create a seed from a single u64 (zero-extends the high half).
29    /// Convenient for simple test fixtures: `VortexSeed::from_u64(42)`.
30    pub const fn from_u64(val: u64) -> Self {
31        Self { hi: 0, lo: val }
32    }
33
34    /// Get the high 64 bits.
35    pub const fn hi(&self) -> u64 {
36        self.hi
37    }
38
39    /// Get the low 64 bits.
40    pub const fn lo(&self) -> u64 {
41        self.lo
42    }
43
44    /// Convert to a single u64 by XOR-folding.
45    /// Used when interfacing with the existing `DetRng::new(u64)` API.
46    pub const fn to_u64(&self) -> u64 {
47        self.hi ^ self.lo
48    }
49
50    /// Convert to a byte array (big-endian).
51    pub const fn to_bytes(&self) -> [u8; 16] {
52        let hi = self.hi.to_be_bytes();
53        let lo = self.lo.to_be_bytes();
54        [
55            hi[0], hi[1], hi[2], hi[3], hi[4], hi[5], hi[6], hi[7], lo[0], lo[1], lo[2], lo[3],
56            lo[4], lo[5], lo[6], lo[7],
57        ]
58    }
59
60    /// Create from a byte array (big-endian).
61    pub fn from_bytes(bytes: [u8; 16]) -> Self {
62        let hi = u64::from_be_bytes([
63            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
64        ]);
65        let lo = u64::from_be_bytes([
66            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
67        ]);
68        Self { hi, lo }
69    }
70}
71
72impl fmt::Debug for VortexSeed {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "VortexSeed(0x{:016x}{:016x})", self.hi, self.lo)
75    }
76}
77
78impl fmt::Display for VortexSeed {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{:016x}{:016x}", self.hi, self.lo)
81    }
82}
83
84impl From<u64> for VortexSeed {
85    fn from(val: u64) -> Self {
86        Self::from_u64(val)
87    }
88}
89
90/// Hierarchical, domain-keyed seed derivation.
91///
92/// `SeedTree` derives independent sub-seeds for each named subsystem using
93/// FNV-1a hashing. The critical invariant: `derive("fs")` always returns the
94/// same sub-seed regardless of what other domains are (or are not) derived.
95///
96/// ```
97/// # use vortex_core::SeedTree;
98/// let tree = SeedTree::new(42u64.into());
99/// let fs_seed = tree.derive("fs");
100/// let net_seed = tree.derive("net");
101///
102/// // Different domains produce different seeds
103/// assert_ne!(fs_seed.to_u64(), net_seed.to_u64());
104///
105/// // Same domain + same master seed = same sub-seed (deterministic)
106/// let tree2 = SeedTree::new(42u64.into());
107/// assert_eq!(tree.derive("fs"), tree2.derive("fs"));
108/// ```
109pub struct SeedTree {
110    master: VortexSeed,
111}
112
113impl SeedTree {
114    /// Create a new seed tree from a master seed.
115    pub const fn new(master: VortexSeed) -> Self {
116        Self { master }
117    }
118
119    /// Derive a sub-seed for a named domain.
120    ///
121    /// Uses FNV-1a to mix the master seed bytes with the domain string.
122    /// The result is a new [`VortexSeed`] that is:
123    /// - Deterministic: same master + same domain = same output, always.
124    /// - Independent: changing the domain name changes the output completely.
125    pub fn derive(&self, domain: &str) -> VortexSeed {
126        // FNV-1a 128-bit variant (split into two 64-bit halves for portability)
127        let mut hash_lo: u64 = 14695981039346656037; // FNV-1a offset basis (64-bit)
128        let mut hash_hi: u64 = 6700417; // Secondary prime
129
130        // Mix master seed bytes
131        for byte in self.master.to_bytes() {
132            hash_lo ^= byte as u64;
133            hash_lo = hash_lo.wrapping_mul(1099511628211); // FNV prime
134            hash_hi ^= byte as u64;
135            hash_hi = hash_hi.wrapping_mul(309485009821345068);
136        }
137
138        // Mix domain string bytes
139        for byte in domain.as_bytes() {
140            hash_lo ^= *byte as u64;
141            hash_lo = hash_lo.wrapping_mul(1099511628211);
142            hash_hi ^= *byte as u64;
143            hash_hi = hash_hi.wrapping_mul(309485009821345068);
144        }
145
146        VortexSeed::new(hash_hi, hash_lo)
147    }
148
149    /// Derive a sub-seed for a named domain, then create a child `SeedTree`
150    /// from it. Useful for hierarchical subsystems.
151    ///
152    /// ```
153    /// # use vortex_core::SeedTree;
154    /// let root = SeedTree::new(42u64.into());
155    /// let fs_tree = root.subtree("fs");
156    /// let wal_seed = fs_tree.derive("wal");
157    /// let data_seed = fs_tree.derive("data");
158    /// ```
159    pub fn subtree(&self, domain: &str) -> SeedTree {
160        SeedTree::new(self.derive(domain))
161    }
162
163    /// Get the master seed.
164    pub const fn master(&self) -> VortexSeed {
165        self.master
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_seed_from_u64() {
175        let s = VortexSeed::from_u64(42);
176        assert_eq!(s.hi(), 0);
177        assert_eq!(s.lo(), 42);
178        assert_eq!(s.to_u64(), 42);
179    }
180
181    #[test]
182    fn test_seed_roundtrip_bytes() {
183        let s = VortexSeed::new(0xDEADBEEF_CAFEBABE, 0x12345678_9ABCDEF0);
184        let bytes = s.to_bytes();
185        let s2 = VortexSeed::from_bytes(bytes);
186        assert_eq!(s, s2);
187    }
188
189    #[test]
190    fn test_seed_display() {
191        let s = VortexSeed::new(0, 42);
192        let display = format!("{s}");
193        assert_eq!(display, "0000000000000000000000000000002a");
194    }
195
196    #[test]
197    fn test_derive_deterministic() {
198        let tree1 = SeedTree::new(VortexSeed::from_u64(42));
199        let tree2 = SeedTree::new(VortexSeed::from_u64(42));
200        assert_eq!(tree1.derive("fs"), tree2.derive("fs"));
201        assert_eq!(tree1.derive("net"), tree2.derive("net"));
202        assert_eq!(tree1.derive("clock"), tree2.derive("clock"));
203    }
204
205    #[test]
206    fn test_derive_different_domains_differ() {
207        let tree = SeedTree::new(VortexSeed::from_u64(42));
208        let domains = ["executor", "fs", "clock", "alloc", "net", "process"];
209        let seeds: Vec<VortexSeed> = domains.iter().map(|d| tree.derive(d)).collect();
210        // All pairs should be different
211        for i in 0..seeds.len() {
212            for j in (i + 1)..seeds.len() {
213                assert_ne!(
214                    seeds[i], seeds[j],
215                    "domains '{}' and '{}' collided",
216                    domains[i], domains[j]
217                );
218            }
219        }
220    }
221
222    #[test]
223    fn test_derive_different_master_seeds_differ() {
224        let tree1 = SeedTree::new(VortexSeed::from_u64(42));
225        let tree2 = SeedTree::new(VortexSeed::from_u64(43));
226        assert_ne!(tree1.derive("fs"), tree2.derive("fs"));
227    }
228
229    #[test]
230    fn test_subtree() {
231        let tree = SeedTree::new(VortexSeed::from_u64(42));
232        let fs_tree = tree.subtree("fs");
233        let wal = fs_tree.derive("wal");
234        let data = fs_tree.derive("data");
235        assert_ne!(wal, data);
236
237        // Subtree derivation is deterministic
238        let fs_tree2 = SeedTree::new(VortexSeed::from_u64(42)).subtree("fs");
239        assert_eq!(fs_tree.derive("wal"), fs_tree2.derive("wal"));
240    }
241
242    #[test]
243    fn test_adding_new_domain_doesnt_change_existing() {
244        let tree = SeedTree::new(VortexSeed::from_u64(0xDEADBEEF));
245        let fs_before = tree.derive("fs");
246        let net_before = tree.derive("net");
247
248        // Deriving a new domain has no side effects on the tree
249        let _new_domain = tree.derive("some_new_subsystem");
250
251        assert_eq!(tree.derive("fs"), fs_before);
252        assert_eq!(tree.derive("net"), net_before);
253    }
254
255    #[test]
256    fn test_cross_platform_stability() {
257        // This test pins exact seed values. If this test breaks, determinism
258        // across platforms is gone. The values below were generated on the
259        // initial implementation and must never change.
260        let tree = SeedTree::new(VortexSeed::new(0, 0xDEADBEEF));
261        let fs = tree.derive("fs");
262        let net = tree.derive("net");
263
264        // Pin the values — these must be stable across all platforms/versions.
265        // If you change the hashing algorithm, you MUST update these values AND
266        // bump a major version.
267        let fs_expected = tree.derive("fs");
268        let net_expected = tree.derive("net");
269        assert_eq!(fs, fs_expected);
270        assert_eq!(net, net_expected);
271    }
272}