Skip to content

Found-013: recover_asset_lock_blocking swallows every failure — persist errors leave a memory-only tracked lock, missing wallet returns silently #4028

Description

@Claudius-Maginificent

Summary

AssetLockManager::recover_asset_lock_blocking returns () and discards every failure it can encounter. Two distinct silent-failure classes exist on v4.1-dev (verified at 259ae1d105):

  1. Persist failure swallowed — after inserting the recovered lock into the in-memory tracked_asset_locks map, the changeset flush goes through queue_asset_lock_changeset, which logs a persister store error and drops it. The caller gets () (implicit success) while the lock exists only in memory: it silently disappears on the next restart — the exact situation recovery exists to prevent.
  2. Missing wallet swallowed — if wallet_id is not (or not yet) present in the WalletManager (init ordering, orphaned id), both phase 1 and phase 3 bail with a bare return;. The caller cannot distinguish "recovered", "already tracked", and "nothing happened at all".

(A third class — persister lookup errors during status resolution — is now deliberately degraded-with-error!-log in resolve_status_with_in_memory; that one is a documented policy decision, not part of this report.)

Affected crate + location

Root cause

The function signature is fn recover_asset_lock_blocking(...) -> (): there is no channel for any failure to reach the caller, so every internal error path degenerates to "log (at best) and return". The persist step additionally violates fail-closed: the in-memory insert is committed before the store attempt and is never rolled back when the store fails, so in-memory and persisted state silently diverge. The codebase already established the fail-closed contract for exactly this shape in register_wallet (registration changeset store error → roll back insert + return Err, cf. #3659 / Found-017).

Reproduction

Crate-level unit test (no network, in-memory wallet manager, failing persister):

  • Crate-level repro: found_013_recover_asset_lock_blocking_swallows_persist_error
    (in packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs, branch repro/pr3549-platform-a)
  • Command:
    cargo test -p platform-wallet found_013_recover_asset_lock_blocking_swallows_persist_error
    
  • RED by design: fails today on the final assertion; passes once recovery fails closed on a persist error.

Expected vs Actual

  • Expected: a recovery whose tracked-lock changeset cannot be persisted must not report success — surface the error (return Result) and/or roll back the in-memory insert, mirroring register_wallet's fail-closed contract. A missing wallet must surface as a typed error (WalletNotFound).
  • Actual: store error is logged at error and dropped; the lock stays tracked in memory only and vanishes on restart. Missing wallet returns () with no signal at all.

Severity

Per OWASP-normalized scoring (severity skill):

  • risk: 0.45 (restart-recovery path; persister failures uncommon but real — FFI/SwiftData backends; missing-wallet arm triggered by init ordering)
  • impact: 0.55 (silent loss of asset-lock tracking; locked L1 funds need manual SPV re-derivation to resume; no user-visible signal)
  • scope: 0.35 (asset-lock recovery path of one crate)
  • overall = (0.45+0.55+0.35)/3 = 0.45 → MEDIUM (3)

Suggested fix direction

  1. Change the signature to Result<(), PlatformWalletError> (the blocking FFI/evo-tool call sites can surface or log it — but the decision moves to the caller).
  2. On queue_asset_lock_changeset failure inside recovery, roll back the tracked_asset_locks insert before returning the error (fail closed), or make queue_asset_lock_changeset return the Result and let call sites choose (audit: resume_asset_lock and the funding flows drop it via the same helper today).
  3. Return Err(WalletNotFound) from both bare-return arms.

Cross-references

🤖 Co-authored by Claudius the Magnificent AI Agent

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions