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.
282pub fn op_return_txout<S: AsRef<bitcoin::script::PushBytes>>(slice: S) -> TxOut {
283    let script = Builder::new()
284        .push_opcode(OP_RETURN)
285        .push_slice(slice)
286        .into_script();
287
288    TxOut {
289        value: Amount::from_sat(0),
290        script_pubkey: script,
291    }
292}
293
294/// Creates a [`TxHandler`] for the `move_to_vault_tx`.
295///
296/// 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.
297///
298/// # Arguments
299///
300/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
301/// * `paramset` - Protocol parameter set.
302///
303/// # Returns
304///
305/// A [`TxHandler`] for the move-to-vault transaction, or a [`BridgeError`] if construction fails.
306pub fn create_move_to_vault_txhandler(
307    deposit_data: &mut DepositData,
308    paramset: &'static ProtocolParamset,
309) -> Result<TxHandler<Unsigned>, BridgeError> {
310    let nofn_xonly_pk = deposit_data.get_nofn_xonly_pk()?;
311    let deposit_outpoint = deposit_data.get_deposit_outpoint();
312    let nofn_script = Arc::new(CheckSig::new(nofn_xonly_pk));
313    let security_council_script = Arc::new(Multisig::from_security_council(
314        deposit_data.security_council.clone(),
315    ));
316
317    let deposit_scripts = deposit_data.get_deposit_scripts(paramset)?;
318
319    Ok(TxHandlerBuilder::new(TransactionType::MoveToVault)
320        .with_version(NON_STANDARD_V3)
321        .add_input(
322            NormalSignatureKind::NotStored,
323            SpendableTxIn::from_scripts(
324                deposit_outpoint,
325                paramset.bridge_amount,
326                deposit_scripts,
327                None,
328                paramset.network,
329            ),
330            SpendPath::ScriptSpend(0),
331            DEFAULT_SEQUENCE,
332        )
333        .add_output(UnspentTxOut::from_scripts(
334            paramset.bridge_amount,
335            vec![nofn_script, security_council_script],
336            None,
337            paramset.network,
338        ))
339        // always use 0 sat anchor for move_tx, this will keep the amount in move to vault tx exactly the bridge amount
340        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
341            0,
342        ))))
343        .finalize())
344}
345
346/// Creates a [`TxHandler`] for the `emergency_stop_tx`.
347///
348/// This transaction moves funds to the address controlled by the security council from the move-to-vault txout.
349/// Used to stop the deposit in case of a security issue. The moved funds will eventually be redeposited using the replacement deposit tx.
350///
351/// # Arguments
352///
353/// * `deposit_data` - Mutable reference to the deposit data for the transaction.
354/// * `move_to_vault_txhandler` - Reference to the move-to-vault transaction handler.
355/// * `paramset` - Protocol parameter set.
356///
357/// # Returns
358///
359/// A [`TxHandler`] for the emergency stop transaction, or a [`BridgeError`] if construction fails.
360pub fn create_emergency_stop_txhandler(
361    deposit_data: &mut DepositData,
362    move_to_vault_txhandler: &TxHandler,
363    paramset: &'static ProtocolParamset,
364) -> Result<TxHandler<Unsigned>, BridgeError> {
365    // Hand calculated, total tx size is 11 + 126 * NUM_EMERGENCY_STOPS
366    const EACH_EMERGENCY_STOP_VBYTES: Amount = Amount::from_sat(126);
367    let security_council = deposit_data.security_council.clone();
368
369    let builder = TxHandlerBuilder::new(TransactionType::EmergencyStop)
370        .add_input(
371            NormalSignatureKind::NotStored,
372            move_to_vault_txhandler.get_spendable_output(UtxoVout::DepositInMove)?,
373            SpendPath::ScriptSpend(0),
374            DEFAULT_SEQUENCE,
375        )
376        .add_output(UnspentTxOut::from_scripts(
377            paramset.bridge_amount - paramset.anchor_amount() - EACH_EMERGENCY_STOP_VBYTES * 3,
378            vec![Arc::new(Multisig::from_security_council(security_council))],
379            None,
380            paramset.network,
381        ))
382        .finalize();
383
384    Ok(builder)
385}
386
387/// Creates a [`TxHandler`] for the `replacement_deposit_tx`.
388///
389/// This transaction replaces a previous deposit with a new deposit.
390/// In the its script, it commits the old move_to_vault txid that it replaces.
391///
392/// # Arguments
393///
394/// * `old_move_txid` - The txid of the old move_to_vault transaction that is being replaced.
395/// * `input_outpoint` - The outpoint of the input to the replacement deposit tx that holds bridge amount.
396/// * `nofn_xonly_pk` - The N-of-N XOnlyPublicKey for the deposit.
397/// * `paramset` - The protocol paramset.
398/// * `security_council` - The security council.
399///
400/// # Returns
401///
402/// A [`TxHandler`] for the replacement deposit transaction, or a [`BridgeError`] if construction fails.
403#[cfg(test)]
404pub fn create_replacement_deposit_txhandler(
405    old_move_txid: Txid,
406    input_outpoint: OutPoint,
407    old_nofn_xonly_pk: XOnlyPublicKey,
408    new_nofn_xonly_pk: XOnlyPublicKey,
409    paramset: &'static ProtocolParamset,
410    security_council: SecurityCouncil,
411) -> Result<TxHandler, BridgeError> {
412    Ok(TxHandlerBuilder::new(TransactionType::ReplacementDeposit)
413        .with_version(NON_STANDARD_V3)
414        .add_input(
415            NormalSignatureKind::NoSignature,
416            SpendableTxIn::from_scripts(
417                input_outpoint,
418                paramset.bridge_amount,
419                vec![
420                    Arc::new(CheckSig::new(old_nofn_xonly_pk)),
421                    Arc::new(Multisig::from_security_council(security_council.clone())),
422                ],
423                None,
424                paramset.network,
425            ),
426            crate::builder::script::SpendPath::ScriptSpend(0),
427            DEFAULT_SEQUENCE,
428        )
429        .add_output(UnspentTxOut::from_scripts(
430            paramset.bridge_amount,
431            vec![
432                Arc::new(ReplacementDepositScript::new(
433                    new_nofn_xonly_pk,
434                    old_move_txid,
435                )),
436                Arc::new(Multisig::from_security_council(security_council)),
437            ],
438            None,
439            paramset.network,
440        ))
441        // always use 0 sat anchor for replacement deposit tx, this will keep the amount in replacement deposit tx exactly the bridge amount
442        .add_output(UnspentTxOut::from_partial(anchor_output(Amount::from_sat(
443            0,
444        ))))
445        .finalize())
446}
447
448/// Creates a Taproot output for a disprove path, combining a script, an additional disprove script, and a hidden node containing the BitVM disprove scripts.
449///
450/// # Arguments
451///
452/// * `operator_timeout_script` - The operator timeout script.
453/// * `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.
454/// * `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.
455/// * `amount` - The output amount.
456/// * `network` - The Bitcoin network.
457///
458/// # Returns
459///
460/// An [`UnspentTxOut`] representing the Taproot TxOut.
461pub fn create_disprove_taproot_output(
462    operator_timeout_script: Arc<dyn SpendableScript>,
463    additional_script: ScriptBuf,
464    disprove_path: DisprovePath,
465    amount: Amount,
466    network: bitcoin::Network,
467) -> UnspentTxOut {
468    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
469    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
470
471    let mut scripts: Vec<ScriptBuf> = vec![additional_script.clone()];
472
473    let builder = match disprove_path.clone() {
474        DisprovePath::Scripts(extra_scripts) => {
475            let mut builder = TaprootBuilder::new();
476
477            builder = builder
478                .add_leaf(1, operator_timeout_script.to_script_buf())
479                .expect("add operator timeout script")
480                .add_leaf(2, additional_script)
481                .expect("add additional script");
482
483            // 1. Calculate depths. This is cheap and doesn't need ownership of scripts.
484            let depths = calculate_taproot_leaf_depths(extra_scripts.len());
485
486            // 2. Zip depths with an iterator over the scripts.
487            //    We clone the `script` inside the loop because the builder needs an owned value.
488            //    This is more efficient than cloning the whole Vec upfront.
489            for (depth, script) in depths.into_iter().zip(extra_scripts.iter()) {
490                let main_tree_depth = 2 + depth;
491                builder = builder
492                    .add_leaf(main_tree_depth, script.clone())
493                    .expect("add inlined disprove script");
494            }
495
496            // 3. Now, move the original `extra_scripts` into `scripts.extend`. No clone needed.
497            scripts.extend(extra_scripts);
498            builder
499        }
500        DisprovePath::HiddenNode(root_hash) => TaprootBuilder::new()
501            .add_leaf(1, operator_timeout_script.to_script_buf())
502            .expect("empty taptree will accept a script node")
503            .add_leaf(2, additional_script)
504            .expect("taptree with one node will accept a node at depth 2")
505            .add_hidden_node(2, TapNodeHash::from_byte_array(*root_hash))
506            .expect("taptree with two nodes will accept a node at depth 2"),
507    };
508
509    let taproot_spend_info = builder
510        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
511        .expect("valid taptree");
512
513    let address = Address::p2tr(
514        &SECP,
515        *UNSPENDABLE_XONLY_PUBKEY,
516        taproot_spend_info.merkle_root(),
517        network,
518    );
519
520    let mut spendable_scripts: Vec<Arc<dyn SpendableScript>> = vec![operator_timeout_script];
521    let other_spendable_scripts: Vec<Arc<dyn SpendableScript>> = scripts
522        .into_iter()
523        .map(|script| Arc::new(OtherSpendable::new(script)) as Arc<dyn SpendableScript>)
524        .collect();
525
526    spendable_scripts.extend(other_spendable_scripts);
527
528    UnspentTxOut::new(
529        TxOut {
530            value: amount,
531            script_pubkey: address.script_pubkey(),
532        },
533        spendable_scripts,
534        Some(taproot_spend_info),
535    )
536}
537
538/// Helper function to create a Taproot output that combines a single script and a root hash containing any number of scripts.
539/// 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
540/// 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
541/// that depends on the specific operator or nofn_pk that is signing the deposit.
542///
543/// # Arguments
544///
545/// * `script` - The one additional script to include in the merkle tree.
546/// * `hidden_node` - The root hash for the merkle tree node. The node can contain any number of scripts.
547/// * `amount` - The output amount.
548/// * `network` - The Bitcoin network.
549///
550/// # Returns
551///
552/// An [`UnspentTxOut`] representing the Taproot TxOut.
553pub fn create_taproot_output_with_hidden_node(
554    script: Arc<dyn SpendableScript>,
555    hidden_node: HiddenNode,
556    amount: Amount,
557    network: bitcoin::Network,
558) -> UnspentTxOut {
559    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
560    use bitcoin::taproot::{TapNodeHash, TaprootBuilder};
561
562    let builder = TaprootBuilder::new()
563        .add_leaf(1, script.to_script_buf())
564        .expect("empty taptree will accept a script node")
565        .add_hidden_node(1, TapNodeHash::from_byte_array(*hidden_node))
566        .expect("taptree with one node will accept a node at depth 1");
567
568    let taproot_spend_info = builder
569        .finalize(&SECP, *UNSPENDABLE_XONLY_PUBKEY)
570        .expect("cannot fail since it is a valid taptree");
571
572    let address = Address::p2tr(
573        &SECP,
574        *UNSPENDABLE_XONLY_PUBKEY,
575        taproot_spend_info.merkle_root(),
576        network,
577    );
578
579    UnspentTxOut::new(
580        TxOut {
581            value: amount,
582            script_pubkey: address.script_pubkey(),
583        },
584        vec![script.clone()],
585        Some(taproot_spend_info),
586    )
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use bitcoin::secp256k1::XOnlyPublicKey;
593    use std::str::FromStr;
594
595    #[test]
596    fn test_security_council_from_str() {
597        // Create some test public keys
598        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
599        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
600
601        // Test valid input
602        let input = format!(
603            "2:{},{}",
604            hex::encode(pk1.serialize()),
605            hex::encode(pk2.serialize())
606        );
607        let council = SecurityCouncil::from_str(&input).unwrap();
608        assert_eq!(council.threshold, 2);
609        assert_eq!(council.pks.len(), 2);
610        assert_eq!(council.pks[0], pk1);
611        assert_eq!(council.pks[1], pk2);
612
613        // Test invalid threshold
614        let input = format!(
615            "3:{},{}",
616            hex::encode(pk1.serialize()),
617            hex::encode(pk2.serialize())
618        );
619        assert!(SecurityCouncil::from_str(&input).is_err());
620
621        // Test invalid hex
622        let input = "2:invalid,pk2";
623        assert!(SecurityCouncil::from_str(input).is_err());
624
625        // Test missing parts
626        assert!(SecurityCouncil::from_str("2").is_err());
627        assert!(SecurityCouncil::from_str(":").is_err());
628
629        // Test too many parts
630        let input = format!(
631            "2:{},{}:extra",
632            hex::encode(pk1.serialize()),
633            hex::encode(pk2.serialize())
634        );
635        assert!(SecurityCouncil::from_str(&input).is_err());
636
637        // Test empty public keys
638        assert!(SecurityCouncil::from_str("2:").is_err());
639    }
640
641    #[test]
642    fn test_security_council_round_trip() {
643        // Create some test public keys
644        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
645        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
646
647        let original = SecurityCouncil {
648            pks: vec![pk1, pk2],
649            threshold: 2,
650        };
651
652        let string = original.to_string();
653        let parsed = SecurityCouncil::from_str(&string).unwrap();
654
655        assert_eq!(original, parsed);
656    }
657}