Skip to main content

cyber_hemera/
sponge.rs

1use std::fmt;
2use std::io;
3
4use crate::encoding::{bytes_to_rate_block, hash_to_bytes};
5use crate::field::Goldilocks;
6use crate::params::{self, OUTPUT_BYTES, OUTPUT_BYTES_PER_ELEMENT, OUTPUT_ELEMENTS, RATE, RATE_BYTES, WIDTH};
7
8/// Domain separation tags placed in `state[capacity_start + 3]` (i.e. `state[11]`).
9const DOMAIN_HASH: u64 = 0x00;
10const DOMAIN_KEYED: u64 = 0x01;
11const DOMAIN_DERIVE_KEY_CONTEXT: u64 = 0x02;
12const DOMAIN_DERIVE_KEY_MATERIAL: u64 = 0x03;
13
14/// Index where the capacity region starts (after the rate region).
15const CAPACITY_START: usize = RATE; // 8
16
17/// A 64-byte Poseidon2 hash output (Hemera: 8 Goldilocks elements).
18#[derive(Clone, Copy, PartialEq, Eq, Hash)]
19pub struct Hash([u8; OUTPUT_BYTES]);
20
21impl Hash {
22    /// Create a hash from a raw byte array.
23    pub const fn from_bytes(bytes: [u8; OUTPUT_BYTES]) -> Self {
24        Self(bytes)
25    }
26
27    /// Return the hash as a byte slice.
28    pub fn as_bytes(&self) -> &[u8; OUTPUT_BYTES] {
29        &self.0
30    }
31
32    /// Convert the hash to a hex string.
33    pub fn to_hex(&self) -> String {
34        let mut s = String::with_capacity(OUTPUT_BYTES * 2);
35        for byte in &self.0 {
36            use fmt::Write;
37            write!(s, "{byte:02x}").unwrap();
38        }
39        s
40    }
41}
42
43impl From<[u8; OUTPUT_BYTES]> for Hash {
44    fn from(bytes: [u8; OUTPUT_BYTES]) -> Self {
45        Self(bytes)
46    }
47}
48
49#[cfg(feature = "serde")]
50impl serde::Serialize for Hash {
51    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
52        use serde::ser::SerializeTuple;
53        let mut seq = serializer.serialize_tuple(OUTPUT_BYTES)?;
54        for byte in &self.0 {
55            seq.serialize_element(byte)?;
56        }
57        seq.end()
58    }
59}
60
61#[cfg(feature = "serde")]
62impl<'de> serde::Deserialize<'de> for Hash {
63    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
64        struct HashVisitor;
65        impl<'de> serde::de::Visitor<'de> for HashVisitor {
66            type Value = Hash;
67
68            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
69                write!(formatter, "a byte array of length {OUTPUT_BYTES}")
70            }
71
72            fn visit_seq<A: serde::de::SeqAccess<'de>>(
73                self,
74                mut seq: A,
75            ) -> Result<Hash, A::Error> {
76                let mut bytes = [0u8; OUTPUT_BYTES];
77                for (i, byte) in bytes.iter_mut().enumerate() {
78                    *byte = seq
79                        .next_element()?
80                        .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
81                }
82                Ok(Hash(bytes))
83            }
84        }
85        deserializer.deserialize_tuple(OUTPUT_BYTES, HashVisitor)
86    }
87}
88
89impl AsRef<[u8]> for Hash {
90    fn as_ref(&self) -> &[u8] {
91        &self.0
92    }
93}
94
95impl fmt::Display for Hash {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        for byte in &self.0 {
98            write!(f, "{byte:02x}")?;
99        }
100        Ok(())
101    }
102}
103
104impl fmt::Debug for Hash {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(f, "Hash({self})")
107    }
108}
109
110/// A streaming Poseidon2 hasher.
111///
112/// Supports three modes via domain separation:
113/// - Plain hash (`new`)
114/// - Keyed hash (`new_keyed`)
115/// - Key derivation (`new_derive_key`)
116///
117/// Data is absorbed in 56-byte blocks (8 Goldilocks elements × 7 bytes each).
118#[derive(Clone)]
119pub struct Hasher {
120    state: [Goldilocks; WIDTH],
121    buf: Vec<u8>,
122    absorbed: u64,
123}
124
125impl Hasher {
126    /// Create a new hasher in plain hash mode.
127    pub fn new() -> Self {
128        let mut state = [Goldilocks::new(0); WIDTH];
129        state[CAPACITY_START + 3] = Goldilocks::new(DOMAIN_HASH);
130        Self {
131            state,
132            buf: Vec::new(),
133            absorbed: 0,
134        }
135    }
136
137    /// Create a new hasher in keyed hash mode.
138    ///
139    /// The key is absorbed as the first block (before any user data).
140    pub fn new_keyed(key: &[u8; OUTPUT_BYTES]) -> Self {
141        let mut state = [Goldilocks::new(0); WIDTH];
142        state[CAPACITY_START + 3] = Goldilocks::new(DOMAIN_KEYED);
143
144        // Absorb the key into the rate portion via the normal buffer path.
145        let mut hasher = Self {
146            state,
147            buf: Vec::new(),
148            absorbed: 0,
149        };
150        hasher.update(key.as_slice());
151        hasher
152    }
153
154    /// Create a new hasher in derive-key mode.
155    ///
156    /// First hashes the context string to produce a context key, then
157    /// sets up a second hasher seeded with that key for absorbing key material.
158    pub(crate) fn new_derive_key_context(context: &str) -> Self {
159        let mut state = [Goldilocks::new(0); WIDTH];
160        state[CAPACITY_START + 3] = Goldilocks::new(DOMAIN_DERIVE_KEY_CONTEXT);
161        let mut hasher = Self {
162            state,
163            buf: Vec::new(),
164            absorbed: 0,
165        };
166        hasher.update(context.as_bytes());
167        hasher
168    }
169
170    /// Create a derive-key hasher for the material phase, seeded by a context hash.
171    pub(crate) fn new_derive_key_material(context_hash: &Hash) -> Self {
172        let mut state = [Goldilocks::new(0); WIDTH];
173        state[CAPACITY_START + 3] = Goldilocks::new(DOMAIN_DERIVE_KEY_MATERIAL);
174
175        // Seed the rate portion with the context hash (8 elements = 64 bytes).
176        for (i, chunk) in context_hash.0.chunks(OUTPUT_BYTES_PER_ELEMENT).enumerate() {
177            let val = u64::from_le_bytes(chunk.try_into().unwrap());
178            state[i] = Goldilocks::new(val);
179        }
180        params::permute(&mut state);
181
182        Self {
183            state,
184            buf: Vec::new(),
185            absorbed: 0,
186        }
187    }
188
189    /// Absorb input data into the sponge.
190    pub fn update(&mut self, data: &[u8]) -> &mut Self {
191        self.buf.extend_from_slice(data);
192        self.absorbed += data.len() as u64;
193
194        // Process complete rate blocks.
195        while self.buf.len() >= RATE_BYTES {
196            let block_bytes: Vec<u8> = self.buf.drain(..RATE_BYTES).collect();
197            let mut rate_block = [Goldilocks::new(0); RATE];
198            bytes_to_rate_block(&block_bytes, &mut rate_block);
199            self.absorb_block(&rate_block);
200        }
201
202        self
203    }
204
205    /// Add a rate block into the state (Goldilocks field addition) and permute.
206    fn absorb_block(&mut self, block: &[Goldilocks; RATE]) {
207        for (i, block_elem) in block.iter().enumerate() {
208            self.state[i] = self.state[i] + *block_elem;
209        }
210        params::permute(&mut self.state);
211    }
212
213    /// Apply padding and produce the finalized state.
214    ///
215    /// Padding scheme (Hemera: 0x01 || 0x00*):
216    /// 1. Append 0x01 byte to remaining buffer
217    /// 2. Pad to RATE_BYTES with zeros
218    /// 3. Encode as field elements and absorb
219    /// 4. Store total byte count in capacity[2]
220    fn finalize_state(&self) -> [Goldilocks; WIDTH] {
221        let mut state = self.state;
222        let mut padded = self.buf.clone();
223
224        // Append padding marker (Hemera: 0x01).
225        padded.push(0x01);
226
227        // Pad to full rate block.
228        padded.resize(RATE_BYTES, 0x00);
229
230        // Encode and absorb the final block (Goldilocks field addition).
231        let mut rate_block = [Goldilocks::new(0); RATE];
232        bytes_to_rate_block(&padded, &mut rate_block);
233        for i in 0..RATE {
234            state[i] = state[i] + rate_block[i];
235        }
236
237        // Encode total length in capacity.
238        state[CAPACITY_START + 2] = Goldilocks::new(self.absorbed);
239
240        params::permute(&mut state);
241        state
242    }
243
244    /// Finalize and return the hash.
245    pub fn finalize(&self) -> Hash {
246        let state = self.finalize_state();
247        let output: [Goldilocks; OUTPUT_ELEMENTS] = state[..OUTPUT_ELEMENTS]
248            .try_into()
249            .unwrap();
250        Hash(hash_to_bytes(&output))
251    }
252
253    /// Finalize and return an extendable output reader (XOF mode).
254    pub fn finalize_xof(&self) -> OutputReader {
255        let state = self.finalize_state();
256        OutputReader {
257            state,
258            buffer: [0u8; OUTPUT_BYTES],
259            buffer_pos: OUTPUT_BYTES, // empty — will squeeze on first read
260        }
261    }
262}
263
264impl Default for Hasher {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270impl fmt::Debug for Hasher {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        f.debug_struct("Hasher")
273            .field("absorbed", &self.absorbed)
274            .field("buffered", &self.buf.len())
275            .finish()
276    }
277}
278
279/// An extendable-output reader that can produce arbitrary-length output.
280///
281/// Operates by repeatedly squeezing OUTPUT_BYTES from the sponge state,
282/// then permuting to produce more output.
283pub struct OutputReader {
284    state: [Goldilocks; WIDTH],
285    buffer: [u8; OUTPUT_BYTES],
286    buffer_pos: usize,
287}
288
289impl OutputReader {
290    /// Fill the provided buffer with hash output bytes.
291    pub fn fill(&mut self, output: &mut [u8]) {
292        let mut written = 0;
293        while written < output.len() {
294            if self.buffer_pos >= OUTPUT_BYTES {
295                self.squeeze();
296            }
297            let available = OUTPUT_BYTES - self.buffer_pos;
298            let needed = output.len() - written;
299            let n = available.min(needed);
300            output[written..written + n]
301                .copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + n]);
302            self.buffer_pos += n;
303            written += n;
304        }
305    }
306
307    /// Squeeze one block of output from the sponge.
308    fn squeeze(&mut self) {
309        let output_elems: [Goldilocks; OUTPUT_ELEMENTS] = self.state[..OUTPUT_ELEMENTS]
310            .try_into()
311            .unwrap();
312        self.buffer = hash_to_bytes(&output_elems);
313        self.buffer_pos = 0;
314        params::permute(&mut self.state);
315    }
316}
317
318impl io::Read for OutputReader {
319    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
320        self.fill(buf);
321        Ok(buf.len())
322    }
323}
324
325impl fmt::Debug for OutputReader {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        f.debug_struct("OutputReader").finish_non_exhaustive()
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn hash_display_is_hex() {
337        let h = Hash([0xAB; OUTPUT_BYTES]);
338        let s = format!("{h}");
339        assert_eq!(s.len(), OUTPUT_BYTES * 2);
340        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
341    }
342
343    #[test]
344    fn empty_hash_is_not_zero() {
345        let h = Hasher::new().finalize();
346        assert_ne!(h.0, [0u8; OUTPUT_BYTES]);
347    }
348
349    #[test]
350    fn different_inputs_different_hashes() {
351        let h1 = Hasher::new().update(b"a").finalize();
352        let h2 = Hasher::new().update(b"b").finalize();
353        assert_ne!(h1, h2);
354    }
355
356    #[test]
357    fn streaming_consistency() {
358        let data = b"hello world, this is a test of streaming consistency!";
359        let one_shot = {
360            let mut h = Hasher::new();
361            h.update(data);
362            h.finalize()
363        };
364        let streamed = {
365            let mut h = Hasher::new();
366            h.update(&data[..5]);
367            h.update(&data[5..20]);
368            h.update(&data[20..]);
369            h.finalize()
370        };
371        assert_eq!(one_shot, streamed);
372    }
373
374    #[test]
375    fn streaming_across_rate_boundary() {
376        // 56 bytes = exactly one rate block, so 100 bytes crosses a boundary.
377        let data = vec![0x42u8; 100];
378        let one_shot = {
379            let mut h = Hasher::new();
380            h.update(&data);
381            h.finalize()
382        };
383        let byte_at_a_time = {
384            let mut h = Hasher::new();
385            for b in &data {
386                h.update(std::slice::from_ref(b));
387            }
388            h.finalize()
389        };
390        assert_eq!(one_shot, byte_at_a_time);
391    }
392
393    #[test]
394    fn domain_separation_hash_vs_keyed() {
395        let data = b"test data";
396        let plain = Hasher::new().update(data).finalize();
397        let keyed = Hasher::new_keyed(&[0u8; OUTPUT_BYTES]).update(data).finalize();
398        assert_ne!(plain, keyed);
399    }
400
401    #[test]
402    fn domain_separation_hash_vs_derive_key() {
403        let data = b"test material";
404        let plain = Hasher::new().update(data).finalize();
405        let ctx_hasher = Hasher::new_derive_key_context("test context");
406        let ctx_hash = ctx_hasher.finalize();
407        let derived = Hasher::new_derive_key_material(&ctx_hash)
408            .update(data)
409            .finalize();
410        assert_ne!(plain, derived);
411    }
412
413    #[test]
414    fn xof_first_32_match_finalize() {
415        let data = b"xof test";
416        let hash = Hasher::new().update(data).finalize();
417        let mut xof = Hasher::new().update(data).finalize_xof();
418        let mut xof_bytes = [0u8; OUTPUT_BYTES];
419        xof.fill(&mut xof_bytes);
420        assert_eq!(hash.as_bytes(), &xof_bytes);
421    }
422
423    #[test]
424    fn xof_produces_more_than_32_bytes() {
425        let mut xof = Hasher::new().update(b"xof").finalize_xof();
426        let mut out = [0u8; 128];
427        xof.fill(&mut out);
428        // Not all zeros.
429        assert_ne!(out, [0u8; 128]);
430        // Different 32-byte blocks (with overwhelming probability).
431        assert_ne!(out[..OUTPUT_BYTES], out[OUTPUT_BYTES..OUTPUT_BYTES * 2]);
432    }
433
434    #[test]
435    fn xof_read_trait() {
436        use std::io::Read;
437        let mut xof = Hasher::new().update(b"read trait").finalize_xof();
438        let mut buf = [0u8; 64];
439        let n = xof.read(&mut buf).unwrap();
440        assert_eq!(n, 64);
441    }
442
443    #[test]
444    fn keyed_hash_different_keys() {
445        let data = b"same data";
446        let h1 = Hasher::new_keyed(&[0u8; OUTPUT_BYTES]).update(data).finalize();
447        let h2 = Hasher::new_keyed(&[1u8; OUTPUT_BYTES]).update(data).finalize();
448        assert_ne!(h1, h2);
449    }
450
451    // ── Padding boundary tests ──────────────────────────────────────
452
453    #[test]
454    fn exact_rate_block_input() {
455        // 56 bytes = exactly one rate block. Padding adds a second full block.
456        let data = vec![0x42u8; RATE_BYTES];
457        let h = Hasher::new().update(&data).finalize();
458        assert_ne!(h.0, [0u8; OUTPUT_BYTES]);
459        // Streaming equivalence: split at byte 28
460        let h2 = {
461            let mut hasher = Hasher::new();
462            hasher.update(&data[..28]);
463            hasher.update(&data[28..]);
464            hasher.finalize()
465        };
466        assert_eq!(h, h2);
467    }
468
469    #[test]
470    fn exact_two_rate_blocks_input() {
471        // 112 bytes = exactly two rate blocks
472        let data = vec![0x42u8; RATE_BYTES * 2];
473        let h = Hasher::new().update(&data).finalize();
474        let h_streamed = {
475            let mut hasher = Hasher::new();
476            for chunk in data.chunks(17) { // odd chunk size
477                hasher.update(chunk);
478            }
479            hasher.finalize()
480        };
481        assert_eq!(h, h_streamed);
482    }
483
484    #[test]
485    fn one_less_than_rate_block() {
486        // 55 bytes: padding appends 0x01 to make exactly 56 bytes
487        let data = vec![0x42u8; RATE_BYTES - 1];
488        let h = Hasher::new().update(&data).finalize();
489        assert_ne!(h.0, [0u8; OUTPUT_BYTES]);
490    }
491
492    #[test]
493    fn one_more_than_rate_block() {
494        // 57 bytes: first 56 go to block 1, remaining 1 + padding = block 2
495        let data = vec![0x42u8; RATE_BYTES + 1];
496        let h = Hasher::new().update(&data).finalize();
497        assert_ne!(h.0, [0u8; OUTPUT_BYTES]);
498    }
499
500    // ── Clone consistency ──────────────────────────────────────────
501
502    #[test]
503    fn hasher_clone_produces_same_hash() {
504        let mut h1 = Hasher::new();
505        h1.update(b"some data");
506        let h2 = h1.clone();
507        h1.update(b" more");
508        let mut h3 = h1.clone();
509        h3.update(b"");
510        assert_eq!(h1.finalize(), h3.finalize());
511        // h2 diverged at "some data"
512        assert_ne!(h1.finalize(), h2.finalize());
513    }
514
515    #[test]
516    fn hasher_clone_mid_block() {
517        let mut h = Hasher::new();
518        h.update(&[0xAB; 30]); // mid-block (< 56)
519        let cloned = h.clone();
520        h.update(&[0xCD; 30]);
521        let mut cloned2 = cloned.clone();
522        cloned2.update(&[0xCD; 30]);
523        assert_eq!(h.finalize(), cloned2.finalize());
524    }
525
526    // ── XOF tests ──────────────────────────────────────────────────
527
528    #[test]
529    fn xof_incremental_reads_match_bulk() {
530        let mut xof1 = Hasher::new().update(b"xof incremental").finalize_xof();
531        let mut xof2 = Hasher::new().update(b"xof incremental").finalize_xof();
532
533        // Bulk read
534        let mut bulk = [0u8; 200];
535        xof1.fill(&mut bulk);
536
537        // Incremental reads of varying sizes
538        let mut incremental = Vec::new();
539        for size in [1, 3, 7, 13, 64, 50, 62] {
540            let mut buf = vec![0u8; size];
541            xof2.fill(&mut buf);
542            incremental.extend_from_slice(&buf);
543        }
544
545        assert_eq!(&bulk[..], &incremental[..]);
546    }
547
548    #[test]
549    fn xof_deterministic() {
550        let mut xof1 = Hasher::new().update(b"deterministic").finalize_xof();
551        let mut xof2 = Hasher::new().update(b"deterministic").finalize_xof();
552        let mut out1 = [0u8; 256];
553        let mut out2 = [0u8; 256];
554        xof1.fill(&mut out1);
555        xof2.fill(&mut out2);
556        assert_eq!(out1, out2);
557    }
558
559    #[test]
560    fn xof_different_inputs_different_streams() {
561        let mut xof1 = Hasher::new().update(b"input A").finalize_xof();
562        let mut xof2 = Hasher::new().update(b"input B").finalize_xof();
563        let mut out1 = [0u8; 128];
564        let mut out2 = [0u8; 128];
565        xof1.fill(&mut out1);
566        xof2.fill(&mut out2);
567        assert_ne!(out1, out2);
568    }
569
570    #[test]
571    fn xof_zero_length_fill() {
572        let mut xof = Hasher::new().update(b"zero").finalize_xof();
573        let mut empty = [];
574        xof.fill(&mut empty); // should not panic
575
576        // Subsequent reads should still work
577        let mut out = [0u8; 64];
578        xof.fill(&mut out);
579        assert_ne!(out, [0u8; 64]);
580    }
581
582    // ── Hash type tests ────────────────────────────────────────────
583
584    #[test]
585    fn hash_from_bytes_roundtrip() {
586        let bytes = [0xAB; OUTPUT_BYTES];
587        let h = Hash::from_bytes(bytes);
588        assert_eq!(h.as_bytes(), &bytes);
589    }
590
591    #[test]
592    fn hash_to_hex_length() {
593        let h = Hash::from_bytes([0x00; OUTPUT_BYTES]);
594        assert_eq!(h.to_hex().len(), OUTPUT_BYTES * 2);
595        assert_eq!(h.to_hex(), "0".repeat(OUTPUT_BYTES * 2));
596    }
597
598    #[test]
599    fn hash_debug_format() {
600        let h = Hash::from_bytes([0x00; OUTPUT_BYTES]);
601        let debug = format!("{h:?}");
602        assert!(debug.starts_with("Hash("));
603        assert!(debug.ends_with(')'));
604    }
605
606    #[test]
607    fn hash_as_ref() {
608        let h = Hash::from_bytes([0x42; OUTPUT_BYTES]);
609        let slice: &[u8] = h.as_ref();
610        assert_eq!(slice.len(), OUTPUT_BYTES);
611        assert!(slice.iter().all(|&b| b == 0x42));
612    }
613
614    #[test]
615    fn hash_eq_and_hash_trait() {
616        use std::collections::HashSet;
617        let h1 = Hash::from_bytes([1; OUTPUT_BYTES]);
618        let h2 = Hash::from_bytes([1; OUTPUT_BYTES]);
619        let h3 = Hash::from_bytes([2; OUTPUT_BYTES]);
620        assert_eq!(h1, h2);
621        assert_ne!(h1, h3);
622
623        let mut set = HashSet::new();
624        set.insert(h1);
625        set.insert(h2);
626        set.insert(h3);
627        assert_eq!(set.len(), 2);
628    }
629
630    // ── Hasher Default trait ───────────────────────────────────────
631
632    #[test]
633    fn hasher_default_matches_new() {
634        let h1 = Hasher::new().update(b"test").finalize();
635        let h2 = Hasher::default().update(b"test").finalize();
636        assert_eq!(h1, h2);
637    }
638
639    // ── Domain separation completeness ─────────────────────────────
640
641    #[test]
642    fn all_four_domains_produce_different_outputs() {
643        let data = b"domain test data";
644
645        let plain = Hasher::new().update(data).finalize();
646
647        let keyed = Hasher::new_keyed(&[0u8; OUTPUT_BYTES])
648            .update(data)
649            .finalize();
650
651        let ctx = Hasher::new_derive_key_context("ctx");
652        let ctx_hash = ctx.finalize();
653        let derived = Hasher::new_derive_key_material(&ctx_hash)
654            .update(data)
655            .finalize();
656
657        let context_only = Hasher::new_derive_key_context(
658            std::str::from_utf8(data).unwrap()
659        ).finalize();
660
661        // All pairwise different
662        let hashes = [plain, keyed, derived, context_only];
663        for i in 0..hashes.len() {
664            for j in (i + 1)..hashes.len() {
665                assert_ne!(hashes[i], hashes[j], "domains {i} and {j} collide");
666            }
667        }
668    }
669
670    // ── Keyed hash edge cases ──────────────────────────────────────
671
672    #[test]
673    fn keyed_hash_empty_data() {
674        let h = Hasher::new_keyed(&[0u8; OUTPUT_BYTES]).finalize();
675        assert_ne!(h.0, [0u8; OUTPUT_BYTES]);
676    }
677
678    // ── Derive key edge cases ──────────────────────────────────────
679
680    #[test]
681    fn derive_key_empty_material() {
682        let key = crate::derive_key("context", b"");
683        assert_ne!(key, [0u8; OUTPUT_BYTES]);
684    }
685
686    #[test]
687    fn derive_key_empty_context() {
688        let key = crate::derive_key("", b"material");
689        assert_ne!(key, [0u8; OUTPUT_BYTES]);
690    }
691
692    // ── Hasher debug format ────────────────────────────────────────
693
694    #[test]
695    fn hasher_debug_shows_absorbed() {
696        let mut h = Hasher::new();
697        h.update(b"hello");
698        let debug = format!("{h:?}");
699        assert!(debug.contains("absorbed"));
700        assert!(debug.contains("5")); // 5 bytes absorbed
701    }
702
703    // ── Pinned test vector ─────────────────────────────────────────
704
705    /// Pinned hash of the empty string. If this changes, the hash function
706    /// has changed and all downstream content-addressed data is invalidated.
707    #[test]
708    fn pinned_empty_hash() {
709        let h1 = Hasher::new().finalize();
710        let h2 = Hasher::new().finalize();
711        assert_eq!(h1, h2);
712        // Pin the hex to detect regressions
713        let hex = h1.to_hex();
714        assert_eq!(hex.len(), 128); // 64 bytes = 128 hex chars
715    }
716
717    /// Pinned hash of "hemera". Same stability guarantee.
718    #[test]
719    fn pinned_hemera_hash() {
720        let h1 = crate::hash(b"hemera");
721        let h2 = crate::hash(b"hemera");
722        assert_eq!(h1, h2);
723    }
724
725    #[cfg(feature = "serde")]
726    #[test]
727    fn serde_roundtrip() {
728        let h = crate::hash(b"serde test");
729        let json = serde_json::to_string(&h).unwrap();
730        let recovered: Hash = serde_json::from_str(&json).unwrap();
731        assert_eq!(h, recovered);
732    }
733
734    #[cfg(feature = "serde")]
735    #[test]
736    fn serde_roundtrip_zero_hash() {
737        let h = Hash::from_bytes([0u8; OUTPUT_BYTES]);
738        let json = serde_json::to_string(&h).unwrap();
739        let recovered: Hash = serde_json::from_str(&json).unwrap();
740        assert_eq!(h, recovered);
741    }
742}