clementine_core/builder/transaction/
creator.rs

1//! # Transaction Handler Creation Logic
2//!
3//! This module provides the logic for constructing, caching, and managing transaction handlers (`TxHandler`) for all transaction types in the Clementine bridge.
4//!
5//! It is responsible for orchestrating the creation of all transaction flows for a given operator, round, and deposit, including collateral, kickoff, challenge, reimbursement, and assertion transactions. It also manages context and database-backed caching to support efficient and correct transaction construction.
6//!
7//! ## Key Types
8//!
9//! - [`KickoffWinternitzKeys`] - Helper for managing Winternitz keys for kickoff transactions, to retrieve the correct keys for a given round.
10//! - [`ReimburseDbCache`] - Retrieves and caches relevant data from the database for transaction handler creation.
11//! - [`ContractContext`] - Holds context for a specific operator, round, and optionally deposit, in short all the information needed to create the relevant transactions.
12//! - [`TxHandlerCache`] - Stores and manages cached transaction handlers for efficient flow construction. This is important during the deposit, as the functions create all transactions for a single operator, kickoff utxo, and deposit tuple, which has common transactions between them. (Mainly round tx and move to vault tx)
13//!
14//! ## Main Functions
15//!
16//! - [`create_txhandlers`] - Orchestrates the creation of all required transaction handlers for a given context and transaction type.
17//! - [`create_round_txhandlers`] - Creates round and ready-to-reimburse transaction handlers for a specific operator and round.
18//!
19
20use super::input::UtxoVout;
21use super::operator_assert::{
22    create_latest_blockhash_timeout_txhandler, create_latest_blockhash_txhandler,
23};
24use super::{remove_txhandler_from_map, RoundTxInput};
25use crate::actor::Actor;
26use crate::bitvm_client::ClementineBitVMPublicKeys;
27use crate::builder;
28use crate::builder::script::{SpendableScript, TimelockScript, WinternitzCommit};
29use crate::builder::transaction::operator_reimburse::DisprovePath;
30use crate::builder::transaction::{
31    create_assert_timeout_txhandlers, create_challenge_timeout_txhandler, create_kickoff_txhandler,
32    create_mini_asserts, create_round_txhandler, create_unspent_kickoff_txhandlers, AssertScripts,
33    TransactionType, TxHandler,
34};
35use crate::config::protocol::ProtocolParamset;
36use crate::database::{Database, DatabaseTransaction};
37use crate::deposit::{DepositData, KickoffData, OperatorData};
38use crate::errors::{BridgeError, TxError};
39use crate::operator::{PublicHash, RoundIndex};
40use bitcoin::hashes::Hash;
41use bitcoin::key::Secp256k1;
42use bitcoin::taproot::TaprootBuilder;
43use bitcoin::{OutPoint, XOnlyPublicKey};
44use bitvm::clementine::additional_disprove::{
45    create_additional_replacable_disprove_script_with_dummy, replace_placeholders_in_script,
46};
47use circuits_lib::bridge_circuit::deposit_constant;
48use eyre::Context;
49use eyre::OptionExt;
50use std::collections::BTreeMap;
51use std::sync::Arc;
52
53// helper function to get a txhandler from a hashmap
54fn get_txhandler(
55    txhandlers: &BTreeMap<TransactionType, TxHandler>,
56    tx_type: TransactionType,
57) -> Result<&TxHandler, TxError> {
58    txhandlers
59        .get(&tx_type)
60        .ok_or(TxError::TxHandlerNotFound(tx_type))
61}
62
63/// Helper struct to get specific kickoff winternitz keys for a sequential collateral tx
64#[derive(Debug, Clone)]
65pub struct KickoffWinternitzKeys {
66    keys: Vec<bitvm::signatures::winternitz::PublicKey>,
67    num_kickoffs_per_round: usize,
68    num_rounds: usize,
69}
70
71impl KickoffWinternitzKeys {
72    /// Creates a new [`KickoffWinternitzKeys`] with the given keys and number per round.
73    pub fn new(
74        keys: Vec<bitvm::signatures::winternitz::PublicKey>,
75        num_kickoffs_per_round: usize,
76        num_rounds: usize,
77    ) -> Result<Self, TxError> {
78        if keys.len() != num_kickoffs_per_round * (num_rounds + 1) {
79            return Err(TxError::KickoffWinternitzKeysDBInconsistency);
80        }
81        Ok(Self {
82            keys,
83            num_kickoffs_per_round,
84            num_rounds,
85        })
86    }
87
88    /// Get the winternitz keys for a specific round tx.
89    ///
90    /// # Arguments
91    /// * `round_idx` - The index of the round.
92    ///
93    /// # Returns
94    /// A slice of Winternitz public keys for the given round.
95    pub fn get_keys_for_round(
96        &self,
97        round_idx: RoundIndex,
98    ) -> Result<&[bitvm::signatures::winternitz::PublicKey], TxError> {
99        // Additionally there are no keys after num_rounds + 1, +1 is because we need additional round to generate
100        // reimbursement connectors of previous round
101        if round_idx == RoundIndex::Collateral || round_idx.to_index() > self.num_rounds + 1 {
102            return Err(TxError::InvalidRoundIndex(round_idx));
103        }
104        let start_idx = (round_idx.to_index())
105            // 0th round is the collateral, there are no keys for the 0th round so we subtract 1
106            .checked_sub(1)
107            .ok_or(TxError::IndexOverflow)?
108            .checked_mul(self.num_kickoffs_per_round)
109            .ok_or(TxError::IndexOverflow)?;
110        let end_idx = start_idx
111            .checked_add(self.num_kickoffs_per_round)
112            .ok_or(TxError::IndexOverflow)?;
113        Ok(&self.keys[start_idx..end_idx])
114    }
115
116    /// Returns keys Vec and consumes self
117    pub fn get_all_keys(self) -> Vec<bitvm::signatures::winternitz::PublicKey> {
118        self.keys
119    }
120}
121
122/// Struct to retrieve and cache data from DB for creating TxHandlers on demand
123/// It can only store information for one deposit and operator pair.
124/// It has two context modes, for rounds or for deposits. Deposit context needs additional information, like the deposit outpoint, which is not needed for rounds.
125/// Round context can only create transactions that do not depend on the deposit, like the round tx and ready to reimburse tx.
126/// Deposit context can create all transactions.
127/// Note: This cache is specific to a single operator, for each operator a new cache is needed.
128#[derive(Debug)]
129pub struct ReimburseDbCache<'a, 'b> {
130    pub db: Database,
131    pub operator_xonly_pk: XOnlyPublicKey,
132    pub deposit_outpoint: Option<bitcoin::OutPoint>,
133    pub paramset: &'static ProtocolParamset,
134    /// Optional database transaction to use for the cache.
135    dbtx: Option<DatabaseTransaction<'a, 'b>>,
136    /// winternitz keys to sign the kickoff tx with the blockhash
137    kickoff_winternitz_keys: Option<KickoffWinternitzKeys>,
138    /// bitvm assert scripts for each assert utxo
139    bitvm_assert_addr: Option<Vec<[u8; 32]>>,
140    /// bitvm disprove scripts taproot merkle tree root hash
141    bitvm_disprove_root_hash: Option<[u8; 32]>,
142    /// Public hashes to acknowledge watchtower challenges
143    challenge_ack_hashes: Option<Vec<PublicHash>>,
144    /// operator data
145    operator_data: Option<OperatorData>,
146    /// latest blockhash root hash
147    latest_blockhash_root_hash: Option<[u8; 32]>,
148    /// replaceable additional disprove script
149    replaceable_additional_disprove_script: Option<Vec<u8>>,
150    /// Operator bitvm keys in kickoff tx for asserts
151    operator_bitvm_keys: Option<ClementineBitVMPublicKeys>,
152}
153
154impl<'a, 'b> ReimburseDbCache<'a, 'b> {
155    /// Creates a db cache that can be used to create txhandlers for a specific operator and deposit/kickoff
156    pub fn new_for_deposit(
157        db: Database,
158        operator_xonly_pk: XOnlyPublicKey,
159        deposit_outpoint: bitcoin::OutPoint,
160        paramset: &'static ProtocolParamset,
161        dbtx: Option<DatabaseTransaction<'a, 'b>>,
162    ) -> Self {
163        Self {
164            db,
165            operator_xonly_pk,
166            deposit_outpoint: Some(deposit_outpoint),
167            paramset,
168            dbtx,
169            kickoff_winternitz_keys: None,
170            bitvm_assert_addr: None,
171            bitvm_disprove_root_hash: None,
172            challenge_ack_hashes: None,
173            operator_data: None,
174            latest_blockhash_root_hash: None,
175            replaceable_additional_disprove_script: None,
176            operator_bitvm_keys: None,
177        }
178    }
179
180    /// Creates a db cache that can be used to create txhandlers for a specific operator and collateral chain
181    pub fn new_for_rounds(
182        db: Database,
183        operator_xonly_pk: XOnlyPublicKey,
184        paramset: &'static ProtocolParamset,
185        dbtx: Option<DatabaseTransaction<'a, 'b>>,
186    ) -> Self {
187        Self {
188            db,
189            operator_xonly_pk,
190            deposit_outpoint: None,
191            paramset,
192            dbtx,
193            kickoff_winternitz_keys: None,
194            bitvm_assert_addr: None,
195            bitvm_disprove_root_hash: None,
196            challenge_ack_hashes: None,
197            operator_data: None,
198            latest_blockhash_root_hash: None,
199            replaceable_additional_disprove_script: None,
200            operator_bitvm_keys: None,
201        }
202    }
203
204    /// Creates a db cache from a contract context. This context can possible include a deposit data, for which it will be equivalent to new_for_deposit, otherwise it will be equivalent to new_for_rounds.
205    pub fn from_context(
206        db: Database,
207        context: &ContractContext,
208        dbtx: Option<DatabaseTransaction<'a, 'b>>,
209    ) -> Self {
210        if context.deposit_data.is_some() {
211            let deposit_data = context
212                .deposit_data
213                .as_ref()
214                .expect("checked in if statement");
215            Self::new_for_deposit(
216                db,
217                context.operator_xonly_pk,
218                deposit_data.get_deposit_outpoint(),
219                context.paramset,
220                dbtx,
221            )
222        } else {
223            Self::new_for_rounds(db, context.operator_xonly_pk, context.paramset, dbtx)
224        }
225    }
226
227    pub async fn get_operator_data(&mut self) -> Result<&OperatorData, BridgeError> {
228        match self.operator_data {
229            Some(ref data) => Ok(data),
230            None => {
231                self.operator_data = Some(
232                    self.db
233                        .get_operator(self.dbtx.as_deref_mut(), self.operator_xonly_pk)
234                        .await
235                        .wrap_err("Failed to get operator data from database")?
236                        .ok_or_eyre(format!(
237                            "Operator not found for xonly_pk {}",
238                            self.operator_xonly_pk
239                        ))?,
240                );
241                Ok(self.operator_data.as_ref().expect("Inserted before"))
242            }
243        }
244    }
245
246    async fn get_bitvm_setup(&mut self, deposit_outpoint: OutPoint) -> Result<(), BridgeError> {
247        let (assert_addr, bitvm_hash, latest_blockhash_root_hash) = self
248            .db
249            .get_bitvm_setup(
250                self.dbtx.as_deref_mut(),
251                self.operator_xonly_pk,
252                deposit_outpoint,
253            )
254            .await
255            .wrap_err("Failed to get bitvm setup in ReimburseDbCache::get_bitvm_setup")?
256            .ok_or(TxError::BitvmSetupNotFound(
257                self.operator_xonly_pk,
258                deposit_outpoint.txid,
259            ))?;
260        self.bitvm_assert_addr = Some(assert_addr);
261        self.bitvm_disprove_root_hash = Some(bitvm_hash);
262        self.latest_blockhash_root_hash = Some(latest_blockhash_root_hash);
263        Ok(())
264    }
265
266    pub async fn get_kickoff_winternitz_keys(
267        &mut self,
268    ) -> Result<&KickoffWinternitzKeys, BridgeError> {
269        match self.kickoff_winternitz_keys {
270            Some(ref keys) => Ok(keys),
271            None => {
272                self.kickoff_winternitz_keys = Some(KickoffWinternitzKeys::new(
273                    self.db
274                        .get_operator_kickoff_winternitz_public_keys(
275                            self.dbtx.as_deref_mut(),
276                            self.operator_xonly_pk,
277                        )
278                        .await
279                        .wrap_err("Failed to get kickoff winternitz keys from database")?,
280                    self.paramset.num_kickoffs_per_round,
281                    self.paramset.num_round_txs,
282                )?);
283                Ok(self
284                    .kickoff_winternitz_keys
285                    .as_ref()
286                    .expect("Inserted before"))
287            }
288        }
289    }
290
291    pub async fn get_bitvm_assert_hash(&mut self) -> Result<&[[u8; 32]], BridgeError> {
292        if let Some(deposit_outpoint) = &self.deposit_outpoint {
293            match self.bitvm_assert_addr {
294                Some(ref addr) => Ok(addr),
295                None => {
296                    self.get_bitvm_setup(*deposit_outpoint).await?;
297                    Ok(self.bitvm_assert_addr.as_ref().expect("Inserted before"))
298                }
299            }
300        } else {
301            Err(TxError::InsufficientContext.into())
302        }
303    }
304
305    pub async fn get_operator_bitvm_keys(
306        &mut self,
307    ) -> Result<&ClementineBitVMPublicKeys, BridgeError> {
308        if let Some(deposit_outpoint) = &self.deposit_outpoint {
309            if let Some(ref keys) = self.operator_bitvm_keys {
310                return Ok(keys);
311            }
312            self.operator_bitvm_keys = Some(ClementineBitVMPublicKeys::from_flattened_vec(
313                &self
314                    .db
315                    .get_operator_bitvm_keys(
316                        self.dbtx.as_deref_mut(),
317                        self.operator_xonly_pk,
318                        *deposit_outpoint,
319                    )
320                    .await?,
321            ));
322            Ok(self.operator_bitvm_keys.as_ref().expect("Inserted before"))
323        } else {
324            Err(TxError::InsufficientContext.into())
325        }
326    }
327
328    pub async fn get_replaceable_additional_disprove_script(
329        &mut self,
330    ) -> Result<&Vec<u8>, BridgeError> {
331        if let Some(ref script) = self.replaceable_additional_disprove_script {
332            return Ok(script);
333        }
334
335        let bridge_circuit_constant = *self.paramset.bridge_circuit_constant()?;
336        let challenge_ack_hashes = self.get_challenge_ack_hashes().await?.to_vec();
337
338        let bitvm_keys = self.get_operator_bitvm_keys().await?;
339
340        let script = create_additional_replacable_disprove_script_with_dummy(
341            bridge_circuit_constant,
342            bitvm_keys.bitvm_pks.0[0].to_vec(),
343            bitvm_keys.latest_blockhash_pk.to_vec(),
344            bitvm_keys.challenge_sending_watchtowers_pk.to_vec(),
345            challenge_ack_hashes,
346        );
347
348        self.replaceable_additional_disprove_script = Some(script);
349        Ok(self
350            .replaceable_additional_disprove_script
351            .as_ref()
352            .expect("Cached above"))
353    }
354
355    pub async fn get_challenge_ack_hashes(&mut self) -> Result<&[PublicHash], BridgeError> {
356        if let Some(deposit_outpoint) = &self.deposit_outpoint {
357            match self.challenge_ack_hashes {
358                Some(ref hashes) => Ok(hashes),
359                None => {
360                    self.challenge_ack_hashes = Some(
361                        self.db
362                            .get_operators_challenge_ack_hashes(
363                                self.dbtx.as_deref_mut(),
364                                self.operator_xonly_pk,
365                                *deposit_outpoint,
366                            )
367                            .await
368                            .wrap_err("Failed to get challenge ack hashes from database in ReimburseDbCache")?
369                            .ok_or(eyre::eyre!(
370                                "Watchtower public hashes not found for operator {0:?} and deposit {1}",
371                                self.operator_xonly_pk,
372                                deposit_outpoint.txid,
373                            ))?,
374                    );
375                    Ok(self.challenge_ack_hashes.as_ref().expect("Inserted before"))
376                }
377            }
378        } else {
379            Err(TxError::InsufficientContext.into())
380        }
381    }
382
383    pub async fn get_bitvm_disprove_root_hash(&mut self) -> Result<&[u8; 32], BridgeError> {
384        if let Some(deposit_outpoint) = &self.deposit_outpoint {
385            match self.bitvm_disprove_root_hash {
386                Some(ref hash) => Ok(hash),
387                None => {
388                    self.get_bitvm_setup(*deposit_outpoint).await?;
389                    Ok(self
390                        .bitvm_disprove_root_hash
391                        .as_ref()
392                        .expect("Inserted before"))
393                }
394            }
395        } else {
396            Err(TxError::InsufficientContext.into())
397        }
398    }
399
400    pub async fn get_latest_blockhash_root_hash(&mut self) -> Result<&[u8; 32], BridgeError> {
401        if let Some(deposit_outpoint) = &self.deposit_outpoint {
402            match self.latest_blockhash_root_hash {
403                Some(ref hash) => Ok(hash),
404                None => {
405                    self.get_bitvm_setup(*deposit_outpoint).await?;
406                    Ok(self
407                        .latest_blockhash_root_hash
408                        .as_ref()
409                        .expect("Inserted before"))
410                }
411            }
412        } else {
413            Err(TxError::InsufficientContext.into())
414        }
415    }
416}
417
418/// Context for a single operator and round, and optionally a single deposit.
419/// Data about deposit and kickoff idx is needed to create the deposit-related transactions.
420/// For non deposit related transactions, like the round tx and ready to reimburse tx, the round idx is enough.
421#[derive(Debug, Clone)]
422pub struct ContractContext {
423    /// required
424    pub operator_xonly_pk: XOnlyPublicKey,
425    pub round_idx: RoundIndex,
426    pub paramset: &'static ProtocolParamset,
427    /// optional (only used for after kickoff)
428    pub kickoff_idx: Option<u32>,
429    pub deposit_data: Option<DepositData>,
430    signer: Option<Actor>,
431}
432
433impl ContractContext {
434    /// Contains all necessary context for creating txhandlers for a specific operator and collateral chain
435    pub fn new_context_for_round(
436        operator_xonly_pk: XOnlyPublicKey,
437        round_idx: RoundIndex,
438        paramset: &'static ProtocolParamset,
439    ) -> Self {
440        Self {
441            operator_xonly_pk,
442            round_idx,
443            paramset,
444            kickoff_idx: None,
445            deposit_data: None,
446            signer: None,
447        }
448    }
449
450    /// Contains all necessary context for creating txhandlers for a specific operator, kickoff utxo, and a deposit
451    pub fn new_context_for_kickoff(
452        kickoff_data: KickoffData,
453        deposit_data: DepositData,
454        paramset: &'static ProtocolParamset,
455    ) -> Self {
456        Self {
457            operator_xonly_pk: kickoff_data.operator_xonly_pk,
458            round_idx: kickoff_data.round_idx,
459            paramset,
460            kickoff_idx: Some(kickoff_data.kickoff_idx),
461            deposit_data: Some(deposit_data),
462            signer: None,
463        }
464    }
465
466    /// Contains all necessary context for creating txhandlers for a specific operator, kickoff utxo, and a deposit
467    /// Additionally holds signer of an actor that can generate the actual winternitz public keys for operator,
468    /// and append evm address to the challenge tx for verifier.
469    pub fn new_context_with_signer(
470        kickoff_data: KickoffData,
471        deposit_data: DepositData,
472        paramset: &'static ProtocolParamset,
473        signer: Actor,
474    ) -> Self {
475        Self {
476            operator_xonly_pk: kickoff_data.operator_xonly_pk,
477            round_idx: kickoff_data.round_idx,
478            paramset,
479            kickoff_idx: Some(kickoff_data.kickoff_idx),
480            deposit_data: Some(deposit_data),
481            signer: Some(signer),
482        }
483    }
484
485    /// Returns if the context is for a kickoff
486    pub fn is_context_for_kickoff(&self) -> bool {
487        self.deposit_data.is_some() && self.kickoff_idx.is_some()
488    }
489}
490
491/// Stores and manages cached transaction handlers for efficient flow construction.
492///
493/// This cache is used to avoid redundant construction of common transactions (such as round and move-to-vault transactions)
494/// when creating all transactions for a single operator, kickoff utxo, and deposit tuple. It is especially important during deposit flows,
495/// where many transactions share common intermediates. The cache tracks the previous ready-to-reimburse transaction and a map of saved
496/// transaction handlers by type.
497/// Note: Why is prev_ready_to_reimburse needed and not just stored in saved_txs? Because saved_txs can include the ReadyToReimburse txhandler for the current round, prev_ready_to_reimburse is specifically from the previous round.
498///
499/// # Fields
500///
501/// - `prev_ready_to_reimburse`: Optionally stores the previous round's ready-to-reimburse transaction handler.
502/// - `saved_txs`: A map from [`TransactionType`] to [`TxHandler`], storing cached transaction handlers for the current context.
503///
504/// # Usage
505///
506/// - Use `store_for_next_kickoff` to cache the current round's main transactions before moving to the next kickoff within the same round.
507/// - Use `store_for_next_round` to update the cache when moving to the next round, preserving the necessary state.
508/// - Use `get_cached_txs` to retrieve and clear the current cache when constructing new transactions.
509/// - Use `get_prev_ready_to_reimburse` to access the previous round's ready-to-reimburse transaction to create the next round's round tx.
510pub struct TxHandlerCache {
511    pub prev_ready_to_reimburse: Option<TxHandler>,
512    pub saved_txs: BTreeMap<TransactionType, TxHandler>,
513}
514
515impl Default for TxHandlerCache {
516    fn default() -> Self {
517        Self::new()
518    }
519}
520
521impl TxHandlerCache {
522    /// Creates a new, empty cache.
523    pub fn new() -> Self {
524        Self {
525            saved_txs: BTreeMap::new(),
526            prev_ready_to_reimburse: None,
527        }
528    }
529    /// Stores txhandlers for the next kickoff, caching MoveToVault, Round, and ReadyToReimburse.
530    ///
531    /// Removes these transaction types from the provided map and stores them in the cache.
532    /// This is used to preserve the state between kickoffs within the same round.
533    pub fn store_for_next_kickoff(
534        &mut self,
535        txhandlers: &mut BTreeMap<TransactionType, TxHandler>,
536    ) -> Result<(), BridgeError> {
537        // can possibly cache next round tx too, as next round has the needed reimburse utxos
538        // but need to implement a new TransactionType for that
539        for tx_type in [
540            TransactionType::MoveToVault,
541            TransactionType::Round,
542            TransactionType::ReadyToReimburse,
543        ]
544        .iter()
545        {
546            let txhandler = txhandlers
547                .remove(tx_type)
548                .ok_or(TxError::TxHandlerNotFound(*tx_type))?;
549            self.saved_txs.insert(*tx_type, txhandler);
550        }
551        Ok(())
552    }
553    /// Stores MoveToVault and previous ReadyToReimburse for the next round.
554    ///
555    /// Moves the MoveToVault and ReadyToReimburse txhandlers from the cache to their respective fields,
556    /// clearing the rest of the cache. This is used to preserve the state between rounds.
557    pub fn store_for_next_round(&mut self) -> Result<(), BridgeError> {
558        let move_to_vault =
559            remove_txhandler_from_map(&mut self.saved_txs, TransactionType::MoveToVault)?;
560        self.prev_ready_to_reimburse = Some(remove_txhandler_from_map(
561            &mut self.saved_txs,
562            TransactionType::ReadyToReimburse,
563        )?);
564        self.saved_txs = BTreeMap::new();
565        self.saved_txs
566            .insert(move_to_vault.get_transaction_type(), move_to_vault);
567        Ok(())
568    }
569    /// Gets the previous ReadyToReimburse txhandler, if any.
570    ///
571    /// This is used to chain rounds together, as the output of the previous ready-to-reimburse transaction
572    /// is needed as input for the next round's round transaction. Without caching, we would have to create the full collateral chain again.
573    pub fn get_prev_ready_to_reimburse(&self) -> Option<&TxHandler> {
574        self.prev_ready_to_reimburse.as_ref()
575    }
576    /// Takes and returns all cached txhandlers, clearing the cache.
577    pub fn get_cached_txs(&mut self) -> BTreeMap<TransactionType, TxHandler> {
578        std::mem::take(&mut self.saved_txs)
579    }
580}
581
582/// Creates all required transaction handlers for a given context and transaction type.
583///
584/// This function builds and caches all necessary transaction handlers for the specified transaction type, operator, round, and deposit context.
585/// It handles the full flow of collateral, kickoff, challenge, reimbursement, and assertion transactions, including round management and challenge handling.
586/// Function returns early if the needed txhandler is already created.
587/// Currently there are 3 kinds of specific transaction types that can be given as parameter that change the logic flow
588/// - AllNeededForDeposit: Creates all transactions, including the round tx's and deposit related tx's.
589/// - Round related tx's (Round, ReadyToReimburse, UnspentKickoff): Creates only round related tx's and returns early.
590/// - MiniAssert and LatestBlockhash: These tx's are created to commit data in their witness using winternitz signatures. To enable signing these transactions, the kickoff transaction (where the input of MiniAssert and LatestBlockhash resides) needs to be created with the full list of scripts in its TxHandler data. This may take some time especially for a deposit where thousands of kickoff tx's are created. That's why if MiniAssert or LatestBlockhash is not requested, these scripts are not created and just the merkle root hash of these scripts is used to create the kickoff tx. But if these tx's are requested, the full list of scripts is needed to create the kickoff tx, to enable signing these transactions with winternitz signatures.
591///
592/// # Arguments
593///
594/// * `transaction_type` - The type of transaction(s) to create.
595/// * `context` - The contract context (operator, round, deposit, etc).
596/// * `txhandler_cache` - Cache for storing/retrieving intermediate txhandlers.
597/// * `db_cache` - Database-backed cache for retrieving protocol data.
598///
599/// # Returns
600///
601/// A map of [`TransactionType`] to [`TxHandler`] for all constructed transactions, or a [`BridgeError`] if construction fails.
602pub async fn create_txhandlers(
603    transaction_type: TransactionType,
604    context: ContractContext,
605    txhandler_cache: &mut TxHandlerCache,
606    db_cache: &mut ReimburseDbCache<'_, '_>,
607) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
608    let paramset = db_cache.paramset;
609
610    let operator_data = db_cache.get_operator_data().await?.clone();
611    let kickoff_winternitz_keys = db_cache.get_kickoff_winternitz_keys().await?.clone();
612
613    let ContractContext {
614        operator_xonly_pk,
615        round_idx,
616        ..
617    } = context;
618
619    if context.operator_xonly_pk != operator_data.xonly_pk {
620        return Err(eyre::eyre!(
621            "Operator xonly pk mismatch between ContractContext and ReimburseDbCache: {:?} != {:?}",
622            context.operator_xonly_pk,
623            operator_data.xonly_pk
624        )
625        .into());
626    }
627
628    let mut txhandlers = txhandler_cache.get_cached_txs();
629    if !txhandlers.contains_key(&TransactionType::Round) {
630        // create round tx, ready to reimburse tx, and unspent kickoff txs if not in cache
631        let round_txhandlers = create_round_txhandlers(
632            paramset,
633            round_idx,
634            &operator_data,
635            &kickoff_winternitz_keys,
636            txhandler_cache.get_prev_ready_to_reimburse(),
637        )?;
638        for round_txhandler in round_txhandlers.into_iter() {
639            txhandlers.insert(round_txhandler.get_transaction_type(), round_txhandler);
640        }
641    }
642
643    if matches!(
644        transaction_type,
645        TransactionType::Round
646            | TransactionType::ReadyToReimburse
647            | TransactionType::UnspentKickoff(_)
648    ) {
649        // return if only one of the collateral tx's were requested
650        // do not continue as we might not have the necessary context for the remaining tx's
651        return Ok(txhandlers);
652    }
653
654    // get the next round txhandler (because reimburse connectors will be in it)
655    let next_round_txhandler = create_round_txhandler(
656        operator_data.xonly_pk,
657        RoundTxInput::Prevout(Box::new(
658            get_txhandler(&txhandlers, TransactionType::ReadyToReimburse)?
659                .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)?,
660        )),
661        kickoff_winternitz_keys.get_keys_for_round(round_idx.next_round())?,
662        paramset,
663    )?;
664
665    let mut deposit_data = context.deposit_data.ok_or(TxError::InsufficientContext)?;
666    let kickoff_data = KickoffData {
667        operator_xonly_pk,
668        round_idx,
669        kickoff_idx: context.kickoff_idx.ok_or(TxError::InsufficientContext)?,
670    };
671
672    if let Some(deposit_outpoint) = db_cache.deposit_outpoint {
673        if deposit_outpoint != deposit_data.get_deposit_outpoint() {
674            return Err(eyre::eyre!(
675                "Deposit outpoint mismatch between ReimburseDbCache and ContractContext: {:?} != {:?}",
676                deposit_outpoint,
677                deposit_data.get_deposit_outpoint()
678            )
679            .into());
680        }
681    } else {
682        return Err(eyre::eyre!(
683            "Deposit outpoint is not set in ReimburseDbCache, but is set in ContractContext: {:?}",
684            deposit_data.get_deposit_outpoint()
685        )
686        .into());
687    }
688
689    if !txhandlers.contains_key(&TransactionType::MoveToVault) {
690        // if not cached create move_txhandler
691        let move_txhandler =
692            builder::transaction::create_move_to_vault_txhandler(&mut deposit_data, paramset)?;
693        txhandlers.insert(move_txhandler.get_transaction_type(), move_txhandler);
694    }
695
696    let challenge_ack_public_hashes = db_cache.get_challenge_ack_hashes().await?.to_vec();
697
698    if challenge_ack_public_hashes.len() != deposit_data.get_num_watchtowers() {
699        return Err(eyre::eyre!(
700            "Expected {} number of challenge ack public hashes, but got {} from db for deposit {:?}",
701            deposit_data.get_num_watchtowers(),
702            challenge_ack_public_hashes.len(),
703            deposit_data
704        )
705        .into());
706    }
707
708    let num_asserts = ClementineBitVMPublicKeys::number_of_assert_txs();
709
710    let move_txid = txhandlers
711        .get(&TransactionType::MoveToVault)
712        .ok_or(TxError::TxHandlerNotFound(TransactionType::MoveToVault))?
713        .get_txid()
714        .to_byte_array();
715
716    let round_txid = txhandlers
717        .get(&TransactionType::Round)
718        .ok_or(TxError::TxHandlerNotFound(TransactionType::Round))?
719        .get_txid()
720        .to_byte_array();
721
722    let vout = UtxoVout::Kickoff(kickoff_data.kickoff_idx as usize).get_vout();
723    let watchtower_challenge_start_idx = UtxoVout::WatchtowerChallenge(0).get_vout();
724    let secp = Secp256k1::verification_only();
725
726    let nofn_key: XOnlyPublicKey = deposit_data.get_nofn_xonly_pk()?;
727
728    let watchtower_xonly_pk = deposit_data.get_watchtowers();
729    let watchtower_pubkeys = watchtower_xonly_pk
730        .iter()
731        .map(|xonly_pk| {
732            let nofn_2week = Arc::new(TimelockScript::new(
733                Some(nofn_key),
734                paramset.watchtower_challenge_timeout_timelock,
735            ));
736
737            let builder = TaprootBuilder::new();
738            let tweaked = builder
739                .add_leaf(0, nofn_2week.to_script_buf())
740                .expect("Valid script leaf")
741                .finalize(&secp, *xonly_pk)
742                .expect("taproot finalize must succeed");
743
744            tweaked.output_key().serialize()
745        })
746        .collect::<Vec<_>>();
747
748    let deposit_constant = deposit_constant(
749        operator_xonly_pk.serialize(),
750        watchtower_challenge_start_idx,
751        &watchtower_pubkeys,
752        move_txid,
753        round_txid,
754        vout,
755        context.paramset.genesis_chain_state_hash,
756    );
757
758    tracing::debug!(
759        target: "ci",
760        "Create txhandlers - Genesis height: {:?}, operator_xonly_pk: {:?}, move_txid: {:?}, round_txid: {:?}, vout: {:?}, watchtower_challenge_start_idx: {:?}, genesis_chain_state_hash: {:?}, deposit_constant: {:?}",
761        context.paramset.genesis_height,
762        operator_xonly_pk,
763        move_txid,
764        round_txid,
765        vout,
766        watchtower_challenge_start_idx,
767        context.paramset.genesis_chain_state_hash,
768        deposit_constant.0,
769    );
770
771    tracing::debug!(
772        "Deposit constant for {:?}: {:?} - deposit outpoint: {:?}",
773        operator_xonly_pk,
774        deposit_constant.0,
775        deposit_data.get_deposit_outpoint(),
776    );
777
778    let payout_tx_blockhash_pk = kickoff_winternitz_keys
779        .get_keys_for_round(round_idx)?
780        .get(kickoff_data.kickoff_idx as usize)
781        .ok_or(TxError::IndexOverflow)?
782        .clone();
783
784    tracing::debug!(
785        target: "ci",
786        "Payout tx blockhash pk: {:?}",
787        payout_tx_blockhash_pk
788    );
789
790    let additional_disprove_script = db_cache
791        .get_replaceable_additional_disprove_script()
792        .await?
793        .clone();
794
795    let additional_disprove_script = replace_placeholders_in_script(
796        additional_disprove_script,
797        payout_tx_blockhash_pk,
798        deposit_constant.0,
799    );
800    let disprove_root_hash = *db_cache.get_bitvm_disprove_root_hash().await?;
801    let latest_blockhash_root_hash = *db_cache.get_latest_blockhash_root_hash().await?;
802
803    let disprove_path = if transaction_type == TransactionType::Disprove {
804        // no need to use db cache here, this is only called once when creating the disprove tx
805        let bitvm_pks = db_cache.get_operator_bitvm_keys().await?;
806        let disprove_scripts = bitvm_pks.get_g16_verifier_disprove_scripts()?;
807        DisprovePath::Scripts(disprove_scripts)
808    } else {
809        DisprovePath::HiddenNode(&disprove_root_hash)
810    };
811
812    let kickoff_txhandler = if matches!(
813        transaction_type,
814        TransactionType::LatestBlockhash | TransactionType::MiniAssert(_)
815    ) {
816        // create scripts if any mini assert tx or latest blockhash tx is specifically requested as it needs
817        // the actual scripts to be able to spend
818        let actor = context.signer.clone().ok_or(TxError::InsufficientContext)?;
819
820        // deposit_data.deposit_outpoint.txid
821
822        let bitvm_pks =
823            actor.generate_bitvm_pks_for_deposit(deposit_data.get_deposit_outpoint(), paramset)?;
824
825        let assert_scripts = bitvm_pks.get_assert_scripts(operator_data.xonly_pk);
826
827        let latest_blockhash_script = Arc::new(WinternitzCommit::new(
828            vec![(bitvm_pks.latest_blockhash_pk.to_vec(), 40)],
829            operator_data.xonly_pk,
830            context.paramset.winternitz_log_d,
831        ));
832
833        let kickoff_txhandler = create_kickoff_txhandler(
834            kickoff_data,
835            get_txhandler(&txhandlers, TransactionType::Round)?,
836            get_txhandler(&txhandlers, TransactionType::MoveToVault)?,
837            &mut deposit_data,
838            operator_data.xonly_pk,
839            AssertScripts::AssertSpendableScript(assert_scripts),
840            disprove_path,
841            additional_disprove_script.clone(),
842            AssertScripts::AssertSpendableScript(vec![latest_blockhash_script]),
843            &challenge_ack_public_hashes,
844            paramset,
845        )?;
846
847        // Create and insert mini_asserts into return Vec
848        let mini_asserts = create_mini_asserts(&kickoff_txhandler, num_asserts, paramset)?;
849
850        for mini_assert in mini_asserts.into_iter() {
851            txhandlers.insert(mini_assert.get_transaction_type(), mini_assert);
852        }
853
854        let latest_blockhash_txhandler =
855            create_latest_blockhash_txhandler(&kickoff_txhandler, paramset)?;
856        txhandlers.insert(
857            latest_blockhash_txhandler.get_transaction_type(),
858            latest_blockhash_txhandler,
859        );
860
861        kickoff_txhandler
862    } else {
863        // use db data for scripts
864        create_kickoff_txhandler(
865            kickoff_data,
866            get_txhandler(&txhandlers, TransactionType::Round)?,
867            get_txhandler(&txhandlers, TransactionType::MoveToVault)?,
868            &mut deposit_data,
869            operator_data.xonly_pk,
870            AssertScripts::AssertScriptTapNodeHash(db_cache.get_bitvm_assert_hash().await?),
871            disprove_path,
872            additional_disprove_script.clone(),
873            AssertScripts::AssertScriptTapNodeHash(&[latest_blockhash_root_hash]),
874            &challenge_ack_public_hashes,
875            paramset,
876        )?
877    };
878
879    txhandlers.insert(kickoff_txhandler.get_transaction_type(), kickoff_txhandler);
880
881    // Creates the challenge_tx handler.
882    let challenge_txhandler = builder::transaction::create_challenge_txhandler(
883        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
884        &operator_data.reimburse_addr,
885        context.signer.map(|s| s.get_evm_address()).transpose()?,
886        paramset,
887    )?;
888    txhandlers.insert(
889        challenge_txhandler.get_transaction_type(),
890        challenge_txhandler,
891    );
892
893    // Creates the challenge timeout txhandler
894    let challenge_timeout_txhandler = create_challenge_timeout_txhandler(
895        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
896        paramset,
897    )?;
898
899    txhandlers.insert(
900        challenge_timeout_txhandler.get_transaction_type(),
901        challenge_timeout_txhandler,
902    );
903
904    let kickoff_not_finalized_txhandler =
905        builder::transaction::create_kickoff_not_finalized_txhandler(
906            get_txhandler(&txhandlers, TransactionType::Kickoff)?,
907            get_txhandler(&txhandlers, TransactionType::ReadyToReimburse)?,
908            paramset,
909        )?;
910    txhandlers.insert(
911        kickoff_not_finalized_txhandler.get_transaction_type(),
912        kickoff_not_finalized_txhandler,
913    );
914
915    let latest_blockhash_timeout_txhandler = create_latest_blockhash_timeout_txhandler(
916        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
917        get_txhandler(&txhandlers, TransactionType::Round)?,
918        paramset,
919    )?;
920    txhandlers.insert(
921        latest_blockhash_timeout_txhandler.get_transaction_type(),
922        latest_blockhash_timeout_txhandler,
923    );
924
925    // create watchtower tx's except WatchtowerChallenges
926    for watchtower_idx in 0..deposit_data.get_num_watchtowers() {
927        // Each watchtower will sign their Groth16 proof of the header chain circuit. Then, the operator will either
928        // - acknowledge the challenge by sending the operator_challenge_ACK_tx, otherwise their burn connector
929        // will get burned by operator_challenge_nack
930        let watchtower_challenge_timeout_txhandler =
931            builder::transaction::create_watchtower_challenge_timeout_txhandler(
932                get_txhandler(&txhandlers, TransactionType::Kickoff)?,
933                watchtower_idx,
934                paramset,
935            )?;
936        txhandlers.insert(
937            watchtower_challenge_timeout_txhandler.get_transaction_type(),
938            watchtower_challenge_timeout_txhandler,
939        );
940
941        let operator_challenge_nack_txhandler =
942            builder::transaction::create_operator_challenge_nack_txhandler(
943                get_txhandler(&txhandlers, TransactionType::Kickoff)?,
944                watchtower_idx,
945                get_txhandler(&txhandlers, TransactionType::Round)?,
946                paramset,
947            )?;
948        txhandlers.insert(
949            operator_challenge_nack_txhandler.get_transaction_type(),
950            operator_challenge_nack_txhandler,
951        );
952
953        let operator_challenge_ack_txhandler =
954            builder::transaction::create_operator_challenge_ack_txhandler(
955                get_txhandler(&txhandlers, TransactionType::Kickoff)?,
956                watchtower_idx,
957                paramset,
958            )?;
959        txhandlers.insert(
960            operator_challenge_ack_txhandler.get_transaction_type(),
961            operator_challenge_ack_txhandler,
962        );
963    }
964
965    if let TransactionType::WatchtowerChallenge(_) = transaction_type {
966        return Err(eyre::eyre!(
967            "Can't directly create a watchtower challenge in create_txhandlers as it needs commit data".to_string(),
968        ).into());
969    }
970
971    let assert_timeouts = create_assert_timeout_txhandlers(
972        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
973        get_txhandler(&txhandlers, TransactionType::Round)?,
974        num_asserts,
975        paramset,
976    )?;
977
978    for assert_timeout in assert_timeouts.into_iter() {
979        txhandlers.insert(assert_timeout.get_transaction_type(), assert_timeout);
980    }
981
982    // Creates the disprove_timeout_tx handler.
983    let disprove_timeout_txhandler = builder::transaction::create_disprove_timeout_txhandler(
984        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
985        paramset,
986    )?;
987
988    txhandlers.insert(
989        disprove_timeout_txhandler.get_transaction_type(),
990        disprove_timeout_txhandler,
991    );
992
993    // Creates the reimburse_tx handler.
994    let reimburse_txhandler = builder::transaction::create_reimburse_txhandler(
995        get_txhandler(&txhandlers, TransactionType::MoveToVault)?,
996        &next_round_txhandler,
997        get_txhandler(&txhandlers, TransactionType::Kickoff)?,
998        kickoff_data.kickoff_idx as usize,
999        paramset,
1000        &operator_data.reimburse_addr,
1001    )?;
1002
1003    txhandlers.insert(
1004        reimburse_txhandler.get_transaction_type(),
1005        reimburse_txhandler,
1006    );
1007
1008    match transaction_type {
1009        TransactionType::AllNeededForDeposit | TransactionType::Disprove => {
1010            let disprove_txhandler = builder::transaction::create_disprove_txhandler(
1011                get_txhandler(&txhandlers, TransactionType::Kickoff)?,
1012                get_txhandler(&txhandlers, TransactionType::Round)?,
1013            )?;
1014
1015            txhandlers.insert(
1016                disprove_txhandler.get_transaction_type(),
1017                disprove_txhandler,
1018            );
1019        }
1020        _ => {}
1021    }
1022
1023    Ok(txhandlers)
1024}
1025
1026/// Creates the round and ready-to-reimburse txhandlers for a specific operator and round index.
1027/// These transactions currently include round tx, ready to reimburse tx, and unspent kickoff txs.
1028///
1029/// # Arguments
1030///
1031/// * `paramset` - Protocol parameter set.
1032/// * `round_idx` - The index of the round.
1033/// * `operator_data` - Data for the operator.
1034/// * `kickoff_winternitz_keys` - All winternitz keys of the operator.
1035/// * `prev_ready_to_reimburse` - Previous ready-to-reimburse txhandler, if any, to not create the full collateral chain if we already have the previous round's ready to reimburse txhandler.
1036///
1037/// # Returns
1038///
1039/// A vector of [`TxHandler`] for the round, ready-to-reimburse, and unspent kickoff transactions, or a [`BridgeError`] if construction fails.
1040pub fn create_round_txhandlers(
1041    paramset: &'static ProtocolParamset,
1042    round_idx: RoundIndex,
1043    operator_data: &OperatorData,
1044    kickoff_winternitz_keys: &KickoffWinternitzKeys,
1045    prev_ready_to_reimburse: Option<&TxHandler>,
1046) -> Result<Vec<TxHandler>, BridgeError> {
1047    let mut txhandlers = Vec::with_capacity(2 + paramset.num_kickoffs_per_round);
1048
1049    let (round_txhandler, ready_to_reimburse_txhandler) = match prev_ready_to_reimburse {
1050        Some(prev_ready_to_reimburse_txhandler) => {
1051            if round_idx == RoundIndex::Collateral || round_idx == RoundIndex::Round(0) {
1052                return Err(
1053                    eyre::eyre!("Round 0 cannot be created from prev_ready_to_reimburse").into(),
1054                );
1055            }
1056            let round_txhandler = builder::transaction::create_round_txhandler(
1057                operator_data.xonly_pk,
1058                RoundTxInput::Prevout(Box::new(
1059                    prev_ready_to_reimburse_txhandler
1060                        .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)?,
1061                )),
1062                kickoff_winternitz_keys.get_keys_for_round(round_idx)?,
1063                paramset,
1064            )?;
1065
1066            let ready_to_reimburse_txhandler =
1067                builder::transaction::create_ready_to_reimburse_txhandler(
1068                    &round_txhandler,
1069                    operator_data.xonly_pk,
1070                    paramset,
1071                )?;
1072            (round_txhandler, ready_to_reimburse_txhandler)
1073        }
1074        None => {
1075            // create nth sequential collateral tx and reimburse generator tx for the operator
1076            builder::transaction::create_round_nth_txhandler(
1077                operator_data.xonly_pk,
1078                operator_data.collateral_funding_outpoint,
1079                paramset.collateral_funding_amount,
1080                round_idx,
1081                kickoff_winternitz_keys,
1082                paramset,
1083            )?
1084        }
1085    };
1086
1087    let unspent_kickoffs = create_unspent_kickoff_txhandlers(
1088        &round_txhandler,
1089        &ready_to_reimburse_txhandler,
1090        paramset,
1091    )?;
1092
1093    txhandlers.push(round_txhandler);
1094    txhandlers.push(ready_to_reimburse_txhandler);
1095
1096    for unspent_kickoff in unspent_kickoffs.into_iter() {
1097        txhandlers.push(unspent_kickoff);
1098    }
1099
1100    Ok(txhandlers)
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105    use super::*;
1106    use crate::bitvm_client::ClementineBitVMPublicKeys;
1107    use crate::builder::transaction::sign::get_kickoff_utxos_to_sign;
1108    use crate::builder::transaction::{TransactionType, TxHandlerBuilder};
1109    use crate::config::BridgeConfig;
1110    use crate::deposit::{DepositInfo, KickoffData};
1111    use crate::rpc::clementine::{SignedTxsWithType, TransactionRequest};
1112    use crate::test::common::citrea::MockCitreaClient;
1113    use crate::test::common::test_actors::TestActors;
1114    use crate::test::common::*;
1115    use bitcoin::{BlockHash, Transaction, XOnlyPublicKey};
1116    use futures::future::try_join_all;
1117    use std::collections::HashMap;
1118    use tokio::sync::mpsc;
1119
1120    fn signed_txs_to_txid(signed_txs: SignedTxsWithType) -> Vec<(TransactionType, bitcoin::Txid)> {
1121        signed_txs
1122            .signed_txs
1123            .into_iter()
1124            .map(|signed_tx| {
1125                (
1126                    signed_tx.transaction_type.unwrap().try_into().unwrap(),
1127                    bitcoin::consensus::deserialize::<Transaction>(&signed_tx.raw_tx)
1128                        .unwrap()
1129                        .compute_txid(),
1130                )
1131            })
1132            .collect()
1133    }
1134
1135    /// This test first creates a vec of transaction types the entity should be able to sign.
1136    /// Afterwards it calls internal_create_signed_txs for verifiers and operators,
1137    /// internal_create_assert_commitment_txs for operators, and internal_create_watchtower_challenge for verifiers
1138    /// and checks if all transaction types that should be signed are returned from these functions.
1139    /// If a transaction type is not found, it means the entity is not able to sign it.
1140    async fn check_if_signable(
1141        actors: TestActors<MockCitreaClient>,
1142        deposit_info: DepositInfo,
1143        deposit_blockhash: BlockHash,
1144        config: BridgeConfig,
1145    ) {
1146        let paramset = config.protocol_paramset();
1147        let deposit_outpoint = deposit_info.deposit_outpoint;
1148
1149        let mut txs_operator_can_sign = vec![
1150            TransactionType::Round,
1151            TransactionType::ReadyToReimburse,
1152            TransactionType::Kickoff,
1153            TransactionType::KickoffNotFinalized,
1154            TransactionType::Challenge,
1155            TransactionType::DisproveTimeout,
1156            TransactionType::Reimburse,
1157            TransactionType::ChallengeTimeout,
1158            TransactionType::LatestBlockhashTimeout,
1159        ];
1160        txs_operator_can_sign
1161            .extend((0..actors.get_num_verifiers()).map(TransactionType::OperatorChallengeNack));
1162        txs_operator_can_sign
1163            .extend((0..actors.get_num_verifiers()).map(TransactionType::OperatorChallengeAck));
1164        txs_operator_can_sign.extend(
1165            (0..ClementineBitVMPublicKeys::number_of_assert_txs())
1166                .map(TransactionType::AssertTimeout),
1167        );
1168        txs_operator_can_sign
1169            .extend((0..paramset.num_kickoffs_per_round).map(TransactionType::UnspentKickoff));
1170        txs_operator_can_sign.extend(
1171            (0..actors.get_num_verifiers()).map(TransactionType::WatchtowerChallengeTimeout),
1172        );
1173
1174        let operator_xonly_pks: Vec<XOnlyPublicKey> = actors.get_operators_xonly_pks();
1175        let mut utxo_idxs: Vec<Vec<usize>> = Vec::with_capacity(operator_xonly_pks.len());
1176
1177        for op_xonly_pk in &operator_xonly_pks {
1178            utxo_idxs.push(get_kickoff_utxos_to_sign(
1179                config.protocol_paramset(),
1180                *op_xonly_pk,
1181                deposit_blockhash,
1182                deposit_outpoint,
1183            ));
1184        }
1185
1186        let (tx, mut rx) = mpsc::unbounded_channel();
1187        let mut created_txs: HashMap<(KickoffData, TransactionType), Vec<bitcoin::Txid>> =
1188            HashMap::new();
1189
1190        // try to sign everything for all operators
1191        let operator_task_handles: Vec<_> = actors
1192            .get_operators()
1193            .iter_mut()
1194            .enumerate()
1195            .map(|(operator_idx, operator_rpc)| {
1196                let txs_operator_can_sign = txs_operator_can_sign.clone();
1197                let mut operator_rpc = operator_rpc.clone();
1198                let utxo_idxs = utxo_idxs.clone();
1199                let tx = tx.clone();
1200                let operator_xonly_pk = operator_xonly_pks[operator_idx];
1201                async move {
1202                    for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
1203                        for &kickoff_idx in &utxo_idxs[operator_idx] {
1204                            let kickoff_data = KickoffData {
1205                                operator_xonly_pk,
1206                                round_idx,
1207                                kickoff_idx: kickoff_idx as u32,
1208                            };
1209                            let start_time = std::time::Instant::now();
1210                            let raw_txs = operator_rpc
1211                                .internal_create_signed_txs(TransactionRequest {
1212                                    deposit_outpoint: Some(deposit_outpoint.into()),
1213                                    kickoff_id: Some(kickoff_data.into()),
1214                                })
1215                                .await
1216                                .unwrap()
1217                                .into_inner();
1218                            // test if all needed tx's are signed
1219                            for tx_type in &txs_operator_can_sign {
1220                                assert!(
1221                                    raw_txs
1222                                        .signed_txs
1223                                        .iter()
1224                                        .any(|signed_tx| signed_tx.transaction_type
1225                                            == Some((*tx_type).into())),
1226                                    "Tx type: {tx_type:?} not found in signed txs for operator"
1227                                );
1228                            }
1229                            tracing::info!(
1230                                "Operator signed txs {:?} from rpc call in time {:?}",
1231                                TransactionType::AllNeededForDeposit,
1232                                start_time.elapsed()
1233                            );
1234                            tx.send((kickoff_data, signed_txs_to_txid(raw_txs)))
1235                                .unwrap();
1236                            let raw_assert_txs = operator_rpc
1237                                .internal_create_assert_commitment_txs(TransactionRequest {
1238                                    deposit_outpoint: Some(deposit_outpoint.into()),
1239                                    kickoff_id: Some(kickoff_data.into()),
1240                                })
1241                                .await
1242                                .unwrap()
1243                                .into_inner();
1244                            tracing::info!(
1245                                "Operator Signed Assert txs of size: {}",
1246                                raw_assert_txs.signed_txs.len()
1247                            );
1248                            tx.send((kickoff_data, signed_txs_to_txid(raw_assert_txs)))
1249                                .unwrap();
1250                        }
1251                    }
1252                }
1253            })
1254            .map(tokio::task::spawn)
1255            .collect();
1256
1257        let mut txs_verifier_can_sign = vec![
1258            TransactionType::Challenge,
1259            TransactionType::KickoffNotFinalized,
1260            TransactionType::LatestBlockhashTimeout,
1261            //TransactionType::Disprove,
1262        ];
1263        txs_verifier_can_sign
1264            .extend((0..actors.get_num_verifiers()).map(TransactionType::OperatorChallengeNack));
1265        txs_verifier_can_sign.extend(
1266            (0..ClementineBitVMPublicKeys::number_of_assert_txs())
1267                .map(TransactionType::AssertTimeout),
1268        );
1269        txs_verifier_can_sign
1270            .extend((0..paramset.num_kickoffs_per_round).map(TransactionType::UnspentKickoff));
1271        txs_verifier_can_sign.extend(
1272            (0..actors.get_num_verifiers()).map(TransactionType::WatchtowerChallengeTimeout),
1273        );
1274
1275        // try to sign everything for all verifiers
1276        // try signing verifier transactions
1277        let verifier_task_handles: Vec<_> = actors
1278            .get_verifiers()
1279            .iter_mut()
1280            .map(|verifier_rpc| {
1281                let txs_verifier_can_sign = txs_verifier_can_sign.clone();
1282                let mut verifier_rpc = verifier_rpc.clone();
1283                let utxo_idxs = utxo_idxs.clone();
1284                let tx = tx.clone();
1285                let operator_xonly_pks = operator_xonly_pks.clone();
1286                async move {
1287                    for (operator_idx, utxo_idx) in utxo_idxs.iter().enumerate() {
1288                        for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
1289                            for &kickoff_idx in utxo_idx {
1290                                let kickoff_data = KickoffData {
1291                                    operator_xonly_pk: operator_xonly_pks[operator_idx],
1292                                    round_idx,
1293                                    kickoff_idx: kickoff_idx as u32,
1294                                };
1295                                let start_time = std::time::Instant::now();
1296                                let raw_txs = verifier_rpc
1297                                    .internal_create_signed_txs(TransactionRequest {
1298                                        deposit_outpoint: Some(deposit_outpoint.into()),
1299                                        kickoff_id: Some(kickoff_data.into()),
1300                                    })
1301                                    .await
1302                                    .unwrap()
1303                                    .into_inner();
1304                                // test if all needed tx's are signed
1305                                for tx_type in &txs_verifier_can_sign {
1306                                    assert!(
1307                                        raw_txs
1308                                            .signed_txs
1309                                            .iter()
1310                                            .any(|signed_tx| signed_tx.transaction_type
1311                                                == Some((*tx_type).into())),
1312                                        "Tx type: {tx_type:?} not found in signed txs for verifier"
1313                                    );
1314                                }
1315                                tracing::info!(
1316                                    "Verifier signed txs {:?} from rpc call in time {:?}",
1317                                    TransactionType::AllNeededForDeposit,
1318                                    start_time.elapsed()
1319                                );
1320                                tx.send((kickoff_data, signed_txs_to_txid(raw_txs)))
1321                                    .unwrap();
1322                                let _watchtower_challenge_tx = verifier_rpc
1323                                    .internal_create_watchtower_challenge(TransactionRequest {
1324                                        deposit_outpoint: Some(deposit_outpoint.into()),
1325                                        kickoff_id: Some(kickoff_data.into()),
1326                                    })
1327                                    .await
1328                                    .unwrap()
1329                                    .into_inner();
1330                            }
1331                        }
1332                    }
1333                }
1334            })
1335            .map(tokio::task::spawn)
1336            .collect();
1337
1338        drop(tx);
1339        while let Some((kickoff_id, txids)) = rx.recv().await {
1340            for (tx_type, txid) in txids {
1341                created_txs
1342                    .entry((kickoff_id, tx_type))
1343                    .or_default()
1344                    .push(txid);
1345            }
1346        }
1347
1348        let mut incorrect = false;
1349
1350        for ((kickoff_id, tx_type), txids) in &created_txs {
1351            // for challenge tx, txids are different because op return with own evm address, skip it
1352            if tx_type == &TransactionType::Challenge {
1353                continue;
1354            }
1355            // check if all txids are equal
1356            if !txids.iter().all(|txid| txid == &txids[0]) {
1357                tracing::error!(
1358                    "Mismatch in Txids for kickoff_id: {:?}, tx_type: {:?}, Txids: {:?}",
1359                    kickoff_id,
1360                    tx_type,
1361                    txids
1362                );
1363                incorrect = true;
1364            }
1365        }
1366        assert!(!incorrect);
1367
1368        try_join_all(operator_task_handles).await.unwrap();
1369        try_join_all(verifier_task_handles).await.unwrap();
1370    }
1371
1372    #[cfg(feature = "automation")]
1373    #[tokio::test(flavor = "multi_thread")]
1374    async fn test_deposit_and_sign_txs() {
1375        let mut config = create_test_config_with_thread_name().await;
1376        let WithProcessCleanup(_, ref rpc, _, _) = create_regtest_rpc(&mut config).await;
1377
1378        let actors = create_actors::<MockCitreaClient>(&config).await;
1379        let (deposit_params, _, deposit_blockhash, _) =
1380            run_single_deposit::<MockCitreaClient>(&mut config, rpc.clone(), None, &actors, None)
1381                .await
1382                .unwrap();
1383
1384        check_if_signable(actors, deposit_params, deposit_blockhash, config.clone()).await;
1385    }
1386
1387    #[tokio::test(flavor = "multi_thread")]
1388    #[cfg(feature = "automation")]
1389    async fn test_replacement_deposit_and_sign_txs() {
1390        let mut config = create_test_config_with_thread_name().await;
1391        let WithProcessCleanup(_, ref rpc, _, _) = create_regtest_rpc(&mut config).await;
1392
1393        let mut actors = create_actors::<MockCitreaClient>(&config).await;
1394        let (_deposit_info, old_move_txid, _deposit_blockhash, _verifiers_public_keys) =
1395            run_single_deposit::<MockCitreaClient>(&mut config, rpc.clone(), None, &actors, None)
1396                .await
1397                .unwrap();
1398
1399        let old_nofn_xonly_pk = actors.get_nofn_aggregated_xonly_pk().unwrap();
1400        // remove 1 verifier then run a replacement deposit
1401        actors.remove_verifier(2).await.unwrap();
1402
1403        let (replacement_deposit_info, _replacement_move_txid, replacement_deposit_blockhash) =
1404            run_single_replacement_deposit(
1405                &mut config,
1406                rpc,
1407                old_move_txid,
1408                &actors,
1409                old_nofn_xonly_pk,
1410            )
1411            .await
1412            .unwrap();
1413
1414        check_if_signable(
1415            actors,
1416            replacement_deposit_info,
1417            replacement_deposit_blockhash,
1418            config.clone(),
1419        )
1420        .await;
1421    }
1422
1423    #[test]
1424    fn test_txhandler_cache_store_for_next_kickoff() {
1425        let mut cache = TxHandlerCache::new();
1426        let mut txhandlers = BTreeMap::new();
1427        txhandlers.insert(
1428            TransactionType::MoveToVault,
1429            TxHandlerBuilder::new(TransactionType::MoveToVault).finalize(),
1430        );
1431        txhandlers.insert(
1432            TransactionType::Round,
1433            TxHandlerBuilder::new(TransactionType::Round).finalize(),
1434        );
1435        txhandlers.insert(
1436            TransactionType::ReadyToReimburse,
1437            TxHandlerBuilder::new(TransactionType::ReadyToReimburse).finalize(),
1438        );
1439        txhandlers.insert(
1440            TransactionType::Kickoff,
1441            TxHandlerBuilder::new(TransactionType::Kickoff).finalize(),
1442        );
1443
1444        // should store the first 3 txhandlers, and not insert kickoff
1445        assert!(cache.store_for_next_kickoff(&mut txhandlers).is_ok());
1446        assert!(txhandlers.len() == 1);
1447        assert!(cache.saved_txs.len() == 3);
1448        assert!(cache.saved_txs.contains_key(&TransactionType::MoveToVault));
1449        assert!(cache.saved_txs.contains_key(&TransactionType::Round));
1450        assert!(cache
1451            .saved_txs
1452            .contains_key(&TransactionType::ReadyToReimburse));
1453        // prev_ready_to_reimburse should be None as it is the first iteration
1454        assert!(cache.prev_ready_to_reimburse.is_none());
1455
1456        // txhandlers should contain all cached tx's
1457        txhandlers = cache.get_cached_txs();
1458        assert!(txhandlers.len() == 3);
1459        assert!(txhandlers.contains_key(&TransactionType::MoveToVault));
1460        assert!(txhandlers.contains_key(&TransactionType::Round));
1461        assert!(txhandlers.contains_key(&TransactionType::ReadyToReimburse));
1462        assert!(cache.store_for_next_kickoff(&mut txhandlers).is_ok());
1463        // prev ready to reimburse still none as we didn't go to next round
1464        assert!(cache.prev_ready_to_reimburse.is_none());
1465
1466        // should delete saved txs and store prev ready to reimburse, but it should keep movetovault
1467        assert!(cache.store_for_next_round().is_ok());
1468        assert!(cache.saved_txs.len() == 1);
1469        assert!(cache.prev_ready_to_reimburse.is_some());
1470        assert!(cache.saved_txs.contains_key(&TransactionType::MoveToVault));
1471
1472        // retrieve cached movetovault
1473        txhandlers = cache.get_cached_txs();
1474
1475        // create new round txs
1476        txhandlers.insert(
1477            TransactionType::ReadyToReimburse,
1478            TxHandlerBuilder::new(TransactionType::ReadyToReimburse).finalize(),
1479        );
1480        txhandlers.insert(
1481            TransactionType::Round,
1482            TxHandlerBuilder::new(TransactionType::Round).finalize(),
1483        );
1484        // add not relevant tx
1485        txhandlers.insert(
1486            TransactionType::WatchtowerChallenge(0),
1487            TxHandlerBuilder::new(TransactionType::WatchtowerChallenge(0)).finalize(),
1488        );
1489
1490        // should add all 3 tx's to cache again
1491        assert!(cache.store_for_next_kickoff(&mut txhandlers).is_ok());
1492        assert!(cache.saved_txs.len() == 3);
1493        assert!(cache.saved_txs.contains_key(&TransactionType::MoveToVault));
1494        assert!(cache.saved_txs.contains_key(&TransactionType::Round));
1495        assert!(cache
1496            .saved_txs
1497            .contains_key(&TransactionType::ReadyToReimburse));
1498        // prev ready to reimburse is still stored
1499        assert!(cache.prev_ready_to_reimburse.is_some());
1500    }
1501}