Skip to content

staticaddr: map loop-in deposits to the swap that spent them - #1251

Open
claravanstaden wants to merge 4 commits into
lightninglabs:masterfrom
claravanstaden:static-reclaim-swap-hash
Open

claravanstaden wants to merge 4 commits into
lightninglabs:masterfrom
claravanstaden:static-reclaim-swap-hash

Conversation

@claravanstaden

@claravanstaden claravanstaden commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

A static loop-in deposit can stay mapped in deposits.swap_hash to a swap that didn't spend it:

What happened Mapped to Should be
Swap A isn't paid before its payment deadline, so the client cancels its invoice and gives the deposits back. Swap B selects them, then the htlc tx A already signed confirms, and A sweeps them back through the htlc timeout path B A

Impact: this needs the server to publish A's htlc tx after A's payment deadline, and other swaps to select all of A's deposits in between, so it's rare. When it happens, the deposits are listed under the other swaps, and A's on-chain cost (#1248) is missing. A still sweeps its htlc timeout path: after a restart it has no deposits mapped to it, so it uses the deposits it signed its htlc tx for.

Fixes:

  • From now on: when A's htlc confirms and A locks the deposits for its timeout sweep, it maps them back to itself. After a restart A first adds the deposits B selected, in the order A signed them. A deposit B already completed with stays with B.
  • Existing deposits: a one-time startup repair (MigrateRepairHtlcTimeoutDepositSwapHash) maps each deposit on the htlc timeout path to the swap whose htlc tx spent it. If lnd can't list the wallet txs, loopd still starts and the repair runs again at the next start.

Both only remap if the tx that spent the deposits spends exactly the deposits the swap signed its htlc tx for: the confirmed tx at runtime, the confirmed lnd wallet tx in the repair, which must also pay to the swap's htlc, since two swaps can select the same deposits. The htlc confirmation is registered by script, so a swap's state alone isn't proof. If it can't tell, the deposit stays as it is.

Not covered:

  • If other swaps took only some of A's deposits, the rest are back in Deposited when A's htlc tx spends them, and the deposit manager drops them from its active deposits. A then can't lock any of its deposits, retries forever and never sweeps, as on master, so the funds stay in A's htlc until someone sweeps them by hand. A restart doesn't help. Separate follow-up.
  • Looped-in deposits that the old migration linked to a failed swap: staticaddr: repair the swap hashes of looped-in deposits #1253.
  • Another tx paying to the htlc script still locks the deposits, as on master: separate follow-up.

Tests

TestMonitorInvoiceAndHtlcTxRemapsReclaimedDeposits runs the takeover with the real store (it fails on master) and checks that another tx paying to the htlc script doesn't move the deposits. TestMonitorInvoiceAndHtlcTxRemapsAfterLegacyFallback runs it through the legacy confirmation fallback with a deposit manager that hands out store copies, like the real one. TestMonitorInvoiceAndHtlcTxMapsOnlyLockedDeposits, TestMonitorInvoiceAndHtlcTxRetriesFailedMapping and TestMonitorInvoiceAndHtlcTxNilDeposit cover deposits B completed with, already locked deposits, a failed mapping and a nil deposit. TestRepairHtlcTimeoutDepositSwapHash covers the repair, including a swap still sweeping, another tx paying to the htlc script, an unconfirmed conflicting tx, a spending tx the wallet doesn't know, two swaps that selected the same deposit, unmapped deposits, and the on-chain cost before and after. TestRepairHtlcTimeoutDepositSwapHashWalletError checks that a failed wallet listing doesn't stop loopd and that the repair runs again. TestMonitorInvoiceAndHtlcTxAddsDepositsAfterRestart covers loopd stopping between locking and mapping, with the missing deposit signed first and last, and checks the deposits are in signed order.

Pull Request Checklist

  • Add an entry to docs/release-notes/release-notes-next.md, or apply the
    no-changelog label (required by CI)

@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch from 9e46c7a to 98b99ac Compare October 15, 2026 05:44
@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch 2 times, most recently from 225407c to a8ace71 Compare October 19, 2026 08:01
@claravanstaden claravanstaden changed the title staticaddr: map reclaimed loop-in deposits back to their swap staticaddr: map loop-in deposits to the swap that spent them Oct 19, 2026
@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch from a8ace71 to b22d610 Compare October 19, 2026 09:51
@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch 3 times, most recently from 93aba9f to 0148cf0 Compare October 19, 2026 17:41
@claravanstaden
claravanstaden marked this pull request as ready for review October 20, 2026 07:52
@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch 6 times, most recently from d79da7b to bd8f66b Compare October 22, 2026 06:43
@litbot-9000

Copy link
Copy Markdown
Collaborator

@bhandras: review reminder
@hieblmi: review reminder

@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch from bd8f66b to 9eae8ff Compare October 30, 2026 07:31
If a static loop-in's payment deadline passes before its htlc confirms,
it gives its deposits back, and another swap can select them, which
maps them to that swap. If the first swap's htlc confirms after all, it
sweeps them through its htlc timeout path, but they stay mapped to the
other swap.

Map the deposits to the swap once they are locked for its htlc timeout
sweep, and again if they already were, in case loopd stopped in
between. A deposit another swap completed with can't be locked and
stays with that swap. Any tx paying to the htlc script confirms it, so
only map them if the confirmed tx spends exactly the deposits the swap
signed its htlc tx for.

After a restart the swap only loads the deposits still mapped to it, so
replace them with all signed deposits, in signed order, which the htlc
timeout sweep needs. If any is no longer active, none are added.

A nil deposit now fails the transition. The loop-in store interface
gains the existing BatchMapDepositsToSwapHashes.
A swap whose payment deadline passed gave its deposits back before its
htlc confirmed after all, which left them mapped to the swap that
selected them next. The previous commit maps them back when that
happens from now on, but deposits that it already happened to stay
mapped to the other swap. The swap that spent them reports no on-chain
cost for them, and if the other swap took only some of its deposits, it
loads only the rest after a restart, so it can't sweep its htlc timeout
path.

Add a migration that maps each deposit on the htlc timeout path to the
swap whose htlc tx spent it: a swap on its own htlc timeout path whose
htlc tx is the confirmed wallet tx that spent the deposit. That tx must
spend exactly the deposits the swap signed its htlc tx for and pay to
the swap's htlc, since the swap's state alone isn't proof and two swaps
can select the same deposits. A deposit stays as it is if the wallet
doesn't know that tx or no swap qualifies. If lnd can't list the wallet
txs, the repair is skipped so that loopd still starts, and it runs
again at the next start.

A swap that still waits for its htlc timeout isn't covered: its htlc
timeout sweep loads only the deposits mapped to it, which is fixed
separately.

The repair has its own migration id, and the lookup of the swaps by the
outpoints they selected and the final write are separate helpers, so
that other repairs can reuse them. The swap state check is shared with
the on-chain cost of a swap.
Run the reclaimed deposit swap hash repair once, after the deposit swap
hash migration. It looks up the txs that spent the deposits in the lnd
wallet.
@claravanstaden
claravanstaden force-pushed the static-reclaim-swap-hash branch from 9eae8ff to 1bd7d35 Compare November 5, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants