Skip to content

Conversation

@ZocoLini
Copy link
Collaborator

@ZocoLini ZocoLini commented Jan 21, 2026

I made this little rewrite of the CheckpointManager for the next reasons:

  • Improved readability
  • Removed all useless behaviour
  • Instead of hardcoding the checkpoints manually we can download them from the network using the library client
  • No more errors or possible missing values runtime
  • Would like in a future to modify the client by only using checkpoints (currently we have dfferent logic for sync from genesis but that is something that checkpoints can handle)

changes made include:

  • Tests rewrite
  • Hardcoded checkpoints moved into a file generated using the lib client and decoded at runtime
  • Removed one FFI function that is not being used in iOS
  • FFI logic updated
  • debug assert to ensure checkpoints consistency during tests
  • reduced mod and struct visibility. Would like to make all checkpoints logic pub(crate) but bcs the ffi currently is not a trivial change
  • Checkpoint struct improved by using HashedBlockHeader

Summary by CodeRabbit

  • Removed Features

    • Removed a public checkpoints-between-heights API.
  • Refactor

    • Unified checkpoint handling: manager now auto-loads network checkpoints, simplified startup/sync logic, and reduced public checkpoint surface (internalized checkpoint representation and APIs).
    • Sync flow simplified to use the unified checkpoint manager for starting point determination.
  • Tests

    • Legacy checkpoint test suite removed; tests and test utilities updated to the new checkpoint API and constructors.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Checkpoint types and loading were refactored to a compact, network-driven model; CheckpointManager now loads embedded checkpoint data and returns tuple-based checkpoint info. FFI removed dash_spv_ffi_checkpoints_between_heights and relocated FFIArray. Client sync and lifecycle logic now use the unified CheckpointManager; checkpoint tests were removed or updated.

Changes

Cohort / File(s) Summary
FFI API Layer
dash-spv-ffi/FFI_API.md, dash-spv-ffi/include/dash_spv_ffi.h, dash-spv-ffi/src/checkpoints.rs
Removed public FFI function dash_spv_ffi_checkpoints_between_heights. Moved FFIArray typedef position in header. FFI checkpoint functions now construct managers from FFINetwork and extract hashes via cp.hash().
Core Checkpoint Structures
dash-spv/src/chain/checkpoints.rs, dash-spv/src/chain/mod.rs
Checkpoint made compact and crate-private (height + HashedBlockHeader) with Checkpoint::new; implements Encodable/Decodable/PartialOrd. CheckpointManager switched from map+index to Vec<Checkpoint> and now loads embedded bytes by Network via new(network). Several accessors removed or return tuples (height, &HashedBlockHeader). Module visibility and re-exports adjusted (mod checkpoints;, pub use checkpoints::CheckpointManager;, pub(crate) use checkpoints::Checkpoint;).
Tests & Test Utilities
dash-spv/src/chain/checkpoint_test.rs, dash-spv/src/test_utils/checkpoint.rs
Removed legacy checkpoint_test.rs. Test utilities updated: Checkpoint::dummy uses Checkpoint::new(...).into(); added CheckpointManager::dummy(heights: &[u32]) -> CheckpointManager using new_with_checkpoints.
Client / Sync Integration
dash-spv/src/client/lifecycle.rs, dash-spv/src/sync/headers/manager.rs
Replaced per-network manual checkpoint construction with unified CheckpointManager::new(network). Lifecycle and header sync now use last_checkpoint_before_height / last_checkpoint_before_timestamp tuple returns as sync start; removed per-block checkpoint validation and removed stored reorg_config field and can_skip_to_checkpoint method. Storage updates simplified to use checkpoint-provided header/state.

Sequence Diagram(s)

sequenceDiagram
    participant Client as ClientLifecycle
    participant Sync as HeaderSyncManager
    participant CP as CheckpointManager
    participant Storage as Storage/DB
    participant Peers as Peers/Network

    rect rgba(200,230,201,0.5)
    Client->>CP: new(network)
    CP-->>Client: CheckpointManager
    end

    rect rgba(187,222,251,0.5)
    Client->>CP: last_checkpoint_before_height(start_height)
    CP-->>Client: (height, &HashedBlockHeader)
    end

    rect rgba(255,224,178,0.5)
    Client->>Sync: init_from_checkpoint(height, state)
    Sync->>Storage: store_chain_state(height, state)
    Sync->>Peers: request_headers_from(height+1)
    Peers-->>Sync: headers batch
    Sync->>Storage: persist headers/cache
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰
I hop through bytes and tidy lines,
Pack checkpoints neat in ordered mines,
One manager bakes them by the net,
Sync starts steady—no loose vignette,
I nibble crumbs and hum, "All fine!"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(dash-spv): checkpoint rewrite' accurately describes the main change, which is a significant rewrite of the CheckpointManager and related checkpoint infrastructure across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@ZocoLini ZocoLini force-pushed the refactor/checkpoints branch from e9bd4cc to c445e6c Compare January 21, 2026 18:25
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: 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 on lines +44 to +50
let mgr = CheckpointManager::new(network.into());

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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, find the relevant files
echo "=== Locating checkpoint-related files ==="
fd -t f "checkpoints\.rs" --type f

echo -e "\n=== Checking dash-spv-ffi/src/checkpoints.rs existence ==="
test -f dash-spv-ffi/src/checkpoints.rs && echo "File exists" || echo "File not found"

echo -e "\n=== Checking dash-spv/src/chain/checkpoints.rs existence ==="
test -f dash-spv/src/chain/checkpoints.rs && echo "File exists" || echo "File not found"

Repository: dashpay/rust-dashcore

Length of output: 318


🏁 Script executed:

#!/bin/bash

# Read the FFI checkpoints.rs file to see the context around lines 44-50 and 70-76
echo "=== FFI checkpoints.rs content ==="
if [ -f dash-spv-ffi/src/checkpoints.rs ]; then
    wc -l dash-spv-ffi/src/checkpoints.rs
    echo ""
    cat -n dash-spv-ffi/src/checkpoints.rs | head -100
fi

Repository: dashpay/rust-dashcore

Length of output: 3152


🏁 Script executed:

#!/bin/bash

# Search for CheckpointManager::new and unreachable! in the dash-spv directory
echo "=== Searching for CheckpointManager in dash-spv ==="
rg -n "CheckpointManager|unreachable!" dash-spv/src/ -A 3 -B 1

Repository: dashpay/rust-dashcore

Length of output: 6924


🏁 Script executed:

#!/bin/bash

# Read the full CheckpointManager::new implementation
echo "=== CheckpointManager::new implementation ==="
sed -n '92,120p' dash-spv/src/chain/checkpoints.rs | cat -n

Repository: dashpay/rust-dashcore

Length of output: 1251


🏁 Script executed:

#!/bin/bash

# Check for other potential panic paths in CheckpointManager methods
echo "=== Methods called from FFI (last_checkpoint_before_height/timestamp) ==="
sed -n '130,210p' dash-spv/src/chain/checkpoints.rs | cat -n

Repository: dashpay/rust-dashcore

Length of output: 3552


🏁 Script executed:

#!/bin/bash

# Verify the exact context and confirm there's no error handling pathway
echo "=== Checking FFI error handling for CheckpointManager::new ==="
sed -n '33,51p' dash-spv-ffi/src/checkpoints.rs | cat -n

echo -e "\n=== Second FFI function ==="
sed -n '59,77p' dash-spv-ffi/src/checkpoints.rs | cat -n

Repository: dashpay/rust-dashcore

Length of output: 1728


🏁 Script executed:

#!/bin/bash

# Check if CheckpointManager provides a fallible constructor alternative
echo "=== All public CheckpointManager methods ==="
rg -n "pub.*fn" dash-spv/src/chain/checkpoints.rs | grep -i "checkpoint"

Repository: dashpay/rust-dashcore

Length of output: 287


Prevent panics from crossing the FFI boundary.

CheckpointManager::new uses unreachable! when checkpoint decoding fails (dash-spv/src/chain/checkpoints.rs line 111). 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.

This applies to both calls at lines 44 and 70 in this file.

🤖 Prompt for AI Agents
In `@dash-spv-ffi/src/checkpoints.rs` around lines 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.

Comment on lines +109 to +111
Err(_) => {
unreachable!("The bytes are hardcoded in the bin, decode cannot fail")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

unreachable! on decode errors could panic with corrupted checkpoint files.

While the checkpoint bytes are indeed hardcoded at compile time, using unreachable! here means any corruption or version mismatch would cause a panic in release builds. Consider either:

  1. Returning a Result from new() to propagate the error, or
  2. At minimum, adding the actual error to the panic message for debugging

As per coding guidelines, library code should avoid unwrap() and expect() in favor of explicit error types.

🛠️ Suggested improvement
                     Err(_) => {
-                        unreachable!("The bytes are hardcoded in the bin, decode cannot fail")
+                        unreachable!("Checkpoint decode failed - checkpoint files may be corrupted or incompatible")
                     }

Or preferably, make CheckpointManager::new return a Result<Self, Error>.

🤖 Prompt for AI Agents
In `@dash-spv/src/chain/checkpoints.rs` around lines 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.

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: 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.

@ZocoLini ZocoLini force-pushed the refactor/checkpoints branch from c445e6c to 457e3b1 Compare January 21, 2026 18:42
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: 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.

@ZocoLini ZocoLini force-pushed the refactor/checkpoints branch from 457e3b1 to 6593eb2 Compare January 21, 2026 19:03
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: 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 on lines +175 to +178
Err(index) => (
self.checkpoints[index - 1].height,
&self.checkpoints[index - 1].hashed_block_header,
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential index underflow if checkpoint invariant is violated in release builds.

The Err(index) branch accesses checkpoints[index - 1], which would underflow if index == 0. This can only happen if the first checkpoint's height is > 0, which violates the invariant checked by debug_assert at line 154. However, debug_assert is stripped in release builds.

Since new_with_checkpoints is pub(crate), consider upgrading the height-0 check to a runtime assert! to prevent silent UB in release:

🛠️ Suggested improvement
-        debug_assert_eq!(checkpoints[0].height, 0, "The first checkpoint must be at height 0");
+        assert_eq!(checkpoints[0].height, 0, "The first checkpoint must be at height 0");
🤖 Prompt for AI Agents
In `@dash-spv/src/chain/checkpoints.rs` around lines 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.

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.

2 participants