Skip to main content

dsfb_densor_runtime/
seal.rs

1//! Deterministic, tamper-evident sealing — a self-contained copy of the DSFB `CanonicalHasher` discipline.
2//!
3//! This crate keeps its own copy (like `dsfb-chemical-engineering-atlas` does) so the runtime substrate has no
4//! dependency on any chemical crate — it is a generic mechanism. The protocol is byte-identical to the edge
5//! crate's: each field is encoded as `len(label) LE | label | len(value) LE | value`, so field order and content
6//! are unambiguous and two runs over the same inputs produce the same digest.
7
8use sha2::{Digest, Sha256};
9
10/// A canonical, order-stable SHA-256 sealer. Build a preimage from labelled fields, then `finalize`.
11pub struct CanonicalHasher {
12    inner: Sha256,
13}
14
15impl Default for CanonicalHasher {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl CanonicalHasher {
22    pub fn new() -> Self {
23        CanonicalHasher {
24            inner: Sha256::new(),
25        }
26    }
27
28    /// Absorb a labelled byte field. Both label and value are length-prefixed (little-endian `u64`) so no two
29    /// distinct (label, value) sequences can collide by concatenation.
30    pub fn field(&mut self, label: &str, bytes: &[u8]) -> &mut Self {
31        self.inner.update((label.len() as u64).to_le_bytes());
32        self.inner.update(label.as_bytes());
33        self.inner.update((bytes.len() as u64).to_le_bytes());
34        self.inner.update(bytes);
35        self
36    }
37
38    /// Absorb a labelled `u64` (8 bytes little-endian).
39    pub fn u64(&mut self, label: &str, v: u64) -> &mut Self {
40        self.field(label, &v.to_le_bytes())
41    }
42
43    /// Absorb a labelled 32-byte digest.
44    pub fn hash32(&mut self, label: &str, h: &[u8; 32]) -> &mut Self {
45        self.field(label, h)
46    }
47
48    /// Finalise to the raw 32-byte digest.
49    pub fn finalize(self) -> [u8; 32] {
50        self.inner.finalize().into()
51    }
52
53    /// Finalise to a 64-char lowercase hex digest.
54    pub fn finalize_hex(self) -> String {
55        to_hex(&self.finalize())
56    }
57}
58
59/// One-shot raw-bytes SHA-256 → 32 bytes.
60pub fn sha256(bytes: &[u8]) -> [u8; 32] {
61    let mut h = Sha256::new();
62    h.update(bytes);
63    h.finalize().into()
64}
65
66/// Lowercase hex of a 32-byte digest.
67pub fn to_hex(h: &[u8; 32]) -> String {
68    let mut s = String::with_capacity(64);
69    for b in h {
70        s.push_str(&format!("{b:02x}"));
71    }
72    s
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn field_order_and_length_prefix_prevent_collisions() {
81        // ("ab","c") vs ("a","bc") must NOT collide thanks to length prefixing.
82        let mut a = CanonicalHasher::new();
83        a.field("ab", b"c");
84        let mut b = CanonicalHasher::new();
85        b.field("a", b"bc");
86        assert_ne!(a.finalize(), b.finalize());
87    }
88
89    #[test]
90    fn seal_is_deterministic() {
91        let mk = || {
92            let mut h = CanonicalHasher::new();
93            h.field("schema", b"x")
94                .u64("n", 3)
95                .hash32("auth", &sha256(b"policy"));
96            h.finalize()
97        };
98        assert_eq!(mk(), mk());
99        assert_eq!(to_hex(&mk()).len(), 64);
100    }
101}