fix(platform-wallet): index-conflicting removal no longer orphans a restored address balance#4013
Conversation
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
✅ Review complete (commit 542490d) |
…estored address balance A reconcile removal (fully consumed input) can resolve to a pool index that collides with a different, funded address's true derivation index. commit_reconciliation leaves the in-memory bijection untouched in that case but still emits the zero-funds removal so the balance can't resurrect. That emitted entry carried the conflicting address_index all the way to durable storage: - Swift persistAddressBalances unconditionally overwrote the row's addressIndex with the conflicting value, even though the derivation index is authoritative from the address-emit path and this callback is documented to refresh only balance/nonce/isUsed. Two durable rows then claim one (accountIndex, addressIndex). - On restart insert_persisted_entry rebuilds the bijection with a BiBTreeMap insert per row; a colliding insert evicts the funded pairing, orphaning its balance from current_balances depending on fetch order. Fix both sides of the persister seam: - Swift: persistAddressBalances no longer writes accountIndex/addressIndex (volatile balance/nonce/isUsed only), so a removal can't rebind an index and the durable store stays a bijection. - Rust: insert_persisted_entry guards the bijection only. A zero row is indistinguishable from a legitimate never-funded derived address, so it still seeds found as before; it inserts into the bijection via insert_no_overwrite so it can never evict a funded pairing, while a funded row keeps overwrite semantics and wins its slot. For unique indices this is identical to the previous unconditional insert. Covers both the SwiftData/FFI and SQLite restore paths, which share this method. Tests: Rust round-trips the conflict through restore in both fetch orders (insert_persisted_entry_removal_remnant_never_orphans_funded_pairing), pins that a never-funded zero row still restores normally (insert_persisted_entry_unfunded_derived_address_restores_normally), and Swift round-trips through persist + reload (AddressBalancePersistTests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a125539 to
542490d
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Prior reviewed SHA a125539 had zero findings, so no findings were carried forward. Both agents (Claude and Codex, all sub-reviewers) reported zero findings on both the latest delta and cumulative head. Verified against worktree at 542490d: the fix hardens the platform-wallet restore path via a bijection guard in insert_persisted_entry (uses insert_no_overwrite only for zero-balance/zero-nonce rows) and drops non-authoritative derivation index writes on the Swift persistAddressBalances callback. No in-scope defects identified.
Source: reviewers claude/opus and codex/gpt-5.5 for general + security-auditor + ffi-engineer; verifier claude/opus.
Prior reconciliation: the previous automated review at a125539e found no issues, so there are no carried-forward prior findings to keep or resolve.
Latest delta: reviewed with git diff a125539e..542490da plus cumulative current-head context.
Issue being fixed or feature implemented
A reconcile removal (a fully consumed input) can resolve to a pool
address_indexthat collides with a different, funded address's truederivation index.
commit_reconciliationcorrectly leaves the in-memory(index → address)bijection untouched in that case, but it still emitsthe zero-funds removal (so a stale balance can't resurrect after a
restart). That emitted entry carried the conflicting
address_indexallthe way to durable storage:
PlatformWalletPersistenceHandler.persistAddressBalances, whichunconditionally overwrote the row's
addressIndexwith theconflicting value — even though that field is authoritative derivation
metadata owned by the address-emit path, and this callback is
documented to refresh only the volatile balance/nonce/
isUsedfields.PersistentPlatformAddressrows end up claimingthe same
(accountIndex, addressIndex).(
PerAccountPlatformAddressState::insert_persisted_entry) does aBiBTreeMapinsert per row. A colliding insert evicts the priorpairing, so — depending on SwiftData fetch order — the funded
address's
(index → address)pairing can be evicted, orphaning itsbalance from
current_balances(the address silently loses itsrestored credits).
This was surfaced by the #3990 sync review and is closed here at the
persister seam.
What was done?
The persister contract has two sides; both are hardened.
Write side — root cause (
swift-sdk).persistAddressBalancesno longer writesaccountIndex/addressIndex.A row's derivation index is fixed the moment its address is derived and
is owned by the address-emit path (
persistPlatformPaymentAddresses);the balance-update callback now touches balance / nonce /
isUsedonly —exactly what its own doc comment already promised. A conflicting removal
therefore zeroes the removed address's balance without ever rebinding its
index, so the durable store stays a bijection.
Read side — defense in depth (
platform-wallet).PerAccountPlatformAddressState::insert_persisted_entrynow guards thebijection. A zero-balance/zero-nonce row is indistinguishable from a
legitimate freshly-derived, never-funded address, so it still seeds
foundexactly as before; but it extends the bijection viainsert_no_overwrite, so it can never evict an incumbent(index → address)pairing, while a funded row keeps overwrite semantics and winsits slot. For the common case of unique indices this is byte-identical to
the previous unconditional insert — it only changes behaviour on an index
collision, keeping the funded pairing regardless of restore order. This
protects any already-persisted store and — because both the SwiftData/FFI
loader and the SQLite loader (
platform_addrs::build_per_account) routethrough this one method — covers both backends.
No FFI struct / ABI change; the entry still carries the indices, the
persister just stops trusting the removal's copy of them.
How Has This Been Tested?
cargo test -p platform-wallet,-p platform-wallet-storage):insert_persisted_entry_removal_remnant_never_orphans_funded_pairinground-trips the conflict through restore in both fetch orders and
asserts that the intersection of
foundand the bijection (i.e. whatcurrent_balancesyields) is exactly the funded balance — theremnant's inert zero row is never paired.
insert_persisted_entry_unfunded_derived_address_restores_normallypins that a real never-funded (
{0, 0}) address still restores intoboth
foundand the bijection, so the guard can't swallow it.commit_reconciliation/platform_addressestests andthe storage crate's SQLite
load_statereconstruction tests staygreen.
swift test,SwiftDashSDKTests): newAddressBalancePersistTestsround-trips through persist + reload —seeds a funded row at index 5 and a row at index 2, applies a removal
for the index-2 address carrying the conflicting index 5, and asserts
the removed row keeps index 2 (zeroed) while the funded row keeps index
5, so no two durable rows share an index.
cargo fmt --all;cargo check -p platform-wallet --all-targets.Breaking Changes
None. No consensus-affecting changes; no FFI ABI change; pre-release, so
no SwiftData migration concern.
Checklist:
For repository code-owners and collaborators only