commit_verify/
id.rs

1// Client-side-validation foundation libraries.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems
11// (InDCS), Switzerland. Copyright (C) 2019-2025 Dr Maxim Orlovsky.
12// All rights under the above copyrights are reserved.
13//
14// Licensed under the Apache License, Version 2.0 (the "License"); you may not
15// use this file except in compliance with the License. You may obtain a copy of
16// the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software
21// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
22// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
23// License for the specific language governing permissions and limitations under
24// the License.
25
26use std::collections::{BTreeMap, BTreeSet};
27#[cfg(feature = "vesper")]
28use std::fmt::{self, Display, Formatter};
29use std::hash::Hash;
30
31use amplify::confinement::{Confined, TinyVec, U64 as U64MAX};
32use amplify::Bytes32;
33use sha2::Sha256;
34use strict_encoding::{Sizing, StreamWriter, StrictDumb, StrictEncode, StrictType};
35use strict_types::typesys::TypeFqn;
36
37use crate::{Conceal, DigestExt, MerkleHash, MerkleLeaves, LIB_NAME_COMMIT_VERIFY};
38
39const COMMIT_MAX_LEN: usize = U64MAX;
40
41/// Type of the collection participating in a commitment id creation.
42#[derive(Clone, Eq, PartialEq, Hash, Debug)]
43pub enum CommitColType {
44    /// A vector-type collection (always correspond to a confined variant of
45    /// [`Vec`]).
46    List,
47    /// A set of unique sorted elements (always correspond to a confined variant
48    /// of [`BTreeSet`]).
49    Set,
50    /// A map of unique sorted keys to values (always correspond to a confined
51    /// variant of [`BTreeMap`]).
52    Map {
53        /// A fully qualified strict type name for the keys.
54        key: TypeFqn,
55    },
56}
57
58/// Step of the commitment id creation.
59#[derive(Clone, Eq, PartialEq, Hash, Debug)]
60pub enum CommitStep {
61    /// Serialization with [`CommitEngine::commit_to_serialized`].
62    Serialized(TypeFqn),
63
64    /// Serialization with either
65    /// - [`CommitEngine::commit_to_linear_list`],
66    /// - [`CommitEngine::commit_to_linear_set`],
67    /// - [`CommitEngine::commit_to_linear_map`].
68    ///
69    /// A specific type of serialization depends on the first field
70    /// ([`CommitColType`]).
71    Collection(CommitColType, Sizing, TypeFqn),
72
73    /// Serialization with [`CommitEngine::commit_to_hash`].
74    Hashed(TypeFqn),
75
76    /// Serialization with [`CommitEngine::commit_to_merkle`].
77    Merklized(TypeFqn),
78
79    /// Serialization with [`CommitEngine::commit_to_concealed`].
80    Concealed {
81        /// The source type to which the concealment is applied.
82        src: TypeFqn,
83        /// The destination type of the concealment.
84        dst: TypeFqn,
85    },
86}
87
88/// A helper engine used in computing commitment ids.
89#[derive(Clone, Debug)]
90pub struct CommitEngine {
91    finished: bool,
92    hasher: Sha256,
93    layout: TinyVec<CommitStep>,
94}
95
96fn commitment_fqn<T: StrictType>() -> TypeFqn {
97    TypeFqn::with(
98        libname!(T::STRICT_LIB_NAME),
99        T::strict_name().expect("commit encoder can commit only to named types"),
100    )
101}
102
103impl CommitEngine {
104    /// Initialize the engine using a type-specific tag string.
105    ///
106    /// The tag should be in a form of a valid URN, ending with a fragment
107    /// specifying the date of the tag, or other form of versioning.
108    pub fn new(tag: &'static str) -> Self {
109        Self {
110            finished: false,
111            hasher: Sha256::from_tag(tag),
112            layout: empty!(),
113        }
114    }
115
116    fn inner_commit_to<T: StrictEncode, const MAX_LEN: usize>(&mut self, value: &T) {
117        debug_assert!(!self.finished);
118        let writer = StreamWriter::new::<MAX_LEN>(&mut self.hasher);
119        let ok = value.strict_write(writer).is_ok();
120        debug_assert!(ok);
121    }
122
123    /// Add a commitment to a strict-encoded value.
124    pub fn commit_to_serialized<T: StrictEncode>(&mut self, value: &T) {
125        let fqn = commitment_fqn::<T>();
126        debug_assert!(
127            Some(&fqn.name) != MerkleHash::strict_name().as_ref() ||
128                fqn.lib.as_str() != MerkleHash::STRICT_LIB_NAME,
129            "do not use `commit_to_serialized` for merklized collections, use `commit_to_merkle` \
130             instead"
131        );
132        debug_assert!(
133            Some(&fqn.name) != StrictHash::strict_name().as_ref() ||
134                fqn.lib.as_str() != StrictHash::STRICT_LIB_NAME,
135            "do not use `commit_to_serialized` for StrictHash types, use `commit_to_hash` instead"
136        );
137        self.layout
138            .push(CommitStep::Serialized(fqn))
139            .expect("too many fields for commitment");
140
141        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&value);
142    }
143
144    /// Add a commitment to a strict-encoded optional value.
145    pub fn commit_to_option<T: StrictEncode + StrictDumb>(&mut self, value: &Option<T>) {
146        let fqn = commitment_fqn::<T>();
147        self.layout
148            .push(CommitStep::Serialized(fqn))
149            .expect("too many fields for commitment");
150
151        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&value);
152    }
153
154    /// Add a commitment to a value which supports [`StrictHash`]ing.
155    pub fn commit_to_hash<T: CommitEncode<CommitmentId = StrictHash> + StrictType>(
156        &mut self,
157        value: &T,
158    ) {
159        let fqn = commitment_fqn::<T>();
160        self.layout
161            .push(CommitStep::Hashed(fqn))
162            .expect("too many fields for commitment");
163
164        self.inner_commit_to::<_, 32>(&value.commit_id());
165    }
166
167    /// Add a commitment to a merklized collection.
168    ///
169    /// The collection must implement [`MerkleLeaves`] trait.
170    pub fn commit_to_merkle<T: MerkleLeaves>(&mut self, value: &T)
171    where T::Leaf: StrictType {
172        let fqn = commitment_fqn::<T::Leaf>();
173        self.layout
174            .push(CommitStep::Merklized(fqn))
175            .expect("too many fields for commitment");
176
177        let root = MerkleHash::merklize(value);
178        self.inner_commit_to::<_, 32>(&root);
179    }
180
181    /// Add a commitment to a type which supports [`Conceal`] procedure (hiding
182    /// some of its data).
183    ///
184    /// First, the conceal procedure is called for the `value`, and then the
185    /// resulting data are serialized using strict encoding.
186    pub fn commit_to_concealed<T>(&mut self, value: &T)
187    where
188        T: Conceal + StrictType,
189        T::Concealed: StrictEncode,
190    {
191        let src = commitment_fqn::<T>();
192        let dst = commitment_fqn::<T::Concealed>();
193        self.layout
194            .push(CommitStep::Concealed {
195                src,
196                dst: dst.clone(),
197            })
198            .expect("too many fields for commitment");
199
200        let concealed = value.conceal();
201        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&concealed);
202    }
203
204    /// Add a commitment to a vector collection.
205    ///
206    /// Does not use merklization and encodes each element as strict encoding
207    /// binary data right in to the hasher.
208    ///
209    /// Additionally to all elements, commits to the length of the collection
210    /// and minimal and maximal dimensions of the confinement.
211    pub fn commit_to_linear_list<T, const MIN: usize, const MAX: usize>(
212        &mut self,
213        collection: &Confined<Vec<T>, MIN, MAX>,
214    ) where
215        T: StrictEncode + StrictDumb,
216    {
217        let fqn = commitment_fqn::<T>();
218        let step =
219            CommitStep::Collection(CommitColType::List, Sizing::new(MIN as u64, MAX as u64), fqn);
220        self.layout
221            .push(step)
222            .expect("too many fields for commitment");
223        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
224    }
225
226    /// Add a commitment to a set collection.
227    ///
228    /// Does not use merklization and encodes each element as strict encoding
229    /// binary data right in to the hasher.
230    ///
231    /// Additionally to all elements, commits to the length of the collection
232    /// and minimal and maximal dimensions of the confinement.
233    pub fn commit_to_linear_set<T, const MIN: usize, const MAX: usize>(
234        &mut self,
235        collection: &Confined<BTreeSet<T>, MIN, MAX>,
236    ) where
237        T: Ord + StrictEncode + StrictDumb,
238    {
239        let fqn = commitment_fqn::<T>();
240        let step =
241            CommitStep::Collection(CommitColType::Set, Sizing::new(MIN as u64, MAX as u64), fqn);
242        self.layout
243            .push(step)
244            .expect("too many fields for commitment");
245        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
246    }
247
248    /// Add a commitment to a mapped collection.
249    ///
250    /// Does not use merklization and encodes each element as strict encoding
251    /// binary data right in to the hasher.
252    ///
253    /// Additionally to all keys and values, commits to the length of the
254    /// collection and minimal and maximal dimensions of the confinement.
255    pub fn commit_to_linear_map<K, V, const MIN: usize, const MAX: usize>(
256        &mut self,
257        collection: &Confined<BTreeMap<K, V>, MIN, MAX>,
258    ) where
259        K: Ord + Hash + StrictEncode + StrictDumb,
260        V: StrictEncode + StrictDumb,
261    {
262        let key_fqn = commitment_fqn::<K>();
263        let val_fqn = commitment_fqn::<V>();
264        let step = CommitStep::Collection(
265            CommitColType::Map { key: key_fqn },
266            Sizing::new(MIN as u64, MAX as u64),
267            val_fqn,
268        );
269        self.layout
270            .push(step)
271            .expect("too many fields for commitment");
272        self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
273    }
274
275    /// Get a reference for the underlying sequence of commit steps.
276    pub fn as_layout(&mut self) -> &[CommitStep] {
277        self.finished = true;
278        self.layout.as_ref()
279    }
280
281    /// Convert into the underlying sequence of commit steps.
282    pub fn into_layout(self) -> TinyVec<CommitStep> { self.layout }
283
284    /// Mark the procedure as completed, preventing any further data from being
285    /// added.
286    pub fn set_finished(&mut self) { self.finished = true; }
287
288    /// Complete the commitment returning the resulting hash.
289    pub fn finish(self) -> Sha256 { self.hasher }
290
291    /// Complete the commitment returning the resulting hash and the description
292    /// of all commitment steps performed during the procedure.
293    pub fn finish_layout(self) -> (Sha256, TinyVec<CommitStep>) { (self.hasher, self.layout) }
294}
295
296/// A trait for types supporting commit-encode procedure.
297///
298/// The procedure is used to generate a cryptographic deterministic commitment
299/// to data encoded in a binary form.
300///
301/// Later the commitment can be used to produce [`CommitmentId`] (which does a
302/// tagged hash of the commitment).
303pub trait CommitEncode {
304    /// Type of the resulting commitment.
305    type CommitmentId: CommitmentId;
306
307    /// Encodes the data for the commitment by writing them directly into a
308    /// [`std::io::Write`] writer instance
309    fn commit_encode(&self, e: &mut CommitEngine);
310}
311
312/// The description of the commitment layout used in production of
313/// [`CommitmentId`] (or other users of [`CommitEncode`]).
314///
315/// The layout description is useful in producing provably correct documentation
316/// of the commitment process for a specific type. For instance, this library
317/// uses it to generate a description of commitments in [Vesper] language.
318///
319/// [Vesper]: https://vesper-lang.org
320#[derive(Getters, Clone, Eq, PartialEq, Hash, Debug)]
321pub struct CommitLayout {
322    idty: TypeFqn,
323    ty: TypeFqn,
324    #[getter(as_copy)]
325    tag: &'static str,
326    fields: TinyVec<CommitStep>,
327}
328
329#[cfg(feature = "vesper")]
330impl Display for CommitLayout {
331    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
332        Display::fmt(&self.to_vesper().display(), f)
333    }
334}
335
336/// A definition of a resulting commitment type, which represent a unique
337/// identifier of the underlying data.
338pub trait CommitmentId: Copy + Ord + From<Sha256> + StrictType {
339    /// A tag string used in initializing SHA256 hasher.
340    const TAG: &'static str;
341}
342
343/// A trait adding blanked implementation generating [`CommitmentLayout`] for
344/// any type implementing [`CommitEncode`].
345pub trait CommitmentLayout: CommitEncode {
346    /// Generate a descriptive commitment layout, which includes a description
347    /// of each encoded field and the used hashing strategies.
348    fn commitment_layout() -> CommitLayout;
349}
350
351impl<T> CommitmentLayout for T
352where T: CommitEncode + StrictType + StrictDumb
353{
354    fn commitment_layout() -> CommitLayout {
355        let dumb = Self::strict_dumb();
356        let fields = dumb.commit().into_layout();
357        CommitLayout {
358            ty: commitment_fqn::<T>(),
359            idty: TypeFqn::with(
360                libname!(Self::CommitmentId::STRICT_LIB_NAME),
361                Self::CommitmentId::strict_name()
362                    .expect("commitment types must have explicit type name"),
363            ),
364            tag: T::CommitmentId::TAG,
365            fields,
366        }
367    }
368}
369
370/// High-level API used in client-side validation for producing a single
371/// commitment to the data, which includes running all necessary procedures like
372/// concealment with [`Conceal`], merklization, strict encoding,
373/// wrapped into [`CommitEncode`], followed by the actual commitment to its
374/// output.
375///
376/// The trait is separate from the `CommitEncode` to prevent custom
377/// implementation of its methods, since `CommitId` can't be manually
378/// implemented for any type since it has a generic blanket implementation.
379pub trait CommitId: CommitEncode {
380    #[doc(hidden)]
381    fn commit(&self) -> CommitEngine;
382
383    /// Performs commitment to client-side-validated data
384    fn commit_id(&self) -> Self::CommitmentId;
385}
386
387impl<T: CommitEncode> CommitId for T {
388    fn commit(&self) -> CommitEngine {
389        let mut engine = CommitEngine::new(T::CommitmentId::TAG);
390        self.commit_encode(&mut engine);
391        engine.set_finished();
392        engine
393    }
394
395    fn commit_id(&self) -> Self::CommitmentId { self.commit().finish().into() }
396}
397
398/// A commitment to the strict encoded-representation of any data.
399///
400/// It is created using tagged hash with [`StrictHash::TAG`] value.
401#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
402#[wrapper(Deref, BorrowSlice, Display, FromStr, Hex, Index, RangeOps)]
403#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
404#[strict_type(lib = LIB_NAME_COMMIT_VERIFY)]
405#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
406pub struct StrictHash(
407    #[from]
408    #[from([u8; 32])]
409    Bytes32,
410);
411
412impl CommitmentId for StrictHash {
413    const TAG: &'static str = "urn:ubideco:strict-types:value-hash#2024-02-10";
414}
415
416impl From<Sha256> for StrictHash {
417    fn from(hash: Sha256) -> Self { hash.finish().into() }
418}
419
420#[cfg(test)]
421pub(crate) mod tests {
422    #![cfg_attr(coverage_nightly, coverage(off))]
423    use super::*;
424
425    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
426    #[derive(StrictType, StrictEncode, StrictDecode)]
427    #[strict_type(lib = "Test")]
428    pub struct DumbConceal(u8);
429
430    impl Conceal for DumbConceal {
431        type Concealed = DumbHash;
432        fn conceal(&self) -> Self::Concealed { DumbHash(0xFF - self.0) }
433    }
434
435    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
436    #[derive(StrictType, StrictEncode, StrictDecode)]
437    #[strict_type(lib = "Test")]
438    #[derive(CommitEncode)]
439    #[commit_encode(crate = self, strategy = strict, id = StrictHash)]
440    pub struct DumbHash(u8);
441
442    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
443    #[derive(StrictType, StrictEncode, StrictDecode)]
444    #[strict_type(lib = "Test")]
445    #[derive(CommitEncode)]
446    #[commit_encode(crate = self, strategy = strict, id = MerkleHash)]
447    pub struct DumbMerkle(u8);
448
449    #[test]
450    fn commit_engine_strict() {
451        let val = 123u64;
452        let mut engine = CommitEngine::new("test");
453        engine.commit_to_serialized(&val);
454        engine.set_finished();
455        let (id, layout) = engine.finish_layout();
456        assert_eq!(layout, tiny_vec![CommitStep::Serialized(TypeFqn::from("_.U64"))]);
457        assert_eq!(
458            id.finish(),
459            Sha256::from_tag("test")
460                .with_raw(&val.to_le_bytes())
461                .finish()
462        );
463    }
464
465    #[test]
466    fn commit_engine_option() {
467        let val = Some(128u64);
468        let mut engine = CommitEngine::new("test");
469        engine.commit_to_option(&val);
470        engine.set_finished();
471        let (id, layout) = engine.finish_layout();
472        assert_eq!(layout, tiny_vec![CommitStep::Serialized(TypeFqn::from("_.U64"))]);
473        assert_eq!(
474            id.finish(),
475            Sha256::from_tag("test")
476                .with_raw(b"\x01\x80\x00\x00\x00\x00\x00\x00\x00")
477                .finish()
478        );
479    }
480
481    #[test]
482    fn commit_engine_conceal() {
483        let val = DumbConceal(123);
484        let mut engine = CommitEngine::new("test");
485        engine.commit_to_concealed(&val);
486        engine.set_finished();
487        let (id, layout) = engine.finish_layout();
488        assert_eq!(layout, tiny_vec![CommitStep::Concealed {
489            src: TypeFqn::from("Test.DumbConceal"),
490            dst: TypeFqn::from("Test.DumbHash")
491        },]);
492        assert_eq!(
493            id.finish(),
494            Sha256::from_tag("test")
495                .with_raw(&(0xFF - val.0).to_le_bytes())
496                .finish()
497        );
498    }
499
500    #[test]
501    fn commit_engine_hash() {
502        let val = DumbHash(10);
503        let mut engine = CommitEngine::new("test");
504        engine.commit_to_hash(&val);
505        engine.set_finished();
506        let (id, layout) = engine.finish_layout();
507        assert_eq!(layout, tiny_vec![CommitStep::Hashed(TypeFqn::from("Test.DumbHash"))]);
508        assert_eq!(
509            id.finish(),
510            Sha256::from_tag("test")
511                .with_raw(val.commit_id().as_slice())
512                .finish()
513        );
514    }
515
516    #[test]
517    fn commit_engine_merkle() {
518        let val = [DumbMerkle(1), DumbMerkle(2), DumbMerkle(3), DumbMerkle(4)];
519        let mut engine = CommitEngine::new("test");
520        engine.commit_to_merkle(&val);
521        engine.set_finished();
522        let (id, layout) = engine.finish_layout();
523        assert_eq!(layout, tiny_vec![CommitStep::Merklized(TypeFqn::from("Test.DumbMerkle"))]);
524        assert_eq!(
525            id.finish(),
526            Sha256::from_tag("test")
527                .with_raw(MerkleHash::merklize(&val).as_slice())
528                .finish()
529        );
530    }
531
532    #[test]
533    fn commit_engine_list() {
534        let val = tiny_vec![0, 1, 2u8];
535        let mut engine = CommitEngine::new("test");
536        engine.commit_to_linear_list(&val);
537        engine.set_finished();
538        let (id, layout) = engine.finish_layout();
539        assert_eq!(layout, tiny_vec![CommitStep::Collection(
540            CommitColType::List,
541            Sizing::new(0, 0xFF),
542            TypeFqn::from("_.U8")
543        )]);
544        assert_eq!(
545            id.finish(),
546            Sha256::from_tag("test")
547                .with_len::<0xFF>(b"\x00\x01\x02")
548                .finish()
549        );
550    }
551
552    #[test]
553    fn commit_engine_set() {
554        let val = tiny_bset![0, 1, 2u8];
555        let mut engine = CommitEngine::new("test");
556        engine.commit_to_linear_set(&val);
557        engine.set_finished();
558        let (id, layout) = engine.finish_layout();
559        assert_eq!(layout, tiny_vec![CommitStep::Collection(
560            CommitColType::Set,
561            Sizing::new(0, 0xFF),
562            TypeFqn::from("_.U8")
563        )]);
564        assert_eq!(
565            id.finish(),
566            Sha256::from_tag("test")
567                .with_len::<0xFF>(b"\x00\x01\x02")
568                .finish()
569        );
570    }
571
572    #[test]
573    fn commit_engine_map() {
574        let val = tiny_bmap! {0 => tn!("A"), 1 => tn!("B"), 2u8 => tn!("C")};
575        let mut engine = CommitEngine::new("test");
576        engine.commit_to_linear_map(&val);
577        engine.set_finished();
578        let (id, layout) = engine.finish_layout();
579        assert_eq!(layout, tiny_vec![CommitStep::Collection(
580            CommitColType::Map {
581                key: TypeFqn::from("_.U8")
582            },
583            Sizing::new(0, 0xFF),
584            TypeFqn::from("StrictTypes.TypeName")
585        )]);
586        assert_eq!(
587            id.finish(),
588            Sha256::from_tag("test")
589                .with_raw(b"\x03\x00\x01A\x01\x01B\x02\x01C")
590                .finish()
591        );
592    }
593
594    #[test]
595    #[should_panic]
596    fn commit_engine_reject_hash() {
597        let val = StrictHash::strict_dumb();
598        let mut engine = CommitEngine::new("test");
599        engine.commit_to_serialized(&val);
600    }
601
602    #[test]
603    #[should_panic]
604    fn commit_engine_reject_merkle() {
605        let val = MerkleHash::strict_dumb();
606        let mut engine = CommitEngine::new("test");
607        engine.commit_to_serialized(&val);
608    }
609}