Skip to content

Commit bb84d8f

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 bb84d8f

File tree

5 files changed

+92
-172
lines changed

5 files changed

+92
-172
lines changed

crates/electrum/src/electrum_ext.rs

Lines changed: 80 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)?;
@@ -440,17 +337,19 @@ fn populate_with_outpoints(
440337
continue;
441338
}
442339
has_residing = true;
443-
full_txs.insert(res.tx_hash, tx.clone());
340+
if tx_graph.get_tx(res.tx_hash).is_none() {
341+
let _ = tx_graph.insert_tx(tx.clone());
342+
}
444343
} else {
445344
if has_spending {
446345
continue;
447346
}
448-
let res_tx = match full_txs.get(&res.tx_hash) {
347+
let res_tx = match tx_graph.get_tx(res.tx_hash) {
449348
Some(tx) => tx,
450349
None => {
451350
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")
351+
let _ = tx_graph.insert_tx(res_tx);
352+
tx_graph.get_tx(res.tx_hash).expect("just inserted")
454353
}
455354
};
456355
has_spending = res_tx
@@ -462,20 +361,18 @@ fn populate_with_outpoints(
462361
}
463362
};
464363

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);
364+
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
365+
let _ = tx_graph.insert_anchor(res.tx_hash, anchor);
469366
}
470367
}
471368
}
472-
Ok(full_txs)
369+
Ok(())
473370
}
474371

475372
fn populate_with_txids(
476373
client: &impl ElectrumApi,
477374
cps: &BTreeMap<u32, CheckPoint>,
478-
relevant_txids: &mut RelevantTxids,
375+
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
479376
txids: impl IntoIterator<Item = Txid>,
480377
) -> Result<(), Error> {
481378
for txid in txids {
@@ -500,9 +397,11 @@ fn populate_with_txids(
500397
None => continue,
501398
};
502399

503-
let tx_entry = relevant_txids.0.entry(txid).or_default();
400+
if tx_graph.get_tx(txid).is_none() {
401+
let _ = tx_graph.insert_tx(tx);
402+
}
504403
if let Some(anchor) = anchor {
505-
tx_entry.insert(anchor);
404+
let _ = tx_graph.insert_anchor(txid, anchor);
506405
}
507406
}
508407
Ok(())
@@ -511,7 +410,7 @@ fn populate_with_txids(
511410
fn populate_with_spks<I: Ord + Clone>(
512411
client: &impl ElectrumApi,
513412
cps: &BTreeMap<u32, CheckPoint>,
514-
relevant_txids: &mut RelevantTxids,
413+
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
515414
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
516415
stop_gap: usize,
517416
batch_size: usize,
@@ -544,11 +443,50 @@ fn populate_with_spks<I: Ord + Clone>(
544443
}
545444

546445
for tx in spk_history {
547-
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
446+
let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
447+
448+
if tx_graph.get_tx(tx.tx_hash).is_none() {
449+
let full_tx = client.transaction_get(&tx.tx_hash)?;
450+
update = TxGraph::<ConfirmationHeightAnchor>::new([full_tx]);
451+
}
452+
548453
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
549-
tx_entry.insert(anchor);
454+
let _ = update.insert_anchor(tx.tx_hash, anchor);
550455
}
456+
457+
let _ = tx_graph.apply_update(update);
551458
}
552459
}
553460
}
554461
}
462+
463+
fn into_confirmation_time_tx_graph(
464+
client: &impl ElectrumApi,
465+
tx_graph: &TxGraph<ConfirmationHeightAnchor>,
466+
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
467+
let relevant_heights = tx_graph
468+
.all_anchors()
469+
.iter()
470+
.map(|(a, _)| a.confirmation_height)
471+
.collect::<HashSet<_>>();
472+
473+
let height_to_time = relevant_heights
474+
.clone()
475+
.into_iter()
476+
.zip(
477+
client
478+
.batch_block_header(relevant_heights)?
479+
.into_iter()
480+
.map(|bh| bh.time as u64),
481+
)
482+
.collect::<HashMap<u32, u64>>();
483+
484+
let new_graph = tx_graph
485+
.clone()
486+
.map_anchors(|a| ConfirmationTimeHeightAnchor {
487+
anchor_block: a.anchor_block,
488+
confirmation_height: a.confirmation_height,
489+
confirmation_time: height_to_time[&a.confirmation_height],
490+
});
491+
Ok(new_graph)
492+
}

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)