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::{
10    AxisExtension, ConstrainedTypeShape, ConstraintRef, IntoBindingValue,
11};
12use uor_foundation_sdk::axis;
13
14use crate::hash::{HashAxis, Sha256Hasher};
15
16axis! {
17    /// Wiki ADR-031 commitment schemes (Merkle, Pedersen, KZG).
18    pub trait CommitmentAxis: AxisExtension {
19        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/CommitmentAxis";
20        const MAX_OUTPUT_BYTES: usize = 96;
21        /// Commit to `input` — emits the commitment bytes into `out`.
22        ///
23        /// # Errors
24        ///
25        /// Returns `ShapeViolation` on malformed input.
26        fn commit(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
27    }
28}
29
30const SHA256_BYTES: usize = 32;
31
32fn shape_violation(constraint: &'static str) -> ShapeViolation {
33    ShapeViolation {
34        shape_iri: "https://uor.foundation/axis/CommitmentAxis/MerkleRoot",
35        constraint_iri: constraint,
36        property_iri: "https://uor.foundation/axis/inputBytes",
37        expected_range: "https://uor.foundation/axis/MerkleLeafSequence",
38        min_count: 0,
39        max_count: 0,
40        kind: uor_foundation::ViolationKind::ValueCheck,
41    }
42}
43
44/// Maximum leaf count any [`MerkleRoot`] instantiation supports.
45/// Depth-6 binary tree; deeper Merkle commitments compose at the verb
46/// level (per ADR-024) rather than baking into the axis kernel.
47pub const MAX_MERKLE_LEAVES: usize = 64;
48
49/// Maximum leaf byte-width — the largest `HashAxis::MAX_OUTPUT_BYTES`
50/// among standard-library hash impls (64 bytes for SHA-512).
51const MAX_LEAF_WIDTH: usize = 64;
52
53/// Parametric Merkle-root commitment over **any** `HashAxis` impl
54/// `H` with `H::MAX_OUTPUT_BYTES = LEAF_BYTES`.
55///
56/// `LEAF_BYTES` is the leaf width (and the root width — Merkle's input
57/// and output share the digest's output size). The default,
58/// [`MerkleRootCommitment`], uses SHA-256 (32-byte leaves and root).
59///
60/// Per ADR-031 a standard-library commitment composes other
61/// standard-library axes (`HashAxis` here); this is the
62/// canonical-reference example of axis composition the wiki commits
63/// to. Two `MerkleRoot<H>` instantiations with structurally-identical
64/// `H` content-address identically per ADR-017.
65#[derive(Debug, Clone, Copy)]
66pub struct MerkleRoot<H: HashAxis, const LEAF_BYTES: usize = SHA256_BYTES>(PhantomData<H>);
67
68impl<H: HashAxis, const LEAF_BYTES: usize> Default for MerkleRoot<H, LEAF_BYTES> {
69    fn default() -> Self {
70        Self(PhantomData)
71    }
72}
73
74impl<H: HashAxis, const LEAF_BYTES: usize> CommitmentAxis for MerkleRoot<H, LEAF_BYTES> {
75    const AXIS_ADDRESS: &'static str =
76        "https://uor.foundation/axis/CommitmentAxis/MerkleRootParametric";
77    const MAX_OUTPUT_BYTES: usize = LEAF_BYTES;
78
79    fn commit(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
80        if LEAF_BYTES == 0 {
81            return Err(shape_violation(
82                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafBytesNonZero",
83            ));
84        }
85        if input.is_empty() || input.len() % LEAF_BYTES != 0 {
86            return Err(shape_violation(
87                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafAlignment",
88            ));
89        }
90        let leaf_count = input.len() / LEAF_BYTES;
91        if !leaf_count.is_power_of_two() {
92            return Err(shape_violation(
93                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/powerOfTwoLeaves",
94            ));
95        }
96        if out.len() < LEAF_BYTES {
97            return Err(shape_violation(
98                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/outputBuffer",
99            ));
100        }
101        if leaf_count > MAX_MERKLE_LEAVES {
102            return Err(shape_violation(
103                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/maxLeaves",
104            ));
105        }
106        // Use a fixed-size working buffer sized for the maximum
107        // supported leaf width across HashAxis impls (64 bytes for
108        // SHA-512). Per-instantiation LEAF_BYTES bounds the actually
109        // used slice.
110        if LEAF_BYTES > MAX_LEAF_WIDTH {
111            return Err(shape_violation(
112                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafBytesInRange",
113            ));
114        }
115        let mut layer = [[0u8; MAX_LEAF_WIDTH]; MAX_MERKLE_LEAVES];
116        for i in 0..leaf_count {
117            layer[i][..LEAF_BYTES].copy_from_slice(&input[i * LEAF_BYTES..(i + 1) * LEAF_BYTES]);
118        }
119        let mut pair_buf = [0u8; 2 * MAX_LEAF_WIDTH];
120        let mut digest_buf = [0u8; MAX_LEAF_WIDTH];
121        let mut len = leaf_count;
122        while len > 1 {
123            let half = len / 2;
124            for i in 0..half {
125                pair_buf[..LEAF_BYTES].copy_from_slice(&layer[2 * i][..LEAF_BYTES]);
126                pair_buf[LEAF_BYTES..2 * LEAF_BYTES]
127                    .copy_from_slice(&layer[2 * i + 1][..LEAF_BYTES]);
128                H::hash(&pair_buf[..2 * LEAF_BYTES], &mut digest_buf[..LEAF_BYTES])?;
129                layer[i][..LEAF_BYTES].copy_from_slice(&digest_buf[..LEAF_BYTES]);
130            }
131            len = half;
132        }
133        out[..LEAF_BYTES].copy_from_slice(&layer[0][..LEAF_BYTES]);
134        Ok(LEAF_BYTES)
135    }
136}
137
138// ADR-052 generic-form companion.
139axis_extension_impl_for_commitment_axis!(
140    @generic MerkleRoot<H, LEAF_BYTES>,
141    [H: HashAxis, const LEAF_BYTES: usize]
142);
143
144/// SHA-256 Merkle root — the canonical default per ADR-031.
145pub type MerkleRootCommitment = MerkleRoot<Sha256Hasher, SHA256_BYTES>;
146
147// ---- MerkleProofShape: ConstrainedTypeShape carrier ----
148
149/// Parametric ConstrainedTypeShape for a Merkle-inclusion proof.
150///
151/// Carries `MAX_DEPTH` sibling-digests of `LEAF_BYTES` each plus a
152/// leaf-index — `(MAX_DEPTH * LEAF_BYTES + 8)` bytes total (the +8
153/// for a u64 leaf-index). Per ADR-031's `MerkleProof<MaxDepth>` shape
154/// commitment.
155#[derive(Debug, Clone, Copy)]
156pub struct MerkleProofShape<const MAX_DEPTH: usize, const LEAF_BYTES: usize = SHA256_BYTES>;
157
158impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> Default
159    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
160{
161    fn default() -> Self {
162        Self
163    }
164}
165
166impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> ConstrainedTypeShape
167    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
168{
169    const IRI: &'static str = "https://uor.foundation/type/ConstrainedType";
170    const SITE_COUNT: usize = MAX_DEPTH * LEAF_BYTES + 8;
171    const CONSTRAINTS: &'static [ConstraintRef] = &[];
172    #[allow(clippy::cast_possible_truncation)]
173    const CYCLE_SIZE: u64 = 256u64.saturating_pow((MAX_DEPTH * LEAF_BYTES + 8) as u32);
174}
175
176impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> uor_foundation::pipeline::__sdk_seal::Sealed
177    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
178{
179}
180impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> GroundedShape
181    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
182{
183}
184impl<const MAX_DEPTH: usize, const LEAF_BYTES: usize> IntoBindingValue
185    for MerkleProofShape<MAX_DEPTH, LEAF_BYTES>
186{
187    const MAX_BYTES: usize = MAX_DEPTH * LEAF_BYTES + 8;
188
189    fn into_binding_bytes(&self, _out: &mut [u8]) -> Result<usize, ShapeViolation> {
190        Ok(0)
191    }
192}