]> Untitled Git - bdk/commitdiff
fix(chain): fix integer overflow in lookahead_to_target
authorbusayo-OD <toyinoluwabusayo@gmail.com>
Thu, 10 Sep 2026 22:42:56 +0000 (23:42 +0100)
committerbusayo-OD <toyinoluwabusayo@gmail.com>
Thu, 10 Sep 2026 22:42:56 +0000 (23:42 +0100)
lookahead_to_target treated an out-of-range target_index as a
silent no-op, which is wrong — it should clamp to BIP32_MAX_INDEX
and replenish up to that point, matching reveal_to_target's
best-effort semantics.

Use saturating_add/saturating_sub, which no longer needs checked
arithmetic since replenish_inner_index's stop_index now clamps
correctly on its own.

crates/chain/src/indexer/keychain_txout.rs

index c5f3b62e567ad2474eecc4ca3fd56796b35b530f..a7ac9d6ef2ef7b8e1d3c3ef110b0a18c143e4357 100644 (file)
@@ -563,11 +563,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     pub fn lookahead_to_target(&mut self, keychain: K, target_index: u32) -> ChangeSet {
         let mut changeset = ChangeSet::default();
         if let Some((next_index, _)) = self.next_index(keychain.clone()) {
-            let temp_lookahead = (target_index + 1)
-                .checked_sub(next_index)
-                .filter(|&index| index > 0);
+            let temp_lookahead = target_index.saturating_add(1).saturating_sub(next_index);
 
-            if let Some(temp_lookahead) = temp_lookahead {
+            if temp_lookahead > 0 {
                 self.replenish_inner_index_keychain(keychain, temp_lookahead);
             }
         }
@@ -1307,4 +1305,28 @@ mod test {
         assert_eq!(spks.len(), 1);
         assert_eq!(spks[0].0, BIP32_MAX_INDEX);
     }
+
+    #[test]
+    fn lookahead_to_target_past_bip32_max_clamps() {
+        let mut index = indexer_with_last_revealed_at(BIP32_MAX_INDEX - 1);
+        // `u32::MAX` is past `BIP32_MAX_INDEX`, so it must clamp down to it and store the final
+        // spk.
+        let changeset = index.lookahead_to_target(0i32, u32::MAX);
+        assert!(
+            changeset.last_revealed.is_empty(),
+            "lookahead must not reveal"
+        );
+        // `spk_at_index` is aware of lookahead spks.
+        assert!(index.spk_at_index(0i32, BIP32_MAX_INDEX).is_some());
+    }
+
+    #[test]
+    fn lookahead_to_target_at_bip32_max_does_not_overflow() {
+        let mut index = indexer_with_last_revealed_at(BIP32_MAX_INDEX);
+        let changeset = index.lookahead_to_target(0i32, u32::MAX - 1);
+        assert!(
+            changeset.last_revealed.is_empty(),
+            "lookahead must not reveal"
+        );
+    }
 }