Skip to content

loopd: leave running loop outs to their own htlc sweep - #1294

Draft
claravanstaden wants to merge 3 commits into
lightninglabs:masterfrom
claravanstaden:loopout-recovery-preimage-state
Draft

claravanstaden wants to merge 3 commits into
lightninglabs:masterfrom
claravanstaden:loopout-recovery-preimage-state

Conversation

@claravanstaden

@claravanstaden claravanstaden commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

The htlc confirmed recovery worker swept a loop out HTLC directly even when the swap was still running in loopd. That sweep reveals the preimage, but nothing recorded the reveal, so the running swap, or the same swap resumed after a restart, kept treating the preimage as secret and could give up on the swap near its expiry while the preimage was already public.

Before and after

Before

  1. A loop out is running in loopd and its HTLC confirms.
  2. The server sends an htlc confirmed notification, and the recovery worker sweeps the HTLC directly, which reveals the preimage.
  3. Nothing records that the preimage is public.
  4. The running swap, or the same swap after a restart, still treats the preimage as secret and can give up on the swap near its expiry.

After

  1. A loop out is running in loopd and its HTLC confirms.
  2. The server sends an htlc confirmed notification. The worker finds the swap by its HTLC address, sees that it is running, and leaves it alone.
  3. The running swap handles its HTLC itself: it records the preimage reveal and sweeps through the sweep batcher, or it has decided not to reveal the preimage.
  4. Swaps that aren't running still get the direct sweep.

Fix

The executor now tracks which loop outs are running, and Client.LoopOutRunning exposes that after pending swaps have been resumed. The worker finds the swap by its HTLC address (not the notified hash) and skips the direct sweep when the swap is running. The HTLC is left to the running swap, which records the preimage reveal and sweeps through the sweep batcher, or has decided not to reveal the preimage. The running check is required: a worker without it, or one whose check fails, never sweeps. Swaps that are not running still get the direct sweep as before.

Tests

  • TestLoopOutRunning: a loop out counts as running while it executes and stops counting after it finishes.
  • TestLoopOutRunningWaitsForResume and TestLoopOutRunningPendingSwap: the check blocks until the executor is ready and the pending swaps are resumed, and a pending swap counts as running right after start.
  • TestExecutorLoopOutRunningHandover: a swap counts as running during the handover to the executor, and a canceled handover clears the mark.
  • TestHtlcConfirmedRecoveryManagerRunningCheckFailClosed: no sweep when the running check is missing or fails.
  • TestHtlcConfirmedRecoveryManagerSkipsRunningSwap: no sweep for a running swap, even when the notification names a different swap hash. Fails without the fix.
  • TestHtlcConfirmedRecoveryManagerSweepsStoppedSwap: a swap that is not running still gets the direct sweep.

Known limits

  • A loop out whose execution stopped with an error is not running but is still pending in the database. It still gets the direct sweep, and nothing records the reveal for it, so a restart resumes it as unrevealed.
  • A notification that arrives while the swap is running is dropped. If the swap then stops without sweeping, only a later notification triggers the direct sweep.
  • The skip covers every running swap, also one that already revealed its preimage and is waiting for its own sweep to confirm.
  • A new swap is stored shortly before it is marked as running.

@claravanstaden claravanstaden self-assigned this Nov 12, 2026
Add Client.LoopOutRunning so other daemon parts can tell whether a loop
out is executing in this client. The executor marks a loop out as
running before it hands the swap over and clears the mark when the swap
goroutine returns. The method waits until pending swaps have been
resumed, so a swap that is about to resume also counts as running.

The htlc recovery worker uses this in the next commit to leave the HTLC
of a running swap alone.
The htlc confirmed recovery worker swept a loop out HTLC directly, even
when the swap was still running in this daemon. That sweep reveals the
preimage, but nothing recorded the reveal. The running swap kept its
unrevealed state, so near the expiry it, or the same swap resumed after
a restart, could give up on the swap while the preimage was already
public.

Skip the direct sweep when the swap is running. The HTLC is left to the
running swap, which records the preimage reveal and sweeps through the
sweep batcher, or has decided not to reveal the preimage. The swap is
found by its HTLC address, not by the notified hash. Swaps that are not
running still get the direct sweep.

The running check is a required dependency of the worker. Without it,
or when it fails, the worker does not sweep.
@claravanstaden
claravanstaden force-pushed the loopout-recovery-preimage-state branch from 1298ecb to 25c5744 Compare November 13, 2026 09:37
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.

1 participant