fix(platform-wallet): build shielded spends against a Platform-recorded anchor#3977
fix(platform-wallet): build shielded spends against a Platform-recorded anchor#3977shumkov wants to merge 3 commits into
Conversation
…TestFlight report B) A TestFlight report described a shielded->Core withdrawal that repeatedly shows "Transaction may have gone through" but never lands, with funds untouched. Investigation (docs/shielded/TESTFLIGHT_FEEDBACK_INVESTIGATION.md) traced it to an anchor mismatch, reproduced deterministically here without a testnet: - platform-wallet (--features shielded) wallet::shielded::file_store::tests:: depth0_spend_anchor_mid_block_is_not_a_recorded_block_boundary_anchor On the real SQLite-backed commitment tree: the spend anchor is the depth-0 (current) tree root (witness(pos,0).root == tree_anchor), but the wallet syncs commitments by index-chunk (CHUNK_SIZE=2048), not by block, so its tree routinely stops mid-block. drive records one anchor per block (block-end root). The test shows a mid-block depth-0 anchor equals neither adjacent block-boundary anchor -> it is a root Platform never recorded. - drive-abci (--features shielded_test_data) ...shielded_withdrawal::tests::proof_verification:: test_valid_proof_with_unrecorded_anchor_returns_invalid_anchor_error A real Orchard proof + correct value balance + sufficient pool, but the anchor is not recorded (no insert_anchor_into_state) -> InvalidAnchorError. Identical to the passing ..._proof_succeeds test except the missing anchor record, isolating the anchor as the sole cause (proof passes first). Chain: the wallet submits a mid-block anchor drive never recorded; drive rejects exactly that with InvalidAnchorError; the withdrawal never lands, and the ambiguous rejection is misreported as "may have gone through". Fix direction (build the proof against a recorded anchor, not the depth-0 tip) is in the investigation doc; not implemented here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shielded spends (withdraw / unshield / transfer / identity-create-from-pool) built the Orchard proof against the wallet's depth-0 (current) commitment-tree root. The index-chunk sync (CHUNK_SIZE=2048) routinely leaves the tree mid-block, and drive records anchors only at block boundaries, so the depth-0 root is frequently one Platform never recorded -> validate_anchor_exists rejects the transition (InvalidAnchorError) and the spend never lands. The ambiguous rejection then surfaces as "Transaction may have gone through" (ShieldedSpendUnconfirmed), the TestFlight report B symptom. Reproduced by two tests committed earlier (wallet mid-block anchor is non-recorded; drive rejects a valid-proof unrecorded anchor). Fix: extract_spends_and_anchor now fetches Platform's recorded anchor set (GetShieldedAnchors) and builds against the shallowest checkpoint depth whose root is recorded, via a pure, unit-testable select_recorded_spends probe: - depth 0 (fully-synced fast path) if its root is recorded; - else walk older checkpoints (1..100), taking the first recorded root, and stop once a selected note post-dates the checkpoint (deeper is older still); - else return the new retryable PlatformWalletError::ShieldedNoRecordedAnchor BEFORE broadcasting. Fund-safety: the anchor is always the root of the same-depth witnesses (MerklePath::root) with a cross-note agreement check, so anchor<->witness stay consistent by construction; the Orchard nullifier is anchor-independent, so an older recorded anchor does not weaken the on-chain double-spend guard (auditor-confirmed). ShieldedNoRecordedAnchor is returned pre-broadcast and routes to each caller's reservation-releasing arm (cancel_pending), so notes are freed and nothing is broadcast. Also: ShieldedStore gains witness_at_depth (witness becomes a depth-0 default); new FFI code ErrorShieldedNoRecordedAnchor = 19 maps to a retryable "still syncing" message (distinct from ShieldedSpendUnconfirmed's "do not retry"). Design was spec'd (docs/shielded/SHIELDED_WITHDRAWAL_ANCHOR_FIX_SPEC.md) and audited by three independent reviewers (soundness / feasibility / scope) before implementation. Wallet suite 304 passing; FFI 107; fmt + clippy clean. Follow-ups (documented in the spec): Report-A sync convergence so a chronically-interrupted wallet reaches a recorded checkpoint; the Swift FFIResultCode case for code 19; optionally returning each anchor's tree size from the query for a fully deterministic selection; and the ShieldedSpendUnconfirmed classification/reservation-release (A). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
✅ Review complete (commit 6a845c2) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds Platform-recorded anchor selection for shielded spends, a depth-aware shielded witness API, retryable ChangesShielded anchor selection fix
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Well-scoped fix: extract_spends_and_anchor now probes Platform-recorded checkpoint depths and returns a retryable pre-broadcast error when none match, with strong test coverage against a real SQLite-backed tree. No blockers: the fix is fund-safe, no consensus-safety issues, and the PR body already documents the acknowledged follow-ups (Swift mirror + potential note re-selection improvement). Two agent 'blocking' claims are actually documented design trade-offs and are downgraded here.
🟡 4 suggestion(s) | 💬 4 nitpick(s)
Findings not posted inline (1)
These findings could not be anchored to the current diff, but they are still part of this review.
- [SUGGESTION]
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:83-90: Swift mirror missing case forErrorShieldedNoRecordedAnchor = 19— Rust now emitsPLATFORM_WALLET_FFI_RESULT_CODE_ERROR_SHIELDED_NO_RECORDED_ANCHOR = 19as a retryable pre-broadcast condition intentionally distinct fromShieldedSpendUnconfirmed = 18, but the Swift result-code initializer falls through to.errorUnknownat thedefault:arm and `PlatformWal...
🤖 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/shielded/operations.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/operations.rs:1602-1619: Depth>0 branch collapses genuine store errors into retryable no-anchor state
At depth > 0, the match arm `Ok(None) | Err(_) => return Ok(None)` treats real `witness_at_depth` failures (poisoned mutex in `FileBackedShieldedStore`, underlying SQLite/IO errors, tree corruption) identically to the expected `NotContained`/not-yet-checkpointed case. Both then feed the outer loop's `None => break` and surface as the retryable `ShieldedNoRecordedAnchor("wait for the next shielded sync")`. The trait contract explicitly distinguishes the two — `Ok(None)` means the depth is unusable, `Err(_)` means the store itself failed — and depth 0 already preserves that distinction by mapping `Err(e)` to `ShieldedMerkleWitnessUnavailable(e.to_string())`. The consequence is diagnostic, not fund-safety (nothing is broadcast on this path), but the operator loses the underlying error message entirely and the user is told to wait for a sync that will never make this succeed. Split the arm so `Err(e)` at depth > 0 propagates (or at minimum emits a `tracing::warn!` before returning `Ok(None)`).
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/operations.rs:1677-1687: Note re-selection over depth-eligible notes not attempted before returning retryable error
`select_notes` in `note_selection.rs` sorts unspent notes by value descending only — position is not considered. If a mid-block wallet has a newly-appended largest note plus older notes that could cover amount+fee, the greedy selector picks the new note; `build_at_depth(depth > 0, false)` then returns `None` because that note post-dates every recorded checkpoint, the walk breaks at the first depth, and the caller returns `ShieldedNoRecordedAnchor` even though the wallet could spend now against a recorded anchor by re-running selection over notes with `position < recorded_checkpoint_size`. The PR body documents this as the intentional design trade-off ("stopping once a selected note post-dates the checkpoint"), so this is not a fund-safety issue and the current path is strictly better than pre-PR (which failed silently). But the tests use a single note at position 0 and never exercise the mixed old/new balance case — the retryable error is fine, but consider whether the deferred `GetShieldedAnchors`-returns-tree-size follow-up is enough, or whether a position-aware selection pass (perhaps as a fallback before returning the retryable error) is worth doing here to reduce the "user asked to wait but the wallet could have spent now" surface.
In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:83-90: Swift mirror missing case for `ErrorShieldedNoRecordedAnchor = 19`
Rust now emits `PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_SHIELDED_NO_RECORDED_ANCHOR = 19` as a retryable pre-broadcast condition intentionally distinct from `ShieldedSpendUnconfirmed = 18`, but the Swift result-code initializer falls through to `.errorUnknown` at the `default:` arm and `PlatformWalletError.init(result:)` then produces `.unknown`. On-device the user sees a generic unknown-error path instead of the "still syncing — try again shortly" retryable UX this PR set up. The PR body explicitly lists the Swift mirror as a follow-up, so this is not a regression, but the Rust-side fix isn't end-to-end effective until the Swift case + `SendViewModel` branch land. Either land the Swift case in this PR, or ensure the follow-up is tracked and the Rust `message` string is at least surfaced in the meantime so users see the retryable copy.
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:83-90: Swift mirror missing case for `ErrorShieldedNoRecordedAnchor = 19`
Rust now emits `PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_SHIELDED_NO_RECORDED_ANCHOR = 19` as a retryable pre-broadcast condition intentionally distinct from `ShieldedSpendUnconfirmed = 18`, but the Swift result-code initializer falls through to `.errorUnknown` at the `default:` arm and `PlatformWalletError.init(result:)` then produces `.unknown`. On-device the user sees a generic unknown-error path instead of the "still syncing — try again shortly" retryable UX this PR set up. The PR body explicitly lists the Swift mirror as a follow-up, so this is not a regression, but the Rust-side fix isn't end-to-end effective until the Swift case + `SendViewModel` branch land. Either land the Swift case in this PR, or ensure the follow-up is tracked and the Rust `message` string is at least surfaced in the meantime so users see the retryable copy.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs (1)
514-612: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect regression test; consider extracting shared bundle-construction helper.
Test logic is correct — omitting
insert_anchor_into_statewhile keeping the proof valid should triggerInvalidAnchorErrorviavalidate_anchor_exists, matching the assertion. This ~90-line block duplicates the note/tree/bundle construction fromtest_valid_shielded_withdrawal_proof_succeeds(lines 409-512) almost verbatim, and a similar helper (build_valid_shielded_withdrawal_bundle) already exists inmod security_audit. Promoting that helper to a shared test utility (or module-levelsuper::helper) would let this test callbuild_valid_shielded_withdrawal_bundle(...)and skip straight to omittinginsert_anchor_into_state.🤖 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-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs` around lines 514 - 612, The test behavior is correct, but the long setup in test_valid_proof_with_unrecorded_anchor_returns_invalid_anchor_error duplicates the bundle-construction flow from test_valid_shielded_withdrawal_proof_succeeds and the existing build_valid_shielded_withdrawal_bundle helper in mod security_audit. Extract or reuse a shared helper for note/tree/builder/proof creation, then have this test call that helper and only omit insert_anchor_into_state before process_transition so the assertion still targets validate_anchor_exists and InvalidAnchorError.
🤖 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 `@docs/shielded/SHIELDED_WITHDRAWAL_ANCHOR_FIX_SPEC.md`:
- Around line 93-105: The interface section is out of sync with the actual
ShieldedStore API: it mentions anchor_at_depth and checkpoint_id_at_depth even
though the trait currently exposes witness_at_depth plus tree_anchor/tree_size.
Update the SHIELDED_WITHDRAWAL_ANCHOR_FIX_SPEC wording to either add the missing
methods to ShieldedStore consistently or rephrase this section to describe the
helper behavior using the existing method names, and keep the references to
ShieldedStore, FileBackedShieldedStore, and InMemoryShieldedStore aligned
throughout.
In `@packages/rs-platform-wallet-ffi/src/error.rs`:
- Around line 127-136: The Swift mirror for PlatformWalletResult is missing the
new Rust error case, so `errorShieldedNoRecordedAnchor = 19` currently falls
through to `.errorUnknown`. Update `PlatformWalletResult` by adding the new enum
case alongside `errorShieldedSpendUnconfirmed`, and extend `init(ffi:)` to map
the FFI value 19 to that case so Swift can distinguish the retryable no-anchor
state from spend-unconfirmed.
---
Nitpick comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs`:
- Around line 514-612: The test behavior is correct, but the long setup in
test_valid_proof_with_unrecorded_anchor_returns_invalid_anchor_error duplicates
the bundle-construction flow from test_valid_shielded_withdrawal_proof_succeeds
and the existing build_valid_shielded_withdrawal_bundle helper in mod
security_audit. Extract or reuse a shared helper for note/tree/builder/proof
creation, then have this test call that helper and only omit
insert_anchor_into_state before process_transition so the assertion still
targets validate_anchor_exists and InvalidAnchorError.
🪄 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: 00c4cd03-753c-4027-a8a6-b4d7c1a5de97
📒 Files selected for processing (9)
docs/shielded/SHIELDED_WITHDRAWAL_ANCHOR_FIX_SPEC.mddocs/shielded/TESTFLIGHT_FEEDBACK_INVESTIGATION.mdpackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rspackages/rs-platform-wallet-ffi/src/error.rspackages/rs-platform-wallet-ffi/src/shielded_send.rspackages/rs-platform-wallet/src/error.rspackages/rs-platform-wallet/src/wallet/shielded/file_store.rspackages/rs-platform-wallet/src/wallet/shielded/operations.rspackages/rs-platform-wallet/src/wallet/shielded/store.rs
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## v4.0-dev #3977 +/- ##
=========================================
Coverage 87.18% 87.18%
=========================================
Files 2632 2632
Lines 327563 327563
=========================================
+ Hits 285592 285593 +1
+ Misses 41971 41970 -1
🚀 New features to boost your workflow:
|
|
CodeRabbit retry completed. I agree these are nonblocking cleanup items rather than blockers: the Swift My approval/merge judgment is unchanged; these are follow-up cleanup points for the branch owner to decide whether to fold in. |
…robe Polish from the #3977 review (all non-blocking; PR already approved): - select_recorded_spends: deserialize each note + decode its cmx ONCE (hoisted before the depth walk) so each probed depth only re-witnesses. - depth > 0: split Ok(None) (note post-dates this checkpoint -> unusable depth) from Err (a genuine store failure), logging the latter via tracing::warn! before treating the depth as unusable, so the diagnostic isn't swallowed; still non-aborting. - extract_spends_and_anchor: run the empty-notes guard before the ShieldedAnchors round-trip. - MAX_ANCHOR_PROBE_DEPTH: document the max_checkpoints coupling. - test: note_newer_than_recorded_checkpoint_breaks_and_returns_retryable_error pins the walk's early-termination + the value-selection trade-off. - spec: align the Interface section with the as-built witness_at_depth-only design (no separate anchor_at_depth / checkpoint_id_at_depth). - swift: mirror the new FFI code as PlatformWalletResultCode .errorShieldedNoRecordedAnchor = 19 + PlatformWalletError .shieldedNoRecordedAnchor, with the init(ffi:)/init(result:) mappings and a retryable doc-comment (distinct from shieldedSpendUnconfirmed). Wallet 305 tests pass; fmt + clippy clean; iOS build (build_ios.sh) succeeds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The current head fixes the prior Swift result-code mapping, empty-selection ordering, redundant note decoding, and early-break test coverage. Two non-blocking issues remain: the new file-backed store method still leaks expected shardtree absence through the error channel, and the value-first selection model can return a retryable wait error even when older notes could spend immediately.
🟡 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/shielded/file_store.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/file_store.rs:301-302: Map expected witness absence to the trait's `Ok(None)` contract
`ShieldedStore::witness_at_depth` documents `Ok(None)` for expected absence cases, including a position appended after the requested checkpoint. The file-backed implementation forwards `ClientPersistentCommitmentTree::witness` errors wholesale, but shardtree returns `QueryError::NotContained` for that normal newer-than-checkpoint case before the grovedb wrapper converts it into `CommitmentTreeError::InvalidData`. The current caller logs and treats the error as an unusable depth, so this is not fund-safety or consensus-critical, but the new trait method's implementation does not match its contract and future callers can easily misclassify normal control flow as store corruption.
In `packages/rs-platform-wallet/src/wallet/shielded/operations.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/operations.rs:1702-1717: Value-first selection can mask spendable older notes
`reserve_unspent_notes` selects and reserves notes before the recorded-anchor depth is known, and `select_notes` sorts by value only. If the largest selected note is newer than every Platform-recorded checkpoint, the depth walk breaks and returns `ShieldedNoRecordedAnchor`; older notes that were not selected may still cover the amount plus fee at a recorded checkpoint. This is fund-safe and now documented/tested as a retryable availability tradeoff, but it can tell the user to wait when an immediately spendable subset exists. Fixing it likely requires selecting after discovering an eligible checkpoint, or retrying selection over notes witnessable at that checkpoint before returning the retryable error.
| tree.witness(Position::from(position), depth) | ||
| .map_err(|e| FileShieldedStoreError(format!("witness({position}, depth {depth}): {e}"))) |
There was a problem hiding this comment.
🟡 Suggestion: Map expected witness absence to the trait's Ok(None) contract
ShieldedStore::witness_at_depth documents Ok(None) for expected absence cases, including a position appended after the requested checkpoint. The file-backed implementation forwards ClientPersistentCommitmentTree::witness errors wholesale, but shardtree returns QueryError::NotContained for that normal newer-than-checkpoint case before the grovedb wrapper converts it into CommitmentTreeError::InvalidData. The current caller logs and treats the error as an unusable depth, so this is not fund-safety or consensus-critical, but the new trait method's implementation does not match its contract and future callers can easily misclassify normal control flow as store corruption.
source: ['codex-rust-quality']
| for depth in 1..MAX_ANCHOR_PROBE_DEPTH { | ||
| match build_at_depth(depth, false)? { | ||
| Some((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, | ||
| // A selected note isn't witnessable this deep; every deeper | ||
| // checkpoint is older still, so none can cover it either. | ||
| None => break, | ||
| } | ||
| } | ||
|
|
||
| Ok((spends, anchor)) | ||
| Err(PlatformWalletError::ShieldedNoRecordedAnchor( | ||
| "no recorded anchor covers the selected notes; wait for the next shielded sync".to_string(), |
There was a problem hiding this comment.
🟡 Suggestion: Value-first selection can mask spendable older notes
reserve_unspent_notes selects and reserves notes before the recorded-anchor depth is known, and select_notes sorts by value only. If the largest selected note is newer than every Platform-recorded checkpoint, the depth walk breaks and returns ShieldedNoRecordedAnchor; older notes that were not selected may still cover the amount plus fee at a recorded checkpoint. This is fund-safe and now documented/tested as a retryable availability tradeoff, but it can tell the user to wait when an immediately spendable subset exists. Fixing it likely requires selecting after discovering an eligible checkpoint, or retrying selection over notes witnessable at that checkpoint before returning the retryable error.
source: ['codex-rust-quality']
Issue being fixed or feature implemented
A TestFlight report (build 3) described a shielded→Core withdrawal that repeatedly fails: the app shows "Transaction may have gone through — waiting for the next shielded sync to confirm. Do not retry," but the balance never moves (funds untouched), across multiple days. A second report ("switching wallets after starting a shielded sync") is the same root cause from the other side (an interrupted sync).
Root cause (reproduced deterministically): shielded spends build the Orchard proof against the wallet's depth-0 (current) commitment-tree root. The note-commitment sync is chunked by index (
CHUNK_SIZE = 2048), not by block, so the wallet's tree routinely sits mid-block; drive records anchors only at block boundaries (one per block, 1000 retained). So the depth-0 root is frequently a value Platform never recorded →validate_anchor_existsrejects the transition withInvalidAnchorError→ the spend never lands, and the ambiguous rejection is misreported asShieldedSpendUnconfirmed("may have gone through").Two tests committed here reproduce it end-to-end without a testnet: the wallet's mid-block depth-0 anchor is provably not a recorded anchor (
platform-wallet), and a real-proof withdrawal with an unrecorded anchor is rejected withInvalidAnchorError(drive-abci).What was done?
extract_spends_and_anchornow fetches Platform's recorded anchor set (GetShieldedAnchors) and builds against the shallowest checkpoint depth whose root Platform recorded, via a pure, unit-testableselect_recorded_spendsprobe:1..100, taking the first recorded root, stopping once a selected note post-dates the checkpoint (deeper is older still);PlatformWalletError::ShieldedNoRecordedAnchorbefore broadcasting.Supporting changes:
ShieldedStoregainswitness_at_depth(witnessbecomes a depth-0 default — non-breaking); a new FFI codeErrorShieldedNoRecordedAnchor = 19maps to a "still syncing — try again shortly" message, distinct fromShieldedSpendUnconfirmed's "do not retry."Fund-safety (please review): the anchor is always the root of the same-depth witnesses (
MerklePath::root) with a cross-note agreement check, so anchor↔witness stay consistent by construction. The Orchard nullifier is(nk, ρ, ψ, cm)-derived and anchor-independent, so spending against an older recorded anchor does not weaken the on-chain double-spend guard.ShieldedNoRecordedAnchoris returned pre-broadcast and routes to each of the four spend paths' reservation-releasing arm (cancel_pending) — notes are freed, nothing is broadcast.The design was written up (
docs/shielded/SHIELDED_WITHDRAWAL_ANCHOR_FIX_SPEC.md, alongside the root-cause investigation) and audited by three independent reviewers (soundness / feasibility / scope) before implementation.How Has This Been Tested?
select_recorded_spends, realFileBackedShieldedStore+ a real Orchard note): mid-block → selects the prior recorded (block-2 / depth-1) anchor, not depth-0; fully-synced → depth-0; no recorded →ShieldedNoRecordedAnchor.cargo test -p platform-wallet --lib --features shielded→ 304 passed; FFI suite 107 passed;fmt --check+clippy --features shieldedclean.Not covered (network-gated): live testnet shielded→Core withdrawal succeeding after the fix — best confirmed on a device once the Swift code-19 case is wired.
Known follow-ups (out of scope, documented in the spec)
ErrorShieldedNoRecordedAnchor = 19case + retryable UI (until then Swift sees it as an unknown code).ShieldedSpendUnconfirmedclassification / no-restart reservation release ("A").Breaking Changes
None externally.
ShieldedStore::witness_at_depthis a new required trait method (both in-tree impls updated;witnesskeeps working via a default);extract_spends_and_anchoris private.Checklist:
🤖 Generated with Claude Code
Summary by CodeRabbit