Skip to main content

tatara_process/
hash.rs

1//! Substrate primitive for BLAKE3 hex digests.
2//!
3//! Two peer entries — [`hex_blake3`] for the in-memory-buffer shape
4//! (`hex::encode(blake3::hash(bytes).as_bytes())`) every three-pillar
5//! attestation producer that fed BLAKE3 a single buffer restated by
6//! hand pre-lift, and [`hex_blake3_hash`] for the streaming-digest
7//! shape (`hex::encode(hash.as_bytes())` where `hash =
8//! blake3::Hasher::finalize()`) every consumer that folded per-item
9//! updates into a `Hasher` before finalizing walked. Both peers ride
10//! through ONE hex-encoding step at [`hex_blake3_hash`] so a future
11//! swap onto `blake3::Hash::to_hex().to_string()` (or a different
12//! encoding — base32, base64url, uppercase hex for a downstream tool)
13//! lands at ONE substrate function and every downstream three-pillar
14//! consumer inherits the upgrade mechanically.
15//!
16//! Return type is `String` for wire-shape stability with the pre-lift
17//! consumers — `ReceiptEnvelope.{intent,artifact,control}_hash` are
18//! typed as `String`, so a `Cow`/`&str` return would force allocation
19//! at every call site regardless.
20//!
21//! # Which peer to call
22//!
23//! - Have `&[u8]` in hand → [`hex_blake3`]. Internally it composes
24//!   [`hex_blake3_hash`] over `blake3::hash(bytes)`.
25//! - Have a `blake3::Hasher` you already folded per-item updates into
26//!   → `hex_blake3_hash(&h.finalize())`. Skips the one-shot round-trip
27//!   through `&[u8]` that would force the caller to materialize the
28//!   full input buffer just to re-hash it.
29
30/// Compute the lowercase 64-char BLAKE3 hex digest of `bytes`.
31///
32/// # Invariants
33///
34/// - **Length:** the returned string is always exactly 64 chars
35///   (BLAKE3's 32-byte digest encoded as lowercase hex).
36/// - **Charset:** every char is one of `[0-9a-f]` (lowercase).
37/// - **Determinism:** byte-identical output across runs for the same
38///   input; matches both the `hex::encode(blake3::hash(x).as_bytes())`
39///   and `blake3::hash(x).to_hex().to_string()` pre-lift spellings.
40///
41/// # `#[must_use]`
42///
43/// Every consumer either stores the returned hex into a receipt
44/// pillar (`intent_hash`, `artifact_hash`, `control_hash`) or feeds
45/// it into a DNS slot / stable name. Dropping the return means the
46/// hash was computed for no observable reason — the attribute
47/// surfaces that as a warning at every call site.
48///
49/// Delegates the terminal `hex::encode(...as_bytes())` step to the
50/// sibling [`hex_blake3_hash`] primitive so the encoding rule lives at
51/// ONE substrate owner; a future re-encoding (base32, base64url,
52/// uppercase, `blake3::Hash::to_hex()`) reaches BOTH the one-shot and
53/// the streaming corner through ONE edit.
54#[must_use]
55pub fn hex_blake3(bytes: &[u8]) -> String {
56    hex_blake3_hash(&blake3::hash(bytes))
57}
58
59/// Streaming-digest peer of [`hex_blake3`] — the ONE substrate owner
60/// of the 1-link `hex::encode(hash.as_bytes())` encoding step every
61/// three-pillar producer that folded per-item updates into a
62/// `blake3::Hasher` walked pre-lift.
63///
64/// # Why it exists
65///
66/// The `Hasher::finalize() → hex::encode(<Hash>.as_bytes())` chain was
67/// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
68/// duplication threshold, each carrying a `Hasher` it fed per-item
69/// updates into before finalizing:
70///
71/// * [`crate::three_pillar::compose_root`] — folds the domain tag +
72///   the four pillars (artifact, control, intent, previous) into a
73///   `blake3::Hasher`, then encodes the final hash. Pre-lift ended
74///   with `hex::encode(h.finalize().as_bytes())` inline; post-lift
75///   ends with `hex_blake3_hash(&h.finalize())`.
76/// * `tatara-reconciler::phase_machine::handle_running` — folds each
77///   observed FluxCD resource's `(apiVersion, kind, ns, name)`
78///   4-slot identity into a `blake3::Hasher`, then encodes the final
79///   hash as the artifact-pillar input for the ATTEST step.
80///
81/// Pre-lift the one-shot corner ([`hex_blake3`]) and the streaming
82/// corner both restated `hex::encode(...as_bytes())` at their own
83/// bodies, leaving a two-place drift trap — a future swap onto
84/// `blake3::Hash::to_hex().to_string()` (a subtler pre-existing
85/// spelling that appears at `crate::hostname::short_hex_blake3`), an
86/// uppercase-hex flip for a downstream tool, or a base32-encoded
87/// variant would have to land at BOTH sites or silently break receipt
88/// verification at the corner that missed the update. Post-lift the
89/// terminal hex step lives at ONE substrate owner and the one-shot
90/// peer's body reduces to `hex_blake3_hash(&blake3::hash(bytes))`.
91///
92/// # Invariants
93///
94/// Same shape as [`hex_blake3`]: 64 chars of lowercase hex, every
95/// char in `[0-9a-f]`, byte-identical to the pre-lift `hex::encode(<
96/// hash>.as_bytes())` spelling.
97///
98/// # `#[must_use]`
99///
100/// Every consumer either stores the returned hex into a receipt
101/// pillar or a `composed_root` slot. Dropping the return means the
102/// caller finalized a `Hasher` for no observable reason — the
103/// attribute surfaces that as a warning at every call site.
104///
105/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
106/// `hex::encode(...as_bytes())` step recurred at two hand-authored
107/// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is
108/// lifted to ONE owner here). THEORY.md §II.1 invariant 5
109/// (composition preserves proofs — the pin
110/// [`tests::hex_blake3_hash_matches_pre_lift_hex_encode_spelling_bytewise`]
111/// binds the streaming corner byte-identically to the pre-lift
112/// spelling, and the cross-corner pin
113/// [`tests::hex_blake3_bytes_form_delegates_through_hex_blake3_hash`]
114/// binds the one-shot peer to `hex_blake3_hash` so a regression in
115/// either surfaces at ONE substrate pin rather than as silent
116/// composed_root drift across every downstream consumer).
117#[must_use]
118pub fn hex_blake3_hash(hash: &blake3::Hash) -> String {
119    hex::encode(hash.as_bytes())
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn hex_blake3_returns_64_char_lowercase_hex() {
128        let d = hex_blake3(b"hello");
129        assert_eq!(d.len(), 64, "BLAKE3 digest hex-encodes to 64 chars");
130        assert!(
131            d.chars()
132                .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
133            "digest must be lowercase hex: {d}"
134        );
135    }
136
137    #[test]
138    fn hex_blake3_is_deterministic() {
139        assert_eq!(hex_blake3(b"hello"), hex_blake3(b"hello"));
140        assert_ne!(hex_blake3(b"hello"), hex_blake3(b"world"));
141    }
142
143    #[test]
144    fn hex_blake3_empty_input_matches_known_digest() {
145        // Known BLAKE3 digest of the empty input — a rename of the
146        // underlying algo (or an accidental salting) would land here
147        // rather than as silent receipt-root drift across every
148        // three-pillar consumer.
149        assert_eq!(
150            hex_blake3(b""),
151            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262",
152        );
153    }
154
155    #[test]
156    fn hex_blake3_matches_pre_lift_hex_encode_spelling_bytewise() {
157        // Byte-identical parity with the `hex::encode(blake3::hash(x).as_bytes())`
158        // pre-lift spelling used at every reconciler + probe + worker
159        // consumer routed onto this primitive; guards against a
160        // substrate-side canonicalization the pre-lift chain does NOT
161        // apply.
162        for buf in [
163            b"" as &[u8],
164            b"x",
165            b"hello",
166            &[0u8; 256],
167            b"tatara-receipt/v1",
168            b"{\"kind\":\"tatara.export\"}",
169        ] {
170            assert_eq!(
171                hex_blake3(buf),
172                hex::encode(blake3::hash(buf).as_bytes()),
173                "pre-lift `hex::encode(...)` spelling drifted for buf.len()={}",
174                buf.len(),
175            );
176        }
177    }
178
179    #[test]
180    fn hex_blake3_matches_pre_lift_to_hex_spelling_bytewise() {
181        // Byte-identical parity with the alternate `blake3::hash(x).to_hex().to_string()`
182        // pre-lift spelling used at `tatara-process::hostname::short_hex_blake3`,
183        // `tatara-export-worker::hex_blake3`, and the `p2p::chunk::blake3_hash`
184        // helpers; catches a divergence between the two workspace
185        // spellings that would otherwise silently break stable-name /
186        // ephemeral-id / receipt-root parity at any consumer that
187        // still spelled it the other way.
188        for buf in [
189            b"" as &[u8],
190            b"x",
191            b"hello",
192            &[0xFFu8; 128],
193            b"pleme-dev/ephemeral-test-01",
194        ] {
195            assert_eq!(
196                hex_blake3(buf),
197                blake3::hash(buf).to_hex().to_string(),
198                "pre-lift `.to_hex().to_string()` spelling drifted for buf.len()={}",
199                buf.len(),
200            );
201        }
202    }
203
204    // ── hex_blake3_hash streaming-digest peer pins ─────────────────
205
206    #[test]
207    fn hex_blake3_hash_matches_pre_lift_hex_encode_spelling_bytewise() {
208        // Byte-identical parity with the `hex::encode(<hash>.as_bytes())`
209        // pre-lift spelling every three-pillar producer that
210        // finalized a `Hasher` walked (compose_root's internal chain
211        // + phase_machine's per-ref artifact-hash fold). Swept across
212        // representative Hasher inputs so a substrate-side re-encoding
213        // (a base32 flip, an uppercase-hex flip, a `to_hex().to_string()`
214        // spelling drift) surfaces HERE rather than as silent
215        // composed_root drift at every downstream consumer.
216        for buf in [
217            b"" as &[u8],
218            b"x",
219            b"hello",
220            &[0u8; 64],
221            &[0xFFu8; 128],
222            b"tatara-process/v1alpha1\n",
223            b"aaaa\ncccc\niiii\npppp",
224        ] {
225            let mut h = blake3::Hasher::new();
226            h.update(buf);
227            let hash = h.finalize();
228            assert_eq!(
229                hex_blake3_hash(&hash),
230                hex::encode(hash.as_bytes()),
231                "pre-lift `hex::encode(<hash>.as_bytes())` spelling drifted for buf.len()={}",
232                buf.len(),
233            );
234        }
235    }
236
237    #[test]
238    fn hex_blake3_hash_matches_pre_lift_to_hex_spelling_bytewise() {
239        // Cross-spelling coherence with the alternate
240        // `blake3::Hash::to_hex().to_string()` form used at
241        // `tatara-process::hostname::short_hex_blake3`; both
242        // spellings MUST produce byte-identical output so a future
243        // consumer routed onto `hex_blake3_hash` cannot silently
244        // diverge from a peer that still spells it the other way.
245        for buf in [b"" as &[u8], b"x", b"hello", &[0xFFu8; 128]] {
246            let mut h = blake3::Hasher::new();
247            h.update(buf);
248            let hash = h.finalize();
249            assert_eq!(
250                hex_blake3_hash(&hash),
251                hash.to_hex().to_string(),
252                "streaming `.to_hex().to_string()` spelling drifted for buf.len()={}",
253                buf.len(),
254            );
255        }
256    }
257
258    #[test]
259    fn hex_blake3_hash_output_is_lowercase_hex_of_blake3_length() {
260        // BLAKE3 produces 32-byte digests; hex-encoded → 64 lowercase
261        // characters. Pin the output shape so a downstream reader's
262        // width assumption (a 26-char base32 slot in the wire form,
263        // for instance) surfaces here rather than as a wire-parse
264        // failure downstream.
265        let mut h = blake3::Hasher::new();
266        h.update(b"pillar-input");
267        let out = hex_blake3_hash(&h.finalize());
268        assert_eq!(out.len(), 64);
269        assert!(out.chars().all(|c| c.is_ascii_hexdigit()));
270        assert!(out.chars().all(|c| !c.is_ascii_uppercase()));
271    }
272
273    #[test]
274    fn hex_blake3_bytes_form_delegates_through_hex_blake3_hash() {
275        // Cross-primitive coherence — the one-shot [`hex_blake3`]
276        // peer MUST agree byte-for-byte with the streaming peer
277        // composed over `blake3::hash(bytes)`. A regression that
278        // specialized ONE peer (a different encoding, a
279        // canonicalization step) would surface HERE rather than as
280        // silent drift between the one-shot and streaming corners
281        // at every downstream consumer.
282        for buf in [
283            b"" as &[u8],
284            b"x",
285            b"hello",
286            &[0u8; 256],
287            b"tatara-receipt/v1",
288            b"{\"kind\":\"tatara.export\"}",
289        ] {
290            assert_eq!(
291                hex_blake3(buf),
292                hex_blake3_hash(&blake3::hash(buf)),
293                "one-shot `hex_blake3` drifted from streaming peer for buf.len()={}",
294                buf.len(),
295            );
296        }
297    }
298
299    #[test]
300    fn hex_blake3_hash_is_deterministic_across_calls() {
301        // BLAKE3 is deterministic; the encoder is pure. A regression
302        // that accidentally seeded a nonce, read a clock, or salted
303        // the encoding would fail loudly HERE rather than as silent
304        // composed_root drift at every downstream consumer.
305        let mut h = blake3::Hasher::new();
306        h.update(b"input");
307        let hash = h.finalize();
308        assert_eq!(hex_blake3_hash(&hash), hex_blake3_hash(&hash));
309    }
310}