From 67733dd617f0d6b41570e8653c0abb3b93bba728 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 26 Aug 2026 17:06:18 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat!:=20retire=20the=20standalone=20indexe?= =?UTF-8?q?d-axis=20provers/verifiers=20=E2=80=94=20PathQuery=20is=20the?= =?UTF-8?q?=20only=20public=20proof=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone prove_indexed_* / verify_indexed_* entry points, their per-axis axis_api wrappers, and the standalone envelope builders and verification cores are now #[cfg(test)]: they survive solely as in-crate oracles that cross-check the unified V1-envelope axis proofs (see tests/envelope_byte_equality_tests.rs) against an independent implementation of the same engines. External callers must use PathQuery's axis constructors with prove_query / verify_path_query. Their wire format (IndexedAxisRangeProof / IndexedAxisPaginatedProof / IndexedAxisAggregateProof) has never been emitted by a released version; retiring it before GROVE_V4 activates means it never becomes consensus-frozen — only the V1 envelope's axis-descent format ships. The shared engine code (descent payload builders, secondary-proof builders, target-chain resolution, axis_lowering) and the trusted-read surface (indexed_*_top_k etc.) are unchanged. Envelope and result types stay public for the oracles and downstream transition code. Docs: unified-path-query.md now names PathQuery as the only public axis-proof surface; count-indexed-tree.md examples rewritten onto new_axis_top_k / new_axis_bounded / new_axis_aggregate_over_value_range. Platform's four remaining direct call sites (ranked top-k prove/verify, having-range prove/verify) switch to the unified surface in a companion platform PR; the unified surface already exists at platform's current grovedb pin, so the two PRs are independently mergeable. Co-Authored-By: Claude Fable 5 --- docs/book/src/count-indexed-tree.md | 124 ++++++++++-------- docs/book/src/unified-path-query.md | 29 ++-- grovedb/src/operations/proof/generate.rs | 12 +- .../operations/proof/indexed_axis/axis_api.rs | 44 +++---- .../operations/proof/indexed_axis/generate.rs | 29 +++- .../src/operations/proof/indexed_axis/mod.rs | 19 +++ .../operations/proof/indexed_axis/verify.rs | 51 +++++-- grovedb/src/operations/proof/mod.rs | 4 +- .../tests/coverage_proof_generate_tests.rs | 2 +- 9 files changed, 200 insertions(+), 114 deletions(-) diff --git a/docs/book/src/count-indexed-tree.md b/docs/book/src/count-indexed-tree.md index fdd3cb140..99bd5feda 100644 --- a/docs/book/src/count-indexed-tree.md +++ b/docs/book/src/count-indexed-tree.md @@ -483,19 +483,28 @@ let entries: Vec> = db .indexed_count_top_k(path, k, /* descending: */ true, transaction, grove_version)? .expect("top-k"); -// Verifiable variant — proof + verification: -let proof_bytes = db - .prove_indexed_count_top_k(path, k, /* descending: */ true, transaction, grove_version)? - .expect("prove"); -let result = GroveDb::verify_indexed_count_top_k( - &proof_bytes, - path, +// Verifiable variant — the axis read through the unified PathQuery +// surface (the only public proof surface for indexed-axis reads): +let path_query = PathQuery::new_axis_top_k( + path_vec, + IndexAxis::Count, k, + /* offset: */ 0, /* descending: */ true, - grove_version, -)?; -// result.entries: AxisEntries::Count(Vec>) -// result.root_hash: [u8; 32] +); +let proof_bytes = db + .prove_query(&path_query, None, grove_version)? + .expect("prove"); +let VerifiedPathQuery::AxisEntries { + root_hash, + entries, + skipped, +} = GroveDb::verify_path_query(&proof_bytes, &path_query, grove_version)? +else { + unreachable!("an axis read verifies to AxisEntries") +}; +// entries: AxisEntries::Count(Vec>) +// root_hash: [u8; 32]; skipped: Some(0) for offset 0 ``` The query returns `IndexedAxisEntry` rows — the count, the primary key, @@ -551,53 +560,61 @@ Internally builds a bounded `Query::insert_range(lo_be..upper)` against the secondary (with `RangeFrom` for `max == u64::MAX`), so iteration seeks directly to the encoded count bounds — no full secondary scan. -### Arbitrary count-indexed query +### Bounded count-indexed query -For predicates beyond top-k / count-range — e.g. "exact count = X", -"count >= X", multiple disjoint count windows — pass an arbitrary -`MerkQuery` over the secondary's keyspace (keys are -`count_value_be ‖ original_key`): +For predicates beyond top-k — "exact count = X" (`lo = hi = X`), +"count >= X" (`hi = u64::MAX`), any inclusive count band — use the +bounded axis read. Both proof sides lower the bounds into the +secondary's keyspace (keys are `count_value_be ‖ original_key`) +through the same shared lowering, so they cannot drift: ```rust -let mut q = MerkQuery::new(); -q.insert_range(3u64.to_be_bytes().to_vec()..6u64.to_be_bytes().to_vec()); -q.left_to_right = true; - +let path_query = PathQuery::new_axis_bounded( + path_vec, + IndexAxis::Count, + /* lo: */ 3, + /* hi: */ 5, // inclusive + limit, + /* descending: */ false, +); let proof_bytes = db - .prove_indexed_count_query(path, q.clone(), Some(limit), tx, grove_version)? + .prove_query(&path_query, None, grove_version)? .expect("prove"); -// Verify with the SAME query (positional binding): -let result = - GroveDb::verify_indexed_count_query(&proof_bytes, path, q, Some(limit), grove_version)?; +// Verify with the SAME query (query-as-input binding): +let verified = GroveDb::verify_path_query(&proof_bytes, &path_query, grove_version)?; ``` -`prove_indexed_count_top_k` is just a thin wrapper around -`prove_indexed_count_query` with a full-range query and the requested -`descending` flag. +Multiple disjoint count windows are one bounded read per window. (The +old standalone entry points that accepted an arbitrary `MerkQuery` +over the secondary keyspace are retired from the public API; they +survive only as `#[cfg(test)]` cross-check oracles.) ### How many entries have count in `[a, b]`? -Because the secondary is a `ProvableCountTree`, this is answered in -`O(log n + k)` via the existing range query against the secondary, -using the same `prove_indexed_count_query` / -`verify_indexed_count_query` shape as count-range reads — the -returned entry list's length is the count, and the proof binds it to -the GroveDB root hash. No per-entry enumeration is needed beyond -what the secondary Merk's range proof already encodes. +Because the secondary's node hashes commit count aggregates, this is +answered in `O(log n)` — without enumerating the matching entries — +via the aggregate axis read with the `Population` fold: ```rust -let mut q = MerkQuery::new(); -q.insert_range(a.to_be_bytes().to_vec()..=b.to_be_bytes().to_vec()); - -let proof = db.prove_indexed_count_query(path, q.clone(), None, tx, grove_version)?; -let result = GroveDb::verify_indexed_count_query(&proof, path, q, None, grove_version)?; - -let count = result.entries.len(); -let root_hash = result.root_hash; +let path_query = PathQuery::new_axis_aggregate_over_value_range( + path_vec, + IndexAxis::Count, + a as i128, // inclusive + b as i128, // inclusive + AggregateFold::Population, +); +let proof = db.prove_query(&path_query, None, grove_version)?.expect("prove"); +let VerifiedPathQuery::AxisAggregate { root_hash, value } = + GroveDb::verify_path_query(&proof, &path_query, grove_version)? +else { + unreachable!("an aggregate axis read verifies to AxisAggregate") +}; +let count = value; // how many entries have count_value in [a, b] ``` -The verifier returns the matched entries (size = count) and the +(Listing the matching entries instead — size = count — is the bounded +read above.) The verifier returns the attested population and the GroveDB root hash. The trivial "total entries" query (`a = 0`, `b = u64::MAX`) is also answered in `O(1)` via the parent's `Element::CountIndexedTree` `count_value` field, which already commits @@ -642,12 +659,12 @@ secondary keys are `(count_be ‖ key)`, an internal index. Use this route when the cidx is just one of several layers in a larger query shape and you don't need count-ordered output. -**2. Dedicated `prove_indexed_count_query` → arbitrary `MerkQuery` -over the secondary keyspace.** Use this when you do want -count-ordered output (top-k, count ranges, count-equality predicates). -Subquery composition with the dedicated proof shape is not exposed — -if you need a hybrid, compose the dedicated proof with a follow-up -`PathQuery`. +**2. Axis reads (`ReadMode::Axis`) → count-ordered output.** Use +`PathQuery::new_axis_top_k` / `new_axis_bounded` / +`new_axis_aggregate_over_value_range` when you do want count-ordered +output (top-k, count bands, count-equality predicates). Subquery +composition below an axis read is not exposed — if you need a hybrid, +compose the axis read with a follow-up `PathQuery`. ```rust // Inside a PathQuery — any standard subquery shape works: @@ -658,9 +675,8 @@ let (root_hash, results) = GroveDb::verify_query(&proof, &path_query, grove_vers ``` V0 generic prove/verify do **not** support cidx descent — V0 is a -frozen wire format. Callers on V0 paths must use the dedicated -`prove_indexed_count_top_k` / `prove_indexed_count_query` entry -points. +frozen wire format. Cidx queries require a grove version that emits +V1 proof envelopes. ### Proof shape @@ -787,8 +803,8 @@ ordering. Top-k descending iteration encounters them last. [overwrite workaround](#cidx-overwrite-workaround) (delete via batch, recreate in a follow-up batch). - **V0 generic prove/verify do not support cidx descents.** V0 is a - frozen wire format. Use V1 generic proofs or the dedicated - `prove_indexed_count_*` entry points. + frozen wire format. Use V1 generic proofs (axis reads go through + `PathQuery`'s axis constructors). ## Implementation-detail items diff --git a/docs/book/src/unified-path-query.md b/docs/book/src/unified-path-query.md index 46d0005bc..9e085b7be 100644 --- a/docs/book/src/unified-path-query.md +++ b/docs/book/src/unified-path-query.md @@ -317,16 +317,25 @@ the same contract the aggregate-on-range shapes use. ## Relationship to the specialized surfaces -Every pre-existing surface remains first-class: the -`prove/verify_indexed_*` methods and their standalone echo-based -envelopes, `AggregateSumPathQuery` and its budgeted reader, and the -per-shape `verify_aggregate_*` entry points. The unified entry points -route to the same engines, and where both a standalone envelope and an -embedded V1 proof exist for the same read, tests pin that they yield -identical entries and reconstruct the same root hash. New callers -should prefer `PathQuery` + `run_path_query` + `verify_path_query`; the -specialized surfaces are the engines underneath and the compatibility -surface for existing integrations. +For indexed-axis proofs, `PathQuery` + `prove_query` + +`verify_path_query` is the **only public surface**. The standalone +`prove/verify_indexed_*` methods and their echo-based envelopes +(`IndexedAxisRangeProof` / `IndexedAxisPaginatedProof` / +`IndexedAxisAggregateProof`) are retired from the public API: they are +compiled `#[cfg(test)]` and kept solely as in-crate oracles that +cross-check the unified V1-envelope axis proofs against an independent +implementation of the same engines. Their wire format was never emitted +by a released version, so retiring them before GROVE_V4 activates means +it never becomes consensus-frozen — only the V1 envelope's axis-descent +format ships. The byte-level relationship between the two families +(shared semantic core, deliberately different outer envelopes, mutual +rejection between verifiers) is pinned in +`grovedb/src/tests/envelope_byte_equality_tests.rs`. + +Other pre-existing surfaces remain first-class: +`AggregateSumPathQuery` and its budgeted reader, and the per-shape +`verify_aggregate_*` entry points. The unified entry points route to +the same engines underneath. Two things deliberately do **not** merge: diff --git a/grovedb/src/operations/proof/generate.rs b/grovedb/src/operations/proof/generate.rs index db49688a5..c5cb77e6d 100644 --- a/grovedb/src/operations/proof/generate.rs +++ b/grovedb/src/operations/proof/generate.rs @@ -1014,8 +1014,9 @@ impl GroveDb { // V0 is a frozen wire format. Adding cidx // descent to it would change the proof bytes, // so V0 will not learn cidx subqueries. Use - // V1 (or the dedicated `prove_indexed_count_*` - // entry points) for cidx queries. + // the V1 envelope (axis reads go through + // `PathQuery`'s axis constructors) for cidx + // queries. Ok(Element::ProvableCountIndexedTree(..)) | Ok(Element::ProvableSumIndexedTree(..)) | Ok(Element::ProvableCountProvableSumIndexedTree(..)) @@ -1025,7 +1026,8 @@ impl GroveDb { return Err(Error::NotSupported( "V0 proofs do not support subqueries into \ CountIndexedTree / ProvableCountIndexedTree; \ - use prove_query_v1 or prove_indexed_count_top_k" + use a V1 proof (axis-ordered reads go through \ + PathQuery::new_axis_top_k)" .to_string(), )) .wrap_with_cost(cost); @@ -2568,8 +2570,8 @@ impl GroveDb { // ProofBytes::CountIndexedTree(secondary ‖ // primary_proof) and chains via // combine_hash_three at this layer. Callers who - // want secondary-ordered output should use - // prove_indexed_count_top_k. + // want secondary-ordered output should use an + // axis read (`PathQuery::new_axis_top_k`). // Cidx descent only for NON-EMPTY primary // (Some(_)): mirrors the regular-tree // pattern above. An empty cidx primary diff --git a/grovedb/src/operations/proof/indexed_axis/axis_api.rs b/grovedb/src/operations/proof/indexed_axis/axis_api.rs index 35ca0732c..0a6cb806d 100644 --- a/grovedb/src/operations/proof/indexed_axis/axis_api.rs +++ b/grovedb/src/operations/proof/indexed_axis/axis_api.rs @@ -24,7 +24,7 @@ impl GroveDb { /// Prove the top-`k` entries of the count axis. Thin wrapper over /// [`Self::prove_indexed_axis_top_k`] with `axis = Count`. #[cfg(feature = "minimal")] - pub fn prove_indexed_count_top_k<'b, B, P>( + pub(crate) fn prove_indexed_count_top_k<'b, B, P>( &self, path: P, k: u16, @@ -48,7 +48,7 @@ impl GroveDb { /// Prove an offset-paginated top-`k` window on the count axis. #[cfg(feature = "minimal")] - pub fn prove_indexed_count_top_k_paginated<'b, B, P>( + pub(crate) fn prove_indexed_count_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -74,7 +74,7 @@ impl GroveDb { /// Prove an arbitrary query against the count-axis secondary. #[cfg(feature = "minimal")] - pub fn prove_indexed_count_query<'b, B, P>( + pub(crate) fn prove_indexed_count_query<'b, B, P>( &self, path: P, secondary_query: MerkQuery, @@ -99,7 +99,7 @@ impl GroveDb { /// Prove the aggregate count of entries whose `count_value` is in /// `[lo_count, hi_count]`. #[cfg(feature = "minimal")] - pub fn prove_indexed_count_aggregate_over_value_range<'b, B, P>( + pub(crate) fn prove_indexed_count_aggregate_over_value_range<'b, B, P>( &self, path: P, lo_count: u64, @@ -123,7 +123,7 @@ impl GroveDb { } /// Verify a count-axis top-k proof. - pub fn verify_indexed_count_top_k( + pub(crate) fn verify_indexed_count_top_k( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -141,7 +141,7 @@ impl GroveDb { } /// Verify a count-axis paginated proof. - pub fn verify_indexed_count_top_k_paginated( + pub(crate) fn verify_indexed_count_top_k_paginated( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -161,7 +161,7 @@ impl GroveDb { } /// Verify a count-axis arbitrary-query proof. - pub fn verify_indexed_count_query( + pub(crate) fn verify_indexed_count_query( proof_bytes: &[u8], path: &[&[u8]], secondary_query: MerkQuery, @@ -179,7 +179,7 @@ impl GroveDb { } /// Verify a count-axis aggregate proof. - pub fn verify_indexed_count_aggregate_over_value_range( + pub(crate) fn verify_indexed_count_aggregate_over_value_range( proof_bytes: &[u8], path: &[&[u8]], expected_lo_count: u64, @@ -201,7 +201,7 @@ impl GroveDb { /// Prove the top-`k` entries of the sum axis. #[cfg(feature = "minimal")] - pub fn prove_indexed_sum_top_k<'b, B, P>( + pub(crate) fn prove_indexed_sum_top_k<'b, B, P>( &self, path: P, k: u16, @@ -228,7 +228,7 @@ impl GroveDb { /// prefix is attested by counted subtree commitments and the proof /// size is O(log n + k) regardless of `offset`. #[cfg(feature = "minimal")] - pub fn prove_indexed_sum_top_k_paginated<'b, B, P>( + pub(crate) fn prove_indexed_sum_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -254,7 +254,7 @@ impl GroveDb { /// Prove an arbitrary query against the sum-axis secondary. #[cfg(feature = "minimal")] - pub fn prove_indexed_sum_query<'b, B, P>( + pub(crate) fn prove_indexed_sum_query<'b, B, P>( &self, path: P, secondary_query: MerkQuery, @@ -279,7 +279,7 @@ impl GroveDb { /// Prove the aggregate sum of entries whose `sum_value` is in /// `[lo_sum, hi_sum]`. #[cfg(feature = "minimal")] - pub fn prove_indexed_sum_aggregate_over_value_range<'b, B, P>( + pub(crate) fn prove_indexed_sum_aggregate_over_value_range<'b, B, P>( &self, path: P, lo_sum: i64, @@ -303,7 +303,7 @@ impl GroveDb { } /// Verify a sum-axis top-k proof. - pub fn verify_indexed_sum_top_k( + pub(crate) fn verify_indexed_sum_top_k( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -321,7 +321,7 @@ impl GroveDb { } /// Verify a sum-axis paginated proof. - pub fn verify_indexed_sum_top_k_paginated( + pub(crate) fn verify_indexed_sum_top_k_paginated( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -341,7 +341,7 @@ impl GroveDb { } /// Verify a sum-axis arbitrary-query proof. - pub fn verify_indexed_sum_query( + pub(crate) fn verify_indexed_sum_query( proof_bytes: &[u8], path: &[&[u8]], secondary_query: MerkQuery, @@ -359,7 +359,7 @@ impl GroveDb { } /// Verify a sum-axis aggregate proof. - pub fn verify_indexed_sum_aggregate_over_value_range( + pub(crate) fn verify_indexed_sum_aggregate_over_value_range( proof_bytes: &[u8], path: &[&[u8]], expected_lo_sum: i64, @@ -383,7 +383,7 @@ impl GroveDb { /// aggregate variant exists — averaging an average over a range is /// not closed-form. #[cfg(feature = "minimal")] - pub fn prove_indexed_avg_top_k<'b, B, P>( + pub(crate) fn prove_indexed_avg_top_k<'b, B, P>( &self, path: P, k: u16, @@ -407,7 +407,7 @@ impl GroveDb { /// Prove an offset-paginated top-`k` window on the avg axis. #[cfg(feature = "minimal")] - pub fn prove_indexed_avg_top_k_paginated<'b, B, P>( + pub(crate) fn prove_indexed_avg_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -433,7 +433,7 @@ impl GroveDb { /// Prove an arbitrary query against the avg-axis secondary. #[cfg(feature = "minimal")] - pub fn prove_indexed_avg_query<'b, B, P>( + pub(crate) fn prove_indexed_avg_query<'b, B, P>( &self, path: P, secondary_query: MerkQuery, @@ -456,7 +456,7 @@ impl GroveDb { } /// Verify an avg-axis top-k proof. - pub fn verify_indexed_avg_top_k( + pub(crate) fn verify_indexed_avg_top_k( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -474,7 +474,7 @@ impl GroveDb { } /// Verify an avg-axis paginated proof. - pub fn verify_indexed_avg_top_k_paginated( + pub(crate) fn verify_indexed_avg_top_k_paginated( proof_bytes: &[u8], path: &[&[u8]], expected_k: u16, @@ -494,7 +494,7 @@ impl GroveDb { } /// Verify an avg-axis arbitrary-query proof. - pub fn verify_indexed_avg_query( + pub(crate) fn verify_indexed_avg_query( proof_bytes: &[u8], path: &[&[u8]], secondary_query: MerkQuery, diff --git a/grovedb/src/operations/proof/indexed_axis/generate.rs b/grovedb/src/operations/proof/indexed_axis/generate.rs index 7f9fb7eda..b97e7a6b3 100644 --- a/grovedb/src/operations/proof/indexed_axis/generate.rs +++ b/grovedb/src/operations/proof/indexed_axis/generate.rs @@ -15,14 +15,19 @@ use grovedb_merk::{ proofs::{encode_into, query::QueryItem as MerkQueryItemForRange, Query as MerkQuery}, }; use grovedb_path::{SubtreePath, SubtreePathBuilder}; -use grovedb_query::{AggregateFold, QueryItem as MerkQueryItem}; +use grovedb_query::AggregateFold; +#[cfg(test)] +use grovedb_query::QueryItem as MerkQueryItem; use grovedb_storage::StorageBatch; use grovedb_version::version::GroveVersion; use crate::{util::TxRef, Element, Error, GroveDb, Transaction, TransactionArg}; +use super::verify::{count_aggregate_inner_range, sum_aggregate_inner_range}; +// Test-oracle-only (see the module doc): the standalone envelope +// builders and their entry points are `#[cfg(test)]`. +#[cfg(test)] use super::{ - verify::{count_aggregate_inner_range, sum_aggregate_inner_range}, AncestorAttestation, IndexedAxisAggregateProof, IndexedAxisPaginatedProof, IndexedAxisRangeProof, }; @@ -169,6 +174,7 @@ fn build_chains_for_keys<'db>( /// list has length N-1 (one entry per intermediate layer). For each /// intermediate layer, open the parent merk and inspect the element /// at the depth's key to determine the chain composition. +#[cfg(test)] fn build_ancestor_attestations<'db>( grovedb: &'db GroveDb, path_keys: &[Vec], @@ -298,6 +304,7 @@ fn build_ancestor_attestations<'db>( /// Build single-key Merk proofs per layer, top-down. `layer_proofs[i]` /// proves the existence of `path_keys[i]` in the Merk at /// `path_keys[..i]`. +#[cfg(test)] fn build_layer_proofs<'db>( grovedb: &'db GroveDb, path_keys: &[Vec], @@ -465,7 +472,8 @@ impl GroveDb { /// /// Any other variant — or a PCPSIT whose TLV does not carry the /// requested axis — is rejected with [`Error::InvalidPath`]. - pub fn prove_indexed_axis_top_k<'b, B, P>( + #[cfg(test)] + pub(crate) fn prove_indexed_axis_top_k<'b, B, P>( &self, path: P, axis: IndexAxis, @@ -496,7 +504,8 @@ impl GroveDb { /// secondary of an indexed-tree at `path`. The query is over the /// secondary's keyspace, which is `(sort_key_be ‖ original_key)` /// per axis (8 + N bytes for count/sum, 16 + N bytes for avg). - pub fn prove_indexed_axis_query<'b, B, P>( + #[cfg(test)] + pub(crate) fn prove_indexed_axis_query<'b, B, P>( &self, path: P, axis: IndexAxis, @@ -568,7 +577,8 @@ impl GroveDb { /// attested `skipped < offset`, which together with the root-bound /// count commitments is a proof that the total population is /// exactly `skipped`. - pub fn prove_indexed_axis_top_k_paginated<'b, B, P>( + #[cfg(test)] + pub(crate) fn prove_indexed_axis_top_k_paginated<'b, B, P>( &self, path: P, axis: IndexAxis, @@ -751,7 +761,8 @@ impl GroveDb { /// /// Errors if `item_key` is not present in the indexed tree's /// primary, or if the axis is not indexed at this path. - pub fn prove_indexed_axis_rank_of_key<'b, B, P>( + #[cfg(test)] + pub(crate) fn prove_indexed_axis_rank_of_key<'b, B, P>( &self, path: P, axis: IndexAxis, @@ -832,7 +843,8 @@ impl GroveDb { /// against the same path). /// /// `lo > hi` is a degenerate range; the proof commits `0`. - pub fn prove_indexed_axis_aggregate_over_value_range<'b, B, P>( + #[cfg(test)] + pub(crate) fn prove_indexed_axis_aggregate_over_value_range<'b, B, P>( &self, path: P, axis: IndexAxis, @@ -893,6 +905,7 @@ impl GroveDb { Ok(bytes).wrap_with_cost(cost) } + #[cfg(test)] fn build_indexed_axis_range_proof<'db, 'b, B: AsRef<[u8]>>( &'db self, path: SubtreePath<'b, B>, @@ -1041,6 +1054,7 @@ impl GroveDb { .wrap_with_cost(cost) } + #[cfg(test)] fn build_indexed_axis_paginated_proof<'db, 'b, B: AsRef<[u8]>>( &'db self, path: SubtreePath<'b, B>, @@ -1203,6 +1217,7 @@ impl GroveDb { .wrap_with_cost(cost) } + #[cfg(test)] fn build_indexed_axis_aggregate_proof<'db, 'b, B: AsRef<[u8]>>( &'db self, path: SubtreePath<'b, B>, diff --git a/grovedb/src/operations/proof/indexed_axis/mod.rs b/grovedb/src/operations/proof/indexed_axis/mod.rs index 30f207651..f0c974646 100644 --- a/grovedb/src/operations/proof/indexed_axis/mod.rs +++ b/grovedb/src/operations/proof/indexed_axis/mod.rs @@ -2,6 +2,24 @@ //! (PCIT), `ProvableSumIndexedTree` (PSIT) and //! `ProvableCountProvableSumIndexedTree` (PCPSIT). //! +//! ## RETIRED from the public API — `PathQuery` is the only proof surface +//! +//! The standalone prove/verify entry points in this module +//! (`prove_indexed_axis_*`, `verify_indexed_axis_*`, and the per-axis +//! `axis_api` wrappers) are **`#[cfg(test)]` test oracles**: they exist so +//! the in-crate suites can cross-check the unified V1-envelope axis proofs +//! against an independent implementation of the same engines (see +//! `tests/envelope_byte_equality_tests.rs` for the byte-level relationship). +//! External callers must use [`crate::PathQuery`]'s axis constructors with +//! `GroveDb::prove_query` / `GroveDb::verify_path_query`. The standalone +//! envelope wire format has never been emitted by a released version, and +//! retiring it before GROVE_V4 activates means it never becomes +//! consensus-frozen. +//! +//! The engine code both surfaces share (descent payload builders, the +//! secondary-proof builders, target-chain resolution, `axis_lowering`) +//! remains live — the unified surface is a thin envelope over it. +//! //! This is the Phase-4 generalization of the Phase-2 `count_indexed` //! envelope: instead of three per-axis families of types each shaped //! identically, the wire format here carries an explicit @@ -61,6 +79,7 @@ //! range is provably empty, and the verifier uses it to accept the empty //! shape only when the range really is out of the axis's domain. +#[cfg(test)] mod axis_api; pub(crate) mod canonical_row; mod envelope; diff --git a/grovedb/src/operations/proof/indexed_axis/verify.rs b/grovedb/src/operations/proof/indexed_axis/verify.rs index 452322998..0879c83d5 100644 --- a/grovedb/src/operations/proof/indexed_axis/verify.rs +++ b/grovedb/src/operations/proof/indexed_axis/verify.rs @@ -7,31 +7,48 @@ //! reconstructed GroveDB root hash is returned for the caller to compare. use grovedb_element::indexed::{encode_count_sort_key, encode_sum_sort_key, IndexAxis}; +use grovedb_merk::{ + proofs::query::QueryItem as MerkQueryItemForRange, + tree::{axes_digest, CryptoHash}, +}; +use grovedb_version::version::GroveVersion; + +use crate::{query_result_type::IndexedAxisEntry, Element, Error}; + +use super::{aggregate_range_out_of_domain, AxisEntries, IndexedTargetChain}; + +// Test-oracle-only imports (see the module doc): the standalone +// verifiers and their inner cores are `#[cfg(test)]`. +#[cfg(test)] use grovedb_merk::{ proofs::{ query::{ verify_aggregate_count_on_range_proof, verify_aggregate_sum_on_range_proof, - verify_count_offset_on_range_proof, QueryItem as MerkQueryItemForRange, - QueryProofVerify, + verify_count_offset_on_range_proof, QueryProofVerify, }, Query as MerkQuery, }, - tree::{axes_digest, combine_hash, combine_hash_three, value_hash, CryptoHash}, + tree::{combine_hash, combine_hash_three, value_hash}, }; +#[cfg(test)] use grovedb_query::{AggregateFold, QueryItem as MerkQueryItem}; -use grovedb_version::{check_grovedb_v0, version::GroveVersion}; +#[cfg(test)] +use grovedb_version::check_grovedb_v0; -use crate::{query_result_type::IndexedAxisEntry, Element, Error, GroveDb}; +#[cfg(test)] +use crate::GroveDb; +#[cfg(test)] use super::{ - aggregate_range_out_of_domain, AncestorAttestation, AxisEntries, IndexedAxisAggregateProof, - IndexedAxisAggregateResult, IndexedAxisPaginatedProof, IndexedAxisPaginatedResult, - IndexedAxisQueryResult, IndexedAxisRangeProof, IndexedTargetChain, + AncestorAttestation, IndexedAxisAggregateProof, IndexedAxisAggregateResult, + IndexedAxisPaginatedProof, IndexedAxisPaginatedResult, IndexedAxisQueryResult, + IndexedAxisRangeProof, }; /// Walk the verifier-side ancestor chain (depths `last_idx - 1` down to /// `0`) and return the final reconstructed root hash. Returns the /// outer GroveDB root hash on success. +#[cfg(test)] fn walk_ancestor_chain( layer_proofs: &[Vec], ancestor_attestations: &[AncestorAttestation], @@ -108,6 +125,7 @@ fn walk_ancestor_chain( /// Returns `(initial_layer_root, cidx_value_bytes)`. The /// `initial_layer_root` is then passed to `walk_ancestor_chain` as the /// starting `current_layer_root` for the ancestor walk. +#[cfg(test)] fn verify_deepest_layer( layer_proofs: &[Vec], path: &[&[u8]], @@ -258,6 +276,7 @@ pub(crate) fn recompute_axis_binding_digest( /// Verify a single-key Merk proof: returns /// `(value_bytes, layer_root_hash, parent_recorded_value_hash)`. +#[cfg(test)] fn execute_single_key_proof( proof_bytes: &[u8], target_key: &[u8], @@ -293,10 +312,11 @@ fn execute_single_key_proof( Ok((value, root_hash, proved.proof)) } +#[cfg(test)] impl GroveDb { /// Verify an `IndexedAxisRangeProof`-shaped top-k proof (full range, /// limit = `expected_k`, direction = `expected_descending`). - pub fn verify_indexed_axis_top_k( + pub(crate) fn verify_indexed_axis_top_k( proof_bytes: &[u8], path: &[&[u8]], expected_axis: IndexAxis, @@ -346,7 +366,7 @@ impl GroveDb { /// /// `secondary_query` MUST match the query supplied at proof time. /// `expected_limit` MUST match the limit supplied at proof time. - pub fn verify_indexed_axis_query( + pub(crate) fn verify_indexed_axis_query( proof_bytes: &[u8], path: &[&[u8]], expected_axis: IndexAxis, @@ -396,7 +416,7 @@ impl GroveDb { } /// Verify an `IndexedAxisPaginatedProof`-shaped paginated proof. - pub fn verify_indexed_axis_top_k_paginated( + pub(crate) fn verify_indexed_axis_top_k_paginated( proof_bytes: &[u8], path: &[&[u8]], expected_axis: IndexAxis, @@ -468,7 +488,7 @@ impl GroveDb { /// Returns the paginated result whose single entry carries the /// item's axis value; `root_hash` must be compared against the /// trusted GroveDB root as usual. - pub fn verify_indexed_axis_rank_of_key( + pub(crate) fn verify_indexed_axis_rank_of_key( proof_bytes: &[u8], path: &[&[u8]], expected_axis: IndexAxis, @@ -517,7 +537,7 @@ impl GroveDb { } /// Verify an `IndexedAxisAggregateProof`-shaped aggregate proof. - pub fn verify_indexed_axis_aggregate_over_value_range( + pub(crate) fn verify_indexed_axis_aggregate_over_value_range( proof_bytes: &[u8], path: &[&[u8]], expected_axis: IndexAxis, @@ -579,6 +599,7 @@ impl GroveDb { } } +#[cfg(test)] fn decode_range_envelope(proof_bytes: &[u8]) -> Result { let config = bincode::config::standard().with_limit::<{ 16 * 1024 * 1024 }>(); let (envelope, consumed): (IndexedAxisRangeProof, _) = @@ -593,6 +614,7 @@ fn decode_range_envelope(proof_bytes: &[u8]) -> Result), /// Terminal attestation for an indexed tree that is itself a query /// result with nothing queried below it: diff --git a/grovedb/src/tests/coverage_proof_generate_tests.rs b/grovedb/src/tests/coverage_proof_generate_tests.rs index a76a7a82f..e4bb69bc2 100644 --- a/grovedb/src/tests/coverage_proof_generate_tests.rs +++ b/grovedb/src/tests/coverage_proof_generate_tests.rs @@ -300,7 +300,7 @@ mod tests { assert!( matches!(err, Error::NotSupported(ref msg) if msg.contains("V0 proofs do not support subqueries into") - && msg.contains("prove_query_v1")), + && msg.contains("use a V1 proof")), "expected the V0 indexed-subquery NotSupported error, got {err:?}" ); } From 5ba6384356b8827af0df85b4d54d4a8dc35b03c4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 26 Aug 2026 18:12:02 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat!:=20retire=20the=20per-axis=20trusted-?= =?UTF-8?q?read=20wrappers=20=E2=80=94=20run=5Fpath=5Fquery=20is=20the=20o?= =?UTF-8?q?nly=20public=20read=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The indexed_*_top_k*, indexed_*_range*, aggregate-read and _keys trusted-read wrappers are now pub(crate): they are the engine run_path_query routes axis shapes to, not a public API. External callers build the same axis PathQuery for reads and proofs alike — one request shape, three consumers (run_path_query, prove_query, verify_path_query). The non-paginated top-k family (indexed_*_top_k / indexed_*_top_k_keys and their generic cores) is #[cfg(test)]: nothing routes to it — the paginated walk with offset = 0 serves that case — so it survives only as a test oracle, same as the standalone proof family. IndexedTopKPage / IndexedTopKKeysPage stay re-exported for downstream transition code. Book examples rewritten onto run_path_query. Co-Authored-By: Claude Fable 5 --- docs/book/src/count-indexed-tree.md | 79 +++++++++++++++++--------- docs/book/src/unified-path-query.md | 7 +++ grovedb/src/operations/indexed_tree.rs | 68 +++++++++++++++------- 3 files changed, 104 insertions(+), 50 deletions(-) diff --git a/docs/book/src/count-indexed-tree.md b/docs/book/src/count-indexed-tree.md index 99bd5feda..847a1fe90 100644 --- a/docs/book/src/count-indexed-tree.md +++ b/docs/book/src/count-indexed-tree.md @@ -478,13 +478,32 @@ Merk is not touched. The verifier receives the primary's root hash plus a ### Top-k by count ```rust -// Shipped API on `GroveDb`: -let entries: Vec> = db - .indexed_count_top_k(path, k, /* descending: */ true, transaction, grove_version)? - .expect("top-k"); +// Trusted read — the axis read through the unified PathQuery surface +// (the only public read surface for indexed-axis queries): +let path_query = PathQuery::new_axis_top_k( + path_vec.clone(), + IndexAxis::Count, + k, + /* offset: */ 0, + /* descending: */ true, +); +let PathQueryRun::AxisEntries { entries, skipped } = db + .run_path_query( + &path_query, + true, // allow_cache + true, // decrease_limit_on_range_with_no_sub_elements + true, // error_if_intermediate_path_tree_not_present + QueryResultType::QueryPathKeyElementTrioResultType, + transaction, + grove_version, + )? + .expect("top-k") +else { + unreachable!("an axis read runs to AxisEntries") +}; +// entries: AxisEntries::Count(Vec>) -// Verifiable variant — the axis read through the unified PathQuery -// surface (the only public proof surface for indexed-axis reads): +// Verifiable variant — the same PathQuery, proved: let path_query = PathQuery::new_axis_top_k( path_vec, IndexAxis::Count, @@ -543,17 +562,19 @@ one key whose reference points at another cannot verify. ### Range by count ```rust -let entries: Vec> = db - .indexed_count_range( - path, - min, // u64, inclusive - max, // u64, inclusive - /* descending: */ false, - /* limit: */ 100, - transaction, - grove_version, - )? +let path_query = PathQuery::new_axis_bounded( + path_vec, + IndexAxis::Count, + min as i128, // inclusive + max as i128, // inclusive + /* limit: */ 100, + /* descending: */ false, +); +let run = db + .run_path_query(/* same arguments as above */)? .expect("count range"); +// PathQueryRun::AxisEntries { entries, skipped: None } — bounded reads +// attest no skip count. ``` Internally builds a bounded `Query::insert_range(lo_be..upper)` against @@ -623,21 +644,23 @@ the size. ### Direction The indexed read APIs support both ascending and descending iteration -through `left_to_right: bool`, mirroring the existing `Query` API. The -common case for top-k is `left_to_right: false` (highest counts first), -which is what `indexed_count_top_k(path, k, descending = true, ..)` produces. -Ascending traversal is also supported for "smallest counts first" / -"items with the lowest counts in [a, b]" patterns. +through the axis constructors' `descending: bool`. The common case for +top-k is `descending = true` (highest counts first). Ascending +traversal is also supported for "smallest counts first" / "items with +the lowest counts in [a, b]" patterns. ### How many entries fall in a count band -`indexed_count_aggregate_over_value_range(path, lo, hi, ..)` answers **how many -entries have a `count_value` in `[lo, hi]`** — a bucket population, in -which each matching entry contributes 1. It is *not* the total of those -entries' counts: over counts `[3, 1, 5]`, the band `[2, 10]` selects the -`3` and the `5` and answers `2`, not `8`. If you want the total, use -`indexed_count_range(path, lo, hi, ..)` to list the selected entries -with their counts and sum them caller-side. +The trusted-read form of the aggregate above — +`PathQuery::new_axis_aggregate_over_value_range(path, IndexAxis::Count, +lo, hi, AggregateFold::Population)` run through `run_path_query` — +answers **how many entries have a `count_value` in `[lo, hi]`** — a +bucket population, in which each matching entry contributes 1 +(`PathQueryRun::AxisAggregate(AxisAggregateValue::Population(_))`). It +is *not* the total of those entries' counts: over counts `[3, 1, 5]`, +the band `[2, 10]` selects the `3` and the `5` and answers `2`, not +`8`. If you want the total, use `AggregateFold::Total`, or a bounded +axis read to list the selected entries with their counts. The walk folds each fully-contained subtree's stored aggregate in one step and descends only along the two range boundaries, so the cost is diff --git a/docs/book/src/unified-path-query.md b/docs/book/src/unified-path-query.md index 9e085b7be..5b687fd9f 100644 --- a/docs/book/src/unified-path-query.md +++ b/docs/book/src/unified-path-query.md @@ -332,6 +332,13 @@ format ships. The byte-level relationship between the two families rejection between verifiers) is pinned in `grovedb/src/tests/envelope_byte_equality_tests.rs`. +The per-axis **trusted-read** wrappers (`indexed_*_top_k*`, +`indexed_*_range*`, the aggregate reads and the `_keys` projections) +are likewise crate-internal: they are the engine `run_path_query` +routes axis shapes to. External callers build the same axis +`PathQuery` for reads and proofs alike — one request shape, three +consumers (`run_path_query`, `prove_query`, `verify_path_query`). + Other pre-existing surfaces remain first-class: `AggregateSumPathQuery` and its budgeted reader, and the per-shape `verify_aggregate_*` entry points. The unified entry points route to diff --git a/grovedb/src/operations/indexed_tree.rs b/grovedb/src/operations/indexed_tree.rs index 61805d77c..3e48b6f52 100644 --- a/grovedb/src/operations/indexed_tree.rs +++ b/grovedb/src/operations/indexed_tree.rs @@ -24,6 +24,21 @@ //! Deep ops *under* a sub-tree of an indexed primary need none of this — //! they propagate through the ordinary //! `propagate_changes_with_transaction_with_initial_deferred` machinery. +//! +//! ## Reads: `PathQuery` is the only public surface +//! +//! The per-axis trusted-read wrappers here (`indexed_*_top_k*`, +//! `indexed_*_range*`, the aggregate reads and the `_keys` projections) +//! are `pub(crate)`: they are the engine [`GroveDb::run_path_query`] +//! routes axis shapes to, not a public API. External callers build a +//! [`crate::PathQuery`] with the axis constructors (`new_axis_top_k`, +//! `new_axis_bounded`, `new_axis_rank_of_key`, +//! `new_axis_aggregate_over_value_range`, or `new_axis` with a keys-only +//! projection) and call `run_path_query` — the same query then proves +//! through `prove_query` / `verify_path_query` without restating the +//! request. The non-paginated top-k family is `#[cfg(test)]`: nothing +//! routes to it (the paginated walk with `offset = 0` serves that case), +//! and it survives only as a test oracle. use grovedb_costs::{ cost_return_on_error, cost_return_on_error_no_add, CostResult, CostsExt, OperationCost, @@ -1308,6 +1323,7 @@ impl GroveDb { /// `(value, original_key)` pairs straight from the secondary, with no /// primary read. Both the resolving wrapper and the keys-only wrapper /// are built on this, so the two can never disagree about the page. + #[cfg(test)] fn indexed_axis_top_k_rows_generic<'b, B, T>( &self, path: SubtreePath<'b, B>, @@ -1337,6 +1353,7 @@ impl GroveDb { /// One implementation of the `indexed__top_k` shape. See the /// per-axis wrappers for the public contract. + #[cfg(test)] fn indexed_axis_top_k_generic<'b, B, T>( &self, path: SubtreePath<'b, B>, @@ -1371,6 +1388,7 @@ impl GroveDb { /// Keys-only `indexed__top_k`: the ranking pairs without /// resolving any primary value. See /// [`Self::indexed_count_top_k_keys`] for why this exists. + #[cfg(test)] fn indexed_axis_top_k_keys_generic<'b, B, T>( &self, path: SubtreePath<'b, B>, @@ -1754,7 +1772,8 @@ impl GroveDb { /// /// For a verifiable variant, see [`Self::prove_indexed_count_top_k`] /// and [`Self::verify_indexed_count_top_k`]. - pub fn indexed_count_top_k<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_count_top_k<'b, B, P>( &self, path: P, k: u16, @@ -1794,7 +1813,7 @@ impl GroveDb { /// variant use [`Self::prove_indexed_count_top_k_paginated`] which /// relies on the merk-level count-offset proof to commit the skipped /// count via `HashWithCount`. - pub fn indexed_count_top_k_paginated<'b, B, P>( + pub(crate) fn indexed_count_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -1826,7 +1845,7 @@ impl GroveDb { /// Bounds are inclusive on both sides; `(0, u64::MAX, false, limit)` /// is equivalent to a full scan. `lo_count > hi_count` returns an /// empty vector. - pub fn indexed_count_range<'b, B, P>( + pub(crate) fn indexed_count_range<'b, B, P>( &self, path: P, lo_count: u64, @@ -1906,7 +1925,7 @@ impl GroveDb { /// verifiable count, use /// [`Self::prove_indexed_count_aggregate_over_value_range`] + /// [`Self::verify_indexed_count_aggregate_over_value_range`]. - pub fn indexed_count_aggregate_over_value_range<'b, B, P>( + pub(crate) fn indexed_count_aggregate_over_value_range<'b, B, P>( &self, path: P, lo_count: u64, @@ -1972,7 +1991,8 @@ impl GroveDb { /// Each returned entry is `(sum, original_key)`. The signed `i64` /// sum is decoded from the secondary's sign-flipped big-endian /// prefix (see [`grovedb_element::indexed::encode_sum_sort_key`]). - pub fn indexed_sum_top_k<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_sum_top_k<'b, B, P>( &self, path: P, k: u16, @@ -2002,7 +2022,7 @@ impl GroveDb { /// secondary merk in `O(log n)` node loads, and /// [`IndexedTopKPage::skipped`] reports the true /// `min(offset, population)`. Not a verifiable / proof-bounded read. - pub fn indexed_sum_top_k_paginated<'b, B, P>( + pub(crate) fn indexed_sum_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -2034,7 +2054,7 @@ impl GroveDb { /// Bounds are inclusive on both sides. `lo_sum > hi_sum` returns /// an empty vector. `lo_sum == i64::MIN && hi_sum == i64::MAX` is /// equivalent to a full scan. - pub fn indexed_sum_range<'b, B, P>( + pub(crate) fn indexed_sum_range<'b, B, P>( &self, path: P, lo_sum: i64, @@ -2106,7 +2126,7 @@ impl GroveDb { /// indexed-tree". Like the count counterpart, this call has no /// cryptographic guarantee; for a verifiable sum use the /// proof-bound variant in the proof submodule. - pub fn indexed_sum_aggregate_over_value_range<'b, B, P>( + pub(crate) fn indexed_sum_aggregate_over_value_range<'b, B, P>( &self, path: P, lo_sum: i64, @@ -2166,7 +2186,7 @@ impl GroveDb { /// /// `O(log n)`: the walk folds contained subtrees' stored counts and /// descends only along the two band boundaries. - pub fn indexed_sum_population_over_value_range<'b, B, P>( + pub(crate) fn indexed_sum_population_over_value_range<'b, B, P>( &self, path: P, lo_sum: i64, @@ -2228,7 +2248,7 @@ impl GroveDb { /// /// `O(log n)`: the walk folds contained subtrees' stored sums and /// descends only along the two band boundaries. - pub fn indexed_count_total_over_value_range<'b, B, P>( + pub(crate) fn indexed_count_total_over_value_range<'b, B, P>( &self, path: P, lo_count: u64, @@ -2298,7 +2318,8 @@ impl GroveDb { /// view if you need one — noting an `f64` view is approximate at /// this scale; the `i128` fixed-point value is the exact consensus /// value. - pub fn indexed_avg_top_k<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_avg_top_k<'b, B, P>( &self, path: P, k: u16, @@ -2328,7 +2349,7 @@ impl GroveDb { /// secondary merk in `O(log n)` node loads, and /// [`IndexedTopKPage::skipped`] reports the true /// `min(offset, population)`. Not a verifiable / proof-bounded read. - pub fn indexed_avg_top_k_paginated<'b, B, P>( + pub(crate) fn indexed_avg_top_k_paginated<'b, B, P>( &self, path: P, k: u16, @@ -2369,7 +2390,7 @@ impl GroveDb { /// "aggregate avg in range" should compute it client-side from /// `indexed_count_aggregate_over_value_range` + `indexed_sum_aggregate_over_value_range` /// against the same path's count and sum secondaries. - pub fn indexed_avg_range<'b, B, P>( + pub(crate) fn indexed_avg_range<'b, B, P>( &self, path: P, lo_avg: i128, @@ -2446,7 +2467,8 @@ impl GroveDb { /// Keys-only [`Self::indexed_count_top_k`]: the top-`k` /// `(count, original_key)` pairs, with no primary value resolved. - pub fn indexed_count_top_k_keys<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_count_top_k_keys<'b, B, P>( &self, path: P, k: u16, @@ -2473,7 +2495,7 @@ impl GroveDb { /// `(count, original_key)` pairs and the skipped count, produced /// entirely inside the pinned secondary view, with no primary value /// resolved. - pub fn indexed_count_top_k_paginated_keys<'b, B, P>( + pub(crate) fn indexed_count_top_k_paginated_keys<'b, B, P>( &self, path: P, k: u16, @@ -2500,7 +2522,7 @@ impl GroveDb { /// Keys-only [`Self::indexed_count_range`]: the in-range /// `(count, original_key)` pairs, with no primary value resolved. - pub fn indexed_count_range_keys<'b, B, P>( + pub(crate) fn indexed_count_range_keys<'b, B, P>( &self, path: P, lo_count: u64, @@ -2538,7 +2560,8 @@ impl GroveDb { } /// Keys-only [`Self::indexed_sum_top_k`]. - pub fn indexed_sum_top_k_keys<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_sum_top_k_keys<'b, B, P>( &self, path: P, k: u16, @@ -2562,7 +2585,7 @@ impl GroveDb { } /// Keys-only [`Self::indexed_sum_top_k_paginated`]. - pub fn indexed_sum_top_k_paginated_keys<'b, B, P>( + pub(crate) fn indexed_sum_top_k_paginated_keys<'b, B, P>( &self, path: P, k: u16, @@ -2588,7 +2611,7 @@ impl GroveDb { } /// Keys-only [`Self::indexed_sum_range`]. - pub fn indexed_sum_range_keys<'b, B, P>( + pub(crate) fn indexed_sum_range_keys<'b, B, P>( &self, path: P, lo_sum: i64, @@ -2626,7 +2649,8 @@ impl GroveDb { } /// Keys-only [`Self::indexed_avg_top_k`]. - pub fn indexed_avg_top_k_keys<'b, B, P>( + #[cfg(test)] + pub(crate) fn indexed_avg_top_k_keys<'b, B, P>( &self, path: P, k: u16, @@ -2650,7 +2674,7 @@ impl GroveDb { } /// Keys-only [`Self::indexed_avg_top_k_paginated`]. - pub fn indexed_avg_top_k_paginated_keys<'b, B, P>( + pub(crate) fn indexed_avg_top_k_paginated_keys<'b, B, P>( &self, path: P, k: u16, @@ -2676,7 +2700,7 @@ impl GroveDb { } /// Keys-only [`Self::indexed_avg_range`]. - pub fn indexed_avg_range_keys<'b, B, P>( + pub(crate) fn indexed_avg_range_keys<'b, B, P>( &self, path: P, lo_avg: i128, From 44bd588467ae4c12964164da3180b49850075101 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 26 Aug 2026 18:14:53 +0200 Subject: [PATCH 3/4] feat: re-export AxisQuery / AxisProjection / AxisTraversal at the crate root With PathQuery as the only public axis surface, callers building a non-default projection (PathQuery::new_axis with AxisQuery::keys_only) need the vocabulary types without adding a grovedb-merk dependency. Co-Authored-By: Claude Fable 5 --- grovedb/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/grovedb/src/lib.rs b/grovedb/src/lib.rs index ac30cf7eb..329c6d8be 100644 --- a/grovedb/src/lib.rs +++ b/grovedb/src/lib.rs @@ -209,6 +209,11 @@ pub use grovedb_merk::proofs::query::query_item::QueryItem; pub use grovedb_merk::proofs::query::SubqueryBranch; #[cfg(any(feature = "minimal", feature = "verify"))] pub use grovedb_merk::proofs::query::VerifyOptions; +/// The axis-read vocabulary: callers build an [`AxisQuery`] (or use +/// `PathQuery`'s typed axis constructors) and run/prove/verify it +/// through the unified surface. +#[cfg(any(feature = "minimal", feature = "verify"))] +pub use grovedb_merk::proofs::query::{AxisProjection, AxisQuery, AxisTraversal}; #[cfg(any(feature = "minimal", feature = "verify"))] pub use grovedb_merk::proofs::Query; #[cfg(any(feature = "minimal", feature = "verify"))] From da279e7137f5e667e09e5d4e7d02be55277f62b9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 26 Aug 2026 18:37:17 +0200 Subject: [PATCH 4/4] bench: run cidx top-k through the public PathQuery surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The benchmark called indexed_count_top_k, which is now a cfg(test) oracle — benches don't compile under cfg(test), so clippy --all-targets failed. Measuring through run_path_query is also the honest benchmark now: it is the route production reads take. Co-Authored-By: Claude Fable 5 --- grovedb/benches/cidx_benchmark.rs | 44 +++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/grovedb/benches/cidx_benchmark.rs b/grovedb/benches/cidx_benchmark.rs index ec6f5644a..dfbc1eb3e 100644 --- a/grovedb/benches/cidx_benchmark.rs +++ b/grovedb/benches/cidx_benchmark.rs @@ -126,26 +126,42 @@ fn populate_plain_count_tree(n: usize) -> (TempDir, GroveDb, &'static GroveVersi (dir, db, grove_version) } -/// top_k via cidx: secondary range scan, `O(log n + k)`. +/// top_k via cidx: secondary range scan, `O(log n + k)` — measured +/// through the public surface (`run_path_query` over an axis +/// `PathQuery`), the route production reads take. #[cfg(feature = "minimal")] fn bench_cidx_top_k(c: &mut Criterion) { + use grovedb::element::IndexAxis; + use grovedb::query_result_type::QueryResultType; + use grovedb::{PathQuery, PathQueryRun}; + let mut group = c.benchmark_group("cidx_top_k"); for &n in &[100usize, 1_000, 10_000] { let (_dir, db, gv) = populate_cidx(n); - group.bench_function(format!("n={}_k=10", n), |b| { - b.iter(|| { - db.indexed_count_top_k([b"cidx".as_slice()].as_ref(), 10, true, None, gv) - .unwrap() - .expect("bench_cidx_top_k: count_indexed_top_k k=10") - }); - }); - group.bench_function(format!("n={}_k=100", n), |b| { - b.iter(|| { - db.indexed_count_top_k([b"cidx".as_slice()].as_ref(), 100, true, None, gv) - .unwrap() - .expect("bench_cidx_top_k: count_indexed_top_k k=100") + for k in [10u16, 100] { + let path_query = + PathQuery::new_axis_top_k(vec![b"cidx".to_vec()], IndexAxis::Count, k, 0, true); + group.bench_function(format!("n={}_k={}", n, k), |b| { + b.iter(|| { + match db + .run_path_query( + &path_query, + true, + true, + true, + QueryResultType::QueryPathKeyElementTrioResultType, + None, + gv, + ) + .unwrap() + .expect("bench_cidx_top_k: axis top-k read") + { + PathQueryRun::AxisEntries { entries, .. } => entries, + other => panic!("expected AxisEntries, got {other:?}"), + } + }); }); - }); + } } group.finish(); }