Skip to content

Commit 9c405e9

Browse files
committed
[blockchain] Add traits to reuse Blockchains 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<T>` 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 bitcoindevkit#549, as BIP47 needs to sync many different descriptors internally. It's also very useful for bitcoindevkit#486.
1 parent 2d83af4 commit 9c405e9

File tree

8 files changed

+289
-13
lines changed

8 files changed

+289
-13
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
1010
- New MSRV set to `1.56`
11+
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
1112

1213

1314
## [v0.18.0] - [v0.17.0]

src/blockchain/electrum.rs

+64-3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain {
7979
}
8080
}
8181

82+
impl StatelessBlockchain for ElectrumBlockchain {}
83+
8284
impl GetHeight for ElectrumBlockchain {
8385
fn get_height(&self) -> Result<u32, Error> {
8486
// TODO: unsubscribe when added to the client, or is there a better call to use here?
@@ -320,8 +322,67 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
320322

321323
#[cfg(test)]
322324
#[cfg(feature = "test-electrum")]
323-
crate::bdk_blockchain_tests! {
324-
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
325-
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
325+
mod test {
326+
use std::sync::Arc;
327+
328+
use super::*;
329+
use crate::database::MemoryDatabase;
330+
use crate::testutils::blockchain_tests::TestClient;
331+
use crate::wallet::{AddressIndex, Wallet};
332+
333+
crate::bdk_blockchain_tests! {
334+
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
335+
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
336+
}
337+
}
338+
339+
fn get_factory() -> (TestClient, Arc<ElectrumBlockchain>) {
340+
let test_client = TestClient::default();
341+
342+
let factory = Arc::new(ElectrumBlockchain::from(
343+
Client::new(&test_client.electrsd.electrum_url).unwrap(),
344+
));
345+
346+
(test_client, factory)
347+
}
348+
349+
#[test]
350+
fn test_electrum_blockchain_factory() {
351+
let (_test_client, factory) = get_factory();
352+
353+
let a = factory.build("aaaaaa", None).unwrap();
354+
let b = factory.build("bbbbbb", None).unwrap();
355+
356+
assert_eq!(
357+
a.client.block_headers_subscribe().unwrap().height,
358+
b.client.block_headers_subscribe().unwrap().height
359+
);
360+
}
361+
362+
#[test]
363+
fn test_electrum_blockchain_factory_sync_wallet() {
364+
let (mut test_client, factory) = get_factory();
365+
366+
let db = MemoryDatabase::new();
367+
let wallet = Wallet::new(
368+
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
369+
None,
370+
bitcoin::Network::Regtest,
371+
db,
372+
)
373+
.unwrap();
374+
375+
let address = wallet.get_address(AddressIndex::New).unwrap();
376+
377+
let tx = testutils! {
378+
@tx ( (@addr address.address) => 50_000 )
379+
};
380+
test_client.receive(tx);
381+
382+
factory
383+
.sync_wallet(&wallet, None, Default::default())
384+
.unwrap();
385+
386+
assert_eq!(wallet.get_balance().unwrap(), 50_000);
326387
}
327388
}

src/blockchain/esplora/reqwest.rs

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain {
101101
}
102102
}
103103

104+
impl StatelessBlockchain for EsploraBlockchain {}
105+
104106
#[maybe_async]
105107
impl GetHeight for EsploraBlockchain {
106108
fn get_height(&self) -> Result<u32, Error> {

src/blockchain/esplora/ureq.rs

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain {
9898
}
9999
}
100100

101+
impl StatelessBlockchain for EsploraBlockchain {}
102+
101103
impl GetHeight for EsploraBlockchain {
102104
fn get_height(&self) -> Result<u32, Error> {
103105
Ok(self.url_client._get_height()?)

src/blockchain/mod.rs

+102-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ use bitcoin::{Transaction, Txid};
2525

2626
use crate::database::BatchDatabase;
2727
use crate::error::Error;
28-
use crate::FeeRate;
28+
use crate::wallet::{wallet_name_from_descriptor, Wallet};
29+
use crate::{FeeRate, KeychainKind};
2930

3031
#[cfg(any(
3132
feature = "electrum",
@@ -164,6 +165,106 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
164165
fn from_config(config: &Self::Config) -> Result<Self, Error>;
165166
}
166167

168+
/// Trait for blockchains that don't contain any state
169+
///
170+
/// Statless blockchains can be used to sync multiple wallets with different descriptors.
171+
///
172+
/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
173+
/// blockchain.
174+
pub trait StatelessBlockchain: Blockchain {}
175+
176+
/// Trait for a factory of blockchains that share the underlying connection or configuration
177+
#[cfg_attr(
178+
not(feature = "async-interface"),
179+
doc = r##"
180+
## Example
181+
182+
This example shows how to sync multiple walles and return the sum of their balances
183+
184+
```no_run
185+
# use bdk::Error;
186+
# use bdk::blockchain::*;
187+
# use bdk::database::*;
188+
# use bdk::wallet::*;
189+
# use bdk::*;
190+
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
191+
Ok(wallets
192+
.iter()
193+
.map(|w| -> Result<_, Error> {
194+
blockchain_factory.sync_wallet(&w, None, SyncOptions::default())?;
195+
w.get_balance()
196+
})
197+
.collect::<Result<Vec<_>, _>>()?
198+
.into_iter()
199+
.sum())
200+
}
201+
```
202+
"##
203+
)]
204+
pub trait BlockchainFactory {
205+
/// The type returned when building a blockchain from this factory
206+
type Inner: Blockchain;
207+
208+
/// Build a new blockchain for the given descriptor wallet_name
209+
///
210+
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
211+
/// from the factory. Since it's not possible to override the value to `None`, set it to
212+
/// `Some(0)` to rescan from the genesis.
213+
fn build(
214+
&self,
215+
wallet_name: &str,
216+
override_skip_blocks: Option<u32>,
217+
) -> Result<Self::Inner, Error>;
218+
219+
/// Build a new blockchain for a given wallet
220+
///
221+
/// Internally uses [`wallet_name_from_descriptor`] to derive the name, and then calls
222+
/// [`BlockchainFactory::build`] to create the blockchain instance.
223+
fn build_for_wallet<D: BatchDatabase>(
224+
&self,
225+
wallet: &Wallet<D>,
226+
override_skip_blocks: Option<u32>,
227+
) -> Result<Self::Inner, Error> {
228+
let wallet_name = wallet_name_from_descriptor(
229+
wallet.public_descriptor(KeychainKind::External)?.unwrap(),
230+
wallet.public_descriptor(KeychainKind::Internal)?,
231+
wallet.network(),
232+
wallet.secp_ctx(),
233+
)?;
234+
self.build(&wallet_name, override_skip_blocks)
235+
}
236+
237+
/// Use [`BlockchainFactory::build_for_wallet`] to get a blockchain, then sync the wallet
238+
///
239+
/// This can be used when a new blockchain would only be used to sync a wallet and then
240+
/// immediately dropped. Keep in mind that specific blockchain factories may perform slow
241+
/// operations to build a blockchain for a given wallet, so if a wallet needs to be synced
242+
/// often it's recommended to use [`BlockchainFactory::build_for_wallet`] to reuse the same
243+
/// blockchain multiple times.
244+
#[cfg(not(any(target_arch = "wasm32", feature = "async-interface")))]
245+
#[cfg_attr(
246+
docsrs,
247+
doc(cfg(not(any(target_arch = "wasm32", feature = "async-interface"))))
248+
)]
249+
fn sync_wallet<D: BatchDatabase>(
250+
&self,
251+
wallet: &Wallet<D>,
252+
override_skip_blocks: Option<u32>,
253+
sync_options: crate::wallet::SyncOptions,
254+
) -> Result<(), Error> {
255+
let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?;
256+
wallet.sync(&blockchain, sync_options)
257+
}
258+
}
259+
260+
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
261+
type Inner = Self;
262+
263+
fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
264+
Ok(Arc::clone(self))
265+
}
266+
}
267+
167268
/// Data sent with a progress update over a [`channel`]
168269
pub type ProgressData = (f32, Option<String>);
169270

src/blockchain/rpc.rs

+116-7
Original file line numberDiff line numberDiff line change
@@ -438,18 +438,127 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
438438
Ok(result.wallets.into_iter().map(|n| n.name).collect())
439439
}
440440

441+
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
442+
///
443+
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
444+
/// objects for different wallet names and with different rescan heights.
445+
///
446+
/// ## Example
447+
///
448+
/// ```no_run
449+
/// # use bdk::bitcoin::Network;
450+
/// # use bdk::blockchain::BlockchainFactory;
451+
/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory};
452+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
453+
/// let factory = RpcBlockchainFactory {
454+
/// url: "http://127.0.0.1:18332".to_string(),
455+
/// auth: Auth::Cookie {
456+
/// file: "/home/user/.bitcoin/.cookie".into(),
457+
/// },
458+
/// network: Network::Testnet,
459+
/// wallet_name_prefix: Some("prefix-".to_string()),
460+
/// default_skip_blocks: 100_000,
461+
/// };
462+
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
463+
/// # Ok(())
464+
/// # }
465+
/// ```
466+
#[derive(Debug, Clone)]
467+
pub struct RpcBlockchainFactory {
468+
/// The bitcoin node url
469+
pub url: String,
470+
/// The bitcoin node authentication mechanism
471+
pub auth: Auth,
472+
/// The network we are using (it will be checked the bitcoin node network matches this)
473+
pub network: Network,
474+
/// The optional prefix used to build the full wallet name for blockchains
475+
pub wallet_name_prefix: Option<String>,
476+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
477+
pub default_skip_blocks: u32,
478+
}
479+
480+
impl BlockchainFactory for RpcBlockchainFactory {
481+
type Inner = RpcBlockchain;
482+
483+
fn build(
484+
&self,
485+
checksum: &str,
486+
override_skip_blocks: Option<u32>,
487+
) -> Result<Self::Inner, Error> {
488+
RpcBlockchain::from_config(&RpcConfig {
489+
url: self.url.clone(),
490+
auth: self.auth.clone(),
491+
network: self.network,
492+
wallet_name: format!(
493+
"{}{}",
494+
self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
495+
checksum
496+
),
497+
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
498+
})
499+
}
500+
}
501+
441502
#[cfg(test)]
442503
#[cfg(feature = "test-rpc")]
443-
crate::bdk_blockchain_tests! {
504+
mod test {
505+
use super::*;
506+
use crate::testutils::blockchain_tests::TestClient;
507+
508+
use bitcoin::Network;
509+
use bitcoincore_rpc::RpcApi;
510+
511+
crate::bdk_blockchain_tests! {
512+
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
513+
let config = RpcConfig {
514+
url: test_client.bitcoind.rpc_url(),
515+
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
516+
network: Network::Regtest,
517+
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
518+
skip_blocks: None,
519+
};
520+
RpcBlockchain::from_config(&config).unwrap()
521+
}
522+
}
523+
524+
fn get_factory() -> (TestClient, RpcBlockchainFactory) {
525+
let test_client = TestClient::default();
444526

445-
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
446-
let config = RpcConfig {
527+
let factory = RpcBlockchainFactory {
447528
url: test_client.bitcoind.rpc_url(),
448-
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
529+
auth: Auth::Cookie {
530+
file: test_client.bitcoind.params.cookie_file.clone(),
531+
},
449532
network: Network::Regtest,
450-
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
451-
skip_blocks: None,
533+
wallet_name_prefix: Some("prefix-".into()),
534+
default_skip_blocks: 0,
452535
};
453-
RpcBlockchain::from_config(&config).unwrap()
536+
537+
(test_client, factory)
538+
}
539+
540+
#[test]
541+
fn test_rpc_blockchain_factory() {
542+
let (_test_client, factory) = get_factory();
543+
544+
let a = factory.build("aaaaaa", None).unwrap();
545+
assert_eq!(a.skip_blocks, Some(0));
546+
assert_eq!(
547+
a.client
548+
.get_wallet_info()
549+
.expect("Node connection isn't working")
550+
.wallet_name,
551+
"prefix-aaaaaa"
552+
);
553+
554+
let b = factory.build("bbbbbb", Some(100)).unwrap();
555+
assert_eq!(b.skip_blocks, Some(100));
556+
assert_eq!(
557+
b.client
558+
.get_wallet_info()
559+
.expect("Node connection isn't working")
560+
.wallet_name,
561+
"prefix-bbbbbb"
562+
);
454563
}
455564
}

src/testutils/mod.rs

-2
Original file line numberDiff line numberDiff line change
@@ -267,5 +267,3 @@ macro_rules! testutils {
267267
(external, internal)
268268
})
269269
}
270-
271-
pub use testutils;

src/wallet/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -4089,6 +4089,8 @@ pub(crate) mod test {
40894089
}
40904090

40914091
/// Deterministically generate a unique name given the descriptors defining the wallet
4092+
///
4093+
/// Compatible with [`wallet_name_from_descriptor`]
40924094
pub fn wallet_name_from_descriptor<T>(
40934095
descriptor: T,
40944096
change_descriptor: Option<T>,

0 commit comments

Comments
 (0)