Skip to content

Conversation

@reez
Copy link

@reez reez commented Oct 1, 2025

Description

This PR clears remaining panic paths from bdk_esplora, replacing them with proper error handling.

Attempting to address bitcoindevkit/bdk_wallet#30 for Esplora

Notes to the reviewers

Open to any and all feedback on this.

Other PR's made along these lines:

Changelog notice

  • Clear remaining panic paths from bdk_esplora.rs.

Checklists

All Submissions:

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

Comment on lines 374 to 375
let last_index =
last_index.ok_or_else(|| Box::new(esplora_client::Error::InvalidResponse))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggestion is to leave last_index as an Option and wrap stop_gap in Some(..) when doing the comparison.

But there's a much better way to handle this, which is to introduce an unused_count variable. Now when processing handles, if txs.is_empty() we increment the unused count, else update the last active index (and reset the unused count). Finally, break once the unused count reaches the gap limit. AFAICT there'd be no reason to keep track of the last_index scanned.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took your suggestion and worked it in; didn't want to push it to this branch yet if I had the wrong idea, but here is the commit reez@35538e2 on a branch that builds on this branch.

I felt like I had to keep two small guardrails from the old logic:

  • processed_any (still surface the “no handles ran” error)
  • gap_limit = stop_gap.max(1)(keeps old “stop_gap==0 still means one empty derivation” behavior)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's an error if handles turns up empty - we need a way to break from the loop once all of the keychain-spks are consumed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe stop_gap = stop_gap.max(parallel_requests), since we check at least 1 spk-index for every request in parallel_requests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super helpful thanks mammal, I made the below changes to that my side branch esplora+mammal that I can eventually fold back into this esplora branch if I'm on the right track

I don't think it's an error if handles turns up empty - we need a way to break from the loop once all of the keychain-spks are consumed.

Good call. I made this change 36073d0 based on that

Maybe stop_gap = stop_gap.max(parallel_requests), since we check at least 1 spk-index for every request in parallel_requests.

Good catch, made another change 590edcf based on this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pulled these changes into this pr now

@oleonardolima oleonardolima moved this to In Progress in BDK Chain Oct 1, 2025
@reez reez marked this pull request as ready for review October 6, 2025 16:48
let (index, txs, evicted) = handle.join().expect("thread must not panic")?;
let handle_result = handle
.join()
.map_err(|_| Box::new(esplora_client::Error::InvalidResponse))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you're dropping the error it won't be very informative to propagate it upward. Instead this module can define its own Error type to handle errors that aren't covered by esplora_client::Error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to address this with new commit 9ea0dd0, trying to do the most simple thing I could think of that addresses what you mentioned

@reez reez changed the title (draft) refactor(esplora): clear remaining panic paths refactor(esplora): clear remaining panic paths Oct 9, 2025
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

Some great simplifications in logic. Some parts are over-engineered.

Note that panics and expects indicate that "we should never hit this - unless there is a bug!" and I think it's justified to have them (where reasonable).

.join()
.map_err(Error::from_thread_panic)?;
let (index, txs, evicted) = handle_result?;
processed_any = true;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont need processed_any

@reez
Copy link
Author

reez commented Oct 30, 2025

Updated PR due to great feedback from Evan, Mammal, Leonardo.

Noteworthy:

  • Introduced a crate-wide Error enum (in lib.rs) with Client and Checkpoint variants. Both async and blocking extensions now return this so real Esplora failures bubble up via Client while checkpoint ordering issues surface as Checkpoint instead of panicking or masquerading as InvalidResponse
  • Removed the custom ThreadPanic and reverted as suggested so we only guard against genuine remote errors
  • Dropped the processed_any guard added previously
  • Eliminated tests that provided no major value

Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid returning errors for internal logic bugs in BDK itself.

Comment on lines +42 to +45
pub enum Error {
Client(esplora_client::Error),
Checkpoint(CheckPoint<BlockHash>),
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing doc-comments for the Error type and it's variants!

We should make it obvious to the reader when the Error::CheckPoint variant occurs.

Edit: I don't think the CheckPoint variant is needed, so this Error type is not needed either.

Comment on lines 245 to +247
tip = tip
.extend(conflicts.into_iter().rev().map(|b| (b.height, b.hash)))
.expect("evicted are in order");
.map_err(Error::Checkpoint)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think panic/expect here is more appropriate.

  1. CheckPoint::extend only errors if a block is not higher than the previous.
  2. The logic to populate conflicts uses heights from local_tip. local_tip is guaranteed to have ordered heights (since it is a CheckPoint type).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants