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