Skip to content

fix(drive): credits-not-balanced from shielded nullifier metadata#3624

Merged
QuantumExplorer merged 6 commits into
v3.1-devfrom
claude/modest-keller-70e2d4
May 11, 2026
Merged

fix(drive): credits-not-balanced from shielded nullifier metadata#3624
QuantumExplorer merged 6 commits into
v3.1-devfrom
claude/modest-keller-70e2d4

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Fixes #3412. During shielded operations on testnet, Drive ABCI halted block execution with corrupted credits not balanced error: ... off by 2. The mismatch was exactly the count of nullifiers spent in the block, not a fee-rounding artifact.

Root cause: RootTree::AddressBalances is a SumTree. The shielded credit pool lived under it as a child SumTree. Recent-nullifier metadata was stored under that pool in a CountSumTree, and each per-block entry was an ItemWithSumItem(serialized_nullifiers, nullifiers.len() as i64). GroveDB sum-tree aggregation bubbled that per-block count up through the pool SumTree into the AddressBalances aggregate, which the end-of-block corruption check reads as credits. Result: each spent nullifier inflated the apparent address-credit total by 1.

What was done?

Two complementary fixes, defense in depth — either alone would fix the bug; together they enforce the separation structurally and locally.

1. New top-level RootTree::ShieldedBalances = 52 SumTree.
The shielded pool moves out from under AddressBalances. The main pool lives at [ShieldedBalances, b\"M\"] (renamed from [AddressBalances, b\"s\"]). The new slot sits between Pools (48) and AddressBalances (56) for semantic clustering with other credit-bearing roots and preserves the AVL diagram. TotalCreditsBalance gains total_in_shielded_balances; ok() enforces >= 0 and includes it in the equality, total_in_trees() includes it. calculate_total_credits_balance_v1 reads the new root (v0 zeroes the field for legacy paths).

2. Element::NotSummed wrap on the recent-nullifiers CountSumTree.
Even with separation, the count sum inside the pool would still pollute the pool's "total shielded credits" aggregate. The recent-nullifiers subtree is now wrapped in Element::new_not_summed(...) so its sum contributes 0 to the enclosing pool SumTree while keeping its own internal sum semantics for compaction. The compaction trigger reads through Element::underlying() to peel the wrapper before pattern-matching CountSumTree(_, count, sum, _).

Constant rename: SHIELDED_CREDIT_POOL_KEY*MAIN_SHIELDED_CREDIT_POOL_KEY*, with the inner byte changed from b\"s\" to b\"M\".

Grovedb bump: 8f25b20dbd83dce (the merge commit of dashpay/grovedb#659 that introduced Element::NotSummed). Side-effect of the bump: orchard 0.12 → 0.13 changed Action::from_parts to return Option<Self>, rejecting identity-randomizer-key actions. Adapted in shielded_common/mod.rs to fail such actions with InvalidShieldedProofError rather than silently dropping them.

End-of-block flow already routes through the updated code: process_block_fees_and_validate_sum_trees/v0/mod.rs:175calculate_total_credits_balance (dispatched to _v1 on drive_version 6+) → TotalCreditsBalance::ok() (now folds in the new field).

How Has This Been Tested?

  • cargo test -p drive --lib3079 passed, 0 failed
  • cargo test -p drive-abci --lib shielded126 passed, 0 failed
  • cargo test -p drive --lib initialization — all 8 pass; root tree element count assertion bumped 16 → 17, and one proof-length assertion shifted from 286 → 320 for the AVL branch that gained the new root (the other 15 proof lengths in the same characterization test are unchanged).
  • cargo fmt --all clean.

Breaking Changes

State-structure change: pre-release on v3.1-dev. Existing dev DBs need to rebuild from genesis (the v3 init layout changed). No released network is affected, so the title is fix: rather than fix!:.

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

🤖 Generated with Claude Code

Fixes #3412: the off-by-N "credits are not balanced after block execution"
error during shielded operations was caused by per-block recent-nullifier
counts being sum-aggregated as if they were credits. AddressBalances is a
SumTree, the shielded credit pool lived under it as a child SumTree, and
recent nullifiers under the pool were stored as ItemWithSumItem(serialized,
nullifiers.len()) inside a CountSumTree. That nullifier count bubbled up
through both parent SumTrees into the AddressBalances aggregate that the
corruption check reads.

Two complementary fixes, defense in depth:

  1. Move the shielded pool out from under AddressBalances into a new
     top-level RootTree::ShieldedBalances = 52 SumTree, with the main pool
     parented inside under MAIN_SHIELDED_CREDIT_POOL_KEY = b"M" (renamed
     from the previous SHIELDED_CREDIT_POOL_KEY = b"s"). TotalCreditsBalance
     gains total_in_shielded_balances; ok() and total_in_trees() include
     it. calculate_total_credits_balance_v1 reads the new root.

  2. Wrap the recent-nullifiers CountSumTree in Element::NotSummed so its
     sum side contributes 0 to the enclosing shielded pool SumTree.
     Compaction trigger code reads through Element::underlying() to peel
     the wrapper.

Bumps the grovedb pin to dbd83dce (the merge commit of dashpay/grovedb#659
that introduced Element::NotSummed). The grovedb bump pulled in orchard
0.13, which changed Action::from_parts to return Option<Self> (rejects
identity-randomizer-key actions) — adapted by failing such actions with
InvalidShieldedProofError.

Tests: cargo test -p drive --lib (3079 passed), cargo test -p drive-abci
--lib shielded (126 passed). The latest-protocol AVL characterization test
needed one proof-length value updated (286 → 320) for the branch that
gained the new root; the other 15 proof lengths in the same test are
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner May 10, 2026 22:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Review Change Stack

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 33 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a087333f-cda4-4d5e-9aa3-a4a3350e6c26

📥 Commits

Reviewing files that changed from the base of the PR and between a28c298 and 944db6a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • packages/rs-dpp/Cargo.toml
  • packages/rs-dpp/src/balances/total_credits_balance/mod.rs
  • packages/rs-drive-abci/Cargo.toml
  • packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/address_funds_transfer/tests.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs
  • packages/rs-drive/Cargo.toml
  • packages/rs-drive/src/drive/balances/calculate_total_credits_balance/mod.rs
  • packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v0/mod.rs
  • packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v1/mod.rs
  • packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v2/mod.rs
  • packages/rs-drive/src/drive/initialization/v0/mod.rs
  • packages/rs-drive/src/drive/initialization/v3/mod.rs
  • packages/rs-drive/src/drive/mod.rs
  • packages/rs-drive/src/drive/shielded/estimated_costs.rs
  • packages/rs-drive/src/drive/shielded/nullifiers/queries.rs
  • packages/rs-drive/src/drive/shielded/nullifiers/store_nullifiers/v0/mod.rs
  • packages/rs-drive/src/drive/shielded/paths.rs
  • packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs
  • packages/rs-drive/src/verify/shielded/verify_compacted_nullifier_changes/v0/mod.rs
  • packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs
  • packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs
  • packages/rs-platform-version/Cargo.toml
  • packages/rs-platform-version/src/version/drive_versions/v7.rs
  • packages/rs-platform-wallet/Cargo.toml
  • packages/rs-sdk-ffi/src/system/queries/path_elements.rs
  • packages/rs-sdk/Cargo.toml
  • packages/wasm-drive-verify/src/state_transition/state_transition_execution_path_queries/token_transition.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/modest-keller-70e2d4

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.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 10, 2026
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 10, 2026

🕓 Ready for review — 4 ahead in queue (commit 944db6a)
Queue position: 5/6

QuantumExplorer and others added 5 commits May 11, 2026 05:49
The v12 protocol transition was still parenting the main shielded credit
pool under AddressBalances and creating only 4 of the 8 inner subtrees,
which would diverge from a fresh genesis-12 chain (created by v3 init) on
any node that actually migrated from v11. Rewrite it to mirror v3 init:

- Insert RootTree::ShieldedBalances as a top-level SumTree.
- Parent the main pool under [ShieldedBalances] / "M".
- Insert all 8 inner subtrees in BFS order: notes, nullifiers,
  anchors_in_pool, total_balance, anchors_by_height, recent_nullifiers
  (wrapped in NotSummed), compacted_nullifiers, expiration_time.
- Update the two post-condition assertions in the in-file tests.

Also fix CI: the grovedb bump (PR #659/#656) added a new
QueryItem::AggregateCountOnRange variant. wasm-drive-verify wasn't in my
local check set and had a non-exhaustive match in
token_transition.rs that broke the macOS workspace build. Reject the
variant with a clear error message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng v1

Adding the ShieldedBalances read to v1 in 2189a91 retroactively changed
the meaning of the v1 calculator that DRIVE_VERSION_V6 (protocol v11) is
still pinned to. Pre-v12 chains have no ShieldedBalances tree, so reading
it there would fail at the corruption check on every block.

Revert v1 to its original four-term shape (it now sets
total_in_shielded_balances: 0 to keep the struct in sync) and introduce
v2 with the five-term equation that includes the ShieldedBalances root.
Bump DRIVE_VERSION_V7 (protocol v12, where the shielded pool first
exists) to use calculator v2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The grovedb bump added two new Element variants (NonCounted, NotSummed)
that wrap an inner element. The path_elements helper had two large
inline matches over Element that became non-exhaustive on macOS CI.

Extract those matches into format_element_data and format_element_type
helpers and add recursive arms for both wrappers — rendered as
non_counted(<inner>) / not_summed(<inner>) so the wrapped element's
value and type are still visible to FFI callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… values

Adding the ShieldedBalances root tree changes the AVL shape of the root
NormalTree (17 entries instead of 16), so every operation that touches a
top-level root key now navigates a slightly different sibling layout.
Processing fees on the v12 path-of-tests dropped by ~49,820 credits each
(about 1.6%), with storage fees unchanged.

Updated the asserted processing_fee and total_fee values (and their
"Was X, now Y" message strings) in each fee_regression test. The
_on_version_11 variants are unaffected because protocol v11 still pins
DRIVE_VERSION_V6, which predates ShieldedBalances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gram

ShieldedBalances (52) is less than AddressBalances (56), so by BST order
it belongs on the left side of AddressBalances — same shape as
Saved Block Transactions (36) hanging left off PreFundedSpecializedBalances
(40). Both are now drawn as left descendants ('/'), not as right
descendants of unrelated parents.

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

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

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

Self Reviewed

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 10, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "edf436b547f967681c1779fd4ab9c5bc54987eb277ce8d6679c12cbb7abddf69"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

@QuantumExplorer
Copy link
Copy Markdown
Member Author

Me and Lukazs went over this one.

@QuantumExplorer QuantumExplorer merged commit 3b9fe6b into v3.1-dev May 11, 2026
64 of 65 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/modest-keller-70e2d4 branch May 11, 2026 09:29
QuantumExplorer added a commit that referenced this pull request May 11, 2026
Conflicts and resolutions:

- Cargo.toml across rs-dpp / rs-drive / rs-drive-abci / rs-platform-
  version / rs-platform-wallet / rs-sdk: grovedb rev bumped on
  v3.1-dev from 1206049b to dbd83dce. Took the incoming rev — it's
  the direction the base is going and the count-tree primitives our
  PR depends on are still present.
- `packages/rs-platform-version/src/version/drive_versions/v7.rs`
  auto-merged: incoming `calculate_total_credits_balance: 2` (PR
  #3624 shielded-nullifier-metadata fix) sits alongside our
  `contract: DRIVE_CONTRACT_METHOD_VERSIONS_V3`
  (count-tree-aware contract-insertion cost estimation).
- `rs-drive-abci/.../shielded_common/mod.rs`: took the incoming
  error message for `Action::from_parts` returning None — the
  origin version names the actual cause ("action has identity
  randomizer key") rather than the generic "invalid action parts"
  we had. Same error type and shape, more informative payload.
- `rs-sdk-ffi/.../path_elements.rs`: took the incoming refactor
  that lifts the inline `Element` rendering into
  `format_element_data` / `format_element_type` helpers (and adds
  `Element::NotSummed` handling we didn't have).
- `wasm-drive-verify/.../token_transition.rs`: took the incoming
  strict rejection of `AggregateCountOnRange` in the token-
  transition path-query shim. That path doesn't drive aggregate-
  count queries, so erroring is safer than the descriptive
  fallthrough we had.
- `Cargo.lock`: regenerated via `cargo generate-lockfile` after
  the Cargo.toml resolutions.

Verified:
- `cargo check -p drive -p drive-abci -p drive-proof-verifier -p
  dash-sdk` — clean.
- 26 `range_countable_index_e2e_tests` pass (no-merge compound
  semantic + StartsWith + estimation + base range path).
- 7 `drive-abci::query::document_count_query` tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(zk): corrupted credits not balanced error after block execution during shielded operations

2 participants