use std::collections::{BTreeMap, BTreeSet};
#[cfg(feature = "vesper")]
use std::fmt::{self, Display, Formatter};
use std::hash::Hash;
use amplify::confinement::{Confined, TinyVec, U64 as U64MAX};
use amplify::Bytes32;
use sha2::Sha256;
use strict_encoding::{Sizing, StreamWriter, StrictDumb, StrictEncode, StrictType};
use strict_types::typesys::TypeFqn;
use crate::{Conceal, DigestExt, MerkleHash, MerkleLeaves, LIB_NAME_COMMIT_VERIFY};
const COMMIT_MAX_LEN: usize = U64MAX;
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum CommitColType {
List,
Set,
Map {
key: TypeFqn,
},
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum CommitStep {
Serialized(TypeFqn),
Collection(CommitColType, Sizing, TypeFqn),
Hashed(TypeFqn),
Merklized(TypeFqn),
Concealed {
src: TypeFqn,
dst: TypeFqn,
},
}
#[derive(Clone, Debug)]
pub struct CommitEngine {
finished: bool,
hasher: Sha256,
layout: TinyVec<CommitStep>,
}
fn commitment_fqn<T: StrictType>() -> TypeFqn {
TypeFqn::with(
libname!(T::STRICT_LIB_NAME),
T::strict_name().expect("commit encoder can commit only to named types"),
)
}
impl CommitEngine {
pub fn new(tag: &'static str) -> Self {
Self {
finished: false,
hasher: Sha256::from_tag(tag),
layout: empty!(),
}
}
fn inner_commit_to<T: StrictEncode, const MAX_LEN: usize>(&mut self, value: &T) {
debug_assert!(!self.finished);
let writer = StreamWriter::new::<MAX_LEN>(&mut self.hasher);
let ok = value.strict_write(writer).is_ok();
debug_assert!(ok);
}
pub fn commit_to_serialized<T: StrictEncode>(&mut self, value: &T) {
let fqn = commitment_fqn::<T>();
debug_assert!(
Some(&fqn.name) != MerkleHash::strict_name().as_ref() ||
fqn.lib.as_str() != MerkleHash::STRICT_LIB_NAME,
"do not use `commit_to_serialized` for merklized collections, use `commit_to_merkle` \
instead"
);
debug_assert!(
Some(&fqn.name) != StrictHash::strict_name().as_ref() ||
fqn.lib.as_str() != StrictHash::STRICT_LIB_NAME,
"do not use `commit_to_serialized` for StrictHash types, use `commit_to_hash` instead"
);
self.layout
.push(CommitStep::Serialized(fqn))
.expect("too many fields for commitment");
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&value);
}
pub fn commit_to_option<T: StrictEncode + StrictDumb>(&mut self, value: &Option<T>) {
let fqn = commitment_fqn::<T>();
self.layout
.push(CommitStep::Serialized(fqn))
.expect("too many fields for commitment");
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&value);
}
pub fn commit_to_hash<T: CommitEncode<CommitmentId = StrictHash> + StrictType>(
&mut self,
value: &T,
) {
let fqn = commitment_fqn::<T>();
self.layout
.push(CommitStep::Hashed(fqn))
.expect("too many fields for commitment");
self.inner_commit_to::<_, 32>(&value.commit_id());
}
pub fn commit_to_merkle<T: MerkleLeaves>(&mut self, value: &T)
where T::Leaf: StrictType {
let fqn = commitment_fqn::<T::Leaf>();
self.layout
.push(CommitStep::Merklized(fqn))
.expect("too many fields for commitment");
let root = MerkleHash::merklize(value);
self.inner_commit_to::<_, 32>(&root);
}
pub fn commit_to_concealed<T>(&mut self, value: &T)
where
T: Conceal + StrictType,
T::Concealed: StrictEncode,
{
let src = commitment_fqn::<T>();
let dst = commitment_fqn::<T::Concealed>();
self.layout
.push(CommitStep::Concealed {
src,
dst: dst.clone(),
})
.expect("too many fields for commitment");
let concealed = value.conceal();
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&concealed);
}
pub fn commit_to_linear_list<T, const MIN: usize, const MAX: usize>(
&mut self,
collection: &Confined<Vec<T>, MIN, MAX>,
) where
T: StrictEncode + StrictDumb,
{
let fqn = commitment_fqn::<T>();
let step =
CommitStep::Collection(CommitColType::List, Sizing::new(MIN as u64, MAX as u64), fqn);
self.layout
.push(step)
.expect("too many fields for commitment");
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
}
pub fn commit_to_linear_set<T, const MIN: usize, const MAX: usize>(
&mut self,
collection: &Confined<BTreeSet<T>, MIN, MAX>,
) where
T: Ord + StrictEncode + StrictDumb,
{
let fqn = commitment_fqn::<T>();
let step =
CommitStep::Collection(CommitColType::Set, Sizing::new(MIN as u64, MAX as u64), fqn);
self.layout
.push(step)
.expect("too many fields for commitment");
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
}
pub fn commit_to_linear_map<K, V, const MIN: usize, const MAX: usize>(
&mut self,
collection: &Confined<BTreeMap<K, V>, MIN, MAX>,
) where
K: Ord + Hash + StrictEncode + StrictDumb,
V: StrictEncode + StrictDumb,
{
let key_fqn = commitment_fqn::<K>();
let val_fqn = commitment_fqn::<V>();
let step = CommitStep::Collection(
CommitColType::Map { key: key_fqn },
Sizing::new(MIN as u64, MAX as u64),
val_fqn,
);
self.layout
.push(step)
.expect("too many fields for commitment");
self.inner_commit_to::<_, COMMIT_MAX_LEN>(&collection);
}
pub fn as_layout(&mut self) -> &[CommitStep] {
self.finished = true;
self.layout.as_ref()
}
pub fn into_layout(self) -> TinyVec<CommitStep> { self.layout }
pub fn set_finished(&mut self) { self.finished = true; }
pub fn finish(self) -> Sha256 { self.hasher }
pub fn finish_layout(self) -> (Sha256, TinyVec<CommitStep>) { (self.hasher, self.layout) }
}
pub trait CommitEncode {
type CommitmentId: CommitmentId;
fn commit_encode(&self, e: &mut CommitEngine);
}
#[derive(Getters, Clone, Eq, PartialEq, Hash, Debug)]
pub struct CommitLayout {
idty: TypeFqn,
ty: TypeFqn,
#[getter(as_copy)]
tag: &'static str,
fields: TinyVec<CommitStep>,
}
#[cfg(feature = "vesper")]
impl Display for CommitLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.to_vesper().display(), f)
}
}
pub trait CommitmentId: Copy + Ord + From<Sha256> + StrictType {
const TAG: &'static str;
}
pub trait CommitmentLayout: CommitEncode {
fn commitment_layout() -> CommitLayout;
}
impl<T> CommitmentLayout for T
where T: CommitEncode + StrictType + StrictDumb
{
fn commitment_layout() -> CommitLayout {
let dumb = Self::strict_dumb();
let fields = dumb.commit().into_layout();
CommitLayout {
ty: commitment_fqn::<T>(),
idty: TypeFqn::with(
libname!(Self::CommitmentId::STRICT_LIB_NAME),
Self::CommitmentId::strict_name()
.expect("commitment types must have explicit type name"),
),
tag: T::CommitmentId::TAG,
fields,
}
}
}
pub trait CommitId: CommitEncode {
#[doc(hidden)]
fn commit(&self) -> CommitEngine;
fn commit_id(&self) -> Self::CommitmentId;
}
impl<T: CommitEncode> CommitId for T {
fn commit(&self) -> CommitEngine {
let mut engine = CommitEngine::new(T::CommitmentId::TAG);
self.commit_encode(&mut engine);
engine.set_finished();
engine
}
fn commit_id(&self) -> Self::CommitmentId { self.commit().finish().into() }
}
#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
#[wrapper(Deref, BorrowSlice, Display, FromStr, Hex, Index, RangeOps)]
#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_COMMIT_VERIFY)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct StrictHash(
#[from]
#[from([u8; 32])]
Bytes32,
);
impl CommitmentId for StrictHash {
const TAG: &'static str = "urn:ubideco:strict-types:value-hash#2024-02-10";
}
impl From<Sha256> for StrictHash {
fn from(hash: Sha256) -> Self { hash.finish().into() }
}
#[cfg(test)]
pub(crate) mod tests {
#![cfg_attr(coverage_nightly, coverage(off))]
use super::*;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = "Test")]
pub struct DumbConceal(u8);
impl Conceal for DumbConceal {
type Concealed = DumbHash;
fn conceal(&self) -> Self::Concealed { DumbHash(0xFF - self.0) }
}
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = "Test")]
#[derive(CommitEncode)]
#[commit_encode(crate = self, strategy = strict, id = StrictHash)]
pub struct DumbHash(u8);
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = "Test")]
#[derive(CommitEncode)]
#[commit_encode(crate = self, strategy = strict, id = MerkleHash)]
pub struct DumbMerkle(u8);
#[test]
fn commit_engine_strict() {
let val = 123u64;
let mut engine = CommitEngine::new("test");
engine.commit_to_serialized(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Serialized(TypeFqn::from("_.U64"))]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(&val.to_le_bytes())
.finish()
);
}
#[test]
fn commit_engine_option() {
let val = Some(128u64);
let mut engine = CommitEngine::new("test");
engine.commit_to_option(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Serialized(TypeFqn::from("_.U64"))]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(b"\x01\x80\x00\x00\x00\x00\x00\x00\x00")
.finish()
);
}
#[test]
fn commit_engine_conceal() {
let val = DumbConceal(123);
let mut engine = CommitEngine::new("test");
engine.commit_to_concealed(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Concealed {
src: TypeFqn::from("Test.DumbConceal"),
dst: TypeFqn::from("Test.DumbHash")
},]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(&(0xFF - val.0).to_le_bytes())
.finish()
);
}
#[test]
fn commit_engine_hash() {
let val = DumbHash(10);
let mut engine = CommitEngine::new("test");
engine.commit_to_hash(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Hashed(TypeFqn::from("Test.DumbHash"))]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(val.commit_id().as_slice())
.finish()
);
}
#[test]
fn commit_engine_merkle() {
let val = [DumbMerkle(1), DumbMerkle(2), DumbMerkle(3), DumbMerkle(4)];
let mut engine = CommitEngine::new("test");
engine.commit_to_merkle(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Merklized(TypeFqn::from("Test.DumbMerkle"))]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(MerkleHash::merklize(&val).as_slice())
.finish()
);
}
#[test]
fn commit_engine_list() {
let val = tiny_vec![0, 1, 2u8];
let mut engine = CommitEngine::new("test");
engine.commit_to_linear_list(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Collection(
CommitColType::List,
Sizing::new(0, 0xFF),
TypeFqn::from("_.U8")
)]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_len::<0xFF>(b"\x00\x01\x02")
.finish()
);
}
#[test]
fn commit_engine_set() {
let val = tiny_bset![0, 1, 2u8];
let mut engine = CommitEngine::new("test");
engine.commit_to_linear_set(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Collection(
CommitColType::Set,
Sizing::new(0, 0xFF),
TypeFqn::from("_.U8")
)]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_len::<0xFF>(b"\x00\x01\x02")
.finish()
);
}
#[test]
fn commit_engine_map() {
let val = tiny_bmap! {0 => tn!("A"), 1 => tn!("B"), 2u8 => tn!("C")};
let mut engine = CommitEngine::new("test");
engine.commit_to_linear_map(&val);
engine.set_finished();
let (id, layout) = engine.finish_layout();
assert_eq!(layout, tiny_vec![CommitStep::Collection(
CommitColType::Map {
key: TypeFqn::from("_.U8")
},
Sizing::new(0, 0xFF),
TypeFqn::from("StrictTypes.TypeName")
)]);
assert_eq!(
id.finish(),
Sha256::from_tag("test")
.with_raw(b"\x03\x00\x01A\x01\x01B\x02\x01C")
.finish()
);
}
#[test]
#[should_panic]
fn commit_engine_reject_hash() {
let val = StrictHash::strict_dumb();
let mut engine = CommitEngine::new("test");
engine.commit_to_serialized(&val);
}
#[test]
#[should_panic]
fn commit_engine_reject_merkle() {
let val = MerkleHash::strict_dumb();
let mut engine = CommitEngine::new("test");
engine.commit_to_serialized(&val);
}
}