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