Skip to content

feat(merk,grovedb): expose aggregate_count proof verification under verify feature#658

Merged
QuantumExplorer merged 4 commits into
developfrom
claude/sad-shamir-5c3f76
May 10, 2026
Merged

feat(merk,grovedb): expose aggregate_count proof verification under verify feature#658
QuantumExplorer merged 4 commits into
developfrom
claude/sad-shamir-5c3f76

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 10, 2026

Summary

The AggregateCountOnRange proof primitive (landed in #656) had its modules gated behind feature = "minimal" in both grovedb-merk and grovedb, making the verifier entry points unreachable from downstream lean-verifier crates that depend on grovedb / grovedb-merk with default-features = false, features = ["verify"] — e.g. dashpay/platform's rs-drive-proof-verifier, which can't currently verify range-count proofs returned by the server.

This widens the module gates to any(feature = "minimal", feature = "verify") and refactors aggregate_count.rs so prover-only items stay feature = "minimal"-gated while the verifier compiles under verify alone.

What changed

  • merk/src/proofs/query/mod.rs — widened the gate on pub mod aggregate_count; and the pub use aggregate_count::verify_aggregate_count_on_range_proof; re-export from feature = "minimal" to any(minimal, verify). Matches the existing pattern used for query::common, query::merge, query::query_item, query::verify in the same file.
  • grovedb/src/operations/proof/mod.rs — same widening on mod aggregate_count;.
  • merk/src/proofs/query/aggregate_count.rs — split internal feature gates:
    • Verifier-side (verify_aggregate_count_on_range_proof, verify_count_shape, classify_subtree, key_strictly_inside, NULL_HASH, SubtreeClassification) — compile under any(minimal, verify).
    • Prover-side (is_provable_count_bearing, provable_count_from_aggregate, impl RefWalker, emit_count_proof, prover-only imports Op/AggregateData/Fetch/RefWalker/ValueDefinedCostType/TreeType/GroveVersion/LinkedList) — stay feature = "minimal".
    • Existing tests gated cfg(all(test, feature = "minimal")).
    • Added a cfg(all(test, feature = "verify")) verify_only_tests module with three tests:
      • empty_merk_returns_null_hash_and_zero_count — basic verifier wiring smoke test.
      • fixture_15_key_range_c_to_l_verifies — hex-encoded proof fixture round-trip.
      • fixture_byte_mutation_does_not_silently_forge_count — single-fixture analogue of the existing fuzz_byte_mutation_no_silent_forgery test, exercising the shape walk + node_hash_with_count recomputation path without any prover dependency.
    • Added an #[ignore]d dump_verify_only_fixtures helper to regenerate the hex fixture if the proof encoding ever changes.

Why

dashpay/platform's #3623 has a DriveDocumentCountQuery::aggregate_count_path_query helper (gated cfg(any(server, verify))) that already builds the matching PathQuery. Once this gate widens, the SDK side's FromProof<DocumentCountQuery> for DocumentCount can drop its current "this is gated upstream" error stub and call GroveDb::verify_aggregate_count_query(&proof_bytes, &path_query, grove_version) directly.

The original gate on aggregate_count was the odd one out — likely an oversight in #656, since proof-verification primitives are exactly what verify is meant to expose.

Test plan

  • cargo check -p grovedb-merk --no-default-features --features verify — clean.
  • cargo check -p grovedb --no-default-features --features verify — clean.
  • cargo clippy -p grovedb-merk --no-default-features --features verify --no-deps — clean (1 pre-existing warning unrelated to these changes).
  • cargo clippy -p grovedb --no-default-features --features verify --no-deps — clean (4 pre-existing warnings in aggregate_sum_query/mod.rs unrelated to these changes).
  • cargo test -p grovedb-merk --features "full,verify" --lib aggregate_count — 36 pass, 1 ignored (the fixture-regeneration helper).
  • cargo test -p grovedb --lib aggregate_count — 32 pass.
  • cargo test -p grovedb-merk --lib aggregate_count (default full features) — 33 pass, no regressions.

Known caveat

The merk crate's lib-test target does not compile under --no-default-features --features verify standalone, but this is a pre-existing condition (verified by stashing this branch's changes and reproducing): tests in merk/src/element/tree_type.rs reference minimal-only items without a cfg(feature = "minimal") gate. The downstream-relevant artifact (the library itself under verify) compiles cleanly, which is what consumers like rs-drive-proof-verifier need.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Expanded proof verification availability across additional build configurations.
  • Tests

    • Added regression tests for proof verification with hardcoded fixtures to ensure output consistency.
    • Includes mutation detection tests to validate proof integrity.

Review Change Stack

…erify feature

The AggregateCountOnRange proof primitive (PR #656) had its modules gated
behind feature = "minimal" in both grovedb-merk and grovedb, making the
verifier entry points (`verify_aggregate_count_on_range_proof` and
`GroveDb::verify_aggregate_count_query`) unreachable from downstream
lean-verifier crates that depend on `grovedb`/`grovedb-merk` with
`default-features = false, features = ["verify"]`.

Widen the module gates to `any(feature = "minimal", feature = "verify")`
to match the pattern already used by `query::common`, `query::merge`,
`query::query_item`, and `query::verify` in the same file. Refactor
aggregate_count.rs to keep prover-only items (`RefWalker`, `Fetch`,
`AggregateData`, `emit_count_proof`, etc.) gated `feature = "minimal"`
while exposing the verifier (`verify_aggregate_count_on_range_proof`,
`verify_count_shape`, `classify_subtree`, `key_strictly_inside`) under
the wider gate.

Add a `cfg(all(test, feature = "verify"))` test module with hex-fixture
round-trip, empty-merk, and byte-mutation no-silent-forgery tests that
exercise the verifier without any prover dependency, plus an `#[ignore]`d
helper to regenerate the fixture if the proof encoding ever changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec60eb8f-1d58-4192-9594-43a61130c38c

📥 Commits

Reviewing files that changed from the base of the PR and between 347bd9b and d3b1bb1.

📒 Files selected for processing (3)
  • grovedb/src/operations/proof/mod.rs
  • merk/src/proofs/query/aggregate_count.rs
  • merk/src/proofs/query/mod.rs

📝 Walkthrough

Walkthrough

This PR restructures feature flags to enable verify-only builds of proof verification code. It broadens the minimal and verify feature gates for count-proof verification modules, gates the prover-side implementation behind the minimal feature, and adds comprehensive verifier-only tests with hardcoded fixtures.

Changes

Count Proof Feature Gating and Verification

Layer / File(s) Summary
Feature Gate Configuration
grovedb/src/operations/proof/mod.rs, merk/src/proofs/query/mod.rs
verify and aggregate_count modules now compile under any(feature = "minimal", feature = "verify") instead of minimal-only. Public re-export of verify_aggregate_count_on_range_proof expanded to both features.
Prover-Side Implementation Gating
merk/src/proofs/query/aggregate_count.rs
Prover-specific imports (LinkedList, cost, version), helper functions (is_provable_count_bearing, provable_count_from_aggregate), the create_aggregate_count_on_range_proof API, and the emit_count_proof emitter are now gated behind #[cfg(feature = "minimal")]. Verifier-side functions remain unconditional.
Verifier-Only Tests and Fixtures
merk/src/proofs/query/aggregate_count.rs
New drift test regenerates a 15-key count proof for RangeInclusive("c".."l") and asserts exact byte/root/count match against hardcoded fixtures. New verify_only_tests module contains fixture constants and tests for empty-proof behavior, fixture verification, and single-byte mutation detection.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A proof splits clean, prover and verifier soon—
Feature gates align, we verify the moon!
Fixtures now hardcoded, mutations caught in flight,
Gating brings us trust; our tests sleep sound at night.

🚥 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 accurately describes the main change: it exposes aggregate_count proof verification under the verify feature flag, which is the core objective reflected in the feature gate changes across multiple files.
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.

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

✨ 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 claude/sad-shamir-5c3f76

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

❌ Patch coverage is 98.41270% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.64%. Comparing base (347bd9b) to head (d3b1bb1).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
merk/src/proofs/query/aggregate_count.rs 98.41% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #658   +/-   ##
========================================
  Coverage    90.63%   90.64%           
========================================
  Files          184      184           
  Lines        54763    54826   +63     
========================================
+ Hits         49637    49699   +62     
- Misses        5126     5127    +1     
Components Coverage Δ
grovedb-core 88.59% <ø> (ø)
merk 92.09% <98.41%> (+0.02%) ⬆️
storage 86.36% <ø> (ø)
commitment-tree 96.43% <ø> (ø)
mmr 96.76% <ø> (ø)
bulk-append-tree 89.14% <ø> (ø)
element 95.24% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

QuantumExplorer and others added 3 commits May 10, 2026 18:24
The verify_only_tests module was gated cfg(all(test, feature = "verify")),
which hid it from CI runs that only enable the default `full` feature
set (which doesn't include `verify`). Codecov flagged the test bodies as
uncovered patch lines as a result.

Widen to cfg(all(test, any(feature = "minimal", feature = "verify"))).
The verifier function is available under both gates and these tests have
no prover dependency, so the `verify`-only gate was buying nothing
except missing CI coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The #[ignore]d dump_verify_only_fixtures test never ran in CI, so its
~17 lines counted as uncovered patch lines. Replace it with an active
verify_only_fixture_matches_fresh_prover_output test that runs every
build, asserts the hardcoded fixture in verify_only_tests still matches
fresh prover output byte-for-byte, and prints regeneration-ready
constants on mismatch. This catches encoding drift automatically and
gets the fixture maintenance lines into CI coverage.

Promote the FIXTURE_*_HEX/COUNT constants to pub(super) so the parity
test can compare against them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both test modules were gated with feature = "minimal" / any(minimal,
verify), but the crate convention (e.g., merk/src/element/tree_type.rs)
is plain #[cfg(test)] — tests rely on default = ["full"] (which pulls
in minimal) being active at test time. The extra gates were defensive
beyond what the surrounding code does.

Also tighten the verify_only_tests doc comment now that the drift check
runs on every build instead of being a separate ignored helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

Reviewed

@QuantumExplorer QuantumExplorer merged commit 1206049 into develop May 10, 2026
8 of 9 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/sad-shamir-5c3f76 branch May 10, 2026 12:12
QuantumExplorer added a commit that referenced this pull request May 10, 2026
Two PRs landed on develop while this PR was open:
- #659: Element::NotSummed wrapper variant
- #658: aggregate_count proof verification under verify feature

Conflicts resolved in 8 files. The substantive resolution work:

DISCRIMINANT COLLISION. The shipped cidx PR used ElementType
discriminant 16 for CountIndexedTree and 17 for
ProvableCountIndexedTree. Develop's NotSummed PR allocated byte 16
as NOT_SUMMED_WRAPPER_DISCRIMINANT. Both are still pre-shipping, so
renumbering is safe — and since the NotSummed wrapper byte semantics
need to round-trip across the wire, the wrapper byte stays at 16
and the cidx discriminants shift:

  CountIndexedTree:                 16 → 17
  ProvableCountIndexedTree:         17 → 18
  NonCountedCountIndexedTree:       144 → 145 (= 0x80 | 17)
  NonCountedProvableCountIndexedTree: 145 → 146 (= 0x80 | 18)

ENUM VARIANT ORDER. Bincode encodes Element variants by ENUM ORDER
(not by ElementType discriminant). The Element enum has been
reordered so NotSummed appears at variant index 16 (matching
NOT_SUMMED_WRAPPER_DISCRIMINANT), and CountIndexedTree /
ProvableCountIndexedTree appear AFTER NotSummed at indices 17/18 —
matching their new ElementType discriminants. Without this
reordering, bincode would still write 16 for CountIndexedTree but
from_serialized_value would interpret 16 as the NotSummed wrapper.

WRAPPER NESTING CHECK in `from_serialized_value`: now explicitly
rejects both nested NonCounted and cross-nesting with NotSummed
(inner_byte == 15 || inner_byte == 16), in addition to the
existing reject-high-bit-twin guard.

Other resolutions are straightforward additions: both branches
added arms to match expressions across helpers.rs, tree_type.rs,
visualize.rs; merged additively. The two `mod tests` blocks in
grovedb-element/src/element/mod.rs (one from each PR) renamed the
cidx-side block to `cidx_tests` to avoid duplicate name.

All 1944+ workspace tests pass post-merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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