Skip to content

Commit f42d5a8

Browse files
feat(esplora): Handle spks with expected txids
Also add `detect_receive_tx_cancel` test. Also rm `miniscript` dependency. Update ci to rm `miniscript/no-std` for `bdk_esplora`. Co-authored-by: Wei Chen <[email protected]>
1 parent 3ab4994 commit f42d5a8

File tree

7 files changed

+329
-47
lines changed

7 files changed

+329
-47
lines changed

.github/workflows/cont_integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ jobs:
9696
- name: Check esplora
9797
working-directory: ./crates/esplora
9898
# TODO "--target thumbv6m-none-eabi" should work but currently does not
99-
run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
99+
run: cargo check --no-default-features --features bdk_chain/hashbrown
100100

101101
check-wasm:
102102
needs: prepare
@@ -128,7 +128,7 @@ jobs:
128128
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
129129
- name: Check esplora
130130
working-directory: ./crates/esplora
131-
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async
131+
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bdk_core/hashbrown,async
132132

133133
fmt:
134134
needs: prepare

crates/esplora/Cargo.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,19 @@ workspace = true
1616

1717
[dependencies]
1818
bdk_core = { path = "../core", version = "0.4.1", default-features = false }
19-
esplora-client = { version = "0.11.0", default-features = false }
19+
esplora-client = { version = "0.11.0", default-features = false }
2020
async-trait = { version = "0.1.66", optional = true }
2121
futures = { version = "0.3.26", optional = true }
22-
miniscript = { version = "12.0.0", optional = true, default-features = false }
2322

2423
[dev-dependencies]
25-
esplora-client = { version = "0.11.0" }
24+
esplora-client = { version = "0.11.0" }
2625
bdk_chain = { path = "../chain" }
2726
bdk_testenv = { path = "../testenv" }
2827
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
2928

3029
[features]
3130
default = ["std", "async-https", "blocking-https"]
32-
std = ["bdk_chain/std", "miniscript?/std"]
31+
std = ["bdk_core/std"]
3332
tokio = ["esplora-client/tokio"]
3433
async = ["async-trait", "futures", "esplora-client/async"]
3534
async-https = ["async", "esplora-client/async-https"]

crates/esplora/src/async_ext.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use async_trait::async_trait;
22
use bdk_core::collections::{BTreeMap, BTreeSet, HashSet};
3-
use bdk_core::spk_client::{FullScanRequest, FullScanResponse, SyncRequest, SyncResponse};
3+
use bdk_core::spk_client::{
4+
FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse,
5+
};
46
use bdk_core::{
5-
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
7+
bitcoin::{BlockHash, OutPoint, Txid},
68
BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate,
79
};
810
use esplora_client::Sleeper;
@@ -62,7 +64,7 @@ where
6264
stop_gap: usize,
6365
parallel_requests: usize,
6466
) -> Result<FullScanResponse<K>, Error> {
65-
let mut request = request.into();
67+
let mut request: FullScanRequest<K> = request.into();
6668
let start_time = request.start_time();
6769
let keychains = request.keychains();
6870

@@ -77,7 +79,9 @@ where
7779
let mut inserted_txs = HashSet::<Txid>::new();
7880
let mut last_active_indices = BTreeMap::<K, u32>::new();
7981
for keychain in keychains {
80-
let keychain_spks = request.iter_spks(keychain.clone());
82+
let keychain_spks = request
83+
.iter_spks(keychain.clone())
84+
.map(|(spk_i, spk)| (spk_i, spk.into()));
8185
let (update, last_active_index) = fetch_txs_with_keychain_spks(
8286
self,
8387
start_time,
@@ -112,7 +116,7 @@ where
112116
request: R,
113117
parallel_requests: usize,
114118
) -> Result<SyncResponse, Error> {
115-
let mut request = request.into();
119+
let mut request: SyncRequest<I> = request.into();
116120
let start_time = request.start_time();
117121

118122
let chain_tip = request.chain_tip();
@@ -129,7 +133,7 @@ where
129133
self,
130134
start_time,
131135
&mut inserted_txs,
132-
request.iter_spks(),
136+
request.iter_spks_with_expected_txids(),
133137
parallel_requests,
134138
)
135139
.await?,
@@ -291,10 +295,10 @@ async fn fetch_txs_with_keychain_spks<I, S>(
291295
parallel_requests: usize,
292296
) -> Result<(TxUpdate<ConfirmationBlockTime>, Option<u32>), Error>
293297
where
294-
I: Iterator<Item = Indexed<ScriptBuf>> + Send,
298+
I: Iterator<Item = Indexed<SpkWithExpectedTxids>> + Send,
295299
S: Sleeper + Clone + Send + Sync,
296300
{
297-
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
301+
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>, HashSet<Txid>);
298302

299303
let mut update = TxUpdate::<ConfirmationBlockTime>::default();
300304
let mut last_index = Option::<u32>::None;
@@ -306,6 +310,8 @@ where
306310
.take(parallel_requests)
307311
.map(|(spk_index, spk)| {
308312
let client = client.clone();
313+
let expected_txids = spk.expected_txids;
314+
let spk = spk.spk;
309315
async move {
310316
let mut last_seen = None;
311317
let mut spk_txs = Vec::new();
@@ -315,9 +321,15 @@ where
315321
last_seen = txs.last().map(|tx| tx.txid);
316322
spk_txs.extend(txs);
317323
if tx_count < 25 {
318-
break Result::<_, Error>::Ok((spk_index, spk_txs));
324+
break;
319325
}
320326
}
327+
let got_txids = spk_txs.iter().map(|tx| tx.txid).collect::<HashSet<_>>();
328+
let evicted_txids = expected_txids
329+
.difference(&got_txids)
330+
.copied()
331+
.collect::<HashSet<_>>();
332+
Result::<TxsOfSpkIndex, Error>::Ok((spk_index, spk_txs, evicted_txids))
321333
}
322334
})
323335
.collect::<FuturesOrdered<_>>();
@@ -326,7 +338,7 @@ where
326338
break;
327339
}
328340

329-
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
341+
for (index, txs, evicted) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
330342
last_index = Some(index);
331343
if !txs.is_empty() {
332344
last_active_index = Some(index);
@@ -338,6 +350,9 @@ where
338350
insert_anchor_or_seen_at_from_status(&mut update, start_time, tx.txid, tx.status);
339351
insert_prevouts(&mut update, tx.vin);
340352
}
353+
update
354+
.evicted_ats
355+
.extend(evicted.into_iter().map(|txid| (txid, start_time)));
341356
}
342357

343358
let last_index = last_index.expect("Must be set since handles wasn't empty.");
@@ -370,7 +385,7 @@ async fn fetch_txs_with_spks<I, S>(
370385
parallel_requests: usize,
371386
) -> Result<TxUpdate<ConfirmationBlockTime>, Error>
372387
where
373-
I: IntoIterator<Item = ScriptBuf> + Send,
388+
I: IntoIterator<Item = SpkWithExpectedTxids> + Send,
374389
I::IntoIter: Send,
375390
S: Sleeper + Clone + Send + Sync,
376391
{

crates/esplora/src/blocking_ext.rs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use bdk_core::collections::{BTreeMap, BTreeSet, HashSet};
2-
use bdk_core::spk_client::{FullScanRequest, FullScanResponse, SyncRequest, SyncResponse};
2+
use bdk_core::spk_client::{
3+
FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse,
4+
};
35
use bdk_core::{
4-
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
6+
bitcoin::{BlockHash, OutPoint, Txid},
57
BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate,
68
};
79
use esplora_client::{OutputStatus, Tx};
@@ -53,7 +55,7 @@ impl EsploraExt for esplora_client::BlockingClient {
5355
stop_gap: usize,
5456
parallel_requests: usize,
5557
) -> Result<FullScanResponse<K>, Error> {
56-
let mut request = request.into();
58+
let mut request: FullScanRequest<K> = request.into();
5759
let start_time = request.start_time();
5860

5961
let chain_tip = request.chain_tip();
@@ -67,7 +69,9 @@ impl EsploraExt for esplora_client::BlockingClient {
6769
let mut inserted_txs = HashSet::<Txid>::new();
6870
let mut last_active_indices = BTreeMap::<K, u32>::new();
6971
for keychain in request.keychains() {
70-
let keychain_spks = request.iter_spks(keychain.clone());
72+
let keychain_spks = request
73+
.iter_spks(keychain.clone())
74+
.map(|(spk_i, spk)| (spk_i, spk.into()));
7175
let (update, last_active_index) = fetch_txs_with_keychain_spks(
7276
self,
7377
start_time,
@@ -120,7 +124,7 @@ impl EsploraExt for esplora_client::BlockingClient {
120124
self,
121125
start_time,
122126
&mut inserted_txs,
123-
request.iter_spks(),
127+
request.iter_spks_with_expected_txids(),
124128
parallel_requests,
125129
)?);
126130
tx_update.extend(fetch_txs_with_txids(
@@ -254,15 +258,15 @@ fn chain_update(
254258
Ok(tip)
255259
}
256260

257-
fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
261+
fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<SpkWithExpectedTxids>>>(
258262
client: &esplora_client::BlockingClient,
259263
start_time: u64,
260264
inserted_txs: &mut HashSet<Txid>,
261265
mut keychain_spks: I,
262266
stop_gap: usize,
263267
parallel_requests: usize,
264268
) -> Result<(TxUpdate<ConfirmationBlockTime>, Option<u32>), Error> {
265-
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
269+
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>, HashSet<Txid>);
266270

267271
let mut update = TxUpdate::<ConfirmationBlockTime>::default();
268272
let mut last_index = Option::<u32>::None;
@@ -273,21 +277,27 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
273277
.by_ref()
274278
.take(parallel_requests)
275279
.map(|(spk_index, spk)| {
276-
std::thread::spawn({
277-
let client = client.clone();
278-
move || -> Result<TxsOfSpkIndex, Error> {
279-
let mut last_seen = None;
280-
let mut spk_txs = Vec::new();
281-
loop {
282-
let txs = client.scripthash_txs(&spk, last_seen)?;
283-
let tx_count = txs.len();
284-
last_seen = txs.last().map(|tx| tx.txid);
285-
spk_txs.extend(txs);
286-
if tx_count < 25 {
287-
break Ok((spk_index, spk_txs));
288-
}
280+
let client = client.clone();
281+
let expected_txids = spk.expected_txids;
282+
let spk = spk.spk;
283+
std::thread::spawn(move || -> Result<TxsOfSpkIndex, Error> {
284+
let mut last_txid = None;
285+
let mut spk_txs = Vec::new();
286+
loop {
287+
let txs = client.scripthash_txs(&spk, last_txid)?;
288+
let tx_count = txs.len();
289+
last_txid = txs.last().map(|tx| tx.txid);
290+
spk_txs.extend(txs);
291+
if tx_count < 25 {
292+
break;
289293
}
290294
}
295+
let got_txids = spk_txs.iter().map(|tx| tx.txid).collect::<HashSet<_>>();
296+
let evicted_txids = expected_txids
297+
.difference(&got_txids)
298+
.copied()
299+
.collect::<HashSet<_>>();
300+
Ok((spk_index, spk_txs, evicted_txids))
291301
})
292302
})
293303
.collect::<Vec<JoinHandle<Result<TxsOfSpkIndex, Error>>>>();
@@ -297,7 +307,7 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
297307
}
298308

299309
for handle in handles {
300-
let (index, txs) = handle.join().expect("thread must not panic")?;
310+
let (index, txs, evicted) = handle.join().expect("thread must not panic")?;
301311
last_index = Some(index);
302312
if !txs.is_empty() {
303313
last_active_index = Some(index);
@@ -309,6 +319,9 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
309319
insert_anchor_or_seen_at_from_status(&mut update, start_time, tx.txid, tx.status);
310320
insert_prevouts(&mut update, tx.vin);
311321
}
322+
update
323+
.evicted_ats
324+
.extend(evicted.into_iter().map(|txid| (txid, start_time)));
312325
}
313326

314327
let last_index = last_index.expect("Must be set since handles wasn't empty.");
@@ -333,7 +346,7 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
333346
/// requests to make in parallel.
334347
///
335348
/// Refer to [crate-level docs](crate) for more.
336-
fn fetch_txs_with_spks<I: IntoIterator<Item = ScriptBuf>>(
349+
fn fetch_txs_with_spks<I: IntoIterator<Item = SpkWithExpectedTxids>>(
337350
client: &esplora_client::BlockingClient,
338351
start_time: u64,
339352
inserted_txs: &mut HashSet<Txid>,

0 commit comments

Comments
 (0)