1use async_trait::async_trait;
14use bitcoin::Address;
15use bitcoin::Amount;
16use bitcoin::BlockHash;
17use bitcoin::FeeRate;
18use bitcoin::Network;
19use bitcoin::OutPoint;
20use bitcoin::ScriptBuf;
21use bitcoin::TxOut;
22use bitcoin::Txid;
23use bitcoincore_rpc::Auth;
24use bitcoincore_rpc::Client;
25use bitcoincore_rpc::RpcApi;
26use eyre::eyre;
27use eyre::Context;
28use eyre::OptionExt;
29use http::StatusCode;
30use reqwest;
31use secrecy::ExposeSecret;
32use secrecy::SecretString;
33use std::iter::Take;
34use std::str::FromStr;
35use std::sync::Arc;
36use std::time::Duration;
37use tokio::time::timeout;
38use tokio_retry::strategy::{jitter, ExponentialBackoff};
39use tokio_retry::RetryIf;
40
41use crate::builder::address::create_taproot_address;
42use crate::builder::transaction::create_round_txhandlers;
43use crate::builder::transaction::input::UtxoVout;
44use crate::builder::transaction::KickoffWinternitzKeys;
45use crate::builder::transaction::TransactionType;
46use crate::builder::transaction::TxHandler;
47use crate::config::protocol::ProtocolParamset;
48use crate::deposit::OperatorData;
49use crate::errors::{BridgeError, FeeErr};
50use crate::operator::RoundIndex;
51
52#[cfg(test)]
53use crate::test::common::citrea::CitreaE2EData;
54#[cfg(test)]
55use crate::{
56 citrea::CitreaClientT,
57 test::common::{are_all_state_managers_synced, test_actors::TestActors},
58};
59
60type Result<T> = std::result::Result<T, BitcoinRPCError>;
61
62const MAX_RETRY_ATTEMPTS: usize = 50;
63
64#[derive(Clone)]
65pub struct RetryConfig {
66 pub initial_delay_millis: u64,
67 pub max_delay: Duration,
68 pub max_attempts: usize,
69 pub backoff_multiplier: u64,
70 pub is_jitter: bool,
71 base_strategy: Arc<Take<ExponentialBackoff>>,
73}
74
75impl RetryConfig {
76 pub fn new(
77 initial_delay_millis: u64,
78 max_delay: Duration,
79 max_attempts: usize,
80 backoff_multiplier: u64,
81 is_jitter: bool,
82 ) -> Self {
83 let factor: u64 = initial_delay_millis / backoff_multiplier;
87
88 let max_attempts = std::cmp::min(max_attempts, MAX_RETRY_ATTEMPTS);
89
90 let base_strategy = Arc::new(
92 ExponentialBackoff::from_millis(backoff_multiplier)
93 .max_delay(max_delay)
94 .factor(factor)
95 .take(max_attempts),
96 );
97
98 Self {
99 initial_delay_millis,
100 max_delay,
101 max_attempts,
102 backoff_multiplier,
103 is_jitter,
104 base_strategy,
105 }
106 }
107
108 pub fn get_strategy(&self) -> Box<dyn Iterator<Item = Duration> + Send> {
109 let base_strategy = (*self.base_strategy).clone();
111
112 if self.is_jitter {
113 Box::new(base_strategy.map(jitter))
114 } else {
115 Box::new(base_strategy)
116 }
117 }
118}
119
120impl Default for RetryConfig {
121 fn default() -> Self {
122 Self::new(100, Duration::from_secs(30), 5, 2, false)
123 }
124}
125
126impl std::fmt::Debug for RetryConfig {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("RetryConfig")
129 .field("initial_delay_millis", &self.initial_delay_millis)
130 .field("max_delay", &self.max_delay)
131 .field("max_attempts", &self.max_attempts)
132 .field("backoff_multiplier", &self.backoff_multiplier)
133 .field("is_jitter", &self.is_jitter)
134 .finish()
135 }
136}
137
138pub trait RetryableError {
140 fn is_retryable(&self) -> bool;
141}
142
143impl RetryableError for bitcoincore_rpc::Error {
144 fn is_retryable(&self) -> bool {
145 tracing::trace!("Checking if error is retryable: {:?}", self);
146 let result = match self {
147 bitcoincore_rpc::Error::JsonRpc(jsonrpc_error) => {
149 let error_str = jsonrpc_error.to_string().to_lowercase();
150 tracing::trace!("JsonRpc error string (lowercase): {}", error_str);
151 let is_retryable = error_str.contains("timeout")
153 || error_str.contains("connection")
154 || error_str.contains("temporary")
155 || error_str.contains("busy")
156 || error_str.contains("unavailable")
157 || error_str.contains("network")
158 || error_str.contains("broken pipe")
159 || error_str.contains("connection reset")
160 || error_str.contains("connection refused")
161 || error_str.contains("host unreachable");
162 tracing::trace!("JsonRpc error is_retryable: {}", is_retryable);
163 is_retryable
164 }
165
166 bitcoincore_rpc::Error::Io(io_error) => {
168 use std::io::ErrorKind;
169 match io_error.kind() {
170 ErrorKind::ConnectionRefused
172 | ErrorKind::ConnectionReset
173 | ErrorKind::ConnectionAborted
174 | ErrorKind::NotConnected
175 | ErrorKind::BrokenPipe
176 | ErrorKind::TimedOut
177 | ErrorKind::Interrupted
178 | ErrorKind::UnexpectedEof => true,
179
180 ErrorKind::PermissionDenied
182 | ErrorKind::NotFound
183 | ErrorKind::InvalidInput
184 | ErrorKind::InvalidData => false,
185
186 _ => true,
188 }
189 }
190
191 bitcoincore_rpc::Error::Auth(_) => false,
193
194 bitcoincore_rpc::Error::UrlParse(_) => false,
196
197 bitcoincore_rpc::Error::InvalidCookieFile => false,
199
200 bitcoincore_rpc::Error::ReturnedError(error_msg) => {
202 let error_str = error_msg.to_lowercase();
203 error_str.contains("loading") ||
205 error_str.contains("warming up") ||
206 error_str.contains("verifying") ||
207 error_str.contains("busy") ||
208 error_str.contains("temporary") ||
209 error_str.contains("try again") ||
210 error_str.contains("timeout") ||
211 !(error_str.contains("insufficient funds") ||
213 error_str.contains("transaction already") ||
214 error_str.contains("invalid") ||
215 error_str.contains("not found") ||
216 error_str.contains("conflict"))
217 }
218
219 bitcoincore_rpc::Error::UnexpectedStructure => true,
222
223 bitcoincore_rpc::Error::BitcoinSerialization(_) => false,
225 bitcoincore_rpc::Error::Hex(_) => false,
226 bitcoincore_rpc::Error::Json(_) => false,
227 bitcoincore_rpc::Error::Secp256k1(_) => false,
228 bitcoincore_rpc::Error::InvalidAmount(_) => false,
229 };
230 tracing::trace!("Final is_retryable result: {}", result);
231 result
232 }
233}
234
235impl RetryableError for BitcoinRPCError {
236 fn is_retryable(&self) -> bool {
237 match self {
238 BitcoinRPCError::TransactionNotConfirmed => true,
239 BitcoinRPCError::TransactionAlreadyInBlock(_) => false,
240 BitcoinRPCError::BumpFeeUTXOSpent(_) => false,
241
242 BitcoinRPCError::BumpFeeError(_, _) => true,
244
245 BitcoinRPCError::Other(err) => {
247 let err_str = err.to_string().to_lowercase();
248 err_str.contains("timeout")
249 || err_str.contains("connection")
250 || err_str.contains("temporary")
251 || err_str.contains("busy")
252 || err_str.contains("network")
253 }
254 }
255 }
256}
257
258#[derive(Clone)]
261pub struct ExtendedBitcoinRpc {
262 url: String,
263 client: Arc<Client>,
264 retry_config: RetryConfig,
265
266 #[cfg(test)]
267 cached_mining_address: Arc<tokio::sync::RwLock<Option<String>>>,
268}
269
270impl std::fmt::Debug for ExtendedBitcoinRpc {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 f.debug_struct("ExtendedBitcoinRpc")
273 .field("url", &self.url)
274 .finish()
275 }
276}
277
278#[derive(Debug, thiserror::Error)]
280pub enum BitcoinRPCError {
281 #[error("Failed to bump fee for Txid of {0} and feerate of {1}")]
282 BumpFeeError(Txid, FeeRate),
283 #[error("Failed to bump fee: UTXO is already spent")]
284 BumpFeeUTXOSpent(OutPoint),
285 #[error("Transaction is already in block: {0}")]
286 TransactionAlreadyInBlock(BlockHash),
287 #[error("Transaction is not confirmed")]
288 TransactionNotConfirmed,
289
290 #[error(transparent)]
291 Other(#[from] eyre::Report),
292}
293
294impl ExtendedBitcoinRpc {
295 pub async fn connect(
316 url: String,
317 user: SecretString,
318 password: SecretString,
319 retry_config: Option<RetryConfig>,
320 ) -> Result<Self> {
321 let config = retry_config.clone().unwrap_or_default();
322
323 let url_clone = url.clone();
324 let user_clone = user.clone();
325 let password_clone = password.clone();
326
327 let retry_strategy = config.get_strategy();
328
329 RetryIf::spawn(
330 retry_strategy,
331 || async {
332 let auth = Auth::UserPass(
333 user_clone.expose_secret().to_string(),
334 password_clone.expose_secret().to_string(),
335 );
336
337 let retry_config = retry_config.clone().unwrap_or_default();
338
339 tracing::debug!(
340 "Attempting to connect to Bitcoin RPC at {} with retry config: {:?}",
341 &url_clone,
342 &retry_config
343 );
344 let rpc = Client::new(&url_clone, auth)
345 .await
346 .wrap_err("Failed to connect to Bitcoin RPC")?;
347
348 tracing::debug!(
350 "Pinging Bitcoin RPC at {} to make sure it's alive",
351 &url_clone
352 );
353 rpc.ping()
354 .await
355 .map_err(|e| eyre::eyre!("Failed to ping Bitcoin RPC: {}", e))?;
356
357 let result: Result<ExtendedBitcoinRpc> = Ok(Self {
358 url: url_clone.clone(),
359 client: Arc::new(rpc),
360 retry_config,
361 #[cfg(test)]
362 cached_mining_address: Arc::new(tokio::sync::RwLock::new(None)),
363 });
364
365 match &result {
366 Ok(_) => tracing::debug!("Connected to Bitcoin RPC successfully"),
367 Err(error) => {
368 if !error.is_retryable() {
369 tracing::debug!("Non-retryable connection error: {}", error);
370 } else {
371 tracing::debug!("Bitcoin RPC connection failed, will retry: {}", error);
372 }
373 }
374 }
375
376 result
377 },
378 |error: &BitcoinRPCError| error.is_retryable(),
379 )
380 .await
381 }
382
383 pub async fn get_new_wallet_address(&self) -> Result<Address> {
385 self.get_new_address(None, None)
386 .await
387 .wrap_err("Failed to get new address")
388 .map(|addr| addr.assume_checked())
389 .map_err(Into::into)
390 }
391
392 pub async fn confirmation_blocks(&self, txid: &bitcoin::Txid) -> Result<u32> {
407 let raw_tx_res = self
408 .get_raw_transaction_info(txid, None)
409 .await
410 .wrap_err("Failed to get transaction info")?;
411 raw_tx_res
412 .confirmations
413 .ok_or_else(|| eyre::eyre!("No confirmation data for transaction {}", txid))
414 .map_err(Into::into)
415 }
416
417 pub async fn get_current_chain_height(&self) -> Result<u32> {
423 let height = self
424 .get_block_count()
425 .await
426 .wrap_err("Failed to get current chain height")?;
427 Ok(u32::try_from(height).wrap_err("Failed to convert block count to u32")?)
428 }
429
430 pub async fn collateral_check(
452 &self,
453 operator_data: &OperatorData,
454 kickoff_wpks: &KickoffWinternitzKeys,
455 paramset: &'static ProtocolParamset,
456 ) -> std::result::Result<bool, BridgeError> {
457 let tx = self
459 .get_tx_of_txid(&operator_data.collateral_funding_outpoint.txid)
460 .await
461 .wrap_err(format!(
462 "Failed to find collateral utxo in chain for outpoint {:?}",
463 operator_data.collateral_funding_outpoint
464 ))?;
465 let collateral_outpoint = match tx
466 .output
467 .get(operator_data.collateral_funding_outpoint.vout as usize)
468 {
469 Some(output) => output,
470 None => {
471 tracing::warn!(
472 "No output at index {} for txid {} while checking for collateral existence",
473 operator_data.collateral_funding_outpoint.vout,
474 operator_data.collateral_funding_outpoint.txid
475 );
476 return Ok(false);
477 }
478 };
479
480 if collateral_outpoint.value != paramset.collateral_funding_amount {
481 tracing::error!(
482 "Collateral amount for collateral {:?} is not correct: expected {}, got {}",
483 operator_data.collateral_funding_outpoint,
484 paramset.collateral_funding_amount,
485 collateral_outpoint.value
486 );
487 return Ok(false);
488 }
489
490 let operator_tpr_address =
491 create_taproot_address(&[], Some(operator_data.xonly_pk), paramset.network).0;
492
493 if collateral_outpoint.script_pubkey != operator_tpr_address.script_pubkey() {
494 tracing::error!(
495 "Collateral script pubkey for collateral {:?} is not correct: expected {}, got {}",
496 operator_data.collateral_funding_outpoint,
497 operator_tpr_address.script_pubkey(),
498 collateral_outpoint.script_pubkey
499 );
500 return Ok(false);
501 }
502
503 let is_on_chain = self
508 .is_tx_on_chain(&operator_data.collateral_funding_outpoint.txid)
509 .await?;
510 if !is_on_chain {
511 return match paramset.network {
512 bitcoin::Network::Bitcoin => Ok(false),
513 _ => Ok(true),
514 };
515 }
516
517 let mut current_collateral_outpoint: OutPoint = operator_data.collateral_funding_outpoint;
518 let mut prev_ready_to_reimburse: Option<TxHandler> = None;
519 for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
521 let txhandlers = create_round_txhandlers(
523 paramset,
524 round_idx,
525 operator_data,
526 kickoff_wpks,
527 prev_ready_to_reimburse.as_ref(),
528 )?;
529
530 let mut round_txhandler_opt = None;
531 let mut ready_to_reimburse_txhandler_opt = None;
532 for txhandler in &txhandlers {
533 match txhandler.get_transaction_type() {
534 TransactionType::Round => round_txhandler_opt = Some(txhandler),
535 TransactionType::ReadyToReimburse => {
536 ready_to_reimburse_txhandler_opt = Some(txhandler)
537 }
538 _ => {}
539 }
540 }
541 if round_txhandler_opt.is_none() || ready_to_reimburse_txhandler_opt.is_none() {
542 return Err(eyre!(
543 "Failed to create round and ready to reimburse txs for round {:?} for operator {}",
544 round_idx,
545 operator_data.xonly_pk
546 ).into());
547 }
548
549 let round_txid = round_txhandler_opt
550 .expect("Round txhandler should exist, checked above")
551 .get_cached_tx()
552 .compute_txid();
553 let is_round_tx_on_chain = self.is_tx_on_chain(&round_txid).await?;
554 if !is_round_tx_on_chain {
555 break;
556 }
557 let block_hash = self.get_blockhash_of_tx(&round_txid).await?;
558 let block_height = self
559 .get_block_info(&block_hash)
560 .await
561 .wrap_err(format!(
562 "Failed to get block info for block hash {block_hash}"
563 ))?
564 .height;
565 if block_height < paramset.start_height as usize {
566 tracing::warn!(
567 "Collateral utxo of operator {operator_data:?} is spent in a block before paramset start height: {block_height} < {0}",
568 paramset.start_height
569 );
570 return Ok(false);
571 }
572 current_collateral_outpoint = OutPoint {
573 txid: round_txid,
574 vout: UtxoVout::CollateralInRound.get_vout(),
575 };
576 if round_idx == RoundIndex::Round(paramset.num_round_txs - 1) {
577 break;
580 }
581 let ready_to_reimburse_txhandler = ready_to_reimburse_txhandler_opt
582 .expect("Ready to reimburse txhandler should exist");
583 let ready_to_reimburse_txid =
584 ready_to_reimburse_txhandler.get_cached_tx().compute_txid();
585 let is_ready_to_reimburse_tx_on_chain =
586 self.is_tx_on_chain(&ready_to_reimburse_txid).await?;
587 if !is_ready_to_reimburse_tx_on_chain {
588 break;
589 }
590
591 current_collateral_outpoint = OutPoint {
592 txid: ready_to_reimburse_txid,
593 vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
594 };
595
596 prev_ready_to_reimburse = Some(ready_to_reimburse_txhandler.clone());
597 }
598
599 Ok(!self.is_utxo_spent(¤t_collateral_outpoint).await?)
603 }
604
605 pub async fn get_blockhash_of_tx(&self, txid: &bitcoin::Txid) -> Result<bitcoin::BlockHash> {
621 let raw_transaction_results = self
622 .get_raw_transaction_info(txid, None)
623 .await
624 .wrap_err("Failed to get transaction info")?;
625 let Some(blockhash) = raw_transaction_results.blockhash else {
626 return Err(eyre::eyre!("Transaction not confirmed: {0}", txid).into());
627 };
628 Ok(blockhash)
629 }
630
631 pub async fn get_block_info_by_height(
642 &self,
643 height: u64,
644 ) -> Result<(bitcoin::BlockHash, bitcoin::block::Header)> {
645 let block_hash = self.get_block_hash(height).await.wrap_err(format!(
646 "Couldn't retrieve block hash from height {height} from rpc"
647 ))?;
648 let block_header = self.get_block_header(&block_hash).await.wrap_err(format!(
649 "Couldn't retrieve block header with block hash {block_hash} from rpc"
650 ))?;
651
652 Ok((block_hash, block_header))
653 }
654
655 #[tracing::instrument(skip(self), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))]
665 pub async fn get_prevout_txs(
666 &self,
667 tx: &bitcoin::Transaction,
668 ) -> Result<Vec<bitcoin::Transaction>> {
669 let mut prevout_txs = Vec::new();
670 for input in &tx.input {
671 let txid = input.previous_output.txid;
672 prevout_txs.push(self.get_tx_of_txid(&txid).await?);
673 }
674 Ok(prevout_txs)
675 }
676
677 pub async fn get_tx_of_txid(&self, txid: &bitcoin::Txid) -> Result<bitcoin::Transaction> {
687 let raw_transaction = self
688 .get_raw_transaction(txid, None)
689 .await
690 .wrap_err("Failed to get raw transaction")?;
691 Ok(raw_transaction)
692 }
693
694 pub async fn is_tx_on_chain(&self, txid: &bitcoin::Txid) -> Result<bool> {
704 Ok(self
705 .get_raw_transaction_info(txid, None)
706 .await
707 .ok()
708 .and_then(|s| s.blockhash)
709 .is_some())
710 }
711
712 pub async fn check_utxo_address_and_amount(
724 &self,
725 outpoint: &OutPoint,
726 address: &ScriptBuf,
727 amount_sats: Amount,
728 ) -> Result<bool> {
729 let tx = self
730 .get_raw_transaction(&outpoint.txid, None)
731 .await
732 .wrap_err("Failed to get transaction")?;
733
734 let current_output = tx
735 .output
736 .get(outpoint.vout as usize)
737 .ok_or(eyre!(
738 "No output at index {} for txid {}",
739 outpoint.vout,
740 outpoint.txid
741 ))?
742 .to_owned();
743
744 let expected_output = TxOut {
745 script_pubkey: address.clone(),
746 value: amount_sats,
747 };
748
749 Ok(expected_output == current_output)
750 }
751
752 pub async fn is_utxo_spent(&self, outpoint: &OutPoint) -> Result<bool> {
767 if !self.is_tx_on_chain(&outpoint.txid).await? {
768 return Err(BitcoinRPCError::TransactionNotConfirmed);
769 }
770
771 let res = self
772 .get_tx_out(&outpoint.txid, outpoint.vout, Some(false))
773 .await
774 .wrap_err("Failed to get transaction output")?;
775
776 Ok(res.is_none())
777 }
778
779 #[cfg(test)]
792 pub async fn mine_blocks(&self, block_num: u64) -> Result<Vec<BlockHash>> {
793 if block_num == 0 {
794 return Ok(vec![]);
795 }
796
797 self.try_mine(block_num).await
798 }
799
800 #[cfg(test)]
803 pub async fn mine_blocks_while_synced<C: CitreaClientT>(
804 &self,
805 block_num: u64,
806 actors: &TestActors<C>,
807 e2e: Option<&CitreaE2EData<'_>>,
808 ) -> Result<Vec<BlockHash>> {
809 match e2e {
810 Some(e2e) if e2e.bitcoin_nodes.iter().count() > 1 => {
812 use bitcoin::secp256k1::rand::{thread_rng, Rng};
813 e2e.bitcoin_nodes
814 .disconnect_nodes()
815 .await
816 .map_err(|e| eyre::eyre!("Failed to disconnect nodes: {}", e))?;
817 let reorg_blocks =
818 thread_rng().gen_range(0..e2e.config.protocol_paramset().finality_depth as u64);
819 let da0 = e2e.bitcoin_nodes.get(0).expect("node 0 should exist");
820 let da1 = e2e.bitcoin_nodes.get(1).expect("node 1 should exist");
821
822 let mut mined_blocks = Vec::new();
823 while mined_blocks.len() < reorg_blocks as usize {
824 if !are_all_state_managers_synced(self, actors).await? {
825 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
827 continue;
828 }
829 da0.generate(1)
830 .await
831 .wrap_err("Failed to generate blocks")?;
832 let new_blocks = da1
834 .generate(1)
835 .await
836 .wrap_err("Failed to generate blocks")?;
837 mined_blocks.extend(new_blocks);
838 }
839 mined_blocks.extend(
840 da1.generate(1)
841 .await
842 .wrap_err("Failed to generate blocks")?,
843 );
844 e2e.bitcoin_nodes
846 .connect_nodes()
847 .await
848 .map_err(|e| eyre::eyre!("Failed to connect nodes: {}", e))?;
849 e2e.bitcoin_nodes
850 .wait_for_sync(None)
851 .await
852 .map_err(|e| eyre::eyre!("Failed to wait for sync: {}", e))?;
853 while mined_blocks.len() != (reorg_blocks + block_num + 1) as usize {
856 if !are_all_state_managers_synced(self, actors).await? {
857 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
859 continue;
860 }
861 mined_blocks.extend(self.mine_blocks(1).await?);
862 }
863 Ok(mined_blocks)
864 }
865 _ => {
866 let mut mined_blocks = Vec::new();
868 while mined_blocks.len() < block_num as usize {
869 if !are_all_state_managers_synced(self, actors).await? {
870 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
872 continue;
873 }
874 let new_blocks = self.mine_blocks(1).await?;
875 mined_blocks.extend(new_blocks);
876 }
877 Ok(mined_blocks)
878 }
879 }
880 }
881
882 #[cfg(test)]
894 async fn try_mine(&self, block_num: u64) -> Result<Vec<BlockHash>> {
895 let address = {
896 let read = self.cached_mining_address.read().await;
897 if let Some(addr) = &*read {
898 addr.clone()
899 } else {
900 drop(read);
901 let mut write = self.cached_mining_address.write().await;
902
903 if let Some(addr) = &*write {
904 addr.clone()
905 } else {
906 let new_addr = self
907 .get_new_address(None, None)
908 .await
909 .wrap_err("Failed to get new address")?
910 .assume_checked()
911 .to_string();
912 *write = Some(new_addr.clone());
913 new_addr
914 }
915 }
916 };
917
918 let address = Address::from_str(&address)
919 .wrap_err("Invalid address format")?
920 .assume_checked();
921 let blocks = self
922 .generate_to_address(block_num, &address)
923 .await
924 .wrap_err("Failed to generate to address")?;
925
926 Ok(blocks)
927 }
928
929 pub async fn mempool_size(&self) -> Result<usize> {
935 let mempool_info = self
936 .get_mempool_info()
937 .await
938 .wrap_err("Failed to get mempool info")?;
939 Ok(mempool_info.size)
940 }
941
942 pub async fn send_to_address(
953 &self,
954 address: &Address,
955 amount_sats: Amount,
956 ) -> Result<OutPoint> {
957 let txid = self
958 .client
959 .send_to_address(
960 address,
961 amount_sats,
962 None,
963 None,
964 None,
965 Some(true),
966 Some(2),
967 Some(bitcoincore_rpc::json::EstimateMode::Conservative),
968 )
969 .await
970 .wrap_err("Failed to send to address")?;
971
972 let tx_result = self
973 .get_transaction(&txid, None)
974 .await
975 .wrap_err("Failed to get transaction")?;
976 let vout = tx_result.details[0].vout;
977
978 Ok(OutPoint { txid, vout })
979 }
980
981 pub async fn get_txout_from_outpoint(&self, outpoint: &OutPoint) -> Result<TxOut> {
991 let tx = self
992 .get_raw_transaction(&outpoint.txid, None)
993 .await
994 .wrap_err("Failed to get transaction")?;
995 let txout = tx
996 .output
997 .get(outpoint.vout as usize)
998 .ok_or(eyre!(
999 "No output at index {} for txid {}",
1000 outpoint.vout,
1001 outpoint.txid
1002 ))?
1003 .to_owned();
1004
1005 Ok(txout)
1006 }
1007
1008 pub async fn bump_fee_with_fee_rate(&self, txid: Txid, fee_rate: FeeRate) -> Result<Txid> {
1034 let transaction_info = self
1036 .get_transaction(&txid, None)
1037 .await
1038 .wrap_err("Failed to get transaction")?;
1039 if transaction_info.info.blockhash.is_some() {
1040 return Err(BitcoinRPCError::TransactionAlreadyInBlock(
1041 transaction_info
1042 .info
1043 .blockhash
1044 .expect("Blockhash should be present"),
1045 ));
1046 }
1047
1048 let tx = transaction_info
1050 .transaction()
1051 .wrap_err("Failed to get transaction")?;
1052 let tx_weight = tx.weight().to_wu();
1053 let current_fee_sat = u64::try_from(
1054 transaction_info
1055 .fee
1056 .expect("Fee should be present")
1057 .to_sat()
1058 .abs(),
1059 )
1060 .wrap_err("Failed to convert fee to sat")?;
1061
1062 let current_fee_rate_sat_kwu = current_fee_sat as f64 * 1000.0 / tx_weight as f64;
1063
1064 tracing::trace!(
1065 "Bump fee with fee rate txid: {txid} - Current fee sat: {current_fee_sat} - current fee rate: {current_fee_rate_sat_kwu}"
1066 );
1067
1068 if current_fee_rate_sat_kwu >= fee_rate.to_sat_per_kwu() as f64 {
1070 return Ok(txid);
1071 }
1072
1073 tracing::trace!(
1074 "Bump fee with fee rate txid: {txid} - Current fee rate: {current_fee_rate_sat_kwu} sat/kwu, target fee rate: {fee_rate} sat/kwu"
1075 );
1076
1077 let network_info = self
1079 .get_network_info()
1080 .await
1081 .wrap_err("Failed to get network info")?;
1082 let incremental_fee = network_info.incremental_fee;
1084 let incremental_fee_rate_sat_kwu = incremental_fee.to_sat() as f64 / 4.0;
1086
1087 let new_fee_rate = FeeRate::from_sat_per_kwu(std::cmp::max(
1089 (current_fee_rate_sat_kwu + incremental_fee_rate_sat_kwu).ceil() as u64,
1090 fee_rate.to_sat_per_kwu(),
1091 ));
1092
1093 tracing::debug!(
1094 "Bumping fee for txid: {txid} from {current_fee_rate_sat_kwu} to {new_fee_rate} with incremental fee {incremental_fee_rate_sat_kwu} - Final fee rate: {new_fee_rate}, current chain fee rate: {fee_rate}"
1095 );
1096
1097 let bump_fee_result = match self
1099 .bump_fee(
1100 &txid,
1101 Some(&bitcoincore_rpc::json::BumpFeeOptions {
1102 fee_rate: Some(bitcoincore_rpc::json::FeeRate::per_vbyte(Amount::from_sat(
1103 new_fee_rate.to_sat_per_vb_ceil(),
1104 ))),
1105 replaceable: Some(true),
1106 ..Default::default()
1107 }),
1108 )
1109 .await
1110 {
1111 Ok(bump_fee_result) => bump_fee_result,
1112 Err(e) => match e {
1114 bitcoincore_rpc::Error::JsonRpc(json_rpc_error) => match json_rpc_error {
1115 bitcoincore_rpc::RpcError::Rpc(rpc_error) => {
1116 if let Some((outpoint_str, _)) =
1117 rpc_error.message.split_once(" is already spent")
1118 {
1119 let outpoint = OutPoint::from_str(outpoint_str)
1120 .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))?;
1121
1122 return Err(BitcoinRPCError::BumpFeeUTXOSpent(outpoint));
1123 }
1124
1125 return Err(eyre::eyre!("{:?}", rpc_error)
1126 .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1127 .into());
1128 }
1129 _ => {
1130 return Err(eyre::eyre!(json_rpc_error)
1131 .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1132 .into());
1133 }
1134 },
1135 _ => {
1136 return Err(eyre::eyre!(e)
1137 .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1138 .into())
1139 }
1140 },
1141 };
1142
1143 Ok(bump_fee_result
1145 .txid
1146 .ok_or_eyre("Failed to get Txid from bump_fee_result")?)
1147 }
1148
1149 pub async fn clone_inner(&self) -> std::result::Result<Self, bitcoincore_rpc::Error> {
1157 Ok(Self {
1158 url: self.url.clone(),
1159 client: self.client.clone(),
1160 retry_config: self.retry_config.clone(),
1161 #[cfg(test)]
1162 cached_mining_address: self.cached_mining_address.clone(),
1163 })
1164 }
1165
1166 pub async fn get_block_by_height(&self, height: u64) -> Result<bitcoin::Block> {
1176 let hash = self
1177 .get_block_info_by_height(height)
1178 .await
1179 .wrap_err("Failed to get block info by height")?
1180 .0;
1181
1182 Ok(self
1183 .get_block(&hash)
1184 .await
1185 .wrap_err("Failed to get block by height")?)
1186 }
1187
1188 pub async fn get_fee_rate(
1198 &self,
1199 network: Network,
1200 mempool_api_host: &Option<String>,
1201 mempool_api_endpoint: &Option<String>,
1202 mempool_fee_rate_multiplier: u64,
1203 mempool_fee_rate_offset_sat_kvb: u64,
1204 fee_rate_hard_cap: u64,
1205 ) -> Result<FeeRate> {
1206 match network {
1207 Network::Regtest => {
1209 tracing::debug!("Using fixed fee rate of 1 sat/vB for {network} network");
1210 Ok(FeeRate::from_sat_per_vb_unchecked(1))
1211 }
1212
1213 Network::Bitcoin | Network::Testnet4 | Network::Signet => {
1215 tracing::debug!("Fetching fee rate for {network} network...");
1216
1217 let mempool_fee = get_fee_rate_from_mempool_space(
1219 mempool_api_host,
1220 mempool_api_endpoint,
1221 network,
1222 )
1223 .await;
1224
1225 let rpc_fee = timeout(Duration::from_secs(30), self.estimate_smart_fee(1, None))
1226 .await
1227 .map_err(|_| eyre!("RPC estimate_smart_fee timed out after 30 seconds"))
1228 .and_then(|result| {
1229 result.wrap_err("Failed to estimate smart fee using Bitcoin Core RPC")
1230 })
1231 .and_then(|estimate| {
1232 estimate.fee_rate.ok_or_else(|| {
1233 eyre!("Failed to extract fee rate from Bitcoin Core RPC response")
1234 })
1235 });
1236
1237 let selected_fee_amount = match (mempool_fee, rpc_fee) {
1239 (Ok(mempool_amt), Ok(rpc_amt)) => {
1240 let multiplier = mempool_fee_rate_multiplier;
1242 let offset = mempool_fee_rate_offset_sat_kvb;
1243 let rpc_amt_sat = rpc_amt.to_sat();
1244
1245 let threshold_sat = multiplier
1246 .checked_mul(rpc_amt_sat)
1247 .and_then(|v| v.checked_add(offset))
1248 .ok_or_else(|| {
1249 eyre!("Overflow when calculating threshold_sat in fee selection ({multiplier} * {rpc_amt_sat} + {offset})")
1250 })?;
1251
1252 let threshold = Amount::from_sat(threshold_sat);
1253
1254 if mempool_amt <= threshold {
1255 tracing::debug!(
1256 "Selected mempool.space fee rate: {} sat/kvB (mempool: {}, rpc: {}, threshold: {})",
1257 mempool_amt.to_sat(),
1258 mempool_amt.to_sat(),
1259 rpc_amt.to_sat(),
1260 threshold
1261 );
1262 mempool_amt
1263 } else {
1264 tracing::debug!(
1265 "Selected Bitcoin Core RPC fee rate: {} sat/kvB (mempool: {}, rpc: {}, threshold: {})",
1266 rpc_amt.to_sat(),
1267 mempool_amt.to_sat(),
1268 rpc_amt.to_sat(),
1269 threshold
1270 );
1271 rpc_amt
1272 }
1273 }
1274 (Ok(mempool_amt), Err(rpc_err)) => {
1275 tracing::warn!(
1276 "RPC fee estimation failed, using mempool.space: {:#}",
1277 rpc_err
1278 );
1279 mempool_amt
1280 }
1281 (Err(mempool_err), Ok(rpc_amt)) => {
1282 tracing::warn!(
1283 "Mempool.space fee fetch failed, using Bitcoin Core RPC: {:#}",
1284 mempool_err
1285 );
1286 rpc_amt
1287 }
1288 (Err(mempool_err), Err(rpc_err)) => {
1289 tracing::warn!(
1290 "Both fee sources failed (mempool: {:#}, rpc: {:#}), using default of 1 sat/vB",
1291 mempool_err, rpc_err
1292 );
1293 Amount::from_sat(1000) }
1295 };
1296
1297 let mut fee_sat_kvb = selected_fee_amount.to_sat();
1299
1300 if fee_sat_kvb > fee_rate_hard_cap * 1000 {
1302 tracing::warn!(
1303 "Fee rate {} sat/kvb exceeds hard cap {} sat/kvb, using hard cap",
1304 fee_sat_kvb,
1305 fee_rate_hard_cap * 1000
1306 );
1307 fee_sat_kvb = fee_rate_hard_cap * 1000;
1308 }
1309
1310 tracing::debug!("Final fee rate: {} sat/kvb", fee_sat_kvb);
1311 Ok(FeeRate::from_sat_per_kwu(fee_sat_kvb.div_ceil(4)))
1312 }
1313
1314 _ => Err(eyre!(
1316 "Fee rate estimation is not supported for network: {:?}",
1317 network
1318 )
1319 .into()),
1320 }
1321 }
1322}
1323
1324async fn get_fee_rate_from_mempool_space(
1329 url: &Option<String>,
1330 endpoint: &Option<String>,
1331 network: Network,
1332) -> Result<Amount> {
1333 let url = url
1334 .as_ref()
1335 .ok_or_else(|| eyre!("Fee rate API host is not configured"))?;
1336
1337 let endpoint = endpoint
1338 .as_ref()
1339 .ok_or_else(|| eyre!("Fee rate API endpoint is not configured"))?;
1340
1341 let url = match network {
1342 Network::Bitcoin => format!(
1343 "{url}{endpoint}",
1345 ),
1346 Network::Testnet4 => format!("{url}testnet4/{endpoint}"),
1347 Network::Signet => {
1349 tracing::warn!("You should use Citrea signet url for mempool.space");
1350 format!("{url}{endpoint}")
1351 }
1352 _ => return Err(eyre!("Unsupported network for mempool.space: {network:?}").into()),
1353 };
1354
1355 let retry_config = RetryConfig::new(250, Duration::from_secs(5), 4, 2, true);
1356
1357 let retry_strategy = retry_config.get_strategy();
1358
1359 let should_retry = |e: &FeeErr| match e {
1361 FeeErr::Timeout => true,
1362 FeeErr::Transport(req_err) => req_err.is_timeout() || req_err.is_connect(),
1363 FeeErr::Status(code) => code.is_server_error() || *code == StatusCode::TOO_MANY_REQUESTS,
1364 FeeErr::JsonDecode(_) | FeeErr::MissingField => false,
1365 };
1366
1367 let fee_sat_per_vb: u64 = RetryIf::spawn(
1368 retry_strategy,
1369 || {
1370 let url = url.clone();
1371 async move {
1372 let resp = timeout(Duration::from_secs(5), reqwest::get(&url))
1373 .await
1374 .map_err(|_| FeeErr::Timeout)?
1375 .map_err(FeeErr::Transport)?;
1376
1377 let status = resp.status();
1378 if !status.is_success() {
1379 return Err(FeeErr::Status(status));
1380 }
1381
1382 let json: serde_json::Value = timeout(Duration::from_secs(5), resp.json())
1383 .await
1384 .map_err(|_| FeeErr::Timeout)?
1385 .map_err(FeeErr::JsonDecode)?;
1386
1387 json.get("fastestFee")
1388 .and_then(|fee| fee.as_u64())
1389 .ok_or(FeeErr::MissingField)
1390 }
1391 },
1392 should_retry,
1393 )
1394 .await
1395 .map_err(|e| eyre::eyre!(e))
1396 .wrap_err_with(|| format!("Failed to fetch/parse fees from {url}"))?;
1397
1398 let fee_rate = Amount::from_sat(fee_sat_per_vb * 1000);
1400
1401 Ok(fee_rate)
1402}
1403
1404#[async_trait]
1405impl RpcApi for ExtendedBitcoinRpc {
1408 async fn call<T: for<'a> serde::de::Deserialize<'a>>(
1409 &self,
1410 cmd: &str,
1411 args: &[serde_json::Value],
1412 ) -> std::result::Result<T, bitcoincore_rpc::Error> {
1413 tracing::trace!("Calling Bitcoin RPC command: {}", cmd);
1414 let strategy = self.retry_config.get_strategy();
1415
1416 let condition = |error: &bitcoincore_rpc::Error| error.is_retryable();
1417
1418 RetryIf::spawn(
1419 strategy,
1420 || async { self.client.call(cmd, args).await },
1421 condition,
1422 )
1423 .await
1424 }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429 use std::collections::HashMap;
1430
1431 use crate::actor::Actor;
1432 use crate::config::protocol::{ProtocolParamset, REGTEST_PARAMSET};
1433 use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
1434 use crate::test::common::{citrea, create_test_config_with_thread_name};
1435 use crate::{
1436 bitvm_client::SECP, extended_bitcoin_rpc::BitcoinRPCError, test::common::create_regtest_rpc,
1437 };
1438 use bitcoin::Amount;
1439 use bitcoin::{amount, key::Keypair, Address, FeeRate, XOnlyPublicKey};
1440 use bitcoincore_rpc::RpcApi;
1441 use citrea_e2e::bitcoin::DEFAULT_FINALITY_DEPTH;
1442 use citrea_e2e::config::{BitcoinConfig, TestCaseDockerConfig};
1443 use citrea_e2e::node::NodeKind;
1444 use citrea_e2e::test_case::TestCaseRunner;
1445 use citrea_e2e::Result;
1446 use citrea_e2e::{config::TestCaseConfig, framework::TestFramework, test_case::TestCase};
1447 use tonic::async_trait;
1448
1449 #[tokio::test]
1450 async fn new_extended_rpc_with_clone() {
1451 let mut config = create_test_config_with_thread_name().await;
1452 let regtest = create_regtest_rpc(&mut config).await;
1453 let rpc = regtest.rpc();
1454
1455 rpc.mine_blocks(101).await.unwrap();
1456 let height = rpc.get_block_count().await.unwrap();
1457 let hash = rpc.get_block_hash(height).await.unwrap();
1458
1459 let cloned_rpc = rpc.clone_inner().await.unwrap();
1460 assert_eq!(cloned_rpc.url, rpc.url);
1461 assert_eq!(cloned_rpc.get_block_count().await.unwrap(), height);
1462 assert_eq!(cloned_rpc.get_block_hash(height).await.unwrap(), hash);
1463 }
1464
1465 #[tokio::test]
1466 async fn tx_checks_in_mempool_and_on_chain() {
1467 let mut config = create_test_config_with_thread_name().await;
1468 let regtest = create_regtest_rpc(&mut config).await;
1469 let rpc = regtest.rpc();
1470
1471 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1472 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1473 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1474
1475 let amount = amount::Amount::from_sat(10000);
1476
1477 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1479 let tx = rpc.get_tx_of_txid(&utxo.txid).await.unwrap();
1480 let txid = tx.compute_txid();
1481 tracing::debug!("TXID: {}", txid);
1482
1483 assert_eq!(tx.output[utxo.vout as usize].value, amount);
1484 assert_eq!(utxo.txid, txid);
1485 assert!(rpc
1486 .check_utxo_address_and_amount(&utxo, &address.script_pubkey(), amount)
1487 .await
1488 .unwrap());
1489
1490 assert!(rpc.confirmation_blocks(&utxo.txid).await.is_err());
1492 assert!(rpc.get_blockhash_of_tx(&utxo.txid).await.is_err());
1493 assert!(!rpc.is_tx_on_chain(&txid).await.unwrap());
1494 assert!(rpc.is_utxo_spent(&utxo).await.is_err());
1495
1496 rpc.mine_blocks(1).await.unwrap();
1497 let height = rpc.get_block_count().await.unwrap();
1498 assert_eq!(height as u32, rpc.get_current_chain_height().await.unwrap());
1499 let blockhash = rpc.get_block_hash(height).await.unwrap();
1500
1501 assert_eq!(rpc.confirmation_blocks(&utxo.txid).await.unwrap(), 1);
1503 assert_eq!(
1504 rpc.get_blockhash_of_tx(&utxo.txid).await.unwrap(),
1505 blockhash
1506 );
1507 assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
1508 assert!(rpc.is_tx_on_chain(&txid).await.unwrap());
1509 assert!(!rpc.is_utxo_spent(&utxo).await.unwrap());
1510
1511 let txout = rpc.get_txout_from_outpoint(&utxo).await.unwrap();
1513 assert_eq!(txout.value, amount);
1514 assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
1515
1516 let height = rpc.get_current_chain_height().await.unwrap();
1517 let (hash, header) = rpc.get_block_info_by_height(height.into()).await.unwrap();
1518 assert_eq!(blockhash, hash);
1519 assert_eq!(rpc.get_block_header(&hash).await.unwrap(), header);
1520 }
1521
1522 #[tokio::test]
1523 async fn bump_fee_with_fee_rate() {
1524 let mut config = create_test_config_with_thread_name().await;
1525 let regtest = create_regtest_rpc(&mut config).await;
1526 let rpc = regtest.rpc();
1527
1528 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1529 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1530 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1531
1532 let amount = amount::Amount::from_sat(10000);
1533
1534 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1536 rpc.mine_blocks(1).await.unwrap();
1537 assert!(rpc
1538 .bump_fee_with_fee_rate(utxo.txid, FeeRate::from_sat_per_vb(1).unwrap())
1539 .await
1540 .inspect_err(|e| {
1541 match e {
1542 BitcoinRPCError::TransactionAlreadyInBlock(_) => {}
1543 _ => panic!("Unexpected error: {e:?}"),
1544 }
1545 })
1546 .is_err());
1547
1548 let current_fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1549
1550 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1553 let txid = rpc
1554 .bump_fee_with_fee_rate(utxo.txid, current_fee_rate)
1555 .await
1556 .unwrap();
1557 assert_eq!(txid, utxo.txid);
1558
1559 let new_fee_rate = FeeRate::from_sat_per_vb_unchecked(10000);
1561 let txid = rpc
1562 .bump_fee_with_fee_rate(utxo.txid, new_fee_rate)
1563 .await
1564 .unwrap();
1565 assert_ne!(txid, utxo.txid);
1566 }
1567
1568 struct ReorgChecks;
1569 #[async_trait]
1570 impl TestCase for ReorgChecks {
1571 fn bitcoin_config() -> BitcoinConfig {
1572 BitcoinConfig {
1573 extra_args: vec![
1574 "-txindex=1",
1575 "-fallbackfee=0.000001",
1576 "-rpcallowip=0.0.0.0/0",
1577 "-dustrelayfee=0",
1578 ],
1579 ..Default::default()
1580 }
1581 }
1582
1583 fn test_config() -> TestCaseConfig {
1584 TestCaseConfig {
1585 with_sequencer: true,
1586 with_batch_prover: false,
1587 n_nodes: HashMap::from([(NodeKind::Bitcoin, 2)]),
1588 docker: TestCaseDockerConfig {
1589 bitcoin: true,
1590 citrea: true,
1591 },
1592 ..Default::default()
1593 }
1594 }
1595
1596 async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
1597 let (da0, da1) = (
1598 f.bitcoin_nodes.get(0).unwrap(),
1599 f.bitcoin_nodes.get(1).unwrap(),
1600 );
1601
1602 let mut config = create_test_config_with_thread_name().await;
1603 const PARAMSET: ProtocolParamset = ProtocolParamset {
1604 finality_depth: DEFAULT_FINALITY_DEPTH as u32,
1605 ..REGTEST_PARAMSET
1606 };
1607 config.protocol_paramset = &PARAMSET;
1608 citrea::update_config_with_citrea_e2e_values(
1609 &mut config,
1610 da0,
1611 f.sequencer.as_ref().expect("Sequencer is present"),
1612 None,
1613 );
1614
1615 let rpc = ExtendedBitcoinRpc::connect(
1616 config.bitcoin_rpc_url.clone(),
1617 config.bitcoin_rpc_user.clone(),
1618 config.bitcoin_rpc_password.clone(),
1619 None,
1620 )
1621 .await
1622 .unwrap();
1623
1624 f.bitcoin_nodes.disconnect_nodes().await?;
1626
1627 let before_reorg_tip_height = rpc.get_block_count().await?;
1628 let before_reorg_tip_hash = rpc.get_block_hash(before_reorg_tip_height).await?;
1629
1630 let address = Actor::new(config.secret_key, config.protocol_paramset.network).address;
1631 let tx = rpc
1632 .send_to_address(&address, Amount::from_sat(10000))
1633 .await?;
1634
1635 assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
1636 rpc.mine_blocks(1).await?;
1637 assert!(rpc.is_tx_on_chain(&tx.txid).await?);
1638
1639 let reorg_depth = 4;
1641 da1.generate(reorg_depth).await.unwrap();
1642 f.bitcoin_nodes.connect_nodes().await?;
1643 f.bitcoin_nodes.wait_for_sync(None).await?;
1644
1645 let current_tip_height = rpc.get_block_count().await?;
1647 assert_eq!(
1648 before_reorg_tip_height + reorg_depth,
1649 current_tip_height,
1650 "Re-org did not occur"
1651 );
1652 let current_tip_hash = rpc.get_block_hash(current_tip_height).await?;
1653 assert_ne!(
1654 before_reorg_tip_hash, current_tip_hash,
1655 "Re-org did not occur"
1656 );
1657
1658 assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
1659
1660 Ok(())
1661 }
1662 }
1663
1664 #[tokio::test]
1665 async fn reorg_checks() -> Result<()> {
1666 TestCaseRunner::new(ReorgChecks).run().await
1667 }
1668
1669 mod retry_config_tests {
1670 use crate::extended_bitcoin_rpc::RetryConfig;
1671
1672 use std::time::Duration;
1673
1674 #[test]
1675 fn test_retry_config_default() {
1676 let config = RetryConfig::default();
1677 assert_eq!(config.initial_delay_millis, 100);
1678 assert_eq!(config.max_delay, Duration::from_secs(30));
1679 assert_eq!(config.max_attempts, 5);
1680 assert_eq!(config.backoff_multiplier, 2);
1681 assert!(!config.is_jitter);
1682 }
1683
1684 #[test]
1685 fn test_retry_config_custom() {
1686 let initial = 200;
1687 let max = Duration::from_secs(10);
1688 let attempts = 7;
1689 let backoff_multiplier = 3;
1690 let jitter = true;
1691 let config = RetryConfig::new(initial, max, attempts, backoff_multiplier, jitter);
1692 assert_eq!(config.initial_delay_millis, initial);
1693 assert_eq!(config.max_delay, max);
1694 assert_eq!(config.max_attempts, attempts);
1695 assert_eq!(config.backoff_multiplier, backoff_multiplier);
1696 assert!(config.is_jitter);
1697 }
1698
1699 #[test]
1700 fn test_retry_strategy_initial_delay() {
1701 let initial_delay_millis = 100;
1704 let backoff_multiplier = 2;
1705 let config = RetryConfig::new(
1706 initial_delay_millis,
1707 Duration::from_secs(30),
1708 5,
1709 backoff_multiplier,
1710 false, );
1712
1713 let mut strategy = config.get_strategy();
1714 let first_delay = strategy.next().expect("Should have first delay");
1715
1716 assert_eq!(
1720 first_delay,
1721 Duration::from_millis(initial_delay_millis),
1722 "First delay should match initial_delay_millis"
1723 );
1724
1725 let second_delay = strategy.next().expect("Should have second delay");
1727 assert_eq!(
1728 second_delay,
1729 Duration::from_millis(initial_delay_millis * backoff_multiplier),
1730 "Second delay should be initial_delay_millis * backoff_multiplier"
1731 );
1732 }
1733 }
1734
1735 mod retryable_error_tests {
1736 use bitcoin::{hashes::Hash, BlockHash, Txid};
1737
1738 use crate::extended_bitcoin_rpc::RetryableError;
1739
1740 use super::*;
1741 use std::io::{Error as IoError, ErrorKind};
1742
1743 #[test]
1744 fn test_bitcoin_rpc_error_retryable_io_errors() {
1745 let retryable_kinds = [
1746 ErrorKind::ConnectionRefused,
1747 ErrorKind::ConnectionReset,
1748 ErrorKind::ConnectionAborted,
1749 ErrorKind::NotConnected,
1750 ErrorKind::BrokenPipe,
1751 ErrorKind::TimedOut,
1752 ErrorKind::Interrupted,
1753 ErrorKind::UnexpectedEof,
1754 ];
1755
1756 for kind in retryable_kinds {
1757 let io_error = IoError::new(kind, "test error");
1758 let rpc_error = bitcoincore_rpc::Error::Io(io_error);
1759 assert!(
1760 rpc_error.is_retryable(),
1761 "ErrorKind::{kind:?} should be retryable"
1762 );
1763 }
1764 }
1765
1766 #[test]
1767 fn test_bitcoin_rpc_error_non_retryable_io_errors() {
1768 let non_retryable_kinds = [
1769 ErrorKind::PermissionDenied,
1770 ErrorKind::NotFound,
1771 ErrorKind::InvalidInput,
1772 ErrorKind::InvalidData,
1773 ];
1774
1775 for kind in non_retryable_kinds {
1776 let io_error = IoError::new(kind, "test error");
1777 let rpc_error = bitcoincore_rpc::Error::Io(io_error);
1778 assert!(
1779 !rpc_error.is_retryable(),
1780 "ErrorKind::{kind:?} should not be retryable"
1781 );
1782 }
1783 }
1784
1785 #[test]
1786 fn test_bitcoin_rpc_error_auth_not_retryable() {
1787 let auth_error = bitcoincore_rpc::Error::Auth("Invalid credentials".to_string());
1788 assert!(!auth_error.is_retryable());
1789 }
1790
1791 #[test]
1792 fn test_bitcoin_rpc_error_url_parse_not_retryable() {
1793 let url_error = url::ParseError::EmptyHost;
1794 let rpc_error = bitcoincore_rpc::Error::UrlParse(url_error);
1795 assert!(!rpc_error.is_retryable());
1796 }
1797
1798 #[test]
1799 fn test_bitcoin_rpc_error_invalid_cookie_not_retryable() {
1800 let rpc_error = bitcoincore_rpc::Error::InvalidCookieFile;
1801 assert!(!rpc_error.is_retryable());
1802 }
1803
1804 #[test]
1805 fn test_bitcoin_rpc_error_returned_error_non_retryable_patterns() {
1806 let non_retryable_messages = [
1807 "insufficient funds",
1808 "transaction already in blockchain",
1809 "invalid transaction",
1810 "not found in mempool",
1811 "transaction conflict",
1812 ];
1813
1814 for msg in non_retryable_messages {
1815 let rpc_error = bitcoincore_rpc::Error::ReturnedError(msg.to_string());
1816 assert!(
1817 !rpc_error.is_retryable(),
1818 "Message '{msg}' should not be retryable"
1819 );
1820 }
1821 }
1822
1823 #[test]
1824 fn test_bitcoin_rpc_error_unexpected_structure_retryable() {
1825 let rpc_error = bitcoincore_rpc::Error::UnexpectedStructure;
1826 assert!(rpc_error.is_retryable());
1827 }
1828
1829 #[test]
1830 fn test_bitcoin_rpc_error_serialization_errors_not_retryable() {
1831 use bitcoin::consensus::encode::Error as EncodeError;
1832
1833 let serialization_errors = [
1834 bitcoincore_rpc::Error::BitcoinSerialization(EncodeError::Io(
1835 IoError::other("test").into(),
1836 )),
1837 bitcoincore_rpc::Error::Json(serde_json::Error::io(IoError::other("test"))),
1839 ];
1840
1841 for error in serialization_errors {
1842 assert!(
1843 !error.is_retryable(),
1844 "Serialization error should not be retryable"
1845 );
1846 }
1847 }
1848
1849 #[test]
1850 fn test_bridge_rpc_error_retryable() {
1851 assert!(
1853 !BitcoinRPCError::TransactionAlreadyInBlock(BlockHash::all_zeros()).is_retryable()
1854 );
1855 assert!(!BitcoinRPCError::BumpFeeUTXOSpent(Default::default()).is_retryable());
1856
1857 let txid = Txid::all_zeros();
1859 let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1860 assert!(BitcoinRPCError::BumpFeeError(txid, fee_rate).is_retryable());
1861
1862 let retryable_other = BitcoinRPCError::Other(eyre::eyre!("timeout occurred"));
1864 assert!(retryable_other.is_retryable());
1865
1866 let non_retryable_other = BitcoinRPCError::Other(eyre::eyre!("permission denied"));
1867 assert!(!non_retryable_other.is_retryable());
1868 }
1869 }
1870
1871 mod rpc_call_retry_tests {
1872
1873 use crate::extended_bitcoin_rpc::RetryableError;
1874
1875 use super::*;
1876 use secrecy::SecretString;
1877
1878 #[tokio::test]
1879 async fn test_rpc_call_retry_with_invalid_credentials() {
1880 let mut config = create_test_config_with_thread_name().await;
1881 let regtest = create_regtest_rpc(&mut config).await;
1882
1883 let working_rpc = regtest.rpc();
1885 let url = working_rpc.url.clone();
1886
1887 let invalid_user = SecretString::new("invalid_user".to_string().into());
1889 let invalid_password = SecretString::new("invalid_password".to_string().into());
1890
1891 let res = ExtendedBitcoinRpc::connect(url, invalid_user, invalid_password, None).await;
1892
1893 assert!(res.is_err());
1894 assert!(!res.unwrap_err().is_retryable());
1895 }
1896
1897 #[tokio::test]
1898 async fn test_rpc_call_retry_with_invalid_host() {
1899 let user = SecretString::new("user".to_string().into());
1900 let password = SecretString::new("password".to_string().into());
1901 let invalid_url = "http://nonexistent-host:8332".to_string();
1902
1903 let res = ExtendedBitcoinRpc::connect(invalid_url, user, password, None).await;
1904
1905 assert!(res.is_err());
1906 assert!(!res.unwrap_err().is_retryable());
1907 }
1908 }
1909
1910 mod convenience_method_tests {
1911 use super::*;
1912
1913 #[tokio::test]
1914 async fn test_get_block_hash_with_retry() {
1915 let mut config = create_test_config_with_thread_name().await;
1916 let regtest = create_regtest_rpc(&mut config).await;
1917 let rpc = regtest.rpc();
1918
1919 rpc.mine_blocks(1).await.unwrap();
1921 let height = rpc.get_block_count().await.unwrap();
1922
1923 let result = rpc.get_block_hash(height).await;
1924 assert!(result.is_ok());
1925
1926 let expected_hash = rpc.get_block_hash(height).await.unwrap();
1927 assert_eq!(result.unwrap(), expected_hash);
1928 }
1929
1930 #[tokio::test]
1931 async fn test_get_tx_out_with_retry() {
1932 let mut config = create_test_config_with_thread_name().await;
1933 let regtest = create_regtest_rpc(&mut config).await;
1934 let rpc = regtest.rpc();
1935
1936 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1938 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1939 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1940 let amount = Amount::from_sat(10000);
1941
1942 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1943
1944 let result = rpc.get_tx_of_txid(&utxo.txid).await;
1945 assert!(result.is_ok());
1946
1947 let tx = result.unwrap();
1948 assert_eq!(tx.compute_txid(), utxo.txid);
1949 }
1950
1951 #[tokio::test]
1952 async fn test_send_to_address_with_retry() {
1953 let mut config = create_test_config_with_thread_name().await;
1954 let regtest = create_regtest_rpc(&mut config).await;
1955 let rpc = regtest.rpc();
1956
1957 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1958 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1959 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1960 let amount = Amount::from_sat(10000);
1961
1962 let result = rpc.send_to_address(&address, amount).await;
1963 assert!(result.is_ok());
1964
1965 let outpoint = result.unwrap();
1966
1967 let tx = rpc.get_tx_of_txid(&outpoint.txid).await.unwrap();
1969 assert_eq!(tx.output[outpoint.vout as usize].value, amount);
1970 }
1971
1972 #[tokio::test]
1973 async fn test_bump_fee_with_retry() {
1974 let mut config = create_test_config_with_thread_name().await;
1975 let regtest = create_regtest_rpc(&mut config).await;
1976 let rpc = regtest.rpc();
1977
1978 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1979 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1980 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1981 let amount = Amount::from_sat(10000);
1982
1983 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1985 let new_fee_rate = FeeRate::from_sat_per_vb_unchecked(10000);
1986
1987 let result = rpc.bump_fee_with_fee_rate(utxo.txid, new_fee_rate).await;
1988 assert!(result.is_ok());
1989
1990 let new_txid = result.unwrap();
1991 assert_ne!(new_txid, utxo.txid);
1993 }
1994 }
1995}