Skip to content

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

Draft
lklimek wants to merge 1 commit into
v3.1-devfrom
fix/rs-sdk-address-sync-found-025
Draft

fix(rs-sdk): address-sync no longer silently discards balance changes for post-snapshot addresses (Found-025)#3650
lklimek wants to merge 1 commit 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

Important

Review skipped

Draft detected.

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

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 556f8cbe-2f12-42d4-8393-ca92d4772ff2

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

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.

1 participant