1use 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#[derive(Clone, Eq, PartialEq, Hash, Debug)]
43pub enum CommitColType {
44 List,
47 Set,
50 Map {
53 key: TypeFqn,
55 },
56}
57
58#[derive(Clone, Eq, PartialEq, Hash, Debug)]
60pub enum CommitStep {
61 Serialized(TypeFqn),
63
64 Collection(CommitColType, Sizing, TypeFqn),
72
73 Hashed(TypeFqn),
75
76 Merklized(TypeFqn),
78
79 Concealed {
81 src: TypeFqn,
83 dst: TypeFqn,
85 },
86}
87
88#[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 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 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 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 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 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 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 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 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 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 pub fn as_layout(&mut self) -> &[CommitStep] {
277 self.finished = true;
278 self.layout.as_ref()
279 }
280
281 pub fn into_layout(self) -> TinyVec<CommitStep> { self.layout }
283
284 pub fn set_finished(&mut self) { self.finished = true; }
287
288 pub fn finish(self) -> Sha256 { self.hasher }
290
291 pub fn finish_layout(self) -> (Sha256, TinyVec<CommitStep>) { (self.hasher, self.layout) }
294}
295
296pub trait CommitEncode {
304 type CommitmentId: CommitmentId;
306
307 fn commit_encode(&self, e: &mut CommitEngine);
310}
311
312#[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
336pub trait CommitmentId: Copy + Ord + From<Sha256> + StrictType {
339 const TAG: &'static str;
341}
342
343pub trait CommitmentLayout: CommitEncode {
346 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
370pub trait CommitId: CommitEncode {
380 #[doc(hidden)]
381 fn commit(&self) -> CommitEngine;
382
383 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#[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}