From: 志宇 Date: Thu, 15 Oct 2026 02:10:45 +0000 (+0000) Subject: fix(chain): don't panic on overlapping descriptors in release X-Git-Url: http://internal-gitweb-vhost/-client-interface/%22https:/src/struct.EncoderStringWriter.html?a=commitdiff_plain;h=bb7373ba163fccf578113a601e3e4fb5205bc230;p=bdk fix(chain): don't panic on overlapping descriptors in release Co-Authored-By: Claude Opus 5.5 --- diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 33b5c972..8790a274 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -37,9 +37,9 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25; /// and only one descriptor and each descriptor has one and only one keychain. The /// [`insert_descriptor`] method will return an error if you try and violate this invariant. This /// rule is a proxy for a stronger rule: no two descriptors should produce the same script pubkey. -/// Having two descriptors produce the same script pubkey should cause whichever keychain derives -/// the script pubkey first to be the effective owner of it but you should not rely on this -/// behaviour. ⚠ It is up you, the developer, not to violate this invariant. +/// Violating this is a contract violation: debug builds panic, and in release builds the keychain +/// that did not derive the script pubkey first cannot reveal it. ⚠ It is up you, the developer, +/// not to violate this invariant. /// /// # Revealed script pubkeys /// @@ -497,10 +497,8 @@ impl KeychainTxOutIndex { /// /// [`KeychainTxOutIndex`] will prevent you from inserting two descriptors which derive the same /// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at - /// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level - /// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have - /// subtle implications up the application stack like one UTXO being missing from one keychain - /// because it has been assigned to another which produces the same script pubkey. + /// other indices. A collision at any other index is a contract violation (see + /// [`KeychainTxOutIndex`]). pub fn insert_descriptor( &mut self, keychain: K, @@ -881,7 +879,9 @@ impl KeychainTxOutIndex { /// /// 1. The descriptor has no wildcard and already has one script revealed. /// 2. The descriptor has already revealed scripts up to the numeric bound. - /// 3. There is no descriptor associated with the given keychain. + /// + /// Also returns `None` if the next script pubkey is owned by another keychain, which is a + /// contract violation (see [`KeychainTxOutIndex`]). pub fn reveal_next_spk(&mut self, keychain: K) -> Option<(Indexed, ChangeSet)> { let mut changeset = ChangeSet::default(); let indexed_spk = self._reveal_next_spk(&mut changeset, keychain)?; @@ -900,10 +900,9 @@ impl KeychainTxOutIndex { changeset.last_revealed.insert(*did, next_index); self.replenish_inner_index(*did, &keychain, self.lookahead); } - let script = self - .inner - .spk_at_index(&(keychain.clone(), next_index)) - .expect("we just inserted it"); + // `None` only if another keychain owns this spk (a contract violation, which already + // trips the `debug_assert!` in `replenish_inner_index`). + let script = self.inner.spk_at_index(&(keychain.clone(), next_index))?; Some((next_index, script)) } diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 263a0fa8..3dea4544 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -746,3 +746,18 @@ fn when_querying_over_a_range_of_keychains_the_utxos_should_show_up() { (10_000 * (6 - 3 - /*the skipped one*/ 1)) as i64 ); } + +/// Overlapping descriptors are a contract violation, but must not panic in release builds. +/// See . +#[test] +#[cfg(not(debug_assertions))] +fn overlapping_descriptor_does_not_panic_in_release() { + const XPUB: &str = "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp"; + let wildcard = parse_descriptor(&format!("wpkh({XPUB}/0/*)")); + let single = parse_descriptor(&format!("wpkh({XPUB}/0/3)")); + + let mut index = KeychainTxOutIndex::::new(25, true); + let _ = index.insert_descriptor(0, wildcard); + let _ = index.insert_descriptor(1, single); + assert!(index.reveal_next_spk(1).is_none()); +}