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());
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_test_util;
1437            for (_, tx) in watchtower_challenges.iter() {
1438                let total_work = total_work_from_wt_tx_test_util(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        let (g16_proof, g16_output, public_inputs) = tokio::task::spawn_blocking(move || {
1562            prove_bridge_circuit(bridge_circuit_host_params, bridge_circuit_elf)
1563        })
1564        .await
1565        .wrap_err("Failed to join the prove_bridge_circuit task")?
1566        .wrap_err("Failed to prove bridge circuit")?;
1567
1568        tracing::info!("Proved bridge circuit in send_asserts");
1569        let public_input_scalar = ark_bn254::Fr::from_be_bytes_mod_order(&g16_output);
1570
1571        #[cfg(test)]
1572        let mut public_inputs = public_inputs;
1573
1574        #[cfg(test)]
1575        {
1576            if self
1577                .config
1578                .test_params
1579                .disrupt_challenge_sending_watchtowers_commit
1580            {
1581                tracing::info!("Disrupting challenge sending watchtowers commit in send_asserts");
1582                public_inputs.challenge_sending_watchtowers[0] ^= 0x01;
1583                tracing::info!(
1584                    "Disrupted challenge sending watchtowers commit: {:?}",
1585                    public_inputs.challenge_sending_watchtowers
1586                );
1587            }
1588        }
1589
1590        tracing::info!(
1591            "Challenge sending watchtowers commit: {:?}",
1592            public_inputs.challenge_sending_watchtowers
1593        );
1594
1595        let asserts = tokio::task::spawn_blocking(move || {
1596            let vk = get_verifying_key();
1597
1598            generate_assertions(g16_proof, vec![public_input_scalar], &vk).map_err(|e| {
1599                eyre::eyre!(
1600                    "Failed to generate {}assertions: {}",
1601                    if is_dev_mode() { "dev mode " } else { "" },
1602                    e
1603                )
1604            })
1605        })
1606        .await
1607        .wrap_err("Generate assertions thread failed with error")??;
1608
1609        tracing::info!("Generated assertions in send_asserts");
1610
1611        #[cfg(test)]
1612        let asserts = self.config.test_params.maybe_corrupt_asserts(asserts);
1613
1614        tracing::debug!(target: "ci", "Assert commitment data: {:?}", asserts);
1615
1616        let assert_txs = self
1617            .create_assert_commitment_txs(
1618                TransactionRequestData {
1619                    kickoff_data,
1620                    deposit_outpoint: deposit_data.get_deposit_outpoint(),
1621                },
1622                ClementineBitVMPublicKeys::get_assert_commit_data(
1623                    asserts,
1624                    &public_inputs.challenge_sending_watchtowers,
1625                ),
1626                Some(&mut dbtx),
1627            )
1628            .await?;
1629
1630        for (tx_type, tx) in assert_txs {
1631            self.tx_sender
1632                .add_tx_to_queue(
1633                    Some(&mut dbtx),
1634                    tx_type,
1635                    &tx,
1636                    &[],
1637                    Some(TxMetadata {
1638                        tx_type,
1639                        operator_xonly_pk: Some(self.signer.xonly_public_key),
1640                        round_idx: Some(kickoff_data.round_idx),
1641                        kickoff_idx: Some(kickoff_data.kickoff_idx),
1642                        deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
1643                    }),
1644                    self.config.protocol_paramset(),
1645                    None,
1646                )
1647                .await?;
1648        }
1649        Ok(())
1650    }
1651
1652    fn data(&self) -> OperatorData {
1653        OperatorData {
1654            xonly_pk: self.signer.xonly_public_key,
1655            collateral_funding_outpoint: self.collateral_funding_outpoint,
1656            reimburse_addr: self.reimburse_addr.clone(),
1657        }
1658    }
1659
1660    #[cfg(feature = "automation")]
1661    async fn send_latest_blockhash(
1662        &self,
1663        mut dbtx: DatabaseTransaction<'_>,
1664        kickoff_data: KickoffData,
1665        deposit_data: DepositData,
1666        latest_blockhash: BlockHash,
1667    ) -> Result<(), BridgeError> {
1668        tracing::info!("Operator sending latest blockhash");
1669        let deposit_outpoint = deposit_data.get_deposit_outpoint();
1670        let (tx_type, tx) = self
1671            .create_latest_blockhash_tx(
1672                TransactionRequestData {
1673                    deposit_outpoint,
1674                    kickoff_data,
1675                },
1676                latest_blockhash,
1677                Some(&mut dbtx),
1678            )
1679            .await?;
1680        if tx_type != TransactionType::LatestBlockhash {
1681            return Err(eyre::eyre!("Latest blockhash tx type is not LatestBlockhash").into());
1682        }
1683        self.tx_sender
1684            .add_tx_to_queue(
1685                Some(dbtx),
1686                tx_type,
1687                &tx,
1688                &[],
1689                Some(TxMetadata {
1690                    tx_type,
1691                    operator_xonly_pk: Some(self.signer.xonly_public_key),
1692                    round_idx: Some(kickoff_data.round_idx),
1693                    kickoff_idx: Some(kickoff_data.kickoff_idx),
1694                    deposit_outpoint: Some(deposit_outpoint),
1695                }),
1696                self.config.protocol_paramset(),
1697                None,
1698            )
1699            .await?;
1700        Ok(())
1701    }
1702
1703    /// For a deposit_id checks that the payer for that deposit is the operator, and the payout blockhash and kickoff txid are set.
1704    async fn validate_payer_is_operator(
1705        &self,
1706        dbtx: Option<DatabaseTransaction<'_>>,
1707        deposit_id: u32,
1708    ) -> Result<(BlockHash, Txid), BridgeError> {
1709        let (payer_xonly_pk, payout_blockhash, kickoff_txid) = self
1710            .db
1711            .get_payer_xonly_pk_blockhash_and_kickoff_txid_from_deposit_id(dbtx, deposit_id)
1712            .await?;
1713
1714        tracing::info!(
1715            "Payer xonly pk and kickoff txid found for the requested deposit, payer xonly pk: {:?}, kickoff txid: {:?}",
1716            payer_xonly_pk,
1717            kickoff_txid
1718        );
1719
1720        // first check if the payer is the operator, and the kickoff is handled
1721        // by the PayoutCheckerTask, meaning kickoff_txid is set
1722        let (payout_blockhash, kickoff_txid) = match (
1723            payer_xonly_pk,
1724            payout_blockhash,
1725            kickoff_txid,
1726        ) {
1727            (Some(payer_xonly_pk), Some(payout_blockhash), Some(kickoff_txid)) => {
1728                if payer_xonly_pk != self.signer.xonly_public_key {
1729                    return Err(eyre::eyre!(
1730                        "Payer is not own operator for deposit, payer xonly pk: {:?}, operator xonly pk: {:?}",
1731                        payer_xonly_pk,
1732                        self.signer.xonly_public_key
1733                    )
1734                    .into());
1735                }
1736                (payout_blockhash, kickoff_txid)
1737            }
1738            _ => {
1739                return Err(eyre::eyre!(
1740                    "Payer info not found for deposit, payout blockhash: {:?}, kickoff txid: {:?}",
1741                    payout_blockhash,
1742                    kickoff_txid
1743                )
1744                .into());
1745            }
1746        };
1747
1748        tracing::info!(
1749            "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: {:?}",
1750            deposit_id,
1751            payer_xonly_pk,
1752            payout_blockhash,
1753            kickoff_txid
1754        );
1755
1756        Ok((payout_blockhash, kickoff_txid))
1757    }
1758
1759    async fn get_next_txs_to_send(
1760        &self,
1761        mut dbtx: Option<DatabaseTransaction<'_>>,
1762        deposit_data: &mut DepositData,
1763        payout_blockhash: BlockHash,
1764        kickoff_txid: Txid,
1765        current_round_idx: RoundIndex,
1766    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
1767        let mut txs_to_send = Vec::new();
1768
1769        // get used kickoff connector for the kickoff txid
1770        let (kickoff_round_idx, kickoff_connector_idx) = self
1771            .db
1772            .get_kickoff_connector_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
1773            .await?;
1774
1775        let context = ContractContext::new_context_for_kickoff(
1776            KickoffData {
1777                operator_xonly_pk: self.signer.xonly_public_key,
1778                round_idx: kickoff_round_idx,
1779                kickoff_idx: kickoff_connector_idx,
1780            },
1781            deposit_data.clone(),
1782            self.config.protocol_paramset(),
1783        );
1784
1785        // get txs for the kickoff
1786        let kickoff_txs = create_and_sign_txs(
1787            self.db.clone(),
1788            &self.signer,
1789            self.config.clone(),
1790            context,
1791            Some(payout_blockhash.to_byte_array().last_20_bytes()),
1792            dbtx.as_deref_mut(),
1793        )
1794        .await?;
1795
1796        // check the current round status compared to the round of the assigned kickoff tx
1797        match current_round_idx
1798            .to_index()
1799            .cmp(&kickoff_round_idx.to_index())
1800        {
1801            std::cmp::Ordering::Less => {
1802                // We need to advance the round manually to be able to start the kickoff
1803                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);
1804                let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1805                txs_to_send.extend(txs);
1806            }
1807            std::cmp::Ordering::Greater => {
1808                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);
1809                // we are at least on the next round, meaning we can get the reimbursement as reimbursement utxos are in the next round
1810                let reimbursement_tx = kickoff_txs
1811                    .iter()
1812                    .find(|(tx_type, _)| tx_type == &TransactionType::Reimburse)
1813                    .ok_or(eyre::eyre!("Reimburse tx not found in kickoff txs"))?;
1814                txs_to_send.push(reimbursement_tx.clone());
1815            }
1816            std::cmp::Ordering::Equal => {
1817                // first check if the kickoff is in chain
1818                if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
1819                    tracing::info!(
1820                        "Kickoff tx is not on chain, can send it, kickoff txid: {:?}",
1821                        kickoff_txid
1822                    );
1823                    let kickoff_tx = kickoff_txs
1824                        .iter()
1825                        .find(|(tx_type, _)| tx_type == &TransactionType::Kickoff)
1826                        .ok_or(eyre::eyre!("Kickoff tx not found in kickoff txs"))?;
1827
1828                    // fetch and save the LCP for if we get challenged and need to provide proof of payout later
1829                    let (_, payout_block_height) = self
1830                        .db
1831                        .get_block_info_from_hash(dbtx.as_deref_mut(), payout_blockhash)
1832                        .await?
1833                        .ok_or_eyre("Couldn't find payout blockhash in bitcoin sync")?;
1834
1835                    let move_txid = deposit_data.get_move_txid(self.config.protocol_paramset())?;
1836
1837                    let (_, _, _, citrea_idx) = self
1838                        .db
1839                        .get_payout_info_from_move_txid(dbtx.as_deref_mut(), move_txid)
1840                        .await?
1841                        .ok_or_eyre("Couldn't find payout info from move txid")?;
1842
1843                    let _ = self
1844                        .citrea_client
1845                        .fetch_validate_and_store_lcp(
1846                            payout_block_height as u64,
1847                            citrea_idx as u32,
1848                            &self.db,
1849                            dbtx.as_deref_mut(),
1850                            self.config.protocol_paramset(),
1851                        )
1852                        .await?;
1853
1854                    // sanity check
1855                    if kickoff_tx.1.compute_txid() != kickoff_txid {
1856                        return Err(eyre::eyre!("Kickoff txid mismatch for deposit outpoint: {}, kickoff txid: {:?}, computed txid: {:?}",
1857                        deposit_data.get_deposit_outpoint(), kickoff_txid, kickoff_tx.1.compute_txid()).into());
1858                    }
1859                    txs_to_send.push(kickoff_tx.clone());
1860                }
1861                // kickoff tx is on chain, check if kickoff finalizer is spent
1862                else if !self
1863                    .rpc
1864                    .is_utxo_spent(&OutPoint {
1865                        txid: kickoff_txid,
1866                        vout: UtxoVout::KickoffFinalizer.get_vout(),
1867                    })
1868                    .await?
1869                {
1870                    // kickoff finalizer is not spent, we need to send challenge timeout
1871                    tracing::info!(
1872                        "Kickoff finalizer is not spent, can send challenge timeout, kickoff txid: {:?}",
1873                        kickoff_txid
1874                    );
1875                    // first check if challenge tx was sent, then we need automation enabled to be able to answer the challenge
1876                    if self
1877                        .rpc
1878                        .is_utxo_spent(&OutPoint {
1879                            txid: kickoff_txid,
1880                            vout: UtxoVout::Challenge.get_vout(),
1881                        })
1882                        .await?
1883                    {
1884                        // challenge tx was sent, we need automation enabled to be able to answer the challenge
1885                        tracing::warn!(
1886                            "Challenge tx was sent for deposit outpoint: {:?}, but automation is not enabled, enable automation!",
1887                            deposit_data.get_deposit_outpoint()
1888                        );
1889                        return Err(eyre::eyre!("WARNING: Challenge tx was sent to kickoff connector {:?}, but automation is not enabled, enable automation!", kickoff_txid).into());
1890                    }
1891                    let challenge_timeout_tx = kickoff_txs
1892                        .iter()
1893                        .find(|(tx_type, _)| tx_type == &TransactionType::ChallengeTimeout)
1894                        .ok_or(eyre::eyre!("Challenge timeout tx not found in kickoff txs"))?;
1895                    txs_to_send.push(challenge_timeout_tx.clone());
1896                } else {
1897                    // if kickoff finalizer is spent, it is time to get the reimbursement
1898                    tracing::info!(
1899                        "Kickoff finalizer is spent, can advance the round manually to get the reimbursement, current round idx: {:?}, kickoff round idx: {:?}",
1900                        current_round_idx,
1901                        kickoff_round_idx
1902                    );
1903                    let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1904                    txs_to_send.extend(txs);
1905                }
1906            }
1907        }
1908        Ok(txs_to_send)
1909    }
1910
1911    /// Transfers the given outpoints to the operator's btc wallet address.
1912    /// The outpoints must belong to the operator's taproot address (xonly key, no merkle root).
1913    /// The function also checks if any outpoint is the collateral of the operator, and returns an error if so.
1914    /// # Arguments
1915    /// - inputs: A vector of tuples, each containing an outpoint and the corresponding txout.
1916    /// # Returns
1917    /// - The signed transaction that sends the given outpoints to the operator's btc wallet address.
1918    pub async fn transfer_outpoints_to_wallet(
1919        &self,
1920        inputs: Vec<(OutPoint, TxOut)>,
1921    ) -> Result<Transaction, BridgeError> {
1922        if inputs.is_empty() {
1923            return Err(eyre!("No outpoints provided for transfer").into());
1924        }
1925
1926        // check if any outpoint is a collateral outpoint
1927        let collateral_outpoints = self
1928            .get_all_collateral_outpoints()
1929            .await
1930            .wrap_err("Failed to get all collateral outpoints")?;
1931        for (outpoint, _) in inputs.iter() {
1932            if collateral_outpoints.contains_key(outpoint) {
1933                let (round_idx, tx_type) = collateral_outpoints
1934                    .get(outpoint)
1935                    .expect("Collateral outpoint should be found in the map");
1936                return Err(
1937                    eyre!("Cannot transfer collateral outpoint {outpoint} belonging to {round_idx:?} {tx_type:?} to wallet").into(),
1938                );
1939            }
1940        }
1941
1942        let destination_script = self
1943            .rpc
1944            .get_new_address(None, Some(AddressType::Bech32m))
1945            .await
1946            .wrap_err("Failed to get new wallet address for transfer")?
1947            .require_network(self.config.protocol_paramset().network)
1948            .wrap_err("Failed to get new address, bitcoin rpc might not match the network")?
1949            .script_pubkey();
1950
1951        let (_, spendinfo) = create_taproot_address(
1952            &[],
1953            Some(self.signer.xonly_public_key),
1954            self.config.protocol_paramset().network,
1955        );
1956
1957        let total_input_value = inputs.iter().try_fold(Amount::ZERO, |acc, (_, txout)| {
1958            acc.checked_add(txout.value)
1959                .ok_or_else(|| eyre!("Input values overflowed while summing"))
1960        })?;
1961
1962        let mut output_txout = TxOut {
1963            value: total_input_value,
1964            script_pubkey: destination_script,
1965        };
1966
1967        let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
1968            .with_version(bitcoin::transaction::Version::TWO);
1969
1970        for (outpoint, txout) in inputs.iter() {
1971            builder = builder.add_input(
1972                NormalSignatureKind::OperatorSighashDefault,
1973                SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
1974                SpendPath::KeySpend,
1975                DEFAULT_SEQUENCE,
1976            );
1977        }
1978
1979        builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
1980
1981        let mut txhandler = builder.finalize();
1982
1983        let fee_rate = self
1984            .rpc
1985            .get_fee_rate(
1986                self.config.protocol_paramset().network,
1987                &self.config.mempool_api_host,
1988                &self.config.mempool_api_endpoint,
1989                self.config.tx_sender_limits.mempool_fee_rate_multiplier,
1990                self.config.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
1991                self.config.tx_sender_limits.fee_rate_hard_cap,
1992            )
1993            .await
1994            .wrap_err("Failed to get fee rate for transfer to wallet tx")?;
1995
1996        // Sign to account for witness weight when calculating fees.
1997        self.signer
1998            .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
1999
2000        let tx_weight_wu = txhandler.get_cached_tx().weight().to_wu();
2001        let fee_sat = (fee_rate.to_sat_per_kwu() * tx_weight_wu).div_ceil(1000);
2002        let fee = Amount::from_sat(fee_sat);
2003
2004        output_txout.value = output_txout
2005            .value
2006            .checked_sub(fee)
2007            .ok_or_else(|| eyre!("Calculated fee exceeds total input value"))?;
2008
2009        let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
2010            .with_version(bitcoin::transaction::Version::TWO);
2011
2012        for (outpoint, txout) in inputs.iter() {
2013            builder = builder.add_input(
2014                NormalSignatureKind::OperatorSighashDefault,
2015                SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
2016                SpendPath::KeySpend,
2017                DEFAULT_SEQUENCE,
2018            );
2019        }
2020
2021        builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
2022
2023        let mut txhandler = builder.finalize();
2024
2025        self.signer
2026            .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
2027
2028        let signed_tx = txhandler.get_cached_tx().clone();
2029
2030        self.rpc
2031            .send_raw_transaction(&signed_tx)
2032            .await
2033            .wrap_err("Failed to send from operator's address to btc wallet address")?;
2034
2035        Ok(signed_tx)
2036    }
2037
2038    /// Gets all collateral outpoints for the operator.
2039    /// Returns a map of outpoint to the round index it belongs to.
2040    async fn get_all_collateral_outpoints(
2041        &self,
2042    ) -> Result<HashMap<OutPoint, (RoundIndex, TransactionType)>, BridgeError> {
2043        let mut outpoints = HashMap::new();
2044        outpoints.insert(
2045            self.collateral_funding_outpoint,
2046            (RoundIndex::Collateral, TransactionType::Round),
2047        );
2048
2049        // Fetch operator kickoff winternitz public keys to build round txs
2050        let operator_winternitz_public_keys = self
2051            .db
2052            .get_operator_kickoff_winternitz_public_keys(None, self.signer.xonly_public_key)
2053            .await?;
2054        let kickoff_wpks = KickoffWinternitzKeys::new(
2055            operator_winternitz_public_keys,
2056            self.config.protocol_paramset().num_kickoffs_per_round,
2057            self.config.protocol_paramset().num_round_txs,
2058        )?;
2059        let operator_data = self.data();
2060
2061        let mut prev_ready_to_reimburse: Option<TxHandler> = None;
2062
2063        // Collect collateral outpoints for each round
2064        for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
2065            let txhandlers = create_round_txhandlers(
2066                self.config.protocol_paramset(),
2067                round_idx,
2068                &operator_data,
2069                &kickoff_wpks,
2070                prev_ready_to_reimburse.as_ref(),
2071            )?;
2072
2073            let round_tx = txhandlers
2074                .iter()
2075                .find(|txhandler| txhandler.get_transaction_type() == TransactionType::Round)
2076                .ok_or(eyre::eyre!("Round tx not found in txhandlers"))?;
2077            let collateral_outpoint = OutPoint {
2078                txid: *round_tx.get_txid(),
2079                vout: UtxoVout::CollateralInRound.get_vout(),
2080            };
2081            outpoints.insert(collateral_outpoint, (round_idx, TransactionType::Round));
2082
2083            let ready_to_reimburse_tx = txhandlers
2084                .iter()
2085                .find(|txhandler| {
2086                    txhandler.get_transaction_type() == TransactionType::ReadyToReimburse
2087                })
2088                .ok_or(eyre::eyre!("Ready to reimburse tx not found in txhandlers"))?;
2089            let ready_to_reimburse_collateral_outpoint = OutPoint {
2090                txid: *ready_to_reimburse_tx.get_txid(),
2091                vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
2092            };
2093            outpoints.insert(
2094                ready_to_reimburse_collateral_outpoint,
2095                (round_idx, TransactionType::ReadyToReimburse),
2096            );
2097            prev_ready_to_reimburse = Some(ready_to_reimburse_tx.clone());
2098        }
2099
2100        Ok(outpoints)
2101    }
2102
2103    /// For a given deposit outpoint, get the txs that are needed to reimburse the deposit.
2104    /// To avoid operator getting slashed, this function only returns the next tx that needs to be sent
2105    /// This fn can track and enable sending of these transactions during a normal reimbursement process.
2106    ///
2107    /// - First, if the current round is less than the round of the kickoff assigned to the deposit by PayoutCheckerTask, it returns the Round TX.
2108    /// - After Round tx is sent, it returns the Kickoff tx.
2109    /// - After Kickoff tx is sent, it returns the challenge timeout tx.
2110    /// - 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.
2111    /// - After all kickoff utxos are spent, and for any live kickoff, all kickoff finalizers are spent, it returns the ReadyToReimburse tx.
2112    /// - After ReadyToReimburse tx is sent, it returns the next Round tx to generate reimbursement utxos.
2113    /// - Finally, after the next round tx is sent, it returns the Reimburse tx.
2114    pub async fn get_reimbursement_txs(
2115        &self,
2116        deposit_outpoint: OutPoint,
2117    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2118        let mut dbtx = self.db.begin_transaction().await?;
2119        // first check if the deposit is in the database
2120        let (deposit_id, mut deposit_data) = self
2121            .db
2122            .get_deposit_data(Some(&mut dbtx), deposit_outpoint)
2123            .await?
2124            .ok_or_eyre(format!(
2125                "Deposit data not found for the requested deposit outpoint: {deposit_outpoint:?}, make sure you send the deposit outpoint, not the move txid."
2126            ))?;
2127
2128        tracing::info!(
2129            "Deposit data found for the requested deposit outpoint: {deposit_outpoint:?}, deposit id: {deposit_id:?}",
2130        );
2131
2132        // validate payer is operator and get payer xonly pk, payout blockhash and kickoff txid
2133        let (payout_blockhash, kickoff_txid) = self
2134            .validate_payer_is_operator(Some(&mut dbtx), deposit_id)
2135            .await?;
2136
2137        let mut current_round_idx = self.db.get_current_round_index(Some(&mut dbtx)).await?;
2138
2139        let mut txs_to_send: Vec<(TransactionType, Transaction)>;
2140
2141        loop {
2142            txs_to_send = self
2143                .get_next_txs_to_send(
2144                    Some(&mut dbtx),
2145                    &mut deposit_data,
2146                    payout_blockhash,
2147                    kickoff_txid,
2148                    current_round_idx,
2149                )
2150                .await?;
2151            if txs_to_send.is_empty() {
2152                // if no txs were returned, and we advanced the round in the db, ask for the next txs again
2153                // with the new round index
2154                let round_idx_after_operations =
2155                    self.db.get_current_round_index(Some(&mut dbtx)).await?;
2156                if round_idx_after_operations != current_round_idx {
2157                    current_round_idx = round_idx_after_operations;
2158                    continue;
2159                }
2160            }
2161            break;
2162        }
2163
2164        dbtx.commit().await?;
2165        Ok(txs_to_send)
2166    }
2167
2168    /// Checks the current round status, and returns the next txs that are safe to send to be
2169    /// able to advance to the next round.
2170    async fn advance_round_manually(
2171        &self,
2172        mut dbtx: Option<DatabaseTransaction<'_>>,
2173        round_idx: RoundIndex,
2174    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2175        if round_idx == RoundIndex::Collateral {
2176            // if current round is collateral, nothing to do except send the first round tx
2177            return self.send_next_round_tx(dbtx, round_idx).await;
2178        }
2179
2180        // get round txhandlers
2181        let context = ContractContext::new_context_for_round(
2182            self.signer.xonly_public_key,
2183            round_idx,
2184            self.config.protocol_paramset(),
2185        );
2186
2187        let txs = create_and_sign_txs(
2188            self.db.clone(),
2189            &self.signer,
2190            self.config.clone(),
2191            context,
2192            None,
2193            dbtx.as_deref_mut(),
2194        )
2195        .await?;
2196
2197        let round_tx = txs
2198            .iter()
2199            .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2200            .ok_or(eyre::eyre!("Round tx not found in txs"))?;
2201
2202        if !self.rpc.is_tx_on_chain(&round_tx.1.compute_txid()).await? {
2203            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());
2204        }
2205
2206        // check if ready to reimburse tx was sent
2207        let ready_to_reimburse_tx = txs
2208            .iter()
2209            .find(|(tx_type, _)| tx_type == &TransactionType::ReadyToReimburse)
2210            .ok_or(eyre::eyre!("Ready to reimburse tx not found in txs"))?;
2211
2212        let mut txs_to_send = Vec::new();
2213
2214        // 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
2215        if !self
2216            .rpc
2217            .is_tx_on_chain(&ready_to_reimburse_tx.1.compute_txid())
2218            .await?
2219        {
2220            tracing::info!("Ready to reimburse tx for round {:?} is not on chain, checking prerequisites to see if we are able to send it
2221            Prerequisites:
2222            - all kickoff utxos are spent
2223            - for all kickoffs, all kickoff finalizers are spent
2224            ", round_idx);
2225            // get max height saved in bitcoin syncer
2226            let current_chain_height = self
2227                .db
2228                .get_max_height(dbtx.as_deref_mut())
2229                .await?
2230                .ok_or_eyre("Max block height is not found in the btc syncer database")?;
2231
2232            let round_txid = round_tx.1.compute_txid();
2233            let (unspent_kickoff_utxos, are_all_utxos_spent_finalized) = self
2234                .find_and_mark_unspent_kickoff_utxos(
2235                    dbtx.as_deref_mut(),
2236                    round_idx,
2237                    round_txid,
2238                    current_chain_height,
2239                )
2240                .await?;
2241
2242            if !unspent_kickoff_utxos.is_empty() {
2243                let burn_txs = self
2244                    .create_burn_unused_kickoff_connectors_tx(round_idx, &unspent_kickoff_utxos)
2245                    .await?;
2246                txs_to_send.extend(burn_txs);
2247            } else if !are_all_utxos_spent_finalized {
2248                // if some utxos are not spent, we need to wait until they are spent
2249                return Err(eyre::eyre!(format!(
2250                    "The transactions that spend the kickoff utxos are not yet finalized, wait until they are finalized. Finality depth: {}
2251                    If they are actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.",
2252                    self.config.protocol_paramset().finality_depth
2253                ))
2254                .into());
2255            } else {
2256                // every kickoff utxo is spent, but we need to check if all kickoff finalizers are spent
2257                // if not, we return and error and wait until they are spent
2258                // if all finalizers are spent, it is safe to send ready to reimburse tx
2259                self.validate_all_kickoff_finalizers_spent(
2260                    dbtx.as_deref_mut(),
2261                    round_idx,
2262                    current_chain_height,
2263                )
2264                .await?;
2265                // all finalizers and kickoff utxos are spent, it is safe to send ready to reimburse tx
2266                txs_to_send.push(ready_to_reimburse_tx.clone());
2267            }
2268        } else {
2269            // ready to reimburse tx is on chain, we need to wait for the timelock to send the next round tx
2270            // first check if next round tx is already sent, that means we can update the database
2271            txs_to_send.extend(self.send_next_round_tx(dbtx, round_idx).await?);
2272        }
2273
2274        Ok(txs_to_send)
2275    }
2276
2277    /// Finds unspent kickoff UTXOs and marks spent ones as used in the database.
2278    /// Returns the unspent kickoff utxos (doesn't matter if finalized or unfinalized) and a boolean to mark if all utxos are spent and finalized
2279    async fn find_and_mark_unspent_kickoff_utxos(
2280        &self,
2281        mut dbtx: Option<DatabaseTransaction<'_>>,
2282        round_idx: RoundIndex,
2283        round_txid: Txid,
2284        current_chain_height: u32,
2285    ) -> Result<(Vec<usize>, bool), BridgeError> {
2286        // check and collect all kickoff utxos that are not spent
2287        let mut unspent_kickoff_utxos = Vec::new();
2288        // a variable to mark if any any kickoff utxo is spent, but still not finalized
2289        let mut fully_finalized_spent = true;
2290        for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2291            let kickoff_utxo = OutPoint {
2292                txid: round_txid,
2293                vout: UtxoVout::Kickoff(kickoff_idx).get_vout(),
2294            };
2295            if !self.rpc.is_utxo_spent(&kickoff_utxo).await? {
2296                unspent_kickoff_utxos.push(kickoff_idx);
2297            } else {
2298                // 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)
2299                // mark so that we don't try to use this utxo anymore
2300                self.db
2301                    .mark_kickoff_connector_as_used(
2302                        dbtx.as_deref_mut(),
2303                        round_idx,
2304                        kickoff_idx as u32,
2305                        None,
2306                    )
2307                    .await?;
2308                // check if the tx that spent the kickoff utxo is finalized
2309                // use btc syncer for this
2310                fully_finalized_spent &= self
2311                    .db
2312                    .check_if_utxo_spending_tx_is_finalized(
2313                        dbtx.as_deref_mut(),
2314                        kickoff_utxo,
2315                        current_chain_height,
2316                        self.config.protocol_paramset(),
2317                    )
2318                    .await?;
2319            }
2320        }
2321        Ok((unspent_kickoff_utxos, fully_finalized_spent))
2322    }
2323
2324    /// Creates a transaction that burns unused kickoff connectors.
2325    async fn create_burn_unused_kickoff_connectors_tx(
2326        &self,
2327        round_idx: RoundIndex,
2328        unspent_kickoff_utxos: &[usize],
2329    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2330        tracing::info!(
2331            "There are unspent kickoff utxos {:?}, creating a tx that spends them",
2332            unspent_kickoff_utxos
2333        );
2334        let operator_winternitz_public_keys = self.generate_kickoff_winternitz_pubkeys()?;
2335        let kickoff_wpks = KickoffWinternitzKeys::new(
2336            operator_winternitz_public_keys,
2337            self.config.protocol_paramset().num_kickoffs_per_round,
2338            self.config.protocol_paramset().num_round_txs,
2339        )?;
2340        // if there are unspent kickoff utxos, create a tx that spends them
2341        let (round_txhandler, _ready_to_reimburse_txhandler) = create_round_nth_txhandler(
2342            self.signer.xonly_public_key,
2343            self.collateral_funding_outpoint,
2344            self.config.protocol_paramset().collateral_funding_amount,
2345            round_idx,
2346            &kickoff_wpks,
2347            self.config.protocol_paramset(),
2348        )?;
2349        let mut burn_unused_kickoff_connectors_txhandler =
2350            create_burn_unused_kickoff_connectors_txhandler(
2351                &round_txhandler,
2352                unspent_kickoff_utxos,
2353                &self.reimburse_addr,
2354                self.config.protocol_paramset(),
2355            )?;
2356
2357        // sign burn unused kickoff connectors tx
2358        self.signer
2359            .tx_sign_and_fill_sigs(&mut burn_unused_kickoff_connectors_txhandler, &[], None)
2360            .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
2361
2362        let burn_unused_kickoff_connectors_txhandler =
2363            burn_unused_kickoff_connectors_txhandler.promote()?;
2364        Ok(vec![(
2365            TransactionType::BurnUnusedKickoffConnectors,
2366            burn_unused_kickoff_connectors_txhandler
2367                .get_cached_tx()
2368                .clone(),
2369        )])
2370    }
2371
2372    /// Validates that all kickoff finalizers are spent for the given round.
2373    async fn validate_all_kickoff_finalizers_spent(
2374        &self,
2375        mut dbtx: Option<DatabaseTransaction<'_>>,
2376        round_idx: RoundIndex,
2377        current_chain_height: u32,
2378    ) -> Result<(), BridgeError> {
2379        // we need to check if all finalizers are spent
2380        for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2381            let kickoff_txid = self
2382                .db
2383                .get_kickoff_txid_for_used_kickoff_connector(
2384                    dbtx.as_deref_mut(),
2385                    round_idx,
2386                    kickoff_idx as u32,
2387                )
2388                .await?;
2389            if let Some(kickoff_txid) = kickoff_txid {
2390                let deposit_outpoint = self
2391                    .db
2392                    .get_deposit_outpoint_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
2393                    .await?;
2394                let kickoff_finalizer_utxo = OutPoint {
2395                    txid: kickoff_txid,
2396                    vout: UtxoVout::KickoffFinalizer.get_vout(),
2397                };
2398                if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
2399                    return Err(eyre::eyre!(
2400                        "For round {:?} and kickoff utxo {:?}, the kickoff tx {:?} is not on chain,
2401                    reimburse the deposit {:?} corresponding to this kickoff first. ",
2402                        round_idx,
2403                        kickoff_idx,
2404                        kickoff_txid,
2405                        deposit_outpoint
2406                    )
2407                    .into());
2408                } else if !self.rpc.is_utxo_spent(&kickoff_finalizer_utxo).await? {
2409                    return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer {:?} is not spent,
2410                    send the challenge timeout tx for the deposit {:?} first", round_idx, kickoff_idx, kickoff_txid, deposit_outpoint).into());
2411                } else if !self
2412                    .db
2413                    .check_if_utxo_spending_tx_is_finalized(
2414                        dbtx.as_deref_mut(),
2415                        kickoff_finalizer_utxo,
2416                        current_chain_height,
2417                        self.config.protocol_paramset(),
2418                    )
2419                    .await?
2420                {
2421                    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: {}
2422                    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());
2423                }
2424            }
2425        }
2426        Ok(())
2427    }
2428
2429    /// 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.
2430    async fn send_next_round_tx(
2431        &self,
2432        mut dbtx: Option<DatabaseTransaction<'_>>,
2433        round_idx: RoundIndex,
2434    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2435        let next_round_context = ContractContext::new_context_for_round(
2436            self.signer.xonly_public_key,
2437            round_idx.next_round(),
2438            self.config.protocol_paramset(),
2439        );
2440        let next_round_txs = create_and_sign_txs(
2441            self.db.clone(),
2442            &self.signer,
2443            self.config.clone(),
2444            next_round_context,
2445            None,
2446            dbtx.as_deref_mut(),
2447        )
2448        .await?;
2449        let next_round_tx = next_round_txs
2450            .iter()
2451            .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2452            .ok_or(eyre::eyre!("Next round tx not found in txs"))?;
2453        let next_round_txid = next_round_tx.1.compute_txid();
2454
2455        if !self.rpc.is_tx_on_chain(&next_round_txid).await? {
2456            // if next round tx is not on chain, we need to wait for the timelock to send it
2457            Ok(vec![next_round_tx.clone()])
2458        } else {
2459            // if next round tx is on chain, we need to update the database
2460            self.db
2461                .update_current_round_index(dbtx, round_idx.next_round())
2462                .await?;
2463            Ok(vec![])
2464        }
2465    }
2466
2467    /// Adds relevant transactions to tx_sender queue for a kickoff.
2468    /// Used during resync to ensure all necessary txs are queued.
2469    #[cfg(feature = "automation")]
2470    async fn queue_relevant_txs_for_new_kickoff(
2471        &self,
2472        dbtx: DatabaseTransaction<'_>,
2473        kickoff_data: KickoffData,
2474        deposit_data: DepositData,
2475    ) -> Result<(), BridgeError> {
2476        let context = ContractContext::new_context_for_kickoff(
2477            kickoff_data,
2478            deposit_data.clone(),
2479            self.config.protocol_paramset(),
2480        );
2481        let signed_txs = create_and_sign_txs(
2482            self.db.clone(),
2483            &self.signer,
2484            self.config.clone(),
2485            context,
2486            // dummy blockhash, as kickoff is already sent and payout blockhash does not affect the txid, we can use a dummy blockhash
2487            Some([0u8; 20]),
2488            Some(dbtx),
2489        )
2490        .await?;
2491        let tx_metadata = Some(TxMetadata {
2492            tx_type: TransactionType::Dummy,
2493            operator_xonly_pk: Some(self.signer.xonly_public_key),
2494            round_idx: Some(kickoff_data.round_idx),
2495            kickoff_idx: Some(kickoff_data.kickoff_idx),
2496            deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2497        });
2498        for (tx_type, signed_tx) in &signed_txs {
2499            match *tx_type {
2500                TransactionType::OperatorChallengeAck(_)
2501                | TransactionType::WatchtowerChallengeTimeout(_)
2502                | TransactionType::ChallengeTimeout
2503                | TransactionType::DisproveTimeout
2504                | TransactionType::Reimburse => {
2505                    self.tx_sender
2506                        .add_tx_to_queue(
2507                            Some(dbtx),
2508                            *tx_type,
2509                            signed_tx,
2510                            &signed_txs,
2511                            tx_metadata,
2512                            self.config.protocol_paramset(),
2513                            None,
2514                        )
2515                        .await?;
2516                }
2517                _ => {}
2518            }
2519        }
2520        Ok(())
2521    }
2522}
2523
2524impl<C> NamedEntity for Operator<C>
2525where
2526    C: CitreaClientT,
2527{
2528    const ENTITY_NAME: &'static str = "operator";
2529    const FINALIZED_BLOCK_CONSUMER_ID_AUTOMATION: &'static str =
2530        "operator_finalized_block_fetcher_automation";
2531    const FINALIZED_BLOCK_CONSUMER_ID_NO_AUTOMATION: &'static str =
2532        "operator_finalized_block_fetcher_no_automation";
2533}
2534
2535#[cfg(feature = "automation")]
2536mod states {
2537
2538    use super::*;
2539    use crate::builder::transaction::{
2540        create_txhandlers, ContractContext, ReimburseDbCache, TxHandler, TxHandlerCache,
2541    };
2542    use crate::states::context::DutyResult;
2543    use crate::states::{block_cache, Duty, Owner, StateManager};
2544    use clementine_primitives::TransactionType;
2545    use std::collections::BTreeMap;
2546    use std::sync::Arc;
2547
2548    #[tonic::async_trait]
2549    impl<C> Owner for Operator<C>
2550    where
2551        C: CitreaClientT,
2552    {
2553        async fn handle_duty(
2554            &self,
2555            dbtx: DatabaseTransaction<'_>,
2556            duty: Duty,
2557        ) -> Result<DutyResult, BridgeError> {
2558            match duty {
2559                Duty::NewReadyToReimburse {
2560                    round_idx,
2561                    operator_xonly_pk,
2562                    used_kickoffs,
2563                } => {
2564                    tracing::info!("Operator {:?} called new ready to reimburse with round_idx: {:?}, operator_xonly_pk: {:?}, used_kickoffs: {:?}",
2565                    self.signer.xonly_public_key, round_idx, operator_xonly_pk, used_kickoffs);
2566                    Ok(DutyResult::Handled)
2567                }
2568                Duty::WatchtowerChallenge { .. } => Ok(DutyResult::Handled),
2569                Duty::AddRelevantTxsToTxSender {
2570                    kickoff_data,
2571                    deposit_data,
2572                } => {
2573                    tracing::info!("Operator {:?} called add relevant txs to tx sender with kickoff_data: {:?}, deposit_data: {:?}",
2574                    self.signer.xonly_public_key, kickoff_data, deposit_data);
2575                    self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2576                        .await?;
2577                    Ok(DutyResult::Handled)
2578                }
2579                Duty::SendOperatorAsserts {
2580                    kickoff_data,
2581                    deposit_data,
2582                    watchtower_challenges,
2583                    payout_blockhash,
2584                    latest_blockhash,
2585                } => {
2586                    tracing::info!("Operator {:?} called send operator asserts with kickoff_data: {:?}, deposit_data: {:?}, number of watchtower_challenges: {}",
2587                    self.signer.xonly_public_key, kickoff_data, deposit_data, watchtower_challenges.len());
2588                    self.send_asserts(
2589                        dbtx,
2590                        kickoff_data,
2591                        deposit_data,
2592                        watchtower_challenges,
2593                        payout_blockhash,
2594                        latest_blockhash,
2595                    )
2596                    .await?;
2597                    Ok(DutyResult::Handled)
2598                }
2599                Duty::VerifierDisprove { .. } => Ok(DutyResult::Handled),
2600                Duty::SendLatestBlockhash {
2601                    kickoff_data,
2602                    deposit_data,
2603                    latest_blockhash,
2604                } => {
2605                    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);
2606                    self.send_latest_blockhash(dbtx, kickoff_data, deposit_data, latest_blockhash)
2607                        .await?;
2608                    Ok(DutyResult::Handled)
2609                }
2610                Duty::CheckIfKickoff {
2611                    txid,
2612                    block_height,
2613                    witness,
2614                    challenged_before: _,
2615                } => {
2616                    tracing::debug!(
2617                        "Operator {:?} called check if kickoff with txid: {:?}, block_height: {:?}",
2618                        self.signer.xonly_public_key,
2619                        txid,
2620                        block_height,
2621                    );
2622
2623                    let kickoff_data = self
2624                        .db
2625                        .get_deposit_data_with_kickoff_txid(Some(dbtx), txid)
2626                        .await?;
2627                    if let Some((deposit_data, kickoff_data)) = kickoff_data {
2628                        StateManager::<Self>::dispatch_new_kickoff_machine(
2629                            &self.db,
2630                            dbtx,
2631                            kickoff_data,
2632                            block_height,
2633                            deposit_data.clone(),
2634                            witness,
2635                        )
2636                        .await?;
2637
2638                        // resend relevant txs
2639                        self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2640                            .await?;
2641                    }
2642
2643                    Ok(DutyResult::Handled)
2644                }
2645            }
2646        }
2647
2648        async fn create_txhandlers(
2649            &self,
2650            dbtx: DatabaseTransaction<'_>,
2651            tx_type: TransactionType,
2652            contract_context: ContractContext,
2653        ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
2654            let mut db_cache =
2655                ReimburseDbCache::from_context(self.db.clone(), &contract_context, Some(dbtx));
2656            let txhandlers = create_txhandlers(
2657                tx_type,
2658                contract_context,
2659                &mut TxHandlerCache::new(),
2660                &mut db_cache,
2661            )
2662            .await?;
2663            Ok(txhandlers)
2664        }
2665
2666        async fn handle_finalized_block(
2667            &self,
2668            _dbtx: DatabaseTransaction<'_>,
2669            _block_id: u32,
2670            _block_height: u32,
2671            _block_cache: Arc<block_cache::BlockCache>,
2672            _light_client_proof_wait_interval_secs: Option<u32>,
2673        ) -> Result<(), BridgeError> {
2674            Ok(())
2675        }
2676
2677        fn is_kickoff_relevant_for_owner(&self, kickoff_data: &KickoffData) -> bool {
2678            kickoff_data.operator_xonly_pk == self.signer.xonly_public_key
2679        }
2680    }
2681}
2682
2683#[cfg(test)]
2684mod tests {
2685    use crate::operator::Operator;
2686    use crate::test::common::citrea::MockCitreaClient;
2687    use crate::test::common::*;
2688    use bitcoin::hashes::Hash;
2689    use bitcoin::{OutPoint, Txid};
2690
2691    #[tokio::test]
2692    #[ignore = "Design changes in progress"]
2693    async fn get_winternitz_public_keys() {
2694        let mut config = create_test_config_with_thread_name().await;
2695        let _regtest = create_regtest_rpc(&mut config).await;
2696
2697        let operator = Operator::<MockCitreaClient>::new(config.clone())
2698            .await
2699            .unwrap();
2700
2701        let deposit_outpoint = OutPoint {
2702            txid: Txid::all_zeros(),
2703            vout: 2,
2704        };
2705
2706        let winternitz_public_key = operator
2707            .generate_assert_winternitz_pubkeys(deposit_outpoint)
2708            .unwrap();
2709        assert_eq!(
2710            winternitz_public_key.len(),
2711            config.protocol_paramset().num_round_txs
2712                * config.protocol_paramset().num_kickoffs_per_round
2713        );
2714    }
2715
2716    #[tokio::test]
2717    async fn operator_get_params() {
2718        let mut config = create_test_config_with_thread_name().await;
2719        let _regtest = create_regtest_rpc(&mut config).await;
2720
2721        let operator = Operator::<MockCitreaClient>::new(config.clone())
2722            .await
2723            .unwrap();
2724        let actual_wpks = operator.generate_kickoff_winternitz_pubkeys().unwrap();
2725
2726        let (mut wpk_rx, _) = operator.get_params().await.unwrap();
2727        let mut idx = 0;
2728        while let Some(wpk) = wpk_rx.recv().await {
2729            assert_eq!(actual_wpks[idx], wpk);
2730            idx += 1;
2731        }
2732        assert_eq!(idx, actual_wpks.len());
2733    }
2734}