@@ -14,21 +14,26 @@ use std::collections::HashSet;
1414use tokio:: sync:: RwLock ;
1515
1616use crate :: error:: SyncResult ;
17- use crate :: storage:: BlockHeaderStorage ;
17+ use crate :: storage:: { BlockHeaderStorage , MetadataStorage } ;
1818use 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> {
191241mod 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