Skip to content

fix(platform-wallet): address sync review feedback for #3990#3991

Open
thepastaclaw wants to merge 3 commits into
dashpay:chore/sync-v4.1-dev-with-v4.0-devfrom
thepastaclaw:tracker-1760-pr3990-coderabbit
Open

fix(platform-wallet): address sync review feedback for #3990#3991
thepastaclaw wants to merge 3 commits into
dashpay:chore/sync-v4.1-dev-with-v4.0-devfrom
thepastaclaw:tracker-1760-pr3990-coderabbit

Conversation

@thepastaclaw

@thepastaclaw thepastaclaw commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Follow-up fixes for CodeRabbit feedback on #3990.\n\nThis branch addresses the platform wallet sync/reconciliation, shielded pending spend cleanup, Swift clear/sync race, and best-effort identity persistence review comments.\n\nValidation:\n- cargo fmt --manifest-path packages/rs-platform-wallet/Cargo.toml --check\n- cargo check -p platform-wallet --features shielded\n\nNote: I could not push directly to dashpay/platform:chore/sync-v4.1-dev-with-v4.0-dev (403/permission denied), so this PR targets that branch.

Summary by CodeRabbit

  • Bug Fixes

    • Improved wallet reconciliation so balance updates are only finalized when local changes are safely saved.
    • Prevented stale or duplicate shielded spend records from reappearing after wallet resets or clears.
    • Made balance syncing more resilient during clear operations, avoiding old data from repopulating the UI.
    • Tightened handling of address and spend reconciliation conflicts to reduce inconsistent wallet states.
  • New Features

    • Added safer clearing behavior in the example app, with the Clear action disabled while cleanup is in progress.

- commit_reconciliation: drop cross-pool index conflicts before mutating
  the found seed or emitting the entry
- file store: purge shielded_pending_spends rows on wallet / all-subwallet
  purge; mirror redrive-row deletion on mark_spent for any known nullifier
- SubwalletState::mark_spent: always clear pending/redrive for a known
  nullifier, returning true only on the first spent transition
- anchor probe: report depth>0 store errors as a distinct skipped outcome
  instead of conflating them with an uncoverable note
- register_from_addresses: best-effort local add_identity so a local
  persistence failure cannot suppress the accepted registration result
- SwiftExampleApp: generation-token invalidation of in-flight balance
  snapshot refreshes during Clear, plus isClearing gating on the
  Sync Now / Clear buttons

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fab1a6a-f352-4b90-b44e-c075b3074f78

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
📝 Walkthrough

Walkthrough

This PR hardens durability across rs-platform-wallet: identity registration persistence becomes best-effort, address reconciliation now reports whether persistence succeeded and gates asset-lock consumption and provider bijection updates on it, and shielded store/operations logic distinguishes store errors from unwitnessable notes and fixes SQLite purge/mark-spent consistency. Swift example app clear flow adds race-prevention state.

Changes

Rust Platform Wallet Durability and Shielded Store Fixes

Layer / File(s) Summary
Best-effort identity persistence
packages/rs-platform-wallet/src/wallet/identity/network/register_from_addresses.rs
Local identity persistence failures or missing wallet info now log warnings and continue instead of erroring out.
Reconciliation persistence contract
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs
reconcile_address_infos_with_persistence returns (changeset, bool) across all paths; reset_sync_state lock ordering changed (provider lock before wallet-manager lock).
Asset-lock consumption gating
packages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs
Consumption of a tracked asset lock is skipped when reconciled balance changes were not durably persisted.
Provider bijection conflict guard
packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs
commit_reconciliation now drops conflicting reconciliation entries instead of only logging and continuing.
Shielded file-store purge/mark_spent SQLite consistency
packages/rs-platform-wallet/src/wallet/shielded/file_store.rs
mark_spent deletion trigger switched to a nullifier-known check; purge_wallet/purge_all_subwallets now delete shielded_pending_spends rows.
SubwalletState mark_spent cleanup ordering
packages/rs-platform-wallet/src/wallet/shielded/store.rs
Pending reservation and redrive cleanup now happen even when a note was already marked spent.
DepthProbe checkpoint anchor probing
packages/rs-platform-wallet/src/wallet/shielded/operations.rs
New DepthProbe enum distinguishes built results, uncoverable notes, and store errors, returning ShieldedMerkleWitnessUnavailable when store errors occurred during probing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Swift Example App Clear-Sync Race Fix

Layer / File(s) Summary
isClearing flag and refreshGeneration token
.../PlatformBalanceSyncService.swift
Adds published isClearing flag and refreshGeneration invalidation token, toggled around clearLocalState.
Gate sync/snapshot on clearing state
.../PlatformBalanceSyncService.swift
performSync and refreshBalanceSnapshot early-return while clearing, and discard stale success/error results based on generation mismatch.
Clear button UI disabled state
.../CoreContentView.swift
"Clear" button disabled condition extended to include isClearing.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CoreContentView
  participant PlatformBalanceSyncService

  User->>CoreContentView: tap Clear
  CoreContentView->>PlatformBalanceSyncService: clearLocalState()
  PlatformBalanceSyncService->>PlatformBalanceSyncService: bump refreshGeneration, isClearing=true
  PlatformBalanceSyncService->>PlatformBalanceSyncService: in-flight refreshBalanceSnapshot captures old generation
  PlatformBalanceSyncService-->>PlatformBalanceSyncService: wipe completes, defer bumps generation, isClearing=false
  PlatformBalanceSyncService->>PlatformBalanceSyncService: stale result generation mismatch -> discarded
Loading
sequenceDiagram
  participant FundFromAssetLock
  participant PlatformAddressWallet
  participant Persister

  FundFromAssetLock->>PlatformAddressWallet: reconcile_address_infos_with_persistence()
  PlatformAddressWallet->>Persister: store(entries)
  Persister-->>PlatformAddressWallet: success or error
  PlatformAddressWallet-->>FundFromAssetLock: (changeset, persisted)
  alt tracked_out_point exists and not persisted
    FundFromAssetLock-->>FundFromAssetLock: log error, return Ok(changeset), skip consume_asset_lock
  else persisted or no tracked_out_point
    FundFromAssetLock->>FundFromAssetLock: consume_asset_lock
  end
Loading

Possibly related PRs

  • dashpay/platform#3959: Both PRs modify the SwiftExampleApp Platform Sync Clear flow in PlatformBalanceSyncService/CoreContentView.
  • dashpay/platform#3969: Both PRs touch the platform-address reconciliation/persistence pipeline in platform_addresses/provider.rs and platform_addresses/wallet.rs.
  • dashpay/platform#3977: Both PRs modify shielded/operations.rs around depth/checkpoint probing for shielded spend witness/anchor selection.

Suggested labels: ready for final review

Suggested reviewers: shumkov, lklimek, llbartekll, ZocoLini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and broadly matches this follow-up sync/reconciliation fix set, though it omits the Swift changes and is a bit generic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Review complete (commit cd9bf65)

@thepastaclaw

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet/src/wallet/shielded/operations.rs`:
- Around line 1786-1832: The new ShieldedMerkleWitnessUnavailable probe failure
is currently being flattened into the generic wallet-operation error path, which
hides its retryable/non-broadcast meaning. Update the FFI handling in
shielded_send.rs to add a dedicated host result code for this error and map
PlatformWalletError::ShieldedMerkleWitnessUnavailable explicitly instead of
routing it through ErrorWalletOperation, preserving the distinct caller
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 774ebae4-08b8-4566-9239-06b6a023aba0

📥 Commits

Reviewing files that changed from the base of the PR and between 42019c5 and 2f6d9bc.

📒 Files selected for processing (9)
  • packages/rs-platform-wallet/src/wallet/identity/network/register_from_addresses.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs
  • packages/rs-platform-wallet/src/wallet/shielded/file_store.rs
  • packages/rs-platform-wallet/src/wallet/shielded/operations.rs
  • packages/rs-platform-wallet/src/wallet/shielded/store.rs
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/PlatformBalanceSyncService.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift

Comment on lines +1786 to +1832
Ok(DepthProbe::Built(spends, anchor))
};

// Fast path: a fully-synced wallet's depth-0 root is a recorded anchor.
let (spends, anchor) = match build_at_depth(0, true)? {
Some(pair) => pair,
// Unreachable — a strict build returns `Some` or errors — but stay
let DepthProbe::Built(spends, anchor) = build_at_depth(0, true)? else {
// Unreachable — a strict build returns `Built` or errors — but stay
// fund-safe (a clean error, never a panic) if that invariant breaks.
None => {
return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable(
"depth-0 witness probe returned no witness for a selected note".to_string(),
));
}
return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable(
"depth-0 witness probe returned no witness for a selected note".to_string(),
));
};
if recorded.contains(&anchor.to_bytes()) {
return Ok((spends, anchor));
}

// Otherwise walk older checkpoints newest→oldest for the shallowest
// recorded root.
let mut skipped_store_error: Option<String> = None;
for depth in 1..MAX_ANCHOR_PROBE_DEPTH {
match build_at_depth(depth, false)? {
Some((spends, anchor)) if recorded.contains(&anchor.to_bytes()) => {
DepthProbe::Built(spends, anchor) if recorded.contains(&anchor.to_bytes()) => {
return Ok((spends, anchor));
}
// A root exists at this depth but Platform didn't record it — try an
// older checkpoint.
Some(_) => continue,
DepthProbe::Built(..) => continue,
// A selected note isn't witnessable this deep; every deeper
// checkpoint is older still, so none can cover it either.
None => break,
DepthProbe::NoteNotCovered => break,
// A store failure only invalidates this one depth's probe — a
// deeper checkpoint may still cover the notes, so keep walking,
// but remember the failure so an exhausted probe surfaces it
// instead of the misleading "no recorded anchor" outcome.
DepthProbe::StoreError(e) => {
skipped_store_error = Some(e);
continue;
}
}
}

if let Some(e) = skipped_store_error {
return Err(PlatformWalletError::ShieldedMerkleWitnessUnavailable(
format!(
"anchor probe skipped checkpoint depths on a store failure (last: {e}); retry the spend"
),
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect FFI mapping + error-code docs for ShieldedMerkleWitnessUnavailable
rg -nP -C4 'ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src
echo '--- error code enum ---'
rg -nP -C2 'ErrorShielded' packages/rs-platform-wallet-ffi/src/error.rs

Repository: dashpay/platform

Length of output: 3534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate ShieldedMerkleWitnessUnavailable =="
rg -n -C3 'ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src packages/rs-platform-wallet/src || true

echo
echo "== inspect FFI error code enum and mapping =="
sed -n '1,320p' packages/rs-platform-wallet-ffi/src/error.rs

echo
echo "== inspect core error definition =="
rg -n -C3 'enum PlatformWalletError|ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet/src || true

Repository: dashpay/platform

Length of output: 22563


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search FFI shielded send path =="
rg -n -C4 'ShieldedMerkleWitnessUnavailable|ShieldedNoRecordedAnchor|ErrorShieldedNoRecordedAnchor|platform_wallet_manager_shielded_identity_create_from_pool|shielded_send' packages/rs-platform-wallet-ffi/src || true

Repository: dashpay/platform

Length of output: 11551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect shielded send conversion path =="
sed -n '460,540p' packages/rs-platform-wallet-ffi/src/shielded_send.rs

echo
echo "== inspect identity-create conversion path =="
sed -n '660,730p' packages/rs-platform-wallet-ffi/src/shielded_send.rs

echo
echo "== grep for generic From<PlatformWalletError> use in shielded_send.rs =="
rg -n -C2 'PlatformWalletFFIResult::from\(e\)|into\(\)|ErrorUnknown|ShieldedMerkleWitnessUnavailable' packages/rs-platform-wallet-ffi/src/shielded_send.rs

Repository: dashpay/platform

Length of output: 7717


Add a dedicated FFI mapping for ShieldedMerkleWitnessUnavailable
packages/rs-platform-wallet-ffi/src/shielded_send.rs currently routes this new probe failure through the generic ErrorWalletOperation path, so callers lose the retryable/non-broadcast distinction. Add a specific host result code for it instead of flattening it into the generic wallet error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/shielded/operations.rs` around lines
1786 - 1832, The new ShieldedMerkleWitnessUnavailable probe failure is currently
being flattened into the generic wallet-operation error path, which hides its
retryable/non-broadcast meaning. Update the FFI handling in shielded_send.rs to
add a dedicated host result code for this error and map
PlatformWalletError::ShieldedMerkleWitnessUnavailable explicitly instead of
routing it through ErrorWalletOperation, preserving the distinct caller
behavior.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review

Follow-up PR that lands six focused fixes on top of #3990: address-reconciliation conflict handling, shielded pending-spend cleanup on purge/already-spent transitions, anchor-probe error discrimination, best-effort local identity persistence, persist-before-consume ordering for asset-lock funding, and a Swift Clear/Sync race guarded by a generation token. Changes are internally consistent with existing lock ordering and error-handling conventions in this codebase. One in-scope suggestion around the new conflict guard's handling of zero-balance (removal) entries, plus one test-coverage note for the new guard branch.

🟡 2 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:1002-1033: Index-conflict guard drops removal entries too, leaving stale balances that can resurrect after restart
  The new guard at provider.rs:1008-1020 `continue`s past *everything* on conflict — the `is_removal` branch that would call `state.found.remove(&entry.address)`, and the `outcome.entries.push(entry)` that would emit a durable zero-out to the persister. It fires whenever `entry.address` is not in the bijection but `entry.address_index` already maps to a different address (the drift condition).

  For the non-removal (credit) branch this is correct: an orphaned `found` insert with no bijection pairing is invisible via `current_balances()` (which filters on the bijection), so dropping it changes nothing observable.

  For the `is_removal` branch it is not: `found.remove` never needed a pairing to be correct, and `PlatformAddressWallet::initialize_from_persisted` (wallet.rs:144-146) re-seeds `account.address_credit_balance` directly from persisted `found`, bypassing the bijection filter. So if a stale non-zero row already exists on disk for `entry.address` from before the pairing drift, a proof-attested zero-balance entry for that same address is silently discarded — the disk row survives, and the next app restart resurrects a balance Platform has already proven is gone.

  A closely related consequence: when every resolved entry is dropped by this guard, `outcome.entries` is empty but `outcome.resolved > 0`, so `reconcile_address_infos_with_persistence` returns `(default, true)` via the 'nothing new to store' branch (wallet.rs:311-315). `fund_from_asset_lock` then reads `persisted=true` and proceeds to `consume_asset_lock` — exactly the ordering the PR is trying to prevent (Consumed lock paired with un-updated durable rows).

  Both issues share the same root cause: the guard is placed too broadly. Gating only the bijection insert (or splitting into `is_removal` / non-removal branches) preserves the guard's intent — never clobber another pairing — while restoring correct emission of removals and making the `persisted=true` path only claim persistence when there was genuinely nothing to write.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:1002-1033: No regression test for the new index-conflict guard branch
  The existing `commit_reconciliation_*` tests do not construct the state-drift precondition the new guard targets (an `address_index` in the bijection paired with a *different* address than the entry's). A targeted test — especially one combining that precondition with a zero-balance removal entry — would both pin the guard's intent and would have caught the removal-branch behavior flagged above. Worth adding one credit-side and one removal-side case.

Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed_or_unparseable), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, failed_or_unparseable), opus (rust-quality), claude-sonnet-5 (rust-quality), gpt-5.5[high] (rust-quality, failed_or_unparseable), opus (ffi-engineer), claude-sonnet-5 (ffi-engineer), gpt-5.5[high] (ffi-engineer, failed_or_unparseable); verifier: opus; specialists: security-auditor, rust-quality, ffi-engineer

Comment on lines 1002 to 1033
// Drop the entry outright when its derivation index is
// already paired with a DIFFERENT address — committing it
// would either evict that pairing (`BiBTreeMap::insert`
// drops conflicting pairs, orphaning the other address's
// `found` entry) or persist an address `current_balances`
// can't round-trip back out of the committed seed.
if state.addresses.get_by_right(&entry.address).is_none()
&& state.addresses.contains_left(&entry.address_index)
{
tracing::error!(
account_index = entry.account_index,
address_index = entry.address_index,
address = %entry.address,
"commit_reconciliation: derivation index already \
maps to a different address — state drift; \
dropping the reconciliation entry"
);
continue;
}
if is_removal {
state.found.remove(&entry.address);
} else {
state.found.insert(entry.address, entry.funds);
}
// Merge pool-resolved addresses into the bijection so
// `current_balances` can pair the fresh funds with a
// derivation index. Never overwrite an existing pairing —
// `BiBTreeMap::insert` evicts conflicting pairs, which
// would orphan another address's `found` entry.
// derivation index. The conflict guard above already
// ensured this never overwrites an existing pairing.
if state.addresses.get_by_right(&entry.address).is_none() {
if state.addresses.contains_left(&entry.address_index) {
tracing::error!(
account_index = entry.account_index,
address_index = entry.address_index,
address = %entry.address,
"commit_reconciliation: derivation index already \
maps to a different address — state drift; \
leaving the bijection untouched"
);
} else {
state.addresses.insert(entry.address_index, entry.address);
}
state.addresses.insert(entry.address_index, entry.address);
}
outcome.entries.push(entry);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Index-conflict guard drops removal entries too, leaving stale balances that can resurrect after restart

The new guard at provider.rs:1008-1020 continues past everything on conflict — the is_removal branch that would call state.found.remove(&entry.address), and the outcome.entries.push(entry) that would emit a durable zero-out to the persister. It fires whenever entry.address is not in the bijection but entry.address_index already maps to a different address (the drift condition).

For the non-removal (credit) branch this is correct: an orphaned found insert with no bijection pairing is invisible via current_balances() (which filters on the bijection), so dropping it changes nothing observable.

For the is_removal branch it is not: found.remove never needed a pairing to be correct, and PlatformAddressWallet::initialize_from_persisted (wallet.rs:144-146) re-seeds account.address_credit_balance directly from persisted found, bypassing the bijection filter. So if a stale non-zero row already exists on disk for entry.address from before the pairing drift, a proof-attested zero-balance entry for that same address is silently discarded — the disk row survives, and the next app restart resurrects a balance Platform has already proven is gone.

A closely related consequence: when every resolved entry is dropped by this guard, outcome.entries is empty but outcome.resolved > 0, so reconcile_address_infos_with_persistence returns (default, true) via the 'nothing new to store' branch (wallet.rs:311-315). fund_from_asset_lock then reads persisted=true and proceeds to consume_asset_lock — exactly the ordering the PR is trying to prevent (Consumed lock paired with un-updated durable rows).

Both issues share the same root cause: the guard is placed too broadly. Gating only the bijection insert (or splitting into is_removal / non-removal branches) preserves the guard's intent — never clobber another pairing — while restoring correct emission of removals and making the persisted=true path only claim persistence when there was genuinely nothing to write.

source: ['claude']

Comment on lines 1002 to 1033
// Drop the entry outright when its derivation index is
// already paired with a DIFFERENT address — committing it
// would either evict that pairing (`BiBTreeMap::insert`
// drops conflicting pairs, orphaning the other address's
// `found` entry) or persist an address `current_balances`
// can't round-trip back out of the committed seed.
if state.addresses.get_by_right(&entry.address).is_none()
&& state.addresses.contains_left(&entry.address_index)
{
tracing::error!(
account_index = entry.account_index,
address_index = entry.address_index,
address = %entry.address,
"commit_reconciliation: derivation index already \
maps to a different address — state drift; \
dropping the reconciliation entry"
);
continue;
}
if is_removal {
state.found.remove(&entry.address);
} else {
state.found.insert(entry.address, entry.funds);
}
// Merge pool-resolved addresses into the bijection so
// `current_balances` can pair the fresh funds with a
// derivation index. Never overwrite an existing pairing —
// `BiBTreeMap::insert` evicts conflicting pairs, which
// would orphan another address's `found` entry.
// derivation index. The conflict guard above already
// ensured this never overwrites an existing pairing.
if state.addresses.get_by_right(&entry.address).is_none() {
if state.addresses.contains_left(&entry.address_index) {
tracing::error!(
account_index = entry.account_index,
address_index = entry.address_index,
address = %entry.address,
"commit_reconciliation: derivation index already \
maps to a different address — state drift; \
leaving the bijection untouched"
);
} else {
state.addresses.insert(entry.address_index, entry.address);
}
state.addresses.insert(entry.address_index, entry.address);
}
outcome.entries.push(entry);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: No regression test for the new index-conflict guard branch

The existing commit_reconciliation_* tests do not construct the state-drift precondition the new guard targets (an address_index in the bijection paired with a different address than the entry's). A targeted test — especially one combining that precondition with a zero-balance removal entry — would both pin the guard's intent and would have caught the removal-branch behavior flagged above. Worth adding one credit-side and one removal-side case.

source: ['claude']

thepastaclaw and others added 2 commits July 3, 2026 01:32
Split the `commit_reconciliation` index-conflict guard so a zero-funds
removal still zeros the in-memory `found` row and is emitted to the
durable persister; only credit entries are dropped when their
pool-resolved derivation index is already paired with a different
address. Without this split a stale persisted balance for the removed
address could resurrect on restart because the reconciliation swallowed
the zero-out.

Adds regression coverage for both branches — the removal case (must
survive, must not touch the bijection) and the credit case (must be
dropped so the pre-existing pairing isn't evicted).

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

The shielded spend paths currently flatten
`PlatformWalletError::ShieldedMerkleWitnessUnavailable` into
`ErrorWalletOperation`, so hosts can't tell it apart from a hard
wallet-operation error and lose the retryable / non-broadcast semantics
(the error fires *before* broadcast, notes are released — safe to retry
after the transient store failure clears).

Map it to a new `ErrorShieldedMerkleWitnessUnavailable = 20` result code
in both the blanket `From<PlatformWalletError>` impl and each dedicated
`shielded_send.rs` match, mirroring `ErrorShieldedNoRecordedAnchor`.
Mirror the code and typed variant in the Swift `PlatformWalletResultCode`
/ `PlatformWalletError` enums so throw-side callers can branch on it.

Adds test coverage in both the blanket-From and `map_spend_result`
paths.

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

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review

Incremental review of head cd9bf65 on top of prior SHA 2f6d9bc. Carried-forward prior findings: both are FIXED at head — the commit_reconciliation index-conflict guard in provider.rs now splits on is_removal (credits still dropped, removals zero state.found and still push to outcome.entries; only the bijection insert is gated on !index_conflict), and two targeted regression tests (commit_reconciliation_index_conflict_still_emits_removal, commit_reconciliation_index_conflict_drops_credit_entry) pin both branches. New in the latest delta: a dedicated FFI code PlatformWalletFFIResultCode::ErrorShieldedMerkleWitnessUnavailable = 20 mirrors the existing ErrorShieldedNoRecordedAnchor pattern end-to-end (Rust #[repr(C)] discriminant, blanket From, both dedicated shielded_send.rs match arms, Swift PlatformWalletResultCode / PlatformWalletError enums, unit tests on both paths). All 8 agent passes converged on zero new findings; verified provider.rs:1022-1059 in the worktree at head. No CodeRabbit inline comments provided.

Note: GitHub does not allow me to approve my own PR, so this is posted as a COMMENT while preserving the verified clean review.

Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed_or_unparseable), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, failed_or_unparseable), opus (rust-quality), claude-sonnet-5 (rust-quality), gpt-5.5[high] (rust-quality, failed_or_unparseable), opus (ffi-engineer), claude-sonnet-5 (ffi-engineer), gpt-5.5[high] (ffi-engineer, failed_or_unparseable); verifier: opus; specialists: security-auditor, rust-quality, ffi-engineer

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