diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 62ff832071..ee1b269586 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -562,6 +562,59 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error { return h.Checkpoint(h) } +// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the +// sweeper. This is used when the remote party goes on chain, and we're able to +// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs +// are resolved via this path. +func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error { + var htlcWitnessType input.StandardWitnessType + if h.isTaproot() { + htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout + } else { + htlcWitnessType = input.HtlcOfferedRemoteTimeout + } + + sweepInput := input.NewCsvInputWithCltv( + &h.htlcResolution.ClaimOutpoint, htlcWitnessType, + &h.htlcResolution.SweepSignDesc, h.broadcastHeight, + h.htlcResolution.CsvDelay, h.htlcResolution.Expiry, + ) + + // Calculate the budget. + // + // TODO(yy): the budget is twice the output's value, which is needed as + // we don't force sweep the output now. To prevent cascading force + // closes, we use all its output value plus a wallet input as the + // budget. This is a temporary solution until we can optionally cancel + // the incoming HTLC, more details in, + // - https://github.com/lightningnetwork/lnd/issues/7969 + budget := calculateBudget( + btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0, + ) + + log.Infof("%T(%x): offering offered remote timeout HTLC output to "+ + "sweeper with deadline %v and budget=%v at height=%v", + h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget, + h.broadcastHeight) + + _, err := h.Sweeper.SweepInput( + sweepInput, + sweep.Params{ + Budget: budget, + + // This is an outgoing HTLC, so we want to make sure + // that we sweep it before the incoming HTLC expires. + DeadlineHeight: h.incomingHTLCExpiryHeight, + Immediate: immediate, + }, + ) + if err != nil { + return err + } + + return nil +} + // spendHtlcOutput handles the initial spend of an HTLC output via the timeout // clause. If this is our local commitment, the second-level timeout TX will be // used to spend the output into the next stage. If this is the remote @@ -582,6 +635,16 @@ func (h *htlcTimeoutResolver) spendHtlcOutput( return nil, err } + // If this is an anchor, and there's no second level txn (direct spend + // from remote commitment), then we'll just send this directly to the + // sweeper. + case h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil: + if err := h.sweepDirectHtlcOutput(immediate); err != nil { + log.Errorf("Sending direct spend to sweeper: %v", err) + + return nil, err + } + // If we have no SignDetails, and we haven't already sent the output to // the utxo nursery, then we'll do so now. case h.htlcResolution.SignDetails == nil && !h.outputIncubating: @@ -763,6 +826,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), h.htlc.RHash, ) + // Calculate the budget for this sweep. budget := calculateBudget( btcutil.Amount(inp.SignDesc().Output.Value), @@ -794,6 +858,12 @@ func (h *htlcTimeoutResolver) handleCommitSpend( claimOutpoint = *op fallthrough + // If we swept an HTLC directly off the remote party's commitment + // transaction, then we'll fall through as we just need to wait until + // the sweep has been confirmed. + case h.htlcResolution.SignedTimeoutTx == nil: + fallthrough + // Finally, if this was an output on our commitment transaction, we'll // wait for the second-level HTLC output to be spent, and for that // transaction itself to confirm.