From fb5c2453a82f2d6db11f1e8aef5f05da6077395d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 3 Sep 2024 18:58:12 -0700 Subject: [PATCH] contractcourt: use the sweeper for HTLC offered remote timeout resolution In this commit, we bring the timeout resolver more in line with the success resolver by using the sweeper to handle the HTLC offered remote timeout outputs. These are outputs that we can sweep directly from the remote party's commitment transaction when they broadcast their version of the commitment transaction. With this change, we slim down the scope slightly by only doing this for anchor channels. Non-anchor channels will continue to use the utxonursery for this output type for now. --- contractcourt/htlc_timeout_resolver.go | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) 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.