Skip to content

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

Draft
Claudius-Maginificent wants to merge 240 commits into
v3.1-devfrom
feat/platform-wallet-rehydration
Draft

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

Conversation

@Claudius-Maginificent

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

Copy link
Copy Markdown
Collaborator

Update — incremental v3.1-dev merge (2026-06-09)

Pushed 855471e6e8..2d22cf3e82. v3.1-dev advanced again (6fa4686..da9d3fe, now v4.0.0-beta.4); merged it up to keep the branch current. The prior merge already folded in 6fa4686, so the merge-base is exactly that commit — this was a small incremental merge of three disjoint shielded-pool PRs:

All three wallet overlap files (manager/mod.rs, error.rs, ffi/persistence.rs) auto-merged with zero conflicts, including preservation of the event_manager ungating against upstream's new shielded-sync edits (re-verified by a non-shielded cargo check -p platform-wallet canary). The only manual resolution was Cargo.lock (took upstream's beta.4 base, reconciled our secrets/rehydration deps without disturbing upstream pins). Wallet-scope gates all green: fmt, check, clippy ×2, cargo test -p platform-wallet-storage, plus platform-wallet default + shielded and platform-wallet-ffi. Behavioral risk ~10% — disjoint feature work, no signature breaks.


Update — comment compaction + v3.1-dev merge (2026-06-09)

Pushed b4a3aa9989..855471e6e8. Two changes since the last update; base retargeted to v3.1-dev (the old base feat/platform-wallet-sqlite-persistor / #3625 was squash-merged upstream and its branch deleted).

Comment compaction (coding-best-practices)

Swept the crate for the verbose prose that accumulated during the audit + remediation campaign: −1,319 net comment lines across 50 source + 19 test files. Proven comments-only (every changed file is code-byte-identical after stripping comments — zero behavioral change) and verified green (fmt, check, clippy ×2, tests incl. doc-tests). Kept: teaching public rustdoc, SAFETY: / INTENTIONAL: blocks, security "why" notes, external-quirk citations. Removed: tombstone/history narration, finding-ID changelogs, and multi-paragraph design essays (they live in git history / SCHEMA.md / SECRETS.md).

Merge origin/v3.1-dev

#3625 is now in v3.1-dev (squash-merged), so this brings the upstreamed base + the rest of the v3.1-dev payload (grovedb 4.0.0, dashcore, drive shielded-fees, dpp, getDocuments v1) into the branch. Resolution:

  • rs-platform-wallet-storage/** — forced to our HEAD (the evolved superset; verified byte-identical). The pre-rename wallet_meta.rs stray was removed.
  • rs-platform-wallet / -ffi — kept both additive sides (our serde_adapters + upstream shielded_*; rehydration FFI + birth-height exports; upstream fix(platform-wallet): zeroize private keys when freeing preview rows #3797 zeroize-on-preview-free).
  • One semantic conflict fixed: upstream gated PlatformWalletManager.event_manager behind #[cfg(feature = "shielded")], but rehydration reads it unconditionally — ungated the field (genuinely-shielded fields stay gated); validated by building both feature configs.

Wallet-scope build gates pass (platform-wallet-storage no-default + all-features, rs-platform-wallet default + shielded, rs-platform-wallet-ffi). Full-workspace CI dispatched against the merged head to validate the disjoint upstream dep bumps.


Update — security audit + whole-crate review remediation (2026-06-09)

Pushed 9e1248cb0a..b4a3aa9989 (31 commits): a full security audit of the secrets/** subsystem plus a whole-crate review and remediation of platform-wallet-storage. All work is verified green; the on-disk SQLite format is unshipped so V001 was edited in place (no V002).

Secrets subsystem hardening (audit → fixes)

No CRITICAL/HIGH; the crypto core verified sound (fresh per-seal XChaCha20-Poly1305 nonces, tag-before-plaintext AEAD, Argon2id bounds-gated before allocation, zeroize+mlock wrappers). Fixes landed:

  • Enabled argon2's zeroize feature (the multi-MiB KDF matrix + key-correlated intermediates were left un-wiped).
  • Added a write-side per-secret size cap — the read path capped at 128 MiB but the write path did not, so an oversized secret could brick the whole shared vault on next open.
  • Parent-directory permission check; folded the KDF header into the passphrase verify-token AAD; sealed PersistableBlob (no-key-material-in-DB enforced at the type level); converted production expect() → typed errors; added parser-fuzz + on-disk tamper tests; narrowed #![allow(unsafe_code)] to per-block with // SAFETY: notes; pinned fd-lock; acknowledged the bincode advisory.

Whole-crate review remediation — one CRITICAL fixed

  • delete_wallet data loss (CRITICAL): a pre-flush BEGIN EXCLUSIVE cross-process lock race silently dropped the buffered changeset (one error path failed to restore the slot before returning). Fixed, and the regression test was adversarially mutation-verified (revert the fix → test goes red).
  • MEDIUM: refuse a second in-process open() on the same path (AlreadyOpen); wallet-scope the identity tombstone (was cross-wallet); application_id / NotAWalletDb restore validation (foreign DB rejected, destination untouched); refinery-panic on malformed schema-history → typed SchemaHistoryMalformed; journal-mode read-back (silent WAL fallback); durability docs corrected (WAL+NORMAL is app-crash, not power-loss, durable); README / SCHEMA.md / SECRETS.md drift corrected to match shipped code; network-FS vault-lock caveat.
  • LOW: fail-hard out-of-range sync watermark; deterministic UTXO account bucketing; centralized i64→u32 safe-casts; kv/blob size-limit unified; backup retention floor; CLI SQLITE_OPEN_URI removed; bincode RUSTSEC-2025-0141 acknowledged.

QA (all green): cargo fmt --all --check; cargo check + clippy under both --no-default-features --features sqlite,cli and --all-features (-D warnings); cargo test -p platform-wallet-storage380 passed / 0 failed. The CRITICAL fix and the new negative tests were adversarially mutation-verified.


Update — downstream-build hotfix: un-gate schema readers used by production load() (2026-06-08)

9e1248cb0a fixes a cfg-gating defect that broke this crate for downstream consumers building without __test-helpers. The production seedless Persister::load() path calls schema readers — schema::wallets::{fetch,parse_network}, identities::{load_state,managed_identity_from_entry}, the asset_locks reader cluster (decode_row, AssetLocksByAccount, imports), blob::decode_outpoint, identity_keys::{decode_entry, IdentityKeyWire::into_entry} — that the test-reconciliation merge (8ba49c37) left gated behind #[cfg(any(test, feature = "__test-helpers"))]. CI passed because --all-features / cargo test enable __test-helpers; a plain dependency build with --features sqlite,cli,secrets,kv (no __test-helpers) failed with 17 "function not found" errors.

Fix: un-gate exactly the readers reached from production load() — they live in the already-sqlite-gated schema module, so they compile precisely when load() does (no kv symbol pulled into a sqlite-only path, no DDL/V001 touched). One genuinely-dead asset_locks::list_active (zero callers, new at this rev) was gated instead, which keeps both clippy unions clean simultaneously. The in-memory changeset field cs.wallet_metadata is a different concept and is untouched.

QA (all green): cargo fmt --all --check; cargo check -p platform-wallet-storage --no-default-features --features sqlite,cli,secrets,kv (the downstream repro — 0 errors); clippy -p platform-wallet-storage under both --all-features and --no-default-features --features sqlite,cli (-D warnings); cargo test -p platform-wallet-storage339 passed / 0 failed.


Update — #3625 (storage crate + wallet_metadatawallets rename) propagated in (2026-06-08)

Propagated the current #3625 tip (c9d1c81) into this branch via merge f69746d5 plus test-reconciliation 8ba49c37. Standard bottom-up parent-into-child propagation — #3625 is under review and is not modified; the new storage-crate work rides into the stack here.

What's new in this propagation:

  • SQLite root table renamed wallet_metadatawallets — disambiguates the root wallets table from the meta_* metadata family. Internal to platform-wallet-storage (SQL identifiers + the schema::wallets module, FK targets, both cascade triggers). The in-memory changeset domain field cs.wallet_metadata / WalletMetadataEntry / FFI on_persist_wallet_metadata_fn is a different concept and is deliberately untouched.
  • Storage hardening (from a data-model/schema audit): dropped redundant idx_identity_keys_identity (left-prefix of the PK); renamed identities.wallet_indexidentity_index (matches its source field); fixed the platform-address load count to count only reconstructed rows (+ regression test); corrected the migration-drift-fingerprint doc (it is intentionally content-blind to allow in-place V001 edits pre-release) and the CHECK-column counts in SCHEMA.md.
  • Conflicts resolved: the two recurring stack conflicts KEEP-BOTH (rs-platform-wallet/src/changeset/mod.rs serde + shielded modules; rs-platform-wallet-ffi/src/manager.rs rehydration FFI + birth-height exports), plus 7 storage-crate conflicts and 2 semantic overlaps (an orphaned schema::wallet_metawallets reference in the new load() loop; behavioral test reconciliation against feat(platform-wallet)!: add platform-wallet-storage crate (sqlite persister) #3625's stricter schema).

QA (all green): cargo fmt --all --check; clippy -p platform-wallet-storage under both --all-features and --no-default-features --features sqlite,cli (-D warnings); clippy -p platform-wallet -p platform-wallet-ffi (-D warnings); cargo test -p platform-wallet-storage --all-features339 passed / 0 failed / 3 ignored; cargo test -p platform-wallet -p platform-wallet-ffi295 passed / 0 failed / 5 ignored.

Stack note (supersedes the older "review against #3672" note below): #3672 (secrets) is now merged. This PR's base is feat/platform-wallet-sqlite-persistor (#3625) — review the diff against #3625.


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 30 commits May 11, 2026 12:24
New workspace crate `platform-wallet-sqlite` implementing the
`PlatformWalletPersistence` trait against a bundled SQLite backend, plus
a `platform-wallet-sqlite` maintenance CLI.

Highlights
- Per-wallet in-memory buffer with `Merge`-respecting `store` + atomic
  per-wallet `flush` (one SQLite transaction per call).
- `FlushMode::{Immediate, Manual}` with `commit_writes` aggregating
  dirty wallets in deterministic order.
- Online backup via `rusqlite::backup::Backup::run_to_completion`,
  source-validating `restore_from`, `prune_backups` retention with
  AND-semantics, automatic pre-migration and pre-delete backups (with
  typed `AutoBackupDisabled` refusal when `auto_backup_dir = None`).
- Refinery-driven barrel migrations under `migrations/`; FK enforcement
  emulated with triggers because barrel's column builder doesn't emit
  composite-key `FK` clauses portably on SQLite.
- `delete_wallet` cascade with `DeleteWalletReport`; `inspect_counts`
  surface for the CLI.
- CLI: `migrate`, `backup`, `restore`, `prune`, `inspect`,
  `delete-wallet` with `--yes` destructive-op guards, humantime
  retention parsing, and stdout/stderr/exit-code conventions matching
  the spec.
- 52 tests across 8 files plus compile-time assertions cover every
  FR/NFR except the ones blocked on upstream `serde`/`bincode`
  derives or a `Wallet::from_persisted` constructor (tracked in
  TODOs in `persister.rs::load` and the test modules' module-docs).

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

Phase 2.2 fix wave — addresses Adams' BLOCK findings.

- PROJ-001: add `platform-wallet-sqlite` to both `--package` lists in
  `tests-rs-workspace.yml` (coverage run and the Ubuntu 4-shard
  fallback) so CI actually executes the crate's tests.
- PROJ-002: append `packages/rs-platform-wallet-sqlite` to every
  enumerated `COPY --parents` block in the Dockerfile (the chef
  prepare stage, the artifact-build stage, and the rs-dapi stage).
  Workspace `Cargo.toml` already lists the member; chef would fail
  with "directory not found" without these copies.
- PROJ-003: allow `wallet-sqlite` in the PR-title conventional-
  scopes list (matches the existing `feat(wallet-sqlite): …` commit).
- PROJ-004: align `dash-sdk` feature flags with sibling
  `rs-platform-wallet` (`dashpay-contract`, `dpns-contract`); document
  why `dpp`, `dash-sdk`, and `bincode` are direct deps (they're
  actually used — Adams' "unused" claim was wrong for all three);
  drop the redundant `serde` feature from bincode.
- PROJ-005: gate `lock_conn_for_test` and `config_for_test` behind
  `cfg(any(test, feature = "test-helpers"))` plus a new
  `test-helpers` dev feature; the crate's own `[dev-dependencies]`
  self-include now activates it for integration tests, so downstream
  consumers cannot reach the helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2.2 fix wave — addresses Diziet, Marvin, Smythe, Trillian BLOCKs.

Library
- D-01: new `SqlitePersister::delete_wallet_skip_backup(wallet_id)`
  entry point that intentionally skips the auto-backup. The CLI's
  `--no-auto-backup` now uses it instead of mutating
  `auto_backup_dir` to `None` (which collided with the
  `AutoBackupDisabled` refusal path and silently broke the flag).
- D-02: `delete_wallet` checks `wallet_metadata` existence BEFORE
  running the auto-backup. Refusing on an unknown wallet id no
  longer leaves an orphaned `.db` in the auto-backup directory.
- D-03: `restore_from` try-acquires an exclusive file lock on the
  destination via `fs2::FileExt::try_lock_exclusive` and raises
  `RestoreDestinationLocked` if the file is held. Falls through on
  filesystems without advisory locking.
- D-04: `restore_from` reads the source DB's max
  `refinery_schema_history.version` and raises
  `SchemaVersionUnsupported { found, expected_range }` when it
  exceeds the highest embedded migration version.
- SEC-001: `restore_from` stages via
  `tempfile::NamedTempFile::new_in(parent)` plus `persist`. The
  previous predictable `<dest>.db.restore-tmp` filename was a
  symlink-plant TOCTOU window.
- DOC-007 / DOC-008: rustdoc on `RetentionPolicy` explains the
  AND-semantics; `DeleteWalletReport.backup_path` documents that
  `None` ONLY happens via the new skip-backup entry point.

CLI
- D-05: `-v`/`-vv`/`-vvv`/`-q` wired to a `tracing_subscriber::fmt`
  subscriber that writes to stderr with an `EnvFilter` defaulted
  from the flag count (`warn` / `info` / `debug` / `trace`); `-q`
  forces `error`.
- `delete-wallet --no-auto-backup` now routes through
  `delete_wallet_skip_backup` and prints empty stdout (no backup
  path) with the `warning: auto-backup skipped (--no-auto-backup)`
  line on stderr.

Tests
- QA-001: new TC-023 in `tests/buffer_semantics.rs` — registers a
  `commit_hook` on the write connection (rusqlite `hooks` feature),
  then drives a flush whose changeset touches `core_sync_state`,
  `wallet_metadata`, and `token_balances`. The hook MUST fire
  exactly once. Atomicity is now empirically verified.
- QA-008: `tests/load_reconstruction.rs::tc043_*` rewritten to
  store non-empty `ContactChangeSet` and `TokenBalanceChangeSet`
  payloads (the previous Defaults were `is_empty()` and got
  skipped by the buffer). The test now reopens the persister,
  directly SQL-queries `contacts_sent` and `token_balances` rows,
  and asserts `ClientStartState.platform_addresses` stays empty.
- SEC-006: new `tests/secrets_scan.rs` greps every file under
  `src/schema/` and `migrations/` for the substrings `private`,
  `mnemonic`, `seed`, `xpriv`, `secret`. A small allow-list lets
  doc comments mention the boundary while catching genuine slips.

Docs
- DOC-002: README CLI synopsis adds an explicit sentence about
  `--yes` being REQUIRED for destructive subcommands, plus a
  logging-flag blurb.
- DOC-016: new per-crate `CHANGELOG.md` with `[Unreleased]` section
  enumerating the additions and security fixes from this fix wave
  (the workspace CHANGELOG is generated from Conventional Commits).
- SECRETS.md audit-hooks section updated to point at
  `tests/secrets_scan.rs` and the TC-082 lint test by file:line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a new `serde` Cargo feature on `platform-wallet`. When enabled,
every type carried in a `PlatformWalletChangeSet` gains
`serde::Serialize` / `serde::Deserialize` derives via
`#[cfg_attr(feature = "serde", derive(...))]`:

- `CoreChangeSet`, `IdentityChangeSet`, `IdentityEntry`,
  `IdentityKeysChangeSet`, `IdentityKeyEntry`,
  `IdentityKeyDerivationIndices`, `ContactChangeSet`,
  `ContactRequestEntry`, `SentContactRequestKey`,
  `ReceivedContactRequestKey`, `PlatformAddressChangeSet`,
  `PlatformAddressBalanceEntry`, `AssetLockChangeSet`,
  `AssetLockEntry`, `TokenBalanceChangeSet`,
  `WalletMetadataEntry`, `AccountRegistrationEntry`,
  `AccountAddressPoolEntry`, and the top-level
  `PlatformWalletChangeSet`.
- Per-identity / DashPay leaf types referenced inside those
  changesets: `BlockTime`, `IdentityStatus`, `DpnsNameInfo`,
  `DashPayProfile`, `ContactRequest`, `EstablishedContact`,
  `PaymentEntry`, `PaymentDirection`, `PaymentStatus`,
  `AssetLockStatus`.

The feature activates `key-wallet/serde` (which transitively flips
`dashcore/serde` and `dash-network/serde`) so every upstream leaf
type already wired with `#[cfg_attr(feature = "serde", ...)]`
(TransactionRecord, Utxo, InstantLock, AccountType, AddressInfo,
AddressPoolType, ExtendedPubKey, Network) round-trips cleanly.

Two upstream types lack their own serde feature and use
`#[serde(with = ...)]` adapters in the new
`src/changeset/serde_adapters.rs` module:
- `AssetLockFundingType` (key-wallet, no `serde` derive) — encoded
  as a stable u8 tag matching the prior hand-rolled blob layout.
- `AddressFunds` (dash-sdk re-export, no serde derive) — encoded
  as a `(nonce, balance)` shadow struct.

One field is marked `#[serde(skip)]`:
- `CoreChangeSet::addresses_derived` carries
  `key_wallet_manager::DerivedAddress`, which has no serde derive
  AND no `key-wallet-manager/serde` feature to activate. The
  breadcrumb is written to a typed table by persisters, not via a
  changeset blob, so skipping costs nothing.

`cargo build -p platform-wallet` (no features) and
`cargo build -p platform-wallet --features serde` both build
clean. `cargo test -p platform-wallet` passes (8 lib tests, 121
integration tests) with and without the new feature. The change
is opt-in; the default-feature build is byte-identical to its
prior shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…allet-storage and restructure for future secrets submodule

PURE rename + restructure — no functional code changes. Carves out a
spot for a future `SecretStore` (sketched in `SECRETS.md`) to land
as a `secrets` submodule inside the same crate, rather than a
separate `platform-wallet-secrets` crate.

Crate metadata
- Cargo package name: `platform-wallet-sqlite` → `platform-wallet-storage`.
- Crate directory: `packages/rs-platform-wallet-sqlite/` →
  `packages/rs-platform-wallet-storage/`.
- Binary name: `platform-wallet-sqlite` → `platform-wallet-storage`.

Module layout
- Everything SQLite-related is now under `src/sqlite/`:
  `mod.rs` (new — re-exports the submodules), `persister.rs`,
  `buffer.rs`, `config.rs`, `error.rs`, `migrations.rs`, `backup.rs`,
  and `schema/`. The `migrations/` Rust-file directory stays at the
  crate root because `refinery::embed_migrations!` resolves its path
  relative to `Cargo.toml`.
- `src/lib.rs` exposes `pub mod sqlite;` plus root re-exports of the
  common types (`SqlitePersister`, `SqlitePersisterConfig`,
  `FlushMode`, `SqlitePersisterError`, `RetentionPolicy`,
  `PruneReport`, `DeleteWalletReport`, `AutoBackupOperation`,
  `JournalMode`, `Synchronous`) so most consumer imports stay
  identical — only the crate name in `Cargo.toml` changes for them.
  A `// pub mod secrets;` marker reserves the future module slot.

Cargo features
- `sqlite` (default) — enables the SQLite persister + every backend-
  specific optional dep (`rusqlite`, `refinery`, `barrel`, `dpp`,
  `dash-sdk`, `key-wallet`, `key-wallet-manager`, `dashcore`,
  `bincode`, `fs2`, `tempfile`, `chrono`, `sha2`).
- `cli` (default) — enables the maintenance binary; implies `sqlite`.
- `secrets` — reserved, no code yet.
- `test-helpers` — crate-private accessors (unchanged semantics);
  now implies `sqlite`.
- `cargo build -p platform-wallet-storage --no-default-features`
  builds the bare crate cleanly (verified).

Tests
- Renamed `tests/<name>.rs` → `tests/sqlite_<name>.rs` (9 files) so
  the future `secrets_<name>.rs` files won't collide. `secrets_scan.rs`
  and `tests/common/` keep their names.
- `secrets_scan.rs` updated to scan `src/sqlite/schema/` (the new
  location of the schema writers) and `migrations/`. Carved out
  `src/secrets/` from the scan up front — that future submodule WILL
  legitimately contain the words `private`, `mnemonic`, `seed`.

Workspace integration
- `Cargo.toml` workspace `members` entry renamed.
- `Dockerfile`: three `COPY --parents` blocks updated.
- `.github/workflows/tests-rs-workspace.yml`: two `--package` lines
  updated.
- `.github/workflows/pr.yml`: added `wallet-storage` alongside the
  existing `wallet-sqlite` allow-list entry (both coexist so PRs
  pending against either name pass).

Gate output
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean.
- `cargo build -p platform-wallet-storage --no-default-features` clean.
- `cargo build -p platform-wallet-storage --bin platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 54 tests, 0 failures.
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.
- `cargo check --workspace --offline` clean.
- `cargo metadata` no longer exposes the old `platform-wallet-sqlite`
  package name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hand-rolled encoder

Replace the hand-rolled `BlobWriter` / `BlobReader` plumbing under
`src/sqlite/schema/` with a single `bincode::serde::encode_to_vec`
call per row, acting on the serde-derived changeset types in
`platform-wallet` (enabled via that crate's `serde` feature, added in
the preceding commit). The encoder swap is the technical-debt cleanup
the workflow-feature plan called for.

Wire format
- Every `_blob` column now starts with a 1-byte schema-revision tag
  (`blob::BLOB_REV = 1`) followed by the bincode-serde body. The tag
  lets future migrations swap encoders without losing existing rows;
  unknown revisions surface as `SqlitePersisterError::Serialization`.
- `blob::encode<T: Serialize>` and `blob::decode<T: DeserializeOwned>`
  are the only public entry points; the previous per-field
  `u8/u32/u64/bytes/opt_*/str` walker is gone.
- The outpoint helpers (`encode_outpoint` / `decode_outpoint`) stay
  in `blob.rs` because outpoints serve as primary-key fragments —
  they were never `_blob` payloads to begin with.

Per-schema-file delta
- `accounts.rs`: dropped the manual `BlobWriter` for both
  `AccountRegistrationEntry` and `AccountAddressPoolEntry`; each row
  now encodes the full entry via `blob::encode`. Schema-stable typed
  columns (`account_type`, `account_index`, `pool_type`) still mirror
  the entry for direct SQL lookups.
- `asset_locks.rs`: collapsed the funding-type-tag / tx-consensus /
  proof-bincode three-part hand-rolled blob into a single
  `blob::encode(&AssetLockEntry)` call. `funding_type` rides through
  the new `platform_wallet::changeset::serde_adapters::asset_lock_funding_type`
  adapter; `Transaction` and `AssetLockProof` round-trip via their
  own serde derives. ~30 LOC removed.
- `contacts.rs`: each `_blob` cell now stores the
  `ContactRequestEntry` / `EstablishedContact` directly.
- `core_state.rs`: `core_transactions.record_blob` now encodes the
  full `TransactionRecord`; `core_instant_locks.islock_blob` encodes
  the `InstantLock` via dashcore's serde derive (which was always
  there, gated on `dashcore/serde` — flipped on by `platform-wallet/
  serde`). The placeholder-record decoder gymnastics in
  `get_tx_record` collapse into a one-line `blob::decode` call.
- `dashpay.rs`: `dashpay_profiles.profile_blob` encodes the whole
  `DashPayProfile`; `dashpay_payments_overlay.overlay_blob` encodes
  each `PaymentEntry`.
- `identities.rs`: `entry_blob` encodes the full `IdentityEntry`;
  new `fetch` helper for tests.
- `identity_keys.rs`: dpp's `IdentityPublicKey` uses
  `serde(tag = "$formatVersion")` which bincode-serde's
  `deserialize_any` requirement can't navigate. Solution: an
  in-crate wire shape (`IdentityKeyWire`) pre-encodes that one field
  via dpp's native `bincode::Encode/Decode` derives while everything
  else stays on bincode-serde. Same "one blob per row" property; one
  layer of indirection for the offending field.

Unblocked tests (Marvin's previously-deferred TC-002..TC-014)
- TC-007 — `IdentityKeyEntry` round-trip including the public key,
  hash, and DIP-9 derivation breadcrumbs; plus an inline NFR-10
  substring scan that asserts the blob contains no
  `private`/`mnemonic`/`seed`/`xpriv` ASCII.
- TC-009 — `PlatformAddressBalanceEntry` round-trip including the
  `AddressFunds` (via the `address_funds` serde adapter).
- TC-010 — `AssetLockEntry` round-trip including the embedded
  `Transaction`, `AssetLockFundingType` (via the
  `asset_lock_funding_type` adapter), and `AssetLockStatus`.
- TC-012 — `DashPayProfile` + `PaymentEntry` round-trip through the
  dashpay tables.
- TC-014 — `AccountRegistrationEntry` round-trip including the
  full `ExtendedPubKey` (via key-wallet's serde derive).

Gate output
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean.
- `cargo build -p platform-wallet-storage --no-default-features` clean.
- `cargo build -p platform-wallet-storage --bin platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 60 tests, 0 failures (up
  from 54 before this commit; +5 new TCs in
  `sqlite_persist_roundtrip.rs` plus +1 in the blob.rs lib-test
  suite).
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.
- `cargo check --workspace --offline` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion version for forward-compat

The refinery migration version on the database already gates schema
evolution at the right granularity — every row in every `_blob`
column is written by code at the same revision, so a per-blob
revision byte was redundant.

Changes
- `src/sqlite/schema/blob.rs`: remove the `BLOB_REV` constant and
  its prepend / strip logic. `encode<T>` is now a one-line wrapper
  over `bincode::serde::encode_to_vec`; `decode<T>` is the matching
  pair over `decode_from_slice`. Net: ~30 LOC dropped from the
  module.
- Drop the two unit tests (`decode_rejects_unknown_rev`,
  `decode_rejects_empty_blob`) that exercised the rev-tag logic
  exclusively — the behaviour they covered no longer exists. The
  `encode_decode_roundtrip` and `outpoint_roundtrip` tests stay.
- `src/sqlite/schema/mod.rs`: update the module-level encoding-policy
  doc to drop the "1-byte schema-rev tag" framing and explain that
  schema evolution is gated by the refinery migration version
  instead.
- `src/sqlite/schema/asset_locks.rs`: drop the analogous comment
  about the rev tag in that module's header.

`encode_outpoint` / `decode_outpoint` are untouched — they're a
separate concern (typed-column primary-key encoding, fixed layout
for indexed lookups, never blob payloads).

Migration concern: NONE. The crate is unreleased; no existing on-disk
`.db` files carry the BLOB_REV byte. Anyone with a wallet-storage
test database between the previous commit and this one needs to
delete it — flagged in the workspace CHANGELOG.

Gate
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean.
- `cargo build -p platform-wallet-storage --bin platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 58 tests, 0 failures
  (down from 60: the two dropped tests were rev-tag-specific).
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `prune` subcommand returns to the unconditional shape: walk the
backup directory, apply the retention policy, unlink, print removed
paths to stdout. Operators who want a preview can list the directory
themselves before running.

Changes
- `src/bin/platform-wallet-storage.rs`: drop the `dry_run: bool`
  field on `PruneArgs`, the `if args.dry_run { ... }` branch in
  `run_prune`, and the `list_backup_dir_for_dry_run` helper (only
  caller was the dry-run branch).
- `README.md`: trim `[--dry-run]` from the `prune` synopsis line.
- `CHANGELOG.md`: note the flag removal in `[Unreleased]`.

No CLI smoke test referenced `--dry-run`, so the 58-test count is
unchanged. Gate is clean: fmt / build / bin build / 58 tests / clippy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…allet-storage rename

PROJ-002: `CoreChangeSet.addresses_derived` doc block referenced
`rs-platform-wallet-sqlite::schema::core_state`, the path the crate
had before `8e0830626d` renamed it to `rs-platform-wallet-storage`
and regrouped the module layout under `sqlite/`. The rename swept
every import + Cargo.toml + workflow file but missed this single
doc-string in the sister crate, which a grep-driven reader would
follow to a dead path.

Replace with the current canonical path:
`platform_wallet_storage::sqlite::schema::core_state`.

No code change. No test change. Independently cherry-pickable into
the future upstream PR alongside `e26945cfdf` (the original
serde-feature commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Error, atomic variants, propagate SQL errors

Atomic-variant error type per the dash-evo-tool error pattern
(`~/git/dash-evo-tool/CLAUDE.md` §Error messages): every variant
carries the upstream error via `#[source]` (or `#[from]` when the
conversion is the only thing the trait does), never via a
stringified copy. Variants do not contain user-facing-prose
`String` fields — the `#[error("...")]` attribute provides the
renderable `Display` form, the typed fields carry diagnostics.

Resolves CODE-002, SEC-002, PROJ-001, CODE-004, CODE-008 (partial),
SEC-001 (library half — CLI half in Commit D). Annotates CODE-001
with INTENTIONAL per triage decision.

Error type
- `SqlitePersisterError` → `WalletStorageError`. The old name lives
  as a `#[deprecated]` type alias so existing callers compile during
  the migration; tests in this crate already use the new name.
- Split `Sqlite` callers into `IntegrityCheckRunFailed`,
  `SourceOpenFailed`, and the generic `Sqlite { source }`. The
  `IntegrityCheckFailed { check_output: String }` variant becomes
  `IntegrityCheckFailed { report: String }` — the SQLite-returned
  diagnostic text is not a user-facing message; the rename
  clarifies that.
- `Serialization(String)` (a stringified bincode error) split into
  `BincodeEncode { source: bincode::error::EncodeError }`,
  `BincodeDecode { source: bincode::error::DecodeError }`, and
  `BlobDecode { reason: &'static str }` for typed-column structural
  errors. `&'static str` is acceptable per the policy — it's a
  compile-time identifier, not a user message.
- `InvalidWalletId(String)` split into `InvalidWalletIdHex { source:
  hex::FromHexError }` and `InvalidWalletIdLength { actual: usize }`.
- `ConfigInvalid(&'static str)` → `ConfigInvalid { reason: &'static str }`.
- `SchemaVersionUnsupported { found: i64, expected_range: String }`
  → `SchemaVersionUnsupported { found: i64, max_supported: i64 }`.
- New variants: `HashDecode { source: dashcore::hashes::Error }`,
  `ConsensusCodec { source: dashcore::consensus::encode::Error }`,
  `IntegerOverflow { field: &'static str, value: u64, target:
  SafeCastTarget }`, `LoadIncomplete { unimplemented: &'static
  [&'static str] }`.
- `From` impls added for every typed source so `?`-style propagation
  works at every writer / reader boundary.
- `From<WalletStorageError> for PersistenceError` renders the full
  `#[source]` chain via a private `DisplayChain` helper instead of
  losing the inner-error context to a single `Display` call.

Safe-cast helper (SEC-002)
- New module `src/sqlite/util/safe_cast.rs` with `u64_to_i64(field:
  &'static str, value: u64) -> Result<i64, WalletStorageError>` and
  the inverse. Every durable-boundary cast in writers/readers now
  routes through these — schema/platform_addrs (balance, sync_height,
  sync_timestamp, last_known_recent_block, nonce, account_index,
  address_index), schema/asset_locks (amount_duffs, account_index),
  schema/token_balances (balance), schema/core_state (utxo.value,
  utxo.height, account_index), schema/identities (no u64 columns —
  identity_index is u32, uses `i64::from`).
- Lossless `u32 → i64` casts swapped to `i64::from(...)` so static
  conversions stay clearly distinct from fallible-cast sites.

Error propagation (CODE-002)
- Every `query_row(...).unwrap_or(default)` that previously
  swallowed real SQL errors (busy-timeout, corrupt, decode) now
  uses `.optional()?.unwrap_or(default)` — `optional()?` collapses
  ONLY the genuine "no rows returned" case into `None`; every other
  error propagates as `WalletStorageError::Sqlite`.
- `current_schema_version` and `count_pending` now return
  `Result<_, WalletStorageError>` instead of swallowing into
  `Option`. Migrate / open paths surface those errors instead of
  silently re-running every migration on a corrupt schema-history.
- `delete_wallet_inner` existence check + per-table row-count
  queries use `.optional()?` so a corrupt child table fails loudly
  instead of reporting 0 rows removed.

Auto-backup dedup (CODE-004)
- `run_auto_backup` extracted as a standalone function in
  `persister.rs`. Both the open-time (`PreMigration`) and library-
  time (`PreDelete`, new `PreRestore`) paths call it. The previous
  `unreachable!("OpenMigration not callable via run_auto_backup")`
  branch is gone — there is no longer a closed-over self that
  prevents the open path from reusing the helper.
- `BackupKind::PreRestore` variant added; `is_backup_file` /
  retention recognise the `pre-restore-` prefix.

LoadIncomplete (PROJ-001)
- `LOAD_UNIMPLEMENTED: &[&str]` pub-const lists the
  `ClientStartState` field paths the persister does not yet
  reconstruct (`["ClientStartState::wallets"]` today).
- Trait-impl `load()` rustdoc explicitly documents the partial-
  reconstruction caveat at the top, points at `LOAD_UNIMPLEMENTED`,
  and emits a `tracing::warn!` on every call until the upstream
  `Wallet::from_persisted` lands.
- New `WalletStorageError::LoadIncomplete` variant exists for
  callers that want to surface the gap as a typed value (not
  returned from `load` itself per the trait contract — see rustdoc).

restore_from auto-backup (SEC-001 library half)
- `SqlitePersister::restore_from(dest, src, auto_backup_dir)` —
  takes a pre-restore auto-backup of the live destination before
  staging the source over it. Refuses with
  `AutoBackupDisabled { operation: Restore }` when `auto_backup_dir`
  is `None`. New `SqlitePersister::restore_from_skip_backup(dest,
  src)` for the CLI's `--no-auto-backup` flag (added to RestoreArgs
  here for the corresponding CLI surface).
- `backup::restore_from` keeps the source-validation +
  destination-lock + staged-tempfile + atomic-persist shape; the
  pre-restore backup is taken by the persister's `_inner` before
  calling into `backup::restore_from`. (SEC-004 — staged-tempfile
  integrity recheck + chmod 600 — also lands in this commit.)

Write probe (CODE-008)
- `ensure_dir`'s predictable `.platform-wallet-storage-write-probe`
  filename replaced by `tempfile::NamedTempFile::new_in(dir)` —
  unguessable name per probe, no race against concurrent persister
  opens.

CODE-001 INTENTIONAL annotation
- Inline comment on the `Mutex<Connection>` declaration documents
  the accept-risk decision: single connection serializes reads
  through the write lock, acceptable for current per-wallet
  workload, revisit if read contention becomes measurable.

Test sweep
- Every `tests/sqlite_*.rs` file migrated from `SqlitePersisterError`
  to `WalletStorageError`. The deprecated alias still resolves but
  emits `#[deprecated]` warnings under `-D deprecated`; live code
  uses the new name. Restore tests call
  `SqlitePersister::restore_from_skip_backup` to avoid threading an
  `auto_backup_dir` through fixture helpers.

Gate
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean (default features).
- `cargo build -p platform-wallet-storage --bin platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 62 tests, 0 failures
  (+4 from new safe_cast unit tests).
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.

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

SEC-003: V001 emulates FK INSERT parent-existence + AFTER-DELETE
cascade via triggers but doesn't cover `UPDATE wallet_id` on
`wallet_metadata` or `UPDATE identity_id` on `identity_keys` /
`dashpay_profiles`. The persister's own writers never mutate those
columns, but if a future migration accidentally introduces such an
UPDATE the result is silent orphaning of child rows. New migration
`V002__defensive_update_triggers.rs` installs `BEFORE UPDATE OF
<id>` triggers on each that raise the canonical
`RAISE(ABORT, 'FOREIGN KEY constraint failed')` — same idiom V001
uses for the parent-existence check, so downstream string matching
stays stable.

V001 stays untouched per the append-only migration policy.

Also: `build.rs` emits `cargo:rerun-if-changed` for each file under
`migrations/`. `refinery::embed_migrations!` is a proc-macro
evaluated at compile time; Cargo doesn't track file-system reads
inside proc macros, so without this build-script directive,
adding/editing a migration file fails to trigger a rebuild of the
embedded list. Discovered while wiring V002 — `tc025` failed
against a stale cache until `migrations.rs` was manually touched.
The build-script closes that gap.

Gate
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 62 tests, 0 failures.
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…caping, scope allow-list, stable enum labels, docs)

Closes the cleanup batch from the Phase-2.8 triage report:
PROJ-003, PROJ-004, SEC-005, SEC-006, CODE-003, DOC-002, DOC-005,
plus a related DOC-001 correction (FK README claim).

PROJ-003 — Remove `wallet-sqlite` from `.github/workflows/pr.yml`.
The three historical commits using that scope are already on the
branch; future commits in this crate use `wallet-storage`. No
reason to keep a deprecated name in the allow-list.

PROJ-004 — Delete `packages/rs-platform-wallet-storage/CHANGELOG.md`.
The user explicitly stated we don't maintain per-crate CHANGELOGs;
the workspace-level CHANGELOG.md is generated from Conventional
Commits and remains the single source of truth.

SEC-005 — Delete the substring-scan block in
`tests/sqlite_persist_roundtrip.rs::tc007_identity_key_entry_roundtrip`.
bincode wire bytes carry no field names, so the substring scan
against `public_key_blob` conveyed intent but enforced nothing.
The load-bearing NFR-10 check is `tests/secrets_scan.rs`, which
greps schema source files. Comment in tc007 redirects readers
there.

SEC-006 — Replace hand-rolled JSON in `run_inspect --format json`
with `serde_json::json!`. `serde_json` added as an optional dep
gated by the `cli` feature. Today's input is safe (table names are
compile-time identifiers; wallet ids are hex), but any future
addition that flows user-controlled bytes into the printer would
break the previous escape-less `print!`.

CODE-003 — `format!("{:?}", entry.account_type)` /
`format!("{:?}", entry.pool_type)` replaced with new pub(crate)
helpers `account_type_db_label(&AccountType) -> &'static str` and
`pool_type_db_label(&AddressPoolType) -> &'static str` in
`schema/accounts.rs`. Both are exhaustive `match` expressions —
adding a variant upstream fails to compile here, forcing an
explicit label decision rather than silent `Debug`-format drift.
`schema/core_state.rs` (derived-addresses writer) uses the same
helpers.

DOC-002 — `tests/secrets_scan.rs` docstring updated: scan path is
`src/sqlite/schema/` not `src/schema/`. Explicitly carves out files
in `src/sqlite/` outside `schema/` plus the future `src/secrets/`
slot as out-of-scope.

DOC-005 — README `--no-default-features` paragraph rewritten:
factual description of what the bare crate provides today (nothing
public), no future-feature framing per user's "no future
placeholders" rule.

DOC-001 (bonus correction) — README schema section updated to
reflect V002's defensive UPDATE triggers. The previous "identical
to native FKs" claim was false on UPDATE before V002; with V002
landed the claim becomes accurate and the section explicitly cites
both migrations.

INTENTIONAL annotations already in place from Commits B/C —
CODE-001 (single connection serialises reads) at
`src/sqlite/persister.rs:78-84`; CODE-007 (prune fails-fast) at
`src/sqlite/backup.rs:200-204`. PROJ-005's accept-risk rationale
is captured inline above the `lock_conn_for_test` accessor at
`src/sqlite/persister.rs:299-307`.

Gate
- `cargo fmt --all -- --check` clean.
- `cargo build -p platform-wallet-storage` clean.
- `cargo build -p platform-wallet-storage --no-default-features` clean.
- `cargo build -p platform-wallet-storage --bin platform-wallet-storage` clean.
- `cargo test -p platform-wallet-storage` — 62 tests, 0 failures.
- `cargo clippy -p platform-wallet-storage --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routine forward-integration. Cargo.lock reconciliation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment-tightening pass per claudius:coding-best-practices, scoped to
PR #3625's own additions:

- sqlite_buffer_semantics.rs: drop `_unused_btreemap` placeholder + its
  "future expansion" comment. `BTreeMap` is genuinely used elsewhere in
  the file (line 301 — `balances` map), so the import stays. Removes a
  speculative-future-state comment and an empty helper that exists only
  to silence a phantom lint.
- sqlite_load_reconstruction.rs: fix stale cross-reference. Module doc
  said "tracked in a TODO in persister.rs::load", but the actual signal
  is the `LOAD_UNIMPLEMENTED` constant + tracing::warn. Replace with the
  accurate present-state pointer.

Plus a single rustfmt fix in
`packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`
that fell out of the v3.1-dev merge — the textual auto-merge produced a
3-arg call spread across 5 lines that rustfmt collapses to one line.
Not a logic change.

Rules driving the changes:
- present-state, not history (sqlite_load_reconstruction.rs)
- comment only when meaningful — dropping speculative placeholders
  (sqlite_buffer_semantics.rs)

Quality gates: `cargo fmt --all` clean, `cargo check --workspace` green,
`cargo clippy -p platform-wallet -p platform-wallet-storage --tests
--no-deps -- -D warnings` green.

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

SEC-011 (Smythe audit, MEDIUM): the restore path already applied `chmod
0o600` after writing the SQLite file (`backup.rs::restore_from`), but
the initial-create path in `SqlitePersister::open` and the
backup-create path in `backup::run_to` did not. Both relied on the
process umask, which can leave a newly created DB world- or
group-readable.

Extracts the existing inline `#[cfg(unix)]` + `Permissions::from_mode(0o600)`
block into a small helper `sqlite::util::permissions::apply_secure_permissions`
(no-op on non-Unix) and calls it at all three sites. The restore path
keeps its existing semantics — it just delegates to the helper now —
so the file mode no longer depends on the process umask anywhere a
SQLite file is created or replaced by this crate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e_cached writers, functional load() (#3643)

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…3633)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: QuantumExplorer <quantum@dash.org>
…oto messages (#3654)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c) (#3652)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
delete_wallet_inner never reconciled the in-memory buffer, so a
buffered-only wallet returned WalletNotFound, the pre-delete backup
excluded buffered writes, and a later commit_writes/flush resurrected
the just-deleted wallet's rows.

Per Nagatha ARCH-001: drain-and-discard the target's buffered changeset
FIRST via the existing Buffer::take_for_flush (no new method, no
deprecated drain alias, no restore on the delete path), then widen the
existence gate to "buffered OR persisted". The drain is unconditional
of FlushMode and runs before the skip_backup branch; locks stay
strictly sequential (buffer lock released before conn lock).

Regression tests (tests/sqlite_delete_buffer_reconcile.rs):
- buffered_only_delete_is_ok_and_no_resurrection
- pre_delete_backup_excludes_buffered_writes
- delete_unknown_wallet_is_not_found
- immediate_after_failed_flush_delete_drains_buffer

Refs: PR #3625 thread r3221229558

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

restore_from validated schema-history presence and the max-supported
version on the first source handle, then dropped it and re-read the
path for the staged copy. A swap during that window could persist an
internally-valid but forward-version / schema-history-missing DB
(validate-then-reopen TOCTOU).

Per Nagatha ARCH-002: MOVE (not duplicate) both gates off `src` and
into the existing step-5 staged block, after run_integrity_check on
the staged copy and before the block closes, reusing the same `staged`
connection (no third handle). All validation now binds to the exact
bytes being persisted. The cheap pre-staging integrity check on `src`
is retained (non-load-bearing, optional per ARCH-002 q6).

Regression tests (tests/sqlite_restore_staged_validation.rs):
- forward_version_rejected_destination_unchanged
- missing_schema_history_rejected_destination_unchanged
- valid_backup_roundtrips

Refs: PR #3625 thread r3221229556

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

blob::decode discarded bincode's bytes-consumed value, so a valid
prefix decoded successfully even when the BLOB column held trailing
garbage — silently accepting corrupt or forward-incompatible payloads
across every blob-backed column.

Compare consumed against blob.len() and return the existing typed
WalletStorageError::blob_decode on mismatch, mirroring the strict
length check in the sibling decode_outpoint.

Regression test (src/sqlite/schema/blob.rs):
- decode_rejects_trailing_bytes

Refs: PR #3625 thread r3221229573

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
lklimek and others added 20 commits June 8, 2026 17:30
…e allow

Add INTENTIONAL accept-risk annotations for the deliberate 2x vault parse
(lax version probe + strict payload, over the 128 MiB-capped one-shot
file) and the reliance on serde_json's default recursion limit (128) for
deep-nesting DoS safety (must never disable it or parse into a Value in
production).

Narrow the VaultLock unsafe island: replace the module-wide
`#![allow(unsafe_code)]` with per-item `#[allow(unsafe_code)]` on the two
`unsafe impl`s and the three raw-pointer blocks (each already carries a
`// SAFETY:` rationale), so the crate-wide `#![deny(unsafe_code)]` keeps
covering every other line. The island's soundness rests on the drop-order
argument, documented inline (no Miri test).

(Accept-risk markers use bare `// INTENTIONAL:` rather than embedding the
transient audit IDs, per the no-ephemeral-IDs-in-committed-code rule.)

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

The new vault parent-directory permission check refuses a group/other-
writable parent; a umask-0002 tempdir lands at 0o775. Tighten the
integration-test `vault_path` helper to 0o700 so the secrets_api guards
exercise the parse/version/perm paths rather than tripping the parent-dir
refusal first.

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

Address QA on the SEC-012 coverage:

- Rename the two header-tamper integration tests to `*_smoke_test` and
  reword their docs to the true property: a salt flip / in-bounds KDF-param
  shift is rejected at open because it changes the DERIVED KEY (salt + KDF
  params feed the KDF), which already fails the verify-token tag — NOT
  because of the header-AAD binding. Salt/params feed both the KDF and the
  AAD, so binding them in is attributable, structural defence-in-depth, not
  a new detection path. The binding itself is guarded by the non-vacuous
  unit test `verify_aad_binds_salt_and_kdf_params`. Removes a docstring
  that falsely claimed the store would "silently re-derive" without the
  binding.

- Extend the structural parser fuzz to mutate the header verify-token
  fields (`salt` / `verify_nonce` / `verify_ct`) to empty / short /
  over-wide / non-hex, asserting a typed Err (or Ok) and never a panic —
  closing the gap where a regression localized to those fields (e.g. an
  empty-`verify_ct` index panic) could ship green.

- Drop the remaining ephemeral review-ID tokens from two doc comments.

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

delete_wallet_inner drains the buffer into a local `cs`, then opens its
pre-flush tx via `conn.transaction_with_behavior(Exclusive)?`. That `?`
propagates a BUSY/LOCKED failure (a real cross-process peer-lock
condition) WITHOUT returning `cs` to `drained_slot`, so the outer
restore_buffer finds an empty slot and the buffered changeset is lost.
The existing pre_flush_failure test only exercises the test-injector
branch (which restores the slot), masking the production `?` path.

These two tests drive the real `?`-propagation path under a peer
EXCLUSIVE lock and assert the documented invariant ("on ANY pre-commit
error, restore_buffer hands the changeset back"). They FAIL against
current code, demonstrating the data loss; they are not adjusted to the
buggy behaviour.

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

Adds QA coverage for the previously-untested identities::apply removed-set
branch. Pins three behaviours: a tombstoned identity is excluded from the
per-wallet load_state while siblings survive; a re-upsert clears the
tombstone; and the tombstone UPDATE is NOT scoped by wallet_id (documents
the asymmetry vs the upsert path's strict per-entry wallet cross-check).

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

Two previously-uncovered invariants get regression tests:

1. Money/balance read-path integer-cast safety. safe_cast guarantees
   a negative i64 stored in a value-bearing column aborts the read with
   IntegerOverflow rather than sign-extending into a huge u64 balance.
   This was asserted only for birth_height/sync_height, not for the
   genuine money columns. New tests pin platform_addresses.balance (via
   the production load() path) and core_utxos.value.

2. commit_writes continue-and-collect: the LockPoisoned short-circuit
   that fills still_pending had no automated coverage (TODO(qa) in
   error.rs). A deterministic test via force_next_flush_to_fail drives
   PersistenceError::LockPoisoned on the first sorted wallet and asserts
   the offender lands in failed while the unattempted wallets land in
   still_pending and are not flushed.

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

Close a coverage gap around backup::backup_timestamp and prune retention:

- Unit-test backup_timestamp over ALL four BackupKind filename shapes
  (wallet-, pre-migration-<from>-to-<to>-, pre-delete-<64hex>-,
  pre-restore-), asserting the EMBEDDED YYYYMMDDTHHMMSSZ token is
  extracted rather than the mtime fallback. The rsplit('-').next()
  extraction is hand-coupled to those shapes; the test guards against a
  future label whose trailing variable segment shifts which token the
  parser sees. A companion test pins that a trailing non-timestamp
  segment returns None (detectable mtime fallback, not a silent
  wrong-token read).

- Add TC-056: prune is content-blind and orders by the embedded
  filename timestamp, proven by inverting mtime vs embedded order. With
  keep_last_n=1 over the auto dir, a pre-delete safety backup is
  silently evicted when it is not newest-by-embedded-ts. Pins the
  documented contract that the auto dir is not a protected vault, so a
  future "protect safety backups" change is a conscious decision.

No production behavior change; tests only.

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

Closes a coverage gap: the existing FK suites only probe the schema via
raw SQL (lock_conn_for_test), never the production store() ->
apply_changeset_to_tx dispatch path with a PARTIAL changeset that carries
a child without its parent.

Asserts, through the real flush path under immediate-FK semantics:
- a child (identity_keys) whose identities parent is absent from both the
  payload and the DB aborts with a Constraint-kind PersistenceError (FK
  reachable via Error::source()), not a panic or silent success;
- the constraint abort is non-transient, so the buffer is WIPED (no
  silent retry) in both Immediate and Manual flush modes — the documented
  caller contract: include the parent in the same store();
- a complete changeset carrying parent+child (and the wallets anchor +
  children) commits, proving the fixed dispatch order writes parent
  before child for the wallet_id and identity_id FK edges.

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

delete_wallet drained the buffered changeset into a local, then opened the
pre-flush EXCLUSIVE tx with a bare `?`. A recoverable SQLITE_BUSY/LOCKED on
that tx propagated without putting the changeset back, so the outer
restore_buffer found an empty slot and the pending wallet state vanished
silently — permanent data loss under the routine cross-process contention the
busy_timeout/EXCLUSIVE design exists to handle. Mirror the sibling apply/commit
branches: set the slot back before returning the error.

Regression: tests/sqlite_delete_partial_commit_window.rs now GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e read-back + schema-history probe

Harden the open/restore trust boundary against foreign, corrupted, and
silently-downgraded SQLite inputs:

- PWS-008: stamp PRAGMA application_id ("PLWT" magic) in V001 and assert
  it in open() pre-migration and restore_from's source + staged
  validation. A foreign refinery-versioned DB is rejected as NotAWalletDb
  before it can be migrated in place or persisted over the live wallet.
- PWS-007: probe refinery_schema_history for well-formed RFC3339
  applied_on + numeric checksum before refinery runs, returning typed
  SchemaHistoryMalformed instead of letting refinery unwrap()-panic.
- PWS-003: read back PRAGMA journal_mode after setting WAL and error with
  JournalModeNotApplied on a silent fallback (WAL->DELETE) — mirrors the
  foreign_keys read-back discipline.

New typed variants are classified in is_transient/error_kind_str (the
wildcard-free taxonomy) and exercised by the error-classification table.
Regression: tests/sqlite_wallet_db_identity.rs (foreign application_id
rejected + destination untouched; malformed schema_history fails typed).

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

PWS-004: two SqlitePersister handles on the same canonical path each own
an independent Mutex<Connection> and write buffer, so a Manual-mode
buffered write on one is invisible to the other's load() — silent state
divergence. Add a process-wide registry of canonicalized open paths;
open() claims the path last (after all fallible setup) and returns typed
AlreadyOpen on a second open; Drop releases the claim regardless of flush
mode. Cross-process peers stay excluded by SQLite's own EXCLUSIVE locking
— this in-process guard complements it. Mirrors the secrets store's
AlreadyLocked discipline.

Note: this commit also carries the persister.rs durability-doc and
LOAD_UNIMPLEMENTED edits staged alongside the registry in the same file.

Regression: tests/sqlite_second_open_guard.rs (second open refused incl.
non-canonical path equivalence; reopen after drop succeeds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t asset-lock/dashpay load docs

- PWS-017: scope the identity tombstone UPDATE to the flush wallet
  (AND wallet_id IS ?2, NULL-safe), so wallet A's removed set can no
  longer tombstone wallet B's identity. Mirrors the upsert's per-entry
  wallet cross-check. qa_tomb3 now asserts cross-wallet ISOLATION.
- PWS-013 (code half): asset_locks::load_state rustdoc said consumed
  locks leave via `removed`; they are RETAINED with status='consumed'
  and load_unconsumed filters them at SQL — corrected to present state.
- PWS-014: document dashpay_profiles/dashpay_payments_overlay as a
  write-only indexed overlay (load() rehydrates DashPay from the
  identities blob, not these tables) + round-trip contract test.
- PWS-030: delete_wallet pre-flush apply now has a REAL SQL-failure
  regression (drop a child table -> apply fails -> buffer restored),
  closing the injector-only blind spot that hid PWS-001.

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

PWS-019: the readers each open-coded the same
`u32::try_from(col).map_err(|_| IntegerOverflow { .. })` boundary cast.
Add `safe_cast::i64_to_u32` (new SafeCastTarget::U32) and route the
account/address-index, nonce, and birth-height readers through it. One
funnel for the INTEGER->u32 boundary, matching the existing
u64_to_i64 / i64_to_u64 helpers.

(core_state.rs cast sites land with the fail-hard watermark change.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bucketing, durability docs, kv/blob unit alignment

Correctness + hygiene tail:

- PWS-005: core_state load no longer silently discards an out-of-range
  sync watermark — routes through sync_height_u32 so a corrupt value
  fails hard, honoring the load() never-skip-corruption invariant.
- PWS-006: the UTXO account_index-by-address lookup now orders
  deterministically (ORDER BY account_type, account_index LIMIT 1) so an
  address derived under multiple account types buckets stably.
- PWS-019: route core_state's remaining i64->u32 casts through the
  shared helper.
- PWS-002: qualify store/commit/Synchronous::Normal docs — WAL+NORMAL is
  app-crash durable, not power-loss durable; point at Synchronous::Full.
- PWS-020: align validate_key to code points (chars().count()) to match
  the SQL `length()` CHECK unit exactly, so the API and the column
  constraint accept the identical key set.
- PWS-022: replace the hand-maintained MAX_VALUE_LEN/BLOB_SIZE_LIMIT_BYTES
  coupling with a single crate-root SIZE_LIMIT_BYTES const.

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

Documentation had drifted from the code it describes — these fixes make the
prose match what load(), the V001 schema, the asset-lock readers, and the
secrets error surface actually do.

- README load() section: rewrite from the old "wallets slot empty / readers
  dormant" claim to the shipped full rehydration — both ClientStartState
  slots are populated, every per-area reader is wired into load(), and the
  summary reports wallets_rehydrated with wallets_pending_rehydration = 0.
- SCHEMA.md identity_keys (3 places): correct to the real V001 shape —
  wallet_id BLOB NOT NULL + derivation_blob columns, PRIMARY KEY
  (wallet_id, identity_id, key_id), two ON DELETE CASCADE FKs (wallets +
  identities), and idx_identity_keys_wallet_identity. Drop identity_keys
  from the "no wallet_id column" bullet.
- SCHEMA.md asset_locks: consumed locks are RETAINED permanently with
  status='consumed' (upsert, never removed); the rehydration feed reads
  load_unconsumed (status NOT IN ('consumed')) so spent locks are never
  resurrected, while full history stays visible via list_active.
- SECRETS.md error surface: add the hardening variants SecretTooLarge,
  InsecureParentDir, and Encrypt to the file-arm and BadStoreFormat lists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 887288a732ac9f4bede5170dfaa2e758eeb10011)
…l filesystems

The EncryptedFileStore AlreadyLocked guarantee rests on fd-lock
(flock/LockFileEx), whose advisory locks interlock only between processes on
the same host on a local POSIX/Windows filesystem. flock does not interlock
over NFS/CIFS/SMB, so two hosts can each acquire the "exclusive" lock and the
resident-vault model then silently last-writer-wins, losing secrets.

State this LOCAL-FS-only limitation in both doc spots — the secrets/file
module docs and the Cargo.toml fd-lock dependency comment — so the lock is
not presented as unconditional. A single vault file must not be shared across
hosts; steer multi-host secret access to the OS-keyring backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0910dc1f1e709341e8aaccdf6198cf5ebde6af28)
peek_schema_version opened the read-only conn with SQLITE_OPEN_URI, which
contradicts the crate's open-conn choke-point that deliberately leaves URI
parsing off so a path can never smuggle file:-URI query parameters (e.g.
?mode=rwc) that could defeat the read-only intent. Drop the flag so the CLI
peek matches that choke-point.

Also fix a stale prune comment that referenced a --backups-dir flag; the
actual flag is --in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4a0339ae466a5b2f6e31ffdd6be054afa4ffce5b)
…e unmaintained)

bincode 2.0.1 is the BLOB-codec trust boundary for every persisted column and
backup, and RUSTSEC-2025-0141 (published 2025-12-16) marks bincode
unmaintained — an informational advisory, not an exploitable CVE, with no
patched release.

Add a crate-local .cargo/audit.toml with a targeted, dated acknowledgement
(not a blanket ignore): the known OOM risk class is defanged by in-crate size
bounds (the bincode codec is configured .with_limit::<BLOB_SIZE_LIMIT_BYTES>(),
plus MAX_VALUE_LEN, SecretTooLarge, and MAX_VAULT_SIZE_BYTES reject oversized
inputs before allocation, and load() is fail-hard). Documents the residual
risk and a migration plan to a maintained codec once the wire format freezes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit a992b8b6fc1c2addc0339220aa11b089a0450961)
…stfmt

JournalModeNotApplied { .. } arm in sqlite_error_classification.rs was
expanded to a three-line block; rustfmt collapses it to one line.

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

The old name implied the cascade phase failed; in reality the peer's
BEGIN EXCLUSIVE makes the pre-flush itself fail with BUSY — the cascade
is never reached. Rename to preflush_begin_exclusive_busy_preserves_buffer
so the name matches what the test actually exercises. Assertions are
unchanged — this is the load-bearing PWS-001 regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base automatically changed from feat/platform-wallet-sqlite-persistor to v3.1-dev June 9, 2026 08:13
lklimek and others added 6 commits June 9, 2026 10:40
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dule

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

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

Comment compaction introduced four doc-comment defects that broke the gate suite.
All fixes are comments-only; stripping comments leaves the code byte-identical.

clippy `doc_lazy_continuation` (-D warnings):
- core_state.rs: a line-leading `+ ` ("ORDER BY + LIMIT 1") read as a Markdown
  bullet, flagging the next line as an unindented list continuation. Reworded to
  "ORDER BY with LIMIT 1" so no doc line starts with a bullet marker.
- secrets/keyring.rs: same trap from a line-leading `+ unlocked collection`
  (D-Bus "session + unlocked collection"). Reworded to "and an unlocked collection".

rustdoc (-D warnings, --all-features --document-private-items) regressions:
- identity_keys.rs: compaction promoted a plain `IdentityKeyWire` code span into an
  intra-doc link to that private struct, tripping `private_intra_doc_links`.
  Restored the plain code span.
- asset_locks.rs: compaction rewrote a qualified-path link label into an explicit
  `[`AssetLockStatus`](full::path)` target whose label already resolves to the same
  item, tripping `redundant_explicit_links`. Restored the qualified-path label form.

The remaining 9 rustdoc errors under that flag set are pre-existing on the PR base
(b4a3aa9 fails the same gate); they are unrelated to the comment work and out of
scope here. The comment commits net-improved that gate (fixed 3 base errors, this
change repairs the 2 they introduced).

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

Brings the now-upstreamed base into PR #3692: v3.1-dev absorbed #3625
(platform-wallet-storage), so it is the effective parent of this stack.

Resolutions:
- platform-wallet-storage: forced to OUR HEAD (strict superset incl. the
  wallet_meta.rs -> wallets.rs rename); removed resurrected stray
  src/sqlite/schema/wallet_meta.rs. Crate tree == pre-merge HEAD exactly.
- rs-platform-wallet / -ffi: kept both additive sides (rehydration FFI +
  birth-height exports; serde_adapters + shielded changeset modules).
  Un-gated PlatformWalletManager.event_manager from upstream's
  #[cfg(feature = "shielded")] because load_from_persistor reads it
  unconditionally on the rehydration path.
- Cargo.lock: regenerated by cargo during post-merge checks.
- Disjoint upstream (rs-dpp/rs-drive/grovedb 4.0.0/dashcore/etc.): merged clean.

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

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.02703% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.18%. Comparing base (6fa4686) to head (855471e).
⚠️ Report is 3 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...rs-platform-wallet-storage/src/secrets/file/mod.rs 82.31% 26 Missing ⚠️
...rm-wallet-storage/src/sqlite/schema/asset_locks.rs 48.83% 22 Missing ⚠️
...-wallet-storage/src/sqlite/schema/identity_keys.rs 64.28% 15 Missing ⚠️
...orm-wallet-storage/src/sqlite/schema/core_state.rs 85.22% 13 Missing ⚠️
...rs-platform-wallet-storage/src/sqlite/persister.rs 85.88% 12 Missing ⚠️
...es/rs-platform-wallet-storage/src/sqlite/backup.rs 93.54% 2 Missing ⚠️
...tform-wallet-storage/src/sqlite/schema/accounts.rs 90.00% 2 Missing ⚠️
...platform-wallet-storage/src/secrets/file/format.rs 99.23% 1 Missing ⚠️
...s/rs-platform-wallet-storage/src/secrets/secret.rs 95.45% 1 Missing ⚠️
...s-platform-wallet-storage/src/sqlite/migrations.rs 96.15% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@              Coverage Diff              @@
##           v3.1-dev    #3692       +/-   ##
=============================================
+ Coverage     69.91%   87.18%   +17.27%     
=============================================
  Files            19     2661     +2642     
  Lines          2712   328679   +325967     
=============================================
+ Hits           1896   286569   +284673     
- Misses          816    42110    +41294     
Components Coverage Δ
dpp 87.67% <ø> (∅)
drive 86.05% <ø> (∅)
drive-abci 89.50% <ø> (∅)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.20% <ø> (∅)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 47.85% <ø> (∅)
🚀 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.

…eateFromShieldedPool #3816, beta.4) into rehydration #3692

Incremental merge of origin/v3.1-dev (6fa4686..da9d3fe). Three wallet
overlap files auto-merged cleanly; only Cargo.lock needed resolution
(took upstream beta.4 base, reconciled our secrets/rehydration deps).
event_manager ungating preserved; non-shielded build verified.

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.

6 participants