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