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