feat: paginated position-range reads with proofs at the BulkAppendTree layer - #786
feat: paginated position-range reads with proofs at the BulkAppendTree layer#786QuantumExplorer wants to merge 8 commits into
Conversation
…f helpers Add the position-range read seam at the shared BulkAppendTree layer so every BulkAppendTree-backed element type (CommitmentTree today, the planned PrivateDocumentStore / DataCommitmentTree) inherits it: - BulkAppendTree::get_range(start, limit): fetch entries for [start, start + limit) clamped to total_count, returned as a RangePage (entries + total_count). Chunk-aligned: each completed chunk overlapping the range is read and deserialized exactly once, so a page costs O(chunks touched) blob reads instead of O(entries) random reads. - position_range_query(start, limit): the canonical Query (8-byte big-endian position keys) shared by prover and verifier. - BulkAppendTreeProof::generate_for_range / verify_range: paginated proof round-trip over the existing chunk-MMR + dense-buffer proof, with completeness enforced; absence past the end falls out of the authenticated total_count (position >= count), not per-position absence proofs. - CommitmentTree::get_range pass-through for the shielded-pool scanning path. Part of work-plan item 4 of #784. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_HASH
A V1 proof descending into an empty BulkAppendTree could never verify:
insert commits the element with a NULL_HASH child hash (there is no
bulk state until the first append) and verify_grovedb's integrity walk
mirrors that, but verify_bulk_append_lower_layer returned the
domain-tagged empty state root blake3("bulk_state" || 0*32 || 0*32),
so the combine_hash chain check always failed.
Return NULL_HASH for a zero-count BulkAppendTree element instead,
matching what the writer commits. This is sound because
verify_and_compute_root has already rejected any proof carrying chunk
or buffer data for a zero-count tree. CommitmentTree keeps the computed
value: its insert commits EMPTY_COMMITMENT_TREE_STATE_ROOT, which folds
in the computed empty bulk root.
Verification-only change on an element type not reachable in any
shipped grove version's state; no wire format or state root changes,
so no GROVE_V* gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nly trees Wire the BulkAppendTree range-read seam through GroveDB, per work-plan item 4 of #784 — clients of append-only stores walk "all entries since my cursor" in pages: - GroveDb::bulk_get_range / GroveDb::commitment_tree_get_range: chunk-aligned page fetch returning a RangePage (entries + total_count), O(chunks touched) instead of O(entries) random reads. - PathQuery::new_bulk_position_range: the canonical query shape for a page — element key plus an 8-byte big-endian position-range subquery — derived identically by prover and verifier from (start, limit). - GroveDb::prove_bulk_position_range: proves one page via the existing V1 proof layering (ProofBytes::BulkAppendTree / CommitmentTree); no wire-format change, so no new GROVE_V* gate. - GroveDb::verify_bulk_position_range_proof: verifies the page entries (ascending, contiguous, complete) and extracts the authenticated total_count from the same proof bytes by subset-verifying the element itself. Absence beyond the end falls out of the provable count (position >= total_count) — no per-position absence proofs. The seam lives at the shared BulkAppendTree layer and dispatches on the element type, so the planned PrivateDocumentStore (#784) and DataCommitmentTree (#783) pick it up by adding their element variants to the prove/verify match arms. Tests cover round-trips across chunk boundaries, empty ranges, ranges past the end, single-entry pages, large multi-chunk pages, empty trees, nested paths, wrong-element errors, narrower-proof rejection, and the cursor-walk scan pattern for both BulkAppendTree and CommitmentTree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Benchmark the append-only scanning hot path on a 4096-entry BulkAppendTree (chunk size 64, 96-byte entries): single-page reads, per-page proof generation and verification at page sizes 16/256/1024, and a full proved cursor-walk of the tree at page size 256. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 25 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughAdded paginated range reads and range proofs for ChangesPosition-range tree access
Canonical range proofs
GroveDB integration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Client
participant GroveDb
participant BulkAppendTree
participant ProofVerifier
Client->>GroveDb: request position range
GroveDb->>BulkAppendTree: read or generate range proof
BulkAppendTree-->>GroveDb: RangePage or serialized proof
GroveDb->>ProofVerifier: verify range proof and total_count
ProofVerifier-->>GroveDb: verified RangePage
GroveDb-->>Client: range entries and total_count
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
grovedb-bulk-append-tree/src/tree/fetch.rs (1)
96-115: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist MMR construction out of the chunk loop.
get_chunk_valuerebuildsMmrStoreandMMR::new_with_overlay(which clonesmmr_overlay) on every call. For a page that touches N chunks, this loop pays N MMR reconstructions and N overlay clones instead of one.BulkAppendTreeProof::generateingrovedb-bulk-append-tree/src/proof/mod.rsalready avoids this: it builds the MMR once and reuses it across all queried chunk indices through aget_nodeclosure.Build the MMR once before the loop and read each chunk's leaf from that single instance, so a wide page (as exercised by
test_range_roundtrip_large_multi_chunk_pageandbulk_range_scan_benchmark.rs) does not pay a repeated overlay clone per chunk.♻️ Proposed fix: build the MMR once before the chunk loop
if start < chunk_end { let first_chunk = start / epoch_size; let last_chunk = (chunk_end - 1) / epoch_size; + let mmr_store = MmrStore::with_key_size(&self.dense_tree.storage, MmrKeySize::U32); + let mmr = MMR::new_with_overlay(self.mmr_size(), &mmr_store, self.mmr_overlay.clone()); for chunk_idx in first_chunk..=last_chunk { - let blob = self.get_chunk_value(chunk_idx)?.ok_or_else(|| { - BulkAppendError::CorruptedData(format!( - "missing chunk blob for index {}", - chunk_idx - )) - })?; + let mmr_pos = leaf_to_pos(chunk_idx); + let node = mmr.batch.element_at_position(mmr_pos).unwrap().map_err(|e| { + BulkAppendError::MmrError(format!( + "failed to read MMR node for chunk {}: {}", + chunk_idx, e + )) + })?; + let blob = node.and_then(|n| n.into_value()).ok_or_else(|| { + BulkAppendError::CorruptedData(format!( + "missing chunk blob for index {}", + chunk_idx + )) + })?; let chunk_entries = deserialize_chunk_blob(&blob)?; let chunk_start = chunk_idx * epoch_size; for (i, value) in chunk_entries.into_iter().enumerate() { let pos = chunk_start + i as u64; if pos >= start && pos < chunk_end { entries.push((pos, value)); } } } }🤖 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-bulk-append-tree/src/tree/fetch.rs` around lines 96 - 115, Refactor the range-fetch logic around the chunk loop to construct the MmrStore and MMR::new_with_overlay once before iterating chunks, then reuse that instance to retrieve each chunk leaf instead of calling get_chunk_value per chunk. Follow the reuse pattern in BulkAppendTreeProof::generate while preserving missing-chunk corruption errors and existing deserialization and filtering behavior.grovedb/src/tests/bulk_append_tree_tests.rs (1)
2411-2432: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a chunk-shared disjoint-range case.
This test covers a narrow proof rejected for a wider range. A second case is worth pinning: prove
[0, 2)and then verify[2, 4). Both ranges sit inside chunk 0, and the proof carries the whole chunk blob, so the completeness check may accept the second request. The behavior is defensible, but a test makes the intended semantics explicit and prevents an accidental change later.🤖 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/src/tests/bulk_append_tree_tests.rs` around lines 2411 - 2432, Extend test_bulk_position_range_proof_wrong_range_rejected with a shared-chunk disjoint-range case: generate a proof for [0, 2) and verify it against [2, 4), asserting the result matches the intended semantics that the whole chunk blob permits this request. Keep the existing wider-range rejection assertion unchanged.grovedb/src/tests/commitment_tree_tests.rs (1)
3015-3035: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a CommitmentTree wrong-range rejection test.
grovedb/src/tests/bulk_append_tree_tests.rshastest_bulk_position_range_proof_wrong_range_rejected, but no CommitmentTree equivalent exists here. The CommitmentTree path uses a different proof envelope (ProofBytes::CommitmentTreewith the sinsemilla prefix) and a different child-hash derivation, so the bulk test does not cover it. Add a test that proves a narrow range and expects verification of a wider range to fail.💚 Proposed test
#[test] fn test_commitment_tree_position_range_proof_wrong_range_rejected() { let grove_version = GroveVersion::latest(); let (db, _) = make_ct_db_with_notes(10); // Proof generated for [0, 2) (inside chunk 0) must not verify a request // for [0, 6), which also needs chunk 1. let narrow_proof = db .prove_bulk_position_range( vec![b"root".to_vec()], b"pool", 0, 2, None, grove_version, ) .unwrap() .expect("prove narrow range"); GroveDb::verify_bulk_position_range_proof( &narrow_proof, vec![b"root".to_vec()], b"pool", 0, 6, grove_version, ) .expect_err("proof for a narrower range must be rejected"); }🤖 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/src/tests/commitment_tree_tests.rs` around lines 3015 - 3035, Add a CommitmentTree wrong-range rejection test near test_commitment_tree_position_range_proof_across_chunk_boundary. Generate a proof for positions 0 through 2 using prove_bulk_position_range, then verify it against the wider range 0 through 6 with GroveDb::verify_bulk_position_range_proof and assert verification fails.grovedb/src/operations/proof/generate.rs (1)
225-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a version gate to this new public entry point.
Every other public proof entry point in this file starts with
check_grovedb_v0_with_cost!(prove_query_many,prove_query,prove_trunk_chunk,prove_branch_chunk).prove_bulk_position_rangehas none. The delegatedprove_querycall still enforces its own gate, so the method is not unguarded in practice. Adding an explicit gate keeps the new API in the version table and lets it be disabled independently later. The same applies toGroveDb::verify_bulk_position_range_proofingrovedb/src/operations/proof/verify.rs.Based on coding guidelines: "When adding functionality, check GroveDB version compatibility".
🤖 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/src/operations/proof/generate.rs` around lines 225 - 234, Add an explicit GroveDB version gate at the start of both prove_bulk_position_range and GroveDb::verify_bulk_position_range_proof, matching the check_grovedb_v0_with_cost! pattern used by the other public proof entry points. Ensure each new API is independently represented in the version compatibility checks before executing its existing logic.Source: Coding guidelines
🤖 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-bulk-append-tree/src/tree/fetch.rs`:
- Around line 96-115: Refactor the range-fetch logic around the chunk loop to
construct the MmrStore and MMR::new_with_overlay once before iterating chunks,
then reuse that instance to retrieve each chunk leaf instead of calling
get_chunk_value per chunk. Follow the reuse pattern in
BulkAppendTreeProof::generate while preserving missing-chunk corruption errors
and existing deserialization and filtering behavior.
In `@grovedb/src/operations/proof/generate.rs`:
- Around line 225-234: Add an explicit GroveDB version gate at the start of both
prove_bulk_position_range and GroveDb::verify_bulk_position_range_proof,
matching the check_grovedb_v0_with_cost! pattern used by the other public proof
entry points. Ensure each new API is independently represented in the version
compatibility checks before executing its existing logic.
In `@grovedb/src/tests/bulk_append_tree_tests.rs`:
- Around line 2411-2432: Extend
test_bulk_position_range_proof_wrong_range_rejected with a shared-chunk
disjoint-range case: generate a proof for [0, 2) and verify it against [2, 4),
asserting the result matches the intended semantics that the whole chunk blob
permits this request. Keep the existing wider-range rejection assertion
unchanged.
In `@grovedb/src/tests/commitment_tree_tests.rs`:
- Around line 3015-3035: Add a CommitmentTree wrong-range rejection test near
test_commitment_tree_position_range_proof_across_chunk_boundary. Generate a
proof for positions 0 through 2 using prove_bulk_position_range, then verify it
against the wider range 0 through 6 with
GroveDb::verify_bulk_position_range_proof and assert verification fails.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 178317cb-131d-4b2b-82d6-99500b180279
📒 Files selected for processing (18)
grovedb-bulk-append-tree/src/lib.rsgrovedb-bulk-append-tree/src/proof/mod.rsgrovedb-bulk-append-tree/src/proof/tests.rsgrovedb-bulk-append-tree/src/tree/fetch.rsgrovedb-bulk-append-tree/src/tree/mod.rsgrovedb-bulk-append-tree/src/tree/tests.rsgrovedb-commitment-tree/src/commitment_tree/mod.rsgrovedb-commitment-tree/src/lib.rsgrovedb/Cargo.tomlgrovedb/benches/bulk_range_scan_benchmark.rsgrovedb/src/lib.rsgrovedb/src/operations/bulk_append_tree.rsgrovedb/src/operations/commitment_tree.rsgrovedb/src/operations/proof/generate.rsgrovedb/src/operations/proof/verify.rsgrovedb/src/query/mod.rsgrovedb/src/tests/bulk_append_tree_tests.rsgrovedb/src/tests/commitment_tree_tests.rs
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #786 +/- ##
===========================================
+ Coverage 92.11% 92.13% +0.01%
===========================================
Files 257 257
Lines 77936 78642 +706
===========================================
+ Hits 71794 72459 +665
- Misses 6142 6183 +41
🚀 New features to boost your workflow:
|
- Build the chunk MMR once per get_range call instead of once per chunk: going through get_chunk_value re-cloned the MMR overlay for every chunk in the page, exactly the repeated work chunk-alignment is meant to avoid. Reuses the same single-MMR pattern as BulkAppendTreeProof::generate. - Add explicit version gates (prove_bulk_position_range, verify_bulk_position_range_proof) matching the other public proof entry points; 0 across GROVE_V1..V4, so no behavior change — the delegated prove_query/verify_query gates still apply transitively. - Pin the shared-chunk disjoint-range semantics with a test: a proof generated for [0, 2) verifies a request for [2, 4) inside the same chunk, because chunk-aligned proofs carry the whole authenticated blob. Rejection of ranges needing an unproved chunk is unchanged. - Add the CommitmentTree-envelope wrong-range rejection test mirroring the BulkAppendTree one (different proof envelope and child-hash derivation, so the bulk test did not cover it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This is Claude. Addressed all four review comments in ad8c8a7:
Validation after the changes: |
codecov/patch flagged the new code at 80.55% against the 90% bar. Cover the arms that are reachable without contrived proofs: - get_range corruption paths: storage claiming chunks / buffered entries it does not hold must error, not silently skip entries. - Version-gate rejections for prove_bulk_position_range and verify_bulk_position_range_proof under an unknown feature version. - verify_bulk_position_range_proof adversarial inputs: a canonical range proof over a plain Tree with 8-byte item keys (element type must be rejected when extracting total_count) and over a nonexistent key (a proof binding no element must not invent a total_count). The remaining uncovered lines are defense-in-depth InvalidProof arms (root-mismatch between sub-proofs of the same bytes, non-8-byte / non-item / non-contiguous rows under a genuine bulk element) that cannot be reached through any proof that survives the lower layer's own checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MemStorageContext (codecov-ignored test util) grows a fail_gets switch that makes every read error, so the chunk-lookup MmrError arm in get_range is exercised: a broken backing store must surface as an error, not a panic or a silent empty page. The buffer-side ok_or_else arm stays uncovered by design: the dense tree's get() errors (never returns Ok(None)) for a missing value below its count, so that arm is unreachable defensive depth, same as the remaining InvalidProof arms in verify_bulk_position_range_proof. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This is Claude. Follow-up on the coverage gate: codecov/patch flagged the new code at 80.55% against the repo's 90% bar, now green at ~90.1% after two commits:
The remaining uncovered lines are deliberately left as-is: they are defense-in-depth All 12 checks are green: full suite (4559 tests run, 4 skipped), clippy |
QuantumExplorer
left a comment
There was a problem hiding this comment.
I found two actionable issues in the new range-read path. The focused range/proof tests pass, but neither issue is covered by the current tests.
| /// | ||
| /// Absence needs no lookup: positions `>= total_count` do not exist, so | ||
| /// a page shorter than `limit` means the end of the tree was reached. | ||
| pub fn get_range(&self, start: u64, limit: u16) -> Result<RangePage, BulkAppendError> { |
There was a problem hiding this comment.
[P1] Preserve range-read storage costs
get_range returns a plain Result, so the MMR element_at_position(...).unwrap() and every buffer get discard their CostResult.cost. Consequently, bulk_get_range and commitment_tree_get_range report none of the page's seeks or loaded bytes—up to 65,535 uncharged buffer reads. Please return and aggregate a CostResult through both GroveDB wrappers so cost limits reflect the actual work.
There was a problem hiding this comment.
This is Claude. Fixed in acb99bd: get_range now returns a CostResult — the chunk-MMR node reads and every dense-buffer read charge their seeks and loaded bytes (buffer reads go through dense_tree.get directly so nothing is discarded), and the costs aggregate through CommitmentTree::get_range, bulk_get_range, and commitment_tree_get_range. Added test_bulk_get_range_reports_storage_costs, which asserts nonzero seek/loaded-byte costs on a chunk+buffer-spanning page and that an 11-entry page loads more bytes than a 1-entry page.
| chunk_idx | ||
| )) | ||
| })?; | ||
| let chunk_entries = deserialize_chunk_blob(&blob)?; |
There was a problem hiding this comment.
[P2] Validate completed chunk length
deserialize_chunk_blob accepts any valid encoded entry count, but this loop assumes each completed chunk contains exactly epoch_size entries. A short chunk silently omits positions; an oversized chunk can overlap positions from the next chunk. The returned page then violates its contiguous-page contract and cursor scans can stall before total_count. Please reject the chunk as corrupted unless chunk_entries.len() == epoch_size as usize.
There was a problem hiding this comment.
This is Claude. Fixed in acb99bd: get_range now rejects a completed chunk as CorruptedData unless it deserializes to exactly epoch_size entries, with a test that tampers the MMR overlay with both a short and an oversized blob. One note on scope: the check is deliberately only on this raw read path — in proof verification the chunk bytes are bound to the state root (a wrong-length blob changes the root and fails the comparison), and the audit NOTE in proof/mod.rs documents that adding a length check there is redundant by design, so that side is unchanged.
Address review on paginated range reads: - get_range now returns a CostResult: the chunk-MMR node reads and each dense-buffer read charge their seeks and loaded bytes, aggregated through CommitmentTree::get_range, bulk_get_range, and commitment_tree_get_range, so cost limits reflect a page's actual work instead of treating up to 65,535 buffer reads as free. The buffer reads go through dense_tree.get directly so their costs are captured. - get_range rejects a completed chunk as corrupted unless it holds exactly epoch_size entries: a short blob would silently omit positions and an oversized one would overlap the next chunk, breaking the contiguous-page contract and stalling cursor scans. Unlike proof verification — where chunk bytes are bound to the state root and a length check is redundant (see the NOTE in proof/mod.rs) — this raw read path has no root comparison backing it. Tests: a tampered-overlay chunk with the wrong entry count (short and oversized) is rejected with CorruptedData, and bulk_get_range reports nonzero seek/loaded-byte costs that grow with page size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements work-plan item 4 of #784: paginated position-range reads with proofs at the
BulkAppendTreelayer — the scanning hot path where clients of append-only stores walk "all entries since my cursor" in pages.What's added
Crate layer (
grovedb-bulk-append-tree) — the shared seamBulkAppendTree::get_range(start, limit)→RangePage { entries, total_count }. Chunk-aligned: each completed chunk overlapping the page is read and deserialized exactly once, so a page costs O(chunks touched) blob reads plus one read per buffer entry — not O(entries) random reads.position_range_query(start, limit): the canonical query shape (8-byte big-endian position keys), derived identically by prover and verifier from(start, limit).BulkAppendTreeProof::generate_for_range/verify_range: paginated proof round-trip over the existing chunk-MMR + dense-buffer proof, completeness enforced.CommitmentTree::get_rangepass-through for the shielded-pool scanning path.GroveDB layer
GroveDb::bulk_get_rangeandGroveDb::commitment_tree_get_range.PathQuery::new_bulk_position_range(path, key, start, limit): canonical page query (element key + position-range subquery).GroveDb::prove_bulk_position_range: proves one page via the existing V1 proof layering (ProofBytes::BulkAppendTree/ProofBytes::CommitmentTree).GroveDb::verify_bulk_position_range_proof: verifies the page entries (ascending, contiguous, complete) and extracts the authenticatedtotal_countfrom the same proof bytes by subset-verifying the element itself. Absence beyond the end falls out of the provable count —position >= total_countdoes not exist — so ranges past the end need no per-position absence proofs; a page shorter thanlimitmeans the scan caught up with the tip.The machinery lives at the shared
BulkAppendTreelayer and dispatches on element type, so the upcomingPrivateDocumentStore(#784) andDataCommitmentTree(#783) inherit it by adding their element variants to the prove/verify match arms.Fix uncovered along the way
A V1 proof descending into an empty
BulkAppendTreecould never verify: insert commits the element with aNULL_HASHchild hash (mirrored byverify_grovedb's integrity walk), butverify_bulk_append_lower_layerreturned the domain-taggedblake3("bulk_state" || 0*32 || 0*32), so thecombine_hashchain check always failed. The verifier now returnsNULL_HASHfor a zero-countBulkAppendTreeelement (sound:verify_and_compute_rootalready rejects any proof carrying data for a zero-count tree).CommitmentTreeis untouched —EMPTY_COMMITMENT_TREE_STATE_ROOTdeliberately folds in the computed empty bulk root.Versioning / compatibility
GROVE_V*flag is introduced.Tests & benches
get_rangeand proof round-trips across chunk boundaries, empty ranges (limit 0), ranges past the end, single-entry pages, large multi-chunk pages, empty trees, saturating-overflow starts, tampered-root and missing-chunk rejection, and a cursor-walk covering the whole tree.BulkAppendTreeandCommitmentTreethroughprove_bulk_position_range/verify_bulk_position_range_proof, plus nested paths, wrong-element errors, and narrower-proof rejection — every round-trip asserts the proof root equals the live DB root.bulk_range_scan_benchmark: paged read / per-page prove / per-page verify at page sizes 16/256/1024, and a full proved cursor-walk (4096 entries, chunk size 64, 96-byte entries).Validation
cargo nextest run --workspace --all-features: 4536 passed, 0 failed, 4 skippedcargo clippy --workspace --all-features -- -D warnings: cleancargo check --workspace --all-features --all-targets: clean (bench compiles)cargo fmt --all --check: clean--testmode: all sections pass🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests