1
2
3use std::borrow::Borrow;
4use std::collections::{HashMap, HashSet};
5use std::str::FromStr as _;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::Context;
10use bark_runtime::Instant;
11use bdk_core::{BlockId, CheckPoint};
12use bdk_esplora::esplora_client;
13use bitcoin::constants::genesis_block;
14use bitcoin::{
15 Amount, Block, BlockHash, FeeRate, Network, OutPoint, Transaction, Txid, Weight,
16};
17use log::{debug, info, warn};
18use tokio::sync::RwLock;
19
20use bitcoin_ext::{BlockHeight, BlockRef, FeeRateExt, TxStatus};
21use bitcoin_ext::rpc;
22#[cfg(feature = "bitcoind-rpc")]
23use bitcoin_ext::rpc::{
24 BitcoinAsyncRpcExt, BitcoinRpcClient, RPC_INVALID_ADDRESS_OR_KEY,
25 RPC_VERIFY_ALREADY_IN_UTXO_SET,
26};
27#[cfg(feature = "bitcoind-rpc")]
28use bitcoind_async_client::Client as BitcoindClient;
29#[cfg(feature = "bitcoind-rpc")]
30use bitcoind_async_client::error::ClientError as BitcoindClientError;
31#[cfg(feature = "bitcoind-rpc")]
32use bitcoind_async_client::traits::{Broadcaster, Reader};
33
34use crate::daemon::tip_watcher::{TipSource, TipWatcher};
35
36const FEE_RATE_TARGET_CONF_FAST: u16 = 1;
37const FEE_RATE_TARGET_CONF_REGULAR: u16 = 3;
38const FEE_RATE_TARGET_CONF_SLOW: u16 = 6;
39
40const TIP_CACHE_TTL: Duration = Duration::from_secs(1);
45
46const FEE_RATES_CACHE_TTL: Duration = Duration::from_secs(30);
49
50#[cfg(feature = "bitcoind-rpc")]
51const MIN_BITCOIND_VERSION: usize = 290000;
52
53#[derive(Clone, Debug)]
66pub enum ChainSourceSpec {
67 Bitcoind {
68 url: String,
70 auth: rpc::Auth,
72 zmq: Option<String>,
75 },
76 Esplora {
77 url: String,
79 },
80}
81
82impl ChainSourceSpec {
83 pub(crate) fn url(&self) -> &String {
84 match self {
85 ChainSourceSpec::Bitcoind { url, .. } => url,
86 ChainSourceSpec::Esplora { url } => url,
87 }
88 }
89}
90
91pub enum ChainSourceClient {
92 #[cfg(feature = "bitcoind-rpc")]
98 Bitcoind {
99 rpc: BitcoindClient,
100 sync: BitcoinRpcClient,
101 },
102 Esplora(esplora_client::AsyncClient),
103}
104
105impl ChainSourceClient {
106 async fn check_network(&self, expected: Network) -> anyhow::Result<()> {
107 match self {
108 #[cfg(feature = "bitcoind-rpc")]
109 ChainSourceClient::Bitcoind { rpc, .. } => {
110 let network = rpc.network().await?;
111 if expected != network {
112 bail!("Network mismatch: expected {:?}, got {:?}", expected, network);
113 }
114 },
115 ChainSourceClient::Esplora(client) => {
116 let res = client.client().get(format!("{}/block-height/0", client.url()))
117 .send().await?.text().await?;
118 let genesis_hash = BlockHash::from_str(&res)
119 .context("bad response from server (not a blockhash). Esplora client possibly misconfigured")?;
120 if genesis_hash != genesis_block(expected).block_hash() {
121 bail!("Network mismatch: expected {:?}, got {:?}", expected, genesis_hash);
122 }
123 },
124 };
125
126 Ok(())
127 }
128}
129
130pub struct ChainSource {
165 inner: ChainSourceClient,
166 network: Network,
167 zmq_endpoint: Option<String>,
169 fee_rates: RwLock<FeeRates>,
170 fee_rates_fetched_at: RwLock<Option<Instant>>,
173 tip_cache: RwLock<Option<(BlockRef, Instant)>>,
176}
177
178impl ChainSource {
179 pub async fn require_version(&self) -> anyhow::Result<()> {
184 #[cfg(feature = "bitcoind-rpc")]
185 if let ChainSourceClient::Bitcoind { rpc, .. } = self.inner() {
186 #[derive(Debug, serde::Deserialize)]
187 struct NetworkInfo { version: usize }
188 let info: NetworkInfo = rpc.call_raw("getnetworkinfo", &[]).await?;
189 if info.version < MIN_BITCOIND_VERSION {
190 bail!("Bitcoin Core version is too old, you can participate in rounds but won't be able to unilaterally exit. Please upgrade to 29.0 or higher.");
191 }
192 }
193
194 Ok(())
195 }
196
197 pub(crate) fn inner(&self) -> &ChainSourceClient {
198 &self.inner
199 }
200
201 pub async fn fee_rates(&self) -> FeeRates {
203 self.fee_rates.read().await.clone()
204 }
205
206 pub fn network(&self) -> Network {
208 self.network
209 }
210
211 pub async fn new(
244 spec: ChainSourceSpec,
245 network: Network,
246 fallback_fee: Option<FeeRate>,
247 #[cfg(feature = "socks5-proxy")] proxy: Option<&str>,
248 ) -> anyhow::Result<Self> {
249 let (inner, zmq_endpoint) = match spec {
250 #[cfg(feature = "bitcoind-rpc")]
251 ChainSourceSpec::Bitcoind { url, auth, zmq } => {
252 let sync = BitcoinRpcClient::new(&url, auth.clone())
262 .context("failed to create sync bitcoind rpc client")?;
263 let async_auth = match auth {
264 rpc::Auth::None => bail!(
265 "bitcoind RPC auth is required (cookie file or user/pass)",
266 ),
267 rpc::Auth::UserPass(u, p) => bitcoind_async_client::Auth::UserPass(u, p),
268 rpc::Auth::CookieFile(p) => bitcoind_async_client::Auth::CookieFile(p),
269 };
270 let rpc = BitcoindClient::new(url, async_auth, None, None, None)
271 .context("failed to create async bitcoind rpc client")?;
272 rpc.require_txindex().await?;
273 (ChainSourceClient::Bitcoind { rpc, sync }, zmq)
274 },
275 #[cfg(not(feature = "bitcoind-rpc"))]
276 ChainSourceSpec::Bitcoind { .. } => bail!(
277 "bitcoind RPC backend is not available: this build was compiled without \
278 the `bitcoind-rpc` feature (notably the wasm-web build)",
279 ),
280 ChainSourceSpec::Esplora { url } => (ChainSourceClient::Esplora({
281 let url = crate::utils::url_with_default_https_scheme(&url);
282 let url = url.strip_suffix("/").unwrap_or(&url);
284 #[cfg(feature = "socks5-proxy")]
285 let mut builder = esplora_client::Builder::new(url);
286 #[cfg(not(feature = "socks5-proxy"))]
287 let builder = esplora_client::Builder::new(url);
288 #[cfg(feature = "socks5-proxy")]
289 if let Some(proxy) = proxy {
290 builder = builder.proxy(proxy);
291 }
292 builder.build_async()
293 .with_context(|| format!("failed to create esplora client for url {}", url))?
294 }), None),
295 };
296
297 inner.check_network(network).await?;
298
299 let fee = fallback_fee.unwrap_or(FeeRate::BROADCAST_MIN);
300 let fee_rates = RwLock::new(FeeRates { fast: fee, regular: fee, slow: fee });
301
302 Ok(Self {
303 inner,
304 network,
305 zmq_endpoint,
306 fee_rates,
307 fee_rates_fetched_at: RwLock::new(None),
308 tip_cache: RwLock::new(None),
309 })
310 }
311
312 async fn fetch_fee_rates(&self) -> anyhow::Result<FeeRates> {
313 match self.inner() {
314 #[cfg(feature = "bitcoind-rpc")]
315 ChainSourceClient::Bitcoind { rpc, .. } => {
316 let get_fee_rate = async |target: u16| -> anyhow::Result<FeeRate> {
317 let fee: rpc::json::EstimateSmartFeeResult = rpc.call_raw(
318 "estimatesmartfee",
319 &[
320 target.into(),
321 serde_json::to_value(rpc::json::EstimateMode::Economical)
322 .expect("serializable"),
323 ],
324 ).await?;
325 if let Some(fee_rate) = fee.fee_rate {
326 Ok(FeeRate::from_amount_per_kvb_ceil(fee_rate))
327 } else {
328 Err(anyhow!("No rate returned from estimate_smart_fee for a {} confirmation target", target))
329 }
330 };
331 Ok(FeeRates {
332 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST).await?,
333 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR).await.expect("should exist"),
334 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW).await.expect("should exist"),
335 })
336 },
337 ChainSourceClient::Esplora(client) => {
338 let estimates = client.get_fee_estimates().await?;
340 let get_fee_rate = |target| {
341 let fee = estimates.get(&target).with_context(||
342 format!("No rate returned from get_fee_estimates for a {} confirmation target", target)
343 )?;
344 FeeRate::from_sat_per_vb_decimal_checked_ceil(*fee).with_context(||
345 format!("Invalid rate returned from get_fee_estimates {} for a {} confirmation target", fee, target)
346 )
347 };
348 Ok(FeeRates {
349 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST)?,
350 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR)?,
351 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW)?,
352 })
353 }
354 }
355 }
356
357 pub fn zmq_endpoint(&self) -> Option<&str> {
361 self.zmq_endpoint.as_deref()
362 }
363
364 async fn fetch_tip(&self) -> anyhow::Result<BlockHeight> {
365 match self.inner() {
366 #[cfg(feature = "bitcoind-rpc")]
367 ChainSourceClient::Bitcoind { rpc, .. } => {
368 Ok(rpc.get_block_count().await? as BlockHeight)
369 },
370 ChainSourceClient::Esplora(client) => {
371 Ok(client.get_height().await?)
372 },
373 }
374 }
375
376 pub async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
377 if let Some((block_ref, fetched_at)) = *self.tip_cache.read().await {
378 if fetched_at.elapsed() < TIP_CACHE_TTL {
379 return Ok(block_ref);
380 }
381 }
382 let block_ref = self.tip_ref_uncached().await?;
383 self.record_observed_tip(block_ref).await;
384 Ok(block_ref)
385 }
386
387 pub async fn tip(&self) -> anyhow::Result<BlockHeight> {
388 Ok(self.tip_ref().await?.height)
389 }
390
391 async fn record_observed_tip(&self, block_ref: BlockRef) {
393 *self.tip_cache.write().await = Some((block_ref, Instant::now()));
394 }
395
396 pub async fn invalidate_caches(&self) {
401 *self.tip_cache.write().await = None;
402 *self.fee_rates_fetched_at.write().await = None;
403 }
404
405 pub(crate) async fn tip_ref_uncached(&self) -> anyhow::Result<BlockRef> {
409 self.block_ref(self.fetch_tip().await?).await
410 }
411
412 pub async fn tip_watcher(
418 self: &Arc<Self>,
419 poll_interval: Duration,
420 ) -> anyhow::Result<TipWatcher> {
421 #[cfg(all(feature = "bitcoind-rpc", not(target_arch = "wasm32")))]
422 if let Some(zmq) = self.zmq_endpoint() {
423 return TipWatcher::start_zmq(self.clone(), zmq, poll_interval).await;
424 }
425 TipWatcher::start_poll(self.clone(), poll_interval).await
426 }
427
428 pub async fn block_ref(&self, height: BlockHeight) -> anyhow::Result<BlockRef> {
429 match self.inner() {
430 #[cfg(feature = "bitcoind-rpc")]
431 ChainSourceClient::Bitcoind { rpc, .. } => {
432 let hash = rpc.get_block_hash(height as u64).await?;
433 Ok(BlockRef { height, hash })
434 },
435 ChainSourceClient::Esplora(client) => {
436 let hash = client.get_block_hash(height).await?;
437 Ok(BlockRef { height, hash })
438 },
439 }
440 }
441
442 pub async fn block(&self, hash: BlockHash) -> anyhow::Result<Option<Block>> {
443 match self.inner() {
444 #[cfg(feature = "bitcoind-rpc")]
445 ChainSourceClient::Bitcoind { rpc, .. } => {
446 match rpc.get_block(&hash).await {
447 Ok(block) => Ok(Some(block)),
448 Err(e) if is_not_found(&e) => Ok(None),
449 Err(e) => Err(e.into()),
450 }
451 },
452 ChainSourceClient::Esplora(client) => {
453 Ok(client.get_block_by_hash(&hash).await?)
454 },
455 }
456 }
457
458 pub async fn mempool_ancestor_info(&self, txid: Txid) -> anyhow::Result<MempoolAncestorInfo> {
461 let mut result = MempoolAncestorInfo::new(txid);
462
463 match self.inner() {
466 #[cfg(feature = "bitcoind-rpc")]
467 ChainSourceClient::Bitcoind { rpc, .. } => {
468 let entry: rpc::json::GetMempoolEntryResult = rpc.call_raw(
469 "getmempoolentry", &[serde_json::to_value(txid).expect("serializable")],
470 ).await?;
471 let err = || anyhow!("missing weight parameter from getmempoolentry");
472
473 result.total_fee = entry.fees.ancestor;
474 result.total_weight = Weight::from_wu(entry.weight.ok_or_else(err)?) +
475 Weight::from_vb(entry.ancestor_size).ok_or_else(err)?;
476 },
477 ChainSourceClient::Esplora(client) => {
478 let status = self.tx_status(txid).await?;
481 if !matches!(status, TxStatus::Mempool) {
482 return Err(anyhow!("{} is not in the mempool, status is {:?}", txid, status));
483 }
484
485 let mut info_map: HashMap<Txid, esplora_client::Tx> = HashMap::new();
486 let mut set = HashSet::from([txid]);
487 while !set.is_empty() {
488 let requests = set.iter().filter_map(|txid| if info_map.contains_key(txid) {
490 None
491 } else {
492 Some((txid, client.get_tx_info(&txid)))
493 }).collect::<Vec<_>>();
494
495 let mut next_set = HashSet::new();
497
498 for (txid, request) in requests {
500 let info = request.await?
501 .ok_or_else(|| anyhow!("unable to retrieve tx info for {}", txid))?;
502 if !info.status.confirmed {
503 for vin in info.vin.iter() {
504 next_set.insert(vin.txid);
505 }
506 }
507 info_map.insert(*txid, info);
508 }
509 set = next_set;
510 }
511 for info in info_map.into_values().filter(|info| !info.status.confirmed) {
513 result.total_fee += info.fee();
514 result.total_weight += info.weight();
515 }
516 },
517 }
518 Ok(result)
520 }
521
522 pub async fn txs_spending_inputs<T: IntoIterator<Item = OutPoint>>(
525 &self,
526 outpoints: T,
527 #[cfg_attr(not(feature = "bitcoind-rpc"), allow(unused_variables))]
528 block_scan_start: BlockHeight,
529 ) -> anyhow::Result<TxsSpendingInputsResult> {
530 let mut res = TxsSpendingInputsResult::new();
531 match self.inner() {
532 #[cfg(feature = "bitcoind-rpc")]
533 ChainSourceClient::Bitcoind { sync, .. } => {
534 let start = block_scan_start.saturating_sub(1);
536 let block_ref = self.block_ref(start).await?;
537 let cp = CheckPoint::new(BlockId {
538 height: block_ref.height,
539 hash: block_ref.hash,
540 });
541
542 debug!("Scanning blocks for spent outpoints with bitcoind, starting at block height {}...", block_scan_start);
543 let outpoint_set = outpoints.into_iter().collect::<HashSet<_>>();
544
545 let sync_client = sync.clone();
548 let cp_for_blocking = cp.clone();
549 res = tokio::task::spawn_blocking(move || -> anyhow::Result<TxsSpendingInputsResult> {
550 let mut res = res;
551 let mut emitter = bdk_bitcoind_rpc::Emitter::new(
552 &sync_client,
553 cp_for_blocking.clone(),
554 cp_for_blocking.height(),
555 bdk_bitcoind_rpc::NO_EXPECTED_MEMPOOL_TXS,
556 );
557 while let Some(em) = emitter.next_block()? {
558 if em.block_height() % 1000 == 0 {
559 info!("Scanned for spent outpoints until block height {}", em.block_height());
560 }
561 for tx in &em.block.txdata {
562 for txin in tx.input.iter() {
563 if outpoint_set.contains(&txin.previous_output) {
564 res.add(
565 txin.previous_output.clone(),
566 tx.compute_txid(),
567 TxStatus::Confirmed(BlockRef {
568 height: em.block_height(),
569 hash: em.block.block_hash().clone(),
570 }),
571 );
572 if res.map.len() == outpoint_set.len() {
573 return Ok(res);
574 }
575 }
576 }
577 }
578 }
579
580 debug!("Finished scanning blocks for spent outpoints, now checking the mempool...");
581 let mempool = emitter.mempool()?;
582 for (tx, _last_seen) in &mempool.update {
583 for txin in tx.input.iter() {
584 if outpoint_set.contains(&txin.previous_output) {
585 res.add(
586 txin.previous_output.clone(),
587 tx.compute_txid(),
588 TxStatus::Mempool,
589 );
590 if res.map.len() == outpoint_set.len() {
591 return Ok(res);
592 }
593 }
594 }
595 }
596 debug!("Finished checking the mempool for spent outpoints");
597 Ok(res)
598 }).await.context("Emitter scan task panicked")??;
599 },
600 ChainSourceClient::Esplora(client) => {
601 for outpoint in outpoints {
602 let output_status = client.get_output_status(&outpoint.txid, outpoint.vout.into()).await?;
603
604 if let Some(output_status) = output_status {
605 if output_status.spent {
606 let tx_status = {
607 let status = output_status.status.expect("Status should be valid if an outpoint is spent");
608 if status.confirmed {
609 TxStatus::Confirmed(BlockRef {
610 height: status.block_height.expect("Confirmed transaction missing block_height"),
611 hash: status.block_hash.expect("Confirmed transaction missing block_hash"),
612 })
613 } else {
614 TxStatus::Mempool
615 }
616 };
617 let txid = output_status.txid.expect("Txid should be valid if an outpoint is spent");
618 res.add(outpoint, txid, tx_status);
619 }
620 }
621 }
622 },
623 }
624
625 Ok(res)
626 }
627
628 pub async fn broadcast_tx(&self, tx: &Transaction) -> anyhow::Result<()> {
629 match self.inner() {
630 #[cfg(feature = "bitcoind-rpc")]
631 ChainSourceClient::Bitcoind { rpc, .. } => {
632 match rpc.send_raw_transaction(tx, None).await {
633 Ok(_) => Ok(()),
634 Err(e) if is_in_utxo_set(&e) => Ok(()),
635 Err(e) => Err(e.into()),
636 }
637 },
638 ChainSourceClient::Esplora(client) => {
639 client.broadcast(tx).await?;
640 Ok(())
641 },
642 }
643 }
644
645 pub async fn broadcast_package(&self, txs: &[impl Borrow<Transaction>]) -> Result<(), BroadcastError> {
646 let package_order = txs.iter()
647 .map(|t| t.borrow().compute_txid())
648 .collect::<Vec<_>>();
649 match self.inner() {
650 #[cfg(feature = "bitcoind-rpc")]
651 ChainSourceClient::Bitcoind { rpc, .. } => {
652 let hexes: Vec<String> = txs.iter()
653 .map(|t| bitcoin::consensus::encode::serialize_hex(t.borrow()))
654 .collect();
655 let res: rpc::SubmitPackageResult = rpc.call_raw("submitpackage", &[hexes.into()])
656 .await
657 .map_err(|e| BroadcastError::Other(e.to_string()))?;
658 if res.package_msg != "success" {
659 return Err(classify_submit_package_errors(
660 &res.package_msg,
661 res.tx_results.values().map(|t| (t.txid, t.error.as_deref())),
662 &package_order,
663 ));
664 }
665 Ok(())
666 },
667 ChainSourceClient::Esplora(client) => {
668 let txs = txs.iter().map(|t| t.borrow().clone()).collect::<Vec<_>>();
669 let res = client.submit_package(&txs, None, None)
670 .await
671 .map_err(|e| BroadcastError::Other(e.to_string()))?;
672 if res.package_msg != "success" {
673 return Err(classify_submit_package_errors(
674 &res.package_msg,
675 res.tx_results.values().map(|t| (t.txid, t.error.as_deref())),
676 &package_order,
677 ));
678 }
679
680 Ok(())
681 },
682 }
683 }
684
685 pub async fn get_tx(&self, txid: &Txid) -> anyhow::Result<Option<Transaction>> {
686 match self.inner() {
687 #[cfg(feature = "bitcoind-rpc")]
688 ChainSourceClient::Bitcoind { rpc, .. } => {
689 match rpc.get_raw_transaction_verbosity_zero(txid).await {
690 Ok(tx) => Ok(Some(tx.0)),
691 Err(e) if is_not_found(&e) => Ok(None),
692 Err(e) => Err(e.into()),
693 }
694 },
695 ChainSourceClient::Esplora(client) => {
696 Ok(client.get_tx(txid).await?)
697 },
698 }
699 }
700
701 pub async fn tx_confirmed(&self, txid: Txid) -> anyhow::Result<Option<BlockHeight>> {
703 Ok(self.tx_status(txid).await?.confirmed_height())
704 }
705
706 pub async fn tx_status(&self, txid: Txid) -> anyhow::Result<TxStatus> {
708 match self.inner() {
709 #[cfg(feature = "bitcoind-rpc")]
710 ChainSourceClient::Bitcoind { rpc, .. } => Ok(bitcoind_tx_status(rpc, txid).await?),
711 ChainSourceClient::Esplora(esplora) => {
712 match esplora.get_tx_info(&txid).await? {
713 Some(info) => match (info.status.block_height, info.status.block_hash) {
714 (Some(block_height), Some(block_hash)) => Ok(TxStatus::Confirmed(BlockRef {
715 height: block_height,
716 hash: block_hash,
717 } )),
718 _ => Ok(TxStatus::Mempool),
719 },
720 None => Ok(TxStatus::NotFound),
721 }
722 },
723 }
724 }
725
726 #[allow(unused)]
727 pub async fn txout_value(&self, outpoint: &OutPoint) -> anyhow::Result<Amount> {
728 let tx = match self.inner() {
729 #[cfg(feature = "bitcoind-rpc")]
730 ChainSourceClient::Bitcoind { rpc, .. } => {
731 rpc.get_raw_transaction_verbosity_zero(&outpoint.txid).await
732 .with_context(|| format!("tx {} unknown", outpoint.txid))?
733 .0
734 },
735 ChainSourceClient::Esplora(client) => {
736 client.get_tx(&outpoint.txid).await?
737 .with_context(|| format!("tx {} unknown", outpoint.txid))?
738 },
739 };
740 Ok(tx.output.get(outpoint.vout as usize).context("outpoint vout out of range")?.value)
741 }
742
743 pub async fn outpoint_spent_confirmed(&self, outpoint: OutPoint) -> anyhow::Result<bool> {
753 match self.inner() {
754 #[cfg(feature = "bitcoind-rpc")]
755 ChainSourceClient::Bitcoind { rpc, .. } => {
756 let utxo = rpc.try_get_tx_out(outpoint, false).await
760 .with_context(|| format!("gettxout {} failed", outpoint))?;
761 Ok(utxo.is_none())
762 },
763 ChainSourceClient::Esplora(client) => {
764 let status = client.get_output_status(&outpoint.txid, outpoint.vout as u64).await
765 .with_context(|| format!("outspend lookup for {} failed", outpoint))?;
766 Ok(status.is_some_and(|s| {
767 s.spent && s.status.is_some_and(|s| s.confirmed)
768 }))
769 },
770 }
771 }
772
773 pub async fn update_fee_rates(&self, fallback_fee: Option<FeeRate>) -> anyhow::Result<()> {
781 if let Some(fetched_at) = *self.fee_rates_fetched_at.read().await {
782 if fetched_at.elapsed() < FEE_RATES_CACHE_TTL {
783 return Ok(());
784 }
785 }
786 let (fee_rates, used_fallback) = match (self.fetch_fee_rates().await, fallback_fee) {
787 (Ok(fee_rates), _) => (fee_rates, false),
788 (Err(e), None) => return Err(e),
789 (Err(e), Some(fallback)) => {
790 warn!("Error getting fee rates, falling back to {} sat/kvB: {}",
791 fallback.to_btc_per_kvb(), e,
792 );
793 (FeeRates { fast: fallback, regular: fallback, slow: fallback }, true)
794 }
795 };
796
797 *self.fee_rates.write().await = fee_rates;
798 if !used_fallback {
799 *self.fee_rates_fetched_at.write().await = Some(Instant::now());
800 }
801 Ok(())
802 }
803}
804
805impl TipSource for ChainSource {
806 async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
807 let block_ref = ChainSource::tip_ref_uncached(self).await?;
808 self.record_observed_tip(block_ref).await;
812 Ok(block_ref)
813 }
814}
815
816#[cfg(feature = "bitcoind-rpc")]
822fn is_not_found(e: &BitcoindClientError) -> bool {
823 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_INVALID_ADDRESS_OR_KEY)
824}
825
826#[cfg(feature = "bitcoind-rpc")]
828fn is_in_utxo_set(e: &BitcoindClientError) -> bool {
829 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_VERIFY_ALREADY_IN_UTXO_SET)
830}
831
832#[cfg(feature = "bitcoind-rpc")]
835async fn bitcoind_tx_status(
836 rpc: &BitcoindClient, txid: Txid,
837) -> Result<TxStatus, BitcoindClientError> {
838 let res: Result<rpc::GetRawTransactionResult, _> = rpc.call_raw(
839 "getrawtransaction",
840 &[serde_json::to_value(txid).expect("serializable"), true.into()],
841 ).await;
842 let info = match res {
843 Ok(info) => info,
844 Err(e) if is_not_found(&e) => return Ok(TxStatus::NotFound),
845 Err(e) => return Err(e),
846 };
847 let Some(hash) = info.blockhash else {
848 return Ok(TxStatus::Mempool);
849 };
850 let header: rpc::json::GetBlockHeaderResult = rpc.call_raw(
851 "getblockheader",
852 &[serde_json::to_value(hash).expect("serializable"), true.into()],
853 ).await?;
854 if header.confirmations > 0 {
855 Ok(TxStatus::Confirmed(BlockRef {
856 height: header.height as BlockHeight,
857 hash: header.hash,
858 }))
859 } else {
860 Ok(TxStatus::Mempool)
861 }
862}
863
864#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
866pub struct FeeRates {
867 pub fast: FeeRate,
869 pub regular: FeeRate,
871 pub slow: FeeRate,
873}
874
875#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
877pub struct MempoolAncestorInfo {
878 pub txid: Txid,
880 pub total_fee: Amount,
883 pub total_weight: Weight,
885}
886
887impl MempoolAncestorInfo {
888 pub fn new(txid: Txid) -> Self {
889 Self {
890 txid,
891 total_fee: Amount::ZERO,
892 total_weight: Weight::ZERO,
893 }
894 }
895
896 pub fn effective_fee_rate(&self) -> Option<FeeRate> {
897 FeeRate::from_amount_and_weight_ceil(self.total_fee, self.total_weight)
898 }
899}
900
901#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
902pub struct TxsSpendingInputsResult {
903 pub map: HashMap<OutPoint, (Txid, TxStatus)>,
904}
905
906impl TxsSpendingInputsResult {
907 pub fn new() -> Self {
908 Self { map: HashMap::new() }
909 }
910
911 pub fn add(&mut self, outpoint: OutPoint, txid: Txid, status: TxStatus) {
912 self.map.insert(outpoint, (txid, status));
913 }
914
915 pub fn get(&self, outpoint: &OutPoint) -> Option<&(Txid, TxStatus)> {
916 self.map.get(outpoint)
917 }
918
919 pub fn confirmed_txids(&self) -> impl Iterator<Item = (Txid, BlockRef)> + '_ {
920 self.map
921 .iter()
922 .filter_map(|(_, (txid, status))| {
923 match status {
924 TxStatus::Confirmed(block) => Some((*txid, *block)),
925 _ => None,
926 }
927 })
928 }
929
930 pub fn mempool_txids(&self) -> impl Iterator<Item = Txid> + '_ {
931 self.map
932 .iter()
933 .filter(|(_, (_, status))| matches!(status, TxStatus::Mempool))
934 .map(|(_, (txid, _))| *txid)
935 }
936}
937
938#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
945pub enum BroadcastError {
946 #[error("transaction already known to the mempool")]
948 AlreadyKnown,
949 #[error("transaction inputs are missing or already spent")]
951 MissingOrSpentInputs,
952 #[error("insufficient fee, rejecting replacement")]
954 InsufficientReplacementFee,
955 #[error("{0}")]
957 Other(String),
958}
959
960impl BroadcastError {
961 pub fn is_mempool_conflict(&self) -> bool {
964 matches!(
965 self,
966 BroadcastError::AlreadyKnown
967 | BroadcastError::MissingOrSpentInputs
968 | BroadcastError::InsufficientReplacementFee,
969 )
970 }
971}
972
973fn classify_submit_package_errors<'a>(
974 package_msg: &str,
975 tx_results: impl Iterator<Item = (Txid, Option<&'a str>)>,
976 package_order: &[Txid],
977) -> BroadcastError {
978 let mut results: Vec<(Txid, Option<&'a str>)> = tx_results.collect();
984 results.sort_by_key(|(txid, _)| {
985 package_order.iter().position(|t| t == txid).unwrap_or(usize::MAX)
986 });
987
988 let mut saw_already_known = false;
989 let mut root_cause = None;
990 for (_, err) in &results {
991 if let Some(err) = err {
992 if err.contains("txn-already-known") {
993 saw_already_known = true;
995 continue;
996 }
997 root_cause = Some(*err);
998 break;
999 } else {
1000 continue;
1001 }
1002 }
1003
1004 match root_cause {
1005 Some(e) if e.contains("bad-txns-inputs-missingorspent") => {
1006 BroadcastError::MissingOrSpentInputs
1007 },
1008 Some(e) if e.contains("insufficient fee, rejecting replacement") => {
1009 BroadcastError::InsufficientReplacementFee
1010 },
1011 Some(_) => {
1012 let combined = results.iter()
1013 .map(|(txid, e)| format!("tx {}: {}", txid, e.unwrap_or("(no error)")))
1014 .collect::<Vec<_>>()
1015 .join(", ");
1016 BroadcastError::Other(format!("msg: '{}', errors: [{}]", package_msg, combined))
1017 },
1018 None if saw_already_known => BroadcastError::AlreadyKnown,
1019 None => BroadcastError::Other(format!("msg: '{}', no tx errors", package_msg)),
1020 }
1021}
1022
1023#[cfg(test)]
1024mod test {
1025 use super::*;
1026 use std::str::FromStr;
1027
1028 #[test]
1029 fn classify_package_errors_attributes_root_cause_in_package_order() {
1030 let parent = Txid::from_str(
1031 "1111111111111111111111111111111111111111111111111111111111111111").unwrap();
1032 let child = Txid::from_str(
1033 "2222222222222222222222222222222222222222222222222222222222222222").unwrap();
1034 let order = [parent, child];
1035
1036 let res = classify_submit_package_errors("transaction failed", [
1039 (parent, None),
1040 (child, Some("bad-txns-inputs-missingorspent")),
1041 ].into_iter(), &order);
1042 assert_eq!(res, BroadcastError::MissingOrSpentInputs);
1043
1044 let res = classify_submit_package_errors("transaction failed", [
1049 (child, Some("bad-txns-inputs-missingorspent")),
1050 (parent, Some("version")),
1051 ].into_iter(), &order);
1052 assert!(matches!(res, BroadcastError::Other(_)), "got {:?}", res);
1053
1054 let res = classify_submit_package_errors("transaction failed", [
1057 (parent, Some("txn-already-known")),
1058 (child, Some("bad-txns-inputs-missingorspent")),
1059 ].into_iter(), &order);
1060 assert_eq!(res, BroadcastError::MissingOrSpentInputs);
1061
1062 let res = classify_submit_package_errors("transaction failed", [
1064 (parent, Some("txn-already-known")),
1065 (child, Some("txn-already-known")),
1066 ].into_iter(), &order);
1067 assert_eq!(res, BroadcastError::AlreadyKnown);
1068
1069 let res = classify_submit_package_errors("transaction failed", [
1071 (parent, None),
1072 (child, Some("insufficient fee, rejecting replacement")),
1073 ].into_iter(), &order);
1074 assert_eq!(res, BroadcastError::InsufficientReplacementFee);
1075
1076 let res = classify_submit_package_errors("package-mempool-limits", [
1078 (parent, None),
1079 (child, None),
1080 ].into_iter(), &order);
1081 assert!(matches!(res, BroadcastError::Other(ref s) if s.contains("package-mempool-limits")));
1082 }
1083}