laser_wire/hashing.rs
1// The one content-hash every SDK shares, so a content-addressed id (a deduped
2// memory item, a converged graph node) is identical across languages. Pure and
3// dependency-free FNV-1a, no clock or entropy (id GENERATION lives SDK-side. This
4// is deterministic hashing, which belongs in the contract). A port reproduces it
5// from the same byte segments and the fixture pins the result.
6
7const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
8const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
9
10fn fnv1a(salt: u8, segments: &[&[u8]]) -> u64 {
11 let mut hash = FNV_OFFSET;
12 hash ^= u64::from(salt);
13 hash = hash.wrapping_mul(FNV_PRIME);
14 for segment in segments {
15 for &byte in *segment {
16 hash ^= u64::from(byte);
17 hash = hash.wrapping_mul(FNV_PRIME);
18 }
19 }
20 hash
21}
22
23/// A deterministic 128-bit content id over `segments`, the concatenation of which
24/// is the content being addressed. Two independently salted FNV-1a passes fill
25/// the high and low halves, so the same segments always yield the same id and a
26/// different input yields a different one. The caller assembles the segments
27/// (e.g. an owner, a discriminator byte, and a body) and the order is part of the
28/// contract.
29pub fn content_id(segments: &[&[u8]]) -> u128 {
30 (u128::from(fnv1a(0x4d, segments)) << 64) | u128::from(fnv1a(0xc7, segments))
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36
37 #[test]
38 fn given_the_same_segments_when_hashed_then_should_be_stable() {
39 let a = content_id(&[b"owner", &[1], b"body"]);
40 let b = content_id(&[b"owner", &[1], b"body"]);
41 assert_eq!(a, b);
42 }
43
44 #[test]
45 fn given_different_segments_when_hashed_then_should_differ() {
46 assert_ne!(content_id(&[b"a"]), content_id(&[b"b"]));
47 }
48
49 #[test]
50 fn given_the_pinned_memory_segments_when_hashed_then_should_match_the_golden_id() {
51 // The cross-SDK golden vector: an empty stream segment, agent "agent", a
52 // separator, the Fact kind byte (1), and body "x". The SDK and the Python
53 // reference both render this u128 as the ULID below, so the dedup id agrees
54 // across languages. Rendered with the crate's Crockford encoder, the same
55 // one every wire id displays through.
56 let id = content_id(&[&[0], b"agent", &[0], &[1], b"x"]);
57 let rendered = crate::agent::RecordId::from_u128(id).to_string();
58 assert_eq!(rendered, "1A9GVS6SJ6SNS4KY0H19130WCW");
59 }
60}