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

bitcoindevkit / bdk / 5353327759

pending completion
5353327759

Pull #1002

github

web-flow
Merge 10651f3d1 into 5d1f922b3
Pull Request #1002: Implement linked-list `LocalChain` and add rpc-chain module/example

584 of 584 new or added lines in 9 files covered. (100.0%)

7904 of 10180 relevant lines covered (77.64%)

5076.01 hits per line

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

0.0
/crates/electrum/src/electrum_ext.rs
1
use bdk_chain::{
2
    bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
3
    keychain::LocalUpdate,
4
    local_chain::CheckPoint,
5
    tx_graph::{self, TxGraph},
6
    Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeAnchor,
7
};
8
use electrum_client::{Client, ElectrumApi, Error};
9
use std::{
10
    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
11
    fmt::Debug,
12
};
13

14
#[derive(Debug, Clone)]
×
15
pub struct ElectrumUpdate<K, A> {
16
    pub graph_update: HashMap<Txid, BTreeSet<A>>,
17
    pub chain_update: CheckPoint,
18
    pub keychain_update: BTreeMap<K, u32>,
19
}
20

21
impl<K, A: Anchor> ElectrumUpdate<K, A> {
22
    pub fn new(cp: CheckPoint) -> Self {
×
23
        Self {
×
24
            graph_update: HashMap::new(),
×
25
            chain_update: cp,
×
26
            keychain_update: BTreeMap::new(),
×
27
        }
×
28
    }
×
29

30
    pub fn missing_full_txs<A2>(&self, graph: &TxGraph<A2>) -> Vec<Txid> {
×
31
        self.graph_update
×
32
            .keys()
×
33
            .filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
×
34
            .cloned()
×
35
            .collect()
×
36
    }
×
37

38
    pub fn finalize(
×
39
        self,
×
40
        client: &Client,
×
41
        seen_at: Option<u64>,
×
42
        missing: Vec<Txid>,
×
43
    ) -> Result<LocalUpdate<K, A>, Error> {
×
44
        let new_txs = client.batch_transaction_get(&missing)?;
×
45
        let mut graph_update = TxGraph::<A>::new(new_txs);
×
46
        for (txid, anchors) in self.graph_update {
×
47
            if let Some(seen_at) = seen_at {
×
48
                let _ = graph_update.insert_seen_at(txid, seen_at);
×
49
            }
×
50
            for anchor in anchors {
×
51
                let _ = graph_update.insert_anchor(txid, anchor);
×
52
            }
×
53
        }
54
        Ok(LocalUpdate {
×
55
            keychain: self.keychain_update,
×
56
            graph: graph_update,
×
57
            tip: self.chain_update,
×
58
        })
×
59
    }
×
60
}
61

62
impl<K> ElectrumUpdate<K, ConfirmationHeightAnchor> {
63
    /// Finalizes the [`ElectrumUpdate`] with `new_txs` and anchors of type
64
    /// [`ConfirmationTimeAnchor`].
65
    ///
66
    /// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
67
    /// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
68
    /// use it.
69
    pub fn finalize_as_confirmation_time(
×
70
        self,
×
71
        client: &Client,
×
72
        seen_at: Option<u64>,
×
73
        missing: Vec<Txid>,
×
74
    ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
×
75
        let update = self.finalize(client, seen_at, missing)?;
×
76

77
        let relevant_heights = {
×
78
            let mut visited_heights = HashSet::new();
×
79
            update
×
80
                .graph
×
81
                .all_anchors()
×
82
                .iter()
×
83
                .map(|(a, _)| a.confirmation_height_upper_bound())
×
84
                .filter(move |&h| visited_heights.insert(h))
×
85
                .collect::<Vec<_>>()
×
86
        };
87

88
        let height_to_time = relevant_heights
×
89
            .clone()
×
90
            .into_iter()
×
91
            .zip(
×
92
                client
×
93
                    .batch_block_header(relevant_heights)?
×
94
                    .into_iter()
×
95
                    .map(|bh| bh.time as u64),
×
96
            )
×
97
            .collect::<HashMap<u32, u64>>();
×
98

×
99
        let graph_additions = {
×
100
            let old_additions = TxGraph::default().determine_additions(&update.graph);
×
101
            tx_graph::Additions {
×
102
                txs: old_additions.txs,
×
103
                txouts: old_additions.txouts,
×
104
                last_seen: old_additions.last_seen,
×
105
                anchors: old_additions
×
106
                    .anchors
×
107
                    .into_iter()
×
108
                    .map(|(height_anchor, txid)| {
×
109
                        let confirmation_height = height_anchor.confirmation_height;
×
110
                        let confirmation_time = height_to_time[&confirmation_height];
×
111
                        let time_anchor = ConfirmationTimeAnchor {
×
112
                            anchor_block: height_anchor.anchor_block,
×
113
                            confirmation_height,
×
114
                            confirmation_time,
×
115
                        };
×
116
                        (time_anchor, txid)
×
117
                    })
×
118
                    .collect(),
×
119
            }
×
120
        };
×
121

×
122
        Ok(LocalUpdate {
×
123
            keychain: update.keychain,
×
124
            graph: {
×
125
                let mut graph = TxGraph::default();
×
126
                graph.apply_additions(graph_additions);
×
127
                graph
×
128
            },
×
129
            tip: update.tip,
×
130
        })
×
131
    }
×
132
}
133

134
pub trait ElectrumExt<A> {
135
    fn get_tip(&self) -> Result<(u32, BlockHash), Error>;
136

137
    fn scan<K: Ord + Clone>(
138
        &self,
139
        prev_tip: Option<CheckPoint>,
140
        keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
141
        txids: impl IntoIterator<Item = Txid>,
142
        outpoints: impl IntoIterator<Item = OutPoint>,
143
        stop_gap: usize,
144
        batch_size: usize,
145
    ) -> Result<ElectrumUpdate<K, A>, Error>;
146

147
    fn scan_without_keychain(
×
148
        &self,
×
149
        prev_tip: Option<CheckPoint>,
×
150
        misc_spks: impl IntoIterator<Item = Script>,
×
151
        txids: impl IntoIterator<Item = Txid>,
×
152
        outpoints: impl IntoIterator<Item = OutPoint>,
×
153
        batch_size: usize,
×
154
    ) -> Result<ElectrumUpdate<(), A>, Error> {
×
155
        let spk_iter = misc_spks
×
156
            .into_iter()
×
157
            .enumerate()
×
158
            .map(|(i, spk)| (i as u32, spk));
×
159

×
160
        self.scan(
×
161
            prev_tip,
×
162
            [((), spk_iter)].into(),
×
163
            txids,
×
164
            outpoints,
×
165
            usize::MAX,
×
166
            batch_size,
×
167
        )
×
168
    }
×
169
}
170

171
impl ElectrumExt<ConfirmationHeightAnchor> for Client {
172
    fn get_tip(&self) -> Result<(u32, BlockHash), Error> {
×
173
        // TODO: unsubscribe when added to the client, or is there a better call to use here?
×
174
        self.block_headers_subscribe()
×
175
            .map(|data| (data.height as u32, data.header.block_hash()))
×
176
    }
×
177

178
    fn scan<K: Ord + Clone>(
×
179
        &self,
×
180
        prev_tip: Option<CheckPoint>,
×
181
        keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
×
182
        txids: impl IntoIterator<Item = Txid>,
×
183
        outpoints: impl IntoIterator<Item = OutPoint>,
×
184
        stop_gap: usize,
×
185
        batch_size: usize,
×
186
    ) -> Result<ElectrumUpdate<K, ConfirmationHeightAnchor>, Error> {
×
187
        let mut request_spks = keychain_spks
×
188
            .into_iter()
×
189
            .map(|(k, s)| (k, s.into_iter()))
×
190
            .collect::<BTreeMap<K, _>>();
×
191
        let mut scanned_spks = BTreeMap::<(K, u32), (Script, bool)>::new();
×
192

×
193
        let txids = txids.into_iter().collect::<Vec<_>>();
×
194
        let outpoints = outpoints.into_iter().collect::<Vec<_>>();
×
195

196
        let update = loop {
×
197
            let mut update = ElectrumUpdate::<K, ConfirmationHeightAnchor>::new(
×
198
                prepare_chain_update(self, prev_tip.clone())?,
×
199
            );
200
            let anchor_block = update.chain_update.block_id();
×
201

×
202
            if !request_spks.is_empty() {
×
203
                if !scanned_spks.is_empty() {
×
204
                    scanned_spks.append(&mut populate_with_spks(
×
205
                        self,
×
206
                        anchor_block,
×
207
                        &mut update,
×
208
                        &mut scanned_spks
×
209
                            .iter()
×
210
                            .map(|(i, (spk, _))| (i.clone(), spk.clone())),
×
211
                        stop_gap,
×
212
                        batch_size,
×
213
                    )?);
×
214
                }
×
215
                for (keychain, keychain_spks) in &mut request_spks {
×
216
                    scanned_spks.extend(
217
                        populate_with_spks(
×
218
                            self,
×
219
                            anchor_block,
×
220
                            &mut update,
×
221
                            keychain_spks,
×
222
                            stop_gap,
×
223
                            batch_size,
×
224
                        )?
×
225
                        .into_iter()
×
226
                        .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
×
227
                    );
228
                }
229
            }
×
230

231
            populate_with_txids(self, anchor_block, &mut update, &mut txids.iter().cloned())?;
×
232

233
            let _txs = populate_with_outpoints(
×
234
                self,
×
235
                anchor_block,
×
236
                &mut update,
×
237
                &mut outpoints.iter().cloned(),
×
238
            )?;
×
239

240
            // check for reorgs during scan process
241
            let server_blockhash = self
×
242
                .block_header(anchor_block.height as usize)?
×
243
                .block_hash();
×
244
            if anchor_block.hash != server_blockhash {
×
245
                continue; // reorg
×
246
            }
×
247

×
248
            update.keychain_update = request_spks
×
249
                .into_keys()
×
250
                .filter_map(|k| {
×
251
                    scanned_spks
×
252
                        .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
×
253
                        .rev()
×
254
                        .find(|(_, (_, active))| *active)
×
255
                        .map(|((_, i), _)| (k, *i))
×
256
                })
×
257
                .collect::<BTreeMap<_, _>>();
×
258
            break update;
×
259
        };
×
260

×
261
        Ok(update)
×
262
    }
×
263
}
264

265
/// Prepare an update "template" based on the checkpoints of the `local_chain`.
266
fn prepare_chain_update(
×
267
    client: &Client,
×
268
    prev_tip: Option<CheckPoint>,
×
269
) -> Result<CheckPoint, Error> {
×
270
    let mut header_notification = client.block_headers_subscribe()?;
×
271

272
    let (new_blocks, mut last_cp) = 'retry: loop {
×
273
        let tip = BlockId {
×
274
            height: header_notification.height as _,
×
275
            hash: header_notification.header.block_hash(),
×
276
        };
×
277
        let tip_parent = BlockId {
×
278
            height: (header_notification.height - 1) as _,
×
279
            hash: header_notification.header.prev_blockhash,
×
280
        };
×
281

×
282
        // this records new blocks, including blocks that are to be replaced
×
283
        let mut new_blocks = [tip_parent, tip]
×
284
            .into_iter()
×
285
            .map(|b| (b.height, b.hash))
×
286
            .collect::<BTreeMap<u32, BlockHash>>();
×
287
        let mut agreement_cp = Option::<CheckPoint>::None;
×
288

289
        for cp in prev_tip.iter().flat_map(CheckPoint::iter) {
×
290
            let cp_block = cp.block_id();
×
291
            // TODO: a batch request may be safer, as a reorg that happens when we are obtaining
292
            //       `block_header`s will result in inconsistencies
293
            let hash = client.block_header(cp_block.height as _)?.block_hash();
×
294
            if hash == cp_block.hash {
×
295
                agreement_cp = Some(cp);
×
296
                break;
×
297
            }
×
298
            new_blocks.insert(cp_block.height, hash);
×
299
        }
300

301
        // check for tip changes
302
        loop {
303
            match client.block_headers_pop()? {
×
304
                Some(new_notification) => {
×
305
                    let new_height = new_notification.height;
×
306
                    header_notification = new_notification;
×
307
                    if new_height as u32 <= tip.height {
×
308
                        // we may have a reorg
309
                        // reorg-detection logic can be improved (false positives are possible)
310
                        continue 'retry;
×
311
                    }
×
312
                }
313
                None => {
314
                    let new_blocks = match &agreement_cp {
×
315
                        // `new_blocks` should only include blocks that are actually new
316
                        Some(agreement_cp) => new_blocks.split_off(&(agreement_cp.height() + 1)),
×
317
                        None => new_blocks,
×
318
                    };
319

320
                    break 'retry (new_blocks, agreement_cp);
×
321
                }
322
            };
323
        }
324
    };
325

326
    // construct checkpoints
327
    for (height, hash) in new_blocks {
×
328
        let cp = match last_cp.clone() {
×
329
            Some(last_cp) => last_cp
×
330
                .extend(BlockId { height, hash })
×
331
                .expect("must extend checkpoint"),
×
332
            None => CheckPoint::new(BlockId { height, hash }),
×
333
        };
334
        last_cp = Some(cp);
×
335
    }
336

337
    Ok(last_cp.expect("must have atleast one checkpoint"))
×
338
}
×
339

340
fn determine_tx_anchor(
×
341
    anchor_block: BlockId,
×
342
    raw_height: i32,
×
343
    txid: Txid,
×
344
) -> Option<ConfirmationHeightAnchor> {
×
345
    // The electrum API has a weird quirk where an unconfirmed transaction is presented with a
×
346
    // height of 0. To avoid invalid representation in our data structures, we manually set
×
347
    // transactions residing in the genesis block to have height 0, then interpret a height of 0 as
×
348
    // unconfirmed for all other transactions.
×
349
    if txid
×
350
        == Txid::from_hex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
×
351
            .expect("must deserialize genesis coinbase txid")
×
352
    {
353
        return Some(ConfirmationHeightAnchor {
×
354
            anchor_block,
×
355
            confirmation_height: 0,
×
356
        });
×
357
    }
×
358
    match raw_height {
×
359
        h if h <= 0 => {
×
360
            debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
×
361
            None
×
362
        }
363
        h => {
×
364
            let h = h as u32;
×
365
            if h > anchor_block.height {
×
366
                None
×
367
            } else {
368
                Some(ConfirmationHeightAnchor {
×
369
                    anchor_block,
×
370
                    confirmation_height: h,
×
371
                })
×
372
            }
373
        }
374
    }
375
}
×
376

377
fn populate_with_outpoints<K>(
×
378
    client: &Client,
×
379
    anchor_block: BlockId,
×
380
    update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
×
381
    outpoints: &mut impl Iterator<Item = OutPoint>,
×
382
) -> Result<HashMap<Txid, Transaction>, Error> {
×
383
    let mut full_txs = HashMap::new();
×
384
    for outpoint in outpoints {
×
385
        let txid = outpoint.txid;
×
386
        let tx = client.transaction_get(&txid)?;
×
387
        debug_assert_eq!(tx.txid(), txid);
×
388
        let txout = match tx.output.get(outpoint.vout as usize) {
×
389
            Some(txout) => txout,
×
390
            None => continue,
×
391
        };
392
        // attempt to find the following transactions (alongside their chain positions), and
393
        // add to our sparsechain `update`:
394
        let mut has_residing = false; // tx in which the outpoint resides
×
395
        let mut has_spending = false; // tx that spends the outpoint
×
396
        for res in client.script_get_history(&txout.script_pubkey)? {
×
397
            if has_residing && has_spending {
×
398
                break;
×
399
            }
×
400

×
401
            if res.tx_hash == txid {
×
402
                if has_residing {
×
403
                    continue;
×
404
                }
×
405
                has_residing = true;
×
406
                full_txs.insert(res.tx_hash, tx.clone());
×
407
            } else {
408
                if has_spending {
×
409
                    continue;
×
410
                }
×
411
                let res_tx = match full_txs.get(&res.tx_hash) {
×
412
                    Some(tx) => tx,
×
413
                    None => {
414
                        let res_tx = client.transaction_get(&res.tx_hash)?;
×
415
                        full_txs.insert(res.tx_hash, res_tx);
×
416
                        full_txs.get(&res.tx_hash).expect("just inserted")
×
417
                    }
418
                };
419
                has_spending = res_tx
×
420
                    .input
×
421
                    .iter()
×
422
                    .any(|txin| txin.previous_output == outpoint);
×
423
                if !has_spending {
×
424
                    continue;
×
425
                }
×
426
            };
427

428
            let anchor = determine_tx_anchor(anchor_block, res.height, res.tx_hash);
×
429

×
430
            let tx_entry = update.graph_update.entry(res.tx_hash).or_default();
×
431
            if let Some(anchor) = anchor {
×
432
                tx_entry.insert(anchor);
×
433
            }
×
434
        }
435
    }
436
    Ok(full_txs)
×
437
}
×
438

439
fn populate_with_txids<K>(
×
440
    client: &Client,
×
441
    anchor_block: BlockId,
×
442
    update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
×
443
    txids: &mut impl Iterator<Item = Txid>,
×
444
) -> Result<(), Error> {
×
445
    for txid in txids {
×
446
        let tx = match client.transaction_get(&txid) {
×
447
            Ok(tx) => tx,
×
448
            Err(electrum_client::Error::Protocol(_)) => continue,
×
449
            Err(other_err) => return Err(other_err),
×
450
        };
451

452
        let spk = tx
×
453
            .output
×
454
            .get(0)
×
455
            .map(|txo| &txo.script_pubkey)
×
456
            .expect("tx must have an output");
×
457

458
        let anchor = match client
×
459
            .script_get_history(spk)?
×
460
            .into_iter()
×
461
            .find(|r| r.tx_hash == txid)
×
462
        {
463
            Some(r) => determine_tx_anchor(anchor_block, r.height, txid),
×
464
            None => continue,
×
465
        };
466

467
        let tx_entry = update.graph_update.entry(txid).or_default();
×
468
        if let Some(anchor) = anchor {
×
469
            tx_entry.insert(anchor);
×
470
        }
×
471
    }
472
    Ok(())
×
473
}
×
474

475
fn populate_with_spks<K, I: Ord + Clone>(
×
476
    client: &Client,
×
477
    anchor_block: BlockId,
×
478
    update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
×
479
    spks: &mut impl Iterator<Item = (I, Script)>,
×
480
    stop_gap: usize,
×
481
    batch_size: usize,
×
482
) -> Result<BTreeMap<I, (Script, bool)>, Error> {
×
483
    let mut unused_spk_count = 0_usize;
×
484
    let mut scanned_spks = BTreeMap::new();
×
485

486
    loop {
×
487
        let spks = (0..batch_size)
×
488
            .map_while(|_| spks.next())
×
489
            .collect::<Vec<_>>();
×
490
        if spks.is_empty() {
×
491
            return Ok(scanned_spks);
×
492
        }
×
493

494
        let spk_histories = client.batch_script_get_history(spks.iter().map(|(_, s)| s))?;
×
495

496
        for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
×
497
            if spk_history.is_empty() {
×
498
                scanned_spks.insert(spk_index, (spk, false));
×
499
                unused_spk_count += 1;
×
500
                if unused_spk_count > stop_gap {
×
501
                    return Ok(scanned_spks);
×
502
                }
×
503
                continue;
×
504
            } else {
×
505
                scanned_spks.insert(spk_index, (spk, true));
×
506
                unused_spk_count = 0;
×
507
            }
×
508

509
            for tx in spk_history {
×
510
                let tx_entry = update.graph_update.entry(tx.tx_hash).or_default();
×
511
                if let Some(anchor) = determine_tx_anchor(anchor_block, tx.height, tx.tx_hash) {
×
512
                    tx_entry.insert(anchor);
×
513
                }
×
514
            }
515
        }
516
    }
517
}
×
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