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