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

bitcoindevkit / bdk / 10505233318

22 Aug 2024 09:12AM UTC coverage: 81.812% (-0.04%) from 81.848%
10505233318

Pull #1566

github

web-flow
Merge 9b75eebe4 into 6008897aa
Pull Request #1566: feat(chain)!: Add `time_of_sync` to `SyncRequest` and `FullScanRequest` WIP

41 of 54 new or added lines in 2 files covered. (75.93%)

2 existing lines in 1 file now uncovered.

11142 of 13619 relevant lines covered (81.81%)

13280.5 hits per line

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

76.5
/crates/electrum/src/bdk_electrum_client.rs
1
use bdk_chain::{
2
    bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid},
3
    collections::{BTreeMap, HashMap},
4
    local_chain::CheckPoint,
5
    spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
6
    tx_graph::TxGraph,
7
    Anchor, BlockId, ConfirmationBlockTime,
8
};
9
use electrum_client::{ElectrumApi, Error, HeaderNotification};
10
use std::{
11
    collections::BTreeSet,
12
    sync::{Arc, Mutex},
13
};
14

15
/// We include a chain suffix of a certain length for the purpose of robustness.
16
const CHAIN_SUFFIX_LENGTH: u32 = 8;
17

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

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

40
    /// Inserts transactions into the transaction cache so that the client will not fetch these
41
    /// transactions.
42
    pub fn populate_tx_cache<A>(&self, tx_graph: impl AsRef<TxGraph<A>>) {
×
43
        let txs = tx_graph
×
44
            .as_ref()
×
45
            .full_txs()
×
46
            .map(|tx_node| (tx_node.txid, tx_node.tx));
×
47

×
48
        let mut tx_cache = self.tx_cache.lock().unwrap();
×
49
        for (txid, tx) in txs {
×
50
            tx_cache.insert(txid, tx);
×
51
        }
×
52
    }
×
53

54
    /// Fetch transaction of given `txid`.
55
    ///
56
    /// If it hits the cache it will return the cached version and avoid making the request.
57
    pub fn fetch_tx(&self, txid: Txid) -> Result<Arc<Transaction>, Error> {
112✔
58
        let tx_cache = self.tx_cache.lock().unwrap();
112✔
59

60
        if let Some(tx) = tx_cache.get(&txid) {
112✔
61
            return Ok(Arc::clone(tx));
88✔
62
        }
24✔
63

24✔
64
        drop(tx_cache);
24✔
65

66
        let tx = Arc::new(self.inner.transaction_get(&txid)?);
24✔
67

68
        self.tx_cache.lock().unwrap().insert(txid, Arc::clone(&tx));
24✔
69

24✔
70
        Ok(tx)
24✔
71
    }
112✔
72

73
    /// Fetch block header of given `height`.
74
    ///
75
    /// If it hits the cache it will return the cached version and avoid making the request.
76
    fn fetch_header(&self, height: u32) -> Result<Header, Error> {
44✔
77
        let block_header_cache = self.block_header_cache.lock().unwrap();
44✔
78

79
        if let Some(header) = block_header_cache.get(&height) {
44✔
80
            return Ok(*header);
31✔
81
        }
13✔
82

13✔
83
        drop(block_header_cache);
13✔
84

13✔
85
        self.update_header(height)
13✔
86
    }
44✔
87

88
    /// Update a block header at given `height`. Returns the updated header.
89
    fn update_header(&self, height: u32) -> Result<Header, Error> {
13✔
90
        let header = self.inner.block_header(height as usize)?;
13✔
91

92
        self.block_header_cache
13✔
93
            .lock()
13✔
94
            .unwrap()
13✔
95
            .insert(height, header);
13✔
96

13✔
97
        Ok(header)
13✔
98
    }
13✔
99

100
    /// Broadcasts a transaction to the network.
101
    ///
102
    /// This is a re-export of [`ElectrumApi::transaction_broadcast`].
103
    pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
×
104
        self.inner.transaction_broadcast(tx)
×
105
    }
×
106

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

136
        let tip_and_latest_blocks = match request.chain_tip() {
4✔
137
            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
4✔
138
            None => None,
×
139
        };
140
        let time_of_sync = request.get_time_of_sync();
4✔
141

4✔
142
        let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
4✔
143
        let mut last_active_indices = BTreeMap::<K, u32>::default();
4✔
144
        for keychain in request.keychains() {
4✔
145
            let spks = request.iter_spks(keychain.clone());
4✔
146
            if let Some(last_active_index) = self.populate_with_spks(
4✔
147
                &mut graph_update,
4✔
148
                spks,
4✔
149
                stop_gap,
4✔
150
                batch_size,
4✔
151
                time_of_sync,
4✔
152
            )? {
4✔
153
                last_active_indices.insert(keychain, last_active_index);
3✔
154
            }
3✔
155
        }
156

157
        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
158
        if fetch_prev_txouts {
4✔
159
            self.fetch_prev_txout(&mut graph_update)?;
×
160
        }
4✔
161

162
        let chain_update = match tip_and_latest_blocks {
4✔
163
            Some((chain_tip, latest_blocks)) => Some(chain_update(
4✔
164
                chain_tip,
4✔
165
                &latest_blocks,
4✔
166
                graph_update.all_anchors(),
4✔
167
            )?),
4✔
168
            _ => None,
×
169
        };
170

171
        Ok(FullScanResult {
4✔
172
            graph_update,
4✔
173
            chain_update,
4✔
174
            last_active_indices,
4✔
175
        })
4✔
176
    }
4✔
177

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

208
        let tip_and_latest_blocks = match request.chain_tip() {
14✔
209
            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
14✔
210
            None => None,
×
211
        };
212
        let time_of_sync = request.get_time_of_sync();
14✔
213

14✔
214
        let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
14✔
215
        self.populate_with_spks(
14✔
216
            &mut graph_update,
14✔
217
            request
14✔
218
                .iter_spks()
14✔
219
                .enumerate()
14✔
220
                .map(|(i, spk)| (i as u32, spk)),
15✔
221
            usize::MAX,
14✔
222
            batch_size,
14✔
223
            time_of_sync,
14✔
224
        )?;
14✔
225
        self.populate_with_txids(&mut graph_update, request.iter_txids(), time_of_sync)?;
14✔
226
        self.populate_with_outpoints(&mut graph_update, request.iter_outpoints(), time_of_sync)?;
14✔
227

228
        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
229
        if fetch_prev_txouts {
14✔
230
            self.fetch_prev_txout(&mut graph_update)?;
14✔
231
        }
×
232

233
        let chain_update = match tip_and_latest_blocks {
14✔
234
            Some((chain_tip, latest_blocks)) => Some(chain_update(
14✔
235
                chain_tip,
14✔
236
                &latest_blocks,
14✔
237
                graph_update.all_anchors(),
14✔
238
            )?),
14✔
239
            None => None,
×
240
        };
241

242
        Ok(SyncResult {
14✔
243
            graph_update,
14✔
244
            chain_update,
14✔
245
        })
14✔
246
    }
14✔
247

248
    /// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
249
    ///
250
    /// Transactions that contains an output with requested spk, or spends form an output with
251
    /// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
252
    /// also included.
253
    fn populate_with_spks(
18✔
254
        &self,
18✔
255
        graph_update: &mut TxGraph<ConfirmationBlockTime>,
18✔
256
        mut spks: impl Iterator<Item = (u32, ScriptBuf)>,
18✔
257
        stop_gap: usize,
18✔
258
        batch_size: usize,
18✔
259
        time_of_sync: Option<u64>,
18✔
260
    ) -> Result<Option<u32>, Error> {
18✔
261
        let mut unused_spk_count = 0_usize;
18✔
262
        let mut last_active_index = Option::<u32>::None;
18✔
263

264
        loop {
60✔
265
            let spks = (0..batch_size)
60✔
266
                .map_while(|_| spks.next())
73✔
267
                .collect::<Vec<_>>();
60✔
268
            if spks.is_empty() {
60✔
269
                return Ok(last_active_index);
15✔
270
            }
45✔
271

272
            let spk_histories = self
45✔
273
                .inner
45✔
274
                .batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
45✔
275

276
            for ((spk_index, _spk), spk_history) in spks.into_iter().zip(spk_histories) {
45✔
277
                if spk_history.is_empty() {
45✔
278
                    unused_spk_count = unused_spk_count.saturating_add(1);
26✔
279
                    if unused_spk_count >= stop_gap {
26✔
280
                        return Ok(last_active_index);
3✔
281
                    }
23✔
282
                    continue;
23✔
283
                } else {
19✔
284
                    last_active_index = Some(spk_index);
19✔
285
                    unused_spk_count = 0;
19✔
286
                }
19✔
287

288
                for tx_res in spk_history {
77✔
289
                    let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
58✔
290
                    self.validate_merkle_for_anchor(
58✔
291
                        graph_update,
58✔
292
                        tx_res.tx_hash,
58✔
293
                        tx_res.height,
58✔
294
                        time_of_sync,
58✔
295
                    )?;
58✔
296
                }
297
            }
298
        }
299
    }
18✔
300

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

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

×
329
                if !has_residing && res.tx_hash == op_txid {
×
330
                    has_residing = true;
×
331
                    let _ = graph_update.insert_tx(Arc::clone(&op_tx));
×
NEW
332
                    self.validate_merkle_for_anchor(
×
NEW
333
                        graph_update,
×
NEW
334
                        res.tx_hash,
×
NEW
335
                        res.height,
×
NEW
336
                        time_of_sync,
×
NEW
337
                    )?;
×
UNCOV
338
                }
×
339

340
                if !has_spending && res.tx_hash != op_txid {
×
341
                    let res_tx = self.fetch_tx(res.tx_hash)?;
×
342
                    // we exclude txs/anchors that do not spend our specified outpoint(s)
343
                    has_spending = res_tx
×
344
                        .input
×
345
                        .iter()
×
346
                        .any(|txin| txin.previous_output == outpoint);
×
347
                    if !has_spending {
×
348
                        continue;
×
349
                    }
×
350
                    let _ = graph_update.insert_tx(Arc::clone(&res_tx));
×
NEW
351
                    self.validate_merkle_for_anchor(
×
NEW
352
                        graph_update,
×
NEW
353
                        res.tx_hash,
×
NEW
354
                        res.height,
×
NEW
355
                        time_of_sync,
×
NEW
356
                    )?;
×
UNCOV
357
                }
×
358
            }
359
        }
360
        Ok(())
14✔
361
    }
14✔
362

363
    /// Populate the `graph_update` with transactions/anchors of the provided `txids`.
364
    fn populate_with_txids(
14✔
365
        &self,
14✔
366
        graph_update: &mut TxGraph<ConfirmationBlockTime>,
14✔
367
        txids: impl IntoIterator<Item = Txid>,
14✔
368
        time_of_sync: Option<u64>,
14✔
369
    ) -> Result<(), Error> {
14✔
370
        for txid in txids {
14✔
371
            let tx = match self.fetch_tx(txid) {
×
372
                Ok(tx) => tx,
×
373
                Err(electrum_client::Error::Protocol(_)) => continue,
×
374
                Err(other_err) => return Err(other_err),
×
375
            };
376

377
            let spk = tx
×
378
                .output
×
379
                .first()
×
380
                .map(|txo| &txo.script_pubkey)
×
381
                .expect("tx must have an output");
×
382

383
            // because of restrictions of the Electrum API, we have to use the `script_get_history`
384
            // call to get confirmation status of our transaction
385
            if let Some(r) = self
×
386
                .inner
×
387
                .script_get_history(spk)?
×
388
                .into_iter()
×
389
                .find(|r| r.tx_hash == txid)
×
390
            {
NEW
391
                self.validate_merkle_for_anchor(graph_update, txid, r.height, time_of_sync)?;
×
392
            }
×
393

394
            let _ = graph_update.insert_tx(tx);
×
395
        }
396
        Ok(())
14✔
397
    }
14✔
398

399
    // Helper function which checks if a transaction is confirmed by validating the merkle proof.
400
    // An anchor is inserted if the transaction is validated to be in a confirmed block.
401
    fn validate_merkle_for_anchor(
58✔
402
        &self,
58✔
403
        graph_update: &mut TxGraph<ConfirmationBlockTime>,
58✔
404
        txid: Txid,
58✔
405
        confirmation_height: i32,
58✔
406
        time_of_sync: Option<u64>,
58✔
407
    ) -> Result<(), Error> {
58✔
408
        if let Ok(merkle_res) = self
58✔
409
            .inner
58✔
410
            .transaction_get_merkle(&txid, confirmation_height as usize)
58✔
411
        {
412
            let mut header = self.fetch_header(merkle_res.block_height as u32)?;
44✔
413
            let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
44✔
414
                &txid,
44✔
415
                &header.merkle_root,
44✔
416
                &merkle_res,
44✔
417
            );
44✔
418

44✔
419
            // Merkle validation will fail if the header in `block_header_cache` is outdated, so we
44✔
420
            // want to check if there is a new header and validate against the new one.
44✔
421
            if !is_confirmed_tx {
44✔
422
                header = self.update_header(merkle_res.block_height as u32)?;
×
423
                is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
×
424
                    &txid,
×
425
                    &header.merkle_root,
×
426
                    &merkle_res,
×
427
                );
×
428
            }
44✔
429

430
            if is_confirmed_tx {
44✔
431
                let _ = graph_update.insert_anchor(
44✔
432
                    txid,
44✔
433
                    ConfirmationBlockTime {
44✔
434
                        confirmation_time: header.time as u64,
44✔
435
                        block_id: BlockId {
44✔
436
                            height: merkle_res.block_height as u32,
44✔
437
                            hash: header.block_hash(),
44✔
438
                        },
44✔
439
                    },
44✔
440
                );
44✔
441
            }
44✔
442
        } else {
443
            // If no merkle proof is returned, then the tx is unconfirmed and we set the last_seen.
444
            if let Some(seen_at) = time_of_sync {
14✔
445
                let _ = graph_update.insert_seen_at(txid, seen_at);
14✔
446
            }
14✔
447
        }
448
        Ok(())
58✔
449
    }
58✔
450

451
    // Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
452
    // which we do not have by default. This data is needed to calculate the transaction fee.
453
    fn fetch_prev_txout(
14✔
454
        &self,
14✔
455
        graph_update: &mut TxGraph<ConfirmationBlockTime>,
14✔
456
    ) -> Result<(), Error> {
14✔
457
        let full_txs: Vec<Arc<Transaction>> =
14✔
458
            graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
54✔
459
        for tx in full_txs {
68✔
460
            for vin in &tx.input {
54✔
461
                let outpoint = vin.previous_output;
54✔
462
                let vout = outpoint.vout;
54✔
463
                let prev_tx = self.fetch_tx(outpoint.txid)?;
54✔
464
                let txout = prev_tx.output[vout as usize].clone();
54✔
465
                let _ = graph_update.insert_txout(outpoint, txout);
54✔
466
            }
467
        }
468
        Ok(())
14✔
469
    }
14✔
470
}
471

472
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are
473
/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
474
fn fetch_tip_and_latest_blocks(
18✔
475
    client: &impl ElectrumApi,
18✔
476
    prev_tip: CheckPoint,
18✔
477
) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
18✔
478
    let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
18✔
479
    let new_tip_height = height as u32;
18✔
480

18✔
481
    // If electrum returns a tip height that is lower than our previous tip, then checkpoints do
18✔
482
    // not need updating. We just return the previous tip and use that as the point of agreement.
18✔
483
    if new_tip_height < prev_tip.height() {
18✔
484
        return Ok((prev_tip, BTreeMap::new()));
×
485
    }
18✔
486

487
    // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
488
    // to construct our checkpoint update.
489
    let mut new_blocks = {
18✔
490
        let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
18✔
491
        let hashes = client
18✔
492
            .block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
18✔
493
            .headers
494
            .into_iter()
18✔
495
            .map(|h| h.block_hash());
144✔
496
        (start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
18✔
497
    };
498

499
    // Find the "point of agreement" (if any).
500
    let agreement_cp = {
18✔
501
        let mut agreement_cp = Option::<CheckPoint>::None;
18✔
502
        for cp in prev_tip.iter() {
55✔
503
            let cp_block = cp.block_id();
55✔
504
            let hash = match new_blocks.get(&cp_block.height) {
55✔
505
                Some(&hash) => hash,
52✔
506
                None => {
507
                    assert!(
3✔
508
                        new_tip_height >= cp_block.height,
3✔
509
                        "already checked that electrum's tip cannot be smaller"
×
510
                    );
511
                    let hash = client.block_header(cp_block.height as _)?.block_hash();
3✔
512
                    new_blocks.insert(cp_block.height, hash);
3✔
513
                    hash
3✔
514
                }
515
            };
516
            if hash == cp_block.hash {
55✔
517
                agreement_cp = Some(cp);
18✔
518
                break;
18✔
519
            }
37✔
520
        }
521
        agreement_cp
18✔
522
    };
18✔
523

18✔
524
    let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
18✔
525

18✔
526
    let new_tip = new_blocks
18✔
527
        .iter()
18✔
528
        // Prune `new_blocks` to only include blocks that are actually new.
18✔
529
        .filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height)
147✔
530
        .map(|(height, hash)| BlockId {
57✔
531
            height: *height,
57✔
532
            hash: *hash,
57✔
533
        })
57✔
534
        .fold(agreement_cp, |prev_cp, block| {
57✔
535
            Some(match prev_cp {
57✔
536
                Some(cp) => cp.push(block).expect("must extend checkpoint"),
57✔
537
                None => CheckPoint::new(block),
×
538
            })
539
        })
57✔
540
        .expect("must have at least one checkpoint");
18✔
541

18✔
542
    Ok((new_tip, new_blocks))
18✔
543
}
18✔
544

545
// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not
546
// surpass `latest_blocks`.
547
fn chain_update<A: Anchor>(
18✔
548
    mut tip: CheckPoint,
18✔
549
    latest_blocks: &BTreeMap<u32, BlockHash>,
18✔
550
    anchors: &BTreeSet<(A, Txid)>,
18✔
551
) -> Result<CheckPoint, Error> {
18✔
552
    for anchor in anchors {
62✔
553
        let height = anchor.0.anchor_block().height;
44✔
554

44✔
555
        // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent
44✔
556
        // in case of a re-org.
44✔
557
        if tip.get(height).is_none() && height <= tip.height() {
44✔
558
            let hash = match latest_blocks.get(&height) {
×
559
                Some(&hash) => hash,
×
560
                None => anchor.0.anchor_block().hash,
×
561
            };
562
            tip = tip.insert(BlockId { hash, height });
×
563
        }
44✔
564
    }
565
    Ok(tip)
18✔
566
}
18✔
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