From 57645fa30999dcdfc12c636382f193b4b3381929 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 27 Aug 2026 14:27:57 +0000 Subject: [PATCH 1/3] test(dash-spv): assert the restart test's storage, not just its progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_masternode_list_sync_with_restart` compared masternode sync progress either side of a restart. A from-scratch network re-sync produces the same progress as a restored one, so the test passed while the list was being rebuilt from nothing every time (dashpay/rust-dashcore#988). It now looks at the disk. After the first session's clean shutdown every directory that session earned must hold a file, and across the restart no directory may disappear or lose files. Fails as written: the first session builds four masternodes and writes no `masternodestate/`, while `block_headers/`, `filter_headers/`, `metadata/` and `peers/` all persist through the same shutdown to the same directory — so the storage layer and the shutdown are ruled out as causes. `filters/` and `blocks/` are left out of the must-hold set on purpose: the client stops as soon as the masternode phase reports `Synced`, which is before the filter phase leaves `WaitForEvents`, so they are legitimately empty here. The no-shrink check still covers them. The engine is read before the shutdown and the count carried into the failure message, so the assertion cannot be satisfied by a session that synced nothing — which is the shape dashpay/rust-dashcore#954 produces. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS --- dash-spv/tests/dashd_masternode/helpers.rs | 91 +++++++++++++++++++ dash-spv/tests/dashd_masternode/tests_sync.rs | 30 +++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/dash-spv/tests/dashd_masternode/helpers.rs b/dash-spv/tests/dashd_masternode/helpers.rs index aa27b7a06..8166df9bb 100644 --- a/dash-spv/tests/dashd_masternode/helpers.rs +++ b/dash-spv/tests/dashd_masternode/helpers.rs @@ -1,3 +1,6 @@ +use std::collections::BTreeMap; +use std::path::Path; + use dash_spv::sync::{MasternodesProgress, SyncEvent, SyncProgress, SyncState}; use dashcore::ephemerealdata::instant_lock::InstantLock; use dashcore::sml::llmq_entry_verification::LLMQEntryVerificationStatus; @@ -14,6 +17,94 @@ use super::setup::{TestContext, SYNC_TIMEOUT}; /// Mine a DKG cycle and wait for the SPV to surface a `MasternodeStateUpdated` /// event above `baseline_height`. +/// Files held under each immediate subdirectory of the storage root, keyed by +/// directory name. +/// +/// A sync writes into these and never removes a whole class of state, so across +/// a restart every directory must still be there and hold at least as much — +/// see [`assert_storage_did_not_shrink`]. +pub(super) fn storage_snapshot(root: &Path) -> BTreeMap { + let mut counts = BTreeMap::new(); + let Ok(entries) = std::fs::read_dir(root) else { + return counts; + }; + for entry in entries.flatten() { + if !entry.path().is_dir() { + continue; + } + let files = walkdir_count(&entry.path()); + counts.insert(entry.file_name().to_string_lossy().into_owned(), files); + } + counts +} + +fn walkdir_count(dir: &Path) -> usize { + let Ok(entries) = std::fs::read_dir(dir) else { + return 0; + }; + entries + .flatten() + .map(|e| { + let path = e.path(); + if path.is_dir() { + walkdir_count(&path) + } else { + 1 + } + }) + .sum() +} + +/// Directories that must hold state once this test's first session has run, and +/// why. `filters` and `blocks` are deliberately absent: the client is stopped +/// as soon as the masternode phase reports `Synced`, which is before the filter +/// phase leaves `WaitForEvents`, so those stay legitimately empty here. +pub(super) const EXPECTED_STORAGE: &[(&str, &str)] = &[ + ("block_headers", "headers synced to the tip"), + ("filter_headers", "filter headers synced to the tip"), + ("metadata", "sync checkpoints"), + ("peers", "peer set and reputations"), + ("masternodestate", "the masternode list this session built"), +]; + +/// Assert every directory in [`EXPECTED_STORAGE`] exists and holds at least one +/// file, reporting all of them at once rather than the first to fail. +pub(super) fn assert_storage_persisted(snapshot: &BTreeMap, what: &str) { + let missing: Vec = EXPECTED_STORAGE + .iter() + .filter(|(dir, _)| snapshot.get(*dir).is_none_or(|files| *files == 0)) + .map(|(dir, why)| format!(" {dir}/ — {why}")) + .collect(); + assert!( + missing.is_empty(), + "{what}: {} storage director{} empty or absent after a clean shutdown:\n{}\n\nstorage holds {snapshot:?}", + missing.len(), + if missing.len() == 1 { "y is" } else { "ies are" }, + missing.join("\n"), + ); +} + +/// Every directory present before a restart must still be present after, with +/// at least as many files. A directory that vanishes or shrinks means a restart +/// threw away state that the previous session had already earned. +pub(super) fn assert_storage_did_not_shrink( + before: &BTreeMap, + after: &BTreeMap, + what: &str, +) { + for (dir, before_count) in before { + match after.get(dir) { + None => panic!( + "{what}: storage directory {dir:?} disappeared across the restart\n before: {before:?}\n after: {after:?}" + ), + Some(after_count) if after_count < before_count => panic!( + "{what}: storage directory {dir:?} shrank across the restart, {before_count} -> {after_count}\n before: {before:?}\n after: {after:?}" + ), + Some(_) => {} + } + } +} + pub(super) async fn mine_dkg_cycle_and_wait( ctx: &mut TestContext, sync_event_receiver: &mut broadcast::Receiver, diff --git a/dash-spv/tests/dashd_masternode/tests_sync.rs b/dash-spv/tests/dashd_masternode/tests_sync.rs index 805e469ad..b38d28783 100644 --- a/dash-spv/tests/dashd_masternode/tests_sync.rs +++ b/dash-spv/tests/dashd_masternode/tests_sync.rs @@ -10,8 +10,9 @@ use dashcore::sml::llmq_entry_verification::LLMQEntryVerificationStatus; use dashcore::sml::llmq_type::LLMQType; use super::helpers::{ - assert_all_rotated_quorums_verified, wait_for_chainlock_height_at_least, - wait_for_masternode_sync, wait_for_mn_state_event, wait_for_mn_state_event_above, + assert_all_rotated_quorums_verified, assert_storage_did_not_shrink, assert_storage_persisted, + storage_snapshot, wait_for_chainlock_height_at_least, wait_for_masternode_sync, + wait_for_mn_state_event, wait_for_mn_state_event_above, wait_for_mn_state_with_stored_cycle_above, }; use super::setup::{ @@ -103,9 +104,29 @@ async fn test_masternode_list_sync_with_restart() { let first_mn_progress = wait_for_masternode_sync(&mut client_handle.progress_receiver, SYNC_TIMEOUT).await; let first_height = first_mn_progress.current_height(); + + // Control: the first session really built a list, so the persistence + // assertion below cannot be satisfied by a client that synced nothing. + let first_masternodes = { + let engine = client_handle.engine.read().await; + engine.masternode_lists.values().map(|list| list.masternodes.len()).max().unwrap_or(0) + }; + assert!( + first_masternodes > 0, + "the first session must have a masternode list before its persistence can be tested" + ); + client_handle.stop().await; drop(client_handle); + // What the first session earned and wrote down. A clean shutdown of a + // fully-synced client must leave every sync phase's state on disk. + let after_first = storage_snapshot(ctx.storage_path()); + assert_storage_persisted( + &after_first, + &format!("after a first session that built {first_masternodes} masternode(s)"), + ); + // Restart with same storage tracing::info!("=== Restarting with same storage ==="); let mut client_handle = create_and_start_client(&config, Arc::clone(&wallet)).await; @@ -123,6 +144,11 @@ async fn test_masternode_list_sync_with_restart() { "Should reach Synced state after restart" ); + // A restart re-syncs on top of what it restored; it never discards a whole + // class of state it already had. + let after_second = storage_snapshot(ctx.storage_path()); + assert_storage_did_not_shrink(&after_first, &after_second, "masternode restart"); + tracing::info!( "Restart verified: first_height={}, second_height={}", first_height, From 747c2f7f81d436d30126a91df48aa5a1bf735b32 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 27 Aug 2026 14:43:43 +0000 Subject: [PATCH 2/3] fix(dash-spv): persist the masternode list and restore it at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `storage/masternode.rs` has had no callers outside `storage/` since the legacy sync engine was deleted, and `DashSpvClient::new` always built a fresh `MasternodeListEngine`. Every start therefore rebuilt the whole list from the network — a full QRInfo plus every MnListDiff — while headers, filters and ChainLocks resumed from disk. On mobile, where the host app restarts the client every minute or two, the rebuild rarely finishes, so a client can run with no masternode list at all despite having synced one in a previous session (dashpay/rust-dashcore#988). Both halves are wired here. `MasternodesManager` takes the state store and writes the engine wherever it reports `MasternodeStateUpdated` — the same condition that makes the new state worth keeping. `DashSpvClient::new` loads the state and seeds the engine, before the managers are built: `MasternodesManager::new` already recovers its resume point from the engine's stored lists, so a restore landing after it would be ignored. Both directions fail soft. An unwritten list costs a rebuild next start; a failed sync costs the list now. Likewise state that cannot be read is logged and rebuilt, which is exactly the old behaviour. `test_masternode_list_sync_with_restart` now passes, and the log shows why: `0 base hash(es)` on the first sync, `Restored masternode state from height 406`, then `1 base hash(es)` on the second — the delta, not a rebuild. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS --- dash-spv/src/client/lifecycle.rs | 32 +++++++++++-- dash-spv/src/storage/mod.rs | 6 +++ dash-spv/src/sync/masternodes/manager.rs | 48 ++++++++++++++++++- dash-spv/src/sync/masternodes/sync_manager.rs | 10 ++-- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index 46e26f71c..d3b17d715 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -13,8 +13,9 @@ use crate::chain::checkpoints::CheckpointManager; use crate::error::{Result, SpvError}; use crate::network::NetworkManager; use crate::storage::{ - PersistentBlockHeaderStorage, PersistentBlockStorage, PersistentFilterHeaderStorage, - PersistentFilterStorage, PersistentMetadataStorage, StorageManager, + MasternodeStateStorage, PersistentBlockHeaderStorage, PersistentBlockStorage, + PersistentFilterHeaderStorage, PersistentFilterStorage, PersistentMetadataStorage, + StorageManager, }; use crate::sync::{ BlockHeadersManager, BlocksManager, ChainLockManager, FilterHeadersManager, FiltersManager, @@ -65,11 +66,31 @@ impl DashSpvClient match serde_json::from_slice(&state.engine_state) { + Ok(restored) => { + engine = restored; + tracing::info!( + "Restored masternode state from height {}", + state.last_height + ); + } + Err(e) => tracing::warn!( + "Could not read persisted masternode state, rebuilding: {}", + e + ), + }, + Ok(None) => tracing::debug!("No persisted masternode state"), + Err(e) => { + tracing::warn!("Could not load masternode state, rebuilding: {}", e) + } + } + Some(Arc::new(RwLock::new(engine))) } else { None } @@ -123,6 +144,7 @@ impl DashSpvClient Arc>; + + fn masternodestate(&self) -> Arc>; } /// Disk-based storage manager with segmented files and async background saving. @@ -282,6 +284,10 @@ impl StorageManager for DiskStorageManager { fn metadata(&self) -> Arc> { Arc::clone(&self.metadata) } + + fn masternodestate(&self) -> Arc> { + Arc::clone(&self.masternodestate) + } } #[async_trait] diff --git a/dash-spv/src/sync/masternodes/manager.rs b/dash-spv/src/sync/masternodes/manager.rs index 428673535..868a96c90 100644 --- a/dash-spv/src/sync/masternodes/manager.rs +++ b/dash-spv/src/sync/masternodes/manager.rs @@ -14,7 +14,9 @@ use tokio::sync::RwLock; use super::pipeline::MnListDiffPipeline; use crate::error::{SyncError, SyncResult}; use crate::network::RequestSender; -use crate::storage::BlockHeaderStorage; +use crate::storage::{ + BlockHeaderStorage, MasternodeState, MasternodeStateStorage, PersistentMasternodeStateStorage, +}; use crate::sync::{MasternodesProgress, SyncEvent, SyncManager, SyncState}; use dashcore::network::message_qrinfo::QRInfo; use dashcore::BlockHash; @@ -299,6 +301,8 @@ pub struct MasternodesManager { network: dashcore::Network, /// Sync state tracking. pub(super) sync_state: MasternodeSyncState, + /// `None` leaves the list in memory only. + pub(super) state_storage: Option>>, } impl MasternodesManager { @@ -307,6 +311,7 @@ impl MasternodesManager { header_storage: Arc>, engine: Arc>, network: dashcore::Network, + state_storage: Option>>, ) -> Self { // Recover sync state from the engine's stored masternode lists so that a // restart can resume from where the previous run left off. @@ -337,6 +342,38 @@ impl MasternodesManager { engine, network, sync_state, + state_storage, + } + } + + /// Best effort: an unwritten list costs a rebuild next start, a failed sync + /// costs the list now. + pub(super) async fn persist_engine(&self, height: u32) { + let Some(storage) = &self.state_storage else { + return; + }; + let engine_state = { + let engine = self.engine.read().await; + match serde_json::to_vec(&*engine) { + Ok(bytes) => bytes, + Err(e) => { + tracing::warn!("Could not serialize masternode engine at {height}: {e}"); + return; + } + } + }; + let state = MasternodeState { + last_height: height, + engine_state, + last_update: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0), + }; + if let Err(e) = storage.write().await.store_masternode_state(&state).await { + tracing::warn!("Could not persist masternode state at {height}: {e}"); + } else { + tracing::debug!("Persisted masternode state at height {height}"); } } @@ -559,6 +596,7 @@ impl MasternodesManager { self.sync_state.last_synced_block_hash = Some(latest_block_hash); self.progress.update_current_height(height); + self.persist_engine(height).await; tracing::debug!("Incremental MnListDiff complete at height {}", height); Ok(vec![SyncEvent::MasternodeStateUpdated { height, @@ -662,6 +700,10 @@ impl MasternodesManager { drop(engine); + if !events.is_empty() { + self.persist_engine(self.progress.current_height()).await; + } + if is_initial_sync { self.set_state(SyncState::Synced); tracing::info!("Masternode sync complete at height {}", self.progress.current_height()); @@ -696,7 +738,7 @@ mod tests { async fn create_test_manager_for(network: dashcore::Network) -> TestMasternodesManager { let storage = DiskStorageManager::with_temp_dir().await.unwrap(); let engine = Arc::new(RwLock::new(MasternodeListEngine::default_for_network(network))); - MasternodesManager::new(storage.block_headers(), engine, network).await + MasternodesManager::new(storage.block_headers(), engine, network, None).await } async fn create_test_manager() -> TestMasternodesManager { @@ -733,6 +775,7 @@ mod tests { block_headers, Arc::new(RwLock::new(engine)), dashcore::Network::Regtest, + None, ) .await; manager.set_state(SyncState::Synced); @@ -964,6 +1007,7 @@ mod tests { storage.block_headers(), Arc::new(RwLock::new(engine)), dashcore::Network::Testnet, + None, ) .await; diff --git a/dash-spv/src/sync/masternodes/sync_manager.rs b/dash-spv/src/sync/masternodes/sync_manager.rs index 1a077a8a2..d59b2b00b 100644 --- a/dash-spv/src/sync/masternodes/sync_manager.rs +++ b/dash-spv/src/sync/masternodes/sync_manager.rs @@ -1097,9 +1097,13 @@ mod tests { .await .unwrap(); let engine = MasternodeListEngine::default_for_network(Network::Regtest); - let mut manager = - MasternodesManager::new(block_headers, Arc::new(RwLock::new(engine)), Network::Regtest) - .await; + let mut manager = MasternodesManager::new( + block_headers, + Arc::new(RwLock::new(engine)), + Network::Regtest, + None, + ) + .await; manager.progress.update_block_header_tip_height(tip); let (tx, mut rx) = mpsc::unbounded_channel(); From a413afd6a5724381ea70c0be29b830e52eeb4918 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 27 Aug 2026 15:09:55 +0000 Subject: [PATCH 3/3] refactor(dash-spv): let the storage own the masternode state format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MasternodeStateStorage` took and returned `MasternodeState`, the on-disk shape, so both callers had to build it: the manager serialized the engine, stamped a timestamp and assembled the struct, and the client took it apart again. Two places knew the encoding, and neither was the one that owns it. The trait now takes and returns the engine. `MasternodeState` stays as the file format and is built and read inside `masternode.rs` alone — it is no longer named outside `storage/`. Changing how the engine is encoded, which the current JSON-array-of-bytes shape will want, is now an edit to one file rather than three. `load_engine` also absorbs the case that is not an error: nothing persisted yet yields the network's default, which is where a first run starts anyway, so the caller loses an `Option` it only ever mapped one way. A file that exists and cannot be read stays an `Err`, because that one is worth seeing — the client logs it and rebuilds from the network. The masternode manager's persistence path goes from 24 lines to 5, the client's restore from 22 to 8. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS --- dash-spv/src/client/lifecycle.rs | 26 +++-------- dash-spv/src/storage/masternode.rs | 58 ++++++++++++++++++++---- dash-spv/src/storage/mod.rs | 14 ++++-- dash-spv/src/sync/masternodes/manager.rs | 23 ++-------- 4 files changed, 68 insertions(+), 53 deletions(-) diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index d3b17d715..11452e20f 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -70,26 +70,12 @@ impl DashSpvClient match serde_json::from_slice(&state.engine_state) { - Ok(restored) => { - engine = restored; - tracing::info!( - "Restored masternode state from height {}", - state.last_height - ); - } - Err(e) => tracing::warn!( - "Could not read persisted masternode state, rebuilding: {}", - e - ), - }, - Ok(None) => tracing::debug!("No persisted masternode state"), - Err(e) => { - tracing::warn!("Could not load masternode state, rebuilding: {}", e) - } - } + let loader = storage.masternodestate(); + let engine = loader.read().await.load_engine(config.network).await; + let engine = engine.unwrap_or_else(|e| { + tracing::warn!("Could not load masternode state, rebuilding: {}", e); + MasternodeListEngine::default_for_network(config.network) + }); Some(Arc::new(RwLock::new(engine))) } else { None diff --git a/dash-spv/src/storage/masternode.rs b/dash-spv/src/storage/masternode.rs index d7ec1dd9f..6ec1a7217 100644 --- a/dash-spv/src/storage/masternode.rs +++ b/dash-spv/src/storage/masternode.rs @@ -2,16 +2,30 @@ use std::path::PathBuf; use async_trait::async_trait; +use dashcore::sml::masternode_list_engine::MasternodeListEngine; +use dashcore::Network; + use crate::{ error::StorageResult, storage::{io::atomic_write, MasternodeState, PersistentStorage}, }; +/// Persistence for the masternode list engine. +/// +/// Takes and returns the engine itself: the on-disk shape is +/// [`MasternodeState`] and stays here, so a caller neither builds it nor knows +/// how it is encoded. #[async_trait] pub trait MasternodeStateStorage { - async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()>; - - async fn load_masternode_state(&self) -> StorageResult>; + async fn store_engine( + &mut self, + engine: &MasternodeListEngine, + height: u32, + ) -> StorageResult<()>; + + /// Always yields an engine: with nothing persisted yet, the network's + /// default, which is what a first run starts from anyway. + async fn load_engine(&self, network: Network) -> StorageResult; } pub struct PersistentMasternodeStateStorage { @@ -39,13 +53,31 @@ impl PersistentStorage for PersistentMasternodeStateStorage { #[async_trait] impl MasternodeStateStorage for PersistentMasternodeStateStorage { - async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> { + async fn store_engine( + &mut self, + engine: &MasternodeListEngine, + height: u32, + ) -> StorageResult<()> { let masternodestate_folder = self.storage_path.join(Self::FOLDER_NAME); let path = masternodestate_folder.join(Self::MASTERNODE_FILE_NAME); tokio::fs::create_dir_all(masternodestate_folder).await?; - let json = serde_json::to_string_pretty(state).map_err(|e| { + let state = MasternodeState { + last_height: height, + engine_state: serde_json::to_vec(engine).map_err(|e| { + crate::error::StorageError::Serialization(format!( + "Failed to serialize masternode engine: {}", + e + )) + })?, + last_update: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0), + }; + + let json = serde_json::to_string_pretty(&state).map_err(|e| { crate::error::StorageError::Serialization(format!( "Failed to serialize masternode state: {}", e @@ -56,21 +88,29 @@ impl MasternodeStateStorage for PersistentMasternodeStateStorage { Ok(()) } - async fn load_masternode_state(&self) -> StorageResult> { + async fn load_engine(&self, network: Network) -> StorageResult { let path = self.storage_path.join(Self::FOLDER_NAME).join(Self::MASTERNODE_FILE_NAME); if !path.exists() { - return Ok(None); + tracing::debug!("No persisted masternode state, starting from the network default"); + return Ok(MasternodeListEngine::default_for_network(network)); } let content = tokio::fs::read_to_string(path).await?; - let state = serde_json::from_str(&content).map_err(|e| { + let state: MasternodeState = serde_json::from_str(&content).map_err(|e| { crate::error::StorageError::Serialization(format!( "Failed to deserialize masternode state: {}", e )) })?; + let engine = serde_json::from_slice(&state.engine_state).map_err(|e| { + crate::error::StorageError::Serialization(format!( + "Failed to deserialize masternode engine: {}", + e + )) + })?; - Ok(Some(state)) + tracing::debug!("Loaded masternode engine from height {}", state.last_height); + Ok(engine) } } diff --git a/dash-spv/src/storage/mod.rs b/dash-spv/src/storage/mod.rs index fb43891ea..cfe974b77 100644 --- a/dash-spv/src/storage/mod.rs +++ b/dash-spv/src/storage/mod.rs @@ -19,6 +19,8 @@ use crate::ClientConfig; use async_trait::async_trait; use dashcore::hash_types::FilterHeader; use dashcore::prelude::CoreBlockHeight; +use dashcore::sml::masternode_list_engine::MasternodeListEngine; +use dashcore::Network; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -438,12 +440,16 @@ impl metadata::MetadataStorage for DiskStorageManager { #[async_trait] impl masternode::MasternodeStateStorage for DiskStorageManager { - async fn store_masternode_state(&mut self, state: &MasternodeState) -> StorageResult<()> { - self.masternodestate.write().await.store_masternode_state(state).await + async fn store_engine( + &mut self, + engine: &MasternodeListEngine, + height: u32, + ) -> StorageResult<()> { + self.masternodestate.write().await.store_engine(engine, height).await } - async fn load_masternode_state(&self) -> StorageResult> { - self.masternodestate.read().await.load_masternode_state().await + async fn load_engine(&self, network: Network) -> StorageResult { + self.masternodestate.read().await.load_engine(network).await } } diff --git a/dash-spv/src/sync/masternodes/manager.rs b/dash-spv/src/sync/masternodes/manager.rs index 868a96c90..0571c1d7c 100644 --- a/dash-spv/src/sync/masternodes/manager.rs +++ b/dash-spv/src/sync/masternodes/manager.rs @@ -15,7 +15,7 @@ use super::pipeline::MnListDiffPipeline; use crate::error::{SyncError, SyncResult}; use crate::network::RequestSender; use crate::storage::{ - BlockHeaderStorage, MasternodeState, MasternodeStateStorage, PersistentMasternodeStateStorage, + BlockHeaderStorage, MasternodeStateStorage, PersistentMasternodeStateStorage, }; use crate::sync::{MasternodesProgress, SyncEvent, SyncManager, SyncState}; use dashcore::network::message_qrinfo::QRInfo; @@ -352,25 +352,8 @@ impl MasternodesManager { let Some(storage) = &self.state_storage else { return; }; - let engine_state = { - let engine = self.engine.read().await; - match serde_json::to_vec(&*engine) { - Ok(bytes) => bytes, - Err(e) => { - tracing::warn!("Could not serialize masternode engine at {height}: {e}"); - return; - } - } - }; - let state = MasternodeState { - last_height: height, - engine_state, - last_update: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0), - }; - if let Err(e) = storage.write().await.store_masternode_state(&state).await { + let engine = self.engine.read().await; + if let Err(e) = storage.write().await.store_engine(&engine, height).await { tracing::warn!("Could not persist masternode state at {height}: {e}"); } else { tracing::debug!("Persisted masternode state at height {height}");