Skip to content

loopd: check depth, deadline and amount before a recovery sweep - #1290

Draft
claravanstaden wants to merge 4 commits into
lightninglabs:masterfrom
claravanstaden:loopout-recovery-htlc-confs
Draft

claravanstaden wants to merge 4 commits into
lightninglabs:masterfrom
claravanstaden:loopout-recovery-htlc-confs

Conversation

@claravanstaden

@claravanstaden claravanstaden commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

The automatic recovery worker sweeps a loop out HTLC through the success path, which reveals the preimage. It skipped three checks the swap itself does before revealing it:

  • It waited for one confirmation of the HTLC, also when the swap stored a higher htlc_confs.
  • It swept at any height, including right before or after the HTLC expiry.
  • It swept an HTLC of any value, including one that holds less than the swap amount.

Before and after

Before

  1. A user starts a loop out with htlc_confs set to 6.
  2. The server sends an htlc confirmed notification, and the recovery worker sweeps the HTLC with sweepHtlc.
  3. The sweep waits for one confirmation, so the preimage is revealed after 1 confirmation instead of 6.
  4. If the notification arrives late, the preimage is revealed close to or after the HTLC expiry, when the server can take the HTLC back through the timeout path and still settle the swap payment.
  5. If the server funded the HTLC with less than the swap amount, the sweep still reveals the preimage, and the server can settle the full swap payment.

After

  1. A user starts a loop out with htlc_confs set to 6.
  2. The server sends an htlc confirmed notification, and the recovery worker calls sweepHtlc, which waits for the swap's stored 6 confirmations.
  3. After the wait, the worker checks the HTLC value and reads the block height.
  4. If the HTLC holds less than the swap amount, or 20 blocks or fewer are left before the HTLC expires, it doesn't sweep. A swap that already revealed its preimage is swept in any case.
  5. Otherwise the sweep reveals the preimage after 6 confirmations, the same as the normal swap flow.

Fix

Three commits:

  • Confirmation depth: when the sweep uses a stored swap, register for the swap's stored HtlcConfirmations instead of one. Stateless recovery has no stored value and still waits for one confirmation. This also applies to loop out sweephtlc in database mode, which now waits for the same depth as the normal swap flow.
  • Reveal deadline: the recovery worker reads the block height after the confirmation wait and refuses the sweep once the height reaches CltvExpiry - MinLoopOutPreimageRevealDelta (20 blocks), the bound the swap applies right before it reveals the preimage. The worker needs the height lookup and doesn't sweep without it.
  • HTLC value: the recovery worker refuses the sweep when the HTLC holds less than the swap's AmountRequested, the check the swap does before it reveals the preimage.

A swap that is already in StatePreimageRevealed is still swept, as in the normal flow. Manual sweeps (loop out sweephtlc) get the confirmation depth but not the deadline or value checks.

Tests

  • TestHtlcConfirmedRecoveryManagerHonorsHtlcConfirmations: recovery sweeps for swaps with a stored depth of 0, 1, 2 and 6 register for 1, 1, 2 and 6 confirmations, then sign and publish. The deeper cases fail on master (registers for one).
  • TestSweepHtlcStateless: a stateless sweep still registers for one confirmation.
  • TestHtlcConfirmedRecoveryManagerRevealDeadline: the worker sweeps one block before the deadline, and doesn't sweep at the deadline, after the expiry, for a timed out swap, when the height lookup fails or when it has no height lookup. A swap that already revealed its preimage is swept at the deadline. The refusing cases fail on master.
  • TestHtlcConfirmedRecoveryManagerChecksHtlcValue: the worker sweeps an HTLC that holds the swap amount or more, and doesn't sweep one that is one sat short or holds half of it. A swap that already revealed its preimage is swept anyway. The refusing cases fail on master.

Known limits

  • The recovery worker handles one notification at a time, so it is busy with a swap until its HTLC reaches the stored depth.
  • loop out sweephtlc in database mode blocks until that depth too. The CLI help says so.
  • The deadline and value checks only apply to the automatic worker. loop out sweephtlc can still sweep at any height and any value.
  • The worker doesn't record that its sweep revealed the preimage. If its sweep is published but doesn't confirm, a later notification after the deadline is refused, so the worker can't bump the fee. The manual sweep still works.
  • The sweep is still published once at the fee rate from the notification, so a sweep published shortly before the deadline at a low fee rate may not confirm in time.
  • The height comes from lnd, so a lagging lnd reports an old height, as in the normal swap flow.

@claravanstaden claravanstaden self-assigned this Nov 12, 2026
@claravanstaden
claravanstaden force-pushed the loopout-recovery-htlc-confs branch 2 times, most recently from ded0733 to cf4fe89 Compare November 13, 2026 13:29
sweepHtlc always waited for one confirmation of the HTLC before it
signed and published the success path sweep, which reveals the
preimage. The automatic recovery worker reuses that helper, so a loop
out that stored a higher htlc_confs value had its preimage revealed
after a single confirmation, and a reorg of that block could cost the
swap amount.

When the sweep uses a stored swap, wait for the swap's stored number of
confirmations instead. Stateless recovery has no stored value and keeps
waiting for one confirmation.
The recovery worker sweeps a loop out HTLC through the success path,
which reveals the preimage. It did so at any height, also when the HTLC
was about to expire or had already expired. The server can then take
the HTLC back through its timeout path and still settle the swap
payment with the preimage. Waiting for the stored htlc_confs moves the
sweep closer to the expiry, which makes this easier to reach.

Read the block height after the confirmation wait and refuse the sweep
once no more than MinLoopOutPreimageRevealDelta blocks are left before
the expiry, the bound the swap itself applies right before it reveals
the preimage. A swap that already revealed its preimage is still swept.
The worker needs the height lookup and doesn't sweep without it. Manual
sweeps are unchanged.
The recovery worker swept any output that paid to a swap's HTLC script,
whatever its value. The sweep reveals the preimage, so a server could
fund the HTLC with a fraction of the swap amount, notify the client,
and settle the full swap payment with the revealed preimage.

Refuse the sweep when the HTLC holds less than the swap's requested
amount, the check the swap itself does before it reveals the preimage.
A swap that already revealed its preimage is still swept. Manual sweeps
are unchanged.
@claravanstaden
claravanstaden force-pushed the loopout-recovery-htlc-confs branch from cf4fe89 to a1f8a63 Compare November 13, 2026 17:40
@claravanstaden claravanstaden changed the title loopd: wait for stored htlc confs before sweeping a loop out loopd: check depth, deadline and amount before a recovery sweep Nov 13, 2026
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