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