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
32#[cfg(test)]
33use super::script::ReplacementDepositScript;
34use super::script::SpendPath;
35use super::script::{CheckSig, Multisig, SpendableScript};
36use crate::builder::address::calculate_taproot_leaf_depths;
37use crate::builder::script::OtherSpendable;
38use crate::builder::transaction::challenge::*;
39use crate::builder::transaction::input::SpendableTxIn;
40use crate::builder::transaction::operator_assert::*;
41use crate::builder::transaction::operator_collateral::*;
42use crate::builder::transaction::operator_reimburse::*;
43use crate::builder::transaction::output::UnspentTxOut;
44use crate::config::protocol::ProtocolParamset;
45use crate::constants::{NON_EPHEMERAL_ANCHOR_AMOUNT, NON_STANDARD_V3};
46use crate::deposit::DepositData;
47#[cfg(test)]
48use crate::deposit::SecurityCouncil;
49use crate::rpc::clementine::grpc_transaction_id;
50use crate::rpc::clementine::GrpcTransactionId;
51use crate::rpc::clementine::{
52    NormalSignatureKind, NormalTransactionId, NumberedTransactionId, NumberedTransactionType,
53};
54use bitcoin::hashes::Hash;
55use bitcoin::opcodes::all::OP_RETURN;
56use bitcoin::script::Builder;
57use bitcoin::transaction::Version;
58use bitcoin::{Address, Amount, ScriptBuf, TxOut};
59#[cfg(test)]
60use bitcoin::{OutPoint, Txid, XOnlyPublicKey};
61use clementine_errors::BridgeError;
62use clementine_primitives::{RoundIndex, TransactionType};
63use input::UtxoVout;
64use std::sync::Arc;
65
66// Exports to the outside
67pub use crate::builder::transaction::txhandler::*;
68pub use creator::{
69    create_round_txhandlers, create_txhandlers, ContractContext, KickoffWinternitzKeys,
70    ReimburseDbCache, TxHandlerCache,
71};
72pub use operator_collateral::{
73    create_burn_unused_kickoff_connectors_txhandler, create_round_nth_txhandler,
74};
75pub use operator_reimburse::{create_optimistic_payout_txhandler, create_payout_txhandler};
76pub use txhandler::Unsigned;
77
78pub mod challenge;
79mod creator;
80pub mod deposit_signature_owner;
81pub mod input;
82mod operator_assert;
83mod operator_collateral;
84mod operator_reimburse;
85pub mod output;
86pub mod sign;
87mod txhandler;
88
89type HiddenNode<'a> = &'a [u8; 32];
90
91// converter from proto type to rust enum
92impl TryFrom<GrpcTransactionId> for TransactionType {
93    type Error = ::prost::UnknownEnumValue;
94    fn try_from(value: GrpcTransactionId) -> Result<Self, Self::Error> {
95        use NormalTransactionId as Normal;
96        use NumberedTransactionType as Numbered;
97        // return err if id is None
98        let inner_id = value.id.ok_or(::prost::UnknownEnumValue(0))?;
99        match inner_id {
100            grpc_transaction_id::Id::NormalTransaction(idx) => {
101                let tx_type = NormalTransactionId::try_from(idx)?;
102                match tx_type {
103                    Normal::Round => Ok(Self::Round),
104                    Normal::Kickoff => Ok(Self::Kickoff),
105                    Normal::MoveToVault => Ok(Self::MoveToVault),
106                    Normal::Payout => Ok(Self::Payout),
107                    Normal::Challenge => Ok(Self::Challenge),
108                    Normal::Disprove => Ok(Self::Disprove),
109                    Normal::DisproveTimeout => Ok(Self::DisproveTimeout),
110                    Normal::Reimburse => Ok(Self::Reimburse),
111                    Normal::AllNeededForDeposit => Ok(Self::AllNeededForDeposit),
112                    Normal::Dummy => Ok(Self::Dummy),
113                    Normal::ReadyToReimburse => Ok(Self::ReadyToReimburse),
114                    Normal::KickoffNotFinalized => Ok(Self::KickoffNotFinalized),
115                    Normal::ChallengeTimeout => Ok(Self::ChallengeTimeout),
116                    Normal::UnspecifiedTransactionType => Err(::prost::UnknownEnumValue(idx)),
117                    Normal::BurnUnusedKickoffConnectors => Ok(Self::BurnUnusedKickoffConnectors),
118                    Normal::YieldKickoffTxid => Ok(Self::YieldKickoffTxid),
119                    Normal::ReplacementDeposit => Ok(Self::ReplacementDeposit),
120                    Normal::LatestBlockhashTimeout => Ok(Self::LatestBlockhashTimeout),
121                    Normal::LatestBlockhash => Ok(Self::LatestBlockhash),
122                    Normal::OptimisticPayout => Ok(Self::OptimisticPayout),
123                }
124            }
125            grpc_transaction_id::Id::NumberedTransaction(transaction_id) => {
126                let tx_type = NumberedTransactionType::try_from(transaction_id.transaction_type)?;
127                match tx_type {
128                    Numbered::WatchtowerChallenge => {
129                        Ok(Self::WatchtowerChallenge(transaction_id.index as usize))
130                    }
131                    Numbered::OperatorChallengeNack => {
132                        Ok(Self::OperatorChallengeNack(transaction_id.index as usize))
133                    }
134                    Numbered::OperatorChallengeAck => {
135                        Ok(Self::OperatorChallengeAck(transaction_id.index as usize))
136                    }
137                    Numbered::AssertTimeout => {
138                        Ok(Self::AssertTimeout(transaction_id.index as usize))
139                    }
140                    Numbered::UnspentKickoff => {
141                        Ok(Self::UnspentKickoff(transaction_id.index as usize))
142                    }
143                    Numbered::MiniAssert => Ok(Self::MiniAssert(transaction_id.index as usize)),
144                    Numbered::WatchtowerChallengeTimeout => Ok(Self::WatchtowerChallengeTimeout(
145                        transaction_id.index as usize,
146                    )),
147                    Numbered::UnspecifiedIndexedTransactionType => {
148                        Err(::prost::UnknownEnumValue(transaction_id.transaction_type))
149                    }
150                }
151            }
152        }
153    }
154}
155
156impl From<TransactionType> for GrpcTransactionId {
157    fn from(value: TransactionType) -> Self {
158        use grpc_transaction_id::Id::*;
159        use NormalTransactionId as Normal;
160        use NumberedTransactionType as Numbered;
161        GrpcTransactionId {
162            id: Some(match value {
163                TransactionType::Round => NormalTransaction(Normal::Round as i32),
164                TransactionType::Kickoff => NormalTransaction(Normal::Kickoff as i32),
165                TransactionType::MoveToVault => NormalTransaction(Normal::MoveToVault as i32),
166                TransactionType::Payout => NormalTransaction(Normal::Payout as i32),
167                TransactionType::Challenge => NormalTransaction(Normal::Challenge as i32),
168                TransactionType::Disprove => NormalTransaction(Normal::Disprove as i32),
169                TransactionType::DisproveTimeout => {
170                    NormalTransaction(Normal::DisproveTimeout as i32)
171                }
172                TransactionType::Reimburse => NormalTransaction(Normal::Reimburse as i32),
173                TransactionType::AllNeededForDeposit => {
174                    NormalTransaction(Normal::AllNeededForDeposit as i32)
175                }
176                TransactionType::Dummy => NormalTransaction(Normal::Dummy as i32),
177                TransactionType::ReadyToReimburse => {
178                    NormalTransaction(Normal::ReadyToReimburse as i32)
179                }
180                TransactionType::KickoffNotFinalized => {
181                    NormalTransaction(Normal::KickoffNotFinalized as i32)
182                }
183                TransactionType::ChallengeTimeout => {
184                    NormalTransaction(Normal::ChallengeTimeout as i32)
185                }
186                TransactionType::ReplacementDeposit => {
187                    NormalTransaction(Normal::ReplacementDeposit as i32)
188                }
189                TransactionType::LatestBlockhashTimeout => {
190                    NormalTransaction(Normal::LatestBlockhashTimeout as i32)
191                }
192                TransactionType::LatestBlockhash => {
193                    NormalTransaction(Normal::LatestBlockhash as i32)
194                }
195                TransactionType::OptimisticPayout => {
196                    NormalTransaction(Normal::OptimisticPayout as i32)
197                }
198                TransactionType::WatchtowerChallenge(index) => {
199                    NumberedTransaction(NumberedTransactionId {
200                        transaction_type: Numbered::WatchtowerChallenge as i32,
201                        index: index as i32,
202                    })
203                }
204                TransactionType::OperatorChallengeNack(index) => {
205                    NumberedTransaction(NumberedTransactionId {
206                        transaction_type: Numbered::OperatorChallengeNack as i32,
207                        index: index as i32,
208                    })
209                }
210                TransactionType::OperatorChallengeAck(index) => {
211                    NumberedTransaction(NumberedTransactionId {
212                        transaction_type: Numbered::OperatorChallengeAck as i32,
213                        index: index as i32,
214                    })
215                }
216                TransactionType::AssertTimeout(index) => {
217                    NumberedTransaction(NumberedTransactionId {
218                        transaction_type: Numbered::AssertTimeout as i32,
219                        index: index as i32,
220                    })
221                }
222                TransactionType::UnspentKickoff(index) => {
223                    NumberedTransaction(NumberedTransactionId {
224                        transaction_type: Numbered::UnspentKickoff as i32,
225                        index: index as i32,
226                    })
227                }
228                TransactionType::MiniAssert(index) => NumberedTransaction(NumberedTransactionId {
229                    transaction_type: Numbered::MiniAssert as i32,
230                    index: index as i32,
231                }),
232                TransactionType::WatchtowerChallengeTimeout(index) => {
233                    NumberedTransaction(NumberedTransactionId {
234                        transaction_type: Numbered::WatchtowerChallengeTimeout as i32,
235                        index: index as i32,
236                    })
237                }
238                TransactionType::BurnUnusedKickoffConnectors => {
239                    NormalTransaction(Normal::BurnUnusedKickoffConnectors as i32)
240                }
241                TransactionType::YieldKickoffTxid => {
242                    NormalTransaction(Normal::YieldKickoffTxid as i32)
243                }
244                TransactionType::EmergencyStop => {
245                    NormalTransaction(Normal::UnspecifiedTransactionType as i32)
246                }
247            }),
248        }
249    }
250}
251
252/// Creates a P2A (anchor) output for Child Pays For Parent (CPFP) fee bumping.
253///
254/// # Returns
255///
256/// A [`TxOut`] with a statically defined script and value, used as an anchor output in protocol transactions. The TxOut is spendable by anyone.
257pub fn anchor_output(amount: Amount) -> TxOut {
258    TxOut {
259        value: amount,
260        script_pubkey: ScriptBuf::from_hex("51024e73").expect("statically valid script"),
261    }
262}
263
264/// A non-ephemeral anchor output. It is used in tx's that should have a non-ephemeral anchor.
265/// Because ephemeral anchors force the tx to have 0 fee.
266pub fn non_ephemeral_anchor_output() -> TxOut {
267    TxOut {
268        value: NON_EPHEMERAL_ANCHOR_AMOUNT,
269        script_pubkey: ScriptBuf::from_hex("51024e73").expect("statically valid script"),
270    }
271}
272
273/// Creates an OP_RETURN output with the given data slice.
274///
275/// # Arguments
276///
277/// * `slice` - The data to embed in the OP_RETURN output.
278///
279/// # Returns
280///
281/// A [`TxOut`] with an OP_RETURN script containing the provided data.
282///
283/// # Warning
284///
285/// Does not check if the data is valid for an OP_RETURN script. Data must be at most 80 bytes.
286pub fn op_return_txout<S: AsRef<bitcoin::script::PushBytes>>(slice: S) -> TxOut {
287    let script = Builder::new()
288        .push_opcode(OP_RETURN)
289        .push_slice(slice)
290        .into_script();
291
292    TxOut {
293        value: Amount::from_sat(0),
294        script_pubkey: script,
295    }
296}
297
298/// Creates a [`TxHandler`] for the `move_to_vault_tx`.
299///
300/// 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.
301///
302/// # Arguments
303///
304/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
305/// * `paramset` - Protocol parameter set.
306///
307/// # Returns
308///
309/// A [`TxHandler`] for the move-to-vault transaction, or a [`BridgeError`] if construction fails.
310pub fn create_move_to_vault_txhandler(
311    deposit_data: &mut DepositData,
312    paramset: &'static ProtocolParamset,
313) -> Result<TxHandler<Unsigned>, BridgeError> {
314    let nofn_xonly_pk = deposit_data.get_nofn_xonly_pk()?;
315    let deposit_outpoint = deposit_data.get_deposit_outpoint();
316    let nofn_script = Arc::new(CheckSig::new(nofn_xonly_pk));
317    let security_council_script = Arc::new(Multisig::from_security_council(
318        deposit_data.security_council.clone(),
319    ));
320
321    let deposit_scripts = deposit_data.get_deposit_scripts(paramset)?;
322
323    Ok(TxHandlerBuilder::new(TransactionType::MoveToVault)
324        .with_version(NON_STANDARD_V3)
325        .add_input(
326            NormalSignatureKind::NotStored,
327            SpendableTxIn::from_scripts(
328                deposit_outpoint,
329                paramset.bridge_amount,
330                deposit_scripts,
331                None,
332                paramset.network,
333            ),
334            SpendPath::ScriptSpend(0),
335            DEFAULT_SEQUENCE,
336        )
337        .add_output(UnspentTxOut::from_scripts(
338            paramset.bridge_amount,
339            vec![nofn_script, security_council_script],
340            None,
341            paramset.network,
342        ))
343        // always use 0 sat anchor for move_tx, this will keep the amount in move to vault tx exactly the bridge amount
344        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
345            0,
346        ))))
347        .finalize())
348}
349
350/// Creates a [`TxHandler`] for the `emergency_stop_tx`.
351///
352/// This transaction moves funds to the address controlled by the security council from the move-to-vault txout.
353/// Used to stop the deposit in case of a security issue. The moved funds will eventually be redeposited using the replacement deposit tx.
354///
355/// # Arguments
356///
357/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
358/// * `move_to_vault_txhandler` - Reference to the move-to-vault transaction handler.
359/// * `paramset` - Protocol parameter set.
360///
361/// # Returns
362///
363/// A [`TxHandler`] for the emergency stop transaction, or a [`BridgeError`] if construction fails.
364pub fn create_emergency_stop_txhandler(
365    deposit_data: &mut DepositData,
366    move_to_vault_txhandler: &TxHandler,
367    paramset: &'static ProtocolParamset,
368) -> Result<TxHandler<Unsigned>, BridgeError> {
369    // Hand calculated, total tx size is 11 + 126 * NUM_EMERGENCY_STOPS
370    const EACH_EMERGENCY_STOP_VBYTES: Amount = Amount::from_sat(126);
371    let security_council = deposit_data.security_council.clone();
372
373    let builder = TxHandlerBuilder::new(TransactionType::EmergencyStop)
374        .add_input(
375            NormalSignatureKind::NotStored,
376            move_to_vault_txhandler.get_spendable_output(UtxoVout::DepositInMove)?,
377            SpendPath::ScriptSpend(0),
378            DEFAULT_SEQUENCE,
379        )
380        .add_output(UnspentTxOut::from_scripts(
381            paramset.bridge_amount - paramset.anchor_amount() - EACH_EMERGENCY_STOP_VBYTES * 3,
382            vec![Arc::new(Multisig::from_security_council(security_council))],
383            None,
384            paramset.network,
385        ))
386        .finalize();
387
388    Ok(builder)
389}
390
391/// Creates a [`TxHandler`] for the `replacement_deposit_tx`.
392///
393/// This transaction replaces a previous deposit with a new deposit.
394/// In the its script, it commits the old move_to_vault txid that it replaces.
395///
396/// # Arguments
397///
398/// * `old_move_txid` - The txid of the old move_to_vault transaction that is being replaced.
399/// * `input_outpoint` - The outpoint of the input to the replacement deposit tx that holds bridge amount.
400/// * `nofn_xonly_pk` - The N-of-N XOnlyPublicKey for the deposit.
401/// * `paramset` - The protocol paramset.
402/// * `security_council` - The security council.
403///
404/// # Returns
405///
406/// A [`TxHandler`] for the replacement deposit transaction, or a [`BridgeError`] if construction fails.
407#[cfg(test)]
408pub fn create_replacement_deposit_txhandler(
409    old_move_txid: Txid,
410    input_outpoint: OutPoint,
411    old_nofn_xonly_pk: XOnlyPublicKey,
412    new_nofn_xonly_pk: XOnlyPublicKey,
413    paramset: &'static ProtocolParamset,
414    security_council: SecurityCouncil,
415) -> Result<TxHandler, BridgeError> {
416    Ok(TxHandlerBuilder::new(TransactionType::ReplacementDeposit)
417        .with_version(NON_STANDARD_V3)
418        .add_input(
419            NormalSignatureKind::NoSignature,
420            SpendableTxIn::from_scripts(
421                input_outpoint,
422                paramset.bridge_amount,
423                vec![
424                    Arc::new(CheckSig::new(old_nofn_xonly_pk)),
425                    Arc::new(Multisig::from_security_council(security_council.clone())),
426                ],
427                None,
428                paramset.network,
429            ),
430            crate::builder::script::SpendPath::ScriptSpend(0),
431            DEFAULT_SEQUENCE,
432        )
433        .add_output(UnspentTxOut::from_scripts(
434            paramset.bridge_amount,
435            vec![
436                Arc::new(ReplacementDepositScript::new(
437                    new_nofn_xonly_pk,
438                    old_move_txid,
439                )),
440                Arc::new(Multisig::from_security_council(security_council)),
441            ],
442            None,
443            paramset.network,
444        ))
445        // always use 0 sat anchor for replacement deposit tx, this will keep the amount in replacement deposit tx exactly the bridge amount
446        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
447            0,
448        ))))
449        .finalize())
450}
451
452/// Creates a Taproot output for a disprove path, combining a script, an additional disprove script, and a hidden node containing the BitVM disprove scripts.
453///
454/// # Arguments
455///
456/// * `operator_timeout_script` - The operator timeout script.
457/// * `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.
458/// * `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.
459/// * `amount` - The output amount.
460/// * `network` - The Bitcoin network.
461///
462/// # Returns
463///
464/// An [`UnspentTxOut`] representing the Taproot TxOut.
465pub fn create_disprove_taproot_output(
466    operator_timeout_script: Arc<dyn SpendableScript>,
467    additional_script: ScriptBuf,
468    disprove_path: DisprovePath,
469    amount: Amount,
470    network: bitcoin::Network,
471) -> UnspentTxOut {
472    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
473    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
474
475    let mut scripts: Vec<ScriptBuf> = vec![additional_script.clone()];
476
477    let builder = match disprove_path.clone() {
478        DisprovePath::Scripts(extra_scripts) => {
479            let mut builder = TaprootBuilder::new();
480
481            builder = builder
482                .add_leaf(1, operator_timeout_script.to_script_buf())
483                .expect("add operator timeout script")
484                .add_leaf(2, additional_script)
485                .expect("add additional script");
486
487            // 1. Calculate depths. This is cheap and doesn't need ownership of scripts.
488            let depths = calculate_taproot_leaf_depths(extra_scripts.len());
489
490            // 2. Zip depths with an iterator over the scripts.
491            //    We clone the `script` inside the loop because the builder needs an owned value.
492            //    This is more efficient than cloning the whole Vec upfront.
493            for (depth, script) in depths.into_iter().zip(extra_scripts.iter()) {
494                let main_tree_depth = 2 + depth;
495                builder = builder
496                    .add_leaf(main_tree_depth, script.clone())
497                    .expect("add inlined disprove script");
498            }
499
500            // 3. Now, move the original `extra_scripts` into `scripts.extend`. No clone needed.
501            scripts.extend(extra_scripts);
502            builder
503        }
504        DisprovePath::HiddenNode(root_hash) => TaprootBuilder::new()
505            .add_leaf(1, operator_timeout_script.to_script_buf())
506            .expect("empty taptree will accept a script node")
507            .add_leaf(2, additional_script)
508            .expect("taptree with one node will accept a node at depth 2")
509            .add_hidden_node(2, TapNodeHash::from_byte_array(*root_hash))
510            .expect("taptree with two nodes will accept a node at depth 2"),
511    };
512
513    let taproot_spend_info = builder
514        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
515        .expect("valid taptree");
516
517    let address = Address::p2tr(
518        &SECP,
519        *UNSPENDABLE_XONLY_PUBKEY,
520        taproot_spend_info.merkle_root(),
521        network,
522    );
523
524    let mut spendable_scripts: Vec<Arc<dyn SpendableScript>> = vec![operator_timeout_script];
525    let other_spendable_scripts: Vec<Arc<dyn SpendableScript>> = scripts
526        .into_iter()
527        .map(|script| Arc::new(OtherSpendable::new(script)) as Arc<dyn SpendableScript>)
528        .collect();
529
530    spendable_scripts.extend(other_spendable_scripts);
531
532    UnspentTxOut::new(
533        TxOut {
534            value: amount,
535            script_pubkey: address.script_pubkey(),
536        },
537        spendable_scripts,
538        Some(taproot_spend_info),
539    )
540}
541
542/// Helper function to create a Taproot output that combines a single script and a root hash containing any number of scripts.
543/// 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
544/// 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
545/// that depends on the specific operator or nofn_pk that is signing the deposit.
546///
547/// # Arguments
548///
549/// * `script` - The one additional script to include in the merkle tree.
550/// * `hidden_node` - The root hash for the merkle tree node. The node can contain any number of scripts.
551/// * `amount` - The output amount.
552/// * `network` - The Bitcoin network.
553///
554/// # Returns
555///
556/// An [`UnspentTxOut`] representing the Taproot TxOut.
557pub fn create_taproot_output_with_hidden_node(
558    script: Arc<dyn SpendableScript>,
559    hidden_node: HiddenNode,
560    amount: Amount,
561    network: bitcoin::Network,
562) -> UnspentTxOut {
563    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
564    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
565
566    let builder = TaprootBuilder::new()
567        .add_leaf(1, script.to_script_buf())
568        .expect("empty taptree will accept a script node")
569        .add_hidden_node(1, TapNodeHash::from_byte_array(*hidden_node))
570        .expect("taptree with one node will accept a node at depth 1");
571
572    let taproot_spend_info = builder
573        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
574        .expect("cannot fail since it is a valid taptree");
575
576    let address = Address::p2tr(
577        &SECP,
578        *UNSPENDABLE_XONLY_PUBKEY,
579        taproot_spend_info.merkle_root(),
580        network,
581    );
582
583    UnspentTxOut::new(
584        TxOut {
585            value: amount,
586            script_pubkey: address.script_pubkey(),
587        },
588        vec![script.clone()],
589        Some(taproot_spend_info),
590    )
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use bitcoin::secp256k1::XOnlyPublicKey;
597    use std::str::FromStr;
598
599    #[test]
600    fn test_security_council_from_str() {
601        // Create some test public keys
602        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
603        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
604
605        // Test valid input
606        let input = format!(
607            "2:{},{}",
608            hex::encode(pk1.serialize()),
609            hex::encode(pk2.serialize())
610        );
611        let council = SecurityCouncil::from_str(&input).unwrap();
612        assert_eq!(council.threshold, 2);
613        assert_eq!(council.pks.len(), 2);
614        assert_eq!(council.pks[0], pk1);
615        assert_eq!(council.pks[1], pk2);
616
617        // Test invalid threshold
618        let input = format!(
619            "3:{},{}",
620            hex::encode(pk1.serialize()),
621            hex::encode(pk2.serialize())
622        );
623        assert!(SecurityCouncil::from_str(&input).is_err());
624
625        // Test invalid hex
626        let input = "2:invalid,pk2";
627        assert!(SecurityCouncil::from_str(input).is_err());
628
629        // Test missing parts
630        assert!(SecurityCouncil::from_str("2").is_err());
631        assert!(SecurityCouncil::from_str(":").is_err());
632
633        // Test too many parts
634        let input = format!(
635            "2:{},{}:extra",
636            hex::encode(pk1.serialize()),
637            hex::encode(pk2.serialize())
638        );
639        assert!(SecurityCouncil::from_str(&input).is_err());
640
641        // Test empty public keys
642        assert!(SecurityCouncil::from_str("2:").is_err());
643    }
644
645    #[test]
646    fn test_security_council_round_trip() {
647        // Create some test public keys
648        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
649        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
650
651        let original = SecurityCouncil {
652            pks: vec![pk1, pk2],
653            threshold: 2,
654        };
655
656        let string = original.to_string();
657        let parsed = SecurityCouncil::from_str(&string).unwrap();
658
659        assert_eq!(original, parsed);
660    }
661}