Skip to content

fix(commitment-tree): reject negative sqlite positions - #746

Merged
QuantumExplorer merged 5 commits into
dashpay:developfrom
thepastaclaw:fix-sqlite-negative-positions
Aug 22, 2026
Merged

fix(commitment-tree): reject negative sqlite positions#746
QuantumExplorer merged 5 commits into
dashpay:developfrom
thepastaclaw:fix-sqlite-negative-positions

Conversation

@thepastaclaw

@thepastaclaw thepastaclaw commented May 23, 2026

Copy link
Copy Markdown
Contributor

PR Body

Summary

  • Reject negative SQLite shard_index and position values when reading
    legacy commitment tree rows.
  • Add non-negative CHECK constraints for newly-created SQLite commitment
    tree tables.
  • Use checked u64i64 conversions before binding shard indexes and
    checkpoint positions on SQLite write/query helper paths.
  • Cover schema rejection, legacy corrupt-row handling, and checked-conversion
    overflow failures for shards, checkpoints, and marks-removed positions.

Closes #766.

Validation

  • cargo build --offline -p grovedb-commitment-tree
  • cargo test --offline -p grovedb-commitment-tree --features sqlite — 66 passed
  • cargo fmt -p grovedb-commitment-tree --check
  • cargo clippy --offline -p grovedb-commitment-tree --features sqlite --tests --no-deps — no new warnings (3 pre-existing unused imports in unrelated commitment_frontier/mod.rs)
  • cargo clippy --offline -- -D warnings — fails on pre-existing unrelated dead-code error in grovedb-bulk-append-tree/src/tree/mod.rs:77
  • Pre-commit hook passed on both follow-up commits
  • Pre-PR code review gate: ship

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened SQLite persistence and retrieval to reject negative shard indices and negative checkpoint/removed-mark positions, while still permitting NULL checkpoint positions.
    • Added range-safe conversions between stored SQLite values and application representations to prevent out-of-range data from being interpreted as valid.
  • Tests

    • Added legacy-schema SQLite tests (with an in-memory legacy DB helper) covering negative-value rejection, correct error messaging, and overflow handling.
    • Verified that rejected checkpoint updates do not persist partial changes.

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds checked SQLite integer conversions and non-negative schema constraints for commitment-tree shard indices and checkpoint positions, then applies them to shard and checkpoint read/write paths. It also adds tests for constraint enforcement, legacy corrupted rows, and overflow rejection.

Changes

Negative Value Validation and Overflow Protection

Layer / File(s) Summary
Validation helpers and schema constraints
grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
Adds non_negative_i64_to_u64 and u64_to_i64, and adds CHECK constraints for shard indices and checkpoint positions.
Outbound conversions for write operations
grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
Validates shard indices, checkpoint positions, and removed-mark positions before binding SQL parameters in shard and checkpoint write paths.
Inbound conversions for read operations
grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
Validates loaded shard indices and checkpoint positions before reconstructing store values, and uses an explicit loop to build checkpoint marks.
Schema, legacy-row, and overflow tests
grovedb-commitment-tree/src/client/sqlite_store_tests.rs
Adds helpers and tests for CHECK enforcement, corrupted legacy rows, and rejected overflow cases across shard and checkpoint operations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR replaces unchecked SQLite bindings with checked conversions, keeps negative-row validation, and adds targeted overflow tests for #766.
Out of Scope Changes check ✅ Passed The schema constraints and tests are directly related to the checked-conversion and negative-row validation work, with no clear unrelated changes.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main goal of rejecting negative SQLite position values in commitment-tree storage.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs`:
- Around line 128-134: Wrap rusqlite errors from both stmt.query_map calls and
their row iterations with contextual mapping using .map_err(|e|
Error::CorruptedData(format!("...: {}", e))) so callers get operation-specific
context; specifically, update the first loop around stmt.query_map(...) that
constructs Address::from_parts(Level::from(SHARD_HEIGHT),
non_negative_i64_to_u64("shard_index", index?)?) to map any query_map/row error
into Error::CorruptedData with a message like "querying shard indices" and do
the same for the second query_map loop at the other location (lines ~393-399)
with a descriptive message for that operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0757e2e-a0d6-4211-9377-70c0dca97249

📥 Commits

Reviewing files that changed from the base of the PR and between 9f67e8c and cd1aafd.

📒 Files selected for processing (2)
  • grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
  • grovedb-commitment-tree/src/client/sqlite_store_tests.rs

Comment thread grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.21429% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.50%. Comparing base (eaad013) to head (6644cce).

Files with missing lines Patch % Lines
...itment-tree/src/client/sqlite_store/sql_helpers.rs 98.21% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #746   +/-   ##
========================================
  Coverage    91.49%   91.50%           
========================================
  Files          240      240           
  Lines        67763    67790   +27     
========================================
+ Hits         62003    62030   +27     
  Misses        5760     5760           
Components Coverage Δ
grovedb-core 89.07% <ø> (ø)
merk 92.32% <ø> (ø)
storage 86.52% <ø> (ø)
commitment-tree 96.14% <98.21%> (+0.08%) ⬆️
mmr 96.79% <ø> (ø)
bulk-append-tree 89.82% <ø> (ø)
element 97.38% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@thepastaclaw
thepastaclaw force-pushed the fix-sqlite-negative-positions branch from cd1aafd to 5ca122d Compare May 23, 2026 20:01
@thepastaclaw

thepastaclaw commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

✅ Review complete (commit 851f1da)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code Review

Small, well-scoped defensive PR for the SQLite commitment-tree store: adds CHECK constraints to freshly created tables and validates legacy i64 rows on read before converting to u64. Read coverage spans the natural enumeration paths (sql_last_shard, sql_get_shard_roots, checkpoint loads, mark loads) and the test suite exercises both schema-level rejection and legacy-row rejection. No in-scope blockers or suggestions found.

Note: GitHub does not allow PastaClaw to approve their own PR, so this clean review is posted as a comment review rather than an approval.

Out-of-scope follow-up noted

  • Write-side u64 as i64 casts and direct sql_get_shard index cast remain unchecked — Multiple write paths (sql_put_shard:114, sql_truncate_shards:146, sql_add_checkpoint:213,224, sql_update_checkpoint_with:323,333) and the targeted read path sql_get_shard:57 still use unchecked as i64 casts on u64 values. On the new schema the added CHECK constraints reject these defensively at write time; on legacy schemas a wrapped negative could be written. Pre-existing pattern, not introduced by this PR. The PR's read-side guarantee is enforced where shards/checkpoints are enumerated, which is the realistic legacy-data exposure path. A direct get_shard(addr) whose index wraps to a negative would require a contrived caller. Worth tracking separately to apply i64::try_from(u64) symmetrically at the write boundary and the index-bind in sql_get_shard.
    • Follow-up: Open a follow-up issue to use i64::try_from(u64) for shard_index and position values at the write boundary and the sql_get_shard query bind in grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs.

@thepastaclaw

thepastaclaw commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Acknowledged the out-of-scope follow-up from the clean review: opened #766 to track replacing the remaining unchecked SQLite u64 as i64 binds with checked conversions at the commitment-tree store boundary.

thepastaclaw added a commit to thepastaclaw/grovedb that referenced this pull request Jun 16, 2026
Several write paths in the sqlite shard store bound u64 shard indexes
and checkpoint/mark positions to sqlite via unchecked \`as i64\` casts.
For values above i64::MAX this silently wraps to a negative i64 and is
rejected by the (post dashpay#746) CHECK constraints at write time, but the
error surfaces as a generic sqlite constraint failure rather than as a
clear out-of-range error from our code.

Add a \`u64_to_i64\` helper that returns
\`SqliteShardStoreError::Serialization\` on overflow and apply it to:
- sql_get_shard / sql_put_shard shard_index binds
- sql_truncate_shards shard_index bind
- sql_add_checkpoint position and mark position binds
- sql_update_checkpoint_with position and mark position binds

Overflow is now detected before any sqlite work happens, so add/update
fail without partially writing a checkpoint row.

The negative-row read validation added in dashpay#746 (the matching
\`non_negative_i64_to_u64\` helper used by read paths) is unchanged.

Adds targeted tests for the failure paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
grovedb-commitment-tree/src/client/sqlite_store_tests.rs (1)

715-737: ⚡ Quick win

Add the symmetric update test for overflowing removed marks.

sql_update_checkpoint_with now validates marks_removed() positions before starting the rewrite transaction, but the tests only cover overflowing the updated tree position. Add the mark-position case to lock in the no-partial-rewrite guarantee for Lines 335-340 in sql_helpers.rs.

Test coverage patch
     fn test_update_checkpoint_with_rejects_position_overflow() {
         let mut store = test_store();
         store
             .add_checkpoint(1, Checkpoint::at_position(Position::from(10)))
             .expect("add");
@@
             TreeState::AtPosition(Position::from(10))
         );
     }
+
+    #[test]
+    fn test_update_checkpoint_with_rejects_mark_position_overflow() {
+        let mut store = test_store();
+        let mut original_marks = BTreeSet::new();
+        original_marks.insert(Position::from(7));
+        store
+            .add_checkpoint(
+                1,
+                Checkpoint::from_parts(
+                    TreeState::AtPosition(Position::from(10)),
+                    original_marks.clone(),
+                ),
+            )
+            .expect("add");
+
+        let err = store
+            .update_checkpoint_with(&1, |cp| {
+                let mut marks = BTreeSet::new();
+                marks.insert(Position::from((i64::MAX as u64) + 1));
+                *cp = Checkpoint::from_parts(TreeState::AtPosition(Position::from(10)), marks);
+                Ok(())
+            })
+            .expect_err("mark position above i64::MAX should overflow");
+        assert_overflow_error(&err, "mark position");
+
+        let loaded = store.get_checkpoint(&1).expect("get").expect("exists");
+        assert_eq!(
+            loaded.tree_state(),
+            TreeState::AtPosition(Position::from(10))
+        );
+        assert_eq!(loaded.marks_removed(), &original_marks);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@grovedb-commitment-tree/src/client/sqlite_store_tests.rs` around lines 715 -
737, Add a new test function that mirrors the existing
test_update_checkpoint_with_rejects_position_overflow test but covers the
marks_removed position overflow case instead of the tree position overflow case.
The new test should create a checkpoint with marks_removed, attempt to update it
with an overflow position for marks_removed (using a value exceeding i64::MAX),
verify that the appropriate overflow error is returned, and confirm that the
original checkpoint remains unchanged because the validation in sql_helpers.rs
lines 335-340 happens before the rewrite transaction starts. This ensures
symmetric test coverage for both position validation paths in the
update_checkpoint_with operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@grovedb-commitment-tree/src/client/sqlite_store_tests.rs`:
- Around line 715-737: Add a new test function that mirrors the existing
test_update_checkpoint_with_rejects_position_overflow test but covers the
marks_removed position overflow case instead of the tree position overflow case.
The new test should create a checkpoint with marks_removed, attempt to update it
with an overflow position for marks_removed (using a value exceeding i64::MAX),
verify that the appropriate overflow error is returned, and confirm that the
original checkpoint remains unchanged because the validation in sql_helpers.rs
lines 335-340 happens before the rewrite transaction starts. This ensures
symmetric test coverage for both position validation paths in the
update_checkpoint_with operation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2a81d5b-4397-483d-a351-066bba9be282

📥 Commits

Reviewing files that changed from the base of the PR and between 851f1da and 2c2e6d5.

📒 Files selected for processing (2)
  • grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs
  • grovedb-commitment-tree/src/client/sqlite_store_tests.rs

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

Leaving a quick note on the current codecov/project red check so it does not look like an untriaged PR regression.

The actionable patch gate is green:

  • codecov/patch: 98.21% of changed lines covered, target 90.00%.

The failing codecov/project report appears to be an upload/reporting skew rather than this diff reducing coverage: Codecov says the head report has one upload fewer than the base (BASE 3, HEAD 2) and shows broad project/component drops across unrelated packages while this PR only changes the commitment-tree SQLite helpers/tests. I’m leaving the branch as-is rather than mutating it just to shake CI.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

thepastaclaw and others added 3 commits July 7, 2026 03:36
Several write paths in the sqlite shard store bound u64 shard indexes
and checkpoint/mark positions to sqlite via unchecked \`as i64\` casts.
For values above i64::MAX this silently wraps to a negative i64 and is
rejected by the (post dashpay#746) CHECK constraints at write time, but the
error surfaces as a generic sqlite constraint failure rather than as a
clear out-of-range error from our code.

Add a \`u64_to_i64\` helper that returns
\`SqliteShardStoreError::Serialization\` on overflow and apply it to:
- sql_get_shard / sql_put_shard shard_index binds
- sql_truncate_shards shard_index bind
- sql_add_checkpoint position and mark position binds
- sql_update_checkpoint_with position and mark position binds

Overflow is now detected before any sqlite work happens, so add/update
fail without partially writing a checkpoint row.

The negative-row read validation added in dashpay#746 (the matching
\`non_negative_i64_to_u64\` helper used by read paths) is unchanged.

Adds targeted tests for the failure paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thepastaclaw and others added 2 commits July 7, 2026 03:36
Follow-up cleanup on the prior commit:

- In `sql_add_checkpoint` and `sql_update_checkpoint_with`, collect mark
  positions through `Iterator::collect::<Result<_, _>>()?` instead of an
  explicit `Vec::with_capacity` + `for ... push` loop. Behavior is
  identical: `collect` short-circuits on the first overflow and returns
  before `conn.unchecked_transaction()?` is called, preserving the
  "no partial write on overflow" property.

- In sqlite_store_tests.rs, factor the repeated overflow-message
  assertion in the six new tests into a small `assert_overflow_error`
  helper, so each test ends with one named assertion line.

No public API change; all 38 sqlite store tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a symmetric test that verifies sql_update_checkpoint_with rejects
overflowing marks_removed positions and preserves the original
checkpoint, mirroring the existing position-overflow case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer force-pushed the fix-sqlite-negative-positions branch from 54a896b to 6644cce Compare July 6, 2026 20:37
@QuantumExplorer

Copy link
Copy Markdown
Member

This is Claude. Rebased this branch onto current develop (maintainer edit) so it sits on top of the recently-merged changes to the same module — notably #767, which renamed sql_list_checkpoints to sql_list_checkpoints_ascending and added a checkpoint-pruning regression test to sqlite_client_tests.rs, plus #755/#741/#728/#750/#742. No changes to this PR's own commits beyond the rebase (5 commits replayed cleanly). Verified locally after rebase: cargo test -p grovedb-commitment-tree --features sqlite — 68 passed, 0 failed, covering both this PR's new checked-conversion tests and the #767 pruning regression together.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@QuantumExplorer
QuantumExplorer merged commit b8e1e5e into dashpay:develop Aug 22, 2026
11 checks passed
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.

Use checked SQLite conversions for commitment-tree positions

2 participants