fedimint_bitcoind/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
#![deny(clippy::pedantic)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
use std::collections::BTreeMap;
use std::env;
use std::fmt::Debug;
use std::sync::{Arc, LazyLock, Mutex};
use anyhow::{Context, Result};
use bitcoin::{Block, BlockHash, ScriptBuf, Transaction, Txid};
use fedimint_core::envs::{
BitcoinRpcConfig, FM_FORCE_BITCOIN_RPC_KIND_ENV, FM_FORCE_BITCOIN_RPC_URL_ENV,
};
use fedimint_core::txoproof::TxOutProof;
use fedimint_core::util::SafeUrl;
use fedimint_core::{apply, async_trait_maybe_send, dyn_newtype_define, Feerate};
use fedimint_logging::LOG_CORE;
use tracing::debug;
#[cfg(feature = "bitcoincore-rpc")]
pub mod bitcoincore;
#[cfg(feature = "esplora-client")]
mod esplora;
// <https://blockstream.info/api/block-height/0>
const MAINNET_GENESIS_BLOCK_HASH: &str =
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
// <https://blockstream.info/testnet/api/block-height/0>
const TESTNET_GENESIS_BLOCK_HASH: &str =
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
// <https://mempool.space/signet/api/block-height/0>
const SIGNET_GENESIS_BLOCK_HASH: &str =
"00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
// See <https://bitcoin.stackexchange.com/questions/122778/is-the-regtest-genesis-hash-always-the-same-or-not>
// <https://github.com/bitcoin/bitcoin/blob/d82283950f5ff3b2116e705f931c6e89e5fdd0be/src/kernel/chainparams.cpp#L478>
const REGTEST_GENESIS_BLOCK_HASH: &str =
"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206";
/// Global factories for creating bitcoin RPCs
static BITCOIN_RPC_REGISTRY: LazyLock<Mutex<BTreeMap<String, DynBitcoindRpcFactory>>> =
LazyLock::new(|| {
Mutex::new(BTreeMap::from([
#[cfg(feature = "esplora-client")]
("esplora".to_string(), esplora::EsploraFactory.into()),
#[cfg(feature = "bitcoincore-rpc")]
("bitcoind".to_string(), bitcoincore::BitcoindFactory.into()),
]))
});
/// Create a bitcoin RPC of a given kind
pub fn create_bitcoind(config: &BitcoinRpcConfig) -> Result<DynBitcoindRpc> {
let registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
let kind = env::var(FM_FORCE_BITCOIN_RPC_KIND_ENV)
.ok()
.unwrap_or_else(|| config.kind.clone());
let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
.ok()
.map(|s| SafeUrl::parse(&s))
.transpose()?
.unwrap_or_else(|| config.url.clone());
debug!(target: LOG_CORE, %kind, %url, "Starting bitcoin rpc");
let maybe_factory = registry.get(&kind);
let factory = maybe_factory.with_context(|| {
anyhow::anyhow!(
"{} rpc not registered, available options: {:?}",
config.kind,
registry.keys()
)
})?;
factory.create_connection(&url)
}
/// Register a new factory for creating bitcoin RPCs
pub fn register_bitcoind(kind: String, factory: DynBitcoindRpcFactory) {
let mut registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
registry.insert(kind, factory);
}
/// Trait for creating new bitcoin RPC clients
pub trait IBitcoindRpcFactory: Debug + Send + Sync {
/// Creates a new bitcoin RPC client connection
fn create_connection(&self, url: &SafeUrl) -> Result<DynBitcoindRpc>;
}
dyn_newtype_define! {
#[derive(Clone)]
pub DynBitcoindRpcFactory(Arc<IBitcoindRpcFactory>)
}
/// Trait that allows interacting with the Bitcoin blockchain
///
/// Functions may panic if the bitcoind node is not reachable.
#[apply(async_trait_maybe_send!)]
pub trait IBitcoindRpc: Debug {
/// Returns the Bitcoin network the node is connected to
async fn get_network(&self) -> Result<bitcoin::Network>;
/// Returns the current block count
async fn get_block_count(&self) -> Result<u64>;
/// Returns the block hash at a given height
///
/// # Panics
/// If the node does not know a block for that height. Make sure to only
/// query blocks of a height less to the one returned by
/// `Self::get_block_count`.
///
/// While there is a corner case that the blockchain shrinks between these
/// two calls (through on average heavier blocks on a fork) this is
/// prevented by only querying hashes for blocks tailing the chain tip
/// by a certain number of blocks.
async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
/// Estimates the fee rate for a given confirmation target. Make sure that
/// all federation members use the same algorithm to avoid widely
/// diverging results. If the node is not ready yet to return a fee rate
/// estimation this function returns `None`.
async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>>;
/// Submits a transaction to the Bitcoin network
///
/// This operation does not return anything as it never OK to consider its
/// success as final anyway. The caller should be retrying
/// broadcast periodically until it confirms the transaction was actually
/// via other means or decides that is no longer relevant.
///
/// Also - most backends considers brodcasting a tx that is already included
/// in the blockchain as an error, which breaks idempotency and requires
/// brittle workarounds just to reliably ignore... just to retry on the
/// higher level anyway.
///
/// Implementations of this error should log errors for debugging purposes
/// when it makes sense.
async fn submit_transaction(&self, transaction: Transaction);
/// If a transaction is included in a block, returns the block height.
/// Note: calling this method with bitcoind as a backend must first call
/// `watch_script_history` or run bitcoind with txindex enabled.
async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
/// Check if a transaction is included in a block
async fn is_tx_in_block(
&self,
txid: &Txid,
block_hash: &BlockHash,
block_height: u64,
) -> Result<bool>;
/// Watches for a script and returns any transactions associated with it
///
/// Should be called at least prior to transactions being submitted or
/// watching may not occur on backends that need it
/// TODO: bitcoind backend is broken
/// `<https://github.com/fedimint/fedimint/issues/5329>`
async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
/// Get script transaction history
///
/// Note: should call `watch_script_history` at least once, before calling
/// this.
async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
/// Returns a proof that a tx is included in the bitcoin blockchain
async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
/// Returns the node's estimated chain sync percentage as a float between
/// 0.0 and 1.0, or `None` if the node doesn't support this feature.
async fn get_sync_percentage(&self) -> Result<Option<f64>>;
/// Returns the Bitcoin RPC config
fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
}
dyn_newtype_define! {
#[derive(Clone)]
pub DynBitcoindRpc(Arc<IBitcoindRpc>)
}