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