Skip to content

feat(dash-spv): masternode diff storage - #993

Draft
ZocoLini wants to merge 7 commits into
devfrom
feat/masternode-diff-storage
Draft

feat(dash-spv): masternode diff storage#993
ZocoLini wants to merge 7 commits into
devfrom
feat/masternode-diff-storage

Conversation

@ZocoLini

Copy link
Copy Markdown
Collaborator

proof of concept to improve the current masternode storage

Why??

The current implementation takes 1.3GB of disk and hundreds of MB in memory, with this refactor I am aiming for something more align with the network msg and, at the same time, reduce the amount of disk and memory the storage takes to feed the dash-spv client

This is still a prototype that I am validating and currently writing

ZocoLini and others added 3 commits August 27, 2026 14:28
`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 (#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 #954 produces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS
`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
(#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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS
`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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NhHBDGiKfiGpy7FwooyfS
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.92701% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.14%. Comparing base (237f79a) to head (61fc19d).
⚠️ Report is 7 commits behind head on dev.

Files with missing lines Patch % Lines
dash-spv/src/sync/chainlock/manager.rs 63.63% 16 Missing ⚠️
dash-spv/src/storage/masternode.rs 90.84% 14 Missing ⚠️
dash/src/sml/masternode_list_engine/helpers.rs 0.00% 11 Missing ⚠️
dash-spv/src/storage/mod.rs 64.70% 6 Missing ⚠️
dash-spv/src/sync/masternodes/manager.rs 83.33% 5 Missing ⚠️
dash-spv/src/client/lifecycle.rs 70.00% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##              dev     #993    +/-   ##
========================================
  Coverage   77.14%   77.14%            
========================================
  Files         329      329            
  Lines       82998    83689   +691     
========================================
+ Hits        64026    64561   +535     
- Misses      18972    19128   +156     
Flag Coverage Δ
core 78.22% <8.33%> (-0.04%) ⬇️
ffi 51.15% <ø> (-1.26%) ⬇️
rpc 20.00% <ø> (ø)
spv 92.05% <83.20%> (+0.04%) ⬆️
wallet 79.57% <ø> (+0.35%) ⬆️
Files with missing lines Coverage Δ
dash-spv/src/sync/masternodes/sync_manager.rs 87.62% <100.00%> (-0.90%) ⬇️
...ernode_list_engine/message_request_verification.rs 84.33% <100.00%> (ø)
dash-spv/src/client/lifecycle.rs 91.08% <70.00%> (-1.22%) ⬇️
dash-spv/src/sync/masternodes/manager.rs 94.14% <83.33%> (-0.60%) ⬇️
dash-spv/src/storage/mod.rs 84.27% <64.70%> (-0.84%) ⬇️
dash/src/sml/masternode_list_engine/helpers.rs 92.30% <0.00%> (-7.70%) ⬇️
dash-spv/src/storage/masternode.rs 91.30% <90.84%> (+71.30%) ⬆️
dash-spv/src/sync/chainlock/manager.rs 90.26% <63.63%> (-3.23%) ⬇️

... and 32 files with indirect coverage changes

ZocoLini and others added 2 commits August 30, 2026 11:48
…ed copy

The masternode storage persisted the engine's block container alongside the
quorum statuses, so every hash/height pair the replay needed was written a
second time next to the header storage that already holds it. Two copies of
the same mapping can only diverge: a header reorg rewrites one of them.

The replay now works exactly like the live sync path, only reading its
messages from disk instead of waiting for peers. The header storage is
injected at construction, `PersistentMasternodeStorage<H: BlockHeaderStorage>`
keeps the shared handle, and heights are resolved against it: QRInfo through
the same `feed_qrinfo_heights_to_engine` the sync manager uses (moved to the
storage module, where both callers reach it), MnListDiff through its file name
plus a lookup of the base hash it extends.

What is left of the old context file is the quorum statuses, so it is named
for them: `quorum_statuses.dat`, written by `store_quorum_statuses`. One that
fails to decode is now ignored with a warning instead of failing the whole
load, since the statuses are a verification cache the replay re-derives.

Tying the storage to `H` also ties it to the same header storage its manager
already carries, instead of hardcoding the concrete type.

`PersistentStorage` is gone from this storage: its `persist` was a no-op, the
messages are written as they arrive, so the background worker no longer wakes
it up.

Verified against dashd regtest: the whole `dash-spv` suite passes (569 unit,
10 dashd_masternode, 30 dashd_sync). The restart test's log shows the replay
rebuilding the lists purely from headers plus stored messages:
"Replayed 1/1 QRInfo and 1/1 MnListDiff messages into 7 masternode lists".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uep5dcAKWZ3GA2Rm7GqUbr
@ZocoLini
ZocoLini force-pushed the feat/masternode-diff-storage branch from 15ef160 to ae334bc Compare August 31, 2026 10:11
@ZocoLini ZocoLini changed the title Feat/masternode diff storage feat(dash-spv): masternode diff storage Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant