Skip to content

chore: bump dashpay/platform pin to PR #3968 tip (seedless rehydration + shielded viewing-key persistence)#919

Open
Claudius-Maginificent wants to merge 17 commits into
v1.0-devfrom
chore/bump-platform-pin-pr3968
Open

chore: bump dashpay/platform pin to PR #3968 tip (seedless rehydration + shielded viewing-key persistence)#919
Claudius-Maginificent wants to merge 17 commits into
v1.0-devfrom
chore/bump-platform-pin-pr3968

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

TL;DR: Bump the pinned dashpay/platform dependency to the current tip of upstream PR #3968, landing seedless wallet rehydration and native persistence for shielded (Orchard) viewing keys, and adapting DET's wallet backend to further API changes as the branch has advanced. Closes review findings from a security/structural/adversarial due-diligence pass on each adaptation, across three rounds so far (initial bump review, a grumpy-review follow-up, and a PR-comment triage follow-up): one HIGH and one MEDIUM security issue, three LOW privacy leaks, one MEDIUM test-coverage gap, and several LOW/MEDIUM nits — all fixed and verified before merge. Correction (2026-07-23): a follow-up grumpy-review pass found this description previously (and incorrectly) claimed "no user-observable behavior change" — see "User-observable changes" below for what actually changed.

User story

As a DET user with a password-protected wallet that sends or receives shielded (Orchard) funds, I want the wallet's underlying platform dependency updated with upstream's latest persistence and security improvements, so my shielded funds stay fully recoverable from my seed phrase and my data directories are properly protected — even though it means a stricter password floor and an extra passphrase prompt per shielded transaction.

Scenario

Base flow

A user creates a wallet (optionally password-protected), uses it for transparent and shielded sends/receives/withdrawals, and expects that data to survive app restarts and be fully recoverable from the seed phrase alone.

Actual behavior (before this PR)

  • Wallet passwords had no enforced minimum length.
  • Once a protected wallet was unlocked, its shielded spend key stayed session-resident, so a passphrase prompt appeared only once per session no matter how many shielded operations followed.
  • Removing a wallet could still leave its shielded viewing key behind in an automatic backup snapshot, even though the user was told the wallet's data was cleared.

Expected behavior (after this PR)

  • New wallet passwords must be at least 8 UTF-8 bytes.
  • A protected wallet now prompts for its passphrase on every shielded transfer/unshield/withdraw, because the spend key is resolved just-in-time per operation and dropped immediately after — private key material is never left resident between operations.
  • Removing a wallet fully deletes its shielded viewing key, including from automatic backups.
  • Wallets already migrated to Tier-2 storage by a narrow, unreleased July-2026 weekly build with a shorter password cannot be opened at this pin — a documented compatibility note for that cohort only.

Detailed discussion

Shielded module changes

This bump changes how DET handles shielded (Orchard) key material end-to-end:

  • Native FVK persistence: upstream platform-wallet-storage now persists each wallet's shielded Full Viewing Key (FVK) natively in its own shielded_viewing_keys table, replacing DET's bespoke DetPersister metadata adapter (retired once upstream gained this support). This is what makes seedless shielded rehydration possible: DET can resync/rebind shielded state on a later launch without re-entering a passphrase.
  • Everything is still 100% derived from the wallet's single BIP-39 seed — no independent or separately-stored key material. Orchard spend authority is derived from the seed via the standard ZIP-32 hierarchical path (m / 32' / coin_type' / account'); only the FVK (view-only, cannot authorize a spend) is what's cached to disk, computed once at bind time. The spend authority itself is re-derived from the seed transiently on every shielded operation and dropped immediately after — never persisted. Losing the persisted FVK costs a rescan, not funds; restoring from seed phrase alone recovers everything. (Verified against the pinned orchard/rust-dashcore/platform-wallet sources — see the PR discussion for the full derivation-path citation.)
  • JIT (just-in-time) seed resolution changes user-facing behavior: shielded_transfer/shielded_unshield/shielded_withdraw now resolve the HD seed through the secret-seam chokepoint fresh for each single spend (src/wallet_backend/shielded.rs), rather than operating on an already-bound, session-resident spend key as before. Net effect: a password-protected (Tier-2) wallet now prompts for its passphrase on every shielded spend, not once per session — see "User-observable changes" below and docs/MCP.md/docs/CLI.md for the headless/MCP automation implication.
  • Residual, accepted risk: FVKs are stored unencrypted at rest, consistent with upstream's own "viewing-grade, not secret" classification (a viewing key can't move funds) — not a blocker, a documented trade-off.
  • Privacy fix, this update: removing a wallet now uses upstream's no-backup deletion entry point, so the shielded viewing key doesn't survive in an automatic pre-delete backup snapshot after the user is told the wallet was removed (see "PR-comment triage follow-up" below).

User-observable changes

This is primarily an internal dependency bump, but it is not behavior-neutral. Three things a user can notice:

  • New wallet passwords must be at least 8 UTF-8 bytes (measured in bytes, not Unicode characters — so a 4-character non-ASCII password like öäüß, 8 bytes, is accepted) — creation is rejected below that floor.
  • Wallets already migrated to Tier-2 storage by a July-2026 weekly build with a shorter password cannot be opened at this pin — a documented compatibility blocker for that narrow, unreleased cohort (see CHANGELOG).
  • Protected (Tier-2) shielded wallets now prompt for a passphrase on every shielded spend (transfer/unshield/withdraw), not just once per session — see docs/MCP.md/docs/CLI.md for the headless/MCP automation implication.

What was done

  • Bumped dash-sdk, rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage git pins, cumulatively d18020f5f376d32bebbd15c4e75f259f4a1ba64c (dashpay/platform PR #3968 tip, branch feat/platform-wallet-storage-rehydration). This is a re-bump within the same open PR: the branch has advanced substantially past each previous pin every time this update runs. Re-verified via git ls-remote that the branch has not moved past 4a1ba64c as of this update.
  • ebbd15c4e75f259f (previously landed, not yet reflected in this description until now): the hardened file and SQLite storage backends now validate permissions on every parent directory (app-data root, SPV dir, per-network dir normalized to owner-only on Unix); PlatformWalletPersistence gained delete_wallet, with DetPersister delegating to the inner SqlitePersister so wallet deletion keeps the typed backend behavior; the secret store enforces an eight-byte passphrase floor for new HD wallets while keeping unmigrated short-password legacy wallets usable; upstream now persists shielded viewing keys natively, with DET's legacy metadata adapter retained for databases written by prior pins.
  • e75f259f4a1ba64c (this update): two further API-drift adaptations forced by the upstream jump — DataContractUpdate state transitions now wait on broadcast_and_wait_for_affected_state instead of the generic broadcast_and_wait, matching upstream's reclassification of contract updates as non-execution-binding (a correctness fix, not a weaker proof: both paths run identical GroveDB + quorum-BLS verification and reject consensus errors first, only the tag-gate differs); and contact receiving-account registration (register_contact_receiving_accounts) now goes through ManagedAccountOperations::add_managed_account_from_xpub instead of a lower-level insert, because the nested rust-dashcore pin bump changed account-generation semantics so that adding a managed account now correctly invalidates stale SPV compact-filter coverage. Only extended public keys cross this seam — no seed or private key material is touched.
  • Cargo.lock is regenerated scoped to the platform package set only at each bump — no crates.io registry version, checksum, or dependency-edge drift introduced.

Due-diligence review

Independent security (Smythe) + structural (Adams) + adversarial (Marvin) review of each adaptation, before merge:

  • SEC-007 (HIGH, confirmed via reproduction): DetPersister::load() originally propagated any single wallet's corrupt/undecodable FVK row as a hard error, failing cold-boot rehydration for every wallet in the store. Fixed to degrade a decode/schema/oversized-value failure to "absent" for the affected wallet only (logged), matching this module's existing kv_get_logged convention. Covered by a two-wallet, two-boot regression test. Update (2026-07-23): this fix and its test lived only in DetPersister, which a later commit in this same PR deleted once upstream gained native FVK support — see "Grumpy-review follow-up fixes" below; the underlying protection is real (now provided by upstream) but DET's own regression coverage for it had lapsed until this update restored it.
  • RUST-001 (MEDIUM): the mixed-changeset reject guard inspected the nested ShieldedChangeSet by field access instead of exhaustive destructuring. Fixed to destructure all six fields.
  • SEC-008/SEC-010 (LOW, privacy-only): removing a wallet never reaped its persisted FVK row, orphaning an Orchard viewing key on disk after wallet deletion. Fixed with a best-effort delete on wallet removal, covered by a regression test. Update (2026-07-23): that delete was fire-and-forget with no user-visible failure signal — fixed, see below.
  • QA-001 (MEDIUM, this update): the initial regression test for the contact-account registration change only exercised the low-level upstream helper's account-generation/filter-invalidation behavior, never the actual monitor_revision gating in register_contact_receiving_accounts — the mechanism this change exists to protect (silent-missed-funds prevention via bloom-filter rebuild). Fixed: the gating logic was extracted into its own production seam (register_contact_accounts_in_managed_wallet), now covered by a regression proving first registration bumps the revision, a duplicate registration is a no-op, and a mixed existing/new batch bumps exactly once.
  • RUST-001 (LOW, this update): the new add_contact_receiving_account helper lacked a doc comment, unlike its sibling free functions in the same file. Fixed — documents the DashpayReceivingFunds-only precondition and the boolean-return meaning.
  • Two LOW code-quality questions from this update's review were resolved with concrete engineering answers rather than blind edits: the defensive non-DashpayReceivingFunds branch is kept (one-line comment: validated at this seam so future callers can't register the wrong account type), and DET's own duplicate-key pre-check is kept (upstream returns InvalidParameter on a duplicate insert rather than an idempotent no-op, so DET's check is load-bearing, not redundant).
  • Residual accepted/deferred (LOW, non-blocking): FVKs are stored unencrypted at rest, consistent with upstream's own "viewing-grade, not secret" classification; this update's update_data_contract.rs broadcast-API switch has no dedicated automated test (verified correct by direct comparison against upstream's verifier source, but flagged rather than covered in this round); a live-testnet smoke test of upstream's tightened freshness-anchor enforcement remains a recommended follow-up (not covered by the committed lib-test ledger, and backend-E2E is excluded from CI).

Grumpy-review follow-up fixes (2026-07-23)

A full 3-agent grumpy-review pass (security/project-consistency/QA) plus coordinator validation against live source found the description above overstated what this PR verifies and ships cleanly. 6 blocking + 3 non-blocking findings were fixed in a follow-up commit:

  • Corrected this description's false "no user-observable behavior change" claim — see "User-observable changes" above — and updated docs/user-stories.md (WAL-001/002) to document the new password floor.
  • Restored DET-side regression coverage for cold-boot resilience to a corrupt shielded-FVK row, exercising the real seedless cold-boot path (the original DET-owned fix/test were deleted when DetPersister was retired; the underlying protection now lives upstream, but DET had zero test pinning it down until this fix).
  • Fixed a real password-floor bug: the new 8-character floor counted Unicode characters instead of UTF-8 bytes, wrongfully rejecting valid non-ASCII passwords upstream would accept (e.g. "öäüß" — 4 chars, 8 bytes). Now measured in bytes, matching upstream exactly, with a regression test.
  • Made wallet-deletion's shielded-FVK cascade failure visible: it was previously fire-and-forget (a background-task failure only logged, never surfaced), silently leaving an unencrypted viewing key on disk after the user was told the wallet was removed. Now raises a persistent warning banner with technical details on failure, covered by a fault-injection regression test.
  • Documented the per-operation shielded-spend passphrase prompt's interaction with headless/MCP automation in docs/MCP.md/docs/CLI.md — standalone/headless transports cannot prompt and will surface SecretPromptUnavailable; use an unprotected wallet for that automation for now.
  • Reworded the new password-floor error messages (WalletPasswordTooShort) to match the jargon-free, actionable sibling convention already used elsewhere in this codebase.
  • Narrowed the lazy Tier-2 re-wrap's error handling to only swallow the documented too-short-password case, propagating any other storage error (I/O faults, corruption) instead of silently absorbing it.
  • Fixed a misleading error mapping (hd_seed_for_shielded_spend's structural-misconfiguration guard returned WalletLocked, telling the user to "unlock" a wallet that wasn't locked) — now returns WalletStateInconsistent.
  • Hardened ensure_data_dir_exists to create new data directories atomically at owner-only permissions (no create-then-chmod TOCTOU window) and to reject following a planted symlink on the existing-directory repair path.

PR-comment triage follow-up (2026-07-23, round 2)

A pass over CodeRabbit/thepastaclaw's inline review threads on this PR found three more issues, all fixed:

  • BLOCKING privacy leak: wallet deletion used the backup-taking delete_wallet entry point, so despite the SEC-008/SEC-010 fix above, an automatic pre-delete backup snapshot still preserved a copy of the removed wallet's unencrypted shielded viewing key on disk. Switched to delete_wallet_skip_backup (the pinned no-backup entry point for exactly this case); regression test extended to assert no auto-backup directory is created.
  • Missing repaint: the async wallet-removal failure warning banner only wrote state into the egui context — without ctx.request_repaint(), an idle UI wouldn't paint it until an unrelated repaint happened to occur. Fixed; new unit test asserts a repaint is requested.
  • bump_monitor_revision over-firing: register_contact_accounts_in_managed_wallet was bumping the monitor revision of every DashpayReceivingFunds account whenever any single one was new, contradicting its own doc comment and causing needless bloom-filter/rescan churn on already-covered contacts every time one new contact was added. Fixed to bump only the specific newly-inserted account; the existing regression test's assertions were corrected accordingly (it had been asserting the old, over-firing behavior) and strengthened to check per-account revisions individually.
  • Also replied to (but did not apply) a hardcoded-test-password nitpick: the three flagged sites all predate this PR or belong to an unrelated PR merged into this branch, and use test-only placeholder strings, not real secrets.
  • CHANGELOG.md wording tightened: password floor now says "UTF-8 bytes" throughout (was inconsistently "characters" in one place), the per-op shielded passphrase prompt got its own user-facing bullet, and the Tier-2 lockout compatibility note was un-buried from a 40-line paragraph into its own line.

Deliberately left out of both follow-up rounds (documented, not silently dropped): the unmerged-upstream-pin risk (inherent to this PR chain, resolves once #3968 merges), unencrypted-at-rest FVKs (consistent with upstream's own "viewing-grade, not secret" classification), the Tier-2 lockout's error-message clarity, a dedicated test for the broadcast_and_wait_for_affected_state switch, an upfront password-floor UI hint, one flaky test (concurrent_spv_start_failure_is_returned_to_every_caller, unrelated to this PR's content, needs separate concurrency diagnosis), and the pre-existing hardcoded test passwords noted above.

Testing

  • cargo fmt --all, targeted wallet_backend (330 passed) and backend_task (411 passed) lib scopes, and cargo clippy --lib --all-features -- -D warnings all clean against the 4a1ba64c pin.
  • The new/updated regression (contact_registration_bumps_monitor_revision_only_for_new_keys) confirmed executing (not a zero-match false pass) with an explicit ... ok in the verification log.
  • Cargo.lock diff for this update confirmed scoped to the platform package set only: 28 platform git packages moved to 4a1ba64c, 11 nested rust-dashcore packages moved 0091c4a18c68d4; no crates.io registry drift.
  • Grumpy-review follow-up commit: cargo fmt --all -- --check, scoped cargo clippy --lib --all-features -- -D warnings, and targeted test scopes (context::wallet_lifecycle 65 passed, model::wallet 99 passed, wallet_backend 333 passed, app_dir 4 passed, backend_task::error 112 passed) all clean, independently re-verified by the coordinator with new/renamed test names confirmed present and passing in the logs (not just an aggregate count).
  • PR-comment triage follow-up commit: same fmt/clippy gates clean; wallet_backend (337 passed) and context::wallet_lifecycle (67 passed) re-run, with contact_registration_bumps_monitor_revision_only_for_new_keys and the two new tests (wallet_data_removal_warning_requests_repaint, the extended remove_wallet_reaps_persisted_shielded_viewing_keys) confirmed present and passing by name in the logs, independently re-verified by the coordinator.

Breaking changes

None for DET consumers — the four crates' public API is additive-only at this pin.

Prior work

  • Upstream: dashpay/platform#3968 — the PR this pin tracks; still open as of this update.
  • Upstream debt to watch: dashpay/platform#3992 — a pre-existing manifest/wallet_id authentication gap, unrelated to this bump.

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Wallet data, shielded viewing keys, key pools, and DashPay accounts now persist and restore across restarts.
    • Shielded spending derives signing authority only when needed; protected wallets prompt for their passphrase for each operation.
    • Wallet passwords must contain at least 8 trimmed UTF-8 bytes, while legacy shorter-password wallets remain supported.
  • Bug Fixes

    • Wallet deletion now removes persisted shielded data and displays a warning if cleanup fails.
    • Improved transaction confirmation, address synchronization, and recovery from transient startup issues.
  • Documentation

    • Clarified protected-wallet behavior and limitations for standalone CLI and MCP usage.

lklimek and others added 3 commits July 21, 2026 11:59
Bumps the four dashpay/platform git dependencies (dash-sdk,
rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage)
from d18020f to f376d32, the tip of platform PR #3968 — "feat(platform-
wallet-storage): embeddable SQLite persistence backend with seedless
rehydration".

Cargo.lock is regenerated scoped to the platform package set: only
dashpay-ecosystem git deps moved (platform, rust-dashcore be6e776→0091c4a,
grovedb v5.0.0→v5.0.1, orchard) — all transitively pinned by the new
platform rev; no crates.io registry drift.

API-drift fixes required by the upstream jump (each verified against the
new upstream source, not guessed):

- AssetLockFunding::FromExistingAssetLock gained a consume_invitation_voucher
  field. Set false at the three generic funding sites (identity register,
  identity top-up, platform-address funding) — none is the DashPay
  invitation-voucher reclaim flow, so a bearer-voucher lock is never consumed.

- shielded_transfer_to / shielded_unshield_to / shielded_withdraw_to each
  gained a seed: &[u8] parameter. This is the seedless-rehydration privilege-
  separation change: the Orchard spend keyset (ASK included) is re-derived
  from the seed for the single call and dropped on return, never left
  resident. DET's shielded_transfer/unshield/withdraw now resolve the HD seed
  just-in-time through the secret-seam chokepoint (with_secret_session),
  mirroring the existing shield_from_balance path; stale "no seed scope
  needed" doc comments corrected.

- PlatformWalletError gained PersisterStore and PersisterRestore variants.
  Added to both exhaustive (no wildcard) match sites — map_shielded_op_error
  and identity_op_error_kind — in the generic persistence bucket alongside
  PersisterLoad / Persistence.

Verification (via cargo-cached.sh ledger):
- cargo clippy --all-features --all-targets -- -D warnings — exit 0
- cargo test --lib --all-features — 2009 passed, 0 failed, 1 ignored
- cargo fmt --all -- --check — exit 0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-ups from the security due-diligence pass on the dashpay/platform
PR #3968 diff (companion to the pin-bump commit on this branch). No
behavior change — comment-only edits plus a supply-chain audit.

- Verified the new reserve-on-hand-out receive-address behavior does NOT
  reach DET. Upstream PlatformAddressWallet::next_unused_receive_address now
  reserves (via key-wallet next_unused_and_reserve), but DET calls neither it
  (DET's platform receive path is its own index-based
  generate_platform_receive_address_with_seed) nor a reserving Core variant.
  DET's Core receive path (next_receive_address → next_receive_address_for_-
  account → AddressPool::next_unused) stays non-reserving; its only change
  (!used → is_available()) is behavior-neutral because DET never reserves.
  Refreshed the TC-012 TODO's stale merge-status note: rust-dashcore#818 is
  now present in the pinned rev, while the Core reserving surface
  (CoreWallet::next_receive_address_and_reserve_for_account) still is not.

- Fixed two stale doc comments spotted in the diff:
  new_watch_only → new_external_signable (context/wallet_lifecycle/tests.rs)
  and AddressInfo.used → AddressInfo.is_used() (backend_task/dashpay/payments.rs).

- cargo audit over the regenerated Cargo.lock: advisory-ID set is identical to
  base v1.0-dev (14 advisories; zero introduced, zero removed) and the yanked-
  crate set is identical — no new advisory beyond the pre-existing, already-
  acknowledged bincode RUSTSEC-2025-0141. dashcore/key-wallet unify to a single
  rev (0091c4a) in the lock; no conflicting revisions.

Verification (via cargo-cached.sh ledger, final tree):
- cargo clippy --all-features --all-targets -- -D warnings — exit 0
- cargo test --lib --all-features — 2009 passed, 0 failed, 1 ignored
- cargo fmt --all -- --check — exit 0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…02/003)

Adversarial-review follow-ups on the shielded seed-resolution change from the
platform#3968 pin bump. No functional change to the happy path.

- QA-003: the doc comments on shielded_transfer/unshield/withdraw claimed a
  locked protected wallet yields WalletLocked "if its seed isn't cached". That
  is wrong: with_secret_session PROMPTS for a protected wallet; a dismissed or
  headless prompt surfaces as SecretPromptCancelled / SecretPromptUnavailable.
  WalletLocked comes ONLY from a non-HD-seed secret sitting at an HD scope (a
  misconfigured wallet). Rewrote all three docs to describe the real prompt
  path and reframe WalletLocked as that defensive type guard.

- QA-002: the new expose_hd_seed()->WalletLocked branch (a genuinely new
  failure mode — these fns took no seed before the bump) had zero unit coverage,
  reachable today only via network-gated #[ignore] e2e. Extracted the mapping
  into a small documented helper, hd_seed_for_shielded_spend, and added two unit
  tests pinning HdSeed->Ok and SingleKey->WalletLocked.

- QA-005: hoisted resolve_wallet + CachedOrchardProver::new() (neither needs the
  seed) out of the secret session in all three fns, matching the already-hoisted
  coordinator — trims the secret-session window's front edge and fails fast
  (ShieldedNotConfigured / WalletNotLoaded) without prompting.

- QA-004: CHANGELOG now names the transitive dashpay git-dep bumps carried by the
  platform pin — rust-dashcore be6e776->0091c4a, grovedb v5.0.0->v5.0.1, and the
  orchard fork dashified-0.14.0->0.14.1.

Verification (via cargo-cached.sh ledger, final tree):
- cargo clippy --all-features --all-targets -- -D warnings — exit 0
- cargo test --lib --all-features — 2011 passed, 0 failed, 1 ignored
  (+2 new: hd_seed_for_shielded_spend_{accepts_hd_seed,
  rejects_non_hd_secret_as_wallet_locked})
- cargo fmt --all -- --check — exit 0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The update repins the upstream wallet backend, shares its SQLite persister across wallet operations, derives shielded spend seeds just in time, prevents invitation-voucher consumption during generic asset-lock resumes, and adds storage, password, migration, broadcast, and lifecycle changes.

Changes

Wallet backend update

Layer / File(s) Summary
Upstream pins and funding resume contracts
Cargo.toml, src/backend_task/identity/..., src/backend_task/wallet/..., src/backend_task/dashpay/payments.rs, src/backend_task/update_data_contract.rs, CHANGELOG.md
Upstream wallet dependencies and documented behavior are updated, generic asset-lock resume paths disable invitation-voucher consumption, and data-contract updates use the affected-state broadcast confirmation flow.
Wallet persistence wiring and lifecycle cleanup
src/wallet_backend/mod.rs, src/context/wallet_lifecycle/...
WalletBackend reuses one SqlitePersister for wallet management, reads, KV access, warm-start loading, and durable deletion; contact-account registration, error mapping, deletion warnings, and persisted viewing-key cleanup coverage are updated.
Shielded spend seed flow
src/wallet_backend/shielded.rs, docs/CLI.md, docs/MCP.md
Shielded transfer, unshield, and withdraw operations derive HD seeds through SecretAccess for each call, with persisted viewing-key binding, helper tests, and protected-wallet transport documentation.
Storage and password hardening
src/app_dir.rs, src/context/mod.rs, src/model/wallet/mod.rs, src/backend_task/error.rs, src/wallet_backend/secret_access.rs, src/wallet_backend/single_key.rs, src/boot.rs, docs/user-stories.md
Data directories receive owner-only Unix permissions, wallet creation enforces the upstream passphrase minimum, filesystem errors are mapped, and legacy migration defers selected failed Tier-2 writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SecretAccess
  participant WalletBackend
  participant ShieldedOperations
  SecretAccess->>WalletBackend: provide HD-scoped SecretPlaintext
  WalletBackend->>WalletBackend: derive HD seed for one spend
  WalletBackend->>ShieldedOperations: execute transfer, unshield, or withdraw with seed
  ShieldedOperations-->>WalletBackend: return operation result
Loading

Suggested reviewers: lklimek, claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the upstream platform pin bump and the main behavioral additions around seedless rehydration and shielded viewing-key persistence.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/bump-platform-pin-pr3968

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

lklimek and others added 5 commits July 22, 2026 09:14
Re-bumps the four dashpay/platform git dependencies (dash-sdk,
rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage)
from f376d32 to ebbd15c4, the current tip of platform PR #3968 —
"feat(platform-wallet-storage): embeddable SQLite persistence backend with
seedless rehydration". Verified via git ls-remote against
feat/platform-wallet-storage-rehydration that the PR has not moved past
this rev.

Cargo.lock is regenerated scoped to the platform package set: 27 platform
git packages moved to ebbd15c (document-history-contract added
transitively); rust-dashcore, grovedb, orchard, and grovestark pins are
unchanged; no crates.io registry drift.

API-drift fixes required by the upstream jump (each verified against the
new upstream source, not guessed):

- PlatformWalletError gained PlatformNodePool, CoreInsufficientFunds,
  AssetLockNotTracked, AssetLockAlreadyConsumed, and
  AssetLockFundingMismatch variants. Classified in both exhaustive (no
  wildcard) match sites -- map_shielded_op_error and
  identity_op_error_kind -- alongside the existing precondition/wallet-state
  bucket.

- The upstream changeset schema gained a shielded.viewing_keys field with a
  new PersistenceCapabilities::SHIELDED_VIEWING_KEYS contract, needed for
  seedless shielded restart (Orchard FVKs must survive a restart without
  the HD seed). Added src/wallet_backend/persister.rs: a thin DetPersister
  adapter wrapping the upstream SqlitePersister -- delegates everything
  except the new viewing-key changeset, which it persists itself via the
  existing DetKv abstraction (already used for app data alongside wallet
  state), scoped per wallet_id and guarded by a mutex. Rejects any mixed
  changeset (viewing keys plus anything else) to preserve upstream's
  ATOMIC_CHANGESETS invariant rather than splitting one commit across the
  typed SQLite tables and the metadata table. Note this supersedes the
  prior "DET does not write its own persister" doc comment: DET now owns
  exactly the one host-specific extension upstream doesn't yet provide,
  not a reimplementation of the persister itself.

- The later review tip also retries transient startup rehydration, selects
  platform-address transfer/withdrawal inputs from hydrated candidates with
  authoritative on-chain balances, freezes the SPV sync watermark when
  persistence fails, and persists address-reservation timestamps plus
  DashPay address used-state updates -- no DET call-site changes required
  for these; CHANGELOG updated to describe the user-visible effect.

Verification ledger logs under
/data/artifacts/dash-evo-tool/2026-07-22/platform-pin-bump/final-ledger/logs/,
all exit 0: fmt --check, build --all-features, clippy --all-features
--all-targets -D warnings, plus targeted lib tests -- wallet_backend
(327 passed), context::wallet_lifecycle (62 passed), shielded (71 passed).

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>
…007, RUST-001)

Independent security (Smythe) and structural (Adams) due-diligence review
of f7ca95f (the platform#3968 pin re-bump) surfaced two MEDIUM findings
that escalated to a confirmed HIGH once Marvin produced a red repro, plus
two cheap LOW nits. All fixed here, on top of f7ca95f, before push.

- SEC-007 (HIGH, confirmed via red repro): DetPersister::load() previously
  `?`-propagated ANY single wallet's corrupt/undecodable shielded-FVK row,
  failing cold-boot rehydration for every wallet in the store, not just the
  corrupted one. load_viewing_keys now distinguishes decode/schema-version/
  truncated/oversized-value failures (degrade to an empty FVK set for the
  affected wallet, with a warning log carrying the wallet id and error
  shape, no FVK bytes) from genuine backend/IO failures (still fatal, e.g.
  LockPoisoned), matching this module's established kv_get_logged
  degrade-to-absent convention (kv.rs) and the sibling
  cold_boot_keeps_wallet_visible_when_persisted_transaction_txid_is_corrupt
  precedent.

- RUST-001 (MEDIUM): is_viewing_key_only now exhaustively destructures the
  nested ShieldedChangeSet (all six upstream fields), not just field-access
  checks — a future 7th upstream field is now a compile error instead of
  being silently ignored by the mixed-changeset reject guard.

- PROJ-001 (LOW): SHIELDED_FVK_KEY renamed "shielded_fvks.v1" ->
  "shielded:fvks:v1" to match this module's colon-namespaced key
  convention, before the old spelling became on-disk schema.

- PROJ-002 (LOW): corrected the Inner.persister doc comment, which read as
  if `pwm` held this exact Arc directly; it now describes that `pwm`
  consumes the DetPersister wrapper around this same SqlitePersister.

- Added 6 tests: a table-driven is_viewing_key_only classification test;
  a store/load FVK round-trip; a test proving oversized/decode failures
  degrade to absent while genuine backend failures stay fatal; mixed-
  changeset and cross-wallet-id rejection tests (both asserting no partial
  write); and a real two-wallet, two-boot cold-start regression test
  (cold_boot_skips_corrupt_fvks_for_one_wallet_and_restores_healthy_wallet)
  that persists both wallets for real, corrupts one's on-disk FVK row
  under its actual upstream wallet id, and confirms cold boot succeeds
  with the healthy wallet's viewing key still installed. A prior
  throwaway proof-of-concept test from the review pass (green-from-start,
  non-probative) was reverted rather than kept.

An open follow-up from the same review — whether persister.rs scoping FVK
rows by the upstream WalletId instead of this module's usual
WalletSeedHash orphans the row when a wallet is later removed (both are
bare `[u8; 32]` aliases, so it type-checks either way) — is still being
verified empirically and will land separately if it proves to be a real
gap.

Verification (via cargo-cached.sh ledger):
- cargo fmt --all -- --check — exit 0
- cargo clippy --all-features --all-targets -- -D warnings — exit 0
- cargo test --lib --all-features wallet_backend — 332 passed, 0 failed
- cargo test --lib --all-features context::wallet_lifecycle — 63 passed, 0 failed
- cargo test --lib --all-features shielded — 71 passed, 0 failed

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>
…8/SEC-010)

Fast-follow from the same platform#3968 pin-bump due-diligence review that
produced eb7e0fa. Confirmed empirically (Smythe, twice): removing a
wallet via forget_wallet_local_state + remove_upstream_wallet never
deletes the persisted shielded:fvks:v1 row persister.rs (eb7e0fa) added.
Root cause: upstream's PlatformWalletPersistence trait has no
delete_wallet method at this rev (a separately-tracked, pre-existing
upstream gap - out of scope here), and DET's own removal path never
touched this new KV row either. Net effect: an Orchard viewing key
outlives the wallet it belonged to, orphaned on disk indefinitely.
Privacy-only (no spend secrets involved - those are already correctly
wiped by the seed-vault deletion this function already does) and
fund-safety-neutral, so this is a fast-follow rather than a blocker
alongside SEC-007/RUST-001, per reviewer sign-off.

- persister.rs: added forget_wallet_viewing_keys(kv, wallet_id), keyed by
  the upstream WalletId (matching what store_viewing_keys/load_viewing_keys
  actually key by - not this module's usual WalletSeedHash), reusing the
  existing SHIELDED_FVK_KEY constant rather than re-typing the key string.
- mod.rs: forget_wallet_local_state now calls this best-effort in the
  `if let Some(wallet_id)` block (wallet_id is only Some when the wallet
  was actually upstream-registered, so there's never a row to reap
  otherwise), logging a warning on failure rather than aborting the rest
  of the wipe - consistent with this function's existing resilience
  contract for lesser (non-secret) cleanup steps.
- tests.rs: added remove_wallet_reaps_persisted_shielded_viewing_keys,
  which registers a wallet, binds shielded (persists an FVK row), verifies
  the row exists, runs the real removal path, then re-opens the SQLite
  file directly and asserts the row is gone.

Verification (via cargo-cached.sh ledger):
- cargo fmt --all -- --check - exit 0
- cargo clippy --all-features --all-targets -- -D warnings - exit 0
- cargo test --lib --all-features wallet_backend - 332 passed, 0 failed
  (includes remove_wallet_reaps_persisted_shielded_viewing_keys)

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>
@lklimek
lklimek marked this pull request as ready for review July 22, 2026 13:08
@lklimek
lklimek enabled auto-merge (squash) July 22, 2026 13:08
@thepastaclaw

thepastaclaw commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — 25 ahead in queue (commit d99073a)
Queue position: 26/30 · 2 reviews active
ETA: start ~23:07 UTC · complete ~23:28 UTC (median 21m across 30 recent reviews; 2 slots)
Queued 2h 25m ago · Last checked: 2026-07-23 18:50 UTC

lklimek and others added 3 commits July 22, 2026 15:53
Re-bumps the four dashpay/platform git dependencies (dash-sdk, rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage) from ebbd15c4 to e75f259, the current tip of platform PR #3968. Verified via gh pr view headRefOid immediately before commit.

Cargo.lock is regenerated scoped to the platform package set: 28 platform git packages moved to e75f259. rust-dashcore, grovedb, orchard, and grovestark pins are unchanged; no crates.io registry versions, checksums, or dependency edges moved.

API-drift fixes required by the upstream jump (each verified against the new upstream source, not guessed):

- The hardened file and SQLite backends now validate permissions on every parent directory. Normalize the app-data root, SPV directory, and per-network directory to owner-only on Unix before opening storage; copied cold-boot fixtures preserve that invariant.

- PlatformWalletPersistence gained delete_wallet with a default UnsupportedOperation result. DetPersister now delegates the method to its inner SqlitePersister so wallet deletion keeps the typed backend behavior and cascade cleanup.

- The secret store now enforces an eight-byte passphrase floor. Reject incompatible passwords for new HD wallets before creating an envelope, while keeping unmigrated short-password legacy wallets usable by deferring rejected Tier-2 rewraps and retaining the legacy envelope.

- Upstream now persists shielded viewing keys natively. The legacy DET metadata adapter remains in place for databases written by prior pins; platform-wallet and platform-wallet-storage retain matching shielded feature flags.

Known upstream compatibility limitation: July 2026 weekly builds could already write a short-password Tier-2 seed under the previous one-byte floor and delete its legacy envelope. e75f259 rejects that password before decryption, and no public downstream compatibility reader exists. CHANGELOG documents that affected profiles must not upgrade until upstream provides read-old/write-new support; this commit must not be pushed or merged as-is for those profiles.

PlatformWalletError did not gain variants in this jump, so the existing exhaustive match classifications remain complete.

Verification:

- cargo fmt --all -- --check — exit 0
- cargo clippy --all-features --all-targets -- -D warnings — exit 0
- cargo test --lib --all-features — 2054 passed, 0 failed, 1 ignored
- cargo test --all-features wallet_backend — 335 passed, 0 failed
- cargo test --all-features context::wallet_lifecycle — 64 passed, 0 failed
- cargo test --all-features shielded — 72 passed, 0 failed, plus 3 ignored backend-e2e cases

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>
Use upstream SqlitePersister for native viewing-key persistence.

Drive delete_wallet after runtime detachment to trigger its verified cascade. Retain the removal integration test and drop redundant workaround coverage. Document why unreleased interim metadata rows are not migrated.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@lklimek lklimek added the claudius-review Triggers automated code review using claudius plugin, runs as a CI job label Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/backend_task/error.rs (1)

1923-1927: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same actionable-text gap as the model-layer message; doc comment also overstates the migration trigger.

This message inherits the same "no call-to-action" gap as WalletCreationError::PasswordTooShort (see consolidated comment). Separately, the doc comment says the password "could not be migrated to Tier-2 protection," but the only current caller of this conversion is Wallet::new_from_seed at wallet-creation time — the lazy-migration path (decrypt_jit) instead logs and defers on a short legacy password rather than producing this error. Consider tightening the doc comment to name the actual trigger so future readers don't assume this fires during migration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend_task/error.rs` around lines 1923 - 1927, Update
WalletPasswordTooShort’s error text to include a clear corrective action,
matching the actionable guidance used by WalletCreationError::PasswordTooShort.
Revise its doc comment to describe wallet creation via Wallet::new_from_seed as
the current trigger, rather than stating that migration to Tier-2 protection
failed.

Source: Path instructions

src/app_dir.rs (1)

80-92: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Close the create-then-chmod window; also secure any newly-created parent directories.

create_dir_all followed by a separate set_permissions leaves a brief window where a freshly-created directory sits at the OS/umask default mode before being tightened, and only the final leaf directory is chmod'd — any parent directories create_dir_all had to create along the way stay at the default mode. DirBuilder::recursive(true).mode(0o700) creates the whole missing chain with the target mode applied to every newly-created directory in one call, per the standard library docs: "Parents that do not exist are created with the same security and permissions settings."

🔒 Proposed fix using DirBuilder
 pub fn ensure_data_dir_exists(data_dir: &Path) -> Result<(), std::io::Error> {
-    fs::create_dir_all(data_dir)?;
-    let metadata = fs::metadata(data_dir)?;
-    if !metadata.is_dir() {
-        return Err(std::io::Error::other("Created path is not a directory"));
-    }
     #[cfg(unix)]
     {
         use std::os::unix::fs::PermissionsExt;
-        fs::set_permissions(data_dir, fs::Permissions::from_mode(0o700))?;
+        use std::os::unix::fs::DirBuilderExt;
+        fs::DirBuilder::new()
+            .recursive(true)
+            .mode(0o700)
+            .create(data_dir)
+            .or_else(|e| if data_dir.is_dir() { Ok(()) } else { Err(e) })?;
+        // Re-tighten in case the directory already existed with looser permissions
+        // (DirBuilder's mode only applies to directories it actually creates).
+        fs::set_permissions(data_dir, fs::Permissions::from_mode(0o700))?;
+    }
+    #[cfg(not(unix))]
+    {
+        fs::create_dir_all(data_dir)?;
+    }
+    let metadata = fs::metadata(data_dir)?;
+    if !metadata.is_dir() {
+        return Err(std::io::Error::other("Created path is not a directory"));
     }
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app_dir.rs` around lines 80 - 92, Update ensure_data_dir_exists to use a
recursive fs::DirBuilder configured with mode 0o700 on Unix, so every
newly-created directory in the missing parent chain is created with the target
permissions without a create-then-chmod window. Preserve the existing metadata
validation and non-Unix behavior, and remove the separate final-directory
set_permissions step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend_task/error.rs`:
- Around line 1923-1927: Update the #[error] message for
TaskError::WalletPasswordTooShort in src/backend_task/error.rs:1923-1927 and
WalletCreationError::PasswordTooShort in src/model/wallet/mod.rs:54-56 to append
an actionable instruction such as asking the user to choose a longer password
and try again, keeping both messages identical.

In `@src/wallet_backend/secret_access.rs`:
- Line 2653: Remove the hardcoded SHORT_LEGACY_PASSWORD literal in the
secret-access test and generate a runtime passphrase that is shorter than the
storage minimum. In src/boot.rs at lines 297 and 332, replace the hardcoded test
passphrases with runtime-generated values and reuse each generated passphrase
for its corresponding passphrase-vault creation; update all affected test setup
while preserving the existing short-passphrase behavior.

---

Nitpick comments:
In `@src/app_dir.rs`:
- Around line 80-92: Update ensure_data_dir_exists to use a recursive
fs::DirBuilder configured with mode 0o700 on Unix, so every newly-created
directory in the missing parent chain is created with the target permissions
without a create-then-chmod window. Preserve the existing metadata validation
and non-Unix behavior, and remove the separate final-directory set_permissions
step.

In `@src/backend_task/error.rs`:
- Around line 1923-1927: Update WalletPasswordTooShort’s error text to include a
clear corrective action, matching the actionable guidance used by
WalletCreationError::PasswordTooShort. Revise its doc comment to describe wallet
creation via Wallet::new_from_seed as the current trigger, rather than stating
that migration to Tier-2 protection failed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2da42431-3594-4220-9702-10960a4c3986

📥 Commits

Reviewing files that changed from the base of the PR and between e1ba7b3 and b43fc5e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • CHANGELOG.md
  • Cargo.toml
  • src/app_dir.rs
  • src/backend_task/error.rs
  • src/boot.rs
  • src/context/mod.rs
  • src/context/wallet_lifecycle/tests.rs
  • src/model/wallet/mod.rs
  • src/wallet_backend/mod.rs
  • src/wallet_backend/secret_access.rs
  • src/wallet_backend/single_key.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml

Comment thread src/backend_task/error.rs
/// legacy envelope remains the source of truth.
#[tokio::test]
async fn short_legacy_seed_password_remains_usable_without_tier2_migration() {
const SHORT_LEGACY_PASSWORD: &str = "short";

@coderabbitai coderabbitai Bot Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove hardcoded test passwords from the changed Rust code. Generate test passphrases at runtime instead of committing literals.

  • src/wallet_backend/secret_access.rs#L2653-L2653: generate a runtime passphrase shorter than the storage floor.
  • src/boot.rs#L297-L297: generate and reuse a runtime passphrase for passphrase-vault creation.
  • src/boot.rs#L332-L332: generate and reuse a runtime passphrase for passphrase-vault creation.

As per coding guidelines, src/**/*.rs must not contain plaintext recovery phrases, private keys, passwords, seeds, or API tokens.

📍 Affects 2 files
  • src/wallet_backend/secret_access.rs#L2653-L2653 (this comment)
  • src/boot.rs#L297-L297
  • src/boot.rs#L332-L332
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet_backend/secret_access.rs` at line 2653, Remove the hardcoded
SHORT_LEGACY_PASSWORD literal in the secret-access test and generate a runtime
passphrase that is shorter than the storage minimum. In src/boot.rs at lines 297
and 332, replace the hardcoded test passphrases with runtime-generated values
and reuse each generated passphrase for its corresponding passphrase-vault
creation; update all affected test setup while preserving the existing
short-passphrase behavior.

Source: Coding guidelines

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've weighed this one and respectfully decline the alarm. The three literals in question — SHORT_LEGACY_PASSWORD = "short" (secret_access.rs:2666) and SecretString::new("legacy-pass") (boot.rs:297, :332) — are throwaway test fixtures that unlock nothing but an in-test tempdir vault created moments earlier in the same function. They are not recovery phrases, not private keys, and grant access to no real wallet or fund. The "never commit secrets" guideline exists to keep material that compromises real assets out of history; a string literally named "short" chosen to be below the storage floor carries none of that risk. Generating them at runtime would add ceremony without removing any exposure. Deliberately keeping them as-is — not an oversight.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked all three sites — none are introduced by this PR and none are real secrets:

  • src/wallet_backend/secret_access.rs:2653 (SENTINEL_PASSPHRASE): pre-existing test constant, authored 2026-07-17, unrelated to this PR's diff.
  • src/boot.rs:297 / src/boot.rs:332 ("legacy-pass"): arrived via the v1.0-dev merge into this branch (commit 69b7546eda, 2026-07-22), authored on unrelated PR fix: centralize validation and fee-reserve hygiene, clean up error banners #927 — not part of this PR's own changes.

All three are test-only placeholder strings used to exercise password-length/legacy-migration logic in unit tests, not plaintext recovery phrases, private keys, or API tokens — the coding guideline's actual target. Given they're out of scope for this PR's diff, leaving them as-is rather than touching unrelated code here. Happy to file a follow-up if there's appetite for switching these to runtime-generated passphrases repo-wide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Claudius-Maginificent, thanks for the detailed provenance. I agree that the src/boot.rs literals and the pre-existing SENTINEL_PASSPHRASE should not have been included in this PR-scoped finding.

However, the added hunk does introduce SHORT_LEGACY_PASSWORD = "short" in src/wallet_backend/secret_access.rs. Even as a harmless test fixture, it is a plaintext password under the repository’s stated src/**/*.rs rule, so that narrow part of the finding still applies.

A minimal runtime-generated sub-floor value would preserve the test’s intent without expanding the PR’s scope. If the project intends to allow fixture passwords, the guideline should be clarified; otherwise, please update this one newly added literal.

lklimek and others added 3 commits July 23, 2026 10:27
…word Tier-2 seeds

The new pin's password-length floor rejects a sub-8-character passphrase
on read, not just write, mirroring the write-side check as a deliberate
defense against a backend-write attacker planting a weakly sealed
envelope. Mark the affected call site so it gets revisited once upstream
adds a scoped migration-read capability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-bumps the four dashpay/platform git dependencies (dash-sdk,
rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage)
from e75f259 to 4a1ba64, the current tip of platform PR #3968.

Cargo.lock is regenerated scoped to the platform package set: 28 platform
git packages moved to 4a1ba64 and 11 rust-dashcore packages moved from
0091c4a to 18c68d4. No crates.io registry versions, checksums, dependency
edges, or unrelated sources moved.

API-drift fixes required by the upstream jump:

- DataContractUpdate waits for affected state instead of generic broadcast
  completion, while retaining the typed TaskError conversion and existing
  DriveProofError recovery path.

- Contact receiving accounts use ManagedAccountOperations so newly added
  accounts invalidate stale compact-filter coverage. Existing contact keys
  remain a true no-op, and only real insertions bump monitor_revision.

- The changelog records authoritative pure-SPV broadcast outcomes and the
  contact-account filter-coverage fix without claiming shielded identity
  creation support in DET.

Verification ledger evidence (all exit 0):

- cargo fmt --all — key 9ff5de5fa46203d0ebbf5580836bddf9 — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T111934-9ff5de5fa46203d0ebbf5580836bddf9-2.log
- cargo test contact_account_registration_invalidates_filter_coverage_once --all-features — key 3cdb20d7ef39ab598af859ff3fcf6020 — 1 passed — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T111944-3cdb20d7ef39ab598af859ff3fcf6020-2.log
- cargo test --lib --all-features wallet_backend — key b062125daa4c22c77c2199c8f74d74a2 — 330 passed — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T112028-b062125daa4c22c77c2199c8f74d74a2-2.log
- cargo test --lib --all-features backend_task — key 2fa67207f0f111422cde6544d6a29bac — 411 passed — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T112049-2fa67207f0f111422cde6544d6a29bac-2.log
- cargo clippy --lib --all-features -- -D warnings — key 4a6af394e0e2293a6da5a0e5f824af5c — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T112100-4a6af394e0e2293a6da5a0e5f824af5c-2.log
- git diff --check — exit 0

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>
Extract the post-derivation contact-account registration loop into the
smallest production seam that can be exercised without wallet persistence,
SPV, or network infrastructure.

The regression now proves that a first contact registration increments the
aggregate monitor revision, a duplicate registration leaves it unchanged,
and a mixed existing/new batch increments it exactly once. It retains the
account-generation and filter-coverage invalidation assertions from the
original lower-level test.

Document the DashpayReceivingFunds-only helper contract and its boolean
result. Keep the defensive variant check for future callers, and retain the
contact-key pre-check because upstream reports duplicate managed account
types as InvalidParameter instead of providing idempotent no-op semantics.

Verification ledger evidence (all exit 0):

- cargo fmt --all — key 7e9402392e7b60075b2c66376240c2b1 — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T115050-7e9402392e7b60075b2c66376240c2b1-2.log
- cargo test contact_registration_bumps_monitor_revision_only_for_new_keys --all-features — key 979201f10ee2e2db3aba25ed91ba11d6 — 1 passed — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T115104-979201f10ee2e2db3aba25ed91ba11d6-2.log
- cargo test --lib --all-features wallet_backend — key c35703b8723b6de9ce7b907df35e85e9 — 330 passed — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T115206-c35703b8723b6de9ce7b907df35e85e9-2.log
- cargo clippy --lib --all-features -- -D warnings — key 7cf2092cee6223ee239e8c37f9e4a35b — /data/git-worktrees/home-ubuntu-git-dash-evo-tool-platform-pin-bump/.claudius-ledger/logs/20260723T115214-7cf2092cee6223ee239e8c37f9e4a35b-2.log
- git diff --check — exit 0

Co-Authored-By: Codex gpt-5.6-sol <noreply@openai.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/wallet_backend/mod.rs (2)

2135-2144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring claims narrower behavior than the code delivers.

The comment states the bump is "only for genuinely newly inserted accounts," but see the companion comment on register_contact_accounts_in_managed_wallet (Lines 2641-2675) — the implementation currently bumps every DashpayReceivingFunds account whenever any single one is new. Update the doc once the helper is fixed, or vice versa.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet_backend/mod.rs` around lines 2135 - 2144, The documentation around
the monitored-account rebuild incorrectly claims only newly inserted accounts
trigger bump_monitor_revision, while register_contact_accounts_in_managed_wallet
currently bumps for every DashpayReceivingFunds account when any one is new.
Align the implementation and documentation by changing that helper to bump only
genuinely newly inserted accounts, preserving the existing behavior for already
registered accounts.

2151-2209: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

bump_monitor_revision fires for every DashpayReceivingFunds account, not just the newly-inserted one.

register_contact_accounts_in_managed_wallet bumps the revision of all DashpayReceivingFunds accounts whenever newly_inserted > 0, contradicting the doc comment on register_contact_receiving_accounts ("Only newly-added accounts trigger a bump_monitor_revision") and the test itself: after the second call, first_contact's revision goes from 1→2 even though only second_contact is new (test asserts revisions == [1, 2], not [1, 1]). This causes needless revision churn on already-covered accounts every time a single new contact is added.

♻️ Bump only the newly-inserted accounts
     let mut newly_inserted = 0;
+    let mut newly_inserted_keys = Vec::new();
     for (account_type, account_xpub) in accounts {
         match add_contact_receiving_account(info, account_type, account_xpub) {
-            Ok(true) => newly_inserted += 1,
+            Ok(true) => {
+                newly_inserted += 1;
+                newly_inserted_keys.push(account_type);
+            }
             Ok(false) => {}
             Err(error) => {
                 tracing::debug!(%error, "Skipping contact account: managed insert failed");
             }
         }
     }
-    if newly_inserted > 0 {
-        for account in info.accounts.all_funding_accounts_mut() {
-            if matches!(
-                account.managed_account_type(),
-                ManagedAccountType::DashpayReceivingFunds { .. }
-            ) {
-                account.bump_monitor_revision();
-            }
-        }
-    }
+    for account in info.accounts.all_funding_accounts_mut() {
+        if newly_inserted_keys
+            .iter()
+            .any(|k| *k == account.managed_account_type().into())
+        {
+            account.bump_monitor_revision();
+        }
+    }
     newly_inserted

(Sketch — adapt to whatever equality/lookup the AccountType/ManagedAccountType types actually support.)

Also applies to: 2641-2675, 3058-3133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet_backend/mod.rs` around lines 2151 - 2209, Update
register_contact_accounts_in_managed_wallet and its callers so
bump_monitor_revision is applied only to accounts newly inserted during the
current registration, not every DashpayReceivingFunds account. Track each
successful insertion or identify newly inserted account types before performing
the bumps, while preserving the existing return count and no-op behavior when
nothing is added. Apply the same correction to the related registration paths
noted in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/wallet_backend/mod.rs`:
- Around line 2135-2144: The documentation around the monitored-account rebuild
incorrectly claims only newly inserted accounts trigger bump_monitor_revision,
while register_contact_accounts_in_managed_wallet currently bumps for every
DashpayReceivingFunds account when any one is new. Align the implementation and
documentation by changing that helper to bump only genuinely newly inserted
accounts, preserving the existing behavior for already registered accounts.
- Around line 2151-2209: Update register_contact_accounts_in_managed_wallet and
its callers so bump_monitor_revision is applied only to accounts newly inserted
during the current registration, not every DashpayReceivingFunds account. Track
each successful insertion or identify newly inserted account types before
performing the bumps, while preserving the existing return count and no-op
behavior when nothing is added. Apply the same correction to the related
registration paths noted in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99fdec5a-0de7-4216-a016-a51a4a1f252e

📥 Commits

Reviewing files that changed from the base of the PR and between b43fc5e and e7b995e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • CHANGELOG.md
  • Cargo.toml
  • src/backend_task/update_data_contract.rs
  • src/wallet_backend/mod.rs
  • src/wallet_backend/secret_access.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • Cargo.toml
  • src/wallet_backend/secret_access.rs
  • CHANGELOG.md

- cover corrupt FVK cold boot and surface asynchronous removal failures (PROJ-001, CALL-001)
- align password validation, documentation, and actionable errors (PROJ-003, PROJ-004, CODE-001)
- narrow lazy Tier-2 re-wrap handling and fix shielded invariant mapping (CODE-003, CODE-004)
- document protected headless shielded spends and harden data-directory creation (CALL-003, SEC-004/CALL-002)

Co-Authored-By: Codex GPT-5 <noreply@openai.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The dependency adapters generally match the pinned upstream APIs, including the corrected contract-update wait path and contact-account revision gating. One blocking privacy defect remains because user-requested wallet deletion creates a pre-deletion backup containing the unencrypted shielded viewing key; the local password guard also rejects some passwords that satisfy the upstream byte-length policy.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/wallet_backend/mod.rs`:
- [BLOCKING] src/wallet_backend/mod.rs:1389: Wallet deletion preserves the viewing key in an automatic backup
  `WalletBackend::new` opens this persister with `SqlitePersisterConfig::new`, whose default enables automatic backups, and the pinned `delete_wallet` implementation takes a full pre-deletion database backup under the network database directory's `backups/auto` subdirectory before cascading wallet-owned rows. That snapshot includes the newly native, unencrypted `shielded_viewing_keys` row, so the Remove Wallet flow deletes the key from the live database while retaining a copy on disk despite telling the user that this version's local wallet data will be cleared. Use the pinned no-backup deletion entry point for this explicit user-requested removal, and extend the regression test beyond querying the live database so it detects creation of a pre-delete backup.

In `src/model/wallet/mod.rs`:
- [SUGGESTION] src/model/wallet/mod.rs:439: Match the storage policy's byte-length password check
  `MIN_PASSPHRASE_LEN` is the persistent store's post-trim byte-length floor, and upstream measures it with `SecretString::trimmed().len()`. This guard instead counts Unicode scalar values, so a password such as four two-byte non-ASCII characters satisfies the eight-byte storage requirement but is rejected by DET before storage is reached. Since this guard was introduced specifically to mirror the upstream floor, use Rust string byte length and add a multibyte boundary case to the existing short-password test.

Comment thread src/wallet_backend/mod.rs Outdated
Comment thread src/model/wallet/mod.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/context/wallet_lifecycle/removal.rs`:
- Around line 65-80: Update both warning paths in the wallet removal flow to
clone and retain an egui context handle, then request a repaint immediately
after show_wallet_data_removal_warning updates the banner. Ensure the
synchronous spawn-failure path also receives a cloned context handle and
requests repaint after surfacing TaskError::TaskManagerShuttingDown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf797716-0fc3-49d0-b79a-bd33cf01b3f8

📥 Commits

Reviewing files that changed from the base of the PR and between e7b995e and 7566c77.

📒 Files selected for processing (10)
  • docs/CLI.md
  • docs/MCP.md
  • docs/user-stories.md
  • src/app_dir.rs
  • src/backend_task/error.rs
  • src/context/wallet_lifecycle/removal.rs
  • src/context/wallet_lifecycle/tests.rs
  • src/model/wallet/mod.rs
  • src/wallet_backend/secret_access.rs
  • src/wallet_backend/shielded.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/backend_task/error.rs
  • src/wallet_backend/secret_access.rs
  • src/model/wallet/mod.rs

Comment thread src/context/wallet_lifecycle/removal.rs

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claudius' grumpy-review — PR #919 (platform pin bump to #3968 tip)

A disciplined, genuinely well-tested dependency bump. I dispatched four specialists (security, structural, adversarial QA, docs) over the 22-file diff — static verification only, since bash/cargo were unavailable in this sandbox; CI remains the compile/test backstop.

Headline: thepastaclaw's still-open blocking thread — that wallet deletion retains the unencrypted shielded viewing key (FVK) in an automatic pre-delete backup — is CONFIRMED. Verified against the pinned upstream source (dashpay/platform @ 4a1ba64c): SqlitePersisterConfig::new defaults auto-backup ON, delete_wallet takes a full physical DB copy to backups/auto/pre-delete-*.db before cascading rows, and delete_wallet_skip_backup exists as the intended remedy. The two remove-wallet regression tests only inspect the live DB, so they are structurally blind to it. Details and the fix are on that thread. This is the one genuinely blocking item — it contradicts an explicit user-facing promise and defeats the PR's own SEC-008/SEC-010 reaping.

Findings: 2 MEDIUM, 9 LOW, 2 INFO.

  • 🔴 MEDIUM — Wallet deletion retains the unencrypted FVK in a pre-delete backup (SEC-001 + QA-001 test gap) → see the open delete_wallet_skip_backup thread.
  • 🟡 MEDIUM — CHANGELOG omits the new per-operation passphrase prompt for protected shielded wallets (this review's CHANGELOG comment).
  • LOW/INFO: password-floor "characters" vs UTF-8-bytes wording drift across three artifacts; duplicated #[error] sentence with no shared source; untested broadcast_and_wait_for_affected_state swap + missing rationale comment; whitespace-only password boundary untested; a dangling CHANGELOG cross-reference in a TODO; a mega-bullet burying the Tier-2 lockout; MCP/CLI caveat naming 3 of 5 shielded tools; a durable-delete failure surfaced in one caller but silently logged in clear_network_database.

What held up under scrutiny (credit where due): the byte-length password floor (with a genuine multibyte test), the narrowed Tier-2 re-wrap error handling, the WalletStateInconsistent mapping, the corrupt-FVK cold-boot resilience test, the monitor-revision gating test, atomic owner-only data-dir creation, and the secret-chokepoint hygiene are all real and non-vacuous. This PR did its homework — two fixable items stand between it and merge.

Full report (HTML + JSON) written to the review artifacts. Not approving while the confirmed privacy regression stands.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread CHANGELOG.md

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claudius' grumpy-review — PR #919 (platform pin bump to #3968 tip), fresh pass

I put four specialists across the 22-file diff — Smythe (security), Adams (structural), Marvin (adversarial QA), Trillian (docs). Static verification only: bash was wedged sandbox-wide this run (a bwrap / /home/.mcp.json HOME misconfig killed every shell for me and every agent), so no cargo/consolidation scripts ran. CI remains the compile/test backstop; the report was hand-assembled.

Headline — still not mergeable, and it's the same blocker. thepastaclaw's open thread — wallet deletion retains the unencrypted shielded viewing key (FVK) in an automatic pre-delete backup — is re-confirmed against the pinned upstream source (dashpay/platform @ 4a1ba64c): SqlitePersisterConfig::new defaults auto-backup ON, delete_wallet (src/wallet_backend/mod.rs:1389) takes a full physical DB copy to backups/auto/pre-delete-*.db before cascading rows, delete_wallet_skip_backup exists as the intended remedy, and the two remove-wallet tests only query the live DB. This pass adds two fresh amplifiers: the F60 "delete all local data" sweep multiplies these snapshots and never removes them, and the removal dialog literally points users at that path as the remediation. Details on that thread.

Findings: 5 MEDIUM, 9 LOW, 1 INFO. Three of the five MEDIUMs are new and posted inline:

  • 🔴 MEDIUM (blocking) — FVK retained in un-pruned pre-delete auto-backup after user-requested deletion → the open delete_wallet_skip_backup thread.
  • 🟡 MEDIUM (new)remove_wallet silently warn!s a failed secret-bearing local wipe while surfacing the non-secret upstream failure; the sibling clear_network_database does the inverse (removal.rs).
  • 🟡 MEDIUM (new) — the broadcast_and_waitbroadcast_and_wait_for_affected_state swap for DataContractUpdate is the only call site of that SDK method in the tree and has zero test coverage (update_data_contract.rs).
  • 🟡 MEDIUM (new) — the headless shielded caveat in docs/CLI.md / docs/MCP.md names 3 of ≥5 affected tools, omitting shield-from-core, shield-from-platform, and shielded_init.
  • 🟡 MEDIUM — CHANGELOG omits the user-facing per-shielded-spend passphrase prompt (already on an open thread).
  • LOW/INFO: char-vs-byte wording drift across three artifacts; unbounded backup growth; app-dir repair TOCTOU (documented residual); monitor-revision doc/code mismatch; duplicated error literal with no cross-test; missing password-floor boundary tests; untested Tier-2-lockout path with misleading copy; a 40-line CHANGELOG mega-bullet burying the "do not upgrade" blocker; a WalletPasswordTooShort doc comment that names a migration path it never fires from; and a missing repaint after the async removal banner (CodeRabbit's open thread).

Credit where due — this PR did its homework. The byte-length password floor (with a genuine multibyte "öäüß" test), atomic owner-only data-dir creation with symlink rejection, the corrupt-FVK cold-boot resilience test that drives the real path, clean typed TaskError variants, and secret-seam chokepoint hygiene all held up under adversarial scrutiny.

Full report (HTML + JSON) written to the review artifacts. Not approving while the confirmed FVK-backup privacy regression stands — the fix is a one-line switch to delete_wallet_skip_backup plus a backup-aware regression test.

🤖 Co-authored by Claudius the Magnificent AI Agent


match state_transition.broadcast_and_wait(sdk, None).await {
match state_transition
.broadcast_and_wait_for_affected_state(sdk, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — a consensus/broadcast-method swap with zero test coverage.

This is the sole call site of broadcast_and_wait_for_affected_state in the entire tree. Every other state-transition broadcast — identity/add_key_to_identity.rs, tokens/pause_tokens.rs, tokens/destroy_frozen_funds.rs, tokens/update_token_config.rs, tokens/resume_tokens.rs — still uses broadcast_and_wait::<StateTransitionProofResult>. The swap arrived with this platform-pin bump, on the one path DET uses to publish an updated data contract on-chain, and its return value feeds self.replace_contract(...) directly (line 82) to replace the app's cached copy.

No unit or integration test exercises update_data_contract at all, and the CHANGELOG doesn't mention the swap or its rationale. Statically I couldn't confirm the exact semantic difference from broadcast_and_wait without the pinned SDK source — specifically whether "affected state" can return a different or partial contract object than the one actually applied. The PR description asserts the two paths are equivalent by direct source comparison; that reasoning is plausible but is exactly the kind of claim that deserves an executable pin.

Recommendation: add at least one test — a mocked-SDK unit test, or an #[ignore]d backend-e2e/live test per tests/backend-e2e/README.md — asserting the local contract cache updates correctly after the broadcast round trip, matching the pattern the token/identity call sites already exercise. And document the method-swap rationale in the CHANGELOG entry.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread docs/CLI.md
Comment on lines +57 to +59
Shielded transfer, unshield, and withdraw operations resolve the wallet seed
just in time for each operation. A password-protected (Tier-2) wallet therefore
requires an interactive passphrase session.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — this caveat names 3 of at least 5 affected shielded operations.

The list here (transfer, unshield, withdraw) is incomplete. In src/wallet_backend/shielded.rs, shield_from_asset_lock (line 152, backing shielded_shield_from_core), shield_from_balance (line 198, backing shielded_shield_from_platform), and ensure_shielded_bound_jit (line 106, backing shielded_init) all route through the identical with_secret_session JIT-seed mechanism — so for a Tier-2 wallet they can equally surface SecretPromptUnavailable in headless mode. Yet the MCP tool table further down (docs/MCP.md:104-112) lists all of them as available.

The consequence is concrete: an automation developer building an unattended pipeline against a protected wallet, reading only this caveat, would reasonably conclude the shield/init tools are unaffected — then hit an undocumented SecretPromptUnavailable on the very first funding step. docs/MCP.md:59-60 carries the identical incomplete wording and needs the same fix.

Recommendation: reword the caveat in both docs to cover the full set — shielding (from Core or Platform), transferring, unshielding, withdrawing, and binding shielded keys (shielded_init) all require an interactive passphrase session for a Tier-2 wallet. The existing SecretPromptUnavailable / use-an-unprotected-wallet guidance below is otherwise accurate and actionable — keep it.

🤖 Co-authored by Claudius the Magnificent AI Agent

let egui_ctx = self.egui_ctx().clone();
let removal = async move {
if let Err(error) = backend.remove_upstream_wallet(&wallet_id).await {
show_wallet_data_removal_warning(&egui_ctx, error);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — this banner surfaces the non-secret failure while the secret one stays silent.

Adding show_wallet_data_removal_warning here (for the upstream watch-only row removal, which carries no secret) leaves remove_wallet with its visibility inverted against sensitivity. A few lines up, the secret-bearing local wipe — forget_wallet_local_state (encrypted seed-envelope vault, single-key sidecar, session cache, shielded rows) — only tracing::warn!s on failure, with no user banner. And the sibling clear_network_database (spv.rs:37-138) does the opposite of both: it surfaces local secret-wipe failures via the typed WalletDataClearIncomplete error but only logs the async upstream failure.

So the same failure class is surfaced in one path and swallowed in the other — and in remove_wallet the one that can leave encrypted seed material on disk is the quiet one. If forget_wallet_local_state fails, the wallet vanishes from the UI while its secrets may still be on disk, and the user is told nothing: a false sense that the material was destroyed. There's a test for this upstream-removal banner (tests.rs:1755) but none for a forget_wallet_local_state failure, because it produces no signal to assert on.

Recommendation: surface a forget_wallet_local_state failure the same way clear_network_database surfaces WalletDataClearIncomplete — a warning banner / typed error stating some wallet data may remain and to retry — and settle both removal paths on one convention: secret-bearing wipe failures are always user-surfaced.

🤖 Co-authored by Claudius the Magnificent AI Agent

…al warning, scope monitor-revision bump to new contacts

Follow-up to the PR #919 comment-triage pass:

- Wallet deletion now uses delete_wallet_skip_backup instead of
  delete_wallet, so removing a wallet no longer leaves an unencrypted
  copy of its shielded viewing key in an automatic pre-delete backup
  snapshot (BLOCKING privacy leak, thepastaclaw). Regression test
  extended to assert no auto-backup directory is created.
- show_wallet_data_removal_warning now calls ctx.request_repaint(),
  so the async wallet-removal failure banner actually appears on an
  idle UI instead of waiting for an unrelated repaint (CodeRabbit).
  New unit test covers this directly.
- register_contact_accounts_in_managed_wallet now bumps monitor_revision
  only on the specific newly-inserted DashpayReceivingFunds account,
  not every such account in the wallet, matching the function's own doc
  comment and avoiding needless bloom-filter/rescan churn on already
  -covered contacts (CodeRabbit nitpick). Regression test corrected and
  strengthened to assert per-account revisions individually.
- CHANGELOG.md: password floor reworded to UTF-8 bytes (not characters,
  matching the actual fix), added the per-operation shielded passphrase
  prompt as its own user-facing bullet, and un-buried the Tier-2
  lockout compatibility note into its own paragraph.

Implemented by Codex Sol (gpt-5.6-sol, high effort), independently
re-verified by the coordinator: fmt/clippy clean, wallet_backend (337
passed) and context::wallet_lifecycle (67 passed) test scopes re-run
with the new/changed test names confirmed present and passing in the
logs, not just an aggregate count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Claudius-Maginificent Claudius-Maginificent changed the title chore: bump dashpay/platform pin to PR #3968 tip (seedless wallet rehydration) chore: bump dashpay/platform pin to PR #3968 tip (seedless rehydration + shielded viewing-key persistence) Jul 23, 2026
@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator Author

Also fixed the bump_monitor_revision nitpick from CodeRabbit's review summary (not a resolvable inline thread, noting it here for the record): register_contact_accounts_in_managed_wallet was bumping every DashpayReceivingFunds account's revision whenever any single one was new. It now bumps only the specific newly-inserted account, matching the function's own doc comment. The existing contact_registration_bumps_monitor_revision_only_for_new_keys test's final assertion was quietly wrong under the old behavior ([1, 2], an artifact of over-bumping) and has been corrected to [1, 1] with per-account checks added. See commit d99073aca.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/context/wallet_lifecycle/tests.rs (1)

1699-1699: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove plaintext seed and password fixtures.

Generate these values at test runtime instead of embedding raw seeds or "cold-boot-fvk-password" in the repository.

  • src/context/wallet_lifecycle/tests.rs#L1699-L1699: generate the removal-test seed at runtime.
  • src/context/wallet_lifecycle/tests.rs#L1769-L1769: generate the failure-path seed at runtime.
  • src/context/wallet_lifecycle/tests.rs#L2586-L2586: generate the alias-validation seed at runtime.
  • src/context/wallet_lifecycle/tests.rs#L3784-L3788: generate both cold-boot seeds and the protected-wallet password at runtime.

As per coding guidelines, “Never commit plaintext recovery phrases, private keys, passwords, seeds, or API tokens in source, tests, fixtures, or documentation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/wallet_lifecycle/tests.rs` at line 1699, Remove embedded
plaintext seed and password fixtures by generating equivalent random values at
test runtime. Update the removal-test seed at
src/context/wallet_lifecycle/tests.rs lines 1699-1699, the failure-path seed at
lines 1769-1769, and the alias-validation seed at lines 2586-2586; update both
cold-boot seeds and the protected-wallet password at lines 3784-3788. Preserve
each test’s existing behavior while ensuring no raw seed or password remains in
the source.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/context/wallet_lifecycle/tests.rs`:
- Line 1699: Remove embedded plaintext seed and password fixtures by generating
equivalent random values at test runtime. Update the removal-test seed at
src/context/wallet_lifecycle/tests.rs lines 1699-1699, the failure-path seed at
lines 1769-1769, and the alias-validation seed at lines 2586-2586; update both
cold-boot seeds and the protected-wallet password at lines 3784-3788. Preserve
each test’s existing behavior while ensuring no raw seed or password remains in
the source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d2e286b-aad1-4937-b347-dfe0873c4e54

📥 Commits

Reviewing files that changed from the base of the PR and between 7566c77 and d99073a.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • docs/user-stories.md
  • src/backend_task/dashpay/payments.rs
  • src/backend_task/error.rs
  • src/context/mod.rs
  • src/context/wallet_lifecycle/removal.rs
  • src/context/wallet_lifecycle/tests.rs
  • src/model/wallet/mod.rs
  • src/wallet_backend/mod.rs
  • src/wallet_backend/single_key.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/backend_task/dashpay/payments.rs
  • src/context/mod.rs
  • src/wallet_backend/single_key.rs
  • src/backend_task/error.rs
  • src/context/wallet_lifecycle/removal.rs
  • docs/user-stories.md
  • src/model/wallet/mod.rs
  • src/wallet_backend/mod.rs

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy-review pass — PR #919 (platform pin bump to #3968 tip)

Ran the full crew (Smythe/security · Adams/consistency · Marvin/QA) against d99073ac. Verdict: a disciplined, genuinely well-tested bump — the JIT shielded seam never leaves spend authority resident, data-dir creation is hardened to owner-only with symlink rejection, FVK reaping has a fault-injection test, and the upstream error enums are matched exhaustively. No new HIGH or CRITICAL.

Two new MEDIUM nits posted inline:

  • The password-floor error message still says "characters" while the rule is enforced and documented in UTF-8 bytes — the one place this PR's own headline correction leaked (and two tests currently pin the wrong wording).
  • The per-op passphrase-prompt caveat lists 3 of ≥6 shielded ops that actually prompt a protected wallet; that omission has now spread into the CHANGELOG.

The three standing MEDIUMs already tracked in earlier threads all still hold at HEAD: the silent secret-bearing wallet-wipe failure (removal.rs — note this diff widened the inversion by adding a banner to the less-sensitive path), the untested broadcast_and_wait_for_affected_state swap (update_data_contract.rs), and the incomplete shielded-ops docs. Plus a handful of LOWs (a WalletLockedWalletStateInconsistent mapping left half-applied in two shielded siblings, an unreachable WalletPasswordTooShort variant, and two coverage gaps).

Nothing blocking that isn't already on record. Not approving while those standing MEDIUM threads remain open — disposition them and this is clean.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread src/backend_task/error.rs

/// A new wallet password is shorter than the persistent secret store's
/// minimum and therefore could not be migrated to Tier-2 protection.
#[error("Wallet passwords must be at least {min} characters. Pick a longer one and try again.")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — this message says "characters" while the floor it describes is enforced (and documented) in UTF-8 bytes.

The headline of this whole PR was that the password floor now counts UTF-8 bytes, not Unicode characters — and the enforcement is correct: model/wallet/mod.rs uses pw.expose_secret().trim().len() < MIN_PASSPHRASE_LEN, and str::len() is a byte count. But the string the user actually reads still says characters:

  • TaskError::WalletPasswordTooShort here (error.rs:1954), and
  • WalletCreationError::PasswordTooShort in src/model/wallet/mod.rs (kept identical by design).

Meanwhile CHANGELOG.md, docs/user-stories.md (WAL-001/002), and this PR's own body all say "8 UTF-8 bytes." So the message contradicts every other artifact and mislabels the unit the code checks. The mislabel is codified further down: the TODO(v1.1) "(8 chars)" note in secret_access.rs, and two tests — wallet_password_too_short_display_matches_model_guidance and password_too_short_display_is_actionable (error.rs:3222-3223) — assert the "characters" wording verbatim, so the tests currently pin the wrong contract rather than protecting the right one.

No security weakening (the reject direction is safe), so this is an accuracy/UX defect — but it's the one place this PR's own correction leaked. Reword both #[error(...)] strings to match the enforced/documented rule in Everyday-User terms, and fix the (8 chars) comment and the two display-assertion tests in the same stroke so the contract, not the stale wording, is what's pinned.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread CHANGELOG.md
Comment on lines +77 to +79
failing during lazy migration. Protected (Tier-2) shielded wallets now
prompt for their passphrase on every shielded transfer, unshield, or
withdraw, rather than once per session as before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — this bullet names 3 of the ≥6 shielded operations that now prompt a protected wallet.

Same gap the open docs/CLI.md / docs/MCP.md thread flags — but it has now propagated into the user-facing CHANGELOG (and the WAL user story), so it's worth catching here too before it becomes the record.

In src/wallet_backend/shielded.rs, six operations resolve the HD seed through with_secret_session and therefore prompt a password-protected (Tier-2) wallet:

  • shielded_transfer, shielded_unshield, shielded_withdraw — the three listed here, plus
  • shield_from_asset_lock (backing shielded_shield_from_core),
  • shield_from_balance (backing shielded_shield_from_platform), and
  • ensure_shielded_bound_jit (the shielded_init bind path).

An automation developer building an unattended pipeline against a protected wallet, reading only "transfer, unshield, or withdraw," will reasonably conclude the shield/init tools are exempt — then hit an undocumented SecretPromptUnavailable on the very first funding or binding step. Reword this bullet (and the CLI/MCP caveats and WAL story) to generalize: every shielded spend or bind that needs signing authority prompts a protected wallet — including shielding from an asset lock, shielding from balance, and the initial shielded bind.

🤖 Co-authored by Claudius the Magnificent AI Agent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claudius-review Triggers automated code review using claudius plugin, runs as a CI job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants