darepod: unlock wallet VTXOs on OOR cleanup timeout - #707
Conversation
When a detached OOR start future never completes, the cleanup waiter's Await returns because cleanupCtx hit its submittedOORCleanupTimeout deadline, leaving cleanupCtx expired. The timeout branch then passed that same expired cleanupCtx to unlockSelectedVTXOsBestEffort, which Tells the wallet actor. ChannelMailbox.Send rejects an already-expired context before enqueue, so the unlock was silently dropped and the wallet-selected VTXOs stayed pinned in SpendingState, potentially indefinitely. In the timeout branch we now derive a fresh bounded context from the detached base via context.WithoutCancel, so the unlock Tell actually reaches the wallet mailbox. The completion and actor-failure branches are unaffected, and the custom-input release path was already context-independent. The existing TestSubmittedOORCleanupTimeoutReleasesCustomInput passes nil for locked and only checks the in-memory custom-input callback, which is why the gap went unnoticed. Add TestSubmittedOORCleanupTimeoutReleasesSelectedVTXOs, which drives the timeout path with a non-nil locked set against a recording wallet actor and asserts an UnlockVTXOsRequest is delivered.
|
@claude review this |
There was a problem hiding this comment.
Code Review
This pull request addresses an issue where wallet-selected VTXOs could remain locked if the OOR cleanup waiter timed out. When a timeout occurs, the original context is expired, causing the wallet actor to reject the unlock message. To fix this, a fresh context with a 30-second timeout is now derived and used for the unlock operation. A corresponding unit test has been added to verify this behavior. I have no additional feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Claude finished @Roasbeef's task in 1m 29s —— View job Review:
|
The bug
cleanupSubmittedOORStartWithTimeoutindarepod/rpc_server.gobuilds adetached, bounded cleanup context:
and then
future.Await(cleanupCtx). When the detached OOR start future nevercompletes,
Awaitreturns becausecleanupCtxhit its deadline, socleanupCtxis now expired. The very next statement passed that same expiredcleanupCtxtounlockSelectedVTXOsBestEffort, which flows towalletRef.Tell(cleanupCtx, &wallet.UnlockVTXOsRequest{...}).ChannelMailbox.Sendrejects an already-expired context before enqueue, so theunlock was silently dropped and the wallet-selected VTXOs stayed pinned in
SpendingState— potentially indefinitely.The custom-input release path was unaffected (its callback is a pure in-memory
map delete, context-independent). Only the wallet-selected VTXO unlock at
timeout was broken.
The fix
In the timeout branch, derive a fresh bounded context from the detached base
(
context.WithTimeout(context.WithoutCancel(ctx), submittedOORUnlockTimeout),30s, with its own
defer cancel()) and use that for the unlock. Thecompletion and actor-failure branches keep using the still-live
cleanupCtx,and the custom-input release still happens in every branch. A name-prefixed
comment explains why the fresh context is required.
The test
TestSubmittedOORCleanupTimeoutReleasesCustomInputpassednilforlockedand only checked the in-memory custom-input callback, which is why the gap went
unnoticed. This adds
TestSubmittedOORCleanupTimeoutReleasesSelectedVTXOs,which drives the timeout path with a non-nil
lockedset against a recordingwallet actor (
sendOORTestWallet) and asserts anUnlockVTXOsRequestisactually delivered on timeout. Verified that the new test fails on the
unpatched code (VTXOs never unlocked) and passes with the fix. The existing
test is unchanged.
Fixes #387