metrale_storage/tier/
hash.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Hash primitives for cache keys that outlive the process: FNV-1a/64
4//! and the `mix64` fold.
5//!
6//! The keys land in the paging peer's swap file, which outlives the client that
7//! wrote them, so the same input must give the same `u64` across toolchains,
8//! platforms and rebuilds. Hence both functions are written out here over fixed
9//! constants instead of coming from `std::hash` or a crate. The users import
10//! them from here: model-engine's SSM tier (`ssm_tier/fingerprint.rs`, and the
11//! wire key in `ssm_tier/arena_store.rs`) and the KV paging namespace
12//! (`kv_paging/ns.rs`).
13//!
14//! FNV-1a is not collision-resistant: each step is an XOR and a multiply by an
15//! odd constant, both invertible, so colliding inputs can be constructed. The
16//! keys separate the configs of one trusted fleet; they are no boundary between
17//! tenants that choose their own configs.
18//!
19//! Owner: storage (tier).
20//! Invariants: `fnv1a_64` and `mix64` are pure `const fn`s of their inputs.
21
22/// 2026-09-25: FNV-1a/64 offset basis.
23pub const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
24/// 2026-09-25: FNV-1a/64 prime.
25pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
26
27const GOLDEN: u64 = 0x9E37_79B9_7F4A_7C15;
28
29/// 2026-09-25: FNV-1a/64 over `bytes`. It reads bytes, not words, so the result
30/// does not depend on endianness.
31pub const fn fnv1a_64(bytes: &[u8]) -> u64 {
32    let mut h = FNV_OFFSET;
33    let mut i = 0;
34    while i < bytes.len() {
35        h = (h ^ bytes[i] as u64).wrapping_mul(FNV_PRIME);
36        i += 1;
37    }
38    h
39}
40
41/// 2026-09-25: The splitmix64 finalizer over `a ^ b * GOLDEN`.
42///
43/// The fold behind the persisted wire keys: model-engine's
44/// `PagingSnapshotStore::wire(key)` is `mix64(key, namespace)`, and the decode
45/// namespace folds `mix64(fingerprint, DECODE_DOMAIN)` with the client salt.
46/// A change to it changes every stored key.
47pub const fn mix64(a: u64, b: u64) -> u64 {
48    let mut h = a ^ b.wrapping_mul(GOLDEN);
49    h ^= h >> 30;
50    h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
51    h ^= h >> 27;
52    h = h.wrapping_mul(0x94D0_49BB_1331_11EB);
53    h ^ (h >> 31)
54}
55
56#[cfg(test)]
57#[path = "hash_tests.rs"]
58mod tests;