Skip to content

fix: verify-only access to indexed-axis proofs + bind subset lower layers - #781

Merged
QuantumExplorer merged 2 commits into
developfrom
feat/indexed-tree-batch-multi-axis
Aug 2, 2026
Merged

fix: verify-only access to indexed-axis proofs + bind subset lower layers#781
QuantumExplorer merged 2 commits into
developfrom
feat/indexed-tree-batch-multi-axis

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

Two follow-ups to the indexed-tree family (#657), found while integrating it into Dash Platform:

1. Expose indexed-axis proof verification to verify-only builds

The verify-side entry points of the indexed-axis proof envelope (verify_indexed_axis_top_k and friends) were minimal-gated at the module level, so a consumer compiling with --no-default-features --features verify (exactly what Dash Platform's drive crate does for its proof-verification layer) could not reach them. The prove-side items are now gated individually and the module is open to both feature sets, mirroring how the rest of the proof code splits prover from verifier. Verified with a reachability probe: without the change, GroveDb::verify_indexed_count_top_k and the other verify entry points fail to resolve under a verify-only build.

2. Bind, don't reject, a lower layer with no query below it

#657 added a hard rejection in verify_layer_proof_v1: a V1 proof supplying a lower layer for a tree the query doesn't descend into errored with "the element bytes would be unbound". The underlying concern is real — a KVValueHash-family node hashes only (key, value_hash), so reporting the element without binding it would let a prover attach a dummy layer and swap in forged bytes under a genuine root hash.

But the check conflates that attack with the normal shape of a subset verification: verify_subset_query exists precisely to run a narrow query against a proof generated for a wider one, and any such proof legitimately descends below the narrow query. The rejection broke previously-passing verifications downstream (Dash Platform extracts a commitment tree's total_count from the same proof bytes that prove a note fetch — a single-key, no-subquery subset query that stops at the tree element).

The fix binds instead of rejecting: the lower layer is consumed for its root hash and the existing combine_hash(H(value), child_root) chain check does the binding; only the reporting differs. Succinct mode still rejects outright with the original message. The result is strictly stronger than v5.0.1, which bound nothing on this path — pinned by two tamper tests (a dummy layer, and a sibling subtree's real-but-wrong layer, both rejected in subset mode).

Tests

  • 3 new tests (plain-Tree repro, two-tamper binding test, and the exact commitment-tree subset shape that broke downstream), all failing before the fix with the reported error verbatim
  • cargo test -p grovedb --lib: 2535 passed, 0 failed
  • cargo test --workspace, cargo clippy --workspace --all-features -- -D warnings, cargo check -p grovedb --no-default-features --features verify: all clean

Known related gap (not addressed here)

Terminally-reported CommitmentTree / MmrTree / BulkAppendTree / DenseAppendOnlyFixedSizeTree elements (no lower layer at all) are bound by nothing in V1 proofs — pre-existing at v5.0.1, independent of this change, and being worked separately since the fix touches the proof format.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved proof verification for parent-level queries and lower-layer root validation.
    • Ensured subset verification correctly reports requested elements while rejecting mismatched or unrequested proof layers.
    • Preserved authentication for empty targets through the verification chain.
  • Compatibility

    • Made verification-only builds available without enabling proof generation.
  • Tests

    • Added regression coverage for commitment-tree queries, nested proofs, strict verification, and invalid lower-layer proofs.

QuantumExplorer and others added 2 commits August 2, 2026 17:28
…builds

The verify-side entry points of the indexed-axis proof envelope
(verify_indexed_axis_top_k and friends) were minimal-gated as a module,
so a consumer compiling with --no-default-features --features verify
could not reach them. Gate the prove-side items individually instead and
open the module to both feature sets, mirroring how the rest of the
proof code splits prover from verifier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #657 added a hard rejection in `verify_layer_proof_v1`: if a V1 proof
carries a lower layer for a tree whose key the query stops at, verification
fails with "the element bytes would be unbound". The concern is real — a
`KVValueHash`-family node hashes only `(key, value_hash)`, so reporting the
element without a child-hash check would let a prover attach a dummy lower
layer and swap in forged element bytes under a genuine root hash. At v5.0.1
that path pushed the element with no binding at all.

But "the proof descends below the query" is the ordinary shape of a SUBSET
verification, not an attack: `verify_subset_query` exists to run a narrower
query against a proof generated for a wider one. Rejecting it broke every
caller that reads a tree element out of a proof that descended into it. Dash
Platform hit this on three shielded-notes tests, which pull a
`CommitmentTree`'s total note count from the note-fetch proof they already
hold via a single-key, no-subquery, limit-1 subset query.

Bind the element instead of refusing it. The lower layer is now consumed for
its root hash in both cases, and the existing `combine_hash(H(value),
child_root)` chain check does the binding; only the reporting differs. With
no query below, none of the lower layer's rows belong in the result set, so
the tree element itself is reported — under its PARENT path, matching
query_raw and the terminal arm. Every lower-layer flavour already derives its
root independently of the query (the query only selects rows), so the
existing MMR / BulkAppend / CommitmentTree / DenseTree verifiers take a
`report_contents` flag and return the root early; Merk layers get their root
from an empty query, which matches nothing and consumes no limit.

Succinct mode (`verify_query`) still rejects outright: for that query the
layer is data the caller never asked for. The result is strictly stronger
than v5.0.1, which bound nothing here.

Tests: subset verification of a tree element against a descending proof, for
both a plain Tree and the exact Platform CommitmentTree note-count shape; two
tamper tests proving the binding is load-bearing in subset mode — a dummy
lower layer and a sibling subtree's real-but-wrong layer are both rejected.
All three fail before this change with the reported error.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change separates proof generation from verification feature gates. V1 verification now supports parent-only queries over nested proofs by authenticating lower-layer roots without returning child rows. Regression tests cover result reporting, strictness, and proof binding.

Changes

V1 proof verification and feature gating

Layer / File(s) Summary
Separate proof generation and verification features
grovedb/src/operations/proof/indexed_axis/axis_api.rs, grovedb/src/operations/proof/indexed_axis/mod.rs, grovedb/src/operations/proof/mod.rs
Indexed-axis proof wrappers and proof generation require minimal. Indexed-axis verification types and entry points compile with minimal or verify.
Verify parent queries with lower-layer roots
grovedb/src/operations/proof/verify.rs
V1 verification derives authenticated lower-layer roots without returning child rows. Parent elements include the verified lower-layer hash, while descending queries continue to report queried contents.
Validate subset and proof binding behavior
grovedb/src/tests/commitment_tree_tests.rs, grovedb/src/tests/succinctness_gap_test.rs
Regression tests cover parent-level CommitmentTree results, strict versus subset verification, and rejection of dummy or mismatched lower-layer proofs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Query
  participant verify_v1
  participant LowerLayerVerifier
  participant ParentResult
  Query->>verify_v1: submit parent-only query and proof
  verify_v1->>LowerLayerVerifier: validate lower layer without reporting contents
  LowerLayerVerifier-->>verify_v1: return authenticated child root
  verify_v1->>ParentResult: bind child root to parent element
  ParentResult-->>Query: return parent result with limit handling
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies both main changes: verify-only indexed-axis proof access and lower-layer binding during subset verification.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/indexed-tree-batch-multi-axis

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.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.10526% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.14%. Comparing base (c9cd467) to head (e2168c1).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
grovedb/src/operations/proof/verify.rs 92.10% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #781      +/-   ##
===========================================
- Coverage    92.14%   92.14%   -0.01%     
===========================================
  Files          254      254              
  Lines        77692    77750      +58     
===========================================
+ Hits         71588    71641      +53     
- Misses        6104     6109       +5     
Components Coverage Δ
grovedb-core 90.40% <92.10%> (+<0.01%) ⬆️
merk 92.89% <ø> (ø)
storage 87.00% <ø> (ø)
commitment-tree 96.05% <ø> (ø)
mmr 96.79% <ø> (ø)
bulk-append-tree 89.82% <ø> (ø)
element 97.95% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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/src/tests/succinctness_gap_test.rs (1)

405-428: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the error variant so the tampers prove the binding.

Both tampers assert only is_err(). Tamper 1 can fail inside execute_proof while decoding the empty proof bytes, which does not exercise the combine_hash check. Tamper 2 supplies a structurally valid sibling proof, so it must fail at the lower-layer hash comparison. Match on Error::InvalidProof and check for the mismatch message. The test then proves the binding rather than any rejection.

♻️ Proposed assertion tightening for tamper 2
     let swapped_bytes = bincode::encode_to_vec(&swapped, config).expect("re-encode");
-    assert!(
-        GroveDb::verify_subset_query(&swapped_bytes, &narrow_query, grove_version).is_err(),
-        "the reported element must be bound to ITS OWN child root, not any valid subtree proof"
-    );
+    match GroveDb::verify_subset_query(&swapped_bytes, &narrow_query, grove_version) {
+        Err(Error::InvalidProof(_, msg)) => assert!(
+            msg.contains("lower layer hash"),
+            "expected a lower-layer hash mismatch, got: {msg}"
+        ),
+        other => panic!(
+            "the reported element must be bound to ITS OWN child root; got {:?}",
+            other.map(|(root, _)| root)
+        ),
+    }
🤖 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/succinctness_gap_test.rs` around lines 405 - 428, Tighten
both tamper assertions around GroveDb::verify_subset_query to require
Error::InvalidProof and verify the error message identifies the child-root/hash
mismatch. Ensure Tamper 1 reaches the combine_hash validation rather than being
accepted merely because execute_proof rejects malformed bytes, while Tamper 2
confirms the structurally valid sibling proof fails specifically against the
reported element’s own child root.
🤖 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/src/tests/succinctness_gap_test.rs`:
- Around line 405-428: Tighten both tamper assertions around
GroveDb::verify_subset_query to require Error::InvalidProof and verify the error
message identifies the child-root/hash mismatch. Ensure Tamper 1 reaches the
combine_hash validation rather than being accepted merely because execute_proof
rejects malformed bytes, while Tamper 2 confirms the structurally valid sibling
proof fails specifically against the reported element’s own child root.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f3045b7-88c0-49f6-bde1-6e68c12f88e0

📥 Commits

Reviewing files that changed from the base of the PR and between c9cd467 and e2168c1.

📒 Files selected for processing (6)
  • grovedb/src/operations/proof/indexed_axis/axis_api.rs
  • grovedb/src/operations/proof/indexed_axis/mod.rs
  • grovedb/src/operations/proof/mod.rs
  • grovedb/src/operations/proof/verify.rs
  • grovedb/src/tests/commitment_tree_tests.rs
  • grovedb/src/tests/succinctness_gap_test.rs

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved

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.

1 participant