contractcourt: add sweep senders in htlcTimeoutResolver

This commit adds new methods to handle making sweep requests based on
the spending path used by the outgoing htlc output.
This commit is contained in:
yyforyongyu 2024-11-14 22:08:00 +08:00
parent cb18940e75
commit bfc95b8b2c
No known key found for this signature in database
GPG key ID: 9BCD95C4FF296868

View file

@ -476,13 +476,9 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
return h.handleCommitSpend(commitSpend)
}
// sweepSecondLevelTx sends a second level timeout transaction to the sweeper.
// sweepTimeoutTx sends a second level timeout transaction to the sweeper.
// This transaction uses the SINLGE|ANYONECANPAY flag.
func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
log.Infof("%T(%x): offering second-layer timeout tx to sweeper: %v",
h, h.htlc.RHash[:],
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
func (h *htlcTimeoutResolver) sweepTimeoutTx() error {
var inp input.Input
if h.isTaproot() {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
@ -513,27 +509,12 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
btcutil.Amount(inp.SignDesc().Output.Value), 2, 0,
)
h.log.Infof("offering 2nd-level HTLC timeout tx to sweeper "+
"with deadline=%v, budget=%v", h.incomingHTLCExpiryHeight,
budget)
// For an outgoing HTLC, it must be swept before the RefundTimeout of
// its incoming HTLC is reached.
//
// TODO(yy): we may end up mixing inputs with different time locks.
// Suppose we have two outgoing HTLCs,
// - HTLC1: nLocktime is 800000, CLTV delta is 80.
// - HTLC2: nLocktime is 800001, CLTV delta is 79.
// This means they would both have an incoming HTLC that expires at
// 800080, hence they share the same deadline but different locktimes.
// However, with current design, when we are at block 800000, HTLC1 is
// offered to the sweeper. When block 800001 is reached, HTLC1's
// sweeping process is already started, while HTLC2 is being offered to
// the sweeper, so they won't be mixed. This can become an issue tho,
// if we decide to sweep per X blocks. Or the contractcourt sees the
// block first while the sweeper is only aware of the last block. To
// properly fix it, we need `blockbeat` to make sure subsystems are in
// sync.
log.Infof("%T(%x): offering second-level HTLC timeout tx to sweeper "+
"with deadline=%v, budget=%v", h, h.htlc.RHash[:],
h.incomingHTLCExpiryHeight, budget)
_, err := h.Sweeper.SweepInput(
inp,
sweep.Params{
@ -551,21 +532,15 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
// resolveSecondLevelTxLegacy sends a second level timeout transaction to the
// utxo nursery. This transaction uses the legacy SIGHASH_ALL flag.
func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error {
log.Debugf("%T(%v): incubating htlc output", h,
h.htlcResolution.ClaimOutpoint)
h.log.Debug("incubating htlc output")
err := h.IncubateOutputs(
// The utxo nursery will take care of broadcasting the second-level
// timeout tx and sweeping its output once it confirms.
return h.IncubateOutputs(
h.ChanPoint, fn.Some(h.htlcResolution),
fn.None[lnwallet.IncomingHtlcResolution](),
h.broadcastHeight, h.incomingHTLCExpiryHeight,
)
if err != nil {
return err
}
h.outputIncubating = true
return h.Checkpoint(h)
}
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
@ -635,7 +610,7 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (
// (the case for anchor type channels). In this case we can re-sign it
// and attach fees at will. We let the sweeper handle this job.
case h.isZeroFeeOutput() && !h.outputIncubating:
if err := h.sweepSecondLevelTx(); err != nil {
if err := h.sweepTimeoutTx(); err != nil {
log.Errorf("Sending timeout tx to sweeper: %v", err)
return nil, err
@ -696,9 +671,6 @@ func (h *htlcTimeoutResolver) watchHtlcSpend() (*chainntnfs.SpendDetail,
func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint,
pkScript []byte) (*chainntnfs.SpendDetail, error) {
log.Infof("%T(%v): waiting for spent of HTLC output %v to be "+
"fully confirmed", h, h.htlcResolution.ClaimOutpoint, op)
// We'll block here until either we exit, or the HTLC output on the
// commitment transaction has been spent.
spend, err := waitForSpend(
@ -770,82 +742,11 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
// the CSV and possible CLTV lock to expire, before sweeping the output
// on the second-level.
case h.isZeroFeeOutput():
waitHeight := h.deriveWaitHeight(
h.htlcResolution.CsvDelay, commitSpend,
)
h.reportLock.Lock()
h.currentReport.Stage = 2
h.currentReport.MaturityHeight = waitHeight
h.reportLock.Unlock()
if h.hasCLTV() {
log.Infof("%T(%x): waiting for CSV and CLTV lock to "+
"expire at height %v", h, h.htlc.RHash[:],
waitHeight)
} else {
log.Infof("%T(%x): waiting for CSV lock to expire at "+
"height %v", h, h.htlc.RHash[:], waitHeight)
}
// We'll use this input index to determine the second-level
// output index on the transaction, as the signatures requires
// the indexes to be the same. We don't look for the
// second-level output script directly, as there might be more
// than one HTLC output to the same pkScript.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
var csvWitnessType input.StandardWitnessType
if h.isTaproot() {
//nolint:ll
csvWitnessType = input.TaprootHtlcOfferedTimeoutSecondLevel
} else {
csvWitnessType = input.HtlcOfferedTimeoutSecondLevel
}
// Let the sweeper sweep the second-level output now that the
// CSV/CLTV locks have expired.
inp := h.makeSweepInput(
op, csvWitnessType,
input.LeaseHtlcOfferedTimeoutSecondLevel,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.CsvDelay,
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
h.htlcResolution.ResolutionBlob,
)
// Calculate the budget for this sweep.
budget := calculateBudget(
btcutil.Amount(inp.SignDesc().Output.Value),
h.Budget.NoDeadlineHTLCRatio,
h.Budget.NoDeadlineHTLC,
)
log.Infof("%T(%x): offering second-level timeout tx output to "+
"sweeper with no deadline and budget=%v at height=%v",
h, h.htlc.RHash[:], budget, waitHeight)
_, err := h.Sweeper.SweepInput(
inp,
sweep.Params{
Budget: budget,
// For second level success tx, there's no rush
// to get it confirmed, so we use a nil
// deadline.
DeadlineHeight: fn.None[int32](),
},
)
err := h.sweepTimeoutTxOutput()
if err != nil {
return nil, err
}
// Update the claim outpoint to point to the second-level
// transaction created by the sweeper.
claimOutpoint = *op
fallthrough
// Finally, if this was an output on our commitment transaction, we'll
@ -1288,3 +1189,131 @@ func (h *htlcTimeoutResolver) isLegacyOutput() bool {
return h.htlcResolution.SignedTimeoutTx != nil &&
h.htlcResolution.SignDetails == nil
}
// waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and
// checks whether the spending reveals the preimage. If the preimage is found,
// it will be added to the preimage beacon to settle the incoming link, and a
// nil spend details will be returned. Otherwise, the spend details will be
// returned, indicating this is a non-preimage spend.
func (h *htlcTimeoutResolver) waitHtlcSpendAndCheckPreimage() (
*chainntnfs.SpendDetail, error) {
// Wait for the htlc output to be spent, which can happen in one of the
// paths,
// 1. The remote party spends the htlc output using the preimage.
// 2. The local party spends the htlc timeout tx from the local
// commitment.
// 3. The local party spends the htlc output directlt from the remote
// commitment.
spend, err := h.watchHtlcSpend()
if err != nil {
return nil, err
}
// If the spend reveals the pre-image, then we'll enter the clean up
// workflow to pass the preimage back to the incoming link, add it to
// the witness cache, and exit.
if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) {
return nil, h.claimCleanUp(spend)
}
return spend, nil
}
// sweepTimeoutTxOutput attempts to sweep the output of the second level
// timeout tx.
func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error {
h.log.Debugf("sweeping output %v from 2nd-level HTLC timeout tx",
h.htlcResolution.ClaimOutpoint)
// This should be non-blocking as we will only attempt to sweep the
// output when the second level tx has already been confirmed. In other
// words, waitHtlcSpendAndCheckPreimage will return immediately.
commitSpend, err := h.waitHtlcSpendAndCheckPreimage()
if err != nil {
return err
}
// Exit early if the spend is nil, as this means it's a remote spend
// using the preimage path, which is handled in claimCleanUp.
if commitSpend == nil {
h.log.Infof("preimage spend detected, skipping 2nd-level " +
"HTLC output sweep")
return nil
}
waitHeight := h.deriveWaitHeight(h.htlcResolution.CsvDelay, commitSpend)
// Now that the sweeper has broadcasted the second-level transaction,
// it has confirmed, and we have checkpointed our state, we'll sweep
// the second level output. We report the resolver has moved the next
// stage.
h.reportLock.Lock()
h.currentReport.Stage = 2
h.currentReport.MaturityHeight = waitHeight
h.reportLock.Unlock()
if h.hasCLTV() {
h.log.Infof("waiting for CSV and CLTV lock to expire at "+
"height %v", waitHeight)
} else {
h.log.Infof("waiting for CSV lock to expire at height %v",
waitHeight)
}
// We'll use this input index to determine the second-level output
// index on the transaction, as the signatures requires the indexes to
// be the same. We don't look for the second-level output script
// directly, as there might be more than one HTLC output to the same
// pkScript.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
var witType input.StandardWitnessType
if h.isTaproot() {
witType = input.TaprootHtlcOfferedTimeoutSecondLevel
} else {
witType = input.HtlcOfferedTimeoutSecondLevel
}
// Let the sweeper sweep the second-level output now that the CSV/CLTV
// locks have expired.
inp := h.makeSweepInput(
op, witType,
input.LeaseHtlcOfferedTimeoutSecondLevel,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
h.htlc.RHash, h.htlcResolution.ResolutionBlob,
)
// Calculate the budget for this sweep.
budget := calculateBudget(
btcutil.Amount(inp.SignDesc().Output.Value),
h.Budget.NoDeadlineHTLCRatio,
h.Budget.NoDeadlineHTLC,
)
h.log.Infof("offering output from 2nd-level timeout tx to sweeper "+
"with no deadline and budget=%v", budget)
// TODO(yy): use the result chan returned from SweepInput to get the
// confirmation status of this sweeping tx so we don't need to make
// anothe subscription via `RegisterSpendNtfn` for this outpoint here
// in the resolver.
_, err = h.Sweeper.SweepInput(
inp,
sweep.Params{
Budget: budget,
// For second level success tx, there's no rush
// to get it confirmed, so we use a nil
// deadline.
DeadlineHeight: fn.None[int32](),
},
)
return err
}