pr4xis_runtime/address.rs
1//! Content-addressing — the one primitive the runtime GROUNDS.
2//!
3//! A [`ContentAddress`] is the BLAKE3 hash (Aumasson, O'Connor, Neves &
4//! Wilcox-O'Hearn 2020, "BLAKE3: one function, fast everywhere") of a
5//! CANONICAL byte encoding of a definition. BLAKE3 is a sound tree-mode hash
6//! (Gunsing, CRYPTO 2022, under the Bertoni–Daemen sound-tree-hashing
7//! conditions), so the same function supports incrementally verified
8//! streaming (Bao — O'Connor) without changing what an address means.
9//! Everything else about the `.prx` format is learned from the meta-`.prx`;
10//! this is the bottom of the reflexive tower — it is what "reference" and
11//! "agreement" MEAN. Two peers agree a definition is the same iff they hash
12//! the same canonical bytes to the same address.
13//!
14//! The canonical ENCODING (which bytes are fed in) is the codec layer's concern
15//! — the target is a multihash-tagged DAG-CBOR canonical form — and the
16//! *definition* fed in is the concept's `morphisms_from` closure + axioms +
17//! lexical entry, NOT its name (definition-bearing addressing, which closes the
18//! G5 wire gap). This primitive is deliberately encoding-agnostic: it grounds
19//! only the hash, so the codec and the definition-encoding can be chosen and
20//! evolved without changing what identity *means*.
21
22use sha2::{Digest, Sha256, Sha512};
23
24/// The hash functions a content address (or an integrity claim over one) may
25/// name. The enum admits only strong functions — BLAKE3 (Aumasson, O'Connor,
26/// Neves & Wilcox-O'Hearn 2020) and SHA-256 / SHA-512 (NIST FIPS 180-4) — so
27/// weak functions (MD5, SHA-1)
28/// are *unrepresentable*: "refuse weak algorithms" is a type invariant, not a
29/// runtime branch. Praxis EMITS addresses under exactly one algorithm per
30/// format epoch ([`ADDRESS_ALGORITHM`]); it VERIFIES claims under any variant
31/// here (the W3C SRI verify-many discipline).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum HashAlgorithm {
34 /// NIST FIPS 180-4 §6.2.
35 Sha256,
36 /// NIST FIPS 180-4 §6.4.
37 Sha512,
38 /// Aumasson, O'Connor, Neves & Wilcox-O'Hearn (2020).
39 Blake3,
40}
41
42/// The ONE algorithm praxis emits content addresses under in this format
43/// epoch. Changing it is a deliberate format-version event (every pin and
44/// known-answer constant re-blesses), never a per-address choice: the
45/// fail-closed gates re-derive addresses from the bytes they admit, so the
46/// algorithm comes from verifier policy — a payload never selects its own
47/// verifier.
48pub const ADDRESS_ALGORITHM: HashAlgorithm = HashAlgorithm::Blake3;
49
50/// Lowercase-hex digest of `bytes` under a named [`HashAlgorithm`] — the
51/// multi-algorithm verify leg. [`ContentAddress::of`] is the emit leg and
52/// always uses [`ADDRESS_ALGORITHM`].
53pub fn hash_hex(algorithm: HashAlgorithm, bytes: &[u8]) -> String {
54 fn hex(bytes: &[u8]) -> String {
55 use std::fmt::Write;
56 let mut s = String::with_capacity(bytes.len() * 2);
57 for b in bytes {
58 write!(s, "{b:02x}").expect("writing to a String is infallible");
59 }
60 s
61 }
62 match algorithm {
63 HashAlgorithm::Sha256 => hex(&Sha256::digest(bytes)),
64 HashAlgorithm::Sha512 => hex(&Sha512::digest(bytes)),
65 HashAlgorithm::Blake3 => hex(blake3::hash(bytes).as_bytes()),
66 }
67}
68
69/// The content address of a canonical byte encoding: a BLAKE3 digest
70/// (Aumasson, O'Connor, Neves & Wilcox-O'Hearn 2020).
71///
72/// The runtime never trusts a self-asserted address — it re-derives the address
73/// from the bytes it is about to admit and compares (the fail-closed load gate);
74/// a mismatch is rejected. `Ord` so addresses key the Merkle-DAG's `BTreeMap`s
75/// deterministically.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
77pub struct ContentAddress([u8; 32]);
78
79impl ContentAddress {
80 /// Ground primitive: the content address of `canonical_bytes`. This is the
81 /// ONE computation the runtime grounds — `.prx` identity bottoms out here.
82 pub fn of(canonical_bytes: &[u8]) -> Self {
83 Self(*blake3::hash(canonical_bytes).as_bytes())
84 }
85
86 /// The raw 32-byte digest.
87 pub fn as_bytes(&self) -> &[u8; 32] {
88 &self.0
89 }
90
91 /// Lowercase hex (64 chars) — the form `praxis.lock` pins use, so a
92 /// `ContentAddress` and a committed pin compare directly.
93 pub fn to_hex(&self) -> String {
94 use std::fmt::Write;
95 let mut s = String::with_capacity(64);
96 for b in &self.0 {
97 // Infallible: writing to a `String` never errors.
98 write!(s, "{b:02x}").expect("writing to a String is infallible");
99 }
100 s
101 }
102
103 /// Parse a 64-character lowercase-hex digest. `None` if the length is wrong
104 /// or any character is not a hex digit.
105 pub fn from_hex(hex: &str) -> Option<Self> {
106 if hex.len() != 64 {
107 return None;
108 }
109 let mut out = [0u8; 32];
110 for (byte, pair) in out.iter_mut().zip(hex.as_bytes().chunks_exact(2)) {
111 let hi = (pair[0] as char).to_digit(16)?;
112 let lo = (pair[1] as char).to_digit(16)?;
113 *byte = ((hi << 4) | lo) as u8;
114 }
115 Some(Self(out))
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn address_is_deterministic() {
125 assert_eq!(ContentAddress::of(b"praxis"), ContentAddress::of(b"praxis"));
126 }
127
128 #[test]
129 fn different_bytes_yield_different_address() {
130 assert_ne!(ContentAddress::of(b"a"), ContentAddress::of(b"b"));
131 }
132
133 #[test]
134 fn hex_round_trips() {
135 let a = ContentAddress::of(b"the ground");
136 let hex = a.to_hex();
137 assert_eq!(hex.len(), 64);
138 assert_eq!(ContentAddress::from_hex(&hex), Some(a));
139 }
140
141 #[test]
142 fn from_hex_rejects_malformed() {
143 assert_eq!(ContentAddress::from_hex("xyz"), None); // wrong length
144 assert_eq!(ContentAddress::from_hex(&"z".repeat(64)), None); // non-hex
145 assert_eq!(ContentAddress::from_hex(&"a".repeat(63)), None); // off-by-one
146 }
147
148 #[test]
149 fn matches_blake3_known_answer() {
150 // Official BLAKE3 empty-input vector (BLAKE3 team test_vectors.json,
151 // input_len = 0): af1349b9...3262.
152 assert_eq!(
153 ContentAddress::of(b"").to_hex(),
154 "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
155 );
156 }
157}