Skip to main content

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}