feat(swift-sdk,platform-wallet): wire shielded send end-to-end (all 4 transitions)#3603
Merged
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 theanchors-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.
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
old
ShieldedWalletwrapper was removed): Type 16 transfer, Type 17unshield, 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 noncesfrom Platform (replacing the old
nonce = 0stub), and signs via ahost
Signer<PlatformAddress>.against
getShieldedAnchors/getMostRecentShieldedAnchorso thespend bundle's anchor matches a Platform-recorded anchor by
construction, with a
ShieldedTreeDivergeddiagnostic when nothingmatches.
platform-wallet — sync / witness correctness
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-
SubwalletIdnotes store.sync_notes_acrosspreviously gated thecommitment-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()(viamax_leaf_position) gates the appendon 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_sizepersist/reload regressiontest.
register);
shieldrejectsamount == 0;remove_walletandcoordinator
clear()now purge per-subwallet store state(
purge_wallet/purge_all_subwallets) so a later re-bind resyncsfrom index 0 instead of resuming behind a stale watermark.
rs-platform-wallet-ffi
shielded_sendmodule (feature-gatedshielded): prover warm-up +readiness getters, and manager-handle FFIs for transfer / unshield /
withdraw / shield.
shieldtakes a*const SignerHandle(resolved viaa
usizeround-trip rather than a&'statictransmute) for the hostkeychain signer.
swift-sdk
PlatformWalletManagerasync methods:shieldedTransfer,shieldedUnshield,shieldedWithdraw,shieldedShield, all run offthe main actor so the ~30 s first-call Halo 2 proof build doesn't
block UI; plus
warmUpShieldedProver()/isShieldedProverReady.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.executeSendreplaces the four shielded placeholderbranches with real FFI calls; the prover is warmed up at app start.
Send matrix after this PR
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 seessuccess once Platform has actually included the transition (#3704).
How Has This Been Tested?
cargo fmt --allclean;cargo check -p rs-platform-wallet --features shieldedgreen against the mergedv3.1-devbase.tree_size_tracks_leaf_count_across_reloadandall_marked_tree_witnesses_every_position_after_reloadregressiontests.
build_ios.sh --target simgreen.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 countat every checkpoint: 8→7, 10→9, 12→11) with nodouble-append, vs. the pre-fix corrupted tree (
id=8 → position 13).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.
ShieldedStoregainstree_size(),purge_wallet(), andpurge_all_subwallets();witnesstakes(position, checkpoint_depth). All impls are in-tree and updated; no out-of-treeconsumers.
SendViewModel.executeSendgains a requiredwalletManagerparameter; 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:
(
broadcast_and_wait), matching the spend-side success contract.ShieldedSyncManager::quiesce()(cancel +drain the in-flight pass) and routed the Clear and stop FFIs through
it, so a sync pass can no longer re-persist notes after Clear returns.
pre-flight balance check reusing rs-sdk's
fetch_inputs_with_noncewith structured error surfacing;
u32checkpoint-id hard-fail; FFIload/free callback-pair validation; network-scoped shielded restore +
Sync Status summary; per-wallet shield single-flight guard;
amount/recipient send validation; and a pure unit-tested
select_shield_inputs.Deferred follow-ups (tracked separately)
shielded_add_account()updates keys but not thecoordinator registry (half-live account; receive-side only).
sync/scale testing.
GroupByIn + rangeno-proof fold(out of scope; documented + regression-tested).
nonce_incunchecked+1in rs-sdk — assessed won't-fix: reachingu32::MAXon a single address is practically unreachable.Checklist:
🤖 Generated with Claude Code