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