1pub use clementine_extended_rpc::{
15 get_fee_rate_from_mempool_space, BitcoinRPCError, ExtendedBitcoinRpc, RetryConfig,
16 RetryableError,
17};
18
19use async_trait::async_trait;
20#[cfg(test)]
21use bitcoin::BlockHash;
22use bitcoin::OutPoint;
23use bitcoincore_rpc::RpcApi;
24use eyre::eyre;
25use eyre::Context;
26
27use crate::builder::address::create_taproot_address;
28use crate::builder::transaction::create_round_txhandlers;
29use crate::builder::transaction::input::UtxoVout;
30use crate::builder::transaction::KickoffWinternitzKeys;
31use crate::builder::transaction::TxHandler;
32use crate::config::protocol::ProtocolParamset;
33use crate::deposit::OperatorData;
34use clementine_errors::BridgeError;
35use clementine_errors::TransactionType;
36use clementine_primitives::RoundIndex;
37
38#[cfg(test)]
39use crate::test::common::citrea::CitreaE2EData;
40#[cfg(test)]
41use crate::{
42 citrea::CitreaClientT,
43 test::common::{are_all_nodes_synced, test_actors::TestActors},
44};
45#[cfg(test)]
46type Result<T> = std::result::Result<T, BitcoinRPCError>;
47
48#[cfg(test)]
49pub const MINE_BLOCK_COUNT: u64 = 1;
50
51#[async_trait]
56pub trait BridgeRpcQueries {
57 async fn collateral_check(
79 &self,
80 operator_data: &OperatorData,
81 kickoff_wpks: &KickoffWinternitzKeys,
82 paramset: &'static ProtocolParamset,
83 ) -> std::result::Result<bool, BridgeError>;
84}
85
86#[async_trait]
87impl BridgeRpcQueries for ExtendedBitcoinRpc {
88 async fn collateral_check(
89 &self,
90 operator_data: &OperatorData,
91 kickoff_wpks: &KickoffWinternitzKeys,
92 paramset: &'static ProtocolParamset,
93 ) -> std::result::Result<bool, BridgeError> {
94 let tx = self
96 .get_tx_of_txid(&operator_data.collateral_funding_outpoint.txid)
97 .await
98 .wrap_err(format!(
99 "Failed to find collateral utxo in chain for outpoint {:?}",
100 operator_data.collateral_funding_outpoint
101 ))?;
102 let collateral_outpoint = match tx
103 .output
104 .get(operator_data.collateral_funding_outpoint.vout as usize)
105 {
106 Some(output) => output,
107 None => {
108 tracing::warn!(
109 "No output at index {} for txid {} while checking for collateral existence",
110 operator_data.collateral_funding_outpoint.vout,
111 operator_data.collateral_funding_outpoint.txid
112 );
113 return Ok(false);
114 }
115 };
116
117 if collateral_outpoint.value != paramset.collateral_funding_amount {
118 tracing::error!(
119 "Collateral amount for collateral {:?} is not correct: expected {}, got {}",
120 operator_data.collateral_funding_outpoint,
121 paramset.collateral_funding_amount,
122 collateral_outpoint.value
123 );
124 return Ok(false);
125 }
126
127 let operator_tpr_address =
128 create_taproot_address(&[], Some(operator_data.xonly_pk), paramset.network).0;
129
130 if collateral_outpoint.script_pubkey != operator_tpr_address.script_pubkey() {
131 tracing::error!(
132 "Collateral script pubkey for collateral {:?} is not correct: expected {}, got {}",
133 operator_data.collateral_funding_outpoint,
134 operator_tpr_address.script_pubkey(),
135 collateral_outpoint.script_pubkey
136 );
137 return Ok(false);
138 }
139
140 let is_on_chain = self
145 .is_tx_on_chain(&operator_data.collateral_funding_outpoint.txid)
146 .await?;
147 if !is_on_chain {
148 return match paramset.network {
149 bitcoin::Network::Bitcoin => Ok(false),
150 _ => Ok(true),
151 };
152 }
153
154 let mut current_collateral_outpoint: OutPoint = operator_data.collateral_funding_outpoint;
155 let mut prev_ready_to_reimburse: Option<TxHandler> = None;
156 for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
158 let txhandlers = create_round_txhandlers(
160 paramset,
161 round_idx,
162 operator_data,
163 kickoff_wpks,
164 prev_ready_to_reimburse.as_ref(),
165 )?;
166
167 let mut round_txhandler_opt = None;
168 let mut ready_to_reimburse_txhandler_opt = None;
169 for txhandler in &txhandlers {
170 match txhandler.get_transaction_type() {
171 TransactionType::Round => round_txhandler_opt = Some(txhandler),
172 TransactionType::ReadyToReimburse => {
173 ready_to_reimburse_txhandler_opt = Some(txhandler)
174 }
175 _ => {}
176 }
177 }
178 if round_txhandler_opt.is_none() || ready_to_reimburse_txhandler_opt.is_none() {
179 return Err(eyre!(
180 "Failed to create round and ready to reimburse txs for round {:?} for operator {}",
181 round_idx,
182 operator_data.xonly_pk
183 ).into());
184 }
185
186 let round_txid = round_txhandler_opt
187 .expect("Round txhandler should exist, checked above")
188 .get_cached_tx()
189 .compute_txid();
190 let is_round_tx_on_chain = self.is_tx_on_chain(&round_txid).await?;
191 if !is_round_tx_on_chain {
192 break;
193 }
194 let block_hash = self.get_blockhash_of_tx(&round_txid).await?;
195 let block_height = self
196 .get_block_info(&block_hash)
197 .await
198 .wrap_err(format!(
199 "Failed to get block info for block hash {block_hash}"
200 ))?
201 .height;
202 if block_height < paramset.start_height as usize {
203 tracing::warn!(
204 "Collateral utxo of operator {operator_data:?} is spent in a block before paramset start height: {block_height} < {0}",
205 paramset.start_height
206 );
207 return Ok(false);
208 }
209 current_collateral_outpoint = OutPoint {
210 txid: round_txid,
211 vout: UtxoVout::CollateralInRound.get_vout(),
212 };
213 if round_idx == RoundIndex::Round(paramset.num_round_txs - 1) {
214 break;
217 }
218 let ready_to_reimburse_txhandler = ready_to_reimburse_txhandler_opt
219 .expect("Ready to reimburse txhandler should exist");
220 let ready_to_reimburse_txid =
221 ready_to_reimburse_txhandler.get_cached_tx().compute_txid();
222 let is_ready_to_reimburse_tx_on_chain =
223 self.is_tx_on_chain(&ready_to_reimburse_txid).await?;
224 if !is_ready_to_reimburse_tx_on_chain {
225 break;
226 }
227
228 current_collateral_outpoint = OutPoint {
229 txid: ready_to_reimburse_txid,
230 vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
231 };
232
233 prev_ready_to_reimburse = Some(ready_to_reimburse_txhandler.clone());
234 }
235
236 Ok(!self.is_utxo_spent(¤t_collateral_outpoint).await?)
240 }
241}
242
243#[cfg(test)]
245#[async_trait]
246pub trait TestRpcExtensions {
247 async fn mine_blocks_while_synced<C: CitreaClientT>(
249 &self,
250 block_num: u64,
251 actors: &TestActors<C>,
252 e2e: Option<&CitreaE2EData<'_>>,
253 ) -> Result<Vec<BlockHash>>;
254}
255
256#[cfg(test)]
257#[async_trait]
258impl TestRpcExtensions for ExtendedBitcoinRpc {
259 async fn mine_blocks_while_synced<C: CitreaClientT>(
260 &self,
261 block_num: u64,
262 actors: &TestActors<C>,
263 e2e: Option<&CitreaE2EData<'_>>,
264 ) -> Result<Vec<BlockHash>> {
265 match e2e {
266 Some(e2e) if e2e.bitcoin_nodes.iter().count() > 1 => {
267 use bitcoin::secp256k1::rand::{thread_rng, Rng};
268 e2e.bitcoin_nodes
269 .disconnect_nodes()
270 .await
271 .map_err(|e| eyre::eyre!("Failed to disconnect nodes: {}", e))?;
272 let reorg_blocks =
273 thread_rng().gen_range(0..e2e.config.protocol_paramset().finality_depth as u64);
274 let da0 = e2e.bitcoin_nodes.get(0).expect("node 0 should exist");
275 let da1 = e2e.bitcoin_nodes.get(1).expect("node 1 should exist");
276
277 let mut mined_blocks = Vec::new();
278 while mined_blocks.len() < reorg_blocks as usize {
279 if !are_all_nodes_synced(self, actors).await? {
280 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
281 continue;
282 }
283 let num_mine_blocks = std::cmp::min(
284 MINE_BLOCK_COUNT,
285 reorg_blocks.saturating_sub(mined_blocks.len() as u64),
286 );
287 da0.generate(num_mine_blocks)
288 .await
289 .wrap_err("Failed to generate blocks")?;
290 let new_blocks = da1
291 .generate(num_mine_blocks)
292 .await
293 .wrap_err("Failed to generate blocks")?;
294 mined_blocks.extend(new_blocks);
295 }
296 mined_blocks.extend(
297 da1.generate(1)
298 .await
299 .wrap_err("Failed to generate blocks")?,
300 );
301 e2e.bitcoin_nodes
302 .connect_nodes()
303 .await
304 .map_err(|e| eyre::eyre!("Failed to connect nodes: {}", e))?;
305 e2e.bitcoin_nodes
306 .wait_for_sync(None)
307 .await
308 .map_err(|e| eyre::eyre!("Failed to wait for sync: {}", e))?;
309 while mined_blocks.len() != (reorg_blocks + block_num + 1) as usize {
310 if !are_all_nodes_synced(self, actors).await? {
311 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
312 continue;
313 }
314 let num_mine_blocks = std::cmp::min(
315 MINE_BLOCK_COUNT,
316 (reorg_blocks + block_num + 1).saturating_sub(mined_blocks.len() as u64),
317 );
318 mined_blocks.extend(self.mine_blocks(num_mine_blocks).await?);
319 }
320 Ok(mined_blocks)
321 }
322 _ => {
323 let mut mined_blocks = Vec::new();
324 while mined_blocks.len() < block_num as usize {
325 if !are_all_nodes_synced(self, actors).await? {
326 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
327 continue;
328 }
329 let num_mine_blocks = std::cmp::min(
330 MINE_BLOCK_COUNT,
331 block_num.saturating_sub(mined_blocks.len() as u64),
332 );
333 let new_blocks = self.mine_blocks(num_mine_blocks).await?;
334 mined_blocks.extend(new_blocks);
335 }
336 Ok(mined_blocks)
337 }
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use std::collections::HashMap;
345
346 use crate::actor::Actor;
347 use crate::config::protocol::{ProtocolParamset, REGTEST_PARAMSET};
348 use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
349 use crate::test::common::{
350 create_test_config_with_thread_name, run_citrea_e2e_with_docker_port_retry,
351 };
352 use crate::{
353 bitvm_client::SECP, extended_bitcoin_rpc::BitcoinRPCError, test::common::create_regtest_rpc,
354 };
355 use bitcoin::Amount;
356 use bitcoin::{amount, key::Keypair, Address, XOnlyPublicKey};
357 use bitcoincore_rpc::RpcApi;
358 use citrea_e2e::bitcoin::DEFAULT_FINALITY_DEPTH;
359 use citrea_e2e::config::{BitcoinConfig, TestCaseDockerConfig};
360 use citrea_e2e::node::NodeKind;
361 use citrea_e2e::test_case::TestCaseRunner;
362 use citrea_e2e::Result;
363 use citrea_e2e::{config::TestCaseConfig, framework::TestFramework, test_case::TestCase};
364 use clementine_primitives::FeeRateKvb;
365 use tonic::async_trait;
366
367 #[tokio::test]
368 async fn new_extended_rpc_with_clone() {
369 let mut config = create_test_config_with_thread_name().await;
370 let regtest = create_regtest_rpc(&mut config).await;
371 let rpc = regtest.rpc();
372
373 rpc.mine_blocks(101).await.unwrap();
374 let height = rpc.get_block_count().await.unwrap();
375 let hash = rpc.get_block_hash(height).await.unwrap();
376
377 let cloned_rpc = rpc.clone_inner().await.unwrap();
378 assert_eq!(cloned_rpc.get_block_count().await.unwrap(), height);
379 assert_eq!(cloned_rpc.get_block_hash(height).await.unwrap(), hash);
380 }
381
382 #[tokio::test]
383 async fn test_rpc_call_retry_with_invalid_credentials() {
384 use crate::extended_bitcoin_rpc::RetryableError;
385 use secrecy::SecretString;
386
387 let mut config = create_test_config_with_thread_name().await;
388 let regtest = create_regtest_rpc(&mut config).await;
389
390 let working_rpc = regtest.rpc();
392 let url = working_rpc.url().to_string();
393
394 let invalid_user = SecretString::new("invalid_user".to_string().into());
396 let invalid_password = SecretString::new("invalid_password".to_string().into());
397
398 let res = ExtendedBitcoinRpc::connect(url, invalid_user, invalid_password, None).await;
399
400 assert!(res.is_err());
401 assert!(!res.unwrap_err().is_retryable());
402 }
403
404 #[tokio::test]
405 async fn tx_checks_in_mempool_and_on_chain() {
406 let mut config = create_test_config_with_thread_name().await;
407 let regtest = create_regtest_rpc(&mut config).await;
408 let rpc = regtest.rpc();
409
410 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
411 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
412 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
413
414 let amount = amount::Amount::from_sat(10000);
415
416 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
418 let tx = rpc.get_tx_of_txid(&utxo.txid).await.unwrap();
419 let txid = tx.compute_txid();
420 tracing::debug!("TXID: {}", txid);
421
422 assert_eq!(tx.output[utxo.vout as usize].value, amount);
423 assert_eq!(utxo.txid, txid);
424 assert!(rpc
425 .check_utxo_address_and_amount(&utxo, &address.script_pubkey(), amount)
426 .await
427 .unwrap());
428
429 assert!(rpc.confirmation_blocks(&utxo.txid).await.is_err());
431 assert!(rpc.get_blockhash_of_tx(&utxo.txid).await.is_err());
432 assert!(!rpc.is_tx_on_chain(&txid).await.unwrap());
433 assert!(rpc.is_utxo_spent(&utxo).await.is_err());
434
435 rpc.mine_blocks(1).await.unwrap();
436 let height = rpc.get_block_count().await.unwrap();
437 assert_eq!(height as u32, rpc.get_current_chain_height().await.unwrap());
438 let blockhash = rpc.get_block_hash(height).await.unwrap();
439
440 assert_eq!(rpc.confirmation_blocks(&utxo.txid).await.unwrap(), 1);
442 assert_eq!(
443 rpc.get_blockhash_of_tx(&utxo.txid).await.unwrap(),
444 blockhash
445 );
446 assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
447 assert!(rpc.is_tx_on_chain(&txid).await.unwrap());
448 assert!(!rpc.is_utxo_spent(&utxo).await.unwrap());
449
450 let txout = rpc.get_txout_from_outpoint(&utxo).await.unwrap();
452 assert_eq!(txout.value, amount);
453 assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
454
455 let height = rpc.get_current_chain_height().await.unwrap();
456 let (hash, header) = rpc.get_block_info_by_height(height.into()).await.unwrap();
457 assert_eq!(blockhash, hash);
458 assert_eq!(rpc.get_block_header(&hash).await.unwrap(), header);
459 }
460
461 #[tokio::test]
462 async fn bump_fee_with_fee_rate() {
463 let mut config = create_test_config_with_thread_name().await;
464 let regtest = create_regtest_rpc(&mut config).await;
465 let rpc = regtest.rpc();
466
467 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
468 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
469 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
470
471 let amount = amount::Amount::from_sat(10000);
472
473 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
475 rpc.mine_blocks(1).await.unwrap();
476 assert!(rpc
477 .bump_fee_with_fee_rate(utxo.txid, FeeRateKvb::from_sat_per_vb(1).unwrap())
478 .await
479 .inspect_err(|e| {
480 match e {
481 BitcoinRPCError::TransactionAlreadyInBlock(_) => {}
482 _ => panic!("Unexpected error: {e:?}"),
483 }
484 })
485 .is_err());
486
487 let current_fee_rate = FeeRateKvb::from_sat_per_vb_unchecked(1);
488
489 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
492 let txid = rpc
493 .bump_fee_with_fee_rate(utxo.txid, current_fee_rate)
494 .await
495 .unwrap();
496 assert_eq!(txid, utxo.txid);
497
498 let new_fee_rate = FeeRateKvb::from_sat_per_vb_unchecked(10000);
500 let txid = rpc
501 .bump_fee_with_fee_rate(utxo.txid, new_fee_rate)
502 .await
503 .unwrap();
504 assert_ne!(txid, utxo.txid);
505 }
506
507 struct ReorgChecks;
508 #[async_trait]
509 impl TestCase for ReorgChecks {
510 fn bitcoin_config() -> BitcoinConfig {
511 BitcoinConfig {
512 extra_args: vec![
513 "-txindex=1",
514 "-fallbackfee=0.000001",
515 "-rpcallowip=0.0.0.0/0",
516 "-dustrelayfee=0",
517 ],
518 ..Default::default()
519 }
520 }
521
522 fn test_config() -> TestCaseConfig {
523 TestCaseConfig {
524 with_sequencer: false,
525 with_batch_prover: false,
526 n_nodes: HashMap::from([(NodeKind::Bitcoin, 2)]),
527 docker: TestCaseDockerConfig {
528 bitcoin: true,
529 citrea: false,
530 },
531 ..Default::default()
532 }
533 }
534
535 async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
536 let (da0, da1) = (
537 f.bitcoin_nodes.get(0).unwrap(),
538 f.bitcoin_nodes.get(1).unwrap(),
539 );
540
541 let mut config = create_test_config_with_thread_name().await;
542 const PARAMSET: ProtocolParamset = ProtocolParamset {
543 finality_depth: DEFAULT_FINALITY_DEPTH as u32,
544 ..REGTEST_PARAMSET
545 };
546 config.protocol_paramset = &PARAMSET;
547 config.bitcoin_rpc_user = da0.config.rpc_user.clone().into();
548 config.bitcoin_rpc_password = da0.config.rpc_password.clone().into();
549 config.bitcoin_rpc_url = format!(
550 "http://127.0.0.1:{}/wallet/{}",
551 da0.config.rpc_port,
552 NodeKind::Bitcoin
553 );
554
555 let rpc = ExtendedBitcoinRpc::connect(
556 config.bitcoin_rpc_url.clone(),
557 config.bitcoin_rpc_user.clone(),
558 config.bitcoin_rpc_password.clone(),
559 None,
560 )
561 .await
562 .unwrap();
563
564 f.bitcoin_nodes.disconnect_nodes().await?;
566
567 let before_reorg_tip_height = rpc.get_block_count().await?;
568 let before_reorg_tip_hash = rpc.get_block_hash(before_reorg_tip_height).await?;
569
570 let address = Actor::new(config.secret_key, config.protocol_paramset.network).address;
571 let tx = rpc
572 .send_to_address(&address, Amount::from_sat(10000))
573 .await?;
574
575 assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
576 rpc.mine_blocks(1).await?;
577 assert!(rpc.is_tx_on_chain(&tx.txid).await?);
578
579 let reorg_depth = 4;
581 da1.generate(reorg_depth).await.unwrap();
582 f.bitcoin_nodes.connect_nodes().await?;
583 f.bitcoin_nodes.wait_for_sync(None).await?;
584
585 let current_tip_height = rpc.get_block_count().await?;
587 assert_eq!(
588 before_reorg_tip_height + reorg_depth,
589 current_tip_height,
590 "Re-org did not occur"
591 );
592 let current_tip_hash = rpc.get_block_hash(current_tip_height).await?;
593 assert_ne!(
594 before_reorg_tip_hash, current_tip_hash,
595 "Re-org did not occur"
596 );
597
598 assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
599
600 Ok(())
601 }
602 }
603
604 #[tokio::test]
605 async fn reorg_checks() -> Result<()> {
606 run_citrea_e2e_with_docker_port_retry(|| TestCaseRunner::new(ReorgChecks).run()).await
607 }
608
609 mod retry_config_tests {
610 use crate::extended_bitcoin_rpc::RetryConfig;
611
612 use std::time::Duration;
613
614 #[test]
615 fn test_retry_config_default() {
616 let config = RetryConfig::default();
617 assert_eq!(config.initial_delay_millis, 100);
618 assert_eq!(config.max_delay, Duration::from_secs(30));
619 assert_eq!(config.max_attempts, 5);
620 assert_eq!(config.backoff_multiplier, 2);
621 assert!(!config.is_jitter);
622 }
623
624 #[test]
625 fn test_retry_config_custom() {
626 let initial = 200;
627 let max = Duration::from_secs(10);
628 let attempts = 7;
629 let backoff_multiplier = 3;
630 let jitter = true;
631 let config = RetryConfig::new(initial, max, attempts, backoff_multiplier, jitter);
632 assert_eq!(config.initial_delay_millis, initial);
633 assert_eq!(config.max_delay, max);
634 assert_eq!(config.max_attempts, attempts);
635 assert_eq!(config.backoff_multiplier, backoff_multiplier);
636 assert!(config.is_jitter);
637 }
638
639 #[test]
640 fn test_retry_strategy_initial_delay() {
641 let initial_delay_millis = 100;
644 let backoff_multiplier = 2;
645 let config = RetryConfig::new(
646 initial_delay_millis,
647 Duration::from_secs(30),
648 5,
649 backoff_multiplier,
650 false, );
652
653 let mut strategy = config.get_strategy();
654 let first_delay = strategy.next().expect("Should have first delay");
655
656 assert_eq!(
660 first_delay,
661 Duration::from_millis(initial_delay_millis),
662 "First delay should match initial_delay_millis"
663 );
664
665 let second_delay = strategy.next().expect("Should have second delay");
667 assert_eq!(
668 second_delay,
669 Duration::from_millis(initial_delay_millis * backoff_multiplier),
670 "Second delay should be initial_delay_millis * backoff_multiplier"
671 );
672 }
673 }
674
675 mod retryable_error_tests {
676 use bitcoin::{hashes::Hash, BlockHash, Txid};
677
678 use crate::extended_bitcoin_rpc::RetryableError;
679
680 use super::*;
681 use std::io::{Error as IoError, ErrorKind};
682
683 #[test]
684 fn test_bitcoin_rpc_error_retryable_io_errors() {
685 let retryable_kinds = [
686 ErrorKind::ConnectionRefused,
687 ErrorKind::ConnectionReset,
688 ErrorKind::ConnectionAborted,
689 ErrorKind::NotConnected,
690 ErrorKind::BrokenPipe,
691 ErrorKind::TimedOut,
692 ErrorKind::Interrupted,
693 ErrorKind::UnexpectedEof,
694 ];
695
696 for kind in retryable_kinds {
697 let io_error = IoError::new(kind, "test error");
698 let rpc_error = bitcoincore_rpc::Error::Io(io_error);
699 assert!(
700 rpc_error.is_retryable(),
701 "ErrorKind::{kind:?} should be retryable"
702 );
703 }
704 }
705
706 #[test]
707 fn test_bitcoin_rpc_error_non_retryable_io_errors() {
708 let non_retryable_kinds = [
709 ErrorKind::PermissionDenied,
710 ErrorKind::NotFound,
711 ErrorKind::InvalidInput,
712 ErrorKind::InvalidData,
713 ];
714
715 for kind in non_retryable_kinds {
716 let io_error = IoError::new(kind, "test error");
717 let rpc_error = bitcoincore_rpc::Error::Io(io_error);
718 assert!(
719 !rpc_error.is_retryable(),
720 "ErrorKind::{kind:?} should not be retryable"
721 );
722 }
723 }
724
725 #[test]
726 fn test_bitcoin_rpc_error_auth_not_retryable() {
727 let auth_error = bitcoincore_rpc::Error::Auth("Invalid credentials".to_string());
728 assert!(!auth_error.is_retryable());
729 }
730
731 #[test]
732 fn test_bitcoin_rpc_error_url_parse_not_retryable() {
733 let url_error = url::ParseError::EmptyHost;
734 let rpc_error = bitcoincore_rpc::Error::UrlParse(url_error);
735 assert!(!rpc_error.is_retryable());
736 }
737
738 #[test]
739 fn test_bitcoin_rpc_error_invalid_cookie_not_retryable() {
740 let rpc_error = bitcoincore_rpc::Error::InvalidCookieFile;
741 assert!(!rpc_error.is_retryable());
742 }
743
744 #[test]
745 fn test_bitcoin_rpc_error_returned_error_non_retryable_patterns() {
746 let non_retryable_messages = [
747 "insufficient funds",
748 "transaction already in blockchain",
749 "invalid transaction",
750 "not found in mempool",
751 "transaction conflict",
752 ];
753
754 for msg in non_retryable_messages {
755 let rpc_error = bitcoincore_rpc::Error::ReturnedError(msg.to_string());
756 assert!(
757 !rpc_error.is_retryable(),
758 "Message '{msg}' should not be retryable"
759 );
760 }
761 }
762
763 #[test]
764 fn test_bitcoin_rpc_error_unexpected_structure_retryable() {
765 let rpc_error = bitcoincore_rpc::Error::UnexpectedStructure;
766 assert!(rpc_error.is_retryable());
767 }
768
769 #[test]
770 fn test_bitcoin_rpc_error_serialization_errors_not_retryable() {
771 use bitcoin::consensus::encode::Error as EncodeError;
772
773 let serialization_errors = [
774 bitcoincore_rpc::Error::BitcoinSerialization(EncodeError::Io(
775 IoError::other("test").into(),
776 )),
777 bitcoincore_rpc::Error::Json(serde_json::Error::io(IoError::other("test"))),
779 ];
780
781 for error in serialization_errors {
782 assert!(
783 !error.is_retryable(),
784 "Serialization error should not be retryable"
785 );
786 }
787 }
788
789 #[test]
790 fn test_bridge_rpc_error_retryable() {
791 assert!(
793 !BitcoinRPCError::TransactionAlreadyInBlock(BlockHash::all_zeros()).is_retryable()
794 );
795 assert!(!BitcoinRPCError::BumpFeeUTXOSpent(Default::default()).is_retryable());
796
797 let txid = Txid::all_zeros();
799 let fee_rate = FeeRateKvb::from_sat_per_vb_unchecked(1);
800 assert!(BitcoinRPCError::BumpFeeError(txid, fee_rate).is_retryable());
801
802 let retryable_other = BitcoinRPCError::Other(eyre::eyre!("timeout occurred"));
804 assert!(retryable_other.is_retryable());
805
806 let non_retryable_other = BitcoinRPCError::Other(eyre::eyre!("permission denied"));
807 assert!(!non_retryable_other.is_retryable());
808 }
809 }
810
811 mod rpc_call_retry_tests {
812
813 use crate::extended_bitcoin_rpc::RetryableError;
814
815 use super::*;
816 use secrecy::SecretString;
817
818 #[tokio::test]
819 async fn test_rpc_call_retry_with_invalid_host() {
820 let user = SecretString::new("user".to_string().into());
821 let password = SecretString::new("password".to_string().into());
822 let invalid_url = "http://nonexistent-host:8332".to_string();
823
824 let res = ExtendedBitcoinRpc::connect(invalid_url, user, password, None).await;
825
826 assert!(res.is_err());
827 assert!(!res.unwrap_err().is_retryable());
828 }
829 }
830
831 mod convenience_method_tests {
832 use super::*;
833
834 #[tokio::test]
835 async fn test_get_block_hash_with_retry() {
836 let mut config = create_test_config_with_thread_name().await;
837 let regtest = create_regtest_rpc(&mut config).await;
838 let rpc = regtest.rpc();
839
840 rpc.mine_blocks(1).await.unwrap();
842 let height = rpc.get_block_count().await.unwrap();
843
844 let result = rpc.get_block_hash(height).await;
845 assert!(result.is_ok());
846
847 let expected_hash = rpc.get_block_hash(height).await.unwrap();
848 assert_eq!(result.unwrap(), expected_hash);
849 }
850
851 #[tokio::test]
852 async fn test_get_tx_out_with_retry() {
853 let mut config = create_test_config_with_thread_name().await;
854 let regtest = create_regtest_rpc(&mut config).await;
855 let rpc = regtest.rpc();
856
857 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
859 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
860 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
861 let amount = Amount::from_sat(10000);
862
863 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
864
865 let result = rpc.get_tx_of_txid(&utxo.txid).await;
866 assert!(result.is_ok());
867
868 let tx = result.unwrap();
869 assert_eq!(tx.compute_txid(), utxo.txid);
870 }
871
872 #[tokio::test]
873 async fn test_send_to_address_with_retry() {
874 let mut config = create_test_config_with_thread_name().await;
875 let regtest = create_regtest_rpc(&mut config).await;
876 let rpc = regtest.rpc();
877
878 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
879 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
880 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
881 let amount = Amount::from_sat(10000);
882
883 let result = rpc.send_to_address(&address, amount).await;
884 assert!(result.is_ok());
885
886 let outpoint = result.unwrap();
887
888 let tx = rpc.get_tx_of_txid(&outpoint.txid).await.unwrap();
890 assert_eq!(tx.output[outpoint.vout as usize].value, amount);
891 }
892
893 #[tokio::test]
894 async fn test_bump_fee_with_retry() {
895 let mut config = create_test_config_with_thread_name().await;
896 let regtest = create_regtest_rpc(&mut config).await;
897 let rpc = regtest.rpc();
898
899 let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
900 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
901 let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
902 let amount = Amount::from_sat(10000);
903
904 let utxo = rpc.send_to_address(&address, amount).await.unwrap();
906 let new_fee_rate = FeeRateKvb::from_sat_per_vb_unchecked(10000);
907
908 let result = rpc.bump_fee_with_fee_rate(utxo.txid, new_fee_rate).await;
909 assert!(result.is_ok());
910
911 let new_txid = result.unwrap();
912 assert_ne!(new_txid, utxo.txid);
914 }
915 }
916}