Skip to content

Commit ab4aef8

Browse files
authored
feat: persist best ChainLock to disk via MetadataStorage (#419)
Add `M: MetadataStorage` generic to `ChainLockManager` so the best validated ChainLock can be saved to disk on every update and restored on initialization.
1 parent c71c254 commit ab4aef8

File tree

7 files changed

+245
-24
lines changed

7 files changed

+245
-24
lines changed

dash-spv/src/client/core.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::mempool_filter::MempoolFilter;
1818
use crate::network::NetworkManager;
1919
use crate::storage::{
2020
PersistentBlockHeaderStorage, PersistentBlockStorage, PersistentFilterHeaderStorage,
21-
PersistentFilterStorage, StorageManager,
21+
PersistentFilterStorage, PersistentMetadataStorage, StorageManager,
2222
};
2323
use crate::sync::SyncCoordinator;
2424
use crate::types::MempoolState;
@@ -107,6 +107,7 @@ pub struct DashSpvClient<W: WalletInterface, N: NetworkManager, S: StorageManage
107107
PersistentFilterHeaderStorage,
108108
PersistentFilterStorage,
109109
PersistentBlockStorage,
110+
PersistentMetadataStorage,
110111
W,
111112
>,
112113
pub(super) running: Arc<RwLock<bool>>,

dash-spv/src/client/lifecycle.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::mempool_filter::MempoolFilter;
1919
use crate::network::NetworkManager;
2020
use crate::storage::{
2121
PersistentBlockHeaderStorage, PersistentBlockStorage, PersistentFilterHeaderStorage,
22-
PersistentFilterStorage, StorageManager,
22+
PersistentFilterStorage, PersistentMetadataStorage, StorageManager,
2323
};
2424
use crate::sync::{
2525
BlockHeadersManager, BlocksManager, ChainLockManager, FilterHeadersManager, FiltersManager,
@@ -57,6 +57,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
5757
PersistentFilterHeaderStorage,
5858
PersistentFilterStorage,
5959
PersistentBlockStorage,
60+
PersistentMetadataStorage,
6061
W,
6162
> = Managers::default();
6263

@@ -94,6 +95,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
9495
));
9596
managers.chainlock = Some(ChainLockManager::new(
9697
storage.block_headers(),
98+
storage.metadata(),
9799
masternode_list_engine.clone(),
98100
));
99101
managers.instantsend = Some(InstantSendManager::new(masternode_list_engine.clone()));

dash-spv/src/storage/metadata.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
};
99

1010
#[async_trait]
11-
pub trait MetadataStorage {
11+
pub trait MetadataStorage: Send + Sync + 'static {
1212
async fn store_metadata(&mut self, key: &str, value: &[u8]) -> StorageResult<()>;
1313

1414
async fn load_metadata(&self, key: &str) -> StorageResult<Option<Vec<u8>>>;

dash-spv/src/storage/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use tokio::sync::RwLock;
2626

2727
use crate::error::StorageResult;
2828
use crate::storage::lockfile::LockFile;
29-
use crate::storage::metadata::PersistentMetadataStorage;
3029
use crate::storage::transactions::PersistentTransactionStorage;
3130
use crate::types::{HashedBlock, HashedBlockHeader, MempoolState, UnconfirmedTransaction};
3231
use crate::ClientConfig;
@@ -38,7 +37,7 @@ pub use crate::storage::blocks::{BlockStorage, PersistentBlockStorage};
3837
pub use crate::storage::filter_headers::{FilterHeaderStorage, PersistentFilterHeaderStorage};
3938
pub use crate::storage::filters::{FilterStorage, PersistentFilterStorage};
4039
pub use crate::storage::masternode::{MasternodeStateStorage, PersistentMasternodeStateStorage};
41-
pub use crate::storage::metadata::MetadataStorage;
40+
pub use crate::storage::metadata::{MetadataStorage, PersistentMetadataStorage};
4241
pub use crate::storage::peers::{PeerStorage, PersistentPeerStorage};
4342
pub use crate::storage::transactions::TransactionStorage;
4443

@@ -83,6 +82,9 @@ pub trait StorageManager:
8382

8483
/// Returns shared access to the block storage.
8584
fn blocks(&self) -> Arc<RwLock<PersistentBlockStorage>>;
85+
86+
/// Returns shared access to the metadata storage.
87+
fn metadata(&self) -> Arc<RwLock<PersistentMetadataStorage>>;
8688
}
8789

8890
/// Disk-based storage manager with segmented files and async background saving.
@@ -274,6 +276,10 @@ impl StorageManager for DiskStorageManager {
274276
fn blocks(&self) -> Arc<RwLock<PersistentBlockStorage>> {
275277
Arc::clone(&self.blocks)
276278
}
279+
280+
fn metadata(&self) -> Arc<RwLock<PersistentMetadataStorage>> {
281+
Arc::clone(&self.metadata)
282+
}
277283
}
278284

279285
#[async_trait]

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

Lines changed: 206 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,26 @@ use std::collections::HashSet;
1414
use tokio::sync::RwLock;
1515

1616
use crate::error::SyncResult;
17-
use crate::storage::BlockHeaderStorage;
17+
use crate::storage::{BlockHeaderStorage, MetadataStorage};
1818
use crate::sync::{ChainLockProgress, SyncEvent};
1919

20+
/// Metadata key for persisting the best validated ChainLock.
21+
const BEST_CHAINLOCK_KEY: &str = "best_chainlock";
22+
2023
/// ChainLock manager for the parallel sync coordinator.
2124
///
2225
/// This manager:
2326
/// - Subscribes to CLSig messages from the network
2427
/// - Validates ChainLocks only after masternode sync is complete
2528
/// - Tracks only the best (highest) validated ChainLock
2629
/// - Emits ChainLockReceived events
27-
pub struct ChainLockManager<H: BlockHeaderStorage> {
30+
pub struct ChainLockManager<H: BlockHeaderStorage, M: MetadataStorage> {
2831
/// Current progress of the manager.
2932
pub(super) progress: ChainLockProgress,
3033
/// Block header storage for hash verification.
3134
header_storage: Arc<RwLock<H>>,
35+
/// Metadata storage for persisting the best chainlock.
36+
metadata_storage: Arc<RwLock<M>>,
3237
/// Masternode engine for BLS signature validation.
3338
masternode_engine: Arc<RwLock<MasternodeListEngine>>,
3439
/// The best (highest height) validated ChainLock.
@@ -39,15 +44,17 @@ pub struct ChainLockManager<H: BlockHeaderStorage> {
3944
masternode_ready: bool,
4045
}
4146

42-
impl<H: BlockHeaderStorage> ChainLockManager<H> {
47+
impl<H: BlockHeaderStorage, M: MetadataStorage> ChainLockManager<H, M> {
4348
/// Create a new ChainLock manager.
4449
pub fn new(
4550
header_storage: Arc<RwLock<H>>,
51+
metadata_storage: Arc<RwLock<M>>,
4652
masternode_engine: Arc<RwLock<MasternodeListEngine>>,
4753
) -> Self {
4854
Self {
4955
progress: ChainLockProgress::default(),
5056
header_storage,
57+
metadata_storage,
5158
masternode_engine,
5259
best_chainlock: None,
5360
requested_chainlocks: HashSet::new(),
@@ -107,8 +114,9 @@ impl<H: BlockHeaderStorage> ChainLockManager<H> {
107114
self.progress.add_valid(1);
108115
self.progress.update_best_validated_height(height);
109116

110-
// Update best ChainLock
117+
// Update best ChainLock and persist to storage
111118
self.best_chainlock = Some(chainlock.clone());
119+
self.save_best_chainlock().await;
112120
} else {
113121
self.progress.add_invalid(1);
114122
}
@@ -119,6 +127,48 @@ impl<H: BlockHeaderStorage> ChainLockManager<H> {
119127
}])
120128
}
121129

130+
/// Persist the best chainlock to metadata storage.
131+
async fn save_best_chainlock(&self) {
132+
let Some(chainlock) = &self.best_chainlock else {
133+
return;
134+
};
135+
match serde_json::to_vec(chainlock) {
136+
Ok(bytes) => {
137+
let mut storage = self.metadata_storage.write().await;
138+
if let Err(e) = storage.store_metadata(BEST_CHAINLOCK_KEY, &bytes).await {
139+
tracing::warn!("Failed to persist best chainlock: {}", e);
140+
}
141+
}
142+
Err(e) => {
143+
tracing::warn!("Failed to serialize best chainlock: {}", e);
144+
}
145+
}
146+
}
147+
148+
/// Load the best chainlock from metadata storage and restore progress.
149+
pub(super) async fn load_best_chainlock(&mut self) {
150+
let storage = self.metadata_storage.read().await;
151+
match storage.load_metadata(BEST_CHAINLOCK_KEY).await {
152+
Ok(Some(bytes)) => match serde_json::from_slice::<ChainLock>(&bytes) {
153+
Ok(chainlock) => {
154+
let height = chainlock.block_height;
155+
tracing::info!("Restored persisted ChainLock at height {}", height);
156+
self.progress.update_best_validated_height(height);
157+
self.best_chainlock = Some(chainlock);
158+
}
159+
Err(e) => {
160+
tracing::warn!("Failed to deserialize persisted chainlock: {}", e);
161+
}
162+
},
163+
Ok(None) => {
164+
tracing::debug!("No persisted chainlock found (fresh start)");
165+
}
166+
Err(e) => {
167+
tracing::warn!("Failed to load persisted chainlock: {}", e);
168+
}
169+
}
170+
}
171+
122172
/// Verify that the ChainLock block hash matches our stored header.
123173
/// Returns true if the hash matches or we don't have the header yet.
124174
/// Returns false if we have the header and the hash doesn't match.
@@ -177,7 +227,7 @@ impl<H: BlockHeaderStorage> ChainLockManager<H> {
177227
}
178228
}
179229

180-
impl<H: BlockHeaderStorage> std::fmt::Debug for ChainLockManager<H> {
230+
impl<H: BlockHeaderStorage, M: MetadataStorage> std::fmt::Debug for ChainLockManager<H, M> {
181231
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182232
f.debug_struct("ChainLockManager")
183233
.field("progress", &self.progress)
@@ -191,20 +241,31 @@ impl<H: BlockHeaderStorage> std::fmt::Debug for ChainLockManager<H> {
191241
mod tests {
192242
use super::*;
193243
use crate::network::MessageType;
194-
use crate::storage::{DiskStorageManager, PersistentBlockHeaderStorage, StorageManager};
244+
use crate::storage::{
245+
DiskStorageManager, PersistentBlockHeaderStorage, PersistentMetadataStorage, StorageManager,
246+
};
195247
use crate::sync::{ManagerIdentifier, SyncManager, SyncManagerProgress, SyncState};
196248
use crate::Network;
197249
use dashcore::bls_sig_utils::BLSSignature;
198250
use dashcore::hashes::Hash;
199251
use dashcore::BlockHash;
200252

201-
type TestChainLockManager = ChainLockManager<PersistentBlockHeaderStorage>;
253+
type TestChainLockManager =
254+
ChainLockManager<PersistentBlockHeaderStorage, PersistentMetadataStorage>;
202255

203256
async fn create_test_manager() -> TestChainLockManager {
204257
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
205258
let engine =
206259
Arc::new(RwLock::new(MasternodeListEngine::default_for_network(Network::Testnet)));
207-
ChainLockManager::new(storage.block_headers(), engine)
260+
ChainLockManager::new(storage.block_headers(), storage.metadata(), engine)
261+
}
262+
263+
async fn create_test_manager_with_storage(
264+
storage: &DiskStorageManager,
265+
) -> TestChainLockManager {
266+
let engine =
267+
Arc::new(RwLock::new(MasternodeListEngine::default_for_network(Network::Testnet)));
268+
ChainLockManager::new(storage.block_headers(), storage.metadata(), engine)
208269
}
209270

210271
fn create_test_chainlock(height: u32) -> ChainLock {
@@ -307,4 +368,141 @@ mod tests {
307368
assert!(manager.is_block_chainlocked(500));
308369
assert!(!manager.is_block_chainlocked(501));
309370
}
371+
372+
#[tokio::test]
373+
async fn test_load_from_empty_storage_returns_none() {
374+
let mut manager = create_test_manager().await;
375+
376+
manager.load_best_chainlock().await;
377+
378+
assert!(manager.best_chainlock().is_none());
379+
assert_eq!(manager.progress.best_validated_height(), 0);
380+
}
381+
382+
#[tokio::test]
383+
async fn test_save_and_load_chainlock_round_trip() {
384+
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
385+
let chainlock = create_test_chainlock(42000);
386+
387+
// Save a chainlock via the first manager
388+
{
389+
let mut manager = create_test_manager_with_storage(&storage).await;
390+
manager.best_chainlock = Some(chainlock.clone());
391+
manager.save_best_chainlock().await;
392+
}
393+
394+
// Load it back via a fresh manager sharing the same storage
395+
{
396+
let mut manager = create_test_manager_with_storage(&storage).await;
397+
assert!(manager.best_chainlock().is_none());
398+
399+
manager.load_best_chainlock().await;
400+
401+
let restored = manager.best_chainlock().expect("chainlock should be restored");
402+
assert_eq!(restored.block_height, 42000);
403+
assert_eq!(restored.block_hash, chainlock.block_hash);
404+
assert_eq!(restored.signature, chainlock.signature);
405+
assert_eq!(manager.progress.best_validated_height(), 42000);
406+
}
407+
}
408+
409+
#[tokio::test]
410+
async fn test_initialize_restores_persisted_chainlock() {
411+
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
412+
let chainlock = create_test_chainlock(99999);
413+
414+
// Persist a chainlock directly via metadata storage
415+
{
416+
let bytes = serde_json::to_vec(&chainlock).unwrap();
417+
let meta_storage = storage.metadata();
418+
let mut meta = meta_storage.write().await;
419+
meta.store_metadata(BEST_CHAINLOCK_KEY, &bytes).await.unwrap();
420+
}
421+
422+
// Create a new manager and call initialize (the SyncManager trait method)
423+
let mut manager = create_test_manager_with_storage(&storage).await;
424+
manager.initialize().await.unwrap();
425+
426+
let restored =
427+
manager.best_chainlock().expect("chainlock should be restored after initialize");
428+
assert_eq!(restored.block_height, 99999);
429+
assert_eq!(manager.progress.best_validated_height(), 99999);
430+
assert_eq!(manager.state(), SyncState::WaitingForConnections);
431+
}
432+
433+
#[tokio::test]
434+
async fn test_process_chainlock_persists_on_validation() {
435+
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
436+
let mut manager = create_test_manager_with_storage(&storage).await;
437+
438+
// Without masternode ready, chainlocks are not validated and not persisted
439+
let chainlock = create_test_chainlock(500);
440+
manager.process_chainlock(&chainlock).await.unwrap();
441+
assert!(manager.best_chainlock().is_none());
442+
443+
// Verify nothing was persisted
444+
{
445+
let meta_storage = storage.metadata();
446+
let meta = meta_storage.read().await;
447+
let loaded = meta.load_metadata(BEST_CHAINLOCK_KEY).await.unwrap();
448+
assert!(loaded.is_none());
449+
}
450+
}
451+
452+
#[tokio::test]
453+
async fn test_save_overwrites_previous_chainlock() {
454+
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
455+
456+
// Save first chainlock
457+
{
458+
let mut manager = create_test_manager_with_storage(&storage).await;
459+
manager.best_chainlock = Some(create_test_chainlock(100));
460+
manager.save_best_chainlock().await;
461+
}
462+
463+
// Save a higher chainlock
464+
{
465+
let mut manager = create_test_manager_with_storage(&storage).await;
466+
manager.best_chainlock = Some(create_test_chainlock(200));
467+
manager.save_best_chainlock().await;
468+
}
469+
470+
// Load and verify only the latest is stored
471+
{
472+
let mut manager = create_test_manager_with_storage(&storage).await;
473+
manager.load_best_chainlock().await;
474+
475+
let restored = manager.best_chainlock().expect("chainlock should be restored");
476+
assert_eq!(restored.block_height, 200);
477+
}
478+
}
479+
480+
#[tokio::test]
481+
async fn test_lower_chainlock_rejected_after_load() {
482+
let storage = DiskStorageManager::with_temp_dir().await.unwrap();
483+
484+
// Save chainlock at height 200
485+
{
486+
let mut manager = create_test_manager_with_storage(&storage).await;
487+
manager.best_chainlock = Some(create_test_chainlock(200));
488+
manager.save_best_chainlock().await;
489+
}
490+
491+
// Load and try to process a lower chainlock
492+
{
493+
let mut manager = create_test_manager_with_storage(&storage).await;
494+
manager.load_best_chainlock().await;
495+
496+
// Try to process a lower chainlock
497+
let lower_chainlock = create_test_chainlock(100);
498+
let events = manager.process_chainlock(&lower_chainlock).await.unwrap();
499+
500+
// Should be rejected (no events)
501+
assert_eq!(events.len(), 0);
502+
503+
// Best should still be 200
504+
let best = manager.best_chainlock().expect("should have best chainlock");
505+
assert_eq!(best.block_height, 200);
506+
}
507+
}
310508
}

0 commit comments

Comments
 (0)