fedimint_bitcoind/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_sign_loss)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(clippy::module_name_repetitions)]
7#![allow(clippy::similar_names)]
8
9use std::cmp::min;
10use std::collections::BTreeMap;
11use std::env;
12use std::fmt::Debug;
13use std::future::Future;
14use std::sync::{Arc, LazyLock, Mutex};
15use std::time::Duration;
16
17use anyhow::Context;
18pub use anyhow::Result;
19use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
20use fedimint_core::envs::{
21    BitcoinRpcConfig, FM_FORCE_BITCOIN_RPC_KIND_ENV, FM_FORCE_BITCOIN_RPC_URL_ENV,
22};
23use fedimint_core::fmt_utils::OptStacktrace;
24use fedimint_core::runtime::sleep;
25use fedimint_core::task::TaskHandle;
26use fedimint_core::txoproof::TxOutProof;
27use fedimint_core::util::SafeUrl;
28use fedimint_core::{apply, async_trait_maybe_send, dyn_newtype_define, Feerate};
29use fedimint_logging::{LOG_BLOCKCHAIN, LOG_CORE};
30use tracing::{debug, info};
31
32#[cfg(feature = "bitcoincore-rpc")]
33pub mod bitcoincore;
34#[cfg(feature = "electrum-client")]
35mod electrum;
36#[cfg(feature = "esplora-client")]
37mod esplora;
38
39// <https://blockstream.info/api/block-height/0>
40const MAINNET_GENESIS_BLOCK_HASH: &str =
41    "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
42// <https://blockstream.info/testnet/api/block-height/0>
43const TESTNET_GENESIS_BLOCK_HASH: &str =
44    "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
45// <https://mempool.space/signet/api/block-height/0>
46const SIGNET_GENESIS_BLOCK_HASH: &str =
47    "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
48// See <https://bitcoin.stackexchange.com/questions/122778/is-the-regtest-genesis-hash-always-the-same-or-not>
49// <https://github.com/bitcoin/bitcoin/blob/d82283950f5ff3b2116e705f931c6e89e5fdd0be/src/kernel/chainparams.cpp#L478>
50const REGTEST_GENESIS_BLOCK_HASH: &str =
51    "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206";
52
53/// Global factories for creating bitcoin RPCs
54static BITCOIN_RPC_REGISTRY: LazyLock<Mutex<BTreeMap<String, DynBitcoindRpcFactory>>> =
55    LazyLock::new(|| {
56        Mutex::new(BTreeMap::from([
57            #[cfg(feature = "esplora-client")]
58            ("esplora".to_string(), esplora::EsploraFactory.into()),
59            #[cfg(feature = "electrum-client")]
60            ("electrum".to_string(), electrum::ElectrumFactory.into()),
61            #[cfg(feature = "bitcoincore-rpc")]
62            ("bitcoind".to_string(), bitcoincore::BitcoindFactory.into()),
63        ]))
64    });
65
66/// Create a bitcoin RPC of a given kind
67pub fn create_bitcoind(config: &BitcoinRpcConfig, handle: TaskHandle) -> Result<DynBitcoindRpc> {
68    let registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
69
70    let kind = env::var(FM_FORCE_BITCOIN_RPC_KIND_ENV)
71        .ok()
72        .unwrap_or_else(|| config.kind.clone());
73    let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
74        .ok()
75        .map(|s| SafeUrl::parse(&s))
76        .transpose()?
77        .unwrap_or_else(|| config.url.clone());
78    debug!(target: LOG_CORE, %kind, %url, "Starting bitcoin rpc");
79    let maybe_factory = registry.get(&kind);
80    let factory = maybe_factory.with_context(|| {
81        anyhow::anyhow!(
82            "{} rpc not registered, available options: {:?}",
83            config.kind,
84            registry.keys()
85        )
86    })?;
87    factory.create_connection(&url, handle)
88}
89
90/// Register a new factory for creating bitcoin RPCs
91pub fn register_bitcoind(kind: String, factory: DynBitcoindRpcFactory) {
92    let mut registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
93    registry.insert(kind, factory);
94}
95
96/// Trait for creating new bitcoin RPC clients
97pub trait IBitcoindRpcFactory: Debug + Send + Sync {
98    /// Creates a new bitcoin RPC client connection
99    fn create_connection(&self, url: &SafeUrl, handle: TaskHandle) -> Result<DynBitcoindRpc>;
100}
101
102dyn_newtype_define! {
103    #[derive(Clone)]
104    pub DynBitcoindRpcFactory(Arc<IBitcoindRpcFactory>)
105}
106
107/// Trait that allows interacting with the Bitcoin blockchain
108///
109/// Functions may panic if the bitcoind node is not reachable.
110#[apply(async_trait_maybe_send!)]
111pub trait IBitcoindRpc: Debug {
112    /// Returns the Bitcoin network the node is connected to
113    async fn get_network(&self) -> Result<bitcoin::Network>;
114
115    /// Returns the current block count
116    async fn get_block_count(&self) -> Result<u64>;
117
118    /// Returns the block hash at a given height
119    ///
120    /// # Panics
121    /// If the node does not know a block for that height. Make sure to only
122    /// query blocks of a height less to the one returned by
123    /// `Self::get_block_count`.
124    ///
125    /// While there is a corner case that the blockchain shrinks between these
126    /// two calls (through on average heavier blocks on a fork) this is
127    /// prevented by only querying hashes for blocks tailing the chain tip
128    /// by a certain number of blocks.
129    async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
130
131    /// Estimates the fee rate for a given confirmation target. Make sure that
132    /// all federation members use the same algorithm to avoid widely
133    /// diverging results. If the node is not ready yet to return a fee rate
134    /// estimation this function returns `None`.
135    async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>>;
136
137    /// Submits a transaction to the Bitcoin network
138    ///
139    /// This operation does not return anything as it never OK to consider its
140    /// success as final anyway. The caller should be retrying
141    /// broadcast periodically until it confirms the transaction was actually
142    /// via other means or decides that is no longer relevant.
143    ///
144    /// Also - most backends considers brodcasting a tx that is already included
145    /// in the blockchain as an error, which breaks idempotency and requires
146    /// brittle workarounds just to reliably ignore... just to retry on the
147    /// higher level anyway.
148    ///
149    /// Implementations of this error should log errors for debugging purposes
150    /// when it makes sense.
151    async fn submit_transaction(&self, transaction: Transaction);
152
153    /// If a transaction is included in a block, returns the block height.
154    /// Note: calling this method with bitcoind as a backend must first call
155    /// `watch_script_history` or run bitcoind with txindex enabled.
156    async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
157
158    /// Check if a transaction is included in a block
159    async fn is_tx_in_block(
160        &self,
161        txid: &Txid,
162        block_hash: &BlockHash,
163        block_height: u64,
164    ) -> Result<bool>;
165
166    /// Watches for a script and returns any transactions associated with it
167    ///
168    /// Should be called at least prior to transactions being submitted or
169    /// watching may not occur on backends that need it
170    /// TODO: bitcoind backend is broken
171    /// `<https://github.com/fedimint/fedimint/issues/5329>`
172    async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
173
174    /// Get script transaction history
175    ///
176    /// Note: should call `watch_script_history` at least once, before calling
177    /// this.
178    async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
179
180    /// Returns a proof that a tx is included in the bitcoin blockchain
181    async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
182
183    /// Returns the Bitcoin RPC config
184    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
185}
186
187dyn_newtype_define! {
188    #[derive(Clone)]
189    pub DynBitcoindRpc(Arc<IBitcoindRpc>)
190}
191
192const RETRY_SLEEP_MIN_MS: Duration = Duration::from_millis(10);
193const RETRY_SLEEP_MAX_MS: Duration = Duration::from_millis(5000);
194
195/// Wrapper around [`IBitcoindRpc`] that will retry failed calls
196#[derive(Debug)]
197pub struct RetryClient<C> {
198    inner: C,
199    task_handle: TaskHandle,
200}
201
202impl<C> RetryClient<C> {
203    pub fn new(inner: C, task_handle: TaskHandle) -> Self {
204        Self { inner, task_handle }
205    }
206
207    /// Retries with an exponential backoff from `RETRY_SLEEP_MIN_MS` to
208    /// `RETRY_SLEEP_MAX_MS`
209    async fn retry_call<T, F, R>(&self, call_fn: F) -> Result<T>
210    where
211        F: Fn() -> R,
212        R: Future<Output = Result<T>>,
213    {
214        let mut retry_time = RETRY_SLEEP_MIN_MS;
215        let ret = loop {
216            match call_fn().await {
217                Ok(ret) => {
218                    break ret;
219                }
220                Err(e) => {
221                    if self.task_handle.is_shutting_down() {
222                        return Err(e);
223                    }
224
225                    info!(target: LOG_BLOCKCHAIN, "Bitcoind error {}, retrying", OptStacktrace(e));
226                    sleep(retry_time).await;
227                    retry_time = min(RETRY_SLEEP_MAX_MS, retry_time * 2);
228                }
229            }
230        };
231        Ok(ret)
232    }
233}
234
235#[apply(async_trait_maybe_send!)]
236impl<C> IBitcoindRpc for RetryClient<C>
237where
238    C: IBitcoindRpc + Sync + Send,
239{
240    async fn get_network(&self) -> Result<Network> {
241        self.retry_call(|| async { self.inner.get_network().await })
242            .await
243    }
244
245    async fn get_block_count(&self) -> Result<u64> {
246        self.retry_call(|| async { self.inner.get_block_count().await })
247            .await
248    }
249
250    async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
251        self.retry_call(|| async { self.inner.get_block_hash(height).await })
252            .await
253    }
254
255    async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>> {
256        self.retry_call(|| async { self.inner.get_fee_rate(confirmation_target).await })
257            .await
258    }
259
260    async fn submit_transaction(&self, transaction: Transaction) {
261        self.inner.submit_transaction(transaction.clone()).await;
262    }
263
264    async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>> {
265        self.retry_call(|| async { self.inner.get_tx_block_height(txid).await })
266            .await
267    }
268
269    async fn is_tx_in_block(
270        &self,
271        txid: &Txid,
272        block_hash: &BlockHash,
273        block_height: u64,
274    ) -> Result<bool> {
275        self.retry_call(|| async {
276            self.inner
277                .is_tx_in_block(txid, block_hash, block_height)
278                .await
279        })
280        .await
281    }
282
283    async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()> {
284        self.retry_call(|| async { self.inner.watch_script_history(script).await })
285            .await
286    }
287
288    async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>> {
289        self.retry_call(|| async { self.inner.get_script_history(script).await })
290            .await
291    }
292
293    async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof> {
294        self.retry_call(|| async { self.inner.get_txout_proof(txid).await })
295            .await
296    }
297
298    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
299        self.inner.get_bitcoin_rpc_config()
300    }
301}