• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bitcoindevkit / bdk / 6046839486

01 Sep 2023 07:48AM UTC coverage: 78.692% (+0.07%) from 78.619%
6046839486

push

github

evanlinjin
Merge bitcoindevkit/bdk#1093: fix: spks_of_all_keychains() shouldn't return an infinite iterator for non-wildcard descriptors

<a class=hub.com/bitcoindevkit/bdk/commit/e48b911c8d23cceab2071cc92085fe6675d55ca9">e48b911c8<a href="https://github.com/bitcoindevkit/bdk/commit/2867e88b64b4a8cf7136cc562ec61c077737a087"> refactor: Make test errors more readable (Daniela Brozzoni)
<a class="double-link" href="https://github.com/bitcoindevkit/bdk/commit/a7a1d9b2fb9fda4d86fe17cf90d9c0d46c4d8609">a7a1d9b2f</a><a href="https://github.com/bitcoindevkit/bdk/commit/2867e88b64b4a8cf7136cc562ec61c077737a087"> fix: non-wildcard descriptors should return an.. ..spk only if index is equal to 0 (Daniela Brozzoni)
</a><a class="double-link" href="https://github.com/bitcoindevkit/bdk/commit/cc1a43c495639f0dbe6da52501cb48f17b8cbbd2">cc1a43c49</a> fix: SpkIterator::new_with_range takes wildcards.. ..into account (Daniela Brozzoni)

Pull request description:

  ### Description

  When you pass a non-wildcard descriptor in `new_with_range`, we make
  sure that the range length is at most 1; if that's not the case, we
  shorten it.
  We would previously use `new_with_range` without this check and with a
  non-wildcard descriptor in `spks_of_all_keychains`, this meant creating
  a spkiterator that would go on producing the same spks over and over
  again, causing some issues with syncing on electrum/esplora.

  To reproduce the bug, run in `example-crates/example_electrum`:
  ```
  cargo run "sh(wsh(or_d(c:pk_k(cPGudvRLDSgeV4hH9NUofLvYxYBSRjju3cpiXmBg9K8G9k1ikCMp),c:pk_k(cSBSBHRrzqSXFmrBhLkZMzQB9q4P9MnAq92v8d9a5UveBc9sLX32))))#zp9pcfs9" scan
  ```

  ### Changelog notice

  - Fixed a bug where `KeychainTxOutIndex::spks_of_all_keychains`/`KeychainTxOutIndex::spks_of_keychain` would return an iterator yielding infinite spks even for non-wildcard descriptors.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  evanlinjin:
    ACK e48b911c8d23cceab2071cc92085fe6675d55ca9

Tree-SHA512: 87627505003696a7db7812fc5b0b523f8cf

52 of 52 new or added lines in 1 file covered. (100.0%)

8040 of 10217 relevant lines covered (78.69%)

425461.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.19
/crates/chain/src/spk_iter.rs
1
use crate::{
2
    bitcoin::{secp256k1::Secp256k1, ScriptBuf},
3
    miniscript::{Descriptor, DescriptorPublicKey},
4
};
5
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
6

7
/// Maximum [BIP32](https://bips.xyz/32) derivation index.
8
pub const BIP32_MAX_INDEX: u32 = (1 << 31) - 1;
9

10
/// An iterator for derived script pubkeys.
11
///
12
/// [`SpkIterator`] is an implementation of the [`Iterator`] trait which possesses its own `next()`
13
/// and `nth()` functions, both of which circumvent the unnecessary intermediate derivations required
14
/// when using their default implementations.
15
///
16
/// ## Examples
17
///
18
/// ```
19
/// use bdk_chain::SpkIterator;
20
/// # use miniscript::{Descriptor, DescriptorPublicKey};
21
/// # use bitcoin::{secp256k1::Secp256k1};
22
/// # use std::str::FromStr;
23
/// # let secp = bitcoin::secp256k1::Secp256k1::signing_only();
24
/// # let (descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
25
/// # let external_spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
26
/// # let external_spk_3 = descriptor.at_derivation_index(3).unwrap().script_pubkey();
27
/// # let external_spk_4 = descriptor.at_derivation_index(4).unwrap().script_pubkey();
28
///
29
/// // Creates a new script pubkey iterator starting at 0 from a descriptor.
30
/// let mut spk_iter = SpkIterator::new(&descriptor);
31
/// assert_eq!(spk_iter.next(), Some((0, external_spk_0)));
32
/// assert_eq!(spk_iter.next(), None);
33
/// ```
34
#[derive(Clone)]
×
35
pub struct SpkIterator<D> {
36
    next_index: u32,
37
    end: u32,
38
    descriptor: D,
39
    secp: Secp256k1<bitcoin::secp256k1::VerifyOnly>,
40
}
41

42
impl<D> SpkIterator<D>
43
where
44
    D: Borrow<Descriptor<DescriptorPublicKey>>,
45
{
46
    /// Creates a new script pubkey iterator starting at 0 from a descriptor.
47
    pub fn new(descriptor: D) -> Self {
3✔
48
        SpkIterator::new_with_range(descriptor, 0..=BIP32_MAX_INDEX)
3✔
49
    }
3✔
50

51
    // Creates a new script pubkey iterator from a descriptor with a given range.
52
    // If the descriptor doesn't have a wildcard, we shorten whichever range you pass in
53
    // to have length <= 1. This means that if you pass in 0..0 or 0..1 the range will
54
    // remain the same, but if you pass in 0..10, we'll shorten it to 0..1
55
    // Also note that if the descriptor doesn't have a wildcard, passing in a range starting
56
    // from n > 0, will return an empty iterator.
57
    pub(crate) fn new_with_range<R>(descriptor: D, range: R) -> Self
1,480✔
58
    where
1,480✔
59
        R: RangeBounds<u32>,
1,480✔
60
    {
1,480✔
61
        let start = match range.start_bound() {
1,480✔
62
            Bound::Included(start) => *start,
1,480✔
63
            Bound::Excluded(start) => *start + 1,
×
64
            Bound::Unbounded => u32::MIN,
×
65
        };
66

67
        let mut end = match range.end_bound() {
1,480✔
68
            Bound::Included(end) => *end + 1,
721✔
69
            Bound::Excluded(end) => *end,
750✔
70
            Bound::Unbounded => u32::MAX,
9✔
71
        };
72

73
        // Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
74
        end = end.min(BIP32_MAX_INDEX + 1);
1,480✔
75

1,480✔
76
        if !descriptor.borrow().has_wildcard() {
1,480✔
77
            // The length of the range should be at most 1
78
            if end != start {
1,008✔
79
                end = start + 1;
227✔
80
            }
786✔
81
        }
472✔
82

83
        Self {
1,480✔
84
            next_index: start,
1,480✔
85
            end,
1,480✔
86
            descriptor,
1,480✔
87
            secp: Secp256k1::verification_only(),
1,480✔
88
        }
1,480✔
89
    }
1,480✔
90
}
91

92
impl<D> Iterator for SpkIterator<D>
93
where
94
    D: Borrow<Descriptor<DescriptorPublicKey>>,
95
{
96
    type Item = (u32, ScriptBuf);
97

98
    fn next(&mut self) -> Option<Self::Item> {
1,298✔
99
        // For non-wildcard descriptors, we expect the first element to be Some((0, spk)), then None after.
1,298✔
100
        // For wildcard descriptors, we expect it to keep iterating until exhausted.
1,298✔
101
        if self.next_index >= self.end {
1,298✔
102
            return None;
777✔
103
        }
521✔
104

521✔
105
        // If the descriptor is non-wildcard, only index 0 will return an spk.
521✔
106
        if !self.descriptor.borrow().has_wildcard() && self.next_index != 0 {
521✔
107
            return None;
3✔
108
        }
518✔
109

518✔
110
        let script = self
518✔
111
            .descriptor
518✔
112
            .borrow()
518✔
113
            .derived_descriptor(&self.secp, self.next_index)
518✔
114
            .expect("the descriptor cannot need hardened derivation")
518✔
115
            .script_pubkey();
518✔
116
        let output = (self.next_index, script);
518✔
117

518✔
118
        self.next_index += 1;
518✔
119

518✔
120
        Some(output)
518✔
121
    }
1,298✔
122

123
    fn nth(&mut self, n: usize) -> Option<Self::Item> {
11✔
124
        self.next_index = self
11✔
125
            .next_index
11✔
126
            .saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
11✔
127
        self.next()
11✔
128
    }
11✔
129
}
130

131
#[cfg(test)]
132
mod test {
133
    use crate::{
134
        bitcoin::secp256k1::Secp256k1,
135
        keychain::KeychainTxOutIndex,
136
        miniscript::{Descriptor, DescriptorPublicKey},
137
        spk_iter::{SpkIterator, BIP32_MAX_INDEX},
138
    };
139

140
    #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
1✔
141
    enum TestKeychain {
142
        External,
143
        Internal,
144
    }
145

146
    fn init_txout_index() -> (
1✔
147
        KeychainTxOutIndex<TestKeychain>,
1✔
148
        Descriptor<DescriptorPublicKey>,
1✔
149
        Descriptor<DescriptorPublicKey>,
1✔
150
    ) {
1✔
151
        let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
1✔
152

1✔
153
        let secp = Secp256k1::signing_only();
1✔
154
        let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
1✔
155
        let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
1✔
156

1✔
157
        txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
1✔
158
        txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
1✔
159

1✔
160
        (txout_index, external_descriptor, internal_descriptor)
1✔
161
    }
1✔
162

163
    #[test]
1✔
164
    #[allow(clippy::iter_nth_zero)]
165
    #[rustfmt::skip]
166
    fn test_spkiterator_wildcard() {
1✔
167
        let (_, external_desc, _) = init_txout_index();
1✔
168
        let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
1✔
169
        let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
1✔
170
        let external_spk_20 = external_desc.at_derivation_index(20).unwrap().script_pubkey();
1✔
171
        let external_spk_21 = external_desc.at_derivation_index(21).unwrap().script_pubkey();
1✔
172
        let external_spk_max = external_desc.at_derivation_index(BIP32_MAX_INDEX).unwrap().script_pubkey();
1✔
173

1✔
174
        let mut external_spk = SpkIterator::new(&external_desc);
1✔
175
        let max_index = BIP32_MAX_INDEX - 22;
1✔
176

1✔
177
        assert_eq!(external_spk.next(), Some((0, external_spk_0)));
1✔
178
        assert_eq!(external_spk.nth(15), Some((16, external_spk_16)));
1✔
179
        assert_eq!(external_spk.nth(3), Some((20, external_spk_20.clone())));
1✔
180
        assert_eq!(external_spk.next(), Some((21, external_spk_21)));
1✔
181
        assert_eq!(
1✔
182
            external_spk.nth(max_index as usize),
1✔
183
            Some((BIP32_MAX_INDEX, external_spk_max))
1✔
184
        );
1✔
185
        assert_eq!(external_spk.nth(0), None);
1✔
186

187
        let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
1✔
188
        assert_eq!(external_spk.nth(20), Some((20, external_spk_20)));
1✔
189
        assert_eq!(external_spk.next(), None);
1✔
190

191
        let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
1✔
192
        assert_eq!(external_spk.nth(21), None);
1✔
193
    }
1✔
194

195
    #[test]
1✔
196
    #[allow(clippy::iter_nth_zero)]
197
    fn test_spkiterator_non_wildcard() {
1✔
198
        let secp = bitcoin::secp256k1::Secp256k1::signing_only();
1✔
199
        let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
1✔
200
        let external_spk_0 = no_wildcard_descriptor
1✔
201
            .at_derivation_index(0)
1✔
202
            .unwrap()
1✔
203
            .script_pubkey();
1✔
204

1✔
205
        let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
1✔
206

1✔
207
        assert_eq!(external_spk.next(), Some((0, external_spk_0.clone())));
1✔
208
        assert_eq!(external_spk.next(), None);
1✔
209

210
        let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
1✔
211

1✔
212
        assert_eq!(external_spk.nth(0), Some((0, external_spk_0.clone())));
1✔
213
        assert_eq!(external_spk.nth(0), None);
1✔
214

215
        let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..0);
1✔
216

1✔
217
        assert_eq!(external_spk.next(), None);
1✔
218

219
        let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..1);
1✔
220

1✔
221
        assert_eq!(external_spk.nth(0), Some((0, external_spk_0.clone())));
1✔
222
        assert_eq!(external_spk.next(), None);
1✔
223

224
        // We test that using new_with_range with range_len > 1 gives back an iterator with
225
        // range_len = 1
226
        let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..10);
1✔
227

1✔
228
        assert_eq!(external_spk.nth(0), Some((0, external_spk_0)));
1✔
229
        assert_eq!(external_spk.nth(0), None);
1✔
230

231
        // non index-0 should NOT return an spk
232
        assert_eq!(
1✔
233
            SpkIterator::new_with_range(&no_wildcard_descriptor, 1..1).next(),
1✔
234
            None
1✔
235
        );
1✔
236
        assert_eq!(
1✔
237
            SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=1).next(),
1✔
238
            None
1✔
239
        );
1✔
240
        assert_eq!(
1✔
241
            SpkIterator::new_with_range(&no_wildcard_descriptor, 1..2).next(),
1✔
242
            None
1✔
243
        );
1✔
244
        assert_eq!(
1✔
245
            SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=2).next(),
1✔
246
            None
1✔
247
        );
1✔
248
    }
1✔
249

250
    // The following dummy traits were created to test if SpkIterator is working properly.
251
    trait TestSendStatic: Send + 'static {
252
        fn test(&self) -> u32 {
×
253
            20
×
254
        }
×
255
    }
256

257
    impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
258
        fn test(&self) -> u32 {
×
259
            20
×
260
        }
×
261
    }
262
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc