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());
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_test_util;
1437 for (_, tx) in watchtower_challenges.iter() {
1438 let total_work = total_work_from_wt_tx_test_util(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 let (g16_proof, g16_output, public_inputs) = tokio::task::spawn_blocking(move || {
1562 prove_bridge_circuit(bridge_circuit_host_params, bridge_circuit_elf)
1563 })
1564 .await
1565 .wrap_err("Failed to join the prove_bridge_circuit task")?
1566 .wrap_err("Failed to prove bridge circuit")?;
1567
1568 tracing::info!("Proved bridge circuit in send_asserts");
1569 let public_input_scalar = ark_bn254::Fr::from_be_bytes_mod_order(&g16_output);
1570
1571 #[cfg(test)]
1572 let mut public_inputs = public_inputs;
1573
1574 #[cfg(test)]
1575 {
1576 if self
1577 .config
1578 .test_params
1579 .disrupt_challenge_sending_watchtowers_commit
1580 {
1581 tracing::info!("Disrupting challenge sending watchtowers commit in send_asserts");
1582 public_inputs.challenge_sending_watchtowers[0] ^= 0x01;
1583 tracing::info!(
1584 "Disrupted challenge sending watchtowers commit: {:?}",
1585 public_inputs.challenge_sending_watchtowers
1586 );
1587 }
1588 }
1589
1590 tracing::info!(
1591 "Challenge sending watchtowers commit: {:?}",
1592 public_inputs.challenge_sending_watchtowers
1593 );
1594
1595 let asserts = tokio::task::spawn_blocking(move || {
1596 let vk = get_verifying_key();
1597
1598 generate_assertions(g16_proof, vec![public_input_scalar], &vk).map_err(|e| {
1599 eyre::eyre!(
1600 "Failed to generate {}assertions: {}",
1601 if is_dev_mode() { "dev mode " } else { "" },
1602 e
1603 )
1604 })
1605 })
1606 .await
1607 .wrap_err("Generate assertions thread failed with error")??;
1608
1609 tracing::info!("Generated assertions in send_asserts");
1610
1611 #[cfg(test)]
1612 let asserts = self.config.test_params.maybe_corrupt_asserts(asserts);
1613
1614 tracing::debug!(target: "ci", "Assert commitment data: {:?}", asserts);
1615
1616 let assert_txs = self
1617 .create_assert_commitment_txs(
1618 TransactionRequestData {
1619 kickoff_data,
1620 deposit_outpoint: deposit_data.get_deposit_outpoint(),
1621 },
1622 ClementineBitVMPublicKeys::get_assert_commit_data(
1623 asserts,
1624 &public_inputs.challenge_sending_watchtowers,
1625 ),
1626 Some(&mut dbtx),
1627 )
1628 .await?;
1629
1630 for (tx_type, tx) in assert_txs {
1631 self.tx_sender
1632 .add_tx_to_queue(
1633 Some(&mut dbtx),
1634 tx_type,
1635 &tx,
1636 &[],
1637 Some(TxMetadata {
1638 tx_type,
1639 operator_xonly_pk: Some(self.signer.xonly_public_key),
1640 round_idx: Some(kickoff_data.round_idx),
1641 kickoff_idx: Some(kickoff_data.kickoff_idx),
1642 deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
1643 }),
1644 self.config.protocol_paramset(),
1645 None,
1646 )
1647 .await?;
1648 }
1649 Ok(())
1650 }
1651
1652 fn data(&self) -> OperatorData {
1653 OperatorData {
1654 xonly_pk: self.signer.xonly_public_key,
1655 collateral_funding_outpoint: self.collateral_funding_outpoint,
1656 reimburse_addr: self.reimburse_addr.clone(),
1657 }
1658 }
1659
1660 #[cfg(feature = "automation")]
1661 async fn send_latest_blockhash(
1662 &self,
1663 mut dbtx: DatabaseTransaction<'_>,
1664 kickoff_data: KickoffData,
1665 deposit_data: DepositData,
1666 latest_blockhash: BlockHash,
1667 ) -> Result<(), BridgeError> {
1668 tracing::info!("Operator sending latest blockhash");
1669 let deposit_outpoint = deposit_data.get_deposit_outpoint();
1670 let (tx_type, tx) = self
1671 .create_latest_blockhash_tx(
1672 TransactionRequestData {
1673 deposit_outpoint,
1674 kickoff_data,
1675 },
1676 latest_blockhash,
1677 Some(&mut dbtx),
1678 )
1679 .await?;
1680 if tx_type != TransactionType::LatestBlockhash {
1681 return Err(eyre::eyre!("Latest blockhash tx type is not LatestBlockhash").into());
1682 }
1683 self.tx_sender
1684 .add_tx_to_queue(
1685 Some(dbtx),
1686 tx_type,
1687 &tx,
1688 &[],
1689 Some(TxMetadata {
1690 tx_type,
1691 operator_xonly_pk: Some(self.signer.xonly_public_key),
1692 round_idx: Some(kickoff_data.round_idx),
1693 kickoff_idx: Some(kickoff_data.kickoff_idx),
1694 deposit_outpoint: Some(deposit_outpoint),
1695 }),
1696 self.config.protocol_paramset(),
1697 None,
1698 )
1699 .await?;
1700 Ok(())
1701 }
1702
1703 async fn validate_payer_is_operator(
1705 &self,
1706 dbtx: Option<DatabaseTransaction<'_>>,
1707 deposit_id: u32,
1708 ) -> Result<(BlockHash, Txid), BridgeError> {
1709 let (payer_xonly_pk, payout_blockhash, kickoff_txid) = self
1710 .db
1711 .get_payer_xonly_pk_blockhash_and_kickoff_txid_from_deposit_id(dbtx, deposit_id)
1712 .await?;
1713
1714 tracing::info!(
1715 "Payer xonly pk and kickoff txid found for the requested deposit, payer xonly pk: {:?}, kickoff txid: {:?}",
1716 payer_xonly_pk,
1717 kickoff_txid
1718 );
1719
1720 let (payout_blockhash, kickoff_txid) = match (
1723 payer_xonly_pk,
1724 payout_blockhash,
1725 kickoff_txid,
1726 ) {
1727 (Some(payer_xonly_pk), Some(payout_blockhash), Some(kickoff_txid)) => {
1728 if payer_xonly_pk != self.signer.xonly_public_key {
1729 return Err(eyre::eyre!(
1730 "Payer is not own operator for deposit, payer xonly pk: {:?}, operator xonly pk: {:?}",
1731 payer_xonly_pk,
1732 self.signer.xonly_public_key
1733 )
1734 .into());
1735 }
1736 (payout_blockhash, kickoff_txid)
1737 }
1738 _ => {
1739 return Err(eyre::eyre!(
1740 "Payer info not found for deposit, payout blockhash: {:?}, kickoff txid: {:?}",
1741 payout_blockhash,
1742 kickoff_txid
1743 )
1744 .into());
1745 }
1746 };
1747
1748 tracing::info!(
1749 "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: {:?}",
1750 deposit_id,
1751 payer_xonly_pk,
1752 payout_blockhash,
1753 kickoff_txid
1754 );
1755
1756 Ok((payout_blockhash, kickoff_txid))
1757 }
1758
1759 async fn get_next_txs_to_send(
1760 &self,
1761 mut dbtx: Option<DatabaseTransaction<'_>>,
1762 deposit_data: &mut DepositData,
1763 payout_blockhash: BlockHash,
1764 kickoff_txid: Txid,
1765 current_round_idx: RoundIndex,
1766 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
1767 let mut txs_to_send = Vec::new();
1768
1769 let (kickoff_round_idx, kickoff_connector_idx) = self
1771 .db
1772 .get_kickoff_connector_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
1773 .await?;
1774
1775 let context = ContractContext::new_context_for_kickoff(
1776 KickoffData {
1777 operator_xonly_pk: self.signer.xonly_public_key,
1778 round_idx: kickoff_round_idx,
1779 kickoff_idx: kickoff_connector_idx,
1780 },
1781 deposit_data.clone(),
1782 self.config.protocol_paramset(),
1783 );
1784
1785 let kickoff_txs = create_and_sign_txs(
1787 self.db.clone(),
1788 &self.signer,
1789 self.config.clone(),
1790 context,
1791 Some(payout_blockhash.to_byte_array().last_20_bytes()),
1792 dbtx.as_deref_mut(),
1793 )
1794 .await?;
1795
1796 match current_round_idx
1798 .to_index()
1799 .cmp(&kickoff_round_idx.to_index())
1800 {
1801 std::cmp::Ordering::Less => {
1802 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);
1804 let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1805 txs_to_send.extend(txs);
1806 }
1807 std::cmp::Ordering::Greater => {
1808 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);
1809 let reimbursement_tx = kickoff_txs
1811 .iter()
1812 .find(|(tx_type, _)| tx_type == &TransactionType::Reimburse)
1813 .ok_or(eyre::eyre!("Reimburse tx not found in kickoff txs"))?;
1814 txs_to_send.push(reimbursement_tx.clone());
1815 }
1816 std::cmp::Ordering::Equal => {
1817 if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
1819 tracing::info!(
1820 "Kickoff tx is not on chain, can send it, kickoff txid: {:?}",
1821 kickoff_txid
1822 );
1823 let kickoff_tx = kickoff_txs
1824 .iter()
1825 .find(|(tx_type, _)| tx_type == &TransactionType::Kickoff)
1826 .ok_or(eyre::eyre!("Kickoff tx not found in kickoff txs"))?;
1827
1828 let (_, payout_block_height) = self
1830 .db
1831 .get_block_info_from_hash(dbtx.as_deref_mut(), payout_blockhash)
1832 .await?
1833 .ok_or_eyre("Couldn't find payout blockhash in bitcoin sync")?;
1834
1835 let move_txid = deposit_data.get_move_txid(self.config.protocol_paramset())?;
1836
1837 let (_, _, _, citrea_idx) = self
1838 .db
1839 .get_payout_info_from_move_txid(dbtx.as_deref_mut(), move_txid)
1840 .await?
1841 .ok_or_eyre("Couldn't find payout info from move txid")?;
1842
1843 let _ = self
1844 .citrea_client
1845 .fetch_validate_and_store_lcp(
1846 payout_block_height as u64,
1847 citrea_idx as u32,
1848 &self.db,
1849 dbtx.as_deref_mut(),
1850 self.config.protocol_paramset(),
1851 )
1852 .await?;
1853
1854 if kickoff_tx.1.compute_txid() != kickoff_txid {
1856 return Err(eyre::eyre!("Kickoff txid mismatch for deposit outpoint: {}, kickoff txid: {:?}, computed txid: {:?}",
1857 deposit_data.get_deposit_outpoint(), kickoff_txid, kickoff_tx.1.compute_txid()).into());
1858 }
1859 txs_to_send.push(kickoff_tx.clone());
1860 }
1861 else if !self
1863 .rpc
1864 .is_utxo_spent(&OutPoint {
1865 txid: kickoff_txid,
1866 vout: UtxoVout::KickoffFinalizer.get_vout(),
1867 })
1868 .await?
1869 {
1870 tracing::info!(
1872 "Kickoff finalizer is not spent, can send challenge timeout, kickoff txid: {:?}",
1873 kickoff_txid
1874 );
1875 if self
1877 .rpc
1878 .is_utxo_spent(&OutPoint {
1879 txid: kickoff_txid,
1880 vout: UtxoVout::Challenge.get_vout(),
1881 })
1882 .await?
1883 {
1884 tracing::warn!(
1886 "Challenge tx was sent for deposit outpoint: {:?}, but automation is not enabled, enable automation!",
1887 deposit_data.get_deposit_outpoint()
1888 );
1889 return Err(eyre::eyre!("WARNING: Challenge tx was sent to kickoff connector {:?}, but automation is not enabled, enable automation!", kickoff_txid).into());
1890 }
1891 let challenge_timeout_tx = kickoff_txs
1892 .iter()
1893 .find(|(tx_type, _)| tx_type == &TransactionType::ChallengeTimeout)
1894 .ok_or(eyre::eyre!("Challenge timeout tx not found in kickoff txs"))?;
1895 txs_to_send.push(challenge_timeout_tx.clone());
1896 } else {
1897 tracing::info!(
1899 "Kickoff finalizer is spent, can advance the round manually to get the reimbursement, current round idx: {:?}, kickoff round idx: {:?}",
1900 current_round_idx,
1901 kickoff_round_idx
1902 );
1903 let txs = self.advance_round_manually(dbtx, current_round_idx).await?;
1904 txs_to_send.extend(txs);
1905 }
1906 }
1907 }
1908 Ok(txs_to_send)
1909 }
1910
1911 pub async fn transfer_outpoints_to_wallet(
1919 &self,
1920 inputs: Vec<(OutPoint, TxOut)>,
1921 ) -> Result<Transaction, BridgeError> {
1922 if inputs.is_empty() {
1923 return Err(eyre!("No outpoints provided for transfer").into());
1924 }
1925
1926 let collateral_outpoints = self
1928 .get_all_collateral_outpoints()
1929 .await
1930 .wrap_err("Failed to get all collateral outpoints")?;
1931 for (outpoint, _) in inputs.iter() {
1932 if collateral_outpoints.contains_key(outpoint) {
1933 let (round_idx, tx_type) = collateral_outpoints
1934 .get(outpoint)
1935 .expect("Collateral outpoint should be found in the map");
1936 return Err(
1937 eyre!("Cannot transfer collateral outpoint {outpoint} belonging to {round_idx:?} {tx_type:?} to wallet").into(),
1938 );
1939 }
1940 }
1941
1942 let destination_script = self
1943 .rpc
1944 .get_new_address(None, Some(AddressType::Bech32m))
1945 .await
1946 .wrap_err("Failed to get new wallet address for transfer")?
1947 .require_network(self.config.protocol_paramset().network)
1948 .wrap_err("Failed to get new address, bitcoin rpc might not match the network")?
1949 .script_pubkey();
1950
1951 let (_, spendinfo) = create_taproot_address(
1952 &[],
1953 Some(self.signer.xonly_public_key),
1954 self.config.protocol_paramset().network,
1955 );
1956
1957 let total_input_value = inputs.iter().try_fold(Amount::ZERO, |acc, (_, txout)| {
1958 acc.checked_add(txout.value)
1959 .ok_or_else(|| eyre!("Input values overflowed while summing"))
1960 })?;
1961
1962 let mut output_txout = TxOut {
1963 value: total_input_value,
1964 script_pubkey: destination_script,
1965 };
1966
1967 let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
1968 .with_version(bitcoin::transaction::Version::TWO);
1969
1970 for (outpoint, txout) in inputs.iter() {
1971 builder = builder.add_input(
1972 NormalSignatureKind::OperatorSighashDefault,
1973 SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
1974 SpendPath::KeySpend,
1975 DEFAULT_SEQUENCE,
1976 );
1977 }
1978
1979 builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
1980
1981 let mut txhandler = builder.finalize();
1982
1983 let fee_rate = self
1984 .rpc
1985 .get_fee_rate(
1986 self.config.protocol_paramset().network,
1987 &self.config.mempool_api_host,
1988 &self.config.mempool_api_endpoint,
1989 self.config.tx_sender_limits.mempool_fee_rate_multiplier,
1990 self.config.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
1991 self.config.tx_sender_limits.fee_rate_hard_cap,
1992 )
1993 .await
1994 .wrap_err("Failed to get fee rate for transfer to wallet tx")?;
1995
1996 self.signer
1998 .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
1999
2000 let tx_weight_wu = txhandler.get_cached_tx().weight().to_wu();
2001 let fee_sat = (fee_rate.to_sat_per_kwu() * tx_weight_wu).div_ceil(1000);
2002 let fee = Amount::from_sat(fee_sat);
2003
2004 output_txout.value = output_txout
2005 .value
2006 .checked_sub(fee)
2007 .ok_or_else(|| eyre!("Calculated fee exceeds total input value"))?;
2008
2009 let mut builder = TxHandlerBuilder::new(TransactionType::Dummy)
2010 .with_version(bitcoin::transaction::Version::TWO);
2011
2012 for (outpoint, txout) in inputs.iter() {
2013 builder = builder.add_input(
2014 NormalSignatureKind::OperatorSighashDefault,
2015 SpendableTxIn::new(*outpoint, txout.clone(), vec![], Some(spendinfo.clone())),
2016 SpendPath::KeySpend,
2017 DEFAULT_SEQUENCE,
2018 );
2019 }
2020
2021 builder = builder.add_output(UnspentTxOut::from_partial(output_txout.clone()));
2022
2023 let mut txhandler = builder.finalize();
2024
2025 self.signer
2026 .tx_sign_and_fill_sigs(&mut txhandler, &[], None)?;
2027
2028 let signed_tx = txhandler.get_cached_tx().clone();
2029
2030 self.rpc
2031 .send_raw_transaction(&signed_tx)
2032 .await
2033 .wrap_err("Failed to send from operator's address to btc wallet address")?;
2034
2035 Ok(signed_tx)
2036 }
2037
2038 async fn get_all_collateral_outpoints(
2041 &self,
2042 ) -> Result<HashMap<OutPoint, (RoundIndex, TransactionType)>, BridgeError> {
2043 let mut outpoints = HashMap::new();
2044 outpoints.insert(
2045 self.collateral_funding_outpoint,
2046 (RoundIndex::Collateral, TransactionType::Round),
2047 );
2048
2049 let operator_winternitz_public_keys = self
2051 .db
2052 .get_operator_kickoff_winternitz_public_keys(None, self.signer.xonly_public_key)
2053 .await?;
2054 let kickoff_wpks = KickoffWinternitzKeys::new(
2055 operator_winternitz_public_keys,
2056 self.config.protocol_paramset().num_kickoffs_per_round,
2057 self.config.protocol_paramset().num_round_txs,
2058 )?;
2059 let operator_data = self.data();
2060
2061 let mut prev_ready_to_reimburse: Option<TxHandler> = None;
2062
2063 for round_idx in RoundIndex::iter_rounds(self.config.protocol_paramset().num_round_txs) {
2065 let txhandlers = create_round_txhandlers(
2066 self.config.protocol_paramset(),
2067 round_idx,
2068 &operator_data,
2069 &kickoff_wpks,
2070 prev_ready_to_reimburse.as_ref(),
2071 )?;
2072
2073 let round_tx = txhandlers
2074 .iter()
2075 .find(|txhandler| txhandler.get_transaction_type() == TransactionType::Round)
2076 .ok_or(eyre::eyre!("Round tx not found in txhandlers"))?;
2077 let collateral_outpoint = OutPoint {
2078 txid: *round_tx.get_txid(),
2079 vout: UtxoVout::CollateralInRound.get_vout(),
2080 };
2081 outpoints.insert(collateral_outpoint, (round_idx, TransactionType::Round));
2082
2083 let ready_to_reimburse_tx = txhandlers
2084 .iter()
2085 .find(|txhandler| {
2086 txhandler.get_transaction_type() == TransactionType::ReadyToReimburse
2087 })
2088 .ok_or(eyre::eyre!("Ready to reimburse tx not found in txhandlers"))?;
2089 let ready_to_reimburse_collateral_outpoint = OutPoint {
2090 txid: *ready_to_reimburse_tx.get_txid(),
2091 vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
2092 };
2093 outpoints.insert(
2094 ready_to_reimburse_collateral_outpoint,
2095 (round_idx, TransactionType::ReadyToReimburse),
2096 );
2097 prev_ready_to_reimburse = Some(ready_to_reimburse_tx.clone());
2098 }
2099
2100 Ok(outpoints)
2101 }
2102
2103 pub async fn get_reimbursement_txs(
2115 &self,
2116 deposit_outpoint: OutPoint,
2117 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2118 let mut dbtx = self.db.begin_transaction().await?;
2119 let (deposit_id, mut deposit_data) = self
2121 .db
2122 .get_deposit_data(Some(&mut dbtx), deposit_outpoint)
2123 .await?
2124 .ok_or_eyre(format!(
2125 "Deposit data not found for the requested deposit outpoint: {deposit_outpoint:?}, make sure you send the deposit outpoint, not the move txid."
2126 ))?;
2127
2128 tracing::info!(
2129 "Deposit data found for the requested deposit outpoint: {deposit_outpoint:?}, deposit id: {deposit_id:?}",
2130 );
2131
2132 let (payout_blockhash, kickoff_txid) = self
2134 .validate_payer_is_operator(Some(&mut dbtx), deposit_id)
2135 .await?;
2136
2137 let mut current_round_idx = self.db.get_current_round_index(Some(&mut dbtx)).await?;
2138
2139 let mut txs_to_send: Vec<(TransactionType, Transaction)>;
2140
2141 loop {
2142 txs_to_send = self
2143 .get_next_txs_to_send(
2144 Some(&mut dbtx),
2145 &mut deposit_data,
2146 payout_blockhash,
2147 kickoff_txid,
2148 current_round_idx,
2149 )
2150 .await?;
2151 if txs_to_send.is_empty() {
2152 let round_idx_after_operations =
2155 self.db.get_current_round_index(Some(&mut dbtx)).await?;
2156 if round_idx_after_operations != current_round_idx {
2157 current_round_idx = round_idx_after_operations;
2158 continue;
2159 }
2160 }
2161 break;
2162 }
2163
2164 dbtx.commit().await?;
2165 Ok(txs_to_send)
2166 }
2167
2168 async fn advance_round_manually(
2171 &self,
2172 mut dbtx: Option<DatabaseTransaction<'_>>,
2173 round_idx: RoundIndex,
2174 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2175 if round_idx == RoundIndex::Collateral {
2176 return self.send_next_round_tx(dbtx, round_idx).await;
2178 }
2179
2180 let context = ContractContext::new_context_for_round(
2182 self.signer.xonly_public_key,
2183 round_idx,
2184 self.config.protocol_paramset(),
2185 );
2186
2187 let txs = create_and_sign_txs(
2188 self.db.clone(),
2189 &self.signer,
2190 self.config.clone(),
2191 context,
2192 None,
2193 dbtx.as_deref_mut(),
2194 )
2195 .await?;
2196
2197 let round_tx = txs
2198 .iter()
2199 .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2200 .ok_or(eyre::eyre!("Round tx not found in txs"))?;
2201
2202 if !self.rpc.is_tx_on_chain(&round_tx.1.compute_txid()).await? {
2203 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());
2204 }
2205
2206 let ready_to_reimburse_tx = txs
2208 .iter()
2209 .find(|(tx_type, _)| tx_type == &TransactionType::ReadyToReimburse)
2210 .ok_or(eyre::eyre!("Ready to reimburse tx not found in txs"))?;
2211
2212 let mut txs_to_send = Vec::new();
2213
2214 if !self
2216 .rpc
2217 .is_tx_on_chain(&ready_to_reimburse_tx.1.compute_txid())
2218 .await?
2219 {
2220 tracing::info!("Ready to reimburse tx for round {:?} is not on chain, checking prerequisites to see if we are able to send it
2221 Prerequisites:
2222 - all kickoff utxos are spent
2223 - for all kickoffs, all kickoff finalizers are spent
2224 ", round_idx);
2225 let current_chain_height = self
2227 .db
2228 .get_max_height(dbtx.as_deref_mut())
2229 .await?
2230 .ok_or_eyre("Max block height is not found in the btc syncer database")?;
2231
2232 let round_txid = round_tx.1.compute_txid();
2233 let (unspent_kickoff_utxos, are_all_utxos_spent_finalized) = self
2234 .find_and_mark_unspent_kickoff_utxos(
2235 dbtx.as_deref_mut(),
2236 round_idx,
2237 round_txid,
2238 current_chain_height,
2239 )
2240 .await?;
2241
2242 if !unspent_kickoff_utxos.is_empty() {
2243 let burn_txs = self
2244 .create_burn_unused_kickoff_connectors_tx(round_idx, &unspent_kickoff_utxos)
2245 .await?;
2246 txs_to_send.extend(burn_txs);
2247 } else if !are_all_utxos_spent_finalized {
2248 return Err(eyre::eyre!(format!(
2250 "The transactions that spend the kickoff utxos are not yet finalized, wait until they are finalized. Finality depth: {}
2251 If they are actually finalized, but this error is returned, it means internal bitcoin syncer is slow or stopped.",
2252 self.config.protocol_paramset().finality_depth
2253 ))
2254 .into());
2255 } else {
2256 self.validate_all_kickoff_finalizers_spent(
2260 dbtx.as_deref_mut(),
2261 round_idx,
2262 current_chain_height,
2263 )
2264 .await?;
2265 txs_to_send.push(ready_to_reimburse_tx.clone());
2267 }
2268 } else {
2269 txs_to_send.extend(self.send_next_round_tx(dbtx, round_idx).await?);
2272 }
2273
2274 Ok(txs_to_send)
2275 }
2276
2277 async fn find_and_mark_unspent_kickoff_utxos(
2280 &self,
2281 mut dbtx: Option<DatabaseTransaction<'_>>,
2282 round_idx: RoundIndex,
2283 round_txid: Txid,
2284 current_chain_height: u32,
2285 ) -> Result<(Vec<usize>, bool), BridgeError> {
2286 let mut unspent_kickoff_utxos = Vec::new();
2288 let mut fully_finalized_spent = true;
2290 for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2291 let kickoff_utxo = OutPoint {
2292 txid: round_txid,
2293 vout: UtxoVout::Kickoff(kickoff_idx).get_vout(),
2294 };
2295 if !self.rpc.is_utxo_spent(&kickoff_utxo).await? {
2296 unspent_kickoff_utxos.push(kickoff_idx);
2297 } else {
2298 self.db
2301 .mark_kickoff_connector_as_used(
2302 dbtx.as_deref_mut(),
2303 round_idx,
2304 kickoff_idx as u32,
2305 None,
2306 )
2307 .await?;
2308 fully_finalized_spent &= self
2311 .db
2312 .check_if_utxo_spending_tx_is_finalized(
2313 dbtx.as_deref_mut(),
2314 kickoff_utxo,
2315 current_chain_height,
2316 self.config.protocol_paramset(),
2317 )
2318 .await?;
2319 }
2320 }
2321 Ok((unspent_kickoff_utxos, fully_finalized_spent))
2322 }
2323
2324 async fn create_burn_unused_kickoff_connectors_tx(
2326 &self,
2327 round_idx: RoundIndex,
2328 unspent_kickoff_utxos: &[usize],
2329 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2330 tracing::info!(
2331 "There are unspent kickoff utxos {:?}, creating a tx that spends them",
2332 unspent_kickoff_utxos
2333 );
2334 let operator_winternitz_public_keys = self.generate_kickoff_winternitz_pubkeys()?;
2335 let kickoff_wpks = KickoffWinternitzKeys::new(
2336 operator_winternitz_public_keys,
2337 self.config.protocol_paramset().num_kickoffs_per_round,
2338 self.config.protocol_paramset().num_round_txs,
2339 )?;
2340 let (round_txhandler, _ready_to_reimburse_txhandler) = create_round_nth_txhandler(
2342 self.signer.xonly_public_key,
2343 self.collateral_funding_outpoint,
2344 self.config.protocol_paramset().collateral_funding_amount,
2345 round_idx,
2346 &kickoff_wpks,
2347 self.config.protocol_paramset(),
2348 )?;
2349 let mut burn_unused_kickoff_connectors_txhandler =
2350 create_burn_unused_kickoff_connectors_txhandler(
2351 &round_txhandler,
2352 unspent_kickoff_utxos,
2353 &self.reimburse_addr,
2354 self.config.protocol_paramset(),
2355 )?;
2356
2357 self.signer
2359 .tx_sign_and_fill_sigs(&mut burn_unused_kickoff_connectors_txhandler, &[], None)
2360 .wrap_err("Failed to sign burn unused kickoff connectors tx")?;
2361
2362 let burn_unused_kickoff_connectors_txhandler =
2363 burn_unused_kickoff_connectors_txhandler.promote()?;
2364 Ok(vec![(
2365 TransactionType::BurnUnusedKickoffConnectors,
2366 burn_unused_kickoff_connectors_txhandler
2367 .get_cached_tx()
2368 .clone(),
2369 )])
2370 }
2371
2372 async fn validate_all_kickoff_finalizers_spent(
2374 &self,
2375 mut dbtx: Option<DatabaseTransaction<'_>>,
2376 round_idx: RoundIndex,
2377 current_chain_height: u32,
2378 ) -> Result<(), BridgeError> {
2379 for kickoff_idx in 0..self.config.protocol_paramset().num_kickoffs_per_round {
2381 let kickoff_txid = self
2382 .db
2383 .get_kickoff_txid_for_used_kickoff_connector(
2384 dbtx.as_deref_mut(),
2385 round_idx,
2386 kickoff_idx as u32,
2387 )
2388 .await?;
2389 if let Some(kickoff_txid) = kickoff_txid {
2390 let deposit_outpoint = self
2391 .db
2392 .get_deposit_outpoint_for_kickoff_txid(dbtx.as_deref_mut(), kickoff_txid)
2393 .await?;
2394 let kickoff_finalizer_utxo = OutPoint {
2395 txid: kickoff_txid,
2396 vout: UtxoVout::KickoffFinalizer.get_vout(),
2397 };
2398 if !self.rpc.is_tx_on_chain(&kickoff_txid).await? {
2399 return Err(eyre::eyre!(
2400 "For round {:?} and kickoff utxo {:?}, the kickoff tx {:?} is not on chain,
2401 reimburse the deposit {:?} corresponding to this kickoff first. ",
2402 round_idx,
2403 kickoff_idx,
2404 kickoff_txid,
2405 deposit_outpoint
2406 )
2407 .into());
2408 } else if !self.rpc.is_utxo_spent(&kickoff_finalizer_utxo).await? {
2409 return Err(eyre::eyre!("For round {:?} and kickoff utxo {:?}, the kickoff finalizer {:?} is not spent,
2410 send the challenge timeout tx for the deposit {:?} first", round_idx, kickoff_idx, kickoff_txid, deposit_outpoint).into());
2411 } else if !self
2412 .db
2413 .check_if_utxo_spending_tx_is_finalized(
2414 dbtx.as_deref_mut(),
2415 kickoff_finalizer_utxo,
2416 current_chain_height,
2417 self.config.protocol_paramset(),
2418 )
2419 .await?
2420 {
2421 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: {}
2422 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());
2423 }
2424 }
2425 }
2426 Ok(())
2427 }
2428
2429 async fn send_next_round_tx(
2431 &self,
2432 mut dbtx: Option<DatabaseTransaction<'_>>,
2433 round_idx: RoundIndex,
2434 ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
2435 let next_round_context = ContractContext::new_context_for_round(
2436 self.signer.xonly_public_key,
2437 round_idx.next_round(),
2438 self.config.protocol_paramset(),
2439 );
2440 let next_round_txs = create_and_sign_txs(
2441 self.db.clone(),
2442 &self.signer,
2443 self.config.clone(),
2444 next_round_context,
2445 None,
2446 dbtx.as_deref_mut(),
2447 )
2448 .await?;
2449 let next_round_tx = next_round_txs
2450 .iter()
2451 .find(|(tx_type, _)| tx_type == &TransactionType::Round)
2452 .ok_or(eyre::eyre!("Next round tx not found in txs"))?;
2453 let next_round_txid = next_round_tx.1.compute_txid();
2454
2455 if !self.rpc.is_tx_on_chain(&next_round_txid).await? {
2456 Ok(vec![next_round_tx.clone()])
2458 } else {
2459 self.db
2461 .update_current_round_index(dbtx, round_idx.next_round())
2462 .await?;
2463 Ok(vec![])
2464 }
2465 }
2466
2467 #[cfg(feature = "automation")]
2470 async fn queue_relevant_txs_for_new_kickoff(
2471 &self,
2472 dbtx: DatabaseTransaction<'_>,
2473 kickoff_data: KickoffData,
2474 deposit_data: DepositData,
2475 ) -> Result<(), BridgeError> {
2476 let context = ContractContext::new_context_for_kickoff(
2477 kickoff_data,
2478 deposit_data.clone(),
2479 self.config.protocol_paramset(),
2480 );
2481 let signed_txs = create_and_sign_txs(
2482 self.db.clone(),
2483 &self.signer,
2484 self.config.clone(),
2485 context,
2486 Some([0u8; 20]),
2488 Some(dbtx),
2489 )
2490 .await?;
2491 let tx_metadata = Some(TxMetadata {
2492 tx_type: TransactionType::Dummy,
2493 operator_xonly_pk: Some(self.signer.xonly_public_key),
2494 round_idx: Some(kickoff_data.round_idx),
2495 kickoff_idx: Some(kickoff_data.kickoff_idx),
2496 deposit_outpoint: Some(deposit_data.get_deposit_outpoint()),
2497 });
2498 for (tx_type, signed_tx) in &signed_txs {
2499 match *tx_type {
2500 TransactionType::OperatorChallengeAck(_)
2501 | TransactionType::WatchtowerChallengeTimeout(_)
2502 | TransactionType::ChallengeTimeout
2503 | TransactionType::DisproveTimeout
2504 | TransactionType::Reimburse => {
2505 self.tx_sender
2506 .add_tx_to_queue(
2507 Some(dbtx),
2508 *tx_type,
2509 signed_tx,
2510 &signed_txs,
2511 tx_metadata,
2512 self.config.protocol_paramset(),
2513 None,
2514 )
2515 .await?;
2516 }
2517 _ => {}
2518 }
2519 }
2520 Ok(())
2521 }
2522}
2523
2524impl<C> NamedEntity for Operator<C>
2525where
2526 C: CitreaClientT,
2527{
2528 const ENTITY_NAME: &'static str = "operator";
2529 const FINALIZED_BLOCK_CONSUMER_ID_AUTOMATION: &'static str =
2530 "operator_finalized_block_fetcher_automation";
2531 const FINALIZED_BLOCK_CONSUMER_ID_NO_AUTOMATION: &'static str =
2532 "operator_finalized_block_fetcher_no_automation";
2533}
2534
2535#[cfg(feature = "automation")]
2536mod states {
2537
2538 use super::*;
2539 use crate::builder::transaction::{
2540 create_txhandlers, ContractContext, ReimburseDbCache, TxHandler, TxHandlerCache,
2541 };
2542 use crate::states::context::DutyResult;
2543 use crate::states::{block_cache, Duty, Owner, StateManager};
2544 use clementine_primitives::TransactionType;
2545 use std::collections::BTreeMap;
2546 use std::sync::Arc;
2547
2548 #[tonic::async_trait]
2549 impl<C> Owner for Operator<C>
2550 where
2551 C: CitreaClientT,
2552 {
2553 async fn handle_duty(
2554 &self,
2555 dbtx: DatabaseTransaction<'_>,
2556 duty: Duty,
2557 ) -> Result<DutyResult, BridgeError> {
2558 match duty {
2559 Duty::NewReadyToReimburse {
2560 round_idx,
2561 operator_xonly_pk,
2562 used_kickoffs,
2563 } => {
2564 tracing::info!("Operator {:?} called new ready to reimburse with round_idx: {:?}, operator_xonly_pk: {:?}, used_kickoffs: {:?}",
2565 self.signer.xonly_public_key, round_idx, operator_xonly_pk, used_kickoffs);
2566 Ok(DutyResult::Handled)
2567 }
2568 Duty::WatchtowerChallenge { .. } => Ok(DutyResult::Handled),
2569 Duty::AddRelevantTxsToTxSender {
2570 kickoff_data,
2571 deposit_data,
2572 } => {
2573 tracing::info!("Operator {:?} called add relevant txs to tx sender with kickoff_data: {:?}, deposit_data: {:?}",
2574 self.signer.xonly_public_key, kickoff_data, deposit_data);
2575 self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2576 .await?;
2577 Ok(DutyResult::Handled)
2578 }
2579 Duty::SendOperatorAsserts {
2580 kickoff_data,
2581 deposit_data,
2582 watchtower_challenges,
2583 payout_blockhash,
2584 latest_blockhash,
2585 } => {
2586 tracing::info!("Operator {:?} called send operator asserts with kickoff_data: {:?}, deposit_data: {:?}, number of watchtower_challenges: {}",
2587 self.signer.xonly_public_key, kickoff_data, deposit_data, watchtower_challenges.len());
2588 self.send_asserts(
2589 dbtx,
2590 kickoff_data,
2591 deposit_data,
2592 watchtower_challenges,
2593 payout_blockhash,
2594 latest_blockhash,
2595 )
2596 .await?;
2597 Ok(DutyResult::Handled)
2598 }
2599 Duty::VerifierDisprove { .. } => Ok(DutyResult::Handled),
2600 Duty::SendLatestBlockhash {
2601 kickoff_data,
2602 deposit_data,
2603 latest_blockhash,
2604 } => {
2605 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);
2606 self.send_latest_blockhash(dbtx, kickoff_data, deposit_data, latest_blockhash)
2607 .await?;
2608 Ok(DutyResult::Handled)
2609 }
2610 Duty::CheckIfKickoff {
2611 txid,
2612 block_height,
2613 witness,
2614 challenged_before: _,
2615 } => {
2616 tracing::debug!(
2617 "Operator {:?} called check if kickoff with txid: {:?}, block_height: {:?}",
2618 self.signer.xonly_public_key,
2619 txid,
2620 block_height,
2621 );
2622
2623 let kickoff_data = self
2624 .db
2625 .get_deposit_data_with_kickoff_txid(Some(dbtx), txid)
2626 .await?;
2627 if let Some((deposit_data, kickoff_data)) = kickoff_data {
2628 StateManager::<Self>::dispatch_new_kickoff_machine(
2629 &self.db,
2630 dbtx,
2631 kickoff_data,
2632 block_height,
2633 deposit_data.clone(),
2634 witness,
2635 )
2636 .await?;
2637
2638 self.queue_relevant_txs_for_new_kickoff(dbtx, kickoff_data, deposit_data)
2640 .await?;
2641 }
2642
2643 Ok(DutyResult::Handled)
2644 }
2645 }
2646 }
2647
2648 async fn create_txhandlers(
2649 &self,
2650 dbtx: DatabaseTransaction<'_>,
2651 tx_type: TransactionType,
2652 contract_context: ContractContext,
2653 ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError> {
2654 let mut db_cache =
2655 ReimburseDbCache::from_context(self.db.clone(), &contract_context, Some(dbtx));
2656 let txhandlers = create_txhandlers(
2657 tx_type,
2658 contract_context,
2659 &mut TxHandlerCache::new(),
2660 &mut db_cache,
2661 )
2662 .await?;
2663 Ok(txhandlers)
2664 }
2665
2666 async fn handle_finalized_block(
2667 &self,
2668 _dbtx: DatabaseTransaction<'_>,
2669 _block_id: u32,
2670 _block_height: u32,
2671 _block_cache: Arc<block_cache::BlockCache>,
2672 _light_client_proof_wait_interval_secs: Option<u32>,
2673 ) -> Result<(), BridgeError> {
2674 Ok(())
2675 }
2676
2677 fn is_kickoff_relevant_for_owner(&self, kickoff_data: &KickoffData) -> bool {
2678 kickoff_data.operator_xonly_pk == self.signer.xonly_public_key
2679 }
2680 }
2681}
2682
2683#[cfg(test)]
2684mod tests {
2685 use crate::operator::Operator;
2686 use crate::test::common::citrea::MockCitreaClient;
2687 use crate::test::common::*;
2688 use bitcoin::hashes::Hash;
2689 use bitcoin::{OutPoint, Txid};
2690
2691 #[tokio::test]
2692 #[ignore = "Design changes in progress"]
2693 async fn get_winternitz_public_keys() {
2694 let mut config = create_test_config_with_thread_name().await;
2695 let _regtest = create_regtest_rpc(&mut config).await;
2696
2697 let operator = Operator::<MockCitreaClient>::new(config.clone())
2698 .await
2699 .unwrap();
2700
2701 let deposit_outpoint = OutPoint {
2702 txid: Txid::all_zeros(),
2703 vout: 2,
2704 };
2705
2706 let winternitz_public_key = operator
2707 .generate_assert_winternitz_pubkeys(deposit_outpoint)
2708 .unwrap();
2709 assert_eq!(
2710 winternitz_public_key.len(),
2711 config.protocol_paramset().num_round_txs
2712 * config.protocol_paramset().num_kickoffs_per_round
2713 );
2714 }
2715
2716 #[tokio::test]
2717 async fn operator_get_params() {
2718 let mut config = create_test_config_with_thread_name().await;
2719 let _regtest = create_regtest_rpc(&mut config).await;
2720
2721 let operator = Operator::<MockCitreaClient>::new(config.clone())
2722 .await
2723 .unwrap();
2724 let actual_wpks = operator.generate_kickoff_winternitz_pubkeys().unwrap();
2725
2726 let (mut wpk_rx, _) = operator.get_params().await.unwrap();
2727 let mut idx = 0;
2728 while let Some(wpk) = wpk_rx.recv().await {
2729 assert_eq!(actual_wpks[idx], wpk);
2730 idx += 1;
2731 }
2732 assert_eq!(idx, actual_wpks.len());
2733 }
2734}