Skip to content

fix(chain)!: handle descriptor SPK collisions - #2327

Open
busayo-OD wants to merge 2 commits into
bitcoindevkit:masterfrom
busayo-OD:fix/descriptor-spk-collision
Open

busayo-OD wants to merge 2 commits into
bitcoindevkit:masterfrom
busayo-OD:fix/descriptor-spk-collision

Conversation

@busayo-OD

Copy link
Copy Markdown

Description

Fixes #2277.

KeychainTxOutIndex does not currently handle collisions when a descriptor derives an SPK that is already owned by another keychain.

This PR allows wildcard descriptors to skip already-owned SPKs when revealing new scripts and continue to the next available SPK. Non-wildcard descriptors are rejected when their only SPK is already claimed, returning the new NoDerivableSpk error variant.

Notes to the reviewers

  • The rejected non-wildcard descriptor is not registered and its state is rolled back.
  • Collided SPKs remain associated with their original keychains and are not recorded as revealed for the wildcard keychain.
  • Regression tests cover non-wildcard descriptor rejection, both insertion orders, and consecutive SPK collisions.
  • The new NoDerivableSpk error needs to be handled in bdk_wallet by mapping it to DescriptorOutputCollision.

Changelog notice

  • Add NoDerivableSpk to InsertDescriptorError.
  • Handle SPK collisions when revealing wildcard descriptor outputs.
  • Reject non-wildcard descriptors whose only SPK is already claimed.

Checklists

All Submissions:

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

Allow wildcard descriptors to skip SPKs already owned by another keychain
when revealing new SPKs.

Reject non-wildcard descriptors whose only SPK is already claimed by
another keychain with a new `NoDerivableSpk` error variant.
Add regression tests for non-wildcard descriptor rejection, both descriptor
insertion orders, wildcard collision handling, and consecutive SPK collisions
during SPK revelation.
@oleonardolima oleonardolima added the bug Something isn't working label Sep 29, 2026
@oleonardolima oleonardolima moved this to Needs Review in BDK Chain Sep 29, 2026

@Dmenec Dmenec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cACK

One question about the description. It says the new error should map to DescriptorOutputCollision in bdk_wallet, but that error doesn't seem to exist there. It is a proposal or something already mentioned somewhere?


// Skip collided indices without updating `last_revealed` until an available index is found.
let mut probe = candidate;
let resolved = loop {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The skip makes reveal_to_target reveal more than asked. If index 3 is owned by another keychain, calling reveal_to_target(0, 3) after revealing up to 2 returns index 4. The loop checks next_index (3) against the target, but _reveal_next_spk then skips to 4. next_index has the same issue, it says 3 but the caller gets 4.

let (k0, k1) = colliding_descriptors();
let mut idx = KeychainTxOutIndex::<u8>::new(25, true);
idx.insert_descriptor(1u8, k1).unwrap();
idx.insert_descriptor(0u8, k0).unwrap();
let _ = idx.reveal_to_target(0u8, 2).unwrap();

assert_eq!(idx.next_index(0u8), Some((3, true)));
let (revealed, _) = idx.reveal_to_target(0u8, 3).unwrap();
assert_eq!(revealed[0].0, 4);
assert_eq!(idx.last_revealed_index(0u8), Some(4));

Maybe a test for it would be great

},
/// The descriptor's only derivable script pubkey is already owned by another keychain, so it
/// cannot produce a usable script pubkey.
NoDerivableSpk {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: what do you think about SpkAlreadyOwned? It would also help to include who owns it.

I don't think there's another case (at least in this scope) that needs the name to be that generic.

/// because it has been assigned to another which produces the same script pubkey.
///
/// A non-wildcard descriptor whose script pubkey is already claimed by another keychain is
/// rejected with [`InsertDescriptorError::NoDerivableSpk`].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: the rejection depends on what's in the other keychain's lookahead, so the same insert can return Ok in one session and NoDerivableSpk after a reload (e.g. /2/* + /2/30 with lookahead 25, after revealing up to 10). Is that intended? Maybe worth a line here.

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

Labels

bug Something isn't working

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

KeychainTxOutIndex panics on overlapping descriptors (non-index-0 spk)

3 participants