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/// Maximum leaf count any [`MerkleRoot`] instantiation supports.
43/// Depth-6 binary tree; deeper Merkle commitments compose at the verb
44/// level (per ADR-024) rather than baking into the axis kernel.
45pub const MAX_MERKLE_LEAVES: usize = 64;
46
47/// Maximum leaf byte-width — the largest `HashAxis::MAX_OUTPUT_BYTES`
48/// among standard-library hash impls (64 bytes for SHA-512).
49const MAX_LEAF_WIDTH: usize = 64;
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        if leaf_count > MAX_MERKLE_LEAVES {
100            return Err(shape_violation(
101                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/maxLeaves",
102            ));
103        }
104        // Use a fixed-size working buffer sized for the maximum
105        // supported leaf width across HashAxis impls (64 bytes for
106        // SHA-512). Per-instantiation LEAF_BYTES bounds the actually
107        // used slice.
108        if LEAF_BYTES > MAX_LEAF_WIDTH {
109            return Err(shape_violation(
110                "https://uor.foundation/axis/CommitmentAxis/MerkleRoot/leafBytesInRange",
111            ));
112        }
113        let mut layer = [[0u8; MAX_LEAF_WIDTH]; MAX_MERKLE_LEAVES];
114        for i in 0..leaf_count {
115            layer[i][..LEAF_BYTES].copy_from_slice(&input[i * LEAF_BYTES..(i + 1) * LEAF_BYTES]);
116        }
117        let mut pair_buf = [0u8; 2 * MAX_LEAF_WIDTH];
118        let mut digest_buf = [0u8; MAX_LEAF_WIDTH];
119        let mut len = leaf_count;
120        while len > 1 {
121            let half = len / 2;
122            for i in 0..half {
123                pair_buf[..LEAF_BYTES].copy_from_slice(&layer[2 * i][..LEAF_BYTES]);
124                pair_buf[LEAF_BYTES..2 * LEAF_BYTES]
125                    .copy_from_slice(&layer[2 * i + 1][..LEAF_BYTES]);
126                H::hash(&pair_buf[..2 * LEAF_BYTES], &mut digest_buf[..LEAF_BYTES])?;
127                layer[i][..LEAF_BYTES].copy_from_slice(&digest_buf[..LEAF_BYTES]);
128            }
129            len = half;
130        }
131        out[..LEAF_BYTES].copy_from_slice(&layer[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}