mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-29 03:01:52 +01:00
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.
This commit is contained in:
parent
941590d384
commit
b2b5ec052b
@ -549,7 +549,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(yy): checkpoint here?
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,6 +572,59 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
|
|||||||
return h.Checkpoint(h)
|
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
|
// 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
|
// 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
|
// used to spend the output into the next stage. If this is the remote
|
||||||
@ -593,8 +645,18 @@ func (h *htlcTimeoutResolver) spendHtlcOutput(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have no SignDetails, and we haven't already sent the output to
|
// If this is a remote commitment there's no second level timeout txn,
|
||||||
// the utxo nursery, then we'll do so now.
|
// and we can just send this directly to the sweeper.
|
||||||
|
case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating:
|
||||||
|
if err := h.sweepDirectHtlcOutput(immediate); err != nil {
|
||||||
|
log.Errorf("Sending direct spend to sweeper: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||||
|
// commitment for a non-anchor channel, so we'll send it to the utxo
|
||||||
|
// nursery.
|
||||||
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
||||||
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
||||||
log.Errorf("Sending timeout tx to nursery: %v", err)
|
log.Errorf("Sending timeout tx to nursery: %v", err)
|
||||||
@ -701,6 +763,13 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
|
// If we swept an HTLC directly off the remote party's commitment
|
||||||
|
// transaction, then we can exit here as there's no second level sweep
|
||||||
|
// to do.
|
||||||
|
case h.htlcResolution.SignedTimeoutTx == nil:
|
||||||
|
break
|
||||||
|
|
||||||
// If the sweeper is handling the second level transaction, wait for
|
// If the sweeper is handling the second level transaction, wait for
|
||||||
// the CSV and possible CLTV lock to expire, before sweeping the output
|
// the CSV and possible CLTV lock to expire, before sweeping the output
|
||||||
// on the second-level.
|
// on the second-level.
|
||||||
@ -774,6 +843,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
h.htlcResolution.CsvDelay,
|
h.htlcResolution.CsvDelay,
|
||||||
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
|
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate the budget for this sweep.
|
// Calculate the budget for this sweep.
|
||||||
budget := calculateBudget(
|
budget := calculateBudget(
|
||||||
btcutil.Amount(inp.SignDesc().Output.Value),
|
btcutil.Amount(inp.SignDesc().Output.Value),
|
||||||
@ -811,6 +881,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
case h.htlcResolution.SignedTimeoutTx != nil:
|
case h.htlcResolution.SignedTimeoutTx != nil:
|
||||||
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
||||||
"delayed output", h, claimOutpoint)
|
"delayed output", h, claimOutpoint)
|
||||||
|
|
||||||
sweepTx, err := waitForSpend(
|
sweepTx, err := waitForSpend(
|
||||||
&claimOutpoint,
|
&claimOutpoint,
|
||||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||||
@ -877,9 +948,11 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
|
|||||||
|
|
||||||
// report returns a report on the resolution state of the contract.
|
// report returns a report on the resolution state of the contract.
|
||||||
func (h *htlcTimeoutResolver) report() *ContractReport {
|
func (h *htlcTimeoutResolver) report() *ContractReport {
|
||||||
// If the sign details are nil, the report will be created by handled
|
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||||
// by the nursery.
|
// commitment for a non-anchor channel, which was handled by the utxo
|
||||||
if h.htlcResolution.SignDetails == nil {
|
// nursery.
|
||||||
|
if h.htlcResolution.SignDetails == nil && h.
|
||||||
|
htlcResolution.SignedTimeoutTx != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,13 +972,20 @@ func (h *htlcTimeoutResolver) initReport() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's no timeout transaction, then we're already effectively in
|
||||||
|
// level two.
|
||||||
|
stage := uint32(1)
|
||||||
|
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||||
|
stage = 2
|
||||||
|
}
|
||||||
|
|
||||||
h.currentReport = ContractReport{
|
h.currentReport = ContractReport{
|
||||||
Outpoint: h.htlcResolution.ClaimOutpoint,
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
Type: ReportOutputOutgoingHtlc,
|
Type: ReportOutputOutgoingHtlc,
|
||||||
Amount: finalAmt,
|
Amount: finalAmt,
|
||||||
MaturityHeight: h.htlcResolution.Expiry,
|
MaturityHeight: h.htlcResolution.Expiry,
|
||||||
LimboBalance: finalAmt,
|
LimboBalance: finalAmt,
|
||||||
Stage: 1,
|
Stage: stage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,12 +294,13 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
chainCfg := ChannelArbitratorConfig{
|
chainCfg := ChannelArbitratorConfig{
|
||||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||||
Notifier: notifier,
|
Notifier: notifier,
|
||||||
|
Sweeper: newMockSweeper(),
|
||||||
PreimageDB: witnessBeacon,
|
PreimageDB: witnessBeacon,
|
||||||
IncubateOutputs: func(wire.OutPoint,
|
IncubateOutputs: func(wire.OutPoint,
|
||||||
fn.Option[lnwallet.OutgoingHtlcResolution],
|
fn.Option[lnwallet.OutgoingHtlcResolution],
|
||||||
fn.Option[lnwallet.IncomingHtlcResolution],
|
fn.Option[lnwallet.IncomingHtlcResolution],
|
||||||
uint32, fn.Option[int32],
|
uint32, fn.Option[int32]) error {
|
||||||
) error {
|
|
||||||
incubateChan <- struct{}{}
|
incubateChan <- struct{}{}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -311,17 +312,19 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolutionChan <- msgs[0]
|
resolutionChan <- msgs[0]
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Budget: *DefaultBudgetConfig(),
|
Budget: *DefaultBudgetConfig(),
|
||||||
QueryIncomingCircuit: func(circuit models.CircuitKey,
|
QueryIncomingCircuit: func(circuit models.CircuitKey,
|
||||||
) *models.CircuitKey {
|
) *models.CircuitKey {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PutResolverReport: func(_ kvdb.RwTx,
|
PutResolverReport: func(_ kvdb.RwTx,
|
||||||
_ *channeldb.ResolverReport,
|
_ *channeldb.ResolverReport) error {
|
||||||
) error {
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -329,8 +332,8 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
cfg := ResolverConfig{
|
cfg := ResolverConfig{
|
||||||
ChannelArbitratorConfig: chainCfg,
|
ChannelArbitratorConfig: chainCfg,
|
||||||
Checkpoint: func(_ ContractResolver,
|
Checkpoint: func(_ ContractResolver,
|
||||||
reports ...*channeldb.ResolverReport,
|
reports ...*channeldb.ResolverReport) error {
|
||||||
) error {
|
|
||||||
checkPointChan <- struct{}{}
|
checkPointChan <- struct{}{}
|
||||||
|
|
||||||
// Send all of our reports into the channel.
|
// Send all of our reports into the channel.
|
||||||
@ -367,13 +370,15 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
|
|
||||||
if testCase.timeout {
|
if testCase.timeout {
|
||||||
timeoutTxID := timeoutTx.TxHash()
|
timeoutTxID := timeoutTx.TxHash()
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
report := &channeldb.ResolverReport{
|
||||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, //nolint:lll
|
||||||
Amount: testHtlcAmt.ToSatoshis(),
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, //nolint:lll
|
||||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, //nolint:lll
|
||||||
SpendTxID: &timeoutTxID,
|
SpendTxID: &timeoutTxID,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
reports = append(reports, report)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,10 +396,21 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// As the output isn't yet in the nursery, we expect that we
|
// If this is a remote commit, then we expct the outputs should receive
|
||||||
// should receive an incubation request.
|
// an incubation request to go through the sweeper, otherwise the
|
||||||
|
// nursery.
|
||||||
|
var sweepChan chan input.Input
|
||||||
|
if testCase.remoteCommit {
|
||||||
|
mockSweeper, ok := resolver.Sweeper.(*mockSweeper)
|
||||||
|
require.True(t, ok)
|
||||||
|
sweepChan = mockSweeper.sweptInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output should be offered to either the sweeper or
|
||||||
|
// the nursery.
|
||||||
select {
|
select {
|
||||||
case <-incubateChan:
|
case <-incubateChan:
|
||||||
|
case <-sweepChan:
|
||||||
case err := <-resolveErr:
|
case err := <-resolveErr:
|
||||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||||
case <-time.After(time.Second * 5):
|
case <-time.After(time.Second * 5):
|
||||||
@ -450,7 +466,6 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
|||||||
t.Fatalf("resolution not sent")
|
t.Fatalf("resolution not sent")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Otherwise, the HTLC should now timeout. First, we
|
// Otherwise, the HTLC should now timeout. First, we
|
||||||
// should get a resolution message with a populated
|
// should get a resolution message with a populated
|
||||||
// failure message.
|
// failure message.
|
||||||
@ -559,10 +574,6 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkpoints := []checkpoint{
|
checkpoints := []checkpoint{
|
||||||
{
|
|
||||||
// Output should be handed off to the nursery.
|
|
||||||
incubating: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// We send a confirmation the sweep tx from published
|
// We send a confirmation the sweep tx from published
|
||||||
// by the nursery.
|
// by the nursery.
|
||||||
@ -594,7 +605,7 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
|||||||
// After the sweep has confirmed, we expect the
|
// After the sweep has confirmed, we expect the
|
||||||
// checkpoint to be resolved, and with the above
|
// checkpoint to be resolved, and with the above
|
||||||
// report.
|
// report.
|
||||||
incubating: true,
|
incubating: false,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
reports: []*channeldb.ResolverReport{
|
reports: []*channeldb.ResolverReport{
|
||||||
claim,
|
claim,
|
||||||
@ -849,9 +860,9 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
|
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remote commitment
|
||||||
// confirms, and the remote spends the output using the success tx, we
|
// confirms, and the remote spends the output using the success tx, we properly
|
||||||
// properly detect this and extract the preimage.
|
// detect this and extract the preimage.
|
||||||
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||||
commitOutpoint := wire.OutPoint{Index: 2}
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
|
||||||
@ -895,10 +906,6 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkpoints := []checkpoint{
|
checkpoints := []checkpoint{
|
||||||
{
|
|
||||||
// Output should be handed off to the nursery.
|
|
||||||
incubating: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// We send a confirmation for the remote's second layer
|
// We send a confirmation for the remote's second layer
|
||||||
// success transcation.
|
// success transcation.
|
||||||
@ -944,7 +951,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
|||||||
// After the sweep has confirmed, we expect the
|
// After the sweep has confirmed, we expect the
|
||||||
// checkpoint to be resolved, and with the above
|
// checkpoint to be resolved, and with the above
|
||||||
// report.
|
// report.
|
||||||
incubating: true,
|
incubating: false,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
reports: []*channeldb.ResolverReport{
|
reports: []*channeldb.ResolverReport{
|
||||||
claim,
|
claim,
|
||||||
@ -1321,8 +1328,8 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
||||||
checkpoints []checkpoint,
|
checkpoints []checkpoint) {
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
defer timeout()()
|
defer timeout()()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user