Skip to content

Commit cc84872

Browse files
committed
Merge #1478: Allow opting out of getting LocalChain updates with FullScanRequest/SyncRequest structures
6d77e2e refactor(chain)!: Rename `spks_with_labels` to `spks_with_indices` (志宇) 584b10a docs(esplora): README example, uncomment async import (志宇) 3eb5dd1 fix(chain): correct `Iterator::size_hint` impl (志宇) 96023c0 docs(chain): improve `SyncRequestBuilder::spks_with_labels` docs (志宇) 0234f70 docs(esplora): Fix typo (志宇) 38f86fe fix: no premature collect (志宇) 44e2a79 feat!: rework `FullScanRequest` and `SyncRequest` (志宇) 16c1c2c docs(esplora): Simplify crate docs (志宇) c93e6fd feat(esplora): always fetch prevouts (志宇) cad3533 feat(esplora): make ext traits more flexible (志宇) Pull request description: Closes #1528 ### Description Some users use `bdk_esplora` to update `bdk_chain` structures *without* a `LocalChain`. ~~This PR introduces "low-level" methods to `EsploraExt` and `EsploraAsyncExt` which populates a `TxGraph` update with associated data.~~ We change `FullScanRequest`/`SyncRequest` to take in the `chain_tip` parameter as an option. Spk-based chain sources (`bdk_electrum` and `bdk_esplora`) will not fetch a chain-update if `chain_tip` is `None`, allowing callers to opt-out of receiving updates for `LocalChain`. We change `FullScanRequest`/`SyncRequest` to have better ergonomics when inspecting the progress of syncing (refer to #1528). We change `FullScanRequest`/`SyncRequest` to be constructed with a builder pattern. This is a better API since we separate request-construction and request-consumption. Additionally, much of the `bdk_esplora` logic has been made more efficient (less calls to Esplora) by utilizing the `/tx/:txid` endpoint (`get_tx_info` method). This method returns the tx and tx_status in one call. The logic for fetching updates for outpoints has been reworked to support parallelism. Documentation has also been updated. ### Notes to reviewers This PR has evolved somewhat. Initially, we were adding more methods on `EsploraExt`/`EsploraAsyncExt` to make syncing/scanning more modular. However, it turns out we can just make the `chain_tip` parameter of the request structures optional. Since we are changing the request structures, we might as well go further and improve the ergonomics of the whole structures (refer to #1528). This is where we are at with this PR. Unfortunately, the changes are now breaking. I think this is an okay tradeoff for the API improvements (before we get to stable). ### Changelog notice * Change request structures in `bdk_chain::spk_client` to be constructed via a builder pattern, make providing a `chain_tip` optional, and have better ergonomics for inspecting progress while syncing. * Change `bdk_esplora` to be more efficient by reducing the number of calls via the `/tx/:txid` endpoint. The logic for fetching outpoint updates has been reworked to support parallelism. * Change `bdk_esplora` to always add prev-txouts to the `TxGraph` update. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [ ] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: ValuedMammal: ACK 6d77e2e notmandatory: ACK 6d77e2e Tree-SHA512: 806cb159a8801f4e33846d18e6053b65d105e452b0f3f9d639b0c3f2e48fb665e632898bf42977901526834587223b0d7ec7ba1f73bb796b5fd8fe91e6f287f7
2 parents 76aec62 + 6d77e2e commit cc84872

File tree

16 files changed

+1273
-958
lines changed

16 files changed

+1273
-958
lines changed

crates/chain/src/spk_client.rs

Lines changed: 483 additions & 285 deletions
Large diffs are not rendered by default.

crates/electrum/src/bdk_electrum_client.rs

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -126,31 +126,43 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
126126
/// [`Wallet.calculate_fee_rate`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee_rate
127127
pub fn full_scan<K: Ord + Clone>(
128128
&self,
129-
request: FullScanRequest<K>,
129+
request: impl Into<FullScanRequest<K>>,
130130
stop_gap: usize,
131131
batch_size: usize,
132132
fetch_prev_txouts: bool,
133133
) -> Result<FullScanResult<K>, Error> {
134-
let (tip, latest_blocks) =
135-
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
136-
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
137-
let mut last_active_indices = BTreeMap::<K, u32>::new();
134+
let mut request: FullScanRequest<K> = request.into();
135+
136+
let tip_and_latest_blocks = match request.chain_tip() {
137+
Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
138+
None => None,
139+
};
138140

139-
for (keychain, spks) in request.spks_by_keychain {
141+
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
142+
let mut last_active_indices = BTreeMap::<K, u32>::default();
143+
for keychain in request.keychains() {
144+
let spks = request.iter_spks(keychain.clone());
140145
if let Some(last_active_index) =
141146
self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)?
142147
{
143148
last_active_indices.insert(keychain, last_active_index);
144149
}
145150
}
146151

147-
let chain_update = chain_update(tip, &latest_blocks, graph_update.all_anchors())?;
148-
149152
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
150153
if fetch_prev_txouts {
151154
self.fetch_prev_txout(&mut graph_update)?;
152155
}
153156

157+
let chain_update = match tip_and_latest_blocks {
158+
Some((chain_tip, latest_blocks)) => Some(chain_update(
159+
chain_tip,
160+
&latest_blocks,
161+
graph_update.all_anchors(),
162+
)?),
163+
_ => None,
164+
};
165+
154166
Ok(FullScanResult {
155167
graph_update,
156168
chain_update,
@@ -180,35 +192,49 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
180192
/// [`CalculateFeeError::MissingTxOut`]: bdk_chain::tx_graph::CalculateFeeError::MissingTxOut
181193
/// [`Wallet.calculate_fee`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee
182194
/// [`Wallet.calculate_fee_rate`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee_rate
183-
pub fn sync(
195+
pub fn sync<I: 'static>(
184196
&self,
185-
request: SyncRequest,
197+
request: impl Into<SyncRequest<I>>,
186198
batch_size: usize,
187199
fetch_prev_txouts: bool,
188200
) -> Result<SyncResult, Error> {
189-
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
190-
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
191-
let mut full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size, false)?;
192-
let (tip, latest_blocks) =
193-
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
194-
195-
self.populate_with_txids(&mut full_scan_res.graph_update, request.txids)?;
196-
self.populate_with_outpoints(&mut full_scan_res.graph_update, request.outpoints)?;
197-
198-
let chain_update = chain_update(
199-
tip,
200-
&latest_blocks,
201-
full_scan_res.graph_update.all_anchors(),
201+
let mut request: SyncRequest<I> = request.into();
202+
203+
let tip_and_latest_blocks = match request.chain_tip() {
204+
Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
205+
None => None,
206+
};
207+
208+
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
209+
self.populate_with_spks(
210+
&mut graph_update,
211+
request
212+
.iter_spks()
213+
.enumerate()
214+
.map(|(i, spk)| (i as u32, spk)),
215+
usize::MAX,
216+
batch_size,
202217
)?;
218+
self.populate_with_txids(&mut graph_update, request.iter_txids())?;
219+
self.populate_with_outpoints(&mut graph_update, request.iter_outpoints())?;
203220

204221
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
205222
if fetch_prev_txouts {
206-
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
223+
self.fetch_prev_txout(&mut graph_update)?;
207224
}
208225

226+
let chain_update = match tip_and_latest_blocks {
227+
Some((chain_tip, latest_blocks)) => Some(chain_update(
228+
chain_tip,
229+
&latest_blocks,
230+
graph_update.all_anchors(),
231+
)?),
232+
None => None,
233+
};
234+
209235
Ok(SyncResult {
236+
graph_update,
210237
chain_update,
211-
graph_update: full_scan_res.graph_update,
212238
})
213239
}
214240

crates/electrum/tests/test_electrum.rs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ where
3939
Spks::IntoIter: ExactSizeIterator + Send + 'static,
4040
{
4141
let mut update = client.sync(
42-
SyncRequest::from_chain_tip(chain.tip()).chain_spks(spks),
42+
SyncRequest::builder().chain_tip(chain.tip()).spks(spks),
4343
BATCH_SIZE,
4444
true,
4545
)?;
@@ -51,9 +51,11 @@ where
5151
.as_secs();
5252
let _ = update.graph_update.update_last_seen_unconfirmed(now);
5353

54-
let _ = chain
55-
.apply_update(update.chain_update.clone())
56-
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
54+
if let Some(chain_update) = update.chain_update.clone() {
55+
let _ = chain
56+
.apply_update(chain_update)
57+
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
58+
}
5759
let _ = graph.apply_update(update.graph_update.clone());
5860

5961
Ok(update)
@@ -103,7 +105,9 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
103105
let cp_tip = env.make_checkpoint_tip();
104106

105107
let sync_update = {
106-
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
108+
let request = SyncRequest::builder()
109+
.chain_tip(cp_tip.clone())
110+
.spks(misc_spks);
107111
client.sync(request, 1, true)?
108112
};
109113

@@ -207,15 +211,17 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
207211
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
208212
// will.
209213
let full_scan_update = {
210-
let request =
211-
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
214+
let request = FullScanRequest::builder()
215+
.chain_tip(cp_tip.clone())
216+
.spks_for_keychain(0, spks.clone());
212217
client.full_scan(request, 3, 1, false)?
213218
};
214219
assert!(full_scan_update.graph_update.full_txs().next().is_none());
215220
assert!(full_scan_update.last_active_indices.is_empty());
216221
let full_scan_update = {
217-
let request =
218-
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
222+
let request = FullScanRequest::builder()
223+
.chain_tip(cp_tip.clone())
224+
.spks_for_keychain(0, spks.clone());
219225
client.full_scan(request, 4, 1, false)?
220226
};
221227
assert_eq!(
@@ -246,8 +252,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
246252
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
247253
// The last active indice won't be updated in the first case but will in the second one.
248254
let full_scan_update = {
249-
let request =
250-
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
255+
let request = FullScanRequest::builder()
256+
.chain_tip(cp_tip.clone())
257+
.spks_for_keychain(0, spks.clone());
251258
client.full_scan(request, 5, 1, false)?
252259
};
253260
let txs: HashSet<_> = full_scan_update
@@ -259,8 +266,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
259266
assert!(txs.contains(&txid_4th_addr));
260267
assert_eq!(full_scan_update.last_active_indices[&0], 3);
261268
let full_scan_update = {
262-
let request =
263-
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
269+
let request = FullScanRequest::builder()
270+
.chain_tip(cp_tip.clone())
271+
.spks_for_keychain(0, spks.clone());
264272
client.full_scan(request, 6, 1, false)?
265273
};
266274
let txs: HashSet<_> = full_scan_update
@@ -311,7 +319,7 @@ fn test_sync() -> anyhow::Result<()> {
311319
let txid = env.send(&addr_to_track, SEND_AMOUNT)?;
312320
env.wait_until_electrum_sees_txid(txid, Duration::from_secs(6))?;
313321

314-
sync_with_electrum(
322+
let _ = sync_with_electrum(
315323
&client,
316324
[spk_to_track.clone()],
317325
&mut recv_chain,
@@ -332,7 +340,7 @@ fn test_sync() -> anyhow::Result<()> {
332340
env.mine_blocks(1, None)?;
333341
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
334342

335-
sync_with_electrum(
343+
let _ = sync_with_electrum(
336344
&client,
337345
[spk_to_track.clone()],
338346
&mut recv_chain,
@@ -353,7 +361,7 @@ fn test_sync() -> anyhow::Result<()> {
353361
env.reorg_empty_blocks(1)?;
354362
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
355363

356-
sync_with_electrum(
364+
let _ = sync_with_electrum(
357365
&client,
358366
[spk_to_track.clone()],
359367
&mut recv_chain,
@@ -373,7 +381,7 @@ fn test_sync() -> anyhow::Result<()> {
373381
env.mine_blocks(1, None)?;
374382
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
375383

376-
sync_with_electrum(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
384+
let _ = sync_with_electrum(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
377385

378386
// Check if balance is correct once transaction is confirmed again.
379387
assert_eq!(

crates/esplora/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ readme = "README.md"
1313

1414
[dependencies]
1515
bdk_chain = { path = "../chain", version = "0.17.0", default-features = false }
16-
esplora-client = { version = "0.8.0", default-features = false }
16+
esplora-client = { version = "0.9.0", default-features = false }
1717
async-trait = { version = "0.1.66", optional = true }
1818
futures = { version = "0.3.26", optional = true }
19-
20-
bitcoin = { version = "0.32.0", optional = true, default-features = false }
2119
miniscript = { version = "12.0.0", optional = true, default-features = false }
2220

2321
[dev-dependencies]

crates/esplora/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# BDK Esplora
22

3-
BDK Esplora extends [`esplora-client`] to update [`bdk_chain`] structures
4-
from an Esplora server.
3+
BDK Esplora extends [`esplora-client`] (with extension traits: [`EsploraExt`] and
4+
[`EsploraAsyncExt`]) to update [`bdk_chain`] structures from an Esplora server.
55

6-
## Usage
6+
The extension traits are primarily intended to satisfy [`SyncRequest`]s with [`sync`] and
7+
[`FullScanRequest`]s with [`full_scan`].
78

8-
There are two versions of the extension trait (blocking and async).
9+
## Usage
910

1011
For blocking-only:
1112
```toml
@@ -27,10 +28,16 @@ To use the extension traits:
2728
// for blocking
2829
use bdk_esplora::EsploraExt;
2930
// for async
30-
// use bdk_esplora::EsploraAsyncExt;
31+
use bdk_esplora::EsploraAsyncExt;
3132
```
3233

3334
For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
3435

3536
[`esplora-client`]: https://docs.rs/esplora-client/
3637
[`bdk_chain`]: https://docs.rs/bdk-chain/
38+
[`EsploraExt`]: crate::EsploraExt
39+
[`EsploraAsyncExt`]: crate::EsploraAsyncExt
40+
[`SyncRequest`]: bdk_chain::spk_client::SyncRequest
41+
[`FullScanRequest`]: bdk_chain::spk_client::FullScanRequest
42+
[`sync`]: crate::EsploraExt::sync
43+
[`full_scan`]: crate::EsploraExt::full_scan

0 commit comments

Comments
 (0)