Skip to content

Commit 0114805

Browse files
committedMar 15, 2022
[blockchain] Add traits to reuse Blockchains across multiple wallets
Add three 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. - `StatefulBlockchain` is the opposite of `StatelessBlockchain`: it provides a method to "clone" a `Blockchain` with an updated internal state (a new wallet checksum and, optionally, a different number of blocks to skip from genesis). Potentially this also allows reusing the underlying connection on `Blockchain` types that support it. - `MultiBlockchain` is a generalization of this concept: it's implemented automatically for every type that implements `StatefulBlockchain` and 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: MultiBlockchain` without having to deal with stateful and statless blockchains individually. These new traits have been implemented for Electrum, Esplora and RPC (the first two being stateless and the latter stateful). 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 9a6db15 commit 0114805

File tree

6 files changed

+77
-0
lines changed

6 files changed

+77
-0
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
1212
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
1313
- 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.
14+
- Add traits to reuse `Blockchain`s across multiple wallets (`MultiBlockchain`, `StatefulBlockchain`, `StatelessBlockchain`).
1415

1516
### Sync API change
1617

‎src/blockchain/electrum.rs

+2
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?

‎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

+51
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,57 @@ 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+
pub trait StatelessBlockchain: WalletSync + GetHeight {}
171+
172+
/// Trait for blockchains that have a wallet-dependent state
173+
///
174+
/// This trait provides [`StatefulBlockchain::clone_with`] to clone the blockchain and update its
175+
/// internal state to point to a different wallet checksum and optionally a different number of
176+
/// blocks to skip when syncing.
177+
///
178+
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
179+
/// to skip from `self`. Since it's not possible to override the value to `None`, set it to
180+
/// `Some(0)` to rescan from the genesis.
181+
pub trait StatefulBlockchain: WalletSync + GetHeight {
182+
/// The type returned when cloning this blockchain
183+
type Cloned: WalletSync + GetHeight;
184+
185+
/// Clone `self` with a different internal state
186+
fn clone_with(
187+
&self,
188+
checksum: &str,
189+
override_skip_blocks: Option<u32>,
190+
) -> Result<Self::Cloned, Error>;
191+
}
192+
193+
/// Trait for collections of blockchains that share an underlying connection
194+
pub trait MultiBlockchain {
195+
/// The type returned when cloning this blockchain
196+
type Inner: WalletSync + GetHeight;
197+
198+
/// Clone `self` with a different internal state
199+
fn get(&self, checksum: &str, override_skip_blocks: Option<u32>) -> Result<Self::Inner, Error>;
200+
}
201+
202+
impl<T: StatelessBlockchain> MultiBlockchain for Arc<T> {
203+
type Inner = Self;
204+
205+
fn get(&self, _checksum: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
206+
Ok(Arc::clone(self))
207+
}
208+
}
209+
210+
impl<T: StatefulBlockchain> MultiBlockchain for T {
211+
type Inner = <T as StatefulBlockchain>::Cloned;
212+
213+
fn get(&self, checksum: &str, override_skip_blocks: Option<u32>) -> Result<Self::Inner, Error> {
214+
self.clone_with(checksum, override_skip_blocks)
215+
}
216+
}
217+
167218
/// Data sent with a progress update over a [`channel`]
168219
pub type ProgressData = (f32, Option<String>);
169220

‎src/blockchain/rpc.rs

+19
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ pub struct RpcBlockchain {
6161
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
6262
skip_blocks: Option<u32>,
6363

64+
/// Keep track of the config used to build this blockchain so that we can tweak it and reuse it
65+
config: RpcConfig,
66+
6467
/// This is a fixed Address used as a hack key to store information on the node
6568
_storage_address: Address,
6669
}
@@ -155,6 +158,21 @@ impl Blockchain for RpcBlockchain {
155158
}
156159
}
157160

161+
impl StatefulBlockchain for RpcBlockchain {
162+
type Cloned = Self;
163+
164+
fn clone_with(&self, checksum: &str, override_skip_blocks: Option<u32>) -> Result<Self, Error> {
165+
let mut new_config = self.config.clone();
166+
167+
new_config.wallet_name = checksum.to_string();
168+
if let Some(skip_blocks) = override_skip_blocks {
169+
new_config.skip_blocks = Some(skip_blocks);
170+
}
171+
172+
RpcBlockchain::from_config(&new_config)
173+
}
174+
}
175+
158176
impl GetTx for RpcBlockchain {
159177
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
160178
Ok(Some(self.client.get_raw_transaction(txid, None)?))
@@ -420,6 +438,7 @@ impl ConfigurableBlockchain for RpcBlockchain {
420438
network,
421439
capabilities,
422440
_storage_address: storage_address,
441+
config: config.clone(),
423442
skip_blocks: config.skip_blocks,
424443
})
425444
}

0 commit comments

Comments
 (0)