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