Skip to content

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

Draft
Claudius-Maginificent wants to merge 62 commits into
feat/platform-wallet-sqlite-persistorfrom
feat/platform-wallet-rehydration
Draft

feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692
Claudius-Maginificent wants to merge 62 commits into
feat/platform-wallet-sqlite-persistorfrom
feat/platform-wallet-rehydration

Conversation

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator

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

Update — latest #3625 propagated (2026-06-03)

Propagated the current #3625 tip (04662411cf, a chore: update Cargo.lock) into this branch via merge 3f2e7d2561 (parents 2f351903cd + 04662411cf). Fast-forward-clean; the delta is Cargo.lock only. The recurring stack-conflict files remain intact — rs-platform-wallet/src/changeset/mod.rs keeps all three feature-gated modules (serde_adapters + shielded_changeset + shielded_sync_start_state) and rs-platform-wallet-ffi/src/manager.rs keeps both the rehydration FFI and the birth-height exports.


Update — #3625 (carrying a v3.1-dev merge) propagated in (latest, 2026-06-03)

Merged the latest parent #3625 (feat/platform-wallet-sqlite-persistor, tip 31660906c2) into this branch (merge 2f351903cd). #3625 had just had v3.1-dev merged into it, so this is the bottom-up, parent-into-child propagation that keeps the stack reconciled:

  • Incoming payload is pure v3.1-dev, disjoint from the wallet crates4.0.0-beta.2 version bumps, rs-platform-version v8/v12 method versions, rs-drive-abci shielded-pool gating, and the dashmate ZeroSSL-renewal fix. It reaches the wallet crates only transitively via dpp/drive. The merge was conflict-free, deleted zero files, and the wallet-storage schema/readers (migrations/V001__initial.rs, sqlite/schema/contacts.rs, sqlite/schema/identity_keys.rs, sqlite/schema/mod.rs) are byte-identical pre/post merge — so the silent-merge-defect class (a reader SELECTing a column an auto-merge dropped) cannot have fired.
  • QA (Marvin) PASS: cargo fmt --check; cargo clippy -p platform-wallet-storage --all-targets --all-features -- -D warnings; cargo check -p platform-wallet-ffi (full dep graph recompiled against the bumped transitive crates — no API breakage from the version bumps); cargo test -p platform-wallet-storage334 passed / 0 failed / 3 ignored, incl. the item-G rehydration roundtrips (g_rt1/2/3, rt4), the unified-contacts lifecycle suite (tc_p4_004*), the schema guards (allowlist_accepts_every_known_table, contact_state_labels_match_enum), and sqlite_hardening_3625.

Update — #3625 contact-table unification merged in, reader reworked (latest, 2026-06-02)

Merged the latest #3625 (932b923b2b, CMT-003) into this branch (merge 052db80f12):

  • Unified contacts table (one lifecycle-state row replacing contacts_sent / contacts_recv / contacts_established) now backs rehydration. The item-G contacts reader was reworked for the unified shape: load_state un-gated to production (no longer #[cfg(feature = "__test-helpers")]), with the load_changeset wrapper retained so the seedless load() still consumes a ContactChangeSet.
  • No silent-merge defect. Independent QA (Marvin) re-checked the exact failure class that previously broke wallet-restore (a reader SELECTing a column the auto-merged schema had dropped): every contacts / identity_keys column the readers touch exists in V001, identity_keys keeps both wallet_id + derivation_blob, the meta_contact trigger fires on the unified contacts table, and all six meta_* tables + PER_WALLET_TABLES are reconciled. All feat(platform-wallet)!: add platform-wallet-storage crate (sqlite persister) #3625 hardening (CMT-001/002/004/005/006) is intact.
  • All gates green: cargo fmt --check, clippy under --all-features and --no-default-features --features sqlite,cli (-D warnings), cargo check -p platform-wallet-ffi; 513 tests pass incl. item-G g_rt1/g_rt2/g_rt3 and the tc_p4_004* unified-contacts lifecycle suite.

Update — reconciled with #3625/v3.1-dev, #3735 removed, item-G verified (latest)

This branch was brought current with the base stack and cleaned up; #3693 (contacts + identity-key rehydration, item G) was folded in.

  • Merged feat/platform-wallet-sqlite-persistor (feat(platform-wallet)!: add platform-wallet-storage crate (sqlite persister) #3625) + v3.1-dev. Brings the per-object metadata facility (meta_* tables + soft-cascade triggers), bincode outpoint encoding, the secrets review fixes, and v3.1-dev (Orchard genesis, dpp/drive updates). All feat(platform-wallet)!: add platform-wallet-storage crate (sqlite persister) #3625 fixes verified intact; no merge regression in either direction.
  • Dead fix(platform-wallet): wallet_id gate on resolver-fed sign entrypoints #3735 wrong-seed-gate scaffolding removed. fix(platform-wallet): wallet_id gate on resolver-fed sign entrypoints #3735 (the FFI sign-time gate) was closed out-of-scope, so this drops the never-constructed PlatformWalletError::WrongSeedForDatabase, the never-imported subtle dep, and the false docs claiming a first-sign gate / seed-unavailable skip exist. The seedless watch-only load() performs no wrong-seed check by design; any sign-time gate is deferred FFI work, not this PR.
  • Item-G (contacts + identity-key rehydration) is wired into load(). LOAD_UNIMPLEMENTED is reduced to core::last_applied_chain_lock; load() now calls the contacts + identity-key readers, so restored wallets recover their contacts/keys. A reconciliation-merge defect was caught and fixed — the identity_keys table had lost its wallet_id column relative to the reader (auto-merge artifact), breaking all rehydration; the table/writer/PER_WALLET_TABLES scope were reconciled and a dual-FK-cascade regression test (tc049_delete_wallet_cascades_identity_keys) added.
  • iOS-style seedless model verified (grumpy-review): zero-seed load_from_persistor, watch-only wallet build (Wallet::new_watch_only), no SeedProvider/3-arg-resolver-at-load; signing stays on-demand via the MnemonicResolver vtable (private key never crosses FFI).

All gates green: cargo clippy under --all-features and --no-default-features --features sqlite,cli (-D warnings), plus the platform-wallet-storage + platform-wallet test suites (incl. the 6 item-G tests).



STACKED PR — review diff against feat/platform-wallet-storage-secrets (PR #3672), not v3.1-dev.

Merge order: #3625 (feat/platform-wallet-sqlite-persistor) → #3672 (feat/platform-wallet-storage-secrets) → this PR.

The wallet_id sign-gate that this PR's earlier iterations bundled has been extracted to PR #3735 (security patch targeting v3.1-dev directly). Land #3735 first; the standard merge-up cycle will pull the gate into this PR's lineage naturally.


Issue being fixed or feature implemented

After the SQLite persister landed (#3625), restarting the wallet app required a full re-scan from birth height — the DB held all the data but nothing reconstituted live wallets from it. This PR closes that gap.

The user story matches how the real iOS host works. The app launches with the Keychain locked. There is no seed in memory. The wallet UI needs to come back instantly with all balances, UTXOs, identities, and asset-lock state — without prompting the user to unlock — so they can see their funds, scroll their history, and decide whether to act. Only when they do act (sign a transaction, register an identity key) does the Keychain unlock and the seed arrive, gated to that one operation. This was validated against dashwallet-ios (swift-sdk-integration branch): loadFromPersistor() is zero-arg, called at app launch with locked Keychain; signing flows take the MnemonicResolverHandle vtable on demand.

The implementation reflects that: load is seedless and watch-only. Every persisted wallet comes back as Wallet::new_watch_only(...) — no key material derived, no signing capability, no seed touched. Wrong-seed detection moves to the sign path — covered by the companion security PR #3735 against v3.1-dev.

What was done?

Seedless watch-only load (rs-platform-wallet)

PlatformWalletManager::load_from_persistor() reconstructs each persisted wallet from the keyless ClientWalletStartState:

pub async fn load_from_persistor(&self) -> Result<LoadOutcome, PlatformWalletError>

For each wallet in the persisted wallets map, the manager:

  • Builds an AccountCollection from the account_manifest: one Account::from_xpub(parent_wallet_id, account_type, account_xpub, network) per AccountRegistrationEntry.
  • Constructs Wallet::new_watch_only(network, wallet_id, accounts)key_wallet::WalletType::WatchOnly variant, no Mnemonic/Seed variant, no key bytes anywhere.
  • Routes the keyless CoreChangeSet (UTXOs, tx records, IS-locks, sync watermarks) into the wallet via the existing apply_persisted_core_state(...) path, which correctly handles non-BIP44 topologies (CoinJoin-only / DashPay) via all_funding_accounts_mut() — the F2 silent-zero balance fix carries through.

A wallet whose persisted rows fail to decode is skipped, not silently mis-loaded. LoadOutcome.skipped carries (WalletId, SkipReason::CorruptPersistedRow { kind: CorruptKind }) where CorruptKind is MissingManifest | MalformedXpub | DecodeError(String). A PlatformEvent::WalletSkippedOnLoad { wallet_id, reason } fires per skip. One corrupt row never aborts the rest. The caller receives Ok(LoadOutcome) (non-empty skipped is success, not an error).

New schema readers

Item Reader Notes
A1 schema::accounts::load_state Reads account_registrations + pools; decodes AccountRegistrationEntry; no Wallet built
B schema::core_state::load_state Bulk reconstructs ManagedWalletInfo — UTXOs, tx records, IS-locks, derived-address flags, sync watermarks, last_applied_chain_lock; routes UTXOs to the first funds-bearing account of any topology (no BIP44 assumption); no silent zero balance
A2 schema::asset_locks::load_unconsumed Status-predicate reader excluding terminal Consumed rows at SQL level (WHERE status NOT IN ('consumed'))

FFI

// before — earlier iteration of this PR (now removed)
int32_t platform_wallet_manager_load_from_persistor(
    const PlatformWalletManagerHandle* manager,
    const PlatformWalletPersisterHandle* persister,
    const ResolverSeedProvider* resolver,
    LoadOutcomeFFI* out_outcome);

// after
int32_t platform_wallet_manager_load_from_persistor(
    const PlatformWalletManagerHandle* manager,
    const PlatformWalletPersisterHandle* persister,
    LoadOutcomeFFI* out_outcome);

The resolver arg is gone — load is purely watch-only. LoadOutcomeFFI surfaces loaded_count / skipped_count / skipped[] so the host can retry skipped wallets after a corruption-fix flow.

Swift wrapper

PlatformWalletManager.swift::loadFromPersistor() aligns to the new 2-arg + outparam C signature (passes nil for the outcome ptr — the iOS host doesn't surface skip reasons to the UI today).

No V002 migration

Every column required for this phase is in V001. No SQL migration is added.

Not in this PR

How Has This Been Tested?

cargo fmt --all --check
cargo clippy -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi --all-targets -- -D warnings
cargo check --workspace
cargo test -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi
cargo test --doc -p platform-wallet -p platform-wallet-storage

Result: 410 tests passed, 0 failed, 8 ignored. Doctests: 3 passed, 0 failed, 1 ignored.

Targeted suite (packages/rs-platform-wallet/tests/rehydration_load.rs):

  • RT-WO — persist N wallets, drop, reopen, load_from_persistor(); assert every wallet comes back as Wallet::WatchOnly with correct wallet_id, accounts, balances. No seed ever touched.
  • RT-Corrupt — feed a corrupt manifest blob for one wallet; assert that wallet appears in LoadOutcome.skipped with CorruptPersistedRow, the other wallets load cleanly, exactly one PlatformEvent::WalletSkippedOnLoad fires.
  • RT-Z — assert LoadOutcome, SkipReason, WalletSkippedOnLoad payloads carry no key material in Display or Debug.

Persister-side readers:

cargo test -p platform-wallet-storage --test sqlite_accounts_reader \
                                       --test sqlite_core_state_reader \
                                       --test sqlite_asset_locks_filter \
                                       --test sqlite_load_wiring \
                                       --test sqlite_load_reconstruction

13/13 tc_p4_* passes including corruption-is-hard-error variants.

Breaking Changes

This PR rewrites a load path that was added in earlier commits of this same PR (and has never shipped). There are no breaking changes against v3.1-dev. For reviewers tracking the in-PR evolution:

  • PlatformWalletManager::load_from_persistor() no longer takes a &dyn SeedProvider (the trait itself was deleted — MnemonicResolverHandle is the on-demand contract).
  • ClientWalletStartState no longer carries a Wallet field (assembled in the manager via Wallet::new_watch_only).
  • FFI dropped the 3rd resolver arg from platform_wallet_manager_load_from_persistor.

No ! in the title because this is additive capability on an unreleased API — v3.1-dev carries none of the previous PR-internal shapes.

AR-7 hygiene

Load path eliminates AR-7 entirely — the 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 discipline (Zeroizing + non_secure_erase()); the sign-time wallet_id gate that enforces it ships in PR #3735.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

Note on the checklist item above: no ! in the title because no public API on v3.1-dev changes. The FFI signature change is internal to this PR branch (never released).

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent


Rebuild note (2026-05-25): History rewritten to remove the sign-gate code that was extracted to PR #3735. The 5-commit minimal rework on top of the original PR-1 rehydration work yields a focused diff: watch-only load via Wallet::new_watch_only, FFI resolver-arg drop, Swift wrapper align. The sign-time wallet_id gate ships via #3735 against v3.1-dev.

lklimek and others added 10 commits May 19, 2026 16:03
…ppers, error, validation, MemoryStore

Group A (Tasks 1–3) of the secret-storage feature. All gated behind the
opt-in `secrets` Cargo feature (never enabled by `default`).

Task 1 — `secrets::secret`: `SecretString` (trimmed MIT fork of
dash-evo-tool `Secret`, the egui `TextBuffer`/`take()` leak path deleted
by construction — SEC-REQ-3.8.1/3.8.2) + net-new byte-oriented
`SecretBytes`. Redacting `Debug`, no `Display`/`Deref`/`Serialize`,
full-capacity zeroize on drop, best-effort `region` mlock,
`subtle::ConstantTimeEq` on `SecretBytes`. The only `unsafe` is the
forked full-capacity wipe in `Drop`, confined behind a narrow
`#[allow(unsafe_code)]` + `// SAFETY:` proof — `#![deny(unsafe_code)]`
stays crate-wide (SEC-REQ-4.8).

Task 2 — `secrets::error::SecretStoreError`: concrete `thiserror` enum,
no boxed dyn error (SEC-REQ-4.4 / TC-082), no `#[non_exhaustive]`, no
secret/passphrase/plaintext/source in any variant, static `#[error]`
strings. `secrets::validate`: 32-byte `WalletId` newtype +
`^[A-Za-z0-9._-]{1,64}$` label allowlist, reject-not-sanitize
(SEC-REQ-4.3, CWE-22/20).

Task 3 — `secrets::store::SecretStore` trait (`get` returns
`Option<SecretBytes>`, never bare `Vec<u8>` — SEC-REQ-4.1) +
`MemoryStore` test double, gated by `__secrets-test-helpers` so it is
unreachable from production builds (SEC-REQ-2.3.1/2.3.2). `src/lib.rs`
slot activated; `secrets` feature wires only the RustSec-clean pinned
crypto (argon2=0.5.3, chacha20poly1305=0.10.1, zeroize=1.8.2,
subtle=2.6.1, region=3.0.2, getrandom; keyring-core 4.x split). MSRV
1.92 verified to compile the full dep set (`aes-gcm` omitted).
`Send + Sync` / object-safety compile-asserts added.

Satisfies SEC-REQ 3.1, 3.2, 3.3, 3.5, 3.6, 3.8.1, 3.8.2, 4.1, 4.2,
4.3, 4.4, 4.5, 4.6, 4.8, 2.0.3, 2.3.1, 2.3.2.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…a20-Poly1305 vault

Group B Task 4. `secrets::file::{mod,format,crypto}`:

- Argon2id KDF (`argon2 0.5.3`): floors m≥19456 KiB / t≥2 / p=1 enforced
  before any derivation; shipped default 64 MiB / t=3; params + 32-byte
  CSPRNG salt stored in the versioned header (SEC-REQ-2.2.1/.2/.3/.4).
- XChaCha20-Poly1305 (`chacha20poly1305 0.10.1`): fresh random 24-byte
  nonce per `put` (counter forbidden); combined decrypt so no
  unverified plaintext is ever materialized (SEC-REQ-2.2.5/.6/.8).
- AAD = canonical length-prefixed `format_version‖wallet_id‖label`,
  defeating blob-swap / version-rollback (SEC-REQ-2.2.7).
- Self-describing magic+version header; unknown version refused, fail
  closed (SEC-REQ-2.2.9).
- 0600 at creation via O_EXCL + fchmod before any ciphertext byte;
  pre-existing loose perms refused; atomic temp→fsync→rename→dir-fsync;
  temp holds only ciphertext, removed on failure (SEC-REQ-2.2.10/.11).
- Atomic rekey: fresh salt + fresh per-entry nonces, no `.bak`
  (SEC-REQ-2.2.12). Passphrase held in `SecretString`, never persisted,
  zeroized on drop; derived key recomputed per op, never retained
  (SEC-REQ-2.2.13).

Satisfies SEC-REQ 2.0.1, 2.0.2, 2.0.4, 2.2.1–2.2.13, 4.1.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…ring-core 4.x split)

Group B Task 5. `secrets::keyring::KeyringStore` over the keyring 4.x
split: `keyring-core 1.0.0` API + per-platform store crates
(linux-keyutils / dbus-secret-service / apple-native / windows-native),
all exact-pinned, RustSec-clean, MSRV-1.92-verified.

- Namespacing: service `dash.platform-wallet-storage`, account
  `{wallet_id_hex}:{label}` — two wallets cannot collide, a different
  app cannot silently read; only the non-secret index appears in
  keyring attributes (SEC-REQ-2.1.2, CWE-312).
- Fail-closed: headless / no Secret Service / no D-Bus → typed
  `BackendUnavailable`; locked → typed error. Never `unwrap`, never a
  silent plaintext / weaker-store fallback (SEC-REQ-2.1.3/.4 / AR-4).
- keyring-core's bare `Vec<u8>` from `get_secret` is wrapped into
  `SecretBytes` and the intermediate zeroized immediately
  (SEC-REQ-3.1/4.1).
- Per-OS threat-coverage rustdoc on the type (SEC-REQ-2.0.4 / 2.1.3).

Backend selection is an explicit operator decision — no auto-fallback
between KeyringStore and EncryptedFileStore (SEC-REQ-2.1.3 / AR-4).

Satisfies SEC-REQ 2.0.1, 2.0.4, 2.1.1, 2.1.2, 2.1.3, 2.1.4.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…egration tests

Group B Task 6.

`tests/secrets_guard.rs` (SEC-REQ-4.5.1): positive string-level scan of
`src/secrets/` asserting no logging/formatting sink
(`tracing::*`/`println!`/`format!`/`panic!`/…) is paired with an
`expose_secret()` result — the guard `tests/secrets_scan.rs`
deliberately does NOT cover this tree. Green on the clean tree; fails
the moment a secret is routed to a sink.

`tests/secrets_api.rs`: `get` returns `Option<SecretBytes>` (type
binding, never `Vec<u8>` — SEC-REQ-4.1); `dyn SecretStore`
object-safety / positive build guard (SEC-REQ-4.5); no boxed dyn error
in `src/secrets/` (TC-082 parity, comment-aware); error `Display` is
static and secret-free (SEC-REQ-2.0.1/3.3, CWE-209); wrapper `Debug`
redacted at the boundary (SEC-REQ-3.3). `MemoryStore` intentionally
unreachable from this external test crate (SEC-REQ-2.3.1).

Satisfies SEC-REQ 4.5, 4.5.1.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…secrets crypto deps

Group B Task 8 (SEC-REQ-4.7). The existing `rustsec/audit-check`
already audits the full `Cargo.lock` — which now pins the
`secrets`-gated crypto (argon2/chacha20poly1305/zeroize/subtle/region/
keyring-core + per-platform stores), so they are advisory-checked even
though `default` does not enable `secrets`. This adds a `cargo-deny
check advisories --all-features` job so the feature-conditional
dependency graph is exercised explicitly, plus a workspace `deny.toml`
(advisory ignore kept in sync with `.cargo/audit.toml`).

Locally verified: `cargo audit` exits 0; none of the secrets crypto
pins carry any RustSec advisory (confirms Smythe §7 first-hand). The
only flagged item, RUSTSEC-2025-0141 (bincode unmaintained), is a
pre-existing unrelated wasm-sdk/dpp dependency, not in the secrets
path.

Satisfies SEC-REQ 4.7.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…d atomic vault write

C1 (HIGH, Marvin QA-001): a `put`/`get`/`delete`/`rekey` against an
EXISTING vault with a passphrase deriving a DIFFERENT key than the
vault was created with previously wrote a mismatched-key entry and
returned Ok, producing an unreadable mixed-key vault. The header now
carries a passphrase-verification token: an XChaCha20-Poly1305 seal of
a fixed constant under the header-Argon2id-derived key, AAD-bound to
`(format_version, wallet_id, "\0verify")` (the leading-NUL label is
disjoint from every allowlisted entry label, so the token can never
alias a real slot). Every operation on an existing vault derives the
key from the supplied passphrase and verifies the token FIRST; a
mismatch fails the Poly1305 tag (constant-time, no extra compare, no
plaintext on failure) and returns `SecretStoreError::WrongPassphrase`
before any entry is read, written, or deleted. New vaults write the
token at creation; `rekey` verifies the old token and writes a fresh
one. `format_version` bumped 1→2; v1/v2 cross-reads fail closed via
the existing `VersionUnsupported` path.

C6 (LOW, Smythe SEC-RA-001): `write_vault` no longer swallows the
directory-fsync result — it is propagated as a typed error so the
atomic temp→fsync→rename→dir-fsync chain (SEC-REQ-2.2.11) is fully
enforced.

C7 (LOW, Marvin QA-004): the temp file now uses a unique name
(`pid` + monotonic counter) created with `O_EXCL` and the destination
is never pre-removed, so a crash can never leave the vault absent and
concurrent writers cannot collide on a fixed temp name. The atomic
rename + fsync ordering is unchanged.

Tests (red→green, file/mod.rs): wrong-pass `put` to existing vault ⇒
`Err(WrongPassphrase)` + vault still readable with the correct pass +
rejected slot never written; wrong-pass `get`/`delete` ⇒
`Err(WrongPassphrase)` + vault unmutated; correct pass round-trips
unchanged. The two wrong-pass tests were FAILED before this fix and
pass after; format (de)serialize round-trips the token fields.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…ringLocked; correct keyring-core attribution

C3 (MED, Adams PROJ-002 / Marvin QA-003): `map_keyring_err` collapsed
keyring-core's `NoStorageAccess` into `BackendUnavailable`, leaving
`SecretStoreError::KeyringLocked` dead. Per keyring-core 1.0.0 docs,
`NoStorageAccess` covers the locked-collection case ("it might be that
the credential store is locked"), so it now maps to `KeyringLocked`,
enabling the unlock-retry UX (SEC-REQ-2.1.4). Genuinely-absent backends
(`NoDefaultStore` / `PlatformFailure`) stay `BackendUnavailable`.
Added `locked_keyring_maps_to_keyring_locked` asserting the locked,
absent, and not-found mappings.

C5 (LOW, Adams PROJ-003 / Marvin QA-004): the module header said
"keyring-core 4.x split" — inaccurate. Reworded to state the lib is
`keyring-core 1.0.0` plus the per-platform store crates; the `keyring`
4.x crate is the sample CLI and is not a dependency. No dependency
change.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…roizes on drop

C4 (MED, Smythe SEC-RA-002 / Adams PROJ-004 / Marvin QA-002): the
rustdoc claimed stored values sit in `SecretBytes`, but the map held a
bare `Vec<u8>` that never zeroized — code contradicted the doc. Fixed
the code (not the doc): the backing map is now
`HashMap<(WalletId,String), SecretBytes>`, closing SEC-REQ-2.3.2 so
even test memory is wiped on drop. Added `stored_value_is_zeroizing_
wrapper` (type-binding assertion) + a `needs_drop::<SecretBytes>()`
compile-time guard.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…rgo.toml comment

C5 (LOW, Adams PROJ-003 / Marvin QA-004): the per-platform-store
dependency comment said "keyring-core 4.x split". Reworded to state
accurately that `keyring-core 1.0.0` is the API and the per-platform
crates provide the backends (the `keyring` 4.x crate is the sample CLI
and is intentionally not depended on). No dependency change.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…etStore API

C2 (MED, Adams PROJ-001): the trait sketch was stale/dangerous —
`get -> Option<Vec<u8>>` (the exact CRITICAL leak SEC-REQ-4.1 forbids)
and the false "feature flag exists today but flips no code" line.
Rewritten to the delivered API: `get -> Result<Option<SecretBytes>,
SecretStoreError>`, accurate `put`/`delete` signatures, the real
backends (KeyringStore/EncryptedFileStore/MemoryStore with their
fail-closed / gating semantics), and the now-true statement that
enabling `secrets` activates the module. Present-state only, no
history narration; no forbidden token introduced into
`src/sqlite/schema/` or `migrations/`.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34d72a52-dc8f-44c0-8224-f63ccbf07d13

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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 and usage tips.

lklimek and others added 5 commits May 20, 2026 14:40
…ult-on

Removes the cargo-deny advisories CI job and its `deny.toml` config in
favour of the existing `rustsec/audit-check` job. Once `secrets` is in
the default feature set, `Cargo.lock` unconditionally pins the
RustSec-clean crypto stack (`argon2`/`chacha20poly1305`/`zeroize`/
`subtle`/`region`/`keyring-core` + per-platform store crates) so a
single audit run covers them all (SEC-REQ-4.7).

`secrets` joins `sqlite`+`cli` as a default feature. Dev-dependency on
self adds `default-features = false` so the off-state CI invocation
(`--no-default-features --features sqlite,cli`) actually exercises the
secrets-disabled graph — otherwise the dev-dep view would silently
re-enable defaults for every integration test.

New `tests/secrets_off_state.rs` is the runtime D4 guard: gated
`#[cfg(not(feature = "secrets"))]`, it builds against the persister
surface only and asserts the off-state graph stays consumable.

T1+T2 land atomically — cargo-deny removal coincides with secrets
going default-on so crypto pins never drop out of audit scope between
commits.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…backends

Retires the crate-local `SecretStore` trait + `SecretStoreError` enum
and rebuilds the `secrets` submodule on
`keyring_core::api::{CredentialApi, CredentialStoreApi}` — the upstream
SPI shipped by `keyring-core 1.0.0`. The `EncryptedFileStore`'s
security construction (Argon2id + XChaCha20-Poly1305 + AAD verify
token + 0600 + atomic temp→rename + dir-fsync + zeroize) is preserved
byte-for-byte; only the trait surface changes.

API-shape mapping (Nagatha §1, variant A — the `:` delimiter is rejected
by the label allowlist):

  service = "dash.platform-wallet-storage/" + hex(wallet_id)
  user    = label

Per-task content:

- **T3** `src/secrets/file/error.rs` — new `FileStoreError` enum
  (`Decrypt`, `WrongPassphrase`, `KdfFailure`, `VersionUnsupported`,
  `MalformedVault`, `InvalidLabel`, `InsecurePermissions`, `Io`).
  Static `#[error]` strings only; no secret in any variant.
  `src/secrets/file/error_bridge.rs` — `FileStoreFailure` unit-only
  marker (Smythe EDIT-3: no `String`/`Vec<u8>`/`Path` fields permitted,
  enforced via a compile-time `Copy` assertion) boxed inside
  `keyring_core::Error::NoStorageAccess` (WrongPassphrase) or
  rendered into `BadStoreFormat`'s static `String` payload. The
  `downcast_failure` helper recovers the marker for D1(b).

- **T4** `src/secrets/file/mod.rs` — `EncryptedFileStore` implements
  `CredentialStoreApi`; per-`(service, user)` entries implement
  `CredentialApi`. The store is held behind an internal `Arc` so
  long-lived credentials can outlive the public handle. `delete` honors
  upstream's `NoEntry`-if-absent contract (D3). `service` parsing
  rejects mismatch with `Invalid("service", _)`; `validated_label` runs
  at `build` time AND every `CredentialApi` op (defence in depth,
  M-2). All twelve in-module security tests port one-for-one through
  the SPI (NoEntry for absence, downcast for typed-error checks).

- **T5** `src/secrets/keyring.rs` — `KeyringStore` wrapper retired in
  favour of the bare `default_credential_store() -> Result<Arc<dyn
  CredentialStoreApi + Send + Sync>, keyring_core::Error>` constructor.
  Headless / unknown OS / D-Bus-less Linux → `NoDefaultStore` per D2
  (typed, single SPI error). Never panics, never falls back.

- **T7** `src/secrets/memory.rs` — `MemoryStore` → `MemoryCredentialStore`
  implementing `CredentialStoreApi`. Internal map keys on
  `(service, user)` strings, values remain `SecretBytes` (SEC-REQ-2.3.2).
  Still gated behind `__secrets-test-helpers`.

- **T8** `src/lib.rs` — object-safety + `Send + Sync` assertions now
  target `keyring_core::Error` and `dyn CredentialStoreApi + Send +
  Sync`. `src/secrets/mod.rs` re-exports the new surface; `pub use
  SecretStore` / `SecretStoreError` retired.

- **Tests** — `tests/secrets_api.rs` rewritten against the SPI; the
  `Vec<u8> → SecretBytes::new` consumer-seam pattern (Smythe EDIT-1:
  no named intermediate `Vec` binding) is the type-shape assertion.
  `tests/secrets_guard.rs` extended with the EDIT-2 EDIT-2 guard:
  no `{{:?}}`-debug-format paired with `keyring_core::Error` in
  `src/secrets/` (since `BadEncoding`/`BadDataFormat` embed raw
  `Vec<u8>`). All twelve `EncryptedFileStore` security invariants
  pass on the new API.

`tests/secrets_seed_provider_adapter.rs` and the
`seed_provider_adapter.rs` source file are NOT landed on this branch:
the `SeedProvider`/`WalletSecret`/`SeedUnavailable` types they consume
live in `rs-platform-wallet` on PR #3692, not on this base. The
rewritten adapter will land on PR #3692's rebase onto this tip — see
the rework report.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…core SPI

Rewrites SECRETS.md as the present-state spec for the secrets
submodule on the upstream `keyring_core::api` SPI:

- Drops the retired `SecretStore` trait listing.
- Documents the `service = "dash.platform-wallet-storage/" + hex(wid)`,
  `user = label` key shape with the allowlist precondition.
- Memory hygiene section codifies Smythe EDIT-1: `SecretBytes::new(...)`
  is the consumer-seam wrapper, no named intermediate `Vec` binding.
- Backends section: `EncryptedFileStore` + `default_credential_store()`
  + test-only `MemoryCredentialStore`.
- Cross-SPI error bridge: `FileStoreFailure` unit-only marker (EDIT-3
  constraint stated as load-bearing), `downcast_failure` recovery
  path, EDIT-2 `{:?}`-format ban on `keyring_core::Error` documented
  with its enforcement test.
- Audit hooks section adds `secrets_off_state` (D4) and rephrases
  `secrets_guard` to cover both leak sinks.
- Cargo features paragraph notes `secrets` is default-on; cargo-deny
  removal is noted via the lockfile-is-audit-coverage rationale.

`src/lib.rs` crate-level doc retouched to point at the new SPI and
backend names (the prior "SecretStore reserved" phrasing retired).

`tests/secrets_scan.rs` exemption comment rephrased to describe the
present state.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…rface

`tests/secrets_default_on_compiles.rs` (M-S4) — a build-only assertion
that the default feature set (`secrets` in) re-exports every public
type/function in the `secrets` submodule. Names: `EncryptedFileStore`,
`SecretBytes`, `SecretString`, `WalletId`, `FileStoreError`,
`FileStoreFailure`, `SERVICE_PREFIX`, `default_credential_store`,
`keyring_core::Error`. Compiling the test target is the assertion;
the body never exercises a backend.

Pairs with `tests/secrets_off_state.rs` (D4 — runtime proof under
`--no-default-features --features sqlite,cli` that the surface
compiles out and the persister still links).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…EDIT-4)

QA-501 (MEDIUM, EDIT-4 forward-compat): `SecretBytes`/`SecretString`
retained `impl PartialEq`/`Eq` despite EDIT-4's binding intent. The
impls delegated to constant-time compares so today's behaviour is
safe, but leaving `==` reachable means future bridge code could
inherit a non-constant-time path or a length-leaking shortcut without
review noticing.

EDIT-4 says: no `==` on secret bytes, no exception. Strip the impls
and let `subtle::ConstantTimeEq::ct_eq` be the only equality path.

- `secret.rs` — removed `impl PartialEq for SecretBytes` /
  `impl Eq for SecretBytes` and `impl PartialEq for SecretString` /
  `impl Eq for SecretString`. `SecretString` gains an
  `impl ConstantTimeEq` so callers keep a constant-time-safe
  equivalence path (was previously implicit inside `PartialEq::eq`).
- Public rustdoc on both types names `PartialEq`/`Eq` in the "not
  implemented" list and points callers at `ConstantTimeEq::ct_eq`.
- `compile_fail` doc-test on each type asserts that `a == b` does NOT
  compile — durable forward-compat guard. If a future change adds
  `PartialEq` back, the doc-test starts compiling and the test fails.
- Test callers migrated:
  - `secret_string_eq_is_value_based` →
    `secret_string_ct_eq_is_value_based`, asserts via
    `bool::from(a.ct_eq(&b))`.
  - `secret_bytes_constant_time_eq` drops its trailing
    `assert_eq!(a, b)` / `assert_ne!(a, c)` lines (the prior
    ct_eq-based assertions above them already covered the same
    invariant).

Workspace-wide grep confirmed no other `==`/`assert_eq!` callers on
`SecretBytes`/`SecretString` exist.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
lklimek and others added 11 commits May 20, 2026 17:10
schema::accounts::load_state reads account_registrations rows back into
a deterministic Vec<AccountRegistrationEntry> manifest — the account-set
oracle and per-account xpub cross-check source for rehydration. Mints no
Wallet, fail-hard on a corrupt blob. RT: sqlite_accounts_reader (3 tests).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…wrong-seed gate (S)

- platform-wallet: storage-agnostic SeedProvider trait with zeroizing,
  Debug-redacted SecretPhrase/SecretSeed newtypes (M-DONT-LEAK-TYPES);
  SeedUnavailable/SecretStoreErrorKind structural projections.
- manager::rehydrate::rehydrate_wallet: fail-closed, constant-time
  wrong-seed gate (compute_wallet_id recompute + per-account xpub
  cross-check via subtle) yielding typed WrongSeedForDatabase that
  carries only the two 32-byte ids. AR-7 noted at the call site.
- manager::rehydrate::apply_persisted_core_state: keyless CoreChangeSet
  → ManagedWalletInfo apply (balance no-silent-zero contract).
- load_from_persistor signature → (&dyn SeedProvider) -> LoadOutcome;
  seed-unavailable ⇒ skip (continue before insert, LoadOutcome.skipped,
  PlatformEvent::WalletSkippedOnLoad); wrong seed ⇒ hard-fail.
- ClientWalletStartState made keyless by type (no Wallet/seed field).
- platform-wallet-storage: secrets-gated CredentialStoreSeedProvider
  adapter over `keyring_core::api::CredentialStoreApi` (mnemonic→seed
  label order, no secret in logs/errors). File-backend WrongPassphrase
  is recovered via `downcast_failure` on the cross-SPI marker so the
  operator-actionable case survives the seam.

RT: seed_provider (4) + rehydrate (3) unit tests, secrets_seed_provider
_adapter (10). secrets_scan/secrets_guard still green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
core_state::load_state rebuilds the keyless CoreChangeSet projection
(unspent UTXOs with address recovered from script+network, tx records,
IS-locks, sync watermarks) for one wallet — the safety-critical balance
source. spent rows excluded; fail-hard on a corrupt blob. Documents the
reconstructed-vs-deferred split: last_applied_chain_lock /
per-account-attribution / coinbase flags re-warm on first post-load
sync (the no-V001-column deviation from dev-plan §5 is recorded inline).

RT-2 (sqlite_core_state_reader): a non-zero balance survives
store→drop→reopen→load→apply, reconstructed exact in the confirmed
bucket — the no-silent-zero contract proven end-to-end. 4 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…on reader (A2)

asset_locks::load_unconsumed excludes terminal 'consumed' rows at the
SQL level so a spent one-shot lock never resurrects as actionable on
rehydration (A04/A08); historical rows stay on disk via load_state.
Corrects the factually-wrong list_active doc-comment (consumed locks do
NOT leave via AssetLockChangeSet::removed — they upsert and persist).

RT-4 (sqlite_asset_locks_filter): mix incl. terminal Consumed — row
still on disk, absent from filtered feed, non-terminal survive. 2 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…d (C)

SqlitePersister::load() now populates ClientStartState.wallets with the
keyless per-wallet payload (network, birth_height, account_manifest,
core_state, identity_manager, Consumed-filtered unused_asset_locks) via
the A1/B/A2 readers + identities::load_state. Return type carries no
Wallet/seed by construction. Real wallets_rehydrated tracing count;
LOAD_UNIMPLEMENTED shrunk to the genuinely-deferred set
(contacts/identity_keys/last_applied_chain_lock); load() rustdoc
corrected.

RT (sqlite_load_wiring): keyless payload round-trips, empty DB stays
empty, metadata-only wallet still present. 3 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
rehydration_load: load_from_persistor through a real
PlatformWalletManager (mock SDK, in-memory keyless persister, test
SeedProvider) —
- seed round-trip: wallet registered + signing-capable by construction;
- RT-W: present-but-wrong seed ⇒ WrongSeedForDatabase, NOT in skipped,
  NO WalletSkippedOnLoad event, wallet absent;
- RT-S: seed absent ⇒ skip (other wallets load, skipped wallet ABSENT
  from manager, LoadOutcome.skipped + exactly one WalletSkippedOnLoad
  event, Ok), then recoverable on a fresh targeted re-load;
- RT-S(ii): KeyringLocked ⇒ StoreUnavailable skip;
- RT-Z: no seed byte leaks into LoadOutcome / SkipReason /
  WrongSeedForDatabase Display+Debug.
5 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…tion (F)

sqlite_load_reconstruction: header rewritten (no longer 'blocked on
upstream Wallet::from_persisted'); tc_p4_006/tc_p4_007 now assert
wallets_rehydrated=N / pending=0 and a populated wallets payload;
tc_p4_012 asserts O(1)-per-wallet + small constant shared overhead
(no brittle magic-number pin) instead of the old fixed-2. All 13 green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… SELECTs (F)

The full-rehydration readers (accounts/core_state load_state) use
prepare() for one-shot SELECTs by design; add them to
READ_ONLY_PREPARE_ALLOWED so tc_p1_003 (writers must use
prepare_cached) stays green without weakening the writer-side rule.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… doc (F2,F3)

F2 (MEDIUM): apply_persisted_core_state previously routed persisted
UTXOs only into the first BIP44 account, silently dropping ALL UTXOs
(→ Ok + balance 0) for CoinJoin-only / non-BIP44 topologies. Now route
into the wallet's first funds-bearing account of ANY topology (BIP44/
BIP32/CoinJoin/DashPay) via all_funding_accounts_mut(); the wallet
total stays exact (it is a sum). A wallet with persisted UTXOs but no
funds account at all fails closed with the new typed
PlatformWalletError::RehydrationTopologyUnsupported (wallet_id +
utxo_count, no key material) instead of a silent zero. Signature is
now Result<(), PlatformWalletError>.

F3 (LOW): moved the last_applied_chain_lock bullet from the
'Reconstructed' to the 'Deferred' rustdoc section (it is always None
from disk — no V001 column).

RT: f2_no_bip44_wallet_nonzero_balance_survives_reopen (CoinJoin-only,
9_000_000 duffs) fail→pass; RT-2 + B-2/B-3/B-4 still green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
F4 (LOW): the plain '!=' wallet_id re-check after insert_wallet was
shadowed-dead — the constant-time rehydrate_wallet gate already proves
compute_wallet_id() == expected_wallet_id pre-insert and a mismatch is
the typed fail-closed WrongSeedForDatabase. The legacy check only
emitted a weaker untyped WalletCreation error and confused readers;
removed. Also wires the F2 apply_persisted_core_state Result into the
hard-fail/rollback path. RT-W still passes (typed WrongSeedForDatabase
from the real gate unaffected).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…PI (F1)

F1 (HIGH): workspace no longer compiled against the new
load_from_persistor signature / keyless ClientWalletStartState.

- New ResolverSeedProvider wraps the existing Swift MnemonicResolver-
  Handle vtable (same mechanism as sign_with_mnemonic_resolver) as a
  SeedProvider — minimal correct seed source, no second secret path,
  no mnemonic round-tripping. Chosen over SecretStoreSeedProvider
  because the iOS host already owns the resolver, not a SecretStore.
- build_wallet_start_state now projects its reconstructed wallet +
  wallet_info into the keyless ClientWalletStartState shape
  (account_manifest from accounts, core_state CoreChangeSet from the
  restored UTXO set + sync watermarks); the local Wallet is dropped
  (manager re-derives from the resolver seed + runs the wrong-seed
  gate).
- platform_wallet_manager_load_from_persistor gains a resolver param
  and an optional *mut LoadOutcomeFFI out-param: the LoadOutcome is no
  longer silently discarded — every skipped (wallet_id, reason) is
  logged AND surfaced (loaded_count/skipped_count/skipped[]) so the
  host can retry the skipped set. New platform_wallet_load_outcome_free
  releases the heap array.

Acceptance: cargo check --workspace AND --all-features both exit 0.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
@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 and others added 5 commits May 25, 2026 13:10
…atch_only

load_from_persistor now rebuilds every persisted wallet watch-only from
its keyless AccountRegistrationEntry manifest (Wallet::new_watch_only)
and applies the keyless core-state projection on top. No seed material
is touched on the load path: signing keys are derived on demand later
through the MnemonicResolverHandle sign entrypoints, which carry the
fail-closed wrong-seed gate themselves.

Drops the SeedProvider port + WalletSecret/SecretPhrase/SecretSeed
payloads (and the storage CredentialStoreSeedProvider adapter that fed
them) — load no longer needs the abstraction. WrongSeedForDatabase
stays on PlatformWalletError for the sign-path gate. RT suite reshapes
to RT-WO (watch-only round-trip) + RT-Corrupt (per-row decode skip with
SkipReason::CorruptPersistedRow{kind: CorruptKind::MissingManifest}) +
RT-Z (no key material in any LoadOutcome / SkipReason surface).

apply_persisted_core_state and its F2/F3/F4 fixes are unchanged.

AR-7 residual risk on the load path is eliminated (no Wallet of a
signing type is constructed during load, so its Debug-leak surface is
gone from this path).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…stor

platform_wallet_manager_load_from_persistor is now a 2-arg call
(manager_handle, out_outcome). The Swift host never passed a real
resolver at load time anyway — load is watch-only, signing keys are
derived later on demand through the same MnemonicResolverHandle vtable
the per-call sign entrypoints already use (next commit lands the
wallet_id gate there).

Drops the MnemonicResolverHandle → platform_wallet::SeedProvider
adapter (rehydration_seed_provider.rs); no consumer left.

LoadOutcomeFFI.SkippedWalletFFI.reason_code reshapes to the new
CorruptKind family (100/101/102) — ABI-bump for #3692 since the seed-
availability codes (0/1/2) it previously carried are gone with the
seedless load path.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…edless FFI

platform_wallet_manager_load_from_persistor is now (handle,
out_outcome) — the resolver argument is gone with the seedless-load
rework. Pass nil for out_outcome (Swift doesn't surface skipped
wallets yet; corrupt rows are logged on the Rust side).

Doc string refreshed to reflect Wallet::new_watch_only as the
underlying load primitive and the on-demand-signing + wrong-seed-gate
contract on the resolver-fed sign entrypoints.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… sign-gate split

The wrong-seed detection moves off the load path and onto the resolver-fed
FFI sign entrypoints. That gate + its coverage now ships in PR #3735
(security patch against v3.1-dev), not here. Drop the dangling reference
to the never-existed `sign_wrong_seed_gate.rs` file and point readers at
PR #3735 instead.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
@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
lklimek added 5 commits May 25, 2026 14:41
…istor' into feat/platform-wallet-storage-secrets

# Conflicts:
#	Cargo.lock
…m-wallet-rehydration

# Conflicts:
#	packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs
#	packages/rs-platform-wallet-storage/src/sqlite/schema/core_state.rs
…istor' into feat/platform-wallet-storage-secrets
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
Claudius-Maginificent and others added 7 commits June 2, 2026 12:44
…m G) (#3693)

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…istor' into feat/platform-wallet-rehydration

# Conflicts:
#	Cargo.lock
#	packages/rs-platform-wallet-storage/Cargo.toml
#	packages/rs-platform-wallet-storage/SECRETS.md
#	packages/rs-platform-wallet-storage/src/secrets/file/crypto.rs
#	packages/rs-platform-wallet-storage/src/secrets/file/error.rs
#	packages/rs-platform-wallet-storage/src/secrets/file/format.rs
#	packages/rs-platform-wallet-storage/src/secrets/file/mod.rs
#	packages/rs-platform-wallet-storage/src/secrets/keyring.rs
#	packages/rs-platform-wallet-storage/src/secrets/mod.rs
#	packages/rs-platform-wallet-storage/src/secrets/store.rs
#	packages/rs-platform-wallet-storage/src/sqlite/schema/accounts.rs
#	packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs
#	packages/rs-platform-wallet-storage/tests/secrets_api.rs
#	packages/rs-platform-wallet/src/events.rs
The #3625->#3692 merge accidentally staged local grumpy-review/triage
scratch (.review-3625/), present in neither merge parent. Remove it from
tracking (kept on disk) and ignore .review-*/ so review output never
gets staged again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3735 closed out-of-scope)

The #3735 FFI sign-gate was deliberately closed out-of-scope and never
merged, leaving dead scaffolding and false docs in #3692. The seedless
watch-only load performs no wrong-seed check; any sign-time gate is
deferred separate FFI work, not part of this PR.

- Drop never-constructed PlatformWalletError::WrongSeedForDatabase
- Drop the unused `subtle` dependency (never imported in this crate;
  the real constant-time usage lives in platform-wallet-storage/secrets)
- Correct docs that claimed a first-sign wrong-seed gate exists
  (rehydrate.rs, load.rs, client_wallet_start_state.rs, FFI manager.rs,
  storage persister.rs, rehydration_load.rs test header)
- Correct WalletSkippedOnLoad docs to describe the only real skip
  (CorruptPersistedRow); the seedless load never touches the seed
- Trim the xpub "cross-check for the wrong-seed gate" clause, keeping
  its real purpose (account-set oracle for the watch-only wallet)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… item-G reader (merge defect)

A clean auto-merge left the `identity_keys` table out of sync with the
code that queries it. The merge kept the older V001 DDL (PK
`(identity_id, key_id)`, no `wallet_id`) but pulled in #3693's item-G
reader, which runs `SELECT ... FROM identity_keys WHERE wallet_id = ?1`.
Result: `load_state` errored with "no such column: wallet_id" for every
wallet, breaking ALL rehydration (the 6 item-G tests plus the broader
load/rehydration suite).

Adopt #3693's item-G `identity_keys` design consistently, reconciled to
the #3625 schema this branch must preserve:

- V001: `identity_keys` gains `wallet_id BLOB NOT NULL` + reserved
  `derivation_blob BLOB`, PK `(wallet_id, identity_id, key_id)`, a
  `wallet_id` FK to `wallet_metadata` and a single-column `identity_id`
  FK to `identities(identity_id)` (HEAD's `identities` keeps its
  `identity_id`-only PK + nullable `wallet_id`, so #3693's composite
  `identities(wallet_id, identity_id)` FK would not create), and the
  composite index `idx_identity_keys_wallet_identity`.
- Writer `apply`: INSERTs all six columns, composite ON CONFLICT and
  DELETE, binds `wallet_id` from the flush scope and cross-checks the
  entry's `wallet_id`.
- `PER_WALLET_TABLES`: `identity_keys` moves `ViaIdentity → DirectColumn`.
- Update the three #3625 tests whose INSERTs used the old 4-column form
  so their intent holds under the new schema.

The six `meta_*` tables + soft-cascade triggers (#3625) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te reserved derivation_blob (QA-001/002)

QA-001: add tc049_delete_wallet_cascades_identity_keys. Seeds both FK
parents (wallet_metadata + wallet-scoped identities), inserts one
identity_keys row, then delete_wallet and asserts the row is purged via
the dual ON DELETE CASCADE (idempotent across both paths) and that the
DeleteWalletReport accounts for the removed row.

QA-002: note that derivation_blob is reserved for a future typed
projection and always NULL today; derivation_indices live inside
public_key_blob (the IdentityKeyWire blob is the single source of truth).
Comment added at the migration column and above the INSERT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lklimek and others added 3 commits June 2, 2026 16:47
…tform-wallet-rehydration

# Conflicts:
#	packages/rs-platform-wallet-storage/src/sqlite/schema/contacts.rs
#	packages/rs-platform-wallet-storage/src/sqlite/schema/mod.rs
#	packages/rs-platform-wallet-storage/tests/sqlite_load_reconstruction.rs
Propagate parent-into-child up the platform-wallet PR stack: #3625 now
carries a v3.1-dev merge (4.0.0-beta.2, shielded-pool gating, protocol
v8/v12 method versions, dashmate ZeroSSL fix). Incoming delta is disjoint
from the wallet crates (transitive via dpp/drive only); no wallet-storage
schema or reader file is modified, no files deleted, zero conflicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hydration

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants