1use core::error::Error;
26use core::fmt::Debug;
27
28use amplify::{ByteArray, Bytes, Bytes32};
29use bc::{Outpoint, Tx, Txid};
30use commit_verify::{
31 CommitId, ConvolveVerifyError, DigestExt, EmbedVerifyError, ReservedBytes, Sha256,
32};
33use dbc::opret::{OpretError, OpretProof};
34use dbc::tapret::TapretProof;
35use single_use_seals::{ClientSideWitness, PublishedWitness, SealWitness, SingleUseSeal};
36use strict_encoding::{StrictDumb, StrictSum};
37
38use crate::WOutpoint;
39
40#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, From)]
43#[display("{0:x}")]
44#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
45#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
47pub struct Noise(Bytes<40>);
48
49impl Noise {
50 pub fn with(outpoint: WOutpoint, mut noise_engine: Sha256, nonce: u64) -> Self {
53 noise_engine.input_raw(&nonce.to_be_bytes());
54 match outpoint {
55 WOutpoint::Wout(wout) => {
56 noise_engine.input_raw(&[WOutpoint::ALL_VARIANTS[0].0]);
57 noise_engine.input_raw(&wout.to_u32().to_be_bytes());
58 }
59 WOutpoint::Extern(outpoint) => {
60 noise_engine.input_raw(&[WOutpoint::ALL_VARIANTS[1].0]);
61 noise_engine.input_raw(outpoint.txid.as_ref());
62 noise_engine.input_raw(&outpoint.vout.to_u32().to_be_bytes());
63 }
64 }
65 let mut noise = [0xFFu8; 40];
66 noise[..32].copy_from_slice(&noise_engine.finish());
67 Self(noise.into())
68 }
69}
70
71pub mod mmb {
86 use amplify::confinement::SmallOrdMap;
87 use commit_verify::{CommitmentId, DigestExt, Sha256};
88
89 use super::*;
90
91 #[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From, Default)]
93 #[wrapper(Deref, BorrowSlice, Display, FromStr, Hex, Index, RangeOps)]
94 #[derive(StrictType, StrictEncode, StrictDecode)]
95 #[strict_type(lib = dbc::LIB_NAME_BPCORE)]
96 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
97 pub struct Message(
98 #[from]
99 #[from([u8; 32])]
100 Bytes32,
101 );
102
103 #[derive(Wrapper, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From)]
109 #[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
110 #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
111 #[strict_type(lib = dbc::LIB_NAME_BPCORE)]
112 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
113 pub struct Commitment(
114 #[from]
115 #[from([u8; 32])]
116 Bytes32,
117 );
118 impl CommitmentId for Commitment {
119 const TAG: &'static str = "urn:lnp-bp:mmb:bundle#2024-11-18";
120 }
121 impl From<Sha256> for Commitment {
122 fn from(hasher: Sha256) -> Self { hasher.finish().into() }
123 }
124
125 impl From<Commitment> for mpc::Message {
126 fn from(msg: Commitment) -> Self { mpc::Message::from_byte_array(msg.to_byte_array()) }
127 }
128
129 #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
132 #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
133 #[strict_type(lib = dbc::LIB_NAME_BPCORE)]
134 #[derive(CommitEncode)]
135 #[commit_encode(strategy = strict, id = Commitment)]
136 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
137 pub struct BundleProof {
138 pub map: SmallOrdMap<u32, Message>,
140 }
141
142 impl BundleProof {
143 pub fn verify(&self, seal: Outpoint, msg: Message, tx: &Tx) -> bool {
145 let Some(input_index) = tx.inputs().position(|input| input.prev_output == seal) else {
148 return false;
149 };
150 let Ok(input_index) = u32::try_from(input_index) else {
151 return false;
152 };
153 let Some(expected) = self.map.get(&input_index) else {
155 return false;
156 };
157 *expected == msg
158 }
159 }
160}
161
162pub mod mpc {
164 use amplify::confinement::MediumOrdMap;
165 use amplify::num::u5;
166 use amplify::ByteArray;
167 pub use commit_verify::mpc::{
168 Commitment, Error, InvalidProof, Leaf, LeafNotKnown, MergeError, MerkleBlock,
169 MerkleConcealed, MerkleProof, MerkleTree, Message, Method, Proof, ProtocolId,
170 MPC_MINIMAL_DEPTH,
171 };
172 use commit_verify::{CommitId, TryCommitVerify};
173
174 use crate::mmb;
175
176 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From)]
179 #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
180 #[strict_type(lib = dbc::LIB_NAME_BPCORE, tags = custom, dumb = Self::Single(strict_dumb!()))]
181 #[cfg_attr(
182 feature = "serde",
183 derive(Serialize, Deserialize),
184 serde(rename_all = "camelCase", untagged)
185 )]
186 pub enum MessageSource {
187 #[from]
189 #[strict_type(tag = 1)]
190 Single(Message),
191
192 #[from]
194 #[strict_type(tag = 2)]
195 Mmb(mmb::BundleProof),
196 }
197
198 impl MessageSource {
199 pub fn mpc_message(&self) -> Message {
201 match self {
202 MessageSource::Single(message) => *message,
203 MessageSource::Mmb(proof) => {
204 Message::from_byte_array(proof.commit_id().to_byte_array())
205 }
206 }
207 }
208 }
209
210 #[derive(
213 Wrapper, WrapperMut, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, From
214 )]
215 #[wrapper(Deref)]
216 #[wrapper_mut(DerefMut)]
217 #[derive(StrictType, StrictEncode, StrictDecode)]
218 #[strict_type(lib = dbc::LIB_NAME_BPCORE)]
219 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
220 pub struct MessageMap(MediumOrdMap<ProtocolId, MessageSource>);
221
222 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
224 #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
225 #[strict_type(lib = dbc::LIB_NAME_BPCORE)]
226 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
227 pub struct Source {
228 pub min_depth: u5,
230 pub entropy: u64,
232 pub messages: MessageMap,
234 }
235
236 impl Source {
237 pub fn into_merkle_tree(self) -> Result<MerkleTree, Error> {
239 let messages = self.messages.0.iter().map(|(id, src)| {
240 let msg = src.mpc_message();
241 (*id, msg)
242 });
243 let source = commit_verify::mpc::MultiSource {
244 method: Method::Sha256t,
245 min_depth: self.min_depth,
246 messages: MediumOrdMap::from_iter_checked(messages),
247 static_entropy: Some(self.entropy),
248 };
249 MerkleTree::try_commit(&source)
250 }
251 }
252}
253
254#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
259#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
260#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
261#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
262pub struct Anchor {
263 pub mmb_proof: mmb::BundleProof,
265 pub mpc_protocol: mpc::ProtocolId,
267 pub mpc_proof: mpc::MerkleProof,
270 pub dbc_proof: Option<TapretProof>,
272 #[cfg_attr(feature = "serde", serde(skip))]
273 pub fallback_proof: ReservedBytes<1>,
276}
277
278impl Anchor {
279 pub fn is_fallback(&self) -> bool { false }
287
288 pub fn verify_fallback(&self) -> Result<(), AnchorError> { Ok(()) }
296}
297
298pub struct Proof {
303 pub mpc_commit: mpc::Commitment,
308 pub dbc_proof: Option<TapretProof>,
310}
311
312#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
314#[display(inner)]
315#[derive(StrictType, StrictEncode, StrictDecode)]
316#[strict_type(lib = dbc::LIB_NAME_BPCORE, tags = custom)]
317#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
318pub enum TxoSealExt {
319 #[strict_type(tag = 0)]
322 Noise(Noise),
323
324 #[strict_type(tag = 1)]
326 Fallback(Outpoint),
327}
328
329impl StrictDumb for TxoSealExt {
330 fn strict_dumb() -> Self { TxoSealExt::Noise(Noise::from(Bytes::from_byte_array([0u8; 40]))) }
331}
332
333#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display)]
341#[display("{primary}/{secondary}")]
342#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
343#[strict_type(lib = dbc::LIB_NAME_BPCORE)]
344#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
345pub struct TxoSeal {
346 pub primary: Outpoint,
348 pub secondary: TxoSealExt,
350}
351
352impl SingleUseSeal for TxoSeal {
353 type Message = mmb::Message;
354 type PubWitness = Tx;
355 type CliWitness = Anchor;
356
357 fn is_included(&self, message: Self::Message, witness: &SealWitness<Self>) -> bool {
358 match self.secondary {
359 TxoSealExt::Noise(_) | TxoSealExt::Fallback(_) if !witness.client.is_fallback() => {
360 witness.client.mmb_proof.verify(self.primary, message, &witness.published)
361 }
362 TxoSealExt::Fallback(fallback) => {
363 witness.client.mmb_proof.verify(fallback, message, &witness.published)
364 }
365 TxoSealExt::Noise(_) => false,
367 }
368 }
369}
370
371impl PublishedWitness<TxoSeal> for Tx {
372 type PubId = Txid;
373 type Error = TxoSealError;
374
375 fn pub_id(&self) -> Txid { self.txid() }
376
377 fn verify_commitment(&self, proof: Proof) -> Result<(), Self::Error> {
378 let out = self
379 .outputs()
380 .find(|out| out.script_pubkey.is_op_return() || out.script_pubkey.is_p2tr())
381 .ok_or(TxoSealError::NoOutput)?;
382 if out.script_pubkey.is_op_return() {
383 if proof.dbc_proof.is_none() {
384 OpretProof::default().verify(&proof.mpc_commit, self).map_err(TxoSealError::from)
385 } else {
386 Err(TxoSealError::InvalidProofType)
387 }
388 } else if let Some(ref dbc_proof) = proof.dbc_proof {
389 dbc_proof.verify(&proof.mpc_commit, self).map_err(TxoSealError::from)
390 } else {
391 Err(TxoSealError::NoTapretProof)
392 }
393 }
394}
395
396impl ClientSideWitness for Anchor {
397 type Proof = Proof;
398 type Seal = TxoSeal;
399 type Error = AnchorError;
400
401 fn convolve_commit(&self, mmb_message: mmb::Message) -> Result<Proof, Self::Error> {
402 self.verify_fallback()?;
403 if self.mmb_proof.map.values().all(|msg| *msg != mmb_message) {
404 return Err(AnchorError::Mmb(mmb_message));
405 }
406 let bundle_id = self.mmb_proof.commit_id();
407 let mpc_message = mpc::Message::from_byte_array(bundle_id.to_byte_array());
408 let mpc_commit = self.mpc_proof.convolve(self.mpc_protocol, mpc_message)?;
409 Ok(Proof {
410 mpc_commit,
411 dbc_proof: self.dbc_proof.clone(),
412 })
413 }
414
415 fn merge(&mut self, other: Self) -> Result<(), impl Error>
416 where Self: Sized {
417 if self.mpc_protocol != other.mpc_protocol
418 || self.mpc_proof != other.mpc_proof
419 || self.dbc_proof != other.dbc_proof
420 || self.fallback_proof != other.fallback_proof
421 || self.mmb_proof != other.mmb_proof
422 {
423 return Err(AnchorMergeError::AnchorMismatch);
424 }
425 Ok(())
426 }
427}
428
429#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
432#[display(doc_comments)]
433pub enum TxoSealError {
434 NoOutput,
436
437 InvalidProofType,
439
440 NoTapretProof,
443
444 #[from]
445 Tapret(ConvolveVerifyError),
447
448 #[from]
449 Opret(EmbedVerifyError<OpretError>),
451}
452
453#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
455#[display(doc_comments)]
456pub enum AnchorMergeError {
457 AnchorMismatch,
459
460 TooManyInputs,
462}
463
464#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
467#[display(inner)]
468pub enum AnchorError {
469 #[from]
471 Mpc(mpc::InvalidProof),
472
473 #[display("message {0} is not part of the anchor")]
475 Mmb(mmb::Message),
476}
477
478#[cfg(test)]
479mod test {
480 #![cfg_attr(coverage_nightly, coverage(off))]
481
482 use amplify::confinement::{Confined, SmallOrdMap};
483 use amplify::num::u5;
484 use bc::secp256k1::{SecretKey, SECP256K1};
485 use bc::{InternalPk, Sats, ScriptPubkey, SeqNo, TapLeafHash, TapScript, TxIn, TxOut, Vout};
486 use commit_verify::{CommitVerify, Digest};
487 use dbc::tapret::{TapretCommitment, TapretPathProof};
488 use single_use_seals::SealError;
489
490 use super::*;
491 use crate::mmb::BundleProof;
492 use crate::mpc::{MessageMap, MessageSource};
493 use crate::TxoSealError;
494
495 fn setup_opret() -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
496 setup(false)
497 }
498
499 fn setup_tapret() -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
500 setup(true)
501 }
502
503 fn setup(tapret: bool) -> (Vec<mmb::Message>, BundleProof, Vec<TxoSeal>, SealWitness<TxoSeal>) {
504 let mut msg = [0u8; 32];
506 let messages = (0u8..=13)
507 .map(|no| {
508 msg[0] = no;
509 mmb::Message::from_byte_array(msg)
510 })
511 .collect::<Vec<_>>();
512
513 let mut bundle = mmb::BundleProof {
515 map: SmallOrdMap::from_iter_checked(
516 messages.iter().enumerate().map(|(i, msg)| (i as u32, *msg)),
517 ),
518 };
519 bundle.map.insert(12, messages[11]).unwrap();
521
522 let noise_engine = Sha256::new_with_prefix("test");
524 let outpoints = messages
525 .iter()
526 .map(|msg| Outpoint::new(Txid::from_byte_array(msg.to_byte_array()), msg[0] as u32))
527 .collect::<Vec<_>>();
528 let seals = outpoints
529 .iter()
530 .enumerate()
531 .map(|(no, outpoint)| {
532 let wout = if no % 2 == 0 {
533 WOutpoint::Extern(*outpoint)
534 } else {
535 WOutpoint::Wout(Vout::from(no as u32))
536 };
537 TxoSeal {
538 primary: *outpoint,
539 secondary: TxoSealExt::Noise(Noise::with(
540 wout,
541 noise_engine.clone(),
542 outpoint.txid[0] as u64,
543 )),
544 }
545 })
546 .collect::<Vec<_>>();
547
548 let protocol = mpc::ProtocolId::from_byte_array([0xADu8; 32]);
549 let msg_sources = MessageSource::Mmb(bundle.clone());
550 let source = mpc::Source {
551 min_depth: u5::with(3),
552 entropy: 0xFE,
553 messages: MessageMap::from(Confined::from_checked(bmap! { protocol => msg_sources })),
554 };
555 let merkle_tree = source.into_merkle_tree().unwrap();
556 let merkle_proofs = merkle_tree.clone().into_proofs().collect::<Vec<_>>();
557 assert_eq!(merkle_proofs.len(), 1);
558 assert_eq!(merkle_proofs[0].0, protocol);
559
560 let nonce = 0;
562 let tapret_commitment = TapretCommitment::with(merkle_tree.commit_id(), nonce);
563 let script_commitment = TapScript::commit(&tapret_commitment);
564 let secret = SecretKey::from_byte_array(&[0x66; 32]).unwrap();
565 let internal_pk = InternalPk::from(secret.x_only_public_key(SECP256K1).0);
566 let tapret_proof = TapretProof {
567 path_proof: TapretPathProof::root(nonce),
568 internal_pk,
569 };
570
571 let merkle_proof = merkle_proofs[0].1.clone();
572 let anchor = Anchor {
573 mmb_proof: bundle.clone(),
574 mpc_protocol: protocol,
575 mpc_proof: merkle_proof,
576 dbc_proof: if tapret { Some(tapret_proof) } else { None },
577 fallback_proof: none!(),
578 };
579
580 let mpc = merkle_tree.commit_id();
582 let tx = Tx {
583 version: default!(),
584 inputs: Confined::from_iter_checked(messages.iter().map(|msg| TxIn {
585 prev_output: outpoints[msg[0] as usize],
586 sig_script: none!(),
587 sequence: SeqNo::ZERO,
588 witness: none!(),
589 })),
590 outputs: Confined::from_checked(vec![TxOut {
591 value: Sats::ZERO,
592 script_pubkey: if tapret {
593 ScriptPubkey::p2tr(
594 internal_pk,
595 Some(TapLeafHash::with_leaf_script(&script_commitment.into()).into()),
596 )
597 } else {
598 ScriptPubkey::op_return(mpc.as_slice())
599 },
600 }]),
601 lock_time: default!(),
602 };
603 let witness = SealWitness::new(tx, anchor);
604
605 (messages, bundle, seals, witness)
606 }
607
608 #[test]
609 fn valid_oprets() {
610 let (messages, bundle, seals, witness) = setup_opret();
611
612 for seal in seals {
613 let outpoint = seal.primary;
614 let pos = outpoint.txid[0] as usize;
615 if pos == 12 {
616 assert!(!bundle.verify(outpoint, messages[pos], &witness.published));
617 assert!(bundle.verify(outpoint, messages[11], &witness.published));
618
619 assert!(!seal.is_included(messages[pos], &witness));
620 witness.verify_seal_closing(seal, messages[pos]).unwrap_err();
621
622 assert!(seal.is_included(messages[11], &witness));
623 witness.verify_seal_closing(seal, messages[11]).unwrap();
624 } else {
625 assert!(bundle.verify(outpoint, messages[pos], &witness.published));
626 assert!(seal.is_included(messages[pos], &witness));
627 witness.verify_seal_closing(seal, messages[pos]).unwrap();
628 }
629 }
630 }
631 #[test]
632 fn valid_taprets() {
633 let (messages, bundle, seals, witness) = setup_tapret();
634
635 for seal in seals {
636 let outpoint = seal.primary;
637 let pos = outpoint.txid[0] as usize;
638 if pos == 12 {
639 assert!(!bundle.verify(outpoint, messages[pos], &witness.published));
640 assert!(bundle.verify(outpoint, messages[11], &witness.published));
641
642 assert!(!seal.is_included(messages[pos], &witness));
643 witness.verify_seal_closing(seal, messages[pos]).unwrap_err();
644
645 assert!(seal.is_included(messages[11], &witness));
646 witness.verify_seal_closing(seal, messages[11]).unwrap();
647 } else {
648 assert!(bundle.verify(outpoint, messages[pos], &witness.published));
649 assert!(seal.is_included(messages[pos], &witness));
650 witness.verify_seal_closing(seal, messages[pos]).unwrap();
651 }
652 }
653 }
654
655 #[test]
656 fn invalid_dbc_type() {
657 let (messages, _bundle, seals, mut witness) = setup_tapret();
658 let tapret = witness.client.dbc_proof;
659 witness.client.dbc_proof = None;
660 assert!(matches!(
661 witness.verify_seal_closing(seals[2], messages[2]).unwrap_err(),
662 SealError::Published(TxoSealError::NoTapretProof)
663 ));
664
665 let (messages, _bundle, seals, mut witness) = setup_opret();
666 witness.client.dbc_proof = tapret;
667 assert!(matches!(
668 witness.verify_seal_closing(seals[2], messages[2]).unwrap_err(),
669 SealError::Published(TxoSealError::InvalidProofType)
670 ));
671 }
672
673 #[test]
674 fn mmb_absent_input() {
675 let (messages, bundle, _seals, witness) = setup_opret();
676
677 let fake_outpoint = Outpoint::new(Txid::from_byte_array([0x13; 32]), 12);
678 assert!(!bundle.verify(fake_outpoint, messages[0], &witness.published));
679 }
680
681 #[test]
682 fn mmb_uncommited_msg() {
683 let (messages, mut bundle, seals, witness) = setup_opret();
684
685 bundle.map.remove(&13).unwrap();
687 assert!(!bundle.verify(seals[13].primary, messages[13], &witness.published));
688 }
689
690 #[test]
691 fn fallback_seal() {
692 let (messages, _bundle, mut seals, witness) = setup_opret();
693
694 seals[1].secondary = TxoSealExt::Fallback(seals[2].primary);
695 witness.verify_seal_closing(seals[1], messages[1]).unwrap();
696 assert!(seals[1].is_included(messages[1], &witness));
697 assert!(!seals[1].is_included(messages[2], &witness));
699 }
700
701 #[test]
702 fn anchor_merge() {
703 let (_, _, _, mut witness) = setup_opret();
704 witness.client.merge(witness.client.clone()).unwrap();
705
706 let mut other = witness.client.clone();
707 other.mpc_protocol = mpc::ProtocolId::from_byte_array([0x13u8; 32]);
708 witness.client.merge(other).unwrap_err();
709 }
710}