]> Untitled Git - bdk/commitdiff
fix(chain): don't panic on overlapping descriptors in release
author志宇 <hello@evanlinjin.me>
Thu, 15 Oct 2026 02:10:45 +0000 (02:10 +0000)
committer志宇 <hello@evanlinjin.me>
Thu, 15 Oct 2026 02:19:56 +0000 (02:19 +0000)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/chain/src/indexer/keychain_txout.rs
crates/chain/tests/test_keychain_txout_index.rs

index 33b5c97270cbde3822e2644b12f88fee48998084..8790a2743f9960fd0824fe07510cc044354670d2 100644 (file)
@@ -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<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     ///
     /// [`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<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     ///
     ///  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<ScriptBuf>, ChangeSet)> {
         let mut changeset = ChangeSet::default();
         let indexed_spk = self._reveal_next_spk(&mut changeset, keychain)?;
@@ -900,10 +900,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
             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))
     }
 
index 263a0fa86ff2997df03ddcf01f98fbae49bc3157..3dea4544b46f70b8aa1b1a583999815b00e7d337 100644 (file)
@@ -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 <https://github.com/bitcoindevkit/bdk/issues/2277>.
+#[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::<u8>::new(25, true);
+    let _ = index.insert_descriptor(0, wildcard);
+    let _ = index.insert_descriptor(1, single);
+    assert!(index.reveal_next_spk(1).is_none());
+}