Skip to main content

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}