]> Untitled Git - bdk/commitdiff
fix(chain): reject an occupied index in insert_spk
authorhassan <144530465+h55n@users.noreply.github.com>
Fri, 2 Oct 2026 00:18:20 +0000 (05:48 +0530)
committer志宇 <hello@evanlinjin.me>
Sun, 18 Oct 2026 09:17:00 +0000 (20:17 +1100)
Reject an occupied index before changing either lookup map. Preserve existing script, scanned-output and used-status mappings on a rejected insertion, matching the existing behavior for an occupied script.

Signed-off-by: hassan <144530465+h55n@users.noreply.github.com>
crates/chain/src/indexer/spk_txout.rs
crates/chain/tests/test_spk_txout_index.rs

index e63eb1201ed50ac426b73a64d5acb2632054e475..1bd889cbbbd6fa75f3e4b310ec5e81f74905a39b 100644 (file)
@@ -193,11 +193,15 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
         &self.spks
     }
 
-    /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in
-    /// the map
+    /// Adds a script pubkey to scan for.
     ///
-    /// the index will look for outputs spending to this spk whenever it scans new data.
+    /// The indexer will look for outputs spending to this spk whenever it scans new data.
+    ///
+    /// Returns `false` and does nothing if the script pubkey or index already exists.
     pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool {
+        if self.spks.contains_key(&index) {
+            return false;
+        }
         match self.spk_indices.entry(spk.clone()) {
             Entry::Vacant(value) => {
                 value.insert(index.clone());
index 591a2b1c6389a97b4bc72624c07d034ba4457512..9f527760acb4c6f0a278dad1b9da0835325c7f37 100644 (file)
@@ -430,3 +430,52 @@ fn outputs_in_range_excluded_bounds() {
         .collect();
     assert_eq!(outputs_empty.len(), 0);
 }
+
+#[test]
+fn insert_spk_rejects_reused_index_and_script() {
+    let old_script = ScriptBuf::from_bytes(vec![0x51]);
+    let new_script = ScriptBuf::from_bytes(vec![0x52]);
+    let mut index = SpkTxOutIndex::<u32>::default();
+    assert!(index.insert_spk(0, old_script.clone()));
+    assert!(!index.insert_spk(0, new_script.clone()));
+    assert!(!index.insert_spk(1, old_script.clone()));
+    assert!(!index.insert_spk(0, old_script.clone()));
+    assert_eq!(index.spk_at_index(&0), Some(old_script.clone()));
+    assert_eq!(index.index_of_spk(&old_script), Some(&0));
+    assert_eq!(index.index_of_spk(&new_script), None);
+    assert_eq!(index.spk_at_index(&1), None);
+    assert_eq!(
+        index.unused_spks(..).collect::<Vec<_>>(),
+        vec![(&0, old_script)]
+    );
+    assert!(index.insert_spk(1, new_script.clone()));
+    assert_eq!(index.index_of_spk(&new_script), Some(&1));
+}
+
+#[test]
+fn insert_spk_preserves_scanned_output_and_used_status() {
+    let old_script = ScriptBuf::from_bytes(vec![0x51]);
+    let new_script = ScriptBuf::from_bytes(vec![0x52]);
+    let mut index = SpkTxOutIndex::<u32>::default();
+    assert!(index.insert_spk(0, old_script.clone()));
+    let old_output = TxOut {
+        value: Amount::from_sat(1000),
+        script_pubkey: old_script.clone(),
+    };
+    let old_outpoint = OutPoint::null();
+    assert_eq!(index.scan_txout(old_outpoint, &old_output), Some(&0));
+    assert!(!index.insert_spk(0, new_script.clone()));
+    assert!(index.is_used(&0));
+    assert_eq!(index.txout(old_outpoint), Some((&0, &old_output)));
+    assert_eq!(index.spk_at_index(&0), Some(old_script));
+    let new_output = TxOut {
+        value: Amount::from_sat(2000),
+        script_pubkey: new_script,
+    };
+    let new_outpoint = OutPoint {
+        vout: 1,
+        ..old_outpoint
+    };
+    assert_eq!(index.scan_txout(new_outpoint, &new_output), None);
+    assert_eq!(index.txout(new_outpoint), None);
+}