use core::error::Error;
use core::fmt::Debug;
use amplify::{ByteArray, Bytes, Bytes32};
use bc::{Outpoint, Tx, Txid};
use commit_verify::{
CommitId, ConvolveVerifyError, DigestExt, EmbedVerifyError, ReservedBytes, Sha256,
};
use dbc::opret::{OpretError, OpretProof};
use dbc::tapret::TapretProof;
use single_use_seals::{ClientSideWitness, PublishedWitness, SealWitness, SingleUseSeal};
use strict_encoding::{StrictDumb, StrictSum};
use crate::WOutpoint;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, From)]
#[display("{0:x}")]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct Noise(Bytes<40>);
impl Noise {
pub fn with(outpoint: WOutpoint, mut noise_engine: Sha256, nonce: u64) -> Self {
noise_engine.input_raw(&nonce.to_be_bytes());
match outpoint {
WOutpoint::Wout(wout) => {
noise_engine.input_raw(&[WOutpoint::ALL_VARIANTS[0].0]);
noise_engine.input_raw(&wout.to_u32().to_be_bytes());
}
WOutpoint::Extern(outpoint) => {
noise_engine.input_raw(&[WOutpoint::ALL_VARIANTS[1].0]);
noise_engine.input_raw(outpoint.txid.as_ref());
noise_engine.input_raw(&outpoint.vout.to_u32().to_be_bytes());
}
}
let mut noise = [0xFFu8; 40];
noise[..32].copy_from_slice(&noise_engine.finish());
Self(noise.into())
}
}
pub mod mmb {
use amplify::confinement::SmallOrdMap;
use commit_verify::{CommitmentId, DigestExt, Sha256};
use super::*;
#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From, Default)]
#[wrapper(Deref, BorrowSlice, Display, FromStr, Hex, Index, RangeOps)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct Message(
#[from]
#[from([u8; 32])]
Bytes32,
);
#[derive(Wrapper, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From)]
#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct Commitment(
#[from]
#[from([u8; 32])]
Bytes32,
);
impl CommitmentId for Commitment {
const TAG: &'static str = "urn:lnp-bp:mmb:bundle#2024-11-18";
}
impl From<Sha256> for Commitment {
fn from(hasher: Sha256) -> Self { hasher.finish().into() }
}
impl From<Commitment> for mpc::Message {
fn from(msg: Commitment) -> Self { mpc::Message::from_byte_array(msg.to_byte_array()) }
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[derive(CommitEncode)]
#[commit_encode(strategy = strict, id = Commitment)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BundleProof {
pub map: SmallOrdMap<u32, Message>,
}
impl BundleProof {
pub fn verify(&self, seal: Outpoint, msg: Message, tx: &Tx) -> bool {
let Some(input_index) = tx.inputs().position(|input| input.prev_output == seal) else {
return false;
};
let Ok(input_index) = u32::try_from(input_index) else {
return false;
};
let Some(expected) = self.map.get(&input_index) else {
return false;
};
*expected == msg
}
}
}
pub mod mpc {
use amplify::confinement::MediumOrdMap;
use amplify::num::u5;
use amplify::ByteArray;
pub use commit_verify::mpc::{
Commitment, Error, InvalidProof, Leaf, LeafNotKnown, MergeError, MerkleBlock,
MerkleConcealed, MerkleProof, MerkleTree, Message, Method, Proof, ProtocolId,
MPC_MINIMAL_DEPTH,
};
use commit_verify::{CommitId, TryCommitVerify};
use crate::mmb;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE, tags = custom, dumb = Self::Single(strict_dumb!()))]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase", untagged)
)]
pub enum MessageSource {
#[from]
#[strict_type(tag = 1)]
Single(Message),
#[from]
#[strict_type(tag = 2)]
Mmb(mmb::BundleProof),
}
impl MessageSource {
pub fn mpc_message(&self) -> Message {
match self {
MessageSource::Single(message) => *message,
MessageSource::Mmb(proof) => {
Message::from_byte_array(proof.commit_id().to_byte_array())
}
}
}
}
#[derive(
Wrapper, WrapperMut, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, From
)]
#[wrapper(Deref)]
#[wrapper_mut(DerefMut)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
pub struct MessageMap(MediumOrdMap<ProtocolId, MessageSource>);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
pub struct Source {
pub min_depth: u5,
pub entropy: u64,
pub messages: MessageMap,
}
impl Source {
pub fn into_merkle_tree(self) -> Result<MerkleTree, Error> {
let messages = self.messages.0.iter().map(|(id, src)| {
let msg = src.mpc_message();
(*id, msg)
});
let source = commit_verify::mpc::MultiSource {
method: Method::Sha256t,
min_depth: self.min_depth,
messages: MediumOrdMap::from_iter_checked(messages),
static_entropy: Some(self.entropy),
};
MerkleTree::try_commit(&source)
}
}
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
pub struct Anchor {
pub mmb_proof: mmb::BundleProof,
pub mpc_protocol: mpc::ProtocolId,
pub mpc_proof: mpc::MerkleProof,
pub dbc_proof: Option<TapretProof>,
#[cfg_attr(feature = "serde", serde(skip))]
pub fallback_proof: ReservedBytes<1>,
}
impl Anchor {
pub fn is_fallback(&self) -> bool { false }
pub fn verify_fallback(&self) -> Result<(), AnchorError> { Ok(()) }
}
pub struct Proof {
pub mpc_commit: mpc::Commitment,
pub dbc_proof: Option<TapretProof>,
}
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
#[display(inner)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE, tags = custom)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
pub enum TxoSealExt {
#[strict_type(tag = 0)]
Noise(Noise),
#[strict_type(tag = 1)]
Fallback(Outpoint),
}
impl StrictDumb for TxoSealExt {
fn strict_dumb() -> Self { TxoSealExt::Noise(Noise::from(Bytes::from_byte_array([0u8; 40]))) }
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display)]
#[display("{primary}/{secondary}")]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TxoSeal {
pub primary: Outpoint,
pub secondary: TxoSealExt,
}
impl SingleUseSeal for TxoSeal {
type Message = mmb::Message;
type PubWitness = Tx;
type CliWitness = Anchor;
fn is_included(&self, message: Self::Message, witness: &SealWitness<Self>) -> bool {
match self.secondary {
TxoSealExt::Noise(_) | TxoSealExt::Fallback(_) if !witness.client.is_fallback() => {
witness.client.mmb_proof.verify(self.primary, message, &witness.published)
}
TxoSealExt::Fallback(fallback) => {
witness.client.mmb_proof.verify(fallback, message, &witness.published)
}
TxoSealExt::Noise(_) => false,
}
}
}
impl PublishedWitness<TxoSeal> for Tx {
type PubId = Txid;
type Error = TxoSealError;
fn pub_id(&self) -> Txid { self.txid() }
fn verify_commitment(&self, proof: Proof) -> Result<(), Self::Error> {
let out = self
.outputs()
.find(|out| out.script_pubkey.is_op_return() || out.script_pubkey.is_p2tr())
.ok_or(TxoSealError::NoOutput)?;
if out.script_pubkey.is_op_return() {
if proof.dbc_proof.is_none() {
OpretProof::default().verify(&proof.mpc_commit, self).map_err(TxoSealError::from)
} else {
Err(TxoSealError::InvalidProofType)
}
} else if let Some(ref dbc_proof) = proof.dbc_proof {
dbc_proof.verify(&proof.mpc_commit, self).map_err(TxoSealError::from)
} else {
Err(TxoSealError::NoTapretProof)
}
}
}
impl ClientSideWitness for Anchor {
type Proof = Proof;
type Seal = TxoSeal;
type Error = AnchorError;
fn convolve_commit(&self, mmb_message: mmb::Message) -> Result<Proof, Self::Error> {
self.verify_fallback()?;
if self.mmb_proof.map.values().all(|msg| *msg != mmb_message) {
return Err(AnchorError::Mmb(mmb_message));
}
let bundle_id = self.mmb_proof.commit_id();
let mpc_message = mpc::Message::from_byte_array(bundle_id.to_byte_array());
let mpc_commit = self.mpc_proof.convolve(self.mpc_protocol, mpc_message)?;
Ok(Proof {
mpc_commit,
dbc_proof: self.dbc_proof.clone(),
})
}
fn merge(&mut self, other: Self) -> Result<(), impl Error>
where Self: Sized {
if self.mpc_protocol != other.mpc_protocol
|| self.mpc_proof != other.mpc_proof
|| self.dbc_proof != other.dbc_proof
|| self.fallback_proof != other.fallback_proof
|| self.mmb_proof != other.mmb_proof
{
return Err(AnchorMergeError::AnchorMismatch);
}
Ok(())
}
}
#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum TxoSealError {
NoOutput,
InvalidProofType,
NoTapretProof,
#[from]
Tapret(ConvolveVerifyError),
#[from]
Opret(EmbedVerifyError<OpretError>),
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum AnchorMergeError {
AnchorMismatch,
TooManyInputs,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
#[display(inner)]
pub enum AnchorError {
#[from]
Mpc(mpc::InvalidProof),
#[display("message {0} is not part of the anchor")]
Mmb(mmb::Message),
}
#[cfg(test)]
mod test {
#![cfg_attr(coverage_nightly, coverage(off))]
use amplify::confinement::{Confined, SmallOrdMap};
use amplify::num::u5;
use bc::secp256k1::{SecretKey, SECP256K1};
use bc::{InternalPk, Sats, ScriptPubkey, SeqNo, TapLeafHash, TapScript, TxIn, TxOut, Vout};
use commit_verify::{CommitVerify, Digest};
use dbc::tapret::{TapretCommitment, TapretPathProof};
use single_use_seals::SealError;
use super::*;
use crate::mmb::BundleProof;
use crate::mpc::{MessageMap, MessageSource};
use crate::TxoSealError;
fn setup_opret() -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
setup(false)
}
fn setup_tapret() -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
setup(true)
}
fn setup(tapret: bool) -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
let mut msg = [0u8; 32];
let messages = (0u8..=13)
.map(|no| {
msg[0] = no;
mmb::Message::from_byte_array(msg)
})
.collect::<Vec<_>>();
let mut bundle = mmb::BundleProof {
map: SmallOrdMap::from_iter_checked(
messages.iter().enumerate().map(|(i, msg)| (i as u32, *msg)),
),
};
bundle.map.insert(12, messages[11]).unwrap();
let noise_engine = Sha256::new_with_prefix("test");
let outpoints = messages
.iter()
.map(|msg| Outpoint::new(Txid::from_byte_array(msg.to_byte_array()), msg[0] as u32))
.collect::<Vec<_>>();
let seals = outpoints
.iter()
.enumerate()
.map(|(no, outpoint)| {
let wout = if no % 2 == 0 {
WOutpoint::Extern(*outpoint)
} else {
WOutpoint::Wout(Vout::from(no as u32))
};
TxoSeal {
primary: *outpoint,
secondary: TxoSealExt::Noise(Noise::with(
wout,
noise_engine.clone(),
outpoint.txid[0] as u64,
)),
}
})
.collect::<Vec<_>>();
let protocol = mpc::ProtocolId::from_byte_array([0xADu8; 32]);
let msg_sources = MessageSource::Mmb(bundle.clone());
let source = mpc::Source {
min_depth: u5::with(3),
entropy: 0xFE,
messages: MessageMap::from(Confined::from_checked(bmap! { protocol => msg_sources })),
};
let merkle_tree = source.into_merkle_tree().unwrap();
let merkle_proofs = merkle_tree.clone().into_proofs().collect::<Vec<_>>();
assert_eq!(merkle_proofs.len(), 1);
assert_eq!(merkle_proofs[0].0, protocol);
let nonce = 0;
let tapret_commitment = TapretCommitment::with(merkle_tree.commit_id(), nonce);
let script_commitment = TapScript::commit(&tapret_commitment);
let secret = SecretKey::from_byte_array(&[0x66; 32]).unwrap();
let internal_pk = InternalPk::from(secret.x_only_public_key(SECP256K1).0);
let tapret_proof = TapretProof {
path_proof: TapretPathProof::root(nonce),
internal_pk,
};
let merkle_proof = merkle_proofs[0].1.clone();
let anchor = Anchor {
mmb_proof: bundle.clone(),
mpc_protocol: protocol,
mpc_proof: merkle_proof,
dbc_proof: if tapret { Some(tapret_proof) } else { None },
fallback_proof: none!(),
};
let mpc = merkle_tree.commit_id();
let tx = Tx {
version: default!(),
inputs: Confined::from_iter_checked(messages.iter().map(|msg| TxIn {
prev_output: outpoints[msg[0] as usize],
sig_script: none!(),
sequence: SeqNo::ZERO,
witness: none!(),
})),
outputs: Confined::from_checked(vec![TxOut {
value: Sats::ZERO,
script_pubkey: if tapret {
ScriptPubkey::p2tr(
internal_pk,
Some(TapLeafHash::with_leaf_script(&script_commitment.into()).into()),
)
} else {
ScriptPubkey::op_return(mpc.as_slice())
},
}]),
lock_time: default!(),
};
let witness = SealWitness::new(tx, anchor);
(messages, bundle, seals, witness)
}
#[test]
fn valid_oprets() {
let (messages, bundle, seals, witness) = setup_opret();
for seal in seals {
let outpoint = seal.primary;
let pos = outpoint.txid[0] as usize;
if pos == 12 {
assert!(!bundle.verify(outpoint, messages[pos], &witness.published));
assert!(bundle.verify(outpoint, messages[11], &witness.published));
assert!(!seal.is_included(messages[pos], &witness));
witness.verify_seal_closing(seal, messages[pos]).unwrap_err();
assert!(seal.is_included(messages[11], &witness));
witness.verify_seal_closing(seal, messages[11]).unwrap();
} else {
assert!(bundle.verify(outpoint, messages[pos], &witness.published));
assert!(seal.is_included(messages[pos], &witness));
witness.verify_seal_closing(seal, messages[pos]).unwrap();
}
}
}
#[test]
fn valid_taprets() {
let (messages, bundle, seals, witness) = setup_tapret();
for seal in seals {
let outpoint = seal.primary;
let pos = outpoint.txid[0] as usize;
if pos == 12 {
assert!(!bundle.verify(outpoint, messages[pos], &witness.published));
assert!(bundle.verify(outpoint, messages[11], &witness.published));
assert!(!seal.is_included(messages[pos], &witness));
witness.verify_seal_closing(seal, messages[pos]).unwrap_err();
assert!(seal.is_included(messages[11], &witness));
witness.verify_seal_closing(seal, messages[11]).unwrap();
} else {
assert!(bundle.verify(outpoint, messages[pos], &witness.published));
assert!(seal.is_included(messages[pos], &witness));
witness.verify_seal_closing(seal, messages[pos]).unwrap();
}
}
}
#[test]
fn invalid_dbc_type() {
let (messages, _bundle, seals, mut witness) = setup_tapret();
let tapret = witness.client.dbc_proof;
witness.client.dbc_proof = None;
assert!(matches!(
witness.verify_seal_closing(seals[2], messages[2]).unwrap_err(),
SealError::Published(TxoSealError::NoTapretProof)
));
let (messages, _bundle, seals, mut witness) = setup_opret();
witness.client.dbc_proof = tapret;
assert!(matches!(
witness.verify_seal_closing(seals[2], messages[2]).unwrap_err(),
SealError::Published(TxoSealError::InvalidProofType)
));
}
#[test]
fn mmb_absent_input() {
let (messages, bundle, _seals, witness) = setup_opret();
let fake_outpoint = Outpoint::new(Txid::from_byte_array([0x13; 32]), 12);
assert!(!bundle.verify(fake_outpoint, messages[0], &witness.published));
}
#[test]
fn mmb_uncommited_msg() {
let (messages, mut bundle, seals, witness) = setup_opret();
bundle.map.remove(&13).unwrap();
assert!(!bundle.verify(seals[13].primary, messages[13], &witness.published));
}
#[test]
fn fallback_seal() {
let (messages, _bundle, mut seals, witness) = setup_opret();
seals[1].secondary = TxoSealExt::Fallback(seals[2].primary);
witness.verify_seal_closing(seals[1], messages[1]).unwrap();
assert!(seals[1].is_included(messages[1], &witness));
assert!(!seals[1].is_included(messages[2], &witness));
}
#[test]
fn anchor_merge() {
let (_, _, _, mut witness) = setup_opret();
witness.client.merge(witness.client.clone()).unwrap();
let mut other = witness.client.clone();
other.mpc_protocol = mpc::ProtocolId::from_byte_array([0x13u8; 32]);
witness.client.merge(other).unwrap_err();
}
}