Skip to content

fix(sdk): address-sync no longer silently discards balance changes for post-snapshot addresses (Found-025)#3650

Open
lklimek wants to merge 2 commits into
v3.1-devfrom
fix/rs-sdk-address-sync-found-025
Open

fix(sdk): address-sync no longer silently discards balance changes for post-snapshot addresses (Found-025)#3650
lklimek wants to merge 2 commits into
v3.1-devfrom
fix/rs-sdk-address-sync-found-025

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented May 15, 2026

Issue being fixed or feature implemented

Fixes the rs-sdk address-sync silent-discard defect (Found-025): the SDK
silently drops platform-returned balance updates for any address absent
from the pre-RPC pending_addresses() snapshot.

Summary

sync_address_balances (packages/rs-sdk/src/platform/address_sync/mod.rs)
builds a key_to_tag lookup once from a single pre-RPC
provider.pending_addresses() snapshot and passes it by immutable
reference into the private incremental_catch_up. In both the compacted
apply loop and the recent apply loop the predicate
if let Some(&(tag, address)) = address_lookup.get(&addr_bytes) had no
else branch
: any balance change the platform returned for an address
derived after the snapshot was dropped with no log, metric, or error —
result.found never got it and on_address_found was never called.
Unlike the tree-scan path, incremental_catch_up never re-polled
pending_addresses() (contrast after_branch_iteration, which refreshes
during the branch loop).

Root cause

  • Stale key_to_tag snapshot: built once from a single pre-RPC
    provider.pending_addresses() call and never refreshed inside
    incremental_catch_up.
  • Missing else: the if let Some(..) = address_lookup.get(..) predicate
    had no fallback, so a snapshot miss was a no-op.
  • Silent balance-change discard: a platform-returned delta for a
    post-snapshot address went to neither result.found nor
    on_address_found, and the wallet's local sync map showed the address
    empty.

Reproduction

  1. Within one sync pass, derive a fresh receive address A after the
    pass's pending_addresses() snapshot is taken (routine under
    concurrent multi-identity funding).
  2. Fund A; let it chain-confirm.
  3. The recent/compacted balance-changes RPC returns A's balance, but the
    SDK discards it: result.found never gets A, on_address_found is
    never called, the wallet's local sync map shows A empty.

On single-threaded runs the derive usually precedes the snapshot, hiding
the bug — hence the symptom is flaky, not always red, in live e2e.
Under concurrent multi-identity funding the derive→fund→sync interleave is
routine, which is why the rs-platform-wallet e2e funding gates
TK-001 / TK-007 / TK-013 / TK-014 / id_005 flaked here (PASS
single-threaded, flaky under 14-thread concurrency).

What was done?

  • Extracted the two inline apply loops into a pure
    pub(crate) apply_address_changes seam — no Sdk, no network, no
    async. It returns the applied (tag, address, funds) updates plus the
    addresses the platform reported a change for that were absent from the
    snapshot (unknown), instead of silently dropping them.
  • New apply_block_changes drives the async on_address_found callbacks
    outside the pure seam, and on an unknown-address miss re-polls
    provider.pending_addresses() (mirroring the existing
    after_branch_iteration tree-scan refresh) and replays only the
    previously-unknown subset
    — so a freshly-derived receive address is
    recovered while known-address AddToCredits deltas are never
    double-counted.
  • An address still unknown after the refresh is logged at warn
    (observable, never silently dropped) per project tracing conventions.
  • Known-address behavior is byte-for-byte identical to the original loops
    (verified by the 27 pre-existing address_sync tests, all still green).

How Has This Been Tested?

cargo build -p dash-sdk, cargo clippy -p dash-sdk --all-targets
(clean), cargo test -p dash-sdk --lib address_sync30/30 pass,
including the 27 pre-existing address_sync tests (no regression from the
refactor).

Three new deterministic #[cfg(test)] regression guards on the pure seam
(no proof/Sdk/network):

  1. found_025_apply_address_changes_surfaces_unknown_address — a known
    address still applies identically; a post-snapshot address is surfaced
    in unknown instead of silently dropped.
  2. found_025_apply_block_changes_recovers_post_snapshot_address — a
    provider that derived an address mid-pass gets the balance applied and
    on_address_found fired after the in-pass refresh (the exact Found-025
    scenario).
  3. found_025_known_delta_not_double_counted_on_refresh — a known
    AddToCredits delta applies exactly once across the refresh+replay.

Red-pre / green-post proof: restoring the silent-discard behavior
(drop the else, no refresh) makes guards 1 and 2 FAIL
(unknown == [], result.found == None); the fix makes all three PASS.
Would have caught Found-025 at unit level. The extracted pub(crate)
pure seam makes the discard a pure-data assertion (no Sdk/proof) pinned
in rs-sdk's own test module.

This is the v3.1-dev production PR. Applying onto #3549 for combined
e2e validation is a separate sequenced step.

Test plan

  • cargo build -p dash-sdk
  • cargo clippy -p dash-sdk --all-targets (clean)
  • cargo test -p dash-sdk --lib address_sync — 30/30 pass
  • 27 pre-existing address_sync tests green (no regression)
  • Guard 1: found_025_apply_address_changes_surfaces_unknown_address
  • Guard 2: found_025_apply_block_changes_recovers_post_snapshot_address
  • Guard 3: found_025_known_delta_not_double_counted_on_refresh
  • Red-pre / green-post proof verified (guards 1+2 fail on restored
    silent-discard logic)

Breaking Changes

None. Public API unchanged; the new seam is pub(crate). Known-address
behavior is identical.

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

Co-authored by Claudius the Magnificent AI Agent

… for post-snapshot addresses (Found-025)

`incremental_catch_up` built its `key_to_tag` lookup once from a single
pre-RPC `provider.pending_addresses()` snapshot and passed it by
immutable reference into both apply loops. The `if let Some(..) =
address_lookup.get(..)` predicate had no `else`, so any balance change
the platform returned for an address derived *after* the snapshot was
dropped with no log, metric, or error — `result.found` never got it and
`on_address_found` was never called. Under concurrent multi-identity
funding the derive-fund-sync interleave is routine, which is why e2e
gates TK-001/007/013/014 and id_005 flaked here.

Extract the two inline apply loops into a pure `pub(crate)
apply_address_changes` seam (no Sdk, no network, no async) that returns
applied updates plus the addresses absent from the snapshot. The new
`apply_block_changes` re-polls `pending_addresses()` when an unknown
address appears (mirroring the tree-scan refresh) and replays only the
previously-unknown subset, so a fresh receive address is recovered and
known-address `AddToCredits` deltas are never double-counted. An address
still unknown after the refresh is logged at `warn` — observable, never
silently dropped. Known-address behavior is byte-for-byte identical.

Adds three deterministic `#[cfg(test)]` regression guards on the pure
seam (no proof/Sdk needed): unknown-address surfacing, post-snapshot
recovery through the refresh, and delta double-count safety. All three
fail on the pre-fix silent-discard logic and pass post-fix.

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

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@lklimek has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 28 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: 11dd0d0f-d02c-4366-af5c-6cf3b7946f58

📥 Commits

Reviewing files that changed from the base of the PR and between cd93b2f and 62a139e.

📒 Files selected for processing (1)
  • packages/rs-sdk/src/platform/address_sync/mod.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rs-sdk-address-sync-found-025

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.

@lklimek lklimek marked this pull request as ready for review May 18, 2026 06:56
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 18, 2026

Review Gate

Commit: 62a139e6

  • Debounce: 1205m ago (need 30m)

  • CI checks: build failure: Swift SDK build / Swift SDK and Example build (warnings as errors)

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (08:02 PM PT Monday)

  • Run review now (check to override)

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.05%. Comparing base (cd93b2f) to head (62a139e).

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3650      +/-   ##
============================================
- Coverage     88.06%   88.05%   -0.01%     
============================================
  Files          2521     2521              
  Lines        308995   308781     -214     
============================================
- Hits         272122   271908     -214     
  Misses        36873    36873              
Components Coverage Δ
dpp 88.01% <ø> (ø)
drive 87.03% <ø> (-0.03%) ⬇️
drive-abci 90.05% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 53.13% <ø> (ø)
🚀 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.
@lklimek lklimek changed the title fix(rs-sdk): address-sync no longer silently discards balance changes for post-snapshot addresses (Found-025) fix(sdk): address-sync no longer silently discards balance changes for post-snapshot addresses (Found-025) May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants