Skip to content

fix(drive): skip the ranked offset by counting instead of walking - #4382

Open
shumkov wants to merge 5 commits into
v4.2-devfrom
fix/ranked-unproved-read-through-prover
Open

fix(drive): skip the ranked offset by counting instead of walking#4382
shumkov wants to merge 5 commits into
v4.2-devfrom
fix/ranked-unproved-read-through-prover

Conversation

@shumkov

@shumkov shumkov commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Ranked queries (SELECT <agg> GROUP BY <prop> ORDER BY <agg> LIMIT k OFFSET m) accept an
unbounded OFFSET, and the comment justifying that described the proving path only:

grovedb's paginated prover attests the skipped region from the counted subtree commitments
instead of walking it … There is no denial-of-service lever here to cap.

The unproved arm did not work that way. It skipped by stepping a storage iterator once per
skipped entry, so the skip alone cost Θ(min(offset, population)) — work proportional to a
number the caller chooses, on a path where nothing bounds it: ranked queries carry no fee, the
dispatcher does no cost accounting, a spawn_blocking query cannot be cancelled by client
disconnect or stream reset, and the gateway's only rate limit is per source IP across the whole
Platform service, so a query competes with state transitions rather than having its own budget.

The ranked surface exists only in v4.2.0-dev.1; no stable release contains it.

What was done?

grovedb gained a counted traversal for plain reads (dashpay/grovedb#792), which is the same
descent its prover already used, minus the proof. It reads each subtree's aggregate count off its
link and collapses any subtree that fits entirely inside the remaining offset rather than stepping
through it. This PR points the unproved executor at it.

Consequences:

  • The skip is O(log n) at any offset, not proportional to it.
  • An offset at or past the population is answered from the root's own count with no descent at
    all
    — the pathological input becomes the cheapest request on the surface rather than the most
    expensive.
  • offset = 0 is untouched: it keeps the plain iterator path and never reads the tree, so the
    common unpaginated request costs exactly what it did before. grovedb pins that with an
    always-on equality test.
  • The proof path is unchanged, and grovedb's proof suites needed no edits.

Measured by grovedb's own harness (seek/byte counters are the machine-independent signal; the full
grid is in that PR). At a million rows, a deep offset drops from a full linear scan to a
tree-depth descent — 22 seeks, ~3.7 KB, ~32 µs — and past the end to a flat 3 seeks / 366 B / 4 µs
at every population size. The counters scale as tree depth (11 → 15 → 18 → 22 across 1e3 → 1e6),
which is the shape the design predicts.

One corner measured and accepted rather than hidden: at a small positive offset with k = 100,
the counted path costs about 5× the linear read in wall-clock (~155 µs against ~30 µs) because
k tree point-gets are slower than k sequential iterator steps. Crossover to counted-wins sits
a few hundred rows in, worst measured cost is ~155 µs, and the alternative — a threshold hybrid
falling back to the linear skip below some offset — would make the skipped-region semantics depend
on the offset value. Uniform semantics won.

Pin: currently the grovedb branch rev, so this is reviewable now; to be re-pinned to the
develop merge commit before merge. That is a one-line change and does not invalidate review of
anything else here.

Supersedes this PR's own earlier approach. It previously served unproved reads by generating a
proof internally and verifying it to recover the entries. That worked, but it paid proof
construction, serialization and verification on every read, put a floor under the common
offset = 0 case, and its retry drew a blocking review for pairing new-state results with old
block metadata. The counted read removes the floor and, by having no proof envelope on the read
path, removes the retry and the state/metadata window with it. History was rewritten because the
old commits implemented an approach the diff no longer contains.

How Has This Been Tested?

  • cargo test -p drive --lib3386 passed, 0 failed
  • cargo test -p drive-abci --lib query::623 passed, 0 failed
  • cargo clippy --workspace --all-features → clean
  • cargo fmt --check --all → clean

The informative result is which assertions moved. Across ~3,400 tests, four needed changing
and every one was a skipped value — three in drive, one on the wire in drive-abci. No entry or
ordering assertion moved, on any axis, in either direction, at any offset or k. That is the
claim this change stakes itself on: the counted read returns exactly what the linear walk
returned, and only the reported skip differs.

Read consistency

The counted page — root, descent and collect — is served from a single transaction raw iterator
with a pinned snapshot plus the transaction overlay, which is the same consistency mechanism the
linear scan it replaces relied on. This matters because the descent performs several reads where
the old scan performed one: without a pinned view, a block committing mid-descent could pair a
parent from the old state with a child from the new one, and merk does not verify a fetched child
against the parent's recorded link hash, so the result would be a silently mixed page rather than
an error. The proved path is not exposed the same way — a torn read there fails the verifier's
ancestor-chain reconciliation — which is why this was specific to the unproved read.

Cost of the guarantee: one extra seek (deep offset 22 → 23; offset 0 unchanged at 5; past-the-end
flat at 4).

Scope of the testing, stated rather than implied: the transaction-overlay behaviour is pinned by a
test. The commit-interleave case is not deterministically testable — there is no hook to pause a
fetch and force a commit mid-descent — so that half is argued from the mechanism, not proven by a
test.

A gate lesson worth keeping

This PR broke CI in a way cargo clippy --workspace --all-features structurally cannot catch, and that is worth writing down because the opposite advice is commonly given.

The pinned grovedb rev exported a type under any(minimal, verify) while the module holding it was gated on minimal alone. Any build enabling verify without minimal failed with error[E0432]: unresolved import. --all-features turns every feature on, so the broken combination never occurs and the check passes; the failure only appears in a narrow cut, and it took CI's Check transport-free feature cut job — reproducible locally as cargo check -p drive --no-default-features --features verify — to surface it. The Kotlin native-library job hit the same error for the same reason.

So the two gates catch different classes and neither substitutes for the other:

  • --workspace --all-features catches unbuilt sibling crates and feature-gated callers of a changed API. It is blind to feature-gating bugs.
  • The narrow cuts (--no-default-features --features verify, and the other combinations CI builds) catch gating bugs. They are blind to most of what breadth catches.

If you are changing a #[cfg], adding a re-export, or bumping a dependency that does either, run the cut as well as the breadth build.

Breaking Changes

No API or wire format change. One wire-visible behaviour change on unproved responses.

RankedPage::skippedGetDocumentsResponseV1.ResultData.Ranked.skipped on the wire — stops
echoing the request. The old read could not tell how far a short walk got, so the server echoed
the requested offset back. The counted descent tracks it, so both paths now report the same
quantity: the requested offset when the skip succeeded, and the ranking's total population when
the walk ran out of groups first. On a five-group ranking asked for a page well past the end, an
unproved response now reports 5 where it previously reported the offset.

A client asserting skipped == requested_offset sees a different value past the end. A client
using it as the rank base for entries[i] — its documented purpose — is unaffected, and gains a
population count it previously had to prove to obtain.

The value is not attested on the unproved path. It equals the attested one on an honest node,
and nothing forces a node to be honest — the same trust model as the entries beside it. The proto,
the Objective-C generated client (the only generated client carrying proto prose), the developer
book and the Rust docs all say so explicitly, so "the true population" is not read as a
cryptographic guarantee. One further nuance is documented at the field: the population comes from
the secondary's root aggregate while the per-node payload check only fires on visited nodes, so in
a corrupt secondary the unproved value can disagree with the true row count where the proved one
would not. On any valid secondary they are identical by construction.

Mixed-network note: the ranked surface first appears in v4.2.0-dev.1, so a network mixing
that tag with newer nodes returns the echoed offset from one and the population from the other for
the same unproved request. Devnet-only exposure, and the proto's new "do not assume this field
equals the offset you requested" advice is safe against both.

Comments corrected

Three comments asserted things the code did not do, one of them the justification for leaving
OFFSET uncapped. They land here rather than earlier on purpose: two of them state the policy, and
an accurate description of an uncapped cost lever is only safe to publish alongside the change that
removes it.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Bug Fixes

    • Ranked pagination now reports the actual number of groups skipped when offsets extend beyond available results.
    • Proved and unproved reads return consistent skip counts, including for empty pages.
    • Ranked queries maintain efficient performance across all offsets using counted traversal.
  • Documentation

    • Clarified pagination behavior, performance, skip counts, and proof attestation.
    • Updated API guidance to distinguish cryptographically attested proved counts from server-reported unproved counts.

@thepastaclaw

thepastaclaw commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — 9 ahead in queue (commit 6f70cc5)
Queue position: 10/10 · 1 review active
ETA: start ~21:53 UTC · complete ~22:08 UTC (median 15m across 30 recent reviews; 2 slots)
Queued 12m ago · Last checked: 2026-08-13 20:40 UTC

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 128d3c8e-9c6e-4489-9575-6e11b275d9b3

📥 Commits

Reviewing files that changed from the base of the PR and between 7091b33 and 6f70cc5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h
  • packages/dapi-grpc/protos/platform/v0/platform.proto
  • packages/rs-dpp/Cargo.toml
  • packages/rs-drive-abci/Cargo.toml
  • packages/rs-drive/Cargo.toml
  • packages/rs-platform-version/Cargo.toml
  • packages/rs-platform-wallet/Cargo.toml
  • packages/rs-sdk/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/rs-sdk/Cargo.toml
  • packages/dapi-grpc/protos/platform/v0/platform.proto
  • packages/rs-dpp/Cargo.toml
  • packages/rs-drive/Cargo.toml

📝 Walkthrough

Walkthrough

Ranked pagination now reports the actual number of groups skipped. Proved and unproved reads use the same value, including past-end queries. Proved responses attest the value cryptographically. Grovedb dependencies, tests, and documentation use the updated behavior.

Changes

Ranked pagination

Layer / File(s) Summary
Ranked execution semantics
packages/rs-drive/src/query/drive_document_ranked_query/...
The executor returns Grovedb’s actual skipped count for count, sum, average, and page results. Documentation describes counted descent and past-end behavior.
Ranked pagination contracts
packages/dapi-grpc/protos/platform/v0/platform.proto, packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h, packages/rs-drive/src/query/drive_document_ranked_query/mod.rs, packages/rs-drive/src/query/drive_document_ranked_query/mode_detection/v0/mod.rs, book/src/drive/ranked-index-examples.md
Public documentation defines the same skipped-count semantics for proved and unproved responses.
Past-end pagination validation
packages/rs-drive-abci/src/query/document_query/v1/tests.rs, packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs, packages/rs-drive/src/query/drive_document_ranked_query/tests.rs
Tests expect the ranking population instead of the requested offset.
Grovedb revision alignment
packages/rs-dpp/Cargo.toml, packages/rs-drive-abci/Cargo.toml, packages/rs-drive/Cargo.toml, packages/rs-platform-version/Cargo.toml, packages/rs-platform-wallet/Cargo.toml, packages/rs-sdk/Cargo.toml
Grovedb dependencies now reference revision 0100cb833075621659a68ddd3696baecc98e55b8.

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

Mergeability Score: ⚪ Minimal · up to 6f70c

The change replaces linear ranked-query offset skipping with counted traversal and updates the reported skipped value past the end; the supplied validation indicates the behavior is merge-ready after normal checks, with no actionable merge-blocking risk remaining.

Sequence Diagram(s)

sequenceDiagram
  participant RankedQuery
  participant Grovedb
  participant RankedPage
  RankedQuery->>Grovedb: Request indexed top-K page with offset
  Grovedb-->>RankedQuery: Return entries and actual skipped count
  RankedQuery->>RankedPage: Map entries and preserve skipped count
  RankedPage-->>RankedQuery: Return ranked pagination response
Loading

Possibly related PRs

Suggested reviewers: lklimek, quantumexplorer, thepastaclaw

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: using counted traversal instead of walking to skip ranked offsets.
✨ 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 fix/ranked-unproved-read-through-prover

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-08-13T20:38:07.478Z

@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/rs-drive/src/query/drive_document_ranked_query/tests.rs (1)

1682-1694: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a separate tolerance constant for proof size.

byte_slack is derived as a storage-loaded-bytes allowance (256 bytes per tree level). Line 1690 reuses it as a proof-size tolerance. The two quantities are unrelated, so a later change to the storage allowance silently changes this tripwire. Define a distinct constant for the proof-size comparison.

🤖 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 `@packages/rs-drive/src/query/drive_document_ranked_query/tests.rs` around
lines 1682 - 1694, Define a dedicated proof-size tolerance constant near the
proof-size assertion, rather than reusing byte_slack. Update the proof_bytes_at
comparison to use this new constant, while leaving byte_slack exclusively for
storage-loaded-bytes allowances.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-drive-abci/src/query/document_query/v1/tests.rs`:
- Around line 3058-3090: Update empty_ranking_proof_rejection and its tests so
only the exact supported GroveError::CorruptedData message “Cannot create proof
for empty tree” is reclassified as QueryError::InvalidArgument. Replace the
substring-based contains predicate with exact message matching, and add a test
case containing the marker within unrelated text that must remain unmapped.

In `@packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs`:
- Around line 171-180: Update the proof-read flow in the ranked query execution
around verify_indexed_axis_top_k_paginated to use snapshot isolation; if
unavailable, add a bounded retry at the dispatcher only when the error is the
specific chain-mismatch verification failure. Preserve immediate propagation for
all other proof or GroveDB errors.

---

Nitpick comments:
In `@packages/rs-drive/src/query/drive_document_ranked_query/tests.rs`:
- Around line 1682-1694: Define a dedicated proof-size tolerance constant near
the proof-size assertion, rather than reusing byte_slack. Update the
proof_bytes_at comparison to use this new constant, while leaving byte_slack
exclusively for storage-loaded-bytes allowances.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 09b317c6-215d-46bb-8681-50ff49f5eb3c

📥 Commits

Reviewing files that changed from the base of the PR and between f05bf82 and f432daa.

📒 Files selected for processing (12)
  • book/src/drive/ranked-index-examples.md
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h
  • packages/dapi-grpc/protos/platform/v0/platform.proto
  • packages/rs-drive-abci/src/query/document_query/v1/mod.rs
  • packages/rs-drive-abci/src/query/document_query/v1/tests.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs
  • packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs
  • packages/rs-drive/src/query/drive_document_ranked_query/executors/top_k_no_proof.rs
  • packages/rs-drive/src/query/drive_document_ranked_query/mod.rs
  • packages/rs-drive/src/query/drive_document_ranked_query/mode_detection.rs
  • packages/rs-drive/src/query/drive_document_ranked_query/tests.rs
  • packages/rs-drive/src/verify/document_ranked/verify_ranked_top_k_proof/v0/mod.rs

Comment thread packages/rs-drive-abci/src/query/document_query/v1/tests.rs Outdated
Comment thread packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs Outdated
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.14035% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.33%. Comparing base (6495991) to head (6f70cc5).

Files with missing lines Patch % Lines
...query/drive_document_ranked_query/execute_top_k.rs 60.37% 21 Missing ⚠️
...ive_document_ranked_query/mode_detection/v0/mod.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4382      +/-   ##
============================================
- Coverage     87.67%   87.33%   -0.35%     
============================================
  Files          2710     2711       +1     
  Lines        345200   346464    +1264     
============================================
- Hits         302667   302576      -91     
- Misses        42533    43888    +1355     
Components Coverage Δ
dpp 88.50% <ø> (-0.46%) ⬇️
drive 85.76% <56.14%> (-0.56%) ⬇️
drive-abci 89.70% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.92% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 47.40% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shumkov

shumkov commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Responding to the nitpick from the review body (it has no thread of its own): separate tolerance constant for proof size — agreed and done in 577fe3d.

There's now a proof_size_slack, derived from what an envelope actually carries per level of the counted descent, with a comment recording why it must stay distinct from byte_slack: the latter bounds storage reads, the two are unrelated quantities, and sharing one constant would let a change to either silently move the other's tripwire. That was a fair catch.

For the record, the two inline comments are answered in their own threads: the retry suggestion was adopted, and the exact-match suggestion for the empty-tree marker was rejected because grovedb wraps merk's constant in its own prefix, so exact matching would prevent the mapper from ever firing.

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should not use the proved path for this, instead there are unproved ways that will make this fast, even faster.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The prover-backed ranked read removes the offset-proportional walk, but the new handler-local retry crosses the state-publication boundary without refreshing the captured PlatformState. A retry that succeeds after the GroveDB commit can therefore return new-state results or proof bytes with the previous block's metadata and signature, so this requires changes before merge.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/query/document_query/v1/mod.rs:1367-1369: Retrying only Drive execution can pair new-state results with old block metadata
  The retry re-executes the Drive request while retaining the `platform_state` reference captured by `QueryService` before the query began. This is unsafe in the exact commit-visible/guard-not-yet-published window the retry is intended to cover: `update_state_cache_v0` publishes the new PlatformState before the database transaction commits, `finalize_block` commits GroveDB, and only afterward stores the new `committed_block_height_guard`. A query that captured the old state before publication can have its first envelope torn by the commit, then successfully rebuild against the newly committed GroveDB state here. Because the guard still has the old height, the service post-check sees it equal to the captured old state's height and accepts the response. Lines 1409-1417 then attach metadata—and, for proved responses, the old block signature and block ID—from that old PlatformState to new-state data or proof bytes. The retry must restart at a boundary that reloads PlatformState and reruns the service consistency checks; a successful local retry cannot safely be wrapped with the existing state object.

Comment thread packages/rs-drive-abci/src/query/document_query/v1/mod.rs Outdated
The `prove = false` arm of a ranked query skipped its OFFSET by stepping
a storage iterator once per skipped entry, so the skip alone cost
`Theta(min(offset, population))` on a surface where offset has no
ceiling. Ranked queries carry no fee, cannot be cancelled once
dispatched, and share their rate budget with state transitions rather
than having one of their own, so that made the skip an unmetered cost
lever for an unauthenticated caller. The proved path never had it: its
prover attests the skipped region from the counted subtree commitments
instead of traversing it.

grovedb now exposes that same counted descent to plain reads
(dashpay/grovedb#792): it reads each subtree's aggregate count off its
link and collapses any subtree that fits inside the remaining offset
rather than stepping through it. Point the unproved executor at it and
the skip becomes `O(log n)` at any offset — and an offset at or past the
population is answered from the root's own count with no descent at all,
making the worst input the cheapest request rather than the most
expensive. `offset = 0` keeps the plain iterator path and never touches
the tree, so the common unpaginated request costs exactly what it did.

Pinned to the grovedb branch rev so this is reviewable now; to be
re-pinned to the develop merge commit before merge.

BEHAVIOUR CHANGE, wire-visible on unproved responses

`RankedPage::skipped`, which reaches the wire as
`GetDocumentsResponseV1.ResultData.Ranked.skipped`, stops echoing the
request. The old read could not report how far a short walk got, so the
server echoed the requested offset back; the counted descent tracks it,
so both paths now report the same quantity — the requested offset when
the skip succeeded, the ranking's population when the walk ran out of
groups first. A client asserting `skipped == requested_offset` will see
a different value past the end; one using it as the rank base for
`entries[i]`, its documented purpose, is unaffected.

The value is not attested on the unproved path. It equals the attested
one on an honest node, and nothing forces a node to be honest — the same
trust model as the entries beside it. The proto, the Objective-C client
that carries proto prose, the developer book and the Rust docs all say
so rather than letting "the true population" read as a guarantee.

Three comments asserted things the code did not do, including the
justification for leaving OFFSET uncapped. They are corrected here
rather than earlier because two of them state the policy, and an
accurate description of an uncapped lever is only safe to publish
alongside the thing that removes it.

Tests: four assertions changed across ~3,400, every one a `skipped`
value — three in drive, one on the wire in drive-abci. No entry or
ordering assertion moved, which is the claim: the counted read returns
what the linear walk returned.

drive --lib 3386 passed; drive-abci --lib query:: 623 passed;
cargo clippy --workspace --all-features and cargo fmt --check --all both
clean.
@shumkov
shumkov force-pushed the fix/ranked-unproved-read-through-prover branch from 577fe3d to 93806d7 Compare August 13, 2026 17:13
@shumkov
shumkov force-pushed the fix/ranked-unproved-read-through-prover branch from 577fe3d to 93806d7 Compare August 13, 2026 17:13
@shumkov shumkov changed the title fix(drive): serve unproved ranked reads through the paginated prover fix(drive): skip the ranked offset by counting instead of walking Aug 13, 2026
`e41d57e0` exported `IndexedTopKPage` under `any(minimal, verify)` while
the module holding it is gated on `minimal` alone, so any build enabling
`verify` without `minimal` failed to compile:

    error[E0432]: unresolved import `operations::indexed_tree`
    note: found an item that was configured out — gated behind `minimal`

That is drive's verifier-only cut, which CI builds as "Check
transport-free feature cut" and which the Kotlin native-library job hits
too. `cc7b3997` narrows the export's gate to match the module's, and
adds a grovedb-side test pinning that the unproved `skipped` equals the
proved path's attested value — the property this PR's assertions rest on.

Re-pinned across all 14 workspace entries with `Cargo.lock` regenerated;
no reference to the old rev remains anywhere in the tree.

Verified with the exact invocation that reproduced the failure:
`cargo check -p drive --no-default-features --features verify`, clean.
…rration

Two fixes from an independent review of the rebuilt diff.

The past-the-end paragraph called `skipped` the ranking's "attested
total population" while the paragraph below it correctly says an
unproved response carries only the node's unverified claim, so the
public contract contradicted itself within one comment. It now says
"reported", and the warrant distinction is stated once, in the place
that explains it. Synced across the proto, the Objective-C client that
carries proto prose, and the book. The request-side proto text that
said the response "echoes the attested number" is corrected the same
way — it echoes nothing, and the number is only attested when proving.

The comments also narrated the change rather than describing the code:
"as it used to", "pinned as gone", "no longer has to", "the
wire-visible half of that behaviour change". That reads as a diary to
anyone who arrives after the change is history, which the repo's
comment rule exists to prevent. Rewritten as current behaviour; the
assertions are unchanged.
@shumkov

shumkov commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Please review the current head (0f44e70130). Your last review was of 577fe3d3, which is a different implementation — this PR was rebuilt, not amended, so a re-read from scratch is worth more than a diff against your previous pass.

What changed since you last saw it:

  • Prove-then-verify is gone. The unproved ranked read no longer generates a grovedb proof and verifies it to recover entries. grovedb exposed its counted descent to plain reads (feat: counted offset skip for unproved ranked paginated reads grovedb#792), so the executor now calls indexed_*_top_k_paginated directly and reads skipped off IndexedTopKPage.
  • The handler-local retry is gone entirely — that was the substance of the previous blocking finding (a retry could pair new-state results with the previously captured PlatformState's block metadata and signature). It is not narrowed or made state-safe; with no proof envelope on the read path there is nothing to tear, so there is nothing to retry.
  • The diff is much smaller: 17 files, +241/−154, down from +1067/−263.
  • The behaviour change that remains is skipped on unproved responses: it reports the skip actually performed rather than echoing the requested offset, and the docs state plainly that on that path it is the node's unverified claim rather than an attested value.

Two of your earlier findings were adopted and remain relevant to the current code: the empty-tree mapper's narrowness, and the separate proof-size tolerance constant. Your suggestion to exact-match the empty-tree marker was declined, with reasoning in that thread — grovedb wraps merk's constant in its own prefix, so an equality predicate would never fire.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@shumkov I will review the current head from scratch. I will assess the direct paginated read path, skipped behavior, error mapping, documentation, and test coverage.


🧠 Learnings used
Learnt from: QuantumExplorer
Repo: dashpay/platform PR: 4266
File: packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs:135-167
Timestamp: 2026-08-02T21:07:14.747Z
Learning: For ranked aggregate proofs, `DriveDocumentRankedQuery::execute_top_k_with_proof` in `packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs` receives Grovedb's empty-tree proof failure as `grovedb::Error::CorruptedData(String)`, not a dedicated typed error. It must preserve the generic `Error::GroveDB` mapping. `packages/rs-drive-abci/src/query/document_query/v1/mod.rs` narrowly maps that specific empty-tree message to a query validation error until Grovedb provides a typed variant.

Learnt from: shumkov
Repo: dashpay/platform PR: 4382
File: packages/rs-drive-abci/src/query/document_query/v1/tests.rs:3058-3090
Timestamp: 2026-08-12T19:16:31.122Z
Learning: In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`, `empty_ranking_proof_rejection` must match `"Cannot create proof for empty tree"` within a `GroveError::CorruptedData` message. GroveDB wraps Merk's marker with dynamic indexed-axis proof context, so the observed error payload is not equal to the bare marker. The mapping must remain limited to the GroveDB corrupted-data error variant; unrelated `CorruptedData`, `PathNotFound`, and non-GroveDB errors must remain unmapped.
✅ Action performed

Review finished.

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

`cc7b3997` predated the fixes for dashpay/grovedb#792's own Linting and
codecov failures. `c4ceac67` is the rev where all 11 of that PR's checks
pass, so this pins the code that was actually verified rather than an
intermediate commit.

All 14 workspace entries plus `Cargo.lock`; no reference to any earlier
rev (`cc7b3997`, `e41d57e0`, `a2791bbd`) remains anywhere in the tree.

Still a branch rev, deliberately: pinning the tested commit beats
pinning an untested one, and the alternative is blocking on a merge.
To be re-pinned to the develop merge commit once #792 lands, since a
Platform PR pinning a branch that could later be deleted is a fair
review objection.

Verified: `cargo check -p drive --no-default-features --features verify`
(the cut that caught the last feature-gate bug) clean; drive ranked 71
passed, drive-abci ranked 18 passed, fmt clean.
@shumkov

shumkov commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Superseding my previous request: review 7091b33212, not 0f44e70130 — that head has been replaced by a dependency re-pin and my earlier request is stale. This is the settled head; no further pushes are planned before merge.

Your last substantive review was of 577fe3d3, which is a different implementation. This PR was rebuilt rather than amended, so a fresh read is worth more than a diff against your previous pass:

  • Prove-then-verify is gone. The unproved ranked read no longer generates a grovedb proof and verifies it to recover entries. grovedb exposed its counted descent to plain reads (feat: counted offset skip for unproved ranked paginated reads grovedb#792), so the executor calls indexed_*_top_k_paginated directly and reads skipped off IndexedTopKPage.
  • The handler-local retry is gone entirely — the substance of the previous blocking finding, where a retry could pair new-state results with the block metadata and signature of the PlatformState captured before the query. It is not narrowed or made state-safe: with no proof envelope on the read path there is nothing to tear, so there is nothing to retry.
  • The diff is much smaller: 17 files, +241/−154, down from +1067/−263.
  • The remaining behaviour change is skipped on unproved responses — it reports the skip actually performed rather than echoing the requested offset, and the docs state that on that path it is the node's unverified claim rather than an attested value.

Two of your earlier findings were adopted and still apply to the current code: the empty-tree mapper's narrowness, and a separate proof-size tolerance constant. Your suggestion to exact-match the empty-tree marker was declined with reasoning in that thread — grovedb wraps merk's constant in its own prefix, so an equality predicate would never fire on a real failure.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== HEAD =='
git rev-parse HEAD
git status --short
echo '== target commit =='
git show --no-ext-diff --no-renames --format=fuller --stat 7091b33212
echo '== merge base and changed files =='
base="$(git merge-base 7091b33212 origin/master 2>/dev/null || git merge-base 7091b33212 origin/main)"
printf 'BASE=%s\n' "$base"
git diff --no-ext-diff --no-renames --name-status "$base" 7091b33212
echo '== changed implementation outline =='
ast-grep outline packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs --items all --view expanded
ast-grep outline packages/rs-drive-abci/src/query/document_query/v1/mod.rs --items all --view expanded
echo '== focused symbols and call sites =='
rg -n -C 5 --glob '*.rs' \
  'indexed_.*_top_k_paginated|IndexedTopKPage|empty_ranking_proof_rejection|proof_size_slack|byte_slack|execute_top_k_with_proof|execute_top_k' \
  packages/rs-drive packages/rs-drive-abci
echo '== target diff =='
git diff --no-ext-diff --no-renames --unified=80 "$base" 7091b33212 -- \
  packages/rs-drive/src/query/drive_document_ranked_query \
  packages/rs-drive-abci/src/query/document_query/v1 \
  packages/rs-drive/Cargo.toml \
  packages/rs-drive-abci/Cargo.toml \
  packages/rs-dpp/Cargo.toml \
  packages/rs-platform-version/Cargo.toml \
  packages/rs-platform-wallet/Cargo.toml \
  packages/rs-sdk/Cargo.toml \
  packages/dapi-grpc/protos/platform/v0/platform.proto \
  book/src/drive/ranked-index-examples.md

Length of output: 3619


🏁 Script executed:

#!/bin/bash
set -u

echo '== refs =='
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes refs/heads | sort

echo '== recent ancestry =='
git log --oneline --decorate --graph -25 HEAD

echo '== candidate merge bases =='
for ref in origin/develop develop upstream/develop origin/master master origin/main main; do
  if git rev-parse --verify -q "$ref" >/dev/null; then
    printf '%s ' "$ref"
    git merge-base HEAD "$ref" || true
  fi
done

echo '== all commits after a likely base, if origin/develop exists =='
if git rev-parse --verify -q origin/develop >/dev/null; then
  base="$(git merge-base HEAD origin/develop)"
  printf 'BASE=%s\n' "$base"
  git log --oneline --reverse "$base"..HEAD
  echo '== changed files =='
  git diff --no-ext-diff --no-renames --name-status "$base"..HEAD
fi

Length of output: 3542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="6495991c5e21f4f211ba326334ce3f717aaaf3bd"
head="7091b332120f35e7155a7867fa98a209c22bca6a"

echo '== changed files =='
git diff --no-ext-diff --no-renames --name-status "$base" "$head"

echo '== implementation outlines =='
ast-grep outline packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs --items all --view expanded
ast-grep outline packages/rs-drive/src/query/drive_document_ranked_query/mod.rs --items all --view expanded
ast-grep outline packages/rs-drive-abci/src/query/document_query/v1/mod.rs --items all --view expanded

echo '== direct read calls, result handling, and empty-tree mapping =='
rg -n -C 8 --glob '*.rs' \
  'indexed_(value|sum|count|average)_top_k_paginated|IndexedTopKPage|empty_ranking_proof_rejection|CorruptedData|proof_size_slack|byte_slack' \
  packages/rs-drive/src/query/drive_document_ranked_query \
  packages/rs-drive-abci/src/query/document_query/v1

echo '== source diff: ranked implementation and ABCI handling =='
git diff --no-ext-diff --no-renames --unified=100 "$base" "$head" -- \
  packages/rs-drive/src/query/drive_document_ranked_query \
  packages/rs-drive-abci/src/query/document_query/v1

echo '== source diff: API docs and dependency manifests =='
git diff --no-ext-diff --no-renames --unified=60 "$base" "$head" -- \
  packages/dapi-grpc/protos/platform/v0/platform.proto \
  packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h \
  book/src/drive/ranked-index-examples.md \
  packages/rs-dpp/Cargo.toml \
  packages/rs-drive/Cargo.toml \
  packages/rs-drive-abci/Cargo.toml \
  packages/rs-platform-version/Cargo.toml \
  packages/rs-platform-wallet/Cargo.toml \
  packages/rs-sdk/Cargo.toml \
  Cargo.lock

echo '== stale GroveDB pins outside the lockfile =='
rg -n --glob 'Cargo.toml' --glob 'Cargo.lock' \
  'a2791bbdca756d6a6113024aec48f09f7a33faa9|cc7b3997be5819c60eb09251f963c1ed1a89306f|e41d57e0|c4ceac67' .

Length of output: 199377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== ranked response conversion =='
rg -n -C 12 --glob '*.rs' \
  'DocumentRankedResponse|RankedPage|skipped:|set_skipped|RankedEntries' \
  packages/rs-drive/src/query/drive_document_ranked_query \
  packages/rs-drive-abci/src/query/document_query/v1/dispatch

echo '== ranked dispatch execution path =='
sed -n '80,190p' packages/rs-drive-abci/src/query/document_query/v1/dispatch/ranked.rs
sed -n '1,135p' packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs
sed -n '135,255p' packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs

echo '== proof-size tolerance definition and uses =='
rg -n -C 12 --glob '*.rs' 'proof_size_slack|byte_slack|proof.*slack|slack.*proof' packages/rs-drive packages/rs-drive-abci

echo '== offset test cases across axes =='
rg -n -C 8 --glob '*.rs' \
  'at_offset\(|offset_pages|past_end|skipped.*[=,].*[0-9]|IndexedTopKPage' \
  packages/rs-drive/src/query/drive_document_ranked_query/tests.rs \
  packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs \
  packages/rs-drive-abci/src/query/document_query/v1/tests.rs

echo '== pinned GroveDB source candidates =='
find "${HOME:-/root}/.cargo/git/checkouts" -type f \
  \( -name '*.rs' -o -name 'Cargo.toml' \) \
  -print 2>/dev/null | grep -E '/grovedb[^/]*/|/grovedb-' | head -n 20 || true
find /tmp -type f -name '*.rs' -print 2>/dev/null | grep -E 'grovedb|indexed.*top.*k' | head -n 20 || true

Length of output: 113333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Avg call without overlapping output =='
nl -ba packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs | sed -n '125,155p'

echo '== proof-size tolerance candidates across the repository =='
rg -n -i -C 8 \
  'proof.{0,40}(size|slack|limit|bound|tolerance)|\
   (size|slack|limit|bound|tolerance).{0,40}proof|\
   byte_slack' \
  . \
  -g '!Cargo.lock' \
  -g '!**/node_modules/**' || true

echo '== all changed lines not previously inspected =='
git diff --no-ext-diff --no-renames --unified=35 \
  6495991c5e21f4f211ba326334ce3f717aaaf3bd \
  7091b332120f35e7155a7867fa98a209c22bca6a -- \
  packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs \
  packages/rs-drive/src/query/drive_document_ranked_query/mode_detection/v0/mod.rs \
  packages/rs-drive/src/query/drive_document_ranked_query/tests.rs \
  packages/rs-drive-abci/src/query/document_query/v1/tests.rs

echo '== GroveDB source tree paths at the pinned revision =='
gh api "repos/dashpay/grovedb/git/trees/c4ceac676630727d01b532314a7e488edbeaf115?recursive=1" \
  --jq '.tree[] | select(.path | test("indexed.*top.*k|top.*k.*indexed"; "i")) | [.path, .url] | `@tsv`' \
  || true

Length of output: 1624


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Avg call =='
sed -n '125,155p' packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs

echo '== proof-size tolerance candidates across the repository =='
rg -n -i -C 8 \
  'proof.{0,40}(size|slack|limit|bound|tolerance)|(size|slack|limit|bound|tolerance).{0,40}proof|byte_slack' \
  . \
  -g '!Cargo.lock' \
  -g '!**/node_modules/**' || true

echo '== remaining changed test lines =='
git diff --no-ext-diff --no-renames --unified=35 \
  6495991c5e21f4f211ba326334ce3f717aaaf3bd \
  7091b332120f35e7155a7867fa98a209c22bca6a -- \
  packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/ranked_index_e2e_tests.rs \
  packages/rs-drive/src/query/drive_document_ranked_query/tests.rs \
  packages/rs-drive-abci/src/query/document_query/v1/tests.rs

echo '== GroveDB source tree paths at the pinned revision =='
gh api "repos/dashpay/grovedb/git/trees/c4ceac676630727d01b532314a7e488edbeaf115?recursive=1" \
  --jq '.tree[] | select(.path | test("indexed.*top.*k|top.*k.*indexed"; "i")) | [.path, .url] | `@tsv`' \
  || true

Length of output: 201544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== exact proof-size tolerance identifiers =='
rg -n -C 6 --glob '*.rs' --glob '*.md' --glob 'Cargo.toml' \
  '\bproof_size_slack\b|\bbyte_slack\b|\bproof.*slack\b|\bslack.*proof\b' \
  packages/rs-drive \
  packages/rs-drive-abci \
  packages/rs-drive-proof-verifier \
  packages/rs-sdk \
  2>/dev/null || true

echo '== pinned GroveDB commit =='
gh api "repos/dashpay/grovedb/commits/c4ceac676630727d01b532314a7e488edbeaf115" \
  --jq '{sha: .sha, message: .commit.message, parents: [.parents[].sha]}' \
  || true

echo '== final direct-read call inventory =='
rg -n -C 3 --glob '*.rs' \
  'indexed_(count|sum|avg)_top_k_paginated\(' \
  packages/rs-drive/src/query/drive_document_ranked_query

Length of output: 4821


@shumkov Fresh review of 7091b33212 is complete. I found no blocking issues.

I checked the direct Count, Sum, and Avg paginated reads. Each path returns GroveDB’s skipped value. The ABCI handler forwards that value unchanged to RankedEntries.skipped.

The Drive and ABCI tests cover the past-end case. They verify that an unproved response reports the reached population. The proved result reports the same value.

I also checked the narrow empty-tree mapping using the prior review context. It remains limited to Error::GroveDB(GroveError::CorruptedData(_)) and "Cannot create proof for empty tree".

All GroveDB dependencies now resolve to c4ceac676630727d01b532314a7e488edbeaf115. GitHub confirms that this commit follows cc7b3997 and includes counted-read regression coverage.

I did not run builds or tests because the review sandbox does not permit repository code execution.


🧠 Learnings used
Learnt from: QuantumExplorer
Repo: dashpay/platform PR: 4266
File: packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs:135-167
Timestamp: 2026-08-02T21:07:14.747Z
Learning: For ranked aggregate proofs, `DriveDocumentRankedQuery::execute_top_k_with_proof` in `packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs` receives Grovedb's empty-tree proof failure as `grovedb::Error::CorruptedData(String)`, not a dedicated typed error. It must preserve the generic `Error::GroveDB` mapping. `packages/rs-drive-abci/src/query/document_query/v1/mod.rs` narrowly maps that specific empty-tree message to a query validation error until Grovedb provides a typed variant.

Learnt from: shumkov
Repo: dashpay/platform PR: 4382
File: packages/rs-drive-abci/src/query/document_query/v1/tests.rs:3058-3090
Timestamp: 2026-08-12T19:16:31.122Z
Learning: In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`, `empty_ranking_proof_rejection` must match `"Cannot create proof for empty tree"` within a `GroveError::CorruptedData` message. GroveDB wraps Merk's marker with dynamic indexed-axis proof context, so the observed error payload is not equal to the bare marker. The mapping must remain limited to the GroveDB corrupted-data error variant; unrelated `CorruptedData`, `PathNotFound`, and non-GroveDB errors must remain unmapped.
✅ Action performed

Review finished.

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

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The counted traversal removes offset-linear work, but positive-offset unproved reads now traverse with unsnapshotted point reads and can cross a block commit, returning mixed data under stale block metadata; this is blocking. The API request docs also still describe the skip as count-attested even though only proof results are attested, while the previous handler-local retry finding is fixed. Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol; openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs`:
- [BLOCKING] packages/rs-drive/src/query/drive_document_ranked_query/execute_top_k.rs:89-95: Counted reads lose the iterator's consistent view across a commit
  For a positive offset below the population, these calls reach GroveDB's `counted_skip_page`, which loads children through successive `RefWalker::walk` point reads. The dispatcher passes `transaction = None`; at pinned GroveDB revision `c4ceac676630727d01b532314a7e488edbeaf115`, `TxRef::new` therefore creates an `OptimisticTransactionDB::transaction()` with default options, whose snapshot setting is false, and each child fetch ultimately calls `Transaction::get`. The replaced implementation collected the skipped and returned rows through one RocksDB iterator, which pins one view for that scan. If a block commits during the new descent, the resident parent can be from the old state while a fetched child comes from the new state. Merk's child load preserves the parent's recorded hash without checking it against the fetched child, and the counted traversal only cross-checks aggregate counts, so a same-population update can produce a mixed page rather than an error. In the existing interval after the GroveDB commit but before `committed_block_height_guard` is stored, the query service can also accept that page and attach metadata from the previously captured `PlatformState`. Run the counted traversal against a storage snapshot, or defer this switch until GroveDB exposes a snapshot-consistent counted-read API.

In `packages/dapi-grpc/protos/platform/v0/platform.proto`:
- [SUGGESTION] packages/dapi-grpc/protos/platform/v0/platform.proto:1128-1131: Request docs still describe the unproved skip as attested
  The `offset` field applies to both proved and unproved ranked requests, but this paragraph still says the skip is count-attested and only describes GroveDB proving it. The implementation and the corrected response-field documentation distinguish the two paths: both use counted descent, but only the proved result attests the count. Update this paragraph and regenerate the Objective-C header, whose corresponding prose still says the response echoes an attested number.

Comment on lines 89 to 95
@@ -71,14 +94,18 @@ impl DriveDocumentRankedQuery<'_> {
transaction,
grove_version,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Counted reads lose the iterator's consistent view across a commit

For a positive offset below the population, these calls reach GroveDB's counted_skip_page, which loads children through successive RefWalker::walk point reads. The dispatcher passes transaction = None; at pinned GroveDB revision c4ceac676630727d01b532314a7e488edbeaf115, TxRef::new therefore creates an OptimisticTransactionDB::transaction() with default options, whose snapshot setting is false, and each child fetch ultimately calls Transaction::get. The replaced implementation collected the skipped and returned rows through one RocksDB iterator, which pins one view for that scan. If a block commits during the new descent, the resident parent can be from the old state while a fetched child comes from the new state. Merk's child load preserves the parent's recorded hash without checking it against the fetched child, and the counted traversal only cross-checks aggregate counts, so a same-population update can produce a mixed page rather than an error. In the existing interval after the GroveDB commit but before committed_block_height_guard is stored, the query service can also accept that page and attach metadata from the previously captured PlatformState. Run the counted traversal against a storage snapshot, or defer this switch until GroveDB exposes a snapshot-consistent counted-read API.

source: ['codex']

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid, and it is a regression this PR introduced. Leaving this thread open until the fix lands rather than closing it on intent.

I verified all three claims against the pinned rev c4ceac67 rather than taking the finding on trust:

  1. snapshot: falsestorage/src/rocksdb_storage/storage.rs:630-632 is fn start_transaction(&'db self) -> Self::Transaction { self.db.transaction() }: bare, no options. The only set_snapshot* anywhere in the storage layer is set_snapshot_consistency(false) at storage.rs:568, on the unrelated SST-ingest path.
  2. The replaced implementation really did pin one viewgrovedb/src/operations/indexed_tree.rs:1385 built a single KVIterator::new(secondary_merk.storage.raw_iter(), &q) and drove the entire while … next_kv() scan from it. A RocksDB iterator takes an implicit snapshot at creation, so the skip and the collect ran against one view.
  3. The new path does independent point readscounted_skip_page descends via merk.walk(...) / RefWalker, each child fetch a Transaction::get. And merk's walk does not verify a fetched child against the parent's recorded link hash, so a torn descent yields a mixed page silently rather than an error.

So the trade was a single consistent scan for O(log n) independent reads, without replacing the guarantee the iterator was providing for free.

Worth adding the asymmetry that explains why this is specific to the unproved path: the proved path performs many independent reads too, but a torn read there fails — the verifier's ancestor-chain reconciliation catches it. The unproved read has no such check, which is exactly why the missing snapshot is load-bearing here and not there.

Remedy agreed: grovedb takes the snapshot, at the layer that owns the traversal, covering both the descent and the collect. Platform cannot fix this from its side — start_transaction takes no options, so there is no snapshotted transaction to pass in. Deferring the switch was considered and rejected: it means reverting to the linear read, which reinstates the uncapped-offset cost lever this change exists to remove.

Explicitly out of scope, as pre-existing rather than introduced here: the window between a grovedb commit becoming visible and committed_block_height_guard being stored, in which the query service can attach stale PlatformState metadata. That applies to any read on this surface and is mostly absorbed by the existing retry in query/service.rs.

The accompanying suggestion is already fixed: the offset request-side paragraph and the matching Objective-C prose no longer describe the unproved skip as count-attested — both now say the descent is counted on either path and only the proved result attests the count.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, and consumed here — re-pinned to grovedb 0100cb83.

The fix serves the whole counted page — root, descent and collect — from one transaction raw iterator with a pinned snapshot plus the transaction overlay, and drops RefWalker from the walk. That is the same mechanism the linear scan it replaced used, so the guarantee this thread identified as missing is restored at the layer that owns the traversal, rather than worked around in Platform.

Cost: one extra seek. Deep offset 22 → 23, offset 0 unchanged at 5, past-the-end flat at 4. The offset-independence this PR is for is unaffected.

What is and is not proven, stated plainly. The transaction-overlay half is pinned by a new test. The commit-interleave half — a block committing mid-descent — is not deterministically testable: there is no fetch-pause hook to force a commit at a chosen point in the walk. That half rests on the mechanism (a single pinned-snapshot iterator cannot observe a write that lands after it is created) rather than on a test asserting it. I would rather say so than let "fixed" imply coverage that does not exist.

Comment on lines +1128 to +1131
// OFFSET 4` is the 5th-best group. The skip is **count-attested**,
// not walked: grovedb proves it from the counted subtree
// commitments, so the proof stays `O(log n + k)` at any offset and
// the response echoes the attested number in
// the response reports the skip it performed in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Request docs still describe the unproved skip as attested

The offset field applies to both proved and unproved ranked requests, but this paragraph still says the skip is count-attested and only describes GroveDB proving it. The implementation and the corrected response-field documentation distinguish the two paths: both use counted descent, but only the proved result attests the count. Update this paragraph and regenerate the Objective-C header, whose corresponding prose still says the response echoes an attested number.

Suggested change
// OFFSET 4` is the 5th-best group. The skip is **count-attested**,
// not walked: grovedb proves it from the counted subtree
// commitments, so the proof stays `O(log n + k)` at any offset and
// the response echoes the attested number in
// the response reports the skip it performed in
// OFFSET 4` is the 5th-best group. The skip is **counted, not
// walked** on both paths: grovedb descends using aggregate subtree
// counts, while the proved path additionally attests those counts
// in its proof. Work and proof size stay `O(log n + k)` at any
// offset, and the response reports the skip it performed in

source: ['codex']

The counted descent performed several point reads where the linear scan
it replaced performed one, and nothing replaced the consistency the
iterator had been providing for free. A block committing mid-descent
could pair a parent from the old state with a child from the new one,
and merk does not verify a fetched child against the parent's recorded
link hash, so the result was a silently mixed page rather than an error.
The proved path is not exposed the same way: a torn read there fails the
verifier's ancestor-chain reconciliation.

grovedb `0100cb83` serves the whole page — root, descent and collect —
from one transaction raw iterator with a pinned snapshot plus the
transaction overlay, the same mechanism the linear scan used, and drops
`RefWalker` from the walk. The guarantee is restored where the traversal
lives rather than worked around here. Cost is one extra seek: deep
offset 22 -> 23, offset 0 unchanged at 5, past-the-end flat at 4.

Testing scope, stated rather than implied: the transaction-overlay
behaviour is pinned by a new grovedb test. The commit-interleave case is
not deterministically testable — there is no hook to pause a fetch and
force a commit mid-descent — so that half rests on the mechanism, not on
a test.

Also folds a review suggestion: the `offset` request-side prose in the
proto and the Objective-C client no longer calls the unproved skip
count-attested. Both paths use the counted descent; only the proved
result attests the count.

Verified: narrow cut `cargo check -p drive --no-default-features
--features verify` clean, drive ranked 71 passed, drive-abci ranked 18
passed, fmt clean.
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.

3 participants