From 3e76f046ba8ad9853bd0103a2684e7db56a77b1e Mon Sep 17 00:00:00 2001 From: Anirudh Date: Wed, 5 Aug 2026 01:44:16 +0530 Subject: [PATCH 1/2] fix(chain): prevent overflow --- crates/chain/src/indexer/keychain_txout.rs | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 7973a0254e..5e31f5ff5c 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -540,11 +540,7 @@ impl KeychainTxOutIndex { 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); - - if let Some(temp_lookahead) = temp_lookahead { + if let Some(temp_lookahead) = Self::lookahead_delta(target_index, next_index) { self.replenish_inner_index_keychain(keychain, temp_lookahead); } } @@ -552,6 +548,17 @@ impl KeychainTxOutIndex { changeset } + /// Computes how many additional lookahead scripts are needed to cover `target_index` + /// (inclusive), given the next index that would be derived (`next_index`). + /// + /// Returns `None` if `target_index` is already covered (i.e. `target_index < next_index`). + fn lookahead_delta(target_index: u32, next_index: u32) -> Option { + target_index + .saturating_add(1) + .checked_sub(next_index) + .filter(|&index| index > 0) + } + fn replenish_inner_index_did(&mut self, did: DescriptorId, lookahead: u32) { if let Some(keychain) = self.descriptor_id_to_keychain.get(&did).cloned() { self.replenish_inner_index(did, &keychain, lookahead); @@ -1146,6 +1153,24 @@ mod test { use bitcoin::secp256k1::Secp256k1; use miniscript::Descriptor; + /// `lookahead_delta` must not panic (via overflow) when `target_index` is `u32::MAX`, and + /// must saturate rather than silently returning a wrong/empty result. + #[test] + fn lookahead_delta_does_not_overflow_at_u32_max() { + // Fresh keychain (next_index = 0): delta should saturate to u32::MAX, not overflow. + assert_eq!( + KeychainTxOutIndex::::lookahead_delta(u32::MAX, 0), + Some(u32::MAX) + ); + // Target already covered by next_index: no lookahead needed. + assert_eq!( + KeychainTxOutIndex::::lookahead_delta(u32::MAX, u32::MAX), + None + ); + // Normal, non-boundary case still behaves as before. + assert_eq!(KeychainTxOutIndex::::lookahead_delta(10, 5), Some(6)); + } + // Test that `KeychainTxOutIndex` uses the spk cache. // And the indexed spks are as expected. #[test] From 932fcc2d242a5b844f1bf7466132a59e76be5783 Mon Sep 17 00:00:00 2001 From: Anirudh Date: Wed, 19 Aug 2026 20:21:24 +0530 Subject: [PATCH 2/2] fix(chain): inline lookahead overflow fix, drop redundant test --- crates/chain/src/indexer/keychain_txout.rs | 36 ++++------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 5e31f5ff5c..1b5c6a478b 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -540,7 +540,12 @@ impl KeychainTxOutIndex { 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()) { - if let Some(temp_lookahead) = Self::lookahead_delta(target_index, next_index) { + let temp_lookahead = target_index + .saturating_add(1) + .checked_sub(next_index) + .filter(|&index| index > 0); + + if let Some(temp_lookahead) = temp_lookahead { self.replenish_inner_index_keychain(keychain, temp_lookahead); } } @@ -548,17 +553,6 @@ impl KeychainTxOutIndex { changeset } - /// Computes how many additional lookahead scripts are needed to cover `target_index` - /// (inclusive), given the next index that would be derived (`next_index`). - /// - /// Returns `None` if `target_index` is already covered (i.e. `target_index < next_index`). - fn lookahead_delta(target_index: u32, next_index: u32) -> Option { - target_index - .saturating_add(1) - .checked_sub(next_index) - .filter(|&index| index > 0) - } - fn replenish_inner_index_did(&mut self, did: DescriptorId, lookahead: u32) { if let Some(keychain) = self.descriptor_id_to_keychain.get(&did).cloned() { self.replenish_inner_index(did, &keychain, lookahead); @@ -1153,24 +1147,6 @@ mod test { use bitcoin::secp256k1::Secp256k1; use miniscript::Descriptor; - /// `lookahead_delta` must not panic (via overflow) when `target_index` is `u32::MAX`, and - /// must saturate rather than silently returning a wrong/empty result. - #[test] - fn lookahead_delta_does_not_overflow_at_u32_max() { - // Fresh keychain (next_index = 0): delta should saturate to u32::MAX, not overflow. - assert_eq!( - KeychainTxOutIndex::::lookahead_delta(u32::MAX, 0), - Some(u32::MAX) - ); - // Target already covered by next_index: no lookahead needed. - assert_eq!( - KeychainTxOutIndex::::lookahead_delta(u32::MAX, u32::MAX), - None - ); - // Normal, non-boundary case still behaves as before. - assert_eq!(KeychainTxOutIndex::::lookahead_delta(10, 5), Some(6)); - } - // Test that `KeychainTxOutIndex` uses the spk cache. // And the indexed spks are as expected. #[test]