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):
- 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.
- 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
- 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).
- 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).
- Return
Err(WalletNotFound) from both bare-return arms.
Cross-references
🤖 Co-authored by Claudius the Magnificent AI Agent
Summary
AssetLockManager::recover_asset_lock_blockingreturns()and discards every failure it can encounter. Two distinct silent-failure classes exist onv4.1-dev(verified at259ae1d105):tracked_asset_locksmap, the changeset flush goes throughqueue_asset_lock_changeset, which logs a persisterstoreerror 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.wallet_idis not (or not yet) present in theWalletManager(init ordering, orphaned id), both phase 1 and phase 3 bail with a barereturn;. 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 inresolve_status_with_in_memory; that one is a documented policy decision, not part of this report.)Affected crate + location
packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs:37-121(recover_asset_lock_blocking; silentreturns at lines 53-56 and 97-100)packages/rs-platform-wallet/src/wallet/asset_lock/manager.rs:92-108(queue_asset_lock_changesetlogs-and-drops thestoreerror)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 inregister_wallet(registration changeset store error → roll back insert + returnErr, cf. #3659 / Found-017).Reproduction
Crate-level unit test (no network, in-memory wallet manager, failing persister):
found_013_recover_asset_lock_blocking_swallows_persist_error(in
packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs, branchrepro/pr3549-platform-a)Expected vs Actual
Result) and/or roll back the in-memory insert, mirroringregister_wallet's fail-closed contract. A missing wallet must surface as a typed error (WalletNotFound).storeerror is logged aterrorand 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):
Suggested fix direction
Result<(), PlatformWalletError>(the blocking FFI/evo-tool call sites can surface or log it — but the decision moves to the caller).queue_asset_lock_changesetfailure inside recovery, roll back thetracked_asset_locksinsert before returning the error (fail closed), or makequeue_asset_lock_changesetreturn theResultand let call sites choose (audit:resume_asset_lockand the funding flows drop it via the same helper today).Err(WalletNotFound)from both bare-returnarms.Cross-references
wait_for_proofandrecover_asset_lock_blockinghard-code BIP-44 — asset-lock proof flow misses CoinJoin / legacy BIP-32 funding #3642 (Found-012) covers a different bug in the same function family (BIP-44-only account lookup) — the two are independent; fixing one does not fix the other.found_013_recover_asset_lock_silent_failure.rs, scaffold).🤖 Co-authored by Claudius the Magnificent AI Agent