feat: platform-wallet backend rewrite (spec + implementation)#860
feat: platform-wallet backend rewrite (spec + implementation)#860lklimek wants to merge 160 commits into
Conversation
Investigation and phased plan for migrating dash-evo-tool key storage and identity management onto the upstream platform-wallet crate. Planning and verification only — no implementation in this change. Blocked on dashpay/platform PR #3625. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The project pivoted from an incremental newtype-based migration to a from-scratch backend rewrite where platform-wallet owns chain sync, HD wallet, identity, DashPay, asset locks, and persistence entirely. Superseded files removed: architecture.md, migration-plan.md, spv-rpc-correctness.md, verification.md. New spec files added: - upstream-reality.md — verified upstream facts, src/spv/ deletion answer, G2 caveat (Wallet::from_persisted gap) - backend-architecture.md — src/wallet_backend/ module, EventBridge replacing reconcile_spv_wallets, error model - backendtask-contract.md — full BackendTask kept/modified/removed table - data-model-and-migration.md — one-time migration procedure with backup/fail-safe, dead fields - removal-inventory.md — DELETE/RETAIN lists, RPC mode fate, thin Core-RPC mining utility - single-key-mock.md — SingleKeyBackend trait boundary and stub design - phasing.md — P0–P5 phase table, QA matrix, highest-risk verdict - open-questions.md — 8 decisions needed from user (rewritten from 5) feature-coverage.md updated with supporting-analysis header and corrected cross-links to new files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Folds the 8 maintainer decisions into the clean-slate spec: pin to dashpay/platform #3625 head now (implementation no longer upstream- blocked); G2 mocked via a PersistedWalletLoader seam (downgraded to a deferred swap); DIP-14/15 = migrate-or-hard-stop+escalate (soft dual- derivation fallback withdrawn); ZMQ audit-then-drop; Devnet discovery stays DET-permanent; hybrid DashPay split; single-key mock-now-swap- later; obsolete tasks hard-removed in the same release. Adds g2-mock-boundary.md and dip14-migration-hardstop.md. Spec only — implementation gated on user approval; the sole release-blocker is the decision-#6 DIP-14/15 migration QA lane. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-14/15 scope - phasing.md: insert P0.5 "Compile Floor" phase between P0 and P1 with full 7-cluster delete/stub/fixup task list; re-sequence phase chain (P0→P0.5→P1→P2→P3→P4 cleanup→P5); add co-land constraint, harness-shape note, data-recoverability confirmation; update crew assignments and QA matrix - removal-inventory.md: add "Retained code requiring SDK-rev signature updates at P0.5" subsection (Clusters E/F/G are RETAIN-with-fixup, not deletion targets) - backendtask-contract.md: add transitional state section describing WalletBackendNotYetWired and SingleKeyWalletsUnsupported error variants introduced at P0.5; document P0.5→P2 inert-dispatch window - dip14-migration-hardstop.md §6.2: replace CKDpriv256 divergence hypothesis with confirmed bounded structural finding from P0 probe - open-questions.md #6: record confirmed P0 DIP-14/15 structural finding as fact - single-key-mock.md: note stub boundary exists from P0.5 onward - README.md: update STATUS callout (P0 done, P0.5 next); update ToC entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e/stub old wallet stack Atomically bumps dash-sdk + rs-sdk-trusted-context-provider from rev 54048b9352… to 738091f734…, adds platform-wallet (feature serde) + platform-wallet-storage git deps at the same rev, and deletes/stubs/ fixes exactly enough of the old SPV/RPC wallet stack to reach a green workspace build and clippy --all-features --all-targets -D warnings. Right, this was a proper bin-fire — 42 SDK-drift errors that cascaded to ~142 once the old SPV module came out. Cluster work per docs/ai-design/2026-05-18-platform-wallet-migration/phasing.md §P0.5: - A DELETE: src/spv/** removed; SpvStatus/SpvStatusSnapshot relocated to src/model/spv_status.rs as DET-owned display value types (no chain sync). CoreBackendMode deleted; system is SPV-only. - B DELETE: reconcile_spv_wallets + the 4 SPV listener/sync fns + spv_manager() accessor + field wiring; start_spv/stop_spv/ clear_spv_data are inert stubs (P2 wires PlatformWalletManager). - C DELETE/STUB: SPV/RPC payment+derivation internals, CoreBackendMode branch sites, core_client_for_wallet/list_core_wallets removed; retained wallet/identity/DashPay dispatch arms return new typed TaskError::WalletBackendNotYetWired; single-key arms return TaskError::SingleKeyWalletsUnsupported; MineBlocks kept on thin Core-RPC client (Regtest/Devnet). - D DELETE/RETAIN-MINIMAL: dead UTXO RPC reload body removed (reload_utxos → SPV no-op); seed-bearing wallet skeleton + select/remove UTXO surface RETAINED (P3 migration + DashPay/identity retained code depend on it). utxos.rs kept (over-deletion avoided). - E/F/G SDK-DRIFT-FIXUP (RETAINED, no stubs): dpp Signer<K> is now #[async_trait] — qualified_identity + Wallet Signer impls made async; try_from_identity_with_signer / sign_external_with_options / shielded build_shield_transition are async (.await added; bridged via futures::executor::block_on in sync shielded ctx). AddressProvider reworked for new assoc-type API (Tag/Address: AddressToBytes, iterator returns, AddressKey→PlatformAddress, highest_found_index removed). Two new typed fieldless TaskError variants (WalletBackendNotYetWired, SingleKeyWalletsUnsupported) with calm/actionable #[error] messages per CLAUDE.md error rules — no String fields. Tests need not pass at P0.5: network-dependent backend-e2e #[ignore] helpers patched to compile with // TODO(P0.5) markers (re-wired in P2). No DB schema change (P3 owns that). RPC-mode toggle / Core-wallet picker UI removed per removal-inventory; remaining dead RPC provider kept behind #[allow(dead_code)] + TODO (removed in P4). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…etLoader Builds the new leak-free wallet seam on the P0.5 green floor. Non-destructive: adds src/wallet_backend/* + typed errors alongside the P0.5 stubs; NOT yet wired into AppContext and BackendTask dispatch still runs the P0.5 stubs (parallel/skeleton per phasing.md P1 row). P2 points the task arms at WalletBackend. - src/wallet_backend/mod.rs — WalletBackend wraps PlatformWalletManager<SqlitePersister>. Single boundary: no upstream type (PlatformWalletManager/PlatformWallet/WalletId/SqlitePersister/ WalletManager) escapes; public API is DET-shaped (new/start/shutdown/ wallet_count/is_syncing). Clone via Arc<Inner>, Send+Sync. Consumes upstream platform-wallet-storage SqlitePersister (no DET-authored persister). start() orchestrates SpvRuntime::start(ClientConfig) + platform-address/identity sync coordinators (see upstream-reality note below). - src/wallet_backend/event_bridge.rs — EventBridge implements platform_wallet PlatformEventHandler + dash_spv EventHandler. Sync, non-blocking callbacks map upstream sync/network/wallet/error events to ConnectionStatus atomics (set_spv_status/_connected_peers/ _last_error — relocated src/model/spv_status.rs types) AND a non-blocking TaskResult::Refresh on the existing MPSC, exactly the P0.5 EventBridge recommendation. - src/wallet_backend/loader.rs — PersistedWalletLoader object-safe trait + SeedReregistrationLoader (reads DET retained encrypted-seed store, yields one DET-opaque WalletRegistration per open wallet; seed zeroized-on-drop, never persisted — secret boundary intact). UpstreamFromPersisted slot reserved (no upstream API yet), mirroring the single-key reserved-impl pattern. Unit tests: seed-redaction, network mapping, G2.4 zero-blast swap-boundary compile. - backend_task/error.rs — 4 new typed variants (WalletBackend, WalletStorage, WalletSyncStartFailed, WalletSeedDecryptFailed, FileSystem) with #[source], no String fields, calm #[error] text. Spec/upstream mismatches (non-blocking, adapted; feed to architect): 1. backend-architecture.md/g2-mock-boundary.md assume a unified PlatformWalletManager::start(). Upstream reality: no such method — WalletBackend::start() orchestrates spv().start(ClientConfig) + *_sync_arc().start() coordinators. 2. g2-mock-boundary G2.1 says call create_wallet_from_seed_bytes THEN load_persisted() THEN start(). Upstream reality: create_wallet_from_seed_bytes already calls load_persisted + initialize_from_persisted + identity discovery internally — loader plug-point is just create_wallet_from_seed_bytes per registration. 3. platform-wallet shielded feature is NOT enabled for DET (serde only), so on_shielded_sync_completed / shielded_sync_arc do not exist — left at upstream no-op default / omitted. get_quorum_public_key stays the typed-error stub: WalletBackend is not yet in AppContext (P1 parallel/skeleton), so there is nothing to delegate to — comment points at P2 (AppContext wiring). build + clippy --all-features --all-targets -D warnings + fmt --check GREEN; 3 wallet_backend unit tests pass; P0.5 floor not regressed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the P0.5 WalletBackendNotYetWired stubs with real WalletBackend
calls and wires the load-bearing get_quorum_public_key path. The
BackendTask action/channel/TaskResult contract and result variants are
preserved (UI unchanged); signatures change only where the wallet model
type changed, per backendtask-contract.md.
Construction wiring: AppContext gains a lazily-built
ArcSwapOption<WalletBackend>. WalletBackend::new is async and needs the
TaskResult sender, which lives on AppState not AppContext — so
AppContext::ensure_wallet_backend(sender) builds it idempotently on the
first wallet/core/identity/DashPay task (run_backend_task threads the
AppState-owned sender there). wallet_backend() returns it or
WalletBackendNotYetWired while unbuilt. AppContext::new is unchanged
(all 5 call sites untouched).
Stub → real:
- CoreTask::RefreshWalletInfo — Core state is upstream-continuous +
EventBridge-pushed; arm ensures backend + optionally refreshes the
retained DAPI platform-address balances. Returns near-instantly.
- WalletTask::GenerateReceiveAddress —
WalletBackend::next_receive_address (upstream CoreWallet).
- CoreTask::SendWalletPayment — WalletBackend::send_payment (upstream
send_to_addresses: build/sign + SpvRuntime broadcast). Result
WalletPayment{txid,..} unchanged.
- CoreTask::CreateRegistration/TopUpAssetLock,
identity register/top-up FundWithWallet,
WalletTask::FundPlatformAddressFromWalletUtxos —
WalletBackend::create_asset_lock_proof (upstream AssetLockManager
builds/broadcasts/tracks-to-proof; returns proof+key+txid). The
retained TopUpAddress SDK transition + DET credit bookkeeping stay.
- transaction_processing::broadcast_raw_transaction —
WalletBackend::broadcast_transaction (upstream SpvBroadcaster).
- SpvProvider::get_quorum_public_key — bridged (block_in_place) to
WalletBackend → SpvRuntime::get_quorum_public_key. Load-bearing
proof-verification path is wired.
WalletBackend additions (all DET-shaped; no upstream type leaks):
seed_hash→WalletId map (upstream WalletId = SHA256(root_xpub‖chaincode)
≠ DET seed_hash = SHA256(seed) — bridged at registration), AssetLockKind
enum (hides upstream AssetLockFundingType), next_receive_address /
send_payment / create_asset_lock_proof / broadcast_transaction /
get_quorum_public_key.
Single-key arms stay permanently on SingleKeyWalletsUnsupported
(Decision #7). New typed TaskError variants from P1 reused;
DashPayContactDerivationIrreconcilable{contact: Identifier} added so
the DashPay arm shape supports P3 quarantine enforcement (no String
field; Base58 contact id is a copyable handle per CLAUDE.md rule 6).
EventBridge live-validation: deterministic mapping unit tests added
(synthetic SyncEvent/SyncProgress/NetworkEvent → ConnectionStatus +
TaskResult::Refresh assertions). Full live network path added as an
#[ignore] backend-e2e test (event_bridge_live.rs) — requires harness
WalletBackend wiring (test-infra, tracked for P3 QA lane).
Superseded DET helpers deleted (M-NO-TOMBSTONES, 0 callers): the
register_spv_address/wallet_network_key/classify_derivation_metadata
address-registration hooks (upstream now owns address derivation, so a
no-caller hook would be dead code — deviation-with-rationale from the
"un-allow and wire" instruction), plus the orphaned create_asset_lock.rs
and refresh_wallet_info.rs core submodules.
Accepts the 3 P1-noted spec/upstream mismatches (no unified start();
create_wallet_from_seed_bytes folds load_persisted; shielded feature
off) — built to upstream reality.
build + clippy --all-features --all-targets -D warnings + fmt --check
GREEN; EventBridge unit tests pass; P0.5/P1 floor not regressed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sion #8) Decision #8: named Core wallets and explicit asset-lock recovery are RPC-only / obsolete under the upstream backend (AssetLockManager tracks locks continuously). Hard-removed with no no-op grace — the tasks AND their UI entry points go in the same change. Backend: - CoreTask::{RecoverAssetLocks, ListCoreWallets} enum variants + their PartialEq arms + dispatch arms removed. - src/backend_task/core/recover_asset_locks.rs deleted. - BackendTaskSuccessResult::{RecoveredAssetLocks, CoreWalletsList} result variants removed. UI entry points: - Core-wallet picker removed from add_new_wallet_screen and import_mnemonic_screen (fields, init, reset_core_wallets_cache, display_task_result/error overrides, ComboBox picker block, the wallet.core_wallet_name UI assignment; the core_wallet_name DB column/model field stays — P3 schema migration owns that). - wallets_screen: "Search for Unused" asset-lock button + the SearchAssetLocks custom action + pending_asset_lock_search / asset_lock_search_banner state machine; the named-Core-wallet selection flow (core_wallet_dialog SelectionDialog, apply_core_wallet_selection, the CoreWalletNotConfigured display_task_error interception + pending_list_* state + RecoveredAssetLocks/CoreWalletsList result handlers). - ui/mod.rs change_context calls to the removed reset_core_wallets_cache / reset_pending_list_state dropped. - backend-e2e TC-006 (RecoverAssetLocks) removed, mirroring the existing TC-007/008/010 REMOVED markers. Orphaned Core-wallet TaskError variants (CoreWalletNotConfigured/ InvalidCoreWalletName/NoCoreWalletsLoaded/WalletDatabasePersistError) are now unconstructed but left for P4 cleanup — they are harmless dead `pub enum` variants (no clippy/correctness impact) and CoreWalletNot- Configured still has an RPC-error-mapping helper + tests; excising them now would over-reach this change. build + clippy --all-features --all-targets -D warnings + fmt --check GREEN; 8 wallet_backend unit tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply Nagatha-ratified edits across the platform-wallet migration spec set: - data-model-and-migration.md: replace implicit Step 3 prose with normative "Migration execution model — two stage, marker-gated (RATIFIED)" subsection. Defines Stage A (SQL v35, sync, pre-unlock, marker-only) and Stage B (async, post-unlock, ensure_wallet_backend seam, tokio::sync::Mutex-gated). Normative marker lifecycle: platform_wallet_migration_pending clears iff all wallets+identities+ contacts classified; dashpay_dip14_quarantine_active independent. Quarantine is a successful terminal classification, not a failure. Restore-from-premigration is an exception path only. - phasing.md: expand P3 phase-table entry; add P3a–P3e sub-steps (Stage-A SQL+markers, predicate TDD, Stage-B wiring, quarantine/ restore invariants, backend-e2e harness). Each sub-step has its own commit target, test requirements, and release-blocking designation. - backend-architecture.md: annotate ensure_wallet_backend (src/context/mod.rs:634) as the Stage-B post-unlock one-time- migration seam. Document AppContext-owned tokio::sync::Mutex, ArcSwapOption ordering guarantee. Add two marker settings fields to AppContext/settings inventory. - dip14-migration-hardstop.md: add normative forward-references for index range (receive ∪ send union from get_contact_address_indices database/dashpay.rs:649, no sampled prefix) and quarantine-as- successful-terminal-classification clearing pending but setting quarantine flag. - README.md: update STATUS callout — P0/P0.5/P1/P2 GREEN; P3 ratified two-stage design, in progress (P3a–P3e); run mid-execution on branch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…backup Stage A of the RATIFIED two-stage marker-gated platform-wallet migration (data-model-and-migration.md). Sync, pre-unlock, in-tx, non-destructive. Stage B (post-unlock async engine) lands in P3c. - DEFAULT_DB_VERSION 34 → 35. - New settings columns `platform_wallet_migration_pending` and `dashpay_dip14_quarantine_active` (DEFAULT 0): added to the fresh CREATE TABLE settings, to ensure_settings_columns_exist (idempotent, column-guarded), and via the v35 tx itself. - v35 apply_version_changes arm: arms platform_wallet_migration_pending inside the v35 transaction. NO destructive step, NO backup inside the live write-tx. Fresh installs start at v35 with the marker clear (nothing to migrate). - C1/C2: retained `<db>.premigration` recovery floor created POST-commit in initialize() via the SQLite online-backup API (rusqlite `backup` feature added), gated by the marker, idempotent (skipped if present, never overwrites the floor), atomic (temp+rename), best-effort (logged, never blocks app start, (re)created on first post-marker launch even if the user never unlocks). The MARKER — not the backup file — is the authoritative pending signal. - settings.rs: typed getters/setters for both markers. Tests (5, all pass): v35 arms marker + non-destructive (legacy wallet row preserved), v35 idempotent re-run is no-op, fresh v35 install has no pending marker, premigration backup created post-migration & is a consistent SQLite file with legacy data, premigration backup not recreated once present (recovery-floor invariant). build + clippy --all-features --all-targets -D warnings + fmt --check GREEN. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d-safety) Pure, synchronous, no-DB/no-SDK predicate (`classify_contact` in src/database/migration_pw.rs). Re-derives a contact's payment addresses with BOTH the legacy DET engine (derive_dashpay_incoming_xpub / derive_payment_address — historical ground truth) and the upstream engine (derive_contact_xpub / derive_contact_payment_address), asserting byte-equality over the contact's ENTIRE historical used-index range. - Index range = UNION of receive [0, highest_receive_index] and send [0, next_send_index-1] (saturating) = [0, max(...)]. No sampled prefix anywhere (the silent fund-loss hazard). `historical_max_index` has a dedicated union-not-receive-only test. - Contact-xpub equality compares the canonical public material (compressed pubkey + chain code); version/parent-fingerprint bytes are path-structural and excluded (they don't affect derived addresses / fund reachability). - Quarantine is a SUCCESSFUL TERMINAL classification (never an error, never triggers restore); it is NEVER inferred from identifier class — always computed over the real index range. Tests written FIRST (7, all pass). They empirically encode the fund-safe truth and surfaced a MATERIAL finding: dip14-migration-hardstop.md §6.2's expectation that low-index contacts (first 28 bytes of identity id zero) are "expected migratable" is DISPROVEN. DET ckd_priv_256 has a `< 2^32` compatibility branch — for low-index it HMACs only index[28..32] (ser_32, 4 bytes); upstream key-wallet ckd_priv for ChildNumber::Normal256 ALWAYS HMACs the full 32-byte index (ser_256), no compatibility shortcut. Different HMAC input ⇒ different child key ⇒ low-index contact funds unreachable via upstream ⇒ MUST quarantine. Convergence holds ONLY for the full-256-bit class on mainnet/account-0 (both engines ser_256; base path m/9'/5'/15'/0' identical). This is the §6.5 release-blocking scenario realised. The predicate correctly quarantines (does NOT silently assume equivalence); the quarantine machinery is the sole safety net until an upstream ser_32-compatibility fix or explicit acceptance. It does NOT block P3 — quarantine is the designed safety net; expected quarantine residue is WIDER than §6.2 predicted (testnet + non-account-0 + ALL low-index contacts). Reported to the architect via memory. `#![allow(dead_code)]` (TODO(P3c)) until the Stage-B engine wires the predicate in next sub-step. build + clippy --all-features --all-targets -D warnings + fmt --check GREEN; 7 predicate tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d accepted trade-off User decision 2026-05-18: drop backwards compatibility, support only what platform-wallet supports. The migrate-or-quarantine / hard-stop apparatus is WITHDRAWN and NOT implemented. Changes applied: - dip14-migration-hardstop.md: SUPERSEDED banner at top; body retained as historical record only. - open-questions.md #6: re-resolved to upstream-only derivation; quarantine apparatus withdrawn. - data-model-and-migration.md: Stage B simplified to single-branch model (no quarantine fork); dashpay_dip14_quarantine_active now INERT/RESERVED (P4 cleanup); new "Accepted fund-accessibility trade-off" subsection with mandatory one-time informational notice spec. - backendtask-contract.md: DashPay row — quarantine error path removed; TaskError::DashPayContactDerivationIrreconcilable noted as P4 candidate. - phasing.md: P3 re-scoped (drop back-compat, upstream-only); P3a/P3b marked DONE; P3c–P3e rewritten; P4/P5 updated; QA matrix updated. - feature-coverage.md, removal-inventory.md, backend-architecture.md, README.md: live quarantine references replaced with pointer to data-model-and-migration.md "Accepted fund-accessibility trade-off". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the P3b classify_contact predicate (+ its 7 tests) with a single-branch Stage-B engine. Back-compat for legacy DashPay DIP-14 derivation is dropped (Decision #6): contacts are re-established on upstream derivation unconditionally — no DET re-derivation, no comparison, no quarantine. The accepted fund-accessibility trade-off is covered by the mandatory one-time notice landing in P3e. Stage B (run_stage_b) is async, post-unlock, marker-gated, and runs behind the AppContext-owned tokio::sync::Mutex (invariant C6, acquired before the pending-marker check in ensure_wallet_backend). Each step is idempotent for crash-and-relaunch: 1. backup precondition — refuse to migrate without the .premigration recovery floor 2. ensure_wallets_registered — re-register wallets from seed (tolerates upstream WalletAlreadyExists; chain identity sync covers step 3) 3. identities — intentional no-op (DET blob + tables retained; repopulated by step-2 chain sync) 4. re-establish every accepted DashPay contact via upstream register_contact_account on account 0, keyed (owner, contact) 5. finalize — single success fork; legacy-table DROP gated off (LEGACY_DROP_ENABLED = false) until P3d invariants land Supporting changes: - wallet_backend: register_dashpay_contact, ensure_wallets_registered, flush_persister; register_persisted_wallets made idempotent - database: load_all_accepted_dashpay_contacts, drop_legacy_migrated_tables, Database.path + db_file_path - error: dedicated TaskError::WalletPersistenceFlushFailed (typed #[source], no String field, calm actionable message) - context: stage_b_migration_lock field + C6-correct wiring Build, clippy --all-features --all-targets -D warnings, and +nightly fmt all green. P3a migration tests (5) pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the launch-time corruption-recovery path and the P3d invariant suite, then lift the gate on the destructive legacy-table DROP. Database::recover_from_premigration_if_corrupt(db_file_path) runs on file paths in app.rs BEFORE the live Database is opened. It restores the live DB from the retained `<db>.premigration` floor ONLY when the floor exists AND the live DB is missing/unopenable/fails PRAGMA integrity_check — the exceptional path. A healthy-but-pending DB is NEVER restored; it is left for the idempotent Stage-B retry. Restore is atomic (copy→temp→rename) and re-runnable (the floor is never consumed). Kept out of initialize() so it stays a pure launch concern. New `p3d` test module (7 tests) pins the deterministic, network-free invariants now that the DROP is live: - backup_exists_before_any_destructive_step (backup-before-destroy) - drop_removes_all_legacy_tables_and_retains_floor (DROP strictly last; retained surfaces survive; floor still holds pre-migration data) - destructive_sequence_is_idempotent (crash-relaunch finalize tail) - no_restore_when_live_db_healthy_but_pending (restore is exceptional) - restore_on_injected_corruption (restore + marker still pending → Stage-B retries) - no_restore_without_floor - restore_is_rerunnable (floor survives repeated corruption cycles) With the invariants green, the LEGACY_DROP_ENABLED gate is removed and Stage-B step 5 now unconditionally runs: durable flush_persister → drop_legacy_migrated_tables (strictly last) → clear marker. Crash between any of these re-runs cleanly (idempotent registration/contacts, DROP TABLE IF EXISTS, idempotent marker clear). All 12 P3a+P3d tests pass. clippy --all-features --all-targets -D warnings and +nightly fmt green. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…otice Add the standalone migration QA harness and the release-blocking post-migration notice — the SOLE compensating control for the accepted DashPay fund-visibility trade-off (Testnet/Devnet or secondary-account contact payments may not appear in this version). Notice (release-blocking, unconditional, never downgraded): - New pure module `migration_notice`: verbatim MIGRATION_NOTICE_TEXT (jargon-free, calming, gives a concrete recourse) + pure `should_show(migration_completed, notice_shown)` decision so the guarantee is unit-provable and cannot silently regress. - New durable settings bits: `platform_wallet_migration_completed` (set by Stage-B finalise, written BEFORE clearing the pending marker so a crash between the two re-runs cleanly) and one-shot `platform_wallet_migration_notice_shown`. `completed` (not merely "not pending") is the discriminator so a fresh install — which also has pending == false — NEVER sees the notice. - AppState::maybe_show_migration_notice: once-per-launch, shows a dismissible info MessageBanner exactly once to every migrated user, persists the one-shot guard immediately (fail-open: never fewer than once). The in-launch guard is intentionally not set while still uncompleted so a migration finishing later this session still fires. QA harness (`tests/migration_pw/`, standalone integration crate, run `cargo test --test migration_pw`): network-free lanes through the public API — release-blocking notice contract (shows once, durable one-shot flag, unconditional, fresh-install-excluded, verbatim & jargon-free), launch-time restore-only-on-injected-corruption, user-never-unlocks, no-restore-without-floor, restore re-runnable, Stage-B cross-launch marker idempotency & reentrant single-source. `tests/kittest/button_sizing.rs` deliberately left untracked (out of scope, must not be touched). 13 harness tests + 14 lib tests (P3a/P3d/migration_notice) green. clippy --all-features --all-targets -D warnings and +nightly fmt green. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ovider P4 dead-code prune — the verified-safe, fully-traced removals: - TaskError::DashPayContactDerivationIrreconcilable: zero callers (the quarantine apparatus was withdrawn, Decision #6). Removed. - dashpay_dip14_quarantine_active settings column + its get/set accessors: never populated by any live path; the only reader was a P3a test assertion (also removed). Dropped from the migration-column helper and the fresh CREATE TABLE settings. (No DB downgrade is supported, so dropping the never-used column is safe; existing DBs simply keep an unused column until a future schema rebuild.) - context_provider::Provider (the RPC SDK ContextProvider): orphaned since chain sync became SPV-only. Zero external references. Removed the struct + its ContextProvider/Clone/Debug impls and now-unused imports. The shared resolve_data_contract / resolve_token_configuration helpers (still used by SpvProvider) are retained unchanged. - Refreshed a stale Stage-A doc comment that referenced "migrate-or-quarantine". ZMQ-listener audit (Decision #3): CONCLUSION = RETAIN. The listener feeds AppContext::received_transaction_finality (asset-lock proofs for Platform identity top-up/registration) and chain-locked-block UI — a non-wallet consumer exists, so it is not dropped. Remaining P4 items deferred (see final report): the "dead UTXO / WalletTransaction model+tables" and the RPC-mode-toggle / Core-wallet -picker / local-node settings UI prune are NOT dead — WalletTransaction and utxos are live fields of the Wallet model rendered by the wallets screen. Safely separating the genuinely-dead subset from the live wallet-display model is an unprovable irreversible step under the current budget (STOP rule); broken out for a dedicated follow-up. clippy --all-features --all-targets -D warnings + +nightly fmt green. 13 migration_pw + 65 database/migration_notice tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…); add WalletSnapshot read model - backend-architecture.md: add WalletBackend read-accessor surface (DetWalletBalance, DetUtxo, WalletTransaction, address_balances), WalletSnapshot ArcSwap push model via EventBridge, TransactionRecord→WalletTransaction mapping placement, and prominently called-out A04 fund-safety mandate (display-only snapshot; spend path remains WalletBackend::send_payment/create_asset_lock_proof) - backendtask-contract.md: note that the UI data-path rewire introduces no BackendTask variant changes — display fed by WalletSnapshot via existing EventBridge Refresh - phasing.md: split P4 into P4a (UI data-path rewire, blocks P4b) and P4b (mechanical prune, only after P4a); add P4 sub-steps section with exit criteria and reviewer gates; add post-migration UI data-path test release-blocking lane to QA matrix; update sequencing note and crew assignments - README.md: update STATUS block — P3c-P3e GREEN, P4-partial done, P4 split noted, display-only gap and WalletSnapshot model described Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the display-only WalletSnapshot push model and rewires the wallets-screen HD reads off the legacy Wallet balance/tx/utxo fields onto the WalletBackend snapshot. What landed: - src/wallet_backend/snapshot.rs: DetWalletBalance, DetUtxo, WalletSnapshot view models + SnapshotStore. Transaction history is event-sourced (upstream drops chain-locked records from memory with keep-finalized-transactions OFF), accumulated in the store; balance is lock-free atomics (pw.balance()); UTXOs via non-blocking try_state(). Relocates the TransactionRecord -> WalletTransaction mapping (the surviving piece of the deleted reconcile_spv_wallets). - WalletBackend: four DET-typed read accessors (wallet_balance, transaction_history, utxos, address_balances) + has_snapshot; registers each wallet's PlatformWallet handle in the store; idempotent WalletAlreadyExists path resolves the id by deterministic seed re-derivation (no error-string parsing). - EventBridge: holds the SnapshotStore; on_wallet_event accumulates the event's TransactionRecords and recomputes/publishes the affected wallet's snapshot, then emits the existing TaskResult::Refresh. - UI rewire (HD only; single-key untouched): wallets_screen balance/tx-table/account-summary/receive-address, address_table utxo-count/balance, account_summary.collect_account_summaries now takes the snapshot address_balances. FUND-SAFETY (A04): the snapshot is DISPLAY-ONLY. Coin selection / tx construction still go through WalletBackend::send_payment / create_asset_lock_proof (upstream live UTXO set, P2). No code path selects spendable inputs from WalletSnapshot. Scope note: the QR funding-arrival detector (capture_qr_funding_utxo_if_available) was intentionally NOT moved to the snapshot. Its output flows into RegisterIdentity/TopUp FundWithUtxo -> *_asset_lock_transaction_for_utxo, a spend path. Snapshot-sourcing it would route coin selection through the snapshot (A04 violation). That path is a pre-existing P2 gap (a display-cache UTXO -> FundWithUtxo spend); fixing it (route via create_asset_lock_proof) is P2-class backend work, recorded as a blocking finding for P4b/P2 follow-up. Leaving this detector reading the legacy wallet.utxos preserves exact prior behavior — P4a introduces no new snapshot->spend coupling. Pre-first-sync snapshot is empty -> UI renders the "syncing" state, not a zero-balance bug. Quality gates GREEN: cargo build --all-features, clippy --all-features --all-targets -D warnings, +nightly fmt --check, 474 lib tests (incl. 5 new snapshot tests + updated event_bridge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ock funding, FundWithUtxo removed w/ disclosure - phasing.md: insert P4a.5 between P4a and P4b (3 paths: AssetLockKind::Shielded + shielded bundle rewire, FundWithUtxo removal, received_transaction_finality slim); gate P4b on P4a.5 exit; add P5 Smythe release-blocking audit with I1–I6 invariants and 3 test lanes; update crew assignments and QA matrix - backend-architecture.md: document AssetLockKind::Shielded; state no upstream funding-outpoint API at #3625 head; document FundWithUtxo removal; document received_transaction_finality as asset-lock-finality-only with ZMQ retained - backendtask-contract.md: split IdentityTask register/top-up row — FundWithUtxo variants removed, document accepted behavior change and disclosure; add Net Frontend Impact item 5 - data-model-and-migration.md: extend mandatory one-time post-migration notice with FundWithUtxo-removal sentence - docs/user-stories.md: add IDN-014 [Removed — upstream-only funding] for FundWithUtxo / QR-direct-fund identity capability Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lock, FundWithUtxo removed, ZMQ finality slimmed
Right, the fund-safety leg lands. Three paths, zero double-spend exposure:
Path 1 — Shielded asset-lock now goes through the one true engine.
Added AssetLockKind::Shielded → upstream AssetLockShieldedAddressTopUp.
Rewired shield_from_asset_lock from the legacy
generic_asset_lock_transaction + select_unspent_utxos_for-over-Wallet.utxos
to WalletBackend::create_asset_lock_proof. Upstream selects inputs from
its authoritative live UTXO set at construction, stores-before-broadcast,
and tracks to finality itself. DET does no coin selection here. (I1, I4)
Path 2 — FundWithUtxo gone, compile-enforced, no dead arm.
Deleted RegisterIdentityFundingMethod::FundWithUtxo and
TopUpIdentityFundingMethod::FundWithUtxo, their backend arms, the entire
QR-direct-fund UI in both identity screens, and
registration_asset_lock_transaction_for_utxo /
top_up_asset_lock_transaction_for_utxo /
asset_lock_transaction_for_utxo_from_private_key. Disclosed in the
shipped one-time notice — added the mandated FundWithUtxo sentence to
MIGRATION_NOTICE_TEXT and its verbatim pins (unit + migration_pw). (I3)
Path 3 — received_transaction_finality slimmed to asset-lock-only.
Deleted the Wallet.utxos / address_balances / legacy utxos-table write
loop and the DashPay-payment side-effect. Retained the asset-lock
detection/registration branch. The orphaned DET-side broadcast/finality
helpers (broadcast_and_commit_asset_lock, wait_for_asset_lock_proof,
broadcast_raw_transaction) — left with zero callers once Path 1+2 land —
are deleted, not orphaned: upstream owns the whole asset-lock lifecycle.
ZMQ call sites stay; the always-empty Vec return preserves them. (I2, I5)
Also removed the last live Wallet.utxos reader in the asset-lock UI
(capture_qr_funding_utxo_if_available) — create_asset_lock_screen already
detects arrival via the upstream-fed CoreItem event; funding_utxo was
write-only dead state.
Exit: zero live Wallet.{utxos,address_balances,transactions} readers in
the HD spend path; zero callers of the deleted *_for_utxo* fns; the
legacy select/build cluster is now fully dead (P4b deletes it).
Build + clippy -D warnings + nightly fmt + full workspace tests GREEN;
backend-e2e/e2e/kittest compile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d cluster (I2)
The whole DET-side parallel spend engine is now unreachable after P4a.5 —
identity register/top-up, shield, and asset-lock funding all route through
the upstream WalletBackend. This deletes the closed, zero-live-caller
cluster outright (not orphaned — I2):
- src/model/wallet/asset_lock_transaction.rs deleted entirely:
select_utxos_with_fee_retry, generic_asset_lock_transaction,
registration_asset_lock_transaction, top_up_asset_lock_transaction,
asset_lock_transaction_from_private_key, plus the now-private-only
calculate_asset_lock_fee helper they exclusively used (zero external
consumers — confirmed workspace-wide).
- Wallet::{build_standard_payment_transaction,
build_multi_recipient_payment_transaction, set_transactions,
select_unspent_utxos_for, remove_selected_utxos, reload_utxos} deleted.
- Their dead unit tests + test-only helpers (test_wallet_with_utxo,
register_test_address) deleted with them; surviving helpers kept.
- Stale imports / doc references cleaned to present-state.
Workspace-wide refs to every parallel-spend-engine fn: zero. I2 holds.
Remaining P4b (legacy Wallet balance fields/methods, src/database/utxo.rs
+ wallet.rs legacy queries, schema cleanup, RPC-mode settings UI) is
BLOCKED: those fields still have many LIVE readers in HD screens P4a never
rewired to WalletSnapshot (top_up/add_new identity, shield, send,
add_new_wallet, wallets_screen balance column, mcp wallet tool). Deleting
them is gated on completing the P4a snapshot rewire of those screens —
P4a scope, not a mechanical prune. Reported, not silently diverged.
Build + clippy -D warnings + nightly fmt + full workspace tests GREEN;
backend-e2e/e2e/kittest compile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrate every remaining live legacy `Wallet` balance/address-balance reader (HD identity/wallet screens + MCP wallet-balance tool) onto the display-only `WalletBackend` `WalletSnapshot` accessors, finishing the P4a data-path rewire that previously covered only `wallets_screen`. - Add centralised `AppContext::snapshot_balance` / `snapshot_has_balance` / `snapshot_address_balances` helpers so call sites are a one-line mechanical swap and the legacy `has_balance` predicate semantics live in one place. - Rewire top_up / add_new_identity / add_new_wallet / shield / send / dashpay-send / wallets-screen dialogs / MCP `core_balances_get`. - `AddressInput` now takes per-wallet snapshot balances explicitly (`WalletWithBalances`) instead of reaching into `Wallet.address_balances` — keeps the component free of `AppContext` and snapshot display-only. - Drop the now-meaningless `Wallet.address_balances` removal sub-block in the network-chooser maintenance action (snapshot is EventBridge- owned and read-only; field is deleted in P4b). Fund-safety unchanged: the snapshot is DISPLAY-ONLY. No spendable-input selection from it — spending stays on `WalletBackend::send_payment` / `create_asset_lock_proof` (A04/I1 gate). Single-key paths (Decision #7) untouched: wallets_screen/mod.rs:469,527 are SingleKeyWallet, not the legacy HD `Wallet` — verified by source, left as-is. Exit: `grep` proves zero live readers of legacy `Wallet.{utxos, address_balances,transactions,confirmed/unconfirmed/total_balance,...}` + balance methods outside single-key / wallet_backend / db / model / tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removes the 8 dead HD Wallet fields (utxos, address_balances, address_total_received, transactions, confirmed_balance, unconfirmed_balance, total_balance, spv_balance_known) and the orphaned balance/total-received method cluster (has_balance, max_balance, confirmed_balance_duffs, spv_confirmed_balance, unconfirmed_balance_duffs, total_balance_duffs, update_spv_balances, update_address_balance, recalculate_affected_address_balances[_with_db], recalculate_address_balance, update_address_total_received). Deletes src/model/wallet/utxos.rs entirely. Grep-proven zero live readers: balances/tx/UTXO reads were relocated to the WalletSnapshot in P4a. Conditional spend functions (select_unspent_utxos_for et al.) were already removed in 1089b56. P4a-gap completion (forced by GREEN-per-commit): two live legacy Wallet.utxos readers P4a missed are retargeted to the established snapshot accessor — send_screen::get_core_addresses and Wallet::unused_bip_44_public_key (display/skip-logic only; spend path stays upstream-authoritative via CoreTask, no money-path change). backend-e2e harness balance/tx reads retargeted to snapshot_balance / WalletBackend::transaction_history (tx_is_ours retargeted, not ignored). Carve-out intact: src/database/utxo.rs, single_key_wallet.rs, and the utxos SQLite table untouched. wallet_addresses-driven known/watched address reconstruction preserved byte-for-byte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deletes the four DB writer methods left with zero production callers after C1: update_address_balance, add_to_address_balance, update_wallet_balances, update_address_total_received — plus their three unit tests. The historical v16/v17 schema-migration functions (add_wallet_balance_columns, add_address_total_received_column, ensure_wallet_columns_exist) are RETAINED: they are existence-guarded idempotent steps on the irreversible migration ladder; removing them would break old-DB upgrades. The dead balance columns are superseded by the forward-only C3 settings/wallet migration, which is the fail-safe path. The column-independent wallet_addresses-driven known/watched reconstruction and platform_address_info path are untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps DEFAULT_DB_VERSION 35 -> 36 and adds a v36 migration arm that drops the orphaned `dashpay_dip14_quarantine_active` settings column via the inverted pragma_table_info existence guard (drop_dead_settings_columns in settings.rs). The column was introduced by an early P3a build and withdrawn with the quarantine apparatus (commit 4499c82); it is no longer created by any code path but lingers in upgraded DBs. Object-disjoint from the post-unlock Stage-B engine: v36 mutates ONLY the settings table, never wallet/utxos/wallet_transactions — no double drop, no ordering hazard. Existence-guarded and idempotent. Spec-vs-source note: the spec's "RPC-era dead settings columns" clause has no satisfiable referent — every other settings column (core_backend_mode, use_local_spv_node, custom_dash_qt_path, etc.) still has live readers. Dropping a live column would be irreversible data loss, so only the provably-orphaned quarantine column is dropped. Adds release-blocking-adjacent test v36_drops_orphaned_quarantine_column_idempotently (present->dropped, idempotent re-run, clean-DB no-op, wallet data untouched). Version-bump fallout: fresh_install test asserts against DEFAULT_DB_VERSION instead of a hardcoded literal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the release-blocking Decision-#7 regression lane proving the P4b carve-out is intact, not merely asserted: - single_key_wallet_loads_utxos_via_retained_get_utxos_by_address: seeds the RETAINED utxos table via the #[cfg(test)] insert_utxo fixture + a single_key_wallet row, loads via get_single_key_wallets, and asserts SingleKeyWallet.utxos is hydrated through the retained get_utxos_by_address path (count, summed value, script round-trip). - decision_7_stub_still_surfaces_single_key_unsupported: pins TaskError::SingleKeyWalletsUnsupported structurally and asserts the user-facing disclosure (unsupported / preserved / future update / HD alternative) verbatim so a regression that silently re-enables single-key spends or weakens the message fails CI. Flips SND-002 [Implemented] -> [Gap]: single-key send is gated by Decision #7 (data + UTXOs retained and load correctly; only the spend action is unavailable). The prior "works the same as HD wallets" text was factually stale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scan reference (SEC-004) data-model-and-migration.md: Stage-B SUCCESS fork now explicitly drops only `wallet` and `wallet_transactions`; `utxos` table called out as RETAINED with rationale (Decision #7 single-key load path, fund-data-loss risk). Dead-fields list corrected: `src/model/wallet/utxos.rs` (HD-only model) is deleted; but `src/database/utxo.rs` and the `utxos` table are explicitly NOT in scope for deletion. phasing.md: settings-migration disjointness note corrected — P3c drops `wallet`/`wallet_transactions` only, not `utxos`. SEC-001 test-ordering requirement added to P5 single-key regression lane: the test must run the Stage-B DROP first, then load the single-key wallet, to prove the `utxos` table survives the migration (prior isolated seed+load approach was tautological). removal-inventory.md: `src/database/utxo.rs` moved from DELETE to RETAINED with Decision #7 rationale. `wallet`/`wallet_transactions` table drop enumeration corrected to exclude `utxos`. backend-architecture.md: new "Seed / Secret Boundary" section added with the accurate SEC-004 statement — `SeedReregistrationLoader` uses in-memory `Zeroizing` material and never persists seeds/keys; no automated `secrets_scan` test exists (future hardening). Replaces the stale `tests/secrets_scan.rs` reference that only appears in the superseded dip14-migration-hardstop.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-wire DET's persisted-wallet load onto the upstream seedless
watch-only rehydration API (PR #3692). Closes the reserved G2 swap
point; removes SeedReregistrationLoader and the seed-bearing
WalletRegistration entirely.
Loader seam reshaped: PersistedWalletLoader::wallets_to_register (sync,
seed-bearing Vec<WalletRegistration>) becomes async load(...) ->
LoadedWallets { loaded, skipped } keyed by DET WalletSeedHash. New
DET-opaque types LoadedWallets / PersistedLoadSkip carry no upstream
type across the seam.
UpstreamFromPersisted delegates to WalletBackend::load_from_persistor_
seedless: one load_from_persistor() call rebuilds every wallet
watch-only (no seed in memory), then each loaded wallet is resolved to
its DET WalletSeedHash via the account-xpub bridge.
Fund-safety gates:
- Gate 1 (fund routing): a loaded wallet is accepted only when its
BIP44 account xpub matches a persisted DET sidecar xpub_encoded.
This IS the published-xpub == scanned-xpub invariant by construction;
an unmatched wallet is rejected, never displayed.
- Gate 2 (signing): the load path stays seedless; the seed enters
memory only at the unlock chokepoint (handle_wallet_unlocked ->
WalletBackend::provide_seed), preserving asset-lock / payment /
DashPay signing.
signer_for / derive_private_key now return WalletLocked (not the
inverted WalletBackendNotYetWired) when the seed is absent.
Deviations from the design doc, forced by upstream reality:
1. The seedless bridge keys on the persisted BIP44 account xpub, not a
root-WalletId derivation: upstream WalletId = SHA256(root_xpub) at
depth 0, but DET persists the depth-3 account xpub, so the parent
cannot be derived from it. The account-xpub match is the literal
routing invariant and needs no migration. A unit test pins that
DET's and upstream's account xpub agree for the same seed.
2. The a5538dc8 identity-funding re-provision moved off the watch-only
load path: add_account derives from the root private key, which a
watch-only wallet lacks. It now runs only when the seed is already
cached; the post-unlock asset-lock chokepoint remains the
authoritative provisioner (TC-021 still covered).
EventBridge surfaces WalletSkippedOnLoad as a structured warn (UI
banner deferred, TODO PROJ-010-T6).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seedless wallet rehydration (PROJ-010) + platform → PR #3692Pushed 5 commits (
Planning docs (rode along): rehydration design (
🤖 Co-authored by Claudius the Magnificent AI Agent |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n sign (Smythe SEC-001/002) After the seedless rehydration re-wire, a wallet's seed reaches the backend (WalletBackend.inner.seeds) only through provide_seed, which is called from the single unlock chokepoint AppContext::handle_wallet_unlocked. The no-password unlock paths opened the seed into the in-memory Wallet but never ran that chokepoint, and bootstrap_loaded_wallets ran only at AppContext::new — before the backend was wired and against an empty wallet map, so it was a no-op. Result: a hydrated no-password HD wallet showed as unlocked yet every signing op (send, asset lock, identity register/top-up, platform-address top-up) failed WalletLocked, while DashPay/shielded (which read the seed straight off the Wallet) kept working — a confusing, fund-blocking dead end. Make seed injection uniform across every unlock transition: - ensure_wallet_backend now runs bootstrap_loaded_wallets at its tail, after hydration populates ctx.wallets, so every cold-booted Open wallet gets its seed pushed to the backend (SEC-002 root cause). The dead call at AppContext::new is removed. - try_open_wallet_no_password and ScreenWithWalletUnlock::should_ask_for_password route through handle_wallet_unlocked after open_no_password, so the per-operation no-password path injects the seed via the same chokepoint (SEC-001). All ~30 call sites pass &self.app_context. - The backend-e2e harness no longer needs its manual post-wire bootstrap_loaded_wallets call — ensure_wallet_backend covers it. Regression test no_password_wallet_resignable_via_unlock_chokepoint reproduces the post-hydration state (Open wallet, empty seed cache), asserts the WalletLocked symptom, then proves the chokepoint restores a usable signer. Fails before the fix, passes after. The account-xpub fund-routing gate and the seedless load path are untouched (Smythe cleared them); the empty-passphrase single-key vault (SEC-003) is out of scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: no-password hydrated wallets couldn't sign (Smythe SEC-001/002)
Symptom: after the seedless rehydration, a no-password HD wallet hydrated as Root cause: seeds reach Fix (one chokepoint, every path funnels through it):
The account-xpub fund-routing gate and seedless load path are untouched. 613 lib tests pass, clippy clean. Smythe re-verify in flight.
🤖 Co-authored by Claudius the Magnificent AI Agent |
…ate-on-error unlock) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gner (foundation)
Wave 1 of the just-in-time secret-access refactor: the additive,
unit-tested foundation. No removals, no consumer rewiring — the eager
residencies (R1 inner.seeds/provide_seed, R2 single_key_unlocked, R3
Wallet::Open) and all call-site swaps are Waves 2-4 and untouched here.
New modules (src/wallet_backend/):
- secret_prompt.rs — the UI<->async seam. SecretPrompt trait,
SecretScope{HdSeed,SingleKey}, SecretPromptRequest/Reply, the settled
RememberPolicy{None,UntilAppClose,For(Duration)} (default None;
For(Duration) is data-model-only for now), SecretPromptCancelled, and a
#[cfg(test)] scripted TestPrompt double. SecretString crosses the seam;
no derived key, no upstream key_wallet types.
- secret_access.rs — the chokepoint. with_secret (FnOnce) and
with_secret_session (AsyncFnOnce) hand plaintext to a closure by borrow;
resolution is session-cache (TTL-honored) -> unprotected fast-path ->
prompt + decrypt-jit + re-ask-on-wrong. Opt-in per-scope session cache,
boxed zeroizing values, poison-safe forget/forget_all.
- det_signer.rs — DetSigner borrows the held secret (no snapshot copy),
implements the upstream key_wallet Signer for the HD path and a path-free
sign_single_key_ecdsa for single keys. Compiles + unit-tested via the
mock prompt; #![allow(dead_code)] until the Wave 2 call-site swap.
Smythe must-fixes baked in: (1) closure form, no storable guard; (2)
borrow-only, exploiting SecretBytes/SecretPlaintext having no Clone; (3)
boxed session secrets so a HashMap rehash leaves no un-wiped copy; (4)
unprotected scopes never prompt; (5) sentinel-confinement tests assert no
secret leaks into any error Display/Debug.
New typed TaskError variants: SecretPromptCancelled, SecretDecryptFailed,
HdPassphraseIncorrect — no user-facing String fields, no error-string
parsing.
24 new unit tests (15 secret_access, 6 secret_prompt, 3 det_signer); full
lib suite green, clippy --all-features --all-targets -D warnings clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wave B of the JIT secret-access refactor: make the prompt infrastructure live without migrating consumers or retiring the eager model (Wave C). - EguiSecretPromptHost: the GUI SecretPrompt. request() enqueues onto an mpsc channel, repaints, and awaits a one-shot reply. AppState drains one request per frame into an ActivePrompt and answers on submit/cancel. - passphrase_modal: shared modal chrome extracted from WalletUnlockPopup (overlay, centered window, focus-once, Cancel/Esc/X/click-outside). WalletUnlockPopup now renders through it; the JIT prompt adds the "Keep this wallet unlocked until I close the app." checkbox (RememberPolicy::UntilAppClose; unchecked = None). No UI for For(Duration). - NullSecretPrompt: headless (MCP/CLI) host; the chokepoint surfaces a typed TaskError::SecretPromptUnavailable for protected scopes (Q-HEADLESS: no env/flag passphrase). Unprotected scopes still resolve headless. - SecretAccess is constructed in WalletBackend::new from the host-chosen prompt (GUI injects the egui host via AppContext::install_secret_prompt; headless keeps NullSecretPrompt). Prompt-copy meta seeded at hydration. - forget_all wired into the network switch (outgoing context) and exposed for teardown. Additive only: DetSigner is not swapped into call sites, the eager seeds/single_key_unlocked/Wallet::Open residencies stay, no consumer signing behavior changes. One Wave-C-pending #[allow(dead_code)] on WalletBackend::secret_access(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… route R3 backend readers through chokepoint Wave C: migrate every secret consumer onto the SecretAccess chokepoint and retire the eager seed-residency model. Signing/derivation now decrypt the seed just-in-time from the encrypted vault and confine it to a closure that zeroizes on return — no long-lived plaintext seed or single-key cache. R1 — HD seed session map (RETIRED): delete Inner.seeds + provide_seed + signer_for + the WalletAssetLockSigner module. send_payment, register_identity, top_up_identity, and create_asset_lock_proof now wrap their signer-driven upstream call in one with_secret_session(HdSeed) scope (one prompt per operation for a passphrase wallet, none for a no-password wallet), building DetSigner::from_held over the borrowed seed; derive_private_key reads the same held seed for the credit-output key. handle_wallet_unlocked stops distributing the seed — it now only promotes a verified-open seed into the session cache (UntilAppClose). register_persisted_wallets no longer reprovisions at load time (the asset-lock chokepoint reprovisions with the JIT seed). R2 — single-key cache (RETIRED): delete Inner.single_key_unlocked, the unlock_with_passphrase / forget_unlocked cache methods, and the import_wif cache-prime. raw_key_bytes/sign_with on the view now serve unprotected keys only; protected single-key signing flows through the new WalletBackend::sign_single_key, which decrypts via with_secret(SingleKey) and signs through DetSigner::sign_single_key_ecdsa. The typed SingleKeyPassphraseRequired/Incorrect errors now surface from the chokepoint. R3 — Wallet::Open whole-session seed (backend readers rerouted): DashPay derive_contact_xpub_material and shielded initialize_shielded_wallet now pull the seed JIT via with_secret(HdSeed) instead of reading Wallet::Open; first_open_wallet_seed is removed. Eager shielded init at unlock is removed — keys derive on first shielded op. RESIDUAL (noted for Smythe): the in-model seed_bytes() derivation readers (address bootstrap, identity-key derivation, the Wallet Signer impl) still read Wallet::Open; fully reshaping WalletSeed::open to verify-not-park is the large gated follow (Q-R3 / T8) and would require rerouting ~20 synchronous model call sites — out of scope for this wave. Fund-routing untouched: the published==scanned account-xpub gate and per-network coin-type derivation are unchanged; only WHERE/WHEN the seed is obtained changed, never WHICH keys are derived. Secrets stay Zeroizing, borrow-only, closure-confined, never logged. Allows removed: secret_access() dead_code and det_signer.rs module-level dead_code. Tests: adapted no_password_wallet_resignable_via_unlock_chokepoint to the JIT model (no-password wallet signs after cold-boot via the unprotected fast-path, no cache); added sec_002_protected_sign_via_chokepoint (protected single-key signs through the chokepoint with wrong-then-right passphrase). 641 lib tests green; clippy --all-features --all-targets clean; kittest DivergentVersion failures are pre-existing (no new failures). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reroute the four async Wallet::Open seed readers off the parked seed
(seed_bytes()) onto the JIT chokepoint (SecretAccess::with_secret), so
each fetches the HD seed just-in-time and derives inside the closure:
- dashpay/contacts.rs — derive_contact_info_keys (contactInfo decrypt)
- dashpay/contact_info.rs — derive_contact_info_keys (contactInfo encrypt)
- dashpay/incoming_payments — register_dashpay_addresses_for_identity
- qualified_identity::sign — ECDSA_HASH160 path-scan recovery fallback
The two contactInfo derivations were identical; their body is lifted into
one wallet_backend helper, derive_contact_info_encryption_keys, so the
BIP-32 derivation lives in a single place and the raw seed never leaves
the wallet_backend seam. QualifiedIdentity gains a non-serialized
secret_access carrier (attached alongside associated_wallets at hydration)
so the model-layer signer can reach the chokepoint; the field is skipped by
Encode/Decode and excluded from PartialEq, exactly like associated_wallets.
Additive/parallel: the ~23 sync in-model readers still use Wallet::Open
until D2-D4, so the build stays green. Wallet::Open, seed_bytes(), the
platform Signer<PlatformAddress> impl, and the bootstrap/private_key family
are deliberately untouched.
Smythe fixes:
- SEC-W-001 (MEDIUM): centralise the DashPay wallet selection on
QualifiedIdentity::dashpay_wallet{,_seed_hash} (lowest seed hash) so the
send side and receive side provably pick the same wallet; add a
multi-wallet regression test proving published == scanned for the selected
wallet and divergent for the other.
- SEC-W-003 (LOW): wrap SingleKeyView::raw_key_bytes in Zeroizing<[u8;32]>
so the bare unprotected single-key array wipes on drop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stop the synchronous in-model HD-seed readers from reaching back into a wallet's parked session seed (self.seed_bytes()). They now take the seed as a borrowed parameter (&[u8;64]) supplied by their async caller's with_secret_session chokepoint scope — one session per bootstrap run. Additive/parallel: Wallet::Open + seed_bytes() stay defined (D4 removes them), so the build stays green; only the SEED SOURCE changes, never the derivation math (same paths, same per-network coin-type, same keys). Parameterized (seed source: self -> borrowed param): - bootstrap_known_addresses + the whole bootstrap_* family (bip32, coinjoin, identity registration/invitation/topup/not-bound, provider, platform-payment) in model/wallet/mod.rs. The bip44 child is unchanged (it derives from the master xpub, never the seed). - Added *_with_seed variants for private_key_at_derivation_path, private_key_for_address, derive_private_key_in_arc_rw_lock_slice, and KeyStorage::get_resolve; the legacy parked-seed variants stay until their remaining callers are drained in D3/D4. Async callers wired through the chokepoint: - context/wallet_lifecycle.rs: bootstrap_wallet_addresses is the single sync seed bridge (reads self once, R3 #17 -> D4); new async sibling bootstrap_wallet_addresses_jit opens with_secret_session per run. bootstrap_loaded_wallets is now async (awaited from ensure_wallet_ backend) and JIT-resolves the seed. It is gated on is_open() so cold boot never forces a surprise passphrase prompt — open wallets resolve via the no-passphrase fast-path or the session cache promoted on unlock. - QualifiedIdentity::sign probes the pure, secret-free wallet_seed_hash_for and opens with_secret only for genuinely wallet-derived keys; Clear / AlwaysClear keys resolve with no seed access and no prompt. - fund_platform_address_from_asset_lock derives the asset-lock key via with_secret + private_key_for_address_with_seed. (The &wallet platform Signer at top_up is R3 #15 — untouched, D3.) Borrow-only: the seed is passed by &Zeroizing/&[u8;64] and derived in place; no owned bare-array copy outlives the scope, no 'static/Box/Arc of the seed. Errors stay typed (no seed/passphrase in TaskError/Debug/logs). Tests (model/wallet/mod.rs): - seed_param_derivation_matches_parked_seed_derivation: byte-identical private keys param-vs-self across one representative path from every bootstrap family, on Testnet and Mainnet — proves zero derivation drift. - private_key_for_address_seed_param_matches, slice_derive_seed_param_ matches: the *_with_seed variants match their legacy counterparts. - seed_param_derivation_error_does_not_leak_seed: confinement — no seed bytes in the seed-param derivation result. D3/D4 surfaces deliberately untouched: Signer<PlatformAddress> for Wallet, the 4 platform-fund SDK sites, get_platform_address_private_key, WalletAddressProvider owned seed, wallet_seed_snapshot, WalletSeed::open reshape, and the sync UI key viewers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y WalletAddressProvider (R3 D3) Replace the still-live `impl Signer<PlatformAddress> for Wallet` — which read the wallet's parked session-long plaintext seed — with a just-in-time `DetPlatformSigner<'a>` at every fund-moving SDK call site. The new signer borrows the HD seed held open by a `with_secret_session` scope, maps the platform address to its DIP-17 path through a pure prebuilt `PlatformPathIndex`, derives locally, and signs with the EXACT same primitive (`dashcore::signer::sign`) the legacy impl used. Only the seed source changes; network is known up front instead of brute-forced across all four (safer, R-4). Fund sites swapped `&wallet` -> `&signer` inside the secret scope: - transfer_platform_credits (transfer_address_funds) - withdraw_from_platform_address (withdraw_address_funds) - fund_platform_address_from_asset_lock (top_up; asset-lock key + signer in one scope) - fund_platform_address_from_wallet_utxos (top_up) FUND-SAFETY PARITY (mandatory): unit tests prove `DetPlatformSigner` produces byte-identical `sign` / `sign_create_witness` output AND byte-identical derived keys to the legacy `Wallet` signer for the same address/data on Testnet AND Mainnet. A divergence here = wrong signatures = lost/failed funds. Rebuild `WalletAddressProvider` to drop its owned `seed: [u8;64]` field (a residency the borrow was meant to prevent, R-1). It now derives the DIP-17 account-level xpub once from a borrowed seed (resolved through the chokepoint by `fetch_platform_address_balances`) and derives every gap-limit child publicly — the final `index` is non-hardened, so the addresses are byte-identical to the seed path (parity test `provider_xpub_matches_seed_derivation`). SEC-D2-001 (LOW): `TaskError::WalletKeyLookupFailed` is now fieldless — drop the user-facing `String` field (project rule #7); callers log the cause via tracing. Borrow discipline: `DetPlatformSigner<'a>` is lifetime-bound to the held seed and the index, never `Box`/`Arc`/`'static`; the SDK takes `&S` (Nagatha R-2). `can_sign_with` is a pure index lookup — no secret, no prompt (R-6). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ey viewers off the parked seed (R3 D3)
Add a `WalletTask::DeriveKeyForDisplay { seed_hash, derivation_path }` backend
task that fetches the HD seed just-in-time through the chokepoint, derives the
key in the backend, and returns only the WIF wrapped in `Secret` end-to-end via
`BackendTaskSuccessResult::WalletKeyForDisplay`. The seed never crosses into the
UI layer (Smythe-approved: same trust boundary as the on-screen display).
Convert the wallets-screen private-key viewers (the "View Key" button in the
address table and the post-unlock display in mod.rs) from the synchronous
`derive_private_key_wif` (which read the wallet's parked seed) to queueing the
request; the `ui()` loop drains it into the backend task and
`display_task_result` renders the returned `Secret` WIF.
The key_info_screen viewers are intentionally NOT converted here — their derived
`RPCPrivateKey` also feeds the on-screen `sign_message` feature, so they need a
dedicated signing chokepoint rather than a display-only WIF task. Deferred to D4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r (Smythe SEC-001) Migrate the last two Signer<PlatformAddress> consumer sites that the Wave D3 swap missed — identity registration and top-up funded by Platform addresses. Both sites built a session-long plaintext wallet snapshot and passed the legacy `impl Signer<PlatformAddress> for Wallet` (`&wallet_clone`) into the SDK fund call. They now build a pure, secret-free `PlatformPathIndex` before the secret scope and sign through a JIT `DetPlatformSigner` that borrows the HD seed only for the duration of the SDK call (seed zeroizes on return, never enters this layer by value) — identical to the four D3-migrated fund sites. - register_identity_from_platform_addresses: `put_with_address_funding` now takes `&signer`; the SDK result flows untouched into the existing `log_drive_proof_error` match arm. - top_up_identity_from_platform_addresses: `top_up_from_addresses` now takes `&signer`; the redundant `is_open()` gate is dropped — the chokepoint resolves an unprotected/session-cached wallet without a prompt and returns WalletLocked only when the seed is truly unavailable. Derivation, coin-type, and signing primitive are unchanged (DetPlatformSigner has proven byte-parity with the legacy Wallet signer); only the seed source moves from the parked seed to the borrowed JIT seed. The legacy `impl Signer<PlatformAddress> for Wallet` and `get_platform_address_private_key` now have zero production callers — their only remaining users are the det_platform_signer parity tests, leaving them clean for D4 removal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… (R3 D4a) The four platform-fund sites surfaced TaskError::ContactWalletSeedUnavailable when expose_hd_seed() returned None inside the HdSeed chokepoint scope. That variant's Display is contact-request-specific and misleading for a generic missing/locked HD seed. Swap to TaskError::WalletLocked — operation-neutral and accurate for the 'no usable seed' case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… (R3 D4a) Convert the seed_bytes() readers that need no persistence change onto the SecretAccess chokepoint, leaving Wallet::Open/seed_bytes() in place for D4c. - Delete dead identity_top_up_ecdsa_private_key / identity_registration_ecdsa_private_key (no callers): removes two parked-seed reads outright. - platform_receive_address: add generate_platform_receive_address_with_seed seed-param variant + WalletTask::GeneratePlatformReceiveAddress backend task; the receive dialog dispatches it instead of generating on the UI thread. - key_info_screen: route the two WIF/hex display reads through WalletTask::DeriveKeyForDisplay and add WalletTask::SignMessageWithKey so the on-screen sign-message feature signs wallet-derived keys in the backend — seed JIT, only the public signature returns. Clear/AlwaysClear keys still sign locally (no seed involved). - handle_wallet_unlocked: skip the parked-seed snapshot for no-password wallets (the chokepoint's unprotected fast-path covers them prompt-free); the protected-wallet snapshot and the register_wallet fresh-open bootstrap stay for D4c, coupled to the WalletSeed::open reshape. Exact derivation preserved (same paths/keys/coin-type); only the seed SOURCE changes. New no-drift test for the platform-address variant; secrets/WIF/keys never enter logs, TaskError, or task results (signatures are public). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e seed reads (D4b) The identity-authentication derivation path is hardened to the leaf, so its public keys cannot be derived from a stored xpub. Instead memoize the derived 33-byte compressed public keys in the DET KV sidecar, keyed per (network, identity_index, key_index) under DetScope::Wallet(seed_hash). The two PUBLIC readers (mod.rs:1045/1069) split into a pure seed-free *_cached variant and a seed-param *_from_seed variant; the three async callers (load_identity, load_identity_from_wallet, discover_identities) do cache-hit-else-one-JIT-cold- fill, partitioning all misses in a request into a single with_secret scope and writing them back. A cold or absent cache self-heals; correctness never depends on the cache being present. Lazy warm hangs off the existing bootstrap_wallet_addresses_jit seed scope for known identity indices; later-discovered identities warm via the read-path cold -fill. The private-key sibling (mod.rs:1112) stays pure-JIT and is untouched. Public material only — never caches private keys. Network-keyed coordinates exclude cross-network reuse. No SQL migration / no refinery touch / no KV schema bump (additive new key, asserted by test). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…D4c-1) Reshape the identity-key chooser to read PUBLIC keys, not private ones: - Rename IdentityKeys -> IdentityKeySpecs (public-only): IdentityKeyEntry carries the cached public key + derivation path, never a PrivateKey. A single constructor (from_cached_public_key / from_seed) derives both from one (network, identity_index, key_index) coordinate (RK-1), so the key submitted to Platform and the key the JIT chokepoint signs with at registration are byte-for-byte identical. to_public_keys_map / to_key_storage read entry.public_key. - Chooser sources keys from the D4b AuthPubkeyCache. A cold cache queues WalletTask::WarmIdentityAuthPubkeys (warm-before-show), shows a "Preparing identity keys…" hint, and leaves the key set empty so registration stays disabled until warm (fail-closed, RK-2). - Advanced-mode WIF column becomes per-row "Show WIF" routed through WalletTask::DeriveKeyForDisplay — the seed never enters ui(). - Production build_identity_registration becomes a public seed-param builder (build_identity_registration_with_seed) plus an async chokepoint wrapper. e2e helper now async, delegates to it, and drops the dead master_key_bytes return across all fixtures/tests. - Delete the now-dead identity_authentication_ecdsa_private_key (mod.rs:1228) and the test-only legacy private_key_at_derivation_path / private_key_for_address; re-anchor their parity tests against a direct BIP-32 reference derivation. - LOW: stale ContactWalletSeedUnavailable -> WalletLocked in derive_key_for_display and the JIT bootstrap. No-drift tests prove public==private derivation across networks/indices and that the reshaped maps/storage round-trip; build + clippy (-D warnings) + lib tests green. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…acy Wallet signer (R3 D4c-2 A)
The shielded shield-transition was the 7th fund site still signing through
the LEGACY parked-seed `impl Signer<PlatformAddress> for Wallet` — D3 swapped
the six address-funding sites but missed this one.
- `build_shield_credit` (sync, spawn_blocking) and `shield_credits` (async)
now build a `DetPlatformSigner` inside a `with_secret_session(HdSeed{..})`
scope, mirroring the 6 D3 fund sites. The pure `PlatformPathIndex` is built
before the secret scope; the borrowed seed zeroizes when the scope returns.
The sync builder bridges the async chokepoint + async builder with the same
`block_on` it already used for `build_shield_transition`.
- With no production caller left, DELETE `impl Signer<PlatformAddress> for
Wallet` and `get_platform_address_private_key` (confirmed zero non-test
callers). DetPlatformSigner has proven byte-parity, so signing behaviour is
unchanged — only the seed source moves. Parity stays anchored by the
det_platform_signer reference tests (direct derive, no deleted impl).
- Also retire the now-dead in-model seed readers this wave drains:
`derive_private_key_in_arc_rw_lock_slice` (legacy parked-seed slice-derive)
and `platform_receive_address` (parked-seed generate); keep the `_with_seed`
variants. Re-anchor their parity tests to direct BIP-32 references.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…okepoint (R3 D4c-2 B)
Retire the last INDIRECT parked-seed readers: every legacy
`KeyStorage::get_resolve` caller now resolves keys without reading a wallet's
parked seed.
- `KeyStorage`: replace `get_resolve` (which read the parked seed via the
slice-derive) with `get_resolve_local` (seed-free; resolves Clear/AlwaysClear,
fails closed on Encrypted/wallet-derived). `get_resolve_with_seed` now
delegates its non-wallet arms to `get_resolve_local` (DRY) and derives
wallet-derived keys from the borrowed JIT seed.
- New `QualifiedIdentity::resolve_private_key_bytes(target, key_id)` is the one
async resolver: `wallet_seed_hash_for` decides whether to open a
`with_secret(HdSeed{..})` scope (`get_resolve_with_seed`) or resolve seed-free
(`get_resolve_local`). `sign` now calls it instead of its inline match.
- DashPay async fund-adjacent sites (`payments`, `contact_requests`,
`auto_accept_proof::verify`) and `auto_accept_proof::generate` route through
the resolver. The two auto-accept fns become `async` (their callers were
async or move to a task).
- Sync UI seed reads become backend tasks: a new
`DashPayTask::GenerateAutoAcceptQrCode` (QR generator) and the existing async
`GroveSTARKTask::GenerateProof`, which now carries the `QualifiedIdentity`
(boxed) and resolves the signing key + ed25519 public key via the chokepoint
in the backend — the seed never enters `ui()`.
- Re-point the identity-key-specs roundtrip test from the deleted `get_resolve`
to `get_resolve_with_seed`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-seed path (R3 D4c-2 C) The production `Wallet::platform_receive_address` (parked-seed) was deleted in part A; its only remaining callers were the backend-e2e suite and the parity mirror (re-anchored in A). Add a test-only `framework::funding::derive_platform_receive_address` that reproduces the old reuse-or-generate behaviour without touching a seed in the test: reuse returns an existing watched platform address (seed-free); generate dispatches `WalletTask::GeneratePlatformReceiveAddress`, which derives the next address through the JIT chokepoint in the backend. Migrate all nine e2e call sites to the async helper and drop the now-unused write-lock/seed plumbing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…et residency (R3 capstone) The last two production parked-seed readers are converted and the `Wallet::seed_bytes()` accessor is deleted, so an open wallet now parks NO plaintext seed. The compile-gate proves 100% elimination: `rg "\.seed_bytes\(\)" src/` returns zero — the only plaintext seed left in the process lives inside a `with_secret*` frame of the chokepoint. Model reshape (verify-not-park): - `WalletSeed::open(password)` decrypts only to PROVE the passphrase, then discards the plaintext (Zeroizing, dropped in-scope) and flips to `Open`. - `OpenWalletSeed` drops its `seed` field — now a plain struct holding only `wallet_info: ClosedKeyItem`. The two-variant `Open`/`Closed` enum is kept; `Open` means "unlocked/verified", carrying no seed. The generic `OpenKeyItem<const N>` collapses (it was only ever `OpenKeyItem<64>`). - `Wallet::seed_bytes()` and the `Drop for WalletSeed` zeroize-impl are removed. Reader conversions: - `register_wallet(wallet, seed: &[u8;64])` takes the freshly-created seed by borrow; the fresh-register bootstrap and password-wallet promotion run off that borrow via the seed-param `bootstrap_wallet_addresses` and a new `promote_seed_to_session`. No parked-seed read. - `handle_wallet_unlocked(wallet, passphrase: Option<&str>)` promotes a password wallet's seed into the JIT session cache by decrypting the stored envelope through the chokepoint (`SecretAccess::promote_hd_seed_with_passphrase`, the same `decrypt_hd_seed` path signing uses) — never a parked seed. `wallet_seed_snapshot` is deleted. Behavior preserved: no-password signing (unprotected fast-path), cold-boot prompt-free startup (password wallets with no passphrase in hand are skipped), and the promote-before-bootstrap ordering invariant. Tests: new `open_wallet_retains_no_plaintext_seed` proves the Open state holds no plaintext; all `#[cfg(test)]` seed_bytes() callers now take the known test seed via a `test_seed()` helper; no-password/unlock/bootstrap behavior tests stay green (`no_password_wallet_resignable_via_unlock_chokepoint` included). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…R3 QA) Final-QA copy/docs cleanup for the JIT secret-access refactor — no fund-logic changes. - QA-001: fix import_single_key kittest tc_sk_004 to assert the rendered "Testnet address" copy instead of the never-rendered "Network: Testnet". - PROJ-001: CHANGELOG Unreleased Changed bullet for sign-time JIT unlock. - PROJ-002: WAL-006 acceptance criteria updated to the JIT model. - PROJ-003: HdPassphraseIncorrect now says "passphrase" (matches the variant name, the JIT retry modal, and the single-key variants) — one concept, one term for i18n. - PROJ-004: det_signer module header now reflects that sign_single_key_ecdsa is wired into WalletBackend::sign_single_key (no stale expect(dead_code)). - PROJ-005: WalletSeed/OpenWalletSeed/open doc comments rephrased to present-state (M-NO-TOMBSTONES). - PROJ-006: tightened the session-cache deadlock-avoidance comment to 4 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eroizing Fund-critical and security coverage for the JIT secret-access refactor (R3 QA). No signing/derivation logic changed. - QA-002: HD DetSigner parity test mirroring the platform-signer one — an independent reference (path -> derive_priv_ecdsa_for_master_seed -> secp.sign_ecdsa) must yield byte-identical signature AND public key, looped over [Testnet, Mainnet]. A pinned (seed, path) -> pubkey vector guards the derivation itself against an internally-consistent regression. - QA-003: cross-scope re-entrancy test — a with_secret_session closure for scope A that .awaits with_secret for scope B resolves both, guarding the documented lock-release-before-await property against a future deadlock. - QA-006: open_no_password now pinned to REJECT a password-protected envelope (length guard) and ACCEPT an unprotected one. - QA-004: assert_can_sign now drives a real sign_ecdsa, so the no-password cold-boot resignable test exercises derive+sign, not just signer construction. - QA-007: context-level test seeds the session cache and runs the exact call finalize_network_switch funnels through (forget_all_secrets), asserting the outgoing cache is emptied. - SEC-103: SingleKeyEntry::decrypt now returns Zeroizing<[u8;32]> so JIT-derived key bytes wipe on drop across the function boundary; callers updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 14 AppState-constructing kittests resolved their data dir via the real ~/.config/Dash-Evo-Tool path, crashing on a Migration(DivergentVersion) from a stale det-app.sqlite and polluting the user's wallet data. They now run through a with_isolated_data_dir helper that redirects DASH_EVO_DATA_DIR (an existing production env hook) to a throwaway tempdir, serialized by a process-global mutex and restored via an RAII guard. No production change needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JIT secret-access refactor — HD seed no longer parked in memory (R3 complete)Pushed 24 commits ( Core (
QA (three independent passes)
Also ( CHANGELOG + user-story WAL-006 updated for the new unlock model.
🤖 Co-authored by Claudius the Magnificent AI Agent |
Summary
From-scratch wallet backend rewrite on the upstream
platform-walletcrate, which owns SPV chain sync internally. DET is now a thin adapter:src/spv/is deleted, the RPC wallet mode is removed,reconcile_spv_walletsis gone, and theBackendTaskaction/channel contract is preserved so the UI layer is largely unchanged. A one-time, two-stage, marker-gated data migration moves existing wallets onto the upstream model with a*.db.premigrationbackup. This branch supersedes — and removes — the prior incremental-migration spec; the spec now reflects the implemented design.NEW since last push —
a90603a6finish-unwire QA cycle + retry + re-pass (workflow-feature, Phase 3 → Phase 2 retry → Phase 3 re-pass)Phase 3 QA quad (Marvin / Smythe / Trillian / Adams) found 2 HIGH + 6 MEDIUM + ~13 LOW. Auto-retry per workflow-feature severity gate. Three parallel Phase 2 retry waves landed 14 commits; a tight Phase 3 re-pass (Marvin + Smythe focused on the deltas) found 2 additional LOWs (QA-R-001 + SEC-009), both closed by the final commit (
a90603a6). 15 commits total in this wave.HIGH findings — both addressed.
cc887fec) — migration sentinel was global (det:migration:finish_unwire:v1) but migration bodies filterWHERE network = ?1. Mainnet upgrade → sentinel set → testnet switch → testnet wallets invisible forever. Fix: per-network sentinel (det:migration:finish_unwire:<network>:v1) +BTreeSet<Network>dispatch guard + banner reconciler reset on network switch. Two regression tests including the mainnet→testnet sequence.48cdb8ad→6052dc72→4d235a5e) —SecretStore::file(path, SecretString::new(""))opened the single-key vault with an empty passphrase, deterministic Argon2id KDF, file-read = key compromise despite.pwsvaultextension implying real encryption. User-chosen UX = Option C: per-key passphrase at import time (mirrors the HD per-wallet password model). NewSingleKeyEntry { ciphertext, salt, nonce, has_passphrase, passphrase_hint }, leading version byte (folds in SEC-005),import_with_passphrase+unlock_with_passphraseAPI, typedTaskError::SingleKeyPassphrase{Required,Incorrect,TooShort,Mismatch}(noStringmessage fields). Import dialog extended with optional checkbox + passphrase + confirm + hint. Backward compat: legacy 32-byte entries decode as unprotected without panic. Sign-time prompt modal explicitly deferred — it would touch every signing path (register_identity,top_up_identity,send_funds,asset_lock_signer,core_zmq_listener); TODO marker atwallet_backend/mod.rs::single_key(), follow-up tracked separately.MEDIUM findings — all addressed.
24ea553b) —TaskError::WalletStorageNotReadyandlegacy_shielded_present_but_sidecar_empty()were both dead surface; dev plan promised enforcement that never landed. Fix:is_wallet_touching()helper + short-circuit gate inrun_backend_taskcovering Wallet / Core / Identity / DashPay / Shielded task families; shielded family additionally consults the NFR-4 pre-flight predicate. Matrix unit test pins the families.23be4330) — deadMigrationStep::label()(only called from an internal unit test) diverged from the livemigration_running_text()inapp.rson 4 of 6 steps. Fix: delete dead label fn; canonical source isapp.rs::migration_running_text(). Internal test redirected.ce676fa7) — 8 sites ofmsg.contains("no such table")violated the "never parse error strings" rule. Fix: typedlegacy_table_exists_namedpre-check before every legacySELECT; rusqlite error arms now unreachable for missing-table case.23be4330) —MigrationState::Failed { reason: String }violated "noStringfields for user-facing messages" and broke Display/Debug separation. Fix:Failed { error: Arc<MigrationError> }; UI callsDisplay::fmtat render time.TaskError::MigrationFailedmirrors the Arc so publisher hands the same refcount to bothErrandFailedstate. ManualPartialEqviaArc::ptr_eq.b182f016) — noCHANGELOG.mdfor downstream consumers. Fix: bootstrap Keep-a-Changelog[Unreleased]with the finish-unwire migration entry.0afcf539+/tmp/marvin-finish-unwire-exceptions.md) — 5 trivial TCs implemented (TC-DEV-001/002/003 + TC-SH-007 + TC-MIG-003); 14 deferred with categorized justification (backend-e2e network-gated, perf benches, a11y framework limits, feature-flag-only). Newtests/legacy_table_surface.rscovers the dev-table no-write invariants.LOWs — all addressed.
f13b9cc4) — UI dialog rendered theTaskError::InvalidWifliteral verbatim instead of via the typed variant; sentence existed twice. Fix: render viaTaskError::InvalidWif { source }.to_string().f13b9cc4) —format!("Network: {}", network_label(...))violated i18n placement (concat fragments, Polish noun-case breakage). Fix:format!("This is a {network} address.", network = ...)— complete sentence, named placeholder.5ae36bc6) — T-PERF-01 had promotedwallet_backend::{hydration, single_key, wallet_meta, wallet_seed_store}modules from internal →pubso benches could reach helpers. Fix: newbenchcargo feature;pub modgated by#[cfg(any(test, feature = "bench"))]. Bench target carriesrequired-features = ["bench"].48cdb8ad) — envelope label was versioned (envelope.v1) but the bincode payload had no leading version byte. Fix: leadingversion: u8; readers default-fill v=1 for legacy entries; round-trip test asserts both paths.48cdb8ad) — migration trusted legacy salt/nonce lengths without validation; corrupted DB rows could panic crypto primitives. Fix: length guards skip-and-log corrupted rows via typedTaskError::MigrationCorruptedEntry; no whole-migration abort.48cdb8ad) — non-64-byte non-password seed silently degraded to Closed wallet with no user feedback. Fix: typedTaskError::SeedLengthInvalid { wallet_label, got, expected }; surfaces a banner explaining which wallet was skipped and why.13706030) — TC-MIG rustdoc citations swapped vs spec. Fix: rustdoc IDs corrected; one citation infinish_unwire.rs:1299left for Bilby-A territory (now part of her commits viacc887fec).Phase 3 re-pass LOWs (closed by
a90603a6):wallet_touching_matrix_is_stabletest docstring claimed every wallet-touching family was pinned; only 3 of 5 were asserted. Fix: addedIdentityTask+DashPayTaskassertions so a future drop from thematches!arm is caught at test time.SingleKeyEntryandStoredSeedEnvelopederivedDebugover aciphertext/encrypted_seedfield that, for unprotected entries, held raw private key bytes. Latent: no current call site exploited it, but a futuretracing::debug!(entry = ?entry, ...)would have leaked the key. Fix: replaced the derive with manualimpl Debugemitting"[redacted]"for the secret-bearing field; new unit tests per struct assert a known plaintext does not appear in the Debug output.Trivial cleanup landed in the wave:
7ad1a426) — "this PR" stale rustdoc reference onopen_secret_store→ stable ADR cross-link.d30f9371— paragraph break in SEC-001's rationale doc comment to satisfydoc_lazy_continuationclippy lint.Test coverage delta: 548 → 563 passing lib tests (+15 net, +10 from SEC-002 per-key passphrase suite + 2 from SEC-001 + 1 from PROJ-001/002 + 2 from SEC-009 redaction). 80 kittests pass + 1 pre-existing failure on
tc_sk_004_valid_wif_renders_address_preview(confirmed pre-existing — not regressed by this wave). New dedicatedtests/legacy_table_surface.rs(3 passing) pins the dev-table no-write invariants.Phase 3 spec correction (Adams nit PROJ-007): previous PR body section claimed
single_key_wallet.rsshrank330 → 77; actual is330 → 83. Acknowledged.Explicitly deferred to follow-up (not blocking):
docs/ai-design/2026-05-29-finish-unwire/notes.md:92is a deliberate post-merge edit.dashpay/docs(separate repo); follow-up issue to be filed./tmp/marvin-finish-unwire-exceptions.mdcategorizes each (backend-e2e / perf benches / a11y / feature-flag-only).Quality gates on merged tip (
a90603a6):cargo clippy --all-features --all-targets -- -D warnings— greencargo test --lib --all-features— 563 passed, 0 failed, 1 ignoredcargo +nightly fmt --all -- --check— cleancargo check --no-default-features— greencargo check --features bench— greenPhase 3 re-pass verdict: PASS (no MEDIUM+ findings remaining). Workflow-feature Phase 4 (Lessons Learned) is the next coordinator step.
NEW since last push —
a4926f5cfinish-unwire campaign (workflow-feature, P1–P4)Closes out the data.db unwire by retiring the legacy DET wallet store entirely.
WalletBackendnow owns the full read/write path;data.dblingers for the deferred migration tool only. 8 commits, eight tasks, three parallel-agent waves at the tail.Landed (in order):
26ac4c43T-W-00 — DET wallet-metadata sidecar (<network>:wallet_meta:<seed_hash>indet-app.sqlite) carryingalias / is_main / core_wallet_name / xpub_encoded. UpstreamWalletMetadataEntrydoesn't carry alias; a sidecar was needed once Stage-B mirror was dropped.a6ba77b1→ superseded by380b09b7T-W-00.5-v2 — per-wallet password support:StoredSeedEnvelope { encrypted_seed, salt, nonce, password_hint, uses_password, xpub_encoded }at SecretStore labelenvelope.v1, scoped byWalletId. (v1 went plaintext-only; user reversed the decision after seeing the UX implications. v2 is what shipped.)06ce4c19T-W-01 — HD wallet cutover.db.get_wallets()cut atcontext/mod.rs:262; cold start now goes throughwallet_backend::hydration::hydrate_wallets_for_network(network)reading sidecar + envelope.b67d45f4T-W-01b — single-key wallet cutover. Cutdb.get_single_key_wallets(); newSingleKeyViewowns<network>:single_key_meta:<addr>sidecar + SecretStore labelsingle_key_priv.<addr>(scoped to fixedSINGLE_KEY_NAMESPACE_ID, not per-WalletId). Per-key passwords rejected at import time — vault scope is the single gate; per-key UX deferred (see follow-up T-MIG-03).42b88a15T-DEV-02 — dead-surface delete insrc/database/. 13pub fndeleted acrosswallet.rs/single_key_wallet.rs/utxo.rs(net -554 LOC) plus 2 stale tests.single_key_wallet.rsshrank 330 → 77 lines. Kept-with-tether log in commit message documents every survivor (UI rename/remove flows, migration helpers,WalletErrorenum with 21 active callsites).2e6a34fdT-PERF-01 — criterion 0.5.1 benches atbenches/wallet_hydration.rsfor cold-start hydration (HD and single-key, N=1/10/100). Smoke-tested viacargo bench -- --test; full measurement run deferred (not gating).a4926f5cT-DOC-01 — kv documentation refresh. New canonical keyspace reference table indocs/ai-design/2026-05-29-finish-unwire/notes.mdcovering all 3 stores (det-app.sqlite, platform-wallet.sqlite, SecretStore) and 23 key patterns. Corrected 4 stale claims, including one risky misstatement:single_key_privwas documented as per-WalletId scope when it's actually a fixed namespace constant — a future migration-tool author following the old text would have shipped broken scoping.Migration-tool input (recorded for the future companion tool):
wallet.encrypted_seed / salt / nonce / master_ecdsa_bip44_account_0_epk / alias / is_main / uses_passwordrows → SecretStoreenvelope.v1+<network>:wallet_meta:<seed_hash>sidecar.single_key_walletrows → SecretStoresingle_key_priv.<addr>(fixedSINGLE_KEY_NAMESPACE_ID) +<network>:single_key_meta:<addr>sidecar.det:migration:finish_unwire:v1indet-app.sqlite(MigrationTask::FinishUnwire).Accepted trade-off (disclosed):
wallet_backend::{hydration, single_key}modules promoted from internal →pub;WalletSeedView::new,WalletMetaView::new,open_secret_store,SingleKeyView::rehydrate_indexexposed; newpubconstructorsSingleKeyView::from_viewsandhydration::hydrate_hd_wallets_from_views. All first-party types — M-PLATFORM-WALLET-FIRST-PARTY untouched — but external bench code is now coupled to internal hydration mechanics. A follow-up could tighten this behind abenchcargo feature.Deferred to follow-up PRs (non-blocking):
Quality gates on merged tip (
a4926f5c):cargo clippy --all-features --all-targets -- -D warnings— greencargo test --lib --all-features— 548 passed, 0 failed, 1 ignoredcargo +nightly fmt --all -- --check— clean (each agent ran it pre-commit)Phase 3 QA (Marvin / Smythe / Trillian / Adams) is the next wave.
NEW since last push —
a5538dc8identity top-up fix (contained exception)IdentityRegistration/IdentityTopUpHD funding accounts were never derived at registration, and the upstreamrs-platform-walletpersisterload()does not reconstruct them — so every top-up (and every relaunch) failed with "Identity top-up account for index N not found". Root cause: upstreamrs-platform-wallethas no host-facing identity-funding-account registrar (no sibling toregister_contact_account). The host is expected to provision the account itself before the asset-lock funding call — confirmed by dashpay/platform PR #3549, which ships an inlineadd_identity_topup_accountprecondition helper in its own test framework rather than adding a production registrar.Fix shape (contained exception, faithful port of #3549's helper):
WalletBackendmethod does the keys-bearing dual insert (Wallet::add_account(AccountType::Identity*, …)+ManagedCoreKeysAccount::from_account(…)→info.core_wallet.accounts.insert_keys_bearing_account(…)), idempotent via direct membership probes (no error-string parsing).peek_next_funding_addressreads both collections, so a single insert is insufficient.u32-onlyIdentityFundingAccountenum) — zerokey_wallet::*past theWalletBackendseam (M-DONT-LEAK-TYPES).create_asset_lock_proofchokepoint so every asset-lock caller (IdentityTask,CoreTask, future) is covered with one idempotent pre-step — TDD caught a third caller (CoreTask::CreateTopUpAssetLock) the per-call-site plan missed; the chokepoint refactor is the minimal correct solution.register_persisted_walletsfor every persisted identity index — the recurrence trap #3549 never tests (fresh wallet per test there); without it, top-up would fail again on every relaunch.9cdcfb25to addregister_identity_funding_accounttors-platform-walletand remove the exception.Proven (live testnet,
RUST_MIN_STACK=16777216):core_tasks::test_tc005_create_top_up_asset_lock— FAIL → PASS (asset lock broadcast, real TXID).identity_tasks::tc_021_identity_funding_account_survives_reload(new) — PASS (restart-survival viaSeedReregistrationLoaderreload chokepoint)."Identity top-up account for index ... not found"occurrences post-fix: 0.tc_020/tc_014were environment-blocked during the verification window (live testnet/Platform-DAPI outage —NoAvailableAddresses, funds not confirming) — NOT the fix; bug symptom count = 0; samecreate_asset_lock_proofchokepoint proven by TC-005/TC-021. Re-confirm pending on a healthy testnet to close them out.Compile-fix
99f0e92d(default-features build): barecargo check(no flags) was broken at 3 sites usingAppContext::sdk()/wallets()in method form — those methods only exist undercfg(any(test, feature = "testing")), while the underlyingpub(crate) sdk: ArcSwap<Sdk>/pub(crate) wallets: RwLock<…>fields are always in-crate accessible. Switched to field access (self.sdk.load().as_ref().clone()/ctx.wallets.read()?) and added an explicitcollect::<Vec<u32>>()annotation. 2 sites from a5538dc8's reload re-provision + 1 pre-existing frombaba200b(P4a) that--all-featureshad been masking. Pure compile-fix, zero logic change, I1–I6 untouched. Verifiedcargo check(defaults),--all-features,--no-default-featuresall EXIT=0.Eager
WalletBackendinitc1ea1c25(b0972b77): kills thedash_sdk::sync10ms retry-loop spam against live DAPI in the pre-unlock window.AppContext::ensure_wallet_backend(sender)+WalletBackend::start()hoisted from lazy-on-first-backend-task to eager-at-AppState::new(and atfinalize_network_switchfor runtime network switches). Case B confirmed via dashpay/platform PR #3549:PlatformWalletManager::newis wallet-independent at construction;SeedReregistrationLoaderskips locked wallets pre-unlock so the manager runs SPV with zero registered wallets, and wallets register lazily on unlock. Defense in depth: tightened theSpvProvider::get_quorum_public_keygate-error map fromContextProviderError::Generic(<user-facing Display>)toContextProviderError::Config("chain backend not initialized (pre-unlock)")so the SDK retry classifier no longer broadcasts the user-facing "temporarily unavailable" copy into logs. Zerokey_wallet::*/SpvRuntime/PlatformWalletManagerreferences introduced in DET — M-DONT-LEAK-TYPES preserved. I1–I6 untouched. All 6 quality gates green (cargo checkdefaults /--all-features/--no-default-features, build, clippy-D warnings,+nightly fmt --check); 446 lib tests pass. Live-environment empirical confirmation (30s pre-unlock log capture, expecting norequest failed, retryinglines) is pending user dev-env verification — the sandbox lacks DAPI config + live network access for that window.Pre-push gate note (tracked TODO): the pre-push gate (and CI) currently runs only
--all-features— that is why this slipped through. Going forward it should also include barecargo checkandcargo check --no-default-features;cfg(any(test, feature = "testing"))-gated accessors are a project pattern, and an --all-features-only check will keep missing this class of regression.CRITICAL fix landed previously — SPV now actually syncs (
71c8baa0)WalletBackend::start()never spawned the upstreamSpvRuntimerun loop, so the wallet never synced in any context — GUI and MCP included (peers stayed at 0 forever; balances never resolved).71c8baa0spawns the run loop instart(). Live-validated against testnet: SPV connects and reaches full sync (~60s, "SPV fully synced — mempool bloom filter active"), framework wallet spendable balance resolves, and real end-to-end flows (asset-lock broadcast, identity registration, token queries, DPNS) pass.Test harness
WalletBackend(no mock seam) so the suite exercises production sync/spend paths.wait_for_wallet_in_spvmatches the typedTaskError::WalletBackend(the upstream runtime's documented "retry in a moment" signal — matched by variant, never by string) and retries within its existing timeout budget. Test-infra only — zerosrc/product change.E2E health (post-last-baseline-run; the new
a5538dc8fix has not yet been re-tallied against the full 59-test suite)Last full baseline run (pre-
a5538dc8-fix, post-6e85c462retry-fix): 48 / 59 pass. Witha5538dc8landed, TC-005, TC-020 and TC-014 are expected to move from FAIL → PASS once testnet conditions allow a clean re-tally (TC-005 proven directly; TC-020/TC-014 share the same chokepoint, environment-blocked this session). Standing non-passes: 3 tracked DashPay/typed-error/address-reuse product bugs + 1 environment + 1 flake-candidate.backend-e2e REQUIRES
RUST_MIN_STACK=16777216(documented intests/backend-e2e/main.rs) — the Dash Platform SDK proof-verification path is deep and overflows the default test-thread stack without it.What shipped (P0 -> P5)
dash-sdkto the dashpay/platform #3625 head; deleted/stubbed the old wallet stack.WalletBackendskeleton +EventBridge+PersistedWalletLoaderseam.BackendTaskrewired ontoWalletBackend(action/channel contract preserved);ListCoreWallets/RecoverAssetLockshard-removed (Decision feat: choose mn to vote with #8).*.db.premigrationbackup; Stage-B simplified platform-wallet migration engine with invariants and legacy-table DROP; mandatory one-time post-migration user notice.WalletSnapshotread model + full wallets/screen data-path rewire onto the snapshot.FundWithUtxoremoved (disclosed), ZMQ finality slimmed.utxostable retained for the single-key carve-out (Decision feat: hide document button #7).Accepted trade-offs (disclosed)
*.db.premigrationbackup preserves pre-migration state.FundWithUtxoremoved: disclosed in the same one-time notice (the §2(d)+FundWithUtxo notice text is spec-verbatim).utxostable kept), swap-in deferred.Quality gates
a5538dc8fix touches funding-account derivation only; spend/coin-selection/broadcast unchanged.FundWithUtxomigration notice is spec-verbatim.Standing release gates / follow-ups
9cdcfb25— upstream-contribution TODO: addregister_identity_funding_accounttors-platform-wallet(sibling toregister_contact_account) so DET's containeda5538dc8exception can be removed.bfbc240f— DashPay incoming contact-request not associated with the sending identity after broadcast (TC-037/TC-043, likely one root cause).25f30d05— TC-019 inverted error precedence:RefreshSingleKeyWalletInfofor an unknown seed returnsWalletBackendNotYetWiredinstead ofWalletNotFound.e8212930— TC-012 receive-address reuse: two consecutivenext_receive_addresscalls return the same address (index not advanced/persisted).6e85c462— test-infra: enforceRUST_MIN_STACK=16777216(re-exec from the harness or document/CI-pin it) so contributors can't omit the documented prerequisite.7deebf4f— TC-066 key-not-visible-after-broadcast (flake-vs-bug, pending one confirmatory re-run).core_backend_modeplumbing/column and the cosmetic Core-wallet picker insend_screenremain; clearing them needs a C3-style schema migration + spend-path-aware review, out of safe scope for the gate-fix pass.Test Plan
RUST_MIN_STACK=16777216 cargo test --test backend-e2e --all-features -- --ignored --test-threads=1— full serial backend-e2e against testnet (48/59 last-baseline; TC-005/020/014 expected → PASS post-a5538dc8once testnet/DAPI healthy).cargo test --all-features --workspace— lib tests + suites green.cargo clippy --all-features --all-targets -- -D warnings— green.cargo +nightly fmt --all -- --check— green.Walletmutation, identity funding-account restart-survival (TC-021).docs/ai-design/2026-05-18-platform-wallet-migration/.🤖 Co-authored by Claudius the Magnificent AI Agent