Skip to content

fix(platform-wallet): preserve reported-consumed asset-lock recovery - #4357

Open
llbartekll wants to merge 7 commits into
v4.2-devfrom
codex/fix-stale-shielded-asset-lock
Open

fix(platform-wallet): preserve reported-consumed asset-lock recovery#4357
llbartekll wants to merge 7 commits into
v4.2-devfrom
codex/fix-stale-shielded-asset-lock

Conversation

@llbartekll

@llbartekll llbartekll commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • recognize the structured already-consumed asset-lock consensus error in both SDK envelopes without parsing display text
  • never treat that unauthenticated DAPI rejection as terminal proof of consumption
  • require/obtain a ChainLock proof, retain the proof, and durably persist RecoveredFromChain (Core-final, Platform consumption unknown)
  • return the existing typed AssetLockAlreadyConsumed result through FFI while leaving unrelated errors unchanged
  • roll back the in-memory state and surface a persistence error if the host store rejects the reconciliation

Tests

  • cargo test -p platform-wallet --features shielded wallet::shielded::fund_from_asset_lock::tests
  • cargo test -p platform-wallet --features shielded asset_lock_already_consumed_tests
  • cargo test -p platform-wallet-ffi --features shielded map_asset_lock_resume_result_preserves_already_consumed_code_only
  • cargo test -p platform-wallet-ffi --features shielded asset_lock_recovery_failures_map_to_stable_codes
  • cargo fmt --all -- --check
  • cargo clippy -p platform-wallet --features shielded --lib --tests -- -D warnings

The reconciliation helper is covered directly for a matching outpoint, an unrelated/mismatched error, and host persistence failure with rollback.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of already-consumed asset locks during shielded sends and retries.
    • Preserved matching asset-lock errors while surfacing unrelated failures normally.
    • Recovered matching locks using ChainLock proofs when Platform-side consumption status is unknown.
    • Added reliable persistence and rollback when updating recovered asset-lock state.
    • Improved consistency across immediate and deferred persistence modes.
  • Documentation

    • Clarified asset-lock recovery states, proof retention, and Platform finality limitations.
    • Updated error descriptions to explain unconfirmed Platform completion during one-shot asset-lock funding.

@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 10, 2026
@thepastaclaw

thepastaclaw commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit b81e364)

@coderabbitai

coderabbitai Bot commented Aug 10, 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 wallet detects exact consumed asset-lock errors, records matching locks as RecoveredFromChain, persists recovery state, and preserves typed errors through shielded funding and FFI resume flows.

Changes

Asset-lock recovery

Layer / File(s) Summary
Consumed asset-lock error detection
packages/rs-platform-wallet/src/error.rs
Matches exact outpoints in protocol and broadcast consensus errors. Tests cover supported wrappers, mismatches, unrelated errors, and display-message lookalikes.
Consumption-unknown state persistence
packages/rs-platform-wallet/src/changeset/traits.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs, packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs, packages/rs-platform-wallet/src/wallet/persister.rs, packages/rs-platform-wallet-storage/src/sqlite/persister.rs
Adds inline-commit capability reporting and ChainLock-backed RecoveredFromChain updates with synchronous persistence and conditional rollback.
Shielded submission reconciliation
packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
Reconciles matching consumed errors, promotes proofs when required, preserves nonterminal state, and tests successful, mismatched, unrelated, and persistence-failure cases.
FFI persistence and error mapping
packages/rs-platform-wallet-ffi/src/persistence.rs, packages/rs-platform-wallet-ffi/src/shielded_send.rs, packages/rs-platform-wallet-ffi/src/error.rs
Runs store notifications before the end callback, rolls back failed rounds, reports inline commits, and preserves AssetLockAlreadyConsumed across funding and resume FFI boundaries.

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

Sequence Diagram(s)

sequenceDiagram
  participant ShieldedFunding
  participant ErrorMatcher
  participant AssetLockTracker
  participant PersistenceBackend
  ShieldedFunding->>ErrorMatcher: match submitted asset-lock outpoint
  ErrorMatcher-->>ShieldedFunding: matching AssetLockAlreadyConsumed
  ShieldedFunding->>AssetLockTracker: mark ChainLock-backed recovery
  AssetLockTracker->>PersistenceBackend: store recovery changeset
  PersistenceBackend-->>AssetLockTracker: commit or persistence error
  AssetLockTracker-->>ShieldedFunding: recovery result
Loading

Possibly related issues

  • dashpay/dash-evo-tool#930: Covers asset-lock recovery after InstantSend or Platform completion issues, which aligns with the recovery state and proof-retention changes.

Possibly related PRs

Suggested reviewers: lklimek, shumkov, 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 and concisely describes the main change: preserving recovery for reported-consumed asset locks in the platform wallet.
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 codex/fix-stale-shielded-asset-lock

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

@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 structured error matching and FFI code preservation are correct, but the reconciliation path trusts an unverified DAPI rejection and can permanently tombstone an asset lock that remains unspent. Persistence callback failures are also hidden from this new path, and the state-changing branch lacks direct orchestration coverage, so changes are required before merge.
Source: reviewer backends: gpt-5.6-sol (general), gpt-5.6-sol (rust-quality), gpt-5.6-sol (ffi-engineer); final verifier backend: gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus 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), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 2 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 `packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:439-447: Do not terminally consume a lock from an unauthenticated rejection
  The SDK handles a wait-stream error before verifying the response proof or quorum signature, and `submit_with_cl_height_retry` explicitly documents that consensus-error responses have no client-side proof or DAPI-quorum check. A malicious or malfunctioning endpoint can therefore inspect the submitted transition's outpoint and fabricate the matching already-consumed error; comparing the error to that outpoint does not authenticate the verdict. This branch then marks the lock `Consumed`, clears its proof, persists the terminal tombstone, and causes future resumes to reject it locally, potentially stranding an asset lock that Platform never consumed. Preserve the typed error if needed, but do not create terminal wallet state from this response alone; terminal reconciliation requires authenticated state evidence, or the wallet must retain a nonterminal/retryable reported-consumed state.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:445-447: Do not acknowledge reconciliation after host persistence fails
  `consume_asset_lock` mutates the in-memory entry and calls `queue_asset_lock_changeset`, but that method logs and discards every `WalletPersister::store` error. The FFI persistence backend returns an error when an asset-lock callback or changeset commit fails, so the `.await?` here cannot observe a host rollback and this branch still reports the typed already-consumed result. After restart, the host can rehydrate the stale ChainLocked/Broadcast row even though the operation claimed to have reconciled it. Propagate the persistence failure before returning `AssetLockAlreadyConsumed`, and restore the previous in-memory lock state if the durable update fails.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:437-457: Test the consumed-error reconciliation branch directly
  The added tests separately exercise the pure error matcher, `consume_asset_lock`, and FFI mapping, but none drives an `Err(dash_sdk::Error)` through this match and verifies the combined behavior. An integration mistake here—using the wrong outpoint, omitting consumption, or wrapping the result back into `PlatformWalletError::Sdk`—would leave every added test passing. Add a focused orchestration test, or extract this branch into a testable helper, and assert that a matching error updates the submitted lock and returns the typed wallet error while unrelated or mismatched-outpoint errors do not mutate persisted status.

Comment thread packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs Outdated
Comment thread packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs Outdated
@llbartekll
llbartekll force-pushed the codex/fix-stale-shielded-asset-lock branch from 70a9297 to 8ac6e36 Compare August 10, 2026 16:23
@llbartekll llbartekll changed the title fix(platform-wallet): reconcile consumed shielded asset locks fix(platform-wallet): preserve reported-consumed asset-lock recovery Aug 10, 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: 1

🧹 Nitpick comments (1)
packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs (1)

399-451: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse the promoted ChainLock proof in reconciliation.

The IS→CL arm promotes the proof to chain_proof, but line 446 passes the original proof to reconcile_asset_lock_submit_error. In that path proof is still AssetLockProof::Instant, so the helper calls upgrade_to_chain_lock_proof a second time. With cl_wait == None that call waits without a bound again, even though the promotion already succeeded. Track the effective proof and pass it to reconciliation.

♻️ Proposed refactor
-        let submit_result = match submit_with_cl_height_retry(settings, |s| {
+        let mut effective_proof = proof.clone();
+        let submit_result = match submit_with_cl_height_retry(settings, |s| {
                 self.asset_locks.queue_asset_lock_changeset(cs);
+                effective_proof = chain_proof.clone();
                 submit_with_cl_height_retry(settings, |s| {
                 return reconcile_asset_lock_submit_error(
                     &self.asset_locks,
                     e,
                     &proof_out_point,
-                    &proof,
+                    &effective_proof,
                     cl_wait,
                 )
                 .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 `@packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`
around lines 399 - 451, Track the effective asset-lock proof across the submit
flow, updating it to the promoted chain_proof in the
is_instant_lock_proof_invalid arm. Pass this effective proof instead of the
original proof to reconcile_asset_lock_submit_error, while preserving the
existing proof for non-promotion paths.
🤖 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/tracking.rs`:
- Around line 196-231: After successfully storing the recovery changeset in the
recovery flow around queue_asset_lock_changeset, flush the per-wallet changeset
before returning so RecoveredFromChain is durable. Handle flush failures using
the existing PersistenceErrorKind conventions, including rollback behavior where
required, and propagate the resulting persistence error consistently with the
current store-error path.

---

Nitpick comments:
In `@packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- Around line 399-451: Track the effective asset-lock proof across the submit
flow, updating it to the promoted chain_proof in the
is_instant_lock_proof_invalid arm. Pass this effective proof instead of the
original proof to reconcile_asset_lock_submit_error, while preserving the
existing proof for non-promotion paths.
🪄 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: 5f69af23-c448-415c-8d47-d0b429bae91a

📥 Commits

Reviewing files that changed from the base of the PR and between 70a9297 and 8ac6e36.

📒 Files selected for processing (5)
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/tracked.rs
  • packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/rs-platform-wallet/src/error.rs

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The previous unauthenticated-consumption blocker and both prior suggestions are fixed: matching reports now retain an authenticated ChainLock proof in nonterminal RecoveredFromChain state, immediate store failures roll back the in-memory candidate, and the extracted reconciliation helper has direct coverage. Two in-scope suggestions remain: the helper does not cross the persistence trait's flush durability boundary, and the InstantLock-to-ChainLock retry discards the effective ChainLock proof before reconciliation.
Source: reviewers gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 2 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 `packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:214-217: Flush the recovery marker before acknowledging reconciliation
  `PlatformWalletPersistence::store` is explicitly a buffering operation, while `flush` is the durability boundary. `SqlitePersister` in `FlushMode::Manual`, for example, returns `Ok(())` here after only merging `RecoveredFromChain` into its in-memory accumulator. The caller consequently returns the typed `AssetLockAlreadyConsumed` result even though a process exit can lose the marker or a later flush can fail. Persist this update through `flush()` before acknowledging reconciliation, and route both store and flush failures through rollback handling. The failure path must also account for transient flush failures retaining the candidate in the backend buffer; otherwise a later flush could persist the candidate after the wallet's in-memory entry was rolled back.

In `packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:439-447: Carry the effective ChainLock proof into error reconciliation
  When the initial InstantLock submission is rejected, the fallback obtains a valid `chain_proof`, records it, and uses it for the second submission. If that submission reports the outpoint as already consumed, this branch nevertheless passes the original InstantLock `proof` to reconciliation. The helper then repeats `upgrade_to_chain_lock_proof`, duplicating SPV and persistence work and potentially replacing the intended typed result with `AssetLockProofWait` if the transaction record becomes unavailable between lookups, even though this flow already owns a valid ChainLock proof. Track the effective proof across the retry, update it after promotion, and pass it to `reconcile_asset_lock_submit_error`; add coverage for an already-consumed result from the ChainLock retry.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs Outdated

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The previous durability-boundary and effective-proof findings are fixed, and the recovery path now retains a ChainLock proof in nonterminal state. Two in-scope suggestions remain: a fatal FFI flush callback can make the live wallet disagree with the already-committed host store, and the fresh BIP44 funding entry point still flattens the newly reachable typed already-consumed result.
Source: reviewer backend model gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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 `packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:214-236: Do not roll back after the FFI store has already committed the marker
  A non-transient `flush()` error does not always mean the candidate failed to reach durable storage. `FFIPersister::store` invokes the per-kind callbacks and then `on_changeset_end_fn`; that callback's documented contract commits the host transaction before `store` returns `Ok(())`. The subsequent `FFIPersister::flush` only invokes `on_flush_fn`, and a nonzero result is reported as a fatal error while the already-committed host row cannot be undone. This branch then restores the previous in-memory lock even though the host may already contain `RecoveredFromChain`, leaving the live wallet inconsistent with restart state. Separate store and flush outcomes and align `FFIPersister`'s flush disposition with its actual commit boundary so rollback occurs only when the candidate is known not to be durable; add coverage for a fatal post-store flush failure.

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:1041-1047: Preserve the typed report for fresh asset-lock funding
  The wallet method now reconciles matching already-consumed reports for every funding mode, including `AssetLockFunding::FromWalletBalance`, so this fresh-funding entry point can return `PlatformWalletError::AssetLockAlreadyConsumed`. It still maps every failure to `ErrorWalletOperation`, unlike the resume entry point and CoinJoin entry point. Swift and Kotlin therefore receive the generic wallet-operation error instead of stable code 24 for an initial BIP44-funded submission, losing the consumption-unknown signal that this PR introduced. Preserve the typed variant here while retaining the existing generic mapping for unrelated errors.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs Outdated
@llbartekll

Copy link
Copy Markdown
Contributor Author

Also addressed the review-body-only FFI suggestion in 4b68180: the fresh BIP44 shielded funding entry point now preserves PlatformWalletError::AssetLockAlreadyConsumed as stable FFI code 24, while unrelated errors remain ErrorWalletOperation. The existing resume mapping test and platform-wallet-ffi Clippy pass.

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The PR now preserves authenticated Core finality in a nonterminal recovery state and correctly returns the typed already-consumed result from both fresh and resumed FFI funding. One persistence-phase bug remains around a post-commit FFI store notification, while the public wording and fresh-funding regression coverage need follow-up; no blocking defects were confirmed.
Source: reviewer backend model gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 2 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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 `packages/rs-platform-wallet/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/error.rs:262-266: Do not describe the unauthenticated report as confirmed consumption
  The documentation correctly states that this variant can represent an unauthenticated Platform report, but its rendered message still says the asset lock "has already been consumed." The public FFI documentation for code 24 similarly describes the one-shot output as already consumed. This PR now returns that message and code while deliberately storing `RecoveredFromChain` because Platform-side consumption remains unknown, so logs or host UI guidance can misclassify a retryable, nonterminal recovery state as authenticated completion. Keep the stable variant and FFI discriminant, but describe the result as a Platform-reported consumption conflict whose Platform completion is unconfirmed.

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:1041-1048: Exercise the fresh-funding FFI mapping through the tested helper
  The fresh BIP44 funding entry point now correctly preserves `AssetLockAlreadyConsumed`, but it implements a separate match from the tested resume mapper. The new unit test only calls `map_asset_lock_resume_result`, so it would still pass if this fresh path regressed to the generic `ErrorWalletOperation` mapping—the exact defect fixed by the final commit. Extract a shared asset-lock funding result mapper that accepts the operation-specific context, use it from both entry points, and test that shared mapping for the typed consumed report, unrelated errors, and success.

In `packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:216-242: Do not roll back after the FFI store has already committed the marker
  (existing thread: https://github.com/dashpay/platform/pull/4357#discussion_r3752217867)
  The fatal-flush case from the prior review is fixed, but the same durable-store rollback invariant still fails during `store()` itself. `FFIPersister::store` invokes `on_changeset_end_fn`, whose documented contract commits the host transaction, before invoking `on_store_fn`; if that later notification returns nonzero, `store()` returns a fatal error even though `RecoveredFromChain` is already durable. This branch treats every `store()` error as rollback-safe and restores the previous in-memory lock, leaving the live wallet behind the state that will be loaded after restart. Move the fallible store notification before the commit and incorporate it into `round_success`, or expose a persistence outcome that distinguishes pre-commit failures from post-commit notification failures. Add coverage for a successful end callback followed by a failing `on_store_fn`.

Comment thread packages/rs-platform-wallet/src/error.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/shielded_send.rs Outdated

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't really make sense to me, why are we only fixing asset lock submission errors when they are to sheilded pool, shouldn't we do it in all cases?

@llbartekll

Copy link
Copy Markdown
Contributor Author

Also addressed the carried-forward post-commit on_store_fn finding in 26eb8ba. The fallible notification now runs only after the per-kind phase succeeds and before on_changeset_end_fn, so a nonzero result is included in the round verdict and the host rolls staged writes back rather than reporting an error after commit. The callback contract docs were updated, and a focused test covers both notification failure before commit and skipping the notification for an already-rejected per-kind round. The Rust/FFI boundary re-review found no blockers; focused tests, the 47-test persistence module run, fmt, and Clippy pass.

@llbartekll

Copy link
Copy Markdown
Contributor Author

@QuantumExplorer Good question. The structured consensus-error recognition itself is shared in error.rs (is_asset_lock_already_consumed, including both SDK envelopes and exact-outpoint matching), but this PR intentionally applies the persistent RecoveredFromChain reconciliation only to Type-5 shielded funding. That state is what drives the shielded recovery row/sheet fixed here. Identity registration/top-up and platform-address funding currently have different post-submit recovery contracts; broadening the same state mutation to them would change behavior beyond this stale shielded-transfer issue and needs operation-specific tests/host handling rather than a mechanical catch-all. I would prefer to keep this PR scoped and follow with a shared reconciliation change for all asset-lock consumers. If you want this PR to own that broader behavior, I can expand it explicitly.

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The three prior findings are fixed: bracketed FFI rounds now include the store notification in their rollback verdict, the public error wording states that Platform completion is unconfirmed, and fresh and resumed funding share the tested typed-result mapper. One persistence callback-contract gap remains: the fix changes the phase of a legacy callback while still being unable to guarantee rollback for supported callback configurations without a changeset-end boundary.
Source: reviewer backend model gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 1 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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 `packages/rs-platform-wallet-ffi/src/persistence.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/persistence.rs:2224-2244: Do not use the legacy store notification as the rollback boundary
  Moving `on_store_fn` before `on_changeset_end_fn` makes failures rollback-safe only for hosts that provide an atomic end callback, while changing the observable phase of an established field in the unversioned public `PersistenceCallbacks` vtable. Existing external hosts previously received `on_store_fn` after the end callback had committed the round; they can now emit an irreversible notification while writes are still staged and may subsequently be rolled back. The new ordering also remains unsafe for a supported partially wired vtable with no `on_changeset_end_fn`: a per-kind callback may write immediately, then `on_store_fn` may fail, causing `store()` to claim that the changeset was rolled back and `mark_asset_lock_consumption_unknown` to restore the old in-memory entry even though the host already contains `RecoveredFromChain`. Preserve the legacy post-commit notification phase and add a distinct size/version-negotiated pre-commit callback or persistence outcome. The synchronous recovery path must require an atomic end boundary or otherwise distinguish committed, uncommitted, and indeterminate store failures before rolling back memory.

@llbartekll

Copy link
Copy Markdown
Contributor Author

Addressed in 06265f5. I restored on_store_fn to its legacy post-commit phase instead of using it as a rollback boundary. Its return is now explicitly advisory (a nonzero value is logged, while store() remains successful because host persistence is already committed), and the focused test pins both end-before-store ordering and that rejected per-kind writes never trigger the notification. I also corrected the begin-callback documentation to match its existing fatal behavior. Verified with the 47 persistence tests, cargo fmt, and Clippy with warnings denied.

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The PR correctly retains authenticated ChainLock evidence in nonterminal recovery state, flushes the recovery marker, and preserves the typed FFI result. One in-scope persistence-contract issue remains: supported legacy or partially wired FFI callback tables cannot reliably distinguish durable, rolled-back, and indeterminate asset-lock writes, so reconciliation can still leave live state inconsistent with restart state.
Source: reviewer backend model gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 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 `packages/rs-platform-wallet-ffi/src/persistence.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/persistence.rs:2253-2277: Do not use the legacy store notification as the rollback boundary
  Restoring `on_store_fn` to its legacy post-commit phase fixes the callback-ordering regression, but the synchronous recovery path still assumes `store()` can classify every result as either durable success or rollback-safe failure. That is not true for supported legacy and partially wired callback tables. Without `on_changeset_end_fn`, an asset-lock callback can write directly and then return an error; `store()` reports that the changeset was rolled back, `store_commits_inline()` returns false, and `mark_asset_lock_consumption_unknown` restores only the in-memory entry even though the host may already contain `RecoveredFromChain`. Conversely, an existing legacy host may use `on_store_fn` as its durable-write boundary; its previously propagated failure is now ignored, allowing reconciliation to succeed when that write failed. A table without `on_persist_asset_locks_fn` also silently skips the recovery row while returning success. Require an attested capability covering atomic asset-lock upsert and restart restoration before this reconciliation, or return an outcome that distinguishes committed, rolled-back, and indeterminate writes. Preserve the legacy `on_store_fn` failure contract for callback sets without a separate atomic commit boundary.

Comment thread packages/rs-platform-wallet-ffi/src/persistence.rs
@llbartekll

Copy link
Copy Markdown
Contributor Author

Addressed the new persistence-contract review in b81e364.

The reconciliation path now requires an attested combination of atomic changesets, tracked asset-lock persistence, and wallet restoration before mutating the in-memory status. FFI derives the tracked-lock leg from the concrete callback, Swift/Kotlin declare it because they wire all required callbacks, and SQLite declares only tracked-lock persistence (it still cannot satisfy the full reconciliation contract without wallet restore). Legacy hosts without an atomic end callback also keep the original nonzero on_store_fn failure behavior; post-commit failures remain advisory only when an end callback has already committed the round.

Validation:

  • platform-wallet shielded reconciliation tests: 10 passed
  • persistence capability tests: 2 passed
  • platform-wallet-ffi persistence tests: 48 passed
  • cargo check -p platform-wallet-storage
  • cargo fmt --all -- --check
  • cargo clippy -p platform-wallet -p platform-wallet-ffi --features shielded --lib --tests -- -D warnings
  • Swift source parse passed

The focused Swift test target is unavailable locally because the generated DashSDKFFI module is absent, and the Kotlin Gradle test cannot configure without an Android SDK.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.63%. Comparing base (86f3878) to head (b81e364).
⚠️ Report is 1 commits behind head on v4.2-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4357      +/-   ##
============================================
- Coverage     87.80%   87.63%   -0.18%     
============================================
  Files          2641     2670      +29     
  Lines        336510   339449    +2939     
============================================
+ Hits         295468   297467    +1999     
- Misses        41042    41982     +940     
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.

@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.

Final validation — Codex/Sol only (Phase 2 disabled)

The exact head fixes the prior persistence-contract issue: shielded asset-lock reconciliation now fails before mutating memory unless the backend attests atomic changesets, tracked asset-lock persistence, and wallet restoration, and the FFI backend intersects that declaration with the required concrete callbacks. The recovery marker is stored and flushed before acknowledgment, while legacy store-notification behavior is preserved; no in-scope findings remain. Source: reviewer backend model gpt-5.6-sol (general, security-auditor, rust-quality, ffi-engineer); final verifier backend model gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

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