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 }