Skip to content

fix(wallet): isolate wallet databases from SPV cache + stop cross-wallet top-up corruption - #954

Merged
lklimek merged 2 commits into
v1.0-devfrom
fix/wallet-storage-isolation-and-cross-wallet-topup
Aug 1, 2026
Merged

fix(wallet): isolate wallet databases from SPV cache + stop cross-wallet top-up corruption#954
lklimek merged 2 commits into
v1.0-devfrom
fix/wallet-storage-isolation-and-cross-wallet-topup

Conversation

@Claudius-Maginificent

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

Copy link
Copy Markdown
Collaborator

TL;DR

Two independent wallet-reliability fixes, split out of PR #901 into their own PR:

  1. Wallet databases no longer live inside the folder the app treats as safe-to-delete network cache.
  2. Funding an identity you don't own (using a different wallet's balance) no longer risks corrupting or misdirecting wallet data.

User story

As a Dash Evo Tool user with more than one wallet or identity on this device, I want my wallet, identity, and key data to stay safe from cache-clearing and from paying for someone else's identity, so a routine action never bricks a wallet or loses track of my funds.

Scenario

Actual behavior

  • Each network's wallet database (identities, keys, UTXOs, contacts) is stored inside the same folder as the temporary blockchain sync cache. Clearing or losing that cache folder could take real wallet data down with it.
  • Paying Platform credits into an identity that belongs to a different wallet on this device could register that identity under the paying wallet by mistake. Restarting the app afterward could then fail to open a wallet with "Saved wallet data appears damaged and cannot be loaded" — and, separately, a later top-up of the paying wallet's own identity at the same position could silently submit for the wrong identity instead.

Expected behavior

  • Wallet databases live in their own files, entirely separate from the disposable network cache.
  • Funding another wallet's identity completes normally and never touches the paying wallet's own identity records.

Detailed discussion

Commit 1 — wallet-database isolation

platform-wallet.sqlite / platform-wallet-shielded.sqlite moved from <data_dir>/spv/<network>/ (shared with the disposable dash-spv chain cache) to <data_dir>/det-<network>.sqlite / det-<network>-shielded.sqlite, siblings of det-app.sqlite. The spv/<network>/ directory now holds only genuinely disposable chain-cache data. No migration — this changes where new/future opens look; an existing install's old-location file is not moved automatically.

Commit 2 — cross-wallet top-up corruption + funds-misdirection fix

Root cause: WalletBackend::top_up_identity unconditionally force-registered the target identity into the paying wallet's upstream IdentityManager before funding. For an identity actually owned by a different wallet, the identities table write is correctly refused by its own cross-wallet ownership guard, but the accompanying identity_keys rows land under the payer's wallet_id anyway — on the next load, the true owner's wallet-scoped reader finds those keys orphaned and fails the entire wallet load fatally. Separately (found during this investigation, not the original report): IdentityManager::add_identity overwrites whatever the payer's own wallet already had at that HD index, so a later top-up of the payer's own identity at the same index could silently submit for the wrong identity.

Funding an identity you don't own is a deliberate, existing feature (top_up_identity_screen, send_screen) and had to keep working. Fix routes on the identity's real recorded owner (StoredQualifiedIdentity.wallet_hash) — not QualifiedIdentity.associated_wallets, which turned out to be populated with every currently-loaded wallet regardless of actual ownership, so it can't be used as a guard. An owned identity still takes the existing orchestrated path; a foreign identity now funds through a manager-free path that never touches the paying wallet's identity manager.

Known gaps, tracked separately, not blocking this PR's correctness for the owned-identity path:

  • Needs live-network verification before shipping (memcan dash-evo-tool TODO 7e1a5fa8): a real foreign top-up end-to-end and the UseAssetLock resume variant are untested on-network; offline coverage stops at UTXO selection.
  • Two upstream functions needed for full IS→CL fallback and asset-lock cleanup on the new path are pub(crate) (memcan platform TODO e39c9d4a) — until exposed, a foreign top-up has no IS→CL retry, and a spent lock used for one can linger in the resumable-funding list.
  • Repair of already-corrupted wallets (from this bug, before the fix) is explicitly out of scope here.

Testing

  • cargo fmt --all — clean.
  • cargo clippy --all-features --all-targets -- -D warnings — clean, independently re-verified on this branch after the rebase off v1.0-dev.
  • New regression tests (offline, no network): wallet_databases_live_outside_the_disposable_spv_cache, cross_wallet_top_up_leaves_the_paying_wallets_identity_state_intact, cross_wallet_top_up_never_displaces_the_paying_wallets_own_identity, resolve_top_up_route_* (×3), reconcile_managed_identities_skips_identities_linked_to_another_wallet, unbound_topup_funding_account_provisions_on_the_watch_only_wallet, only_an_index_less_lock_can_fund_a_foreign_identity_top_up, an_already_spent_lock_is_refused_before_submission, plus updated fixtures across wallet_lifecycle::tests — all independently re-run and confirmed passing by name, not just aggregate count.

Breaking changes

None for the owned-identity path (unchanged). The cross-wallet top-up path now succeeds via a different internal route; behavior from the user's perspective (identity gets funded) is unchanged pending live-network confirmation.

Prior work

Split out of #901 (DPNS voting redesign) — unrelated in scope, bundled there only because both were being worked in the same session. Originates from investigating a real user-reported "Saved wallet data appears damaged" incident; see memcan dash-evo-tool TODOs 17420d0e (this PR, done) and 7e1a5fa8 (live-network follow-up, open), and platform TODOs 4fbe235c, 50cdd209, e39c9d4a for related upstream items.

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Wallets can now fund identities owned by another wallet without altering the paying wallet’s identity records.
    • Previously broadcast funding operations can be resumed with clearer recovery errors.
  • Bug Fixes

    • Wallet databases are now kept separate from disposable network caches.
    • Improved protection against using consumed or ineligible funding locks.
  • Documentation

    • Updated storage locations, identity funding behavior, and cache-clearing guidance.

lklimek added 2 commits July 31, 2026 23:40
…able SPV cache dir

platform-wallet.sqlite and platform-wallet-shielded.sqlite (identities,
keys, UTXOs, contacts, Orchard state) lived inside <data_dir>/spv/<network>/,
the same directory as the disposable, resyncable dash-spv chain cache
(headers, filters, blocks, masternode state, peers). A user or a future
"clear disposable cache" feature deleting that directory would take
irreplaceable wallet state down with it.

Each network's wallet database now lives directly under <data_dir>/, as a
sibling of det-app.sqlite: det-<network>.sqlite and
det-<network>-shielded.sqlite. The spv/<network>/ directory now holds only
genuinely disposable chain-cache data.

No migration: this only changes where new/future opens look. An existing
install's spv/<network>/platform-wallet.sqlite is not moved automatically.

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
…t ownership

top_up_identity force-registered the target identity into the paying
wallet's upstream IdentityManager whenever it wasn't already managed there
-- true for every identity actually owned by a different wallet. The
identities table write is correctly refused by its own cross-wallet
ownership guard, but the accompanying identity_keys rows land under the
payer's wallet_id anyway. On the next load the true owning wallet's
wallet-scoped reader finds those keys' owner absent from its managed set
and fails the entire wallet load fatally ("Saved wallet data appears
damaged and cannot be loaded"). Separately, IdentityManager::add_identity
overwrites whatever the payer's own wallet already had at that HD index,
so a later top-up of the payer's own identity at the same index could
silently submit for the wrong identity -- funds misdirection, not just a
load failure.

Funding an identity you don't own, from a different wallet's balance, is a
deliberate feature (top_up_identity_screen, send_screen) and must keep
working. Fixed by routing on the identity's real recorded owner
(StoredQualifiedIdentity.wallet_hash, not the associated_wallets field --
confirmed to be populated with every currently-loaded wallet regardless of
actual ownership, so unusable as a guard): an owned identity still takes
the existing orchestrated path; a foreign identity now funds through a
manager-free path (WalletBackend::create_asset_lock_proof +
TopUpIdentity::top_up_identity_with_private_key) that never touches the
paying wallet's identity manager.

Needs live-network verification before shipping (tracked separately,
memcan dash-evo-tool TODO 7e1a5fa8): offline tests cover routing,
provisioning, and the corruption regression, but a real foreign top-up
end-to-end, and the UseAssetLock resume variant, are untested on-network.
Two upstream functions needed for full IS-to-CL fallback and asset-lock
cleanup on the new path are pub(crate) (tracked as platform TODO
e39c9d4a) -- until exposed, a foreign top-up has no IS-to-CL retry and a
spent lock used for one can linger in the resumable-funding list.

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR moves durable wallet and shielded databases outside disposable SPV caches. It adds ownership-aware identity top-up routing, index-less funding accounts, asset-lock resumption, error handling, and regression coverage for cross-wallet funding isolation.

Changes

Identity storage and cross-wallet top-ups

Layer / File(s) Summary
Durable network storage
src/wallet_backend/mod.rs, src/context/wallet_lifecycle/*, src/database/initialization.rs, docs/kv-keys.md, tests/backend-e2e/wallet_reregistration.rs
Wallet and shielded databases use per-network paths outside the disposable SPV directory. Documentation and lifecycle tests use the new locations.
Unbound top-up funding
src/wallet_backend/identity_ops.rs, src/wallet_backend/payments.rs, src/backend_task/error.rs
The wallet provisions an index-less top-up account and can resume tracked, unconsumed eligible asset locks. New errors report consumed or ineligible locks.
Ownership-aware identity top-ups
src/backend_task/identity/top_up_identity.rs, src/backend_task/identity/mod.rs, src/wallet_backend/mod.rs, docs/user-stories.md, CHANGELOG.md, src/context/identity_db.rs
Top-ups route wallet-owned identities through HD-slot funding and foreign identities through SDK asset-lock funding. Tests cover routing, identity ownership isolation, account provisioning, and reconciliation behavior.

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

Possibly related PRs

Suggested reviewers: lklimek

🚥 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 and concisely summarizes both main fixes: wallet database isolation and prevention of cross-wallet top-up corruption.
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 fix/wallet-storage-isolation-and-cross-wallet-topup

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.

@Claudius-Maginificent
Claudius-Maginificent marked this pull request as ready for review July 31, 2026 23:43
@thepastaclaw

thepastaclaw commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

⛔ Blockers found — Sonnet deferred (commit 27231c2)
Canonical validated blockers: 1

@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/identity/top_up_identity.rs (1)

196-236: 🗄️ Data Integrity & Integration | 🔵 Trivial

Confirm the TODO's tracked-consumption gap is acceptable for this release.

The TODO at lines 207-211 states that a spent lock used through this path keeps its pre-consumption status in the funding list, because consume_asset_lock stays pub(crate) upstream. A user (or the UI's "recover unfinished funding" list) could see the same already-used lock as still eligible, and attempt to reuse it. Platform's own asset-lock consumption checks would likely reject a genuine reuse, but the local funding list still misrepresents the lock's state until reconciliation catches up.

Since this is already tracked as an upstream contribution, do you want me to open a tracking issue for restoring consume_asset_lock/upgrade_to_chain_lock_proof calls once upstream exposes them?

🤖 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/identity/top_up_identity.rs` around lines 196 - 236, Track
the missing asset-lock consumption and recovery behavior in the existing
upstream TODO around top_up_foreign_identity, including restoring
consume_asset_lock and upgrade_to_chain_lock_proof once upstream exposes them.
For this release, leave the current implementation unchanged and ensure the
tracked issue covers removing used locks from resumable funding and retrying
rejected InstantSend proofs.
src/context/wallet_lifecycle/tests.rs (1)

4519-4560: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the dispatch reaches funding before checking anti-displacement.

cross_wallet_top_up_never_displaces_the_paying_wallets_own_identity discards the dispatch result as _error and asserts only that own still resolves to itself afterward. If the ownership-aware routing rejects the call early (before it can reach the write that would displace own), this assertion passes trivially regardless of whether the anti-displacement fix is present.

Its sibling test, cross_wallet_top_up_leaves_the_paying_wallets_identity_state_intact, adds an explicit check that the error is not IdentityNotWalletOwned/IdentityIndexMismatch so the test proves the call reached funding. Add the same check here so a regression in the anti-displacement logic cannot be masked by an unrelated early rejection.

♻️ Suggested addition
     let _error = dispatch_wallet_funded_top_up(&ctx, &sender, &foreign, &payer_arc, 0).await;
+    assert!(
+        !matches!(
+            _error,
+            TaskError::IdentityNotWalletOwned { .. } | TaskError::IdentityIndexMismatch { .. }
+        ),
+        "the foreign top-up must reach funding, not be rejected as unowned, so this test \
+         actually exercises the anti-displacement path: {_error:?}"
+    );
🤖 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` around lines 4519 - 4560, Update
cross_wallet_top_up_never_displaces_the_paying_wallets_own_identity to retain
and inspect the result from dispatch_wallet_funded_top_up, asserting it does not
fail with IdentityNotWalletOwned or IdentityIndexMismatch. Keep the existing
resolution assertion, ensuring the test verifies funding was reached before
validating that the payer’s own identity remained intact.
🤖 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 `@docs/kv-keys.md`:
- Line 166: Update the key name in the documentation table row to escape the
pipe within the `<decline|cancel>` placeholder as `\|`, keeping the row’s
five-column Markdown structure intact.

In `@src/wallet_backend/payments.rs`:
- Around line 893-932: In resume_unbound_topup_asset_lock, provision the
seed-derived identity top-up funding account inside the with_secret_session
closure before calling wallet.asset_locks().resume_asset_lock. Reuse the same
idempotent provisioning operation and arguments used by create_asset_lock_proof,
then preserve the existing resume and key-derivation flow.

---

Nitpick comments:
In `@src/backend_task/identity/top_up_identity.rs`:
- Around line 196-236: Track the missing asset-lock consumption and recovery
behavior in the existing upstream TODO around top_up_foreign_identity, including
restoring consume_asset_lock and upgrade_to_chain_lock_proof once upstream
exposes them. For this release, leave the current implementation unchanged and
ensure the tracked issue covers removing used locks from resumable funding and
retrying rejected InstantSend proofs.

In `@src/context/wallet_lifecycle/tests.rs`:
- Around line 4519-4560: Update
cross_wallet_top_up_never_displaces_the_paying_wallets_own_identity to retain
and inspect the result from dispatch_wallet_funded_top_up, asserting it does not
fail with IdentityNotWalletOwned or IdentityIndexMismatch. Keep the existing
resolution assertion, ensuring the test verifies funding was reached before
validating that the payer’s own identity remained intact.
🪄 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: c200f746-5ba7-4819-ba31-8f7879a5a273

📥 Commits

Reviewing files that changed from the base of the PR and between 39b3992 and 27231c2.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/kv-keys.md
  • docs/user-stories.md
  • src/backend_task/error.rs
  • src/backend_task/identity/mod.rs
  • src/backend_task/identity/top_up_identity.rs
  • src/backend_task/migration/finish_unwire.rs
  • src/context/identity_db.rs
  • src/context/wallet_lifecycle/mod.rs
  • src/context/wallet_lifecycle/registration.rs
  • src/context/wallet_lifecycle/spv.rs
  • src/context/wallet_lifecycle/tests.rs
  • src/database/initialization.rs
  • src/wallet_backend/identity_ops.rs
  • src/wallet_backend/mod.rs
  • src/wallet_backend/payments.rs
  • tests/backend-e2e/wallet_reregistration.rs

Comment thread docs/kv-keys.md
| `det:dashpay:blocked:<base58_contact_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: contact is blocked |
| `det:dashpay:declined:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: incoming contact request declined |
| `det:dashpay:withdrawn:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: outgoing contact request withdrawn |
| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the | inside the key name to fix the broken table row.

The unescaped | in <decline|cancel> splits this row into 6 cells instead of 5, as flagged by markdownlint. Escape it as \| so the table renders correctly.

🐛 Proposed fix
-| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
+| `det:dashpay:request_action:<decline\|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
| `det:dashpay:request_action:<decline\|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 166-166: Table column count
Expected: 5; Actual: 6; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 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 `@docs/kv-keys.md` at line 166, Update the key name in the documentation table
row to escape the pipe within the `<decline|cancel>` placeholder as `\|`,
keeping the row’s five-column Markdown structure intact.

Source: Linters/SAST tools

Comment on lines +893 to +932
pub(crate) async fn resume_unbound_topup_asset_lock(
&self,
seed_hash: &WalletSeedHash,
out_point: dash_sdk::dpp::dashcore::OutPoint,
) -> Result<
(
dash_sdk::dpp::prelude::AssetLockProof,
dash_sdk::dpp::dashcore::PrivateKey,
),
TaskError,
> {
let tracked = self
.list_tracked_asset_locks(seed_hash)
.await?
.into_iter()
.find(|lock| lock.out_point == out_point);
unbound_topup_lock_eligible(
tracked
.as_ref()
.map(|lock| (lock.funding_type, &lock.status)),
)?;

let scope = Self::hd_scope(seed_hash);
self.inner
.secret_access
.with_secret_session(&scope, async |session| {
let wallet = self.resolve_wallet(seed_hash).await?;
let (proof, credit_output_path) = wallet
.asset_locks()
.resume_asset_lock(&out_point, None)
.await
.map_err(|e| TaskError::WalletBackend {
source: Arc::new(e),
})?;
let private_key =
self.derive_private_key_from_held(session.plaintext(), &credit_output_path)?;
Ok((proof, private_key))
})
.await
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching payments/identity/seam:"
fd -a 'payments\.rs|identity_ops\.rs|secret_seam\.rs|Cargo\.toml|Cargo\.lock' . | sed 's#^\./##'

echo
echo "Relevant symbols in src/wallet_backend:"
rg -n "resume_unbound_topup_asset_lock|ensure_unbound_topup_funding_account|unbound_topup_lock_eligible|create_asset_lock_proof|top_up_identity|provision_identity_funding_account|IdentityTopUpNotBound|asset_locks" src/wallet_backend -S

echo
echo "payments.rs outline:"
ast-grep outline src/wallet_backend/payments.rs --match resume_unbound_topup_asset_lock --view expanded || true
ast-grep outline src/wallet_backend/payments.rs --match create_asset_lock_proof --view expanded || true

echo
echo "payments.rs relevant lines 800-970:"
sed -n '800,970p' src/wallet_backend/payments.rs | nl -ba -v800

echo
echo "identity_ops.rs relevant lines around funding/account resolution:"
rg -n "ensure_identity_funding_accounts|FromExistingAssetLock|profile_topup|provision_identity_funding_account" src/wallet_backend -S -C 8

Repository: dashpay/dash-evo-tool

Length of output: 4807


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Cargo dependencies mentioning platform_wallet / dash_sdk:"
rg -n "platform_wallet|dash_sdk|KeyWallet|AssetLockManager|resume_asset_lock|create_asset_lock_proof|asset_locks" Cargo.toml Cargo.lock -S

echo
echo "Cargo.lock platform_wallet versions:"
sed -n '/name = "platform_wallet"/,/^name =/p' Cargo.lock | head -80 || true

Repository: dashpay/dash-evo-tool

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "payments.rs section 800-970"
sed -n '800,970p' src/wallet_backend/payments.rs | cat -n | sed 's/^\s*//'

echo
echo "identity_ops.rs provisioning helpers section"
sed -n '320,490p' src/wallet_backend/identity_ops.rs | cat -n | sed 's/^\s*//'

echo
echo "mod.rs provisioning references"
sed -n '630,680p' src/wallet_backend/mod.rs | cat -n | sed 's/^\s*//'
sed -n '2370,2445p' src/wallet_backend/mod.rs | cat -n | sed 's/^\s*//'
sed -n '2530,2565p' src/wallet_backend/mod.rs | cat -n | sed -s 's/^\s*//'

echo
echo "Cargo files"
sed -n '1,140p' Cargo.toml
sed -n '1,80p' Cargo.lock | cat -n

Repository: dashpay/dash-evo-tool

Length of output: 31736


🌐 Web query:

Dash Platform SDK platform_wallet AssetLockManager resume_asset_lock source

💡 Result:

In the Dash Platform SDK, the AssetLockManager is a component within the platform-wallet package responsible for managing the lifecycle and orchestration of asset locks [1][2]. The resume_asset_lock operation is a functional component of the AssetLockManager designed to resume the processing or funding workflow associated with an existing asset lock [1][2][3]. Key technical aspects include: 1. Purpose: It is used in workflows like identity top-ups or shielded funding, allowing a wallet to resume an interrupted or pending asset lock process rather than starting a new one [2][3]. 2. Implementation: It is implemented within the rs-platform-wallet crate (e.g., in modules like orchestration.rs or manager.rs) [2]. The functionality is also exposed via Foreign Function Interface (FFI) to support cross-language usage in SDKs like the Swift SDK (e.g., as platform_wallet_manager_resume_fund_from_asset_lock) [2][3]. 3. Current Status and Considerations: Recent development updates (as of mid-2026) have focused on completing the AssetLockManager FFI, adding tracking accessors, and integrating SPV event subscription [1]. Technical discussions have highlighted the need for robust Platform-side re-validation when using resume operations to ensure the integrity of the asset lock state [4]. Developers should refer to the rs-platform-wallet package within the dashpay/platform repository for the specific source implementation, as it is part of the core wallet orchestration logic rather than the high-level Evo SDK facade [1][2][5].

Citations:


🌐 Web query:

github dashpay/platform AssetLockManager resume_asset_lock KeyWallet add_account identity_topup_not_bound

💡 Result:

In the Dash Platform ecosystem, the components you referenced relate to wallet management, asset lock orchestration, and identity funding. AssetLockManager is a core component within the platform wallet architecture responsible for tracking, orchestrating, and resuming asset lock operations [1][2]. It manages the lifecycle of asset locks, including subscribing to SPV (Simplified Payment Verification) events, handling changesets for tracked asset locks, and coordinating retry logic for state transitions that require L1 (Core) locking transactions [1][2][3]. The resume_asset_lock operation allows the wallet to continue a previously interrupted top-up or funding process, often by resolving funding through ChainLock (CL) or Instant Send (IS) fallback mechanisms [2][3]. KeyWallet refers to a component in the rust-dashcore library that manages wallet accounts, including the recent addition of PlatformPayment account support [4][5]. The add_account function is used to insert new managed accounts into the wallet's collection [6]. The mention of identity_topup_not_bound appears in the context of the wallet's state management (specifically within clear or cleanup methods), where it represents an unbound identity top-up record that hasn't been associated with a specific identity or successfully processed [4]. In summary: - AssetLockManager handles the technical orchestration of L1-to-Platform funding [2][3]. - resume_asset_lock enables recovery of these funding transactions [2]. - KeyWallet manages the account structures, including those for platform payments [4][5]. - identity_topup_not_bound is a state tracking field for top-up operations within the wallet that are not yet bound to an identity [4].

Citations:


🌐 Web query:

site:github.com/dashpay/platform AssetLockManager resume_asset_lock "funding_type"

💡 Result:

The AssetLockManager provides functionality for managing the lifecycle of asset locks within the Dash Platform, including mechanisms for resuming interrupted funding operations [1]. The specific operation resume_asset_lock (often exposed via Foreign Function Interface as platform_address_wallet_resume_top_up_with_existing_asset_lock_signer or similar variants like platform_wallet_manager_shielded_resume_fund_from_asset_lock) is designed to continue a funding process that was previously initiated but not completed [2][3]. Regarding funding_type (often represented by the AssetLockFundingType enum), it is a critical parameter used to determine how the asset lock is derived and processed [3][4]. When resuming a funding operation, it is essential that the funding_type matches the type used during the initial creation of the asset lock [3]. Key points regarding this process include: - Consistency: If the funding_type is incorrectly specified during a resume operation, the wallet may attempt to derive addresses from the wrong account, leading to failed state transitions or rejections [3]. - Implementation details: Funding types distinguish between different sources and methodologies, such as AssetLockAddressTopUp (Type 14) and AssetLockShieldedAddressTopUp (Type 18) [3]. - Error Handling: Resuming operations have historically faced challenges, such as failures to properly re-validate Platform-side consumption or silent failure handling, which developers should monitor for in current implementations [5][6]. For technical implementation, developers typically interact with these managers via language-specific SDKs (such as the Swift SDK) which marshal arguments to the underlying Rust AssetLockManager FFI entry points [2][3].

Citations:


🌐 Web query:

site:github.com/dashpay/platform resume_asset_lock "pub async"

💡 Result:

The term "resume_asset_lock" refers to an asynchronous function used within the Dash Platform codebase (specifically in the rs-platform-wallet library) to resume wallet-related operations involving asset locks [1][2][3]. In the context of the Dash Platform, asset locks are mechanisms used to lock Dash output on-chain to mint credits or fund identities [4]. Because these processes can span multiple states (e.g., Built, Broadcast, InstantSendLocked, ChainLocked), the platform provides functionality to resume interrupted processes [1][5]. Key details regarding "resume_asset_lock" and its related operations include: - Functionality: It is an async function that allows the wallet to resume funding or top-up operations using existing asset locks [1][2][3]. It operates by tracking the status of asset locks and retrying or continuing the process from the locally-tracked state [6][5]. - Rust Implementation: The operation is implemented in Rust, often utilizing the async/await pattern to handle network and consensus-dependent operations without blocking execution [1]. - FFI and SDK Usage: The functionality is exposed to higher-level languages (like Swift) via Foreign Function Interface (FFI) wrappers [2][3]. For example, there are specific FFI functions such as platform_address_wallet_resume_top_up_with_existing_asset_lock_signer and platform_wallet_manager_shielded_resume_fund_from_asset_lock, which are then exposed as async methods in the Dash Swift SDK [2][3]. - Recent Updates: As of May 2026, the Dash Platform v3.1.0-dev.2 release included significant updates to these systems, including the addition of resumable asset lock operations and improvements to the underlying async architecture [1]. It is important to note that the resume functionality is generally designed to proceed based on locally tracked status and may not always re-validate the current state against the platform in real-time, which is a known area of technical discussion regarding the consistency of these operations [5].

Citations:


🌐 Web query:

site:github.com/dashpay/platform "resume_asset_lock"

💡 Result:

In the context of the Dash Platform ecosystem, resume_asset_lock refers to a functional mechanism within the platform-wallet package designed to continue or recover the lifecycle of an asset lock transaction that has been previously initiated but did not complete successfully [1][2][3]. Key technical aspects include: Purpose and Functionality: It enables the wallet to resume the funding process for an identity registration or top-up using an existing, previously broadcasted asset lock transaction, rather than creating and broadcasting a new one [1][2][4]. This is particularly useful in scenarios where a transaction times out waiting for consensus, such as InstantSend (IS) or ChainLock (CL) finality [3][4]. Implementation: - It is part of the platform-wallet orchestration logic [1][3]. - It is exposed through Foreign Function Interface (FFI) bindings (e.g., as part of platform-wallet-ffi) and wrapped for higher-level languages like Swift (e.g., resumeTopUpFromAssetLock or shieldedResumeFundFromAssetLock) [2][3]. - It leverages the TrackedAssetLock mechanism to identify and manage the state of these locks [1][4]. Contextual Considerations: - Error Handling and Retries: While it is a critical component for robust wallet operation, developers have noted that certain implementations, such as the finality timeout retry path, have historically required careful wiring to ensure the original lock is resumed rather than ignored in favor of new locks [4]. - Security and Validation: Ongoing development and issue tracking have highlighted the importance of platform-side re-validation when resuming asset locks to ensure integrity [5][6]. This functionality is central to ensuring that user funds are not unnecessarily locked or orphaned due to transient network or consensus timing issues [4].

Citations:


Provision the unbound top-up funding account before resuming.

resume_asset_lock(&out_point, None) needs the same seed-derived funding account that create_asset_lock_proof registers. A cold reload leaves identity_topup_not_bound absent, because identity-funding re-provision is deferred there.

Add the idempotent provisioning call inside the held-seed session, matching create_asset_lock_proof.

🐛 Proposed fix
         let scope = Self::hd_scope(seed_hash);
         self.inner
             .secret_access
             .with_secret_session(&scope, async |session| {
+                let plaintext = session.plaintext();
+                let seed = plaintext
+                    .expose_hd_seed()
+                    .ok_or(TaskError::WalletStateInconsistent)?;
+                self.ensure_unbound_topup_funding_account(seed_hash, seed)
+                    .await?;
                 let wallet = self.resolve_wallet(seed_hash).await?;
                 let (proof, credit_output_path) = wallet
                     .asset_locks()
                     .resume_asset_lock(&out_point, None)
                     .await
                     .map_err(|e| TaskError::WalletBackend {
                         source: Arc::new(e),
                     })?;
                 let private_key =
                     self.derive_private_key_from_held(session.plaintext(), &credit_output_path)?;
                 Ok((proof, private_key))
             })
             .await
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub(crate) async fn resume_unbound_topup_asset_lock(
&self,
seed_hash: &WalletSeedHash,
out_point: dash_sdk::dpp::dashcore::OutPoint,
) -> Result<
(
dash_sdk::dpp::prelude::AssetLockProof,
dash_sdk::dpp::dashcore::PrivateKey,
),
TaskError,
> {
let tracked = self
.list_tracked_asset_locks(seed_hash)
.await?
.into_iter()
.find(|lock| lock.out_point == out_point);
unbound_topup_lock_eligible(
tracked
.as_ref()
.map(|lock| (lock.funding_type, &lock.status)),
)?;
let scope = Self::hd_scope(seed_hash);
self.inner
.secret_access
.with_secret_session(&scope, async |session| {
let wallet = self.resolve_wallet(seed_hash).await?;
let (proof, credit_output_path) = wallet
.asset_locks()
.resume_asset_lock(&out_point, None)
.await
.map_err(|e| TaskError::WalletBackend {
source: Arc::new(e),
})?;
let private_key =
self.derive_private_key_from_held(session.plaintext(), &credit_output_path)?;
Ok((proof, private_key))
})
.await
}
pub(crate) async fn resume_unbound_topup_asset_lock(
&self,
seed_hash: &WalletSeedHash,
out_point: dash_sdk::dpp::dashcore::OutPoint,
) -> Result<
(
dash_sdk::dpp::prelude::AssetLockProof,
dash_sdk::dpp::dashcore::PrivateKey,
),
TaskError,
> {
let tracked = self
.list_tracked_asset_locks(seed_hash)
.await?
.into_iter()
.find(|lock| lock.out_point == out_point);
unbound_topup_lock_eligible(
tracked
.as_ref()
.map(|lock| (lock.funding_type, &lock.status)),
)?;
let scope = Self::hd_scope(seed_hash);
self.inner
.secret_access
.with_secret_session(&scope, async |session| {
let plaintext = session.plaintext();
let seed = plaintext
.expose_hd_seed()
.ok_or(TaskError::WalletStateInconsistent)?;
self.ensure_unbound_topup_funding_account(seed_hash, seed)
.await?;
let wallet = self.resolve_wallet(seed_hash).await?;
let (proof, credit_output_path) = wallet
.asset_locks()
.resume_asset_lock(&out_point, None)
.await
.map_err(|e| TaskError::WalletBackend {
source: Arc::new(e),
})?;
let private_key =
self.derive_private_key_from_held(session.plaintext(), &credit_output_path)?;
Ok((proof, private_key))
})
.await
}
🤖 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/payments.rs` around lines 893 - 932, In
resume_unbound_topup_asset_lock, provision the seed-derived identity top-up
funding account inside the with_secret_session closure before calling
wallet.asset_locks().resume_asset_lock. Reuse the same idempotent provisioning
operation and arguments used by create_asset_lock_proof, then preserve the
existing resume and key-derivation flow.

@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 cross-wallet routing fix prevents the targeted identity-manager corruption, but the database relocation is not upgrade-safe. Released builds store durable wallet and shielded state at the old SPV-directory paths, while this head opens fresh replacement databases without migrating that state; one new provisioning test also never exercises the branch it claims to cover.

Source: reviewers codex/general=gpt-5.6-sol(completed); codex/rust-quality=gpt-5.6-sol(completed); verifier=codex/verifier=gpt-5.6-sol(completed); coordinator=openclaw-agent/cliproxy/gpt-5.6-sol(orchestration-only).

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:481-524: Migrate the released databases before opening the new paths
  The released `v1.0.0-weekly.20260731` code opens `<data_dir>/spv/<network>/platform-wallet.sqlite` and configures `<data_dir>/spv/<network>/platform-wallet-shielded.sqlite`. This head instead opens `<data_dir>/det-<network>.sqlite` and `<data_dir>/det-<network>-shielded.sqlite`, but no code checks or migrates the released locations. `SqlitePersister::open` creates the missing destination, so an ordinary upgrade loads an empty persister and makes the old wallets, identities, keys, address state, UTXOs, tracked asset locks, contacts, scheduled votes, contracts, tokens, and other per-network `DetKv` data inaccessible. The shielded coordinator similarly loses access to its previous state. Migrate both databases before either destination is opened, make the operation idempotent and crash-safe, preserve SQLite WAL/SHM state, define safe handling when both paths exist, and add an upgrade test beginning with populated databases in the released layout.

In `src/context/wallet_lifecycle/tests.rs`:
- [SUGGESTION] src/context/wallet_lifecycle/tests.rs:4345-4355: Exercise the missing-account provisioning branch
  `register_test_wallet` registers the upstream wallet with `WalletAccountCreationOptions::Default`. In the pinned key-wallet revision, the default special-purpose accounts include `IdentityTopUpNotBoundToIdentity`, and `ManagedWalletInfo::from_wallet` copies that account into the managed collection. Both membership probes are therefore already true before the first `ensure_unbound_topup_funding_account` call, so both calls return through the early no-op path. The test still passes if the new derivation and insertion branches are broken. Build a watch-only fixture whose account manifest deliberately omits this account, then verify that provisioning restores it in both the key-wallet and managed collections.

Comment thread src/wallet_backend/mod.rs
Comment on lines 481 to +524
@@ -495,12 +516,12 @@ impl WalletBackend {

// Wire the upstream shielded coordinator into the manager.
//
// Uses a dedicated SQLite file (`platform-wallet-shielded.sqlite`) owned
// Uses a dedicated SQLite file (`det-<network>-shielded.sqlite`) owned
// entirely by the upstream coordinator — the single source of truth for
// all Orchard state. The coordinator starts empty — no wallets are bound
// until `ensure_shielded_bound` runs (on wallet unlock). Subsequent
// calls with the same path are idempotent (upstream no-ops).
pwm.configure_shielded(spv_storage_dir.join("platform-wallet-shielded.sqlite"))
pwm.configure_shielded(shielded_database_path(ctx.data_dir(), network))

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.

🔴 Blocking: Migrate the released databases before opening the new paths

The released v1.0.0-weekly.20260731 code opens <data_dir>/spv/<network>/platform-wallet.sqlite and configures <data_dir>/spv/<network>/platform-wallet-shielded.sqlite. This head instead opens <data_dir>/det-<network>.sqlite and <data_dir>/det-<network>-shielded.sqlite, but no code checks or migrates the released locations. SqlitePersister::open creates the missing destination, so an ordinary upgrade loads an empty persister and makes the old wallets, identities, keys, address state, UTXOs, tracked asset locks, contacts, scheduled votes, contracts, tokens, and other per-network DetKv data inaccessible. The shielded coordinator similarly loses access to its previous state. Migrate both databases before either destination is opened, make the operation idempotent and crash-safe, preserve SQLite WAL/SHM state, define safe handling when both paths exist, and add an upgrade test beginning with populated databases in the released layout.

source: ['codex']

Comment on lines +4345 to +4355
let seed = [0xAEu8; 64];
let (seed_hash, _wallet_arc) = register_test_wallet(&ctx, &backend, seed, "payer").await;

backend
.ensure_unbound_topup_funding_account(&seed_hash, &seed)
.await
.expect("the index-less top-up account must provision on a watch-only wallet");
backend
.ensure_unbound_topup_funding_account(&seed_hash, &seed)
.await
.expect("the second call must be a no-op, proving both collections hold the account");

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.

🟡 Suggestion: Exercise the missing-account provisioning branch

register_test_wallet registers the upstream wallet with WalletAccountCreationOptions::Default. In the pinned key-wallet revision, the default special-purpose accounts include IdentityTopUpNotBoundToIdentity, and ManagedWalletInfo::from_wallet copies that account into the managed collection. Both membership probes are therefore already true before the first ensure_unbound_topup_funding_account call, so both calls return through the early no-op path. The test still passes if the new derivation and insertion branches are broken. Build a watch-only fixture whose account manifest deliberately omits this account, then verify that provisioning restores it in both the key-wallet and managed collections.

source: ['codex']

@lklimek
lklimek merged commit b8fd039 into v1.0-dev Aug 1, 2026
6 checks passed
@lklimek
lklimek deleted the fix/wallet-storage-isolation-and-cross-wallet-topup branch August 1, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants