Repository navigation
staticaddr/loopin: confirm only the htlc tx that spent deposits - #1293
Draft
claravanstaden wants to merge 2 commits into
Draft
claravanstaden wants to merge 2 commits into
claravanstaden wants to merge 2 commits into
Conversation
The loop-in monitor registers for the htlc confirmation by pkScript only, so lnd reports the first confirmed tx that pays to the htlc script. The server knows that script and can confirm a dust output to it from its own funds, without spending the deposits and without paying the invoice. The monitor took that as the htlc confirmation and locked the deposits for a timeout sweep of an htlc output that doesn't exist. The deposits had no way out of that state. Now a confirmation only counts if its tx spends exactly the deposits the htlc tx was signed for and pays to the htlc. lnd reports a script match only once, so after a tx that doesn't qualify, the monitor watches the spend of the first deposit and registers the confirmation of the htlc tx that spent it by txid. A missing confirmation is rejected the same way. The htlc tx can spend the deposits before the monitor sees its confirmation, for example if the monitor starts after the htlc expiry. Releasing the deposits then would abandon the htlc. So once the spend watch reports the htlc tx, the htlc timeout waits for its confirmation. And before the timeout releases the deposits, it asks lnd's wallet whether an htlc tx spent them. If none did, the swap times out and the deposits are released, as before. A reorg drops the txid and the note that the deposits are spent. The next match then goes through the same checks, and the swap doesn't wait forever for an htlc tx that is gone, because the timeout asks lnd again. The spend watch reports its own reorgs, since lnd before v0.21 sends no reorg for a confirmation it didn't report yet. After a spend reorg the monitor watches the deposit spend again, so it also finds another signed fee variant of the htlc tx.
Note that a static address loop-in only takes a confirmation of the htlc tx that spent its deposits.
claravanstaden
force-pushed
the
static-loopin-decoy-htlc-conf
branch
from
November 13, 2026 09:37
50d9627 to
686ef37
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The static address loop-in client registers for the htlc confirmation by pkScript only, so lnd reports the first confirmed tx that pays to the htlc script. The server knows that script and can confirm a dust output to it from its own funds, then not pay the invoice. The client took that as the htlc confirmation and locked the deposits for a timeout sweep of an htlc output that doesn't exist, and the deposits had no way out of that state.
Before and after
Before
After
Fix
A confirmation now only counts if its tx spends exactly the swap's deposits and pays to the htlc. A missing confirmation is rejected too. lnd reports a script match only once, so after a tx that doesn't qualify, the monitor watches the spend of the first deposit and registers the confirmation of the htlc tx that spent it by txid. The inputs are compared with the outpoints the htlc tx was signed for.
The htlc tx can spend the deposits before the monitor sees its confirmation, for example when the monitor starts after the htlc expiry. So once the spend watch reports the htlc tx, the htlc timeout waits for its confirmation, and before the timeout releases the deposits it asks lnd's wallet whether an htlc tx spent them. It asks again at every block, so the swap stops waiting if lnd no longer lists the tx. If the htlc tx never spends the deposits, the swap times out and releases them, as before.
A reorg drops the txid, so the next match goes through the same checks. The spend watch has its own reorg channel, because lnd before v0.21 sends no reorg for a confirmation it didn't report yet. After a spend reorg the monitor watches the deposit spend again, so it also finds another signed fee variant of the htlc tx.
Tests
TestMonitorInvoiceAndHtlcTxIgnoresDecoyHtlcConf: a dust decoy and a nil confirmation don't lock the deposits, the htlc tx found from the deposit spend is confirmed by txid and locks them, and a deposit spend by another tx doesn't. Fails on master.TestIsHtlcTx: input set and htlc output checks, including a signed snapshot that differs from the deposit rows.TestMonitorHtlcTimeoutWaitsForSpentDeposits: the htlc timeout waits when the spend watch or lnd's wallet reports the htlc tx, stops waiting when lnd no longer lists it, retries on a lookup error, and releases the deposits when nothing spent them.TestMonitorDropsHtlcTxOnReorg: a spend reorg or confirmation reorg drops the htlc tx, another fee variant is found, and a replaced registration's reorg isn't handled twice.TestMonitorDepositSpendWatchErrorsandTestMonitorWatchesSignedDepositOutpoints: spend watch errors retry the monitor, and the watch uses the signed outpoints.Known limits
This fits with #1269, which makes the timeout sweep take the htlc tx from the chain. This PR doesn't persist the htlc outpoint.