Skip to content

Commit 0cbf254

Browse files
committed
refactor(esplora): clear remaining panic paths
1 parent faf520d commit 0cbf254

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ async fn chain_update<S: Sleeper>(
264264

265265
tip = tip
266266
.extend(conflicts.into_iter().rev().map(|b| (b.height, b.hash)))
267-
.expect("evicted are in order");
267+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
268268

269269
for (anchor, _txid) in anchors {
270270
let height = anchor.block_id.height;
@@ -314,8 +314,9 @@ where
314314
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>, HashSet<Txid>);
315315

316316
let mut update = TxUpdate::<ConfirmationBlockTime>::default();
317-
let mut last_index = Option::<u32>::None;
318317
let mut last_active_index = Option::<u32>::None;
318+
let mut consecutive_unused = 0usize;
319+
let gap_limit = stop_gap.max(parallel_requests.max(1));
319320

320321
loop {
321322
let handles = keychain_spks
@@ -352,8 +353,10 @@ where
352353
}
353354

354355
for (index, txs, evicted) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
355-
last_index = Some(index);
356-
if !txs.is_empty() {
356+
if txs.is_empty() {
357+
consecutive_unused = consecutive_unused.saturating_add(1);
358+
} else {
359+
consecutive_unused = 0;
357360
last_active_index = Some(index);
358361
}
359362
for tx in txs {
@@ -368,13 +371,7 @@ where
368371
.extend(evicted.into_iter().map(|txid| (txid, start_time)));
369372
}
370373

371-
let last_index = last_index.expect("Must be set since handles wasn't empty.");
372-
let gap_limit_reached = if let Some(i) = last_active_index {
373-
last_index >= i.saturating_add(stop_gap as u32)
374-
} else {
375-
last_index + 1 >= stop_gap as u32
376-
};
377-
if gap_limit_reached {
374+
if consecutive_unused >= gap_limit {
378375
break;
379376
}
380377
}
@@ -571,6 +568,15 @@ mod test {
571568
}};
572569
}
573570

571+
#[test]
572+
fn ensure_last_index_none_returns_error() {
573+
let last_index: Option<u32> = None;
574+
let err = last_index
575+
.ok_or_else(|| Box::new(esplora_client::Error::InvalidResponse))
576+
.unwrap_err();
577+
assert!(matches!(*err, esplora_client::Error::InvalidResponse));
578+
}
579+
574580
// Test that `chain_update` fails due to wrong network.
575581
#[tokio::test]
576582
async fn test_chain_update_wrong_network_error() -> anyhow::Result<()> {

crates/esplora/src/blocking_ext.rs

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ fn chain_update(
249249

250250
tip = tip
251251
.extend(conflicts.into_iter().rev().map(|b| (b.height, b.hash)))
252-
.expect("evicted are in order");
252+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
253253

254254
for (anchor, _) in anchors {
255255
let height = anchor.block_id.height;
@@ -282,8 +282,10 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<SpkWithExpectedTxids>
282282
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>, HashSet<Txid>);
283283

284284
let mut update = TxUpdate::<ConfirmationBlockTime>::default();
285-
let mut last_index = Option::<u32>::None;
286285
let mut last_active_index = Option::<u32>::None;
286+
let mut consecutive_unused = 0usize;
287+
let mut processed_any = false;
288+
let gap_limit = stop_gap.max(1);
287289

288290
loop {
289291
let handles = keychain_spks
@@ -316,13 +318,22 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<SpkWithExpectedTxids>
316318
.collect::<Vec<JoinHandle<Result<TxsOfSpkIndex, Error>>>>();
317319

318320
if handles.is_empty() {
321+
if !processed_any {
322+
return Err(Box::new(esplora_client::Error::InvalidResponse));
323+
}
319324
break;
320325
}
321326

322327
for handle in handles {
323-
let (index, txs, evicted) = handle.join().expect("thread must not panic")?;
324-
last_index = Some(index);
325-
if !txs.is_empty() {
328+
let handle_result = handle
329+
.join()
330+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
331+
let (index, txs, evicted) = handle_result?;
332+
processed_any = true;
333+
if txs.is_empty() {
334+
consecutive_unused = consecutive_unused.saturating_add(1);
335+
} else {
336+
consecutive_unused = 0;
326337
last_active_index = Some(index);
327338
}
328339
for tx in txs {
@@ -337,13 +348,7 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<SpkWithExpectedTxids>
337348
.extend(evicted.into_iter().map(|txid| (txid, start_time)));
338349
}
339350

340-
let last_index = last_index.expect("Must be set since handles wasn't empty.");
341-
let gap_limit_reached = if let Some(i) = last_active_index {
342-
last_index >= i.saturating_add(stop_gap as u32)
343-
} else {
344-
last_index + 1 >= stop_gap as u32
345-
};
346-
if gap_limit_reached {
351+
if consecutive_unused >= gap_limit {
347352
break;
348353
}
349354
}
@@ -417,7 +422,10 @@ fn fetch_txs_with_txids<I: IntoIterator<Item = Txid>>(
417422
}
418423

419424
for handle in handles {
420-
let (txid, tx_info) = handle.join().expect("thread must not panic")?;
425+
let handle_result = handle
426+
.join()
427+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
428+
let (txid, tx_info) = handle_result?;
421429
if let Some(tx_info) = tx_info {
422430
if inserted_txs.insert(txid) {
423431
update.txs.push(tx_info.to_tx().into());
@@ -478,7 +486,10 @@ fn fetch_txs_with_outpoints<I: IntoIterator<Item = OutPoint>>(
478486
}
479487

480488
for handle in handles {
481-
if let Some(op_status) = handle.join().expect("thread must not panic")? {
489+
let handle_result = handle
490+
.join()
491+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
492+
if let Some(op_status) = handle_result? {
482493
let spend_txid = match op_status.txid {
483494
Some(txid) => txid,
484495
None => continue,
@@ -511,7 +522,7 @@ fn fetch_txs_with_outpoints<I: IntoIterator<Item = OutPoint>>(
511522
#[cfg(test)]
512523
#[cfg_attr(coverage_nightly, coverage(off))]
513524
mod test {
514-
use crate::blocking_ext::{chain_update, fetch_latest_blocks};
525+
use crate::blocking_ext::{chain_update, fetch_latest_blocks, Error};
515526
use bdk_chain::bitcoin;
516527
use bdk_chain::bitcoin::hashes::Hash;
517528
use bdk_chain::bitcoin::Txid;
@@ -529,6 +540,35 @@ mod test {
529540
}};
530541
}
531542

543+
#[test]
544+
fn thread_join_panic_maps_to_error() {
545+
let handle = std::thread::spawn(|| -> Result<(), Error> {
546+
panic!("expected panic for test coverage");
547+
});
548+
549+
let res = (|| -> Result<(), Error> {
550+
let handle_result = handle
551+
.join()
552+
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
553+
handle_result?;
554+
Ok(())
555+
})();
556+
557+
assert!(matches!(
558+
*res.unwrap_err(),
559+
esplora_client::Error::InvalidResponse
560+
));
561+
}
562+
563+
#[test]
564+
fn ensure_last_index_none_returns_error() {
565+
let last_index: Option<u32> = None;
566+
let err = last_index
567+
.ok_or_else(|| Box::new(esplora_client::Error::InvalidResponse))
568+
.unwrap_err();
569+
assert!(matches!(*err, esplora_client::Error::InvalidResponse));
570+
}
571+
532572
macro_rules! local_chain {
533573
[ $(($height:expr, $block_hash:expr)), * ] => {{
534574
#[allow(unused_mut)]

0 commit comments

Comments
 (0)