11use 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 } ;
89use 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.
1213const 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
75135impl < 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(
383440fn 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+
421494fn 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