Skip to content

fix(drive,drive-abci): post-merge follow-ups for shielded anchor refactor#3606

Merged
QuantumExplorer merged 2 commits intov3.1-devfrom
platform-wallet/shielded-anchor-followups
May 6, 2026
Merged

fix(drive,drive-abci): post-merge follow-ups for shielded anchor refactor#3606
QuantumExplorer merged 2 commits intov3.1-devfrom
platform-wallet/shielded-anchor-followups

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 6, 2026

Issue being fixed or feature implemented

Follow-up review of #3605 (retire SHIELDED_MOST_RECENT_ANCHOR_KEY, derive most-recent from [8], never empty the index under prune). Four findings from the automated review, all verified valid against the merged v3.1-dev code.

What was done?

1. [BLOCKING] estimated_costs.rs: layout was still pre-PR

add_estimation_costs_for_shielded_pool_operations registered the shielded credit pool with the old shape — EstimatedLevel(4, false), items_size: Some((1, 32, None, 2)), comment claiming "9 elements (7 subtrees + 2 items)" — even though [7] is gone. The pool now holds 7 subtrees + 1 item (the SHIELDED_TOTAL_BALANCE_KEY SumItem):

  • EstimatedLevel(4, false)EstimatedLevel(3, false) (ceil(log2(8)) = 3)
  • items_size: Some((1, 32, None, 2))Some((1, 8, None, 1)) (one i64 SumItem, 8 bytes)
  • Comment updated to match.

This metadata feeds ShieldedPoolOperationType::into_low_level_drive_operations() whenever shielded ops are costed in estimation mode (apply=false), so without this fix every shielded note/nullifier/balance fee estimate would have over-reported pool depth + item count by one each.

2. prune_anchors_v0: use .pop() instead of recomputing max_key

entries_below comes from grove_get_raw_path_query over a Query::new() (default left_to_right = true) with RangeTo against SHIELDED_ANCHORS_BY_HEIGHT_KEY, whose keys are big-endian u64 — so GroveDB returns the entries in ascending numeric order. The previous code cloned every key into a new Vec<u8> to find the maximum, then walked the vec again to filter that key out — O(N) extra work plus per-entry allocation on a path that runs every block. .pop() on the already-sorted result accomplishes the same thing with zero scans or allocations.

3. query_most_recent_shielded_anchor_v0: document the empty-index convention

The non-proven branch returns Anchor(vec![0u8; 32]) when [8] is empty; the proven branch returns a proof that verify_most_recent_shielded_anchor_v0 decodes to Ok(None). Asymmetric handling that only stays coherent if the SDK's response decoder keeps treating the all-zeros anchor as "absent." The right long-term fix is a third "absent" variant on the gRPC oneof, but that's a wire-format change. For now, added a comment block at the sentinel-emitting site cross-referencing the verifier and flagging the lock-step requirement, so the implicit convention isn't invisible to future maintainers.

4. read_latest_recorded_shielded_anchor_v0: revert to crate-local

The helper was bumped to pub purely so the drive-abci strategy test could reach across crates. The doc justified that visibility with a caller (the non-proven getMostRecentShieldedAnchor path) that doesn't actually use it — that path calls grove_get_raw_path_query directly on shielded_latest_recorded_anchor_path_query() and parses the result inline. Reverted to pub(in crate::drive) matching record_shielded_pool_anchor_if_changed_v0, fixed the doc, and inlined the strategy test's read against the same shared PathQuery the production handler uses. Internal storage helpers don't need to bleed onto the public Drive surface for test convenience.

How Has This Been Tested?

  • cargo test -p drive --lib drive::shielded → 60 passed
  • cargo test -p drive --lib verify::shielded → 22 passed
  • cargo test -p drive-abci --lib query::shielded → 54 passed
  • cargo clippy -p drive -p drive-abci --all-targets clean
  • cargo fmt --all clean

Breaking Changes

None. The estimated-costs fix changes fee-estimation outputs by one Merk depth level + one item slot for shielded pool root-layer operations — small, in the direction of "now correct" — but doesn't affect on-chain state shape or RPC wire format. v3.1-dev hasn't shipped, so the v0 method bodies are amended in place.

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

Related

Follows up #3605; unblocks the spend rows of #3603 once the regtest restarts past the prune fix landing.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Optimized shielded pool storage layout for fewer elements and reduced tree depth.
    • Updated pruning to rely on ascending key order when excluding the live anchor.
    • Narrowed visibility of an internal anchor helper and updated tests to use the canonical index approach.
  • Bug Fixes

    • Corrupted/non-item entries in anchor queries now surface an explicit error instead of treating them as empty.
  • Documentation

    • Added explanatory comments on anchor conventions and structural simplifications.

…ctor

Four review findings from the automated review of #3605, all
verified valid against the merged code on v3.1-dev.

1. [BLOCKING] estimated_costs.rs: shielded credit pool layer info
   still modeled the pre-PR layout — `EstimatedLevel(4, false)`,
   `items_size: Some((1, 32, None, 2))`, comment "9 elements (7
   subtrees + 2 items)". After retiring `[7]` the pool actually
   holds 7 subtrees + 1 item (the `SHIELDED_TOTAL_BALANCE_KEY`
   SumItem). Updated to `EstimatedLevel(3, false)` (ceil(log2(8))
   = 3) and `items_size: Some((1, 8, None, 1))` (one i64 SumItem).
   Stateless fee estimation for shielded ops would otherwise over-
   report pool depth + item count by one each.

2. prune_anchors_v0 was building a `Vec<Vec<u8>>` of every key
   below cutoff just to find the maximum, then walking the
   collection again to filter that key out — O(N) extra work + a
   per-entry allocation on a path that runs every block. The
   `entries_below` vec comes from a `Query::new()` (default
   `left_to_right = true`) over a `RangeTo`, so GroveDB returns
   the entries in ascending big-endian-u64 → numeric order; the
   live anchor is the last element. Pop it and discard, no scan
   no allocation.

3. The `getMostRecentShieldedAnchor` non-proven branch returns
   `Anchor(vec![0u8; 32])` for an empty index; the proven branch
   returns a proof that `verify_most_recent_shielded_anchor_v0`
   maps to `Ok(None)`. Asymmetric handling that only stays
   coherent if the SDK's response decoder treats the all-zeros
   anchor as "absent" — preserved for wire-format compat. Added
   a comment block at the sentinel-emitting site cross-referencing
   the verifier and noting that any future "absent" variant on
   the response message would need to retire the sentinel in
   lock-step.

4. `Drive::read_latest_recorded_shielded_anchor_v0` was bumped to
   `pub` solely to let the strategy test reach across crates, and
   the doc justified that with a caller (the non-proven query
   path) that doesn't actually use it. Reverted visibility to
   `pub(in crate::drive)` to match `record_shielded_pool_anchor_if_changed_v0`,
   fixed the doc to describe the real callers, and inlined the
   strategy test's read against the same shared `PathQuery`
   (`shielded_latest_recorded_anchor_path_query`) the production
   handler uses. Internal storage helpers don't need to bleed
   onto the public Drive surface for test convenience.

60 drive shielded + 22 verify-shielded + 54 drive-abci shielded
query tests pass; clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v3.1.0 milestone May 6, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

Anchor handling and storage were adjusted: the shielded credit pool layout was simplified (9→8 elements, depth 4→3), anchor derivation moved to the anchors-by-height subtree, helper visibility tightened, tests switched to path-query-based anchor reads, and the non‑proved query path now errors (CorruptedElementType) on unexpected element types instead of returning the zero-anchor sentinel.

Changes

Shielded Anchor Management Refactoring

Layer / File(s) Summary
Data Shape / Layout
packages/rs-drive/src/drive/shielded/estimated_costs.rs
SumTree layout reduced to 8 elements (7 subtrees + 1 item); depth changed from 4→3. Items layout: previously two 32-byte items → now a single 8-byte item (total balance). Comments added documenting anchor derivation from anchors-by-height.
Core Logic: Anchor Extraction / Erroring
packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs
Inserted EMPTY-INDEX CONVENTION comment block and changed non-proved path to return CorruptedElementType when an entry is not an Item; preserved None→zero-anchor behavior; added DriveError import for new error path.
Core Logic: Pruning
packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs
Prune path that computes deletions now pops the last entry from entries_below (relying on ascending keys) to exclude the live anchor instead of filtering by max key; comments added clarifying rationale.
Wiring / Helper Visibility
packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs
Visibility of read_latest_recorded_shielded_anchor_v0 narrowed from pub to pub(in crate::drive); docs updated to reference the path-query/proofs canonical flow.
Tests / Integration
packages/rs-drive-abci/tests/strategy_tests/test_cases/shielded_tests.rs
Replaced direct helper-based anchor reads with shielded_latest_recorded_anchor_path_query + grove_get_raw_path_query to derive most-recent anchor from path-query results; adjusted assertions to validate key length/non-zero and cross-check anchor-to-height mapping.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • dashpay/platform#3605: Modifies getMostRecentShieldedAnchor path-query logic and touches similar anchor extraction behavior.

Poem

🐰
I hopped through trees of anchors, light and spry,
Pruned the highest branch and watched the old slot fly.
Now eight sweet boughs hold balances and song;
Paths show the newest anchor—steady, not wrong.
I nibble comments and skip along.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: post-merge follow-ups addressing four verified findings from the shielded anchor refactor (estimated costs, prune logic, query documentation, and visibility changes).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch platform-wallet/shielded-anchor-followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 6, 2026

Review Gate

Commit: 7795b955

  • Debounce: 3m ago (need 30m)

  • CI checks: checks still running (1 pending)

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (12:51 PM PT Wednesday)

  • Run review now (check to override)

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 72.72727% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.29%. Comparing base (7b23bc7) to head (7795b95).

Files with missing lines Patch % Lines
...ci/src/query/shielded/most_recent_anchor/v0/mod.rs 25.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3606      +/-   ##
============================================
- Coverage     88.29%   88.29%   -0.01%     
============================================
  Files          2479     2479              
  Lines        301660   301659       -1     
============================================
- Hits         266350   266346       -4     
- Misses        35310    35313       +3     
Components Coverage Δ
dpp 87.95% <ø> (ø)
drive 87.37% <100.00%> (-0.01%) ⬇️
drive-abci 90.24% <25.00%> (-0.01%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 55.66% <ø> (ø)
🚀 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.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Targeted post-merge follow-up to PR #3605: fixes the shielded-pool estimated-costs metadata to match the new 8-element layout, swaps the prune-anchors max scan for a .pop() on already-sorted results, documents the empty-index sentinel convention, and reverts a helper visibility. All four changes verified correct against source. One real asymmetry remains: the non-proven query path silently collapses corrupted entries to the empty-index sentinel, while the proven path returns CorruptedProof.

Reviewed commit: 0337019

🟡 1 suggestion(s) | 💬 2 nitpick(s)

2 additional findings

💬 nitpick: Pin `left_to_right = true` so `.pop()` correctness is locally self-checking

packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs (lines 45-46)

The new .pop() strategy at line 109 is only correct because Query::new() defaults left_to_right to true and the keys are big-endian u64, so GroveDB returns ascending numeric order and the live anchor is the last element. The comment at lines 99-106 explains this, but the assumption isn't pinned in code: a future refactor of the query — or a change to GroveDB's default — would silently flip which entry survives pruning, producing the exact desync this function exists to prevent. Setting below_query.left_to_right = true; explicitly after construction makes the invariant local and self-checking with no runtime cost.

💡 Suggested change
        let mut below_query = Query::new();
        below_query.left_to_right = true;
        below_query.insert_item(QueryItem::RangeTo(..cutoff_height.to_be_bytes().to_vec()));
💬 nitpick: No regression test pins the shielded credit pool layer shape (depth + item count)

packages/rs-drive/src/drive/shielded/estimated_costs.rs (lines 90-112)

This file lacks a unit/integration test asserting that add_estimation_costs_for_shielded_pool_operations registers the pool layer with the actual on-disk shape. The original mismatch this PR fixes (depth 4, items=2 after [7] was retired) shipped because no test compared the registered EstimatedLayerInformation against the production tree's actual subtree/item counts. A small assertion-style test — insert all real subtree keys + the SumItem in setup_drive_with_initial_state_structure, then read the tree and assert EstimatedLevel(ceil(log2(n)), false) and items_size.weight == observed_items — would catch the next time someone adds or removes a slot from the pool layer without updating this file. The PR fixes the current mismatch by hand; a structural test would prevent the next round.

🤖 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/shielded/most_recent_anchor/v0/mod.rs`:
- [SUGGESTION] lines 84-87: Non-proven path silently maps corrupted anchor entries to the empty-index sentinel
  The new comment block documents the empty-index convention but the catch-all `_ =>` arm still flattens two semantically distinct cases into `vec![0u8; 32]`: (1) a genuinely empty index, and (2) a non-`Item` element under `SHIELDED_ANCHORS_BY_HEIGHT_KEY`, which would indicate state corruption. The proven path (`verify_most_recent_shielded_anchor_v0` at lines 50-54) explicitly returns `ProofError::CorruptedProof` for the same condition, and `read_latest_recorded_shielded_anchor_v0` returns `DriveError::CorruptedElementType` — so the two API modes diverge on the same underlying state, and clients hitting the non-proven gRPC path lose the ability to distinguish absence from a broken invariant. The pre-PR comment used to document this case explicitly; the new comment only covers the empty-index path. Either split the match so a non-`Item` returns `DriveError::CorruptedElementType` (matching the other read paths), or restore an explicit note in the comment that the catch-all also masks the corruption case. If applying the concrete suggestion, wrap `DriveError::CorruptedElementType` in the outer `drive::error::Error::Drive` variant required by the drive-abci error type.

…ries

The previous catch-all `_ => vec![0u8; 32]` arm collapsed two
semantically distinct states into the empty-index sentinel:

  * `None` — index genuinely empty (never recorded an anchor).
  * `Some((_, non-`Item`))` — state corruption under
    `SHIELDED_ANCHORS_BY_HEIGHT_KEY`.

The proven path (`verify_most_recent_shielded_anchor_v0`) already
surfaces the second case as `ProofError::CorruptedProof`; the raw
read helper (`Drive::read_latest_recorded_shielded_anchor_v0`)
surfaces it as `DriveError::CorruptedElementType`. Folding it into
the zero sentinel on the non-prove gRPC path made the two API
modes diverge on the same underlying state — clients couldn't tell
absence from a broken invariant.

Split the match into three explicit arms; corruption now returns
`Error::Drive(drive::error::Error::Drive(DriveError::CorruptedElementType(...)))`,
matching the rest of the read paths.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs (1)

63-93: 💤 Low value

Optional: consolidate the two adjacent comment blocks.

The EMPTY-INDEX CONVENTION block (lines 63–84) and the corruption-handling block (lines 85–93) are stacked without a separator and partially restate context (both reference the proven-path verifier). Consider merging them into a single block with two sub-sections (e.g., "Empty index" and "Corruption") so the rationale for each match arm is easier to scan and future edits don't drift between the two blocks.

🤖 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-abci/src/query/shielded/most_recent_anchor/v0/mod.rs`
around lines 63 - 93, Merge the two adjacent comment blocks into a single
consolidated block that has two short subsections (e.g., "Empty index" and
"Corruption"): in "Empty index" describe the EMPTY-INDEX CONVENTION and the
vec![0u8; 32] sentinel and how the proven path uses
verify_most_recent_shielded_anchor_v0 returning Ok(None) for an empty index; in
"Corruption" state the rule that a non-`Item` element under
SHIELDED_ANCHORS_BY_HEIGHT_KEY is a state-corruption signal and reference the
prove-mode verifier (verify_most_recent_shielded_anchor_v0) and the raw-read
helper (Drive::read_latest_recorded_shielded_anchor_v0) which surface this as
CorruptedProof / CorruptedElementType, making clear the non-prove path must not
fold corrupted elements into the empty-index sentinel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs`:
- Around line 63-93: Merge the two adjacent comment blocks into a single
consolidated block that has two short subsections (e.g., "Empty index" and
"Corruption"): in "Empty index" describe the EMPTY-INDEX CONVENTION and the
vec![0u8; 32] sentinel and how the proven path uses
verify_most_recent_shielded_anchor_v0 returning Ok(None) for an empty index; in
"Corruption" state the rule that a non-`Item` element under
SHIELDED_ANCHORS_BY_HEIGHT_KEY is a state-corruption signal and reference the
prove-mode verifier (verify_most_recent_shielded_anchor_v0) and the raw-read
helper (Drive::read_latest_recorded_shielded_anchor_v0) which surface this as
CorruptedProof / CorruptedElementType, making clear the non-prove path must not
fold corrupted elements into the empty-index sentinel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a5c80a9-1841-42aa-bfec-f6a0a1f4613e

📥 Commits

Reviewing files that changed from the base of the PR and between 0337019 and 7795b95.

📒 Files selected for processing (1)
  • packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs

@QuantumExplorer
Copy link
Copy Markdown
Member Author

Self Reviewed

@QuantumExplorer QuantumExplorer merged commit 08a0bbc into v3.1-dev May 6, 2026
21 checks passed
@QuantumExplorer QuantumExplorer deleted the platform-wallet/shielded-anchor-followups branch May 6, 2026 19:58
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.

2 participants