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