Skip to main content

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}