clementine_core/
verifier.rs

1use crate::actor::{verify_schnorr, Actor, TweakCache, WinternitzDerivationPath};
2use crate::bitcoin_syncer::BitcoinSyncer;
3use crate::bitvm_client::{ClementineBitVMPublicKeys, REPLACE_SCRIPTS_LOCK};
4use crate::builder::address::{create_taproot_address, taproot_builder_with_scripts};
5use crate::builder::block_cache;
6use crate::builder::script::{
7    extract_winternitz_commits, extract_winternitz_commits_with_sigs, SpendableScript,
8    TimelockScript, WinternitzCommit,
9};
10use crate::builder::sighash::{
11    create_nofn_sighash_stream, create_operator_sighash_stream, PartialSignatureInfo, SignatureInfo,
12};
13use crate::builder::transaction::deposit_signature_owner::EntityType;
14use crate::builder::transaction::input::UtxoVout;
15use crate::builder::transaction::sign::{create_and_sign_txs, TransactionRequestData};
16#[cfg(feature = "automation")]
17use crate::builder::transaction::ReimburseDbCache;
18use crate::builder::transaction::{
19    create_emergency_stop_txhandler, create_move_to_vault_txhandler,
20    create_optimistic_payout_txhandler, ContractContext, TxHandler,
21};
22use crate::builder::transaction::{create_round_txhandlers, KickoffWinternitzKeys};
23use crate::citrea::CitreaClientT;
24use crate::config::protocol::ProtocolParamset;
25use crate::config::BridgeConfig;
26use crate::constants::{
27    self, MAX_ALL_SESSIONS_BYTES, MAX_EXTRA_WATCHTOWERS, MAX_NUM_SESSIONS,
28    NON_EPHEMERAL_ANCHOR_AMOUNT, NUM_NONCES_LIMIT, TEN_MINUTES_IN_SECS,
29};
30use crate::database::{Database, DatabaseTransaction};
31use crate::deposit::{DepositData, KickoffData, OperatorData};
32use crate::extended_bitcoin_rpc::{BridgeRpcQueries, ExtendedBitcoinRpc};
33#[cfg(feature = "automation")]
34use crate::header_chain_prover::HeaderChainProver;
35use crate::metrics::SyncStatusProvider;
36use crate::musig2;
37#[cfg(feature = "automation")]
38use crate::operator::Operator;
39use crate::rpc::clementine::{EntityStatus, NormalSignatureKind, OperatorKeys, TaggedSignature};
40use crate::rpc::ecdsa_verification_sig::{
41    recover_address_from_ecdsa_signature, OptimisticPayoutMessage,
42};
43#[cfg(feature = "automation")]
44use crate::states::StateManager;
45use crate::task::entity_metric_publisher::{
46    EntityMetricPublisher, ENTITY_METRIC_PUBLISHER_INTERVAL,
47};
48use crate::task::lcp_syncer::LcpSyncerTask;
49use crate::task::manager::BackgroundTaskManager;
50use crate::task::{IntoTask, TaskExt};
51
52#[cfg(feature = "automation")]
53use crate::tx_sender::{TxSender, TxSenderClient};
54#[cfg(feature = "automation")]
55use crate::tx_sender_queue::TxSenderClientQueueExt;
56#[cfg(feature = "automation")]
57use crate::utils::FeePayingType;
58use crate::utils::TxMetadata;
59use crate::utils::{monitor_standalone_task, NamedEntity};
60use alloy::primitives::PrimitiveSignature;
61use bitcoin::hashes::Hash;
62use bitcoin::key::rand::Rng;
63use bitcoin::key::Secp256k1;
64use bitcoin::secp256k1::schnorr::Signature;
65use bitcoin::secp256k1::Message;
66use bitcoin::taproot::{self, TaprootBuilder};
67use bitcoin::{Address, Amount, ScriptBuf, Txid, Witness, XOnlyPublicKey};
68use bitcoin::{OutPoint, TxOut};
69use bitcoincore_rpc::RpcApi;
70use bitvm::clementine::additional_disprove::{
71    replace_placeholders_in_script, validate_assertions_for_additional_script,
72};
73use bitvm::signatures::winternitz;
74#[cfg(feature = "automation")]
75use circuits_lib::bridge_circuit::groth16::CircuitGroth16Proof;
76use circuits_lib::bridge_circuit::transaction::CircuitTransaction;
77use circuits_lib::bridge_circuit::{
78    deposit_constant, get_first_op_return_output, parse_op_return_data,
79};
80use circuits_lib::common::constants::MAX_NUMBER_OF_WATCHTOWERS;
81use clementine_errors::BridgeError;
82use clementine_errors::{TransactionType, TxError};
83use clementine_primitives::RoundIndex;
84use clementine_primitives::UTXO;
85use eyre::{Context, ContextCompat, OptionExt, Result};
86use secp256k1::ffi::MUSIG_SECNONCE_LEN;
87use secp256k1::musig::{AggregatedNonce, PartialSignature, PublicNonce, SecretNonce};
88#[cfg(feature = "automation")]
89use std::collections::BTreeMap;
90use std::collections::{HashMap, HashSet, VecDeque};
91use std::pin::pin;
92use std::sync::Arc;
93use std::time::Duration;
94use tokio::sync::mpsc;
95use tokio_stream::StreamExt;
96
97#[derive(Debug)]
98pub struct NonceSession {
99    /// Nonces used for a deposit session (last nonce is for the movetx signature)
100    pub nonces: Vec<SecretNonce>,
101}
102
103#[derive(Debug)]
104pub struct AllSessions {
105    sessions: HashMap<u128, NonceSession>,
106    session_queue: VecDeque<u128>,
107    /// store all previously used ids to never use them again
108    /// reason is that we remove a session in deposit_sign and add it back later, we might
109    /// create a new one with the same id in between removal and addition
110    used_ids: HashSet<u128>,
111}
112
113impl AllSessions {
114    pub fn new() -> Self {
115        Self {
116            sessions: HashMap::new(),
117            session_queue: VecDeque::new(),
118            used_ids: HashSet::new(),
119        }
120    }
121
122    /// Adds a new session to the AllSessions with the given id..
123    /// If the current byte size of all sessions exceeds MAX_ALL_SESSIONS_BYTES, the oldest session is removed until the byte size is under the limit.
124    pub fn add_new_session_with_id(
125        &mut self,
126        new_nonce_session: NonceSession,
127        id: u128,
128    ) -> Result<(), eyre::Report> {
129        if new_nonce_session.nonces.is_empty() {
130            // empty session, return error
131            return Err(eyre::eyre!("Empty session attempted to be added"));
132        }
133
134        if self.sessions.contains_key(&id) {
135            return Err(eyre::eyre!("Nonce session with id {id} already exists"));
136        }
137
138        let mut total_needed = Self::session_bytes(&new_nonce_session)?
139            .checked_add(self.total_sessions_byte_size()?)
140            .ok_or_else(|| eyre::eyre!("Session size calculation overflow in add_new_session"))?;
141
142        loop {
143            // check byte size and session count, if session count is already at the limit or byte size is higher than limit
144            // we remove the oldest session until the conditions are met
145            if total_needed <= MAX_ALL_SESSIONS_BYTES && self.sessions.len() < MAX_NUM_SESSIONS {
146                break;
147            }
148            total_needed = total_needed
149                .checked_sub(self.remove_oldest_session()?)
150                .ok_or_else(|| eyre::eyre!("Session size calculation overflow"))?;
151        }
152
153        // save the session to the HashMap and the session id queue
154        self.sessions.insert(id, new_nonce_session);
155        self.session_queue.push_back(id);
156        self.used_ids.insert(id);
157        Ok(())
158    }
159
160    /// Adds a new session to the AllSessions with a random id.
161    /// Returns the id of the added session.
162    pub fn add_new_session_with_random_id(
163        &mut self,
164        new_nonce_session: NonceSession,
165    ) -> Result<u128, eyre::Report> {
166        // generate unused id
167        let random_id = self.get_new_unused_id();
168        self.add_new_session_with_id(new_nonce_session, random_id)?;
169        Ok(random_id)
170    }
171
172    /// Removes a session from the AllSessions with the given id.
173    /// Also removes it from the session queue, because we might add the session with the same id later
174    /// (as in [`deposit_sign`]).
175    /// Returns the removed session.
176    pub fn remove_session_with_id(&mut self, id: u128) -> Result<NonceSession, eyre::Report> {
177        let session = self.sessions.remove(&id).ok_or_eyre("Session not found")?;
178        // remove the id from the session queue
179        self.session_queue.retain(|x| *x != id);
180        Ok(session)
181    }
182
183    /// Generates a new unused id for a nonce session.
184    /// The important thing it that the id not easily predictable.
185    fn get_new_unused_id(&mut self) -> u128 {
186        let mut random_id = bitcoin::secp256k1::rand::thread_rng().gen_range(0..=u128::MAX);
187        while self.used_ids.contains(&random_id) {
188            random_id = bitcoin::secp256k1::rand::thread_rng().gen_range(0..=u128::MAX);
189        }
190        random_id
191    }
192
193    /// Removes the oldest session from the AllSessions.
194    /// Returns the number of bytes removed.
195    fn remove_oldest_session(&mut self) -> Result<usize, eyre::Report> {
196        match self.session_queue.pop_front() {
197            Some(oldest_id) => {
198                let removed_session = self.sessions.remove(&oldest_id);
199                match removed_session {
200                    Some(session) => Ok(Self::session_bytes(&session)?),
201                    None => Ok(0),
202                }
203            }
204            None => Err(eyre::eyre!("No session to remove")),
205        }
206    }
207
208    fn session_bytes(session: &NonceSession) -> Result<usize, eyre::Report> {
209        // 132 bytes per nonce
210        session
211            .nonces
212            .len()
213            .checked_mul(MUSIG_SECNONCE_LEN)
214            .ok_or_eyre("Calculation overflow in session_bytes")
215    }
216
217    /// Returns the total byte size of all secnonces in the AllSessions.
218    pub fn total_sessions_byte_size(&self) -> Result<usize, eyre::Report> {
219        // Should never overflow as it counts bytes in usize
220        let mut total_bytes: usize = 0;
221
222        for (_, session) in self.sessions.iter() {
223            total_bytes = total_bytes
224                .checked_add(Self::session_bytes(session)?)
225                .ok_or_eyre("Calculation overflow in total_byte_size")?;
226        }
227
228        Ok(total_bytes)
229    }
230}
231
232impl Default for AllSessions {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238pub struct VerifierServer<C: CitreaClientT> {
239    pub verifier: Verifier<C>,
240    background_tasks: BackgroundTaskManager,
241}
242
243impl<C> VerifierServer<C>
244where
245    C: CitreaClientT,
246{
247    pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
248        let verifier = Verifier::new(config.clone()).await?;
249        let background_tasks = BackgroundTaskManager::default();
250
251        Ok(VerifierServer {
252            verifier,
253            background_tasks,
254        })
255    }
256
257    /// Starts the background tasks for the verifier.
258    /// If called multiple times, it will restart only the tasks that are not already running.
259    pub async fn start_background_tasks(&self) -> Result<(), BridgeError> {
260        let rpc = ExtendedBitcoinRpc::connect(
261            self.verifier.config.bitcoin_rpc_url.clone(),
262            self.verifier.config.bitcoin_rpc_user.clone(),
263            self.verifier.config.bitcoin_rpc_password.clone(),
264            None,
265        )
266        .await?;
267
268        // initialize and run automation features
269        #[cfg(feature = "automation")]
270        {
271            let tx_sender_cfg = self.verifier.config.tx_sender_config();
272            let tx_sender = TxSender::new(tx_sender_cfg).await?;
273
274            self.background_tasks
275                .ensure_task_looping(tx_sender.into_task())
276                .await;
277            let state_manager = StateManager::new(
278                self.verifier.db.clone(),
279                self.verifier.clone(),
280                self.verifier.rpc.clone(),
281                self.verifier.config.clone(),
282            )
283            .await?;
284
285            let should_run_state_mgr = {
286                #[cfg(test)]
287                {
288                    self.verifier.config.test_params.should_run_state_manager
289                }
290                #[cfg(not(test))]
291                {
292                    true
293                }
294            };
295
296            if should_run_state_mgr {
297                // start tracking operators if they exist in the db
298                let operators = self.verifier.db.get_operators(None).await?;
299                if !operators.is_empty() {
300                    let mut dbtx = self.verifier.db.begin_transaction().await?;
301                    for operator in operators {
302                        StateManager::<Verifier<C>>::dispatch_new_round_machine(
303                            &self.verifier.db,
304                            &mut dbtx,
305                            OperatorData {
306                                xonly_pk: operator.0,
307                                reimburse_addr: operator.1,
308                                collateral_funding_outpoint: operator.2,
309                            },
310                        )
311                        .await?;
312                    }
313                    dbtx.commit().await?;
314                }
315                self.background_tasks
316                    .ensure_task_looping(state_manager.block_fetcher_task().await?)
317                    .await;
318                self.background_tasks
319                    .ensure_task_looping(state_manager.into_task())
320                    .await;
321            }
322        }
323
324        self.background_tasks
325            .ensure_task_looping(
326                LcpSyncerTask::new(
327                    self.verifier.db.clone(),
328                    Verifier::<C>::LCP_SYNCER_CONSUMER_ID.to_string(),
329                    self.verifier.config.protocol_paramset(),
330                    self.verifier.clone(),
331                )
332                .await?
333                .into_buffered_errors(20, 3, Duration::from_secs(10))
334                .with_delay(crate::bitcoin_syncer::BTC_SYNCER_POLL_DELAY),
335            )
336            .await;
337
338        let syncer = BitcoinSyncer::new(
339            self.verifier.db.clone(),
340            rpc.clone(),
341            self.verifier.config.protocol_paramset(),
342        )
343        .await?;
344
345        self.background_tasks
346            .ensure_task_looping(syncer.into_task())
347            .await;
348
349        self.background_tasks
350            .ensure_task_looping(
351                EntityMetricPublisher::<Verifier<C>, C>::new(
352                    self.verifier.db.clone(),
353                    rpc.clone(),
354                    self.verifier.config.clone(),
355                    self.verifier.citrea_client.clone(),
356                )
357                .with_delay(ENTITY_METRIC_PUBLISHER_INTERVAL),
358            )
359            .await;
360
361        Ok(())
362    }
363
364    pub async fn get_current_status(&self) -> Result<EntityStatus, BridgeError> {
365        let stopped_tasks = self.background_tasks.get_stopped_tasks().await?;
366        // Determine if automation is enabled
367        let automation_enabled = cfg!(feature = "automation");
368
369        let sync_status = Verifier::<C>::get_sync_status(
370            &self.verifier.db,
371            &self.verifier.rpc,
372            &self.verifier.config,
373            &self.verifier.citrea_client,
374        )
375        .await?;
376
377        Ok(EntityStatus {
378            automation: automation_enabled,
379            wallet_balance: sync_status
380                .wallet_balance
381                .map(|balance| format!("{} BTC", balance.to_btc())),
382            tx_sender_synced_height: sync_status.tx_sender_synced_height,
383            finalized_synced_height: sync_status.finalized_synced_height,
384            hcp_last_proven_height: sync_status.hcp_last_proven_height,
385            rpc_tip_height: sync_status.rpc_tip_height,
386            bitcoin_syncer_synced_height: sync_status.btc_syncer_synced_height,
387            stopped_tasks: Some(stopped_tasks),
388            state_manager_next_height: sync_status.state_manager_next_height,
389            btc_fee_rate_sat_vb: sync_status.bitcoin_fee_rate_sat_vb,
390            citrea_l2_block_height: sync_status.citrea_l2_block_height,
391            lcp_synced_height: sync_status.lcp_synced_height,
392        })
393    }
394}
395
396#[derive(Debug, Clone)]
397pub struct Verifier<C: CitreaClientT> {
398    rpc: ExtendedBitcoinRpc,
399
400    pub(crate) signer: Actor,
401    pub(crate) db: Database,
402    pub(crate) config: BridgeConfig,
403    pub(crate) nonces: Arc<tokio::sync::Mutex<AllSessions>>,
404    #[cfg(feature = "automation")]
405    pub tx_sender: TxSenderClient,
406    #[cfg(feature = "automation")]
407    pub header_chain_prover: HeaderChainProver,
408    pub citrea_client: C,
409}
410
411impl<C> Verifier<C>
412where
413    C: CitreaClientT,
414{
415    pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
416        let signer = Actor::new(config.secret_key, config.protocol_paramset().network);
417
418        let rpc = ExtendedBitcoinRpc::connect(
419            config.bitcoin_rpc_url.clone(),
420            config.bitcoin_rpc_user.clone(),
421            config.bitcoin_rpc_password.clone(),
422            None,
423        )
424        .await?;
425
426        let db = Database::new(&config).await?;
427
428        let citrea_client = C::new(
429            config.citrea_rpc_url.clone(),
430            config.citrea_light_client_prover_url.clone(),
431            config.citrea_chain_id,
432            None,
433            config.citrea_request_timeout,
434        )
435        .await?;
436
437        let all_sessions = AllSessions::new();
438
439        #[cfg(feature = "automation")]
440        let tx_sender =
441            TxSenderClient::new(clementine_tx_sender::TxSenderDb::from_pool(db.get_pool()));
442
443        #[cfg(feature = "automation")]
444        let header_chain_prover = HeaderChainProver::new(&config, rpc.clone()).await?;
445
446        let verifier = Verifier {
447            rpc,
448            signer,
449            db: db.clone(),
450            config: config.clone(),
451            nonces: Arc::new(tokio::sync::Mutex::new(all_sessions)),
452            #[cfg(feature = "automation")]
453            tx_sender,
454            #[cfg(feature = "automation")]
455            header_chain_prover,
456            citrea_client,
457        };
458        Ok(verifier)
459    }
460
461    /// Verifies all unspent kickoff signatures sent by the operator, converts them to TaggedSignature
462    /// as they will be saved as TaggedSignatures to the db.
463    fn verify_unspent_kickoff_sigs(
464        &self,
465        collateral_funding_outpoint: OutPoint,
466        operator_xonly_pk: XOnlyPublicKey,
467        wallet_reimburse_address: Address,
468        unspent_kickoff_sigs: Vec<Signature>,
469        kickoff_wpks: &KickoffWinternitzKeys,
470    ) -> Result<Vec<TaggedSignature>, BridgeError> {
471        let mut tweak_cache = TweakCache::default();
472        let mut tagged_sigs = Vec::with_capacity(unspent_kickoff_sigs.len());
473        let mut prev_ready_to_reimburse: Option<TxHandler> = None;
474        let operator_data = OperatorData {
475            xonly_pk: operator_xonly_pk,
476            collateral_funding_outpoint,
477            reimburse_addr: wallet_reimburse_address.clone(),
478        };
479        let mut cur_sig_index = 0;
480        for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
481            let txhandlers = create_round_txhandlers(
482                self.config.protocol_paramset(),
483                round_idx,
484                &operator_data,
485                kickoff_wpks,
486                prev_ready_to_reimburse.as_ref(),
487            )?;
488            for txhandler in txhandlers {
489                if let TransactionType::UnspentKickoff(kickoff_idx) =
490                    txhandler.get_transaction_type()
491                {
492                    let partial = PartialSignatureInfo {
493                        operator_idx: 0, // dummy value
494                        round_idx,
495                        kickoff_utxo_idx: kickoff_idx,
496                    };
497                    let sighashes = txhandler
498                        .calculate_shared_txins_sighash(EntityType::OperatorSetup, partial)?;
499                    for sighash in sighashes {
500                        let message = Message::from_digest(sighash.0.to_byte_array());
501                        verify_schnorr(
502                            &unspent_kickoff_sigs[cur_sig_index],
503                            &message,
504                            operator_xonly_pk,
505                            sighash.1.tweak_data,
506                            Some(&mut tweak_cache),
507                        )
508                        .map_err(|e| {
509                            eyre::eyre!(
510                                "Verifier{}: Unspent kickoff signature verification failed for num sig {}: {}",
511                                self.signer.xonly_public_key.to_string(),
512                                cur_sig_index + 1,
513                                e
514                            )
515                        })?;
516                        tagged_sigs.push(TaggedSignature {
517                            signature: unspent_kickoff_sigs[cur_sig_index].serialize().to_vec(),
518                            signature_id: Some(sighash.1.signature_id),
519                        });
520                        cur_sig_index += 1;
521                    }
522                } else if let TransactionType::ReadyToReimburse = txhandler.get_transaction_type() {
523                    prev_ready_to_reimburse = Some(txhandler);
524                }
525            }
526        }
527
528        Ok(tagged_sigs)
529    }
530
531    /// Checks if all operators in verifier's db that are still in protocol are in the deposit.
532    /// Checks if all operators in the deposit data from aggregator are in the verifier's DB.
533    /// Afterwards, it checks if the given deposit outpoint is valid. First it checks if the tx exists on chain,
534    /// then it checks if the amount in TxOut is equal to bridge_amount and if the script is correct.
535    ///
536    /// # Arguments
537    /// * `deposit_data` - The deposit data to check.
538    ///
539    /// # Returns
540    /// * `()` if the deposit is valid, `BridgeError::InvalidDeposit` if the deposit is invalid.
541    async fn is_deposit_valid(&self, deposit_data: &mut DepositData) -> Result<(), BridgeError> {
542        // check if security council is the same as in our config
543        if deposit_data.security_council != self.config.security_council {
544            let reason = format!(
545                "Security council in deposit is not the same as in the config, expected {:?}, got {:?}",
546                self.config.security_council,
547                deposit_data.security_council
548            );
549            tracing::error!("{reason}");
550            return Err(BridgeError::InvalidDeposit(reason));
551        }
552        // check if extra watchtowers (non verifier watchtowers) are not greater than the maximum allowed
553        if deposit_data.actors.watchtowers.len() > MAX_EXTRA_WATCHTOWERS {
554            let reason = format!(
555                "Number of extra watchtowers in deposit is greater than the maximum allowed, expected at most {}, got {}",
556                MAX_EXTRA_WATCHTOWERS,
557                deposit_data.actors.watchtowers.len()
558            );
559            tracing::error!("{reason}");
560            return Err(BridgeError::InvalidDeposit(reason));
561        }
562        // check if total watchtowers are not greater than the maximum allowed
563        if deposit_data.get_num_watchtowers() > MAX_NUMBER_OF_WATCHTOWERS {
564            let reason = format!(
565                "Number of watchtowers in deposit is greater than the maximum allowed, expected at most {}, got {}",
566                MAX_NUMBER_OF_WATCHTOWERS,
567                deposit_data.get_num_watchtowers()
568            );
569            tracing::error!("{reason}");
570            return Err(BridgeError::InvalidDeposit(reason));
571        }
572
573        // check if all verifiers are unique
574        if !deposit_data.are_all_verifiers_unique() {
575            let reason = format!(
576                "Verifiers in deposit are not unique: {:?}",
577                deposit_data.actors.verifiers
578            );
579            tracing::error!("{reason}");
580            return Err(BridgeError::InvalidDeposit(reason));
581        }
582
583        // check if all watchtowers are unique
584        if !deposit_data.are_all_watchtowers_unique() {
585            let reason = format!(
586                "Watchtowers in deposit are not unique: {:?}",
587                deposit_data.actors.watchtowers
588            );
589            tracing::error!("{reason}");
590            return Err(BridgeError::InvalidDeposit(reason));
591        }
592
593        // check if all operators are unique
594        if !deposit_data.are_all_operators_unique() {
595            let reason = format!(
596                "Operators in deposit are not unique: {:?}",
597                deposit_data.actors.operators
598            );
599            tracing::error!("{reason}");
600            return Err(BridgeError::InvalidDeposit(reason));
601        }
602
603        let operators_in_deposit_data = deposit_data.get_operators();
604        // check if all operators that still have collateral are in the deposit
605        let operators_in_db = self.db.get_operators(None).await?;
606        for (xonly_pk, reimburse_addr, collateral_funding_outpoint) in operators_in_db.iter() {
607            let operator_data = OperatorData {
608                xonly_pk: *xonly_pk,
609                collateral_funding_outpoint: *collateral_funding_outpoint,
610                reimburse_addr: reimburse_addr.clone(),
611            };
612            let kickoff_winternitz_pks = self
613                .db
614                .get_operator_kickoff_winternitz_public_keys(None, *xonly_pk)
615                .await?;
616            let kickoff_wpks = KickoffWinternitzKeys::new(
617                kickoff_winternitz_pks,
618                self.config.protocol_paramset().num_kickoffs_per_round,
619                self.config.protocol_paramset().num_round_txs,
620            )?;
621            let is_collateral_usable = self
622                .rpc
623                .collateral_check(
624                    &operator_data,
625                    &kickoff_wpks,
626                    self.config.protocol_paramset(),
627                )
628                .await?;
629            // if operator is not in deposit but its collateral is still on chain, return false
630            if !operators_in_deposit_data.contains(xonly_pk) && is_collateral_usable {
631                let reason = format!(
632                    "Operator {xonly_pk:?} is is still in protocol but not in the deposit data from aggregator",
633                );
634                tracing::error!("{reason}");
635                return Err(BridgeError::InvalidDeposit(reason));
636            }
637            // if operator is in deposit, but the collateral is not usable, return false
638            if operators_in_deposit_data.contains(xonly_pk) && !is_collateral_usable {
639                let reason = format!(
640                    "Operator {xonly_pk:?} is in the deposit data from aggregator but its collateral is spent, operator cannot fulfill withdrawals anymore",
641                );
642                tracing::error!("{reason}");
643                return Err(BridgeError::InvalidDeposit(reason));
644            }
645        }
646        // check if there are any operators in the deposit that are not in the DB.
647        for operator_xonly_pk in operators_in_deposit_data {
648            if !operators_in_db
649                .iter()
650                .any(|(xonly_pk, _, _)| xonly_pk == &operator_xonly_pk)
651            {
652                let reason = format!(
653                    "Operator {operator_xonly_pk:?} is in the deposit data from aggregator but not in the verifier's DB, cannot sign deposit"
654                );
655                tracing::error!("{reason}");
656                return Err(BridgeError::InvalidDeposit(reason));
657            }
658        }
659        // check if deposit script in deposit_outpoint is valid
660        let deposit_scripts: Vec<ScriptBuf> = deposit_data
661            .get_deposit_scripts(self.config.protocol_paramset())?
662            .into_iter()
663            .map(|s| s.to_script_buf())
664            .collect();
665        // what the deposit scriptpubkey is in the deposit_outpoint should be according to the deposit data
666        let expected_scriptpubkey = create_taproot_address(
667            &deposit_scripts,
668            None,
669            self.config.protocol_paramset().network,
670        )
671        .0
672        .script_pubkey();
673        let deposit_outpoint = deposit_data.get_deposit_outpoint();
674        let deposit_txid = deposit_outpoint.txid;
675        let deposit_tx = self
676            .rpc
677            .get_tx_of_txid(&deposit_txid)
678            .await
679            .wrap_err("Deposit tx could not be found on chain")?;
680        let deposit_txout_in_chain = deposit_tx
681            .output
682            .get(deposit_outpoint.vout as usize)
683            .ok_or(eyre::eyre!(
684                "Deposit vout not found in tx {}, vout: {}",
685                deposit_txid,
686                deposit_outpoint.vout
687            ))?;
688        if deposit_txout_in_chain.value != self.config.protocol_paramset().bridge_amount {
689            let reason = format!(
690                "Deposit amount is not correct, expected {}, got {}",
691                self.config.protocol_paramset().bridge_amount,
692                deposit_txout_in_chain.value
693            );
694            tracing::error!("{reason}");
695            return Err(BridgeError::InvalidDeposit(reason));
696        }
697        if deposit_txout_in_chain.script_pubkey != expected_scriptpubkey {
698            let reason = format!(
699                "Deposit script pubkey in deposit outpoint does not match the deposit data, expected {:?}, got {:?}",
700                expected_scriptpubkey,
701                deposit_txout_in_chain.script_pubkey
702            );
703            tracing::error!("{reason}");
704            return Err(BridgeError::InvalidDeposit(reason));
705        }
706        // check if deposit outpoint is included in a block with height >= start_height
707        let tx_info = self
708            .rpc
709            .get_raw_transaction_info(&deposit_txid, None)
710            .await
711            .wrap_err("Failed to get deposit transaction info")?;
712        let blockhash = tx_info.blockhash.ok_or_else(|| {
713            BridgeError::InvalidDeposit("Deposit transaction is not confirmed".to_string())
714        })?;
715        let block_height = self
716            .rpc
717            .get_block_info(&blockhash)
718            .await
719            .wrap_err(format!(
720                "Failed to get block info for deposit tx block hash: {blockhash}",
721            ))?
722            .height;
723        let start_height = self.config.protocol_paramset().start_height;
724        if (block_height as u32) < start_height {
725            let reason = format!(
726                "Deposit transaction is included in a block with height {block_height} which is less than start_height {start_height}",
727            );
728            tracing::error!("{reason}");
729            return Err(BridgeError::InvalidDeposit(reason));
730        }
731        Ok(())
732    }
733
734    pub async fn set_operator(
735        &self,
736        collateral_funding_outpoint: OutPoint,
737        operator_xonly_pk: XOnlyPublicKey,
738        wallet_reimburse_address: Address,
739        operator_winternitz_public_keys: Vec<winternitz::PublicKey>,
740        unspent_kickoff_sigs: Vec<Signature>,
741    ) -> Result<(), BridgeError> {
742        tracing::info!("Setting operator: {:?}", operator_xonly_pk);
743        let operator_data = OperatorData {
744            xonly_pk: operator_xonly_pk,
745            collateral_funding_outpoint,
746            reimburse_addr: wallet_reimburse_address,
747        };
748
749        let kickoff_wpks = KickoffWinternitzKeys::new(
750            operator_winternitz_public_keys,
751            self.config.protocol_paramset().num_kickoffs_per_round,
752            self.config.protocol_paramset().num_round_txs,
753        )?;
754
755        if !self
756            .rpc
757            .collateral_check(
758                &operator_data,
759                &kickoff_wpks,
760                self.config.protocol_paramset(),
761            )
762            .await?
763        {
764            return Err(eyre::eyre!(
765                "Collateral utxo of operator {:?} does not exist or is not usable in bitcoin, cannot set operator",
766                operator_xonly_pk,
767            )
768            .into());
769        }
770
771        let tagged_sigs = self.verify_unspent_kickoff_sigs(
772            collateral_funding_outpoint,
773            operator_xonly_pk,
774            operator_data.reimburse_addr.clone(),
775            unspent_kickoff_sigs,
776            &kickoff_wpks,
777        )?;
778
779        let operator_winternitz_public_keys = kickoff_wpks.get_all_keys();
780        let mut dbtx = self.db.begin_transaction().await?;
781        // Save the operator details to the db
782        self.db
783            .insert_operator_if_not_exists(
784                Some(&mut dbtx),
785                operator_xonly_pk,
786                &operator_data.reimburse_addr,
787                collateral_funding_outpoint,
788            )
789            .await?;
790
791        self.db
792            .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
793                Some(&mut dbtx),
794                operator_xonly_pk,
795                operator_winternitz_public_keys,
796            )
797            .await?;
798
799        let sigs_per_round = self.config.get_num_unspent_kickoff_sigs()
800            / self.config.protocol_paramset().num_round_txs;
801        let tagged_sigs_per_round: Vec<Vec<TaggedSignature>> = tagged_sigs
802            .chunks(sigs_per_round)
803            .map(|chunk| chunk.to_vec())
804            .collect();
805
806        for (round_idx, sigs) in tagged_sigs_per_round.into_iter().enumerate() {
807            self.db
808                .insert_unspent_kickoff_sigs_if_not_exist(
809                    Some(&mut dbtx),
810                    operator_xonly_pk,
811                    RoundIndex::Round(round_idx),
812                    sigs,
813                )
814                .await?;
815        }
816
817        #[cfg(feature = "automation")]
818        {
819            StateManager::<Self>::dispatch_new_round_machine(&self.db, &mut dbtx, operator_data)
820                .await?;
821        }
822        dbtx.commit().await?;
823        tracing::info!("Operator: {:?} set successfully", operator_xonly_pk);
824        Ok(())
825    }
826
827    pub async fn nonce_gen(
828        &self,
829        num_nonces: u32,
830    ) -> Result<(u128, Vec<PublicNonce>), BridgeError> {
831        // reject if too many nonces are requested
832        if num_nonces > NUM_NONCES_LIMIT {
833            return Err(eyre::eyre!(
834                "Number of nonces requested is too high, max allowed is {}, requested: {}",
835                NUM_NONCES_LIMIT,
836                num_nonces
837            )
838            .into());
839        }
840        if num_nonces == 0 {
841            return Err(
842                eyre::eyre!("Number of nonces requested is 0, cannot generate nonces").into(),
843            );
844        }
845        let (sec_nonces, pub_nonces): (Vec<SecretNonce>, Vec<PublicNonce>) = (0..num_nonces)
846            .map(|_| {
847                // nonce pair needs keypair and a rng
848                let (sec_nonce, pub_nonce) = musig2::nonce_pair(&self.signer.keypair)?;
849                Ok((sec_nonce, pub_nonce))
850            })
851            .collect::<Result<Vec<(SecretNonce, PublicNonce)>, BridgeError>>()?
852            .into_iter()
853            .unzip();
854
855        let session = NonceSession { nonces: sec_nonces };
856
857        // save the session
858        let session_id = {
859            let all_sessions = &mut *self.nonces.lock().await;
860            all_sessions.add_new_session_with_random_id(session)?
861        };
862
863        Ok((session_id, pub_nonces))
864    }
865
866    pub async fn deposit_sign(
867        &self,
868        mut deposit_data: DepositData,
869        session_id: u128,
870        mut agg_nonce_rx: mpsc::Receiver<AggregatedNonce>,
871    ) -> Result<mpsc::Receiver<Result<PartialSignature, BridgeError>>, BridgeError> {
872        self.citrea_client
873            .check_nofn_correctness(deposit_data.get_nofn_xonly_pk()?)
874            .await?;
875
876        self.is_deposit_valid(&mut deposit_data).await?;
877
878        // set deposit data to db before starting to sign, ensures that if the deposit data already exists in db, it matches the one
879        // given by the aggregator currently. We do not want to sign 2 different deposits for same deposit_outpoint
880        self.db
881            .insert_deposit_data_if_not_exists(
882                None,
883                &mut deposit_data,
884                self.config.protocol_paramset(),
885            )
886            .await?;
887
888        let verifier = self.clone();
889        let (partial_sig_tx, partial_sig_rx) = mpsc::channel(constants::DEFAULT_CHANNEL_SIZE);
890        let verifier_index = deposit_data.get_verifier_index(&self.signer.public_key)?;
891        let verifiers_public_keys = deposit_data.get_verifiers();
892        let monitor_sender = partial_sig_tx.clone();
893
894        let deposit_blockhash = self
895            .rpc
896            .get_blockhash_of_tx(&deposit_data.get_deposit_outpoint().txid)
897            .await?;
898
899        let handle = tokio::spawn(async move {
900            // Take the lock and extract the session before entering the async block
901            // Extract the session and remove it from the map to release the lock early
902            let mut session = {
903                let mut session_map = verifier.nonces.lock().await;
904                session_map.remove_session_with_id(session_id)?
905            };
906            session.nonces.reverse();
907
908            let mut nonce_idx: usize = 0;
909
910            let mut sighash_stream = Box::pin(create_nofn_sighash_stream(
911                verifier.db.clone(),
912                verifier.config.clone(),
913                deposit_data.clone(),
914                deposit_blockhash,
915                false,
916            ));
917            let num_required_sigs = verifier.config.get_num_required_nofn_sigs(&deposit_data);
918
919            if num_required_sigs + 2 != session.nonces.len() {
920                return Err(eyre::eyre!(
921                    "Expected nonce count to be {} (num_required_sigs + 2, for movetx & emergency stop), got {}",
922                    num_required_sigs + 2,
923                    session.nonces.len()
924                ).into());
925            }
926
927            while let Some(agg_nonce) = agg_nonce_rx.recv().await {
928                let sighash = sighash_stream
929                    .next()
930                    .await
931                    .ok_or(eyre::eyre!("No sighash received"))??;
932                tracing::debug!("Verifier {} found sighash: {:?}", verifier_index, sighash);
933
934                let nonce = session
935                    .nonces
936                    .pop()
937                    .ok_or(eyre::eyre!("No nonce available"))?;
938
939                let partial_sig = musig2::partial_sign(
940                    verifiers_public_keys.clone(),
941                    None,
942                    nonce,
943                    agg_nonce,
944                    verifier.signer.keypair,
945                    Message::from_digest(*sighash.0.as_byte_array()),
946                )?;
947
948                partial_sig_tx
949                    .send(Ok(partial_sig))
950                    .await
951                    .wrap_err("Failed to send partial signature")?;
952
953                nonce_idx += 1;
954                tracing::debug!(
955                    "Verifier {} signed and sent sighash {} of {}",
956                    verifier_index,
957                    nonce_idx,
958                    num_required_sigs
959                );
960                if nonce_idx == num_required_sigs {
961                    break;
962                }
963            }
964
965            if session.nonces.len() != 2 {
966                return Err(eyre::eyre!(
967                    "Expected 2 nonces remaining in session, one for move tx and one for emergency stop, got {}, indicating aggregated nonce stream ended prematurely",
968                    session.nonces.len()
969                ).into());
970            }
971
972            let mut session_map = verifier.nonces.lock().await;
973            session_map.add_new_session_with_id(session, session_id)?;
974
975            Ok::<(), BridgeError>(())
976        });
977        monitor_standalone_task(handle, "Verifier deposit_sign", monitor_sender);
978
979        Ok(partial_sig_rx)
980    }
981
982    pub async fn deposit_finalize(
983        &self,
984        deposit_data: &mut DepositData,
985        session_id: u128,
986        mut sig_receiver: mpsc::Receiver<Signature>,
987        mut agg_nonce_receiver: mpsc::Receiver<AggregatedNonce>,
988        mut operator_sig_receiver: mpsc::Receiver<Signature>,
989    ) -> Result<(PartialSignature, PartialSignature), BridgeError> {
990        self.citrea_client
991            .check_nofn_correctness(deposit_data.get_nofn_xonly_pk()?)
992            .await?;
993
994        self.is_deposit_valid(deposit_data).await?;
995
996        let mut tweak_cache = TweakCache::default();
997        let deposit_blockhash = self
998            .rpc
999            .get_blockhash_of_tx(&deposit_data.get_deposit_outpoint().txid)
1000            .await?;
1001
1002        let mut sighash_stream = pin!(create_nofn_sighash_stream(
1003            self.db.clone(),
1004            self.config.clone(),
1005            deposit_data.clone(),
1006            deposit_blockhash,
1007            true,
1008        ));
1009
1010        let num_required_nofn_sigs = self.config.get_num_required_nofn_sigs(deposit_data);
1011        let num_required_nofn_sigs_per_kickoff = self
1012            .config
1013            .get_num_required_nofn_sigs_per_kickoff(deposit_data);
1014        let num_required_op_sigs = self.config.get_num_required_operator_sigs(deposit_data);
1015        let num_required_op_sigs_per_kickoff = self
1016            .config
1017            .get_num_required_operator_sigs_per_kickoff(deposit_data);
1018
1019        let operator_xonly_pks = deposit_data.get_operators();
1020        let num_operators = deposit_data.get_num_operators();
1021
1022        let ProtocolParamset {
1023            num_round_txs,
1024            num_kickoffs_per_round,
1025            ..
1026        } = *self.config.protocol_paramset();
1027
1028        let mut verified_sigs = vec![
1029            vec![
1030                vec![
1031                    Vec::<TaggedSignature>::with_capacity(
1032                        num_required_nofn_sigs_per_kickoff + num_required_op_sigs_per_kickoff
1033                    );
1034                    num_kickoffs_per_round
1035                ];
1036                num_round_txs + 1
1037            ];
1038            num_operators
1039        ];
1040
1041        let mut kickoff_txids = vec![vec![vec![]; num_round_txs + 1]; num_operators];
1042
1043        // ------ N-of-N SIGNATURES VERIFICATION ------
1044
1045        let mut nonce_idx: usize = 0;
1046
1047        while let Some(sighash) = sighash_stream.next().await {
1048            let typed_sighash = sighash.wrap_err("Failed to read from sighash stream")?;
1049
1050            let &SignatureInfo {
1051                operator_idx,
1052                round_idx,
1053                kickoff_utxo_idx,
1054                signature_id,
1055                tweak_data,
1056                kickoff_txid,
1057            } = &typed_sighash.1;
1058
1059            if signature_id == NormalSignatureKind::YieldKickoffTxid.into() {
1060                kickoff_txids[operator_idx][round_idx.to_index()].push((
1061                    kickoff_txid
1062                        .expect("Kickoff txid must be Some with YieldKickoffTxid signature kind"),
1063                    kickoff_utxo_idx,
1064                ));
1065                continue;
1066            }
1067
1068            let sig = sig_receiver
1069                .recv()
1070                .await
1071                .ok_or_eyre("No signature received")?;
1072
1073            tracing::debug!("Verifying Final nofn Signature {}", nonce_idx + 1);
1074
1075            verify_schnorr(
1076                &sig,
1077                &Message::from(typed_sighash.0),
1078                deposit_data.get_nofn_xonly_pk()?,
1079                tweak_data,
1080                Some(&mut tweak_cache),
1081            )
1082            .wrap_err_with(|| {
1083                format!(
1084                    "Failed to verify nofn signature {} with signature info {:?}",
1085                    nonce_idx + 1,
1086                    typed_sighash.1
1087                )
1088            })?;
1089
1090            let tagged_sig = TaggedSignature {
1091                signature: sig.serialize().to_vec(),
1092                signature_id: Some(signature_id),
1093            };
1094            verified_sigs[operator_idx][round_idx.to_index()][kickoff_utxo_idx].push(tagged_sig);
1095
1096            tracing::debug!("Final Signature Verified");
1097
1098            nonce_idx += 1;
1099        }
1100
1101        if nonce_idx != num_required_nofn_sigs {
1102            return Err(eyre::eyre!(
1103                "Did not receive enough nofn signatures. Needed: {}, received: {}",
1104                num_required_nofn_sigs,
1105                nonce_idx
1106            )
1107            .into());
1108        }
1109
1110        tracing::info!(
1111            "Verifier{} Finished verifying final signatures of NofN",
1112            self.signer.xonly_public_key.to_string()
1113        );
1114
1115        let move_tx_agg_nonce = agg_nonce_receiver
1116            .recv()
1117            .await
1118            .ok_or(eyre::eyre!("Aggregated nonces channel ended prematurely"))?;
1119
1120        let emergency_stop_agg_nonce = agg_nonce_receiver
1121            .recv()
1122            .await
1123            .ok_or(eyre::eyre!("Aggregated nonces channel ended prematurely"))?;
1124
1125        tracing::info!(
1126            "Verifier{} Received move tx and emergency stop aggregated nonces",
1127            self.signer.xonly_public_key.to_string()
1128        );
1129        // ------ OPERATOR SIGNATURES VERIFICATION ------
1130
1131        let num_required_total_op_sigs = num_required_op_sigs * deposit_data.get_num_operators();
1132        let mut total_op_sig_count = 0;
1133
1134        // get operator data
1135        let operators_data = deposit_data.get_operators();
1136
1137        // get signatures of operators and verify them
1138        for (operator_idx, &op_xonly_pk) in operators_data.iter().enumerate() {
1139            let mut op_sig_count = 0;
1140            // generate the sighash stream for operator
1141            let mut sighash_stream = pin!(create_operator_sighash_stream(
1142                self.db.clone(),
1143                op_xonly_pk,
1144                self.config.clone(),
1145                deposit_data.clone(),
1146                deposit_blockhash,
1147            ));
1148            while let Some(operator_sig) = operator_sig_receiver.recv().await {
1149                let typed_sighash = sighash_stream
1150                    .next()
1151                    .await
1152                    .ok_or_eyre("Operator sighash stream ended prematurely")??;
1153
1154                tracing::debug!(
1155                    "Verifying Final operator signature {} for operator {}, signature info {:?}",
1156                    op_sig_count + 1,
1157                    operator_idx,
1158                    typed_sighash.1
1159                );
1160
1161                let &SignatureInfo {
1162                    operator_idx,
1163                    round_idx,
1164                    kickoff_utxo_idx,
1165                    signature_id,
1166                    kickoff_txid: _,
1167                    tweak_data,
1168                } = &typed_sighash.1;
1169
1170                verify_schnorr(
1171                    &operator_sig,
1172                    &Message::from(typed_sighash.0),
1173                    op_xonly_pk,
1174                    tweak_data,
1175                    Some(&mut tweak_cache),
1176                )
1177                .wrap_err_with(|| {
1178                    format!(
1179                        "Operator {} Signature {}: verification failed. Signature info: {:?}.",
1180                        operator_idx,
1181                        op_sig_count + 1,
1182                        typed_sighash.1
1183                    )
1184                })?;
1185
1186                let tagged_sig = TaggedSignature {
1187                    signature: operator_sig.serialize().to_vec(),
1188                    signature_id: Some(signature_id),
1189                };
1190                verified_sigs[operator_idx][round_idx.to_index()][kickoff_utxo_idx]
1191                    .push(tagged_sig);
1192
1193                op_sig_count += 1;
1194                total_op_sig_count += 1;
1195                if op_sig_count == num_required_op_sigs {
1196                    break;
1197                }
1198            }
1199        }
1200
1201        if total_op_sig_count != num_required_total_op_sigs {
1202            return Err(eyre::eyre!(
1203                "Did not receive enough operator signatures. Needed: {}, received: {}",
1204                num_required_total_op_sigs,
1205                total_op_sig_count
1206            )
1207            .into());
1208        }
1209
1210        tracing::info!(
1211            "Verifier{} Finished verifying final signatures of operators",
1212            self.signer.xonly_public_key.to_string()
1213        );
1214        // ----- MOVE TX SIGNING
1215
1216        // Generate partial signature for move transaction
1217        let move_txhandler =
1218            create_move_to_vault_txhandler(deposit_data, self.config.protocol_paramset())?;
1219
1220        let move_txid = move_txhandler.get_cached_tx().compute_txid();
1221
1222        let move_tx_sighash = move_txhandler.calculate_script_spend_sighash_indexed(
1223            0,
1224            0,
1225            bitcoin::TapSighashType::Default,
1226        )?;
1227
1228        let movetx_secnonce = {
1229            let mut session_map = self.nonces.lock().await;
1230            let session = session_map
1231                .sessions
1232                .get_mut(&session_id)
1233                .ok_or_else(|| eyre::eyre!("Could not find session id {session_id}"))?;
1234            session
1235                .nonces
1236                .pop()
1237                .ok_or_eyre("No move tx secnonce in session")?
1238        };
1239
1240        let emergency_stop_secnonce = {
1241            let mut session_map = self.nonces.lock().await;
1242            let session = session_map
1243                .sessions
1244                .get_mut(&session_id)
1245                .ok_or_else(|| eyre::eyre!("Could not find session id {session_id}"))?;
1246            session
1247                .nonces
1248                .pop()
1249                .ok_or_eyre("No emergency stop secnonce in session")?
1250        };
1251
1252        // sign move tx and save everything to db if everything is correct
1253        let move_tx_partial_sig = musig2::partial_sign(
1254            deposit_data.get_verifiers(),
1255            None,
1256            movetx_secnonce,
1257            move_tx_agg_nonce,
1258            self.signer.keypair,
1259            Message::from_digest(move_tx_sighash.to_byte_array()),
1260        )?;
1261
1262        tracing::info!(
1263            "Verifier{} Finished signing move tx",
1264            self.signer.xonly_public_key.to_string()
1265        );
1266
1267        let emergency_stop_txhandler = create_emergency_stop_txhandler(
1268            deposit_data,
1269            &move_txhandler,
1270            self.config.protocol_paramset(),
1271        )?;
1272
1273        let emergency_stop_sighash = emergency_stop_txhandler
1274            .calculate_script_spend_sighash_indexed(
1275                0,
1276                0,
1277                bitcoin::TapSighashType::SinglePlusAnyoneCanPay,
1278            )?;
1279
1280        let emergency_stop_partial_sig = musig2::partial_sign(
1281            deposit_data.get_verifiers(),
1282            None,
1283            emergency_stop_secnonce,
1284            emergency_stop_agg_nonce,
1285            self.signer.keypair,
1286            Message::from_digest(emergency_stop_sighash.to_byte_array()),
1287        )?;
1288
1289        tracing::info!(
1290            "Verifier{} Finished signing emergency stop tx",
1291            self.signer.xonly_public_key.to_string()
1292        );
1293
1294        // Save signatures to db
1295        let mut dbtx = self.db.begin_transaction().await?;
1296        // Deposit is not actually finalized here, its only finalized after the aggregator gets all the partial sigs and checks the aggregated sig
1297        for (operator_idx, (operator_xonly_pk, operator_sigs)) in operator_xonly_pks
1298            .into_iter()
1299            .zip(verified_sigs.into_iter())
1300            .enumerate()
1301        {
1302            // skip indexes until round 0 (currently 0th index corresponds to collateral, which doesn't have any sigs)
1303            for (round_idx, mut op_round_sigs) in operator_sigs
1304                .into_iter()
1305                .enumerate()
1306                .skip(RoundIndex::Round(0).to_index())
1307            {
1308                if kickoff_txids[operator_idx][round_idx].len()
1309                    != self.config.protocol_paramset().num_signed_kickoffs
1310                {
1311                    return Err(eyre::eyre!(
1312                        "Number of signed kickoff utxos for operator: {}, round: {} is wrong. Expected: {}, got: {}",
1313                                operator_xonly_pk, round_idx, self.config.protocol_paramset().num_signed_kickoffs, kickoff_txids[operator_idx][round_idx].len()
1314                    ).into());
1315                }
1316                for (kickoff_txid, kickoff_idx) in &kickoff_txids[operator_idx][round_idx] {
1317                    tracing::trace!(
1318                        "Setting deposit signatures for {:?}, {:?}, {:?} {:?}",
1319                        operator_xonly_pk,
1320                        round_idx, // rounds start from 1
1321                        kickoff_idx,
1322                        kickoff_txid
1323                    );
1324
1325                    self.db
1326                        .insert_deposit_signatures_if_not_exist(
1327                            Some(&mut dbtx),
1328                            deposit_data.get_deposit_outpoint(),
1329                            operator_xonly_pk,
1330                            RoundIndex::from_index(round_idx),
1331                            *kickoff_idx,
1332                            *kickoff_txid,
1333                            std::mem::take(&mut op_round_sigs[*kickoff_idx]),
1334                        )
1335                        .await?;
1336                }
1337            }
1338        }
1339        // Check move_txid/kickoffs consistency and dispatch finalized kickoffs to state managers
1340        // First acquire lock to ensure that CheckIfKickoff duties are not dispatched during resync checks on deposit_finalize. It is a read lock so that multiple new_deposit calls can run this in parallel.
1341        // The lock is to ensure this race condition doesn't happen: A kickoff is almost finalized (one block left), kickoffs are checkedd in function below and added to state manager if they are finalized, if they are not finalized during check but become finalized + processed by state manager before dbtx in deposit_finalize is committed, the kickoff won't be added to the state manager.
1342        // Very unlikely to happen (because it needs both a db loss + resync conditions here + a kickoff to be just a small amount of time away from being finalized + state manager sync happening before dbtx is committed), but the performance cost of the lock is also negligible because the write lock will be acquired only when a kickoff utxo (in round tx) is spent.
1343        #[cfg(feature = "automation")]
1344        let _guard = crate::states::round::CHECK_KICKOFF_LOCK.read().await;
1345        self.check_kickoffs_on_chain_and_track(deposit_data, &mut dbtx, kickoff_txids, move_txid)
1346            .await?;
1347        dbtx.commit().await?;
1348
1349        Ok((move_tx_partial_sig, emergency_stop_partial_sig))
1350    }
1351
1352    /// Validates move_txid/kickoffs consistency and dispatches finalized kickoffs to state managers.
1353    ///
1354    /// If move_txid is NOT in canonical chain but kickoffs ARE, returns an error.
1355    /// Otherwise, dispatches finalized kickoffs to state managers for resync.
1356    ///
1357    /// Returns the list of (txid, block_height) for kickoffs that exist in canonical + finalized blocks.
1358    async fn check_kickoffs_on_chain_and_track(
1359        &self,
1360        deposit_data: &DepositData,
1361        dbtx: DatabaseTransaction<'_>,
1362        kickoff_txids: Vec<Vec<Vec<(Txid, usize)>>>,
1363        move_txid: Txid,
1364    ) -> Result<Vec<(Txid, u32)>, BridgeError> {
1365        // Build a mapping from txid -> (operator_xonly_pk, round_idx, kickoff_idx)
1366        let mut kickoff_metadata: std::collections::HashMap<
1367            Txid,
1368            (XOnlyPublicKey, RoundIndex, usize),
1369        > = std::collections::HashMap::new();
1370        let mut all_kickoff_txids: Vec<Txid> = Vec::new();
1371
1372        for (operator_idx, operator_xonly_pk) in
1373            deposit_data.get_operators().into_iter().enumerate()
1374        {
1375            for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs)
1376            {
1377                for (kickoff_txid, kickoff_idx) in
1378                    &kickoff_txids[operator_idx][round_idx.to_index()]
1379                {
1380                    kickoff_metadata
1381                        .insert(*kickoff_txid, (operator_xonly_pk, round_idx, *kickoff_idx));
1382                    all_kickoff_txids.push(*kickoff_txid);
1383                }
1384            }
1385        }
1386
1387        if all_kickoff_txids.is_empty() {
1388            return Ok(Vec::new());
1389        }
1390
1391        // Query all txids (move + kickoffs) in one DB call
1392        let mut txids_to_check = all_kickoff_txids.clone();
1393        txids_to_check.push(move_txid);
1394
1395        let canonical_txids_with_heights = self
1396            .db
1397            .get_canonical_block_heights_for_txids(Some(dbtx), &txids_to_check)
1398            .await?;
1399
1400        // Check move_txid/kickoffs consistency
1401        let canonical_txid_set: std::collections::HashSet<Txid> = canonical_txids_with_heights
1402            .iter()
1403            .map(|(txid, _)| *txid)
1404            .collect();
1405
1406        let move_txid_in_chain = canonical_txid_set.contains(&move_txid);
1407
1408        // if move txid is in chain, that means the deposit was already finished before, and current new_deposit call is just to resign,
1409        // meaning if there are any kickoffs already in chain, it is ok.
1410        if !move_txid_in_chain {
1411            // Check if any kickoffs are in chain
1412            let kickoffs_in_chain: Vec<_> = all_kickoff_txids
1413                .iter()
1414                .filter(|txid| canonical_txid_set.contains(*txid))
1415                .filter_map(|txid| {
1416                    kickoff_metadata
1417                        .get(txid)
1418                        .map(|(op_pk, round_idx, kickoff_idx)| {
1419                            (*txid, *op_pk, *round_idx, *kickoff_idx)
1420                        })
1421                })
1422                .collect();
1423
1424            if !kickoffs_in_chain.is_empty() {
1425                let kickoff_details: Vec<String> = kickoffs_in_chain
1426                    .iter()
1427                    .map(|(txid, op_pk, round_idx, kickoff_idx)| {
1428                        format!(
1429                            "txid={txid}, operator={op_pk}, round={round_idx:?}, kickoff_idx={kickoff_idx}"
1430                        )
1431                    })
1432                    .collect();
1433
1434                return Err(eyre::eyre!(
1435                    "Deposit rejected: move_txid {} is NOT in chain, but {} kickoff(s) ARE in chain. \
1436                     This means that an operator sent kickoffs before movetx was sent, indicating malicious behavior. Kickoffs in chain: - {}",
1437                    move_txid,
1438                    kickoffs_in_chain.len(),
1439                    kickoff_details.join("\n  - ")
1440                )
1441                .into());
1442            }
1443        }
1444
1445        // Filter to only kickoff txids (exclude move_txid) that are canonical
1446        let canonical_kickoffs: Vec<(Txid, u32)> = canonical_txids_with_heights
1447            .into_iter()
1448            .filter(|(txid, _)| *txid != move_txid)
1449            .collect();
1450
1451        if canonical_kickoffs.is_empty() {
1452            return Ok(Vec::new());
1453        }
1454
1455        // Get the current chain tip height for finalization check
1456        let current_chain_height = self
1457            .db
1458            .get_max_height(Some(dbtx))
1459            .await?
1460            .ok_or_else(|| eyre::eyre!("No blocks in bitcoin_syncer database"))?;
1461
1462        // Filter for finalized blocks
1463        let finalized_txids: Vec<(Txid, u32)> = canonical_kickoffs
1464            .into_iter()
1465            .filter(|(_, height)| {
1466                self.config
1467                    .protocol_paramset()
1468                    .is_block_finalized(*height, current_chain_height)
1469            })
1470            .collect();
1471
1472        if finalized_txids.is_empty() {
1473            return Ok(Vec::new());
1474        }
1475
1476        // next part is only relevant if automation is enabled
1477        // but be warned that if verifier automation is not enabled but operator automation is enabled,
1478        // that can cause issues here as operator kickoff state manager will never be dispatched.
1479
1480        #[cfg(feature = "automation")]
1481        {
1482            // Group by block height to minimize block reads
1483            let mut txids_by_height: std::collections::HashMap<u32, Vec<Txid>> =
1484                std::collections::HashMap::new();
1485            for (txid, height) in &finalized_txids {
1486                txids_by_height.entry(*height).or_default().push(*txid);
1487            }
1488
1489            // For each block height, get the full block and extract witnesses
1490            for (block_height, txids_in_block) in txids_by_height {
1491                let block = self
1492                .db
1493                .get_full_block(Some(dbtx), block_height)
1494                .await?
1495                .ok_or_else(|| {
1496                    eyre::eyre!(
1497                        "Block at height {} not found in database despite being in bitcoin_syncer_txs",
1498                        block_height
1499                    )
1500                })?;
1501
1502                for txid in txids_in_block {
1503                    // Find the transaction in the block
1504                    let tx = block
1505                        .txdata
1506                        .iter()
1507                        .find(|tx| tx.compute_txid() == txid)
1508                        .ok_or_else(|| {
1509                            eyre::eyre!(
1510                                "Transaction {} not found in block at height {}",
1511                                txid,
1512                                block_height
1513                            )
1514                        })?;
1515
1516                    let witness = tx
1517                        .input
1518                        .first()
1519                        .ok_or_else(|| eyre::eyre!("Kickoff transaction {txid} has no inputs"))?
1520                        .witness
1521                        .clone();
1522                    let (operator_xonly_pk, round_idx, kickoff_idx) =
1523                        kickoff_metadata
1524                            .get(&txid)
1525                            .ok_or_else(|| eyre::eyre!("Metadata not found for txid {}", txid))?;
1526
1527                    StateManager::<Self>::dispatch_new_kickoff_machine(
1528                        &self.db,
1529                        dbtx,
1530                        KickoffData {
1531                            operator_xonly_pk: *operator_xonly_pk,
1532                            round_idx: *round_idx,
1533                            kickoff_idx: *kickoff_idx as u32,
1534                        },
1535                        block_height,
1536                        deposit_data.clone(),
1537                        witness.clone(),
1538                    )
1539                    .await?;
1540
1541                    // send it to operator state manager too, if the state manager queue for the operator exists
1542                    // if it doesn't exist, it means this verifier does not have an operator with automation enabled
1543                    if self
1544                        .db
1545                        .pgmq_queue_exists(&StateManager::<Operator<C>>::queue_name(), Some(dbtx))
1546                        .await?
1547                    {
1548                        StateManager::<Operator<C>>::dispatch_new_kickoff_machine(
1549                            &self.db,
1550                            dbtx,
1551                            KickoffData {
1552                                operator_xonly_pk: *operator_xonly_pk,
1553                                round_idx: *round_idx,
1554                                kickoff_idx: *kickoff_idx as u32,
1555                            },
1556                            block_height,
1557                            deposit_data.clone(),
1558                            witness.clone(),
1559                        )
1560                        .await?;
1561                    }
1562                }
1563            }
1564        }
1565
1566        Ok(finalized_txids)
1567    }
1568
1569    #[allow(clippy::too_many_arguments)]
1570    pub async fn sign_optimistic_payout(
1571        &self,
1572        nonce_session_id: u128,
1573        agg_nonce: AggregatedNonce,
1574        deposit_id: u32,
1575        input_signature: taproot::Signature,
1576        input_outpoint: OutPoint,
1577        output_script_pubkey: ScriptBuf,
1578        output_amount: Amount,
1579        verification_signature: Option<PrimitiveSignature>,
1580    ) -> Result<PartialSignature, BridgeError> {
1581        // if the withdrawal utxo is spent, no reason to sign optimistic payout
1582        if self.rpc.is_utxo_spent(&input_outpoint).await? {
1583            return Err(
1584                eyre::eyre!("Withdrawal utxo {:?} is already spent", input_outpoint).into(),
1585            );
1586        }
1587
1588        // check for some standard script pubkeys
1589        if !(output_script_pubkey.is_p2tr()
1590            || output_script_pubkey.is_p2pkh()
1591            || output_script_pubkey.is_p2sh()
1592            || output_script_pubkey.is_p2wpkh()
1593            || output_script_pubkey.is_p2wsh())
1594        {
1595            return Err(eyre::eyre!(format!(
1596                "Output script pubkey is not a valid script pubkey: {}, must be p2tr, p2pkh, p2sh, p2wpkh, or p2wsh",
1597                output_script_pubkey
1598            )).into());
1599        }
1600
1601        // if verification address is set in config, check if verification signature is valid
1602        if let Some(address_in_config) = self.config.aggregator_verification_address {
1603            // check if verification signature is provided by aggregator
1604            if let Some(verification_signature) = verification_signature {
1605                let address_from_sig =
1606                    recover_address_from_ecdsa_signature::<OptimisticPayoutMessage>(
1607                        deposit_id,
1608                        input_signature,
1609                        input_outpoint,
1610                        output_script_pubkey.clone(),
1611                        output_amount,
1612                        verification_signature,
1613                    )?;
1614
1615                // check if verification signature is signed by the address in config
1616                if address_from_sig != address_in_config {
1617                    return Err(BridgeError::InvalidECDSAVerificationSignature);
1618                }
1619            } else {
1620                // if verification signature is not provided, but verification address is set in config, return error
1621                return Err(BridgeError::ECDSAVerificationSignatureMissing);
1622            }
1623        }
1624
1625        // check if withdrawal is valid first
1626        let move_txid = self
1627            .db
1628            .get_move_to_vault_txid_from_citrea_deposit(None, deposit_id)
1629            .await?
1630            .ok_or_else(|| {
1631                BridgeError::from(eyre::eyre!("Deposit not found for id: {}", deposit_id))
1632            })?;
1633
1634        // amount in move_tx is exactly the bridge amount
1635        if output_amount
1636            > self.config.protocol_paramset().bridge_amount - NON_EPHEMERAL_ANCHOR_AMOUNT
1637        {
1638            return Err(eyre::eyre!(
1639                "Output amount is greater than the bridge amount: {} > {}",
1640                output_amount,
1641                self.config.protocol_paramset().bridge_amount - NON_EPHEMERAL_ANCHOR_AMOUNT
1642            )
1643            .into());
1644        }
1645
1646        // check if withdrawal utxo is correct
1647        let withdrawal_utxo = self
1648            .db
1649            .get_withdrawal_utxo_from_citrea_withdrawal(None, deposit_id)
1650            .await?;
1651
1652        if withdrawal_utxo != input_outpoint {
1653            return Err(eyre::eyre!(
1654                "Withdrawal utxo is not correct: {:?} != {:?}",
1655                withdrawal_utxo,
1656                input_outpoint
1657            )
1658            .into());
1659        }
1660
1661        let mut deposit_data = self
1662            .db
1663            .get_deposit_data_with_move_tx(None, move_txid)
1664            .await?
1665            .ok_or_eyre("Deposit data corresponding to move txid not found")?;
1666
1667        let withdrawal_prevout = self.rpc.get_txout_from_outpoint(&input_outpoint).await?;
1668        let withdrawal_utxo = UTXO {
1669            outpoint: input_outpoint,
1670            txout: withdrawal_prevout,
1671        };
1672        let output_txout = TxOut {
1673            value: output_amount,
1674            script_pubkey: output_script_pubkey,
1675        };
1676
1677        let opt_payout_txhandler = create_optimistic_payout_txhandler(
1678            &mut deposit_data,
1679            withdrawal_utxo,
1680            output_txout,
1681            input_signature,
1682            self.config.protocol_paramset(),
1683        )?;
1684        // txin at index 1 is deposited utxo in movetx
1685        let sighash = opt_payout_txhandler.calculate_script_spend_sighash_indexed(
1686            1,
1687            0,
1688            bitcoin::TapSighashType::Default,
1689        )?;
1690
1691        let opt_payout_secnonce = {
1692            let mut session_map = self.nonces.lock().await;
1693            let session = session_map
1694                .sessions
1695                .get_mut(&nonce_session_id)
1696                .ok_or_else(|| eyre::eyre!("Could not find session id {nonce_session_id}"))?;
1697            session
1698                .nonces
1699                .pop()
1700                .ok_or_eyre("No move tx secnonce in session")?
1701        };
1702
1703        let opt_payout_partial_sig = musig2::partial_sign(
1704            deposit_data.get_verifiers(),
1705            None,
1706            opt_payout_secnonce,
1707            agg_nonce,
1708            self.signer.keypair,
1709            Message::from_digest(sighash.to_byte_array()),
1710        )?;
1711
1712        Ok(opt_payout_partial_sig)
1713    }
1714
1715    pub async fn set_operator_keys(
1716        &self,
1717        mut deposit_data: DepositData,
1718        keys: OperatorKeys,
1719        operator_xonly_pk: XOnlyPublicKey,
1720    ) -> Result<(), BridgeError> {
1721        let mut dbtx = self.db.begin_transaction().await?;
1722        self.citrea_client
1723            .check_nofn_correctness(deposit_data.get_nofn_xonly_pk()?)
1724            .await?;
1725
1726        self.is_deposit_valid(&mut deposit_data).await?;
1727
1728        self.db
1729            .insert_deposit_data_if_not_exists(
1730                Some(&mut dbtx),
1731                &mut deposit_data,
1732                self.config.protocol_paramset(),
1733            )
1734            .await?;
1735
1736        let hashes: Vec<[u8; 20]> = keys
1737            .challenge_ack_digests
1738            .into_iter()
1739            .map(|x| {
1740                x.hash.try_into().map_err(|e: Vec<u8>| {
1741                    eyre::eyre!("Invalid hash length, expected 20 bytes, got {}", e.len())
1742                })
1743            })
1744            .collect::<Result<Vec<[u8; 20]>, eyre::Report>>()?;
1745
1746        if hashes.len() != self.config.get_num_challenge_ack_hashes(&deposit_data) {
1747            return Err(eyre::eyre!(
1748                "Invalid number of challenge ack hashes received from operator {:?}: got: {} expected: {}",
1749                operator_xonly_pk,
1750                hashes.len(),
1751                self.config.get_num_challenge_ack_hashes(&deposit_data)
1752            ).into());
1753        }
1754
1755        let operator_data = self
1756            .db
1757            .get_operator(Some(&mut dbtx), operator_xonly_pk)
1758            .await?
1759            .ok_or(BridgeError::OperatorNotFound(operator_xonly_pk))?;
1760
1761        self.db
1762            .insert_operator_challenge_ack_hashes_if_not_exist(
1763                Some(&mut dbtx),
1764                operator_xonly_pk,
1765                deposit_data.get_deposit_outpoint(),
1766                &hashes,
1767            )
1768            .await?;
1769
1770        if keys.winternitz_pubkeys.len() != ClementineBitVMPublicKeys::number_of_flattened_wpks() {
1771            tracing::error!(
1772                "Invalid number of winternitz keys received from operator {:?}: got: {} expected: {}",
1773                operator_xonly_pk,
1774                keys.winternitz_pubkeys.len(),
1775                ClementineBitVMPublicKeys::number_of_flattened_wpks()
1776            );
1777            return Err(eyre::eyre!(
1778                "Invalid number of winternitz keys received from operator {:?}: got: {} expected: {}",
1779                operator_xonly_pk,
1780                keys.winternitz_pubkeys.len(),
1781                ClementineBitVMPublicKeys::number_of_flattened_wpks()
1782            )
1783            .into());
1784        }
1785
1786        let winternitz_keys: Vec<winternitz::PublicKey> = keys
1787            .winternitz_pubkeys
1788            .into_iter()
1789            .map(|x| x.try_into())
1790            .collect::<Result<_, BridgeError>>()?;
1791
1792        let bitvm_pks = ClementineBitVMPublicKeys::from_flattened_vec(&winternitz_keys);
1793
1794        let assert_tx_addrs = bitvm_pks
1795            .get_assert_taproot_leaf_hashes(operator_data.xonly_pk)
1796            .iter()
1797            .map(|x| x.to_byte_array())
1798            .collect::<Vec<_>>();
1799
1800        // wrap around a mutex lock to avoid OOM
1801        let guard = REPLACE_SCRIPTS_LOCK.lock().await;
1802        let start = std::time::Instant::now();
1803        let scripts: Vec<ScriptBuf> = bitvm_pks.get_g16_verifier_disprove_scripts()?;
1804
1805        let taproot_builder = taproot_builder_with_scripts(scripts);
1806
1807        let root_hash = taproot_builder
1808            .try_into_taptree()
1809            .expect("taproot builder always builds a full taptree")
1810            .root_hash()
1811            .to_byte_array();
1812
1813        // bitvm scripts are dropped, release the lock
1814        drop(guard);
1815        tracing::debug!("Built taproot tree in {:?}", start.elapsed());
1816
1817        let latest_blockhash_wots = bitvm_pks.latest_blockhash_pk.to_vec();
1818
1819        let latest_blockhash_script = WinternitzCommit::new(
1820            vec![(latest_blockhash_wots, 40)],
1821            operator_data.xonly_pk,
1822            self.config.protocol_paramset().winternitz_log_d,
1823        )
1824        .to_script_buf();
1825
1826        let latest_blockhash_root_hash = taproot_builder_with_scripts(&[latest_blockhash_script])
1827            .try_into_taptree()
1828            .expect("taproot builder always builds a full taptree")
1829            .root_hash()
1830            .to_raw_hash()
1831            .to_byte_array();
1832
1833        self.db
1834            .insert_operator_bitvm_keys_if_not_exist(
1835                Some(&mut dbtx),
1836                operator_xonly_pk,
1837                deposit_data.get_deposit_outpoint(),
1838                bitvm_pks.to_flattened_vec(),
1839            )
1840            .await?;
1841        // Save the public input wots to db along with the root hash
1842        self.db
1843            .insert_bitvm_setup_if_not_exists(
1844                Some(&mut dbtx),
1845                operator_xonly_pk,
1846                deposit_data.get_deposit_outpoint(),
1847                &assert_tx_addrs,
1848                &root_hash,
1849                &latest_blockhash_root_hash,
1850            )
1851            .await?;
1852
1853        dbtx.commit().await?;
1854        Ok(())
1855    }
1856
1857    /// Checks if the operator who sent the kickoff matches the payout data saved in our db
1858    /// Payout data in db is updated during citrea sync.
1859    async fn is_kickoff_malicious(
1860        &self,
1861        kickoff_witness: Witness,
1862        deposit_data: &mut DepositData,
1863        kickoff_data: KickoffData,
1864        dbtx: DatabaseTransaction<'_>,
1865    ) -> Result<bool, BridgeError> {
1866        let move_txid =
1867            create_move_to_vault_txhandler(deposit_data, self.config.protocol_paramset())?
1868                .get_cached_tx()
1869                .compute_txid();
1870
1871        let payout_info = self
1872            .db
1873            .get_payout_info_from_move_txid(Some(dbtx), move_txid)
1874            .await?;
1875        let Some((operator_xonly_pk_opt, payout_blockhash, _, _)) = payout_info else {
1876            tracing::warn!(
1877                "No payout info found in db for move txid {move_txid}, assuming malicious"
1878            );
1879            return Ok(true);
1880        };
1881
1882        let Some(operator_xonly_pk) = operator_xonly_pk_opt else {
1883            tracing::warn!("No operator xonly pk found in payout tx OP_RETURN, assuming malicious");
1884            return Ok(true);
1885        };
1886
1887        if operator_xonly_pk != kickoff_data.operator_xonly_pk {
1888            tracing::warn!("Operator xonly pk for the payout does not match with the kickoff_data");
1889            return Ok(true);
1890        }
1891
1892        let wt_derive_path = WinternitzDerivationPath::Kickoff(
1893            kickoff_data.round_idx,
1894            kickoff_data.kickoff_idx,
1895            self.config.protocol_paramset(),
1896        );
1897        let commits = extract_winternitz_commits(
1898            kickoff_witness,
1899            &[wt_derive_path],
1900            self.config.protocol_paramset(),
1901        )?;
1902        let blockhash_data = commits.first();
1903        // only last 20 bytes of the blockhash is committed
1904        let truncated_blockhash = &payout_blockhash[12..];
1905        if let Some(committed_blockhash) = blockhash_data {
1906            if committed_blockhash != truncated_blockhash {
1907                tracing::warn!("Payout blockhash does not match committed hash: committed: {:?}, truncated payout blockhash: {:?}",
1908                        blockhash_data, truncated_blockhash);
1909                return Ok(true);
1910            }
1911        } else {
1912            return Err(eyre::eyre!("Couldn't retrieve committed data from witness").into());
1913        }
1914        Ok(false)
1915    }
1916
1917    async fn get_signed_txs_for_kickoff(
1918        &self,
1919        dbtx: DatabaseTransaction<'_>,
1920        kickoff_data: KickoffData,
1921        deposit_data: DepositData,
1922    ) -> Result<
1923        (
1924            Vec<(TransactionType, bitcoin::Transaction)>,
1925            TxMetadata,
1926            bitcoin::Transaction,
1927        ),
1928        BridgeError,
1929    > {
1930        let deposit_outpoint = deposit_data.get_deposit_outpoint();
1931        let context = ContractContext::new_context_with_signer(
1932            kickoff_data,
1933            deposit_data,
1934            self.config.protocol_paramset(),
1935            self.signer.clone(),
1936        );
1937
1938        let signed_txs = create_and_sign_txs(
1939            self.db.clone(),
1940            &self.signer,
1941            self.config.clone(),
1942            context,
1943            None, // No need, verifier will not send kickoff tx
1944            Some(dbtx),
1945        )
1946        .await?;
1947
1948        let challenge_tx = signed_txs
1949            .iter()
1950            .find_map(|(tx_type, signed_tx)| {
1951                (*tx_type == TransactionType::Challenge).then(|| signed_tx.clone())
1952            })
1953            .ok_or_else(|| eyre::eyre!("Could not find challenge tx in signed_txs"))?;
1954
1955        let tx_metadata = TxMetadata {
1956            tx_type: TransactionType::Dummy, // will be replaced in add_tx_to_queue / insert_try_to_send
1957            operator_xonly_pk: Some(kickoff_data.operator_xonly_pk),
1958            round_idx: Some(kickoff_data.round_idx),
1959            kickoff_idx: Some(kickoff_data.kickoff_idx),
1960            deposit_outpoint: Some(deposit_outpoint),
1961        };
1962
1963        Ok((signed_txs, tx_metadata, challenge_tx))
1964    }
1965
1966    /// Checks if the kickoff is malicious and sends the appropriate txs if it is.
1967    /// Returns true if the kickoff is malicious.
1968    /// Sends a challenge tx if the kickoff is first malicious one in the current round, and the network is not mainnet or testnet4.
1969    pub async fn handle_kickoff<'a>(
1970        &'a self,
1971        dbtx: DatabaseTransaction<'a>,
1972        kickoff_witness: Witness,
1973        mut deposit_data: DepositData,
1974        kickoff_data: KickoffData,
1975        challenged_before: bool,
1976    ) -> Result<bool, BridgeError> {
1977        let is_malicious = self
1978            .is_kickoff_malicious(kickoff_witness, &mut deposit_data, kickoff_data, dbtx)
1979            .await?;
1980
1981        let deposit_outpoint = deposit_data.get_deposit_outpoint();
1982
1983        let (_signed_txs, tx_metadata, challenge_tx) = self
1984            .get_signed_txs_for_kickoff(dbtx, kickoff_data, deposit_data)
1985            .await?;
1986
1987        if is_malicious {
1988            tracing::warn!(
1989                "Malicious {} detected. {} Challenge tx: {} for deposit {}",
1990                kickoff_data,
1991                match challenged_before {
1992                    false => "This is the first malicious kickoff in the current round.",
1993                    true => "This is not the first malicious kickoff in the current round.",
1994                },
1995                bitcoin::consensus::encode::serialize_hex(&challenge_tx),
1996                deposit_outpoint
1997            );
1998            // do not automatically send challenge txs on mainnet or testnet4
1999            if !challenged_before
2000                && !matches!(
2001                    self.config.protocol_paramset().network,
2002                    bitcoin::Network::Bitcoin | bitcoin::Network::Testnet4
2003                )
2004            {
2005                #[cfg(feature = "automation")]
2006                self.tx_sender
2007                    .add_tx_to_queue(
2008                        dbtx,
2009                        TransactionType::Challenge,
2010                        &challenge_tx,
2011                        &[],
2012                        Some(tx_metadata),
2013                        self.config.protocol_paramset(),
2014                        None,
2015                    )
2016                    .await?;
2017            }
2018        } else {
2019            tracing::debug!(
2020                "Non-malicious {} detected, challenge tx: {}",
2021                kickoff_data,
2022                bitcoin::consensus::encode::serialize_hex(&challenge_tx)
2023            );
2024        }
2025
2026        Ok(is_malicious)
2027    }
2028
2029    #[cfg(feature = "automation")]
2030    /// Queues the relevant txs for a kickoff when the kickoff was challenged.
2031    /// These are: AssertTimeout, KickoffNotFinalized, LatestBlockhashTimeout, OperatorChallengeNack
2032    async fn queue_txs_for_challenged_kickoff(
2033        &self,
2034        dbtx: DatabaseTransaction<'_>,
2035        kickoff_data: KickoffData,
2036        deposit_data: DepositData,
2037        kickoff_txid: Txid,
2038    ) -> Result<(), BridgeError> {
2039        let (signed_txs, tx_metadata, _challenge_tx) = self
2040            .get_signed_txs_for_kickoff(dbtx, kickoff_data, deposit_data)
2041            .await?;
2042
2043        // try to send them
2044        for (tx_type, signed_tx) in &signed_txs {
2045            match *tx_type {
2046                TransactionType::AssertTimeout(_)
2047                | TransactionType::KickoffNotFinalized
2048                | TransactionType::LatestBlockhashTimeout
2049                | TransactionType::OperatorChallengeNack(_) => {
2050                    self.tx_sender
2051                        .add_tx_to_queue(
2052                            dbtx,
2053                            *tx_type,
2054                            signed_tx,
2055                            &signed_txs,
2056                            Some(tx_metadata),
2057                            self.config.protocol_paramset(),
2058                            None, // limit
2059                        )
2060                        .await?;
2061                }
2062                // Technically verifiers do not need to send watchtower challenge timeout tx,
2063                // but in state manager we attempt to disprove only if all watchtower challenges utxos are spent
2064                // so if verifiers do not send timeouts, operators can abuse this (by not sending watchtower challenge timeouts)
2065                // to not get disproven
2066                TransactionType::WatchtowerChallengeTimeout(idx) => {
2067                    self.tx_sender
2068                        .insert_try_to_send(
2069                            dbtx,
2070                            Some(TxMetadata {
2071                                tx_type: TransactionType::WatchtowerChallengeTimeout(idx),
2072                                ..tx_metadata
2073                            }),
2074                            signed_tx,
2075                            FeePayingType::CPFP,
2076                            None,
2077                            &[OutPoint {
2078                                txid: kickoff_txid,
2079                                vout: UtxoVout::KickoffFinalizer.get_vout(),
2080                            }],
2081                            &[],
2082                            &[],
2083                            &[],
2084                        )
2085                        .await?;
2086                }
2087                _ => {}
2088            }
2089        }
2090
2091        Ok(())
2092    }
2093
2094    #[cfg(feature = "automation")]
2095    async fn send_watchtower_challenge(
2096        &self,
2097        kickoff_data: KickoffData,
2098        deposit_data: DepositData,
2099        dbtx: DatabaseTransaction<'_>,
2100    ) -> Result<(), BridgeError> {
2101        let current_tip_hcp = self
2102            .header_chain_prover
2103            .get_tip_header_chain_proof()
2104            .await?;
2105
2106        let (work_only_proof, work_output) = self
2107            .header_chain_prover
2108            .prove_work_only(current_tip_hcp.0)
2109            .await?;
2110
2111        let g16: [u8; 256] = work_only_proof
2112            .inner
2113            .groth16()
2114            .wrap_err("Work only receipt is not groth16")?
2115            .seal
2116            .to_owned()
2117            .try_into()
2118            .map_err(|e: Vec<u8>| {
2119                eyre::eyre!(
2120                    "Invalid g16 proof length, expected 256 bytes, got {}",
2121                    e.len()
2122                )
2123            })?;
2124
2125        let g16_proof = CircuitGroth16Proof::from_seal(&g16);
2126        let mut commit_data: Vec<u8> = g16_proof
2127            .to_compressed()
2128            .wrap_err("Couldn't compress g16 proof")?
2129            .to_vec();
2130
2131        let total_work =
2132            borsh::to_vec(&work_output.work_u128).wrap_err("Couldn't serialize total work")?;
2133
2134        #[cfg(test)]
2135        {
2136            let wt_ind = self
2137                .config
2138                .test_params
2139                .all_verifiers_secret_keys
2140                .iter()
2141                .position(|x| x == &self.config.secret_key)
2142                .ok_or_else(|| eyre::eyre!("Verifier secret key not found in test params"))?;
2143
2144            self.config
2145                .test_params
2146                .maybe_disrupt_commit_data_for_total_work(&mut commit_data, wt_ind);
2147        }
2148
2149        commit_data.extend_from_slice(&total_work);
2150
2151        tracing::info!("Watchtower prepared commit data, trying to send watchtower challenge");
2152
2153        self.queue_watchtower_challenge(kickoff_data, deposit_data, commit_data, dbtx)
2154            .await
2155    }
2156
2157    async fn queue_watchtower_challenge(
2158        &self,
2159        kickoff_data: KickoffData,
2160        deposit_data: DepositData,
2161        commit_data: Vec<u8>,
2162        dbtx: DatabaseTransaction<'_>,
2163    ) -> Result<(), BridgeError> {
2164        let (tx_type, challenge_tx) = self
2165            .create_watchtower_challenge(
2166                TransactionRequestData {
2167                    deposit_outpoint: deposit_data.get_deposit_outpoint(),
2168                    kickoff_data,
2169                },
2170                &commit_data,
2171                Some(dbtx),
2172            )
2173            .await?;
2174
2175        #[cfg(feature = "automation")]
2176        {
2177            self.tx_sender
2178                .add_tx_to_queue(
2179                    dbtx,
2180                    tx_type,
2181                    &challenge_tx,
2182                    &[],
2183                    Some(TxMetadata {
2184                        tx_type,
2185                        operator_xonly_pk: Some(kickoff_data.operator_xonly_pk),
2186                        round_idx: Some(kickoff_data.round_idx),
2187                        kickoff_idx: Some(kickoff_data.kickoff_idx),
2188                        deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2189                    }),
2190                    self.config.protocol_paramset(),
2191                    None,
2192                )
2193                .await?;
2194
2195            tracing::info!(
2196                "Committed watchtower challenge, commit data: {:?}",
2197                commit_data
2198            );
2199        }
2200
2201        Ok(())
2202    }
2203
2204    #[tracing::instrument(skip(self, dbtx))]
2205    async fn update_citrea_deposit_and_withdrawals(
2206        &self,
2207        dbtx: DatabaseTransaction<'_>,
2208        l2_height_start: u64,
2209        l2_height_end: u64,
2210        block_height: u32,
2211    ) -> Result<(), BridgeError> {
2212        let last_deposit_idx = self.db.get_last_deposit_idx(Some(dbtx)).await?;
2213        tracing::debug!("Last Citrea deposit idx: {:?}", last_deposit_idx);
2214
2215        let last_withdrawal_idx = self.db.get_last_withdrawal_idx(Some(dbtx)).await?;
2216        tracing::debug!("Last Citrea withdrawal idx: {:?}", last_withdrawal_idx);
2217
2218        let new_deposits = self
2219            .citrea_client
2220            .collect_deposit_move_txids(last_deposit_idx, l2_height_end)
2221            .await?;
2222        tracing::debug!("New deposits received from Citrea: {:?}", new_deposits);
2223
2224        let new_withdrawals = self
2225            .citrea_client
2226            .collect_withdrawal_utxos(last_withdrawal_idx, l2_height_end)
2227            .await?;
2228        tracing::debug!(
2229            "New withdrawals received from Citrea: {:?}",
2230            new_withdrawals
2231        );
2232
2233        for (idx, move_to_vault_txid) in new_deposits {
2234            tracing::info!(
2235                "Saving move to vault txid {:?} with index {} for Citrea deposits",
2236                move_to_vault_txid,
2237                idx
2238            );
2239            self.db
2240                .upsert_move_to_vault_txid_from_citrea_deposit(
2241                    Some(dbtx),
2242                    idx as u32,
2243                    &move_to_vault_txid,
2244                )
2245                .await?;
2246        }
2247
2248        for (idx, withdrawal_utxo_outpoint) in new_withdrawals {
2249            tracing::info!(
2250                "Saving withdrawal utxo {:?} with index {} for Citrea withdrawals",
2251                withdrawal_utxo_outpoint,
2252                idx
2253            );
2254            self.db
2255                .update_withdrawal_utxo_from_citrea_withdrawal(
2256                    Some(dbtx),
2257                    idx as u32,
2258                    withdrawal_utxo_outpoint,
2259                    block_height,
2260                )
2261                .await?;
2262        }
2263
2264        let replacement_move_txids = self
2265            .citrea_client
2266            .get_replacement_deposit_move_txids(l2_height_start + 1, l2_height_end)
2267            .await?;
2268
2269        for (idx, new_move_txid) in replacement_move_txids {
2270            tracing::info!(
2271                "Setting replacement move txid: {:?} -> {:?}",
2272                idx,
2273                new_move_txid
2274            );
2275            self.db
2276                .update_replacement_deposit_move_txid(dbtx, idx, new_move_txid)
2277                .await?;
2278        }
2279
2280        Ok(())
2281    }
2282
2283    async fn update_finalized_payouts(
2284        &self,
2285        dbtx: DatabaseTransaction<'_>,
2286        block_id: u32,
2287        block_cache: &block_cache::BlockCache,
2288    ) -> Result<(), BridgeError> {
2289        let payout_txids = self
2290            .db
2291            .get_payout_txs_for_withdrawal_utxos(Some(dbtx), block_id)
2292            .await?;
2293
2294        let block = &block_cache.block;
2295
2296        let block_hash = block.block_hash();
2297
2298        let mut payout_txs_and_payer_operator_idx = vec![];
2299        for (idx, payout_txid) in payout_txids {
2300            let payout_tx_idx = block_cache.txids.get(&payout_txid);
2301            if payout_tx_idx.is_none() {
2302                tracing::error!(
2303                    "Payout tx not found in block cache: {:?} and in block: {:?}",
2304                    payout_txid,
2305                    block_id
2306                );
2307                tracing::error!("Block cache: {:?}", block_cache);
2308                return Err(eyre::eyre!("Payout tx not found in block cache").into());
2309            }
2310            let payout_tx_idx = payout_tx_idx.expect("Payout tx not found in block cache");
2311            let payout_tx = &block.txdata[*payout_tx_idx];
2312            // Find the first output that contains OP_RETURN
2313            let circuit_payout_tx = CircuitTransaction::from(payout_tx.clone());
2314            let op_return_output = get_first_op_return_output(&circuit_payout_tx);
2315
2316            // If OP_RETURN doesn't exist in any outputs, or the data in OP_RETURN is not a valid xonly_pubkey,
2317            // operator_xonly_pk will be set to None, and the corresponding column in DB set to NULL.
2318            // This can happen if optimistic payout is used, or an operator constructs the payout tx wrong.
2319            let operator_xonly_pk = op_return_output
2320                .and_then(|output| parse_op_return_data(&output.script_pubkey))
2321                .and_then(|bytes| XOnlyPublicKey::from_slice(bytes).ok());
2322
2323            if operator_xonly_pk.is_none() {
2324                tracing::info!(
2325                    "No valid operator xonly pk found in payout tx {:?} OP_RETURN. Either it is an optimistic payout or the operator constructed the payout tx wrong",
2326                    payout_txid
2327                );
2328            }
2329
2330            tracing::info!(
2331                "A new payout tx detected for withdrawal {}, payout txid: {:?}, operator xonly pk: {:?}",
2332                idx,
2333                payout_txid,
2334                operator_xonly_pk
2335            );
2336
2337            payout_txs_and_payer_operator_idx.push((
2338                idx,
2339                payout_txid,
2340                operator_xonly_pk,
2341                block_hash,
2342            ));
2343        }
2344
2345        self.db
2346            .update_payout_txs_and_payer_operator_xonly_pk(
2347                Some(dbtx),
2348                payout_txs_and_payer_operator_idx,
2349            )
2350            .await?;
2351
2352        Ok(())
2353    }
2354
2355    async fn send_unspent_kickoff_connectors(
2356        &self,
2357        dbtx: DatabaseTransaction<'_>,
2358        round_idx: RoundIndex,
2359        operator_xonly_pk: XOnlyPublicKey,
2360        used_kickoffs: HashSet<usize>,
2361    ) -> Result<(), BridgeError> {
2362        if used_kickoffs.len() == self.config.protocol_paramset().num_kickoffs_per_round {
2363            // ok, every kickoff spent
2364            return Ok(());
2365        }
2366
2367        let unspent_kickoff_txs = self
2368            .create_and_sign_unspent_kickoff_connector_txs(round_idx, operator_xonly_pk, Some(dbtx))
2369            .await?;
2370        for (tx_type, tx) in unspent_kickoff_txs {
2371            if let TransactionType::UnspentKickoff(kickoff_idx) = tx_type {
2372                if used_kickoffs.contains(&kickoff_idx) {
2373                    continue;
2374                }
2375                #[cfg(feature = "automation")]
2376                self.tx_sender
2377                    .add_tx_to_queue(
2378                        dbtx,
2379                        tx_type,
2380                        &tx,
2381                        &[],
2382                        Some(TxMetadata {
2383                            tx_type,
2384                            operator_xonly_pk: Some(operator_xonly_pk),
2385                            round_idx: Some(round_idx),
2386                            kickoff_idx: Some(kickoff_idx as u32),
2387                            deposit_outpoint: None,
2388                        }),
2389                        self.config.protocol_paramset(),
2390                        None,
2391                    )
2392                    .await?;
2393            }
2394        }
2395        Ok(())
2396    }
2397
2398    /// Verifies the conditions required to disprove an operator's actions using the "additional" disprove path.
2399    ///
2400    /// This function handles specific, non-Groth16 challenges. It reconstructs a unique challenge script
2401    /// based on on-chain data and constants (`deposit_constant`). It then validates the operator's
2402    /// provided assertions (`operator_asserts`) and acknowledgements (`operator_acks`) against this script.
2403    /// The goal is to produce a spendable witness for the disprove transaction if the operator is found to be at fault.
2404    ///
2405    /// # Arguments
2406    /// * `deposit_data` - Mutable data for the specific deposit being challenged.
2407    /// * `kickoff_data` - Information about the kickoff transaction that initiated this challenge.
2408    /// * `latest_blockhash` - The witness containing Winternitz signature for the latest Bitcoin blockhash.
2409    /// * `payout_blockhash` - The witness containing Winternitz signature for the payout transaction's blockhash.
2410    /// * `operator_asserts` - A map of witnesses from the operator, containing their assertions (claims).
2411    /// * `operator_acks` - A map of witnesses from the operator, containing their acknowledgements of watchtower challenges.
2412    /// * `txhandlers` - A map of transaction builders, used here to retrieve TXIDs of dependent transactions.
2413    ///
2414    /// # Returns
2415    /// - `Ok(Some(bitcoin::Witness))` if the operator's claims are successfully proven false, returning the complete witness needed to spend the disprove script path.
2416    /// - `Ok(None)` if the operator's claims are valid under this specific challenge, and no disprove is possible.
2417    /// - `Err(BridgeError)` if any error occurs during script reconstruction or validation.
2418    #[cfg(feature = "automation")]
2419    #[allow(clippy::too_many_arguments)]
2420    async fn verify_additional_disprove_conditions(
2421        &self,
2422        deposit_data: &mut DepositData,
2423        kickoff_data: &KickoffData,
2424        latest_blockhash: &Witness,
2425        payout_blockhash: &Witness,
2426        operator_asserts: &HashMap<usize, Witness>,
2427        operator_acks: &HashMap<usize, Witness>,
2428        txhandlers: &BTreeMap<TransactionType, TxHandler>,
2429        db_cache: &mut ReimburseDbCache<'_>,
2430    ) -> Result<Option<bitcoin::Witness>, BridgeError> {
2431        use bitvm::clementine::additional_disprove::debug_assertions_for_additional_script;
2432
2433        let nofn_key = deposit_data.get_nofn_xonly_pk().inspect_err(|e| {
2434            tracing::error!("Error getting nofn xonly pk: {:?}", e);
2435        })?;
2436
2437        let move_txid = txhandlers
2438            .get(&TransactionType::MoveToVault)
2439            .ok_or(TxError::TxHandlerNotFound(TransactionType::MoveToVault))?
2440            .get_txid()
2441            .to_byte_array();
2442
2443        let round_txid = txhandlers
2444            .get(&TransactionType::Round)
2445            .ok_or(TxError::TxHandlerNotFound(TransactionType::Round))?
2446            .get_txid()
2447            .to_byte_array();
2448
2449        let vout = UtxoVout::Kickoff(kickoff_data.kickoff_idx as usize).get_vout();
2450
2451        let watchtower_challenge_start_idx = UtxoVout::WatchtowerChallenge(0).get_vout();
2452
2453        let secp = Secp256k1::verification_only();
2454
2455        let watchtower_xonly_pk = deposit_data.get_watchtowers();
2456        let watchtower_pubkeys = watchtower_xonly_pk
2457            .iter()
2458            .map(|xonly_pk| {
2459                // Create timelock script that this watchtower key will commit to
2460                let nofn_2week = Arc::new(TimelockScript::new(
2461                    Some(nofn_key),
2462                    self.config
2463                        .protocol_paramset
2464                        .watchtower_challenge_timeout_timelock,
2465                ));
2466
2467                let builder = TaprootBuilder::new();
2468                let tweaked = builder
2469                    .add_leaf(0, nofn_2week.to_script_buf())
2470                    .expect("Valid script leaf")
2471                    .finalize(&secp, *xonly_pk)
2472                    .expect("taproot finalize must succeed");
2473
2474                tweaked.output_key().serialize()
2475            })
2476            .collect::<Vec<_>>();
2477
2478        let deposit_constant = deposit_constant(
2479            kickoff_data.operator_xonly_pk.serialize(),
2480            watchtower_challenge_start_idx,
2481            &watchtower_pubkeys,
2482            move_txid,
2483            round_txid,
2484            vout,
2485            self.config.protocol_paramset.genesis_chain_state_hash,
2486        );
2487
2488        tracing::debug!("Deposit constant: {:?}", deposit_constant);
2489
2490        let kickoff_winternitz_keys = db_cache.get_kickoff_winternitz_keys().await?.clone();
2491
2492        let payout_tx_blockhash_pk = kickoff_winternitz_keys
2493            .get_keys_for_round(kickoff_data.round_idx)?
2494            .get(kickoff_data.kickoff_idx as usize)
2495            .ok_or(TxError::IndexOverflow)?
2496            .clone();
2497
2498        let replaceable_additional_disprove_script = db_cache
2499            .get_replaceable_additional_disprove_script()
2500            .await?;
2501
2502        let additional_disprove_script = replace_placeholders_in_script(
2503            replaceable_additional_disprove_script.clone(),
2504            payout_tx_blockhash_pk,
2505            deposit_constant.0,
2506        );
2507
2508        let witness = operator_asserts
2509            .get(&0)
2510            .wrap_err("No witness found in operator asserts")?
2511            .clone();
2512
2513        let deposit_outpoint = deposit_data.get_deposit_outpoint();
2514        let paramset = self.config.protocol_paramset();
2515
2516        let commits = extract_winternitz_commits_with_sigs(
2517            witness,
2518            &ClementineBitVMPublicKeys::mini_assert_derivations_0(deposit_outpoint, paramset),
2519            self.config.protocol_paramset(),
2520        )?;
2521
2522        let mut challenge_sending_watchtowers_signature = Witness::new();
2523        let len = commits.len();
2524
2525        for elem in commits[len - 1].iter() {
2526            challenge_sending_watchtowers_signature.push(elem);
2527        }
2528
2529        let mut g16_public_input_signature = Witness::new();
2530
2531        for elem in commits[len - 2].iter() {
2532            g16_public_input_signature.push(elem);
2533        }
2534
2535        let num_of_watchtowers = deposit_data.get_num_watchtowers();
2536
2537        let mut operator_acks_vec: Vec<Option<[u8; 20]>> = vec![None; num_of_watchtowers];
2538
2539        for (idx, witness) in operator_acks.iter() {
2540            tracing::debug!(
2541                "Processing operator ack for idx: {}, witness: {:?}",
2542                idx,
2543                witness
2544            );
2545
2546            let pre_image: [u8; 20] = witness
2547                .nth(1)
2548                .wrap_err("No pre-image found in operator ack witness")?
2549                .try_into()
2550                .wrap_err("Invalid pre-image length, expected 20 bytes")?;
2551            if *idx >= operator_acks_vec.len() {
2552                return Err(eyre::eyre!(
2553                    "Operator ack index {} out of bounds for vec of length {}",
2554                    idx,
2555                    operator_acks_vec.len()
2556                )
2557                .into());
2558            }
2559            operator_acks_vec[*idx] = Some(pre_image);
2560
2561            tracing::debug!(target: "ci", "Operator ack for idx {}", idx);
2562        }
2563
2564        // take only winternitz signatures from the witness
2565
2566        let latest_blockhash = extract_winternitz_commits_with_sigs(
2567            latest_blockhash.clone(),
2568            &[ClementineBitVMPublicKeys::get_latest_blockhash_derivation(
2569                deposit_outpoint,
2570                paramset,
2571            )],
2572            self.config.protocol_paramset(),
2573        )?;
2574
2575        let payout_blockhash = extract_winternitz_commits_with_sigs(
2576            payout_blockhash.clone(),
2577            &[
2578                ClementineBitVMPublicKeys::get_payout_tx_blockhash_derivation(
2579                    deposit_outpoint,
2580                    paramset,
2581                ),
2582            ],
2583            self.config.protocol_paramset(),
2584        )?;
2585
2586        let mut latest_blockhash_new = Witness::new();
2587        for element in latest_blockhash
2588            .into_iter()
2589            .next()
2590            .expect("Must have one element")
2591        {
2592            latest_blockhash_new.push(element);
2593        }
2594
2595        let mut payout_blockhash_new = Witness::new();
2596        for element in payout_blockhash
2597            .into_iter()
2598            .next()
2599            .expect("Must have one element")
2600        {
2601            payout_blockhash_new.push(element);
2602        }
2603
2604        tracing::debug!(
2605            target: "ci",
2606            "Verify additional disprove conditions - Genesis height: {:?}, operator_xonly_pk: {:?}, move_txid: {:?}, round_txid: {:?}, vout: {:?}, watchtower_challenge_start_idx: {:?}, genesis_chain_state_hash: {:?}, deposit_constant: {:?}",
2607            self.config.protocol_paramset.genesis_height,
2608            kickoff_data.operator_xonly_pk,
2609            move_txid,
2610            round_txid,
2611            vout,
2612            watchtower_challenge_start_idx,
2613            self.config.protocol_paramset.genesis_chain_state_hash,
2614            deposit_constant
2615        );
2616
2617        tracing::debug!(
2618            target: "ci",
2619            "Payout blockhash: {:?}\nLatest blockhash: {:?}\nChallenge sending watchtowers signature: {:?}\nG16 public input signature: {:?}",
2620            payout_blockhash_new,
2621            latest_blockhash_new,
2622            challenge_sending_watchtowers_signature,
2623            g16_public_input_signature
2624        );
2625
2626        let additional_disprove_witness = validate_assertions_for_additional_script(
2627            additional_disprove_script.clone(),
2628            g16_public_input_signature.clone(),
2629            payout_blockhash_new.clone(),
2630            latest_blockhash_new.clone(),
2631            challenge_sending_watchtowers_signature.clone(),
2632            operator_acks_vec.clone(),
2633        );
2634
2635        let debug_additional_disprove_script = debug_assertions_for_additional_script(
2636            additional_disprove_script.clone(),
2637            g16_public_input_signature.clone(),
2638            payout_blockhash_new.clone(),
2639            latest_blockhash_new.clone(),
2640            challenge_sending_watchtowers_signature.clone(),
2641            operator_acks_vec,
2642        );
2643
2644        tracing::info!(
2645            "Debug additional disprove script: {:?}",
2646            debug_additional_disprove_script
2647        );
2648
2649        tracing::info!(
2650            "Additional disprove witness: {:?}",
2651            additional_disprove_witness
2652        );
2653
2654        Ok(additional_disprove_witness)
2655    }
2656
2657    /// Constructs, signs, and broadcasts the "additional" disprove transaction.
2658    ///
2659    /// This function is called after `verify_additional_disprove_conditions` successfully returns a witness.
2660    /// It takes this witness, places it into the disprove transaction's script spend path, adds the required
2661    /// operator and verifier signatures, and broadcasts the finalized transaction to the Bitcoin network.
2662    ///
2663    /// # Arguments
2664    /// * `txhandlers` - A map containing the pre-built `Disprove` transaction handler.
2665    /// * `kickoff_data` - Contextual data from the kickoff transaction.
2666    /// * `deposit_data` - Contextual data for the deposit being challenged.
2667    /// * `additional_disprove_witness` - The witness generated by `verify_additional_disprove_conditions`, proving the operator's fault.
2668    ///
2669    /// # Returns
2670    /// - `Ok(())` on successful broadcast of the transaction.
2671    /// - `Err(BridgeError)` if signing or broadcasting fails.
2672    #[cfg(feature = "automation")]
2673    async fn send_disprove_tx_additional(
2674        &self,
2675        dbtx: DatabaseTransaction<'_>,
2676        txhandlers: &BTreeMap<TransactionType, TxHandler>,
2677        kickoff_data: KickoffData,
2678        deposit_data: DepositData,
2679        additional_disprove_witness: Witness,
2680    ) -> Result<(), BridgeError> {
2681        let verifier_xonly_pk = self.signer.xonly_public_key;
2682
2683        let mut disprove_txhandler = txhandlers
2684            .get(&TransactionType::Disprove)
2685            .wrap_err("Disprove txhandler not found in txhandlers")?
2686            .clone();
2687
2688        let disprove_input = additional_disprove_witness
2689            .iter()
2690            .map(|x| x.to_vec())
2691            .collect::<Vec<_>>();
2692
2693        disprove_txhandler
2694            .set_p2tr_script_spend_witness(&disprove_input, 0, 1)
2695            .inspect_err(|e| {
2696                tracing::error!("Error setting disprove input witness: {:?}", e);
2697            })?;
2698
2699        let operators_sig = self
2700            .db
2701            .get_deposit_signatures(
2702                Some(dbtx),
2703                deposit_data.get_deposit_outpoint(),
2704                kickoff_data.operator_xonly_pk,
2705                kickoff_data.round_idx,
2706                kickoff_data.kickoff_idx as usize,
2707            )
2708            .await?
2709            .ok_or_eyre("No operator signature found for the disprove tx")?;
2710
2711        let mut tweak_cache = TweakCache::default();
2712
2713        self.signer
2714            .tx_sign_and_fill_sigs(
2715                &mut disprove_txhandler,
2716                operators_sig.as_ref(),
2717                Some(&mut tweak_cache),
2718            )
2719            .inspect_err(|e| {
2720                tracing::error!(
2721                    "Error signing disprove tx for verifier {:?}: {:?}",
2722                    verifier_xonly_pk,
2723                    e
2724                );
2725            })
2726            .wrap_err("Failed to sign disprove tx")?;
2727
2728        let disprove_tx = disprove_txhandler.get_cached_tx().clone();
2729
2730        tracing::debug!("Disprove txid: {:?}", disprove_tx.compute_txid());
2731
2732        tracing::warn!(
2733            "Additional disprove tx created for verifier {:?} with kickoff_data: {:?}, deposit_data: {:?}",
2734            verifier_xonly_pk,
2735            kickoff_data,
2736            deposit_data
2737        );
2738
2739        self.tx_sender
2740            .add_tx_to_queue(
2741                dbtx,
2742                TransactionType::Disprove,
2743                &disprove_tx,
2744                &[],
2745                Some(TxMetadata {
2746                    tx_type: TransactionType::Disprove,
2747                    deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2748                    operator_xonly_pk: Some(kickoff_data.operator_xonly_pk),
2749                    round_idx: Some(kickoff_data.round_idx),
2750                    kickoff_idx: Some(kickoff_data.kickoff_idx),
2751                }),
2752                self.config.protocol_paramset(),
2753                None,
2754            )
2755            .await?;
2756        Ok(())
2757    }
2758
2759    /// Performs the primary G16 proof verification to disprove an operator's claim.
2760    ///
2761    /// This is a complex function that aggregates all of the operator's assertions, which are commitments
2762    /// from a Winternitz one-time signature scheme. It meticulously parses and reorganizes these commitments
2763    /// into the precise input format required by the underlying Groth16 SNARK verifier (`validate_assertions`).
2764    /// It then invokes the verifier to check for a faulty computation.
2765    ///
2766    /// # Arguments
2767    /// * `deposit_data` - Mutable data for the specific deposit being challenged.
2768    /// * `operator_asserts` - A map containing all 33 required operator assertion witnesses.
2769    ///
2770    /// # Returns
2771    /// - `Ok(Some((index, script)))` if the ZK proof is faulty. The tuple contains the `StructuredScript`
2772    ///   that can be executed on-chain and its `index` in the Taproot tree.
2773    /// - `Ok(None)` if the ZK proof is valid.
2774    /// - `Err(BridgeError)` if any error occurs during data processing or ZK proof verification.
2775    #[cfg(feature = "automation")]
2776    async fn verify_disprove_conditions(
2777        &self,
2778        deposit_data: &mut DepositData,
2779        operator_asserts: &HashMap<usize, Witness>,
2780        db_cache: &mut ReimburseDbCache<'_>,
2781    ) -> Result<Option<(usize, Vec<Vec<u8>>)>, BridgeError> {
2782        use bitvm::chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256};
2783        use bridge_circuit_host::utils::get_verifying_key;
2784
2785        let bitvm_pks = db_cache.get_operator_bitvm_keys().await?.clone();
2786        let disprove_scripts = bitvm_pks.get_g16_verifier_disprove_scripts()?;
2787
2788        let deposit_outpoint = deposit_data.get_deposit_outpoint();
2789        let paramset = self.config.protocol_paramset();
2790
2791        // Pre-allocate commit vectors. Initializing with known sizes or empty vectors
2792        // is slightly more efficient as it can prevent reallocations.
2793        let mut g16_public_input_commit: Vec<Vec<Vec<u8>>> = vec![vec![vec![]]; NUM_PUBS];
2794        let mut num_u256_commits: Vec<Vec<Vec<u8>>> = vec![vec![vec![]]; NUM_U256];
2795        let mut intermediate_value_commits: Vec<Vec<Vec<u8>>> = vec![vec![vec![]]; NUM_HASH];
2796
2797        tracing::info!("Number of operator asserts: {}", operator_asserts.len());
2798
2799        if operator_asserts.len() != ClementineBitVMPublicKeys::number_of_assert_txs() {
2800            return Err(eyre::eyre!(
2801                "Expected exactly {} operator asserts, got {}",
2802                ClementineBitVMPublicKeys::number_of_assert_txs(),
2803                operator_asserts.len()
2804            )
2805            .into());
2806        }
2807
2808        for i in 0..operator_asserts.len() {
2809            let witness = operator_asserts
2810                .get(&i)
2811                .ok_or_eyre(format!("Expected operator assert at index {i}, got None"))?
2812                .clone();
2813
2814            let mut commits = extract_winternitz_commits_with_sigs(
2815                witness,
2816                &ClementineBitVMPublicKeys::get_assert_derivations(i, deposit_outpoint, paramset),
2817                self.config.protocol_paramset(),
2818            )?;
2819
2820            // Similar to the original operator asserts ordering, here we reorder into the format that BitVM expects.
2821            // For the first transaction, we have specific commits that need to be assigned to their respective arrays.
2822            // It includes the g16 public input commit, the last 2 num_u256 commits, and the last 3 intermediate value commits.
2823            // The rest of the commits are assigned to the num_u256_commits and intermediate_value_commits arrays.
2824            match i {
2825                0 => {
2826                    // Remove the last commit, which is for challenge-sending watchtowers
2827                    commits.pop();
2828                    let len = commits.len();
2829
2830                    // Assign specific commits to their respective arrays by removing from the end.
2831                    // This is slightly more efficient than removing from arbitrary indices.
2832                    g16_public_input_commit[0] = commits.remove(len - 1);
2833                    num_u256_commits[10] = commits.remove(len - 2);
2834                    num_u256_commits[11] = commits.remove(len - 3);
2835                    num_u256_commits[12] = commits.remove(len - 4);
2836                    num_u256_commits[13] = commits.remove(len - 5);
2837                }
2838                1 | 2 => {
2839                    // Handles i = 1 and i = 2
2840                    for j in 0..5 {
2841                        num_u256_commits[5 * (i - 1) + j] = commits
2842                            .pop()
2843                            .expect("Should not panic: `num_u256_commits` index out of bounds");
2844                    }
2845                }
2846                _ if i >= 3 && i < ClementineBitVMPublicKeys::number_of_assert_txs() => {
2847                    // Handles i from 3 to number_of_assert_txs() - 1
2848                    for j in 0..11 {
2849                        intermediate_value_commits[11 * (i - 3) + j] = commits.pop().expect(
2850                            "Should not panic: `intermediate_value_commits` index out of bounds",
2851                        );
2852                    }
2853                }
2854                _ => {
2855                    // Catch-all for any other 'i' values
2856                    panic!(
2857                        "Unexpected operator assert index: {i}; expected 0 to {}.",
2858                        ClementineBitVMPublicKeys::number_of_assert_txs() - 1
2859                    );
2860                }
2861            }
2862        }
2863
2864        tracing::info!("Converting assert commits to required format");
2865        tracing::info!(
2866            "g16_public_input_commit[0]: {:?}",
2867            g16_public_input_commit[0]
2868        );
2869
2870        // Helper closure to parse commit data into the ([u8; 20], u8) format.
2871        // This avoids code repetition and improves readability.
2872        let fill_from_commits = |source: &Vec<Vec<u8>>,
2873                                 target: &mut [[u8; 21]]|
2874         -> Result<(), BridgeError> {
2875            // We iterate over chunks of 2 `Vec<u8>` elements at a time.
2876            for (i, chunk) in source.chunks_exact(2).enumerate() {
2877                let mut sig_array: [u8; 21] = [0; 21];
2878                let sig: [u8; 20] = <[u8; 20]>::try_from(chunk[0].as_slice()).map_err(|_| {
2879                    eyre::eyre!(
2880                        "Invalid signature length, expected 20 bytes, got {}",
2881                        chunk[0].len()
2882                    )
2883                })?;
2884
2885                sig_array[..20].copy_from_slice(&sig);
2886
2887                let u8_part: u8 = *chunk[1].first().unwrap_or(&0);
2888                sig_array[20] = u8_part;
2889
2890                target[i] = sig_array;
2891            }
2892            Ok(())
2893        };
2894
2895        let mut first_box = Box::new([[[0u8; 21]; 67]; NUM_PUBS]);
2896        fill_from_commits(&g16_public_input_commit[0], &mut first_box[0])?;
2897
2898        let mut second_box = Box::new([[[0u8; 21]; 67]; NUM_U256]);
2899        for i in 0..NUM_U256 {
2900            fill_from_commits(&num_u256_commits[i], &mut second_box[i])?;
2901        }
2902
2903        let mut third_box = Box::new([[[0u8; 21]; 35]; NUM_HASH]);
2904        for i in 0..NUM_HASH {
2905            fill_from_commits(&intermediate_value_commits[i], &mut third_box[i])?;
2906        }
2907
2908        tracing::info!("Boxes created");
2909
2910        let vk = get_verifying_key();
2911
2912        tracing::debug!(
2913            "Signed asserts:\n{:?}\n{:?}\n{:?}",
2914            first_box,
2915            second_box,
2916            third_box
2917        );
2918        tracing::debug!("BitVM public keys: {:?}", bitvm_pks.bitvm_pks);
2919
2920        let res = tokio::task::spawn_blocking(move || {
2921            use bitvm::chunk::api::validate_assertions_return_vector;
2922
2923            validate_assertions_return_vector(
2924                &vk,
2925                (first_box, second_box, third_box),
2926                disprove_scripts
2927                    .as_slice()
2928                    .try_into()
2929                    .expect("static bitvm_cache contains exactly 364 disprove scripts"),
2930            )
2931        })
2932        .await
2933        .wrap_err("Validate assertions thread failed with error")?
2934        .map_err(|e| eyre::eyre!("Error validating assertions for disprove conditions: {}", e))?;
2935
2936        match res {
2937            None => {
2938                tracing::info!("No disprove witness found");
2939                Ok(None)
2940            }
2941            Some((index, disprove_script)) => {
2942                tracing::info!("Disprove witness found");
2943                tracing::debug!(
2944                    "Disprove script index: {}, Disprove script: {}",
2945                    index,
2946                    hex::encode(disprove_script.clone().concat())
2947                );
2948                Ok(Some((index, disprove_script)))
2949            }
2950        }
2951    }
2952
2953    /// Constructs, signs, and broadcasts the primary disprove transaction based on the operator assertions.
2954    ///
2955    /// This function takes the `StructuredScript` and its `index` returned by `verify_disprove_conditions`.
2956    /// It compiles the script, extracts the witness data (the push-only elements), and places it into the correct
2957    /// script path (`index`) of the disprove transaction. It then adds the necessary operator and verifier
2958    /// signatures before broadcasting the transaction to the Bitcoin network.
2959    ///
2960    /// # Arguments
2961    /// * `txhandlers` - A map containing the pre-built `Disprove` transaction handler.
2962    /// * `kickoff_data` - Contextual data from the kickoff transaction.
2963    /// * `deposit_data` - Contextual data for the deposit being challenged.
2964    /// * `disprove_script` - A tuple containing the executable `StructuredScript` and its Taproot leaf `index`, as returned by `verify_disprove_conditions`.
2965    ///
2966    /// # Returns
2967    /// - `Ok(())` on successful broadcast of the transaction.
2968    /// - `Err(BridgeError)` if signing or broadcasting fails.
2969    #[cfg(feature = "automation")]
2970    async fn send_disprove_tx(
2971        &self,
2972        dbtx: DatabaseTransaction<'_>,
2973        txhandlers: &BTreeMap<TransactionType, TxHandler>,
2974        kickoff_data: KickoffData,
2975        deposit_data: DepositData,
2976        disprove_inputs: (usize, Vec<Vec<u8>>),
2977    ) -> Result<(), BridgeError> {
2978        let verifier_xonly_pk = self.signer.xonly_public_key;
2979
2980        let mut disprove_txhandler = txhandlers
2981            .get(&TransactionType::Disprove)
2982            .wrap_err("Disprove txhandler not found in txhandlers")?
2983            .clone();
2984
2985        // Use expect so it doesn't try to disprove continuously due to error tolerance in state manager.
2986
2987        disprove_txhandler
2988            .set_p2tr_script_spend_witness(&disprove_inputs.1, 0, disprove_inputs.0 + 2)
2989            .inspect_err(|e| {
2990                tracing::error!("Error setting disprove input witness: {:?}", e);
2991            })?;
2992
2993        let operators_sig = self
2994            .db
2995            .get_deposit_signatures(
2996                Some(dbtx),
2997                deposit_data.get_deposit_outpoint(),
2998                kickoff_data.operator_xonly_pk,
2999                kickoff_data.round_idx,
3000                kickoff_data.kickoff_idx as usize,
3001            )
3002            .await?
3003            .ok_or_eyre("No operator signature found for the disprove tx")?;
3004
3005        let mut tweak_cache = TweakCache::default();
3006
3007        self.signer
3008            .tx_sign_and_fill_sigs(
3009                &mut disprove_txhandler,
3010                operators_sig.as_ref(),
3011                Some(&mut tweak_cache),
3012            )
3013            .inspect_err(|e| {
3014                tracing::error!(
3015                    "Error signing disprove tx for verifier {:?}: {:?}",
3016                    verifier_xonly_pk,
3017                    e
3018                );
3019            })
3020            .wrap_err("Failed to sign disprove tx")?;
3021
3022        let disprove_tx = disprove_txhandler.get_cached_tx().clone();
3023
3024        tracing::debug!("Disprove txid: {:?}", disprove_tx.compute_txid());
3025
3026        tracing::warn!(
3027            "BitVM disprove tx created for verifier {:?} with kickoff_data: {:?}, deposit_data: {:?}",
3028            verifier_xonly_pk,
3029            kickoff_data,
3030            deposit_data
3031        );
3032
3033        self.tx_sender
3034            .add_tx_to_queue(
3035                dbtx,
3036                TransactionType::Disprove,
3037                &disprove_tx,
3038                &[],
3039                Some(TxMetadata {
3040                    tx_type: TransactionType::Disprove,
3041                    deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
3042                    operator_xonly_pk: Some(kickoff_data.operator_xonly_pk),
3043                    round_idx: Some(kickoff_data.round_idx),
3044                    kickoff_idx: Some(kickoff_data.kickoff_idx),
3045                }),
3046                self.config.protocol_paramset(),
3047                None,
3048            )
3049            .await?;
3050        Ok(())
3051    }
3052
3053    pub async fn handle_finalized_block(
3054        &self,
3055        mut dbtx: DatabaseTransaction<'_>,
3056        block_id: u32,
3057        block_height: u32,
3058        block_cache: Arc<block_cache::BlockCache>,
3059        light_client_proof_wait_interval_secs: Option<u32>,
3060    ) -> Result<(), BridgeError> {
3061        tracing::info!("Verifier handling finalized block height: {}", block_height);
3062
3063        // before a certain number of blocks, citrea doesn't produce proofs (defined in citrea config)
3064        let max_attempts = light_client_proof_wait_interval_secs.unwrap_or(TEN_MINUTES_IN_SECS);
3065        let timeout = Duration::from_secs(max_attempts as u64);
3066
3067        let (l2_height_start, l2_height_end) = self
3068            .citrea_client
3069            .get_citrea_l2_height_range(
3070                block_height.into(),
3071                timeout,
3072                self.config.protocol_paramset(),
3073            )
3074            .await
3075            .inspect_err(|e| tracing::error!("Error getting citrea l2 height range: {:?}", e))?;
3076
3077        tracing::debug!(
3078            "l2_height_start: {:?}, l2_height_end: {:?}, collecting deposits and withdrawals...",
3079            l2_height_start,
3080            l2_height_end
3081        );
3082        self.update_citrea_deposit_and_withdrawals(
3083            dbtx,
3084            l2_height_start,
3085            l2_height_end,
3086            block_height,
3087        )
3088        .await?;
3089
3090        self.update_finalized_payouts(dbtx, block_id, &block_cache)
3091            .await?;
3092
3093        #[cfg(feature = "automation")]
3094        {
3095            // Save unproven block cache to the database
3096            self.header_chain_prover
3097                .save_unproven_block_cache(Some(&mut dbtx), &block_cache)
3098                .await?;
3099            while (self.header_chain_prover.prove_if_ready().await?).is_some() {
3100                // Continue until prove_if_ready returns None
3101                // If it doesn't return None, it means next batch_size amount of blocks were proven
3102            }
3103            // notify that lcp was processed for this height to state manager
3104            StateManager::<Self>::dispatch_lcp_processed(&self.db, dbtx, block_height).await?;
3105        }
3106
3107        Ok(())
3108    }
3109}
3110
3111impl<C> NamedEntity for Verifier<C>
3112where
3113    C: CitreaClientT,
3114{
3115    const ENTITY_NAME: &'static str = "verifier";
3116    const FINALIZED_BLOCK_CONSUMER_ID_AUTOMATION: &'static str =
3117        "verifier_finalized_block_fetcher_automation";
3118    const LCP_SYNCER_CONSUMER_ID: &'static str = "verifier_lcp_syncer";
3119}
3120
3121#[cfg(feature = "automation")]
3122mod states {
3123    use super::*;
3124    use crate::builder::transaction::{
3125        create_txhandlers, ContractContext, ReimburseDbCache, TxHandlerCache,
3126    };
3127    use crate::states::context::DutyResult;
3128    use crate::states::{Duty, Owner};
3129    use std::collections::BTreeMap;
3130    use tonic::async_trait;
3131
3132    #[async_trait]
3133    impl<C> Owner for Verifier<C>
3134    where
3135        C: CitreaClientT,
3136    {
3137        async fn handle_duty(
3138            &self,
3139            dbtx: DatabaseTransaction<'_>,
3140            duty: Duty,
3141        ) -> Result<DutyResult, BridgeError> {
3142            let verifier_xonly_pk = &self.signer.xonly_public_key;
3143            match duty {
3144                Duty::NewReadyToReimburse {
3145                    round_idx,
3146                    operator_xonly_pk,
3147                    used_kickoffs,
3148                } => {
3149                    tracing::info!(
3150                    "Verifier {:?} called new ready to reimburse with round_idx: {:?}, operator_idx: {}, used_kickoffs: {:?}",
3151                    verifier_xonly_pk, round_idx, operator_xonly_pk, used_kickoffs
3152                );
3153                    self.send_unspent_kickoff_connectors(
3154                        dbtx,
3155                        round_idx,
3156                        operator_xonly_pk,
3157                        used_kickoffs,
3158                    )
3159                    .await?;
3160                    Ok(DutyResult::Handled)
3161                }
3162                Duty::WatchtowerChallenge {
3163                    kickoff_data,
3164                    deposit_data,
3165                } => {
3166                    tracing::warn!(
3167                    "Verifier {:?} called watchtower challenge with kickoff_data: {:?}, deposit_data: {:?}",
3168                    verifier_xonly_pk, kickoff_data, deposit_data
3169                );
3170                    self.send_watchtower_challenge(kickoff_data, deposit_data, dbtx)
3171                        .await?;
3172
3173                    tracing::info!("Verifier sent watchtower challenge",);
3174
3175                    Ok(DutyResult::Handled)
3176                }
3177                Duty::AddNecessaryTxsForKickoff { .. } => {
3178                    // Verifiers do not need to add any transactions for kickoff if it is not challenged
3179                    Ok(DutyResult::Handled)
3180                }
3181                Duty::SendOperatorAsserts { .. } => Ok(DutyResult::Handled),
3182                Duty::AddRelevantTxsToTxSenderIfChallenged {
3183                    kickoff_data,
3184                    deposit_data,
3185                } => {
3186                    tracing::info!("Verifier {:?} called add relevant txs to tx sender with kickoff_data: {:?}", verifier_xonly_pk, kickoff_data);
3187                    let kickoff_txid = self
3188                        .db
3189                        .get_kickoff_txid_from_deposit_and_kickoff_data(
3190                            Some(dbtx),
3191                            deposit_data.get_deposit_outpoint(),
3192                            &kickoff_data,
3193                        )
3194                        .await?
3195                        .ok_or_else(|| {
3196                            eyre::eyre!(
3197                                "Kickoff txid not found in deposit_signatures for kickoff_data: {:?}",
3198                                kickoff_data
3199                            )
3200                        })?;
3201                    self.queue_txs_for_challenged_kickoff(
3202                        dbtx,
3203                        kickoff_data,
3204                        deposit_data,
3205                        kickoff_txid,
3206                    )
3207                    .await?;
3208                    Ok(DutyResult::Handled)
3209                }
3210                Duty::VerifierDisprove {
3211                    kickoff_data,
3212                    mut deposit_data,
3213                    operator_asserts,
3214                    operator_acks,
3215                    payout_blockhash,
3216                    latest_blockhash,
3217                } => {
3218                    #[cfg(test)]
3219                    {
3220                        if !self
3221                            .config
3222                            .test_params
3223                            .should_disprove(&self.signer.public_key, &deposit_data)?
3224                        {
3225                            return Ok(DutyResult::Handled);
3226                        }
3227                    }
3228                    let context = ContractContext::new_context_with_signer(
3229                        kickoff_data,
3230                        deposit_data.clone(),
3231                        self.config.protocol_paramset(),
3232                        self.signer.clone(),
3233                    );
3234
3235                    let mut db_cache =
3236                        ReimburseDbCache::from_context(self.db.clone(), &context, Some(dbtx));
3237
3238                    let mut tx_handler_cache = TxHandlerCache::new();
3239
3240                    let mut txhandlers = create_txhandlers(
3241                        TransactionType::AllNeededForDeposit,
3242                        context.clone(),
3243                        &mut tx_handler_cache,
3244                        &mut db_cache,
3245                    )
3246                    .await?;
3247
3248                    // Attempt to find an additional disprove witness first
3249                    if let Some(additional_disprove_witness) = self
3250                        .verify_additional_disprove_conditions(
3251                            &mut deposit_data,
3252                            &kickoff_data,
3253                            &latest_blockhash,
3254                            &payout_blockhash,
3255                            &operator_asserts,
3256                            &operator_acks,
3257                            &txhandlers,
3258                            &mut db_cache,
3259                        )
3260                        .await?
3261                    {
3262                        tracing::info!(
3263                            "The additional public inputs for the bridge proof provided by operator {:?} for the deposit are incorrect.",
3264                            kickoff_data.operator_xonly_pk
3265                        );
3266                        self.send_disprove_tx_additional(
3267                            dbtx,
3268                            &txhandlers,
3269                            kickoff_data,
3270                            deposit_data,
3271                            additional_disprove_witness,
3272                        )
3273                        .await?;
3274                    } else {
3275                        tracing::info!(
3276                            "The additional public inputs for the bridge proof provided by operator {:?} for the deposit are correct.",
3277                            kickoff_data.operator_xonly_pk
3278                        );
3279
3280                        // If no additional witness, try to find a standard disprove witness
3281                        match self
3282                            .verify_disprove_conditions(
3283                                &mut deposit_data,
3284                                &operator_asserts,
3285                                &mut db_cache,
3286                            )
3287                            .await?
3288                        {
3289                            Some((index, disprove_inputs)) => {
3290                                tracing::info!(
3291                                    "The public inputs for the bridge proof provided by operator {:?} for the deposit are incorrect.",
3292                                    kickoff_data.operator_xonly_pk
3293                                );
3294
3295                                tx_handler_cache.store_for_next_kickoff(&mut txhandlers)?;
3296
3297                                // Only this one creates a tx handler in which scripts exist, other txhandlers only include scripts as hidden nodes.
3298                                let txhandlers_with_disprove = create_txhandlers(
3299                                    TransactionType::Disprove,
3300                                    context,
3301                                    &mut tx_handler_cache,
3302                                    &mut db_cache,
3303                                )
3304                                .await?;
3305
3306                                self.send_disprove_tx(
3307                                    dbtx,
3308                                    &txhandlers_with_disprove,
3309                                    kickoff_data,
3310                                    deposit_data,
3311                                    (index, disprove_inputs),
3312                                )
3313                                .await?;
3314                            }
3315                            None => {
3316                                tracing::info!(
3317                                    "The public inputs for the bridge proof provided by operator {:?} for the deposit are correct.",
3318                                    kickoff_data.operator_xonly_pk
3319                                );
3320                            }
3321                        }
3322                    }
3323
3324                    Ok(DutyResult::Handled)
3325                }
3326                Duty::SendLatestBlockhash { .. } => Ok(DutyResult::Handled),
3327                Duty::CheckIfKickoff {
3328                    txid,
3329                    block_height,
3330                    witness,
3331                } => {
3332                    tracing::debug!(
3333                        "Verifier {:?} called check if kickoff with txid: {:?}, block_height: {:?}",
3334                        verifier_xonly_pk,
3335                        txid,
3336                        block_height,
3337                    );
3338                    let db_kickoff_data = self
3339                        .db
3340                        .get_deposit_data_with_kickoff_txid(Some(dbtx), txid)
3341                        .await?;
3342                    if let Some((deposit_data, kickoff_data)) = db_kickoff_data {
3343                        tracing::debug!(
3344                            "New kickoff found {:?}, for deposit: {:?}",
3345                            kickoff_data,
3346                            deposit_data.get_deposit_outpoint()
3347                        );
3348                        // add kickoff machine if there is a new kickoff
3349                        // do not add if kickoff finalizer is already spent => kickoff is finished
3350                        // this can happen if we are resyncing
3351                        StateManager::<Self>::dispatch_new_kickoff_machine(
3352                            &self.db,
3353                            dbtx,
3354                            kickoff_data,
3355                            block_height,
3356                            deposit_data.clone(),
3357                            witness.clone(),
3358                        )
3359                        .await?;
3360                    }
3361                    Ok(DutyResult::Handled)
3362                }
3363                Duty::CheckIfKickoffMalicious {
3364                    kickoff_witness,
3365                    deposit_data,
3366                    kickoff_data,
3367                    challenged_before,
3368                } => {
3369                    let is_malicious = self
3370                        .handle_kickoff(
3371                            dbtx,
3372                            kickoff_witness,
3373                            deposit_data,
3374                            kickoff_data,
3375                            challenged_before,
3376                        )
3377                        .await?;
3378                    Ok(DutyResult::CheckIfKickoffMalicious {
3379                        challenged: is_malicious,
3380                    })
3381                }
3382            }
3383        }
3384
3385        async fn create_txhandlers(
3386            &self,
3387            dbtx: DatabaseTransaction<'_>,
3388            tx_type: TransactionType,
3389            contract_context: ContractContext,
3390        ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
3391            let mut db_cache =
3392                ReimburseDbCache::from_context(self.db.clone(), &contract_context, Some(dbtx));
3393            let txhandlers = create_txhandlers(
3394                tx_type,
3395                contract_context,
3396                &mut TxHandlerCache::new(),
3397                &mut db_cache,
3398            )
3399            .await?;
3400            Ok(txhandlers)
3401        }
3402    }
3403}
3404
3405#[cfg(test)]
3406mod tests {
3407    use super::*;
3408    use crate::rpc::ecdsa_verification_sig::OperatorWithdrawalMessage;
3409    use crate::test::common::citrea::MockCitreaClient;
3410    use crate::test::common::*;
3411    use bitcoin::Block;
3412    use std::str::FromStr;
3413
3414    #[tokio::test]
3415    #[cfg(feature = "automation")]
3416    async fn test_database_operations_idempotency() {
3417        let mut config = create_test_config_with_thread_name().await;
3418        let _regtest = create_regtest_rpc(&mut config).await;
3419
3420        let verifier = Verifier::<MockCitreaClient>::new(config.clone())
3421            .await
3422            .unwrap();
3423
3424        // Test header chain prover save operation idempotency
3425        let test_block = Block {
3426            header: bitcoin::block::Header {
3427                version: bitcoin::block::Version::ONE,
3428                prev_blockhash: bitcoin::BlockHash::all_zeros(),
3429                merkle_root: bitcoin::TxMerkleNode::all_zeros(),
3430                time: 1234567890,
3431                bits: bitcoin::CompactTarget::from_consensus(0x207fffff),
3432                nonce: 12345,
3433            },
3434            txdata: vec![], // empty transactions
3435        };
3436        let block_cache = block_cache::BlockCache::from_block(test_block, 100u32);
3437
3438        // First save
3439        let mut dbtx1 = verifier.db.begin_transaction().await.unwrap();
3440        let result1 = verifier
3441            .header_chain_prover
3442            .save_unproven_block_cache(Some(&mut dbtx1), &block_cache)
3443            .await;
3444        assert!(result1.is_ok(), "First save should succeed");
3445        dbtx1.commit().await.unwrap();
3446
3447        // Second save with same data should be idempotent
3448        let mut dbtx2 = verifier.db.begin_transaction().await.unwrap();
3449        let result2 = verifier
3450            .header_chain_prover
3451            .save_unproven_block_cache(Some(&mut dbtx2), &block_cache)
3452            .await;
3453        assert!(result2.is_ok(), "Second save should succeed (idempotent)");
3454        dbtx2.commit().await.unwrap();
3455    }
3456
3457    #[tokio::test]
3458    async fn test_recover_address_from_signature() {
3459        let input_signature = taproot::Signature::from_slice(&hex::decode("e8b82defd5e7745731737d210ad3f649541fd1e3173424fe6f9152b11cf8a1f9e24a176690c2ab243fb80ccc43369b2aba095b011d7a3a7c2a6953ef6b10264300").unwrap())
3460		.unwrap();
3461        let input_outpoint = OutPoint::from_str(
3462            "0000000000000000000000000000000000000000000000000000000000000000:0",
3463        )
3464        .unwrap();
3465        let output_script_pubkey =
3466            ScriptBuf::from_hex("0000000000000000000000000000000000000000000000000000000000000000")
3467                .unwrap();
3468        let output_amount = Amount::from_sat(1000000000000000000);
3469        let deposit_id = 1;
3470
3471        let opt_payout_sig = PrimitiveSignature::from_str("0x165b7303ffe40149e297be9f1112c1484fcbd464bec26036e5a6142da92249ed7de398295ecac9e41943e326d44037073643a89049177b43c4a09f98787eafa91b")
3472		.unwrap();
3473        let address = recover_address_from_ecdsa_signature::<OptimisticPayoutMessage>(
3474            deposit_id,
3475            input_signature,
3476            input_outpoint,
3477            output_script_pubkey.clone(),
3478            output_amount,
3479            opt_payout_sig,
3480        )
3481        .unwrap();
3482        assert_eq!(
3483            address,
3484            alloy::primitives::Address::from_str("0x281df03154e98484B786EDEf7EfF592a270F1Fb1")
3485                .unwrap()
3486        );
3487
3488        let op_withdrawal_sig = PrimitiveSignature::from_str("0xe540662d2ea0aeb29adeeb81a824bcb00e3d2a51d2c28e3eab6305168904e4cb7549e5abe78a91e58238a3986a5faf2ca9bbaaa79e0d0489a96ee275f7db9b111c")
3489				.unwrap();
3490        let address = recover_address_from_ecdsa_signature::<OperatorWithdrawalMessage>(
3491            deposit_id,
3492            input_signature,
3493            input_outpoint,
3494            output_script_pubkey.clone(),
3495            output_amount,
3496            op_withdrawal_sig,
3497        )
3498        .unwrap();
3499        assert_eq!(
3500            address,
3501            alloy::primitives::Address::from_str("0x281df03154e98484B786EDEf7EfF592a270F1Fb1")
3502                .unwrap()
3503        );
3504
3505        // using OperatorWithdrawalMessage signature for OptimisticPayoutMessage should fail
3506        let address = recover_address_from_ecdsa_signature::<OptimisticPayoutMessage>(
3507            deposit_id,
3508            input_signature,
3509            input_outpoint,
3510            output_script_pubkey,
3511            output_amount,
3512            op_withdrawal_sig,
3513        )
3514        .unwrap();
3515        assert_ne!(
3516            address,
3517            alloy::primitives::Address::from_str("0x281df03154e98484B786EDEf7EfF592a270F1Fb1")
3518                .unwrap()
3519        );
3520    }
3521}