1use ark_ff::PrimeField;
2use circuits_lib::common::constants::FIRST_FIVE_OUTPUTS;
3
4use crate::actor::{Actor, TweakCache, WinternitzDerivationPath};
5use crate::bitvm_client::{ClementineBitVMPublicKeys, SECP};
6use crate::builder::sighash::{create_operator_sighash_stream, PartialSignatureInfo};
7use crate::builder::transaction::deposit_signature_owner::EntityType;
8use crate::builder::transaction::input::UtxoVout;
9use crate::builder::transaction::sign::{create_and_sign_txs, TransactionRequestData};
10use crate::builder::transaction::{
11 create_burn_unused_kickoff_connectors_txhandler, create_round_nth_txhandler,
12 create_round_txhandlers, ContractContext, KickoffWinternitzKeys, TransactionType, TxHandler,
13};
14use crate::citrea::CitreaClientT;
15use crate::config::BridgeConfig;
16use crate::database::Database;
17use crate::database::DatabaseTransaction;
18use crate::deposit::{DepositData, KickoffData, OperatorData};
19use crate::errors::BridgeError;
20use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
21
22use crate::metrics::L1SyncStatusProvider;
23use crate::rpc::clementine::{EntityStatus, StoppedTasks};
24use crate::task::entity_metric_publisher::{
25 EntityMetricPublisher, ENTITY_METRIC_PUBLISHER_INTERVAL,
26};
27use crate::task::manager::BackgroundTaskManager;
28use crate::task::payout_checker::{PayoutCheckerTask, PAYOUT_CHECKER_POLL_DELAY};
29use crate::task::TaskExt;
30use crate::utils::{monitor_standalone_task, Last20Bytes, ScriptBufExt};
31use crate::utils::{NamedEntity, TxMetadata};
32use crate::{builder, constants, UTXO};
33use bitcoin::hashes::Hash;
34use bitcoin::secp256k1::schnorr::Signature;
35use bitcoin::secp256k1::{schnorr, Message};
36use bitcoin::{taproot, Address, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
37use bitcoincore_rpc::json::AddressType;
38use bitcoincore_rpc::RpcApi;
39use bitvm::signatures::winternitz;
40
41use eyre::{Context, OptionExt};
42use tokio::sync::mpsc;
43use tokio_stream::StreamExt;
44
45#[cfg(feature = "automation")]
46use {
47 crate::{
48 builder::script::extract_winternitz_commits,
49 header_chain_prover::HeaderChainProver,
50 states::StateManager,
51 task::IntoTask,
52 tx_sender::{ActivatedWithOutpoint, ActivatedWithTxid, TxSenderClient},
53 utils::FeePayingType,
54 },
55 bitcoin::Witness,
56 bitvm::chunk::api::generate_assertions,
57 bridge_circuit_host::{
58 bridge_circuit_host::{
59 create_spv, prove_bridge_circuit, MAINNET_BRIDGE_CIRCUIT_ELF,
60 REGTEST_BRIDGE_CIRCUIT_ELF, REGTEST_BRIDGE_CIRCUIT_ELF_TEST, SIGNET_BRIDGE_CIRCUIT_ELF,
61 SIGNET_BRIDGE_CIRCUIT_ELF_TEST, TESTNET4_BRIDGE_CIRCUIT_ELF,
62 TESTNET4_BRIDGE_CIRCUIT_ELF_TEST,
63 },
64 structs::{BridgeCircuitHostParams, WatchtowerContext},
65 },
66 circuits_lib::bridge_circuit::structs::LightClientProof,
67 std::collections::HashMap,
68};
69
70pub type SecretPreimage = [u8; 20];
71pub type PublicHash = [u8; 20];
72
73#[derive(
78 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Ord, PartialOrd,
79)]
80pub enum RoundIndex {
81 Collateral,
82 Round(usize), }
84
85impl RoundIndex {
86 pub fn to_index(&self) -> usize {
88 match self {
89 RoundIndex::Collateral => 0,
90 RoundIndex::Round(index) => *index + 1,
91 }
92 }
93
94 pub fn from_index(index: usize) -> Self {
97 if index == 0 {
98 RoundIndex::Collateral
99 } else {
100 RoundIndex::Round(index - 1)
101 }
102 }
103
104 pub fn next_round(&self) -> Self {
106 match self {
107 RoundIndex::Collateral => RoundIndex::Round(0),
108 RoundIndex::Round(index) => RoundIndex::Round(*index + 1),
109 }
110 }
111
112 pub fn iter_rounds(num_rounds: usize) -> impl Iterator<Item = RoundIndex> {
115 Self::iter_rounds_range(0, num_rounds)
116 }
117
118 pub fn iter_rounds_range(start: usize, end: usize) -> impl Iterator<Item = RoundIndex> {
121 (start..end).map(RoundIndex::Round)
122 }
123}
124
125pub struct OperatorServer<C: CitreaClientT> {
126 pub operator: Operator<C>,
127 background_tasks: BackgroundTaskManager,
128}
129
130#[derive(Debug, Clone)]
131pub struct Operator<C: CitreaClientT> {
132 pub rpc: ExtendedBitcoinRpc,
133 pub db: Database,
134 pub signer: Actor,
135 pub config: BridgeConfig,
136 pub collateral_funding_outpoint: OutPoint,
137 pub(crate) reimburse_addr: Address,
138 #[cfg(feature = "automation")]
139 pub tx_sender: TxSenderClient,
140 #[cfg(feature = "automation")]
141 pub header_chain_prover: HeaderChainProver,
142 pub citrea_client: C,
143}
144
145impl<C> OperatorServer<C>
146where
147 C: CitreaClientT,
148{
149 pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
150 let operator = Operator::new(config.clone()).await?;
151 let background_tasks = BackgroundTaskManager::default();
152
153 Ok(Self {
154 operator,
155 background_tasks,
156 })
157 }
158
159 pub async fn start_background_tasks(&self) -> Result<(), BridgeError> {
162 #[cfg(feature = "automation")]
164 {
165 let state_manager = StateManager::new(
166 self.operator.db.clone(),
167 self.operator.clone(),
168 self.operator.rpc.clone(),
169 self.operator.config.clone(),
170 )
171 .await?;
172
173 let should_run_state_mgr = {
174 #[cfg(test)]
175 {
176 self.operator.config.test_params.should_run_state_manager
177 }
178 #[cfg(not(test))]
179 {
180 true
181 }
182 };
183
184 if should_run_state_mgr {
185 self.background_tasks
186 .ensure_task_looping(state_manager.block_fetcher_task().await?)
187 .await;
188 self.background_tasks
189 .ensure_task_looping(state_manager.into_task())
190 .await;
191 }
192 }
193
194 self.background_tasks
196 .ensure_task_looping(
197 PayoutCheckerTask::new(self.operator.db.clone(), self.operator.clone())
198 .with_delay(PAYOUT_CHECKER_POLL_DELAY),
199 )
200 .await;
201
202 self.background_tasks
203 .ensure_task_looping(
204 EntityMetricPublisher::<Operator<C>>::new(
205 self.operator.db.clone(),
206 self.operator.rpc.clone(),
207 self.operator.config.clone(),
208 )
209 .with_delay(ENTITY_METRIC_PUBLISHER_INTERVAL),
210 )
211 .await;
212
213 tracing::info!("Payout checker task started");
214
215 #[cfg(feature = "automation")]
217 {
218 self.operator.track_rounds().await?;
220 tracing::info!("Operator round state tracked");
221 }
222
223 Ok(())
224 }
225
226 pub async fn get_current_status(&self) -> Result<EntityStatus, BridgeError> {
227 let stopped_tasks = match self.background_tasks.get_stopped_tasks().await {
228 Ok(stopped_tasks) => stopped_tasks,
229 Err(e) => {
230 tracing::error!("Failed to get stopped tasks: {:?}", e);
231 StoppedTasks {
232 stopped_tasks: vec![format!("Stopped tasks fetch failed {:?}", e)],
233 }
234 }
235 };
236
237 let automation_enabled = cfg!(feature = "automation");
239
240 let sync_status = Operator::<C>::get_l1_status(
241 &self.operator.db,
242 &self.operator.rpc,
243 &self.operator.config,
244 )
245 .await?;
246
247 Ok(EntityStatus {
248 automation: automation_enabled,
249 wallet_balance: sync_status
250 .wallet_balance
251 .map(|balance| format!("{} BTC", balance.to_btc())),
252 tx_sender_synced_height: sync_status.tx_sender_synced_height,
253 finalized_synced_height: sync_status.finalized_synced_height,
254 hcp_last_proven_height: sync_status.hcp_last_proven_height,
255 rpc_tip_height: sync_status.rpc_tip_height,
256 bitcoin_syncer_synced_height: sync_status.btc_syncer_synced_height,
257 stopped_tasks: Some(stopped_tasks),
258 state_manager_next_height: sync_status.state_manager_next_height,
259 btc_fee_rate_sat_vb: sync_status.bitcoin_fee_rate_sat_vb,
260 })
261 }
262
263 pub async fn shutdown(&mut self) {
264 self.background_tasks.graceful_shutdown().await;
265 }
266}
267
268impl<C> Operator<C>
269where
270 C: CitreaClientT,
271{
272 pub async fn new(config: BridgeConfig) -> Result<Self, BridgeError> {
274 let signer = Actor::new(config.secret_key, config.protocol_paramset().network);
275
276 let db = Database::new(&config).await?;
277 let rpc = ExtendedBitcoinRpc::connect(
278 config.bitcoin_rpc_url.clone(),
279 config.bitcoin_rpc_user.clone(),
280 config.bitcoin_rpc_password.clone(),
281 None,
282 )
283 .await?;
284
285 #[cfg(feature = "automation")]
286 let tx_sender = TxSenderClient::new(db.clone(), Self::TX_SENDER_CONSUMER_ID.to_string());
287
288 if config.operator_withdrawal_fee_sats.is_none() {
289 return Err(eyre::eyre!("Operator withdrawal fee is not set").into());
290 }
291
292 let mut dbtx = db.begin_transaction().await?;
294 let op_data = db
295 .get_operator(Some(&mut dbtx), signer.xonly_public_key)
296 .await?;
297 let (collateral_funding_outpoint, reimburse_addr) = match op_data {
298 Some(operator_data) => {
299 (
302 operator_data.collateral_funding_outpoint,
303 operator_data.reimburse_addr,
304 )
305 }
306 None => {
307 let reimburse_addr = match &config.operator_reimbursement_address {
310 Some(reimburse_addr) => {
311 reimburse_addr
312 .to_owned()
313 .require_network(config.protocol_paramset().network)
314 .wrap_err(format!("Invalid operator reimbursement address provided in config: {:?} for network: {:?}", reimburse_addr, config.protocol_paramset().network))?
315 }
316 None => {
317 rpc
318 .get_new_address(Some("OperatorReimbursement"), Some(AddressType::Bech32m))
319 .await
320 .wrap_err("Failed to get new address")?
321 .require_network(config.protocol_paramset().network)
322 .wrap_err(format!("Invalid operator reimbursement address generated for the network in config: {:?}
323 Possibly the provided rpc's network and network given in config doesn't match", config.protocol_paramset().network))?
324 }
325 };
326 let outpoint = match &config.operator_collateral_funding_outpoint {
327 Some(outpoint) => {
328 let collateral_tx = rpc
330 .get_tx_of_txid(&outpoint.txid)
331 .await
332 .wrap_err("Failed to get collateral funding tx")?;
333 let collateral_txout = collateral_tx
334 .output
335 .get(outpoint.vout as usize)
336 .ok_or_eyre("Invalid vout index for collateral funding tx")?;
337 if collateral_txout.value
338 != config.protocol_paramset().collateral_funding_amount
339 {
340 return Err(eyre::eyre!("Operator collateral funding outpoint given in config has a different amount than the one specified in config..
341 Bridge collateral funding amount: {:?}, Amount in given outpoint: {:?}", config.protocol_paramset().collateral_funding_amount, collateral_txout.value).into());
342 }
343 if collateral_txout.script_pubkey != signer.address.script_pubkey() {
344 return Err(eyre::eyre!("Operator collateral funding outpoint given in config has a different script pubkey than the pubkey matching to the operator's secret key. Script pubkey should correspond to taproot address with no scripts and internal key equal to the operator's xonly public key.
345 Script pubkey in given outpoint: {:?}, Script pubkey should be: {:?}", collateral_txout.script_pubkey, signer.address.script_pubkey()).into());
346 }
347 *outpoint
348 }
349 None => {
350 rpc.send_to_address(
352 &signer.address,
353 config.protocol_paramset().collateral_funding_amount,
354 )
355 .await?
356 }
357 };
358 (outpoint, reimburse_addr)
359 }
360 };
361
362 db.insert_operator_if_not_exists(
363 Some(&mut dbtx),
364 signer.xonly_public_key,
365 &reimburse_addr,
366 collateral_funding_outpoint,
367 )
368 .await?;
369 dbtx.commit().await?;
370 let citrea_client = C::new(
371 config.citrea_rpc_url.clone(),
372 config.citrea_light_client_prover_url.clone(),
373 config.citrea_chain_id,
374 None,
375 config.citrea_request_timeout,
376 )
377 .await?;
378
379 tracing::info!(
380 "Operator xonly pk: {:?}, db created with name: {:?}",
381 signer.xonly_public_key,
382 config.db_name
383 );
384
385 #[cfg(feature = "automation")]
386 let header_chain_prover = HeaderChainProver::new(&config, rpc.clone()).await?;
387
388 Ok(Operator {
389 rpc,
390 db: db.clone(),
391 signer,
392 config,
393 collateral_funding_outpoint,
394 #[cfg(feature = "automation")]
395 tx_sender,
396 citrea_client,
397 #[cfg(feature = "automation")]
398 header_chain_prover,
399 reimburse_addr,
400 })
401 }
402
403 #[cfg(feature = "automation")]
404 pub async fn send_initial_round_tx(&self, round_tx: &Transaction) -> Result<(), BridgeError> {
405 let mut dbtx = self.db.begin_transaction().await?;
406 self.tx_sender
407 .insert_try_to_send(
408 &mut dbtx,
409 Some(TxMetadata {
410 tx_type: TransactionType::Round,
411 operator_xonly_pk: None,
412 round_idx: Some(RoundIndex::Round(0)),
413 kickoff_idx: None,
414 deposit_outpoint: None,
415 }),
416 round_tx,
417 FeePayingType::CPFP,
418 None,
419 &[],
420 &[],
421 &[],
422 &[],
423 )
424 .await?;
425 dbtx.commit().await?;
426 Ok(())
427 }
428
429 pub async fn get_params(
442 &self,
443 ) -> Result<
444 (
445 mpsc::Receiver<winternitz::PublicKey>,
446 mpsc::Receiver<schnorr::Signature>,
447 ),
448 BridgeError,
449 > {
450 tracing::info!("Generating operator params");
451 tracing::info!("Generating kickoff winternitz pubkeys");
452 let wpks = self.generate_kickoff_winternitz_pubkeys()?;
453 tracing::info!("Kickoff winternitz pubkeys generated");
454 let (wpk_tx, wpk_rx) = mpsc::channel(wpks.len());
455 let kickoff_wpks = KickoffWinternitzKeys::new(
456 wpks,
457 self.config.protocol_paramset().num_kickoffs_per_round,
458 self.config.protocol_paramset().num_round_txs,
459 )?;
460 tracing::info!("Starting to generate unspent kickoff signatures");
461 let kickoff_sigs = self.generate_unspent_kickoff_sigs(&kickoff_wpks)?;
462 tracing::info!("Unspent kickoff signatures generated");
463 let wpks = kickoff_wpks.get_all_keys();
464 let (sig_tx, sig_rx) = mpsc::channel(kickoff_sigs.len());
465
466 tokio::spawn(async move {
467 for wpk in wpks {
468 wpk_tx
469 .send(wpk)
470 .await
471 .wrap_err("Failed to send winternitz public key")?;
472 }
473
474 for sig in kickoff_sigs {
475 sig_tx
476 .send(sig)
477 .await
478 .wrap_err("Failed to send kickoff signature")?;
479 }
480
481 Ok::<(), BridgeError>(())
482 });
483
484 Ok((wpk_rx, sig_rx))
485 }
486
487 pub async fn deposit_sign(
488 &self,
489 mut deposit_data: DepositData,
490 ) -> Result<mpsc::Receiver<Result<schnorr::Signature, BridgeError>>, BridgeError> {
491 self.citrea_client
492 .check_nofn_correctness(deposit_data.get_nofn_xonly_pk()?)
493 .await?;
494
495 let mut tweak_cache = TweakCache::default();
496 let (sig_tx, sig_rx) = mpsc::channel(constants::DEFAULT_CHANNEL_SIZE);
497 let monitor_err_sender = sig_tx.clone();
498
499 let deposit_blockhash = self
500 .rpc
501 .get_blockhash_of_tx(&deposit_data.get_deposit_outpoint().txid)
502 .await?;
503
504 let mut sighash_stream = Box::pin(create_operator_sighash_stream(
505 self.db.clone(),
506 self.signer.xonly_public_key,
507 self.config.clone(),
508 deposit_data,
509 deposit_blockhash,
510 ));
511
512 let signer = self.signer.clone();
513 let handle = tokio::spawn(async move {
514 while let Some(sighash) = sighash_stream.next().await {
515 let (sighash, sig_info) = sighash?;
517 let sig = signer.sign_with_tweak_data(
518 sighash,
519 sig_info.tweak_data,
520 Some(&mut tweak_cache),
521 )?;
522
523 sig_tx
524 .send(Ok(sig))
525 .await
526 .wrap_err("Failed to send signature in operator deposit sign")?;
527 }
528
529 Ok::<(), BridgeError>(())
530 });
531 monitor_standalone_task(handle, "Operator deposit sign", monitor_err_sender);
532
533 Ok(sig_rx)
534 }
535
536 #[cfg(feature = "automation")]
538 pub async fn track_rounds(&self) -> Result<(), BridgeError> {
539 let mut dbtx = self.db.begin_transaction().await?;
540 self.db
543 .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
544 Some(&mut dbtx),
545 self.signer.xonly_public_key,
546 self.generate_kickoff_winternitz_pubkeys()?,
547 )
548 .await?;
549
550 StateManager::<Operator<C>>::dispatch_new_round_machine(
551 self.db.clone(),
552 &mut dbtx,
553 self.data(),
554 )
555 .await?;
556 dbtx.commit().await?;
557 Ok(())
558 }
559
560 fn is_profitable(
562 input_amount: Amount,
563 withdrawal_amount: Amount,
564 bridge_amount_sats: Amount,
565 operator_withdrawal_fee_sats: Amount,
566 ) -> bool {
567 let withdrawal_diff = match withdrawal_amount
569 .to_sat()
570 .checked_sub(input_amount.to_sat())
571 {
572 Some(diff) => Amount::from_sat(diff),
573 None => {
574 tracing::warn!(
576 "Some user gave more amount than the withdrawal amount as input for withdrawal"
577 );
578 return true;
579 }
580 };
581
582 if withdrawal_diff > bridge_amount_sats {
583 return false;
584 }
585
586 let net_profit = match bridge_amount_sats.checked_sub(withdrawal_diff) {
588 Some(profit) => profit,
589 None => return false, };
591
592 net_profit >= operator_withdrawal_fee_sats
595 }
596
597 pub async fn withdraw(
619 &self,
620 withdrawal_index: u32,
621 in_signature: taproot::Signature,
622 in_outpoint: OutPoint,
623 out_script_pubkey: ScriptBuf,
624 out_amount: Amount,
625 ) -> Result<Transaction, BridgeError> {
626 tracing::info!(
627 "Withdrawing with index: {}, in_signature: {:?}, in_outpoint: {:?}, out_script_pubkey: {}, out_amount: {}",
628 withdrawal_index,
629 in_signature,
630 in_outpoint,
631 out_script_pubkey,
632 out_amount
633 );
634
635 let input_prevout = self.rpc.get_txout_from_outpoint(&in_outpoint).await?;
637 let input_utxo = UTXO {
638 outpoint: in_outpoint,
639 txout: input_prevout,
640 };
641 let output_txout = TxOut {
642 value: out_amount,
643 script_pubkey: out_script_pubkey,
644 };
645
646 let withdrawal_utxo = self
648 .db
649 .get_withdrawal_utxo_from_citrea_withdrawal(None, withdrawal_index)
650 .await?;
651
652 if withdrawal_utxo != input_utxo.outpoint {
653 return Err(eyre::eyre!("Input UTXO does not match withdrawal UTXO from Citrea: Input Outpoint: {0}, Withdrawal Outpoint (from Citrea): {1}", input_utxo.outpoint, withdrawal_utxo).into());
654 }
655
656 let operator_withdrawal_fee_sats =
657 self.config
658 .operator_withdrawal_fee_sats
659 .ok_or(BridgeError::ConfigError(
660 "Operator withdrawal fee sats is not specified in configuration file"
661 .to_string(),
662 ))?;
663 if !Self::is_profitable(
664 input_utxo.txout.value,
665 output_txout.value,
666 self.config.protocol_paramset().bridge_amount,
667 operator_withdrawal_fee_sats,
668 ) {
669 return Err(eyre::eyre!("Not enough fee for operator").into());
670 }
671
672 let user_xonly_pk = &input_utxo
673 .txout
674 .script_pubkey
675 .try_get_taproot_pk()
676 .wrap_err("Input utxo script pubkey is not a valid taproot script")?;
677
678 let payout_txhandler = builder::transaction::create_payout_txhandler(
679 input_utxo,
680 output_txout,
681 self.signer.xonly_public_key,
682 in_signature,
683 self.config.protocol_paramset().network,
684 )?;
685
686 let sighash = payout_txhandler.calculate_sighash_txin(0, in_signature.sighash_type)?;
689
690 SECP.verify_schnorr(
691 &in_signature.signature,
692 &Message::from_digest(*sighash.as_byte_array()),
693 user_xonly_pk,
694 )
695 .wrap_err("Failed to verify signature received from user for payout txin. Ensure the signature uses SinglePlusAnyoneCanPay sighash type.")?;
696
697 let fee_rate_result = self
698 .rpc
699 .get_fee_rate(
700 self.config.protocol_paramset.network,
701 &self.config.mempool_api_host,
702 &self.config.mempool_api_endpoint,
703 self.config.tx_sender_limits.mempool_fee_rate_multiplier,
704 self.config.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
705 self.config.tx_sender_limits.fee_rate_hard_cap,
706 )
707 .await;
708
709 let fee_rate_option = match fee_rate_result {
710 Ok(fee_rate) => Some(Amount::from_sat(fee_rate.to_sat_per_vb_ceil() * 1000)),
711 Err(e) => {
712 tracing::warn!("Failed to get fee rate from mempool API; funding tx with automatic fee rate. Error: {e:?}");
713 None
714 }
715 };
716
717 let funded_tx = self
719 .rpc
720 .fund_raw_transaction(
721 payout_txhandler.get_cached_tx(),
722 Some(&bitcoincore_rpc::json::FundRawTransactionOptions {
723 add_inputs: Some(true),
724 change_address: None,
725 change_position: Some(1),
726 change_type: None,
727 include_watching: None,
728 lock_unspents: Some(false),
729 fee_rate: fee_rate_option,
730 subtract_fee_from_outputs: None,
731 replaceable: None,
732 conf_target: None,
733 estimate_mode: None,
734 }),
735 None,
736 )
737 .await
738 .wrap_err("Failed to fund raw transaction")?
739 .hex;
740
741 let signed_tx = self
742 .rpc
743 .sign_raw_transaction_with_wallet(&funded_tx, None, None)
744 .await
745 .wrap_err("Failed to sign withdrawal transaction")?
746 .hex;
747
748 let signed_tx: Transaction = bitcoin::consensus::deserialize(&signed_tx)
749 .wrap_err("Failed to deserialize signed withdrawal transaction")?;
750
751 self.rpc
752 .send_raw_transaction(&signed_tx)
753 .await
754 .wrap_err("Failed to send withdrawal transaction")?;
755
756 Ok(signed_tx)
757 }
758
759 pub fn generate_assert_winternitz_pubkeys(
766 &self,
767 deposit_outpoint: bitcoin::OutPoint,
768 ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
769 tracing::debug!("Generating assert winternitz pubkeys");
770 let bitvm_pks = self
771 .signer
772 .generate_bitvm_pks_for_deposit(deposit_outpoint, self.config.protocol_paramset())?;
773
774 let flattened_wpks = bitvm_pks.to_flattened_vec();
775
776 Ok(flattened_wpks)
777 }
778 pub fn generate_kickoff_winternitz_pubkeys(
786 &self,
787 ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
788 let mut winternitz_pubkeys =
789 Vec::with_capacity(self.config.get_num_kickoff_winternitz_pks());
790
791 for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs + 1)
793 {
794 for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
795 let path = WinternitzDerivationPath::Kickoff(
796 round_idx,
797 kickoff_idx as u32,
798 self.config.protocol_paramset(),
799 );
800 winternitz_pubkeys.push(self.signer.derive_winternitz_pk(path)?);
801 }
802 }
803
804 if winternitz_pubkeys.len() != self.config.get_num_kickoff_winternitz_pks() {
805 return Err(eyre::eyre!(
806 "Expected {} number of kickoff winternitz pubkeys, but got {}",
807 self.config.get_num_kickoff_winternitz_pks(),
808 winternitz_pubkeys.len()
809 )
810 .into());
811 }
812
813 Ok(winternitz_pubkeys)
814 }
815
816 pub fn generate_unspent_kickoff_sigs(
817 &self,
818 kickoff_wpks: &KickoffWinternitzKeys,
819 ) -> Result<Vec<Signature>, BridgeError> {
820 let mut tweak_cache = TweakCache::default();
821 let mut sigs: Vec<Signature> =
822 Vec::with_capacity(self.config.get_num_unspent_kickoff_sigs());
823 let mut prev_ready_to_reimburse: Option<TxHandler> = None;
824 let operator_data = OperatorData {
825 xonly_pk: self.signer.xonly_public_key,
826 collateral_funding_outpoint: self.collateral_funding_outpoint,
827 reimburse_addr: self.reimburse_addr.clone(),
828 };
829 for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
830 let txhandlers = create_round_txhandlers(
831 self.config.protocol_paramset(),
832 round_idx,
833 &operator_data,
834 kickoff_wpks,
835 prev_ready_to_reimburse.as_ref(),
836 )?;
837 for txhandler in txhandlers {
838 if let TransactionType::UnspentKickoff(kickoff_idx) =
839 txhandler.get_transaction_type()
840 {
841 let partial = PartialSignatureInfo {
842 operator_idx: 0, round_idx,
844 kickoff_utxo_idx: kickoff_idx,
845 };
846 let sighashes = txhandler
847 .calculate_shared_txins_sighash(EntityType::OperatorSetup, partial)?;
848 let signed_sigs: Result<Vec<_>, _> = sighashes
849 .into_iter()
850 .map(|(sighash, sig_info)| {
851 self.signer.sign_with_tweak_data(
852 sighash,
853 sig_info.tweak_data,
854 Some(&mut tweak_cache),
855 )
856 })
857 .collect();
858 sigs.extend(signed_sigs?);
859 }
860 if let TransactionType::ReadyToReimburse = txhandler.get_transaction_type() {
861 prev_ready_to_reimburse = Some(txhandler);
862 }
863 }
864 }
865 if sigs.len() != self.config.get_num_unspent_kickoff_sigs() {
866 return Err(eyre::eyre!(
867 "Expected {} number of unspent kickoff sigs, but got {}",
868 self.config.get_num_unspent_kickoff_sigs(),
869 sigs.len()
870 )
871 .into());
872 }
873 Ok(sigs)
874 }
875
876 pub fn generate_challenge_ack_preimages_and_hashes(
877 &self,
878 deposit_data: &DepositData,
879 ) -> Result<Vec<PublicHash>, BridgeError> {
880 let mut hashes = Vec::with_capacity(self.config.get_num_challenge_ack_hashes(deposit_data));
881
882 for watchtower_idx in 0..deposit_data.get_num_watchtowers() {
883 let path = WinternitzDerivationPath::ChallengeAckHash(
884 watchtower_idx as u32,
885 deposit_data.get_deposit_outpoint(),
886 self.config.protocol_paramset(),
887 );
888 let hash = self.signer.generate_public_hash_from_path(path)?;
889 hashes.push(hash);
890 }
891
892 if hashes.len() != self.config.get_num_challenge_ack_hashes(deposit_data) {
893 return Err(eyre::eyre!(
894 "Expected {} number of challenge ack hashes, but got {}",
895 self.config.get_num_challenge_ack_hashes(deposit_data),
896 hashes.len()
897 )
898 .into());
899 }
900
901 Ok(hashes)
902 }
903
904 pub async fn handle_finalized_payout<'a>(
905 &'a self,
906 dbtx: DatabaseTransaction<'a, '_>,
907 deposit_outpoint: OutPoint,
908 payout_tx_blockhash: BlockHash,
909 ) -> Result<bitcoin::Txid, BridgeError> {
910 let (deposit_id, deposit_data) = self
911 .db
912 .get_deposit_data(Some(dbtx), deposit_outpoint)
913 .await?
914 .ok_or(BridgeError::DatabaseError(sqlx::Error::RowNotFound))?;
915
916 let (round_idx, kickoff_idx) = self
918 .db
919 .get_unused_and_signed_kickoff_connector(
920 Some(dbtx),
921 deposit_id,
922 self.signer.xonly_public_key,
923 )
924 .await?
925 .ok_or(BridgeError::DatabaseError(sqlx::Error::RowNotFound))?;
926
927 let current_round_index = self.db.get_current_round_index(Some(dbtx)).await?;
928 #[cfg(feature = "automation")]
929 if current_round_index != round_idx {
930 if current_round_index.next_round() != round_idx {
934 return Err(eyre::eyre!(
935 "Internal error: Expected the current round ({:?}) to be equal to or 1 less than the round of the first available kickoff for deposit reimbursement ({:?}) for deposit {:?}. If the round is less than the current round, there is an issue with the logic of the fn that gets the first available kickoff. If the round is greater, that means the next round do not have any kickoff connectors available for reimbursement, which should not be possible.",
936 current_round_index, round_idx, deposit_outpoint
937 ).into());
938 }
939 self.end_round(dbtx).await?;
941 }
942
943 let kickoff_data = KickoffData {
945 operator_xonly_pk: self.signer.xonly_public_key,
946 round_idx,
947 kickoff_idx,
948 };
949
950 let payout_tx_blockhash = payout_tx_blockhash.as_byte_array().last_20_bytes();
951
952 #[cfg(test)]
953 let payout_tx_blockhash = self
954 .config
955 .test_params
956 .maybe_disrupt_payout_tx_block_hash_commit(payout_tx_blockhash);
957
958 let context = ContractContext::new_context_for_kickoff(
959 kickoff_data,
960 deposit_data,
961 self.config.protocol_paramset(),
962 );
963
964 let signed_txs = create_and_sign_txs(
965 self.db.clone(),
966 &self.signer,
967 self.config.clone(),
968 context,
969 Some(payout_tx_blockhash),
970 Some(dbtx),
971 )
972 .await?;
973
974 let tx_metadata = Some(TxMetadata {
975 tx_type: TransactionType::Dummy, operator_xonly_pk: Some(self.signer.xonly_public_key),
977 round_idx: Some(round_idx),
978 kickoff_idx: Some(kickoff_idx),
979 deposit_outpoint: Some(deposit_outpoint),
980 });
981
982 for (tx_type, signed_tx) in &signed_txs {
984 match *tx_type {
985 TransactionType::Kickoff
986 | TransactionType::OperatorChallengeAck(_)
987 | TransactionType::WatchtowerChallengeTimeout(_)
988 | TransactionType::ChallengeTimeout
989 | TransactionType::DisproveTimeout
990 | TransactionType::Reimburse => {
991 #[cfg(feature = "automation")]
992 self.tx_sender
993 .add_tx_to_queue(
994 dbtx,
995 *tx_type,
996 signed_tx,
997 &signed_txs,
998 tx_metadata,
999 &self.config,
1000 None,
1001 )
1002 .await?;
1003 }
1004 _ => {}
1005 }
1006 }
1007
1008 let kickoff_txid = signed_txs
1009 .iter()
1010 .find_map(|(tx_type, tx)| {
1011 if let TransactionType::Kickoff = tx_type {
1012 Some(tx.compute_txid())
1013 } else {
1014 None
1015 }
1016 })
1017 .ok_or(eyre::eyre!(
1018 "Couldn't find kickoff tx in signed_txs".to_string(),
1019 ))?;
1020
1021 self.db
1023 .mark_kickoff_connector_as_used(Some(dbtx), round_idx, kickoff_idx, Some(kickoff_txid))
1024 .await?;
1025
1026 Ok(kickoff_txid)
1027 }
1028
1029 #[cfg(feature = "automation")]
1030 async fn start_first_round(
1031 &self,
1032 dbtx: DatabaseTransaction<'_, '_>,
1033 kickoff_wpks: KickoffWinternitzKeys,
1034 ) -> Result<(), BridgeError> {
1035 let (mut first_round_tx, _) = create_round_nth_txhandler(
1037 self.signer.xonly_public_key,
1038 self.collateral_funding_outpoint,
1039 self.config.protocol_paramset().collateral_funding_amount,
1040 RoundIndex::Round(0),
1041 &kickoff_wpks,
1042 self.config.protocol_paramset(),
1043 )?;
1044
1045 self.signer
1046 .tx_sign_and_fill_sigs(&mut first_round_tx, &[], None)
1047 .wrap_err("Failed to sign first round tx")?;
1048
1049 self.tx_sender
1050 .insert_try_to_send(
1051 dbtx,
1052 Some(TxMetadata {
1053 tx_type: TransactionType::Round,
1054 operator_xonly_pk: None,
1055 round_idx: Some(RoundIndex::Round(0)),
1056 kickoff_idx: None,
1057 deposit_outpoint: None,
1058 }),
1059 first_round_tx.get_cached_tx(),
1060 FeePayingType::CPFP,
1061 None,
1062 &[],
1063 &[],
1064 &[],
1065 &[],
1066 )
1067 .await?;
1068
1069 self.db
1071 .update_current_round_index(Some(dbtx), RoundIndex::Round(0))
1072 .await?;
1073
1074 Ok(())
1075 }
1076
1077 #[cfg(feature = "automation")]
1078 pub async fn end_round<'a>(
1079 &'a self,
1080 dbtx: DatabaseTransaction<'a, '_>,
1081 ) -> Result<(), BridgeError> {
1082 let current_round_index = self.db.get_current_round_index(Some(dbtx)).await?;
1084
1085 let mut activation_prerequisites = Vec::new();
1086
1087 let operator_winternitz_public_keys = self
1088 .db
1089 .get_operator_kickoff_winternitz_public_keys(None, self.signer.xonly_public_key)
1090 .await?;
1091 let kickoff_wpks = KickoffWinternitzKeys::new(
1092 operator_winternitz_public_keys,
1093 self.config.protocol_paramset().num_kickoffs_per_round,
1094 self.config.protocol_paramset().num_round_txs,
1095 )?;
1096
1097 if current_round_index == RoundIndex::Collateral {
1099 return self.start_first_round(dbtx, kickoff_wpks).await;
1100 }
1101
1102 let (current_round_txhandler, mut ready_to_reimburse_txhandler) =
1103 create_round_nth_txhandler(
1104 self.signer.xonly_public_key,
1105 self.collateral_funding_outpoint,
1106 self.config.protocol_paramset().collateral_funding_amount,
1107 current_round_index,
1108 &kickoff_wpks,
1109 self.config.protocol_paramset(),
1110 )?;
1111
1112 let (mut next_round_txhandler, _) = create_round_nth_txhandler(
1113 self.signer.xonly_public_key,
1114 self.collateral_funding_outpoint,
1115 self.config.protocol_paramset().collateral_funding_amount,
1116 current_round_index.next_round(),
1117 &kickoff_wpks,
1118 self.config.protocol_paramset(),
1119 )?;
1120
1121 let mut tweak_cache = TweakCache::default();
1122
1123 self.signer
1125 .tx_sign_and_fill_sigs(
1126 &mut ready_to_reimburse_txhandler,
1127 &[],
1128 Some(&mut tweak_cache),
1129 )
1130 .wrap_err("Failed to sign ready to reimburse tx")?;
1131
1132 self.signer
1134 .tx_sign_and_fill_sigs(&mut next_round_txhandler, &[], Some(&mut tweak_cache))
1135 .wrap_err("Failed to sign next round tx")?;
1136
1137 let current_round_txid = current_round_txhandler.get_cached_tx().compute_txid();
1138 let ready_to_reimburse_tx = ready_to_reimburse_txhandler.get_cached_tx();
1139 let next_round_tx = next_round_txhandler.get_cached_tx();
1140
1141 let ready_to_reimburse_txid = ready_to_reimburse_tx.compute_txid();
1142
1143 let mut unspent_kickoff_connector_indices = Vec::new();
1144
1145 for kickoff_connector_idx in
1147 0..self.config.protocol_paramset().num_kickoffs_per_round as u32
1148 {
1149 let kickoff_txid = self
1150 .db
1151 .get_kickoff_txid_for_used_kickoff_connector(
1152 Some(dbtx),
1153 current_round_index,
1154 kickoff_connector_idx,
1155 )
1156 .await?;
1157 match kickoff_txid {
1158 Some(kickoff_txid) => {
1159 activation_prerequisites.push(ActivatedWithOutpoint {
1160 outpoint: OutPoint {
1161 txid: kickoff_txid,
1162 vout: UtxoVout::KickoffFinalizer.get_vout(), },
1164 relative_block_height: self.config.protocol_paramset().finality_depth - 1,
1165 });
1166 }
1167 None => {
1168 let unspent_kickoff_connector = OutPoint {
1169 txid: current_round_txid,
1170 vout: UtxoVout::Kickoff(kickoff_connector_idx as usize).get_vout(),
1171 };
1172 unspent_kickoff_connector_indices.push(kickoff_connector_idx as usize);
1173 self.db
1174 .mark_kickoff_connector_as_used(
1175 Some(dbtx),
1176 current_round_index,
1177 kickoff_connector_idx,
1178 None,
1179 )
1180 .await?;
1181 activation_prerequisites.push(ActivatedWithOutpoint {
1182 outpoint: unspent_kickoff_connector,
1183 relative_block_height: self.config.protocol_paramset().finality_depth - 1,
1184 });
1185 }
1186 }
1187 }
1188
1189 let mut burn_unspent_kickoff_connectors_tx =
1191 create_burn_unused_kickoff_connectors_txhandler(
1192 ¤t_round_txhandler,
1193 &unspent_kickoff_connector_indices,
1194 &self.signer.address,
1195 self.config.protocol_paramset(),
1196 )?;
1197
1198 self.signer
1200 .tx_sign_and_fill_sigs(
1201 &mut burn_unspent_kickoff_connectors_tx,
1202 &[],
1203 Some(&mut tweak_cache),
1204 )
1205 .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
1206
1207 self.tx_sender
1208 .insert_try_to_send(
1209 dbtx,
1210 Some(TxMetadata {
1211 tx_type: TransactionType::BurnUnusedKickoffConnectors,
1212 operator_xonly_pk: Some(self.signer.xonly_public_key),
1213 round_idx: Some(current_round_index),
1214 kickoff_idx: None,
1215 deposit_outpoint: None,
1216 }),
1217 burn_unspent_kickoff_connectors_tx.get_cached_tx(),
1218 FeePayingType::CPFP,
1219 None,
1220 &[],
1221 &[],
1222 &[],
1223 &[],
1224 )
1225 .await?;
1226
1227 self.tx_sender
1229 .insert_try_to_send(
1230 dbtx,
1231 Some(TxMetadata {
1232 tx_type: TransactionType::ReadyToReimburse,
1233 operator_xonly_pk: Some(self.signer.xonly_public_key),
1234 round_idx: Some(current_round_index),
1235 kickoff_idx: None,
1236 deposit_outpoint: None,
1237 }),
1238 ready_to_reimburse_tx,
1239 FeePayingType::CPFP,
1240 None,
1241 &[],
1242 &[],
1243 &[],
1244 &activation_prerequisites,
1245 )
1246 .await?;
1247
1248 self.tx_sender
1250 .insert_try_to_send(
1251 dbtx,
1252 Some(TxMetadata {
1253 tx_type: TransactionType::Round,
1254 operator_xonly_pk: Some(self.signer.xonly_public_key),
1255 round_idx: Some(current_round_index.next_round()),
1256 kickoff_idx: None,
1257 deposit_outpoint: None,
1258 }),
1259 next_round_tx,
1260 FeePayingType::CPFP,
1261 None,
1262 &[],
1263 &[],
1264 &[ActivatedWithTxid {
1265 txid: ready_to_reimburse_txid,
1266 relative_block_height: self
1267 .config
1268 .protocol_paramset()
1269 .operator_reimburse_timelock
1270 as u32,
1271 }],
1272 &[],
1273 )
1274 .await?;
1275
1276 self.db
1278 .update_current_round_index(Some(dbtx), current_round_index.next_round())
1279 .await?;
1280
1281 Ok(())
1282 }
1283
1284 #[cfg(feature = "automation")]
1285 async fn send_asserts(
1286 &self,
1287 dbtx: DatabaseTransaction<'_, '_>,
1288 kickoff_data: KickoffData,
1289 deposit_data: DepositData,
1290 watchtower_challenges: HashMap<usize, Transaction>,
1291 _payout_blockhash: Witness,
1292 latest_blockhash: Witness,
1293 ) -> Result<(), BridgeError> {
1294 use bridge_circuit_host::utils::{get_verifying_key, is_dev_mode};
1295 use citrea_sov_rollup_interface::zk::light_client_proof::output::LightClientCircuitOutput;
1296
1297 let context = ContractContext::new_context_for_kickoff(
1298 kickoff_data,
1299 deposit_data.clone(),
1300 self.config.protocol_paramset(),
1301 );
1302 let mut db_cache = crate::builder::transaction::ReimburseDbCache::from_context(
1303 self.db.clone(),
1304 &context,
1305 Some(dbtx),
1306 );
1307 let txhandlers = builder::transaction::create_txhandlers(
1308 TransactionType::Kickoff,
1309 context,
1310 &mut crate::builder::transaction::TxHandlerCache::new(),
1311 &mut db_cache,
1312 )
1313 .await?;
1314 let move_txid = txhandlers
1315 .get(&TransactionType::MoveToVault)
1316 .ok_or(eyre::eyre!(
1317 "Move to vault txhandler not found in send_asserts"
1318 ))?
1319 .get_cached_tx()
1320 .compute_txid();
1321 let kickoff_tx = txhandlers
1322 .get(&TransactionType::Kickoff)
1323 .ok_or(eyre::eyre!("Kickoff txhandler not found in send_asserts"))?
1324 .get_cached_tx();
1325
1326 #[cfg(test)]
1327 self.config
1328 .test_params
1329 .maybe_save_kickoff_and_wtc_txs(kickoff_tx, &watchtower_challenges, 1, &self.rpc)
1330 .await?;
1331
1332 let (payout_op_xonly_pk_opt, payout_block_hash, payout_txid, deposit_idx) = self
1333 .db
1334 .get_payout_info_from_move_txid(Some(dbtx), move_txid)
1335 .await
1336 .wrap_err("Failed to get payout info from db during sending asserts.")?
1337 .ok_or_eyre(format!(
1338 "Payout info not found in db while sending asserts for move txid: {move_txid}"
1339 ))?;
1340
1341 let payout_op_xonly_pk = payout_op_xonly_pk_opt.ok_or_eyre(format!(
1342 "Payout operator xonly pk not found in payout info DB while sending asserts for deposit move txid: {move_txid}"
1343 ))?;
1344
1345 tracing::info!("Sending asserts for deposit_idx: {deposit_idx:?}");
1346
1347 if payout_op_xonly_pk != kickoff_data.operator_xonly_pk {
1348 return Err(eyre::eyre!(
1349 "Payout operator xonly pk does not match kickoff operator xonly pk in send_asserts"
1350 )
1351 .into());
1352 }
1353
1354 let (payout_block_height, payout_block) = self
1355 .db
1356 .get_full_block_from_hash(Some(dbtx), payout_block_hash)
1357 .await?
1358 .ok_or_eyre(format!(
1359 "Payout block {payout_op_xonly_pk:?} {payout_block_hash:?} not found in db",
1360 ))?;
1361
1362 let payout_tx_index = payout_block
1363 .txdata
1364 .iter()
1365 .position(|tx| tx.compute_txid() == payout_txid)
1366 .ok_or_eyre(format!(
1367 "Payout txid {payout_txid:?} not found in block {payout_op_xonly_pk:?} {payout_block_hash:?}"
1368 ))?;
1369 let payout_tx = &payout_block.txdata[payout_tx_index];
1370 tracing::debug!("Calculated payout tx in send_asserts: {:?}", payout_tx);
1371
1372 let lcp_receipt = self
1373 .citrea_client
1374 .fetch_validate_and_store_lcp(
1375 payout_block_height as u64,
1376 deposit_idx as u32,
1377 &self.db,
1378 Some(dbtx),
1379 self.config.protocol_paramset(),
1380 )
1381 .await?;
1382 let proof_output: LightClientCircuitOutput = borsh::from_slice(&lcp_receipt.journal.bytes)
1383 .wrap_err("Failed to deserialize light client circuit output")?;
1384 let l2_height = proof_output.last_l2_height;
1385 let light_client_proof = LightClientProof {
1386 lc_journal: lcp_receipt.journal.bytes.clone(),
1387 };
1388
1389 tracing::info!("Got light client proof in send_asserts");
1390
1391 let storage_proof = self
1392 .citrea_client
1393 .get_storage_proof(l2_height, deposit_idx as u32)
1394 .await
1395 .wrap_err(format!(
1396 "Failed to get storage proof for move txid {move_txid:?}, l2 height {l2_height}, deposit_idx {deposit_idx}",
1397 ))?;
1398
1399 tracing::debug!("Got storage proof in send_asserts {storage_proof:?}");
1400
1401 let wt_derive_path = ClementineBitVMPublicKeys::get_latest_blockhash_derivation(
1403 deposit_data.get_deposit_outpoint(),
1404 self.config.protocol_paramset(),
1405 );
1406 let commits = extract_winternitz_commits(
1407 latest_blockhash,
1408 &[wt_derive_path],
1409 self.config.protocol_paramset(),
1410 )?;
1411
1412 let latest_blockhash_last_20: [u8; 20] = commits
1413 .first()
1414 .ok_or_eyre("Failed to get latest blockhash in send_asserts")?
1415 .to_owned()
1416 .try_into()
1417 .map_err(|_| eyre::eyre!("Committed latest blockhash is not 20 bytes long"))?;
1418
1419 #[cfg(test)]
1420 let latest_blockhash_last_20 = self
1421 .config
1422 .test_params
1423 .maybe_disrupt_latest_block_hash_commit(latest_blockhash_last_20);
1424
1425 let rpc_current_finalized_height = self
1426 .rpc
1427 .get_current_chain_height()
1428 .await?
1429 .saturating_sub(self.config.protocol_paramset().finality_depth - 1);
1430
1431 self.db
1433 .fetch_and_save_missing_blocks(
1434 Some(dbtx),
1435 &self.rpc,
1436 self.config.protocol_paramset().genesis_height,
1437 rpc_current_finalized_height + 1,
1438 )
1439 .await?;
1440
1441 let current_height = self
1442 .db
1443 .get_latest_finalized_block_height(Some(dbtx))
1444 .await?
1445 .ok_or_eyre("Failed to get current finalized block height")?;
1446
1447 let block_hashes = self
1448 .db
1449 .get_block_info_from_range(
1450 Some(dbtx),
1451 self.config.protocol_paramset().genesis_height as u64,
1452 current_height,
1453 )
1454 .await?;
1455
1456 let latest_blockhash_index = block_hashes
1458 .iter()
1459 .position(|(block_hash, _)| {
1460 block_hash.as_byte_array().last_20_bytes() == latest_blockhash_last_20
1461 })
1462 .ok_or_eyre("Failed to find latest blockhash in send_asserts")?;
1463
1464 let latest_blockhash = block_hashes[latest_blockhash_index].0;
1465
1466 let (current_hcp, _hcp_height) = self
1467 .header_chain_prover
1468 .prove_till_hash(latest_blockhash)
1469 .await?;
1470
1471 #[cfg(test)]
1472 let mut total_works: Vec<[u8; 16]> = Vec::with_capacity(watchtower_challenges.len());
1473
1474 #[cfg(test)]
1475 {
1476 use bridge_circuit_host::utils::total_work_from_wt_tx;
1477 for (_, tx) in watchtower_challenges.iter() {
1478 let total_work = total_work_from_wt_tx(tx);
1479 total_works.push(total_work);
1480 }
1481 tracing::debug!("Total works: {:?}", total_works);
1482 }
1483
1484 #[cfg(test)]
1485 let current_hcp = self
1486 .config
1487 .test_params
1488 .maybe_override_current_hcp(
1489 current_hcp,
1490 payout_block_hash,
1491 &block_hashes,
1492 &self.header_chain_prover,
1493 total_works.clone(),
1494 )
1495 .await?;
1496
1497 tracing::info!("Got header chain proof in send_asserts");
1498
1499 let blockhashes_serialized: Vec<[u8; 32]> = block_hashes
1500 .iter()
1501 .take(latest_blockhash_index + 1)
1502 .map(|(h, _)| h.to_byte_array())
1503 .collect();
1504
1505 #[cfg(test)]
1506 let blockhashes_serialized = self
1507 .config
1508 .test_params
1509 .maybe_override_blockhashes_serialized(
1510 blockhashes_serialized,
1511 payout_block_height,
1512 self.config.protocol_paramset().genesis_height,
1513 total_works,
1514 );
1515
1516 tracing::debug!(
1517 "Genesis height - Before SPV: {},",
1518 self.config.protocol_paramset().genesis_height
1519 );
1520
1521 let spv = create_spv(
1522 payout_tx.clone(),
1523 &blockhashes_serialized,
1524 payout_block.clone(),
1525 payout_block_height,
1526 self.config.protocol_paramset().genesis_height,
1527 payout_tx_index as u32,
1528 )?;
1529 tracing::info!("Calculated spv proof in send_asserts");
1530
1531 let mut wt_contexts = Vec::new();
1532 for (_, tx) in watchtower_challenges.iter() {
1533 wt_contexts.push(WatchtowerContext {
1534 watchtower_tx: tx.clone(),
1535 prevout_txs: self.rpc.get_prevout_txs(tx).await?,
1536 });
1537 }
1538
1539 #[cfg(test)]
1540 {
1541 if self.config.test_params.operator_forgot_watchtower_challenge {
1542 tracing::info!("Disrupting watchtower challenges in send_asserts");
1543 wt_contexts.pop();
1544 }
1545 }
1546
1547 let watchtower_challenge_connector_start_idx =
1548 (FIRST_FIVE_OUTPUTS + ClementineBitVMPublicKeys::number_of_assert_txs()) as u32;
1549
1550 let bridge_circuit_host_params = BridgeCircuitHostParams::new_with_wt_tx(
1551 kickoff_tx.clone(),
1552 spv,
1553 current_hcp,
1554 light_client_proof,
1555 lcp_receipt,
1556 storage_proof,
1557 self.config.protocol_paramset().network,
1558 &wt_contexts,
1559 watchtower_challenge_connector_start_idx,
1560 )
1561 .wrap_err("Failed to create bridge circuit host params in send_asserts")?;
1562
1563 let bridge_circuit_elf = match self.config.protocol_paramset().network {
1564 bitcoin::Network::Bitcoin => MAINNET_BRIDGE_CIRCUIT_ELF,
1565 bitcoin::Network::Testnet4 => {
1566 if is_dev_mode() {
1567 TESTNET4_BRIDGE_CIRCUIT_ELF_TEST
1568 } else {
1569 TESTNET4_BRIDGE_CIRCUIT_ELF
1570 }
1571 }
1572 bitcoin::Network::Signet => {
1573 if is_dev_mode() {
1574 SIGNET_BRIDGE_CIRCUIT_ELF_TEST
1575 } else {
1576 SIGNET_BRIDGE_CIRCUIT_ELF
1577 }
1578 }
1579 bitcoin::Network::Regtest => {
1580 if is_dev_mode() {
1581 REGTEST_BRIDGE_CIRCUIT_ELF_TEST
1582 } else {
1583 REGTEST_BRIDGE_CIRCUIT_ELF
1584 }
1585 }
1586 _ => {
1587 return Err(eyre::eyre!(
1588 "Unsupported network {:?} in send_asserts",
1589 self.config.protocol_paramset().network
1590 )
1591 .into())
1592 }
1593 };
1594 tracing::info!("Starting proving bridge circuit to send asserts");
1595
1596 #[cfg(test)]
1597 self.config
1598 .test_params
1599 .maybe_dump_bridge_circuit_params_to_file(&bridge_circuit_host_params)?;
1600
1601 #[cfg(test)]
1602 self.config
1603 .test_params
1604 .maybe_dump_bridge_circuit_params_to_file(&bridge_circuit_host_params)?;
1605
1606 let (g16_proof, g16_output, public_inputs) = tokio::task::spawn_blocking(move || {
1607 prove_bridge_circuit(bridge_circuit_host_params, bridge_circuit_elf)
1608 })
1609 .await
1610 .wrap_err("Failed to join the prove_bridge_circuit task")?
1611 .wrap_err("Failed to prove bridge circuit")?;
1612
1613 tracing::info!("Proved bridge circuit in send_asserts");
1614 let public_input_scalar = ark_bn254::Fr::from_be_bytes_mod_order(&g16_output);
1615
1616 #[cfg(test)]
1617 let mut public_inputs = public_inputs;
1618
1619 #[cfg(test)]
1620 {
1621 if self
1622 .config
1623 .test_params
1624 .disrupt_challenge_sending_watchtowers_commit
1625 {
1626 tracing::info!("Disrupting challenge sending watchtowers commit in send_asserts");
1627 public_inputs.challenge_sending_watchtowers[0] ^= 0x01;
1628 tracing::info!(
1629 "Disrupted challenge sending watchtowers commit: {:?}",
1630 public_inputs.challenge_sending_watchtowers
1631 );
1632 }
1633 }
1634
1635 tracing::info!(
1636 "Challenge sending watchtowers commit: {:?}",
1637 public_inputs.challenge_sending_watchtowers
1638 );
1639
1640 let asserts = tokio::task::spawn_blocking(move || {
1641 let vk = get_verifying_key();
1642
1643 generate_assertions(g16_proof, vec![public_input_scalar], &vk).map_err(|e| {
1644 eyre::eyre!(
1645 "Failed to generate {}assertions: {}",
1646 if is_dev_mode() { "dev mode " } else { "" },
1647 e
1648 )
1649 })
1650 })
1651 .await
1652 .wrap_err("Generate assertions thread failed with error")??;
1653
1654 tracing::warn!("Generated assertions in send_asserts");
1655
1656 #[cfg(test)]
1657 let asserts = self.config.test_params.maybe_corrupt_asserts(asserts);
1658
1659 let assert_txs = self
1660 .create_assert_commitment_txs(
1661 TransactionRequestData {
1662 kickoff_data,
1663 deposit_outpoint: deposit_data.get_deposit_outpoint(),
1664 },
1665 ClementineBitVMPublicKeys::get_assert_commit_data(
1666 asserts,
1667 &public_inputs.challenge_sending_watchtowers,
1668 ),
1669 Some(dbtx),
1670 )
1671 .await?;
1672
1673 for (tx_type, tx) in assert_txs {
1674 self.tx_sender
1675 .add_tx_to_queue(
1676 dbtx,
1677 tx_type,
1678 &tx,
1679 &[],
1680 Some(TxMetadata {
1681 tx_type,
1682 operator_xonly_pk: Some(self.signer.xonly_public_key),
1683 round_idx: Some(kickoff_data.round_idx),
1684 kickoff_idx: Some(kickoff_data.kickoff_idx),
1685 deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
1686 }),
1687 &self.config,
1688 None,
1689 )
1690 .await?;
1691 }
1692 Ok(())
1693 }
1694
1695 #[cfg(feature = "automation")]
1696 fn data(&self) -> OperatorData {
1697 OperatorData {
1698 xonly_pk: self.signer.xonly_public_key,
1699 collateral_funding_outpoint: self.collateral_funding_outpoint,
1700 reimburse_addr: self.reimburse_addr.clone(),
1701 }
1702 }
1703
1704 #[cfg(feature = "automation")]
1705 async fn send_latest_blockhash(
1706 &self,
1707 dbtx: DatabaseTransaction<'_, '_>,
1708 kickoff_data: KickoffData,
1709 deposit_data: DepositData,
1710 latest_blockhash: BlockHash,
1711 ) -> Result<(), BridgeError> {
1712 tracing::warn!("Operator sending latest blockhash");
1713 let deposit_outpoint = deposit_data.get_deposit_outpoint();
1714 let (tx_type, tx) = self
1715 .create_latest_blockhash_tx(
1716 TransactionRequestData {
1717 deposit_outpoint,
1718 kickoff_data,
1719 },
1720 latest_blockhash,
1721 Some(dbtx),
1722 )
1723 .await?;
1724 if tx_type != TransactionType::LatestBlockhash {
1725 return Err(eyre::eyre!("Latest blockhash tx type is not LatestBlockhash").into());
1726 }
1727 self.tx_sender
1728 .add_tx_to_queue(
1729 dbtx,
1730 tx_type,
1731 &tx,
1732 &[],
1733 Some(TxMetadata {
1734 tx_type,
1735 operator_xonly_pk: Some(self.signer.xonly_public_key),
1736 round_idx: Some(kickoff_data.round_idx),
1737 kickoff_idx: Some(kickoff_data.kickoff_idx),
1738 deposit_outpoint: Some(deposit_outpoint),
1739 }),
1740 &self.config,
1741 None,
1742 )
1743 .await?;
1744 Ok(())
1745 }
1746
1747 async fn validate_payer_is_operator(
1749 &self,
1750 dbtx: Option<DatabaseTransaction<'_, '_>>,
1751 deposit_id: u32,
1752 ) -> Result<(BlockHash, Txid), BridgeError> {
1753 let (payer_xonly_pk, payout_blockhash, kickoff_txid) = self
1754 .db
1755 .get_payer_xonly_pk_blockhash_and_kickoff_txid_from_deposit_id(dbtx, deposit_id)
1756 .await?;
1757
1758 tracing::info!(
1759 "Payer xonly pk and kickoff txid found for the requested deposit, payer xonly pk: {:?}, kickoff txid: {:?}",
1760 payer_xonly_pk,
1761 kickoff_txid
1762 );
1763
1764 let (payout_blockhash, kickoff_txid) = match (
1767 payer_xonly_pk,
1768 payout_blockhash,
1769 kickoff_txid,
1770 ) {
1771 (Some(payer_xonly_pk), Some(payout_blockhash), Some(kickoff_txid)) => {
1772 if payer_xonly_pk != self.signer.xonly_public_key {
1773 return Err(eyre::eyre!(
1774 "Payer is not own operator for deposit, payer xonly pk: {:?}, operator xonly pk: {:?}",
1775 payer_xonly_pk,
1776 self.signer.xonly_public_key
1777 )
1778 .into());
1779 }
1780 (payout_blockhash, kickoff_txid)
1781 }
1782 _ => {
1783 return Err(eyre::eyre!(
1784 "Payer info not found for deposit, payout blockhash: {:?}, kickoff txid: {:?}",
1785 payout_blockhash,
1786 kickoff_txid
1787 )
1788 .into());
1789 }
1790 };
1791
1792 tracing::info!(
1793 "Payer xonly pk, payout blockhash and kickoff txid found and valid for own operator for the requested deposit id: {}, payer xonly pk: {:?}, payout blockhash: {:?}, kickoff txid: {:?}",
1794 deposit_id,
1795 payer_xonly_pk,
1796 payout_blockhash,
1797 kickoff_txid
1798 );
1799
1800 Ok((payout_blockhash, kickoff_txid))
1801 }
1802
1803 async fn get_next_txs_to_send(
1804 &self,
1805 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
1806 deposit_data: &mut DepositData,
1807 payout_blockhash: BlockHash,
1808 kickoff_txid: Txid,
1809 current_round_idx: RoundIndex,
1810 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
1811 let mut txs_to_send = Vec::new();
1812
1813 let (kickoff_round_idx, kickoff_connector_idx) = self
1815 .db
1816 .get_kickoff_connector_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
1817 .await?;
1818
1819 let context = ContractContext::new_context_for_kickoff(
1820 KickoffData {
1821 operator_xonly_pk: self.signer.xonly_public_key,
1822 round_idx: kickoff_round_idx,
1823 kickoff_idx: kickoff_connector_idx,
1824 },
1825 deposit_data.clone(),
1826 self.config.protocol_paramset(),
1827 );
1828
1829 let kickoff_txs = create_and_sign_txs(
1831 self.db.clone(),
1832 &self.signer,
1833 self.config.clone(),
1834 context,
1835 Some(payout_blockhash.to_byte_array().last_20_bytes()),
1836 dbtx.as_deref_mut(),
1837 )
1838 .await?;
1839
1840 match current_round_idx
1842 .to_index()
1843 .cmp(&kickoff_round_idx.to_index())
1844 {
1845 std::cmp::Ordering::Less => {
1846 tracing::info!("We need to advance the round manually to be able to start the kickoff, current round idx: {:?}, kickoff round idx: {:?}", current_round_idx, kickoff_round_idx);
1848 let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1849 txs_to_send.extend(txs);
1850 }
1851 std::cmp::Ordering::Greater => {
1852 tracing::info!("We are at least on the next round, meaning we can get the reimbursement as reimbursement utxos are in the next round, current round idx: {:?}, kickoff round idx: {:?}", current_round_idx, kickoff_round_idx);
1853 let reimbursement_tx = kickoff_txs
1855 .iter()
1856 .find(|(tx_type, _)| tx_type == &TransactionType::Reimburse)
1857 .ok_or(eyre::eyre!("Reimburse tx not found in kickoff txs"))?;
1858 txs_to_send.push(reimbursement_tx.clone());
1859 }
1860 std::cmp::Ordering::Equal => {
1861 if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
1863 tracing::info!(
1864 "Kickoff tx is not on chain, can send it, kickoff txid: {:?}",
1865 kickoff_txid
1866 );
1867 let kickoff_tx = kickoff_txs
1868 .iter()
1869 .find(|(tx_type, _)| tx_type == &TransactionType::Kickoff)
1870 .ok_or(eyre::eyre!("Kickoff tx not found in kickoff txs"))?;
1871
1872 let (_, payout_block_height) = self
1874 .db
1875 .get_block_info_from_hash(dbtx.as_deref_mut(), payout_blockhash)
1876 .await?
1877 .ok_or_eyre("Couldn't find payout blockhash in bitcoin sync")?;
1878
1879 let move_txid = deposit_data.get_move_txid(self.config.protocol_paramset())?;
1880
1881 let (_, _, _, citrea_idx) = self
1882 .db
1883 .get_payout_info_from_move_txid(dbtx.as_deref_mut(), move_txid)
1884 .await?
1885 .ok_or_eyre("Couldn't find payout info from move txid")?;
1886
1887 let _ = self
1888 .citrea_client
1889 .fetch_validate_and_store_lcp(
1890 payout_block_height as u64,
1891 citrea_idx as u32,
1892 &self.db,
1893 dbtx.as_deref_mut(),
1894 self.config.protocol_paramset(),
1895 )
1896 .await?;
1897
1898 if kickoff_tx.1.compute_txid() != kickoff_txid {
1900 return Err(eyre::eyre!("Kickoff txid mismatch for deposit outpoint: {}, kickoff txid: {:?}, computed txid: {:?}",
1901 deposit_data.get_deposit_outpoint(), kickoff_txid, kickoff_tx.1.compute_txid()).into());
1902 }
1903 txs_to_send.push(kickoff_tx.clone());
1904 }
1905 else if !self
1907 .rpc
1908 .is_utxo_spent(&OutPoint {
1909 txid: kickoff_txid,
1910 vout: UtxoVout::KickoffFinalizer.get_vout(),
1911 })
1912 .await?
1913 {
1914 tracing::info!(
1916 "Kickoff finalizer is not spent, can send challenge timeout, kickoff txid: {:?}",
1917 kickoff_txid
1918 );
1919 if self
1921 .rpc
1922 .is_utxo_spent(&OutPoint {
1923 txid: kickoff_txid,
1924 vout: UtxoVout::Challenge.get_vout(),
1925 })
1926 .await?
1927 {
1928 tracing::warn!(
1930 "Challenge tx was sent for deposit outpoint: {:?}, but automation is not enabled, enable automation!",
1931 deposit_data.get_deposit_outpoint()
1932 );
1933 return Err(eyre::eyre!("WARNING: Challenge tx was sent to kickoff connector {:?}, but automation is not enabled, enable automation!", kickoff_txid).into());
1934 }
1935 let challenge_timeout_tx = kickoff_txs
1936 .iter()
1937 .find(|(tx_type, _)| tx_type == &TransactionType::ChallengeTimeout)
1938 .ok_or(eyre::eyre!("Challenge timeout tx not found in kickoff txs"))?;
1939 txs_to_send.push(challenge_timeout_tx.clone());
1940 } else {
1941 tracing::info!(
1943 "Kickoff finalizer is spent, can advance the round manually to get the reimbursement, current round idx: {:?}, kickoff round idx: {:?}",
1944 current_round_idx,
1945 kickoff_round_idx
1946 );
1947 let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1948 txs_to_send.extend(txs);
1949 }
1950 }
1951 }
1952 Ok(txs_to_send)
1953 }
1954
1955 pub async fn get_reimbursement_txs(
1967 &self,
1968 deposit_outpoint: OutPoint,
1969 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
1970 let mut dbtx = self.db.begin_transaction().await?;
1971 let (deposit_id, mut deposit_data) = self
1973 .db
1974 .get_deposit_data(Some(&mut dbtx), deposit_outpoint)
1975 .await?
1976 .ok_or_eyre(format!(
1977 "Deposit data not found for the requested deposit outpoint: {deposit_outpoint:?}, make sure you send the deposit outpoint, not the move txid."
1978 ))?;
1979
1980 tracing::info!(
1981 "Deposit data found for the requested deposit outpoint: {deposit_outpoint:?}, deposit id: {deposit_id:?}",
1982 );
1983
1984 let (payout_blockhash, kickoff_txid) = self
1986 .validate_payer_is_operator(Some(&mut dbtx), deposit_id)
1987 .await?;
1988
1989 let mut current_round_idx = self.db.get_current_round_index(Some(&mut dbtx)).await?;
1990
1991 let mut txs_to_send: Vec<(TransactionType, Transaction)>;
1992
1993 loop {
1994 txs_to_send = self
1995 .get_next_txs_to_send(
1996 Some(&mut dbtx),
1997 &mut deposit_data,
1998 payout_blockhash,
1999 kickoff_txid,
2000 current_round_idx,
2001 )
2002 .await?;
2003 if txs_to_send.is_empty() {
2004 let round_idx_after_operations =
2007 self.db.get_current_round_index(Some(&mut dbtx)).await?;
2008 if round_idx_after_operations != current_round_idx {
2009 current_round_idx = round_idx_after_operations;
2010 continue;
2011 }
2012 }
2013 break;
2014 }
2015
2016 dbtx.commit().await?;
2017 Ok(txs_to_send)
2018 }
2019
2020 async fn advance_round_manually(
2023 &self,
2024 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
2025 round_idx: RoundIndex,
2026 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2027 if round_idx == RoundIndex::Collateral {
2028 return self.send_next_round_tx(dbtx, round_idx).await;
2030 }
2031
2032 let context = ContractContext::new_context_for_round(
2034 self.signer.xonly_public_key,
2035 round_idx,
2036 self.config.protocol_paramset(),
2037 );
2038
2039 let txs = create_and_sign_txs(
2040 self.db.clone(),
2041 &self.signer,
2042 self.config.clone(),
2043 context,
2044 None,
2045 dbtx.as_deref_mut(),
2046 )
2047 .await?;
2048
2049 let round_tx = txs
2050 .iter()
2051 .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2052 .ok_or(eyre::eyre!("Round tx not found in txs"))?;
2053
2054 if !self.rpc.is_tx_on_chain(&round_tx.1.compute_txid()).await? {
2055 return Err(eyre::eyre!("Round tx for round {:?} is not on chain, but the database shows we are on this round, error", round_idx).into());
2056 }
2057
2058 let ready_to_reimburse_tx = txs
2060 .iter()
2061 .find(|(tx_type, _)| tx_type == &TransactionType::ReadyToReimburse)
2062 .ok_or(eyre::eyre!("Ready to reimburse tx not found in txs"))?;
2063
2064 let mut txs_to_send = Vec::new();
2065
2066 if !self
2068 .rpc
2069 .is_tx_on_chain(&ready_to_reimburse_tx.1.compute_txid())
2070 .await?
2071 {
2072 tracing::info!("Ready to reimburse tx for round {:?} is not on chain, checking prerequisites to see if we are able to send it
2073 Prerequisites:
2074 - all kickoff utxos are spent
2075 - for all kickoffs, all kickoff finalizers are spent
2076 ", round_idx);
2077 let current_chain_height = self
2079 .db
2080 .get_max_height(dbtx.as_deref_mut())
2081 .await?
2082 .ok_or_eyre("Max block height is not found in the btc syncer database")?;
2083
2084 let round_txid = round_tx.1.compute_txid();
2085 let (unspent_kickoff_utxos, are_all_utxos_spent_finalized) = self
2086 .find_and_mark_unspent_kickoff_utxos(
2087 dbtx.as_deref_mut(),
2088 round_idx,
2089 round_txid,
2090 current_chain_height,
2091 )
2092 .await?;
2093
2094 if !unspent_kickoff_utxos.is_empty() {
2095 let burn_txs = self
2096 .create_burn_unused_kickoff_connectors_tx(round_idx, &unspent_kickoff_utxos)
2097 .await?;
2098 txs_to_send.extend(burn_txs);
2099 } else if !are_all_utxos_spent_finalized {
2100 return Err(eyre::eyre!(format!(
2102 "The transactions that spend the kickoff utxos are not yet finalized, wait until they are finalized. Finality depth: {}
2103 If they are actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.",
2104 self.config.protocol_paramset().finality_depth
2105 ))
2106 .into());
2107 } else {
2108 self.validate_all_kickoff_finalizers_spent(
2112 dbtx.as_deref_mut(),
2113 round_idx,
2114 current_chain_height,
2115 )
2116 .await?;
2117 txs_to_send.push(ready_to_reimburse_tx.clone());
2119 }
2120 } else {
2121 txs_to_send.extend(self.send_next_round_tx(dbtx, round_idx).await?);
2124 }
2125
2126 Ok(txs_to_send)
2127 }
2128
2129 async fn find_and_mark_unspent_kickoff_utxos(
2132 &self,
2133 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
2134 round_idx: RoundIndex,
2135 round_txid: Txid,
2136 current_chain_height: u32,
2137 ) -> Result<(Vec<usize>, bool), BridgeError> {
2138 let mut unspent_kickoff_utxos = Vec::new();
2140 let mut fully_finalized_spent = true;
2142 for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2143 let kickoff_utxo = OutPoint {
2144 txid: round_txid,
2145 vout: UtxoVout::Kickoff(kickoff_idx).get_vout(),
2146 };
2147 if !self.rpc.is_utxo_spent(&kickoff_utxo).await? {
2148 unspent_kickoff_utxos.push(kickoff_idx);
2149 } else {
2150 self.db
2153 .mark_kickoff_connector_as_used(
2154 dbtx.as_deref_mut(),
2155 round_idx,
2156 kickoff_idx as u32,
2157 None,
2158 )
2159 .await?;
2160 fully_finalized_spent &= self
2163 .db
2164 .check_if_utxo_spending_tx_is_finalized(
2165 dbtx.as_deref_mut(),
2166 kickoff_utxo,
2167 current_chain_height,
2168 self.config.protocol_paramset(),
2169 )
2170 .await?;
2171 }
2172 }
2173 Ok((unspent_kickoff_utxos, fully_finalized_spent))
2174 }
2175
2176 async fn create_burn_unused_kickoff_connectors_tx(
2178 &self,
2179 round_idx: RoundIndex,
2180 unspent_kickoff_utxos: &[usize],
2181 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2182 tracing::info!(
2183 "There are unspent kickoff utxos {:?}, creating a tx that spends them",
2184 unspent_kickoff_utxos
2185 );
2186 let operator_winternitz_public_keys = self.generate_kickoff_winternitz_pubkeys()?;
2187 let kickoff_wpks = KickoffWinternitzKeys::new(
2188 operator_winternitz_public_keys,
2189 self.config.protocol_paramset().num_kickoffs_per_round,
2190 self.config.protocol_paramset().num_round_txs,
2191 )?;
2192 let (round_txhandler, _ready_to_reimburse_txhandler) = create_round_nth_txhandler(
2194 self.signer.xonly_public_key,
2195 self.collateral_funding_outpoint,
2196 self.config.protocol_paramset().collateral_funding_amount,
2197 round_idx,
2198 &kickoff_wpks,
2199 self.config.protocol_paramset(),
2200 )?;
2201 let mut burn_unused_kickoff_connectors_txhandler =
2202 create_burn_unused_kickoff_connectors_txhandler(
2203 &round_txhandler,
2204 unspent_kickoff_utxos,
2205 &self.reimburse_addr,
2206 self.config.protocol_paramset(),
2207 )?;
2208
2209 self.signer
2211 .tx_sign_and_fill_sigs(&mut burn_unused_kickoff_connectors_txhandler, &[], None)
2212 .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
2213
2214 let burn_unused_kickoff_connectors_txhandler =
2215 burn_unused_kickoff_connectors_txhandler.promote()?;
2216 Ok(vec![(
2217 TransactionType::BurnUnusedKickoffConnectors,
2218 burn_unused_kickoff_connectors_txhandler
2219 .get_cached_tx()
2220 .clone(),
2221 )])
2222 }
2223
2224 async fn validate_all_kickoff_finalizers_spent(
2226 &self,
2227 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
2228 round_idx: RoundIndex,
2229 current_chain_height: u32,
2230 ) -> Result<(), BridgeError> {
2231 for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2233 let kickoff_txid = self
2234 .db
2235 .get_kickoff_txid_for_used_kickoff_connector(
2236 dbtx.as_deref_mut(),
2237 round_idx,
2238 kickoff_idx as u32,
2239 )
2240 .await?;
2241 if let Some(kickoff_txid) = kickoff_txid {
2242 let deposit_outpoint = self
2243 .db
2244 .get_deposit_outpoint_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
2245 .await?;
2246 let kickoff_finalizer_utxo = OutPoint {
2247 txid: kickoff_txid,
2248 vout: UtxoVout::KickoffFinalizer.get_vout(),
2249 };
2250 if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
2251 return Err(eyre::eyre!(
2252 "For round {:?} and kickoff utxo {:?}, the kickoff tx {:?} is not on chain,
2253 reimburse the deposit {:?} corresponding to this kickoff first. ",
2254 round_idx,
2255 kickoff_idx,
2256 kickoff_txid,
2257 deposit_outpoint
2258 )
2259 .into());
2260 } else if !self.rpc.is_utxo_spent(&kickoff_finalizer_utxo).await? {
2261 return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer {:?} is not spent,
2262 send the challenge timeout tx for the deposit {:?} first", round_idx, kickoff_idx, kickoff_txid, deposit_outpoint).into());
2263 } else if !self
2264 .db
2265 .check_if_utxo_spending_tx_is_finalized(
2266 dbtx.as_deref_mut(),
2267 kickoff_finalizer_utxo,
2268 current_chain_height,
2269 self.config.protocol_paramset(),
2270 )
2271 .await?
2272 {
2273 return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer utxo {:?} is spent, but not yet finalized, wait until it is finalized. Finality depth: {}
2274 If the transaction is actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.", round_idx, kickoff_idx, kickoff_finalizer_utxo, self.config.protocol_paramset().finality_depth).into());
2275 }
2276 }
2277 }
2278 Ok(())
2279 }
2280
2281 async fn send_next_round_tx(
2283 &self,
2284 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
2285 round_idx: RoundIndex,
2286 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2287 let next_round_context = ContractContext::new_context_for_round(
2288 self.signer.xonly_public_key,
2289 round_idx.next_round(),
2290 self.config.protocol_paramset(),
2291 );
2292 let next_round_txs = create_and_sign_txs(
2293 self.db.clone(),
2294 &self.signer,
2295 self.config.clone(),
2296 next_round_context,
2297 None,
2298 dbtx.as_deref_mut(),
2299 )
2300 .await?;
2301 let next_round_tx = next_round_txs
2302 .iter()
2303 .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2304 .ok_or(eyre::eyre!("Next round tx not found in txs"))?;
2305 let next_round_txid = next_round_tx.1.compute_txid();
2306
2307 if !self.rpc.is_tx_on_chain(&next_round_txid).await? {
2308 Ok(vec![next_round_tx.clone()])
2310 } else {
2311 self.db
2313 .update_current_round_index(dbtx, round_idx.next_round())
2314 .await?;
2315 Ok(vec![])
2316 }
2317 }
2318}
2319
2320impl<C> NamedEntity for Operator<C>
2321where
2322 C: CitreaClientT,
2323{
2324 const ENTITY_NAME: &'static str = "operator";
2325 const TX_SENDER_CONSUMER_ID: &'static str = "verifier_tx_sender";
2327 const FINALIZED_BLOCK_CONSUMER_ID_AUTOMATION: &'static str =
2328 "operator_finalized_block_fetcher_automation";
2329 const FINALIZED_BLOCK_CONSUMER_ID_NO_AUTOMATION: &'static str =
2330 "operator_finalized_block_fetcher_no_automation";
2331}
2332
2333#[cfg(feature = "automation")]
2334mod states {
2335
2336 use super::*;
2337 use crate::builder::transaction::{
2338 create_txhandlers, ContractContext, ReimburseDbCache, TransactionType, TxHandler,
2339 TxHandlerCache,
2340 };
2341 use crate::states::context::DutyResult;
2342 use crate::states::{block_cache, Duty, Owner, StateManager};
2343 use std::collections::BTreeMap;
2344 use std::sync::Arc;
2345
2346 #[tonic::async_trait]
2347 impl<C> Owner for Operator<C>
2348 where
2349 C: CitreaClientT,
2350 {
2351 async fn handle_duty(
2352 &self,
2353 dbtx: DatabaseTransaction<'_, '_>,
2354 duty: Duty,
2355 ) -> Result<DutyResult, BridgeError> {
2356 match duty {
2357 Duty::NewReadyToReimburse {
2358 round_idx,
2359 operator_xonly_pk,
2360 used_kickoffs,
2361 } => {
2362 tracing::info!("Operator {:?} called new ready to reimburse with round_idx: {:?}, operator_xonly_pk: {:?}, used_kickoffs: {:?}",
2363 self.signer.xonly_public_key, round_idx, operator_xonly_pk, used_kickoffs);
2364 Ok(DutyResult::Handled)
2365 }
2366 Duty::WatchtowerChallenge { .. } => Ok(DutyResult::Handled),
2367 Duty::SendOperatorAsserts {
2368 kickoff_data,
2369 deposit_data,
2370 watchtower_challenges,
2371 payout_blockhash,
2372 latest_blockhash,
2373 } => {
2374 tracing::warn!("Operator {:?} called send operator asserts with kickoff_data: {:?}, deposit_data: {:?}, watchtower_challenges: {:?}",
2375 self.signer.xonly_public_key, kickoff_data, deposit_data, watchtower_challenges.len());
2376 self.send_asserts(
2377 dbtx,
2378 kickoff_data,
2379 deposit_data,
2380 watchtower_challenges,
2381 payout_blockhash,
2382 latest_blockhash,
2383 )
2384 .await?;
2385 Ok(DutyResult::Handled)
2386 }
2387 Duty::VerifierDisprove { .. } => Ok(DutyResult::Handled),
2388 Duty::SendLatestBlockhash {
2389 kickoff_data,
2390 deposit_data,
2391 latest_blockhash,
2392 } => {
2393 tracing::warn!("Operator {:?} called send latest blockhash with kickoff_id: {:?}, deposit_data: {:?}, latest_blockhash: {:?}", self.signer.xonly_public_key, kickoff_data, deposit_data, latest_blockhash);
2394 self.send_latest_blockhash(dbtx, kickoff_data, deposit_data, latest_blockhash)
2395 .await?;
2396 Ok(DutyResult::Handled)
2397 }
2398 Duty::CheckIfKickoff {
2399 txid,
2400 block_height,
2401 witness,
2402 challenged_before: _,
2403 } => {
2404 tracing::debug!(
2405 "Operator {:?} called check if kickoff with txid: {:?}, block_height: {:?}",
2406 self.signer.xonly_public_key,
2407 txid,
2408 block_height,
2409 );
2410
2411 let kickoff_data = self
2412 .db
2413 .get_deposit_data_with_kickoff_txid(Some(dbtx), txid)
2414 .await?;
2415 if let Some((deposit_data, kickoff_data)) = kickoff_data {
2416 StateManager::<Self>::dispatch_new_kickoff_machine(
2417 self.db.clone(),
2418 dbtx,
2419 kickoff_data,
2420 block_height,
2421 deposit_data.clone(),
2422 witness,
2423 )
2424 .await?;
2425
2426 let context = ContractContext::new_context_for_kickoff(
2428 kickoff_data,
2429 deposit_data.clone(),
2430 self.config.protocol_paramset(),
2431 );
2432 let signed_txs = create_and_sign_txs(
2433 self.db.clone(),
2434 &self.signer,
2435 self.config.clone(),
2436 context,
2437 Some([0u8; 20]),
2438 Some(dbtx),
2439 )
2440 .await?;
2441 let tx_metadata = Some(TxMetadata {
2442 tx_type: TransactionType::Dummy,
2443 operator_xonly_pk: Some(self.signer.xonly_public_key),
2444 round_idx: Some(kickoff_data.round_idx),
2445 kickoff_idx: Some(kickoff_data.kickoff_idx),
2446 deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2447 });
2448 for (tx_type, signed_tx) in &signed_txs {
2449 match *tx_type {
2450 TransactionType::OperatorChallengeAck(_)
2451 | TransactionType::WatchtowerChallengeTimeout(_)
2452 | TransactionType::ChallengeTimeout
2453 | TransactionType::DisproveTimeout
2454 | TransactionType::Reimburse => {
2455 self.tx_sender
2456 .add_tx_to_queue(
2457 dbtx,
2458 *tx_type,
2459 signed_tx,
2460 &signed_txs,
2461 tx_metadata,
2462 &self.config,
2463 None,
2464 )
2465 .await?;
2466 }
2467 _ => {}
2468 }
2469 }
2470 }
2471
2472 Ok(DutyResult::Handled)
2473 }
2474 }
2475 }
2476
2477 async fn create_txhandlers(
2478 &self,
2479 dbtx: DatabaseTransaction<'_, '_>,
2480 tx_type: TransactionType,
2481 contract_context: ContractContext,
2482 ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
2483 let mut db_cache =
2484 ReimburseDbCache::from_context(self.db.clone(), &contract_context, Some(dbtx));
2485 let txhandlers = create_txhandlers(
2486 tx_type,
2487 contract_context,
2488 &mut TxHandlerCache::new(),
2489 &mut db_cache,
2490 )
2491 .await?;
2492 Ok(txhandlers)
2493 }
2494
2495 async fn handle_finalized_block(
2496 &self,
2497 _dbtx: DatabaseTransaction<'_, '_>,
2498 _block_id: u32,
2499 _block_height: u32,
2500 _block_cache: Arc<block_cache::BlockCache>,
2501 _light_client_proof_wait_interval_secs: Option<u32>,
2502 ) -> Result<(), BridgeError> {
2503 Ok(())
2504 }
2505 }
2506}
2507
2508#[cfg(test)]
2509mod tests {
2510 use crate::operator::Operator;
2511 use crate::test::common::citrea::MockCitreaClient;
2512 use crate::test::common::*;
2513 use bitcoin::hashes::Hash;
2514 use bitcoin::{OutPoint, Txid};
2515
2516 #[tokio::test]
2517 #[ignore = "Design changes in progress"]
2518 async fn get_winternitz_public_keys() {
2519 let mut config = create_test_config_with_thread_name().await;
2520 let _regtest = create_regtest_rpc(&mut config).await;
2521
2522 let operator = Operator::<MockCitreaClient>::new(config.clone())
2523 .await
2524 .unwrap();
2525
2526 let deposit_outpoint = OutPoint {
2527 txid: Txid::all_zeros(),
2528 vout: 2,
2529 };
2530
2531 let winternitz_public_key = operator
2532 .generate_assert_winternitz_pubkeys(deposit_outpoint)
2533 .unwrap();
2534 assert_eq!(
2535 winternitz_public_key.len(),
2536 config.protocol_paramset().num_round_txs
2537 * config.protocol_paramset().num_kickoffs_per_round
2538 );
2539 }
2540
2541 #[tokio::test]
2542 async fn operator_get_params() {
2543 let mut config = create_test_config_with_thread_name().await;
2544 let _regtest = create_regtest_rpc(&mut config).await;
2545
2546 let operator = Operator::<MockCitreaClient>::new(config.clone())
2547 .await
2548 .unwrap();
2549 let actual_wpks = operator.generate_kickoff_winternitz_pubkeys().unwrap();
2550
2551 let (mut wpk_rx, _) = operator.get_params().await.unwrap();
2552 let mut idx = 0;
2553 while let Some(wpk) = wpk_rx.recv().await {
2554 assert_eq!(actual_wpks[idx], wpk);
2555 idx += 1;
2556 }
2557 assert_eq!(idx, actual_wpks.len());
2558 }
2559}