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 pub nonces: Vec<SecretNonce>,
94}
95
96#[derive(Debug)]
97pub struct AllSessions {
98 sessions: HashMap<u128, NonceSession>,
99 session_queue: VecDeque<u128>,
100 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 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 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 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 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 pub fn add_new_session_with_random_id(
156 &mut self,
157 new_nonce_session: NonceSession,
158 ) -> Result<u128, eyre::Report> {
159 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 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 self.session_queue.retain(|x| *x != id);
173 Ok(session)
174 }
175
176 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 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 session
204 .nonces
205 .len()
206 .checked_mul(MUSIG_SECNONCE_LEN)
207 .ok_or_eyre("Calculation overflow in session_bytes")
208 }
209
210 pub fn total_sessions_byte_size(&self) -> Result<usize, eyre::Report> {
212 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 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 #[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 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 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 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 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, 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 async fn is_deposit_valid(&self, deposit_data: &mut DepositData) -> Result<(), BridgeError> {
552 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 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 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 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 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 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 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 !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 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 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 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 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 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 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 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 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 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 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 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 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 let operators_data = deposit_data.get_operators();
1122
1123 for (operator_idx, &op_xonly_pk) in operators_data.iter().enumerate() {
1125 let mut op_sig_count = 0;
1126 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 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 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 let mut dbtx = self.db.begin_transaction().await?;
1280 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 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, kickoff_idx
1307 )
1308 .into());
1309 }
1310
1311 tracing::trace!(
1312 "Setting deposit signatures for {:?}, {:?}, {:?} {:?}",
1313 operator_xonly_pk,
1314 round_idx, 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 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 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 let Some(address_in_config) = self.config.aggregator_verification_address {
1372 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 if address_from_sig != address_in_config {
1386 return Err(BridgeError::InvalidECDSAVerificationSignature);
1387 }
1388 } else {
1389 return Err(BridgeError::ECDSAVerificationSignatureMissing);
1391 }
1392 }
1393
1394 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 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 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 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 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 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 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 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 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 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 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, Some(dbtx),
1726 )
1727 .await?;
1728
1729 let tx_metadata = TxMetadata {
1730 tx_type: TransactionType::Dummy, 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 for (tx_type, signed_tx) in &signed_txs {
1739 if *tx_type == TransactionType::Challenge && challenged_before {
1740 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 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 let circuit_payout_tx = CircuitTransaction::from(payout_tx.clone());
2027 let op_return_output = get_first_op_return_output(&circuit_payout_tx);
2028
2029 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 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 #[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 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 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 #[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 #[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 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 match i {
2538 0 => {
2539 commits.pop();
2541 let len = commits.len();
2542
2543 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 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 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 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 let fill_from_commits = |source: &Vec<Vec<u8>>,
2586 target: &mut [[u8; 21]]|
2587 -> Result<(), BridgeError> {
2588 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 #[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 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 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 }
2811 }
2812
2813 Ok(())
2814 }
2815}
2816
2817#[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 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 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 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 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 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![], };
3153 let block_cache = Arc::new(block_cache::BlockCache::from_block(
3154 test_block,
3155 block_height,
3156 ));
3157
3158 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 tracing::info!("First call result: {:?}", result1);
3171
3172 dbtx1.commit().await.unwrap();
3174
3175 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 tracing::info!("Second call result: {:?}", result2);
3188
3189 dbtx2.commit().await.unwrap();
3191
3192 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 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![], };
3222 let block_cache = block_cache::BlockCache::from_block(test_block, 100u32);
3223
3224 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 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 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}