From 27367e1cec885cff9429ce375cfd36925aa5d4f0 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:12:27 +0100 Subject: [PATCH] bitcoin: add electrum backend --- Cargo.lock | 222 +++++++++++++++- Cargo.toml | 4 + src/bitcoin/electrum/mod.rs | 259 +++++++++++++++++++ src/bitcoin/electrum/utils.rs | 98 +++++++ src/bitcoin/electrum/wallet.rs | 458 +++++++++++++++++++++++++++++++++ src/bitcoin/mod.rs | 204 ++++++++++++++- src/config.rs | 10 + src/lib.rs | 40 ++- 8 files changed, 1280 insertions(+), 15 deletions(-) create mode 100644 src/bitcoin/electrum/mod.rs create mode 100644 src/bitcoin/electrum/utils.rs create mode 100644 src/bitcoin/electrum/wallet.rs diff --git a/Cargo.lock b/Cargo.lock index 335a18d52..91059d2b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,12 +62,32 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "bdk_chain" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c601c4dc7e6c3efa538a0afbb43b964cefab9a9b5e8f352fa0ca38145448a5e7" +dependencies = [ + "bitcoin", + "miniscript", +] + [[package]] name = "bdk_coin_select" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c084bf76f0f67546fc814ffa82044144be1bb4618183a15016c162f8b087ad4" +[[package]] +name = "bdk_electrum" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28906275aeb1f71dc32045670f06c8a26fb17cc62151a99f7425d258f4bda589" +dependencies = [ + "bdk_chain", + "electrum-client", +] + [[package]] name = "bech32" version = "0.10.0-beta" @@ -139,6 +159,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.0.83" @@ -172,7 +198,24 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "electrum-client" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2" +dependencies = [ + "bitcoin", + "byteorder", + "libc", + "log", + "rustls", + "serde", + "serde_json", + "webpki-roots", + "winapi", ] [[package]] @@ -268,6 +311,7 @@ version = "6.0.0" dependencies = [ "backtrace", "bdk_coin_select", + "bdk_electrum", "bip39", "dirs", "fern", @@ -438,6 +482,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.30.0" @@ -458,12 +517,44 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.28.0" @@ -521,6 +612,12 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "syn" version = "2.0.46" @@ -591,6 +688,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "vcpkg" version = "0.2.15" @@ -609,13 +712,50 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -624,13 +764,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -639,42 +795,90 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index b3ce9f770..31fc8f63a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,10 @@ miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] } # Coin selection algorithms for spend transaction creation. bdk_coin_select = "0.3" +# For Electrum backend. This is the latest version with the same bitcoin version as +# the miniscript dependency. +bdk_electrum = { version = "0.14" } + # Don't reinvent the wheel dirs = "5.0" diff --git a/src/bitcoin/electrum/mod.rs b/src/bitcoin/electrum/mod.rs new file mode 100644 index 000000000..b52a09ee1 --- /dev/null +++ b/src/bitcoin/electrum/mod.rs @@ -0,0 +1,259 @@ +use std::collections::{HashMap, HashSet}; + +use bdk_electrum::{ + bdk_chain::{ + bitcoin::{self, hashes::Hash, BlockHash, OutPoint}, + local_chain::LocalChain, + spk_client::SyncRequest, + }, + electrum_client::{Client, Config, ElectrumApi}, + ElectrumExt, +}; +use miniscript::bitcoin::bip32::ChildNumber; + +mod utils; +mod wallet; +use crate::{ + bitcoin::{expected_genesis_hash, Block, BlockChainTip, Coin, MempoolEntry}, + config, + descriptors::LianaDescriptor, +}; +use utils::{ + block_hash, block_id_from_tip, chain_tip, height_usize_from_i32, mempool_entry_from_graph, +}; + +/// An error in the Electrum interface. +#[derive(Debug)] +pub enum ElectrumError { + Server(bdk_electrum::electrum_client::Error), + GenesisHashMismatch( + BlockHash, /*expected hash*/ + BlockHash, /*actual hash*/ + ), + BdkWallet(String), +} + +impl std::fmt::Display for ElectrumError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ElectrumError::Server(e) => write!(f, "Electrum error: '{}'.", e), + ElectrumError::GenesisHashMismatch(expected_hash, actual_hash) => { + write!( + f, + "Genesis hash mismatch. The genesis hash is expected to be '{}' but was found to be '{}'.", + expected_hash, actual_hash + ) + } + ElectrumError::BdkWallet(e) => write!(f, "BDK wallet error: '{}'.", e), + } + } +} + +/// Interface for Electrum backend. +pub struct Electrum { + client: ElectrumClient, + bdk_wallet: wallet::BdkWallet, +} + +impl Electrum { + pub fn new( + electrum_config: &config::ElectrumConfig, + network: &bitcoin::Network, + main_descriptor: &LianaDescriptor, + ) -> Result { + let client = ElectrumClient::new(&electrum_config.addr.to_string(), network)?; + let genesis_hash = client.genesis_block().hash; + let bdk_wallet = wallet::BdkWallet::new(main_descriptor, genesis_hash); + + Ok(Self { client, bdk_wallet }) + } + + pub fn client(&self) -> &ElectrumClient { + &self.client + } + + pub fn wallet_coins(&self, outpoints: Option<&[OutPoint]>) -> HashMap { + self.bdk_wallet.coins(outpoints) + } + + pub fn init_wallet( + &mut self, + tip: &BlockChainTip, + coins: &[Coin], + txs: &[bitcoin::Transaction], + receive_index: ChildNumber, + change_index: ChildNumber, + ) { + self.bdk_wallet + .init(tip, coins, txs, receive_index, change_index) + } + + pub fn sync_wallet( + &mut self, + receive_index: ChildNumber, + change_index: ChildNumber, + ) -> Result<(), ElectrumError> { + self.bdk_wallet + .sync_with_electrum(&self.client.0, receive_index, change_index) + .map_err(ElectrumError::Server) + } + + pub fn common_ancestor(&self, tip: &BlockChainTip) -> Option { + self.bdk_wallet + .common_ancestor_with_electrum(&self.client.0, tip) + } + + pub fn wallet_transaction( + &self, + txid: &bitcoin::Txid, + ) -> Option<(bitcoin::Transaction, Option)> { + self.bdk_wallet.get_transaction(txid) + } +} + +// If Electrum takes more than 3 minutes to answer one of our queries, fail. +const RPC_SOCKET_TIMEOUT: u8 = 180; + +// Number of retries while communicating with the Electrum server. +// A retry happens with exponential back-off (base 2) so this makes us give up after (1+2+4+8+16+32=) 63 seconds. +const RETRY_LIMIT: u8 = 6; + +pub struct ElectrumClient(Client); + +impl ElectrumClient { + /// Create a new client and perform sanity checks. + pub fn new(url: &str, network: &bitcoin::Network) -> Result { + let config = Config::builder() + .retry(RETRY_LIMIT) + .timeout(Some(RPC_SOCKET_TIMEOUT)) + .build(); + let client = bdk_electrum::electrum_client::Client::from_config(url, config) + .map_err(ElectrumError::Server)?; + let ele_client = Self(client); + ele_client.sanity_checks(network)?; + Ok(ele_client) + } + + fn sanity_checks(&self, network: &bitcoin::Network) -> Result<(), ElectrumError> { + let server_features = self.0.server_features().map_err(ElectrumError::Server)?; + log::debug!("{:?}", server_features); + let server_hash = { + let mut hash = server_features.genesis_hash; + hash.reverse(); + BlockHash::from_byte_array(hash) + }; + let expected_hash = expected_genesis_hash(network); + if server_hash != expected_hash { + return Err(ElectrumError::GenesisHashMismatch( + expected_hash, + server_hash, + )); + } + Ok(()) + } + + pub fn chain_tip(&self) -> BlockChainTip { + chain_tip(&self.0) + } + + pub fn block_hash(&self, height: i32) -> Option { + block_hash(&self.0, height) + } + + pub fn is_in_chain(&self, tip: &BlockChainTip) -> bool { + self.block_hash(tip.height) + .map(|bh| bh == tip.hash) + .unwrap_or(false) + } + + pub fn genesis_block_timestamp(&self) -> u32 { + self.0 + .block_header(0) + .expect("Genesis block must always be there") + .time + } + + pub fn genesis_block(&self) -> BlockChainTip { + let hash = self + .0 + .block_header(0) + .expect("Genesis block hash must always be there") + .block_hash(); + BlockChainTip { hash, height: 0 } + } + + pub fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result { + self.0 + .transaction_broadcast(tx) + .map_err(ElectrumError::Server) + } + + pub fn tip_time(&self) -> Option { + let tip_height = self.chain_tip().height; + self.0 + .block_header(height_usize_from_i32(tip_height)) + .ok() + .map(|bh| bh.time) + } + + // FIXME: We need to get ancestors & descendants. + /// Get mempool entry. + pub fn mempool_entry( + &self, + txid: &bitcoin::Txid, + ) -> Result, ElectrumError> { + let chain_tip = self.chain_tip(); + let mut local_chain = LocalChain::from_genesis_hash(self.genesis_block().hash).0; + if chain_tip.height > 0 { + let _ = local_chain + .insert_block(block_id_from_tip(chain_tip)) + .expect("only contains genesis block"); + } + let request = SyncRequest::from_chain_tip(local_chain.tip()).chain_txids(vec![*txid]); + let sync_result = self + .0 + .sync(request, 10, true) + .map_err(ElectrumError::Server)? + .with_confirmation_time_height_anchor(&self.0) + .map_err(ElectrumError::Server)?; + let graph = sync_result.graph_update; + let entry = mempool_entry_from_graph(&graph, &local_chain, txid); + Ok(entry) + } + + // FIXME: We need to get ancestors & descendants. + /// Get mempool spenders of the given outpoints. + pub fn mempool_spenders( + &self, + outpoints: &[bitcoin::OutPoint], + ) -> Result, ElectrumError> { + let chain_tip = self.chain_tip(); + let mut local_chain = LocalChain::from_genesis_hash(self.genesis_block().hash).0; + if chain_tip.height > 0 { + let _ = local_chain + .insert_block(block_id_from_tip(chain_tip)) + .expect("only contains genesis block"); + } + let request = + SyncRequest::from_chain_tip(local_chain.tip()).chain_outpoints(outpoints.to_vec()); + let sync_result = self + .0 + .sync(request, 10, true) + .map_err(ElectrumError::Server)? + .with_confirmation_time_height_anchor(&self.0) + .map_err(ElectrumError::Server)?; + let graph = sync_result.graph_update; + let txids: HashSet<_> = outpoints + .iter() + .flat_map(|op| graph.outspends(*op)) + .collect(); + let mut entries = Vec::new(); + for txid in txids { + let entry = mempool_entry_from_graph(&graph, &local_chain, txid); + if let Some(entry) = entry { + entries.push(entry); + } + } + Ok(entries) + } +} diff --git a/src/bitcoin/electrum/utils.rs b/src/bitcoin/electrum/utils.rs new file mode 100644 index 000000000..49284d8b0 --- /dev/null +++ b/src/bitcoin/electrum/utils.rs @@ -0,0 +1,98 @@ +use std::convert::TryInto; + +use bdk_electrum::{ + bdk_chain::{ + bitcoin, local_chain::LocalChain, BlockId, ChainPosition, ConfirmationTimeHeightAnchor, + TxGraph, + }, + electrum_client::{Client, ElectrumApi, HeaderNotification}, +}; + +use crate::bitcoin::{BlockChainTip, BlockInfo, MempoolEntry, MempoolEntryFees}; + +pub fn height_u32_from_i32(height: i32) -> u32 { + height.try_into().expect("height must fit into u32") +} + +pub fn height_i32_from_u32(height: u32) -> i32 { + height.try_into().expect("height must fit into i32") +} + +pub fn height_i32_from_usize(height: usize) -> i32 { + height.try_into().expect("height must fit into i32") +} + +pub fn height_usize_from_i32(height: i32) -> usize { + height.try_into().expect("height must fit into usize") +} + +pub fn height_usize_from_u32(height: u32) -> usize { + height.try_into().expect("height must fit into usize") +} + +pub fn block_id_from_tip(tip: BlockChainTip) -> BlockId { + BlockId { + height: height_u32_from_i32(tip.height), + hash: tip.hash, + } +} + +pub fn block_info_from_anchor(anchor: ConfirmationTimeHeightAnchor) -> BlockInfo { + BlockInfo { + height: height_i32_from_u32(anchor.confirmation_height), + time: anchor + .confirmation_time + .try_into() + .expect("u32 by consensus"), + } +} + +// FIXME: need to get ancestors & descendants +pub fn mempool_entry_from_graph( + graph: &TxGraph, + local_chain: &LocalChain, + txid: &bitcoin::Txid, +) -> Option { + // Return an entry only if the tx is unconfirmed. + let entry = if let Some(ChainPosition::Unconfirmed(_)) = + graph.get_chain_position(local_chain, local_chain.tip().block_id(), *txid) + { + graph.get_tx(*txid).map(|tx| { + let vsize: u64 = tx.vsize().try_into().expect("vsize must fit in u64"); + let fee = bitcoin::Amount::from_sat( + graph.calculate_fee(&tx).expect("we have all prev txouts"), + ); + let fees = MempoolEntryFees { + base: fee, + ancestor: fee, + descendant: fee, + }; + MempoolEntry { + vsize, + ancestor_vsize: vsize, + fees, + } + }) + } else { + None + }; + entry +} + +pub fn chain_tip(client: &Client) -> BlockChainTip { + let HeaderNotification { height, .. } = client.block_headers_subscribe().expect("must succeed"); + let new_tip_height = height_i32_from_usize(height); + let new_tip_hash = block_hash(client, new_tip_height).expect("we just fetched this height"); + BlockChainTip { + height: new_tip_height, + hash: new_tip_hash, + } +} + +pub fn block_hash(client: &Client, height: i32) -> Option { + let hash = client + .block_header(height_usize_from_i32(height)) + .ok()? + .block_hash(); + Some(hash) +} diff --git a/src/bitcoin/electrum/wallet.rs b/src/bitcoin/electrum/wallet.rs new file mode 100644 index 000000000..692d7561d --- /dev/null +++ b/src/bitcoin/electrum/wallet.rs @@ -0,0 +1,458 @@ +use std::{ + collections::{BTreeMap, HashMap}, + convert::TryInto, + sync::Arc, +}; + +use bdk_electrum::{ + bdk_chain::{ + bitcoin::{self, bip32, BlockHash, OutPoint, ScriptBuf, TxOut}, + keychain::KeychainTxOutIndex, + local_chain::{CheckPoint, LocalChain}, + miniscript::{Descriptor, DescriptorPublicKey}, + spk_client::{FullScanRequest, SyncRequest}, + tx_graph::{self, TxGraph}, + BlockId, ChainPosition, ConfirmationTimeHeightAnchor, IndexedTxGraph, + }, + electrum_client::{Client, ElectrumApi, Error}, + ElectrumExt, +}; +use miniscript::bitcoin::bip32::ChildNumber; + +use super::utils::{ + block_id_from_tip, block_info_from_anchor, chain_tip, height_i32_from_u32, height_u32_from_i32, + height_usize_from_u32, +}; +use crate::{ + bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY}, + descriptors::LianaDescriptor, +}; + +// TODO: Move and reuse `liana::database::sqlite::utils::LOOK_AHEAD_LIMIT`? +const LOOK_AHEAD_LIMIT: u32 = 200; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum KeychainType { + Receive, + Change, +} + +pub struct BdkWallet { + graph: IndexedTxGraph>, + local_chain: LocalChain, + // Store descriptors for use when getting SPKs. + receive_desc: Descriptor, + change_desc: Descriptor, +} + +impl BdkWallet { + /// Create a new BDK wallet using existing data from the database. + pub fn new(main_descriptor: &LianaDescriptor, genesis_hash: BlockHash) -> Self { + let local_chain = LocalChain::from_genesis_hash(genesis_hash).0; + + let receive_desc = main_descriptor + .receive_descriptor() + .as_descriptor_public_key(); + + let change_desc = main_descriptor + .change_descriptor() + .as_descriptor_public_key(); + + BdkWallet { + graph: { + let mut indexer = KeychainTxOutIndex::::new(LOOK_AHEAD_LIMIT); + let _ = indexer.insert_descriptor(KeychainType::Receive, receive_desc.clone()); + let _ = indexer.insert_descriptor(KeychainType::Change, change_desc.clone()); + IndexedTxGraph::new(indexer) + }, + local_chain, + receive_desc: receive_desc.clone(), + change_desc: change_desc.clone(), + } + } + + /// Initialize a new BDK wallet from the given data. + pub fn init( + &mut self, + tip: &BlockChainTip, + coins: &[Coin], + txs: &[bitcoin::Transaction], + receive_index: ChildNumber, + change_index: ChildNumber, + ) { + // This should only be run once before any syncing has been done. + if self.local_chain.tip().height() > 0 { + return; + } + log::debug!("initializing BDK wallet as of tip: {tip}"); + // This will be our anchor for any confirmed transactions. + let anchor_block = block_id_from_tip(*tip); + if tip.height > 0 { + log::debug!("inserting block into local chain: {:?}", anchor_block); + let _ = self + .local_chain + .insert_block(anchor_block) + .expect("local chain only contains genesis block"); + } + log::debug!("Number of coins to load: {}.", coins.len()); + log::debug!("Number of txs to load: {}.", txs.len()); + + // Update the last used derivation index for both change and receive addresses. + log::debug!( + "revealing SPKs up to receive index {receive_index} and change index {change_index}" + ); + self.reveal_spks(receive_index, change_index); + + // Update the existing coins and transactions information using a TxGraph changeset. + let mut graph_cs = tx_graph::ChangeSet::default(); + for tx in txs { + graph_cs.txs.insert(Arc::new(tx.clone())); + } + for coin in coins { + // First of all insert the txout itself. + let script_pubkey = self.get_spk(coin.derivation_index, coin.is_change); + let txout = TxOut { + script_pubkey, + value: coin.amount, + }; + graph_cs.txouts.insert(coin.outpoint, txout); + // If the coin's deposit transaction is confirmed, tell BDK by inserting an anchor. + // Otherwise, we could insert a last seen timestamp but we don't have such data stored in + // the table. + if let Some(block) = coin.block_info { + graph_cs.anchors.insert(( + ConfirmationTimeHeightAnchor { + confirmation_height: height_u32_from_i32(block.height), + confirmation_time: block.time.into(), + anchor_block, + }, + coin.outpoint.txid, + )); + } + // If the coin's spending transaction is confirmed, do the same. + if let Some(block) = coin.spend_block { + let spend_txid = coin.spend_txid.expect("Must be present if confirmed."); + graph_cs.anchors.insert(( + ConfirmationTimeHeightAnchor { + confirmation_height: height_u32_from_i32(block.height), + confirmation_time: block.time.into(), + anchor_block, + }, + spend_txid, + )); + } + } + let mut graph = TxGraph::default(); + graph.apply_changeset(graph_cs); + let _ = self.graph.apply_update(graph); + } + + /// Reveal SPKs based on derivation indices set in DB. + fn reveal_spks(&mut self, receive_index: ChildNumber, change_index: ChildNumber) { + let mut last_active_indices = BTreeMap::new(); + last_active_indices.insert(KeychainType::Receive, receive_index.into()); + last_active_indices.insert(KeychainType::Change, change_index.into()); + let _ = self + .graph + .index + .reveal_to_target_multi(&last_active_indices); + } + + fn get_spk(&self, der_index: bip32::ChildNumber, is_change: bool) -> ScriptBuf { + // Try to get it from the BDK wallet cache first, failing that derive it from the appropriate + // descriptor. + let chain_kind = if is_change { + KeychainType::Change + } else { + KeychainType::Receive + }; + if let Some(spk) = self.graph.index.spk_at_index(chain_kind, der_index.into()) { + spk.to_owned() + } else { + let desc = if is_change { + &self.change_desc + } else { + &self.receive_desc + }; + desc.at_derivation_index(der_index.into()) + .expect("Not multipath and index isn't hardened.") + .script_pubkey() + } + } + + /// Get the coins currently stored by the `BdkWallet` optionally filtered by `outpoints`. + /// If `outpoints` is `None`, no filter will be applied. + /// If `outpoints` is an empty slice, no coins will be returned. + pub fn coins(&self, outpoints: Option<&[bitcoin::OutPoint]>) -> HashMap { + // Get an iterator over all the wallet txos (not only the currently unspent ones) by using + // lower level methods. + let tx_graph = self.graph.graph(); + let txo_index = &self.graph.index; + let tip_id = self.local_chain.tip().block_id(); + let wallet_txos = + tx_graph.filter_chain_txouts(&self.local_chain, tip_id, txo_index.outpoints()); + let mut wallet_coins = HashMap::new(); + // Go through all the wallet txos and create a coin for each. + for ((k, i), full_txo) in wallet_txos { + let outpoint = full_txo.outpoint; + if outpoints.map(|ops| !ops.contains(&outpoint)) == Some(true) { + continue; + } + let amount = full_txo.txout.value; + let derivation_index = i.into(); + let is_change = matches!(k, KeychainType::Change); + let block_info = match full_txo.chain_position { + ChainPosition::Unconfirmed(_) => None, + ChainPosition::Confirmed(anchor) => Some(block_info_from_anchor(anchor)), + }; + + // Immature if from a coinbase transaction with less than a hundred confs. + let is_immature = full_txo.is_on_coinbase + && block_info + .and_then(|blk| { + let tip_height: i32 = height_i32_from_u32(tip_id.height); + tip_height + .checked_sub(blk.height) + .map(|confs| confs < COINBASE_MATURITY) + }) + .unwrap_or(true); + + // Get spend status of this coin. + let (mut spend_txid, mut spend_block) = (None, None); + if let Some((spend_pos, txid)) = full_txo.spent_by { + spend_txid = Some(txid); + spend_block = match spend_pos { + ChainPosition::Unconfirmed(_) => None, + ChainPosition::Confirmed(anchor) => Some(block_info_from_anchor(anchor)), + }; + } + let coin = crate::bitcoin::Coin { + outpoint, + amount, + derivation_index, + is_change, + is_immature, + block_info, + spend_txid, + spend_block, + }; + wallet_coins.insert(coin.outpoint, coin); + } + wallet_coins + } + + pub fn get_transaction( + &self, + txid: &bitcoin::Txid, + ) -> Option<(bitcoin::Transaction, Option)> { + self.graph.graph().get_tx_node(*txid).map(|tx_node| { + let block = tx_node.anchors.first().map(|info| Block { + hash: info.anchor_block.hash, + height: height_i32_from_u32(info.confirmation_height), + time: info.confirmation_time.try_into().expect("u32 by consensus"), + }); + let tx = tx_node.tx.as_ref().clone(); + (tx, block) + }) + } + + /// Sync the wallet with the Electrum server. + pub fn sync_with_electrum( + &mut self, + client: &Client, + receive_index: ChildNumber, + change_index: ChildNumber, + ) -> Result<(), Error> { + self.reveal_spks(receive_index, change_index); + let chain_tip = self.local_chain.tip(); + log::debug!( + "local chain tip height before sync with electrum: {}", + chain_tip.block_id().height + ); + + const BATCH_SIZE: usize = 200; + // We'll only need to calculate fees of mempool transactions and this will be done separately from our graph + // so we don't need to fetch prev txouts. In any case, we'll already have these for our own transactions. + const FETCH_PREV_TXOUTS: bool = false; + const STOP_GAP: usize = 50; + + let (chain_update, mut graph_update, keychain_update) = if chain_tip.height() > 0 { + log::info!("Performing sync."); + let mut request = + SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(self.graph.graph()); + + let all_spks: Vec<_> = self + .graph + .index + .inner() // we include lookahead SPKs + .all_spks() + .iter() + .map(|(_, script)| script.clone()) + .collect(); + request = request.chain_spks(all_spks); + log::debug!("num SPKs for sync: {}", request.spks.len()); + + let sync_result = client + .sync(request, BATCH_SIZE, FETCH_PREV_TXOUTS)? + .with_confirmation_time_height_anchor(client)?; + (sync_result.chain_update, sync_result.graph_update, None) + } else { + log::info!("Performing full scan."); + let mut request = FullScanRequest::from_chain_tip(chain_tip.clone()) + .cache_graph_txs(self.graph.graph()); + + for (k, spks) in self.graph.index.all_unbounded_spk_iters() { + request = request.set_spks_for_keychain(k, spks); + } + let scan_result = client + .full_scan(request, STOP_GAP, BATCH_SIZE, FETCH_PREV_TXOUTS)? + .with_confirmation_time_height_anchor(client)?; + ( + scan_result.chain_update, + scan_result.graph_update, + Some(scan_result.last_active_indices), + ) + }; + if let Some(keychain_update) = keychain_update { + let _ = self.graph.index.reveal_to_target_multi(&keychain_update); + } + log::debug!( + "local chain tip height after sync with electrum: {}", + chain_update.height() + ); + let _ = self + .local_chain + .apply_update(chain_update) + .expect("update connects to local chain"); + + // Unconfirmed transactions have their last seen as 0, so we override to the current time + // so that conflicts can be properly handled. + let now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("must be greater than unix epoch") + .as_secs(); + + for tx in &graph_update.initial_changeset().txs { + let txid = tx.txid(); + if let Some(ChainPosition::Unconfirmed(last_seen)) = graph_update.get_chain_position( + &self.local_chain, + self.local_chain.tip().block_id(), + txid, + ) { + let prev_last_seen = if let Some(ChainPosition::Unconfirmed(last_seen)) = self + .graph + .graph() + .get_chain_position(&self.local_chain, self.local_chain.tip().block_id(), txid) + { + last_seen + } else { + last_seen + }; + log::debug!( + "changing last seen for txid '{}' from {} to {}", + txid, + prev_last_seen, + now + ); + let _ = graph_update.insert_seen_at(txid, now); + } + } + let _ = self.graph.apply_update(graph_update); + Ok(()) + } + + /// Get the block that `tip` has in common with the Electrum server + /// based on the wallet's checkpoints. + pub fn common_ancestor_with_electrum( + &self, + client: &Client, + tip: &BlockChainTip, + ) -> Option { + let server_tip_height = chain_tip(client).height as u32; + let tip_block = BlockId { + hash: tip.hash, + height: height_u32_from_i32(tip.height), + }; + // Get a local chain that includes all our checkpoints up to and including `tip`. + // Typically, the local chain's tip should be the same as `tip`, but the local chain's tip + // may have advanced slightly in case the Electrum tip changed while performing a round of + // updates and we restarted. + let local_chain = { + let mut chain = self.local_chain.clone(); + // We want to disconnect all checkpoints *after* `tip`, but `disconnect_from` is inclusive. + // So call `disconnect_from` and then re-insert `tip`. + + // We can only get an error if `tip` is the genesis block, but we should never + // be trying to find the common ancestor of the genesis block. + let _ = chain + .disconnect_from(tip_block) + .expect("we should not be trying to find common ancestor with genesis block"); + let _ = chain + .insert_block(tip_block) + .expect("we have already removed this block from chain"); + chain + }; + // The following code is based on the function `construct_update_tip`. See: + // https://github.com/bitcoindevkit/bdk/blob/4a8452f9b8f8128affbb60665016fedb48f07cd6/crates/electrum/src/electrum_ext.rs#L284 + // TODO: Is the following comment and code correct in our case? Could the electrum tip be lower and a reorged chain? + + // If electrum returns a tip height that is lower than our previous tip, then checkpoints do + // not need updating. We just return the previous tip and use that as the point of agreement. + // if new_tip_height < prev_tip.height() { + // return Ok((prev_tip.clone(), Some(prev_tip.height()))); + // } + + const CHAIN_SUFFIX_LENGTH: u32 = 8; + // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this + // to construct our checkpoint update. + let mut new_blocks = { + let start_height = server_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1); + let hashes = client + .block_headers( + height_usize_from_u32(start_height), + CHAIN_SUFFIX_LENGTH as _, + ) + .ok()? + .headers + .into_iter() + .map(|h| h.block_hash()); + (start_height..).zip(hashes).collect::>() + }; + + // Find the "point of agreement" (if any). + let agreement_cp = { + let mut agreement_cp = Option::::None; + for cp in local_chain + .tip() + .iter() + .filter(|cp| cp.height() <= server_tip_height) + { + let cp_block = cp.block_id(); + let hash = match new_blocks.get(&cp_block.height) { + Some(&hash) => hash, + None => { + assert!( + cp_block.height <= server_tip_height, + "already checked that server tip cannot be smaller" + ); + let hash = client + .block_header(height_usize_from_u32(cp_block.height)) + .ok()? + .block_hash(); + new_blocks.insert(cp_block.height, hash); + hash + } + }; + if hash == cp_block.hash { + agreement_cp = Some(cp); + break; + } + } + agreement_cp + }; + agreement_cp.as_ref().map(|cp| BlockChainTip { + height: height_i32_from_u32(cp.height()), + hash: cp.hash(), + }) + } +} diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index 18bbe66f9..7cbb7b424 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -3,6 +3,7 @@ //! Broadcast transactions, poll for new unspent coins, gather fee estimates. pub mod d; +pub mod electrum; pub mod poller; use crate::{ @@ -11,7 +12,7 @@ use crate::{ }; pub use d::{MempoolEntry, MempoolEntryFees, SyncProgress}; -use std::{fmt, sync}; +use std::{fmt, str::FromStr, sync}; use miniscript::bitcoin::{self, address, bip32::ChildNumber}; @@ -20,6 +21,27 @@ type SpentCoin = (bitcoin::OutPoint, bitcoin::Txid, i32, u32); const COINBASE_MATURITY: i32 = 100; +// See https://github.com/bitcoin/bitcoin/blob/v27.1/src/kernel/chainparams.cpp. +/// The expected genesis block hash. +pub fn expected_genesis_hash(network: &bitcoin::Network) -> bitcoin::BlockHash { + let hash = match network { + bitcoin::Network::Bitcoin => { + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + } + bitcoin::Network::Signet => { + "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6" + } + bitcoin::Network::Testnet => { + "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" + } + bitcoin::Network::Regtest => { + "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" + } + net => panic!("Unexpected network '{}'", net), + }; + bitcoin::BlockHash::from_str(hash).expect("must be valid") +} + /// Information about a block #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub struct Block { @@ -430,6 +452,186 @@ impl BitcoinInterface for d::BitcoinD { } } +impl BitcoinInterface for electrum::Electrum { + fn has_wallet_to_initialize(&self) -> bool { + true + } + + fn init_wallet( + &mut self, + tip: &BlockChainTip, + coins: &[Coin], + txs: &[bitcoin::Transaction], + receive_index: ChildNumber, + change_index: ChildNumber, + ) { + self.init_wallet(tip, coins, txs, receive_index, change_index) + } + + fn sync_wallet( + &mut self, + receive_index: ChildNumber, + change_index: ChildNumber, + ) -> Result<(), String> { + self.sync_wallet(receive_index, change_index) + .map_err(|e| e.to_string()) + } + + fn received_coins( + &self, + tip: &BlockChainTip, + _descs: &[descriptors::SinglePathLianaDesc], + ) -> Vec { + // Get those wallet coins that are either unconfirmed or have a confirmation height + // after tip. The poller will then discard any that had already been received. + self.wallet_coins(None) + .values() + .filter_map(|c| { + let height = c.block_info.map(|info| info.height); + if height.filter(|h| *h <= tip.height).is_some() { + None + } else { + Some(UTxO { + outpoint: c.outpoint, + block_height: height, + amount: c.amount, + address: UTxOAddress::DerivIndex(c.derivation_index, c.is_change), + is_immature: c.is_immature, + }) + } + }) + .collect() + } + + fn confirmed_coins( + &self, + outpoints: &[bitcoin::OutPoint], + ) -> (Vec<(bitcoin::OutPoint, i32, u32)>, Vec) { + let wallet_coins = &self.wallet_coins(Some(outpoints)); + let mut confirmed = Vec::new(); + let mut expired = Vec::new(); + for op in outpoints { + if let Some(w_c) = wallet_coins.get(op) { + if let Some(block) = w_c.block_info { + confirmed.push((w_c.outpoint, block.height, block.time)); + } + } else { + expired.push(*op); + } + } + (confirmed, expired) + } + + fn spending_coins( + &self, + outpoints: &[bitcoin::OutPoint], + ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> { + let wallet_coins = &self.wallet_coins(Some(outpoints)); + outpoints + .iter() + .filter_map(|op| { + if let Some(w_c) = wallet_coins.get(op) { + w_c.spend_txid.map(|txid| (w_c.outpoint, txid)) + } else { + None + } + }) + .collect() + } + + fn spent_coins( + &self, + outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)], + ) -> (Vec, Vec) { + let ops: Vec<_> = outpoints.iter().map(|(op, _)| op).copied().collect(); + let wallet_coins = &self.wallet_coins(Some(&ops)); + let mut spent = Vec::new(); + let mut expired_spending = Vec::new(); + + for (op, spend_txid) in outpoints { + if let Some(w_c) = wallet_coins.get(op) { + if w_c.spend_txid != Some(*spend_txid) { + expired_spending.push(*op); + } + if let Some(block) = w_c.spend_block { + spent.push((*op, *spend_txid, block.height, block.time)); + } + } + } + (spent, expired_spending) + } + + fn genesis_block_timestamp(&self) -> u32 { + self.client().genesis_block_timestamp() + } + + fn genesis_block(&self) -> BlockChainTip { + self.client().genesis_block() + } + + fn chain_tip(&self) -> BlockChainTip { + self.client().chain_tip() + } + + fn is_in_chain(&self, tip: &BlockChainTip) -> bool { + self.client().is_in_chain(tip) + } + + fn common_ancestor(&self, tip: &BlockChainTip) -> Option { + self.common_ancestor(tip) + } + + fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result<(), String> { + match self.client().broadcast_tx(tx) { + Ok(_txid) => Ok(()), + Err(e) => Err(e.to_string()), + } + } + + fn wallet_transaction( + &self, + txid: &bitcoin::Txid, + ) -> Option<(bitcoin::Transaction, Option)> { + self.wallet_transaction(txid) + } + + fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option { + self.client().mempool_entry(txid).unwrap_or(None) + } + + fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec { + self.client() + .mempool_spenders(outpoints) + .unwrap_or_default() + } + + fn sync_progress(&self) -> SyncProgress { + // FIXME + let blocks = self.chain_tip().height as u64; + SyncProgress::new(1.0, blocks, blocks) + } + + fn start_rescan( + &self, + _desc: &descriptors::LianaDescriptor, + _timestamp: u32, + ) -> Result<(), String> { + todo!() + } + + fn rescan_progress(&self) -> Option { + None + } + + fn block_before_date(&self, _timestamp: u32) -> Option { + unimplemented!("db should not be marked as rescanning") + } + + fn tip_time(&self) -> Option { + self.client().tip_time() + } +} + // FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way? impl BitcoinInterface for sync::Arc> { fn genesis_block_timestamp(&self) -> u32 { diff --git a/src/config.rs b/src/config.rs index 6312b9e46..cf7dc63c0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -91,6 +91,9 @@ pub enum BitcoinBackend { /// Settings specific to bitcoind as the Bitcoin interface. #[serde(rename = "bitcoind_config")] Bitcoind(BitcoindConfig), + /// Settings specific to Electrum as the Bitcoin interface. + #[serde(rename = "electrum_config")] + Electrum(ElectrumConfig), } /// RPC authentication options. @@ -123,6 +126,13 @@ pub struct BitcoindConfig { pub addr: SocketAddr, } +/// Everything we need to know for talking to Electrum serenely. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ElectrumConfig { + /// The IP:port Electrum's RPC is listening on. + pub addr: SocketAddr, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BitcoinConfig { /// The network we are operating on, one of "bitcoin", "testnet", "regtest", "signet" diff --git a/src/lib.rs b/src/lib.rs index fa30706ac..b8f99a9e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,10 @@ mod testutils; pub use bip39; pub use miniscript; -pub use crate::bitcoin::d::{BitcoinD, BitcoindError, WalletError}; +pub use crate::bitcoin::{ + d::{BitcoinD, BitcoindError, WalletError}, + electrum::{Electrum, ElectrumError}, +}; #[cfg(feature = "daemon")] use crate::jsonrpc::server::{rpcserver_loop, rpcserver_setup}; use crate::{ @@ -92,10 +95,12 @@ pub enum StartupError { DefaultDataDirNotFound, DatadirCreation(path::PathBuf, io::Error), MissingBitcoindConfig, + MissingElectrumConfig, MissingBitcoinBackendConfig, DbMigrateBitcoinTxs(&'static str), Database(SqliteDbError), Bitcoind(BitcoindError), + Electrum(ElectrumError), #[cfg(unix)] Daemonization(&'static str), #[cfg(windows)] @@ -118,6 +123,10 @@ impl fmt::Display for StartupError { f, "Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration." ), + Self::MissingElectrumConfig => write!( + f, + "Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration." + ), Self::MissingBitcoinBackendConfig => write!( f, "No Bitcoin backend entry in the configuration." @@ -128,6 +137,7 @@ impl fmt::Display for StartupError { ), Self::Database(e) => write!(f, "Error initializing database: '{}'.", e), Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e), + Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e), #[cfg(unix)] Self::Daemonization(e) => write!(f, "Error when daemonizing: '{}'.", e), #[cfg(windows)] @@ -265,10 +275,10 @@ fn setup_bitcoind( #[cfg(target_os = "windows")] let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", ""); - let config::BitcoinBackend::Bitcoind(bitcoind_config) = config - .bitcoin_backend - .as_ref() - .ok_or(StartupError::MissingBitcoindConfig)?; + let bitcoind_config = match config.bitcoin_backend.as_ref() { + Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config, + _ => Err(StartupError::MissingBitcoindConfig)?, + }; let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?; bitcoind.node_sanity_checks( config.bitcoin_config.network, @@ -292,6 +302,22 @@ fn setup_bitcoind( Ok(bitcoind) } +// Connect to Electrum and do some sanity checks. Then create a BDK-based wallet. +// If all went well, returns the interface to Electrum. +fn setup_electrum(config: &Config) -> Result { + let electrum_config = match config.bitcoin_backend.as_ref() { + Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config, + _ => Err(StartupError::MissingElectrumConfig)?, + }; + let electrum = Electrum::new( + electrum_config, + &config.bitcoin_config.network, + &config.main_descriptor, + ) + .map_err(StartupError::Electrum)?; + Ok(electrum) +} + #[derive(Clone)] pub struct DaemonControl { config: Config, @@ -407,6 +433,10 @@ impl DaemonHandle { sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")), ) as sync::Arc>, + (None, Some(config::BitcoinBackend::Electrum(..))) => { + sync::Arc::from(sync::Mutex::from(setup_electrum(&config)?)) + as sync::Arc> + } (None, None) => Err(StartupError::MissingBitcoinBackendConfig)?, };