Skip to content

feat(swift-sdk,platform-wallet): wire shielded send end-to-end (all 4 transitions)#3603

Merged
QuantumExplorer merged 85 commits into
v3.1-devfrom
platform-wallet/shielded-spend-ffi
May 21, 2026
Merged

feat(swift-sdk,platform-wallet): wire shielded send end-to-end (all 4 transitions)#3603
QuantumExplorer merged 85 commits into
v3.1-devfrom
platform-wallet/shielded-spend-ffi

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 5, 2026

Status: ✅ Working end-to-end

The full shielded send matrix now works on regtest — client plumbing,
anchor selection, witness construction, proof, and broadcast. The two
blockers that previously held this PR are both resolved:

  1. Platform-side anchor/prune desync (the old "blocked" banner) —
    fixed and merged independently in fix(drive,drive-abci): retire SHIELDED_MOST_RECENT_ANCHOR_KEY; derive most-recent from [8] and never empty it #3605 (retire
    SHIELDED_MOST_RECENT_ANCHOR_KEY; derive most-recent from the
    anchors-by-height tree and never empty it), with follow-ups fix(drive,drive-abci): post-merge follow-ups for shielded anchor refactor #3606
    and fix(drive): rebalance shielded credit pool subtree keys by access frequency #3607.
  2. Client-side repeat-spend failure ("Anchor not found in the
    recorded anchors tree", Shielded: repeat spend fails with "Anchor not found in the recorded anchors tree" #3703) — fixed in this PR (see below).
    Verified live: two consecutive shielded transfers succeed and the
    rebuilt commitment tree has consistent checkpoints.

Issue being fixed or feature implemented

The Send Dash sheet's four shielded flows all fell through to a
placeholder error even though the spend operations already existed on
the Rust side. This PR threads all four end-to-end so the full Send Dash
matrix works, and fixes the shielded sync/witness regressions that
surfaced once real spends ran against a multi-wallet, shared commitment
tree.

Fixes #3703.

What was done?

platform-wallet — shielded send wiring

  • Free-function spend surface (post Phase-4d coordinator refactor; the
    old ShieldedWallet wrapper was removed): Type 16 transfer, Type 17
    unshield, Type 19 withdraw, and Type 15 shield-from-account. The
    shield helper auto-selects Platform Payment inputs in ascending
    derivation order covering amount + fee, fetches per-input nonces
    from Platform (replacing the old nonce = 0 stub), and signs via a
    host Signer<PlatformAddress>.
  • Spend pre-flight: anchor selection walks local checkpoint depths
    against getShieldedAnchors / getMostRecentShieldedAnchor so the
    spend bundle's anchor matches a Platform-recorded anchor by
    construction, with a ShieldedTreeDiverged diagnostic when nothing
    matches.

platform-wallet — sync / witness correctness

  • Witness "mark-all" fix: the shared commitment tree now marks
    every appended position. The tree is chain-wide and wallets bind at
    different times; deciding retention from "is this position owned right
    now" left a note appended before its owner bound permanently
    unwitnessable (balance showed, spend failed with "Merkle witness
    unavailable"). Per-wallet ownership is tracked separately in the
    per-SubwalletId notes store.
  • Repeat-spend fix (Shielded: repeat spend fails with "Anchor not found in the recorded anchors tree" #3703): sync_notes_across previously gated the
    commitment-tree append on the minimum per-subwallet watermark, so a
    re-fetch from a chunk boundary (or a lagging / late-bound subwallet)
    re-appended positions the tree already held — duplicating leaves,
    corrupting shardtree's internal nodes, and producing per-position
    witnesses that resolved against roots Platform never recorded. New
    ShieldedStore::tree_size() (via max_leaf_position) gates the append
    on the tree's own leaf count (append-once, global); the checkpoint id
    is the true post-append tree size (strictly monotonic, collision-free);
    note saving is gated per-subwallet watermark so a caught-up subwallet
    doesn't re-derive. Includes a tree_size persist/reload regression
    test.
  • Lifecycle fixes: rebind is replace-not-merge (unregister before
    register); shield rejects amount == 0; remove_wallet and
    coordinator clear() now purge per-subwallet store state
    (purge_wallet / purge_all_subwallets) so a later re-bind resyncs
    from index 0 instead of resuming behind a stale watermark.

rs-platform-wallet-ffi

shielded_send module (feature-gated shielded): prover warm-up +
readiness getters, and manager-handle FFIs for transfer / unshield /
withdraw / shield. shield takes a *const SignerHandle (resolved via
a usize round-trip rather than a &'static transmute) for the host
keychain signer.

swift-sdk

  • PlatformWalletManager async methods: shieldedTransfer,
    shieldedUnshield, shieldedWithdraw, shieldedShield, all run off
    the main actor so the ~30 s first-call Halo 2 proof build doesn't
    block UI; plus warmUpShieldedProver() / isShieldedProverReady.
  • Sync Status card rework: the Shielded Sync Status card now shows
    the network-wide picture — Total Shielded Balance summed across
    every wallet's unspent notes, and a Notes Synced watermark so large
    pools show sync progress — instead of a single bound wallet. The
    Orchard address row and per-account state breakdown are removed; sync
    status, counters, and actions are unchanged. Display-only via
    SwiftData @Query.

swift-example-app

SendViewModel.executeSend replaces the four shielded placeholder
branches with real FFI calls; the prover is warmed up at app start.

Send matrix after this PR

Source Destination Client Broadcast
Core Core works works
Platform Shielded works works
Shielded Shielded works works (verified, 2 consecutive spends)
Shielded Platform works works
Shielded Core works works

All five shielded transitions (shield, shield-from-asset-lock, transfer,
unshield, withdraw) wait for proven execution via
broadcast_and_wait::<StateTransitionProofResult>, so the host only sees
success once Platform has actually included the transition (#3704).

How Has This Been Tested?

  • cargo fmt --all clean; cargo check -p rs-platform-wallet --features shielded green against the merged v3.1-dev base.
  • platform-wallet shielded unit tests pass, including the new
    tree_size_tracks_leaf_count_across_reload and
    all_marked_tree_witnesses_every_position_after_reload regression
    tests.
  • build_ios.sh --target sim green.
  • Live regtest, driven through the iOS simulator UI: two consecutive
    shielded transfers from a 0.808 DASH wallet both succeed (the second
    is the case that previously failed with "Anchor not found"). On-disk
    verification: the rebuilt commitment tree has consistent checkpoints
    (id == leaf count at every checkpoint: 8→7, 10→9, 12→11) with no
    double-append, vs. the pre-fix corrupted tree (id=8 → position 13).
  • Reworked Sync Status card verified in the simulator: Total Shielded
    Balance shows the cross-wallet aggregate, Notes Synced reflects the
    watermark, and the Orchard address / per-account rows are gone.

Breaking Changes

None at the consensus level.

  • ShieldedStore gains tree_size(), purge_wallet(), and
    purge_all_subwallets(); witness takes (position, checkpoint_depth). All impls are in-tree and updated; no out-of-tree
    consumers.
  • SendViewModel.executeSend gains a required walletManager
    parameter; the only call site is in-tree and updated.

Review hardening folded into this PR

Beyond the original scope, this PR resolves these review findings:

Deferred follow-ups (tracked separately)

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

🤖 Generated with Claude Code

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

Shielded: repeat spend fails with "Anchor not found in the recorded anchors tree"

2 participants