seals/
txout.rs

1// Bitcoin protocol single-use-seals library.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22//! Bitcoin single-use-seals defined by a transaction output and closed by
23//! spending that output ("TxOut seals").
24
25use 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/// A noise, which acts as a placeholder for seal definitions lacking fallback seal (see
41/// [`TxoSealExt::Noise`]).
42#[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    /// Construct a new noise object using entropy from a pre-initialized SHA256 engine, some nonce
51    /// and main [`WTxoSeal`] definition outpoint.
52    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
71/// Multi-message bundles.
72///
73/// Multi-message bundles allow putting multiple independent messages into a single commitment under
74/// a single MPC protocol. This is achieved by associating each single message with a subset of
75/// witness transaction outputs, which is provably disjoint with other subsets for all other
76/// messages under the same protocol.
77///
78/// The proof of disjoint is in [`mmb::BundleProof`], each individual message is kept in
79/// [`mmb::Message`], and the final commitment to all messages is represented by [`mmb::Commitment`]
80/// structure.
81///
82/// # See also
83///
84/// Multiprotocol commitments in [`commit_verify::mpc`]
85pub mod mmb {
86    use amplify::confinement::SmallOrdMap;
87    use commit_verify::{CommitmentId, DigestExt, Sha256};
88
89    use super::*;
90
91    /// A message for a multi-message bundling.
92    #[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    /// The final commitment to all messages under a multi-message bundle.
104    ///
105    /// The commitment is produced by a linear strict-encoding of the data in a [`BundleProof`].
106    /// The data are not merklized since, in order to verify the proof, all messages must be anyway
107    /// present in explicit form.
108    #[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    /// The proof that each message is associated with a separate subset of witness transaction
130    /// inputs, and all of these subsets are disjoint.
131    #[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        /// Map from a transaction input number to a specific message which is associated with it.
139        pub map: SmallOrdMap<u32, Message>,
140    }
141
142    impl BundleProof {
143        /// Verify that the proof matches the witness transaction structure.
144        pub fn verify(&self, seal: Outpoint, msg: Message, tx: &Tx) -> bool {
145            // Verify that there is a witness transaction input which spends a TxO matching the
146            // single-use seal definition.
147            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            // Check that this output belongs to the same message as expected.
154            let Some(expected) = self.map.get(&input_index) else {
155                return false;
156            };
157            *expected == msg
158        }
159    }
160}
161
162/// Module extends [`commit_verify::mpc`] module with multi-message bundle commitments.
163pub 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    /// The source of an [`mpc::Message`], which can be either a single message or a multimessage
177    /// bundle (in the form of a proof).
178    #[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        /// A single message.
188        #[from]
189        #[strict_type(tag = 1)]
190        Single(Message),
191
192        /// A multi-message bundle.
193        #[from]
194        #[strict_type(tag = 2)]
195        Mmb(mmb::BundleProof),
196    }
197
198    impl MessageSource {
199        /// Construct a [`mpc::Message`] from the provided source.
200        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    /// The message map which associates each protocol with a source of the message (an instance of
211    /// a [`MessageSource`]).
212    #[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    /// The information for constructing [`mpc::MerkleTree`].
223    #[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        /// The minimal depth of the tree.
229        pub min_depth: u5,
230        /// Entropy see for constructing all the non-protocol leafs of the tree.
231        pub entropy: u64,
232        /// The protocols and messages to put into the tree.
233        pub messages: MessageMap,
234    }
235
236    impl Source {
237        /// Construct a [`mpc::MerkleTree`] from the source data.
238        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/// Anchor is a client-side witness for the bitcoin txout seals.
255///
256/// Anchor is a set of data required for the client-side validation of a bitcoin txout single-use
257/// seal, which can't be recovered from the transaction and other public information itself.
258#[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    /// The proof that each witness transaction input is used only in a single bundle.
264    pub mmb_proof: mmb::BundleProof,
265    /// The protocol under which the client-side witness is valid.
266    pub mpc_protocol: mpc::ProtocolId,
267    /// The inclusion proof (using multiprotocol commitments) of a commitment under the
268    /// [`mpc_protocol`] into the published witness.
269    pub mpc_proof: mpc::MerkleProof,
270    /// The deterministic bitcoin commitment proof that the witness commitment is valid.
271    pub dbc_proof: Option<TapretProof>,
272    #[cfg_attr(feature = "serde", serde(skip))]
273    /// Reserved for the future proofs regarding fallback seals.
274    // TODO: This should become an option once fallback proofs are ready
275    pub fallback_proof: ReservedBytes<1>,
276}
277
278impl Anchor {
279    /// Detect whether an anchor corresponds to a fallback proof or not.
280    ///
281    /// # Nota bene
282    ///
283    /// In the current version fallback proofs are not implemented, and this method always returns
284    /// `false`.
285    // TODO: (v0.13) Change when the fallback proofs are ready
286    pub fn is_fallback(&self) -> bool { false }
287
288    /// Verify the fallback proof.
289    ///
290    /// # Nota bene
291    ///
292    /// In the current version fallback proofs are not implemented, and this method always returns
293    /// `Ok(()) `(since if there is no fallback proof defined, it is a case of a valid situation).
294    // TODO: (v0.13) Change when the fallback proofs are ready
295    pub fn verify_fallback(&self) -> Result<(), AnchorError> { Ok(()) }
296}
297
298/// Proof data for verification of deterministic bitcoin commitment produced from anchor.
299///
300/// This proof is used to do the final verification of the single-use seal closing in the witness
301/// transaction (published witness).
302pub struct Proof {
303    /// The message to which the witness transaction must commit with deterministic bitcoin
304    /// commitment.
305    ///
306    /// The message is produced from the multiprotocol commitment data of an [`Anchor`].
307    pub mpc_commit: mpc::Commitment,
308    /// The deterministic bitcoin commitment proof that the witness commitment is valid.
309    pub dbc_proof: Option<TapretProof>,
310}
311
312/// The value for a fallback seal definition.
313#[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    /// The fallback seal is not defined. The noise data are used instead to obfuscate the main
320    /// seal.
321    #[strict_type(tag = 0)]
322    Noise(Noise),
323
324    /// The fallback seal is defined as a known UTXO.
325    #[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/// The bitcoin TxO-based single-use seal protocol (see [`SingleUseSeal`]).
334///
335/// # Nota bene
336///
337/// Unlike [`crate::WTxoSeal`], this seal always contains information about the defined seal.
338/// It is constructed once a "previous" witness transaction, which contains a commitment to a
339/// [`crate::WTxoSeal`] definition, is constructed, and its transaction id becomes known.
340#[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    /// A primary seal definition.
347    pub primary: Outpoint,
348    /// A fallback seal definition.
349    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            // If we are provided a fallback proof but no fallback seal were defined
366            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/// Errors verifying Txo-based single use seal closing with a provided witness, under [`TxoSeal`]
430/// implementation of [`SingleUseSeals`] protocol.
431#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
432#[display(doc_comments)]
433pub enum TxoSealError {
434    /// witness transaction contains no taproot or OP_RETURN output.
435    NoOutput,
436
437    /// the first witness transaction DBC-compatible output does not match the provided proof type.
438    InvalidProofType,
439
440    /// the first witness transaction DBC-compatible output is taproot, but no tapret proof is
441    /// provided.
442    NoTapretProof,
443
444    #[from]
445    /// invalid tapret commitment.
446    Tapret(ConvolveVerifyError),
447
448    #[from]
449    /// invalid opret commitment.
450    Opret(EmbedVerifyError<OpretError>),
451}
452
453/// Error merging information from multiple anchors for the same witness (see [`Anchor::merge`]).
454#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
455#[display(doc_comments)]
456pub enum AnchorMergeError {
457    /// anchor mismatch in the merge procedure
458    AnchorMismatch,
459
460    /// anchor is invalid: too many inputs
461    TooManyInputs,
462}
463
464/// An error involving [`Anchor`] into a final [`Proof`] for the single-use seal published witness
465/// verification.
466#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Error, From)]
467#[display(inner)]
468pub enum AnchorError {
469    /// Invalid multiprotocol commitment proof (see [`commit_verify::mpc`]).
470    #[from]
471    Mpc(mpc::InvalidProof),
472
473    /// Invalid multiprotocol bundle (see [`mmb`]).
474    #[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        // Construct messages
505        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        // Construct bundle proof
514        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        // Make message No 12 equal to 11, so messsage no 12 is not used
520        bundle.map.insert(12, messages[11]).unwrap();
521
522        // Construct seals
523        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        // Tapret
561        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        // Construct a witness transaction
581        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        // a non-committed message
686        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        // And not a wrong message
698        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}