clementine_core/
operator.rs

1use ark_ff::PrimeField;
2use circuits_lib::common::constants::FIRST_FIVE_OUTPUTS;
3
4use crate::actor::{Actor, TweakCache, WinternitzDerivationPath};
5use crate::bitvm_client::{ClementineBitVMPublicKeys, SECP};
6use crate::builder::address::create_taproot_address;
7use crate::builder::script::SpendPath;
8use crate::builder::sighash::{create_operator_sighash_stream, PartialSignatureInfo};
9use crate::builder::transaction::deposit_signature_owner::EntityType;
10use crate::builder::transaction::input::{SpendableTxIn, UtxoVout};
11use crate::builder::transaction::output::UnspentTxOut;
12use crate::builder::transaction::sign::{create_and_sign_txs, TransactionRequestData};
13use crate::builder::transaction::{
14    create_burn_unused_kickoff_connectors_txhandler, create_round_nth_txhandler,
15    create_round_txhandlers, ContractContext, KickoffWinternitzKeys, TxHandler, TxHandlerBuilder,
16    DEFAULT_SEQUENCE,
17};
18use crate::citrea::CitreaClientT;
19use crate::config::BridgeConfig;
20use crate::database::Database;
21use crate::database::DatabaseTransaction;
22use crate::deposit::{DepositData, KickoffData, OperatorData};
23use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
24use clementine_errors::BridgeError;
25use clementine_primitives::TransactionType;
26
27use crate::metrics::L1SyncStatusProvider;
28use crate::rpc::clementine::{EntityStatus, NormalSignatureKind, StoppedTasks};
29use crate::task::entity_metric_publisher::{
30    EntityMetricPublisher, ENTITY_METRIC_PUBLISHER_INTERVAL,
31};
32use crate::task::manager::BackgroundTaskManager;
33use crate::task::payout_checker::{PayoutCheckerTask, PAYOUT_CHECKER_POLL_DELAY};
34use crate::task::TaskExt;
35use crate::utils::{monitor_standalone_task, Last20Bytes, ScriptBufExt};
36use crate::utils::{NamedEntity, TxMetadata};
37use crate::{builder, constants};
38use bitcoin::hashes::Hash;
39use bitcoin::secp256k1::schnorr::Signature;
40use bitcoin::secp256k1::{schnorr, Message};
41use bitcoin::{taproot, Address, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
42use bitcoincore_rpc::json::AddressType;
43use bitcoincore_rpc::RpcApi;
44use bitvm::signatures::winternitz;
45use clementine_primitives::UTXO;
46use clementine_primitives::{PublicHash, RoundIndex};
47
48use eyre::{eyre, Context, OptionExt};
49use std::collections::HashMap;
50use tokio::sync::mpsc;
51use tokio_stream::StreamExt;
52
53#[cfg(feature = "automation")]
54use {
55    crate::{
56        builder::script::extract_winternitz_commits,
57        header_chain_prover::HeaderChainProver,
58        states::StateManager,
59        task::IntoTask,
60        tx_sender::{ActivatedWithOutpoint, ActivatedWithTxid, TxSenderClient},
61        utils::FeePayingType,
62    },
63    bitcoin::Witness,
64    bitvm::chunk::api::generate_assertions,
65    bridge_circuit_host::{
66        bridge_circuit_host::{
67            create_spv, prove_bridge_circuit, MAINNET_BRIDGE_CIRCUIT_ELF,
68            REGTEST_BRIDGE_CIRCUIT_ELF, REGTEST_BRIDGE_CIRCUIT_ELF_TEST, SIGNET_BRIDGE_CIRCUIT_ELF,
69            SIGNET_BRIDGE_CIRCUIT_ELF_TEST, TESTNET4_BRIDGE_CIRCUIT_ELF,
70            TESTNET4_BRIDGE_CIRCUIT_ELF_TEST,
71        },
72        structs::{BridgeCircuitHostParams, WatchtowerContext},
73    },
74    circuits_lib::bridge_circuit::structs::LightClientProof,
75};
76
77pub struct OperatorServer<C: CitreaClientT> {
78    pub operator: Operator<C>,
79    background_tasks: BackgroundTaskManager,
80}
81
82#[derive(Debug, Clone)]
83pub struct Operator<C: CitreaClientT> {
84    pub rpc: ExtendedBitcoinRpc,
85    pub db: Database,
86    pub signer: Actor,
87    pub config: BridgeConfig,
88    pub collateral_funding_outpoint: OutPoint,
89    pub(crate) reimburse_addr: Address,
90    #[cfg(feature = "automation")]
91    pub tx_sender: TxSenderClient<Database>,
92    #[cfg(feature = "automation")]
93    pub header_chain_prover: HeaderChainProver,
94    pub citrea_client: C,
95}
96
97impl<C> OperatorServer<C>
98where
99    C: CitreaClientT,
100{
101    pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
102        let operator = Operator::new(config.clone()).await?;
103        let background_tasks = BackgroundTaskManager::default();
104
105        Ok(Self {
106            operator,
107            background_tasks,
108        })
109    }
110
111    /// Starts the background tasks for the operator.
112    /// If called multiple times, it will restart only the tasks that are not already running.
113    pub async fn start_background_tasks(&self) -> Result<(), BridgeError> {
114        // initialize and run state manager
115        #[cfg(feature = "automation")]
116        {
117            let state_manager = StateManager::new(
118                self.operator.db.clone(),
119                self.operator.clone(),
120                self.operator.rpc.clone(),
121                self.operator.config.clone(),
122            )
123            .await?;
124
125            let should_run_state_mgr = {
126                #[cfg(test)]
127                {
128                    self.operator.config.test_params.should_run_state_manager
129                }
130                #[cfg(not(test))]
131                {
132                    true
133                }
134            };
135
136            if should_run_state_mgr {
137                self.background_tasks
138                    .ensure_task_looping(state_manager.block_fetcher_task().await?)
139                    .await;
140                self.background_tasks
141                    .ensure_task_looping(state_manager.into_task())
142                    .await;
143            }
144        }
145
146        // run payout checker task
147        self.background_tasks
148            .ensure_task_looping(
149                PayoutCheckerTask::new(self.operator.db.clone(), self.operator.clone())
150                    .with_delay(PAYOUT_CHECKER_POLL_DELAY),
151            )
152            .await;
153
154        self.background_tasks
155            .ensure_task_looping(
156                EntityMetricPublisher::<Operator<C>>::new(
157                    self.operator.db.clone(),
158                    self.operator.rpc.clone(),
159                    self.operator.config.clone(),
160                )
161                .with_delay(ENTITY_METRIC_PUBLISHER_INTERVAL),
162            )
163            .await;
164
165        tracing::info!("Payout checker task started");
166
167        // track the operator's round state
168        #[cfg(feature = "automation")]
169        {
170            // Will not start a new state machine if one for the operator already exists.
171            self.operator.track_rounds().await?;
172            tracing::info!("Operator round state tracked");
173        }
174
175        Ok(())
176    }
177
178    pub async fn get_current_status(&self) -> Result<EntityStatus, BridgeError> {
179        let stopped_tasks = match self.background_tasks.get_stopped_tasks().await {
180            Ok(stopped_tasks) => stopped_tasks,
181            Err(e) => {
182                tracing::error!("Failed to get stopped tasks: {:?}", e);
183                StoppedTasks {
184                    stopped_tasks: vec![format!("Stopped tasks fetch failed {:?}", e)],
185                }
186            }
187        };
188
189        // Determine if automation is enabled
190        let automation_enabled = cfg!(feature = "automation");
191
192        let sync_status = Operator::<C>::get_l1_status(
193            &self.operator.db,
194            &self.operator.rpc,
195            &self.operator.config,
196        )
197        .await?;
198
199        Ok(EntityStatus {
200            automation: automation_enabled,
201            wallet_balance: sync_status
202                .wallet_balance
203                .map(|balance| format!("{} BTC", balance.to_btc())),
204            tx_sender_synced_height: sync_status.tx_sender_synced_height,
205            finalized_synced_height: sync_status.finalized_synced_height,
206            hcp_last_proven_height: sync_status.hcp_last_proven_height,
207            rpc_tip_height: sync_status.rpc_tip_height,
208            bitcoin_syncer_synced_height: sync_status.btc_syncer_synced_height,
209            stopped_tasks: Some(stopped_tasks),
210            state_manager_next_height: sync_status.state_manager_next_height,
211            btc_fee_rate_sat_vb: sync_status.bitcoin_fee_rate_sat_vb,
212        })
213    }
214}
215
216impl<C> Operator<C>
217where
218    C: CitreaClientT,
219{
220    /// Creates a new `Operator`.
221    pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
222        let signer = Actor::new(config.secret_key, config.protocol_paramset().network);
223
224        let db = Database::new(&config).await?;
225        let rpc = ExtendedBitcoinRpc::connect(
226            config.bitcoin_rpc_url.clone(),
227            config.bitcoin_rpc_user.clone(),
228            config.bitcoin_rpc_password.clone(),
229            None,
230        )
231        .await?;
232
233        #[cfg(feature = "automation")]
234        let tx_sender = TxSenderClient::new(db.clone(), Self::TX_SENDER_CONSUMER_ID.to_string());
235
236        if config.operator_withdrawal_fee_sats.is_none() {
237            return Err(eyre::eyre!("Operator withdrawal fee is not set").into());
238        }
239
240        // check if we store our collateral outpoint already in db
241        let mut dbtx = db.begin_transaction().await?;
242        let op_data = db
243            .get_operator(Some(&mut dbtx), signer.xonly_public_key)
244            .await?;
245        let (collateral_funding_outpoint, reimburse_addr) = match op_data {
246            Some(operator_data) => {
247                // Operator data is already set in db, we don't actually need to do anything.
248                // set_operator_checked will give error if the values set in config and db doesn't match.
249                (
250                    operator_data.collateral_funding_outpoint,
251                    operator_data.reimburse_addr,
252                )
253            }
254            None => {
255                if config.protocol_paramset().network == bitcoin::Network::Bitcoin
256                    && (config.operator_reimbursement_address.is_none()
257                        || config.operator_reimbursement_address.is_none())
258                {
259                    let wallet_address = rpc.get_new_wallet_address().await?;
260                    return Err(eyre::eyre!(
261                        "Operator collateral funding outpoint or reimbursement address is not set in the configuration. \
262                         To initialize the operator, please send {} BTC to the operator's address {}. \
263                         After funding, set OPERATOR_COLLATERAL_FUNDING_OUTPOINT in your configuration to the outpoint of the funded transaction, \
264                         and set OPERATOR_REIMBURSEMENT_ADDRESS to {}. \
265                         Use your operator's Bitcoin wallet address {} to fund the operator to pay withdrawals.",
266                        config.protocol_paramset().collateral_funding_amount.to_btc(),
267                        signer.address,
268                        signer.address,
269                        wallet_address
270                    ).into());
271                }
272                // Operator data is not set in db, then we check if any collateral outpoint and reimbursement address is set in config.
273                // If so we create a new operator using those data, otherwise we generate new collateral outpoint and reimbursement address.
274                let reimburse_addr = match &config.operator_reimbursement_address {
275                    Some(reimburse_addr) => {
276                        reimburse_addr
277                            .to_owned()
278                            .require_network(config.protocol_paramset().network)
279                            .wrap_err(format!("Invalid operator reimbursement address provided in config: {:?} for network: {:?}", reimburse_addr, config.protocol_paramset().network))?
280                    }
281                    None => {
282                        rpc
283                        .get_new_address(Some("OperatorReimbursement"), Some(AddressType::Bech32m))
284                        .await
285                        .wrap_err("Failed to get new address")?
286                        .require_network(config.protocol_paramset().network)
287                        .wrap_err(format!("Invalid operator reimbursement address generated for the network in config: {:?}
288                                Possibly the provided rpc's network and network given in config doesn't match", config.protocol_paramset().network))?
289                    }
290                };
291                let outpoint = match &config.operator_collateral_funding_outpoint {
292                    Some(outpoint) => {
293                        // check if outpoint exists on chain and has exactly collateral funding amount
294                        let collateral_tx = rpc
295                            .get_tx_of_txid(&outpoint.txid)
296                            .await
297                            .wrap_err("Failed to get collateral funding tx")?;
298                        let collateral_txout = collateral_tx
299                            .output
300                            .get(outpoint.vout as usize)
301                            .ok_or_eyre("Invalid vout index for collateral funding tx")?;
302                        if collateral_txout.script_pubkey != signer.address.script_pubkey() {
303                            return Err(eyre::eyre!("Operator collateral funding outpoint given in config has a different script pubkey than the pubkey matching to the operator's secret key. Script pubkey should correspond to taproot address with no scripts and internal key equal to the operator's xonly public key. Script pubkey in given outpoint: {:?}, Script pubkey should be: {:?}", collateral_txout.script_pubkey, signer.address.script_pubkey()).into());
304                        }
305                        if collateral_txout.value
306                            != config.protocol_paramset().collateral_funding_amount
307                        {
308                            return Err(eyre::eyre!("Operator collateral funding outpoint given in config has a different amount than the one specified in config..
309                                Bridge collateral funding amount: {:?}, Amount in given outpoint: {:?}", config.protocol_paramset().collateral_funding_amount, collateral_txout.value).into());
310                        }
311                        *outpoint
312                    }
313                    None => {
314                        // create a new outpoint that has collateral funding amount
315                        rpc.send_to_address(
316                            &signer.address,
317                            config.protocol_paramset().collateral_funding_amount,
318                        )
319                        .await?
320                    }
321                };
322                (outpoint, reimburse_addr)
323            }
324        };
325
326        db.insert_operator_if_not_exists(
327            Some(&mut dbtx),
328            signer.xonly_public_key,
329            &reimburse_addr,
330            collateral_funding_outpoint,
331        )
332        .await?;
333        dbtx.commit().await?;
334        let citrea_client = C::new(
335            config.citrea_rpc_url.clone(),
336            config.citrea_light_client_prover_url.clone(),
337            config.citrea_chain_id,
338            None,
339            config.citrea_request_timeout,
340        )
341        .await?;
342
343        tracing::info!(
344            "Operator xonly pk: {:?}, db created with name: {:?}",
345            signer.xonly_public_key,
346            config.db_name
347        );
348
349        #[cfg(feature = "automation")]
350        let header_chain_prover = HeaderChainProver::new(&config, rpc.clone()).await?;
351
352        Ok(Operator {
353            rpc,
354            db: db.clone(),
355            signer,
356            config,
357            collateral_funding_outpoint,
358            #[cfg(feature = "automation")]
359            tx_sender,
360            citrea_client,
361            #[cfg(feature = "automation")]
362            header_chain_prover,
363            reimburse_addr,
364        })
365    }
366
367    #[cfg(feature = "automation")]
368    pub async fn send_initial_round_tx(&self, round_tx: &Transaction) -> Result<(), BridgeError> {
369        let mut dbtx = self.db.begin_transaction().await?;
370        self.tx_sender
371            .insert_try_to_send(
372                Some(&mut dbtx),
373                Some(TxMetadata {
374                    tx_type: TransactionType::Round,
375                    operator_xonly_pk: None,
376                    round_idx: Some(RoundIndex::Round(0)),
377                    kickoff_idx: None,
378                    deposit_outpoint: None,
379                }),
380                round_tx,
381                FeePayingType::CPFP,
382                None,
383                &[],
384                &[],
385                &[],
386                &[],
387            )
388            .await?;
389        dbtx.commit().await?;
390        Ok(())
391    }
392
393    /// Returns an operator's winternitz public keys and challenge ackpreimages
394    /// & hashes.
395    ///
396    /// # Returns
397    ///
398    /// - [`mpsc::Receiver`]: A [`tokio`] data channel with a type of
399    ///   [`winternitz::PublicKey`] and size of operator's winternitz public
400    ///   keys count
401    /// - [`mpsc::Receiver`]: A [`tokio`] data channel with a type of
402    ///   [`PublicHash`] and size of operator's challenge ack preimages & hashes
403    ///   count
404    ///
405    pub async fn get_params(
406        &self,
407    ) -> Result<
408        (
409            mpsc::Receiver<winternitz::PublicKey>,
410            mpsc::Receiver<schnorr::Signature>,
411        ),
412        BridgeError,
413    > {
414        tracing::info!("Generating operator params");
415        tracing::info!("Generating kickoff winternitz pubkeys");
416        let wpks = self.generate_kickoff_winternitz_pubkeys()?;
417        tracing::info!("Kickoff winternitz pubkeys generated");
418        let (wpk_tx, wpk_rx) = mpsc::channel(wpks.len());
419        let kickoff_wpks = KickoffWinternitzKeys::new(
420            wpks,
421            self.config.protocol_paramset().num_kickoffs_per_round,
422            self.config.protocol_paramset().num_round_txs,
423        )?;
424        tracing::info!("Starting to generate unspent kickoff signatures");
425        let kickoff_sigs = self.generate_unspent_kickoff_sigs(&kickoff_wpks)?;
426        tracing::info!("Unspent kickoff signatures generated");
427        let wpks = kickoff_wpks.get_all_keys();
428        let (sig_tx, sig_rx) = mpsc::channel(kickoff_sigs.len());
429
430        tokio::spawn(async move {
431            for wpk in wpks {
432                wpk_tx
433                    .send(wpk)
434                    .await
435                    .wrap_err("Failed to send winternitz public key")?;
436            }
437
438            for sig in kickoff_sigs {
439                sig_tx
440                    .send(sig)
441                    .await
442                    .wrap_err("Failed to send kickoff signature")?;
443            }
444
445            Ok::<(), BridgeError>(())
446        });
447
448        Ok((wpk_rx, sig_rx))
449    }
450
451    pub async fn deposit_sign(
452        &self,
453        mut deposit_data: DepositData,
454    ) -> Result<mpsc::Receiver<Result<schnorr::Signature, BridgeError>>, BridgeError> {
455        self.citrea_client
456            .check_nofn_correctness(deposit_data.get_nofn_xonly_pk()?)
457            .await?;
458
459        let mut tweak_cache = TweakCache::default();
460        let (sig_tx, sig_rx) = mpsc::channel(constants::DEFAULT_CHANNEL_SIZE);
461        let monitor_err_sender = sig_tx.clone();
462
463        let deposit_blockhash = self
464            .rpc
465            .get_blockhash_of_tx(&deposit_data.get_deposit_outpoint().txid)
466            .await?;
467
468        let mut sighash_stream = Box::pin(create_operator_sighash_stream(
469            self.db.clone(),
470            self.signer.xonly_public_key,
471            self.config.clone(),
472            deposit_data,
473            deposit_blockhash,
474        ));
475
476        let signer = self.signer.clone();
477        let handle = tokio::spawn(async move {
478            while let Some(sighash) = sighash_stream.next().await {
479                // None because utxos that operators need to sign do not have scripts
480                let (sighash, sig_info) = sighash?;
481                let sig = signer.sign_with_tweak_data(
482                    sighash,
483                    sig_info.tweak_data,
484                    Some(&mut tweak_cache),
485                )?;
486
487                sig_tx
488                    .send(Ok(sig))
489                    .await
490                    .wrap_err("Failed to send signature in operator deposit sign")?;
491            }
492
493            Ok::<(), BridgeError>(())
494        });
495        monitor_standalone_task(handle, "Operator deposit sign", monitor_err_sender);
496
497        Ok(sig_rx)
498    }
499
500    /// Creates the round state machine by adding a system event to the database
501    #[cfg(feature = "automation")]
502    pub async fn track_rounds(&self) -> Result<(), BridgeError> {
503        let mut dbtx = self.db.begin_transaction().await?;
504        // set operators own kickoff winternitz public keys before creating the round state machine
505        // as round machine needs kickoff keys to create the first round tx
506        self.db
507            .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
508                Some(&mut dbtx),
509                self.signer.xonly_public_key,
510                self.generate_kickoff_winternitz_pubkeys()?,
511            )
512            .await?;
513
514        StateManager::<Operator<C>>::dispatch_new_round_machine(&self.db, &mut dbtx, self.data())
515            .await?;
516        dbtx.commit().await?;
517        Ok(())
518    }
519
520    /// Checks if the withdrawal amount is within the acceptable range.
521    fn is_profitable(
522        input_amount: Amount,
523        withdrawal_amount: Amount,
524        bridge_amount_sats: Amount,
525        operator_withdrawal_fee_sats: Amount,
526    ) -> bool {
527        // Use checked_sub to safely handle potential underflow
528        let withdrawal_diff = match withdrawal_amount
529            .to_sat()
530            .checked_sub(input_amount.to_sat())
531        {
532            Some(diff) => Amount::from_sat(diff),
533            None => {
534                // input amount is greater than withdrawal amount, so it's profitable but doesn't make sense
535                tracing::warn!(
536                    "Some user gave more amount than the withdrawal amount as input for withdrawal"
537                );
538                return true;
539            }
540        };
541
542        if withdrawal_diff > bridge_amount_sats {
543            return false;
544        }
545
546        // Calculate net profit after the withdrawal using checked_sub to prevent panic
547        let net_profit = match bridge_amount_sats.checked_sub(withdrawal_diff) {
548            Some(profit) => profit,
549            None => return false, // If underflow occurs, it's not profitable
550        };
551
552        // Net profit must be bigger than withdrawal fee.
553        // net profit doesn't take into account the fees, but operator_withdrawal_fee_sats should
554        net_profit >= operator_withdrawal_fee_sats
555    }
556
557    /// Prepares a withdrawal by:
558    ///
559    /// 1. Checking if the withdrawal has been made on Citrea
560    /// 2. Verifying the given signature
561    /// 3. Checking if the withdrawal is profitable or not
562    /// 4. Funding the withdrawal transaction using TxSender RBF option
563    ///
564    /// # Parameters
565    ///
566    /// - `withdrawal_idx`: Citrea withdrawal UTXO index
567    /// - `in_signature`: User's signature that is going to be used for signing
568    ///   withdrawal transaction input
569    /// - `in_outpoint`: User's input for the payout transaction
570    /// - `out_script_pubkey`: User's script pubkey which will be used
571    ///   in the payout transaction's output
572    /// - `out_amount`: Payout transaction output's value
573    ///
574    /// # Returns
575    ///
576    /// - Ok(()) if the withdrawal checks are successful and a payout transaction is added to the TxSender
577    /// - Err(BridgeError) if the withdrawal checks fail
578    pub async fn withdraw(
579        &self,
580        withdrawal_index: u32,
581        in_signature: taproot::Signature,
582        in_outpoint: OutPoint,
583        out_script_pubkey: ScriptBuf,
584        out_amount: Amount,
585    ) -> Result<Transaction, BridgeError> {
586        tracing::info!(
587            "Withdrawing with index: {}, in_signature: {:?}, in_outpoint: {:?}, out_script_pubkey: {}, out_amount: {}",
588            withdrawal_index,
589            in_signature,
590            in_outpoint,
591            out_script_pubkey,
592            out_amount
593        );
594
595        // Prepare input and output of the payout transaction.
596        let input_prevout = self.rpc.get_txout_from_outpoint(&in_outpoint).await?;
597        let input_utxo = UTXO {
598            outpoint: in_outpoint,
599            txout: input_prevout,
600        };
601        let output_txout = TxOut {
602            value: out_amount,
603            script_pubkey: out_script_pubkey,
604        };
605
606        // Check Citrea for the withdrawal state.
607        let withdrawal_utxo = self
608            .db
609            .get_withdrawal_utxo_from_citrea_withdrawal(None, withdrawal_index)
610            .await?;
611
612        if withdrawal_utxo != input_utxo.outpoint {
613            return Err(eyre::eyre!("Input UTXO does not match withdrawal UTXO from Citrea: Input Outpoint: {0}, Withdrawal Outpoint (from Citrea): {1}", input_utxo.outpoint, withdrawal_utxo).into());
614        }
615
616        let operator_withdrawal_fee_sats =
617            self.config
618                .operator_withdrawal_fee_sats
619                .ok_or(BridgeError::ConfigError(
620                    "Operator withdrawal fee sats is not specified in configuration file"
621                        .to_string(),
622                ))?;
623        if !Self::is_profitable(
624            input_utxo.txout.value,
625            output_txout.value,
626            self.config.protocol_paramset().bridge_amount,
627            operator_withdrawal_fee_sats,
628        ) {
629            return Err(eyre::eyre!("Not enough fee for operator").into());
630        }
631
632        let user_xonly_pk = &input_utxo
633            .txout
634            .script_pubkey
635            .try_get_taproot_pk()
636            .wrap_err("Input utxo script pubkey is not a valid taproot script")?;
637
638        let payout_txhandler = builder::transaction::create_payout_txhandler(
639            input_utxo,
640            output_txout,
641            self.signer.xonly_public_key,
642            in_signature,
643            self.config.protocol_paramset().network,
644        )?;
645
646        // tracing::info!("Payout txhandler: {:?}", hex::encode(bitcoin::consensus::serialize(&payout_txhandler.get_cached_tx())));
647
648        let sighash = payout_txhandler.calculate_sighash_txin(0, in_signature.sighash_type)?;
649
650        SECP.verify_schnorr(
651            &in_signature.signature,
652            &Message::from_digest(*sighash.as_byte_array()),
653            user_xonly_pk,
654        )
655        .wrap_err("Failed to verify signature received from user for payout txin. Ensure the signature uses SinglePlusAnyoneCanPay sighash type.")?;
656
657        let fee_rate_result = self
658            .rpc
659            .get_fee_rate(
660                self.config.protocol_paramset.network,
661                &self.config.mempool_api_host,
662                &self.config.mempool_api_endpoint,
663                self.config.tx_sender_limits.mempool_fee_rate_multiplier,
664                self.config.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
665                self.config.tx_sender_limits.fee_rate_hard_cap,
666            )
667            .await;
668
669        let fee_rate_option = match fee_rate_result {
670            Ok(fee_rate) => Some(Amount::from_sat(fee_rate.to_sat_per_vb_ceil() * 1000)),
671            Err(e) => {
672                tracing::warn!("Failed to get fee rate from mempool API; funding tx with automatic fee rate. Error: {e:?}");
673                None
674            }
675        };
676
677        // send payout tx using RBF
678        let funded_tx = self
679            .rpc
680            .fund_raw_transaction(
681                payout_txhandler.get_cached_tx(),
682                Some(&bitcoincore_rpc::json::FundRawTransactionOptions {
683                    add_inputs: Some(true),
684                    change_address: None,
685                    change_position: Some(1),
686                    change_type: None,
687                    include_watching: None,
688                    lock_unspents: Some(false),
689                    fee_rate: fee_rate_option,
690                    subtract_fee_from_outputs: None,
691                    replaceable: None,
692                    conf_target: None,
693                    estimate_mode: None,
694                }),
695                None,
696            )
697            .await
698            .wrap_err("Failed to fund raw transaction")?
699            .hex;
700
701        let signed_tx = self
702            .rpc
703            .sign_raw_transaction_with_wallet(&funded_tx, None, None)
704            .await
705            .wrap_err("Failed to sign withdrawal transaction")?
706            .hex;
707
708        let signed_tx: Transaction = bitcoin::consensus::deserialize(&signed_tx)
709            .wrap_err("Failed to deserialize signed withdrawal transaction")?;
710
711        self.rpc
712            .send_raw_transaction(&signed_tx)
713            .await
714            .wrap_err("Failed to send withdrawal transaction")?;
715
716        Ok(signed_tx)
717    }
718
719    /// Generates Winternitz public keys for every  BitVM assert tx for a deposit.
720    ///
721    /// # Returns
722    ///
723    /// - [`Vec<Vec<winternitz::PublicKey>>`]: Winternitz public keys for
724    ///   `watchtower index` row and `BitVM assert tx index` column.
725    pub fn generate_assert_winternitz_pubkeys(
726        &self,
727        deposit_outpoint: bitcoin::OutPoint,
728    ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
729        tracing::debug!("Generating assert winternitz pubkeys");
730        let bitvm_pks = self
731            .signer
732            .generate_bitvm_pks_for_deposit(deposit_outpoint, self.config.protocol_paramset())?;
733
734        let flattened_wpks = bitvm_pks.to_flattened_vec();
735
736        Ok(flattened_wpks)
737    }
738    /// Generates Winternitz public keys for every blockhash commit to be used in kickoff utxos.
739    /// Unique for each kickoff utxo of operator.
740    ///
741    /// # Returns
742    ///
743    /// - [`Vec<Vec<winternitz::PublicKey>>`]: Winternitz public keys for
744    ///   `round_index` row and `kickoff_idx` column.
745    pub fn generate_kickoff_winternitz_pubkeys(
746        &self,
747    ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
748        let mut winternitz_pubkeys =
749            Vec::with_capacity(self.config.get_num_kickoff_winternitz_pks());
750
751        // we need num_round_txs + 1 because the last round includes reimburse generators of previous round
752        for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs + 1)
753        {
754            for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
755                let path = WinternitzDerivationPath::Kickoff(
756                    round_idx,
757                    kickoff_idx as u32,
758                    self.config.protocol_paramset(),
759                );
760                winternitz_pubkeys.push(self.signer.derive_winternitz_pk(path)?);
761            }
762        }
763
764        if winternitz_pubkeys.len() != self.config.get_num_kickoff_winternitz_pks() {
765            return Err(eyre::eyre!(
766                "Expected {} number of kickoff winternitz pubkeys, but got {}",
767                self.config.get_num_kickoff_winternitz_pks(),
768                winternitz_pubkeys.len()
769            )
770            .into());
771        }
772
773        Ok(winternitz_pubkeys)
774    }
775
776    pub fn generate_unspent_kickoff_sigs(
777        &self,
778        kickoff_wpks: &KickoffWinternitzKeys,
779    ) -> Result<Vec<Signature>, BridgeError> {
780        let mut tweak_cache = TweakCache::default();
781        let mut sigs: Vec<Signature> =
782            Vec::with_capacity(self.config.get_num_unspent_kickoff_sigs());
783        let mut prev_ready_to_reimburse: Option<TxHandler> = None;
784        let operator_data = OperatorData {
785            xonly_pk: self.signer.xonly_public_key,
786            collateral_funding_outpoint: self.collateral_funding_outpoint,
787            reimburse_addr: self.reimburse_addr.clone(),
788        };
789        for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
790            let txhandlers = create_round_txhandlers(
791                self.config.protocol_paramset(),
792                round_idx,
793                &operator_data,
794                kickoff_wpks,
795                prev_ready_to_reimburse.as_ref(),
796            )?;
797            for txhandler in txhandlers {
798                if let TransactionType::UnspentKickoff(kickoff_idx) =
799                    txhandler.get_transaction_type()
800                {
801                    let partial = PartialSignatureInfo {
802                        operator_idx: 0, // dummy value
803                        round_idx,
804                        kickoff_utxo_idx: kickoff_idx,
805                    };
806                    let sighashes = txhandler
807                        .calculate_shared_txins_sighash(EntityType::OperatorSetup, partial)?;
808                    let signed_sigs: Result<Vec<_>, _> = sighashes
809                        .into_iter()
810                        .map(|(sighash, sig_info)| {
811                            self.signer.sign_with_tweak_data(
812                                sighash,
813                                sig_info.tweak_data,
814                                Some(&mut tweak_cache),
815                            )
816                        })
817                        .collect();
818                    sigs.extend(signed_sigs?);
819                }
820                if let TransactionType::ReadyToReimburse = txhandler.get_transaction_type() {
821                    prev_ready_to_reimburse = Some(txhandler);
822                }
823            }
824        }
825        if sigs.len() != self.config.get_num_unspent_kickoff_sigs() {
826            return Err(eyre::eyre!(
827                "Expected {} number of unspent kickoff sigs, but got {}",
828                self.config.get_num_unspent_kickoff_sigs(),
829                sigs.len()
830            )
831            .into());
832        }
833        Ok(sigs)
834    }
835
836    pub fn generate_challenge_ack_preimages_and_hashes(
837        &self,
838        deposit_data: &DepositData,
839    ) -> Result<Vec<PublicHash>, BridgeError> {
840        let mut hashes = Vec::with_capacity(self.config.get_num_challenge_ack_hashes(deposit_data));
841
842        for watchtower_idx in 0..deposit_data.get_num_watchtowers() {
843            let path = WinternitzDerivationPath::ChallengeAckHash(
844                watchtower_idx as u32,
845                deposit_data.get_deposit_outpoint(),
846                self.config.protocol_paramset(),
847            );
848            let hash = self.signer.generate_public_hash_from_path(path)?;
849            hashes.push(hash);
850        }
851
852        if hashes.len() != self.config.get_num_challenge_ack_hashes(deposit_data) {
853            return Err(eyre::eyre!(
854                "Expected {} number of challenge ack hashes, but got {}",
855                self.config.get_num_challenge_ack_hashes(deposit_data),
856                hashes.len()
857            )
858            .into());
859        }
860
861        Ok(hashes)
862    }
863
864    pub async fn handle_finalized_payout<'a>(
865        &'a self,
866        dbtx: DatabaseTransaction<'a>,
867        deposit_outpoint: OutPoint,
868        payout_tx_blockhash: BlockHash,
869    ) -> Result<bitcoin::Txid, BridgeError> {
870        let (deposit_id, deposit_data) = self
871            .db
872            .get_deposit_data(Some(dbtx), deposit_outpoint)
873            .await?
874            .ok_or(BridgeError::DatabaseError(sqlx::Error::RowNotFound))?;
875
876        // get unused kickoff connector
877        let (round_idx, kickoff_idx) = self
878            .db
879            .get_unused_and_signed_kickoff_connector(
880                Some(dbtx),
881                deposit_id,
882                self.signer.xonly_public_key,
883            )
884            .await?
885            .ok_or(BridgeError::DatabaseError(sqlx::Error::RowNotFound))?;
886
887        let current_round_index = self.db.get_current_round_index(Some(dbtx)).await?;
888        #[cfg(feature = "automation")]
889        if current_round_index != round_idx {
890            // we currently have no free kickoff connectors in the current round, so we need to end round first
891            // if current_round_index should only be smaller than round_idx, and should not be smaller by more than 1
892            // so sanity check:
893            if current_round_index.next_round() != round_idx {
894                return Err(eyre::eyre!(
895                    "Internal error: Expected the current round ({:?}) to be equal to or 1 less than the round of the first available kickoff for deposit reimbursement ({:?}) for deposit {:?}. If the round is less than the current round, there is an issue with the logic of the fn that gets the first available kickoff. If the round is greater, that means the next round do not have any kickoff connectors available for reimbursement, which should not be possible.",
896                    current_round_index, round_idx, deposit_outpoint
897                ).into());
898            }
899            // start the next round to be able to get reimbursement for the payout
900            self.end_round(dbtx).await?;
901        }
902
903        // get signed txs,
904        let kickoff_data = KickoffData {
905            operator_xonly_pk: self.signer.xonly_public_key,
906            round_idx,
907            kickoff_idx,
908        };
909
910        let payout_tx_blockhash = payout_tx_blockhash.as_byte_array().last_20_bytes();
911
912        #[cfg(test)]
913        let payout_tx_blockhash = self
914            .config
915            .test_params
916            .maybe_disrupt_payout_tx_block_hash_commit(payout_tx_blockhash);
917
918        let context = ContractContext::new_context_for_kickoff(
919            kickoff_data,
920            deposit_data,
921            self.config.protocol_paramset(),
922        );
923
924        let signed_txs = create_and_sign_txs(
925            self.db.clone(),
926            &self.signer,
927            self.config.clone(),
928            context,
929            Some(payout_tx_blockhash),
930            Some(dbtx),
931        )
932        .await?;
933
934        let tx_metadata = Some(TxMetadata {
935            tx_type: TransactionType::Dummy, // will be replaced in add_tx_to_queue
936            operator_xonly_pk: Some(self.signer.xonly_public_key),
937            round_idx: Some(round_idx),
938            kickoff_idx: Some(kickoff_idx),
939            deposit_outpoint: Some(deposit_outpoint),
940        });
941
942        // try to send them
943        for (tx_type, signed_tx) in &signed_txs {
944            match *tx_type {
945                TransactionType::Kickoff
946                | TransactionType::OperatorChallengeAck(_)
947                | TransactionType::WatchtowerChallengeTimeout(_)
948                | TransactionType::ChallengeTimeout
949                | TransactionType::DisproveTimeout
950                | TransactionType::Reimburse => {
951                    #[cfg(feature = "automation")]
952                    self.tx_sender
953                        .add_tx_to_queue(
954                            Some(dbtx),
955                            *tx_type,
956                            signed_tx,
957                            &signed_txs,
958                            tx_metadata,
959                            self.config.protocol_paramset(),
960                            None,
961                        )
962                        .await?;
963                }
964                _ => {}
965            }
966        }
967
968        let kickoff_txid = signed_txs
969            .iter()
970            .find_map(|(tx_type, tx)| {
971                if let TransactionType::Kickoff = tx_type {
972                    Some(tx.compute_txid())
973                } else {
974                    None
975                }
976            })
977            .ok_or(eyre::eyre!(
978                "Couldn't find kickoff tx in signed_txs".to_string(),
979            ))?;
980
981        // mark the kickoff connector as used
982        self.db
983            .mark_kickoff_connector_as_used(Some(dbtx), round_idx, kickoff_idx, Some(kickoff_txid))
984            .await?;
985
986        Ok(kickoff_txid)
987    }
988
989    #[cfg(feature = "automation")]
990    async fn start_first_round(
991        &self,
992        mut dbtx: DatabaseTransaction<'_>,
993        kickoff_wpks: KickoffWinternitzKeys,
994    ) -> Result<(), BridgeError> {
995        // try to send the first round tx
996        let (mut first_round_tx, _) = create_round_nth_txhandler(
997            self.signer.xonly_public_key,
998            self.collateral_funding_outpoint,
999            self.config.protocol_paramset().collateral_funding_amount,
1000            RoundIndex::Round(0),
1001            &kickoff_wpks,
1002            self.config.protocol_paramset(),
1003        )?;
1004
1005        self.signer
1006            .tx_sign_and_fill_sigs(&mut first_round_tx, &[], None)
1007            .wrap_err("Failed to sign first round tx")?;
1008
1009        self.tx_sender
1010            .insert_try_to_send(
1011                Some(&mut dbtx),
1012                Some(TxMetadata {
1013                    tx_type: TransactionType::Round,
1014                    operator_xonly_pk: None,
1015                    round_idx: Some(RoundIndex::Round(0)),
1016                    kickoff_idx: None,
1017                    deposit_outpoint: None,
1018                }),
1019                first_round_tx.get_cached_tx(),
1020                FeePayingType::CPFP,
1021                None,
1022                &[],
1023                &[],
1024                &[],
1025                &[],
1026            )
1027            .await?;
1028
1029        // update current round index to 1
1030        self.db
1031            .update_current_round_index(Some(dbtx), RoundIndex::Round(0))
1032            .await?;
1033
1034        Ok(())
1035    }
1036
1037    #[cfg(feature = "automation")]
1038    pub async fn end_round<'a>(
1039        &'a self,
1040        mut dbtx: DatabaseTransaction<'a>,
1041    ) -> Result<(), BridgeError> {
1042        // get current round index
1043        let current_round_index = self.db.get_current_round_index(Some(&mut dbtx)).await?;
1044
1045        let mut activation_prerequisites = Vec::new();
1046
1047        let operator_winternitz_public_keys = self
1048            .db
1049            .get_operator_kickoff_winternitz_public_keys(None, self.signer.xonly_public_key)
1050            .await?;
1051        let kickoff_wpks = KickoffWinternitzKeys::new(
1052            operator_winternitz_public_keys,
1053            self.config.protocol_paramset().num_kickoffs_per_round,
1054            self.config.protocol_paramset().num_round_txs,
1055        )?;
1056
1057        // if we are at round 0, which is just the collateral, we need to start the first round
1058        if current_round_index == RoundIndex::Collateral {
1059            return self.start_first_round(dbtx, kickoff_wpks).await;
1060        }
1061
1062        let (current_round_txhandler, mut ready_to_reimburse_txhandler) =
1063            create_round_nth_txhandler(
1064                self.signer.xonly_public_key,
1065                self.collateral_funding_outpoint,
1066                self.config.protocol_paramset().collateral_funding_amount,
1067                current_round_index,
1068                &kickoff_wpks,
1069                self.config.protocol_paramset(),
1070            )?;
1071
1072        let (mut next_round_txhandler, _) = create_round_nth_txhandler(
1073            self.signer.xonly_public_key,
1074            self.collateral_funding_outpoint,
1075            self.config.protocol_paramset().collateral_funding_amount,
1076            current_round_index.next_round(),
1077            &kickoff_wpks,
1078            self.config.protocol_paramset(),
1079        )?;
1080
1081        let mut tweak_cache = TweakCache::default();
1082
1083        // sign ready to reimburse tx
1084        self.signer
1085            .tx_sign_and_fill_sigs(
1086                &mut ready_to_reimburse_txhandler,
1087                &[],
1088                Some(&mut tweak_cache),
1089            )
1090            .wrap_err("Failed to sign ready to reimburse tx")?;
1091
1092        // sign next round tx
1093        self.signer
1094            .tx_sign_and_fill_sigs(&mut next_round_txhandler, &[], Some(&mut tweak_cache))
1095            .wrap_err("Failed to sign next round tx")?;
1096
1097        let current_round_txid = current_round_txhandler.get_cached_tx().compute_txid();
1098        let ready_to_reimburse_tx = ready_to_reimburse_txhandler.get_cached_tx();
1099        let next_round_tx = next_round_txhandler.get_cached_tx();
1100
1101        let ready_to_reimburse_txid = ready_to_reimburse_tx.compute_txid();
1102
1103        let mut unspent_kickoff_connector_indices = Vec::new();
1104
1105        // get kickoff txid for used kickoff connector
1106        for kickoff_connector_idx in
1107            0..self.config.protocol_paramset().num_kickoffs_per_round as u32
1108        {
1109            let kickoff_txid = self
1110                .db
1111                .get_kickoff_txid_for_used_kickoff_connector(
1112                    Some(&mut dbtx),
1113                    current_round_index,
1114                    kickoff_connector_idx,
1115                )
1116                .await?;
1117            match kickoff_txid {
1118                Some(kickoff_txid) => {
1119                    activation_prerequisites.push(ActivatedWithOutpoint {
1120                        outpoint: OutPoint {
1121                            txid: kickoff_txid,
1122                            vout: UtxoVout::KickoffFinalizer.get_vout(), // Kickoff finalizer output index
1123                        },
1124                        relative_block_height: self.config.protocol_paramset().finality_depth - 1,
1125                    });
1126                }
1127                None => {
1128                    let unspent_kickoff_connector = OutPoint {
1129                        txid: current_round_txid,
1130                        vout: UtxoVout::Kickoff(kickoff_connector_idx as usize).get_vout(),
1131                    };
1132                    unspent_kickoff_connector_indices.push(kickoff_connector_idx as usize);
1133                    self.db
1134                        .mark_kickoff_connector_as_used(
1135                            Some(&mut dbtx),
1136                            current_round_index,
1137                            kickoff_connector_idx,
1138                            None,
1139                        )
1140                        .await?;
1141                    activation_prerequisites.push(ActivatedWithOutpoint {
1142                        outpoint: unspent_kickoff_connector,
1143                        relative_block_height: self.config.protocol_paramset().finality_depth - 1,
1144                    });
1145                }
1146            }
1147        }
1148
1149        // Burn unused kickoff connectors
1150        let mut burn_unspent_kickoff_connectors_tx =
1151            create_burn_unused_kickoff_connectors_txhandler(
1152                &current_round_txhandler,
1153                &unspent_kickoff_connector_indices,
1154                &self.signer.address,
1155                self.config.protocol_paramset(),
1156            )?;
1157
1158        // sign burn unused kickoff connectors tx
1159        self.signer
1160            .tx_sign_and_fill_sigs(
1161                &mut burn_unspent_kickoff_connectors_tx,
1162                &[],
1163                Some(&mut tweak_cache),
1164            )
1165            .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
1166
1167        self.tx_sender
1168            .insert_try_to_send(
1169                Some(&mut dbtx),
1170                Some(TxMetadata {
1171                    tx_type: TransactionType::BurnUnusedKickoffConnectors,
1172                    operator_xonly_pk: Some(self.signer.xonly_public_key),
1173                    round_idx: Some(current_round_index),
1174                    kickoff_idx: None,
1175                    deposit_outpoint: None,
1176                }),
1177                burn_unspent_kickoff_connectors_tx.get_cached_tx(),
1178                FeePayingType::CPFP,
1179                None,
1180                &[],
1181                &[],
1182                &[],
1183                &[],
1184            )
1185            .await?;
1186
1187        // send ready to reimburse tx
1188        self.tx_sender
1189            .insert_try_to_send(
1190                Some(&mut dbtx),
1191                Some(TxMetadata {
1192                    tx_type: TransactionType::ReadyToReimburse,
1193                    operator_xonly_pk: Some(self.signer.xonly_public_key),
1194                    round_idx: Some(current_round_index),
1195                    kickoff_idx: None,
1196                    deposit_outpoint: None,
1197                }),
1198                ready_to_reimburse_tx,
1199                FeePayingType::CPFP,
1200                None,
1201                &[],
1202                &[],
1203                &[],
1204                &activation_prerequisites,
1205            )
1206            .await?;
1207
1208        // send next round tx
1209        self.tx_sender
1210            .insert_try_to_send(
1211                Some(&mut dbtx),
1212                Some(TxMetadata {
1213                    tx_type: TransactionType::Round,
1214                    operator_xonly_pk: Some(self.signer.xonly_public_key),
1215                    round_idx: Some(current_round_index.next_round()),
1216                    kickoff_idx: None,
1217                    deposit_outpoint: None,
1218                }),
1219                next_round_tx,
1220                FeePayingType::CPFP,
1221                None,
1222                &[],
1223                &[],
1224                &[ActivatedWithTxid {
1225                    txid: ready_to_reimburse_txid,
1226                    relative_block_height: self
1227                        .config
1228                        .protocol_paramset()
1229                        .operator_reimburse_timelock
1230                        as u32,
1231                }],
1232                &[],
1233            )
1234            .await?;
1235
1236        // update current round index
1237        self.db
1238            .update_current_round_index(Some(dbtx), current_round_index.next_round())
1239            .await?;
1240
1241        Ok(())
1242    }
1243
1244    #[cfg(feature = "automation")]
1245    async fn send_asserts(
1246        &self,
1247        mut dbtx: DatabaseTransaction<'_>,
1248        kickoff_data: KickoffData,
1249        deposit_data: DepositData,
1250        watchtower_challenges: HashMap<usize, Transaction>,
1251        _payout_blockhash: Witness,
1252        latest_blockhash: Witness,
1253    ) -> Result<(), BridgeError> {
1254        use bridge_circuit_host::utils::{get_verifying_key, is_dev_mode};
1255        use citrea_sov_rollup_interface::zk::light_client_proof::output::LightClientCircuitOutput;
1256
1257        let context = ContractContext::new_context_for_kickoff(
1258            kickoff_data,
1259            deposit_data.clone(),
1260            self.config.protocol_paramset(),
1261        );
1262        let mut db_cache = crate::builder::transaction::ReimburseDbCache::from_context(
1263            self.db.clone(),
1264            &context,
1265            Some(&mut dbtx),
1266        );
1267        let txhandlers = builder::transaction::create_txhandlers(
1268            TransactionType::Kickoff,
1269            context,
1270            &mut crate::builder::transaction::TxHandlerCache::new(),
1271            &mut db_cache,
1272        )
1273        .await?;
1274        let move_txid = txhandlers
1275            .get(&TransactionType::MoveToVault)
1276            .ok_or(eyre::eyre!(
1277                "Move to vault txhandler not found in send_asserts"
1278            ))?
1279            .get_cached_tx()
1280            .compute_txid();
1281        let kickoff_tx = txhandlers
1282            .get(&TransactionType::Kickoff)
1283            .ok_or(eyre::eyre!("Kickoff txhandler not found in send_asserts"))?
1284            .get_cached_tx();
1285
1286        #[cfg(test)]
1287        self.config
1288            .test_params
1289            .maybe_save_kickoff_and_wtc_txs(kickoff_tx, &watchtower_challenges, 1, &self.rpc)
1290            .await?;
1291
1292        let (payout_op_xonly_pk_opt, payout_block_hash, payout_txid, deposit_idx) = self
1293            .db
1294            .get_payout_info_from_move_txid(Some(&mut dbtx), move_txid)
1295            .await
1296            .wrap_err("Failed to get payout info from db during sending asserts.")?
1297            .ok_or_eyre(format!(
1298                "Payout info not found in db while sending asserts for move txid: {move_txid}"
1299            ))?;
1300
1301        let payout_op_xonly_pk = payout_op_xonly_pk_opt.ok_or_eyre(format!(
1302            "Payout operator xonly pk not found in payout info DB while sending asserts for deposit move txid: {move_txid}"
1303        ))?;
1304
1305        tracing::info!("Sending asserts for deposit_idx: {deposit_idx:?}");
1306
1307        if payout_op_xonly_pk != kickoff_data.operator_xonly_pk {
1308            return Err(eyre::eyre!(
1309                "Payout operator xonly pk does not match kickoff operator xonly pk in send_asserts"
1310            )
1311            .into());
1312        }
1313
1314        let (payout_block_height, payout_block) = self
1315            .db
1316            .get_full_block_from_hash(Some(&mut dbtx), payout_block_hash)
1317            .await?
1318            .ok_or_eyre(format!(
1319                "Payout block {payout_op_xonly_pk:?} {payout_block_hash:?} not found in db",
1320            ))?;
1321
1322        let payout_tx_index = payout_block
1323            .txdata
1324            .iter()
1325            .position(|tx| tx.compute_txid() == payout_txid)
1326            .ok_or_eyre(format!(
1327                "Payout txid {payout_txid:?} not found in block {payout_op_xonly_pk:?} {payout_block_hash:?}"
1328            ))?;
1329        let payout_tx = &payout_block.txdata[payout_tx_index];
1330        tracing::debug!("Calculated payout tx in send_asserts: {:?}", payout_tx);
1331
1332        let lcp_receipt = self
1333            .citrea_client
1334            .fetch_validate_and_store_lcp(
1335                payout_block_height as u64,
1336                deposit_idx as u32,
1337                &self.db,
1338                Some(&mut dbtx),
1339                self.config.protocol_paramset(),
1340            )
1341            .await?;
1342        let proof_output: LightClientCircuitOutput = borsh::from_slice(&lcp_receipt.journal.bytes)
1343            .wrap_err("Failed to deserialize light client circuit output")?;
1344        let l2_height = proof_output.last_l2_height;
1345        let light_client_proof = LightClientProof {
1346            lc_journal: lcp_receipt.journal.bytes.clone(),
1347        };
1348
1349        tracing::info!("Got light client proof in send_asserts");
1350
1351        let storage_proof = self
1352            .citrea_client
1353            .get_storage_proof(l2_height, deposit_idx as u32)
1354            .await
1355            .wrap_err(format!(
1356                "Failed to get storage proof for move txid {move_txid:?}, l2 height {l2_height}, deposit_idx {deposit_idx}",
1357            ))?;
1358
1359        tracing::debug!("Got storage proof in send_asserts {storage_proof:?}");
1360
1361        // get committed latest blockhash
1362        let wt_derive_path = ClementineBitVMPublicKeys::get_latest_blockhash_derivation(
1363            deposit_data.get_deposit_outpoint(),
1364            self.config.protocol_paramset(),
1365        );
1366        let commits = extract_winternitz_commits(
1367            latest_blockhash,
1368            &[wt_derive_path],
1369            self.config.protocol_paramset(),
1370        )?;
1371
1372        let latest_blockhash_last_20: [u8; 20] = commits
1373            .first()
1374            .ok_or_eyre("Failed to get latest blockhash in send_asserts")?
1375            .to_owned()
1376            .try_into()
1377            .map_err(|_| eyre::eyre!("Committed latest blockhash is not 20 bytes long"))?;
1378
1379        #[cfg(test)]
1380        let latest_blockhash_last_20 = self
1381            .config
1382            .test_params
1383            .maybe_disrupt_latest_block_hash_commit(latest_blockhash_last_20);
1384
1385        let rpc_current_finalized_height = self
1386            .rpc
1387            .get_current_chain_height()
1388            .await?
1389            .saturating_sub(self.config.protocol_paramset().finality_depth - 1);
1390
1391        // update headers in case the sync (state machine handle_finalized_block) is behind
1392        self.db
1393            .fetch_and_save_missing_blocks(
1394                Some(&mut dbtx),
1395                &self.rpc,
1396                self.config.protocol_paramset().genesis_height,
1397                rpc_current_finalized_height + 1,
1398            )
1399            .await?;
1400
1401        let current_height = self
1402            .db
1403            .get_latest_finalized_block_height(Some(&mut dbtx))
1404            .await?
1405            .ok_or_eyre("Failed to get current finalized block height")?;
1406
1407        let block_hashes = self
1408            .db
1409            .get_block_info_from_range(
1410                Some(&mut dbtx),
1411                self.config.protocol_paramset().genesis_height as u64,
1412                current_height,
1413            )
1414            .await?;
1415
1416        // find out which blockhash is latest_blockhash (only last 20 bytes is committed to Witness)
1417        let latest_blockhash_index = block_hashes
1418            .iter()
1419            .position(|(block_hash, _)| {
1420                block_hash.as_byte_array().last_20_bytes() == latest_blockhash_last_20
1421            })
1422            .ok_or_eyre("Failed to find latest blockhash in send_asserts")?;
1423
1424        let latest_blockhash = block_hashes[latest_blockhash_index].0;
1425
1426        let (current_hcp, _hcp_height) = self
1427            .header_chain_prover
1428            .prove_till_hash(latest_blockhash)
1429            .await?;
1430
1431        #[cfg(test)]
1432        let mut total_works: Vec<[u8; 16]> = Vec::with_capacity(watchtower_challenges.len());
1433
1434        #[cfg(test)]
1435        {
1436            use bridge_circuit_host::utils::total_work_from_wt_tx;
1437            for (_, tx) in watchtower_challenges.iter() {
1438                let total_work = total_work_from_wt_tx(tx);
1439                total_works.push(total_work);
1440            }
1441            tracing::debug!("Total works: {:?}", total_works);
1442        }
1443
1444        #[cfg(test)]
1445        let current_hcp = self
1446            .config
1447            .test_params
1448            .maybe_override_current_hcp(
1449                current_hcp,
1450                payout_block_hash,
1451                &block_hashes,
1452                &self.header_chain_prover,
1453                total_works.clone(),
1454            )
1455            .await?;
1456
1457        tracing::info!("Got header chain proof in send_asserts");
1458
1459        let blockhashes_serialized: Vec<[u8; 32]> = block_hashes
1460            .iter()
1461            .take(latest_blockhash_index + 1)
1462            .map(|(h, _)| h.to_byte_array())
1463            .collect();
1464
1465        #[cfg(test)]
1466        let blockhashes_serialized = self
1467            .config
1468            .test_params
1469            .maybe_override_blockhashes_serialized(
1470                blockhashes_serialized,
1471                payout_block_height,
1472                self.config.protocol_paramset().genesis_height,
1473                total_works,
1474            );
1475
1476        tracing::debug!(
1477            "Genesis height - Before SPV: {},",
1478            self.config.protocol_paramset().genesis_height
1479        );
1480
1481        let spv = create_spv(
1482            payout_tx.clone(),
1483            &blockhashes_serialized,
1484            payout_block.clone(),
1485            payout_block_height,
1486            self.config.protocol_paramset().genesis_height,
1487            payout_tx_index as u32,
1488        )?;
1489        tracing::info!("Calculated spv proof in send_asserts");
1490
1491        let mut wt_contexts = Vec::new();
1492        for (_, tx) in watchtower_challenges.iter() {
1493            wt_contexts.push(WatchtowerContext {
1494                watchtower_tx: tx.clone(),
1495                prevout_txs: self.rpc.get_prevout_txs(tx).await?,
1496            });
1497        }
1498
1499        #[cfg(test)]
1500        {
1501            if self.config.test_params.operator_forgot_watchtower_challenge {
1502                tracing::info!("Disrupting watchtower challenges in send_asserts");
1503                wt_contexts.pop();
1504            }
1505        }
1506
1507        let watchtower_challenge_connector_start_idx =
1508            (FIRST_FIVE_OUTPUTS + ClementineBitVMPublicKeys::number_of_assert_txs()) as u32;
1509
1510        let bridge_circuit_host_params = BridgeCircuitHostParams::new_with_wt_tx(
1511            kickoff_tx.clone(),
1512            spv,
1513            current_hcp,
1514            light_client_proof,
1515            lcp_receipt,
1516            storage_proof,
1517            self.config.protocol_paramset().network,
1518            &wt_contexts,
1519            watchtower_challenge_connector_start_idx,
1520        )
1521        .wrap_err("Failed to create bridge circuit host params in send_asserts")?;
1522
1523        let bridge_circuit_elf = match self.config.protocol_paramset().network {
1524            bitcoin::Network::Bitcoin => MAINNET_BRIDGE_CIRCUIT_ELF,
1525            bitcoin::Network::Testnet4 => {
1526                if is_dev_mode() {
1527                    TESTNET4_BRIDGE_CIRCUIT_ELF_TEST
1528                } else {
1529                    TESTNET4_BRIDGE_CIRCUIT_ELF
1530                }
1531            }
1532            bitcoin::Network::Signet => {
1533                if is_dev_mode() {
1534                    SIGNET_BRIDGE_CIRCUIT_ELF_TEST
1535                } else {
1536                    SIGNET_BRIDGE_CIRCUIT_ELF
1537                }
1538            }
1539            bitcoin::Network::Regtest => {
1540                if is_dev_mode() {
1541                    REGTEST_BRIDGE_CIRCUIT_ELF_TEST
1542                } else {
1543                    REGTEST_BRIDGE_CIRCUIT_ELF
1544                }
1545            }
1546            _ => {
1547                return Err(eyre::eyre!(
1548                    "Unsupported network {:?} in send_asserts",
1549                    self.config.protocol_paramset().network
1550                )
1551                .into())
1552            }
1553        };
1554        tracing::info!("Starting proving bridge circuit to send asserts");
1555
1556        #[cfg(test)]
1557        self.config
1558            .test_params
1559            .maybe_dump_bridge_circuit_params_to_file(&bridge_circuit_host_params)?;
1560
1561        #[cfg(test)]
1562        self.config
1563            .test_params
1564            .maybe_dump_bridge_circuit_params_to_file(&bridge_circuit_host_params)?;
1565
1566        let (g16_proof, g16_output, public_inputs) = tokio::task::spawn_blocking(move || {
1567            prove_bridge_circuit(bridge_circuit_host_params, bridge_circuit_elf)
1568        })
1569        .await
1570        .wrap_err("Failed to join the prove_bridge_circuit task")?
1571        .wrap_err("Failed to prove bridge circuit")?;
1572
1573        tracing::info!("Proved bridge circuit in send_asserts");
1574        let public_input_scalar = ark_bn254::Fr::from_be_bytes_mod_order(&g16_output);
1575
1576        #[cfg(test)]
1577        let mut public_inputs = public_inputs;
1578
1579        #[cfg(test)]
1580        {
1581            if self
1582                .config
1583                .test_params
1584                .disrupt_challenge_sending_watchtowers_commit
1585            {
1586                tracing::info!("Disrupting challenge sending watchtowers commit in send_asserts");
1587                public_inputs.challenge_sending_watchtowers[0] ^= 0x01;
1588                tracing::info!(
1589                    "Disrupted challenge sending watchtowers commit: {:?}",
1590                    public_inputs.challenge_sending_watchtowers
1591                );
1592            }
1593        }
1594
1595        tracing::info!(
1596            "Challenge sending watchtowers commit: {:?}",
1597            public_inputs.challenge_sending_watchtowers
1598        );
1599
1600        let asserts = tokio::task::spawn_blocking(move || {
1601            let vk = get_verifying_key();
1602
1603            generate_assertions(g16_proof, vec![public_input_scalar], &vk).map_err(|e| {
1604                eyre::eyre!(
1605                    "Failed to generate {}assertions: {}",
1606                    if is_dev_mode() { "dev mode " } else { "" },
1607                    e
1608                )
1609            })
1610        })
1611        .await
1612        .wrap_err("Generate assertions thread failed with error")??;
1613
1614        tracing::info!("Generated assertions in send_asserts");
1615
1616        #[cfg(test)]
1617        let asserts = self.config.test_params.maybe_corrupt_asserts(asserts);
1618
1619        tracing::debug!(target: "ci", "Assert commitment data: {:?}", asserts);
1620
1621        let assert_txs = self
1622            .create_assert_commitment_txs(
1623                TransactionRequestData {
1624                    kickoff_data,
1625                    deposit_outpoint: deposit_data.get_deposit_outpoint(),
1626                },
1627                ClementineBitVMPublicKeys::get_assert_commit_data(
1628                    asserts,
1629                    &public_inputs.challenge_sending_watchtowers,
1630                ),
1631                Some(&mut dbtx),
1632            )
1633            .await?;
1634
1635        for (tx_type, tx) in assert_txs {
1636            self.tx_sender
1637                .add_tx_to_queue(
1638                    Some(&mut dbtx),
1639                    tx_type,
1640                    &tx,
1641                    &[],
1642                    Some(TxMetadata {
1643                        tx_type,
1644                        operator_xonly_pk: Some(self.signer.xonly_public_key),
1645                        round_idx: Some(kickoff_data.round_idx),
1646                        kickoff_idx: Some(kickoff_data.kickoff_idx),
1647                        deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
1648                    }),
1649                    self.config.protocol_paramset(),
1650                    None,
1651                )
1652                .await?;
1653        }
1654        Ok(())
1655    }
1656
1657    fn data(&self) -> OperatorData {
1658        OperatorData {
1659            xonly_pk: self.signer.xonly_public_key,
1660            collateral_funding_outpoint: self.collateral_funding_outpoint,
1661            reimburse_addr: self.reimburse_addr.clone(),
1662        }
1663    }
1664
1665    #[cfg(feature = "automation")]
1666    async fn send_latest_blockhash(
1667        &self,
1668        mut dbtx: DatabaseTransaction<'_>,
1669        kickoff_data: KickoffData,
1670        deposit_data: DepositData,
1671        latest_blockhash: BlockHash,
1672    ) -> Result<(), BridgeError> {
1673        tracing::info!("Operator sending latest blockhash");
1674        let deposit_outpoint = deposit_data.get_deposit_outpoint();
1675        let (tx_type, tx) = self
1676            .create_latest_blockhash_tx(
1677                TransactionRequestData {
1678                    deposit_outpoint,
1679                    kickoff_data,
1680                },
1681                latest_blockhash,
1682                Some(&mut dbtx),
1683            )
1684            .await?;
1685        if tx_type != TransactionType::LatestBlockhash {
1686            return Err(eyre::eyre!("Latest blockhash tx type is not LatestBlockhash").into());
1687        }
1688        self.tx_sender
1689            .add_tx_to_queue(
1690                Some(dbtx),
1691                tx_type,
1692                &tx,
1693                &[],
1694                Some(TxMetadata {
1695                    tx_type,
1696                    operator_xonly_pk: Some(self.signer.xonly_public_key),
1697                    round_idx: Some(kickoff_data.round_idx),
1698                    kickoff_idx: Some(kickoff_data.kickoff_idx),
1699                    deposit_outpoint: Some(deposit_outpoint),
1700                }),
1701                self.config.protocol_paramset(),
1702                None,
1703            )
1704            .await?;
1705        Ok(())
1706    }
1707
1708    /// For a deposit_id checks that the payer for that deposit is the operator, and the payout blockhash and kickoff txid are set.
1709    async fn validate_payer_is_operator(
1710        &self,
1711        dbtx: Option<DatabaseTransaction<'_>>,
1712        deposit_id: u32,
1713    ) -> Result<(BlockHash, Txid), BridgeError> {
1714        let (payer_xonly_pk, payout_blockhash, kickoff_txid) = self
1715            .db
1716            .get_payer_xonly_pk_blockhash_and_kickoff_txid_from_deposit_id(dbtx, deposit_id)
1717            .await?;
1718
1719        tracing::info!(
1720            "Payer xonly pk and kickoff txid found for the requested deposit, payer xonly pk: {:?}, kickoff txid: {:?}",
1721            payer_xonly_pk,
1722            kickoff_txid
1723        );
1724
1725        // first check if the payer is the operator, and the kickoff is handled
1726        // by the PayoutCheckerTask, meaning kickoff_txid is set
1727        let (payout_blockhash, kickoff_txid) = match (
1728            payer_xonly_pk,
1729            payout_blockhash,
1730            kickoff_txid,
1731        ) {
1732            (Some(payer_xonly_pk), Some(payout_blockhash), Some(kickoff_txid)) => {
1733                if payer_xonly_pk != self.signer.xonly_public_key {
1734                    return Err(eyre::eyre!(
1735                        "Payer is not own operator for deposit, payer xonly pk: {:?}, operator xonly pk: {:?}",
1736                        payer_xonly_pk,
1737                        self.signer.xonly_public_key
1738                    )
1739                    .into());
1740                }
1741                (payout_blockhash, kickoff_txid)
1742            }
1743            _ => {
1744                return Err(eyre::eyre!(
1745                    "Payer info not found for deposit, payout blockhash: {:?}, kickoff txid: {:?}",
1746                    payout_blockhash,
1747                    kickoff_txid
1748                )
1749                .into());
1750            }
1751        };
1752
1753        tracing::info!(
1754            "Payer xonly pk, payout blockhash and kickoff txid found and valid for own operator for the requested deposit id: {}, payer xonly pk: {:?}, payout blockhash: {:?}, kickoff txid: {:?}",
1755            deposit_id,
1756            payer_xonly_pk,
1757            payout_blockhash,
1758            kickoff_txid
1759        );
1760
1761        Ok((payout_blockhash, kickoff_txid))
1762    }
1763
1764    async fn get_next_txs_to_send(
1765        &self,
1766        mut dbtx: Option<DatabaseTransaction<'_>>,
1767        deposit_data: &mut DepositData,
1768        payout_blockhash: BlockHash,
1769        kickoff_txid: Txid,
1770        current_round_idx: RoundIndex,
1771    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
1772        let mut txs_to_send = Vec::new();
1773
1774        // get used kickoff connector for the kickoff txid
1775        let (kickoff_round_idx, kickoff_connector_idx) = self
1776            .db
1777            .get_kickoff_connector_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
1778            .await?;
1779
1780        let context = ContractContext::new_context_for_kickoff(
1781            KickoffData {
1782                operator_xonly_pk: self.signer.xonly_public_key,
1783                round_idx: kickoff_round_idx,
1784                kickoff_idx: kickoff_connector_idx,
1785            },
1786            deposit_data.clone(),
1787            self.config.protocol_paramset(),
1788        );
1789
1790        // get txs for the kickoff
1791        let kickoff_txs = create_and_sign_txs(
1792            self.db.clone(),
1793            &self.signer,
1794            self.config.clone(),
1795            context,
1796            Some(payout_blockhash.to_byte_array().last_20_bytes()),
1797            dbtx.as_deref_mut(),
1798        )
1799        .await?;
1800
1801        // check the current round status compared to the round of the assigned kickoff tx
1802        match current_round_idx
1803            .to_index()
1804            .cmp(&kickoff_round_idx.to_index())
1805        {
1806            std::cmp::Ordering::Less => {
1807                // We need to advance the round manually to be able to start the kickoff
1808                tracing::info!("We need to advance the round manually to be able to start the kickoff, current round idx: {:?}, kickoff round idx: {:?}", current_round_idx, kickoff_round_idx);
1809                let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1810                txs_to_send.extend(txs);
1811            }
1812            std::cmp::Ordering::Greater => {
1813                tracing::info!("We are at least on the next round, meaning we can get the reimbursement as reimbursement utxos are in the next round, current round idx: {:?}, kickoff round idx: {:?}", current_round_idx, kickoff_round_idx);
1814                // we are at least on the next round, meaning we can get the reimbursement as reimbursement utxos are in the next round
1815                let reimbursement_tx = kickoff_txs
1816                    .iter()
1817                    .find(|(tx_type, _)| tx_type == &TransactionType::Reimburse)
1818                    .ok_or(eyre::eyre!("Reimburse tx not found in kickoff txs"))?;
1819                txs_to_send.push(reimbursement_tx.clone());
1820            }
1821            std::cmp::Ordering::Equal => {
1822                // first check if the kickoff is in chain
1823                if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
1824                    tracing::info!(
1825                        "Kickoff tx is not on chain, can send it, kickoff txid: {:?}",
1826                        kickoff_txid
1827                    );
1828                    let kickoff_tx = kickoff_txs
1829                        .iter()
1830                        .find(|(tx_type, _)| tx_type == &TransactionType::Kickoff)
1831                        .ok_or(eyre::eyre!("Kickoff tx not found in kickoff txs"))?;
1832
1833                    // fetch and save the LCP for if we get challenged and need to provide proof of payout later
1834                    let (_, payout_block_height) = self
1835                        .db
1836                        .get_block_info_from_hash(dbtx.as_deref_mut(), payout_blockhash)
1837                        .await?
1838                        .ok_or_eyre("Couldn't find payout blockhash in bitcoin sync")?;
1839
1840                    let move_txid = deposit_data.get_move_txid(self.config.protocol_paramset())?;
1841
1842                    let (_, _, _, citrea_idx) = self
1843                        .db
1844                        .get_payout_info_from_move_txid(dbtx.as_deref_mut(), move_txid)
1845                        .await?
1846                        .ok_or_eyre("Couldn't find payout info from move txid")?;
1847
1848                    let _ = self
1849                        .citrea_client
1850                        .fetch_validate_and_store_lcp(
1851                            payout_block_height as u64,
1852                            citrea_idx as u32,
1853                            &self.db,
1854                            dbtx.as_deref_mut(),
1855                            self.config.protocol_paramset(),
1856                        )
1857                        .await?;
1858
1859                    // sanity check
1860                    if kickoff_tx.1.compute_txid() != kickoff_txid {
1861                        return Err(eyre::eyre!("Kickoff txid mismatch for deposit outpoint: {}, kickoff txid: {:?}, computed txid: {:?}",
1862                        deposit_data.get_deposit_outpoint(), kickoff_txid, kickoff_tx.1.compute_txid()).into());
1863                    }
1864                    txs_to_send.push(kickoff_tx.clone());
1865                }
1866                // kickoff tx is on chain, check if kickoff finalizer is spent
1867                else if !self
1868                    .rpc
1869                    .is_utxo_spent(&OutPoint {
1870                        txid: kickoff_txid,
1871                        vout: UtxoVout::KickoffFinalizer.get_vout(),
1872                    })
1873                    .await?
1874                {
1875                    // kickoff finalizer is not spent, we need to send challenge timeout
1876                    tracing::info!(
1877                        "Kickoff finalizer is not spent, can send challenge timeout, kickoff txid: {:?}",
1878                        kickoff_txid
1879                    );
1880                    // first check if challenge tx was sent, then we need automation enabled to be able to answer the challenge
1881                    if self
1882                        .rpc
1883                        .is_utxo_spent(&OutPoint {
1884                            txid: kickoff_txid,
1885                            vout: UtxoVout::Challenge.get_vout(),
1886                        })
1887                        .await?
1888                    {
1889                        // challenge tx was sent, we need automation enabled to be able to answer the challenge
1890                        tracing::warn!(
1891                            "Challenge tx was sent for deposit outpoint: {:?}, but automation is not enabled, enable automation!",
1892                            deposit_data.get_deposit_outpoint()
1893                        );
1894                        return Err(eyre::eyre!("WARNING: Challenge tx was sent to kickoff connector {:?}, but automation is not enabled, enable automation!", kickoff_txid).into());
1895                    }
1896                    let challenge_timeout_tx = kickoff_txs
1897                        .iter()
1898                        .find(|(tx_type, _)| tx_type == &TransactionType::ChallengeTimeout)
1899                        .ok_or(eyre::eyre!("Challenge timeout tx not found in kickoff txs"))?;
1900                    txs_to_send.push(challenge_timeout_tx.clone());
1901                } else {
1902                    // if kickoff finalizer is spent, it is time to get the reimbursement
1903                    tracing::info!(
1904                        "Kickoff finalizer is spent, can advance the round manually to get the reimbursement, current round idx: {:?}, kickoff round idx: {:?}",
1905                        current_round_idx,
1906                        kickoff_round_idx
1907                    );
1908                    let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1909                    txs_to_send.extend(txs);
1910                }
1911            }
1912        }
1913        Ok(txs_to_send)
1914    }
1915
1916    /// Transfers the given outpoints to the operator's btc wallet address.
1917    /// The outpoints must belong to the operator's taproot address (xonly key, no merkle root).
1918    /// The function also checks if any outpoint is the collateral of the operator, and returns an error if so.
1919    /// # Arguments
1920    /// - inputs: A vector of tuples, each containing an outpoint and the corresponding txout.
1921    /// # Returns
1922    /// - The signed transaction that sends the given outpoints to the operator's btc wallet address.
1923    pub async fn transfer_outpoints_to_wallet(
1924        &self,
1925        inputs: Vec<(OutPoint, TxOut)>,
1926    ) -> Result<Transaction, BridgeError> {
1927        if inputs.is_empty() {
1928            return Err(eyre!("No outpoints provided for transfer").into());
1929        }
1930
1931        // check if any outpoint is a collateral outpoint
1932        let collateral_outpoints = self
1933            .get_all_collateral_outpoints()
1934            .await
1935            .wrap_err("Failed to get all collateral outpoints")?;
1936        for (outpoint, _) in inputs.iter() {
1937            if collateral_outpoints.contains_key(outpoint) {
1938                let (round_idx, tx_type) = collateral_outpoints
1939                    .get(outpoint)
1940                    .expect("Collateral outpoint should be found in the map");
1941                return Err(
1942                    eyre!("Cannot transfer collateral outpoint {outpoint} belonging to {round_idx:?} {tx_type:?} to wallet").into(),
1943                );
1944            }
1945        }
1946
1947        let destination_script = self
1948            .rpc
1949            .get_new_address(None, Some(AddressType::Bech32m))
1950            .await
1951            .wrap_err("Failed to get new wallet address for transfer")?
1952            .require_network(self.config.protocol_paramset().network)
1953            .wrap_err("Failed to get new address, bitcoin rpc might not match the network")?
1954            .script_pubkey();
1955
1956        let (_, spendinfo) = create_taproot_address(
1957            &[],
1958            Some(self.signer.xonly_public_key),
1959            self.config.protocol_paramset().network,
1960        );
1961
1962        let total_input_value = inputs.iter().try_fold(Amount::ZERO, |acc, (_, txout)| {
1963            acc.checked_add(txout.value)
1964                .ok_or_else(|| eyre!("Input values overflowed while summing"))
1965        })?;
1966
1967        let mut output_txout = TxOut {
1968            value: total_input_value,
1969            script_pubkey: destination_script,
1970        };
1971
1972        let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
1973            .with_version(bitcoin::transaction::Version::TWO);
1974
1975        for (outpoint, txout) in inputs.iter() {
1976            builder = builder.add_input(
1977                NormalSignatureKind::OperatorSighashDefault,
1978                SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
1979                SpendPath::KeySpend,
1980                DEFAULT_SEQUENCE,
1981            );
1982        }
1983
1984        builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
1985
1986        let mut txhandler = builder.finalize();
1987
1988        let fee_rate = self
1989            .rpc
1990            .get_fee_rate(
1991                self.config.protocol_paramset().network,
1992                &self.config.mempool_api_host,
1993                &self.config.mempool_api_endpoint,
1994                self.config.tx_sender_limits.mempool_fee_rate_multiplier,
1995                self.config.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
1996                self.config.tx_sender_limits.fee_rate_hard_cap,
1997            )
1998            .await
1999            .wrap_err("Failed to get fee rate for transfer to wallet tx")?;
2000
2001        // Sign to account for witness weight when calculating fees.
2002        self.signer
2003            .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
2004
2005        let tx_weight_wu = txhandler.get_cached_tx().weight().to_wu();
2006        let fee_sat = (fee_rate.to_sat_per_kwu() * tx_weight_wu).div_ceil(1000);
2007        let fee = Amount::from_sat(fee_sat);
2008
2009        output_txout.value = output_txout
2010            .value
2011            .checked_sub(fee)
2012            .ok_or_else(|| eyre!("Calculated fee exceeds total input value"))?;
2013
2014        let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
2015            .with_version(bitcoin::transaction::Version::TWO);
2016
2017        for (outpoint, txout) in inputs.iter() {
2018            builder = builder.add_input(
2019                NormalSignatureKind::OperatorSighashDefault,
2020                SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
2021                SpendPath::KeySpend,
2022                DEFAULT_SEQUENCE,
2023            );
2024        }
2025
2026        builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
2027
2028        let mut txhandler = builder.finalize();
2029
2030        self.signer
2031            .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
2032
2033        let signed_tx = txhandler.get_cached_tx().clone();
2034
2035        self.rpc
2036            .send_raw_transaction(&signed_tx)
2037            .await
2038            .wrap_err("Failed to send from operator's address to btc wallet address")?;
2039
2040        Ok(signed_tx)
2041    }
2042
2043    /// Gets all collateral outpoints for the operator.
2044    /// Returns a map of outpoint to the round index it belongs to.
2045    async fn get_all_collateral_outpoints(
2046        &self,
2047    ) -> Result<HashMap<OutPoint, (RoundIndex, TransactionType)>, BridgeError> {
2048        let mut outpoints = HashMap::new();
2049        outpoints.insert(
2050            self.collateral_funding_outpoint,
2051            (RoundIndex::Collateral, TransactionType::Round),
2052        );
2053
2054        // Fetch operator kickoff winternitz public keys to build round txs
2055        let operator_winternitz_public_keys = self
2056            .db
2057            .get_operator_kickoff_winternitz_public_keys(None, self.signer.xonly_public_key)
2058            .await?;
2059        let kickoff_wpks = KickoffWinternitzKeys::new(
2060            operator_winternitz_public_keys,
2061            self.config.protocol_paramset().num_kickoffs_per_round,
2062            self.config.protocol_paramset().num_round_txs,
2063        )?;
2064        let operator_data = self.data();
2065
2066        let mut prev_ready_to_reimburse: Option<TxHandler> = None;
2067
2068        // Collect collateral outpoints for each round
2069        for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
2070            let txhandlers = create_round_txhandlers(
2071                self.config.protocol_paramset(),
2072                round_idx,
2073                &operator_data,
2074                &kickoff_wpks,
2075                prev_ready_to_reimburse.as_ref(),
2076            )?;
2077
2078            let round_tx = txhandlers
2079                .iter()
2080                .find(|txhandler| txhandler.get_transaction_type() == TransactionType::Round)
2081                .ok_or(eyre::eyre!("Round tx not found in txhandlers"))?;
2082            let collateral_outpoint = OutPoint {
2083                txid: *round_tx.get_txid(),
2084                vout: UtxoVout::CollateralInRound.get_vout(),
2085            };
2086            outpoints.insert(collateral_outpoint, (round_idx, TransactionType::Round));
2087
2088            let ready_to_reimburse_tx = txhandlers
2089                .iter()
2090                .find(|txhandler| {
2091                    txhandler.get_transaction_type() == TransactionType::ReadyToReimburse
2092                })
2093                .ok_or(eyre::eyre!("Ready to reimburse tx not found in txhandlers"))?;
2094            let ready_to_reimburse_collateral_outpoint = OutPoint {
2095                txid: *ready_to_reimburse_tx.get_txid(),
2096                vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
2097            };
2098            outpoints.insert(
2099                ready_to_reimburse_collateral_outpoint,
2100                (round_idx, TransactionType::ReadyToReimburse),
2101            );
2102            prev_ready_to_reimburse = Some(ready_to_reimburse_tx.clone());
2103        }
2104
2105        Ok(outpoints)
2106    }
2107
2108    /// For a given deposit outpoint, get the txs that are needed to reimburse the deposit.
2109    /// To avoid operator getting slashed, this function only returns the next tx that needs to be sent
2110    /// This fn can track and enable sending of these transactions during a normal reimbursement process.
2111    ///
2112    /// - First, if the current round is less than the round of the kickoff assigned to the deposit by PayoutCheckerTask, it returns the Round TX.
2113    /// - After Round tx is sent, it returns the Kickoff tx.
2114    /// - After Kickoff tx is sent, it returns the challenge timeout tx.
2115    /// - After challenge timeout tx is sent, it returns BurnUnusedKickoffConnectors tx. If challenge timeout tx is not sent, and but challenge utxo was spent, it means the kickoff was challenged, thus the fn returns an error as it cannot handle the challenge process. Automation is required to answer the challenge.
2116    /// - After all kickoff utxos are spent, and for any live kickoff, all kickoff finalizers are spent, it returns the ReadyToReimburse tx.
2117    /// - After ReadyToReimburse tx is sent, it returns the next Round tx to generate reimbursement utxos.
2118    /// - Finally, after the next round tx is sent, it returns the Reimburse tx.
2119    pub async fn get_reimbursement_txs(
2120        &self,
2121        deposit_outpoint: OutPoint,
2122    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2123        let mut dbtx = self.db.begin_transaction().await?;
2124        // first check if the deposit is in the database
2125        let (deposit_id, mut deposit_data) = self
2126            .db
2127            .get_deposit_data(Some(&mut dbtx), deposit_outpoint)
2128            .await?
2129            .ok_or_eyre(format!(
2130                "Deposit data not found for the requested deposit outpoint: {deposit_outpoint:?}, make sure you send the deposit outpoint, not the move txid."
2131            ))?;
2132
2133        tracing::info!(
2134            "Deposit data found for the requested deposit outpoint: {deposit_outpoint:?}, deposit id: {deposit_id:?}",
2135        );
2136
2137        // validate payer is operator and get payer xonly pk, payout blockhash and kickoff txid
2138        let (payout_blockhash, kickoff_txid) = self
2139            .validate_payer_is_operator(Some(&mut dbtx), deposit_id)
2140            .await?;
2141
2142        let mut current_round_idx = self.db.get_current_round_index(Some(&mut dbtx)).await?;
2143
2144        let mut txs_to_send: Vec<(TransactionType, Transaction)>;
2145
2146        loop {
2147            txs_to_send = self
2148                .get_next_txs_to_send(
2149                    Some(&mut dbtx),
2150                    &mut deposit_data,
2151                    payout_blockhash,
2152                    kickoff_txid,
2153                    current_round_idx,
2154                )
2155                .await?;
2156            if txs_to_send.is_empty() {
2157                // if no txs were returned, and we advanced the round in the db, ask for the next txs again
2158                // with the new round index
2159                let round_idx_after_operations =
2160                    self.db.get_current_round_index(Some(&mut dbtx)).await?;
2161                if round_idx_after_operations != current_round_idx {
2162                    current_round_idx = round_idx_after_operations;
2163                    continue;
2164                }
2165            }
2166            break;
2167        }
2168
2169        dbtx.commit().await?;
2170        Ok(txs_to_send)
2171    }
2172
2173    /// Checks the current round status, and returns the next txs that are safe to send to be
2174    /// able to advance to the next round.
2175    async fn advance_round_manually(
2176        &self,
2177        mut dbtx: Option<DatabaseTransaction<'_>>,
2178        round_idx: RoundIndex,
2179    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2180        if round_idx == RoundIndex::Collateral {
2181            // if current round is collateral, nothing to do except send the first round tx
2182            return self.send_next_round_tx(dbtx, round_idx).await;
2183        }
2184
2185        // get round txhandlers
2186        let context = ContractContext::new_context_for_round(
2187            self.signer.xonly_public_key,
2188            round_idx,
2189            self.config.protocol_paramset(),
2190        );
2191
2192        let txs = create_and_sign_txs(
2193            self.db.clone(),
2194            &self.signer,
2195            self.config.clone(),
2196            context,
2197            None,
2198            dbtx.as_deref_mut(),
2199        )
2200        .await?;
2201
2202        let round_tx = txs
2203            .iter()
2204            .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2205            .ok_or(eyre::eyre!("Round tx not found in txs"))?;
2206
2207        if !self.rpc.is_tx_on_chain(&round_tx.1.compute_txid()).await? {
2208            return Err(eyre::eyre!("Round tx for round {:?} is not on chain, but the database shows we are on this round, error", round_idx).into());
2209        }
2210
2211        // check if ready to reimburse tx was sent
2212        let ready_to_reimburse_tx = txs
2213            .iter()
2214            .find(|(tx_type, _)| tx_type == &TransactionType::ReadyToReimburse)
2215            .ok_or(eyre::eyre!("Ready to reimburse tx not found in txs"))?;
2216
2217        let mut txs_to_send = Vec::new();
2218
2219        // to be able to send ready to reimburse tx, we need to make sure, all kickoff utxos are spent, and for all kickoffs, all kickoff finalizers are spent
2220        if !self
2221            .rpc
2222            .is_tx_on_chain(&ready_to_reimburse_tx.1.compute_txid())
2223            .await?
2224        {
2225            tracing::info!("Ready to reimburse tx for round {:?} is not on chain, checking prerequisites to see if we are able to send it
2226            Prerequisites:
2227            - all kickoff utxos are spent
2228            - for all kickoffs, all kickoff finalizers are spent
2229            ", round_idx);
2230            // get max height saved in bitcoin syncer
2231            let current_chain_height = self
2232                .db
2233                .get_max_height(dbtx.as_deref_mut())
2234                .await?
2235                .ok_or_eyre("Max block height is not found in the btc syncer database")?;
2236
2237            let round_txid = round_tx.1.compute_txid();
2238            let (unspent_kickoff_utxos, are_all_utxos_spent_finalized) = self
2239                .find_and_mark_unspent_kickoff_utxos(
2240                    dbtx.as_deref_mut(),
2241                    round_idx,
2242                    round_txid,
2243                    current_chain_height,
2244                )
2245                .await?;
2246
2247            if !unspent_kickoff_utxos.is_empty() {
2248                let burn_txs = self
2249                    .create_burn_unused_kickoff_connectors_tx(round_idx, &unspent_kickoff_utxos)
2250                    .await?;
2251                txs_to_send.extend(burn_txs);
2252            } else if !are_all_utxos_spent_finalized {
2253                // if some utxos are not spent, we need to wait until they are spent
2254                return Err(eyre::eyre!(format!(
2255                    "The transactions that spend the kickoff utxos are not yet finalized, wait until they are finalized. Finality depth: {}
2256                    If they are actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.",
2257                    self.config.protocol_paramset().finality_depth
2258                ))
2259                .into());
2260            } else {
2261                // every kickoff utxo is spent, but we need to check if all kickoff finalizers are spent
2262                // if not, we return and error and wait until they are spent
2263                // if all finalizers are spent, it is safe to send ready to reimburse tx
2264                self.validate_all_kickoff_finalizers_spent(
2265                    dbtx.as_deref_mut(),
2266                    round_idx,
2267                    current_chain_height,
2268                )
2269                .await?;
2270                // all finalizers and kickoff utxos are spent, it is safe to send ready to reimburse tx
2271                txs_to_send.push(ready_to_reimburse_tx.clone());
2272            }
2273        } else {
2274            // ready to reimburse tx is on chain, we need to wait for the timelock to send the next round tx
2275            // first check if next round tx is already sent, that means we can update the database
2276            txs_to_send.extend(self.send_next_round_tx(dbtx, round_idx).await?);
2277        }
2278
2279        Ok(txs_to_send)
2280    }
2281
2282    /// Finds unspent kickoff UTXOs and marks spent ones as used in the database.
2283    /// Returns the unspent kickoff utxos (doesn't matter if finalized or unfinalized) and a boolean to mark if all utxos are spent and finalized
2284    async fn find_and_mark_unspent_kickoff_utxos(
2285        &self,
2286        mut dbtx: Option<DatabaseTransaction<'_>>,
2287        round_idx: RoundIndex,
2288        round_txid: Txid,
2289        current_chain_height: u32,
2290    ) -> Result<(Vec<usize>, bool), BridgeError> {
2291        // check and collect all kickoff utxos that are not spent
2292        let mut unspent_kickoff_utxos = Vec::new();
2293        // a variable to mark if any any kickoff utxo is spent, but still not finalized
2294        let mut fully_finalized_spent = true;
2295        for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2296            let kickoff_utxo = OutPoint {
2297                txid: round_txid,
2298                vout: UtxoVout::Kickoff(kickoff_idx).get_vout(),
2299            };
2300            if !self.rpc.is_utxo_spent(&kickoff_utxo).await? {
2301                unspent_kickoff_utxos.push(kickoff_idx);
2302            } else {
2303                // set the kickoff connector as used (it will do nothing if the utxo is already in db, so it won't overwrite the kickoff txid)
2304                // mark so that we don't try to use this utxo anymore
2305                self.db
2306                    .mark_kickoff_connector_as_used(
2307                        dbtx.as_deref_mut(),
2308                        round_idx,
2309                        kickoff_idx as u32,
2310                        None,
2311                    )
2312                    .await?;
2313                // check if the tx that spent the kickoff utxo is finalized
2314                // use btc syncer for this
2315                fully_finalized_spent &= self
2316                    .db
2317                    .check_if_utxo_spending_tx_is_finalized(
2318                        dbtx.as_deref_mut(),
2319                        kickoff_utxo,
2320                        current_chain_height,
2321                        self.config.protocol_paramset(),
2322                    )
2323                    .await?;
2324            }
2325        }
2326        Ok((unspent_kickoff_utxos, fully_finalized_spent))
2327    }
2328
2329    /// Creates a transaction that burns unused kickoff connectors.
2330    async fn create_burn_unused_kickoff_connectors_tx(
2331        &self,
2332        round_idx: RoundIndex,
2333        unspent_kickoff_utxos: &[usize],
2334    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2335        tracing::info!(
2336            "There are unspent kickoff utxos {:?}, creating a tx that spends them",
2337            unspent_kickoff_utxos
2338        );
2339        let operator_winternitz_public_keys = self.generate_kickoff_winternitz_pubkeys()?;
2340        let kickoff_wpks = KickoffWinternitzKeys::new(
2341            operator_winternitz_public_keys,
2342            self.config.protocol_paramset().num_kickoffs_per_round,
2343            self.config.protocol_paramset().num_round_txs,
2344        )?;
2345        // if there are unspent kickoff utxos, create a tx that spends them
2346        let (round_txhandler, _ready_to_reimburse_txhandler) = create_round_nth_txhandler(
2347            self.signer.xonly_public_key,
2348            self.collateral_funding_outpoint,
2349            self.config.protocol_paramset().collateral_funding_amount,
2350            round_idx,
2351            &kickoff_wpks,
2352            self.config.protocol_paramset(),
2353        )?;
2354        let mut burn_unused_kickoff_connectors_txhandler =
2355            create_burn_unused_kickoff_connectors_txhandler(
2356                &round_txhandler,
2357                unspent_kickoff_utxos,
2358                &self.reimburse_addr,
2359                self.config.protocol_paramset(),
2360            )?;
2361
2362        // sign burn unused kickoff connectors tx
2363        self.signer
2364            .tx_sign_and_fill_sigs(&mut burn_unused_kickoff_connectors_txhandler, &[], None)
2365            .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
2366
2367        let burn_unused_kickoff_connectors_txhandler =
2368            burn_unused_kickoff_connectors_txhandler.promote()?;
2369        Ok(vec![(
2370            TransactionType::BurnUnusedKickoffConnectors,
2371            burn_unused_kickoff_connectors_txhandler
2372                .get_cached_tx()
2373                .clone(),
2374        )])
2375    }
2376
2377    /// Validates that all kickoff finalizers are spent for the given round.
2378    async fn validate_all_kickoff_finalizers_spent(
2379        &self,
2380        mut dbtx: Option<DatabaseTransaction<'_>>,
2381        round_idx: RoundIndex,
2382        current_chain_height: u32,
2383    ) -> Result<(), BridgeError> {
2384        // we need to check if all finalizers are spent
2385        for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2386            let kickoff_txid = self
2387                .db
2388                .get_kickoff_txid_for_used_kickoff_connector(
2389                    dbtx.as_deref_mut(),
2390                    round_idx,
2391                    kickoff_idx as u32,
2392                )
2393                .await?;
2394            if let Some(kickoff_txid) = kickoff_txid {
2395                let deposit_outpoint = self
2396                    .db
2397                    .get_deposit_outpoint_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
2398                    .await?;
2399                let kickoff_finalizer_utxo = OutPoint {
2400                    txid: kickoff_txid,
2401                    vout: UtxoVout::KickoffFinalizer.get_vout(),
2402                };
2403                if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
2404                    return Err(eyre::eyre!(
2405                        "For round {:?} and kickoff utxo {:?}, the kickoff tx {:?} is not on chain,
2406                    reimburse the deposit {:?} corresponding to this kickoff first. ",
2407                        round_idx,
2408                        kickoff_idx,
2409                        kickoff_txid,
2410                        deposit_outpoint
2411                    )
2412                    .into());
2413                } else if !self.rpc.is_utxo_spent(&kickoff_finalizer_utxo).await? {
2414                    return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer {:?} is not spent,
2415                    send the challenge timeout tx for the deposit {:?} first", round_idx, kickoff_idx, kickoff_txid, deposit_outpoint).into());
2416                } else if !self
2417                    .db
2418                    .check_if_utxo_spending_tx_is_finalized(
2419                        dbtx.as_deref_mut(),
2420                        kickoff_finalizer_utxo,
2421                        current_chain_height,
2422                        self.config.protocol_paramset(),
2423                    )
2424                    .await?
2425                {
2426                    return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer utxo {:?} is spent, but not yet finalized, wait until it is finalized. Finality depth: {}
2427                    If the transaction is actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.", round_idx, kickoff_idx, kickoff_finalizer_utxo, self.config.protocol_paramset().finality_depth).into());
2428                }
2429            }
2430        }
2431        Ok(())
2432    }
2433
2434    /// Checks if the next round tx is on chain, if it is, updates the database, otherwise returns the round tx that needs to be sent.
2435    async fn send_next_round_tx(
2436        &self,
2437        mut dbtx: Option<DatabaseTransaction<'_>>,
2438        round_idx: RoundIndex,
2439    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2440        let next_round_context = ContractContext::new_context_for_round(
2441            self.signer.xonly_public_key,
2442            round_idx.next_round(),
2443            self.config.protocol_paramset(),
2444        );
2445        let next_round_txs = create_and_sign_txs(
2446            self.db.clone(),
2447            &self.signer,
2448            self.config.clone(),
2449            next_round_context,
2450            None,
2451            dbtx.as_deref_mut(),
2452        )
2453        .await?;
2454        let next_round_tx = next_round_txs
2455            .iter()
2456            .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2457            .ok_or(eyre::eyre!("Next round tx not found in txs"))?;
2458        let next_round_txid = next_round_tx.1.compute_txid();
2459
2460        if !self.rpc.is_tx_on_chain(&next_round_txid).await? {
2461            // if next round tx is not on chain, we need to wait for the timelock to send it
2462            Ok(vec![next_round_tx.clone()])
2463        } else {
2464            // if next round tx is on chain, we need to update the database
2465            self.db
2466                .update_current_round_index(dbtx, round_idx.next_round())
2467                .await?;
2468            Ok(vec![])
2469        }
2470    }
2471
2472    /// Adds relevant transactions to tx_sender queue for a kickoff.
2473    /// Used during resync to ensure all necessary txs are queued.
2474    #[cfg(feature = "automation")]
2475    async fn queue_relevant_txs_for_new_kickoff(
2476        &self,
2477        dbtx: DatabaseTransaction<'_>,
2478        kickoff_data: KickoffData,
2479        deposit_data: DepositData,
2480    ) -> Result<(), BridgeError> {
2481        let context = ContractContext::new_context_for_kickoff(
2482            kickoff_data,
2483            deposit_data.clone(),
2484            self.config.protocol_paramset(),
2485        );
2486        let signed_txs = create_and_sign_txs(
2487            self.db.clone(),
2488            &self.signer,
2489            self.config.clone(),
2490            context,
2491            // dummy blockhash, as kickoff is already sent and payout blockhash does not affect the txid, we can use a dummy blockhash
2492            Some([0u8; 20]),
2493            Some(dbtx),
2494        )
2495        .await?;
2496        let tx_metadata = Some(TxMetadata {
2497            tx_type: TransactionType::Dummy,
2498            operator_xonly_pk: Some(self.signer.xonly_public_key),
2499            round_idx: Some(kickoff_data.round_idx),
2500            kickoff_idx: Some(kickoff_data.kickoff_idx),
2501            deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2502        });
2503        for (tx_type, signed_tx) in &signed_txs {
2504            match *tx_type {
2505                TransactionType::OperatorChallengeAck(_)
2506                | TransactionType::WatchtowerChallengeTimeout(_)
2507                | TransactionType::ChallengeTimeout
2508                | TransactionType::DisproveTimeout
2509                | TransactionType::Reimburse => {
2510                    self.tx_sender
2511                        .add_tx_to_queue(
2512                            Some(dbtx),
2513                            *tx_type,
2514                            signed_tx,
2515                            &signed_txs,
2516                            tx_metadata,
2517                            self.config.protocol_paramset(),
2518                            None,
2519                        )
2520                        .await?;
2521                }
2522                _ => {}
2523            }
2524        }
2525        Ok(())
2526    }
2527}
2528
2529impl<C> NamedEntity for Operator<C>
2530where
2531    C: CitreaClientT,
2532{
2533    const ENTITY_NAME: &'static str = "operator";
2534    // operators use their verifier's tx sender
2535    const TX_SENDER_CONSUMER_ID: &'static str = "verifier_tx_sender";
2536    const FINALIZED_BLOCK_CONSUMER_ID_AUTOMATION: &'static str =
2537        "operator_finalized_block_fetcher_automation";
2538    const FINALIZED_BLOCK_CONSUMER_ID_NO_AUTOMATION: &'static str =
2539        "operator_finalized_block_fetcher_no_automation";
2540}
2541
2542#[cfg(feature = "automation")]
2543mod states {
2544
2545    use super::*;
2546    use crate::builder::transaction::{
2547        create_txhandlers, ContractContext, ReimburseDbCache, TxHandler, TxHandlerCache,
2548    };
2549    use crate::states::context::DutyResult;
2550    use crate::states::{block_cache, Duty, Owner, StateManager};
2551    use clementine_primitives::TransactionType;
2552    use std::collections::BTreeMap;
2553    use std::sync::Arc;
2554
2555    #[tonic::async_trait]
2556    impl<C> Owner for Operator<C>
2557    where
2558        C: CitreaClientT,
2559    {
2560        async fn handle_duty(
2561            &self,
2562            dbtx: DatabaseTransaction<'_>,
2563            duty: Duty,
2564        ) -> Result<DutyResult, BridgeError> {
2565            match duty {
2566                Duty::NewReadyToReimburse {
2567                    round_idx,
2568                    operator_xonly_pk,
2569                    used_kickoffs,
2570                } => {
2571                    tracing::info!("Operator {:?} called new ready to reimburse with round_idx: {:?}, operator_xonly_pk: {:?}, used_kickoffs: {:?}",
2572                    self.signer.xonly_public_key, round_idx, operator_xonly_pk, used_kickoffs);
2573                    Ok(DutyResult::Handled)
2574                }
2575                Duty::WatchtowerChallenge { .. } => Ok(DutyResult::Handled),
2576                Duty::AddRelevantTxsToTxSender {
2577                    kickoff_data,
2578                    deposit_data,
2579                } => {
2580                    tracing::info!("Operator {:?} called add relevant txs to tx sender with kickoff_data: {:?}, deposit_data: {:?}",
2581                    self.signer.xonly_public_key, kickoff_data, deposit_data);
2582                    self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2583                        .await?;
2584                    Ok(DutyResult::Handled)
2585                }
2586                Duty::SendOperatorAsserts {
2587                    kickoff_data,
2588                    deposit_data,
2589                    watchtower_challenges,
2590                    payout_blockhash,
2591                    latest_blockhash,
2592                } => {
2593                    tracing::info!("Operator {:?} called send operator asserts with kickoff_data: {:?}, deposit_data: {:?}, number of watchtower_challenges: {}",
2594                    self.signer.xonly_public_key, kickoff_data, deposit_data, watchtower_challenges.len());
2595                    self.send_asserts(
2596                        dbtx,
2597                        kickoff_data,
2598                        deposit_data,
2599                        watchtower_challenges,
2600                        payout_blockhash,
2601                        latest_blockhash,
2602                    )
2603                    .await?;
2604                    Ok(DutyResult::Handled)
2605                }
2606                Duty::VerifierDisprove { .. } => Ok(DutyResult::Handled),
2607                Duty::SendLatestBlockhash {
2608                    kickoff_data,
2609                    deposit_data,
2610                    latest_blockhash,
2611                } => {
2612                    tracing::info!("Operator {:?} called send latest blockhash with kickoff_id: {:?}, deposit_data: {:?}, latest_blockhash: {:?}", self.signer.xonly_public_key, kickoff_data, deposit_data, latest_blockhash);
2613                    self.send_latest_blockhash(dbtx, kickoff_data, deposit_data, latest_blockhash)
2614                        .await?;
2615                    Ok(DutyResult::Handled)
2616                }
2617                Duty::CheckIfKickoff {
2618                    txid,
2619                    block_height,
2620                    witness,
2621                    challenged_before: _,
2622                } => {
2623                    tracing::debug!(
2624                        "Operator {:?} called check if kickoff with txid: {:?}, block_height: {:?}",
2625                        self.signer.xonly_public_key,
2626                        txid,
2627                        block_height,
2628                    );
2629
2630                    let kickoff_data = self
2631                        .db
2632                        .get_deposit_data_with_kickoff_txid(Some(dbtx), txid)
2633                        .await?;
2634                    if let Some((deposit_data, kickoff_data)) = kickoff_data {
2635                        StateManager::<Self>::dispatch_new_kickoff_machine(
2636                            &self.db,
2637                            dbtx,
2638                            kickoff_data,
2639                            block_height,
2640                            deposit_data.clone(),
2641                            witness,
2642                        )
2643                        .await?;
2644
2645                        // resend relevant txs
2646                        self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2647                            .await?;
2648                    }
2649
2650                    Ok(DutyResult::Handled)
2651                }
2652            }
2653        }
2654
2655        async fn create_txhandlers(
2656            &self,
2657            dbtx: DatabaseTransaction<'_>,
2658            tx_type: TransactionType,
2659            contract_context: ContractContext,
2660        ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
2661            let mut db_cache =
2662                ReimburseDbCache::from_context(self.db.clone(), &contract_context, Some(dbtx));
2663            let txhandlers = create_txhandlers(
2664                tx_type,
2665                contract_context,
2666                &mut TxHandlerCache::new(),
2667                &mut db_cache,
2668            )
2669            .await?;
2670            Ok(txhandlers)
2671        }
2672
2673        async fn handle_finalized_block(
2674            &self,
2675            _dbtx: DatabaseTransaction<'_>,
2676            _block_id: u32,
2677            _block_height: u32,
2678            _block_cache: Arc<block_cache::BlockCache>,
2679            _light_client_proof_wait_interval_secs: Option<u32>,
2680        ) -> Result<(), BridgeError> {
2681            Ok(())
2682        }
2683
2684        fn is_kickoff_relevant_for_owner(&self, kickoff_data: &KickoffData) -> bool {
2685            kickoff_data.operator_xonly_pk == self.signer.xonly_public_key
2686        }
2687    }
2688}
2689
2690#[cfg(test)]
2691mod tests {
2692    use crate::operator::Operator;
2693    use crate::test::common::citrea::MockCitreaClient;
2694    use crate::test::common::*;
2695    use bitcoin::hashes::Hash;
2696    use bitcoin::{OutPoint, Txid};
2697
2698    #[tokio::test]
2699    #[ignore = "Design changes in progress"]
2700    async fn get_winternitz_public_keys() {
2701        let mut config = create_test_config_with_thread_name().await;
2702        let _regtest = create_regtest_rpc(&mut config).await;
2703
2704        let operator = Operator::<MockCitreaClient>::new(config.clone())
2705            .await
2706            .unwrap();
2707
2708        let deposit_outpoint = OutPoint {
2709            txid: Txid::all_zeros(),
2710            vout: 2,
2711        };
2712
2713        let winternitz_public_key = operator
2714            .generate_assert_winternitz_pubkeys(deposit_outpoint)
2715            .unwrap();
2716        assert_eq!(
2717            winternitz_public_key.len(),
2718            config.protocol_paramset().num_round_txs
2719                * config.protocol_paramset().num_kickoffs_per_round
2720        );
2721    }
2722
2723    #[tokio::test]
2724    async fn operator_get_params() {
2725        let mut config = create_test_config_with_thread_name().await;
2726        let _regtest = create_regtest_rpc(&mut config).await;
2727
2728        let operator = Operator::<MockCitreaClient>::new(config.clone())
2729            .await
2730            .unwrap();
2731        let actual_wpks = operator.generate_kickoff_winternitz_pubkeys().unwrap();
2732
2733        let (mut wpk_rx, _) = operator.get_params().await.unwrap();
2734        let mut idx = 0;
2735        while let Some(wpk) = wpk_rx.recv().await {
2736            assert_eq!(actual_wpks[idx], wpk);
2737            idx += 1;
2738        }
2739        assert_eq!(idx, actual_wpks.len());
2740    }
2741}