From 2d83af49051f5d33105315164f3edf7f72f3741f Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 23 Mar 2022 12:00:24 +0100 Subject: [PATCH 1/3] Move testutils macro module before the others This allows using the `testuitils` macro in their tests as well --- src/lib.rs | 15 ++++++++------- src/testutils/mod.rs | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c3de5ae84..6db7103e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,6 +249,14 @@ pub extern crate sled; #[cfg(feature = "sqlite")] pub extern crate rusqlite; +// We should consider putting this under a feature flag but we need the macro in doctests so we need +// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. +// +// Stuff in here is too rough to document atm +#[doc(hidden)] +#[macro_use] +pub mod testutils; + #[allow(unused_imports)] #[macro_use] pub(crate) mod error; @@ -277,10 +285,3 @@ pub use wallet::Wallet; pub fn version() -> &'static str { env!("CARGO_PKG_VERSION", "unknown") } - -// We should consider putting this under a feature flag but we need the macro in doctests so we need -// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. -// -// Stuff in here is too rough to document atm -#[doc(hidden)] -pub mod testutils; diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs index f05c9df48..b10f1a3ba 100644 --- a/src/testutils/mod.rs +++ b/src/testutils/mod.rs @@ -267,3 +267,5 @@ macro_rules! testutils { (external, internal) }) } + +pub use testutils; From 9c405e9c70e417dea0e610f9d44e99911d6b4e44 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 15 Mar 2022 10:48:00 +0100 Subject: [PATCH 2/3] [blockchain] Add traits to reuse `Blockchain`s across multiple wallets Add two new traits: - `StatelessBlockchain` is used to tag `Blockchain`s that don't have any wallet-specic state, i.e. they can be used as-is to sync multiple wallets. - `BlockchainFactory` is a trait for objects that can build multiple blockchains for different descriptors. It's implemented automatically for every `Arc` where `T` is a `StatelessBlockchain`. This allows a piece of code that deals with multiple sub-wallets to just get a `&B: BlockchainFactory` to sync all of them. These new traits have been implemented for Electrum, Esplora and RPC (the first two being stateless and the latter having a dedicated `RpcBlockchainFactory` struct). It hasn't been implemented on the CBF blockchain, because I don't think it would work in its current form (it throws away old block filters, so it's hard to go back and rescan). This is the first step for #549, as BIP47 needs to sync many different descriptors internally. It's also very useful for #486. --- CHANGELOG.md | 1 + src/blockchain/electrum.rs | 67 +++++++++++++++- src/blockchain/esplora/reqwest.rs | 2 + src/blockchain/esplora/ureq.rs | 2 + src/blockchain/mod.rs | 103 ++++++++++++++++++++++++- src/blockchain/rpc.rs | 123 ++++++++++++++++++++++++++++-- src/testutils/mod.rs | 2 - src/wallet/mod.rs | 2 + 8 files changed, 289 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb9f31c3..22152cf4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm` - New MSRV set to `1.56` +- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`). ## [v0.18.0] - [v0.17.0] diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 0b8691bc1..f8ac758ce 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain { } } +impl StatelessBlockchain for ElectrumBlockchain {} + impl GetHeight for ElectrumBlockchain { fn get_height(&self) -> Result { // TODO: unsubscribe when added to the client, or is there a better call to use here? @@ -320,8 +322,67 @@ impl ConfigurableBlockchain for ElectrumBlockchain { #[cfg(test)] #[cfg(feature = "test-electrum")] -crate::bdk_blockchain_tests! { - fn test_instance(test_client: &TestClient) -> ElectrumBlockchain { - ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap()) +mod test { + use std::sync::Arc; + + use super::*; + use crate::database::MemoryDatabase; + use crate::testutils::blockchain_tests::TestClient; + use crate::wallet::{AddressIndex, Wallet}; + + crate::bdk_blockchain_tests! { + fn test_instance(test_client: &TestClient) -> ElectrumBlockchain { + ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap()) + } + } + + fn get_factory() -> (TestClient, Arc) { + let test_client = TestClient::default(); + + let factory = Arc::new(ElectrumBlockchain::from( + Client::new(&test_client.electrsd.electrum_url).unwrap(), + )); + + (test_client, factory) + } + + #[test] + fn test_electrum_blockchain_factory() { + let (_test_client, factory) = get_factory(); + + let a = factory.build("aaaaaa", None).unwrap(); + let b = factory.build("bbbbbb", None).unwrap(); + + assert_eq!( + a.client.block_headers_subscribe().unwrap().height, + b.client.block_headers_subscribe().unwrap().height + ); + } + + #[test] + fn test_electrum_blockchain_factory_sync_wallet() { + let (mut test_client, factory) = get_factory(); + + let db = MemoryDatabase::new(); + let wallet = Wallet::new( + "wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)", + None, + bitcoin::Network::Regtest, + db, + ) + .unwrap(); + + let address = wallet.get_address(AddressIndex::New).unwrap(); + + let tx = testutils! { + @tx ( (@addr address.address) => 50_000 ) + }; + test_client.receive(tx); + + factory + .sync_wallet(&wallet, None, Default::default()) + .unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); } } diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index 2141b8e67..f68bdd8a1 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain { } } +impl StatelessBlockchain for EsploraBlockchain {} + #[maybe_async] impl GetHeight for EsploraBlockchain { fn get_height(&self) -> Result { diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 55f1cf764..50493f9cb 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain { } } +impl StatelessBlockchain for EsploraBlockchain {} + impl GetHeight for EsploraBlockchain { fn get_height(&self) -> Result { Ok(self.url_client._get_height()?) diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 714fdf6a9..cf593c3c8 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -25,7 +25,8 @@ use bitcoin::{Transaction, Txid}; use crate::database::BatchDatabase; use crate::error::Error; -use crate::FeeRate; +use crate::wallet::{wallet_name_from_descriptor, Wallet}; +use crate::{FeeRate, KeychainKind}; #[cfg(any( feature = "electrum", @@ -164,6 +165,106 @@ pub trait ConfigurableBlockchain: Blockchain + Sized { fn from_config(config: &Self::Config) -> Result; } +/// Trait for blockchains that don't contain any state +/// +/// Statless blockchains can be used to sync multiple wallets with different descriptors. +/// +/// [`BlockchainFactory`] is automatically implemented for `Arc` where `T` is a stateless +/// blockchain. +pub trait StatelessBlockchain: Blockchain {} + +/// Trait for a factory of blockchains that share the underlying connection or configuration +#[cfg_attr( + not(feature = "async-interface"), + doc = r##" +## Example + +This example shows how to sync multiple walles and return the sum of their balances + +```no_run +# use bdk::Error; +# use bdk::blockchain::*; +# use bdk::database::*; +# use bdk::wallet::*; +# use bdk::*; +fn sum_of_balances(blockchain_factory: B, wallets: &[Wallet]) -> Result { + Ok(wallets + .iter() + .map(|w| -> Result<_, Error> { + blockchain_factory.sync_wallet(&w, None, SyncOptions::default())?; + w.get_balance() + }) + .collect::, _>>()? + .into_iter() + .sum()) +} +``` +"## +)] +pub trait BlockchainFactory { + /// The type returned when building a blockchain from this factory + type Inner: Blockchain; + + /// Build a new blockchain for the given descriptor wallet_name + /// + /// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks + /// from the factory. Since it's not possible to override the value to `None`, set it to + /// `Some(0)` to rescan from the genesis. + fn build( + &self, + wallet_name: &str, + override_skip_blocks: Option, + ) -> Result; + + /// Build a new blockchain for a given wallet + /// + /// Internally uses [`wallet_name_from_descriptor`] to derive the name, and then calls + /// [`BlockchainFactory::build`] to create the blockchain instance. + fn build_for_wallet( + &self, + wallet: &Wallet, + override_skip_blocks: Option, + ) -> Result { + let wallet_name = wallet_name_from_descriptor( + wallet.public_descriptor(KeychainKind::External)?.unwrap(), + wallet.public_descriptor(KeychainKind::Internal)?, + wallet.network(), + wallet.secp_ctx(), + )?; + self.build(&wallet_name, override_skip_blocks) + } + + /// Use [`BlockchainFactory::build_for_wallet`] to get a blockchain, then sync the wallet + /// + /// This can be used when a new blockchain would only be used to sync a wallet and then + /// immediately dropped. Keep in mind that specific blockchain factories may perform slow + /// operations to build a blockchain for a given wallet, so if a wallet needs to be synced + /// often it's recommended to use [`BlockchainFactory::build_for_wallet`] to reuse the same + /// blockchain multiple times. + #[cfg(not(any(target_arch = "wasm32", feature = "async-interface")))] + #[cfg_attr( + docsrs, + doc(cfg(not(any(target_arch = "wasm32", feature = "async-interface")))) + )] + fn sync_wallet( + &self, + wallet: &Wallet, + override_skip_blocks: Option, + sync_options: crate::wallet::SyncOptions, + ) -> Result<(), Error> { + let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?; + wallet.sync(&blockchain, sync_options) + } +} + +impl BlockchainFactory for Arc { + type Inner = Self; + + fn build(&self, _wallet_name: &str, _override_skip_blocks: Option) -> Result { + Ok(Arc::clone(self)) + } +} + /// Data sent with a progress update over a [`channel`] pub type ProgressData = (f32, Option); diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 78d166e3a..7eb059204 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -438,18 +438,127 @@ fn list_wallet_dir(client: &Client) -> Result, Error> { Ok(result.wallets.into_iter().map(|n| n.name).collect()) } +/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`] +/// +/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`] +/// objects for different wallet names and with different rescan heights. +/// +/// ## Example +/// +/// ```no_run +/// # use bdk::bitcoin::Network; +/// # use bdk::blockchain::BlockchainFactory; +/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory}; +/// # fn main() -> Result<(), Box> { +/// let factory = RpcBlockchainFactory { +/// url: "http://127.0.0.1:18332".to_string(), +/// auth: Auth::Cookie { +/// file: "/home/user/.bitcoin/.cookie".into(), +/// }, +/// network: Network::Testnet, +/// wallet_name_prefix: Some("prefix-".to_string()), +/// default_skip_blocks: 100_000, +/// }; +/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct RpcBlockchainFactory { + /// The bitcoin node url + pub url: String, + /// The bitcoin node authentication mechanism + pub auth: Auth, + /// The network we are using (it will be checked the bitcoin node network matches this) + pub network: Network, + /// The optional prefix used to build the full wallet name for blockchains + pub wallet_name_prefix: Option, + /// Default number of blocks to skip which will be inherited by blockchain unless overridden + pub default_skip_blocks: u32, +} + +impl BlockchainFactory for RpcBlockchainFactory { + type Inner = RpcBlockchain; + + fn build( + &self, + checksum: &str, + override_skip_blocks: Option, + ) -> Result { + RpcBlockchain::from_config(&RpcConfig { + url: self.url.clone(), + auth: self.auth.clone(), + network: self.network, + wallet_name: format!( + "{}{}", + self.wallet_name_prefix.as_ref().unwrap_or(&String::new()), + checksum + ), + skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)), + }) + } +} + #[cfg(test)] #[cfg(feature = "test-rpc")] -crate::bdk_blockchain_tests! { +mod test { + use super::*; + use crate::testutils::blockchain_tests::TestClient; + + use bitcoin::Network; + use bitcoincore_rpc::RpcApi; + + crate::bdk_blockchain_tests! { + fn test_instance(test_client: &TestClient) -> RpcBlockchain { + let config = RpcConfig { + url: test_client.bitcoind.rpc_url(), + auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() }, + network: Network::Regtest, + wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ), + skip_blocks: None, + }; + RpcBlockchain::from_config(&config).unwrap() + } + } + + fn get_factory() -> (TestClient, RpcBlockchainFactory) { + let test_client = TestClient::default(); - fn test_instance(test_client: &TestClient) -> RpcBlockchain { - let config = RpcConfig { + let factory = RpcBlockchainFactory { url: test_client.bitcoind.rpc_url(), - auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() }, + auth: Auth::Cookie { + file: test_client.bitcoind.params.cookie_file.clone(), + }, network: Network::Regtest, - wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ), - skip_blocks: None, + wallet_name_prefix: Some("prefix-".into()), + default_skip_blocks: 0, }; - RpcBlockchain::from_config(&config).unwrap() + + (test_client, factory) + } + + #[test] + fn test_rpc_blockchain_factory() { + let (_test_client, factory) = get_factory(); + + let a = factory.build("aaaaaa", None).unwrap(); + assert_eq!(a.skip_blocks, Some(0)); + assert_eq!( + a.client + .get_wallet_info() + .expect("Node connection isn't working") + .wallet_name, + "prefix-aaaaaa" + ); + + let b = factory.build("bbbbbb", Some(100)).unwrap(); + assert_eq!(b.skip_blocks, Some(100)); + assert_eq!( + b.client + .get_wallet_info() + .expect("Node connection isn't working") + .wallet_name, + "prefix-bbbbbb" + ); } } diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs index b10f1a3ba..f05c9df48 100644 --- a/src/testutils/mod.rs +++ b/src/testutils/mod.rs @@ -267,5 +267,3 @@ macro_rules! testutils { (external, internal) }) } - -pub use testutils; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 914d10894..071b16e00 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -4089,6 +4089,8 @@ pub(crate) mod test { } /// Deterministically generate a unique name given the descriptors defining the wallet +/// +/// Compatible with [`wallet_name_from_descriptor`] pub fn wallet_name_from_descriptor( descriptor: T, change_descriptor: Option, From 8795da48397800effeffae8417c6e749ec488212 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 15 Apr 2022 22:12:34 +0200 Subject: [PATCH 3/3] wallet: Move `wallet_name_from_descriptor` above the tests --- src/wallet/mod.rs | 62 +++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 071b16e00..20e0c2630 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1610,6 +1610,37 @@ where } } +/// Deterministically generate a unique name given the descriptors defining the wallet +/// +/// Compatible with [`wallet_name_from_descriptor`] +pub fn wallet_name_from_descriptor( + descriptor: T, + change_descriptor: Option, + network: Network, + secp: &SecpCtx, +) -> Result +where + T: IntoWalletDescriptor, +{ + //TODO check descriptors contains only public keys + let descriptor = descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?; + if let Some(change_descriptor) = change_descriptor { + let change_descriptor = change_descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + wallet_name.push_str( + get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), + ); + } + + Ok(wallet_name) +} + /// Return a fake wallet that appears to be funded for testing. pub fn get_funded_wallet( descriptor: &str, @@ -4087,34 +4118,3 @@ pub(crate) mod test { ); } } - -/// Deterministically generate a unique name given the descriptors defining the wallet -/// -/// Compatible with [`wallet_name_from_descriptor`] -pub fn wallet_name_from_descriptor( - descriptor: T, - change_descriptor: Option, - network: Network, - secp: &SecpCtx, -) -> Result -where - T: IntoWalletDescriptor, -{ - //TODO check descriptors contains only public keys - let descriptor = descriptor - .into_wallet_descriptor(secp, network)? - .0 - .to_string(); - let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?; - if let Some(change_descriptor) = change_descriptor { - let change_descriptor = change_descriptor - .into_wallet_descriptor(secp, network)? - .0 - .to_string(); - wallet_name.push_str( - get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), - ); - } - - Ok(wallet_name) -}