From b9dd11a56d8be5b547c693945cc9ca9e17e85820 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 13 Feb 2026 05:31:04 +0100 Subject: [PATCH 1/2] fix: separate filter scan height from synced height Introduces `filter_committed_height` on `WalletInterface`, separate from `synced_height`. `process_block` updates `synced_height` per-block (needed for balance/maturity calculations), but `FiltersManager` uses `filter_committed_height` for restart recovery. This prevents a bug where per-block `synced_height` advances past uncommitted filter batches, causing the rescan on restart to skip heights that need rescanning for newly discovered addresses. It's not the best solution I think, hence I added the TODO for now. I will look into this at some point later. --- dash-spv/src/sync/filters/manager.rs | 20 +++++++++++-------- dash-spv/src/sync/filters/sync_manager.rs | 4 ++-- key-wallet-manager/src/wallet_interface.rs | 15 ++++++++++++++ key-wallet-manager/src/wallet_manager/mod.rs | 3 +++ .../src/wallet_manager/process_block.rs | 11 ++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/dash-spv/src/sync/filters/manager.rs b/dash-spv/src/sync/filters/manager.rs index ce13c2c9d..db5cbc329 100644 --- a/dash-spv/src/sync/filters/manager.rs +++ b/dash-spv/src/sync/filters/manager.rs @@ -132,10 +132,11 @@ impl SyncResult> { self.set_state(SyncState::Syncing); - // Get wallet state - let (wallet_birth_height, wallet_synced_height) = { + // Use filter_committed_height for restart recovery instead of + // synced_height, which advances per-block and may exceed committed scan progress. + let (wallet_birth_height, wallet_committed_height) = { let wallet = self.wallet.read().await; - (wallet.earliest_required_height().await, wallet.synced_height()) + (wallet.earliest_required_height().await, wallet.filter_committed_height()) }; // Get stored filters tip @@ -147,8 +148,8 @@ impl 0 { - wallet_birth_height.max(wallet_synced_height + 1) + let scan_start = if wallet_committed_height > 0 { + wallet_birth_height.max(wallet_committed_height + 1) } else { wallet_birth_height } @@ -498,9 +499,12 @@ impl self.committed_height { + self.committed_height = end; + self.wallet.write().await.update_filter_committed_height(end); + } + self.processing_height = end + 1; tracing::info!( "Committed batch {}-{}, committed_height now {}", diff --git a/dash-spv/src/sync/filters/sync_manager.rs b/dash-spv/src/sync/filters/sync_manager.rs index 44b589e69..c48d197d7 100644 --- a/dash-spv/src/sync/filters/sync_manager.rs +++ b/dash-spv/src/sync/filters/sync_manager.rs @@ -39,10 +39,10 @@ impl< async fn initialize(&mut self) -> SyncResult<()> { let wallet = self.wallet.read().await; - let synced_height = wallet.synced_height(); + let committed_height = wallet.filter_committed_height(); drop(wallet); - self.progress.update_current_height(synced_height); + self.progress.update_current_height(committed_height); self.set_state(SyncState::WaitingForConnections); tracing::info!( diff --git a/key-wallet-manager/src/wallet_interface.rs b/key-wallet-manager/src/wallet_interface.rs index 2cabc2dfc..02b04e3ee 100644 --- a/key-wallet-manager/src/wallet_interface.rs +++ b/key-wallet-manager/src/wallet_interface.rs @@ -75,6 +75,21 @@ pub trait WalletInterface: Send + Sync + 'static { /// Update the wallet's synced height. This also triggers balance updates. fn update_synced_height(&mut self, height: CoreBlockHeight); + /// Return the height at which filter scanning was last committed. + /// Defaults to `synced_height()` for implementations that don't separate these concepts. + // TODO: This can probably somehow be combined with synced_height(). + fn filter_committed_height(&self) -> CoreBlockHeight { + self.synced_height() + } + + /// Update the filter committed height. Call when a height is fully processed + /// (including any rescans for newly discovered addresses). + fn update_filter_committed_height(&mut self, height: CoreBlockHeight) { + if height > self.synced_height() { + self.update_synced_height(height); + } + } + /// Provide a human-readable description of the wallet implementation. /// /// Implementations are encouraged to include high-level state such as the diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index 70254c476..7117a9350 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -80,6 +80,8 @@ pub struct WalletManager { network: Network, /// Last fully processed block height. synced_height: CoreBlockHeight, + /// Height at which filter scanning was last committed. + filter_committed_height: CoreBlockHeight, /// Immutable wallets indexed by wallet ID wallets: BTreeMap, /// Mutable wallet info indexed by wallet ID @@ -95,6 +97,7 @@ impl WalletManager { Self { network, synced_height: 0, + filter_committed_height: 0, wallets: BTreeMap::new(), wallet_infos: BTreeMap::new(), #[cfg(feature = "std")] diff --git a/key-wallet-manager/src/wallet_manager/process_block.rs b/key-wallet-manager/src/wallet_manager/process_block.rs index 32b074977..052709503 100644 --- a/key-wallet-manager/src/wallet_manager/process_block.rs +++ b/key-wallet-manager/src/wallet_manager/process_block.rs @@ -133,6 +133,17 @@ impl WalletInterface for WalletM } } + fn filter_committed_height(&self) -> CoreBlockHeight { + self.filter_committed_height + } + + fn update_filter_committed_height(&mut self, height: CoreBlockHeight) { + self.filter_committed_height = height; + if height > self.synced_height { + self.update_synced_height(height); + } + } + async fn describe(&self) -> String { let wallet_count = self.wallet_infos.len(); if wallet_count == 0 { From f563e33f99a6fa005a8427ab4460c3c9c57c05c4 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 17 Feb 2026 14:02:34 +0100 Subject: [PATCH 2/2] fix: use committed_height for sync completion checks Sync completion checks in `start_download` and `check_sync_complete` used `progress.current_height()` which tracks stored filter height, not the height committed to wallet. This could lead to sync complete before all batches were committed, causing skipped heights on restart. --- dash-spv/src/sync/filters/manager.rs | 14 +++++++------- dash-spv/src/sync/filters/sync_manager.rs | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dash-spv/src/sync/filters/manager.rs b/dash-spv/src/sync/filters/manager.rs index db5cbc329..494845297 100644 --- a/dash-spv/src/sync/filters/manager.rs +++ b/dash-spv/src/sync/filters/manager.rs @@ -65,7 +65,7 @@ pub struct FiltersManager< /// Active batches being processed (keyed by start_height). pub(super) active_batches: BTreeMap, /// Height that has been committed to wallet (all blocks up to this height processed). - committed_height: u32, + pub(super) committed_height: u32, /// Current block height being processed (for progress tracking). processing_height: u32, /// Blocks remaining that need to be processed. @@ -159,11 +159,11 @@ impl self.progress.filter_header_tip_height() { // Only emit FiltersSyncComplete if we've also reached the chain tip // This prevents premature sync complete while filter headers are still syncing - if self.progress.current_height() >= self.progress.target_height() { + if self.committed_height >= self.progress.target_height() { self.set_state(SyncState::Synced); tracing::info!("Filters already synced to {}", self.progress.target_height()); return Ok(vec![SyncEvent::FiltersSyncComplete { - tip_height: self.progress.current_height(), + tip_height: self.committed_height, }]); } // Caught up to available filter headers but chain tip not reached yet @@ -423,15 +423,15 @@ impl= self.progress.filter_header_tip_height() - && self.progress.current_height() >= self.progress.target_height() + && self.committed_height >= self.progress.filter_header_tip_height() + && self.committed_height >= self.progress.target_height() { if self.state() == SyncState::Syncing { self.set_state(SyncState::Synced); } - tracing::info!("Filter sync complete at height {}", self.progress.current_height()); + tracing::info!("Filter sync complete at height {}", self.committed_height); events.push(SyncEvent::FiltersSyncComplete { - tip_height: self.progress.current_height(), + tip_height: self.committed_height, }); } diff --git a/dash-spv/src/sync/filters/sync_manager.rs b/dash-spv/src/sync/filters/sync_manager.rs index c48d197d7..6b51d1198 100644 --- a/dash-spv/src/sync/filters/sync_manager.rs +++ b/dash-spv/src/sync/filters/sync_manager.rs @@ -42,6 +42,7 @@ impl< let committed_height = wallet.filter_committed_height(); drop(wallet); + self.committed_height = committed_height; self.progress.update_current_height(committed_height); self.set_state(SyncState::WaitingForConnections);