Skip to content

feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692

Open
Claudius-Maginificent wants to merge 58 commits into
v4.1-devfrom
feat/platform-wallet-rehydration
Open

feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692
Claudius-Maginificent wants to merge 58 commits into
v4.1-devfrom
feat/platform-wallet-rehydration

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Why this PR exists

  • Problem: restarting a wallet client from persisted storage needs three things the persisted state must serve without the seed: detection (the SPV watch set — script pubkeys/addresses to watch), spending (UTXO set for input selection), and resume (sync watermarks, chain-lock state, pending asset locks). On iOS the Keychain is locked at launch, so none of this may depend on seed access.
  • What breaks without it: without seedless rehydration, every launch must re-derive and re-scan from wallet birth height before the wallet is usable — minutes of network sync for a wallet that was fully synced one app-restart ago.
  • Design constraint (validated against swift-sdk): launch is seedless and watch-only; signing unlocks the seed on-demand later.
  • Independence: reshapes ClientWalletStartState and rewrites every in-tree consumer in the same PR. The storage-reader half is feat(platform-wallet): persistence readers + seedless load() wiring (split from #3692) #3968 (also independent); the two combine on the dash-evo-tool integration branch.

What was done (rs-platform-wallet + -ffi + swift)

PlatformWalletManager::load_from_persistor() reconstructs every persisted wallet from its full snapshot (ClientWalletStartState.core_wallet_info):

  • Each wallet returns as Wallet::new_watch_only(...) from stored account xpubs — no seed, no signing-key derivation on the load path.
  • The persisted snapshot (UTXOs, tx records, IS-locks, sync watermarks, balances) is applied verbatim across any account topology, failing closed rather than reconstructing a silent-zero balance. The earlier keyless-projection fallback path (re-deriving pool state from raw addresses when no snapshot existed) has been removed entirely — no real persister ever produced that shape, so it was dead code carrying real complexity.
  • load_from_persistor now returns a 3-state outcome (Loaded / Partial / NoneUsable) instead of a boolean-ish result: an already-registered wallet is reported as a distinct skip reason rather than silently folded into "loaded", and a structurally-corrupt row is skipped, not fatal.
  • Defines the rehydration data model the feat(platform-wallet): persistence readers + seedless load() wiring (split from #3692) #3968 readers consume (ClientWalletStartState, load-result types).
  • Review-comment triage batch (23 findings, all fix except one confirmed false_positive): removed the obsolete warn_if_non_default_account compensator now that per-account attribution is real; reframed the manifest trust-boundary and snapshot_accounts_match_manifest docs from "tamper detection" to accurate self-consistency framing; added an integer-overflow guard to the FFI slice_from_raw helper; fixed xpub-error misclassification (MalformedXpub now distinct from generic decode errors); fixed a Swift-side ordering bug where a failed load could leave stale skip-state from a prior successful load.
  • QA follow-up batch (5 findings from an independent test/security/consistency pass, all fix): removed the latent From<LoadOutcome> for Result<LoadOutcome, PlatformWalletError> conversion and the LoadIncomplete error variant entirely — nothing in-tree used it, and it silently collapsed a harmless idempotent repeat load and a genuine total-corruption failure into the same Err; callers use .loaded()/.skipped() and inspect skip reasons directly instead. Deleted the three now-unreachable PlatformWalletError variants (RehydrationTopologyUnsupported/RehydrationPoolMismatch/RehydrationPoolTypeMismatch) left over from the projection-fallback removal above. Made manager::rehydrate a private module (it exposed nothing public). Added regression tests for the slice_from_raw overflow guard and an end-to-end MalformedXpub classification test.

Known follow-up needed (not fixed in this PR)

  • Manifest authentication: build_watch_only_wallet / load_from_persistor trust the persisted account manifest as-is — no cryptographic binding to wallet_id. Verified the root xpub can't be recovered from what's persisted (hardened derivation only), so there's no in-crate fix; the real fix is a persisted commitment/MAC in rs-platform-wallet-storage, tracked by feat(platform-wallet): manifest integrity checksum (Risk-6/R12.5 follow-up) #3992.
  • Chain-lock cache: last_applied_chain_lock is restored from local storage without re-verifying the BLS/quorum signature; bounded by downstream network re-verification of the asset-lock proof, so low risk today but same underlying gap.

Testing

cargo fmt --all --check; cargo clippy --all-targets --all-features -- -D warnings clean on platform-wallet, platform-wallet-ffi, and rs-sdk-ffi. cargo test on platform-wallet + platform-wallet-ffi + rs-sdk-ffi: 693 passed, 0 failed across 16 test binaries at the point the review-comment triage batch and the v4.1-dev merge landed (including new rollback/empty-first-run/concurrent-load regression tests). After the QA follow-up batch: cargo test on platform-wallet + platform-wallet-ffi: 365 passed, 0 failed across 12 test binaries, including the new overflow-guard and end-to-end MalformedXpub tests. Independently re-verified by a separate QA pass (test execution, security audit, and structural/plan-completion review); the 5 LOW-severity findings it surfaced were triaged and fixed in the QA follow-up batch above — no MEDIUM+ findings at any point. Swift changes hand-verified against the Rust FFI contract (no Swift toolchain in this environment).

Breaking changes

Two, both against this PR's own prior (unreleased) shape, targeting v4.1-dev:

  • ClientWalletStartState.core_wallet_info is now a required Box<ManagedWalletInfo> (was Option<...>); the keyless-projection fallback fields are removed.
  • load_from_persistor's outcome is now a 3-state enum (Loaded/Partial/NoneUsable) rather than the previous 2-state shape; the exported FFI symbol name and LoadOutcomeFFI struct layout are unchanged (additive only).

Checklist

  • Self-review performed
  • Tests added/updated
  • Docs updated where needed

🤖 Co-authored by Claudius the Magnificent AI Agent

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces seedless watch-only wallet rehydration, returns per-row load outcomes with skip tracking, exposes skipped-wallet results through FFI/Swift, and updates related identity replay, core-bridge warnings, and signer test stubs.

Changes

Watch-only rehydration and load outcomes

Layer / File(s) Summary
Persisted state and outcome contracts
packages/rs-platform-wallet/src/changeset/client_wallet_start_state.rs, packages/rs-platform-wallet/src/manager/load_outcome.rs, packages/rs-platform-wallet/src/error.rs, packages/rs-platform-wallet/src/changeset/client_start_state.rs, packages/rs-platform-wallet/src/lib.rs, packages/rs-platform-wallet/src/manager/mod.rs, packages/rs-platform-wallet/src/changeset/changeset.rs, packages/rs-platform-wallet/src/changeset/traits.rs, packages/rs-platform-wallet/README.md, packages/rs-platform-wallet/src/events.rs
ClientWalletStartState moves to keyless reconstruction fields, LoadOutcome/SkipReason/CorruptKind are added, ClientStartState gains skipped, and the public module/docs/event surface is updated for load-skip reporting.
Wallet rebuild and core-state replay
packages/rs-platform-wallet/src/manager/rehydrate.rs, packages/rs-platform-wallet/src/error.rs
New helpers build watch-only wallets, apply persisted core state, reconcile address pools, and add fail-closed rehydration error variants with tests.
Load loop and identity replay
packages/rs-platform-wallet/src/manager/load.rs, packages/rs-platform-wallet/src/wallet/apply.rs, packages/rs-platform-wallet/src/wallet/identity/state/manager/apply.rs, packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs, packages/rs-platform-wallet/src/wallet/platform_wallet.rs
load_from_persistor now returns LoadOutcome, continues past per-row corruption, and shared identity/contact replay is delegated to a new helper.
FFI outcome structs and Swift consumption
packages/rs-platform-wallet-ffi/src/core_wallet_types.rs, packages/rs-platform-wallet-ffi/src/manager.rs, packages/rs-platform-wallet-ffi/src/persistence.rs, packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
The Rust FFI now reports skipped wallets with stable reason codes and freeable outcome storage, the persister classifies corrupt rows, and Swift stores the skipped-wallet list while skipping already-rejected ids.

Core bridge UTXO warnings

Layer / File(s) Summary
Non-default-account UTXO warnings
packages/rs-platform-wallet/src/changeset/core_bridge.rs
TransactionDetected and BlockProcessed now warn when persisted UTXOs originate from non-default account indexes, with regression tests preserving projection behavior.

Mnemonic signer and test stubs

Layer / File(s) Summary
Extended public key derivation
packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs, packages/rs-sdk-ffi/src/signer_simple.rs, packages/rs-dpp/src/state_transition/mod.rs, packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs, packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
Mnemonic-based derivation gains a shared helper and extended_public_key, and the DPP signer test stubs implement the new trait method.

Sequence Diagram(s)

sequenceDiagram
  participant FFIPersister
  participant PlatformWalletManager
  participant PlatformEventManager
  participant SwiftPlatformWalletManager
  FFIPersister->>PlatformWalletManager: load() returns ClientStartState
  PlatformWalletManager->>PlatformEventManager: on_wallet_skipped_on_load(wallet_id, reason)
  PlatformWalletManager->>SwiftPlatformWalletManager: return LoadOutcome / skipped wallets
Loading

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • dashpay/platform#3634 - Also changes packages/rs-platform-wallet-ffi/src/persistence.rs in the wallet restore path and the keyless start-state projection.
  • dashpay/platform#3828 - Related to the same core_bridge.rs UTXO projection behavior and account-index handling.
  • dashpay/platform#3973 - Touches the same Cargo.toml workspace dependency pins.

Suggested labels: ready for final review

Suggested reviewers: QuantumExplorer, llbartekll, shumkov, lklimek, ZocoLini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: seedless watch-only rehydration from the persistor in platform-wallet.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/platform-wallet-rehydration

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.

@github-actions

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-05-22T10:49:18.857Z

@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator Author

🔁 Seedless-load rework landed — 34532e57d5..6f22c0e9f5

Per design validation against dashwallet-ios swift-sdk-integration: real iOS host loads watch-only at app launch (Keychain locked) and signs on-demand via the existing MnemonicResolverHandle vtable. The original seeded-load model (load takes &dyn SeedProvider, derives Wallet per persisted id, runs constant-time wrong-seed gate at load) did not match the real consumer. Pushed 7 commits flipping the model. Existing PR description above is stale — supersedes it for the deltas below.

What's new

  • load_from_persistor() is now seedlesspub async fn load_from_persistor(&self) -> Result<LoadOutcome, PlatformWalletError>. Manager reconstructs each persisted wallet via Wallet::new_watch_only(network, wallet_id, accounts), with accounts assembled from the keyless account_manifest (one Account::from_xpub(...) per registration entry). No seed touched on the load path.
  • Wrong-seed gate moved to the sign pathcrate::sign_gate::verify_seed_matches_wallet_id(root_pub, expected_wallet_id) -> bool (constant-time via subtle::ConstantTimeEq). Wired into all 4 resolver-fed FFI sign entrypoints:
    • sign_with_mnemonic_resolver.rs::dash_sdk_sign_with_mnemonic_resolver_and_path
    • identity_derive_and_persist.rs::dash_sdk_derive_and_persist_identity_keys
    • derive_identity_key_at_slot.rs::dash_sdk_derive_identity_key_at_slot_with_resolver
    • shielded_sync.rs::platform_wallet_manager_bind_shielded
  • SeedProvider trait + adapter + FFI shim deletedMnemonicResolverHandle (which already existed) is the sole on-demand signing contract. Files removed: rs-platform-wallet/src/seed_provider.rs, rs-platform-wallet-ffi/src/rehydration_seed_provider.rs, rs-platform-wallet-storage/src/secrets/seed_provider_adapter.rs (+ its test).
  • FFI signature: platform_wallet_manager_load_from_persistor(manager, persister, *mut LoadOutcomeFFI) — dropped the 3rd resolver arg (reverts b7508a0d47's 3-arg shape).
  • Swift PlatformWalletManager.swift aligns to the 2-arg + outparam C signature (passes nil for the outcome ptr).

ABI deltas (new in this PR, no v3.1-dev impact)

  • New result code ErrorWrongSeedForWallet = 13 in PlatformWalletFFIResultCode.
  • SkippedWalletFFI::reason_code family remapped to 100/101/102 for the new CorruptKind variants (MissingManifest, MalformedXpub, DecodeError). The old 0/1/2 (seed-availability) are gone — those skip-triggers don't exist anymore.

AR-7 hygiene

  • Load path eliminates AR-7 entirely — manager never constructs WalletType::Mnemonic|Seed; only WalletType::WatchOnly (no key material). AR-7's residual Debug concern was about derived Wallet values on the load path; that path no longer derives.
  • Sign path keeps AR-7 disciplineZeroizing + non_secure_erase() on the gate's throwaway master key on the mismatch path, before returning the error tag. No {:?} over any secret type.

Test reshape

  • tests/rehydration_load.rs — RT-WO, RT-Corrupt, RT-Z (watch-only construction, corrupt-row hard-fail, secret-hygiene). Wrong-seed scenarios removed from this file (no seed at load anymore).
  • Co-located gate tests in each of the 4 FFI sign entrypoints:
    • sign_with_mnemonic_resolver: happy + wrong + full-buffer-zero assertion
    • identity_derive_and_persist: wrong + persister-callback-never-fires assertion
    • derive_identity_key_at_slot: happy + wrong (asserts populated out_row on happy, zero/null fields on mismatch)
    • shielded_sync: wrong + null-resolver-handle-rejected (happy path requires full SDK + coordinator + commitment-tree — covered structurally by gate-fires-before-storage-touch assertion)
  • New sign_gate::tests unit module exercises CT helper directly.

What survived (unchanged by this rework)

  • Keyless PersistedWalletData / ClientWalletStartState (commit b9af9935)
  • A1/B/A2 schema readers (accounts::load_state, core_state::load_state, asset_locks::load_unconsumed)
  • F2 no-BIP44 silent-zero balance fix (commit 96a9aa90)
  • F4 dead post-insert wallet_id re-check removal (commit 62bd4754)
  • WrongSeedForDatabase typed error (re-homed to sign path)

Verification

  • cargo fmt --all --check: pass
  • cargo clippy -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi --all-targets -- -D warnings: pass
  • cargo check --workspace: pass
  • cargo test -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi: 414 passed, 0 failed, 7 ignored
  • cargo test --doc -p platform-wallet -p platform-wallet-storage: 3 passed, 1 ignored
  • cargo check --workspace --all-features: fails on pre-existing ShieldedChangeSet: Serialize/Deserialize gap at changeset.rs:914, verified reproducible on origin/feat/platform-wallet-rehydration@34532e57 — out of scope for this rework

🤖 Co-authored by Claudius the Magnificent AI Agent

@Claudius-Maginificent Claudius-Maginificent changed the title feat(platform-wallet): add full wallet rehydration from persistor + seed (skip-and-report) feat(platform-wallet): watch-only rehydration from persistor + wallet_id sign-gate (seedless load) May 25, 2026
@lklimek lklimek force-pushed the feat/platform-wallet-rehydration branch from 0e92cb4 to f57b117 Compare May 25, 2026 11:21
@Claudius-Maginificent Claudius-Maginificent changed the title feat(platform-wallet): watch-only rehydration from persistor + wallet_id sign-gate (seedless load) feat(platform-wallet): watch-only rehydration from persistor (seedless load) May 25, 2026
Base automatically changed from feat/platform-wallet-storage-secrets to feat/platform-wallet-sqlite-persistor May 29, 2026 11:28
@lklimek lklimek added this to the v4.0.0 milestone Jun 1, 2026
Base automatically changed from feat/platform-wallet-sqlite-persistor to v3.1-dev June 9, 2026 08:13
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.18%. Comparing base (b9e57c0) to head (83d9eff).

Additional details and impacted files
@@            Coverage Diff            @@
##           v4.1-dev    #3692   +/-   ##
=========================================
  Coverage     87.18%   87.18%           
=========================================
  Files          2632     2632           
  Lines        327563   327563           
=========================================
  Hits         285592   285592           
  Misses        41971    41971           
Components Coverage Δ
dpp 87.70% <ø> (ø)
drive 86.14% <ø> (ø)
drive-abci 89.45% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.20% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.55% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lklimek

lklimek commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review all

@thepastaclaw

thepastaclaw commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 83d9eff)

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@lklimek On it — I’ll re-review the full PR.

✅ Action performed

Full review finished.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

I verified the reported wallet rehydration findings against the PR head and surrounding call paths. Three correctness issues are in scope for this restore/storage work: the SQLite account-registration key is lossy, repeated loads collide with already-registered wallets, and the FFI load path still turns malformed account xpub rows into whole-load failures before the manager can skip them per wallet.

🔴 6 blocking | 🟡 1 suggestion(s)

Findings not posted inline (3)

These findings could not be anchored to the current diff, but they are still part of this review.

  • [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/accounts.rs:107-111: Account registration primary key collapses distinct account variants — The writer upserts account_registrations on (wallet_id, account_type, account_index), and the migration declares the same primary key. That key is narrower than key_wallet's account identity: PlatformPayment is keyed by (account, key_class), and DashPay receiving/external accounts are k...
  • [BLOCKING] packages/rs-platform-wallet/src/manager/load.rs:159-168: Repeated restore fails on wallets already in the managerload_from_persistor always calls wm.insert_wallet(wallet, platform_info) for every persisted wallet. The underlying key-wallet-manager implementation returns WalletExists when the wallet id is already registered, and this code maps that to a hard WalletCreation error that aborts the loa...
  • [BLOCKING] packages/rs-platform-wallet-ffi/src/persistence.rs:1543-1545: Malformed FFI account xpub aborts the whole restore — The FFI persister builds each WalletRestoreEntryFFI with build_wallet_start_state(entry)? before returning ClientStartState to PlatformWalletManager::load_from_persistor. Inside that helper, malformed account_xpub_bytes returns PersistenceError while decoding the account xpub, so one...
🤖 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-storage/src/sqlite/schema/accounts.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/accounts.rs:107-111: Account registration primary key collapses distinct account variants
  The writer upserts `account_registrations` on `(wallet_id, account_type, account_index)`, and the migration declares the same primary key. That key is narrower than `key_wallet`'s account identity: `PlatformPayment` is keyed by `(account, key_class)`, and DashPay receiving/external accounts are keyed by `(index, user_identity_id, friend_identity_id)`. The blob stores the full `AccountRegistrationEntry`, but inserting two such variants with the same label and numeric index overwrites the first row before `load_state` can reconstruct the manifest. A restored watch-only wallet can therefore lose accounts and their address/platform state. Persist enough discriminator columns, or an unambiguous serialized account-type key, and include it in the conflict key/order.
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/accounts.rs:107-111: Account registration primary key collapses distinct account variants
  The writer upserts `account_registrations` on `(wallet_id, account_type, account_index)`, and the migration declares the same primary key. That key is narrower than `key_wallet`'s account identity: `PlatformPayment` is keyed by `(account, key_class)`, and DashPay receiving/external accounts are keyed by `(index, user_identity_id, friend_identity_id)`. The blob stores the full `AccountRegistrationEntry`, but inserting two such variants with the same label and numeric index overwrites the first row before `load_state` can reconstruct the manifest. A restored watch-only wallet can therefore lose accounts and their address/platform state. Persist enough discriminator columns, or an unambiguous serialized account-type key, and include it in the conflict key/order.

In `packages/rs-platform-wallet/src/manager/load.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/manager/load.rs:159-168: Repeated restore fails on wallets already in the manager
  `load_from_persistor` always calls `wm.insert_wallet(wallet, platform_info)` for every persisted wallet. The underlying `key-wallet-manager` implementation returns `WalletExists` when the wallet id is already registered, and this code maps that to a hard `WalletCreation` error that aborts the load batch. A second `load_from_persistor` call on the same manager, or a restore after the same wallet has already been registered in memory, fails instead of treating the persisted wallet as already satisfied. Handle `WalletExists` or pre-check the existing wallet id so repeated restore is idempotent and still proceeds to the remaining persisted wallets.
- [BLOCKING] packages/rs-platform-wallet/src/manager/load.rs:159-168: Repeated restore fails on wallets already in the manager
  `load_from_persistor` always calls `wm.insert_wallet(wallet, platform_info)` for every persisted wallet. The underlying `key-wallet-manager` implementation returns `WalletExists` when the wallet id is already registered, and this code maps that to a hard `WalletCreation` error that aborts the load batch. A second `load_from_persistor` call on the same manager, or a restore after the same wallet has already been registered in memory, fails instead of treating the persisted wallet as already satisfied. Handle `WalletExists` or pre-check the existing wallet id so repeated restore is idempotent and still proceeds to the remaining persisted wallets.

In `packages/rs-platform-wallet-ffi/src/persistence.rs`:
- [BLOCKING] packages/rs-platform-wallet-ffi/src/persistence.rs:1543-1545: Malformed FFI account xpub aborts the whole restore
  The FFI persister builds each `WalletRestoreEntryFFI` with `build_wallet_start_state(entry)?` before returning `ClientStartState` to `PlatformWalletManager::load_from_persistor`. Inside that helper, malformed `account_xpub_bytes` returns `PersistenceError` while decoding the account xpub, so one corrupt SwiftData account row makes the entire load callback fail. That bypasses the new per-wallet skip contract documented on the manager and FFI surfaces, and prevents otherwise valid persisted wallets from loading. Convert per-entry account decode failures into a skipped wallet entry or an equivalent row-local corruption marker that the manager can report through `LoadOutcome::skipped`.
- [BLOCKING] packages/rs-platform-wallet-ffi/src/persistence.rs:1543-1545: Malformed FFI account xpub aborts the whole restore
  The FFI persister builds each `WalletRestoreEntryFFI` with `build_wallet_start_state(entry)?` before returning `ClientStartState` to `PlatformWalletManager::load_from_persistor`. Inside that helper, malformed `account_xpub_bytes` returns `PersistenceError` while decoding the account xpub, so one corrupt SwiftData account row makes the entire load callback fail. That bypasses the new per-wallet skip contract documented on the manager and FFI surfaces, and prevents otherwise valid persisted wallets from loading. Convert per-entry account decode failures into a skipped wallet entry or an equivalent row-local corruption marker that the manager can report through `LoadOutcome::skipped`.

In `packages/rs-platform-wallet/src/manager/mod.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/mod.rs:8: Rehydration internals are exposed as public API
  `pub mod rehydrate` exposes `manager::rehydrate::apply_persisted_core_state` to downstream crates even though it mutates `ManagedWalletInfo` according to load-path-specific assumptions such as first-funds-account UTXO routing and bounded address-pool probing. The only external uses are storage integration tests, so making this part of the public SDK API hardens internal restore details that should be free to change. Keep the module/function crate-private and move those cross-crate assertions behind an internal test feature or into `rs-platform-wallet` tests.

Comment thread packages/rs-platform-wallet/src/manager/mod.rs Outdated

@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

🧹 Nitpick comments (2)
packages/rs-platform-wallet/README.md (1)

95-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language tag to the architecture diagram fence.

Static analysis flags the fenced block as missing a language specifier (MD040).

🔧 Proposed fix
-```
+```text
               commands (send, register, sync, …)
🤖 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/README.md` around lines 95 - 108, The fenced
architecture diagram in the README is missing a language specifier and is
triggering MD040. Update the fenced block that contains the diagram to use the
appropriate text language tag so the diagram remains unchanged but the fence is
explicitly labeled; this is the only fix needed in the README section with the
architecture diagram.

Source: Linters/SAST tools

packages/rs-platform-wallet/tests/rehydration_load.rs (1)

276-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared corrupt-row builder to avoid duplicated literals.

The 8-field ClientWalletStartState corrupt-row literal (empty account_manifest) is repeated verbatim in rt_corrupt_row_skipped_and_other_loads and rt_z_secret_hygiene_surfaces. A small helper (e.g. fn corrupt_state() -> ClientWalletStartState) would cut duplication and the risk of the two copies drifting.

Also applies to: 346-379

🤖 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/tests/rehydration_load.rs` around lines 276 -
339, The corrupt-row `ClientWalletStartState` literal is duplicated across
`rt_corrupt_row_skipped_and_other_loads` and `rt_z_secret_hygiene_surfaces`, so
extract it into a shared helper to avoid drift. Add a small builder/helper like
`corrupt_state()` near the existing test helpers and use it in both tests
instead of repeating the 8-field struct initialization with an empty
`account_manifest`.
🤖 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/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 380-383: The load path in PlatformWalletManager leaves
lastLoadSkippedWallets stale when platform_wallet_manager_load_from_persistor
fails and loadResult.check() throws. Update the failing branch around the
loadResult/check sequence to clear lastLoadSkippedWallets before rethrowing so
the published state always reflects the most recent load attempt. Use the
existing load outcome handling in PlatformWalletManager to locate the
retry/error path.

---

Nitpick comments:
In `@packages/rs-platform-wallet/README.md`:
- Around line 95-108: The fenced architecture diagram in the README is missing a
language specifier and is triggering MD040. Update the fenced block that
contains the diagram to use the appropriate text language tag so the diagram
remains unchanged but the fence is explicitly labeled; this is the only fix
needed in the README section with the architecture diagram.

In `@packages/rs-platform-wallet/tests/rehydration_load.rs`:
- Around line 276-339: The corrupt-row `ClientWalletStartState` literal is
duplicated across `rt_corrupt_row_skipped_and_other_loads` and
`rt_z_secret_hygiene_surfaces`, so extract it into a shared helper to avoid
drift. Add a small builder/helper like `corrupt_state()` near the existing test
helpers and use it in both tests instead of repeating the 8-field struct
initialization with an empty `account_manifest`.
🪄 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: b46e26b2-2733-4046-a5c8-d7a24f463d38

📥 Commits

Reviewing files that changed from the base of the PR and between 61c9fd7 and 7a2e06e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • Cargo.toml
  • packages/rs-dpp/src/state_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet_types.rs
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
  • packages/rs-platform-wallet/README.md
  • packages/rs-platform-wallet/src/changeset/client_wallet_start_state.rs
  • packages/rs-platform-wallet/src/changeset/traits.rs
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet/src/manager/load.rs
  • packages/rs-platform-wallet/src/manager/load_outcome.rs
  • packages/rs-platform-wallet/src/manager/rehydrate.rs
  • packages/rs-platform-wallet/tests/rehydration_load.rs
  • packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs
  • packages/rs-sdk-ffi/src/signer_simple.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
✅ Files skipped from review due to trivial changes (3)
  • packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
  • packages/rs-platform-wallet/src/changeset/traits.rs
  • packages/rs-sdk-ffi/src/signer_simple.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/rs-platform-wallet/src/manager/load_outcome.rs
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet/src/manager/load.rs
  • packages/rs-platform-wallet/src/manager/rehydrate.rs

…-load errors (#3980 follow-ups) (#3984)

Co-authored-by: Quantum Explorer <quantum@dash.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>

@Claudius-Maginificent Claudius-Maginificent 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.

Inline architecture notes on the new "Persistence architecture" README section: 6 points where this wording disagrees with the standing risk assessment (Model A / gate G3 is still an open decision). Each thread is marked for human resolution — agents must not auto-resolve.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread packages/rs-platform-wallet/README.md
Comment thread packages/rs-platform-wallet/README.md

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Incremental verification vs prior review at 9dbca2f. The latest delta (9dbca2f..7a2e06e) is the #3980 follow-up: ClientWalletStartState gains a core_wallet_info: Option<Box<ManagedWalletInfo>> full-snapshot path so the FFI/iOS persister can hand across the pre-restored managed state verbatim (preserving per-account UTXO attribution and pool contents the flat projection cannot round-trip), a mark↔refill fixpoint fixes a real address-reuse bug in the wedge zone past the discovery horizon, RehydrateRowError is consolidated into CorruptKind, Swift now actually consumes/frees LoadOutcomeFFI, and new integration tests cover attribution preservation, idempotent reload, and mismatch skipping. Carried forward: the prior pub mod rehydrate; architectural suggestion is STILL VALID at HEAD — mod.rs:8 is unchanged and the only in-tree callers of rehydrate::* remain the sibling private mod load; (load.rs:147, :209). One new suggestion added by all three specialist agents: the PR description advertises a dedicated CorruptKind::SnapshotIdentityMismatch (FFI code 103 + Swift mapping) for the new snapshot/row cross-check, but that variant does not exist anywhere in the tree (verified via grep — zero hits) and the mismatch path at load.rs:175-190 still uses CorruptKind::DecodeError(format!(...)); the sonnet5-ffi-engineer also verified that both sides of the compare (info.wallet_id and expected_wallet_id) derive from the same untrusted entry.wallet_id field (persistence.rs:1546, :2893, :2901), so the added check is provably vacuous for the FFI path and provides no defense against the trust-boundary gap the PR description references.

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

🟡 2 suggestion(s)

Carried-forward findings already raised (1)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [SUGGESTION] (deduped existing open thread) packages/rs-platform-wallet/src/manager/mod.rs:8: Rehydration internals are exposed as public API with no external consumer (carried forward) — Carried forward from prior review rounds — re-verified STILL VALID at HEAD 7a2e06e (mod.rs:8 is unchanged in the latest delta). pub mod rehydrate; publishes the seedless-restore helpers build_watch_only_wallet and apply_persisted_core_state as platform_wallet::manager::rehydrate::* to...
🤖 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/manager/load.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:175-190: PR description claims a `CorruptKind::SnapshotIdentityMismatch` variant + FFI code 103 that don't exist; the snapshot/row cross-check is also vacuous for the FFI path
  The PR description states the snapshot/row mismatch case is now surfaced via a dedicated `CorruptKind::SnapshotIdentityMismatch` (FFI code 103 + Swift mapping) `replacing the previous DecodeError conflation`. Verified against HEAD 7a2e06e9: no such variant exists anywhere in the tree (grep across `rs-platform-wallet`, `rs-platform-wallet-ffi`, and `swift-sdk` returns zero matches); `CorruptKind` still has only `MissingManifest` / `MalformedXpub` / `DecodeError(String)` (`manager/load_outcome.rs:38-49`); no FFI code 103 exists in `rs-platform-wallet-ffi/src/manager.rs` (only 100/101/102/199/200); Swift's `SkippedWalletOnLoad.reasonDescription` has no case 103; and the mismatch path at `manager/load.rs:175-190` still uses `CorruptKind::DecodeError(format!(...))` — the exact conflation the PR description says was replaced. Consequences: (1) the new regression test `rt_snapshot_wallet_id_mismatch_is_skipped` can only assert the generic `CorruptKind::DecodeError(_)` wildcard, unable to distinguish this from any other decode failure; (2) downstream FFI/Swift consumers cannot render a specific message for 'snapshot describes a different wallet' vs a generic decode failure; (3) this stretches `CorruptKind::DecodeError`'s own doc contract ('any other structural decode / projection failure ... never a raw row byte slice or a hex-encoded key') by embedding `hex::encode(info.wallet_id)` in the runtime string. Additionally, for the real FFI ingestion path the cross-check is provably vacuous: both compared quantities derive from the same untrusted `entry.wallet_id` at the same call site — `expected_wallet_id` is the map key set via `out.wallets.insert(entry.wallet_id, wallet_state)` (`persistence.rs:1546`), and `info.wallet_id` is set via `ManagedWalletInfo::from_wallet(&wallet, ...)` where `wallet = Wallet::new_external_signable(network, entry.wallet_id, accounts)` (`persistence.rs:2893, :2901`), so the check compares a value to itself. Either update the PR description to reflect what actually shipped, or add the promised `CorruptKind::SnapshotIdentityMismatch { snapshot_wallet_id, expected_wallet_id, snapshot_network, expected_network }` variant with the corresponding FFI code 103 + Swift case, wire the mismatch site to it, and switch the two rehydration integration tests to the typed variant. If the intent was defense against future persister bugs (rather than against a tampered store), the docstring should say so — the current comment 'a mismatch is a corrupt row' overstates what the check actually catches on the FFI path.

In `packages/rs-platform-wallet/src/manager/mod.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/mod.rs:8: Rehydration internals are exposed as public API with no external consumer (carried forward)
  Carried forward from prior review rounds — re-verified STILL VALID at HEAD 7a2e06e9 (`mod.rs:8` is unchanged in the latest delta). `pub mod rehydrate;` publishes the seedless-restore helpers `build_watch_only_wallet` and `apply_persisted_core_state` as `platform_wallet::manager::rehydrate::*` to downstream crates (including the FFI crate boundary via `rs-platform-wallet-ffi`). These helpers mutate `ManagedWalletInfo` under load-path-specific reconstruction assumptions — including the unauthenticated-manifest trust boundary explicitly documented on `PlatformWalletPersistence::load` (`changeset/traits.rs`) and on `build_watch_only_wallet` (`manager/rehydrate.rs`), and now the projection-fallback role the new PR-boundary contract explicitly marks as transitional in `README.md`. Re-verified in-tree: the only callers of `rehydrate::*` remain `super::rehydrate::build_watch_only_wallet` (`manager/load.rs:147`) and `super::rehydrate::apply_persisted_core_state` (`manager/load.rs:209`), both from the sibling private `mod load;`. Module-private visibility (`mod rehydrate;`) suffices; keeping it `pub` freezes internal recovery mechanics — including the manifest-authentication gap and the transitional projection path — as SDK/FFI-visible API without an intentional external contract, widening the blast radius of the eventual manifest-authentication fix. Since no external caller exists, this remains a drop-in visibility downgrade.

@Claudius-Maginificent Claudius-Maginificent 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.

Reworked the open architecture threads per maintainer feedback (final-state premise): 3 remain, now as click-applyable suggestions with source-permalink evidence; the authority/MAC point is withdrawn (invariant 6 already discloses the boundary).

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread packages/rs-platform-wallet/README.md Outdated
Comment thread packages/rs-platform-wallet/README.md Outdated
Comment thread packages/rs-platform-wallet/README.md Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Incremental verification vs prior review at 7a2e06e. The latest delta (7a2e06e..5460c0e) is the #3984 follow-up: CorruptKind::SnapshotIdentityMismatch is now a real typed variant (load_outcome.rs:52) wired end-to-end through FFI code 103 (rs-platform-wallet-ffi/src/manager.rs:189, :242) and Swift case 103 (PlatformWalletManager.swift:349), the mismatch site at load.rs:172-183 constructs the typed variant, snapshot_accounts_match_manifest (load.rs:338-366) extends the cross-check with an account-set comparison, and PlatformWalletError::PersisterLoad(#[from] PersistenceError) preserves the persister's typed classification instead of a flattened string. Prior-2 is FIXED. Carried forward: prior-1 (pub mod rehydrate;) STILL VALID at HEAD (mod.rs:8 unchanged, only in-tree callers remain the sibling private mod load;). Two new suggestions from the delta: (a) the account-set half of snapshot_accounts_match_manifest is provably self-referential for the only production producer of core_wallet_info — both account_manifest (persistence.rs:3468) and the snapshot's account set (from ManagedWalletInfo::from_wallet(&wallet, ...) at persistence.rs:2901) derive from the same wallet.accounts object built once from entry.accounts (persistence.rs:2893) — so it catches manager-level wiring bugs (as the two new mock-persister integration tests demonstrate) but cannot fire against a tampered SwiftData row; (b) PlatformWalletError::PersisterLoad's transient/permanent classification is dropped at the FFI boundary because the From<PlatformWalletError> for PlatformWalletFFIResult impl (rs-platform-wallet-ffi/src/error.rs:205-244) has no dedicated arm and falls through to ErrorUnknown, so Swift's loadFromPersistor cannot distinguish retryable persister hiccups from fatal ones — undermining the stated design intent of the typed-error delta for the only cross-language caller.

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

🟡 3 suggestion(s)

Carried-forward prior findings (1)
  • [SUGGESTION] packages/rs-platform-wallet/src/manager/mod.rs:8: Rehydration internals are exposed as public API with no external consumer (STILL VALID) — Carried forward from prior review rounds — re-verified STILL VALID at HEAD 5460c0e (mod.rs:8 unchanged in the latest delta). pub mod rehydrate; publishes the seedless-restore helpers build_watch_only_wallet and apply_persisted_core_state as platform_wallet::manager::rehydrate::* to downstream crates (including the FFI crate boundary via rs-platform-wallet-ffi). These helpers mutate ManagedWalletInfo under load-path-specific reconstruction assumptions — including the unauthenticated-manifest trust boundary documented on PlatformWalletPersistence::load, build_watch_only_wallet, and now load_from_persistor itself (manager/load.rs:71-77), and the projection-fallback role the new PR-boundary contract explicitly marks as transitional. Re-verified in-tree: the only callers of rehydrate::* remain super::rehydrate::build_watch_only_wallet (manager/load.rs:142) and super::rehydrate::apply_persisted_core_state (manager/load.rs:202), both from the sibling private mod load;. Module-private visibility (mod rehydrate;) suffices; keeping it pub freezes internal recovery mechanics — including the manifest-authentication gap and the transitional projection path — as SDK/FFI-visible API without an intentional external contract. Since no external caller exists, this remains a drop-in visibility downgrade.
New findings in the latest delta (2)
  • [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:338: New account-set cross-check is a manager-level self-consistency guard, not tamper detection — worth saying so explicitly — This delta extends the snapshot/row cross-check with snapshot_accounts_match_manifest (load.rs:338-366) comparing account_manifest against the core_wallet_info snapshot's all_managed_accounts() set. For the only in-tree producer of core_wallet_info: Some(...)build_wallet_start_state in rs-platform-wallet-ffi/src/persistence.rs — both compared quantities derive from the same in-memory wallet built once in that call: account_manifest comes from wallet.accounts.all_accounts() (persistence.rs:3468-3476), and the snapshot's account set comes from ManagedWalletInfo::from_wallet(&wallet, ...) (persistence.rs:2901) where wallet = Wallet::new_external_signable(network, entry.wallet_id, accounts) (persistence.rs:2893). Both trace back to the single entry.accounts FFI array, so the check cannot fire against a tampered/corrupted SwiftData row — any tampering that alters one side alters the other identically. It does have real value: as the two new tests (rt_snapshot_account_set_mismatch_is_skipped, rt_snapshot_mismatch_skip_coexists_with_healthy_load) prove, it catches divergence in any future persister that populates the two fields via genuinely separate decode paths, or a manager-level wiring bug. But the doc comment on snapshot_accounts_match_manifest and the trust-boundary block on load_from_persistor should say explicitly that this check is a persister-implementation self-consistency guard, not a defense against a tampered store — otherwise readers will assume it closes the manifest-authentication gap the PR description already correctly tracks as a rs-platform-wallet-storage follow-up. Consider adding a one-line note that real defense against row tampering requires the tracked persisted MAC/commitment schema change.

  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205: PersisterLoad transient/permanent classification is dropped at the FFI boundary — the point of the typed-error change never reaches Swift — This delta added PlatformWalletError::PersisterLoad(#[from] PersistenceError) specifically so the persister's is_transient() / PersistenceErrorKind classification survives propagation instead of being flattened to a string, motivated by callers that would want to distinguish retryable failures. The two new integration tests (rt_persister_load_transient_error_is_typed_and_retryable, rt_persister_load_permanent_error_is_typed_and_not_retryable) confirm the Rust-side plumbing. But the only cross-language consumer today is platform_wallet_manager_load_from_persistor (rs-platform-wallet-ffi/src/manager.rs:396-419), which pipes manager.load_from_persistor()'s Result through unwrap_result_or_return!error.into() → the blanket From<PlatformWalletError> for PlatformWalletFFIResult impl at rs-platform-wallet-ffi/src/error.rs:205-244. That impl explicitly enumerates NoSpendableInputs/OnlyOutputAddressesFunded/OnlyDustInputs, WalletAlreadyExists, and the three Shielded* variants, then falls through _ => ErrorUnknown for everything else — including the new PersisterLoad. Swift's loadFromPersistor() (PlatformWalletManager.swift:377-388) receives code == ErrorUnknown plus a stringified Display and throws a generic PlatformWalletError, so the retry-vs-fatal distinction the delta was built to preserve never reaches the only caller across a language boundary. If a real product need for transient-retry doesn't exist at the Swift layer, the typed variant is purely Rust-internal and the PR description overstates its scope. If it does exist, either add a dedicated PlatformWalletFFIResultCode (e.g. ErrorPersisterLoadTransient/ErrorPersisterLoadPermanent, or a single code + boolean out-param) and mirror it in Swift, or at minimum add an explicit PersisterLoad arm to the From impl that maps to a stable-code, transient-carrying variant.

Prior findings resolved (1)
  • FIXED: PR description claims a CorruptKind::SnapshotIdentityMismatch variant + FFI code 103 that don't exist; the snapshot/row cross-check is also vacuous for the FFI path — FIXED. The typed variant CorruptKind::SnapshotIdentityMismatch is defined at packages/rs-platform-wallet/src/manager/load_outcome.rs:52 with a matching Display arm at :64-66. LOAD_SKIP_REASON_SNAPSHOT_IDENTITY_MISMATCH = 103 is declared at rs-platform-wallet-ffi/src/manager.rs:189 and mapped in skip_reason_code at :242, with the ABI wire value guarded by load_skip_reason_wire_values_are_stable (:591) and the mapping guarded by skip_reason_code_maps_known_kinds_to_constants (:611-613). Swift's SkippedWalletOnLoad.reasonDescription maps case 103 at PlatformWalletManager.swift:349. The mismatch site at manager/load.rs:172-183 now constructs the typed variant directly instead of CorruptKind::DecodeError(format!(...)) with an embedded hex-encoded wallet_id. The cross-check was also extended with a real structural snapshot_accounts_match_manifest comparison (load.rs:338-366), and two new integration tests (rt_snapshot_account_set_mismatch_is_skipped, rt_snapshot_mismatch_skip_coexists_with_healthy_load) plus the retitled rt_snapshot_wallet_id_mismatch_is_skipped assert the typed variant. The residual FFI-path vacuity concern (both sides derive from the same entry.accounts) is superseded by a new, more specific in-scope finding above and remains tracked out-of-scope for the manifest-authentication follow-up.
🤖 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/manager/mod.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/mod.rs:8: Rehydration internals are exposed as public API with no external consumer (carried forward)
  Carried forward from prior review rounds — re-verified STILL VALID at HEAD 5460c0e1 (`mod.rs:8` unchanged in the latest delta). `pub mod rehydrate;` publishes the seedless-restore helpers `build_watch_only_wallet` and `apply_persisted_core_state` as `platform_wallet::manager::rehydrate::*` to downstream crates (including the FFI crate boundary via `rs-platform-wallet-ffi`). These helpers mutate `ManagedWalletInfo` under load-path-specific reconstruction assumptions — including the unauthenticated-manifest trust boundary documented on `PlatformWalletPersistence::load`, `build_watch_only_wallet`, and now `load_from_persistor` itself (`manager/load.rs:71-77`), and the projection-fallback role the new PR-boundary contract explicitly marks as transitional. Re-verified in-tree: the only callers of `rehydrate::*` remain `super::rehydrate::build_watch_only_wallet` (`manager/load.rs:142`) and `super::rehydrate::apply_persisted_core_state` (`manager/load.rs:202`), both from the sibling private `mod load;`. Module-private visibility (`mod rehydrate;`) suffices; keeping it `pub` freezes internal recovery mechanics — including the manifest-authentication gap and the transitional projection path — as SDK/FFI-visible API without an intentional external contract. Since no external caller exists, this remains a drop-in visibility downgrade.

In `packages/rs-platform-wallet/src/manager/load.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:338: New account-set cross-check is a manager-level self-consistency guard, not tamper detection — worth saying so explicitly
  This delta extends the snapshot/row cross-check with `snapshot_accounts_match_manifest` (load.rs:338-366) comparing `account_manifest` against the `core_wallet_info` snapshot's `all_managed_accounts()` set. For the only in-tree producer of `core_wallet_info: Some(...)` — `build_wallet_start_state` in `rs-platform-wallet-ffi/src/persistence.rs` — both compared quantities derive from the same in-memory `wallet` built once in that call: `account_manifest` comes from `wallet.accounts.all_accounts()` (persistence.rs:3468-3476), and the snapshot's account set comes from `ManagedWalletInfo::from_wallet(&wallet, ...)` (persistence.rs:2901) where `wallet = Wallet::new_external_signable(network, entry.wallet_id, accounts)` (persistence.rs:2893). Both trace back to the single `entry.accounts` FFI array, so the check cannot fire against a tampered/corrupted SwiftData row — any tampering that alters one side alters the other identically. It does have real value: as the two new tests (`rt_snapshot_account_set_mismatch_is_skipped`, `rt_snapshot_mismatch_skip_coexists_with_healthy_load`) prove, it catches divergence in any future persister that populates the two fields via genuinely separate decode paths, or a manager-level wiring bug. But the doc comment on `snapshot_accounts_match_manifest` and the trust-boundary block on `load_from_persistor` should say explicitly that this check is a persister-implementation self-consistency guard, not a defense against a tampered store — otherwise readers will assume it closes the manifest-authentication gap the PR description already correctly tracks as a `rs-platform-wallet-storage` follow-up. Consider adding a one-line note that real defense against row tampering requires the tracked persisted MAC/commitment schema change.

In `packages/rs-platform-wallet-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205: `PersisterLoad` transient/permanent classification is dropped at the FFI boundary — the point of the typed-error change never reaches Swift
  This delta added `PlatformWalletError::PersisterLoad(#[from] PersistenceError)` specifically so the persister's `is_transient()` / `PersistenceErrorKind` classification survives propagation instead of being flattened to a string, motivated by callers that would want to distinguish retryable failures. The two new integration tests (`rt_persister_load_transient_error_is_typed_and_retryable`, `rt_persister_load_permanent_error_is_typed_and_not_retryable`) confirm the Rust-side plumbing. But the only cross-language consumer today is `platform_wallet_manager_load_from_persistor` (`rs-platform-wallet-ffi/src/manager.rs:396-419`), which pipes `manager.load_from_persistor()`'s `Result` through `unwrap_result_or_return!` → `error.into()` → the blanket `From<PlatformWalletError> for PlatformWalletFFIResult` impl at `rs-platform-wallet-ffi/src/error.rs:205-244`. That impl explicitly enumerates `NoSpendableInputs`/`OnlyOutputAddressesFunded`/`OnlyDustInputs`, `WalletAlreadyExists`, and the three `Shielded*` variants, then falls through `_ => ErrorUnknown` for everything else — including the new `PersisterLoad`. Swift's `loadFromPersistor()` (`PlatformWalletManager.swift:377-388`) receives `code == ErrorUnknown` plus a stringified `Display` and throws a generic `PlatformWalletError`, so the retry-vs-fatal distinction the delta was built to preserve never reaches the only caller across a language boundary. If a real product need for transient-retry doesn't exist at the Swift layer, the typed variant is purely Rust-internal and the PR description overstates its scope. If it does exist, either add a dedicated `PlatformWalletFFIResultCode` (e.g. `ErrorPersisterLoadTransient`/`ErrorPersisterLoadPermanent`, or a single code + boolean out-param) and mirror it in Swift, or at minimum add an explicit `PersisterLoad` arm to the `From` impl that maps to a stable-code, transient-carrying variant.

Comment thread packages/rs-platform-wallet/src/manager/load.rs
Co-authored-by: Claudius the Magnificent AI, on behalf of lklimek <8431764+Claudius-Maginificent@users.noreply.github.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

The 5460c0e..8bcb192 delta is README-only (wording clarifications about read-model semantics and the single-writer invariant), so no new code-level findings arise from the latest delta. Reconciling the three prior findings from history/5460c0e1: all three are re-verified STILL_VALID at HEAD — pub mod rehydrate; at manager/mod.rs:8 unchanged, snapshot_accounts_match_manifest at manager/load.rs:325-366 unchanged with the same doc block, and the From<PlatformWalletError> for PlatformWalletFFIResult impl at rs-platform-wallet-ffi/src/error.rs:205-244 unchanged with PersisterLoad still falling through to ErrorUnknown at line 240. Carried forward as suggestions; no in-scope blockers.

🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: `PersisterLoad` transient/permanent classification is dropped at the FFI boundary
  Re-verified STILL VALID at HEAD 8bcb1929 (latest delta is README-only; the impl and match arms at error.rs:205-244 are unchanged). The prior delta added `PlatformWalletError::PersisterLoad(#[from] PersistenceError)` specifically so `PersistenceError::is_transient()` / `PersistenceErrorKind` survives propagation instead of being flattened to a string, and the Rust-side plumbing is exercised by `rt_persister_load_transient_error_is_typed_and_retryable` / `rt_persister_load_permanent_error_is_typed_and_not_retryable`. But the only cross-language caller today, `platform_wallet_manager_load_from_persistor` in `packages/rs-platform-wallet-ffi/src/manager.rs`, pipes the error through `unwrap_result_or_return!` → `error.into()` → the blanket `From<PlatformWalletError> for PlatformWalletFFIResult` impl, which explicitly enumerates `NoSpendableInputs`/`OnlyOutputAddressesFunded`/`OnlyDustInputs`, `WalletAlreadyExists`, and the three `Shielded*` variants, then falls through `_ => ErrorUnknown` at line 240 for everything else — including `PersisterLoad`. Swift's `loadFromPersistor()` receives `code == ErrorUnknown` plus a stringified `Display` and cannot distinguish a retryable persister hiccup from a fatal one. Either add dedicated `ErrorPersisterLoadTransient` / `ErrorPersisterLoadPermanent` codes (or a single code plus a `bool transient` out-param) with a Swift-side mirror, or downscope the PR description to note the typed variant is currently Rust-internal only.

In `packages/rs-platform-wallet/src/manager/load.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:325-366: Snapshot/manifest cross-check doc reads as tamper detection but is a self-consistency guard
  Re-verified STILL VALID at HEAD 8bcb1929 — `snapshot_accounts_match_manifest` (load.rs:338-366) and its doc block (load.rs:325-337) are unchanged. For the only in-tree producer of `core_wallet_info: Some(...)` — `build_wallet_start_state` in `rs-platform-wallet-ffi/src/persistence.rs` — both `account_manifest` (from `wallet.accounts.all_accounts()`) and the snapshot's account set (from `ManagedWalletInfo::from_wallet(&wallet, ...)`) derive from the same in-memory `wallet` built once from a single `entry.accounts` FFI array. Any tampering on the persisted row affects both sides identically, so this check cannot fire against a tampered SwiftData row; it catches persister wiring bugs and would catch a future persister that decodes the two fields via genuinely separate paths. The current doc frames the check as guarding against 'a different wallet [that] must not be consumed', which reads as tamper defense and undermines the PR's own tracking of the unauthenticated-manifest gap. One line clarifying that this is a persister-implementation self-consistency guard — and that real defense against row tampering requires the tracked `rs-platform-wallet-storage` MAC/commitment change — would prevent over-trust.

Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, 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: rust-quality, ffi-engineer

Comment thread packages/rs-platform-wallet/src/manager/load.rs
Comment thread packages/rs-dpp/src/state_transition/mod.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/manager.rs
Comment thread packages/rs-platform-wallet-ffi/src/persistence.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/persistence.rs Outdated
Comment thread packages/rs-platform-wallet/src/changeset/traits.rs
Comment thread packages/rs-platform-wallet/src/manager/load.rs Outdated
Comment thread packages/rs-platform-wallet/src/manager/load.rs
Comment thread packages/rs-platform-wallet/src/manager/load.rs
Comment thread packages/rs-platform-wallet/src/manager/load.rs Outdated
lklimek and others added 8 commits July 3, 2026 13:10
…ojection fallback

ClientWalletStartState now always carries a full ManagedWalletInfo
snapshot. Make core_wallet_info non-optional (Box<ManagedWalletInfo>)
and remove the core_state / used_core_addresses projection fields.

The FFI/iOS persister already always supplied a snapshot, so the None
branch in load_from_persistor and the projection-replay path
(apply_persisted_core_state / extend_pools_for_restored_addresses in
rehydrate) were dead in production. Remove them and the unit tests that
exercised only that removed logic. build_watch_only_wallet and its tests
stay — they still feed the snapshot path.

BREAKING CHANGE: ClientWalletStartState loses core_state and
used_core_addresses; core_wallet_info is no longer Option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pensator

The storage layer now resolves per-UTXO account attribution from the
core_derived_addresses table (address→account_index lookup over the
addresses_derived delta), so core_utxos.account_index is no longer
hardcoded to 0. Remove warn_if_non_default_account, non_default_account_index,
and the inline aggregated-warn block in the BlockProcessed arm — the real
index is already threaded through addresses_derived, which both event arms
forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d as skip

Turn LoadOutcome from a struct into a 3-state enum — Loaded (full),
Partial (some loaded, some skipped), NoneUsable (rows present, all
skipped) — so callers can tell a clean load from one that left wallets
behind. Add `impl From<LoadOutcome> for Result<LoadOutcome, _>` (partial
and nothing-usable map to the new PlatformWalletError::LoadIncomplete)
per reviewer request, plus loaded()/skipped() accessors.

An already-registered wallet (idempotency check, or WalletExists at
insert) is now reported as a SkipReason::AlreadyRegistered skip instead
of being silently folded into loaded, so the caller can distinguish "not
freshly loaded" from a genuine load.

FFI: adapt manager.rs to the accessor API and add reason code 300
(already-registered); LoadOutcomeFFI count pair encodes the 3-state.

Swift: fix stale-skip-state bug — assign lastLoadSkippedWallets before
the throwing check() so a failed load clears prior skip state; map
reason code 300.

BREAKING CHANGE: LoadOutcome is now an enum; its loaded/skipped fields
become accessor methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tack

Condense the load() trust-boundary note in traits.rs: drop the "known,
currently-unaddressed gap" framing and state plainly that manifest-
integrity verification is the storage layer's responsibility, out of this
trait's scope. Point to the platform-wallet-storage manifest-integrity
work rather than re-explaining the mechanism.

Expand build_watch_only_wallet's doc with the concrete key-substitution
attack: a tampered/untrusted-backup store swaps a valid account_xpub
while keeping expected_wallet_id, so the rebuilt wallet derives receive
addresses from the attacker's key under the original id and silently
redirects incoming funds. State that this crate does not defend against
it and cross-reference the traits.rs note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…doc cleanups

- slice_from_raw: reject a host-supplied len > isize::MAX (from_raw_parts
  bound) with an empty slice instead of risking UB, mirroring
  decode_cmx_array's guard.
- build_wallet_start_state: wrap the Account::from_xpub failure in the
  MalformedXpubError marker so it classifies as MalformedXpub (101) like
  the sibling bincode-decode step, not the generic decode error (102).
- Replace an iOS-specific "SwiftData" mention with technology-agnostic
  "corrupt persisted row" wording.
- Drop the "(SECRETS.md: ...)" reference on the keyless-handoff comment
  and strengthen the invariant, naming private keys, unencrypted seeds,
  and passwords as excluded material.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ressions

- hard_fail_rolls_back_already_loaded_healthy_wallet (in-crate, since the
  mid-batch hard-fail needs pub(crate) platform-address types + bimap):
  a platform-address restore failure on a later wallet must evict an
  already-inserted healthy wallet from both self.wallets and
  wallet_manager and return Err. Nothing covered this before.
- rt_empty_first_run_is_clean_with_no_handler_calls: a fresh install
  (default ClientStartState, no rows) loads as LoadOutcome::Loaded with
  empty loaded/skipped and zero skip-handler calls.
- rt_concurrent_loads_register_each_wallet_exactly_once: two concurrent
  load passes over a shared multi-wallet snapshot both succeed, each
  wallet registers exactly once, and overlaps surface as
  AlreadyRegistered skips (exercising the check-then-act idempotency
  window).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ydration

Applies all 23 triaged review decisions (22 fix + 1 false_positive) from the
consolidated PR #3692 review report, implemented by Bilby and QA'd clean by
Marvin/Smythe/Adams (0 MEDIUM+ findings).
…let-rehydration

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs
#	packages/rs-sdk-ffi/src/signer_simple.rs

@lklimek lklimek 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.

minor LOWs to address

@lklimek lklimek requested a review from thepastaclaw July 3, 2026 14:33

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Cumulative re-review at head 14d83af. Latest delta cleanly removes the projection-fallback path, tightens LoadOutcome to a 3-state enum with a new AlreadyRegistered skip reason, adds an integer-overflow guard on the FFI slice helper, and typed-classifies malformed xpub failures — all backed by new rollback/idempotency/concurrency tests. Carried-forward prior findings: PersisterLoad FFI classification (STILL_VALID) and pub mod rehydrate (STILL_VALID). Fixed: prior snapshot/manifest doc framing (now explicitly a self-consistency check). New latent issue in the delta: the new From<LoadOutcome> for Result conversion routes AlreadyRegistered-only skips through LoadIncomplete, which turns idempotent repeat loads into Err for any caller using ? — currently unused, but the doc comment on LoadOutcome (load_outcome.rs:91-95) actively recommends this conversion.

Source: reviewers opus/opus, sonnet5/claude-sonnet-5, codex/gpt-5.5[high] (failed: codex/gpt-5.5[high]:general, codex/gpt-5.5[high]:rust-quality, codex/gpt-5.5[high]:ffi-engineer) for general + rust-quality + ffi-engineer; verifier opus/opus.

🟡 2 suggestion(s) | 💬 1 nitpick(s)

Carried-forward prior findings (2)
  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: STILL_VALID: PersisterLoad transient/permanent classification is dropped at the FFI boundary — Carried forward from the prior review (byte-identical at 14d83af). PlatformWalletError::PersisterLoad(PersistenceError) deliberately preserves is_transient() / PersistenceErrorKind — this PR even added rt_persister_load_transient_error_is_typed_and_retryable / ..._permanent_... to pin that contract in Rust. But the blanket impl From<PlatformWalletError> for PlatformWalletFFIResult has no arm for it, so it falls through to _ => ErrorUnknown. Since platform_wallet_manager_load_from_persistor is the primary Rust caller that surfaces PersisterLoad to Swift, the transient-vs-fatal signal a host would need to drive an app-launch retry loop (e.g. SQLite BUSY vs decode failure) collapses into a single opaque code with only the Display string to inspect — the internal test coverage of the classification has no consumer at the boundary. The new LoadIncomplete variant falls...

  • [NITPICK] packages/rs-platform-wallet/src/manager/mod.rs:8: STILL_VALID: pub mod rehydrate exposes no public items — Carried forward from the prior review (unchanged at 14d83af). After the projection-fallback removal, rehydrate.rs contains exactly one non-test item, pub(super) fn build_watch_only_wallet, plus tests. mod.rs:8 still declares it pub mod rehydrate;, so external crates can use platform_wallet::manager::rehydrate; but reach nothing inside — the module contributes an empty entry to the crate's public rustdoc surface. Neighbours mod load; and mod wallet_lifecycle; are already private; matching them here keeps behaviour identical while removing the vestigial visibility.

New findings in the latest delta (1)
  • [SUGGESTION] packages/rs-platform-wallet/src/manager/load_outcome.rs:149-166: From<LoadOutcome> for Result misclassifies idempotent AlreadyRegistered-only skips as LoadIncompletefrom_parts routes any loaded.is_empty() && !skipped.is_empty() outcome into NoneUsable, and the new From<LoadOutcome> for Result impl unconditionally converts that (and Partial) into Err(PlatformWalletError::LoadIncomplete). That collapses two very different situations into the same failure: (a) real corruption (CorruptPersistedRow) and (b) a fully-successful idempotent repeat where every skip is AlreadyRegistered — the exact case the new rt_idempotent_repeat_restore test pins as expected behaviour, and the case load.rs's own doc frames as the steady-state repeat-load path. This mismatch is amplified by LoadOutcome's own rustdoc (load_outcome.rs:91-95), which actively recommends Result::from/.into() "when an incomplete load should read as a failure" — the first Rust caller who takes that advice will see every steady-state repeat load reported as Err. Nothing...
Prior findings resolved (1)
  • Snapshot/manifest cross-check doc reads as tamper detection but is a self-consistency guard — FIXED at 14d83af. The rustdoc on snapshot_accounts_match_manifest (load.rs:310-317) now explicitly says 'A self-consistency check between two pieces of the same persisted row, not an authenticity guard' and points to the real trust boundary in build_watch_only_wallet. Directly addresses the prior framing concern.
🤖 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/manager/load_outcome.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load_outcome.rs:149-166: `From<LoadOutcome> for Result` misclassifies idempotent `AlreadyRegistered`-only skips as `LoadIncomplete`
  `from_parts` routes any `loaded.is_empty() && !skipped.is_empty()` outcome into `NoneUsable`, and the new `From<LoadOutcome> for Result` impl unconditionally converts that (and `Partial`) into `Err(PlatformWalletError::LoadIncomplete)`. That collapses two very different situations into the same failure: (a) real corruption (`CorruptPersistedRow`) and (b) a fully-successful idempotent repeat where every skip is `AlreadyRegistered` — the exact case the new `rt_idempotent_repeat_restore` test pins as expected behaviour, and the case `load.rs`'s own doc frames as the steady-state repeat-load path. This mismatch is amplified by `LoadOutcome`'s own rustdoc (load_outcome.rs:91-95), which actively recommends `Result::from`/`.into()` "when an incomplete load should read as a failure" — the first Rust caller who takes that advice will see every steady-state repeat load reported as `Err`. Nothing in-tree calls the conversion yet (the FFI reads `.loaded()`/`.skipped()` directly), so this is latent rather than actively breaking today, but it's new API added in this PR. Either treat an outcome whose skips are all `AlreadyRegistered` as `Ok` (arguably it's a successful no-op, not incomplete), or drop the blanket `From` impl and `LoadIncomplete` variant until a real caller needs `?`-based incomplete-load handling that distinguishes corruption from idempotency.

In `packages/rs-platform-wallet-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: STILL_VALID: `PersisterLoad` transient/permanent classification is dropped at the FFI boundary
  Carried forward from the prior review (byte-identical at 14d83af). `PlatformWalletError::PersisterLoad(PersistenceError)` deliberately preserves `is_transient()` / `PersistenceErrorKind` — this PR even added `rt_persister_load_transient_error_is_typed_and_retryable` / `..._permanent_...` to pin that contract in Rust. But the blanket `impl From<PlatformWalletError> for PlatformWalletFFIResult` has no arm for it, so it falls through to `_ => ErrorUnknown`. Since `platform_wallet_manager_load_from_persistor` is the primary Rust caller that surfaces `PersisterLoad` to Swift, the transient-vs-fatal signal a host would need to drive an app-launch retry loop (e.g. SQLite BUSY vs decode failure) collapses into a single opaque code with only the `Display` string to inspect — the internal test coverage of the classification has no consumer at the boundary. The new `LoadIncomplete` variant falls into the same catch-all (not currently reachable via any FFI entry point, but it widens the same gap). Add a dedicated `PlatformWalletFFIResultCode` for `PersisterLoad` and map it (or add an accessor for the underlying `is_transient()` bit) so the fidelity survives the crossing.

In `packages/rs-platform-wallet/src/manager/mod.rs`:
- [NITPICK] packages/rs-platform-wallet/src/manager/mod.rs:8: STILL_VALID: `pub mod rehydrate` exposes no public items
  Carried forward from the prior review (unchanged at 14d83af). After the projection-fallback removal, `rehydrate.rs` contains exactly one non-test item, `pub(super) fn build_watch_only_wallet`, plus tests. `mod.rs:8` still declares it `pub mod rehydrate;`, so external crates can `use platform_wallet::manager::rehydrate;` but reach nothing inside — the module contributes an empty entry to the crate's public rustdoc surface. Neighbours `mod load;` and `mod wallet_lifecycle;` are already private; matching them here keeps behaviour identical while removing the vestigial visibility.

Comment thread packages/rs-platform-wallet/src/manager/load_outcome.rs Outdated
Comment thread packages/rs-platform-wallet/src/manager/mod.rs Outdated
lklimek and others added 4 commits July 3, 2026 14:49
…malformed-xpub

Add a unit test for slice_from_raw's isize::MAX overflow guard, asserting a
non-null pointer with an over-bound length yields an empty slice without a
dereference.

Add an end-to-end test that drives build_wallet_start_state with a well-formed
buffer that is not a decodable ExtendedPubKey and asserts the failure classifies
as CorruptKind::MalformedXpub, covering the reclassification at its real call
site rather than only the classification helper in isolation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dead rehydration variants

Remove PlatformWalletError::LoadIncomplete and the
`From<LoadOutcome> for Result<LoadOutcome, PlatformWalletError>` impl: nothing
in-tree drives the `.into()`/`?` conversion — callers read LoadOutcome via
loaded()/skipped() directly — so the impl was confusing latent API surface that
also erased the skip-reason distinction between a harmless already-registered
repeat and genuine corruption. Reintroduce a typed incomplete-load error only
when a real caller needs it. LoadOutcome/load rustdoc now point callers at the
skip reasons instead of the removed conversion.

Also delete the three dead rehydration error variants
(RehydrationTopologyUnsupported, RehydrationPoolMismatch,
RehydrationPoolTypeMismatch) — no constructors remain in the workspace — and the
AddressPoolType import they were the only users of.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The module's only non-test item is `pub(super) fn build_watch_only_wallet`, used
solely from `manager::load`, so it exposes nothing externally usable. Narrow the
declaration to `mod rehydrate;`, matching the private sibling `mod load;` /
`mod wallet_lifecycle;`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, latent API removal)

Fixes the 5 LOW findings from Phase 3 QA plus a bot-flagged module-visibility
nit, all triaged by the maintainer. Removes the latent LoadIncomplete
conversion instead of documenting it, per an independent bot review raising
the same idempotent/corruption collapse concern.
lklimek
lklimek previously approved these changes Jul 3, 2026
…e afcff156

Three FixedKeySigner test mocks implemented key_wallet::signer::Signer's
extended_public_key, which afcff156 moved into a separate
ExtendedPubKeySigner subtrait — the same break already fixed in
rs-sdk-ffi's mnemonic_resolver_core_signer.rs, missed here because the
post-merge verification was scoped to rs-platform-wallet/-ffi/rs-sdk-ffi
and never ran a workspace-wide check. Nothing in rs-dpp calls
.extended_public_key() on these mocks or requires ExtendedPubKeySigner,
so the method is dead weight, not a missing impl — deleted outright
along with the now-unused ExtendedPubKey imports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Cumulative re-review at head f8c9722 reconciling the three prior findings from 14d83af. Two prior findings are FIXED by outright removal in this delta: prior-1 (From<LoadOutcome> for Result misclassification) — the impl and LoadIncomplete variant are deleted with no remaining workspace references; and prior-3 (pub mod rehydrate) — demoted to private mod rehydrate; at manager/mod.rs:8. Prior-2 (PersisterLoad transient/permanent classification dropped at the FFI boundary) is carried forward STILL_VALID: packages/rs-platform-wallet-ffi/src/error.rs:205-244 was untouched, and the blanket From<PlatformWalletError> for PlatformWalletFFIResult still routes every PersisterLoad(_) through _ => ErrorUnknown at line 240 despite the Rust-side error preserving is_transient(). No new latest-delta findings surfaced from any reviewer; the delta's two added FFI tests (slice_from_raw overflow guard, MalformedXpub end-to-end classification) are appropriately scoped regressions.

🟡 1 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-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: `PersisterLoad` transient/permanent classification is dropped at the FFI boundary
  Carried forward from the prior review — `packages/rs-platform-wallet-ffi/src/error.rs` was not touched by the 14d83af1..f8c9722c delta, verified byte-identical at head.

  `PlatformWalletError::PersisterLoad(#[from] PersistenceError)` deliberately preserves `PersistenceError::is_transient()` and `PersistenceErrorKind` — the crate has dedicated tests (`rt_persister_load_transient_error_is_typed_and_retryable` / `..._permanent_...`) pinning that contract on the Rust side. But the blanket `impl From<PlatformWalletError> for PlatformWalletFFIResult` at lines 205-244 explicitly enumerates only `NoSpendableInputs`/`OnlyOutputAddressesFunded`/`OnlyDustInputs`, `WalletAlreadyExists`, and the three `Shielded*` variants; everything else — including `PersisterLoad(_)` — falls through the `_ => ErrorUnknown` catch-all at line 240.

  `platform_wallet_manager_load_from_persistor` is the primary Rust caller that can surface `PersisterLoad` to Swift via this blanket conversion. A host that wants to distinguish a retryable persister hiccup (SQLite BUSY, iOS keychain locked at launch) from a permanent decode failure has no typed signal to branch on — only the rendered `Display` string, which is not a stable contract. The typed retry classification the Rust side goes out of its way to preserve therefore never reaches the host, at exactly the boundary that motivated the seedless-load design.

  Suggested fix: add a dedicated `PlatformWalletFFIResultCode::ErrorPersisterLoad` (or split `ErrorPersisterLoadTransient` / `ErrorPersisterLoadPermanent`) with an explicit match arm inspecting `err.is_transient()`, or expose the `is_transient()` bit through a separate FFI accessor so the classification survives the crossing.

Source: reviewers: opus (general), claude-sonnet-5 (general, failed_or_unparseable), gpt-5.5[high] (general, 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: rust-quality, ffi-engineer

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Cumulative re-review at head 83d9eff. Latest delta (f8c9722..83d9eff) is a mechanical cleanup: three test-only FixedKeySigner impls in rs-dpp lose the obsolete extended_public_key method (and its unused ExtendedPubKey import) after the pinned rust-dashcore afcff156 split extended_public_key into a separate ExtendedPubKeySigner supertrait. That change is correct, test-only, and does not touch any FFI/consensus surface — no new findings. The single carried-forward finding (prior-1: PersisterLoad transient/permanent classification collapsed to ErrorUnknown at the FFI boundary) is re-verified STILL_VALID: packages/rs-platform-wallet-ffi/src/error.rs:205-244 is unchanged, and PlatformWalletError::PersisterLoad(#[from] PersistenceError) still exists at packages/rs-platform-wallet/src/error.rs:23, so the typed retryable/permanent signal never crosses to the Swift loadFromPersistor() caller.

Source: reviewers opus/opus, sonnet5/claude-sonnet-5, codex/gpt-5.5[high] (failed: codex/gpt-5.5[high]:general, codex/gpt-5.5[high]:rust-quality, codex/gpt-5.5[high]:ffi-engineer) for general + rust-quality + ffi-engineer; verifier opus/opus.

🟡 1 suggestion(s)

Carried-forward prior findings (1)
  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: STILL_VALID: PersisterLoad transient/permanent classification is dropped at the FFI boundary — Re-verified at head 83d9effc: this file is byte-identical to the prior head, while PlatformWalletError::PersisterLoad(#[from] PersistenceError) still preserves PersistenceError::is_transient() on the Rust side and still falls through the blanket FFI conversion's _ => ErrorUnknown arm. The latest delta only touched rs-dpp signer test stubs, so there are no new latest-delta findings.
🤖 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-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:205-244: Carried forward (STILL_VALID): PersisterLoad transient/permanent classification is dropped at the FFI boundary
  Re-verified at head 83d9effc — this file is byte-identical to the prior head (the f8c9722c..83d9effc delta only touched rs-dpp signer test stubs).

  `PlatformWalletError::PersisterLoad(#[from] PersistenceError)` at `packages/rs-platform-wallet/src/error.rs:23` deliberately preserves `PersistenceError::is_transient()` / `PersistenceErrorKind` so a `SQLITE_BUSY` hiccup or an iOS keychain-locked-at-launch race stays distinguishable from a permanent decode failure. The Rust side pins that contract with dedicated tests (`rt_persister_load_transient_error_is_typed_and_retryable` / `..._permanent_...` in `packages/rs-platform-wallet/tests/rehydration_load.rs`).

  But the blanket `impl From<PlatformWalletError> for PlatformWalletFFIResult` at lines 205-244 only special-cases `NoSpendableInputs` / `OnlyOutputAddressesFunded` / `OnlyDustInputs`, `WalletAlreadyExists`, and the three `Shielded*` variants; `PersisterLoad(_)` falls through the `_ => ErrorUnknown` catch-all at line 240. `platform_wallet_manager_load_from_persistor` is the primary caller that surfaces `PersisterLoad` to Swift via this blanket conversion, so a Swift host receives `code == ErrorUnknown` plus only a `Display` string — the retry-vs-fatal signal never crosses the boundary the seedless-load design was built for.

  Fix options: (a) add dedicated `PlatformWalletFFIResultCode::ErrorPersisterLoadTransient` / `ErrorPersisterLoadPermanent` variants (or a single `ErrorPersisterLoad` with a `bool transient` out-param) and a match arm here that inspects `err.is_transient()`, mirrored on the Swift side; or (b) if there is no near-term Swift consumer, downscope the typed variant in the PR description as Rust-internal only. Either shape closes the gap without disturbing existing arms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants