Skip to content

Commit 9033e0e

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 9033e0e

File tree

5 files changed

+76
-177
lines changed

5 files changed

+76
-177
lines changed

crates/electrum/src/electrum_ext.rs

Lines changed: 64 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,15 @@
11
use bdk_chain::{
2-
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
2+
bitcoin::{OutPoint, ScriptBuf, Txid},
33
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,
4+
tx_graph::TxGraph,
5+
BlockId, ConfirmationTimeHeightAnchor,
126
};
7+
use electrum_client::{ElectrumApi, Error, HeaderNotification};
8+
use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
139

1410
/// We include a chain suffix of a certain length for the purpose of robustness.
1511
const CHAIN_SUFFIX_LENGTH: u32 = 8;
1612

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-
12013
/// Combination of chain and transactions updates from electrum
12114
///
12215
/// We have to update the chain and the txids at the same time since we anchor the txids to
@@ -125,11 +18,11 @@ impl RelevantTxids {
12518
pub struct ElectrumUpdate {
12619
/// Chain update
12720
pub chain_update: local_chain::Update,
128-
/// Transaction updates from electrum
129-
pub relevant_txids: RelevantTxids,
21+
/// Tracks electrum updates in TxGraph
22+
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
13023
}
13124

132-
/// Trait to extend [`Client`] functionality.
25+
/// Trait to extend [`electrum_client::Client`] functionality.
13326
pub trait ElectrumExt {
13427
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
13528
/// returns updates for [`bdk_chain`] data structures.
@@ -153,7 +46,7 @@ pub trait ElectrumExt {
15346
///
15447
/// - `prev_tip`: the most recent blockchain tip present locally
15548
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
156-
/// - `txids`: transactions for which we want updated [`Anchor`]s
49+
/// - `txids`: transactions for which we want updated [`bdk_chain::Anchor`]s
15750
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
15851
/// want to include in the update
15952
///
@@ -190,7 +83,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
19083

19184
let (electrum_update, keychain_update) = loop {
19285
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
193-
let mut relevant_txids = RelevantTxids::default();
86+
let mut graph_update = TxGraph::<ConfirmationTimeHeightAnchor>::default();
19487
let cps = tip
19588
.iter()
19689
.take(10)
@@ -202,7 +95,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
20295
scanned_spks.append(&mut populate_with_spks(
20396
self,
20497
&cps,
205-
&mut relevant_txids,
98+
&mut graph_update,
20699
&mut scanned_spks
207100
.iter()
208101
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
@@ -215,7 +108,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
215108
populate_with_spks(
216109
self,
217110
&cps,
218-
&mut relevant_txids,
111+
&mut graph_update,
219112
keychain_spks,
220113
stop_gap,
221114
batch_size,
@@ -251,7 +144,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
251144
break (
252145
ElectrumUpdate {
253146
chain_update,
254-
relevant_txids,
147+
graph_update,
255148
},
256149
keychain_update,
257150
);
@@ -287,10 +180,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
287180
.map(|cp| (cp.height(), cp))
288181
.collect::<BTreeMap<u32, CheckPoint>>();
289182

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)?;
183+
populate_with_txids(self, &cps, &mut electrum_update.graph_update, txids)?;
184+
populate_with_outpoints(self, &cps, &mut electrum_update.graph_update, outpoints)?;
294185

295186
Ok(electrum_update)
296187
}
@@ -373,10 +264,16 @@ fn construct_update_tip(
373264
///
374265
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
375266
fn determine_tx_anchor(
267+
client: &impl ElectrumApi,
376268
cps: &BTreeMap<u32, CheckPoint>,
377269
raw_height: i32,
378270
txid: Txid,
379-
) -> Option<ConfirmationHeightAnchor> {
271+
) -> Option<ConfirmationTimeHeightAnchor> {
272+
let confirmation_time = client
273+
.block_header(raw_height as usize)
274+
.expect("header must exist")
275+
.time as u64;
276+
380277
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
381278
// height of 0. To avoid invalid representation in our data structures, we manually set
382279
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
@@ -386,9 +283,10 @@ fn determine_tx_anchor(
386283
.expect("must deserialize genesis coinbase txid")
387284
{
388285
let anchor_block = cps.values().next()?.block_id();
389-
return Some(ConfirmationHeightAnchor {
286+
return Some(ConfirmationTimeHeightAnchor {
390287
anchor_block,
391288
confirmation_height: 0,
289+
confirmation_time,
392290
});
393291
}
394292
match raw_height {
@@ -402,9 +300,10 @@ fn determine_tx_anchor(
402300
if h > anchor_block.height {
403301
None
404302
} else {
405-
Some(ConfirmationHeightAnchor {
303+
Some(ConfirmationTimeHeightAnchor {
406304
anchor_block,
407305
confirmation_height: h,
306+
confirmation_time,
408307
})
409308
}
410309
}
@@ -414,10 +313,9 @@ fn determine_tx_anchor(
414313
fn populate_with_outpoints(
415314
client: &impl ElectrumApi,
416315
cps: &BTreeMap<u32, CheckPoint>,
417-
relevant_txids: &mut RelevantTxids,
316+
tx_graph: &mut TxGraph<ConfirmationTimeHeightAnchor>,
418317
outpoints: impl IntoIterator<Item = OutPoint>,
419-
) -> Result<HashMap<Txid, Transaction>, Error> {
420-
let mut full_txs = HashMap::new();
318+
) -> Result<(), Error> {
421319
for outpoint in outpoints {
422320
let txid = outpoint.txid;
423321
let tx = client.transaction_get(&txid)?;
@@ -431,6 +329,8 @@ fn populate_with_outpoints(
431329
let mut has_residing = false; // tx in which the outpoint resides
432330
let mut has_spending = false; // tx that spends the outpoint
433331
for res in client.script_get_history(&txout.script_pubkey)? {
332+
let mut update = TxGraph::<ConfirmationTimeHeightAnchor>::default();
333+
434334
if has_residing && has_spending {
435335
break;
436336
}
@@ -440,17 +340,19 @@ fn populate_with_outpoints(
440340
continue;
441341
}
442342
has_residing = true;
443-
full_txs.insert(res.tx_hash, tx.clone());
343+
if tx_graph.get_tx(res.tx_hash).is_none() {
344+
update = TxGraph::<ConfirmationTimeHeightAnchor>::new([tx.clone()]);
345+
}
444346
} else {
445347
if has_spending {
446348
continue;
447349
}
448-
let res_tx = match full_txs.get(&res.tx_hash) {
350+
let res_tx = match tx_graph.get_tx(res.tx_hash) {
449351
Some(tx) => tx,
450352
None => {
451353
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")
354+
update = TxGraph::<ConfirmationTimeHeightAnchor>::new([res_tx]);
355+
tx_graph.get_tx(res.tx_hash).expect("just inserted")
454356
}
455357
};
456358
has_spending = res_tx
@@ -462,23 +364,25 @@ fn populate_with_outpoints(
462364
}
463365
};
464366

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);
367+
if let Some(anchor) = determine_tx_anchor(client, cps, res.height, res.tx_hash) {
368+
let _ = update.insert_anchor(res.tx_hash, anchor);
469369
}
370+
371+
let _ = tx_graph.apply_update(update);
470372
}
471373
}
472-
Ok(full_txs)
374+
Ok(())
473375
}
474376

475377
fn populate_with_txids(
476378
client: &impl ElectrumApi,
477379
cps: &BTreeMap<u32, CheckPoint>,
478-
relevant_txids: &mut RelevantTxids,
380+
tx_graph: &mut TxGraph<ConfirmationTimeHeightAnchor>,
479381
txids: impl IntoIterator<Item = Txid>,
480382
) -> Result<(), Error> {
481383
for txid in txids {
384+
let mut update = TxGraph::<ConfirmationTimeHeightAnchor>::default();
385+
482386
let tx = match client.transaction_get(&txid) {
483387
Ok(tx) => tx,
484388
Err(electrum_client::Error::Protocol(_)) => continue,
@@ -496,22 +400,27 @@ fn populate_with_txids(
496400
.into_iter()
497401
.find(|r| r.tx_hash == txid)
498402
{
499-
Some(r) => determine_tx_anchor(cps, r.height, txid),
403+
Some(r) => determine_tx_anchor(client, cps, r.height, txid),
500404
None => continue,
501405
};
502406

503-
let tx_entry = relevant_txids.0.entry(txid).or_default();
407+
if tx_graph.get_tx(txid).is_none() {
408+
update = TxGraph::<ConfirmationTimeHeightAnchor>::new([tx]);
409+
}
410+
504411
if let Some(anchor) = anchor {
505-
tx_entry.insert(anchor);
412+
let _ = update.insert_anchor(txid, anchor);
506413
}
414+
415+
let _ = tx_graph.apply_update(update);
507416
}
508417
Ok(())
509418
}
510419

511420
fn populate_with_spks<I: Ord + Clone>(
512421
client: &impl ElectrumApi,
513422
cps: &BTreeMap<u32, CheckPoint>,
514-
relevant_txids: &mut RelevantTxids,
423+
tx_graph: &mut TxGraph<ConfirmationTimeHeightAnchor>,
515424
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
516425
stop_gap: usize,
517426
batch_size: usize,
@@ -544,10 +453,18 @@ fn populate_with_spks<I: Ord + Clone>(
544453
}
545454

546455
for tx in spk_history {
547-
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
548-
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
549-
tx_entry.insert(anchor);
456+
let mut update = TxGraph::<ConfirmationTimeHeightAnchor>::default();
457+
458+
if tx_graph.get_tx(tx.tx_hash).is_none() {
459+
let full_tx = client.transaction_get(&tx.tx_hash)?;
460+
update = TxGraph::<ConfirmationTimeHeightAnchor>::new([full_tx]);
461+
}
462+
463+
if let Some(anchor) = determine_tx_anchor(client, cps, tx.height, tx.tx_hash) {
464+
let _ = update.insert_anchor(tx.tx_hash, anchor);
550465
}
466+
467+
let _ = tx_graph.apply_update(update);
551468
}
552469
}
553470
}

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)