nitro_hash/lib.rs
1//! # nitro_hash
2//!
3//! A lightweight algebraic fingerprinting / hash system based on
4//! polynomial interpolation over a finite field.
5//!
6//! ## Overview
7//!
8//! This crate builds a configurable hash by:
9//! - Interpreting input as field elements
10//! - Constructing a randomized polynomial
11//! - Evaluating via Lagrange interpolation
12//! - Encoding outputs as bytes
13//!
14//! ## Output sizes
15//!
16//! Each configuration uses `K` evaluation points:
17//!
18//! - 2 → 122-bit hash
19//! - 3 → 183-bit hash
20//! - 4 → 244-bit hash
21//! - 5 → 305-bit hash
22//!
23
24#[derive(Copy, Clone)]
25/// Configuration for the hashing system.
26///
27/// Contains the modulus used for finite field arithmetic.
28/// The default is a 61-bit Mersenne prime.
29pub struct HashConfig {
30 /// Prime modulus used for all field operations.
31 pub salt: u64,
32 p: u64
33}
34
35/// Converts a vector of `u64` field elements into raw bytes.
36///
37/// Each `u64` is encoded in little-endian format (8 bytes).
38///
39/// # Arguments
40/// - `data`: field elements to serialize
41///
42/// # Returns
43/// A byte vector of length `8 * data.len()`.
44pub fn to_vec_u8(data: &[u64]) -> Vec<u8> {
45 let mut v: Vec<u8> = vec![];
46
47 for val in data {
48 v.extend_from_slice(&val.to_le_bytes());
49 }
50
51 v
52}
53
54/// SplitMix64 PRNG.
55///
56/// Used to generate pseudo-random evaluation points for interpolation.
57///
58/// This is a fast, high-quality integer mixer.
59fn splitmix64(mut x: u64) -> u64 {
60 x = x.wrapping_add(0x9e3779b97f4a7c15);
61 x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
62 x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
63 x ^ (x >> 31)
64}
65
66/// Modular exponentiation: computes `a^e mod m`.
67///
68/// Used internally for computing modular inverses via Fermat's little theorem.
69///
70/// # Panics
71/// None (safe under overflow using `u128` intermediates).
72fn pow_mod(mut a: u64, mut e: u64, m: u64) -> u64 {
73 let mut r: u64 = 1;
74
75 while e > 0 {
76 if e & 1 == 1 {
77 r = ((r as u128 * a as u128) % m as u128) as u64;
78 }
79 a = ((a as u128 * a as u128) % m as u128) as u64;
80 e >>= 1;
81 }
82
83 r
84}
85
86/// Computes modular multiplicative inverse in a prime field.
87///
88/// Uses Fermat's little theorem:
89/// ```text
90/// a⁻¹ ≡ a^(p−2) mod p
91/// ```
92fn inverse_mod(a: u64, p: u64) -> u64 {
93 pow_mod(a, p - 2, p)
94}
95
96fn seed_from_data(data: &[u64], salt: u64) -> u64 {
97 let mut seed: u64 = 0x9e3779b97f4a7c15 ^ salt; // golden ratio constant
98
99 for &x in data {
100 seed = seed.wrapping_add(x);
101 seed = splitmix64(seed);
102 }
103
104 seed
105}
106
107/// Performs Lagrange interpolation over a finite field.
108///
109/// Evaluates the interpolating polynomial defined by `points`
110/// at multiple evaluation points.
111///
112/// # Arguments
113/// - `points`: (x, y) samples defining the polynomial
114/// - `x`: evaluation points
115/// - `k`: number of evaluation outputs used
116/// - `p`: modulus of the finite field
117///
118/// # Returns
119/// A vector of evaluated polynomial values in `[0, p)`.
120fn interpolate(
121 points: Vec<(u64, u64)>,
122 x: [u64; 5],
123 k: u8,
124 p: u64
125) -> [u64; 5] {
126 let mut evals: [u64; 5] = [0; 5];
127
128 for (idx, x_val) in x.iter().enumerate() {
129 if idx > k as usize {
130 break;
131 }
132
133 let mut eval: u128 = 0;
134
135 for point in points.iter() {
136 let mut basis: u128 = point.1 as u128;
137
138 for point_ in points.iter() {
139 if point_ == point {
140 continue;
141 }
142
143 basis *= ((p as i128 + *x_val as i128 - point.0 as i128) % p as i128) as u128;
144 basis %= p as u128;
145
146 basis *= inverse_mod(
147 ((point_.0 as i128 - point.0 as i128) % p as i128) as u64,
148 p
149 ) as u128;
150 basis %= p as u128;
151 }
152
153 eval = (eval + basis) % p as u128;
154 }
155
156 evals[idx] = eval as u64;
157 }
158
159 evals
160}
161
162/// Converts input data into `(index, value)` pairs.
163///
164/// Each `u64` is treated as a coefficient in the interpolation domain.
165fn x_vals(data: &Vec<u64>) -> Vec<(u64, u64)> {
166 data.iter()
167 .enumerate()
168 .map(|(i, y)| (i as u64, *y))
169 .collect()
170}
171
172/// Generates deterministic pseudo-random evaluation points.
173///
174/// The points depend on:
175/// - input data
176/// - hash strength `K`
177/// - modulus `p`
178fn x_vec(data: &Vec<u64>, k: u8, p: u64, salt: u64) -> [u64; 5] {
179 let seed = seed_from_data(&data[0..], salt);
180 let mut out: [u64; 5] = [0; 5];
181
182 out[0] = splitmix64(seed) % p;
183 out[1] = splitmix64(out[0]) % p;
184 out[2] = splitmix64(out[1]) % p;
185
186 if k >= 4 {out[3] = splitmix64(out[2]) % p;}
187 if k == 5 {out[4] = splitmix64(out[3]) % p;}
188
189 out
190}
191
192/// Converts a 32-byte array into a hex string.
193pub fn hex(bytes: Vec<u8>) -> String {
194 bytes.iter().map(|b| format!("{:02x}", b)).collect()
195}
196
197/// Converts a string into a vector of `u64` blocks.
198///
199/// Each block contains up to 8 bytes in little-endian format.
200pub fn str_to_vec_i64(s: &str) -> Vec<u64> {
201 let bytes = s.as_bytes();
202 let mut out = Vec::with_capacity((bytes.len() + 7) / 8);
203
204 let mut i = 0;
205 while i < bytes.len() {
206 let mut arr = [0u8; 8];
207 let end = (i + 8).min(bytes.len());
208
209 arr[..end - i].copy_from_slice(&bytes[i..end]);
210
211 out.push(u64::from_le_bytes(arr));
212 i += 8;
213 }
214
215 out
216}
217
218impl HashConfig {
219 /// Internal generic hash function.
220 ///
221 /// Computes a polynomial fingerprint of the input using `K` evaluation points.
222 ///
223 /// # Note
224 /// Returns raw byte representation of field outputs.
225 pub fn _hash<const K: usize>(self, s: &Vec<u64>) -> [u64; 5] {
226 interpolate(
227 x_vals(s),
228 x_vec(s, K as u8, self.p, self.salt),
229 K as u8,
230 self.p
231 )
232 }
233
234 /// 122-bit hash (K = 2 evaluations & very small entropy for real usage)
235 pub fn hash122(self, s: &Vec<u64>) -> Vec<u8> {
236 to_vec_u8(&self._hash::<4>(s)[0..2])
237 }
238
239 /// 183-bit hash (K = 3 evaluations & recommended using for better performance)
240 pub fn hash183(self, s: &Vec<u64>) -> Vec<u8> {
241 to_vec_u8(&self._hash::<4>(s)[0..3])
242 }
243
244 /// 244-bit hash (K = 4 evaluations & recommended for most usages)
245 pub fn hash244(self, s: &Vec<u64>) -> Vec<u8> {
246 to_vec_u8(&self._hash::<4>(s)[0..4])
247 }
248
249 /// 305-bit hash (K = 5 evaluations & not recommended for most usages)
250 pub fn hash305(self, s: &Vec<u64>) -> Vec<u8> {
251 to_vec_u8(&self._hash::<4>(s))
252 }
253
254 /// Creates a new hash configuration using a 61-bit Mersenne prime field.
255 pub fn new(salt: u64) -> HashConfig {
256 HashConfig {
257 p: (1 << 61) - 1,
258 salt
259 }
260 }
261}