fix(platform-wallet): snapshot identity cross-check + typed persister-load errors (#3980 follow-ups)#3984
Conversation
…s rehydration Review follow-ups for the watch-only rehydration PR: - Carry the FFI persister's fully-restored ManagedWalletInfo across load() as ClientWalletStartState::core_wallet_info (still keyless: no Wallet, no seed). The manager consumes the snapshot directly, so per-account UTXO/tx-record attribution, exact pool contents (derived-but-unused deep addresses stay in the SPV watch set), and per-index used flags survive a restart verbatim - and the second eager gap-window derivation on the launch path disappears. The core_state/used_core_addresses projection remains as the fallback for persisters that cannot reconstruct a snapshot (SQLite until #3968). - Run the rehydration mark<->refill loop to a fixpoint so a previously used address in the wedge zone (past the discovery horizon, within gap-refill reach) is re-marked used instead of coming back pool-visible as fresh (address-reuse leak; regression test added). - Hoist the load_from_persistor idempotency check ahead of per-wallet reconstruction so repeat loads skip the rebuild instead of discarding it at the WalletExists arm; validate a carried snapshot's wallet_id/network against its row and skip mismatches as corrupt. - Consume LoadOutcomeFFI in Swift loadFromPersistor: skipped wallets are logged, surfaced via lastLoadSkippedWallets, excluded from the get_wallet loop (no more spurious NotFound in lastError), and the outcome is freed via platform_wallet_load_outcome_free. - Drop RehydrateRowError (variant-for-variant mirror of CorruptKind); build_watch_only_wallet returns CorruptKind directly. - Reuse vec_to_ptr for the FFI skipped array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the agreed model: platform-wallet is the single writer of wallet-state tables and the authority for state transitions; the persisted store is the durable history and the client's read model (display never blocks on load/unlock); the store schema is a versioned public contract; load() consumes the store verbatim (no re-derivation); persist errors are hard errors because local-only state (manifest, used-flags, metadata) is unrecoverable by rescan; load is seedless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…entityMismatch A carried ManagedWalletInfo snapshot is now cross-checked against its persisted row on two axes instead of one: - wallet_id/network mismatch previously reused CorruptKind::DecodeError, conflating an unreadable row with a well-formed but wrong-row snapshot. It now maps to a dedicated CorruptKind::SnapshotIdentityMismatch. - the snapshot's account set is validated against the row's account manifest (the oracle that builds the watch-only wallet). A snapshot describing a different topology is skipped rather than consumed. The comparison ignores the account families the two enumerations structurally differ on (BLS/EdDSA provider keys vs PlatformPayment) so a legitimate snapshot is never rejected. Both surface through the existing per-row skip machinery (no panic, batch continues). The new variant gets FFI wire code 103 (LOAD_SKIP_REASON_SNAPSHOT_IDENTITY_MISMATCH); Swift reasonDescription maps 103 and cites the wire-contract source, as the constants are not exposed as named symbols in the generated C header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2po24WxgfDKoWP61ueG2Q
…hy load Adds a combined-batch case where a SnapshotIdentityMismatch skip and a successful snapshot load occur in the same load_from_persistor pass: the healthy row loads, the mismatched row is skipped and its handler fires exactly once, and the batch returns Ok. Mirrors the existing corrupt-manifest + healthy-load combined test for the snapshot path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2po24WxgfDKoWP61ueG2Q
load_from_persistor wrapped every persister load failure as WalletCreation(String), flattening the backend's typed error and losing its retry classification. It now propagates a dedicated PersisterLoad variant carrying the PersistenceError verbatim, so callers keep is_transient() / PersistenceErrorKind and can distinguish a transient backend hiccup (retry) from a permanent failure (do not retry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2po24WxgfDKoWP61ueG2Q
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 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 |
|
✅ Review complete (commit 40806d6) |
…ation) PR #3980 was squash-merged into feat/platform-wallet-rehydration; the squash is byte-identical tree-wide to this branch's fork point, so the only net delta preserved here is the 3 WS-A follow-up commits. Conflicts (all in files WS-A intentionally supersedes) resolved in favour of the WS-A changes: - manager/load.rs: SnapshotIdentityMismatch + account-set cross-check over the original DecodeError-only snapshot check. - tests/rehydration_load.rs: new-variant assertions + the added account-mismatch, combined-batch, and typed-persister-load tests. - swift PlatformWalletManager.swift: reasonCode 103 mapping + wire-contract citation. No semantic overlap: the squash added nothing this branch lacked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2po24WxgfDKoWP61ueG2Q
There was a problem hiding this comment.
Code Review
PR #3984 correctly implements the two stated scope items: a snapshot-vs-manifest cross-check surfaced as CorruptKind::SnapshotIdentityMismatch with wire code 103 propagated end-to-end, and typed PersistenceError preservation through load_from_persistor via #[from]. Verified against 161d8b8: the identity check at load.rs:172-183, the filtered comparison at load.rs:338-366, and the new integration tests all check out; FFI ownership and Swift defer-based free are correct. Four in-scope observations (all suggestion/nitpick) around defense-in-depth of the account-set comparison, diagnostic detail lost by collapsing three failure modes into one dataless variant, LoadOutcome::loaded conflating idempotent-skip with fresh-load, and Swift-side hardcoded wire codes.
Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, 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: security-auditor, rust-quality, ffi-engineer
🟡 2 suggestion(s) | 💬 2 nitpick(s)
Findings not posted inline (2)
These findings could not be anchored to the current diff, but they are still part of this review.
- [NITPICK]
packages/rs-platform-wallet/src/manager/load.rs:124-136: Idempotent-skip path is indistinguishable from fresh-load inLoadOutcome::loaded— The idempotency fast-path (lines 130-135) pushesexpected_wallet_idontooutcome.loadedwhen a wallet is already registered, identical to the fresh-reconstruction push at end of loop.LoadOutcome::loaded's doc comment only says "Wallets fully reconstructed and registered, in load order" — i... - [NITPICK]
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift:341-356: SwiftreasonDescriptionhardcodes numeric wire codes with no compile-time tie-back to Rust —reasonDescriptionmatches integer literals100/101/102/103/199/200against Rust-sideLOAD_SKIP_REASON_*pub const u32values that never surface as named symbols in the generated C header. The Rust FFI has a unit-test pinning the numeric contract on its side, but nothing prevents...
🤖 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:338-366: Snapshot/manifest cross-check has defense-in-depth gaps in filtered families and empty-set case
`snapshot_accounts_match_manifest` compares only the set of `AccountType` variants after filtering out `ProviderOperatorKeys`, `ProviderPlatformKeys`, and `PlatformPayment { .. }` (line 345-352). The filter is justified — those are the known enumeration asymmetries between `Wallet::all_accounts` and `ManagedWalletInfo::all_managed_accounts` — but three residual gaps remain in the very scenario this function was added to defend against (a wrong-row / tampered snapshot):
1. **xpubs are not compared.** `info.wallet_id` is a persisted field, not a re-derivation. A row with matching `wallet_id`/`network`/account-type set but attacker-controlled per-account xpubs will pass, and `wallet_info = *info` then consumes those pools into the SPV watch set while `build_watch_only_wallet` uses the manifest's xpubs — divergent detection/attribution surfaces without any signing-side failure.
2. **Asymmetric filtering hides side-specific extras.** `PlatformPayment` is filtered on the manifest side (where it can appear) and `ProviderOperatorKeys`/`ProviderPlatformKeys` on the snapshot side (where they can appear). A snapshot with an *extra* `ProviderOperatorKeys` the manifest wouldn't attest to, or a manifest with an *extra* `PlatformPayment` the snapshot wouldn't attest to, is invisible because each is filtered on exactly the side that would have caught it.
3. **Vacuous empty-set match.** If a row's manifest reduces to only filtered families (e.g. a single `PlatformPayment` entry — `build_watch_only_wallet` only requires non-empty) and the snapshot's comparable set is also empty, `∅ == ∅` returns `true` and any wrong-row snapshot with `wallet_id`/`network` set to match sails through. Unlikely on real wallets (which always carry a Standard account), but silently defeats the guard when it occurs.
All three sit behind the documented trust boundary (the persister is trusted as-is, tracked follow-up for cryptographic manifest binding to `wallet_id`), so none is a new hole opened by this PR — but each undermines the specific narrowing this PR claims to add. Consider requiring at least one comparable account on both sides, comparing per-account xpubs (or a hash) for the shared families, and rejecting snapshot-only families that aren't in the manifest.
In `packages/rs-platform-wallet/src/manager/load_outcome.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load_outcome.rs:44-66: SnapshotIdentityMismatch collapses three distinct failure modes into one undifferentiated variant
The new `SnapshotIdentityMismatch` variant is a dataless enum tag, and `load.rs:172-175` sets it uniformly for all three branches of the `||` guard: `wallet_id` mismatch, `network` mismatch, or account-set divergence. Before this PR, a `wallet_id`/`network` mismatch produced a formatted `DecodeError` string that named exactly which field diverged and with what values, readable directly from the `on_wallet_skipped_on_load` log line. Now the FFI log (`manager.rs:429-432`, unchanged) records only `wallet_id` + the static string "snapshot does not match its persisted row". An operator triaging a skipped row from logs can no longer tell whether wallet_id, network, or the account set tripped the guard without reproducing locally. The variant-vs-string trade-off is otherwise fine (variants are match-able and stable) — worth carrying at least a small discriminant enum (`WalletId`/`Network`/`AccountSet`) so the check that failed remains observable without leaking snapshot bytes.
| fn snapshot_accounts_match_manifest( | ||
| info: &ManagedWalletInfo, | ||
| manifest: &[crate::changeset::AccountRegistrationEntry], | ||
| ) -> bool { | ||
| use key_wallet::account::AccountType; | ||
| use std::collections::BTreeSet; | ||
|
|
||
| fn comparable(t: &AccountType) -> bool { | ||
| !matches!( | ||
| t, | ||
| AccountType::ProviderOperatorKeys | ||
| | AccountType::ProviderPlatformKeys | ||
| | AccountType::PlatformPayment { .. } | ||
| ) | ||
| } | ||
|
|
||
| let manifest_types: BTreeSet<AccountType> = manifest | ||
| .iter() | ||
| .map(|e| e.account_type) | ||
| .filter(comparable) | ||
| .collect(); | ||
| let snapshot_types: BTreeSet<AccountType> = info | ||
| .all_managed_accounts() | ||
| .iter() | ||
| .map(|a| a.managed_account_type().to_account_type()) | ||
| .filter(comparable) | ||
| .collect(); | ||
| manifest_types == snapshot_types | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Snapshot/manifest cross-check has defense-in-depth gaps in filtered families and empty-set case
snapshot_accounts_match_manifest compares only the set of AccountType variants after filtering out ProviderOperatorKeys, ProviderPlatformKeys, and PlatformPayment { .. } (line 345-352). The filter is justified — those are the known enumeration asymmetries between Wallet::all_accounts and ManagedWalletInfo::all_managed_accounts — but three residual gaps remain in the very scenario this function was added to defend against (a wrong-row / tampered snapshot):
- xpubs are not compared.
info.wallet_idis a persisted field, not a re-derivation. A row with matchingwallet_id/network/account-type set but attacker-controlled per-account xpubs will pass, andwallet_info = *infothen consumes those pools into the SPV watch set whilebuild_watch_only_walletuses the manifest's xpubs — divergent detection/attribution surfaces without any signing-side failure. - Asymmetric filtering hides side-specific extras.
PlatformPaymentis filtered on the manifest side (where it can appear) andProviderOperatorKeys/ProviderPlatformKeyson the snapshot side (where they can appear). A snapshot with an extraProviderOperatorKeysthe manifest wouldn't attest to, or a manifest with an extraPlatformPaymentthe snapshot wouldn't attest to, is invisible because each is filtered on exactly the side that would have caught it. - Vacuous empty-set match. If a row's manifest reduces to only filtered families (e.g. a single
PlatformPaymententry —build_watch_only_walletonly requires non-empty) and the snapshot's comparable set is also empty,∅ == ∅returnstrueand any wrong-row snapshot withwallet_id/networkset to match sails through. Unlikely on real wallets (which always carry a Standard account), but silently defeats the guard when it occurs.
All three sit behind the documented trust boundary (the persister is trusted as-is, tracked follow-up for cryptographic manifest binding to wallet_id), so none is a new hole opened by this PR — but each undermines the specific narrowing this PR claims to add. Consider requiring at least one comparable account on both sides, comparing per-account xpubs (or a hash) for the shared families, and rejecting snapshot-only families that aren't in the manifest.
source: ['claude']
There was a problem hiding this comment.
Still present at 40806d6.
| @@ -53,6 +61,9 @@ impl std::fmt::Display for CorruptKind { | |||
| match self { | |||
| Self::MissingManifest => f.write_str("missing account manifest"), | |||
| Self::MalformedXpub => f.write_str("malformed account xpub"), | |||
| Self::SnapshotIdentityMismatch => { | |||
| f.write_str("snapshot does not match its persisted row") | |||
| } | |||
There was a problem hiding this comment.
🟡 Suggestion: SnapshotIdentityMismatch collapses three distinct failure modes into one undifferentiated variant
The new SnapshotIdentityMismatch variant is a dataless enum tag, and load.rs:172-175 sets it uniformly for all three branches of the || guard: wallet_id mismatch, network mismatch, or account-set divergence. Before this PR, a wallet_id/network mismatch produced a formatted DecodeError string that named exactly which field diverged and with what values, readable directly from the on_wallet_skipped_on_load log line. Now the FFI log (manager.rs:429-432, unchanged) records only wallet_id + the static string "snapshot does not match its persisted row". An operator triaging a skipped row from logs can no longer tell whether wallet_id, network, or the account set tripped the guard without reproducing locally. The variant-vs-string trade-off is otherwise fine (variants are match-able and stable) — worth carrying at least a small discriminant enum (WalletId/Network/AccountSet) so the check that failed remains observable without leaking snapshot bytes.
source: ['claude']
There was a problem hiding this comment.
Still present at 40806d6.
There was a problem hiding this comment.
Code Review
The latest delta 161d8b8..40806d6 is empty (retarget/merge onto the squashed #3980 base with an identical tree), so no new code was introduced in this pass. All four prior findings were re-verified directly against HEAD and are STILL VALID at unchanged line ranges: two suggestion-level concerns (defense-in-depth gaps in snapshot_accounts_match_manifest; loss of diagnostic granularity in the dataless SnapshotIdentityMismatch variant) and two nitpicks (idempotent-skip conflation in LoadOutcome::loaded; Swift-side hardcoded FFI wire codes). No blocking issues.
Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed), opus (security-auditor), claude-sonnet-5 (security-auditor), gpt-5.5[high] (security-auditor, 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: security-auditor, rust-quality, ffi-engineer
🟡 2 suggestion(s) | 💬 2 nitpick(s)
Findings not posted inline (2)
These findings could not be anchored to the current diff, but they are still part of this review.
- [NITPICK]
packages/rs-platform-wallet/src/manager/load.rs:124-136: Idempotent-skip path is indistinguishable from fresh-load inLoadOutcome::loaded— Carried forward from the prior review (161d8b8) — verified byte-identical at HEAD. The idempotency fast-path (lines 130-135) pushesexpected_wallet_idontooutcome.loadedwhen a wallet is already registered, identical in shape to the fresh-reconstruction push at end-of-loop (line 302) and th... - [NITPICK]
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift:344-354: SwiftreasonDescriptionhardcodes numeric FFI wire codes with no compile-time tie-back to Rust — Carried forward from the prior review (161d8b8) — verified byte-identical at HEAD.reasonDescriptionmatches integer literals100/101/102/103/199/200against Rust-sideLOAD_SKIP_REASON_*pub const u32values (rs-platform-wallet-ffi/src/manager.rs) that never surface as named...
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/load.rs:338-366: Snapshot/manifest cross-check has defense-in-depth gaps against a wrong-row snapshot — Carried forward from the prior review (161d8b8) — verified byte-identical at HEAD 40806d6.snapshot_accounts_match_manifestcompares only the set ofAccountTypevariants after filteringProviderOperatorKeys,ProviderPlatformKeys, andPlatformPayment { .. }(lines 345-352). The filter...
🤖 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:44-66: SnapshotIdentityMismatch collapses three failure modes and loses diagnostic granularity
Carried forward from the prior review (161d8b87) — verified byte-identical at HEAD. `CorruptKind::SnapshotIdentityMismatch` is a dataless variant (line 52) and `load.rs:172-183` sets it uniformly across all three branches of the `||` guard: `wallet_id` mismatch, `network` mismatch, and account-set divergence. Before this PR, a `wallet_id`/`network` mismatch produced a formatted `DecodeError` string naming exactly which field diverged and its two values. Now the `Display` impl (lines 64-66) always renders the identical static string "snapshot does not match its persisted row", and the FFI log (`rs-platform-wallet-ffi/src/manager.rs`) records only `wallet_id` + this static string. An operator triaging a skipped row from logs alone can no longer tell whether wallet_id, network, or the account set tripped the guard without reproducing locally.
The enum-over-string design is otherwise the right call (match-able and does not leak snapshot bytes) — carrying a small discriminant sub-enum (`WalletId`/`Network`/`AccountSet`) as data on the variant would preserve which check failed without regressing on the secrecy goal.
In `packages/rs-platform-wallet/src/manager/load.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:338-366: Snapshot/manifest cross-check has defense-in-depth gaps against a wrong-row snapshot
Carried forward from the prior review (161d8b87) — verified byte-identical at HEAD 40806d64. `snapshot_accounts_match_manifest` compares only the set of `AccountType` variants after filtering `ProviderOperatorKeys`, `ProviderPlatformKeys`, and `PlatformPayment { .. }` (lines 345-352). The filter is correct — those are the known enumeration asymmetries between `Wallet::all_accounts` and `ManagedWalletInfo::all_managed_accounts` — but three residual gaps remain in the exact scenario this function was added to defend against:
1. **xpubs are not compared.** `info.wallet_id` is a persisted field, not a re-derivation. A row with matching `wallet_id`/`network`/account-type set but attacker- or corruption-supplied per-account xpubs still passes. `wallet_info = *info` (line 166) then consumes those pools verbatim into the SPV watch set while `build_watch_only_wallet` uses the manifest's xpubs — divergent detection/attribution surfaces without any signing-side failure.
2. **Asymmetric filtering hides side-specific extras.** `PlatformPayment` is filtered on the manifest side (where it legitimately appears) and `ProviderOperatorKeys`/`ProviderPlatformKeys` on the snapshot side (where they legitimately appear). A snapshot with an *extra* `ProviderOperatorKeys`/`ProviderPlatformKeys` the manifest doesn't attest to, or a manifest with an extra `PlatformPayment` the snapshot doesn't attest to, is invisible because each is filtered on exactly the side that would have caught it.
3. **Vacuous empty-set match.** If a row's manifest reduces to only filtered families (e.g. a single `PlatformPayment` entry — `build_watch_only_wallet` only requires non-empty) and the snapshot's comparable set is also empty, `∅ == ∅` returns `true` and any wrong-row snapshot with matching `wallet_id`/`network` sails through.
All three sit behind the documented trust boundary (`# Trust boundary` docs, tracked follow-up for cryptographic manifest binding to `wallet_id`), so none is a new hole this PR opens — but each undermines the specific narrowing this PR claims to add. The lowest-cost hardening is guarding the empty-set case (`return false` when both comparable sets are empty); comparing a hash of per-account xpubs for the shared families and rejecting snapshot-only families would close the other two.
| @@ -53,6 +61,9 @@ impl std::fmt::Display for CorruptKind { | |||
| match self { | |||
| Self::MissingManifest => f.write_str("missing account manifest"), | |||
| Self::MalformedXpub => f.write_str("malformed account xpub"), | |||
| Self::SnapshotIdentityMismatch => { | |||
| f.write_str("snapshot does not match its persisted row") | |||
| } | |||
There was a problem hiding this comment.
🟡 Suggestion: SnapshotIdentityMismatch collapses three failure modes and loses diagnostic granularity
Carried forward from the prior review (161d8b8) — verified byte-identical at HEAD. CorruptKind::SnapshotIdentityMismatch is a dataless variant (line 52) and load.rs:172-183 sets it uniformly across all three branches of the || guard: wallet_id mismatch, network mismatch, and account-set divergence. Before this PR, a wallet_id/network mismatch produced a formatted DecodeError string naming exactly which field diverged and its two values. Now the Display impl (lines 64-66) always renders the identical static string "snapshot does not match its persisted row", and the FFI log (rs-platform-wallet-ffi/src/manager.rs) records only wallet_id + this static string. An operator triaging a skipped row from logs alone can no longer tell whether wallet_id, network, or the account set tripped the guard without reproducing locally.
The enum-over-string design is otherwise the right call (match-able and does not leak snapshot bytes) — carrying a small discriminant sub-enum (WalletId/Network/AccountSet) as data on the variant would preserve which check failed without regressing on the secrecy goal.
source: ['claude']
Issue being fixed or feature implemented
Follow-ups to #3980's review threads, stacked on its head branch (
fable/3692-review-fixes):ManagedWalletInfodisagreed with the row'saccount_manifestwas only checked forwallet_id/network, and a wrong-row snapshot was reported asDecodeError— conflating "wrong data" with "unreadable bytes".load_from_persistorflattened persister failures intoWalletCreation(String), discardingis_transient()/PersistenceErrorKind, so callers could not distinguish retryable from permanent load failures.What was done?
snapshot_accounts_match_manifest(manager/load.rs) compares the snapshot's account set against the row'saccount_manifest; mismatches skip the row via the existing skip machinery. The comparison covers only the account families both enumerations can carry — the manifest (Wallet::all_accounts) is ECDSA-only and includesPlatformPayment; the snapshot (ManagedWalletInfo::all_managed_accounts) is the mirror image (BLS/EdDSA provider keys, noPlatformPayment) — so legitimate snapshots are never rejected.CorruptKind::SnapshotIdentityMismatchvariant (withDisplay), used forwallet_id/networkand account-set mismatches; FFI wire codeLOAD_SKIP_REASON_SNAPSHOT_IDENTITY_MISMATCH = 103mapped inrs-platform-wallet-ffiand in SwiftreasonDescription(numericcase 103+ comment citing the wire-contract source).PlatformWalletError::PersisterLoad(#[from] PersistenceError);load_from_persistornow propagates the backend error, preservingis_transient()/PersistenceErrorKind.How Has This Been Tested?
rehydration_loadintegration suite extended 7 → 11 tests:rt_snapshot_account_set_mismatch_is_skipped,rt_snapshot_mismatch_skip_coexists_with_healthy_load,rt_persister_load_transient_error_is_typed_and_retryable,rt_persister_load_permanent_error_is_typed_and_not_retryable; existingrt_snapshot_wallet_id_mismatch_is_skippedasserts the new variant.platform-wallet,platform-wallet-ffi,platform-wallet-storage: 688 passed, 0 failed.cargo fmtandclippy -D warningsclean.Breaking Changes
None.
CorruptKindis#[non_exhaustive]; the new variant and wire code 103 are additive.Checklist:
For repository code-owners and collaborators only
Attribution
🤖 Co-authored by Claudius the Magnificent AI Agent