Skip to content

Conversation

@xdustinface
Copy link
Collaborator

@xdustinface xdustinface commented Jan 29, 2026

This is to allow storing blocks during sync to avoid the need of re-fetching for potential rescan. At the moment we don't have persistens wallets in the CLI and storing the blocks also helps there to make restarts ~15s on my machine compared to ~50s without cached blocks.

I was thinking about maybe making storing blocks optional by config although i think some "clear sync cache" command for the spv client would be better since usually there are not many blocks stored anyway unless its super heavy used wallets.

Summary by CodeRabbit

  • New Features

    • Added persistent block storage to cache full blockchain blocks across restarts and integrated it into disk storage management.
    • Blocks are now stored automatically during block processing, introducing an observable storage error path.
  • Tests

    • Added unit tests covering store/load, persistence across restarts, nonexistent blocks, and gap handling.
  • Chores

    • Added test utilities to simplify constructing block instances for tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds persistent block storage to Dash SPV: a new async BlockStorage trait and PersistentBlockStorage that caches and persists HashedBlock instances, integrates into DiskStorageManager, and writes blocks during sync processing with unit tests and test helpers.

Changes

Cohort / File(s) Summary
Block Storage Module
dash-spv/src/storage/blocks.rs
New async BlockStorage trait and PersistentBlockStorage implementation using RwLock<SegmentCache<HashedBlock>>, an available_heights set, open/persist lifecycle, store/load operations, and unit tests.
Storage Integration
dash-spv/src/storage/mod.rs
Adds mod blocks; and pub use ...{BlockStorage, PersistentBlockStorage}; Extends StorageManager bounds, adds blocks: Arc<RwLock<PersistentBlockStorage>> to DiskStorageManager, initializes/persists/clears it, runs background persistence, and implements BlockStorage delegation.
Types & Serialization
dash-spv/src/types.rs, dash-spv/src/storage/segments.rs
Introduces HashedBlock type (caches BlockHash, conversions, Encodable/Decodable, PartialEq). Adds Persistable impl and sentinel() for HashedBlock to support segment cache persistence.
Sync Path
dash-spv/src/sync/legacy/message_handlers.rs
When handling block messages, stores a HashedBlock to block storage after wallet processing and maps storage errors to SyncError::Storage (new observable error path).
Tests & Utilities
dash-spv/src/test_utils/mod.rs, dash-spv/src/test_utils/types.rs
Adds types test module and HashedBlock::dummy() helper for constructing test HashedBlock instances.

Sequence Diagram

sequenceDiagram
    participant Handler as Block Message Handler
    participant Manager as DiskStorageManager
    participant BlockStore as PersistentBlockStorage
    participant Cache as SegmentCache
    participant Disk as Filesystem

    Handler->>BlockStore: store_block(height, hashed_block)
    BlockStore->>BlockStore: Insert height into available_heights
    BlockStore->>Cache: insert(hashed_block, at height)
    Cache->>Cache: segment insert

    Note over Manager,BlockStore: background persistence loop
    Manager->>BlockStore: persist(storage_path)
    BlockStore->>Cache: persist()
    Cache->>Disk: write segment files

    Handler->>BlockStore: load_block(height)
    BlockStore->>BlockStore: check available_heights
    alt height tracked
        BlockStore->>Cache: load(height)
        Cache->>BlockStore: return HashedBlock
        BlockStore->>Handler: Some(HashedBlock)
    else not tracked
        BlockStore->>Handler: None
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and hide each block away,
Into segment burrows where hashes softly play,
From sync to disk I hop with care,
Safe heights remembered in my lair,
A rabbit's cheer for blocks that stay!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and accurately summarizes the main change: introducing PersistentBlockStorage, which is the core feature implemented across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/block-storage

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dash-spv/src/sync/legacy/message_handlers.rs (1)

642-655: Skip persistence when height is unknown (and drop wallet lock before I/O).
If get_header_height_by_hash returns None, persisting at height 0 can corrupt the cache (overwriting genesis). Also, the new storage await happens while holding the wallet write lock; dropping the lock earlier reduces contention.

🔧 Suggested fix
-        let block_height = storage
+        let block_height = storage
             .get_header_height_by_hash(&block_hash)
             .await
             .map_err(|e| SyncError::Storage(format!("Failed to get block height: {}", e)))?
-            .unwrap_or(0);
+            ;
 
-        let result = wallet.process_block(block, block_height).await;
+        let result = wallet.process_block(block, block_height.unwrap_or(0)).await;
+
+        drop(wallet);
 
-        storage
-            .store_block(block_height, HashedBlock::from(block))
-            .await
-            .map_err(|e| SyncError::Storage(e.to_string()))?;
+        if let Some(height) = block_height {
+            storage
+                .store_block(height, HashedBlock::from(block))
+                .await
+                .map_err(|e| SyncError::Storage(e.to_string()))?;
+        } else {
+            tracing::warn!("Skipping block persistence: height unknown for {}", block_hash);
+        }
-
-        drop(wallet);
🤖 Fix all issues with AI agents
In `@dash-spv/src/storage/blocks.rs`:
- Around line 57-64: The current rebuild of available_heights uses
SegmentCache::get_items which asserts no sentinels and can panic on sparse
storage; change this to use a gap-tolerant iterator that scans the raw segment
entries and skips HashedBlock::sentinel values instead of calling get_items. Add
a helper (e.g., SegmentCache::scan_items_or a new segments::scan_raw_items) that
yields Option<HashedBlock> per index or filters out sentinels, then update the
loop that builds available_heights (the block-height loop referencing
blocks.start_height(), blocks.tip_height(), and HashedBlock::sentinel()) to
iterate that helper and only insert heights for non-sentinel entries.
- Around line 83-85: store_block currently unconditionally inserts into storage
which can trigger Segment::insert debug-assert if a slot already contains data;
update store_block to guard against duplicate height inserts by checking for
existing entries before inserting (e.g., consult available_heights or the
segment at height) and short-circuit when the height is already present, or call
an explicit overwrite/replace path on blocks (the call site
blocks.write().await.store_items_at_height should be replaced with a guard +
either skip insert or invoke a replace method). Ensure you reference and handle
available_heights and the blocks.write().await store operation so duplicate
insertions (reorgs/duplicates) don't hit Segment::insert debug panics.

In `@dash-spv/src/types.rs`:
- Around line 165-173: When decoding HashedBlock in the Decodable impl
(consensus_decode), after reading hash via BlockHash::consensus_decode and block
via Block::consensus_decode, compute the block's canonical hash (e.g.,
block.hash() or Block::hash()) and compare it to the decoded hash; if they
differ, return an Err(dashcore::consensus::encode::Error::ParseFailed(...)) (or
another appropriate consensus encode Error) to reject the payload so the
invariant hash ↔ block is enforced and corrupted/tampered data is not accepted.

let mut blocks: SegmentCache<HashedBlock> =
SegmentCache::load_or_new(&blocks_folder).await.map_err(|e| {
tracing::error!("Failed to load block segments from {:?}: {}", blocks_folder, e);
e
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating some logs here when we are propagating the error doesn't make sense to me, its the caller responsability to handle it/log it or continue the propagation until someone knows what to do, if not we end up with multiple logs for the same error

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone!

block_headers: Arc<RwLock<PersistentBlockHeaderStorage>>,
filter_headers: Arc<RwLock<PersistentFilterHeaderStorage>>,
filters: Arc<RwLock<PersistentFilterStorage>>,
block_storage: Arc<RwLock<PersistentBlockStorage>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would prefer blocks here, following the other fields name convention

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me too! :)

This is to allow storing blocks during sync to avoid the need of re-fetching for potential rescan. At the moment we don't have persistens wallets in the CLI and storing the blocks also helps there to make restarts ~15s on my machine compared to ~50s without cached blocks.

 I was thinking about maybe making storing blocks optional by config although i think some "clear sync cache" command for the spv client would be better since usually there are not many blocks stored anyway unless its super heavy used wallets.
Copy link
Collaborator

@ZocoLini ZocoLini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect, ty :D

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dash-spv/src/sync/legacy/message_handlers.rs (1)

643-654: Avoid persisting blocks when header height is unknown (cache corruption risk).

If get_header_height_by_hash returns None, the current unwrap_or(0) will store the block at height 0, which can overwrite/poison the genesis entry and misindex the cache.

🔧 Suggested guard to skip storage when height is unknown
-        let block_height = storage
+        let block_height_opt = storage
             .get_header_height_by_hash(&block_hash)
             .await
             .map_err(|e| SyncError::Storage(format!("Failed to get block height: {}", e)))?
-            .unwrap_or(0);
+            ;
+        let block_height = block_height_opt.unwrap_or(0);
@@
-        storage
-            .store_block(block_height, HashedBlock::from(block))
-            .await
-            .map_err(|e| SyncError::Storage(e.to_string()))?;
+        if let Some(height) = block_height_opt {
+            storage
+                .store_block(height, HashedBlock::from(block))
+                .await
+                .map_err(|e| SyncError::Storage(e.to_string()))?;
+        } else {
+            tracing::warn!(
+                "Skipping block storage for {}: height not found in headers",
+                block_hash
+            );
+        }

@xdustinface xdustinface merged commit 32506e2 into v0.42-dev Jan 30, 2026
53 checks passed
@xdustinface xdustinface deleted the feat/block-storage branch January 30, 2026 00:25
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.

3 participants