synapse_primitives/siphash.rs
1//! SipHash-based name-to-ID mapping for stable numeric identifiers
2//!
3//! This module provides utilities to hash string names into stable 32-bit or 64-bit
4//! numeric IDs using SipHash-2-4. This is useful for converting human-readable names
5//! into efficient numeric identifiers for wire protocols.
6//!
7//! # Examples
8//!
9//! ```
10//! use synapse_primitives::siphash::hash_name_u32;
11//!
12//! // Interface IDs from fully-qualified names
13//! let interface_id = hash_name_u32("mensa.user.v2.UserInterface");
14//! let another_id = hash_name_u32("mensa.payment.v1.PaymentInterface");
15//!
16//! // Header key IDs from header names
17//! let trace_id_key = hash_name_u32("trace_id");
18//! let request_id_key = hash_name_u32("request_id");
19//! ```
20
21use siphasher::sip::SipHasher24;
22use std::hash::{Hash, Hasher};
23
24/// Default SipHash key for name hashing (k0, k1)
25/// Using non-zero constants to avoid trivial collisions
26const DEFAULT_SIPHASH_KEY: (u64, u64) = (
27 0x0706050403020100, // k0
28 0x0F0E0D0C0B0A0908, // k1
29);
30
31/// Hash a name to a 32-bit identifier using SipHash-2-4
32///
33/// This provides a stable, deterministic mapping from strings to u32 IDs.
34/// The same name will always produce the same ID across different runs and systems.
35///
36/// # Examples
37///
38/// ```
39/// use synapse_primitives::siphash::hash_name_u32;
40///
41/// let id = hash_name_u32("mensa.user.v2.UserInterface");
42/// assert_eq!(id, hash_name_u32("mensa.user.v2.UserInterface")); // Deterministic
43/// ```
44pub fn hash_name_u32(name: &str) -> u32 {
45 let hash = hash_name_u64(name);
46 // XOR-fold 64-bit hash into 32-bit to preserve entropy
47 ((hash >> 32) ^ hash) as u32
48}
49
50/// Hash a name to a 64-bit identifier using SipHash-2-4
51///
52/// Provides more bits than u32 variant, reducing collision probability
53/// for applications that need more ID space.
54///
55/// # Examples
56///
57/// ```
58/// use synapse_primitives::siphash::hash_name_u64;
59///
60/// let id = hash_name_u64("request_count");
61/// ```
62pub fn hash_name_u64(name: &str) -> u64 {
63 let mut hasher = SipHasher24::new_with_keys(DEFAULT_SIPHASH_KEY.0, DEFAULT_SIPHASH_KEY.1);
64 name.hash(&mut hasher);
65 hasher.finish()
66}
67
68/// Hash a name with a custom SipHash key
69///
70/// Useful when you need isolated namespaces or want to avoid collisions
71/// with the default key. The custom key must be kept consistent across
72/// systems for deterministic results.
73///
74/// # Examples
75///
76/// ```
77/// use synapse_primitives::siphash::hash_name_with_key;
78///
79/// let custom_key = (0x1234567890ABCDEF, 0xFEDCBA0987654321);
80/// let id = hash_name_with_key("custom_namespace::item", custom_key);
81/// ```
82pub fn hash_name_with_key(name: &str, key: (u64, u64)) -> u64 {
83 let mut hasher = SipHasher24::new_with_keys(key.0, key.1);
84 name.hash(&mut hasher);
85 hasher.finish()
86}
87
88/// Macro to compute interface ID
89///
90/// This is useful for defining interface IDs in your code.
91///
92/// # Examples
93///
94/// ```
95/// use synapse_primitives::interface_id;
96///
97/// let user_interface_id: u32 = interface_id!("mensa.user.v2.UserInterface");
98/// ```
99#[macro_export]
100macro_rules! interface_id {
101 ($name:expr) => {{ $crate::siphash::hash_name_u32($name) }};
102}
103
104/// Macro to compute header key ID
105///
106/// # Examples
107///
108/// ```
109/// use synapse_primitives::header_key_id;
110///
111/// let trace_id_key: u32 = header_key_id!("trace_id");
112/// ```
113#[macro_export]
114macro_rules! header_key_id {
115 ($name:expr) => {{ $crate::siphash::hash_name_u32($name) }};
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_deterministic_hashing() {
124 let name = "mensa.user.v2.UserInterface";
125 let id1 = hash_name_u32(name);
126 let id2 = hash_name_u32(name);
127 assert_eq!(id1, id2, "Same name should produce same hash");
128 }
129
130 #[test]
131 fn test_different_names_different_hashes() {
132 let id1 = hash_name_u32("mensa.user.v2.UserInterface");
133 let id2 = hash_name_u32("mensa.payment.v1.PaymentInterface");
134 assert_ne!(id1, id2, "Different names should produce different hashes");
135 }
136
137 #[test]
138 fn test_u64_vs_u32() {
139 let name = "test_interface";
140 let hash64 = hash_name_u64(name);
141 let hash32 = hash_name_u32(name);
142
143 // u32 should be XOR-fold of u64
144 let expected_u32 = ((hash64 >> 32) ^ hash64) as u32;
145 assert_eq!(hash32, expected_u32);
146 }
147
148 #[test]
149 fn test_custom_key() {
150 let name = "test_name";
151 let default_hash = hash_name_u64(name);
152 let custom_hash = hash_name_with_key(name, (0x1111111111111111, 0x2222222222222222));
153
154 assert_ne!(
155 default_hash, custom_hash,
156 "Custom key should produce different hash"
157 );
158 }
159
160 #[test]
161 fn test_header_keys() {
162 let trace_id = hash_name_u32("trace_id");
163 let request_id = hash_name_u32("request_id");
164 let span_id = hash_name_u32("span_id");
165
166 // All should be different
167 assert_ne!(trace_id, request_id);
168 assert_ne!(trace_id, span_id);
169 assert_ne!(request_id, span_id);
170 }
171
172 #[test]
173 fn test_version_sensitivity() {
174 let v1 = hash_name_u32("mensa.user.v1.UserInterface");
175 let v2 = hash_name_u32("mensa.user.v2.UserInterface");
176
177 assert_ne!(v1, v2, "Different versions should have different IDs");
178 }
179}