clementine_core/builder/transaction/
mod.rs

1//! # builder::transaction
2//!
3//!
4//! This module provides the core logic for constructing, handling, and signing the various Bitcoin transactions
5//! required by the Clementine bridge protocol. It defines the creation, and validation of
6//! transaction flows involving operators, verifiers, watchtowers, and the security council, aimed to make it
7//! easy to create transactions and sign them properly.
8//!
9//! ## Overview
10//!
11//! The transaction builder is responsible for:
12//! - Defining all transaction types and their flows in the protocol (see [`TransactionType`]).
13//! - Building and signing transactions for deposit, withdrawal, challenge, reimbursement, and related operations.
14//! - Storing transaction inputs/outputs, scripts, and Taproot spend information.
15//! - Providing utilities to speed up transaction creating during a deposit using caching tx and db data.
16//!
17//! ## Main Components
18//!
19//! - [`mod.rs`] - The main entry point, re-exporting key types and functions. Defines some helper functions for creating taproot outputs.
20//! - [`creator.rs`] - Contains the functions to create multiple TxHandler's for a deposit and related structs for caching. In particular, it contains the functions to create TxHandler's for all transactions generated during a deposit from a single kickoff.
21//! - [`operator_collateral.rs`] - Handles the creation of operator-specific collateral-related transactions, such as round, ready-to-reimburse, and unspent kickoff transactions.
22//! - [`operator_reimburse.rs`] - Implements the creation of reimbursement and payout transactions, including logic for operator compensation and optimistic payouts.
23//! - [`operator_assert.rs`] - Provides functions for creating BitVM assertion and timeout transactions.
24//! - [`challenge.rs`] - Handles the creation of challenge, disprove, and watchtower challenge transactions, supporting protocol dispute resolution and fraud proofs.
25//! - [`sign.rs`] - Contains logic for signing transactions using data in the [`TxHandler`].
26//! - [`txhandler.rs`] - Defines the [`TxHandler`] abstraction, which wraps a transaction and its metadata, and provides methods for signing, finalizing, and extracting transaction data.
27//! - [`input.rs`] - Defines types and utilities for transaction inputs used in the [`TxHandler`].
28//! - [`output.rs`] - Defines types and utilities for transaction outputs used in the [`TxHandler`].
29//! - [`deposit_signature_owner.rs`] - Maps which TxIn signatures are signed by which protocol entities, additionally supporting different Sighash types.
30//!
31
32use super::script::{CheckSig, Multisig, SpendableScript};
33use super::script::{ReplacementDepositScript, SpendPath};
34use crate::builder::address::calculate_taproot_leaf_depths;
35use crate::builder::script::OtherSpendable;
36use crate::builder::transaction::challenge::*;
37use crate::builder::transaction::input::SpendableTxIn;
38use crate::builder::transaction::operator_assert::*;
39use crate::builder::transaction::operator_collateral::*;
40use crate::builder::transaction::operator_reimburse::*;
41use crate::builder::transaction::output::UnspentTxOut;
42use crate::config::protocol::ProtocolParamset;
43use crate::constants::{NON_EPHEMERAL_ANCHOR_AMOUNT, NON_STANDARD_V3};
44use crate::deposit::{DepositData, SecurityCouncil};
45use crate::errors::BridgeError;
46use crate::operator::RoundIndex;
47use crate::rpc::clementine::grpc_transaction_id;
48use crate::rpc::clementine::GrpcTransactionId;
49use crate::rpc::clementine::{
50    NormalSignatureKind, NormalTransactionId, NumberedTransactionId, NumberedTransactionType,
51};
52use bitcoin::hashes::Hash;
53use bitcoin::opcodes::all::OP_RETURN;
54use bitcoin::script::Builder;
55use bitcoin::transaction::Version;
56use bitcoin::{
57    Address, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, XOnlyPublicKey,
58};
59use hex;
60use input::UtxoVout;
61use serde::{Deserialize, Serialize};
62use std::sync::Arc;
63use thiserror::Error;
64
65// Exports to the outside
66pub use crate::builder::transaction::txhandler::*;
67pub use creator::{
68    create_round_txhandlers, create_txhandlers, ContractContext, KickoffWinternitzKeys,
69    ReimburseDbCache, TxHandlerCache,
70};
71pub use operator_collateral::{
72    create_burn_unused_kickoff_connectors_txhandler, create_round_nth_txhandler,
73};
74pub use operator_reimburse::{create_optimistic_payout_txhandler, create_payout_txhandler};
75pub use txhandler::Unsigned;
76
77pub mod challenge;
78mod creator;
79pub mod deposit_signature_owner;
80pub mod input;
81mod operator_assert;
82mod operator_collateral;
83mod operator_reimburse;
84pub mod output;
85pub mod sign;
86mod txhandler;
87
88type HiddenNode<'a> = &'a [u8; 32];
89
90#[derive(Debug, Error)]
91pub enum TxError {
92    /// TxInputNotFound is returned when the input is not found in the transaction
93    #[error("Could not find input of transaction")]
94    TxInputNotFound,
95    #[error("Could not find output of transaction")]
96    TxOutputNotFound,
97    #[error("Attempted to set witness when it's already set")]
98    WitnessAlreadySet,
99    #[error("Script with index {0} not found for transaction")]
100    ScriptNotFound(usize),
101    #[error("Insufficient Context data for the requested TxHandler")]
102    InsufficientContext,
103    #[error("No scripts in TxHandler for the TxIn with index {0}")]
104    NoScriptsForTxIn(usize),
105    #[error("No script in TxHandler for the index {0}")]
106    NoScriptAtIndex(usize),
107    #[error("Spend Path in SpentTxIn in TxHandler not specified")]
108    SpendPathNotSpecified,
109    #[error("Actor does not own the key needed in P2TR keypath")]
110    NotOwnKeyPath,
111    #[error("public key of Checksig in script is not owned by Actor")]
112    NotOwnedScriptPath,
113    #[error("Couldn't find needed signature from database for tx: {:?}", _0)]
114    SignatureNotFound(TransactionType),
115    #[error("Couldn't find needed txhandler during creation for tx: {:?}", _0)]
116    TxHandlerNotFound(TransactionType),
117    #[error("BitvmSetupNotFound for operator {0:?}, deposit_txid {1}")]
118    BitvmSetupNotFound(XOnlyPublicKey, Txid),
119    #[error("Transaction input is missing spend info")]
120    MissingSpendInfo,
121    #[error("Incorrect watchtower challenge data length")]
122    IncorrectWatchtowerChallengeDataLength,
123    #[error("Latest blockhash script must be a single script")]
124    LatestBlockhashScriptNumber,
125    #[error("Round index cannot be used to create a Round transaction: {0:?}")]
126    InvalidRoundIndex(RoundIndex),
127    #[error("Index overflow")]
128    IndexOverflow,
129    #[error("Kickoff winternitz keys in DB has wrong size compared to paramset")]
130    KickoffWinternitzKeysDBInconsistency,
131
132    #[error(transparent)]
133    Other(#[from] eyre::Report),
134}
135
136/// Types of all transactions that can be created. Some transactions have a
137/// (usize) index as there are multiple instances of the same transaction type
138/// per kickoff.
139#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
140pub enum TransactionType {
141    // --- Transaction Types ---
142    AssertTimeout(usize),
143    BurnUnusedKickoffConnectors,
144    Challenge,
145    ChallengeTimeout,
146    Disprove,
147    DisproveTimeout,
148    EmergencyStop,
149    Kickoff,
150    KickoffNotFinalized,
151    LatestBlockhash,
152    LatestBlockhashTimeout,
153    MiniAssert(usize),
154    MoveToVault,
155    OperatorChallengeAck(usize),
156    OperatorChallengeNack(usize),
157    OptimisticPayout,
158    Payout,
159    ReadyToReimburse,
160    Reimburse,
161    ReplacementDeposit,
162    Round,
163    UnspentKickoff(usize),
164    WatchtowerChallenge(usize),
165    WatchtowerChallengeTimeout(usize),
166
167    // --- Transaction Subsets ---
168    AllNeededForDeposit, // this will include all tx's that is to be signed for a deposit for verifiers
169    YieldKickoffTxid, // This is just to yield kickoff txid from the sighash stream, not used for anything else, sorry
170
171    /// For testing and for values to be replaced later.
172    Dummy,
173}
174
175// converter from proto type to rust enum
176impl TryFrom<GrpcTransactionId> for TransactionType {
177    type Error = ::prost::UnknownEnumValue;
178    fn try_from(value: GrpcTransactionId) -> Result<Self, Self::Error> {
179        use NormalTransactionId as Normal;
180        use NumberedTransactionType as Numbered;
181        // return err if id is None
182        let inner_id = value.id.ok_or(::prost::UnknownEnumValue(0))?;
183        match inner_id {
184            grpc_transaction_id::Id::NormalTransaction(idx) => {
185                let tx_type = NormalTransactionId::try_from(idx)?;
186                match tx_type {
187                    Normal::Round => Ok(Self::Round),
188                    Normal::Kickoff => Ok(Self::Kickoff),
189                    Normal::MoveToVault => Ok(Self::MoveToVault),
190                    Normal::Payout => Ok(Self::Payout),
191                    Normal::Challenge => Ok(Self::Challenge),
192                    Normal::Disprove => Ok(Self::Disprove),
193                    Normal::DisproveTimeout => Ok(Self::DisproveTimeout),
194                    Normal::Reimburse => Ok(Self::Reimburse),
195                    Normal::AllNeededForDeposit => Ok(Self::AllNeededForDeposit),
196                    Normal::Dummy => Ok(Self::Dummy),
197                    Normal::ReadyToReimburse => Ok(Self::ReadyToReimburse),
198                    Normal::KickoffNotFinalized => Ok(Self::KickoffNotFinalized),
199                    Normal::ChallengeTimeout => Ok(Self::ChallengeTimeout),
200                    Normal::UnspecifiedTransactionType => Err(::prost::UnknownEnumValue(idx)),
201                    Normal::BurnUnusedKickoffConnectors => Ok(Self::BurnUnusedKickoffConnectors),
202                    Normal::YieldKickoffTxid => Ok(Self::YieldKickoffTxid),
203                    Normal::ReplacementDeposit => Ok(Self::ReplacementDeposit),
204                    Normal::LatestBlockhashTimeout => Ok(Self::LatestBlockhashTimeout),
205                    Normal::LatestBlockhash => Ok(Self::LatestBlockhash),
206                    Normal::OptimisticPayout => Ok(Self::OptimisticPayout),
207                }
208            }
209            grpc_transaction_id::Id::NumberedTransaction(transaction_id) => {
210                let tx_type = NumberedTransactionType::try_from(transaction_id.transaction_type)?;
211                match tx_type {
212                    Numbered::WatchtowerChallenge => {
213                        Ok(Self::WatchtowerChallenge(transaction_id.index as usize))
214                    }
215                    Numbered::OperatorChallengeNack => {
216                        Ok(Self::OperatorChallengeNack(transaction_id.index as usize))
217                    }
218                    Numbered::OperatorChallengeAck => {
219                        Ok(Self::OperatorChallengeAck(transaction_id.index as usize))
220                    }
221                    Numbered::AssertTimeout => {
222                        Ok(Self::AssertTimeout(transaction_id.index as usize))
223                    }
224                    Numbered::UnspentKickoff => {
225                        Ok(Self::UnspentKickoff(transaction_id.index as usize))
226                    }
227                    Numbered::MiniAssert => Ok(Self::MiniAssert(transaction_id.index as usize)),
228                    Numbered::WatchtowerChallengeTimeout => Ok(Self::WatchtowerChallengeTimeout(
229                        transaction_id.index as usize,
230                    )),
231                    Numbered::UnspecifiedIndexedTransactionType => {
232                        Err(::prost::UnknownEnumValue(transaction_id.transaction_type))
233                    }
234                }
235            }
236        }
237    }
238}
239
240impl From<TransactionType> for GrpcTransactionId {
241    fn from(value: TransactionType) -> Self {
242        use grpc_transaction_id::Id::*;
243        use NormalTransactionId as Normal;
244        use NumberedTransactionType as Numbered;
245        GrpcTransactionId {
246            id: Some(match value {
247                TransactionType::Round => NormalTransaction(Normal::Round as i32),
248                TransactionType::Kickoff => NormalTransaction(Normal::Kickoff as i32),
249                TransactionType::MoveToVault => NormalTransaction(Normal::MoveToVault as i32),
250                TransactionType::Payout => NormalTransaction(Normal::Payout as i32),
251                TransactionType::Challenge => NormalTransaction(Normal::Challenge as i32),
252                TransactionType::Disprove => NormalTransaction(Normal::Disprove as i32),
253                TransactionType::DisproveTimeout => {
254                    NormalTransaction(Normal::DisproveTimeout as i32)
255                }
256                TransactionType::Reimburse => NormalTransaction(Normal::Reimburse as i32),
257                TransactionType::AllNeededForDeposit => {
258                    NormalTransaction(Normal::AllNeededForDeposit as i32)
259                }
260                TransactionType::Dummy => NormalTransaction(Normal::Dummy as i32),
261                TransactionType::ReadyToReimburse => {
262                    NormalTransaction(Normal::ReadyToReimburse as i32)
263                }
264                TransactionType::KickoffNotFinalized => {
265                    NormalTransaction(Normal::KickoffNotFinalized as i32)
266                }
267                TransactionType::ChallengeTimeout => {
268                    NormalTransaction(Normal::ChallengeTimeout as i32)
269                }
270                TransactionType::ReplacementDeposit => {
271                    NormalTransaction(Normal::ReplacementDeposit as i32)
272                }
273                TransactionType::LatestBlockhashTimeout => {
274                    NormalTransaction(Normal::LatestBlockhashTimeout as i32)
275                }
276                TransactionType::LatestBlockhash => {
277                    NormalTransaction(Normal::LatestBlockhash as i32)
278                }
279                TransactionType::OptimisticPayout => {
280                    NormalTransaction(Normal::OptimisticPayout as i32)
281                }
282                TransactionType::WatchtowerChallenge(index) => {
283                    NumberedTransaction(NumberedTransactionId {
284                        transaction_type: Numbered::WatchtowerChallenge as i32,
285                        index: index as i32,
286                    })
287                }
288                TransactionType::OperatorChallengeNack(index) => {
289                    NumberedTransaction(NumberedTransactionId {
290                        transaction_type: Numbered::OperatorChallengeNack as i32,
291                        index: index as i32,
292                    })
293                }
294                TransactionType::OperatorChallengeAck(index) => {
295                    NumberedTransaction(NumberedTransactionId {
296                        transaction_type: Numbered::OperatorChallengeAck as i32,
297                        index: index as i32,
298                    })
299                }
300                TransactionType::AssertTimeout(index) => {
301                    NumberedTransaction(NumberedTransactionId {
302                        transaction_type: Numbered::AssertTimeout as i32,
303                        index: index as i32,
304                    })
305                }
306                TransactionType::UnspentKickoff(index) => {
307                    NumberedTransaction(NumberedTransactionId {
308                        transaction_type: Numbered::UnspentKickoff as i32,
309                        index: index as i32,
310                    })
311                }
312                TransactionType::MiniAssert(index) => NumberedTransaction(NumberedTransactionId {
313                    transaction_type: Numbered::MiniAssert as i32,
314                    index: index as i32,
315                }),
316                TransactionType::WatchtowerChallengeTimeout(index) => {
317                    NumberedTransaction(NumberedTransactionId {
318                        transaction_type: Numbered::WatchtowerChallengeTimeout as i32,
319                        index: index as i32,
320                    })
321                }
322                TransactionType::BurnUnusedKickoffConnectors => {
323                    NormalTransaction(Normal::BurnUnusedKickoffConnectors as i32)
324                }
325                TransactionType::YieldKickoffTxid => {
326                    NormalTransaction(Normal::YieldKickoffTxid as i32)
327                }
328                TransactionType::EmergencyStop => {
329                    NormalTransaction(Normal::UnspecifiedTransactionType as i32)
330                }
331            }),
332        }
333    }
334}
335
336/// Creates a P2A (anchor) output for Child Pays For Parent (CPFP) fee bumping.
337///
338/// # Returns
339///
340/// A [`TxOut`] with a statically defined script and value, used as an anchor output in protocol transactions. The TxOut is spendable by anyone.
341pub fn anchor_output(amount: Amount) -> TxOut {
342    TxOut {
343        value: amount,
344        script_pubkey: ScriptBuf::from_hex("51024e73").expect("statically valid script"),
345    }
346}
347
348/// A non-ephemeral anchor output. It is used in tx's that should have a non-ephemeral anchor.
349/// Because ephemeral anchors force the tx to have 0 fee.
350pub fn non_ephemeral_anchor_output() -> TxOut {
351    TxOut {
352        value: NON_EPHEMERAL_ANCHOR_AMOUNT,
353        script_pubkey: ScriptBuf::from_hex("51024e73").expect("statically valid script"),
354    }
355}
356
357/// Creates an OP_RETURN output with the given data slice.
358///
359/// # Arguments
360///
361/// * `slice` - The data to embed in the OP_RETURN output.
362///
363/// # Returns
364///
365/// A [`TxOut`] with an OP_RETURN script containing the provided data.
366///
367/// # Warning
368///
369/// Does not check if the data is valid for an OP_RETURN script. Data must be at most 80 bytes.
370pub fn op_return_txout<S: AsRef<bitcoin::script::PushBytes>>(slice: S) -> TxOut {
371    let script = Builder::new()
372        .push_opcode(OP_RETURN)
373        .push_slice(slice)
374        .into_script();
375
376    TxOut {
377        value: Amount::from_sat(0),
378        script_pubkey: script,
379    }
380}
381
382/// Creates a [`TxHandler`] for the `move_to_vault_tx`.
383///
384/// This transaction moves funds to a N-of-N address from the deposit address created by the user that deposits into Citrea after all signature collection operations are done for the deposit.
385///
386/// # Arguments
387///
388/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
389/// * `paramset` - Protocol parameter set.
390///
391/// # Returns
392///
393/// A [`TxHandler`] for the move-to-vault transaction, or a [`BridgeError`] if construction fails.
394pub fn create_move_to_vault_txhandler(
395    deposit_data: &mut DepositData,
396    paramset: &'static ProtocolParamset,
397) -> Result<TxHandler<Unsigned>, BridgeError> {
398    let nofn_xonly_pk = deposit_data.get_nofn_xonly_pk()?;
399    let deposit_outpoint = deposit_data.get_deposit_outpoint();
400    let nofn_script = Arc::new(CheckSig::new(nofn_xonly_pk));
401    let security_council_script = Arc::new(Multisig::from_security_council(
402        deposit_data.security_council.clone(),
403    ));
404
405    let deposit_scripts = deposit_data.get_deposit_scripts(paramset)?;
406
407    Ok(TxHandlerBuilder::new(TransactionType::MoveToVault)
408        .with_version(NON_STANDARD_V3)
409        .add_input(
410            NormalSignatureKind::NotStored,
411            SpendableTxIn::from_scripts(
412                deposit_outpoint,
413                paramset.bridge_amount,
414                deposit_scripts,
415                None,
416                paramset.network,
417            ),
418            SpendPath::ScriptSpend(0),
419            DEFAULT_SEQUENCE,
420        )
421        .add_output(UnspentTxOut::from_scripts(
422            paramset.bridge_amount,
423            vec![nofn_script, security_council_script],
424            None,
425            paramset.network,
426        ))
427        // always use 0 sat anchor for move_tx, this will keep the amount in move to vault tx exactly the bridge amount
428        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
429            0,
430        ))))
431        .finalize())
432}
433
434/// Creates a [`TxHandler`] for the `emergency_stop_tx`.
435///
436/// This transaction moves funds to the address controlled by the security council from the move-to-vault txout.
437/// Used to stop the deposit in case of a security issue. The moved funds will eventually be redeposited using the replacement deposit tx.
438///
439/// # Arguments
440///
441/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
442/// * `move_to_vault_txhandler` - Reference to the move-to-vault transaction handler.
443/// * `paramset` - Protocol parameter set.
444///
445/// # Returns
446///
447/// A [`TxHandler`] for the emergency stop transaction, or a [`BridgeError`] if construction fails.
448pub fn create_emergency_stop_txhandler(
449    deposit_data: &mut DepositData,
450    move_to_vault_txhandler: &TxHandler,
451    paramset: &'static ProtocolParamset,
452) -> Result<TxHandler<Unsigned>, BridgeError> {
453    // Hand calculated, total tx size is 11 + 126 * NUM_EMERGENCY_STOPS
454    const EACH_EMERGENCY_STOP_VBYTES: Amount = Amount::from_sat(126);
455    let security_council = deposit_data.security_council.clone();
456
457    let builder = TxHandlerBuilder::new(TransactionType::EmergencyStop)
458        .add_input(
459            NormalSignatureKind::NotStored,
460            move_to_vault_txhandler.get_spendable_output(UtxoVout::DepositInMove)?,
461            SpendPath::ScriptSpend(0),
462            DEFAULT_SEQUENCE,
463        )
464        .add_output(UnspentTxOut::from_scripts(
465            paramset.bridge_amount - paramset.anchor_amount() - EACH_EMERGENCY_STOP_VBYTES * 3,
466            vec![Arc::new(Multisig::from_security_council(security_council))],
467            None,
468            paramset.network,
469        ))
470        .finalize();
471
472    Ok(builder)
473}
474
475/// Combines multiple emergency stop transactions into a single transaction.
476///
477/// # Arguments
478///
479/// * `txs` - A vector of (Txid, Transaction) pairs, each representing a signed emergency stop transaction using Sighash Single | AnyoneCanPay.
480/// * `add_anchor` - If true, an anchor output will be appended to the outputs.
481///
482/// # Returns
483///
484/// A new [`Transaction`] that merges all inputs and outputs from the provided transactions, optionally adding an anchor output.
485///
486/// # Warning
487///
488/// This function does not perform any safety checks and assumes all inputs/outputs are valid and compatible.
489pub fn combine_emergency_stop_txhandler(
490    txs: Vec<(Txid, Transaction)>,
491    add_anchor: bool,
492    paramset: &'static ProtocolParamset,
493) -> Transaction {
494    let (inputs, mut outputs): (Vec<TxIn>, Vec<TxOut>) = txs
495        .into_iter()
496        .map(|(_, tx)| (tx.input[0].clone(), tx.output[0].clone()))
497        .unzip();
498
499    if add_anchor {
500        outputs.push(anchor_output(paramset.anchor_amount()));
501    }
502
503    Transaction {
504        version: Version::non_standard(2),
505        lock_time: bitcoin::absolute::LockTime::ZERO,
506        input: inputs,
507        output: outputs,
508    }
509}
510
511/// Creates a [`TxHandler`] for the `replacement_deposit_tx`.
512///
513/// This transaction replaces a previous deposit with a new deposit.
514/// In the its script, it commits the old move_to_vault txid that it replaces.
515///
516/// # Arguments
517///
518/// * `old_move_txid` - The txid of the old move_to_vault transaction that is being replaced.
519/// * `input_outpoint` - The outpoint of the input to the replacement deposit tx that holds bridge amount.
520/// * `nofn_xonly_pk` - The N-of-N XOnlyPublicKey for the deposit.
521/// * `paramset` - The protocol paramset.
522/// * `security_council` - The security council.
523///
524/// # Returns
525///
526/// A [`TxHandler`] for the replacement deposit transaction, or a [`BridgeError`] if construction fails.
527pub fn create_replacement_deposit_txhandler(
528    old_move_txid: Txid,
529    input_outpoint: OutPoint,
530    old_nofn_xonly_pk: XOnlyPublicKey,
531    new_nofn_xonly_pk: XOnlyPublicKey,
532    paramset: &'static ProtocolParamset,
533    security_council: SecurityCouncil,
534) -> Result<TxHandler, BridgeError> {
535    Ok(TxHandlerBuilder::new(TransactionType::ReplacementDeposit)
536        .with_version(NON_STANDARD_V3)
537        .add_input(
538            NormalSignatureKind::NoSignature,
539            SpendableTxIn::from_scripts(
540                input_outpoint,
541                paramset.bridge_amount,
542                vec![
543                    Arc::new(CheckSig::new(old_nofn_xonly_pk)),
544                    Arc::new(Multisig::from_security_council(security_council.clone())),
545                ],
546                None,
547                paramset.network,
548            ),
549            crate::builder::script::SpendPath::ScriptSpend(0),
550            DEFAULT_SEQUENCE,
551        )
552        .add_output(UnspentTxOut::from_scripts(
553            paramset.bridge_amount,
554            vec![
555                Arc::new(ReplacementDepositScript::new(
556                    new_nofn_xonly_pk,
557                    old_move_txid,
558                )),
559                Arc::new(Multisig::from_security_council(security_council)),
560            ],
561            None,
562            paramset.network,
563        ))
564        // always use 0 sat anchor for replacement deposit tx, this will keep the amount in replacement deposit tx exactly the bridge amount
565        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
566            0,
567        ))))
568        .finalize())
569}
570
571/// Creates a Taproot output for a disprove path, combining a script, an additional disprove script, and a hidden node containing the BitVM disprove scripts.
572///
573/// # Arguments
574///
575/// * `operator_timeout_script` - The operator timeout script.
576/// * `additional_script` - An additional script to include in the Taproot tree. This single additional script is generated by Clementine bridge in addition to the disprove scripts coming from BitVM side.
577/// * `disprove_root_hash` - The root hash for the hidden script merkle tree node. The scripts included in the root hash are the BitVM disprove scripts.
578/// * `amount` - The output amount.
579/// * `network` - The Bitcoin network.
580///
581/// # Returns
582///
583/// An [`UnspentTxOut`] representing the Taproot TxOut.
584pub fn create_disprove_taproot_output(
585    operator_timeout_script: Arc<dyn SpendableScript>,
586    additional_script: ScriptBuf,
587    disprove_path: DisprovePath,
588    amount: Amount,
589    network: bitcoin::Network,
590) -> UnspentTxOut {
591    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
592    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
593
594    let mut scripts: Vec<ScriptBuf> = vec![additional_script.clone()];
595
596    let builder = match disprove_path.clone() {
597        DisprovePath::Scripts(extra_scripts) => {
598            let mut builder = TaprootBuilder::new();
599
600            builder = builder
601                .add_leaf(1, operator_timeout_script.to_script_buf())
602                .expect("add operator timeout script")
603                .add_leaf(2, additional_script)
604                .expect("add additional script");
605
606            // 1. Calculate depths. This is cheap and doesn't need ownership of scripts.
607            let depths = calculate_taproot_leaf_depths(extra_scripts.len());
608
609            // 2. Zip depths with an iterator over the scripts.
610            //    We clone the `script` inside the loop because the builder needs an owned value.
611            //    This is more efficient than cloning the whole Vec upfront.
612            for (depth, script) in depths.into_iter().zip(extra_scripts.iter()) {
613                let main_tree_depth = 2 + depth;
614                builder = builder
615                    .add_leaf(main_tree_depth, script.clone())
616                    .expect("add inlined disprove script");
617            }
618
619            // 3. Now, move the original `extra_scripts` into `scripts.extend`. No clone needed.
620            scripts.extend(extra_scripts);
621            builder
622        }
623        DisprovePath::HiddenNode(root_hash) => TaprootBuilder::new()
624            .add_leaf(1, operator_timeout_script.to_script_buf())
625            .expect("empty taptree will accept a script node")
626            .add_leaf(2, additional_script)
627            .expect("taptree with one node will accept a node at depth 2")
628            .add_hidden_node(2, TapNodeHash::from_byte_array(*root_hash))
629            .expect("taptree with two nodes will accept a node at depth 2"),
630    };
631
632    let taproot_spend_info = builder
633        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
634        .expect("valid taptree");
635
636    let address = Address::p2tr(
637        &SECP,
638        *UNSPENDABLE_XONLY_PUBKEY,
639        taproot_spend_info.merkle_root(),
640        network,
641    );
642
643    let mut spendable_scripts: Vec<Arc<dyn SpendableScript>> = vec![operator_timeout_script];
644    let other_spendable_scripts: Vec<Arc<dyn SpendableScript>> = scripts
645        .into_iter()
646        .map(|script| Arc::new(OtherSpendable::new(script)) as Arc<dyn SpendableScript>)
647        .collect();
648
649    spendable_scripts.extend(other_spendable_scripts);
650
651    UnspentTxOut::new(
652        TxOut {
653            value: amount,
654            script_pubkey: address.script_pubkey(),
655        },
656        spendable_scripts,
657        Some(taproot_spend_info),
658    )
659}
660
661/// Helper function to create a Taproot output that combines a single script and a root hash containing any number of scripts.
662/// The main use case for this function is to speed up the tx creating during a deposit. We don't need to create and combine all the
663/// scripts in the taproot repeatedly, but cache and combine the common scripts for each kickoff tx to a root hash, and add an additional script
664/// that depends on the specific operator or nofn_pk that is signing the deposit.
665///
666/// # Arguments
667///
668/// * `script` - The one additional script to include in the merkle tree.
669/// * `hidden_node` - The root hash for the merkle tree node. The node can contain any number of scripts.
670/// * `amount` - The output amount.
671/// * `network` - The Bitcoin network.
672///
673/// # Returns
674///
675/// An [`UnspentTxOut`] representing the Taproot TxOut.
676pub fn create_taproot_output_with_hidden_node(
677    script: Arc<dyn SpendableScript>,
678    hidden_node: HiddenNode,
679    amount: Amount,
680    network: bitcoin::Network,
681) -> UnspentTxOut {
682    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
683    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
684
685    let builder = TaprootBuilder::new()
686        .add_leaf(1, script.to_script_buf())
687        .expect("empty taptree will accept a script node")
688        .add_hidden_node(1, TapNodeHash::from_byte_array(*hidden_node))
689        .expect("taptree with one node will accept a node at depth 1");
690
691    let taproot_spend_info = builder
692        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
693        .expect("cannot fail since it is a valid taptree");
694
695    let address = Address::p2tr(
696        &SECP,
697        *UNSPENDABLE_XONLY_PUBKEY,
698        taproot_spend_info.merkle_root(),
699        network,
700    );
701
702    UnspentTxOut::new(
703        TxOut {
704            value: amount,
705            script_pubkey: address.script_pubkey(),
706        },
707        vec![script.clone()],
708        Some(taproot_spend_info),
709    )
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use bitcoin::secp256k1::XOnlyPublicKey;
716    use std::str::FromStr;
717
718    #[test]
719    fn test_security_council_from_str() {
720        // Create some test public keys
721        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
722        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
723
724        // Test valid input
725        let input = format!(
726            "2:{},{}",
727            hex::encode(pk1.serialize()),
728            hex::encode(pk2.serialize())
729        );
730        let council = SecurityCouncil::from_str(&input).unwrap();
731        assert_eq!(council.threshold, 2);
732        assert_eq!(council.pks.len(), 2);
733        assert_eq!(council.pks[0], pk1);
734        assert_eq!(council.pks[1], pk2);
735
736        // Test invalid threshold
737        let input = format!(
738            "3:{},{}",
739            hex::encode(pk1.serialize()),
740            hex::encode(pk2.serialize())
741        );
742        assert!(SecurityCouncil::from_str(&input).is_err());
743
744        // Test invalid hex
745        let input = "2:invalid,pk2";
746        assert!(SecurityCouncil::from_str(input).is_err());
747
748        // Test missing parts
749        assert!(SecurityCouncil::from_str("2").is_err());
750        assert!(SecurityCouncil::from_str(":").is_err());
751
752        // Test too many parts
753        let input = format!(
754            "2:{},{}:extra",
755            hex::encode(pk1.serialize()),
756            hex::encode(pk2.serialize())
757        );
758        assert!(SecurityCouncil::from_str(&input).is_err());
759
760        // Test empty public keys
761        assert!(SecurityCouncil::from_str("2:").is_err());
762    }
763
764    #[test]
765    fn test_security_council_round_trip() {
766        // Create some test public keys
767        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
768        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
769
770        let original = SecurityCouncil {
771            pks: vec![pk1, pk2],
772            threshold: 2,
773        };
774
775        let string = original.to_string();
776        let parsed = SecurityCouncil::from_str(&string).unwrap();
777
778        assert_eq!(original, parsed);
779    }
780}