Skip to content

refactor(dash-spv)!: generate the checkpoints from the network - #375

Merged
xdustinface merged 3 commits into
devfrom
refactor/checkpoints
Oct 22, 2026
Merged

xdustinface merged 3 commits into
devfrom
refactor/checkpoints

Conversation

@ZocoLini

@ZocoLini ZocoLini commented Jan 21, 2026 •

Copy link
Copy Markdown
Collaborator

The checkpoints were written by hand, and the one at mainnet height 750000 had the wrong time and bits (1491953700 / 0x1a075a02 instead of 1507424630 / 0x1a027884). The client rebuilt the checkpoint header from those fields, so once the difficulty of the headers after it is checked (#1161), a wrong field rejects the real chain.

Changes

  • generate-checkpoints binary. It syncs the block headers of a network from genesis, without a wallet, filters or masternodes. Then it reads, through DashSpvClient::header_at, the checkpoint block and the 24 headers before it every 50000 blocks (at least 1000 below the tip). It validates each set with BlockHeaderValidator and writes them to dash-spv/checkpoints/<network>.checkpoints.
  • Embedded checkpoint files. A Checkpoint is now its height and those 25 headers, the checkpoint block last, and the files are embedded with include_bytes!. The hand-written lists, and the fields and lookups only tests used (chain_work, masternode_list_name, timestamp lookup, validate_block, should_reject_fork), are gone.
  • Start from a checkpoint. A client starting from a checkpoint stores its 25 headers from height - 24, so the difficulty of the blocks after it can be checked from the first one on. The rebuilt header with a zeroed version is gone.
  • Genesis. Genesis is no longer a checkpoint, since it has no headers before it. It is still initialized on its own, as before: the old checkpoint at 0 was never used.

The generated blocks match the hand-written ones (prev hash, merkle root and nonce) at every height except for the time and bits at mainnet 750000. Mainnet now has 51 checkpoints up to 2550000 (100 KB), and testnet 31 up to 1550000 (61 KB).

To regenerate them:

cargo run --release -p dash-spv --bin generate-checkpoints -- -n mainnet -d <dir>
cargo run --release -p dash-spv --bin generate-checkpoints -- -n testnet -d <dir>

Breaking: Checkpoint and CheckpointManager have a new API.

Fixes the checkpoint review points on #1161.

🤖 Generated with Claude Code

PR Hygiene · da2f182

  • Bots — coderabbitai not yet — /skip-bots proceeds without the ones not yet reported
  • Self-review — post /self-reviewed
  • Build green
  • Approvals
    • dash-spv (dash-spv/Cargo.toml, dash-spv/build.rs, dash-spv/checkpoints/mainnet.json and 10 more) — QuantumExplorer or xdustinface

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

@coderabbitai

coderabbitai Bot commented Jan 21, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

The checkpoint subsystem is refactored from a multi-field public struct with HashMap-based storage to a compact structure using a Vec with simplified field access. CheckpointManager now accepts a Network parameter to load embedded network-specific checkpoint data. Related modules and test utilities are updated to use the new API. The test module is removed.

Changes

Cohort / File(s) Summary
Checkpoint Core Refactoring
dash-spv/src/chain/checkpoints.rs
Restructured Checkpoint from public multi-field struct to private height/HashedBlockHeader pair with new(), height(), header(), hash() accessors. CheckpointManager changed from HashMap-based to Vec-based storage. New constructor CheckpointManager::new(network) loads embedded bytes; added new_with_checkpoints() and checkpoints_between(). Removed get_checkpoint(), validate_block(), last_checkpoint(), checkpoint_heights(); changed last_checkpoint_before_height() return type.
Module Visibility Changes
dash-spv/src/chain/mod.rs
Reduced checkpoint module visibility from pub mod to mod; changed re-exports from pub use to pub(crate) use for Checkpoint and CheckpointManager; removed test module declaration.
Test Module Removal
dash-spv/src/chain/checkpoint_test.rs
Deleted entire test file (182 lines) containing comprehensive unit tests for CheckpointManager methods including get_sync_checkpoint(), should_reject_fork(), protocol_version(), validate_block(), and consistency checks.
Integration Updates
dash-spv/src/client/lifecycle.rs, dash-spv/src/test_utils/checkpoint.rs, dash-spv/src/sync/block_headers/manager.rs, dash-spv/src/sync/block_headers/pipeline.rs
Updated to use new CheckpointManager API: network-based construction instead of manual checkpoint lists; uses last_checkpoint_before_height() for header retrieval; replaced checkpoint iteration with checkpoints_between() call; updated test utilities to construct dummy checkpoints via new API.
Documentation Updates
docs/implementation-notes/CHECKPOINT_IMPLEMENTATION.md
Refined formatting and structure; updated examples and field descriptions to reflect new Checkpoint/CheckpointManager API; added Benefits and Future Enhancements sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks: Passed 3
Check name Status Explanation
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: refactoring dash-spv to generate checkpoints from the network.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/checkpoints
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from e9bd4cc to c445e6c Compare January 21, 2026 18:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@dash-spv-ffi/src/checkpoints.rs`:
- Around line 44-50: Wrap calls to CheckpointManager::new in catch_unwind in the
FFI functions so panics from checkpoint decoding cannot unwind across the extern
"C" boundary; if catch_unwind catches a panic, call set_last_error with a
descriptive message and return a non-success FFIErrorCode, otherwise proceed to
call methods like CheckpointManager::last_checkpoint_before_height and return
FFIErrorCode::Success as before. Apply this change to both places where
CheckpointManager::new is invoked in this file (the two constructor call sites
that then call last_checkpoint_before_height and the later use at the other call
site), mapping panic results to an error code via set_last_error instead of
letting the panic propagate. Ensure you preserve the existing success path
behavior (setting out_height/out_hash) when no panic occurs.

In `@dash-spv/src/chain/checkpoints.rs`:
- Around line 109-111: The unreachable! in the decode error branch (inside the
bytes decode used in CheckpointManager::new) can panic on corrupted or
mismatched checkpoint data; change CheckpointManager::new to return Result<Self,
Error> (define or reuse an appropriate error type), replace the unreachable!
branch to return Err(with the decoding error wrapped) and update callers to
propagate/handle the Result; if you opt for the minimal change, include the
actual decode error in the panic message (e.g., panic!("failed to decode
checkpoint bytes: {}", err)) — but preferred is changing new() to return Result
and converting all places that call CheckpointManager::new to handle the error.
🧹 Nitpick comments (2)
dash-spv/src/test_utils/checkpoint.rs (1)

14-18: Make dummy input requirements explicit.

CheckpointManager::new_with_checkpoints expects heights sorted and starting at genesis (0); adding explicit asserts here provides clearer test failures.

♻️ Proposed guardrails
 impl CheckpointManager {
     pub fn dummy(heights: &[u32]) -> CheckpointManager {
+        debug_assert!(
+            heights.first() == Some(&0),
+            "CheckpointManager::dummy expects heights to start with genesis (0)"
+        );
+        debug_assert!(
+            heights.windows(2).all(|w| w[0] <= w[1]),
+            "CheckpointManager::dummy expects heights sorted ascending"
+        );
         let checkpoints =
             heights.iter().map(|height| Checkpoint::dummy(*height)).collect::<Vec<_>>();
 
         CheckpointManager::new_with_checkpoints(checkpoints)
     }
 }
dash-spv/src/sync/headers/manager.rs (1)

200-200: Step 3 comment may be stale after removing checkpoint validation.

The comment references "Step 3" but the checkpoint validation loop mentioned in the summary was removed. Consider renaming this to simply "Process the Entire Batch" or updating the step numbering to reflect the current flow (Step 1: Handle Empty Batch, Step 2: Validate Batch, Step 3: Process).

Comment thread dash-spv-ffi/src/checkpoints.rs Outdated
Comment thread dash-spv/src/chain/checkpoints.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@dash-spv/src/client/lifecycle.rs`:
- Around line 254-256: Update the misleading comment that currently claims the
checkpoint header is not stored; instead reflect that the code does store the
checkpoint header via store_headers_at_height. Locate the comment next to the
call/use of store_headers_at_height in the lifecycle code (around the checkpoint
handling logic) and change the text to clearly state that the checkpoint header
is saved at that height and that subsequent headers will be requested from peers
starting after this stored checkpoint.
♻️ Duplicate comments (2)
dash-spv-ffi/src/checkpoints.rs (1)

44-50: Guard against panics crossing the FFI boundary.

CheckpointManager::new can panic on decode errors (via unreachable! in the loader), which would unwind across extern "C" here. Consider catch_unwind + set_last_error, or a fallible constructor that maps to an FFIErrorCode.

Also applies to: 70-76

dash-spv/src/chain/checkpoints.rs (1)

101-111: Avoid panicking on decode failures.

Even with embedded bytes, corruption or mismatched files can trigger unreachable! and panic in library code. Consider a fallible constructor or error mapping instead. As per coding guidelines, avoid panics in library code.

🧹 Nitpick comments (1)
dash-spv/src/chain/checkpoints.rs (1)

12-22: Avoid hardcoding checkpoint heights.

Consider generating the heights alongside the checkpoint bytes (or embedding a generated heights file) to prevent drift and keep network parameters out of source code. As per coding guidelines, avoid hardcoding network parameters.

Comment thread dash-spv/src/client/lifecycle.rs Outdated
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from c445e6c to 457e3b1 Compare January 21, 2026 18:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/headers/manager.rs (1)

351-360: Lazy-load checkpoint header to avoid blocking fresh syncs with empty storage.

prepare_sync unconditionally calls get_start_height() and get_header() at lines 351-360, which fails immediately with SyncError::Storage when storage is empty (since get_start_height() returns None). This error propagates before the genesis fallback logic at lines 375–400 can execute, breaking fresh syncs.

Since checkpoint_header is only used within conditional blocks checking is_synced_from_checkpoint() (lines 365, 528), only load it when actually syncing from a checkpoint:

Suggested fix: Load checkpoint header conditionally
-        let first_height = storage
-            .get_start_height()
-            .await
-            .ok_or(SyncError::Storage("Failed to get start height".to_string()))?;
-        let checkpoint_header = storage
-            .get_header(first_height)
-            .await
-            .map_err(|e| SyncError::Storage(format!("Failed to get first header: {}", e)))?
-            .ok_or(SyncError::Storage("Storage didn't return first header".to_string()))?;
+        let checkpoint_header = if self.is_synced_from_checkpoint() {
+            let first_height = storage
+                .get_start_height()
+                .await
+                .ok_or(SyncError::Storage("Failed to get start height".to_string()))?;
+            Some(
+                storage
+                    .get_header(first_height)
+                    .await
+                    .map_err(|e| SyncError::Storage(format!("Failed to get first header: {}", e)))?
+                    .ok_or(SyncError::Storage("Storage didn't return first header".to_string()))?,
+            )
+        } else {
+            None
+        };

Then guard all uses of checkpoint_header with if let Some(ref checkpoint_header) = checkpoint_header { ... }.

🤖 Fix all issues with AI agents
In `@dash-spv/src/chain/checkpoints.rs`:
- Around line 187-191: The code currently uses expect when initializing
best_checkpoint, which can panic in release builds; make new_with_checkpoints
fallible by changing its signature to return Result<CheckpointManager,
YourErrorType>, replace the debug_assert! with an explicit runtime check that
returns Err if checkpoints.is_empty(), and in the initialization code (where
checkpoints.next().expect(...)) handle the None case by returning that Err
instead of panicking; update callers of new_with_checkpoints to propagate or
handle the Result accordingly and pick or define an appropriate error
enum/struct for empty checkpoints.
♻️ Duplicate comments (4)
dash-spv-ffi/src/checkpoints.rs (2)

44-50: Prevent panics from crossing the FFI boundary.

CheckpointManager::new uses unreachable! when checkpoint decoding fails. While the bytes are hardcoded, panics in library code will unwind across the extern "C" boundary and cause undefined behavior. Wrap the constructor call with catch_unwind to map panics to FFIErrorCode via set_last_error, or refactor CheckpointManager to support fallible construction.

🔒 Recommended fix using catch_unwind
+use std::panic::catch_unwind;
+
 pub unsafe extern "C" fn dash_spv_ffi_checkpoint_before_height(
     network: FFINetwork,
     height: u32,
     out_height: *mut u32,
     out_hash: *mut u8,
 ) -> i32 {
     if out_height.is_null() || out_hash.is_null() {
         set_last_error("Null output pointer provided");
         return FFIErrorCode::NullPointer as i32;
     }

-    let mgr = CheckpointManager::new(network.into());
+    let mgr = match catch_unwind(|| CheckpointManager::new(network.into())) {
+        Ok(m) => m,
+        Err(_) => {
+            set_last_error("Failed to initialize checkpoint manager");
+            return FFIErrorCode::InternalError as i32;
+        }
+    };

     let (height, cp) = mgr.last_checkpoint_before_height(height);
     *out_height = height;
     let hash = cp.hash().to_byte_array();
     std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32);
     FFIErrorCode::Success as i32
 }

Based on learnings, in Rust FFI code, unwrap() must not be used on operations that can panic, as it can cause undefined behavior when panics cross FFI boundaries.


70-76: Same panic risk applies here.

The CheckpointManager::new call at line 70 has the same panic risk as identified in dash_spv_ffi_checkpoint_before_height. Apply the same catch_unwind pattern to prevent undefined behavior.

dash-spv/src/client/lifecycle.rs (1)

229-268: Fix contradictory comment about checkpoint header storage.

The code at line 247 explicitly stores the checkpoint header via store_headers_at_height, but the comment at lines 254-255 states "Don't store the checkpoint header itself." This contradiction was previously flagged and marked as addressed, but the inconsistency remains.

💡 Suggested fix
-            // Don't store the checkpoint header itself - we'll request headers from peers
-            // starting from this checkpoint
+            // The checkpoint header is stored above; we'll request subsequent headers from peers
dash-spv/src/chain/checkpoints.rs (1)

101-111: Decode failures still panic via unreachable!.

If embedded bytes are corrupted or incompatible, unreachable! will panic in release. Prefer returning a Result from CheckpointManager::new, or at least include the decode error for diagnosability. As per coding guidelines, avoid panics in library code.

🧹 Nitpick comments (2)
dash-spv/src/test_utils/checkpoint.rs (1)

13-19: Consider documenting the height 0 requirement.

The CheckpointManager::new_with_checkpoints has a debug_assert requiring the first checkpoint to be at height 0. Test callers using dummy(heights) must include 0 in the heights slice; otherwise, tests will panic in debug builds.

📝 Suggested documentation
 impl CheckpointManager {
+    /// Creates a dummy CheckpointManager for testing.
+    ///
+    /// # Panics (debug builds)
+    /// Panics if `heights` is empty or does not start with 0.
     pub fn dummy(heights: &[u32]) -> CheckpointManager {
         let checkpoints =
             heights.iter().map(|height| Checkpoint::dummy(*height)).collect::<Vec<_>>();

         CheckpointManager::new_with_checkpoints(checkpoints)
     }
 }
dash-spv/src/sync/headers/manager.rs (1)

579-582: Wire a real timestamp into checkpoint selection.

Using last_checkpoint_before_timestamp(0) will always return the earliest checkpoint (likely genesis), which removes most of the checkpoint benefit. Consider threading wallet creation time or a config-supplied timestamp into this call once available.

Comment thread dash-spv/src/chain/checkpoints.rs Outdated
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 457e3b1 to 6593eb2 Compare January 21, 2026 19:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@dash-spv/src/chain/checkpoints.rs`:
- Around line 175-178: The Err(index) branch uses self.checkpoints[index - 1]
which underflows if index == 0; upgrade the debug-only invariant in
new_with_checkpoints to a runtime check (replace the debug_assert that ensures
the first checkpoint's height is 0 with assert! or explicitly return Err) so the
function enforces the first checkpoint's height == 0 in release builds and
prevents the underflow when Err(index) is taken; update new_with_checkpoints
(and its error handling) to either assert! that checkpoints.first().height == 0
or handle index == 0 explicitly before accessing index - 1.

In `@dash-spv/src/test_utils/checkpoint.rs`:
- Around line 14-18: The dummy helper currently forwards the provided heights
slice directly to CheckpointManager::new_with_checkpoints which debug-asserts
non-empty, sorted order, and starting at 0; normalize the input in dummy by
ensuring there is at least one height (insert 0 if empty), ensuring 0 is present
(insert 0 if missing), sorting the heights ascending, and removing duplicates
before mapping to Checkpoint::dummy and calling
CheckpointManager::new_with_checkpoints so the invariants (non-empty, sorted,
starts at 0) are always satisfied; update the dummy function accordingly.
♻️ Duplicate comments (4)
dash-spv-ffi/src/checkpoints.rs (1)

44-46: Guard against panics crossing the FFI boundary.

CheckpointManager::new can panic on decode errors (via unreachable!), which would unwind across extern "C". Wrap the construction with catch_unwind and map failures to set_last_error + a non-success FFIErrorCode instead of panicking.

Also applies to: 70-72

dash-spv/src/client/lifecycle.rs (1)

254-255: Update the misleading checkpoint storage comment.

The code stores the checkpoint header via store_headers_at_height, so the comment currently states the opposite.

✏️ Suggested comment update
-            // Don't store the checkpoint header itself - we'll request headers from peers
-            // starting from this checkpoint
+            // Store the checkpoint header and request subsequent headers from peers
+            // starting from this checkpoint
dash-spv/src/chain/checkpoints.rs (2)

109-111: unreachable! on decode errors could panic with corrupted checkpoint files.

This was flagged in a previous review but remains unaddressed. While checkpoint bytes are compile-time constants, unreachable! provides no useful diagnostic if corruption occurs. Per coding guidelines, library code should avoid panic-inducing constructs.

Consider making new() return Result<Self, Error> or at minimum include the error in the panic message.


189-190: expect() in library code violates coding guidelines.

The past review marked this as addressed, but expect() is still present. Per coding guidelines: "Avoid unwrap() and expect() in library code; use explicit error types."

Given the internal invariant that CheckpointManager is never empty, this is unlikely to fail at runtime, but the guideline violation remains. Consider either:

  1. Changing the return type to Option<(CoreBlockHeight, &HashedBlockHeader)>, or
  2. Using indexing with a comment documenting the invariant: &self.checkpoints[0]
🧹 Nitpick comments (2)
dash-spv/src/chain/checkpoints.rs (2)

192-197: Redundant height comparison in timestamp search loop.

The condition checkpoint.height >= best_checkpoint.height on line 194 is always true because checkpoints are iterated in ascending height order. This check can be removed to simplify the logic.

♻️ Suggested simplification
         for checkpoint in checkpoints {
-            if checkpoint.hashed_block_header.header().time <= timestamp
-                && checkpoint.height >= best_checkpoint.height
-            {
+            if checkpoint.hashed_block_header.header().time <= timestamp {
                 best_checkpoint = checkpoint;
             }
         }

49-53: Consider implementing Ord alongside PartialOrd.

The PartialOrd implementation always returns Some(...), indicating a total ordering. Implementing Ord would be more semantically correct and enable use of methods that require Ord. This is a minor improvement.

♻️ Suggested addition
 impl PartialOrd for Checkpoint {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
         Some(self.height.cmp(&other.height))
     }
 }
+
+impl Ord for Checkpoint {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.height.cmp(&other.height)
+    }
+}
+
+impl Eq for Checkpoint {}

Comment thread dash-spv/src/chain/checkpoints.rs Outdated
Comment thread dash-spv/src/test_utils/checkpoint.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@github-actions github-actions Bot added the merge-conflict The PR conflicts with the target branch. label Jan 23, 2026
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 6593eb2 to 288b085 Compare January 26, 2026 21:20
@github-actions github-actions Bot added merge-conflict The PR conflicts with the target branch. and removed merge-conflict The PR conflicts with the target branch. labels Jan 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 288b085 to c51d2b2 Compare February 24, 2026 20:55
@github-actions github-actions Bot removed the merge-conflict The PR conflicts with the target branch. label Feb 24, 2026
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from c51d2b2 to 45bd3e7 Compare February 24, 2026 20:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/client/lifecycle.rs (1)

63-83: ⚠️ Potential issue | 🟠 Major

Remove ChainLockManager initialization from the filters block to prevent panic when filters are enabled without masternodes.

The code initializes ChainLockManager at lines 76–82 inside the if config.enable_filters block, but ChainLockManager requires a MasternodeListEngine, which is only created when enable_masternodes=true. With filters enabled and masternodes disabled (a valid configuration), masternode_engine is None and .expect() panics. ChainLockManager is correctly initialized again at lines 90–101 inside the if config.enable_masternodes block—this is the only place it should be created.

Remove the duplicate ChainLockManager initialization from the filters block:

Suggested fix
         if config.enable_filters {
             managers.filter_headers =
                 Some(FilterHeadersManager::new(storage.block_headers(), storage.filter_headers()));
             managers.filters = Some(FiltersManager::new(
                 wallet.clone(),
                 storage.block_headers(),
                 storage.filter_headers(),
                 storage.filters(),
             ));
-            let masternode_list_engine = masternode_engine
-                .clone()
-                .expect("Masternode list engine must exist if masternodes are enabled");
-            managers.chainlock = Some(ChainLockManager::new(
-                storage.block_headers(),
-                storage.metadata(),
-                masternode_list_engine.clone(),
-            ));
             managers.blocks =
                 Some(BlocksManager::new(wallet.clone(), storage.block_headers(), storage.blocks()));
         }

This also aligns with the coding guideline to avoid expect() in library code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/client/lifecycle.rs` around lines 63 - 83, Remove the
ChainLockManager creation from inside the if config.enable_filters block (delete
the masternode_list_engine.expect(...) and managers.chainlock =
Some(ChainLockManager::new(...)) there) so filters can be enabled without
masternodes; only initialize ChainLockManager in the if
config.enable_masternodes block where masternode_engine is guaranteed, and avoid
using expect() on masternode_engine (handle the None case or gate the code
behind enable_masternodes) so managers.chainlock is only set when a valid
MasternodeListEngine exists.
♻️ Duplicate comments (2)
dash-spv/src/chain/checkpoints.rs (2)

89-107: ⚠️ Potential issue | 🟡 Minor

Avoid panicking on checkpoint decode failures.
unreachable! will still panic if embedded bytes are corrupted or incompatible; consider returning a proper error instead of aborting.

#!/bin/bash
# Locate call sites if you make CheckpointManager::new fallible.
rg -n "CheckpointManager::new\s*\(" -C2

As per coding guidelines: Use proper error types (thiserror) and propagate errors appropriately; avoid unwrap/expect in library code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/chain/checkpoints.rs` around lines 89 - 107, The loop in
CheckpointManager::new (which calls checkpoints_bytes and
Checkpoint::consensus_decode) uses unreachable! on any non-IO decode error and
will panic if the embedded bytes are corrupted; make construction fallible
instead: change CheckpointManager::new to return Result<Self, YourErrorType>
(define an error enum with thiserror), map the non-EOF encode::Error variants
into a decode error variant and propagate that Result up, and at call sites
update usages discovered (e.g., CheckpointManager::new) to handle the Result
rather than allowing a panic.

145-149: ⚠️ Potential issue | 🟡 Minor

Guard against Err(0) underflow in binary_search.
If height < first checkpoint (or the invariant is violated in release), index - 1 will underflow.

🛠️ Suggested fix
-        match self.checkpoints.binary_search_by_key(&height, |checkpoint| checkpoint.height) {
-            Ok(index) => &self.checkpoints[index],
-            Err(index) => &self.checkpoints[index - 1],
-        }
+        match self.checkpoints.binary_search_by_key(&height, |checkpoint| checkpoint.height) {
+            Ok(index) => &self.checkpoints[index],
+            Err(0) => &self.checkpoints[0],
+            Err(index) => &self.checkpoints[index - 1],
+        }
#!/bin/bash
# Review call sites to confirm height is never below the first checkpoint.
rg -n "last_checkpoint_before_height" -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/chain/checkpoints.rs` around lines 145 - 149, The binary_search
result can produce Err(0) which makes index - 1 underflow in
last_checkpoint_before_height; update last_checkpoint_before_height to guard the
Err case: when Err(0) return a reference to the first checkpoint (e.g.
&self.checkpoints[0] or self.checkpoints.first().expect(...)) instead of doing
index - 1, and keep the existing Ok branch unchanged; locate the method named
last_checkpoint_before_height and the Vec field checkpoints to apply this check
(or alternatively change the signature to return Option<&Checkpoint> and return
None for heights below the first checkpoint if call sites are updated
accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dash-spv/src/chain/checkpoints.rs`:
- Around line 152-168: The checkpoints_between function can panic when
start_height > end_height because start_index may be > end_index before slicing;
update checkpoints_between (and its callers such as BlockHeadersPipeline::init)
to validate or normalize inputs: check if start_height >= end_height and either
return an empty Vec or swap/adjust the heights, then compute
start_index/end_index and only slice when start_index <= end_index; ensure
start_index and end_index are clamped to checkpoints.len() to avoid
out-of-bounds slicing and document the chosen behavior.

---

Outside diff comments:
In `@dash-spv/src/client/lifecycle.rs`:
- Around line 63-83: Remove the ChainLockManager creation from inside the if
config.enable_filters block (delete the masternode_list_engine.expect(...) and
managers.chainlock = Some(ChainLockManager::new(...)) there) so filters can be
enabled without masternodes; only initialize ChainLockManager in the if
config.enable_masternodes block where masternode_engine is guaranteed, and avoid
using expect() on masternode_engine (handle the None case or gate the code
behind enable_masternodes) so managers.chainlock is only set when a valid
MasternodeListEngine exists.

---

Duplicate comments:
In `@dash-spv/src/chain/checkpoints.rs`:
- Around line 89-107: The loop in CheckpointManager::new (which calls
checkpoints_bytes and Checkpoint::consensus_decode) uses unreachable! on any
non-IO decode error and will panic if the embedded bytes are corrupted; make
construction fallible instead: change CheckpointManager::new to return
Result<Self, YourErrorType> (define an error enum with thiserror), map the
non-EOF encode::Error variants into a decode error variant and propagate that
Result up, and at call sites update usages discovered (e.g.,
CheckpointManager::new) to handle the Result rather than allowing a panic.
- Around line 145-149: The binary_search result can produce Err(0) which makes
index - 1 underflow in last_checkpoint_before_height; update
last_checkpoint_before_height to guard the Err case: when Err(0) return a
reference to the first checkpoint (e.g. &self.checkpoints[0] or
self.checkpoints.first().expect(...)) instead of doing index - 1, and keep the
existing Ok branch unchanged; locate the method named
last_checkpoint_before_height and the Vec field checkpoints to apply this check
(or alternatively change the signature to return Option<&Checkpoint> and return
None for heights below the first checkpoint if call sites are updated
accordingly).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6593eb2 and 45bd3e7.

📒 Files selected for processing (10)
  • dash-spv/checkpoints/mainnet.checkpoints
  • dash-spv/checkpoints/testnet.checkpoints
  • dash-spv/src/chain/checkpoint_test.rs
  • dash-spv/src/chain/checkpoints.rs
  • dash-spv/src/chain/mod.rs
  • dash-spv/src/client/lifecycle.rs
  • dash-spv/src/sync/block_headers/manager.rs
  • dash-spv/src/sync/block_headers/pipeline.rs
  • dash-spv/src/test_utils/checkpoint.rs
  • docs/implementation-notes/CHECKPOINT_IMPLEMENTATION.md
💤 Files with no reviewable changes (1)
  • dash-spv/src/chain/checkpoint_test.rs
✅ Files skipped from review due to trivial changes (1)
  • docs/implementation-notes/CHECKPOINT_IMPLEMENTATION.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • dash-spv/src/test_utils/checkpoint.rs

Comment thread dash-spv/src/chain/checkpoints.rs Outdated
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 45bd3e7 to 362f762 Compare February 24, 2026 23:40
@github-actions

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@github-actions github-actions Bot added the merge-conflict The PR conflicts with the target branch. label Feb 27, 2026
@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 20, 2026
@ZocoLini
ZocoLini marked this pull request as draft September 20, 2026 16:52
@github-actions github-actions Bot removed the waiting-bots Waiting for the review bots to report on this head label Sep 20, 2026
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 362f762 to 9f3e136 Compare October 19, 2026 12:46
@github-actions github-actions Bot removed the merge-conflict The PR conflicts with the target branch. label Oct 19, 2026
@ZocoLini
ZocoLini marked this pull request as ready for review October 19, 2026 12:46
@ZocoLini ZocoLini changed the title refactor(dash-spv): checkpoint rewrite refactor(dash-spv)!: generate the checkpoints from the network Oct 19, 2026
@github-actions github-actions Bot added bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. merge-conflict The PR conflicts with the target branch. labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

The checkpoints were written by hand, and the one at mainnet height 750000
had the wrong time and bits. The header of a checkpoint was rebuilt from
those fields, so once the difficulty of the headers after it is checked,
a wrong field rejects the real chain.

A new `generate-checkpoints` binary syncs the headers of a network from
genesis without a wallet and writes, every 50000 blocks, the checkpoint
block and the 24 headers before it, validated, to
`checkpoints/<network>.checkpoints`, which are embedded. A client starting
from a checkpoint stores those 25 headers, so the difficulty of the blocks
after it can be checked from the first one on.

The generated blocks match the hand-written ones except for the time and
bits at mainnet 750000. Genesis is no longer a checkpoint: it is still
initialized on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ZocoLini
ZocoLini force-pushed the refactor/checkpoints branch from 2921382 to 104e2c0 Compare October 20, 2026 16:35
@github-actions github-actions Bot removed the merge-conflict The PR conflicts with the target branch. label Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 20, 2026
`CheckpointManager::for_network` decoded the embedded checkpoints on every
call, and its users kept the result in an `Arc`. The managers of mainnet and
testnet are now decoded once into private statics, and `for_network` returns
a `&'static` reference that the headers manager and pipeline keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@ZocoLini
ZocoLini requested a review from xdustinface October 20, 2026 17:30
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai review

No review for 752a3c7f yet, so PR Hygiene is asking once. If nothing arrives, the requirement is dropped for this commit and the pull request is labelled bot-review-skipped.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 21, 2026
xdustinface
xdustinface previously approved these changes Oct 21, 2026

@xdustinface xdustinface left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the binary files are never gonna change, only new headers will be added,

New headers being added is exactly when the binary files will change and they also might change in other cases like. Like how you are adding the extra hashes now would change them as well unless i miss something.

Now those changes cause an unreadable binary diff.. of course, AI makes it easier after all.

readable JSON weights way more than binary, and for a library that matters in my opinion

Surely size matters this feels like a micro-optimisation. You could probably just convert them in build.rs even though the saving would be small anyway.

we already had a 'readable' version and we had the wrong values anyway.

Thats whats fixed when they get generated yeah. Kinda irrelevant to what format it was though, a manually crafted binary file could have had the same issue.

I would like to hear a stronger argument before deciding to go with JSON

At the end i don't want to keep arguing so if its that important to you feel free to merge it but imo having the trust anchors of the chain not readable to humans to save few kb seems as strange as the fact that i now have to get an agent going to convert me the files when i just want to have a look at them.

…ld.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 21, 2026
@ZocoLini

Copy link
Copy Markdown
Collaborator Author

@xdustinface

You could probably just convert them in build.rs even though the saving would be small anyway.

that was actually a good idea, implemented

@xdustinface
xdustinface merged commit 0705781 into dev Oct 22, 2026
64 of 66 checks passed
@xdustinface
xdustinface deleted the refactor/checkpoints branch October 22, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants