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

bitcoindevkit / bdk / 15717531900

17 Jun 2025 08:36PM CUT coverage: 82.396%. Remained the same
15717531900

push

github

notmandatory
Merge bitcoindevkit/bdk#1978: ci: automated update to rustc 1.87.0

<a class=hub.com/bitcoindevkit/bdk/commit/<a class="double-link" href="https://git"><a class=hub.com/bitcoindevkit/bdk/commit/54e2b533a261ade79bfc6e17215ddde22c5f3248">54e2b533a ci: automated update to rustc 1.87.0 (Github Action)

Pull request description:

  Automated update to Github CI workflow `cont_integration.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action

ACKs for top commit:
  ValuedMammal:
    ACK 54e2b533a261ade79bfc6e17215ddde22c5f3248
  notmandatory:
    ACK 54e2b533a261ade79bfc6e17215ddde22c5f3248

Tree-SHA512: ead615e8250fbb4a1db4ac182adabf82954

5579 of 6771 relevant lines covered (82.4%)

31375.22 hits per line

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

78.73
/crates/electrum/src/bdk_electrum_client.rs
1
use bdk_core::{
2
    bitcoin::{block::Header, BlockHash, OutPoint, Transaction, Txid},
3
    collections::{BTreeMap, HashMap, HashSet},
4
    spk_client::{
5
        FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse,
6
    },
7
    BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate,
8
};
9
use electrum_client::{ElectrumApi, Error, HeaderNotification};
10
use std::sync::{Arc, Mutex};
11

12
/// We include a chain suffix of a certain length for the purpose of robustness.
13
const CHAIN_SUFFIX_LENGTH: u32 = 8;
14

15
/// Wrapper around an [`electrum_client::ElectrumApi`] which includes an internal in-memory
16
/// transaction cache to avoid re-fetching already downloaded transactions.
17
#[derive(Debug)]
18
pub struct BdkElectrumClient<E> {
19
    /// The internal [`electrum_client::ElectrumApi`]
20
    pub inner: E,
21
    /// The transaction cache
22
    tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
23
    /// The header cache
24
    block_header_cache: Mutex<HashMap<u32, Header>>,
25
}
26

27
impl<E: ElectrumApi> BdkElectrumClient<E> {
28
    /// Creates a new bdk client from a [`electrum_client::ElectrumApi`]
29
    pub fn new(client: E) -> Self {
9✔
30
        Self {
9✔
31
            inner: client,
9✔
32
            tx_cache: Default::default(),
9✔
33
            block_header_cache: Default::default(),
9✔
34
        }
9✔
35
    }
9✔
36

37
    /// Inserts transactions into the transaction cache so that the client will not fetch these
38
    /// transactions.
39
    pub fn populate_tx_cache(&self, txs: impl IntoIterator<Item = impl Into<Arc<Transaction>>>) {
×
40
        let mut tx_cache = self.tx_cache.lock().unwrap();
×
41
        for tx in txs {
×
42
            let tx = tx.into();
×
43
            let txid = tx.compute_txid();
×
44
            tx_cache.insert(txid, tx);
×
45
        }
×
46
    }
×
47

48
    /// Fetch transaction of given `txid`.
49
    ///
50
    /// If it hits the cache it will return the cached version and avoid making the request.
51
    pub fn fetch_tx(&self, txid: Txid) -> Result<Arc<Transaction>, Error> {
213✔
52
        let tx_cache = self.tx_cache.lock().unwrap();
213✔
53

54
        if let Some(tx) = tx_cache.get(&txid) {
213✔
55
            return Ok(Arc::clone(tx));
82✔
56
        }
131✔
57

131✔
58
        drop(tx_cache);
131✔
59

60
        let tx = Arc::new(self.inner.transaction_get(&txid)?);
131✔
61

62
        self.tx_cache.lock().unwrap().insert(txid, Arc::clone(&tx));
131✔
63

131✔
64
        Ok(tx)
131✔
65
    }
213✔
66

67
    /// Fetch block header of given `height`.
68
    ///
69
    /// If it hits the cache it will return the cached version and avoid making the request.
70
    fn fetch_header(&self, height: u32) -> Result<Header, Error> {
146✔
71
        let block_header_cache = self.block_header_cache.lock().unwrap();
146✔
72

73
        if let Some(header) = block_header_cache.get(&height) {
146✔
74
            return Ok(*header);
31✔
75
        }
115✔
76

115✔
77
        drop(block_header_cache);
115✔
78

115✔
79
        self.update_header(height)
115✔
80
    }
146✔
81

82
    /// Update a block header at given `height`. Returns the updated header.
83
    fn update_header(&self, height: u32) -> Result<Header, Error> {
115✔
84
        let header = self.inner.block_header(height as usize)?;
115✔
85

86
        self.block_header_cache
115✔
87
            .lock()
115✔
88
            .unwrap()
115✔
89
            .insert(height, header);
115✔
90

115✔
91
        Ok(header)
115✔
92
    }
115✔
93

94
    /// Broadcasts a transaction to the network.
95
    ///
96
    /// This is a re-export of [`ElectrumApi::transaction_broadcast`].
97
    pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
×
98
        self.inner.transaction_broadcast(tx)
×
99
    }
×
100

101
    /// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
102
    /// returns updates for [`bdk_chain`] data structures.
103
    ///
104
    /// - `request`: struct with data required to perform a spk-based blockchain client full scan,
105
    ///   see [`FullScanRequest`].
106
    /// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
107
    ///   associated transactions.
108
    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
109
    ///   request.
110
    /// - `fetch_prev_txouts`: specifies whether we want previous `TxOut`s for fee calculation. Note
111
    ///   that this requires additional calls to the Electrum server, but is necessary for
112
    ///   calculating the fee on a transaction if your wallet does not own the inputs. Methods like
113
    ///   [`Wallet.calculate_fee`] and [`Wallet.calculate_fee_rate`] will return a
114
    ///   [`CalculateFeeError::MissingTxOut`] error if those `TxOut`s are not present in the
115
    ///   transaction graph.
116
    ///
117
    /// [`bdk_chain`]: ../bdk_chain/index.html
118
    /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut
119
    /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee
120
    /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate
121
    pub fn full_scan<K: Ord + Clone>(
4✔
122
        &self,
4✔
123
        request: impl Into<FullScanRequest<K>>,
4✔
124
        stop_gap: usize,
4✔
125
        batch_size: usize,
4✔
126
        fetch_prev_txouts: bool,
4✔
127
    ) -> Result<FullScanResponse<K>, Error> {
4✔
128
        let mut request: FullScanRequest<K> = request.into();
4✔
129
        let start_time = request.start_time();
4✔
130

131
        let tip_and_latest_blocks = match request.chain_tip() {
4✔
132
            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
4✔
133
            None => None,
×
134
        };
135

136
        let mut tx_update = TxUpdate::<ConfirmationBlockTime>::default();
4✔
137
        let mut last_active_indices = BTreeMap::<K, u32>::default();
4✔
138
        for keychain in request.keychains() {
4✔
139
            let spks = request
4✔
140
                .iter_spks(keychain.clone())
4✔
141
                .map(|(spk_i, spk)| (spk_i, SpkWithExpectedTxids::from(spk)));
30✔
142
            if let Some(last_active_index) =
3✔
143
                self.populate_with_spks(start_time, &mut tx_update, spks, stop_gap, batch_size)?
4✔
144
            {
3✔
145
                last_active_indices.insert(keychain, last_active_index);
3✔
146
            }
3✔
147
        }
148

149
        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
150
        if fetch_prev_txouts {
4✔
151
            self.fetch_prev_txout(&mut tx_update)?;
×
152
        }
4✔
153

154
        let chain_update = match tip_and_latest_blocks {
4✔
155
            Some((chain_tip, latest_blocks)) => Some(chain_update(
4✔
156
                chain_tip,
4✔
157
                &latest_blocks,
4✔
158
                tx_update.anchors.iter().cloned(),
4✔
159
            )?),
4✔
160
            _ => None,
×
161
        };
162

163
        Ok(FullScanResponse {
4✔
164
            tx_update,
4✔
165
            chain_update,
4✔
166
            last_active_indices,
4✔
167
        })
4✔
168
    }
4✔
169

170
    /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
171
    /// and returns updates for [`bdk_chain`] data structures.
172
    ///
173
    /// - `request`: struct with data required to perform a spk-based blockchain client sync, see
174
    ///   [`SyncRequest`]
175
    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
176
    ///   request
177
    /// - `fetch_prev_txouts`: specifies whether we want previous `TxOut`s for fee calculation. Note
178
    ///   that this requires additional calls to the Electrum server, but is necessary for
179
    ///   calculating the fee on a transaction if your wallet does not own the inputs. Methods like
180
    ///   [`Wallet.calculate_fee`] and [`Wallet.calculate_fee_rate`] will return a
181
    ///   [`CalculateFeeError::MissingTxOut`] error if those `TxOut`s are not present in the
182
    ///   transaction graph.
183
    ///
184
    /// If the scripts to sync are unknown, such as when restoring or importing a keychain that
185
    /// may include scripts that have been used, use [`full_scan`] with the keychain.
186
    ///
187
    /// [`full_scan`]: Self::full_scan
188
    /// [`bdk_chain`]: ../bdk_chain/index.html
189
    /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut
190
    /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee
191
    /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate
192
    pub fn sync<I: 'static>(
19✔
193
        &self,
19✔
194
        request: impl Into<SyncRequest<I>>,
19✔
195
        batch_size: usize,
19✔
196
        fetch_prev_txouts: bool,
19✔
197
    ) -> Result<SyncResponse, Error> {
19✔
198
        let mut request: SyncRequest<I> = request.into();
19✔
199
        let start_time = request.start_time();
19✔
200

201
        let tip_and_latest_blocks = match request.chain_tip() {
19✔
202
            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
18✔
203
            None => None,
1✔
204
        };
205

206
        let mut tx_update = TxUpdate::<ConfirmationBlockTime>::default();
19✔
207
        self.populate_with_spks(
19✔
208
            start_time,
19✔
209
            &mut tx_update,
19✔
210
            request
19✔
211
                .iter_spks_with_expected_txids()
19✔
212
                .enumerate()
19✔
213
                .map(|(i, spk)| (i as u32, spk)),
20✔
214
            usize::MAX,
19✔
215
            batch_size,
19✔
216
        )?;
19✔
217
        self.populate_with_txids(start_time, &mut tx_update, request.iter_txids())?;
19✔
218
        self.populate_with_outpoints(start_time, &mut tx_update, request.iter_outpoints())?;
19✔
219

220
        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
221
        if fetch_prev_txouts {
19✔
222
            self.fetch_prev_txout(&mut tx_update)?;
18✔
223
        }
1✔
224

225
        let chain_update = match tip_and_latest_blocks {
19✔
226
            Some((chain_tip, latest_blocks)) => Some(chain_update(
18✔
227
                chain_tip,
18✔
228
                &latest_blocks,
18✔
229
                tx_update.anchors.iter().cloned(),
18✔
230
            )?),
18✔
231
            None => None,
1✔
232
        };
233

234
        Ok(SyncResponse {
19✔
235
            tx_update,
19✔
236
            chain_update,
19✔
237
        })
19✔
238
    }
19✔
239

240
    /// Populate the `tx_update` with transactions/anchors associated with the given `spks`.
241
    ///
242
    /// Transactions that contains an output with requested spk, or spends form an output with
243
    /// requested spk will be added to `tx_update`. Anchors of the aforementioned transactions are
244
    /// also included.
245
    fn populate_with_spks(
23✔
246
        &self,
23✔
247
        start_time: u64,
23✔
248
        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
23✔
249
        mut spks_with_expected_txids: impl Iterator<Item = (u32, SpkWithExpectedTxids)>,
23✔
250
        stop_gap: usize,
23✔
251
        batch_size: usize,
23✔
252
    ) -> Result<Option<u32>, Error> {
23✔
253
        let mut unused_spk_count = 0_usize;
23✔
254
        let mut last_active_index = Option::<u32>::None;
23✔
255

256
        loop {
70✔
257
            let spks = (0..batch_size)
70✔
258
                .map_while(|_| spks_with_expected_txids.next())
87✔
259
                .collect::<Vec<_>>();
70✔
260
            if spks.is_empty() {
70✔
261
                return Ok(last_active_index);
20✔
262
            }
50✔
263

264
            let spk_histories = self
50✔
265
                .inner
50✔
266
                .batch_script_get_history(spks.iter().map(|(_, s)| s.spk.as_script()))?;
50✔
267

268
            for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
50✔
269
                if spk_history.is_empty() {
50✔
270
                    unused_spk_count = unused_spk_count.saturating_add(1);
27✔
271
                    if unused_spk_count >= stop_gap {
27✔
272
                        return Ok(last_active_index);
3✔
273
                    }
24✔
274
                } else {
23✔
275
                    last_active_index = Some(spk_index);
23✔
276
                    unused_spk_count = 0;
23✔
277
                }
23✔
278

279
                let spk_history_set = spk_history
47✔
280
                    .iter()
47✔
281
                    .map(|res| res.tx_hash)
160✔
282
                    .collect::<HashSet<_>>();
47✔
283

47✔
284
                tx_update.evicted_ats.extend(
47✔
285
                    spk.expected_txids
47✔
286
                        .difference(&spk_history_set)
47✔
287
                        .map(|&txid| (txid, start_time)),
47✔
288
                );
47✔
289

290
                for tx_res in spk_history {
207✔
291
                    tx_update.txs.push(self.fetch_tx(tx_res.tx_hash)?);
160✔
292
                    match tx_res.height.try_into() {
160✔
293
                        // Returned heights 0 & -1 are reserved for unconfirmed txs.
294
                        Ok(height) if height > 0 => {
159✔
295
                            self.validate_merkle_for_anchor(tx_update, tx_res.tx_hash, height)?;
146✔
296
                        }
297
                        _ => {
14✔
298
                            tx_update.seen_ats.insert((tx_res.tx_hash, start_time));
14✔
299
                        }
14✔
300
                    }
301
                }
302
            }
303
        }
304
    }
23✔
305

306
    /// Populate the `tx_update` with associated transactions/anchors of `outpoints`.
307
    ///
308
    /// Transactions in which the outpoint resides, and transactions that spend from the outpoint
309
    /// are included. Anchors of the aforementioned transactions are included.
310
    fn populate_with_outpoints(
19✔
311
        &self,
19✔
312
        start_time: u64,
19✔
313
        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
19✔
314
        outpoints: impl IntoIterator<Item = OutPoint>,
19✔
315
    ) -> Result<(), Error> {
19✔
316
        for outpoint in outpoints {
19✔
317
            let op_txid = outpoint.txid;
×
318
            let op_tx = self.fetch_tx(op_txid)?;
×
319
            let op_txout = match op_tx.output.get(outpoint.vout as usize) {
×
320
                Some(txout) => txout,
×
321
                None => continue,
×
322
            };
323
            debug_assert_eq!(op_tx.compute_txid(), op_txid);
×
324

325
            // attempt to find the following transactions (alongside their chain positions), and
326
            // add to our sparsechain `update`:
327
            let mut has_residing = false; // tx in which the outpoint resides
×
328
            let mut has_spending = false; // tx that spends the outpoint
×
329
            for res in self.inner.script_get_history(&op_txout.script_pubkey)? {
×
330
                if has_residing && has_spending {
×
331
                    break;
×
332
                }
×
333

×
334
                if !has_residing && res.tx_hash == op_txid {
×
335
                    has_residing = true;
×
336
                    tx_update.txs.push(Arc::clone(&op_tx));
×
337
                    match res.height.try_into() {
×
338
                        // Returned heights 0 & -1 are reserved for unconfirmed txs.
339
                        Ok(height) if height > 0 => {
×
340
                            self.validate_merkle_for_anchor(tx_update, res.tx_hash, height)?;
×
341
                        }
342
                        _ => {
×
343
                            tx_update.seen_ats.insert((res.tx_hash, start_time));
×
344
                        }
×
345
                    }
346
                }
×
347

348
                if !has_spending && res.tx_hash != op_txid {
×
349
                    let res_tx = self.fetch_tx(res.tx_hash)?;
×
350
                    // we exclude txs/anchors that do not spend our specified outpoint(s)
351
                    has_spending = res_tx
×
352
                        .input
×
353
                        .iter()
×
354
                        .any(|txin| txin.previous_output == outpoint);
×
355
                    if !has_spending {
×
356
                        continue;
×
357
                    }
×
358
                    tx_update.txs.push(Arc::clone(&res_tx));
×
359
                    match res.height.try_into() {
×
360
                        // Returned heights 0 & -1 are reserved for unconfirmed txs.
361
                        Ok(height) if height > 0 => {
×
362
                            self.validate_merkle_for_anchor(tx_update, res.tx_hash, height)?;
×
363
                        }
364
                        _ => {
×
365
                            tx_update.seen_ats.insert((res.tx_hash, start_time));
×
366
                        }
×
367
                    }
368
                }
×
369
            }
370
        }
371
        Ok(())
19✔
372
    }
19✔
373

374
    /// Populate the `tx_update` with transactions/anchors of the provided `txids`.
375
    fn populate_with_txids(
19✔
376
        &self,
19✔
377
        start_time: u64,
19✔
378
        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
19✔
379
        txids: impl IntoIterator<Item = Txid>,
19✔
380
    ) -> Result<(), Error> {
19✔
381
        for txid in txids {
19✔
382
            let tx = match self.fetch_tx(txid) {
×
383
                Ok(tx) => tx,
×
384
                Err(electrum_client::Error::Protocol(_)) => continue,
×
385
                Err(other_err) => return Err(other_err),
×
386
            };
387

388
            let spk = tx
×
389
                .output
×
390
                .first()
×
391
                .map(|txo| &txo.script_pubkey)
×
392
                .expect("tx must have an output");
×
393

394
            // because of restrictions of the Electrum API, we have to use the `script_get_history`
395
            // call to get confirmation status of our transaction
396
            if let Some(r) = self
×
397
                .inner
×
398
                .script_get_history(spk)?
×
399
                .into_iter()
×
400
                .find(|r| r.tx_hash == txid)
×
401
            {
402
                match r.height.try_into() {
×
403
                    // Returned heights 0 & -1 are reserved for unconfirmed txs.
404
                    Ok(height) if height > 0 => {
×
405
                        self.validate_merkle_for_anchor(tx_update, txid, height)?;
×
406
                    }
407
                    _ => {
×
408
                        tx_update.seen_ats.insert((r.tx_hash, start_time));
×
409
                    }
×
410
                }
411
            }
×
412

413
            tx_update.txs.push(tx);
×
414
        }
415
        Ok(())
19✔
416
    }
19✔
417

418
    // Helper function which checks if a transaction is confirmed by validating the merkle proof.
419
    // An anchor is inserted if the transaction is validated to be in a confirmed block.
420
    fn validate_merkle_for_anchor(
146✔
421
        &self,
146✔
422
        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
146✔
423
        txid: Txid,
146✔
424
        confirmation_height: usize,
146✔
425
    ) -> Result<(), Error> {
146✔
426
        if let Ok(merkle_res) = self
146✔
427
            .inner
146✔
428
            .transaction_get_merkle(&txid, confirmation_height)
146✔
429
        {
430
            let mut header = self.fetch_header(merkle_res.block_height as u32)?;
146✔
431
            let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
146✔
432
                &txid,
146✔
433
                &header.merkle_root,
146✔
434
                &merkle_res,
146✔
435
            );
146✔
436

146✔
437
            // Merkle validation will fail if the header in `block_header_cache` is outdated, so we
146✔
438
            // want to check if there is a new header and validate against the new one.
146✔
439
            if !is_confirmed_tx {
146✔
440
                header = self.update_header(merkle_res.block_height as u32)?;
×
441
                is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
×
442
                    &txid,
×
443
                    &header.merkle_root,
×
444
                    &merkle_res,
×
445
                );
×
446
            }
146✔
447

448
            if is_confirmed_tx {
146✔
449
                tx_update.anchors.insert((
146✔
450
                    ConfirmationBlockTime {
146✔
451
                        confirmation_time: header.time as u64,
146✔
452
                        block_id: BlockId {
146✔
453
                            height: merkle_res.block_height as u32,
146✔
454
                            hash: header.block_hash(),
146✔
455
                        },
146✔
456
                    },
146✔
457
                    txid,
146✔
458
                ));
146✔
459
            }
146✔
460
        }
×
461
        Ok(())
146✔
462
    }
146✔
463

464
    // Helper function which fetches the `TxOut`s of our relevant transactions' previous
465
    // transactions, which we do not have by default. This data is needed to calculate the
466
    // transaction fee.
467
    fn fetch_prev_txout(
19✔
468
        &self,
19✔
469
        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
19✔
470
    ) -> Result<(), Error> {
19✔
471
        let mut no_dup = HashSet::<Txid>::new();
19✔
472
        for tx in &tx_update.txs {
174✔
473
            if !tx.is_coinbase() && no_dup.insert(tx.compute_txid()) {
155✔
474
                for vin in &tx.input {
53✔
475
                    let outpoint = vin.previous_output;
53✔
476
                    let vout = outpoint.vout;
53✔
477
                    let prev_tx = self.fetch_tx(outpoint.txid)?;
53✔
478
                    let txout = prev_tx.output[vout as usize].clone();
53✔
479
                    let _ = tx_update.txouts.insert(outpoint, txout);
53✔
480
                }
481
            }
102✔
482
        }
483
        Ok(())
19✔
484
    }
19✔
485
}
486

487
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are
488
/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
489
fn fetch_tip_and_latest_blocks(
22✔
490
    client: &impl ElectrumApi,
22✔
491
    prev_tip: CheckPoint,
22✔
492
) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
22✔
493
    let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
22✔
494
    let new_tip_height = height as u32;
22✔
495

22✔
496
    // If electrum returns a tip height that is lower than our previous tip, then checkpoints do
22✔
497
    // not need updating. We just return the previous tip and use that as the point of agreement.
22✔
498
    if new_tip_height < prev_tip.height() {
22✔
499
        return Ok((prev_tip, BTreeMap::new()));
×
500
    }
22✔
501

502
    // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
503
    // to construct our checkpoint update.
504
    let mut new_blocks = {
22✔
505
        let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
22✔
506
        let hashes = client
22✔
507
            .block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
22✔
508
            .headers
509
            .into_iter()
22✔
510
            .map(|h| h.block_hash());
176✔
511
        (start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
22✔
512
    };
513

514
    // Find the "point of agreement" (if any).
515
    let agreement_cp = {
22✔
516
        let mut agreement_cp = Option::<CheckPoint>::None;
22✔
517
        for cp in prev_tip.iter() {
59✔
518
            let cp_block = cp.block_id();
59✔
519
            let hash = match new_blocks.get(&cp_block.height) {
59✔
520
                Some(&hash) => hash,
52✔
521
                None => {
522
                    assert!(
7✔
523
                        new_tip_height >= cp_block.height,
7✔
524
                        "already checked that electrum's tip cannot be smaller"
×
525
                    );
526
                    let hash = client.block_header(cp_block.height as _)?.block_hash();
7✔
527
                    new_blocks.insert(cp_block.height, hash);
7✔
528
                    hash
7✔
529
                }
530
            };
531
            if hash == cp_block.hash {
59✔
532
                agreement_cp = Some(cp);
22✔
533
                break;
22✔
534
            }
37✔
535
        }
536
        agreement_cp
22✔
537
    };
22✔
538

22✔
539
    let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
22✔
540

22✔
541
    let new_tip = new_blocks
22✔
542
        .iter()
22✔
543
        // Prune `new_blocks` to only include blocks that are actually new.
22✔
544
        .filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height)
183✔
545
        .map(|(height, hash)| BlockId {
89✔
546
            height: *height,
89✔
547
            hash: *hash,
89✔
548
        })
89✔
549
        .fold(agreement_cp, |prev_cp, block| {
89✔
550
            Some(match prev_cp {
89✔
551
                Some(cp) => cp.push(block).expect("must extend checkpoint"),
89✔
552
                None => CheckPoint::new(block),
×
553
            })
554
        })
89✔
555
        .expect("must have at least one checkpoint");
22✔
556

22✔
557
    Ok((new_tip, new_blocks))
22✔
558
}
22✔
559

560
// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not
561
// surpass `latest_blocks`.
562
fn chain_update(
22✔
563
    mut tip: CheckPoint,
22✔
564
    latest_blocks: &BTreeMap<u32, BlockHash>,
22✔
565
    anchors: impl Iterator<Item = (ConfirmationBlockTime, Txid)>,
22✔
566
) -> Result<CheckPoint, Error> {
22✔
567
    for (anchor, _txid) in anchors {
168✔
568
        let height = anchor.block_id.height;
146✔
569

146✔
570
        // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent
146✔
571
        // in case of a re-org.
146✔
572
        if tip.get(height).is_none() && height <= tip.height() {
146✔
573
            let hash = match latest_blocks.get(&height) {
93✔
574
                Some(&hash) => hash,
×
575
                None => anchor.block_id.hash,
93✔
576
            };
577
            tip = tip.insert(BlockId { hash, height });
93✔
578
        }
53✔
579
    }
580
    Ok(tip)
22✔
581
}
22✔
582

583
#[cfg(test)]
584
mod test {
585
    use crate::{bdk_electrum_client::TxUpdate, BdkElectrumClient};
586
    use bdk_chain::bitcoin::{OutPoint, Transaction, TxIn};
587
    use bdk_core::collections::BTreeMap;
588
    use bdk_testenv::{utils::new_tx, TestEnv};
589
    use std::sync::Arc;
590

591
    #[cfg(feature = "default")]
592
    #[test]
593
    fn test_fetch_prev_txout_with_coinbase() {
1✔
594
        let env = TestEnv::new().unwrap();
1✔
595
        let electrum_client =
1✔
596
            electrum_client::Client::new(env.electrsd.electrum_url.as_str()).unwrap();
1✔
597
        let client = BdkElectrumClient::new(electrum_client);
1✔
598

1✔
599
        // Create a coinbase transaction.
1✔
600
        let coinbase_tx = Transaction {
1✔
601
            input: vec![TxIn {
1✔
602
                previous_output: OutPoint::null(),
1✔
603
                ..Default::default()
1✔
604
            }],
1✔
605
            ..new_tx(0)
1✔
606
        };
1✔
607

1✔
608
        assert!(coinbase_tx.is_coinbase());
1✔
609

610
        // Test that `fetch_prev_txout` does not process coinbase transactions. Calling
611
        // `fetch_prev_txout` on a coinbase transaction will trigger a `fetch_tx` on a transaction
612
        // with a txid of all zeros. If `fetch_prev_txout` attempts to fetch this transaction, this
613
        // assertion will fail.
614
        let mut tx_update = TxUpdate::default();
1✔
615
        tx_update.txs = vec![Arc::new(coinbase_tx)];
1✔
616
        assert!(client.fetch_prev_txout(&mut tx_update).is_ok());
1✔
617

618
        // Ensure that the txouts are empty.
619
        assert_eq!(tx_update.txouts, BTreeMap::default());
1✔
620
    }
1✔
621
}
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