Skip to content

fix(sdk): query compacted address balances when the recent proof has no boundary - #5572

Open
Claudius-Maginificent wants to merge 2 commits into
fix/sdk-contract-keys-test-settingsfrom
fix/sdk-address-sync-compacted
Open

Claudius-Maginificent wants to merge 2 commits into
fix/sdk-contract-keys-test-settingsfrom
fix/sdk-address-sync-compacted

Conversation

@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

A wallet's address balance sync could miss a credit and report the address as empty. This hits the first sync of every fresh wallet, and any sync whose recent-zone cursor has been compacted away. The credit stays lost until the next full rescan, which is about 7 days by default.

Seen on testnet in platform-wallet e2e SH-040. The trunk checkpoint was at 642131, and a 2.22e9-credit funding landed in a block in (642133, 642138]. Drive then compacted that block out of the recent zone, so the recent query returned only block 642139. GetAddressInfo showed 2.22e9, but the wallet saw 0 and shielding failed with PlatformShieldCapacityExceeded { available: 0 }.

Root cause: Phase 2 of incremental_catch_up read "the recent proof has no boundary node" as "the recent tree is empty, nothing to compact" and skipped the compacted query. That reading is wrong:

  • A RangeFrom proof, used whenever last_known_recent_block == 0, does not carry its start key as a boundary.
  • A RangeAfter start key that was compacted away leaves no boundary either.

In both cases the blocks since the checkpoint may be sitting in the compacted tree. The function's own doc already said the compacted phase "always runs" for RangeFrom, but the code did not do that.

What was done?

  • Ok(None) from get_last_recent_block_from_proof now runs the compacted phase. The misleading comments and doc lines are corrected.
  • The Phase 2 decision is extracted into the pure needs_compacted_phase(recent_proof, checkpoint_height) so it can be unit-tested with real proofs. SDK mock expectations always return an empty Proof, which fails to decode and takes the "run compacted to be safe" arm, so a mock-driven sync test could never reach this bug.
  • Stacked on test(sdk): fill the verified protocol version in identities contract keys query tests #5571 (test-only compile fix for cargo test -p dash-sdk --lib on v5.1-dev), needed to run these tests.

Double counting is still prevented by the existing per-operation height pin in apply_op, already covered by apply_block_changes_height_filter_drops_below_cursor and apply_block_changes_drops_recent_delta_already_inside_pinned_absolute:

  • Compacted operations at or below the trunk checkpoint are dropped.
  • A recent block that also comes back inside a compacted range (compaction racing between the two RPCs) is counted once.

Cost: one extra compacted RPC per sync, only when the recent proof has no boundary.

This is client-side code only. No consensus path, versioned method or version table is touched.

How Has This Been Tested?

New unit tests for needs_compacted_phase in packages/rs-sdk/src/platform/address_sync/mod.rs. They build real, decodable GroveDB proofs for the recent-tree layer:

  • should_run_compacted_phase_when_recent_proof_has_no_boundary: the SH-040 shape (only block 642139, checkpoint 642131) and a genuinely empty recent zone. Both assert the proof decodes to Ok(None). Failed before the fix; passes after.
  • should_skip_compacted_phase_when_a_boundary_reaches_the_checkpoint: boundary equal to or above the checkpoint skips compacted.
  • should_run_compacted_phase_when_the_boundary_predates_the_checkpoint
  • should_run_compacted_phase_when_recent_proof_is_undecodable

Commands run:

  • cargo test -p dash-sdk --lib address_sync: 45 passed
  • cargo test -p dash-sdk --test main address_sync: passed
  • cargo clippy -p dash-sdk --all-features --all-targets -- -D warnings: clean
  • cargo fmt --all

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • My Rust, FFI, WASM, Swift or Kotlin changes follow the coding conventions
  • 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
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request
  • If I added or changed a system contract's document schemas, I checked the contract viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

https://claude.ai/code/session_019H9THphMnuQyh9oPY13SoE

lklimek and others added 2 commits October 24, 2026 11:20
…f has no boundary

Extract the Phase 2 decision of incremental_catch_up into the pure
needs_compacted_phase and test it with real, decodable recent proofs.
Mock expectations return an empty proof, whose decode error takes the
"run compacted" arm and hides the bug.

Reproduces testnet SH-040: the trunk checkpoint predates a funding in a
since-compacted block, and the RangeFrom recent proof holds a later block
but no boundary, so the compacted phase is skipped and the credit is
lost. The no-boundary test fails until the fix lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019H9THphMnuQyh9oPY13SoE
…no boundary

Phase 2 of incremental catch-up read "no boundary in the recent proof"
as "recent tree empty, nothing to compact" and skipped the compacted
query. A RangeFrom proof (every first sync of a fresh wallet) does not
carry its start key as a boundary, and a RangeAfter start key that was
compacted away leaves none either. Either way, a credit that landed after
the checkpoint in a since-compacted block was lost until the next full
rescan (about 7 days by default). Seen on testnet as a 2.22e9-credit
funding the wallet reported as 0.

Run the compacted phase whenever the proof carries no boundary. The
per-operation height pin in apply_op keeps compacted operations at or
below the checkpoint, and recent blocks already inside a compacted range,
from being counted twice. Cost: one extra compacted RPC in that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019H9THphMnuQyh9oPY13SoE
@coderabbitai

coderabbitai Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@thepastaclaw

thepastaclaw commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit ddbcda1) · triage: normal

@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.

Final validation — Phase 1 + Phase 2

The change correctly treats a decodable recent proof without a boundary as inconclusive and conservatively runs the compacted phase. The new tests directly exercise the extracted production decision across boundary, no-boundary, preceding-boundary, and undecodable-proof cases; no in-scope correctness, security, architectural, or consensus issues were found. Supplied CI checks completed successfully.

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — This is a contained Rust client-side synchronization logic change affecting balance discovery and compacted/recent proof handling, so it requires substantive correctness review but does not modify a critical consensus, cryptographic, or funds-movement surface.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity quota lookup failed), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort high); agent phase2-reviewer
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Infallible try_into uses unwrap in production path — Out of scope and pre-existing — the unwrap is in untouched code, is guarded by an explicit eight-byte length check, and is unrelated to the compacted-balance fix.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

This branch has not been deployed

No deployments
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