Skip to content

fix(platform-wallet): finalize reconstructed asset locks as RecoveredFromChain, in-session - #4347

Merged
QuantumExplorer merged 6 commits into
v4.2-devfrom
fix/asset-lock-restore-finality
Aug 9, 2026
Merged

fix(platform-wallet): finalize reconstructed asset locks as RecoveredFromChain, in-session#4347
QuantumExplorer merged 6 commits into
v4.2-devfrom
fix/asset-lock-restore-finality

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 9, 2026

Copy link
Copy Markdown
Member

Follow-up to #4342: its restore-scan reconstruction never actually produced the RecoveredFromChain terminal on a real restore. Observed on a restored testnet wallet (dashwallet-ios): all 9 reconstructed locks sat at Broadcast for the whole session and only reached ChainLocked after a restart — both inside the pending window every host reads as "in flight", so every historical funding tx rendered as a pending transfer.

Why it happened

  1. The filter scan detects historical funding txs before any chainlock is applied, so entries insert at the pre-finality Broadcast status (recovered_status's non-final arm) — never at RecoveredFromChain.
  2. The bulk InBlock → InChainLockedBlock promotion the tip chainlock performs surfaces only as ChainLockProcessed, which the wallet-event adapter mapped to metadata persistence alone. The promoted records never re-flow through TransactionDetected/BlockProcessed, so reconstruction/enrichment never saw them.
  3. When enrichment did run (restart re-emissions), it upgraded to ChainLocked, not RecoveredFromChain.

Fixes

  • enrich_from_record upgrades to RecoveredFromChain (+ chain proof). A lock a live flow is completing leaves the proof-less Broadcast/IS-locked window within seconds (wait_for_proof attaches the proof via advance_asset_lock_status, which still overwrites unconditionally in the benign race) — what remains proof-less at finality is by elimination a lock nobody is completing, and "final on Core, consumption unknown" is the truthful terminal.
  • The adapter routes ChainLockProcessed through a new enrich_tracked_asset_locks_from_chain_lock. Deliberately record-free: under the default keep-finalized-transactions=OFF the promotion evicts the records and the event retains only txids, so the hook upgrades the tracked entries directly by txid and builds the chain proof from the chainlock's own height (the same fact the resume path's CL-from-metadata fallback relies on). Funding-family filtering happens on the event's account-type keys, so chainlocks promoting plain payments never take the wallet-manager write lock.
  • resume_asset_lock no longer downgrades a RecoveredFromChain entry into the pending window: a resume proves nothing new about Platform-side consumption. Without this, the launch-time catchUpStuckAssetLocks sweep (or any explicit resume that doesn't end in a spend) silently resurrected the false-"Pending" state.

Verification

  • cargo test -p platform-wallet --lib: 601/601 (new tests: chainlock-promotion upgrade at reconstruction and bridge level; resume keeps RecoveredFromChain; existing enrich/resume tests updated to the new terminals).
  • On-device (restored testnet wallet, dashwallet-ios dashpay build): fresh wipe-and-recover scan inserted all 9 historical locks at Broadcast, and the first post-sync chainlock upgraded all 9 to statusRaw 5 + proof in the same sessionattaching chain proof to tracked asset lock from chainlock promotion ×9 in the SDK log, funding types classified (identity registration / platform / shielded), and the host app renders them outside both pending and consumed.

Known residual (narrow): a foreground catchUpStuckAssetLocks sweep that fires mid-restore-scan can still resume a just-reconstructed Broadcast row to ChainLocked+proof before the chainlock arm reaches it, parking that row at 3. The window is the few minutes between insert and the next chainlock, only on a restore in progress.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved asset-lock recovery after chain finality is confirmed.
    • Finalized asset locks now retain recovered status and include chain proof.
    • Pre-finality asset locks are promoted when finality information becomes available.
    • Recovery and resume flows preserve finalized asset-lock state consistently.
    • Prevented terminal consumed asset locks from being overwritten or removed by stale updates.
  • Tests

    • Added end-to-end and persistence coverage for recovery, finality promotion, and terminal status ordering.

…FromChain, in-session

#4342's restore-scan reconstruction never actually produced its
RecoveredFromChain terminal on a real restore (observed on a restored
testnet wallet: all 9 reconstructed locks stuck at Broadcast for the
whole session, ChainLocked after a restart — both of which hosts read
as "in flight", so every historical funding tx rendered as a pending
transfer):

- The filter scan detects historical funding txs BEFORE any chainlock
  is applied, so entries insert at the pre-finality Broadcast status
  (the recovered_status non-final arm) rather than RecoveredFromChain.
- The bulk promotion to InChainLockedBlock that the tip chainlock
  performs surfaces ONLY as ChainLockProcessed, which the wallet-event
  adapter mapped to metadata persistence alone — the promoted records
  never re-flowed through reconstruction, so the entries stayed
  pre-finality until a restart happened to re-emit them.
- And when enrichment did run, it upgraded to ChainLocked, not
  RecoveredFromChain.

Fixes:

- enrich_from_record now upgrades proof-less Broadcast/IS-locked
  entries to RecoveredFromChain + chain proof. A lock a live flow is
  completing leaves that window within seconds (wait_for_proof attaches
  the proof via advance_asset_lock_status, which still overwrites
  unconditionally in the benign race), so what remains proof-less at
  finality is by elimination a lock nobody is completing — "final on
  Core, consumption unknown" is the truthful terminal.
- The adapter routes ChainLockProcessed.locked_transactions through a
  new enrich_tracked_asset_locks_from_chain_lock: funding-family
  account keys are filtered lock-free, the promoted records are read
  back from the wallet, and the same per-record reconstruction step
  (extracted as apply_record) runs — so the upgrade lands in the same
  session, riding the same drained batch to the store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98b60a9a-59f3-495c-93a8-1be54479a432

📥 Commits

Reviewing files that changed from the base of the PR and between c97da6f and 68723cd.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs
  • packages/rs-platform-wallet-storage/tests/sqlite_persist_roundtrip.rs
  • packages/rs-platform-wallet/src/changeset/changeset.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs
  • packages/rs-platform-wallet/src/changeset/changeset.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift

📝 Walkthrough

Walkthrough

This change promotes eligible pre-finality asset locks to RecoveredFromChain when chain-lock finality is observed. It attaches chain proofs, persists the updates, preserves the recovered status during resume, and prevents stale writes from replacing Consumed records.

Changes

Asset lock recovery

Layer / File(s) Summary
Reconstruction status and proof handling
packages/rs-platform-wallet/src/wallet/asset_lock/sync/reconstruction.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs, packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs
Finalized records now use RecoveredFromChain with a chain proof. Reconstruction and resume preserve this status. Tests cover promotion, resume behavior, live-flow advancement, and consumption.
Chain-lock promotion and event integration
packages/rs-platform-wallet/src/wallet/asset_lock/sync/reconstruction.rs, packages/rs-platform-wallet/src/changeset/core_bridge.rs
ChainLockProcessed events promote eligible tracked locks using locked transaction IDs and chain-lock height. Integration tests verify proof attachment and persistence.
Consumed status protection
packages/rs-platform-wallet/src/changeset/changeset.rs, packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs, packages/rs-platform-wallet-storage/tests/sqlite_persist_roundtrip.rs, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift, .codecov.yml
Changeset, SQLite, and Swift persistence paths reject stale non-Consumed updates over existing Consumed records. Tests verify valid ordering and terminal-state protection. Codecov excludes the separately tested storage package.

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

Sequence Diagram(s)

sequenceDiagram
  participant ChainLockProcessed
  participant CoreBridge
  participant Reconstruction
  participant AssetLockAdapter
  ChainLockProcessed->>CoreBridge: Provide chain-lock height and locked transactions
  CoreBridge->>Reconstruction: Enrich tracked asset locks
  Reconstruction->>AssetLockAdapter: Persist RecoveredFromChain status and chain proof
Loading

Possibly related issues

  • dashpay/platform#4238 — Adds chain-lock finality promotion for tracked asset locks.
  • dashpay/dash-evo-tool#930 — Addresses stuck broadcast asset locks during reconstruction and promotion.

Possibly related PRs

Suggested reviewers: shumkov, lklimek, llbartekll, zocolini

🚥 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 identifies the primary change: finalizing reconstructed asset locks as RecoveredFromChain during the current wallet session.
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/asset-lock-restore-finality

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

@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 `@packages/rs-platform-wallet/src/wallet/asset_lock/sync/reconstruction.rs`:
- Around line 242-270: Distinguish active live completion from reconstruction
before assigning RecoveredFromChain: in
packages/rs-platform-wallet/src/wallet/asset_lock/sync/reconstruction.rs:242-270,
require an explicit completion-ownership or reconstruction-provenance signal in
addition to proof absence and Broadcast/InstantSendLocked status; preserve the
live-flow status at reconstruction.rs:312-312 and apply the same exclusion
during chain-lock promotion at reconstruction.rs:420-462. Document this
lifecycle signal in
packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs:64-70, and add a
regression test covering a live proof-less Broadcast entry during chain-lock
promotion.
🪄 Autofix

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: 1cbef70f-8ed6-494a-9a7f-2abf29665ba8

📥 Commits

Reviewing files that changed from the base of the PR and between 98c5227 and e7f06d2.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet/src/changeset/core_bridge.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/reconstruction.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs

@thepastaclaw

thepastaclaw commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — next in queue (commit 68723cd)
Queue position: 1/2
ETA: start ~08:40 UTC · complete ~08:54 UTC (median 14m across 30 recent reviews; 2 slots)
Queued 15m ago · Last checked: 2026-08-09 08:40 UTC

QuantumExplorer and others added 3 commits August 9, 2026 07:09
…sification

Review follow-up: enrichment's "nothing live is completing it" rule is
structural (proof-less + pre-finality), not provenance-based, so a
chainlock promotion CAN transiently classify a still-waiting live lock
RecoveredFromChain. Pin the convergence guarantee with a regression
test — the live pipeline's unconditional advance_asset_lock_status
overwrites the transient classification and consumption still reaches
Consumed — and document the lifecycle on the variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…layer

Review follow-up (stale recovery writes vs live state): the wallet-event
adapter persists enrichment snapshots from its batched drain, while
live flows persist synchronously through the changeset queue — so a
stale RecoveredFromChain snapshot, taken under the wallet lock before a
live consumption, could land at the store AFTER the Consumed write and
regress durable/host state (merge and the upserts were unconditional
last-write-wins).

A total status ordering would be wrong — every non-terminal transition
is legitimately bidirectional (live advances overwrite
RecoveredFromChain, defensive resumes re-enter Broadcast) — but
Consumed is terminal, and terminality is enforceable without vetoing
real transitions. Enforce it at all three write layers, making the
arrival order of racing snapshots immaterial:

- AssetLockChangeSet::merge skips a non-Consumed entry over a Consumed
  one (guards intra-batch folds);
- the rs-platform-wallet-storage upsert adds a WHERE guard rejecting
  non-consumed over consumed;
- swift-sdk persistAssetLocks skips a non-4 snapshot over a statusRaw-4
  row.

Tests: the exact adversarial interleaving through the real sqlite
persister (Broadcast → Consumed → stale RecoveredFromChain stays
Consumed; Consumed still lands over RecoveredFromChain), plus a merge
unit test covering both directions and the legitimate non-terminal LWW.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.12%. Comparing base (71402fa) to head (68723cd).
⚠️ Report is 2 commits behind head on v4.2-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4347      +/-   ##
============================================
- Coverage     87.78%   86.12%   -1.66%     
============================================
  Files          2677     2704      +27     
  Lines        342433   345265    +2832     
============================================
- Hits         300594   297363    -3231     
- Misses        41839    47902    +6063     
Components Coverage Δ
dpp 88.86% <ø> (ø)
drive 86.25% <ø> (ø)
drive-abci 89.66% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.88% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 48.02% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Its tests run in the wallet fast-path workflow (tests-rs-wallet.yml),
which intentionally omits coverage upload — so on wallet-scoped PRs
codecov receives no data for this crate and codecov/patch fails
spuriously on any change to it (observed on #4347: 14 "missing" lines
that the sqlite roundtrip suite in fact exercises). Mirrors the
existing rs-platform-wallet/src ignore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.

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 `@packages/rs-platform-wallet/src/changeset/changeset.rs`:
- Around line 969-979: Preserve terminal Consumed asset locks across merges and
persistence: in AssetLockChangeSet::merge at
packages/rs-platform-wallet/src/changeset/changeset.rs:969-979, remove
tombstones for effective Consumed entries and clear older tombstones when a
newer Consumed entry arrives; update the delete statement at
packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs:53-54 to
avoid deleting rows whose stored status is consumed; add the
Consumed-then-removed roundtrip assertion at
packages/rs-platform-wallet-storage/tests/sqlite_persist_roundtrip.rs:586-620;
and in PlatformWalletPersistenceHandler at
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:260-272,
skip removal when PersistentAssetLock.statusRaw is 4.
🪄 Autofix

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: 2ff63c42-d6a2-4231-b990-3939d3f657ad

📥 Commits

Reviewing files that changed from the base of the PR and between c8bdd4b and c97da6f.

📒 Files selected for processing (5)
  • .codecov.yml
  • packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs
  • packages/rs-platform-wallet-storage/tests/sqlite_persist_roundtrip.rs
  • packages/rs-platform-wallet/src/changeset/changeset.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift

Comment thread packages/rs-platform-wallet/src/changeset/changeset.rs Outdated
Review follow-up: the Consumed-is-terminal guards covered stale
upserts but not stale `removed` tombstones — merge could retain a
Consumed entry alongside a tombstone, and both stores apply upserts
before removals, so the delete would win. The only removal emitter
(untrack_asset_lock) fires exclusively for Built rows whose broadcast
was definitively rejected, so a Consumed/removed pair for one outpoint
has no legitimate producer; this is defense in depth matching the
upsert guard, and consistent with Consumed rows being deliberately
retained for historical lookup.

- merge: a Consumed entry clears an earlier-folded tombstone, and an
  incoming tombstone is dropped when the effective entry is Consumed;
- sqlite delete gains `AND status != 'consumed'`;
- swift persistAssetLocks skips deleting a statusRaw-4 row;
- tests: merge covers all three tombstone directions, and the sqlite
  interleaving test adds stale-removal-after-Consumed (row survives)
  plus the legitimate rejected-Built removal (row deletes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 23a0dc3 into v4.2-dev Aug 9, 2026
20 checks passed
@QuantumExplorer
QuantumExplorer deleted the fix/asset-lock-restore-finality branch August 9, 2026 08:39
QuantumExplorer added a commit to dashpay/dashwallet-ios that referenced this pull request Aug 9, 2026
…e pin

The "Platform release pin" bullet still named v4.1-dev and the long-gone
local/tx-decode-plus branch. State how pinning actually works (the
../platform sibling checkout's branch, consumed by path) and the real
release floor: v4.2-dev at or past dashpay/platform#4347, without which
restored wallets render every historical funding tx as a pending
transfer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bfoss765 added a commit to bfoss765/platform that referenced this pull request Aug 9, 2026
…eam asset-lock substrate: dashpay#4342 restore-time reconstruction, dashpay#4346 proof-blob validation, dashpay#4347 RecoveredFromChain finalization)

Ports three merged upstream commits that rework the asset-lock substrate,
plus the call-site adaptation qa5's richer 7-arg builder signature needs.
Verified green on this branch: platform-wallet 635 passed, 0 failed.

dashpay#4342's only conflict was changeset/core_bridge.rs, where qa5 carried the
watermark-fault logic inline and upstream had extracted it into
commit_batch + BatchDiagnostics. Resolved toward upstream: it preserves
the freeze guard, the is_empty_no_records skip and the SYNC WATERMARK
FROZEN marker, and additionally fixes an accounting bug qa5 had — a
height was counted persisted before store() rather than only in the Ok
arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants