Skip to main content

prism_crypto/
commitment.rs

1//! `CommitmentAxis` declaration, parametric Merkle reference impl, and
2//! shape carriers.
3
4#![allow(missing_docs)]
5
6use core::marker::PhantomData;
7
8use uor_foundation::enforcement::{GroundedShape, ShapeViolation};
9use uor_foundation::pipeline::{ConstrainedTypeShape, ConstraintRef, IntoBindingValue, TermValue};
10use uor_foundation_sdk::axis;
11
12use crate::hash::{HashAxis, Sha256Hasher};
13
14axis! {
15    /// Wiki ADR-031 commitment schemes (Merkle, Pedersen, KZG).
16    pub trait CommitmentAxis: AxisExtension {
17        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/CommitmentAxis";
18        const MAX_OUTPUT_BYTES: usize = 96;
19        /// Commit to `input` — emits the commitment bytes into `out`.
20        ///
21        /// # Errors
22        ///
23        /// Returns `ShapeViolation` on malformed input.
24        fn commit(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
25    }
26}
27
28const SHA256_BYTES: usize = 32;
29
30fn shape_violation(constraint: &'static str) -> ShapeViolation {
31    ShapeViolation {
32        shape_iri: "https://uor.foundation/axis/CommitmentAxis/MerkleRoot",
33        constraint_iri: constraint,
34        property_iri: "https://uor.foundation/axis/inputBytes",
35        expected_range: "https://uor.foundation/axis/MerkleLeafSequence",
36        min_count: 0,
37        max_count: 0,
38        kind: uor_foundation::ViolationKind::ValueCheck,
39    }
40}
41
42/// Depth of the streaming-Merkle subtree stack: `usize::BITS` slots.
43/// A binary tree with `N` leaves has height `⌈log2 N⌉`, and any leaf
44/// sequence a `usize`-indexed slice can address has `N ≤ usize::MAX`, so
45/// `usize::BITS` stack slots accommodate the **maximum possible** tree —
46/// there is no arbitrary leaf-count ceiling (§ 11.10). The commit kernel
47/// reads leaves directly from `input` and combines equal-height subtrees
48/// on this `O(log N)` stack, so leaf count scales arbitrarily.
49const MERKLE_STACK_DEPTH: usize = usize::BITS as usize;
50
51/// Parametric Merkle-root commitment over **any** `HashAxis` impl
52/// `H` with `H::MAX_OUTPUT_BYTES = LEAF_BYTES`.
53///
54/// `LEAF_BYTES` is the leaf width (and the root width — Merkle's input
55/// and output share the digest's output size). The default,
56/// [`MerkleRootCommitment`], uses SHA-256 (32-byte leaves and root).
57///
58/// Per ADR-031 a standard-library commitment composes other
59/// standard-library axes (`HashAxis` here); this is the
60/// canonical-reference example of axis composition the wiki commits
61/// to. Two `MerkleRoot<H>` instantiations with structurally-identical
62/// `H` content-address identically per ADR-017.
63#[derive(Debug, Clone, Copy)]
64pub struct MerkleRoot<H: HashAxis, const LEAF_BYTES: usize = SHA256_BYTES>(PhantomData<H>);
65
66impl<H: HashAxis, const LEAF_BYTES: usize> Default for MerkleRoot<H, LEAF_BYTES> {
67    fn default() -> Self {
68        Self(PhantomData)
69    }
70}
71
72impl<H: HashAxis, const LEAF_BYTES: usize> CommitmentAxis for MerkleRoot<H, LEAF_BYTES> {
73    const AXIS_ADDRESS: &'static str =
74        "https://uor.foundation/axis/CommitmentAxis/MerkleRootParametric";
75    const MAX_OUTPUT_BYTES: usize = LEAF_BYTES;
76
77    fn commit(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
78        if LEAF_BYTES == 0 {
79            return Err(shape_violation(
80                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafBytesNonZero",
81            ));
82        }
83        if input.is_empty() || input.len() % LEAF_BYTES != 0 {
84            return Err(shape_violation(
85                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafAlignment",
86            ));
87        }
88        let leaf_count = input.len() / LEAF_BYTES;
89        if !leaf_count.is_power_of_two() {
90            return Err(shape_violation(
91                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/powerOfTwoLeaves",
92            ));
93        }
94        if out.len() < LEAF_BYTES {
95            return Err(shape_violation(
96                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/outputBuffer",
97            ));
98        }
99        // Streaming Merkle: read leaves left-to-right directly from
100        // `input` and combine equal-height subtrees on an `O(log N)`
101        // stack. All scratch is sized by the const-generic `LEAF_BYTES`
102        // (no fixed leaf-width cap) and the stack has `usize::BITS` slots
103        // (no fixed leaf-count cap — see `MERKLE_STACK_DEPTH`). This is
104        // identical, leaf-for-leaf, to the bottom-up pairing
105        // `hash(node[2i] || node[2i+1])` for the power-of-two leaf counts
106        // this kernel admits.
107        let mut roots = [[0u8; LEAF_BYTES]; MERKLE_STACK_DEPTH];
108        let mut levels = [0u32; MERKLE_STACK_DEPTH];
109        let mut top = 0usize;
110        for i in 0..leaf_count {
111            let mut cur = [0u8; LEAF_BYTES];
112            cur.copy_from_slice(&input[i * LEAF_BYTES..(i + 1) * LEAF_BYTES]);
113            let mut cur_level = 0u32;
114            // Fold the current subtree with any same-height neighbour on
115            // top of the stack; the popped entry is the left sibling.
116            while top > 0 && levels[top - 1] == cur_level {
117                top -= 1;
118                let mut pair = [[0u8; LEAF_BYTES]; 2];
119                pair[0] = roots[top];
120                pair[1] = cur;
121                let mut digest = [0u8; LEAF_BYTES];
122                H::hash(pair.as_flattened(), &mut digest[..])?;
123                cur = digest;
124                cur_level += 1;
125            }
126            roots[top] = cur;
127            levels[top] = cur_level;
128            top += 1;
129        }
130        // Power-of-two leaf count ⇒ the stack collapses to a single root.
131        out[..LEAF_BYTES].copy_from_slice(&roots[0][..LEAF_BYTES]);
132        Ok(LEAF_BYTES)
133    }
134}
135
136// ADR-052 generic-form companion.
137axis_extension_impl_for_commitment_axis!(
138    @generic MerkleRoot<H, LEAF_BYTES>,
139    [H: HashAxis, const LEAF_BYTES: usize]
140);
141
142/// SHA-256 Merkle root — the canonical default per ADR-031.
143pub type MerkleRootCommitment = MerkleRoot<Sha256Hasher, SHA256_BYTES>;
144
145// ---- MerkleProofShape: ConstrainedTypeShape carrier ----
146
147/// Parametric ConstrainedTypeShape for a Merkle-inclusion proof.
148///
149/// Carries `MAX_DEPTH` sibling-digests of `LEAF_BYTES` each plus a
150/// leaf-index — `(MAX_DEPTH * LEAF_BYTES + 8)` bytes total (the +8
151/// for a u64 leaf-index). Per ADR-031's `MerkleProof<MaxDepth>` shape
152/// commitment.
153#[derive(Debug, Clone, Copy)]
154pub struct MerkleProofShape<const MAX_DEPTH: usize, const LEAF_BYTES: usize = SHA256_BYTES>;
155
156impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> Default
157    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
158{
159    fn default() -> Self {
160        Self
161    }
162}
163
164impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> ConstrainedTypeShape
165    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
166{
167    const IRI: &'static str = "https://uor.foundation/type/ConstrainedType";
168    const SITE_COUNT: usize = MAX_DEPTH * LEAF_BYTES + 8;
169    const CONSTRAINTS: &'static [ConstraintRef] = &[];
170    #[allow(clippy::cast_possible_truncation)]
171    const CYCLE_SIZE: u64 = 256u64.saturating_pow((MAX_DEPTH * LEAF_BYTES + 8) as u32);
172}
173
174impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> uor_foundation::pipeline::__sdk_seal::Sealed
175    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
176{
177}
178impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> GroundedShape
179    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
180{
181}
182impl<'a, const MAX_DEPTH: usize, const LEAF_BYTES: usize> IntoBindingValue<'a>
183    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
184{
185    fn as_binding_value<const INLINE_BYTES: usize>(&self) -> TermValue<'a, INLINE_BYTES> {
186        TermValue::empty()
187    }
188}