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 canonical encoding 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.
45///
46/// An implementation must be self-delimiting: the shape of its writes (which
47/// primitive writes, in what order) may depend only on values it has already
48/// written. Write a length (with [`StateHasher::bytes`],
49/// [`StateHasher::sequence`] or [`StateHasher::length`]) before
50/// variable-length data, and a tag before an optional value or enum variant.
51/// Otherwise distinct values encode alike: if `Slot(Option<u32>)` writes its
52/// `u32` only when `Some`, `sequence(&[Slot(None), Slot(Some(5))])` and
53/// `sequence(&[Slot(Some(5)), Slot(None)])` hash the same.
54pub trait CanonicalWrite {
55    /// Writes this value using its documented canonical byte representation.
56    fn canonical_write(&self, out: &mut StateHasher);
57}
58
59/// An explicit BLAKE3 writer for deterministic state.
60///
61/// The encoding is schema-driven: each write appends its fixed-width
62/// little-endian bytes without a type tag, and [`bytes`](Self::bytes) and
63/// [`sequence`](Self::sequence) prefix a `u32` length. The schema is the shape
64/// of the primitive writes: which methods, in what order. Write sequences with
65/// the same schema hash differently whenever their values differ, provided
66/// each [`CanonicalWrite`] it uses is self-delimiting. Different schemas may encode alike —
67/// `u16(0x1234)` equals `u8(0x34); u8(0x12)` — so a caller that hashes several
68/// kinds of state in one stream, or changes what it writes, separates them
69/// itself with a leading tag or version (for example `u32(KIND)`).
70#[derive(Default)]
71pub struct StateHasher {
72    inner: blake3::Hasher,
73}
74
75impl StateHasher {
76    /// Creates an empty state hasher.
77    #[must_use]
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Appends one byte.
83    pub fn u8(&mut self, value: u8) {
84        self.inner.update(&[value]);
85    }
86    /// Appends a two-byte little-endian integer.
87    pub fn u16(&mut self, value: u16) {
88        self.inner.update(&value.to_le_bytes());
89    }
90    /// Appends a four-byte little-endian integer.
91    pub fn u32(&mut self, value: u32) {
92        self.inner.update(&value.to_le_bytes());
93    }
94    /// Appends an eight-byte little-endian integer.
95    pub fn u64(&mut self, value: u64) {
96        self.inner.update(&value.to_le_bytes());
97    }
98    /// Appends a signed four-byte little-endian integer.
99    pub fn i32(&mut self, value: i32) {
100        self.inner.update(&value.to_le_bytes());
101    }
102    /// Appends a signed eight-byte little-endian integer.
103    pub fn i64(&mut self, value: i64) {
104        self.inner.update(&value.to_le_bytes());
105    }
106    /// Appends a boolean as exactly `0` or `1`.
107    pub fn bool(&mut self, value: bool) {
108        self.u8(u8::from(value));
109    }
110
111    /// Appends bytes preceded by their checked `u32` length.
112    pub fn bytes(&mut self, bytes: &[u8]) {
113        self.length(bytes.len());
114        self.inner.update(bytes);
115    }
116
117    /// Appends a collection length as `u32`.
118    ///
119    /// # Panics
120    ///
121    /// Panics when `length` cannot be represented in the frozen format.
122    pub fn length(&mut self, length: usize) {
123        self.u32(u32::try_from(length).expect("canonical collection length exceeds u32"));
124    }
125
126    /// Appends a canonical value.
127    pub fn write<T: CanonicalWrite + ?Sized>(&mut self, value: &T) {
128        value.canonical_write(self);
129    }
130
131    /// Appends an ordered sequence with a `u32` length prefix.
132    pub fn sequence<T: CanonicalWrite>(&mut self, values: &[T]) {
133        self.length(values.len());
134        for value in values {
135            self.write(value);
136        }
137    }
138
139    /// Finalizes the digest.
140    #[must_use]
141    pub fn finish(self) -> Digest {
142        Digest(*self.inner.finalize().as_bytes())
143    }
144}
145
146macro_rules! canonical_integer {
147    ($type:ty, $method:ident) => {
148        impl CanonicalWrite for $type {
149            fn canonical_write(&self, out: &mut StateHasher) {
150                out.$method(*self);
151            }
152        }
153    };
154}
155
156canonical_integer!(u8, u8);
157canonical_integer!(u16, u16);
158canonical_integer!(u32, u32);
159canonical_integer!(u64, u64);
160canonical_integer!(i32, i32);
161canonical_integer!(i64, i64);
162
163impl CanonicalWrite for bool {
164    fn canonical_write(&self, out: &mut StateHasher) {
165        out.bool(*self);
166    }
167}
168
169/// Computes the canonical digest of one value.
170#[must_use]
171pub fn digest_of<T: CanonicalWrite + ?Sized>(value: &T) -> Digest {
172    let mut hasher = StateHasher::new();
173    hasher.write(value);
174    hasher.finish()
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use wasm_bindgen_test::wasm_bindgen_test;
181
182    /// #249: `bool` writes through the `CanonicalWrite` trait are the typed
183    /// bool encoding, so `true` and `false` hash differently and match the
184    /// direct method.
185    #[wasm_bindgen_test(unsupported = test)]
186    fn bool_writes_through_the_trait_are_canonical() {
187        let mut direct = StateHasher::new();
188        direct.bool(true);
189        assert_eq!(digest_of(&true), direct.finish());
190        assert_ne!(digest_of(&true), digest_of(&false));
191    }
192
193    #[wasm_bindgen_test(unsupported = test)]
194    fn length_prefixes_and_order_are_unambiguous() {
195        let mut split_a = StateHasher::new();
196        split_a.bytes(b"ab");
197        split_a.bytes(b"c");
198        let mut split_b = StateHasher::new();
199        split_b.bytes(b"a");
200        split_b.bytes(b"bc");
201        assert_ne!(split_a.finish(), split_b.finish());
202        let mut first = StateHasher::new();
203        first.sequence(&[1_u32, 2]);
204        let mut second = StateHasher::new();
205        second.sequence(&[2_u32, 1]);
206        assert_ne!(first.finish(), second.finish());
207    }
208
209    #[wasm_bindgen_test(unsupported = test)]
210    fn canonical_digest_is_frozen() {
211        let mut hasher = StateHasher::new();
212        hasher.u16(0x1234);
213        hasher.i32(-2);
214        hasher.bool(true);
215        hasher.bytes(b"sgl");
216        assert_eq!(
217            hasher.finish().to_hex(),
218            "e18ef0865fe504e976275cc32bb1b8bbcbf812fd92b8e1e21be43c29a979444a"
219        );
220    }
221
222    #[wasm_bindgen_test(unsupported = test)]
223    fn raw_content_digest_is_exact_blake3() {
224        assert_eq!(
225            Digest::hash_bytes(b"abc").to_hex(),
226            "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
227        );
228    }
229}