Skip to content

Commit 3eb319f

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 e9bab6b commit 3eb319f

File tree

8 files changed

+282
-6
lines changed

8 files changed

+282
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
1111
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
1212
- Improve key generation traits
13+
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
1314

1415
## [v0.17.0] - [v0.16.1]
1516

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

+99-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, SyncOptions, Wallet};
29+
use crate::{FeeRate, KeychainKind};
2930

3031
#[cfg(any(
3132
feature = "electrum",
@@ -164,6 +165,103 @@ 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(feature = "async-interface"))]
245+
#[cfg_attr(docsrs, doc(cfg(not(feature = "async-interface"))))]
246+
fn sync_wallet<D: BatchDatabase>(
247+
&self,
248+
wallet: &Wallet<D>,
249+
override_skip_blocks: Option<u32>,
250+
sync_options: SyncOptions,
251+
) -> Result<(), Error> {
252+
let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?;
253+
wallet.sync(&blockchain, sync_options)
254+
}
255+
}
256+
257+
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
258+
type Inner = Self;
259+
260+
fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
261+
Ok(Arc::clone(self))
262+
}
263+
}
264+
167265
/// Data sent with a progress update over a [`channel`]
168266
pub type ProgressData = (f32, Option<String>);
169267

src/blockchain/rpc.rs

+112
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,67 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
441441
Ok(result.wallets.into_iter().map(|n| n.name).collect())
442442
}
443443

444+
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
445+
///
446+
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
447+
/// objects for different wallet names and with different rescan heights.
448+
///
449+
/// ## Example
450+
///
451+
/// ```no_run
452+
/// # use bdk::bitcoin::Network;
453+
/// # use bdk::blockchain::BlockchainFactory;
454+
/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory};
455+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
456+
/// let factory = RpcBlockchainFactory {
457+
/// url: "http://127.0.0.1:18332".to_string(),
458+
/// auth: Auth::Cookie {
459+
/// file: "/home/user/.bitcoin/.cookie".into(),
460+
/// },
461+
/// network: Network::Testnet,
462+
/// wallet_name_prefix: Some("prefix-".to_string()),
463+
/// default_skip_blocks: 100_000,
464+
/// };
465+
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
466+
/// # Ok(())
467+
/// # }
468+
/// ```
469+
#[derive(Debug, Clone)]
470+
pub struct RpcBlockchainFactory {
471+
/// The bitcoin node url
472+
pub url: String,
473+
/// The bitcoin node authentication mechanism
474+
pub auth: Auth,
475+
/// The network we are using (it will be checked the bitcoin node network matches this)
476+
pub network: Network,
477+
/// The optional prefix used to build the full wallet name for blockchains
478+
pub wallet_name_prefix: Option<String>,
479+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
480+
pub default_skip_blocks: u32,
481+
}
482+
483+
impl BlockchainFactory for RpcBlockchainFactory {
484+
type Inner = RpcBlockchain;
485+
486+
fn build(
487+
&self,
488+
checksum: &str,
489+
override_skip_blocks: Option<u32>,
490+
) -> Result<Self::Inner, Error> {
491+
RpcBlockchain::from_config(&RpcConfig {
492+
url: self.url.clone(),
493+
auth: self.auth.clone(),
494+
network: self.network,
495+
wallet_name: format!(
496+
"{}{}",
497+
self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
498+
checksum
499+
),
500+
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
501+
})
502+
}
503+
}
504+
444505
#[cfg(test)]
445506
#[cfg(feature = "test-rpc")]
446507
crate::bdk_blockchain_tests! {
@@ -456,3 +517,54 @@ crate::bdk_blockchain_tests! {
456517
RpcBlockchain::from_config(&config).unwrap()
457518
}
458519
}
520+
521+
#[cfg(test)]
522+
#[cfg(feature = "test-rpc")]
523+
mod test {
524+
use super::*;
525+
use crate::testutils::blockchain_tests::TestClient;
526+
527+
use bitcoin::Network;
528+
use bitcoincore_rpc::RpcApi;
529+
530+
fn get_factory() -> (TestClient, RpcBlockchainFactory) {
531+
let test_client = TestClient::default();
532+
533+
let factory = RpcBlockchainFactory {
534+
url: test_client.bitcoind.rpc_url(),
535+
auth: Auth::Cookie {
536+
file: test_client.bitcoind.params.cookie_file.clone(),
537+
},
538+
network: Network::Regtest,
539+
wallet_name_prefix: Some("prefix-".into()),
540+
default_skip_blocks: 0,
541+
};
542+
543+
(test_client, factory)
544+
}
545+
546+
#[test]
547+
fn test_rpc_blockchain_factory() {
548+
let (_test_client, factory) = get_factory();
549+
550+
let a = factory.build("aaaaaa", None).unwrap();
551+
assert_eq!(a.skip_blocks, Some(0));
552+
assert_eq!(
553+
a.client
554+
.get_wallet_info()
555+
.expect("Node connection isn't working")
556+
.wallet_name,
557+
"prefix-aaaaaa"
558+
);
559+
560+
let b = factory.build("bbbbbb", Some(100)).unwrap();
561+
assert_eq!(b.skip_blocks, Some(100));
562+
assert_eq!(
563+
b.client
564+
.get_wallet_info()
565+
.expect("Node connection isn't working")
566+
.wallet_name,
567+
"prefix-bbbbbb"
568+
);
569+
}
570+
}

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
@@ -4093,6 +4093,8 @@ pub(crate) mod test {
40934093
}
40944094

40954095
/// Deterministically generate a unique name given the descriptors defining the wallet
4096+
///
4097+
/// Compatible with [`wallet_name_from_descriptor`]
40964098
pub fn wallet_name_from_descriptor<T>(
40974099
descriptor: T,
40984100
change_descriptor: Option<T>,

0 commit comments

Comments
 (0)