feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692
Draft
Claudius-Maginificent wants to merge 240 commits into
Draft
feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692Claudius-Maginificent wants to merge 240 commits into
Claudius-Maginificent wants to merge 240 commits into
Conversation
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>
…let-sqlite-persistor
…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>
…platform-wallet (#3644)
…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>
# Conflicts: # Cargo.lock
…let-sqlite-persistor
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>
…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
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 Report❌ Patch coverage is 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
🚀 New features to boost your workflow:
|
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Update — incremental v3.1-dev merge (2026-06-09)
Pushed
855471e6e8..2d22cf3e82. v3.1-dev advanced again (6fa4686..da9d3fe, nowv4.0.0-beta.4); merged it up to keep the branch current. The prior merge already folded in6fa4686, so the merge-base is exactly that commit — this was a small incremental merge of three disjoint shielded-pool PRs:IdentityCreateFromShieldedPoolstate transition4.0.0-beta.4All three wallet overlap files (
manager/mod.rs,error.rs,ffi/persistence.rs) auto-merged with zero conflicts, including preservation of theevent_managerungating against upstream's new shielded-sync edits (re-verified by a non-shieldedcargo check -p platform-walletcanary). The only manual resolution wasCargo.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, plusplatform-walletdefault + shielded andplatform-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 tov3.1-dev(the old basefeat/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-renamewallet_meta.rsstray was removed.rs-platform-wallet/-ffi— kept both additive sides (ourserde_adapters+ upstreamshielded_*; rehydration FFI + birth-height exports; upstream fix(platform-wallet): zeroize private keys when freeing preview rows #3797 zeroize-on-preview-free).PlatformWalletManager.event_managerbehind#[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-storageno-default + all-features,rs-platform-walletdefault + 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 thesecrets/**subsystem plus a whole-crate review and remediation ofplatform-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+mlockwrappers). Fixes landed:argon2'szeroizefeature (the multi-MiB KDF matrix + key-correlated intermediates were left un-wiped).PersistableBlob(no-key-material-in-DB enforced at the type level); converted productionexpect()→ typed errors; added parser-fuzz + on-disk tamper tests; narrowed#![allow(unsafe_code)]to per-block with// SAFETY:notes; pinnedfd-lock; acknowledged the bincode advisory.Whole-crate review remediation — one CRITICAL fixed
delete_walletdata loss (CRITICAL): a pre-flushBEGIN EXCLUSIVEcross-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).open()on the same path (AlreadyOpen); wallet-scope the identity tombstone (was cross-wallet);application_id/NotAWalletDbrestore validation (foreign DB rejected, destination untouched); refinery-panic on malformed schema-history → typedSchemaHistoryMalformed; 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.i64→u32safe-casts; kv/blob size-limit unified; backup retention floor; CLISQLITE_OPEN_URIremoved; bincodeRUSTSEC-2025-0141acknowledged.QA (all green):
cargo fmt --all --check;cargo check+ clippy under both--no-default-features --features sqlite,cliand--all-features(-D warnings);cargo test -p platform-wallet-storage→ 380 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)9e1248cb0afixes a cfg-gating defect that broke this crate for downstream consumers building without__test-helpers. The production seedlessPersister::load()path calls schema readers —schema::wallets::{fetch,parse_network},identities::{load_state,managed_identity_from_entry}, theasset_locksreader 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 testenable__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-gatedschemamodule, so they compile precisely whenload()does (nokvsymbol pulled into asqlite-only path, no DDL/V001 touched). One genuinely-deadasset_locks::list_active(zero callers, new at this rev) was gated instead, which keeps both clippy unions clean simultaneously. The in-memory changeset fieldcs.wallet_metadatais 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-storageunder both--all-featuresand--no-default-features --features sqlite,cli(-D warnings);cargo test -p platform-wallet-storage→ 339 passed / 0 failed.Update — #3625 (storage crate +
wallet_metadata→walletsrename) propagated in (2026-06-08)Propagated the current #3625 tip (
c9d1c81) into this branch via mergef69746d5plus test-reconciliation8ba49c37. 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:
wallet_metadata→wallets— disambiguates the root wallets table from themeta_*metadata family. Internal toplatform-wallet-storage(SQL identifiers + theschema::walletsmodule, FK targets, both cascade triggers). The in-memory changeset domain fieldcs.wallet_metadata/WalletMetadataEntry/ FFIon_persist_wallet_metadata_fnis a different concept and is deliberately untouched.idx_identity_keys_identity(left-prefix of the PK); renamedidentities.wallet_index→identity_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 inSCHEMA.md.rs-platform-wallet/src/changeset/mod.rsserde + shielded modules;rs-platform-wallet-ffi/src/manager.rsrehydration FFI + birth-height exports), plus 7 storage-crate conflicts and 2 semantic overlaps (an orphanedschema::wallet_meta→walletsreference in the newload()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-storageunder both--all-featuresand--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-features→ 339 passed / 0 failed / 3 ignored;cargo test -p platform-wallet -p platform-wallet-ffi→ 295 passed / 0 failed / 5 ignored.Update — latest #3625 propagated (2026-06-03)
Propagated the current #3625 tip (
04662411cf, achore: update Cargo.lock) into this branch via merge3f2e7d2561(parents2f351903cd+04662411cf). Fast-forward-clean; the delta is Cargo.lock only. The recurring stack-conflict files remain intact —rs-platform-wallet/src/changeset/mod.rskeeps all three feature-gated modules (serde_adapters+shielded_changeset+shielded_sync_start_state) andrs-platform-wallet-ffi/src/manager.rskeeps 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, tip31660906c2) into this branch (merge2f351903cd). #3625 had just hadv3.1-devmerged into it, so this is the bottom-up, parent-into-child propagation that keeps the stack reconciled:4.0.0-beta.2version bumps,rs-platform-versionv8/v12 method versions,rs-drive-abcishielded-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.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-storage→ 334 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), andsqlite_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 (merge052db80f12):contactstable (one lifecycle-state row replacingcontacts_sent/contacts_recv/contacts_established) now backs rehydration. The item-G contacts reader was reworked for the unified shape:load_stateun-gated to production (no longer#[cfg(feature = "__test-helpers")]), with theload_changesetwrapper retained so the seedlessload()still consumes aContactChangeSet.contacts/identity_keyscolumn the readers touch exists in V001,identity_keyskeeps bothwallet_id+derivation_blob, themeta_contacttrigger fires on the unifiedcontactstable, and all sixmeta_*tables +PER_WALLET_TABLESare reconciled. All feat(platform-wallet)!: add platform-wallet-storage crate (sqlite persister) #3625 hardening (CMT-001/002/004/005/006) is intact.cargo fmt --check, clippy under--all-featuresand--no-default-features --features sqlite,cli(-D warnings),cargo check -p platform-wallet-ffi; 513 tests pass incl. item-Gg_rt1/g_rt2/g_rt3and thetc_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.
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.PlatformWalletError::WrongSeedForDatabase, the never-importedsubtledep, and the false docs claiming a first-sign gate / seed-unavailable skip exist. The seedless watch-onlyload()performs no wrong-seed check by design; any sign-time gate is deferred FFI work, not this PR.load().LOAD_UNIMPLEMENTEDis reduced tocore::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 — theidentity_keystable had lost itswallet_idcolumn relative to the reader (auto-merge artifact), breaking all rehydration; the table/writer/PER_WALLET_TABLESscope were reconciled and a dual-FK-cascade regression test (tc049_delete_wallet_cascades_identity_keys) added.load_from_persistor, watch-only wallet build (Wallet::new_watch_only), noSeedProvider/3-arg-resolver-at-load; signing stays on-demand via theMnemonicResolvervtable (private key never crosses FFI).All gates green:
cargo clippyunder--all-featuresand--no-default-features --features sqlite,cli(-D warnings), plus theplatform-wallet-storage+platform-wallettest suites (incl. the 6 item-G tests).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-integrationbranch):loadFromPersistor()is zero-arg, called at app launch with locked Keychain; signing flows take theMnemonicResolverHandlevtable 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 againstv3.1-dev.What was done?
Seedless watch-only load (
rs-platform-wallet)PlatformWalletManager::load_from_persistor()reconstructs each persisted wallet from the keylessClientWalletStartState:For each wallet in the persisted
walletsmap, the manager:AccountCollectionfrom theaccount_manifest: oneAccount::from_xpub(parent_wallet_id, account_type, account_xpub, network)perAccountRegistrationEntry.Wallet::new_watch_only(network, wallet_id, accounts)—key_wallet::WalletType::WatchOnlyvariant, noMnemonic/Seedvariant, no key bytes anywhere.CoreChangeSet(UTXOs, tx records, IS-locks, sync watermarks) into the wallet via the existingapply_persisted_core_state(...)path, which correctly handles non-BIP44 topologies (CoinJoin-only / DashPay) viaall_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.skippedcarries(WalletId, SkipReason::CorruptPersistedRow { kind: CorruptKind })whereCorruptKindisMissingManifest | MalformedXpub | DecodeError(String). APlatformEvent::WalletSkippedOnLoad { wallet_id, reason }fires per skip. One corrupt row never aborts the rest. The caller receivesOk(LoadOutcome)(non-emptyskippedis success, not an error).New schema readers
schema::accounts::load_stateaccount_registrations+ pools; decodesAccountRegistrationEntry; noWalletbuiltschema::core_state::load_stateManagedWalletInfo— 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 balanceschema::asset_locks::load_unconsumedConsumedrows at SQL level (WHERE status NOT IN ('consumed'))FFI
The resolver arg is gone — load is purely watch-only.
LoadOutcomeFFIsurfacesloaded_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 (passesnilfor 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
ClientWalletStartStatehas no contacts slot; wiring requires a changeset-shape change. Deferred to PR-3 (feat(platform-wallet): add contacts and identity-key rehydration (item G) #3693).v3.1-devdirectly). Once merged + merge-up, the gate ships with this PR's lineage too.How Has This Been Tested?
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):load_from_persistor(); assert every wallet comes back asWallet::WatchOnlywith correctwallet_id, accounts, balances. No seed ever touched.LoadOutcome.skippedwithCorruptPersistedRow, the other wallets load cleanly, exactly onePlatformEvent::WalletSkippedOnLoadfires.LoadOutcome,SkipReason,WalletSkippedOnLoadpayloads carry no key material inDisplayorDebug.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_reconstruction13/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 —MnemonicResolverHandleis the on-demand contract).ClientWalletStartStateno longer carries aWalletfield (assembled in the manager viaWallet::new_watch_only).platform_wallet_manager_load_from_persistor.No
!in the title because this is additive capability on an unreleased API —v3.1-devcarries none of the previous PR-internal shapes.AR-7 hygiene
Load path eliminates AR-7 entirely — the manager never constructs
WalletType::Mnemonic|Seed, onlyWalletType::WatchOnly(no key material). AR-7's residualDebugconcern was about derivedWalletvalues 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:
For repository code-owners and collaborators only
🤖 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 againstv3.1-dev.