use std::hash::{Hash, Hasher};
const FNV_OFFSET_A: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_OFFSET_B: u64 = 0x9ae1_6a3b_2f90_404f;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fnv1a(bytes: &[u8], offset: u64) -> u64 {
let mut hash = offset;
for b in bytes {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct ContentDigest(u128);
impl ContentDigest {
#[must_use]
pub fn of_bytes(bytes: &[u8]) -> Self {
Self(u128::from(fnv1a(bytes, FNV_OFFSET_A)) << 64 | u128::from(fnv1a(bytes, FNV_OFFSET_B)))
}
#[must_use]
pub const fn value(self) -> u128 {
self.0
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
}
impl std::fmt::Display for ContentDigest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:032x}", self.0)
}
}
pub trait IntegrityCheck {
fn content_digest(&self) -> ContentDigest;
}
struct SeededFnv {
state: u64,
}
impl SeededFnv {
const fn new(seed: u64) -> Self {
Self { state: seed }
}
}
impl std::hash::Hasher for SeededFnv {
fn finish(&self) -> u64 {
self.state
}
fn write(&mut self, bytes: &[u8]) {
for b in bytes {
self.state ^= u64::from(*b);
self.state = self.state.wrapping_mul(FNV_PRIME);
}
}
}
impl<T: Hash + ?Sized> IntegrityCheck for T {
fn content_digest(&self) -> ContentDigest {
let mut a = SeededFnv::new(FNV_OFFSET_A);
self.hash(&mut a);
let mut b = SeededFnv::new(FNV_OFFSET_B);
self.hash(&mut b);
ContentDigest::from_hashes(a.finish(), b.finish())
}
}
impl ContentDigest {
fn from_hashes(a: u64, b: u64) -> Self {
Self(u128::from(a) << 64 | u128::from(b))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct KeyFingerprint(u128);
impl KeyFingerprint {
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
let mut v = Self(
u128::from(fnv1a(bytes, FNV_OFFSET_A)) << 64 | u128::from(fnv1a(bytes, FNV_OFFSET_B)),
);
v.0 ^= u128::from(bytes.len() as u64).wrapping_mul(u128::from(FNV_PRIME));
v
}
#[must_use]
pub const fn value(self) -> u128 {
self.0
}
}
impl std::fmt::Display for KeyFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:032x}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct KeyAddress {
fingerprint: KeyFingerprint,
placement: u64,
}
impl KeyAddress {
#[must_use]
pub fn of(key: &[u8], placement_strategy: Placement) -> Self {
Self {
fingerprint: KeyFingerprint::of(key),
placement: placement_strategy.hash(key),
}
}
#[must_use]
pub const fn fingerprint(self) -> KeyFingerprint {
self.fingerprint
}
#[must_use]
pub const fn placement(self) -> u64 {
self.placement
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Placement {
#[default]
Default,
AllToZero,
CollidingPair { prefix: &'static [u8] },
}
impl Placement {
#[must_use]
pub fn hash(self, bytes: &[u8]) -> u64 {
match self {
Self::Default => fnv1a(bytes, FNV_OFFSET_A),
Self::AllToZero => 0,
Self::CollidingPair { prefix } => {
if bytes.starts_with(prefix) {
42
} else {
(bytes.len() as u64) ^ 0xdead_beef_cafe_f00d
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_distinguishes_values() {
assert_ne!(
ContentDigest::of_bytes(b"alpha"),
ContentDigest::of_bytes(b"beta")
);
assert_eq!(
ContentDigest::of_bytes(b"alpha"),
ContentDigest::of_bytes(b"alpha")
);
}
#[test]
fn digest_notices_a_single_flipped_bit() {
let a = ContentDigest::of_bytes(b"order-1234");
let b = ContentDigest::of_bytes(b"order-1235");
assert_ne!(a, b);
}
#[test]
fn digest_of_empty_is_stable() {
let d = ContentDigest::of_bytes(b"");
assert!(!d.is_zero());
assert_eq!(d, ContentDigest::of_bytes(b""));
}
#[test]
fn fingerprint_mixes_length() {
assert_ne!(KeyFingerprint::of(b"ab"), KeyFingerprint::of(b"ab\x00"));
}
#[test]
fn fingerprint_distinguishes_adjacent_keys() {
let mut seen = std::collections::HashSet::new();
for i in 0..10_000_u32 {
assert!(
seen.insert(KeyFingerprint::of(&i.to_le_bytes())),
"fingerprint collision at {i} in 10k keys"
);
}
}
#[test]
fn blanket_digest_matches_for_equal_values() {
let a = String::from("hello");
let b = String::from("hello");
assert_eq!(a.content_digest(), b.content_digest());
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = vec![1, 2, 3];
assert_eq!(v.content_digest(), w.content_digest());
}
#[derive(Default)]
struct StreamRecorder {
bytes: Vec<u8>,
}
impl std::hash::Hasher for StreamRecorder {
fn finish(&self) -> u64 {
0
}
fn write(&mut self, bytes: &[u8]) {
self.bytes.extend_from_slice(bytes);
}
}
#[test]
fn blanket_digest_is_two_seeded_fnv_lanes_over_the_hash_stream() {
testkit::proves!("cia.integrity.digest_is_cryptographic");
for i in 0..1_000_u32 {
let value = i.to_le_bytes();
let mut recorder = StreamRecorder::default();
value.as_slice().hash(&mut recorder);
let stream = &recorder.bytes;
let expected = ContentDigest::from_hashes(
fnv1a(stream, FNV_OFFSET_A),
fnv1a(stream, FNV_OFFSET_B),
);
assert_eq!(
value.as_slice().content_digest(),
expected,
"the blanket digest is not two seeded FNV lanes over the Hash stream \
(input {i})"
);
}
}
#[test]
fn blanket_digest_lanes_are_not_a_rotate_and_xor() {
for i in 0..1_000_u32 {
let d = i.to_le_bytes().as_slice().content_digest();
let high = (d.value() >> 64) as u64;
let low = d.value() as u64;
assert_ne!(
low,
high ^ high.rotate_left(32),
"the lanes are the old rotate-and-xor of a single hash ({i})"
);
}
}
#[test]
fn blanket_digest_uses_both_lanes_across_a_corpus() {
let mut highs = std::collections::HashSet::new();
let mut lows = std::collections::HashSet::new();
let mut digests = std::collections::HashSet::new();
for i in 0..5_000_u32 {
let d = i.to_le_bytes().as_slice().content_digest();
highs.insert((d.value() >> 64) as u64);
lows.insert(d.value() as u64);
digests.insert(d);
}
assert_eq!(digests.len(), 5_000, "digest collision across 5k values");
assert!(
highs.len() > 4_000 && lows.len() > 4_000,
"a lane is degenerate: highs={} lows={}",
highs.len(),
lows.len()
);
}
#[test]
fn seeded_hasher_matches_the_byte_lane_it_mirrors() {
testkit::proves!("cia.integrity.digest");
let mut hasher = SeededFnv::new(FNV_OFFSET_A);
hasher.write(b"payload");
assert_eq!(hasher.finish(), fnv1a(b"payload", FNV_OFFSET_A));
}
#[test]
fn placement_strategies_really_collide() {
assert_eq!(
Placement::AllToZero.hash(b"a"),
Placement::AllToZero.hash(b"b")
);
let p = Placement::CollidingPair { prefix: b"k" };
assert_eq!(p.hash(b"k1"), p.hash(b"k2"));
assert_ne!(p.hash(b"k1"), p.hash(b"other"));
assert_ne!(Placement::Default.hash(b"a"), Placement::Default.hash(b"b"));
}
#[test]
fn digest_display_is_fixed_width() {
assert_eq!(ContentDigest::of_bytes(b"x").to_string().len(), 32);
assert_eq!(KeyFingerprint::of(b"x").to_string().len(), 32);
}
}