Skip to main content

sgl_core/
hash.rs

1//! Explicit, target-independent BLAKE3 state hashing.
2
3use core::fmt;
4
5/// A 32-byte canonical BLAKE3 digest.
6#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Digest(pub [u8; 32]);
8
9impl Digest {
10    /// Computes the exact, unframed BLAKE3 digest of `bytes`.
11    ///
12    /// Use this for authored content hashes. Deterministic simulation state should
13    /// continue to use [`StateHasher`] so its typed framing remains explicit.
14    #[must_use]
15    pub fn hash_bytes(bytes: &[u8]) -> Self {
16        Self(*blake3::hash(bytes).as_bytes())
17    }
18
19    /// Returns lower-case hexadecimal with exactly 64 characters.
20    #[must_use]
21    pub fn to_hex(self) -> String {
22        const HEX: &[u8; 16] = b"0123456789abcdef";
23        let mut result = String::with_capacity(64);
24        for byte in self.0 {
25            result.push(char::from(HEX[usize::from(byte >> 4)]));
26            result.push(char::from(HEX[usize::from(byte & 15)]));
27        }
28        result
29    }
30}
31
32impl fmt::Debug for Digest {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(formatter, "Digest({self})")
35    }
36}
37
38impl fmt::Display for Digest {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        formatter.write_str(&self.to_hex())
41    }
42}
43
44/// Values with one frozen canonical byte representation.
45pub trait CanonicalWrite {
46    /// Writes this value using its documented canonical byte representation.
47    fn canonical_write(&self, out: &mut StateHasher);
48}
49
50/// An explicit BLAKE3 writer for deterministic state.
51#[derive(Default)]
52pub struct StateHasher {
53    inner: blake3::Hasher,
54}
55
56impl StateHasher {
57    /// Creates an empty state hasher.
58    #[must_use]
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Appends one byte.
64    pub fn u8(&mut self, value: u8) {
65        self.inner.update(&[value]);
66    }
67    /// Appends a two-byte little-endian integer.
68    pub fn u16(&mut self, value: u16) {
69        self.inner.update(&value.to_le_bytes());
70    }
71    /// Appends a four-byte little-endian integer.
72    pub fn u32(&mut self, value: u32) {
73        self.inner.update(&value.to_le_bytes());
74    }
75    /// Appends an eight-byte little-endian integer.
76    pub fn u64(&mut self, value: u64) {
77        self.inner.update(&value.to_le_bytes());
78    }
79    /// Appends a signed four-byte little-endian integer.
80    pub fn i32(&mut self, value: i32) {
81        self.inner.update(&value.to_le_bytes());
82    }
83    /// Appends a signed eight-byte little-endian integer.
84    pub fn i64(&mut self, value: i64) {
85        self.inner.update(&value.to_le_bytes());
86    }
87    /// Appends a boolean as exactly `0` or `1`.
88    pub fn bool(&mut self, value: bool) {
89        self.u8(u8::from(value));
90    }
91
92    /// Appends bytes preceded by their checked `u32` length.
93    pub fn bytes(&mut self, bytes: &[u8]) {
94        self.length(bytes.len());
95        self.inner.update(bytes);
96    }
97
98    /// Appends a collection length as `u32`.
99    ///
100    /// # Panics
101    ///
102    /// Panics when `length` cannot be represented in the frozen format.
103    pub fn length(&mut self, length: usize) {
104        self.u32(u32::try_from(length).expect("canonical collection length exceeds u32"));
105    }
106
107    /// Appends a canonical value.
108    pub fn write<T: CanonicalWrite + ?Sized>(&mut self, value: &T) {
109        value.canonical_write(self);
110    }
111
112    /// Appends an ordered sequence with a `u32` length prefix.
113    pub fn sequence<T: CanonicalWrite>(&mut self, values: &[T]) {
114        self.length(values.len());
115        for value in values {
116            self.write(value);
117        }
118    }
119
120    /// Finalizes the digest.
121    #[must_use]
122    pub fn finish(self) -> Digest {
123        Digest(*self.inner.finalize().as_bytes())
124    }
125}
126
127macro_rules! canonical_integer {
128    ($type:ty, $method:ident) => {
129        impl CanonicalWrite for $type {
130            fn canonical_write(&self, out: &mut StateHasher) {
131                out.$method(*self);
132            }
133        }
134    };
135}
136
137canonical_integer!(u8, u8);
138canonical_integer!(u16, u16);
139canonical_integer!(u32, u32);
140canonical_integer!(u64, u64);
141canonical_integer!(i32, i32);
142canonical_integer!(i64, i64);
143
144impl CanonicalWrite for bool {
145    fn canonical_write(&self, out: &mut StateHasher) {
146        out.bool(*self);
147    }
148}
149
150/// Computes the canonical digest of one value.
151#[must_use]
152pub fn digest_of<T: CanonicalWrite + ?Sized>(value: &T) -> Digest {
153    let mut hasher = StateHasher::new();
154    hasher.write(value);
155    hasher.finish()
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use wasm_bindgen_test::wasm_bindgen_test;
162
163    /// #249: `bool` writes through the `CanonicalWrite` trait are the typed
164    /// bool encoding, so `true` and `false` hash differently and match the
165    /// direct method.
166    #[wasm_bindgen_test(unsupported = test)]
167    fn bool_writes_through_the_trait_are_canonical() {
168        let mut direct = StateHasher::new();
169        direct.bool(true);
170        assert_eq!(digest_of(&true), direct.finish());
171        assert_ne!(digest_of(&true), digest_of(&false));
172    }
173
174    #[wasm_bindgen_test(unsupported = test)]
175    fn length_prefixes_and_order_are_unambiguous() {
176        let mut split_a = StateHasher::new();
177        split_a.bytes(b"ab");
178        split_a.bytes(b"c");
179        let mut split_b = StateHasher::new();
180        split_b.bytes(b"a");
181        split_b.bytes(b"bc");
182        assert_ne!(split_a.finish(), split_b.finish());
183        let mut first = StateHasher::new();
184        first.sequence(&[1_u32, 2]);
185        let mut second = StateHasher::new();
186        second.sequence(&[2_u32, 1]);
187        assert_ne!(first.finish(), second.finish());
188    }
189
190    #[wasm_bindgen_test(unsupported = test)]
191    fn canonical_digest_is_frozen() {
192        let mut hasher = StateHasher::new();
193        hasher.u16(0x1234);
194        hasher.i32(-2);
195        hasher.bool(true);
196        hasher.bytes(b"sgl");
197        assert_eq!(
198            hasher.finish().to_hex(),
199            "e18ef0865fe504e976275cc32bb1b8bbcbf812fd92b8e1e21be43c29a979444a"
200        );
201    }
202
203    #[wasm_bindgen_test(unsupported = test)]
204    fn raw_content_digest_is_exact_blake3() {
205        assert_eq!(
206            Digest::hash_bytes(b"abc").to_hex(),
207            "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
208        );
209    }
210}