Skip to content

Commit c298bf2

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 e5e207a commit c298bf2

File tree

7 files changed

+210
-2
lines changed

7 files changed

+210
-2
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
1818
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
1919
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
20+
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
2021

2122
### Sync API change
2223

@@ -441,4 +442,4 @@ final transaction is created by calling `finish` on the builder.
441442
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
442443
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
443444
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
444-
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD
445+
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD

src/blockchain/electrum.rs

+28
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?
@@ -325,3 +327,29 @@ crate::bdk_blockchain_tests! {
325327
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
326328
}
327329
}
330+
331+
#[cfg(test)]
332+
#[cfg(feature = "test-electrum")]
333+
mod test {
334+
use std::sync::Arc;
335+
336+
use super::*;
337+
use crate::testutils::blockchain_tests::TestClient;
338+
339+
#[test]
340+
fn test_electrum_blockchain_factory() {
341+
let test_client = TestClient::default();
342+
343+
let factory = Arc::new(ElectrumBlockchain::from(
344+
Client::new(&test_client.electrsd.electrum_url).unwrap(),
345+
));
346+
347+
let a = factory.build("aaaaaa", None).unwrap();
348+
let b = factory.build("bbbbbb", None).unwrap();
349+
350+
assert_eq!(
351+
a.client.block_headers_subscribe_raw().unwrap().height,
352+
b.client.block_headers_subscribe().unwrap().height
353+
);
354+
}
355+
}

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

+66
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,72 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
164164
fn from_config(config: &Self::Config) -> Result<Self, Error>;
165165
}
166166

167+
/// Trait for blockchains that don't contain any state
168+
///
169+
/// Statless blockchains can be used to sync multiple wallets with different descriptors.
170+
///
171+
/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
172+
/// blockchain.
173+
pub trait StatelessBlockchain: Blockchain {}
174+
175+
/// Trait for a factory of blockchains that share the underlying connection or configuration
176+
#[cfg_attr(
177+
not(feature = "async-interface"),
178+
doc = r##"
179+
## Example
180+
181+
This example shows how to sync multiple walles and return the sum of their balances
182+
183+
```no_run
184+
# use bdk::Error;
185+
# use bdk::blockchain::*;
186+
# use bdk::database::*;
187+
# use bdk::wallet::*;
188+
# use bdk::*;
189+
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
190+
Ok(wallets
191+
.iter()
192+
.map(|w| -> Result<_, Error> {
193+
let wallet_name = wallet_name_from_descriptor(
194+
w.public_descriptor(KeychainKind::External)?.unwrap(),
195+
w.public_descriptor(KeychainKind::Internal)?,
196+
w.network(),
197+
w.secp_ctx()
198+
)?;
199+
w.sync(&blockchain_factory.build(&wallet_name, None)?, SyncOptions::default())?;
200+
w.get_balance()
201+
})
202+
.collect::<Result<Vec<_>, _>>()?
203+
.into_iter()
204+
.sum())
205+
}
206+
```
207+
"##
208+
)]
209+
pub trait BlockchainFactory {
210+
/// The type returned when building a blockchain from this factory
211+
type Inner: Blockchain;
212+
213+
/// Build a new blockchain for the given descriptor wallet_name
214+
///
215+
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
216+
/// from the factory. Since it's not possible to override the value to `None`, set it to
217+
/// `Some(0)` to rescan from the genesis.
218+
fn build(
219+
&self,
220+
wallet_name: &str,
221+
override_skip_blocks: Option<u32>,
222+
) -> Result<Self::Inner, Error>;
223+
}
224+
225+
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
226+
type Inner = Self;
227+
228+
fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
229+
Ok(Arc::clone(self))
230+
}
231+
}
232+
167233
/// Data sent with a progress update over a [`channel`]
168234
pub type ProgressData = (f32, Option<String>);
169235

src/blockchain/rpc.rs

+108-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
//! file: "/home/user/.bitcoin/.cookie".into(),
2626
//! },
2727
//! network: bdk::bitcoin::Network::Testnet,
28-
//! wallet_name: "wallet_name".to_string(),
28+
//! wallet_name: Some("wallet_name".to_string(),
2929
//! skip_blocks: None,
3030
//! };
3131
//! let blockchain = RpcBlockchain::from_config(&config);
@@ -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+
Ok(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,49 @@ 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::blockchain::*;
526+
use crate::testutils::blockchain_tests::TestClient;
527+
528+
use bitcoin::Network;
529+
use bitcoincore_rpc::RpcApi;
530+
531+
#[test]
532+
fn test_rpc_blockchain_factory() {
533+
let test_client = TestClient::default();
534+
535+
let factory = RpcBlockchainFactory {
536+
url: test_client.bitcoind.rpc_url(),
537+
auth: Auth::Cookie {
538+
file: test_client.bitcoind.params.cookie_file.clone(),
539+
},
540+
network: Network::Regtest,
541+
wallet_name_prefix: Some("prefix-".into()),
542+
default_skip_blocks: 0,
543+
};
544+
545+
let a = factory.build("aaaaaa", None).unwrap();
546+
assert_eq!(a.skip_blocks, Some(0));
547+
assert_eq!(
548+
a.client
549+
.get_wallet_info()
550+
.expect("Node connection is working")
551+
.wallet_name,
552+
"prefix-aaaaaa"
553+
);
554+
555+
let b = factory.build("bbbbbb", Some(100)).unwrap();
556+
assert_eq!(b.skip_blocks, Some(100));
557+
assert_eq!(
558+
a.client
559+
.get_wallet_info()
560+
.expect("Node connection is working")
561+
.wallet_name,
562+
"prefix-bbbbbb"
563+
);
564+
}
565+
}

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)