Skip to content

Commit 6f39514

Browse files
committed
refactor(electrum): remove RelevantTxids and track txs in TxGraph
This PR removes `RelevantTxids` from the electrum crate and tracks transactions in a `TxGraph`. This removes the need to separately construct a `TxGraph` after a `full_scan` or `sync`.
1 parent 62619d3 commit 6f39514

File tree

5 files changed

+101
-172
lines changed

5 files changed

+101
-172
lines changed

crates/electrum/src/electrum_ext.rs

Lines changed: 89 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,16 @@
11
use bdk_chain::{
2-
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
2+
bitcoin::{OutPoint, ScriptBuf, Txid},
3+
collections::{HashMap, HashSet},
34
local_chain::{self, CheckPoint},
4-
tx_graph::{self, TxGraph},
5-
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
6-
};
7-
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
8-
use std::{
9-
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
10-
fmt::Debug,
11-
str::FromStr,
5+
tx_graph::TxGraph,
6+
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
127
};
8+
use electrum_client::{ElectrumApi, Error, HeaderNotification};
9+
use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
1310

1411
/// We include a chain suffix of a certain length for the purpose of robustness.
1512
const CHAIN_SUFFIX_LENGTH: u32 = 8;
1613

17-
/// Represents updates fetched from an Electrum server, but excludes full transactions.
18-
///
19-
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
20-
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
21-
/// fetch the full transactions from Electrum and finalize the update.
22-
#[derive(Debug, Default, Clone)]
23-
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
24-
25-
impl RelevantTxids {
26-
/// Determine the full transactions that are missing from `graph`.
27-
///
28-
/// Refer to [`RelevantTxids`] for more details.
29-
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
30-
self.0
31-
.keys()
32-
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
33-
.cloned()
34-
.collect()
35-
}
36-
37-
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
38-
///
39-
/// Refer to [`RelevantTxids`] for more details.
40-
pub fn into_tx_graph(
41-
self,
42-
client: &Client,
43-
missing: Vec<Txid>,
44-
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
45-
let new_txs = client.batch_transaction_get(&missing)?;
46-
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
47-
for (txid, anchors) in self.0 {
48-
for anchor in anchors {
49-
let _ = graph.insert_anchor(txid, anchor);
50-
}
51-
}
52-
Ok(graph)
53-
}
54-
55-
/// Finalizes the update by fetching `missing` txids from the `client`, where the
56-
/// resulting [`TxGraph`] has anchors of type [`ConfirmationTimeHeightAnchor`].
57-
///
58-
/// Refer to [`RelevantTxids`] for more details.
59-
///
60-
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
61-
// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
62-
// use it.
63-
pub fn into_confirmation_time_tx_graph(
64-
self,
65-
client: &Client,
66-
missing: Vec<Txid>,
67-
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
68-
let graph = self.into_tx_graph(client, missing)?;
69-
70-
let relevant_heights = {
71-
let mut visited_heights = HashSet::new();
72-
graph
73-
.all_anchors()
74-
.iter()
75-
.map(|(a, _)| a.confirmation_height_upper_bound())
76-
.filter(move |&h| visited_heights.insert(h))
77-
.collect::<Vec<_>>()
78-
};
79-
80-
let height_to_time = relevant_heights
81-
.clone()
82-
.into_iter()
83-
.zip(
84-
client
85-
.batch_block_header(relevant_heights)?
86-
.into_iter()
87-
.map(|bh| bh.time as u64),
88-
)
89-
.collect::<HashMap<u32, u64>>();
90-
91-
let graph_changeset = {
92-
let old_changeset = TxGraph::default().apply_update(graph);
93-
tx_graph::ChangeSet {
94-
txs: old_changeset.txs,
95-
txouts: old_changeset.txouts,
96-
last_seen: old_changeset.last_seen,
97-
anchors: old_changeset
98-
.anchors
99-
.into_iter()
100-
.map(|(height_anchor, txid)| {
101-
let confirmation_height = height_anchor.confirmation_height;
102-
let confirmation_time = height_to_time[&confirmation_height];
103-
let time_anchor = ConfirmationTimeHeightAnchor {
104-
anchor_block: height_anchor.anchor_block,
105-
confirmation_height,
106-
confirmation_time,
107-
};
108-
(time_anchor, txid)
109-
})
110-
.collect(),
111-
}
112-
};
113-
114-
let mut new_graph = TxGraph::default();
115-
new_graph.apply_changeset(graph_changeset);
116-
Ok(new_graph)
117-
}
118-
}
119-
12014
/// Combination of chain and transactions updates from electrum
12115
///
12216
/// We have to update the chain and the txids at the same time since we anchor the txids to
@@ -125,11 +19,11 @@ impl RelevantTxids {
12519
pub struct ElectrumUpdate {
12620
/// Chain update
12721
pub chain_update: local_chain::Update,
128-
/// Transaction updates from electrum
129-
pub relevant_txids: RelevantTxids,
22+
/// Tracks electrum updates in TxGraph
23+
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
13024
}
13125

132-
/// Trait to extend [`Client`] functionality.
26+
/// Trait to extend [`electrum_client::Client`] functionality.
13327
pub trait ElectrumExt {
13428
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
13529
/// returns updates for [`bdk_chain`] data structures.
@@ -153,7 +47,7 @@ pub trait ElectrumExt {
15347
///
15448
/// - `prev_tip`: the most recent blockchain tip present locally
15549
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
156-
/// - `txids`: transactions for which we want updated [`Anchor`]s
50+
/// - `txids`: transactions for which we want updated [`bdk_chain::Anchor`]s
15751
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
15852
/// want to include in the update
15953
///
@@ -190,7 +84,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
19084

19185
let (electrum_update, keychain_update) = loop {
19286
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
193-
let mut relevant_txids = RelevantTxids::default();
87+
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
19488
let cps = tip
19589
.iter()
19690
.take(10)
@@ -202,7 +96,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
20296
scanned_spks.append(&mut populate_with_spks(
20397
self,
20498
&cps,
205-
&mut relevant_txids,
99+
&mut tx_graph,
206100
&mut scanned_spks
207101
.iter()
208102
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
@@ -215,7 +109,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
215109
populate_with_spks(
216110
self,
217111
&cps,
218-
&mut relevant_txids,
112+
&mut tx_graph,
219113
keychain_spks,
220114
stop_gap,
221115
batch_size,
@@ -237,6 +131,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
237131
introduce_older_blocks: true,
238132
};
239133

134+
let graph_update = into_confirmation_time_tx_graph(self, &tx_graph)?;
135+
240136
let keychain_update = request_spks
241137
.into_keys()
242138
.filter_map(|k| {
@@ -251,7 +147,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
251147
break (
252148
ElectrumUpdate {
253149
chain_update,
254-
relevant_txids,
150+
graph_update,
255151
},
256152
keychain_update,
257153
);
@@ -287,10 +183,12 @@ impl<A: ElectrumApi> ElectrumExt for A {
287183
.map(|cp| (cp.height(), cp))
288184
.collect::<BTreeMap<u32, CheckPoint>>();
289185

290-
populate_with_txids(self, &cps, &mut electrum_update.relevant_txids, txids)?;
291-
292-
let _txs =
293-
populate_with_outpoints(self, &cps, &mut electrum_update.relevant_txids, outpoints)?;
186+
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
187+
populate_with_txids(self, &cps, &mut tx_graph, txids)?;
188+
populate_with_outpoints(self, &cps, &mut tx_graph, outpoints)?;
189+
let _ = electrum_update
190+
.graph_update
191+
.apply_update(into_confirmation_time_tx_graph(self, &tx_graph)?);
294192

295193
Ok(electrum_update)
296194
}
@@ -414,10 +312,9 @@ fn determine_tx_anchor(
414312
fn populate_with_outpoints(
415313
client: &impl ElectrumApi,
416314
cps: &BTreeMap<u32, CheckPoint>,
417-
relevant_txids: &mut RelevantTxids,
315+
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
418316
outpoints: impl IntoIterator<Item = OutPoint>,
419-
) -> Result<HashMap<Txid, Transaction>, Error> {
420-
let mut full_txs = HashMap::new();
317+
) -> Result<(), Error> {
421318
for outpoint in outpoints {
422319
let txid = outpoint.txid;
423320
let tx = client.transaction_get(&txid)?;
@@ -431,6 +328,8 @@ fn populate_with_outpoints(
431328
let mut has_residing = false; // tx in which the outpoint resides
432329
let mut has_spending = false; // tx that spends the outpoint
433330
for res in client.script_get_history(&txout.script_pubkey)? {
331+
let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
332+
434333
if has_residing && has_spending {
435334
break;
436335
}
@@ -440,17 +339,19 @@ fn populate_with_outpoints(
440339
continue;
441340
}
442341
has_residing = true;
443-
full_txs.insert(res.tx_hash, tx.clone());
342+
if tx_graph.get_tx(res.tx_hash).is_none() {
343+
update = TxGraph::<ConfirmationHeightAnchor>::new([tx.clone()]);
344+
}
444345
} else {
445346
if has_spending {
446347
continue;
447348
}
448-
let res_tx = match full_txs.get(&res.tx_hash) {
349+
let res_tx = match tx_graph.get_tx(res.tx_hash) {
449350
Some(tx) => tx,
450351
None => {
451352
let res_tx = client.transaction_get(&res.tx_hash)?;
452-
full_txs.insert(res.tx_hash, res_tx);
453-
full_txs.get(&res.tx_hash).expect("just inserted")
353+
update = TxGraph::<ConfirmationHeightAnchor>::new([res_tx]);
354+
tx_graph.get_tx(res.tx_hash).expect("just inserted")
454355
}
455356
};
456357
has_spending = res_tx
@@ -462,23 +363,25 @@ fn populate_with_outpoints(
462363
}
463364
};
464365

465-
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
466-
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
467-
if let Some(anchor) = anchor {
468-
tx_entry.insert(anchor);
366+
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
367+
let _ = update.insert_anchor(res.tx_hash, anchor);
469368
}
369+
370+
let _ = tx_graph.apply_update(update);
470371
}
471372
}
472-
Ok(full_txs)
373+
Ok(())
473374
}
474375

475376
fn populate_with_txids(
476377
client: &impl ElectrumApi,
477378
cps: &BTreeMap<u32, CheckPoint>,
478-
relevant_txids: &mut RelevantTxids,
379+
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
479380
txids: impl IntoIterator<Item = Txid>,
480381
) -> Result<(), Error> {
481382
for txid in txids {
383+
let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
384+
482385
let tx = match client.transaction_get(&txid) {
483386
Ok(tx) => tx,
484387
Err(electrum_client::Error::Protocol(_)) => continue,
@@ -500,18 +403,23 @@ fn populate_with_txids(
500403
None => continue,
501404
};
502405

503-
let tx_entry = relevant_txids.0.entry(txid).or_default();
406+
if tx_graph.get_tx(txid).is_none() {
407+
update = TxGraph::<ConfirmationHeightAnchor>::new([tx]);
408+
}
409+
504410
if let Some(anchor) = anchor {
505-
tx_entry.insert(anchor);
411+
let _ = update.insert_anchor(txid, anchor);
506412
}
413+
414+
let _ = tx_graph.apply_update(update);
507415
}
508416
Ok(())
509417
}
510418

511419
fn populate_with_spks<I: Ord + Clone>(
512420
client: &impl ElectrumApi,
513421
cps: &BTreeMap<u32, CheckPoint>,
514-
relevant_txids: &mut RelevantTxids,
422+
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
515423
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
516424
stop_gap: usize,
517425
batch_size: usize,
@@ -544,11 +452,50 @@ fn populate_with_spks<I: Ord + Clone>(
544452
}
545453

546454
for tx in spk_history {
547-
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
455+
let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
456+
457+
if tx_graph.get_tx(tx.tx_hash).is_none() {
458+
let full_tx = client.transaction_get(&tx.tx_hash)?;
459+
update = TxGraph::<ConfirmationHeightAnchor>::new([full_tx]);
460+
}
461+
548462
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
549-
tx_entry.insert(anchor);
463+
let _ = update.insert_anchor(tx.tx_hash, anchor);
550464
}
465+
466+
let _ = tx_graph.apply_update(update);
551467
}
552468
}
553469
}
554470
}
471+
472+
fn into_confirmation_time_tx_graph(
473+
client: &impl ElectrumApi,
474+
tx_graph: &TxGraph<ConfirmationHeightAnchor>,
475+
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
476+
let relevant_heights = tx_graph
477+
.all_anchors()
478+
.iter()
479+
.map(|(a, _)| a.confirmation_height)
480+
.collect::<HashSet<_>>();
481+
482+
let height_to_time = relevant_heights
483+
.clone()
484+
.into_iter()
485+
.zip(
486+
client
487+
.batch_block_header(relevant_heights)?
488+
.into_iter()
489+
.map(|bh| bh.time as u64),
490+
)
491+
.collect::<HashMap<u32, u64>>();
492+
493+
let new_graph = tx_graph
494+
.clone()
495+
.map_anchors(|a| ConfirmationTimeHeightAnchor {
496+
anchor_block: a.anchor_block,
497+
confirmation_height: a.confirmation_height,
498+
confirmation_time: height_to_time[&a.confirmation_height],
499+
});
500+
Ok(new_graph)
501+
}

crates/electrum/src/lib.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,10 @@
77
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
88
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
99
//! sync or full scan the user receives relevant blockchain data and output updates for
10-
//! [`bdk_chain`] including [`RelevantTxids`].
11-
//!
12-
//! The [`RelevantTxids`] only includes `txid`s and not full transactions. The caller is responsible
13-
//! for obtaining full transactions before applying new data to their [`bdk_chain`]. This can be
14-
//! done with these steps:
15-
//!
16-
//! 1. Determine which full transactions are missing. Use [`RelevantTxids::missing_full_txs`].
17-
//!
18-
//! 2. Obtaining the full transactions. To do this via electrum use [`ElectrumApi::batch_transaction_get`].
10+
//! [`bdk_chain`] including [`bdk_chain::TxGraph`], which includes `txid`s and full transactions.
1911
//!
2012
//! Refer to [`example_electrum`] for a complete example.
2113
//!
22-
//! [`ElectrumApi::batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
2314
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
2415
2516
#![warn(missing_docs)]

0 commit comments

Comments
 (0)