From 1cf692d4606c47f481fc14cc3da4112ebc5a8420 Mon Sep 17 00:00:00 2001 From: bhuvan-somisetty Date: Wed, 28 Oct 2026 10:42:35 +0530 Subject: [PATCH] fix(chain): return false for untracked indices in SpkTxOutIndex::is_used Previously, `SpkTxOutIndex::is_used` checked `!self.unused.contains(index)`. Because `self.unused` only tracks indices that were explicitly added to the index, any untracked index was absent from `self.unused`, causing `is_used` to evaluate to `true`. Check `self.spks.contains_key(index)` in `is_used` to ensure untracked indices return `false`. Also update documentation and add unit tests for `SpkTxOutIndex` and `KeychainTxOutIndex`. Closes #2444 Signed-off-by: bhuvan-somisetty --- crates/chain/src/indexer/keychain_txout.rs | 2 ++ crates/chain/src/indexer/spk_txout.rs | 4 ++- .../chain/tests/test_keychain_txout_index.rs | 26 +++++++++++++++++++ crates/chain/tests/test_spk_txout_index.rs | 16 ++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 4717cffdc..e6cb1aa78 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -390,6 +390,8 @@ impl KeychainTxOutIndex { /// Here, "unused" means that after the script pubkey was stored in the index, the index has /// never scanned a transaction output with it. /// + /// Returns `false` for an unrevealed index. + /// /// This calls [`SpkTxOutIndex::is_used`] internally. pub fn is_used(&self, keychain: K, index: u32) -> bool { self.inner.is_used(&(keychain, index)) diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs index 1bd889cbb..0c1688ed1 100644 --- a/crates/chain/src/indexer/spk_txout.rs +++ b/crates/chain/src/indexer/spk_txout.rs @@ -246,8 +246,10 @@ impl SpkTxOutIndex { /// /// Here, "unused" means that after the script pubkey was stored in the index, the index has /// never scanned a transaction output with it. + /// + /// Returns `false` if the script pubkey at `index` is not present in the index. pub fn is_used(&self, index: &I) -> bool { - !self.unused.contains(index) + self.spks.contains_key(index) && !self.unused.contains(index) } /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 3dea4544b..8c736653b 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -761,3 +761,29 @@ fn overlapping_descriptor_does_not_panic_in_release() { let _ = index.insert_descriptor(1, single); assert!(index.reveal_next_spk(1).is_none()); } + +#[test] +fn is_used_returns_false_for_unrevealed_or_untracked_index() { + let mut index = KeychainTxOutIndex::::default(); + assert!(!index.is_used(TestKeychain::External, 0)); + assert!(!index.is_used(TestKeychain::External, 100)); + + let descriptor = parse_descriptor(DESCRIPTORS[0]); + let _ = index + .insert_descriptor(TestKeychain::External, descriptor) + .unwrap(); + + // Not revealed yet, so not added to the internal spk index + assert!(!index.is_used(TestKeychain::External, 0)); + assert!(!index.is_used(TestKeychain::External, 1)); + + // Reveal index 0 + let _ = index.reveal_next_spk(TestKeychain::External).unwrap(); + assert!(!index.is_used(TestKeychain::External, 0)); + assert!(!index.is_used(TestKeychain::External, 1)); + + // Mark index 0 as used + assert!(index.mark_used(TestKeychain::External, 0)); + assert!(index.is_used(TestKeychain::External, 0)); + assert!(!index.is_used(TestKeychain::External, 1)); +} diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index 9f527760a..71066f203 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -196,6 +196,22 @@ fn spk_txout_spent_created_txouts() { ); } +#[test] +fn is_used_returns_false_for_untracked_index() { + let mut index = SpkTxOutIndex::::default(); + assert!(!index.is_used(&0)); + assert!(!index.is_used(&1)); + + let spk = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap(); + index.insert_spk(0, spk); + assert!(!index.is_used(&0)); + assert!(!index.is_used(&1)); + + index.mark_used(&0); + assert!(index.is_used(&0)); + assert!(!index.is_used(&1)); +} + #[test] fn mark_used() { let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();