Skip to content

Commit 9c0f6cd

Browse files
committed
feat: stream transactions and progress during sync
Add .on_event() API with PartialUpdate and AnchorsResolved events, TxUpdate::drain_since() for safe incremental application, and wire emission into Electrum and Esplora backends. Addresses #2202.
1 parent 6d03fc3 commit 9c0f6cd

14 files changed

Lines changed: 1276 additions & 178 deletions

File tree

crates/core/src/spk_client.rs

Lines changed: 268 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
use crate::{
33
alloc::{boxed::Box, collections::VecDeque, vec::Vec},
44
collections::{BTreeMap, HashMap, HashSet},
5-
CheckPoint, ConfirmationBlockTime, Indexed,
5+
CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate, TxUpdateCursor,
66
};
77
use bitcoin::{BlockHash, OutPoint, Script, ScriptBuf, Txid};
88

9-
type InspectSync<I> = dyn FnMut(SyncItem<I>, SyncProgress) + Send + 'static;
10-
11-
type InspectFullScan<K> = dyn FnMut(K, u32, &Script) + Send + 'static;
9+
type OnSyncEvent<I, A> = dyn for<'a> FnMut(SyncRequestEvent<'a, I, A>) + Send + 'static;
10+
type OnFullScanEvent<K, A> = dyn for<'a> FnMut(FullScanRequestEvent<'a, K, A>) + Send + 'static;
1211

1312
/// An item reported to the [`inspect`](SyncRequestBuilder::inspect) closure of [`SyncRequest`].
1413
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -86,6 +85,117 @@ impl SyncProgress {
8685
}
8786
}
8887

88+
/// Progress of a [`FullScanRequest`] (total unknown until `stop_gap` terminates the scan).
89+
#[derive(Debug, Clone, Default)]
90+
pub struct FullScanProgress {
91+
/// Script pubkeys consumed across all keychains.
92+
pub keychain_spks_consumed: usize,
93+
/// Consecutive unused script pubkeys past the last-revealed index.
94+
pub consecutive_unused: usize,
95+
/// Transactions discovered so far.
96+
pub txs_discovered: usize,
97+
}
98+
99+
/// Events emitted during [`SyncRequest`] execution.
100+
#[derive(Debug)]
101+
pub enum SyncRequestEvent<'a, I, A = ConfirmationBlockTime> {
102+
/// A sync item is about to be fetched from the chain source.
103+
ItemStarted(SyncItem<'a, I>, SyncProgress),
104+
/// Incremental transaction data safe to pass to
105+
/// [`TxGraph::apply_update`](../../bdk_chain/tx_graph/struct.TxGraph.html#method.
106+
/// apply_update).
107+
///
108+
/// On Electrum, confirmed transactions may lack anchors until [`AnchorsResolved`].
109+
PartialUpdate(TxUpdate<A>),
110+
/// Anchor-only update after Electrum resolves confirmation proofs.
111+
AnchorsResolved(TxUpdate<A>),
112+
}
113+
114+
/// Events emitted during [`FullScanRequest`] execution.
115+
#[derive(Debug)]
116+
pub enum FullScanRequestEvent<'a, K, A = ConfirmationBlockTime> {
117+
/// A script pubkey is about to be scanned.
118+
SpkStarted {
119+
/// Keychain being scanned.
120+
keychain: K,
121+
/// Derivation index of the script pubkey.
122+
index: u32,
123+
/// Script pubkey.
124+
script: &'a Script,
125+
/// Scan progress so far.
126+
progress: FullScanProgress,
127+
},
128+
/// Incremental transaction data safe to pass to
129+
/// [`TxGraph::apply_update`](../../bdk_chain/tx_graph/struct.TxGraph.html#method.
130+
/// apply_update).
131+
///
132+
/// On Electrum, confirmed transactions may lack anchors until [`AnchorsResolved`].
133+
PartialUpdate(TxUpdate<A>),
134+
/// Anchor-only update after Electrum resolves confirmation proofs.
135+
AnchorsResolved(TxUpdate<A>),
136+
}
137+
138+
/// Backend-facing sink for emitting [`SyncRequestEvent`]s.
139+
///
140+
/// Constructed via [`SyncRequest::event_sink`]. Intended for official chain sources.
141+
pub struct SyncRequestEventSink<'a, I, D = BlockHash> {
142+
request: &'a mut SyncRequest<I, D>,
143+
}
144+
145+
impl<'a, I, D> SyncRequestEventSink<'a, I, D> {
146+
/// Emit a [`SyncRequestEvent::PartialUpdate`] if `update` is non-empty.
147+
pub fn emit_partial_update(&mut self, update: TxUpdate<ConfirmationBlockTime>) {
148+
if !update.is_empty() {
149+
(self.request.on_event)(SyncRequestEvent::PartialUpdate(update));
150+
}
151+
}
152+
153+
/// Emit a [`SyncRequestEvent::AnchorsResolved`] if `update` is non-empty.
154+
pub fn emit_anchors_resolved(&mut self, update: TxUpdate<ConfirmationBlockTime>) {
155+
if !update.is_empty() {
156+
(self.request.on_event)(SyncRequestEvent::AnchorsResolved(update));
157+
}
158+
}
159+
}
160+
161+
/// Backend-facing sink for emitting [`FullScanRequestEvent`]s.
162+
///
163+
/// Constructed via [`FullScanRequest::event_sink`]. Intended for official chain sources.
164+
pub struct FullScanRequestEventSink<'a, K, D = BlockHash> {
165+
request: &'a mut FullScanRequest<K, D>,
166+
}
167+
168+
impl<'a, K: Ord + Clone, D> FullScanRequestEventSink<'a, K, D> {
169+
/// Emit a [`FullScanRequestEvent::PartialUpdate`] if `update` is non-empty.
170+
pub fn emit_partial_update(&mut self, update: TxUpdate<ConfirmationBlockTime>) {
171+
if !update.is_empty() {
172+
(self.request.on_event)(FullScanRequestEvent::PartialUpdate(update));
173+
}
174+
}
175+
176+
/// Emit a [`FullScanRequestEvent::AnchorsResolved`] if `update` is non-empty.
177+
pub fn emit_anchors_resolved(&mut self, update: TxUpdate<ConfirmationBlockTime>) {
178+
if !update.is_empty() {
179+
(self.request.on_event)(FullScanRequestEvent::AnchorsResolved(update));
180+
}
181+
}
182+
183+
/// Update full-scan progress counters (called by chain sources between batches).
184+
pub fn set_full_scan_progress(&mut self, progress: FullScanProgress) {
185+
self.request.full_scan_progress = progress;
186+
}
187+
188+
/// Access the current full-scan progress.
189+
pub fn full_scan_progress(&self) -> &FullScanProgress {
190+
&self.request.full_scan_progress
191+
}
192+
193+
/// Mutable access to full-scan progress.
194+
pub fn full_scan_progress_mut(&mut self) -> &mut FullScanProgress {
195+
&mut self.request.full_scan_progress
196+
}
197+
}
198+
89199
/// [`Script`] with expected [`Txid`] histories.
90200
#[derive(Debug, Clone)]
91201
pub struct SpkWithExpectedTxids {
@@ -202,13 +312,31 @@ impl<I, D> SyncRequestBuilder<I, D> {
202312
self
203313
}
204314

315+
/// Register a callback for sync events including progress and partial transaction updates.
316+
///
317+
/// When partial updates are applied during sync, [`SyncResponse::tx_update`] at the end
318+
/// contains only undrained remainder (empty when everything was streamed).
319+
pub fn on_event<F>(mut self, on_event: F) -> Self
320+
where
321+
F: for<'a> FnMut(SyncRequestEvent<'a, I>) + Send + 'static,
322+
{
323+
self.inner.emit_partial_updates = true;
324+
self.inner.on_event = Box::new(on_event);
325+
self
326+
}
327+
205328
/// Set the closure that will inspect every sync item visited.
206-
pub fn inspect<F>(mut self, inspect: F) -> Self
329+
///
330+
/// This is sugar over [`Self::on_event`] for progress-only callbacks.
331+
pub fn inspect<F>(self, mut inspect: F) -> Self
207332
where
208333
F: FnMut(SyncItem<I>, SyncProgress) + Send + 'static,
209334
{
210-
self.inner.inspect = Box::new(inspect);
211-
self
335+
self.on_event(move |event| {
336+
if let SyncRequestEvent::ItemStarted(item, progress) = event {
337+
inspect(item, progress);
338+
}
339+
})
212340
}
213341

214342
/// Build the [`SyncRequest`].
@@ -266,7 +394,10 @@ pub struct SyncRequest<I = (), D = BlockHash> {
266394
txids_consumed: usize,
267395
outpoints: VecDeque<OutPoint>,
268396
outpoints_consumed: usize,
269-
inspect: Box<InspectSync<I>>,
397+
on_event: Box<OnSyncEvent<I, ConfirmationBlockTime>>,
398+
/// Whether to emit partial [`TxUpdate`]s during sync (set by
399+
/// [`SyncRequestBuilder::on_event`]).
400+
pub emit_partial_updates: bool,
270401
}
271402

272403
impl<I, D> From<SyncRequestBuilder<I, D>> for SyncRequest<I, D> {
@@ -296,7 +427,8 @@ impl<I, D> SyncRequest<I, D> {
296427
txids_consumed: 0,
297428
outpoints: VecDeque::new(),
298429
outpoints_consumed: 0,
299-
inspect: Box::new(|_, _| ()),
430+
on_event: Box::new(|_| ()),
431+
emit_partial_updates: false,
300432
},
301433
}
302434
}
@@ -393,9 +525,46 @@ impl<I, D> SyncRequest<I, D> {
393525
SyncIter::<I, D, OutPoint>::new(self)
394526
}
395527

528+
/// Returns a sink for emitting sync events from chain sources.
529+
pub fn event_sink(&mut self) -> SyncRequestEventSink<'_, I, D> {
530+
SyncRequestEventSink { request: self }
531+
}
532+
533+
/// Emit a partial [`TxUpdate`] if [`Self::emit_partial_updates`] is enabled.
534+
pub fn try_emit_partial_update(
535+
&mut self,
536+
cursor: &mut Option<TxUpdateCursor<ConfirmationBlockTime>>,
537+
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
538+
) {
539+
if !self.emit_partial_updates {
540+
return;
541+
}
542+
let cursor = cursor.get_or_insert_with(TxUpdateCursor::new);
543+
let delta = tx_update.drain_since(cursor);
544+
if !delta.is_empty() {
545+
(self.on_event)(SyncRequestEvent::PartialUpdate(delta));
546+
}
547+
}
548+
549+
/// Emit an anchor-only [`TxUpdate`] if [`Self::emit_partial_updates`] is enabled.
550+
pub fn try_emit_anchors_resolved(
551+
&mut self,
552+
cursor: &mut Option<TxUpdateCursor<ConfirmationBlockTime>>,
553+
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
554+
) {
555+
if !self.emit_partial_updates {
556+
return;
557+
}
558+
let cursor = cursor.get_or_insert_with(TxUpdateCursor::new);
559+
let delta = tx_update.drain_since(cursor);
560+
if !delta.is_empty() {
561+
(self.on_event)(SyncRequestEvent::AnchorsResolved(delta));
562+
}
563+
}
564+
396565
fn _call_inspect(&mut self, item: SyncItem<I>) {
397566
let progress = self.progress();
398-
(*self.inspect)(item, progress);
567+
(self.on_event)(SyncRequestEvent::ItemStarted(item, progress));
399568
}
400569
}
401570

@@ -468,13 +637,37 @@ impl<K: Ord, D> FullScanRequestBuilder<K, D> {
468637
self
469638
}
470639

640+
/// Register a callback for full-scan events including progress and partial transaction updates.
641+
///
642+
/// When partial updates are applied during the scan, [`FullScanResponse::tx_update`] at the end
643+
/// contains only undrained remainder (empty when everything was streamed).
644+
pub fn on_event<F>(mut self, on_event: F) -> Self
645+
where
646+
F: for<'a> FnMut(FullScanRequestEvent<'a, K>) + Send + 'static,
647+
{
648+
self.inner.emit_partial_updates = true;
649+
self.inner.on_event = Box::new(on_event);
650+
self
651+
}
652+
471653
/// Set the closure that will inspect every sync item visited.
472-
pub fn inspect<F>(mut self, inspect: F) -> Self
654+
///
655+
/// This is sugar over [`Self::on_event`] for progress-only callbacks.
656+
pub fn inspect<F>(self, mut inspect: F) -> Self
473657
where
474658
F: FnMut(K, u32, &Script) + Send + 'static,
475659
{
476-
self.inner.inspect = Box::new(inspect);
477-
self
660+
self.on_event(move |event| {
661+
if let FullScanRequestEvent::SpkStarted {
662+
keychain,
663+
index,
664+
script,
665+
..
666+
} = event
667+
{
668+
inspect(keychain, index, script);
669+
}
670+
})
478671
}
479672

480673
/// Build the [`FullScanRequest`].
@@ -497,7 +690,11 @@ pub struct FullScanRequest<K, D = BlockHash> {
497690
chain_tip: Option<CheckPoint<D>>,
498691
spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = Indexed<ScriptBuf>> + Send>>,
499692
last_revealed: BTreeMap<K, u32>,
500-
inspect: Box<InspectFullScan<K>>,
693+
full_scan_progress: FullScanProgress,
694+
on_event: Box<OnFullScanEvent<K, ConfirmationBlockTime>>,
695+
/// Whether to emit partial [`TxUpdate`]s during full scan (set by
696+
/// [`FullScanRequestBuilder::on_event`]).
697+
pub emit_partial_updates: bool,
501698
}
502699

503700
impl<K, D> From<FullScanRequestBuilder<K, D>> for FullScanRequest<K, D> {
@@ -522,7 +719,9 @@ impl<K: Ord + Clone, D> FullScanRequest<K, D> {
522719
chain_tip: None,
523720
spks_by_keychain: BTreeMap::new(),
524721
last_revealed: BTreeMap::new(),
525-
inspect: Box::new(|_, _, _| ()),
722+
full_scan_progress: FullScanProgress::default(),
723+
on_event: Box::new(|_| ()),
724+
emit_partial_updates: false,
526725
},
527726
}
528727
}
@@ -572,12 +771,48 @@ impl<K: Ord + Clone, D> FullScanRequest<K, D> {
572771

573772
/// Iterate over indexed [`ScriptBuf`]s contained in this request of the given `keychain`.
574773
pub fn iter_spks(&mut self, keychain: K) -> impl Iterator<Item = Indexed<ScriptBuf>> + '_ {
575-
let spks = self.spks_by_keychain.get_mut(&keychain);
576-
let inspect = &mut self.inspect;
577774
KeychainSpkIter {
578-
keychain,
579-
spks,
580-
inspect,
775+
keychain: keychain.clone(),
776+
spks: self.spks_by_keychain.get_mut(&keychain),
777+
full_scan_progress: &mut self.full_scan_progress,
778+
on_event: &mut self.on_event,
779+
}
780+
}
781+
782+
/// Returns a sink for emitting full-scan events from chain sources.
783+
pub fn event_sink(&mut self) -> FullScanRequestEventSink<'_, K, D> {
784+
FullScanRequestEventSink { request: self }
785+
}
786+
787+
/// Emit a partial [`TxUpdate`] if [`Self::emit_partial_updates`] is enabled.
788+
pub fn try_emit_partial_update(
789+
&mut self,
790+
cursor: &mut Option<TxUpdateCursor<ConfirmationBlockTime>>,
791+
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
792+
) {
793+
if !self.emit_partial_updates {
794+
return;
795+
}
796+
let cursor = cursor.get_or_insert_with(TxUpdateCursor::new);
797+
let delta = tx_update.drain_since(cursor);
798+
if !delta.is_empty() {
799+
(self.on_event)(FullScanRequestEvent::PartialUpdate(delta));
800+
}
801+
}
802+
803+
/// Emit an anchor-only [`TxUpdate`] if [`Self::emit_partial_updates`] is enabled.
804+
pub fn try_emit_anchors_resolved(
805+
&mut self,
806+
cursor: &mut Option<TxUpdateCursor<ConfirmationBlockTime>>,
807+
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
808+
) {
809+
if !self.emit_partial_updates {
810+
return;
811+
}
812+
let cursor = cursor.get_or_insert_with(TxUpdateCursor::new);
813+
let delta = tx_update.drain_since(cursor);
814+
if !delta.is_empty() {
815+
(self.on_event)(FullScanRequestEvent::AnchorsResolved(delta));
581816
}
582817
}
583818
}
@@ -619,15 +854,26 @@ impl<K, A> FullScanResponse<K, A> {
619854
struct KeychainSpkIter<'r, K> {
620855
keychain: K,
621856
spks: Option<&'r mut Box<dyn Iterator<Item = Indexed<ScriptBuf>> + Send>>,
622-
inspect: &'r mut Box<InspectFullScan<K>>,
857+
full_scan_progress: &'r mut FullScanProgress,
858+
on_event: &'r mut Box<OnFullScanEvent<K, ConfirmationBlockTime>>,
623859
}
624860

625861
impl<K: Ord + Clone> Iterator for KeychainSpkIter<'_, K> {
626862
type Item = Indexed<ScriptBuf>;
627863

628864
fn next(&mut self) -> Option<Self::Item> {
629865
let (i, spk) = self.spks.as_mut()?.next()?;
630-
(*self.inspect)(self.keychain.clone(), i, &spk);
866+
self.full_scan_progress.keychain_spks_consumed = self
867+
.full_scan_progress
868+
.keychain_spks_consumed
869+
.saturating_add(1);
870+
let progress = self.full_scan_progress.clone();
871+
(self.on_event)(FullScanRequestEvent::SpkStarted {
872+
keychain: self.keychain.clone(),
873+
index: i,
874+
script: spk.as_script(),
875+
progress,
876+
});
631877
Some((i, spk))
632878
}
633879
}

0 commit comments

Comments
 (0)