Skip to content

Commit c3295e3

Browse files
committed
feat(electrum)!: introduce TxCache
We maintain a cache of full transactions so we avoid re-fetching from Electrum if not needed. In addition, the `ElectrumUpdate` struct has the anchor type as a generic which can be mapped to update any receiving `TxGraph`.
1 parent d7272cd commit c3295e3

File tree

5 files changed

+167
-140
lines changed

5 files changed

+167
-140
lines changed

crates/electrum/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ electrum-client = { version = "0.19" }
1919
[dev-dependencies]
2020
bdk_testenv = { path = "../testenv", default-features = false }
2121
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
22-
anyhow = "1"
22+
anyhow = "1"

crates/electrum/src/electrum_ext.rs

Lines changed: 116 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,86 @@
11
use bdk_chain::{
2-
bitcoin::{OutPoint, ScriptBuf, Txid},
3-
collections::{HashMap, HashSet},
2+
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
3+
collections::{BTreeMap, HashMap, HashSet},
44
local_chain::CheckPoint,
55
tx_graph::TxGraph,
6-
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
6+
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
77
};
8+
use core::{fmt::Debug, str::FromStr};
89
use electrum_client::{ElectrumApi, Error, HeaderNotification};
9-
use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
10+
use std::sync::Arc;
1011

1112
/// We include a chain suffix of a certain length for the purpose of robustness.
1213
const CHAIN_SUFFIX_LENGTH: u32 = 8;
1314

15+
/// Type that maintains a cache of [`Arc`]-wrapped transactions.
16+
pub type TxCache = HashMap<Txid, Arc<Transaction>>;
17+
1418
/// Combination of chain and transactions updates from electrum
1519
///
1620
/// We have to update the chain and the txids at the same time since we anchor the txids to
1721
/// the same chain tip that we check before and after we gather the txids.
1822
#[derive(Debug)]
19-
pub struct ElectrumUpdate {
23+
pub struct ElectrumUpdate<A = ConfirmationHeightAnchor> {
2024
/// Chain update
2125
pub chain_update: CheckPoint,
2226
/// Tracks electrum updates in TxGraph
23-
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
27+
pub graph_update: TxGraph<A>,
28+
}
29+
30+
impl<A: Clone + Ord> ElectrumUpdate<A> {
31+
/// Transform the [`ElectrumUpdate`] to have [`bdk_chain::Anchor`]s of another type.
32+
///
33+
/// Refer to [`TxGraph::map_anchors`].
34+
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> ElectrumUpdate<A2>
35+
where
36+
F: FnMut(A) -> A2,
37+
{
38+
ElectrumUpdate {
39+
chain_update: self.chain_update,
40+
graph_update: self.graph_update.map_anchors(f),
41+
}
42+
}
43+
}
44+
45+
impl ElectrumUpdate {
46+
/// Transforms the [`TxGraph`]'s [`bdk_chain::Anchor`] type to [`ConfirmationTimeHeightAnchor`].
47+
pub fn into_confirmation_time_update(
48+
self,
49+
client: &impl ElectrumApi,
50+
) -> Result<ElectrumUpdate<ConfirmationTimeHeightAnchor>, Error> {
51+
let relevant_heights = self
52+
.graph_update
53+
.all_anchors()
54+
.iter()
55+
.map(|(a, _)| a.confirmation_height)
56+
.collect::<HashSet<_>>();
57+
58+
let height_to_time = relevant_heights
59+
.clone()
60+
.into_iter()
61+
.zip(
62+
client
63+
.batch_block_header(relevant_heights)?
64+
.into_iter()
65+
.map(|bh| bh.time as u64),
66+
)
67+
.collect::<HashMap<u32, u64>>();
68+
69+
let chain_update = self.chain_update;
70+
let graph_update =
71+
self.graph_update
72+
.clone()
73+
.map_anchors(|a| ConfirmationTimeHeightAnchor {
74+
anchor_block: a.anchor_block,
75+
confirmation_height: a.confirmation_height,
76+
confirmation_time: height_to_time[&a.confirmation_height],
77+
});
78+
79+
Ok(ElectrumUpdate {
80+
chain_update,
81+
graph_update,
82+
})
83+
}
2484
}
2585

2686
/// Trait to extend [`electrum_client::Client`] functionality.
@@ -35,11 +95,11 @@ pub trait ElectrumExt {
3595
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
3696
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
3797
/// single batch request.
38-
fn full_scan<K: Ord + Clone, A: Anchor>(
98+
fn full_scan<K: Ord + Clone>(
3999
&self,
100+
tx_cache: &mut TxCache,
40101
prev_tip: CheckPoint,
41102
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
42-
full_txs: Option<&TxGraph<A>>,
43103
stop_gap: usize,
44104
batch_size: usize,
45105
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
@@ -61,42 +121,42 @@ pub trait ElectrumExt {
61121
/// may include scripts that have been used, use [`full_scan`] with the keychain.
62122
///
63123
/// [`full_scan`]: ElectrumExt::full_scan
64-
fn sync<A: Anchor>(
124+
fn sync(
65125
&self,
126+
tx_cache: &mut TxCache,
66127
prev_tip: CheckPoint,
67128
misc_spks: impl IntoIterator<Item = ScriptBuf>,
68-
full_txs: Option<&TxGraph<A>>,
69129
txids: impl IntoIterator<Item = Txid>,
70130
outpoints: impl IntoIterator<Item = OutPoint>,
71131
batch_size: usize,
72132
) -> Result<ElectrumUpdate, Error>;
73133
}
74134

75135
impl<E: ElectrumApi> ElectrumExt for E {
76-
fn full_scan<K: Ord + Clone, A: Anchor>(
136+
fn full_scan<K: Ord + Clone>(
77137
&self,
138+
tx_cache: &mut TxCache,
78139
prev_tip: CheckPoint,
79140
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
80-
full_txs: Option<&TxGraph<A>>,
81141
stop_gap: usize,
82142
batch_size: usize,
83143
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
84144
let mut request_spks = keychain_spks
85145
.into_iter()
86146
.map(|(k, s)| (k, s.into_iter()))
87147
.collect::<BTreeMap<K, _>>();
148+
149+
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
150+
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
151+
// cannot be collected. In addition, we keep track of whether an spk has an active tx
152+
// history for determining the `last_active_index`.
153+
// * key: (keychain, spk_index) that identifies the spk.
154+
// * val: (script_pubkey, has_tx_history).
88155
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
89156

90157
let (electrum_update, keychain_update) = loop {
91158
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
92-
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
93-
if let Some(txs) = full_txs {
94-
let _ =
95-
tx_graph.apply_update(txs.clone().map_anchors(|a| ConfirmationHeightAnchor {
96-
anchor_block: a.anchor_block(),
97-
confirmation_height: a.confirmation_height_upper_bound(),
98-
}));
99-
}
159+
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
100160
let cps = tip
101161
.iter()
102162
.take(10)
@@ -108,7 +168,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
108168
scanned_spks.append(&mut populate_with_spks(
109169
self,
110170
&cps,
111-
&mut tx_graph,
171+
tx_cache,
172+
&mut graph_update,
112173
&mut scanned_spks
113174
.iter()
114175
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
@@ -121,7 +182,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
121182
populate_with_spks(
122183
self,
123184
&cps,
124-
&mut tx_graph,
185+
tx_cache,
186+
&mut graph_update,
125187
keychain_spks,
126188
stop_gap,
127189
batch_size,
@@ -140,8 +202,6 @@ impl<E: ElectrumApi> ElectrumExt for E {
140202

141203
let chain_update = tip;
142204

143-
let graph_update = into_confirmation_time_tx_graph(self, &tx_graph)?;
144-
145205
let keychain_update = request_spks
146206
.into_keys()
147207
.filter_map(|k| {
@@ -165,11 +225,11 @@ impl<E: ElectrumApi> ElectrumExt for E {
165225
Ok((electrum_update, keychain_update))
166226
}
167227

168-
fn sync<A: Anchor>(
228+
fn sync(
169229
&self,
230+
tx_cache: &mut TxCache,
170231
prev_tip: CheckPoint,
171232
misc_spks: impl IntoIterator<Item = ScriptBuf>,
172-
full_txs: Option<&TxGraph<A>>,
173233
txids: impl IntoIterator<Item = Txid>,
174234
outpoints: impl IntoIterator<Item = OutPoint>,
175235
batch_size: usize,
@@ -179,10 +239,10 @@ impl<E: ElectrumApi> ElectrumExt for E {
179239
.enumerate()
180240
.map(|(i, spk)| (i as u32, spk));
181241

182-
let (mut electrum_update, _) = self.full_scan(
242+
let (electrum_update, _) = self.full_scan(
243+
tx_cache,
183244
prev_tip.clone(),
184245
[((), spk_iter)].into(),
185-
full_txs,
186246
usize::MAX,
187247
batch_size,
188248
)?;
@@ -195,11 +255,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
195255
.collect::<BTreeMap<u32, CheckPoint>>();
196256

197257
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
198-
populate_with_txids(self, &cps, &mut tx_graph, txids)?;
258+
populate_with_txids(self, &cps, tx_cache, &mut tx_graph, txids)?;
199259
populate_with_outpoints(self, &cps, &mut tx_graph, outpoints)?;
200-
let _ = electrum_update
201-
.graph_update
202-
.apply_update(into_confirmation_time_tx_graph(self, &tx_graph)?);
203260

204261
Ok(electrum_update)
205262
}
@@ -383,11 +440,12 @@ fn populate_with_outpoints(
383440
fn populate_with_txids(
384441
client: &impl ElectrumApi,
385442
cps: &BTreeMap<u32, CheckPoint>,
386-
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
443+
tx_cache: &mut TxCache,
444+
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
387445
txids: impl IntoIterator<Item = Txid>,
388446
) -> Result<(), Error> {
389447
for txid in txids {
390-
let tx = match client.transaction_get(&txid) {
448+
let tx = match fetch_tx(client, tx_cache, txid) {
391449
Ok(tx) => tx,
392450
Err(electrum_client::Error::Protocol(_)) => continue,
393451
Err(other_err) => return Err(other_err),
@@ -408,20 +466,36 @@ fn populate_with_txids(
408466
None => continue,
409467
};
410468

411-
if tx_graph.get_tx(txid).is_none() {
412-
let _ = tx_graph.insert_tx(tx);
469+
if graph_update.get_tx(txid).is_none() {
470+
// TODO: We need to be able to insert an `Arc` of a transaction.
471+
let _ = graph_update.insert_tx(tx);
413472
}
414473
if let Some(anchor) = anchor {
415-
let _ = tx_graph.insert_anchor(txid, anchor);
474+
let _ = graph_update.insert_anchor(txid, anchor);
416475
}
417476
}
418477
Ok(())
419478
}
420479

480+
fn fetch_tx<C: ElectrumApi>(
481+
client: &C,
482+
tx_cache: &mut TxCache,
483+
txid: Txid,
484+
) -> Result<Arc<Transaction>, Error> {
485+
use bdk_chain::collections::hash_map::Entry;
486+
Ok(match tx_cache.entry(txid) {
487+
Entry::Occupied(entry) => entry.get().clone(),
488+
Entry::Vacant(entry) => entry
489+
.insert(Arc::new(client.transaction_get(&txid)?))
490+
.clone(),
491+
})
492+
}
493+
421494
fn populate_with_spks<I: Ord + Clone>(
422495
client: &impl ElectrumApi,
423496
cps: &BTreeMap<u32, CheckPoint>,
424-
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
497+
tx_cache: &mut TxCache,
498+
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
425499
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
426500
stop_gap: usize,
427501
batch_size: usize,
@@ -453,51 +527,12 @@ fn populate_with_spks<I: Ord + Clone>(
453527
unused_spk_count = 0;
454528
}
455529

456-
for tx in spk_history {
457-
let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
458-
459-
if tx_graph.get_tx(tx.tx_hash).is_none() {
460-
let full_tx = client.transaction_get(&tx.tx_hash)?;
461-
update = TxGraph::<ConfirmationHeightAnchor>::new([full_tx]);
530+
for tx_res in spk_history {
531+
let _ = graph_update.insert_tx(fetch_tx(client, tx_cache, tx_res.tx_hash)?);
532+
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
533+
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
462534
}
463-
464-
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
465-
let _ = update.insert_anchor(tx.tx_hash, anchor);
466-
}
467-
468-
let _ = tx_graph.apply_update(update);
469535
}
470536
}
471537
}
472538
}
473-
474-
fn into_confirmation_time_tx_graph(
475-
client: &impl ElectrumApi,
476-
tx_graph: &TxGraph<ConfirmationHeightAnchor>,
477-
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
478-
let relevant_heights = tx_graph
479-
.all_anchors()
480-
.iter()
481-
.map(|(a, _)| a.confirmation_height)
482-
.collect::<HashSet<_>>();
483-
484-
let height_to_time = relevant_heights
485-
.clone()
486-
.into_iter()
487-
.zip(
488-
client
489-
.batch_block_header(relevant_heights)?
490-
.into_iter()
491-
.map(|bh| bh.time as u64),
492-
)
493-
.collect::<HashMap<u32, u64>>();
494-
495-
let new_graph = tx_graph
496-
.clone()
497-
.map_anchors(|a| ConfirmationTimeHeightAnchor {
498-
anchor_block: a.anchor_block,
499-
confirmation_height: a.confirmation_height,
500-
confirmation_time: height_to_time[&a.confirmation_height],
501-
});
502-
Ok(new_graph)
503-
}

0 commit comments

Comments
 (0)