Skip to content

Commit 1439955

Browse files
authored
fix: Stop loading headers twice into ChainState::headers (#258)
* Add test Results on current `v0.41-dev` ---- test_load_headers_from_storage::genesis_1_block stdout ---- thread 'test_load_headers_from_storage::genesis_1_block' (8727544) panicked at dash-spv/tests/header_sync_test.rs:380:5: assertion `left == right` failed: Loaded count mismatch left: 0 right: 1 ---- test_load_headers_from_storage::checkpoint_1_block stdout ---- thread 'test_load_headers_from_storage::checkpoint_1_block' (8727541) panicked at dash-spv/tests/header_sync_test.rs:381:5: assertion `left == right` failed: Chain state count mismatch left: 1 right: 2 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ---- test_load_headers_from_storage::checkpoint_60000_blocks stdout ---- thread 'test_load_headers_from_storage::checkpoint_60000_blocks' (8727542) panicked at dash-spv/tests/header_sync_test.rs:381:5: assertion `left == right` failed: Chain state count mismatch left: 60000 right: 120000 ---- test_load_headers_from_storage::genesis_60000_blocks stdout ---- thread 'test_load_headers_from_storage::genesis_60000_blocks' (8727545) panicked at dash-spv/tests/header_sync_test.rs:380:5: assertion `left == right` failed: Loaded count mismatch left: 59999 right: 60000 * Stop loading headers twice into `ChainState::headers` We currently load the headers twice into the header vector in `HeaderSyncManager::load_headers_from_storage`: - First1. within `storage.load_chain_state` - And then within `load_headers_from_storage` itself further down ``` 2025-12-04T16:05:36.717779Z INFO 🚀 Starting sync from checkpoint at height 1900000 instead of genesis (requested start height: 2200000) 2025-12-04T16:05:36.727966Z INFO ✅ Loaded 2 headers into HeaderSyncManager in 0.00s (1129 headers/sec) .... 2025-12-05T04:31:15.980982Z ERROR Sequential sync manager error handling message: Validation error: Block at height 2300000 does not match checkpoint ``` ``` 2025-12-04T16:09:37.959349Z INFO 🚀 Starting sync from checkpoint at height 1900000 instead of genesis (requested start height: 2200000) 2025-12-04T16:09:37.966703Z INFO ✅ Loaded 1 headers into HeaderSyncManager in 0.00s (862 headers/sec) ´´´
1 parent 8854643 commit 1439955

File tree

3 files changed

+56
-89
lines changed

3 files changed

+56
-89
lines changed

dash-spv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ tempfile = "3.0"
6060
tokio-test = "0.4"
6161
env_logger = "0.10"
6262
hex = "0.4"
63+
test-case = "3.3"
6364

6465
[[bin]]
6566
name = "dash-spv"

dash-spv/src/sync/headers/manager.rs

Lines changed: 9 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -102,116 +102,37 @@ impl<S: StorageManager + Send + Sync + 'static, N: NetworkManager + Send + Sync
102102

103103
/// Load headers from storage into the chain state
104104
pub async fn load_headers_from_storage(&mut self, storage: &S) -> SyncResult<u32> {
105+
let start_time = std::time::Instant::now();
106+
let mut loaded_count = 0;
107+
let mut tip_height = 0;
105108
// First, try to load the persisted chain state which may contain sync_base_height
106109
if let Ok(Some(stored_chain_state)) = storage.load_chain_state().await {
107110
tracing::info!(
108111
"Loaded chain state from storage with sync_base_height: {}",
109112
stored_chain_state.sync_base_height,
110113
);
111-
// Update our chain state with the loaded one to preserve sync_base_height
114+
// Update our chain state with the loaded one
112115
{
116+
loaded_count = stored_chain_state.headers.len();
117+
tip_height = stored_chain_state.tip_height();
113118
self.cached_sync_base_height = stored_chain_state.sync_base_height;
114119
let mut cs = self.chain_state.write().await;
115120
*cs = stored_chain_state;
116121
}
117122
}
118123

119-
// Get the current tip height from storage
120-
let tip_height = storage
121-
.get_tip_height()
122-
.await
123-
.map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?;
124-
125-
let Some(tip_height) = tip_height else {
126-
tracing::debug!("No headers found in storage");
127-
// If we're syncing from a checkpoint, this is expected
128-
if self.is_synced_from_checkpoint() {
129-
tracing::info!("No headers in storage for checkpoint sync - this is expected");
130-
return Ok(0);
131-
}
132-
return Ok(0);
133-
};
134-
135-
if tip_height == 0 && !self.is_synced_from_checkpoint() {
136-
tracing::debug!("Only genesis block in storage");
137-
return Ok(0);
138-
}
139-
140-
tracing::info!("Loading {} headers from storage into HeaderSyncManager", tip_height);
141-
let start_time = std::time::Instant::now();
142-
143-
// Load headers in batches
144-
const BATCH_SIZE: u32 = 10_000;
145-
let mut loaded_count = 0u32;
146-
147-
// For checkpoint syncs we start at the checkpoint base; otherwise we skip genesis (already present).
148-
let mut current_height = self.get_sync_base_height().max(1);
149-
150-
while current_height <= tip_height {
151-
let end_height = (current_height + BATCH_SIZE - 1).min(tip_height);
152-
153-
// Load batch from storage
154-
let headers_result = storage.load_headers(current_height..end_height + 1).await;
155-
156-
match headers_result {
157-
Ok(headers) if !headers.is_empty() => {
158-
// Add headers to chain state
159-
{
160-
let mut cs = self.chain_state.write().await;
161-
for header in headers {
162-
cs.add_header(header);
163-
loaded_count += 1;
164-
}
165-
}
166-
}
167-
Ok(_) => {
168-
// Empty headers - this can happen for checkpoint sync with minimal headers
169-
tracing::debug!(
170-
"No headers found for range {}..{} - continuing",
171-
current_height,
172-
end_height + 1
173-
);
174-
// Break out of the loop since we've reached the end of available headers
175-
break;
176-
}
177-
Err(e) => {
178-
// For checkpoint sync with only 1 header stored, this is expected
179-
if self.is_synced_from_checkpoint() && loaded_count == 0 && tip_height == 0 {
180-
tracing::info!(
181-
"No additional headers to load for checkpoint sync - this is expected"
182-
);
183-
return Ok(0);
184-
}
185-
return Err(SyncError::Storage(format!("Failed to load headers: {}", e)));
186-
}
187-
}
188-
189-
// Progress logging
190-
if loaded_count.is_multiple_of(50_000) || loaded_count == tip_height {
191-
let elapsed = start_time.elapsed();
192-
let headers_per_sec = loaded_count as f64 / elapsed.as_secs_f64();
193-
tracing::info!(
194-
"Loaded {}/{} headers ({:.0} headers/sec)",
195-
loaded_count,
196-
tip_height,
197-
headers_per_sec
198-
);
199-
}
200-
201-
current_height = end_height + 1;
202-
}
203-
204124
self.total_headers_synced = tip_height;
205125

206126
let elapsed = start_time.elapsed();
207127
tracing::info!(
208-
"✅ Loaded {} headers into HeaderSyncManager in {:.2}s ({:.0} headers/sec)",
128+
"✅ Loaded {} headers for tip height {} into HeaderSyncManager in {:.2}s ({:.0} headers/sec)",
209129
loaded_count,
130+
tip_height,
210131
elapsed.as_secs_f64(),
211132
loaded_count as f64 / elapsed.as_secs_f64()
212133
);
213134

214-
Ok(loaded_count)
135+
Ok(loaded_count as u32)
215136
}
216137

217138
/// Handle a Headers message

dash-spv/tests/header_sync_test.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::time::Duration;
55
use dash_spv::{
66
client::{ClientConfig, DashSpvClient},
77
network::PeerNetworkManager,
8-
storage::{MemoryStorageManager, StorageManager},
8+
storage::{DiskStorageManager, MemoryStorageManager, StorageManager},
9+
sync::{HeaderSyncManager, ReorgConfig},
910
types::{ChainState, ValidationMode},
1011
};
1112
use dashcore::{block::Header as BlockHeader, block::Version, Network};
@@ -14,6 +15,8 @@ use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
1415
use key_wallet_manager::wallet_manager::WalletManager;
1516
use log::{debug, info};
1617
use std::sync::Arc;
18+
use tempfile::TempDir;
19+
use test_case::test_case;
1720
use tokio::sync::RwLock;
1821

1922
#[tokio::test]
@@ -332,3 +335,45 @@ async fn test_header_storage_consistency() {
332335

333336
info!("Header storage consistency test completed");
334337
}
338+
339+
#[test_case(0, 0 ; "genesis_0_blocks")]
340+
#[test_case(0, 1 ; "genesis_1_block")]
341+
#[test_case(0, 60000 ; "genesis_60000_blocks")]
342+
#[test_case(100, 0 ; "checkpoint_0_blocks")]
343+
#[test_case(170000, 1 ; "checkpoint_1_block")]
344+
#[test_case(12345, 60000 ; "checkpoint_60000_blocks")]
345+
#[tokio::test]
346+
async fn test_load_headers_from_storage(sync_base_height: u32, header_count: usize) {
347+
// Setup: Create storage with 100 headers
348+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
349+
let mut storage = DiskStorageManager::new(temp_dir.path().to_path_buf())
350+
.await
351+
.expect("Failed to create storage");
352+
353+
let test_headers = create_test_header_chain(header_count);
354+
355+
// Store chain state
356+
let mut chain_state = ChainState::new_for_network(Network::Dash);
357+
chain_state.sync_base_height = sync_base_height;
358+
chain_state.headers = test_headers.clone();
359+
storage.store_chain_state(&chain_state).await.expect("Failed to store chain state");
360+
361+
// Create HeaderSyncManager and load headers
362+
let config = ClientConfig::new(Network::Dash);
363+
let chain_state = Arc::new(RwLock::new(ChainState::new_for_network(Network::Dash)));
364+
let mut header_sync = HeaderSyncManager::<DiskStorageManager, PeerNetworkManager>::new(
365+
&config,
366+
ReorgConfig::default(),
367+
chain_state.clone(),
368+
)
369+
.expect("Failed to create HeaderSyncManager");
370+
371+
// Load headers from storage
372+
let loaded_count =
373+
header_sync.load_headers_from_storage(&storage).await.expect("Failed to load headers");
374+
375+
let cs = chain_state.read().await;
376+
377+
assert_eq!(loaded_count as usize, header_count, "Loaded count mismatch");
378+
assert_eq!(header_count, cs.headers.len(), "Chain state count mismatch");
379+
}

0 commit comments

Comments
 (0)