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

bitcoindevkit / bdk / 9737955023

01 Jul 2024 04:01AM UTC coverage: 83.182% (+0.2%) from 83.029%
9737955023

Pull #1478

github

web-flow
Merge d05135804 into 22368ab7b
Pull Request #1478: Make `bdk_esplora` more modular

358 of 422 new or added lines in 2 files covered. (84.83%)

213 existing lines in 7 files now uncovered.

11148 of 13402 relevant lines covered (83.18%)

16721.16 hits per line

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

72.95
/crates/electrum/src/bdk_electrum_client.rs
1
use bdk_chain::{
2
    bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
3
    collections::{BTreeMap, HashMap, HashSet},
4
    local_chain::CheckPoint,
5
    spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
6
    tx_graph::TxGraph,
7
    BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
8
};
9
use core::str::FromStr;
10
use electrum_client::{ElectrumApi, Error, HeaderNotification};
11
use std::sync::{Arc, Mutex};
12

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

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

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

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

×
43
        let mut tx_cache = self.tx_cache.lock().unwrap();
×
44
        for (txid, tx) in txs {
×
45
            tx_cache.insert(txid, tx);
×
46
        }
×
47
    }
×
48

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

55
        if let Some(tx) = tx_cache.get(&txid) {
46✔
56
            return Ok(Arc::clone(tx));
36✔
57
        }
10✔
58

10✔
59
        drop(tx_cache);
10✔
60

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

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

10✔
65
        Ok(tx)
10✔
66
    }
46✔
67

68
    /// Broadcasts a transaction to the network.
69
    ///
70
    /// This is a re-export of [`ElectrumApi::transaction_broadcast`].
71
    pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
×
72
        self.inner.transaction_broadcast(tx)
×
73
    }
×
74

75
    /// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
76
    /// returns updates for [`bdk_chain`] data structures.
77
    ///
78
    /// - `request`: struct with data required to perform a spk-based blockchain client full scan,
79
    ///              see [`FullScanRequest`]
80
    /// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
81
    ///              associated transactions
82
    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
83
    ///              request
84
    /// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
85
    pub fn full_scan<K: Ord + Clone>(
10✔
86
        &self,
10✔
87
        request: FullScanRequest<K>,
10✔
88
        stop_gap: usize,
10✔
89
        batch_size: usize,
10✔
90
        fetch_prev_txouts: bool,
10✔
91
    ) -> Result<ElectrumFullScanResult<K>, Error> {
10✔
92
        let mut request_spks = request.spks_by_keychain;
10✔
93

10✔
94
        // We keep track of already-scanned spks just in case a reorg happens and we need to do a
10✔
95
        // rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
10✔
96
        // cannot be collected. In addition, we keep track of whether an spk has an active tx
10✔
97
        // history for determining the `last_active_index`.
10✔
98
        // * key: (keychain, spk_index) that identifies the spk.
10✔
99
        // * val: (script_pubkey, has_tx_history).
10✔
100
        let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
10✔
101

102
        let update = loop {
10✔
103
            let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
10✔
104
            let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
10✔
105
            let cps = tip
10✔
106
                .iter()
10✔
107
                .take(10)
10✔
108
                .map(|cp| (cp.height(), cp))
90✔
109
                .collect::<BTreeMap<u32, CheckPoint>>();
10✔
110

10✔
111
            if !request_spks.is_empty() {
10✔
112
                if !scanned_spks.is_empty() {
10✔
113
                    scanned_spks.append(
×
114
                        &mut self.populate_with_spks(
×
115
                            &cps,
×
116
                            &mut graph_update,
×
117
                            &mut scanned_spks
×
118
                                .iter()
×
119
                                .map(|(i, (spk, _))| (i.clone(), spk.clone())),
×
120
                            stop_gap,
×
121
                            batch_size,
×
122
                        )?,
×
123
                    );
124
                }
10✔
125
                for (keychain, keychain_spks) in &mut request_spks {
20✔
126
                    scanned_spks.extend(
10✔
127
                        self.populate_with_spks(
10✔
128
                            &cps,
10✔
129
                            &mut graph_update,
10✔
130
                            keychain_spks,
10✔
131
                            stop_gap,
10✔
132
                            batch_size,
10✔
133
                        )?
10✔
134
                        .into_iter()
10✔
135
                        .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
10✔
136
                    );
137
                }
138
            }
×
139

140
            // check for reorgs during scan process
141
            let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
10✔
142
            if tip.hash() != server_blockhash {
10✔
143
                continue; // reorg
×
144
            }
10✔
145

10✔
146
            // Fetch previous `TxOut`s for fee calculation if flag is enabled.
10✔
147
            if fetch_prev_txouts {
10✔
148
                self.fetch_prev_txout(&mut graph_update)?;
×
149
            }
10✔
150

151
            let chain_update = tip;
10✔
152

10✔
153
            let keychain_update = request_spks
10✔
154
                .into_keys()
10✔
155
                .filter_map(|k| {
10✔
156
                    scanned_spks
10✔
157
                        .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
10✔
158
                        .rev()
10✔
159
                        .find(|(_, (_, active))| *active)
10✔
160
                        .map(|((_, i), _)| (k, *i))
10✔
161
                })
10✔
162
                .collect::<BTreeMap<_, _>>();
10✔
163

10✔
164
            break FullScanResult {
10✔
165
                graph_update,
10✔
166
                chain_update,
10✔
167
                last_active_indices: keychain_update,
10✔
168
            };
10✔
169
        };
10✔
170

10✔
171
        Ok(ElectrumFullScanResult(update))
10✔
172
    }
10✔
173

174
    /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
175
    /// and returns updates for [`bdk_chain`] data structures.
176
    ///
177
    /// - `request`: struct with data required to perform a spk-based blockchain client sync,
178
    ///              see [`SyncRequest`]
179
    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
180
    ///              request
181
    /// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
182
    ///              calculation
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
    pub fn sync(
10✔
189
        &self,
10✔
190
        request: SyncRequest,
10✔
191
        batch_size: usize,
10✔
192
        fetch_prev_txouts: bool,
10✔
193
    ) -> Result<ElectrumSyncResult, Error> {
10✔
194
        let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
10✔
195
            .set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
10✔
196
        let mut full_scan_res = self
10✔
197
            .full_scan(full_scan_req, usize::MAX, batch_size, false)?
10✔
198
            .with_confirmation_height_anchor();
10✔
199

200
        let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
10✔
201
        let cps = tip
10✔
202
            .iter()
10✔
203
            .take(10)
10✔
204
            .map(|cp| (cp.height(), cp))
90✔
205
            .collect::<BTreeMap<u32, CheckPoint>>();
10✔
206

10✔
207
        self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
10✔
208
        self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
10✔
209

210
        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
211
        if fetch_prev_txouts {
10✔
212
            self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
1✔
213
        }
9✔
214

215
        Ok(ElectrumSyncResult(SyncResult {
10✔
216
            chain_update: full_scan_res.chain_update,
10✔
217
            graph_update: full_scan_res.graph_update,
10✔
218
        }))
10✔
219
    }
10✔
220

221
    /// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
222
    ///
223
    /// Transactions that contains an output with requested spk, or spends form an output with
224
    /// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
225
    /// also included.
226
    ///
227
    /// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
228
    fn populate_with_spks<I: Ord + Clone>(
10✔
229
        &self,
10✔
230
        cps: &BTreeMap<u32, CheckPoint>,
10✔
231
        graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
10✔
232
        spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
10✔
233
        stop_gap: usize,
10✔
234
        batch_size: usize,
10✔
235
    ) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
10✔
236
        let mut unused_spk_count = 0_usize;
10✔
237
        let mut scanned_spks = BTreeMap::new();
10✔
238

239
        loop {
20✔
240
            let spks = (0..batch_size)
20✔
241
                .map_while(|_| spks.next())
30✔
242
                .collect::<Vec<_>>();
20✔
243
            if spks.is_empty() {
20✔
244
                return Ok(scanned_spks);
10✔
245
            }
10✔
246

247
            let spk_histories = self
10✔
248
                .inner
10✔
249
                .batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
10✔
250

251
            for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
10✔
252
                if spk_history.is_empty() {
10✔
UNCOV
253
                    scanned_spks.insert(spk_index, (spk, false));
×
UNCOV
254
                    unused_spk_count += 1;
×
UNCOV
255
                    if unused_spk_count > stop_gap {
×
UNCOV
256
                        return Ok(scanned_spks);
×
UNCOV
257
                    }
×
UNCOV
258
                    continue;
×
259
                } else {
10✔
260
                    scanned_spks.insert(spk_index, (spk, true));
10✔
261
                    unused_spk_count = 0;
10✔
262
                }
10✔
263

264
                for tx_res in spk_history {
55✔
265
                    let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
45✔
266
                    if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
45✔
267
                        let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
37✔
268
                    }
37✔
269
                }
270
            }
271
        }
272
    }
10✔
273

274
    // Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
275
    // which we do not have by default. This data is needed to calculate the transaction fee.
276
    fn fetch_prev_txout(
1✔
277
        &self,
1✔
278
        graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
1✔
279
    ) -> Result<(), Error> {
1✔
280
        let full_txs: Vec<Arc<Transaction>> =
1✔
281
            graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
1✔
282
        for tx in full_txs {
2✔
283
            for vin in &tx.input {
1✔
284
                let outpoint = vin.previous_output;
1✔
285
                let vout = outpoint.vout;
1✔
286
                let prev_tx = self.fetch_tx(outpoint.txid)?;
1✔
287
                let txout = prev_tx.output[vout as usize].clone();
1✔
288
                let _ = graph_update.insert_txout(outpoint, txout);
1✔
289
            }
290
        }
291
        Ok(())
1✔
292
    }
1✔
293

294
    /// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
295
    ///
296
    /// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
297
    /// included. Anchors of the aforementioned transactions are included.
298
    ///
299
    /// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
300
    fn populate_with_outpoints(
10✔
301
        &self,
10✔
302
        cps: &BTreeMap<u32, CheckPoint>,
10✔
303
        graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
10✔
304
        outpoints: impl IntoIterator<Item = OutPoint>,
10✔
305
    ) -> Result<(), Error> {
10✔
306
        for outpoint in outpoints {
10✔
UNCOV
307
            let op_txid = outpoint.txid;
×
UNCOV
308
            let op_tx = self.fetch_tx(op_txid)?;
×
UNCOV
309
            let op_txout = match op_tx.output.get(outpoint.vout as usize) {
×
UNCOV
310
                Some(txout) => txout,
×
UNCOV
311
                None => continue,
×
312
            };
UNCOV
313
            debug_assert_eq!(op_tx.compute_txid(), op_txid);
×
314

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

×
UNCOV
324
                if !has_residing && res.tx_hash == op_txid {
×
UNCOV
325
                    has_residing = true;
×
UNCOV
326
                    let _ = graph_update.insert_tx(Arc::clone(&op_tx));
×
UNCOV
327
                    if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
×
UNCOV
328
                        let _ = graph_update.insert_anchor(res.tx_hash, anchor);
×
UNCOV
329
                    }
×
UNCOV
330
                }
×
331

UNCOV
332
                if !has_spending && res.tx_hash != op_txid {
×
333
                    let res_tx = self.fetch_tx(res.tx_hash)?;
×
334
                    // we exclude txs/anchors that do not spend our specified outpoint(s)
335
                    has_spending = res_tx
×
336
                        .input
×
337
                        .iter()
×
UNCOV
338
                        .any(|txin| txin.previous_output == outpoint);
×
339
                    if !has_spending {
×
UNCOV
340
                        continue;
×
UNCOV
341
                    }
×
UNCOV
342
                    let _ = graph_update.insert_tx(Arc::clone(&res_tx));
×
343
                    if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
×
344
                        let _ = graph_update.insert_anchor(res.tx_hash, anchor);
×
345
                    }
×
346
                }
×
347
            }
348
        }
349
        Ok(())
10✔
350
    }
10✔
351

352
    /// Populate the `graph_update` with transactions/anchors of the provided `txids`.
353
    fn populate_with_txids(
10✔
354
        &self,
10✔
355
        cps: &BTreeMap<u32, CheckPoint>,
10✔
356
        graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
10✔
357
        txids: impl IntoIterator<Item = Txid>,
10✔
358
    ) -> Result<(), Error> {
10✔
359
        for txid in txids {
10✔
UNCOV
360
            let tx = match self.fetch_tx(txid) {
×
361
                Ok(tx) => tx,
×
362
                Err(electrum_client::Error::Protocol(_)) => continue,
×
363
                Err(other_err) => return Err(other_err),
×
364
            };
365

366
            let spk = tx
×
367
                .output
×
368
                .first()
×
369
                .map(|txo| &txo.script_pubkey)
×
370
                .expect("tx must have an output");
×
371

372
            // because of restrictions of the Electrum API, we have to use the `script_get_history`
373
            // call to get confirmation status of our transaction
UNCOV
374
            let anchor = match self
×
UNCOV
375
                .inner
×
UNCOV
376
                .script_get_history(spk)?
×
UNCOV
377
                .into_iter()
×
UNCOV
378
                .find(|r| r.tx_hash == txid)
×
379
            {
UNCOV
380
                Some(r) => determine_tx_anchor(cps, r.height, txid),
×
UNCOV
381
                None => continue,
×
382
            };
383

UNCOV
384
            let _ = graph_update.insert_tx(tx);
×
UNCOV
385
            if let Some(anchor) = anchor {
×
386
                let _ = graph_update.insert_anchor(txid, anchor);
×
387
            }
×
388
        }
389
        Ok(())
10✔
390
    }
10✔
391
}
392

393
/// The result of [`BdkElectrumClient::full_scan`].
394
///
395
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
396
/// [`ConfirmationTimeHeightAnchor`] anchor types.
397
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
398

399
impl<K> ElectrumFullScanResult<K> {
400
    /// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
401
    pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
10✔
402
        self.0
10✔
403
    }
10✔
404

405
    /// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
406
    ///
407
    /// This requires additional calls to the Electrum server.
UNCOV
408
    pub fn with_confirmation_time_height_anchor(
×
UNCOV
409
        self,
×
410
        client: &BdkElectrumClient<impl ElectrumApi>,
×
411
    ) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
×
412
        let res = self.0;
×
413
        Ok(FullScanResult {
×
UNCOV
414
            graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
×
UNCOV
415
            chain_update: res.chain_update,
×
UNCOV
416
            last_active_indices: res.last_active_indices,
×
417
        })
UNCOV
418
    }
×
419
}
420

421
/// The result of [`BdkElectrumClient::sync`].
422
///
423
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
424
/// [`ConfirmationTimeHeightAnchor`] anchor types.
425
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
426

427
impl ElectrumSyncResult {
428
    /// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
UNCOV
429
    pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
×
UNCOV
430
        self.0
×
UNCOV
431
    }
×
432

433
    /// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
434
    ///
435
    /// This requires additional calls to the Electrum server.
436
    pub fn with_confirmation_time_height_anchor(
10✔
437
        self,
10✔
438
        client: &BdkElectrumClient<impl ElectrumApi>,
10✔
439
    ) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
10✔
440
        let res = self.0;
10✔
441
        Ok(SyncResult {
10✔
442
            graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
10✔
443
            chain_update: res.chain_update,
10✔
444
        })
445
    }
10✔
446
}
447

448
fn try_into_confirmation_time_result(
10✔
449
    graph_update: TxGraph<ConfirmationHeightAnchor>,
10✔
450
    client: &impl ElectrumApi,
10✔
451
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
10✔
452
    let relevant_heights = graph_update
10✔
453
        .all_anchors()
10✔
454
        .iter()
10✔
455
        .map(|(a, _)| a.confirmation_height)
37✔
456
        .collect::<HashSet<_>>();
10✔
457

458
    let height_to_time = relevant_heights
10✔
459
        .clone()
10✔
460
        .into_iter()
10✔
461
        .zip(
10✔
462
            client
10✔
463
                .batch_block_header(relevant_heights)?
10✔
464
                .into_iter()
10✔
465
                .map(|bh| bh.time as u64),
37✔
466
        )
10✔
467
        .collect::<HashMap<u32, u64>>();
10✔
468

10✔
469
    Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
37✔
470
        anchor_block: a.anchor_block,
37✔
471
        confirmation_height: a.confirmation_height,
37✔
472
        confirmation_time: height_to_time[&a.confirmation_height],
37✔
473
    }))
37✔
474
}
10✔
475

476
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
477
fn construct_update_tip(
20✔
478
    client: &impl ElectrumApi,
20✔
479
    prev_tip: CheckPoint,
20✔
480
) -> Result<(CheckPoint, Option<u32>), Error> {
20✔
481
    let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
20✔
482
    let new_tip_height = height as u32;
20✔
483

20✔
484
    // If electrum returns a tip height that is lower than our previous tip, then checkpoints do
20✔
485
    // not need updating. We just return the previous tip and use that as the point of agreement.
20✔
486
    if new_tip_height < prev_tip.height() {
20✔
UNCOV
487
        return Ok((prev_tip.clone(), Some(prev_tip.height())));
×
488
    }
20✔
489

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

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

20✔
527
    let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
20✔
528

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

20✔
542
    Ok((new_tip, agreement_height))
20✔
543
}
20✔
544

545
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
546
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
547
///
548
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
549
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
550
///
551
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
552
fn determine_tx_anchor(
45✔
553
    cps: &BTreeMap<u32, CheckPoint>,
45✔
554
    raw_height: i32,
45✔
555
    txid: Txid,
45✔
556
) -> Option<ConfirmationHeightAnchor> {
45✔
557
    // The electrum API has a weird quirk where an unconfirmed transaction is presented with a
45✔
558
    // height of 0. To avoid invalid representation in our data structures, we manually set
45✔
559
    // transactions residing in the genesis block to have height 0, then interpret a height of 0 as
45✔
560
    // unconfirmed for all other transactions.
45✔
561
    if txid
45✔
562
        == Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
45✔
563
            .expect("must deserialize genesis coinbase txid")
45✔
564
    {
UNCOV
565
        let anchor_block = cps.values().next()?.block_id();
×
UNCOV
566
        return Some(ConfirmationHeightAnchor {
×
UNCOV
567
            anchor_block,
×
UNCOV
568
            confirmation_height: 0,
×
UNCOV
569
        });
×
570
    }
45✔
571
    match raw_height {
45✔
572
        h if h <= 0 => {
45✔
573
            debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
8✔
574
            None
8✔
575
        }
576
        h => {
37✔
577
            let h = h as u32;
37✔
578
            let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
37✔
579
            if h > anchor_block.height {
37✔
UNCOV
580
                None
×
581
            } else {
582
                Some(ConfirmationHeightAnchor {
37✔
583
                    anchor_block,
37✔
584
                    confirmation_height: h,
37✔
585
                })
37✔
586
            }
587
        }
588
    }
589
}
45✔
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