Skip to content

Commit b6cdf1e

Browse files
committed
avoid re-downloading filters during SyncPhase::DownloadingFilters
1 parent c76ddbb commit b6cdf1e

File tree

5 files changed

+152
-30
lines changed

5 files changed

+152
-30
lines changed

dash-spv/src/storage/disk/filters.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,69 @@ impl DiskStorageManager {
182182
Ok(*self.cached_filter_tip_height.read().await)
183183
}
184184

185+
/// Get the highest stored compact filter height by scanning the filters directory.
186+
/// This checks which filters are actually persisted on disk, not just filter headers.
187+
///
188+
/// Returns None if no filters are stored, otherwise returns the highest height found.
189+
///
190+
/// Note: This only counts individual filter files ({height}.dat), not segment files.
191+
pub async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
192+
let filters_dir = self.base_path.join("filters");
193+
194+
// If filters directory doesn't exist, no filters are stored
195+
if !filters_dir.exists() {
196+
return Ok(None);
197+
}
198+
199+
let mut max_height: Option<u32> = None;
200+
201+
// Read directory entries
202+
let mut entries = tokio::fs::read_dir(&filters_dir).await?;
203+
204+
while let Some(entry) = entries.next_entry().await? {
205+
let path = entry.path();
206+
207+
// Skip if not a file
208+
if !path.is_file() {
209+
continue;
210+
}
211+
212+
// Check if it's a .dat file
213+
if path.extension().and_then(|e| e.to_str()) != Some("dat") {
214+
continue;
215+
}
216+
217+
// Extract height from filename (format: "{height}.dat")
218+
// Only parse if filename is PURELY numeric (not "filter_segment_0001")
219+
// This ensures we only count individual filter files, not segments
220+
let filename = match path.file_stem().and_then(|f| f.to_str()) {
221+
Some(name) if name.chars().all(|c| c.is_ascii_digit()) => name,
222+
_ => continue, // Skip non-numeric names like "filter_segment_0001"
223+
};
224+
225+
// Since filename only contains digits, this can never fail and can be optimized
226+
// but we'll keep it to ensure correctness
227+
let height: u32 = match filename.parse() {
228+
Ok(h) => h,
229+
Err(_) => continue,
230+
};
231+
232+
// Sanity check - testnet/mainnet should never exceed 2M
233+
if height > 2_000_000 {
234+
tracing::warn!(
235+
"Found suspiciously high filter file: {}.dat (height {}), ignoring",
236+
filename,
237+
height
238+
);
239+
continue;
240+
}
241+
242+
max_height = Some(max_height.map_or(height, |current| current.max(height)));
243+
}
244+
245+
Ok(max_height)
246+
}
247+
185248
/// Store a compact filter.
186249
pub async fn store_filter(&mut self, height: u32, filter: &[u8]) -> StorageResult<()> {
187250
let path = self.base_path.join(format!("filters/{}.dat", height));

dash-spv/src/storage/disk/state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,10 @@ impl StorageManager for DiskStorageManager {
553553
Self::get_filter_tip_height(self).await
554554
}
555555

556+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
557+
Self::get_stored_filter_height(self).await
558+
}
559+
556560
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> {
557561
Self::store_masternode_state(self, state).await
558562
}

dash-spv/src/storage/memory.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,15 @@ impl StorageManager for MemoryStorageManager {
258258
}
259259
}
260260

261+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>> {
262+
// For memory storage, find the highest filter in the HashMap
263+
if self.filters.is_empty() {
264+
Ok(None)
265+
} else {
266+
Ok(self.filters.keys().max().copied())
267+
}
268+
}
269+
261270
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> {
262271
self.masternode_state = Some(state.clone());
263272
Ok(())

dash-spv/src/storage/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ pub trait StorageManager: Send + Sync {
127127
/// Get the current filter tip blockchain height.
128128
async fn get_filter_tip_height(&self) -> StorageResult<Option<u32>>;
129129

130+
/// Get the highest stored compact filter height by checking which filters are persisted.
131+
/// This is distinct from filter header tip - it shows which filters are actually downloaded.
132+
async fn get_stored_filter_height(&self) -> StorageResult<Option<u32>>;
133+
130134
/// Store masternode state.
131135
async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()>;
132136

dash-spv/src/sync/phase_execution.rs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -149,42 +149,84 @@ impl<
149149
.map_err(|e| SyncError::Storage(format!("Failed to get filter tip: {}", e)))?
150150
.unwrap_or(0);
151151

152-
if filter_header_tip > 0 {
153-
// Download all filters for complete blockchain history
154-
// This ensures the wallet can find transactions from any point in history
155-
let start_height = self.header_sync.get_sync_base_height().max(1);
156-
let count = filter_header_tip - start_height + 1;
152+
// No filter headers available, skip to next phase
153+
if filter_header_tip == 0 {
154+
self.transition_to_next_phase(storage, network, "No filter headers available")
155+
.await?;
156+
return Ok(());
157+
}
157158

158-
tracing::info!(
159-
"Starting filter download from height {} to {} ({} filters)",
160-
start_height,
161-
filter_header_tip,
162-
count
163-
);
159+
tracing::info!(
160+
"🔍 Filter download check: filter_header_tip={}, sync_base_height={}",
161+
filter_header_tip,
162+
self.header_sync.get_sync_base_height()
163+
);
164164

165-
// Update the phase to track the expected total
166-
if let SyncPhase::DownloadingFilters {
167-
total_filters,
168-
..
169-
} = &mut self.current_phase
170-
{
171-
*total_filters = count;
172-
}
165+
// Check what filters are already stored to resume download
166+
let stored_filter_height =
167+
storage.get_stored_filter_height().await.map_err(|e| {
168+
SyncError::Storage(format!("Failed to get stored filter height: {}", e))
169+
})?;
173170

174-
// Use the filter sync manager to download filters
175-
self.filter_sync
176-
.sync_filters_with_flow_control(
177-
network,
178-
storage,
179-
Some(start_height),
180-
Some(count),
181-
)
182-
.await?;
171+
tracing::info!(
172+
"🔍 Stored filter height from disk scan: {:?}",
173+
stored_filter_height
174+
);
175+
176+
// Resume from the next height after the last stored filter
177+
// If no filters are stored, start from sync_base_height or 1
178+
let start_height = if let Some(stored_height) = stored_filter_height {
179+
tracing::info!(
180+
"Found stored filters up to height {}, resuming from height {}",
181+
stored_height,
182+
stored_height + 1
183+
);
184+
stored_height + 1
183185
} else {
184-
// No filter headers available, skip to next phase
185-
self.transition_to_next_phase(storage, network, "No filter headers available")
186+
let base_height = self.header_sync.get_sync_base_height().max(1);
187+
tracing::info!("No stored filters found, starting from height {}", base_height);
188+
base_height
189+
};
190+
191+
// If we've already downloaded all filters, skip to next phase
192+
if start_height > filter_header_tip {
193+
tracing::info!(
194+
"All filters already downloaded (stored up to {}, tip is {}), skipping to next phase",
195+
start_height - 1,
196+
filter_header_tip
197+
);
198+
self.transition_to_next_phase(storage, network, "Filters already synced")
186199
.await?;
200+
return Ok(());
201+
}
202+
203+
let count = filter_header_tip - start_height + 1;
204+
205+
tracing::info!(
206+
"Starting filter download from height {} to {} ({} filters)",
207+
start_height,
208+
filter_header_tip,
209+
count
210+
);
211+
212+
// Update the phase to track the expected total
213+
if let SyncPhase::DownloadingFilters {
214+
total_filters,
215+
..
216+
} = &mut self.current_phase
217+
{
218+
*total_filters = count;
187219
}
220+
221+
// Use the filter sync manager to download filters
222+
self.filter_sync
223+
.sync_filters_with_flow_control(
224+
network,
225+
storage,
226+
Some(start_height),
227+
Some(count),
228+
)
229+
.await?;
188230
}
189231

190232
SyncPhase::DownloadingBlocks {

0 commit comments

Comments
 (0)