Skip to content

Commit b2b5ec0

Browse files
Roasbeefguggero
authored andcommitted
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.
1 parent 941590d commit b2b5ec0

File tree

2 files changed

+123
-36
lines changed

2 files changed

+123
-36
lines changed

contractcourt/htlc_timeout_resolver.go

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
549549
return err
550550
}
551551

552-
// TODO(yy): checkpoint here?
553552
return err
554553
}
555554

@@ -573,6 +572,59 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
573572
return h.Checkpoint(h)
574573
}
575574

575+
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
576+
// sweeper. This is used when the remote party goes on chain, and we're able to
577+
// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs
578+
// are resolved via this path.
579+
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error {
580+
var htlcWitnessType input.StandardWitnessType
581+
if h.isTaproot() {
582+
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
583+
} else {
584+
htlcWitnessType = input.HtlcOfferedRemoteTimeout
585+
}
586+
587+
sweepInput := input.NewCsvInputWithCltv(
588+
&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
589+
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
590+
h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
591+
)
592+
593+
// Calculate the budget.
594+
//
595+
// TODO(yy): the budget is twice the output's value, which is needed as
596+
// we don't force sweep the output now. To prevent cascading force
597+
// closes, we use all its output value plus a wallet input as the
598+
// budget. This is a temporary solution until we can optionally cancel
599+
// the incoming HTLC, more details in,
600+
// - https://github.com/lightningnetwork/lnd/issues/7969
601+
budget := calculateBudget(
602+
btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0,
603+
)
604+
605+
log.Infof("%T(%x): offering offered remote timeout HTLC output to "+
606+
"sweeper with deadline %v and budget=%v at height=%v",
607+
h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget,
608+
h.broadcastHeight)
609+
610+
_, err := h.Sweeper.SweepInput(
611+
sweepInput,
612+
sweep.Params{
613+
Budget: budget,
614+
615+
// This is an outgoing HTLC, so we want to make sure
616+
// that we sweep it before the incoming HTLC expires.
617+
DeadlineHeight: h.incomingHTLCExpiryHeight,
618+
Immediate: immediate,
619+
},
620+
)
621+
if err != nil {
622+
return err
623+
}
624+
625+
return nil
626+
}
627+
576628
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
577629
// clause. If this is our local commitment, the second-level timeout TX will be
578630
// used to spend the output into the next stage. If this is the remote
@@ -593,8 +645,18 @@ func (h *htlcTimeoutResolver) spendHtlcOutput(
593645
return nil, err
594646
}
595647

596-
// If we have no SignDetails, and we haven't already sent the output to
597-
// the utxo nursery, then we'll do so now.
648+
// If this is a remote commitment there's no second level timeout txn,
649+
// and we can just send this directly to the sweeper.
650+
case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating:
651+
if err := h.sweepDirectHtlcOutput(immediate); err != nil {
652+
log.Errorf("Sending direct spend to sweeper: %v", err)
653+
654+
return nil, err
655+
}
656+
657+
// If we have a SignedTimeoutTx but no SignDetails, this is a local
658+
// commitment for a non-anchor channel, so we'll send it to the utxo
659+
// nursery.
598660
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
599661
if err := h.sendSecondLevelTxLegacy(); err != nil {
600662
log.Errorf("Sending timeout tx to nursery: %v", err)
@@ -701,6 +763,13 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
701763
)
702764

703765
switch {
766+
767+
// If we swept an HTLC directly off the remote party's commitment
768+
// transaction, then we can exit here as there's no second level sweep
769+
// to do.
770+
case h.htlcResolution.SignedTimeoutTx == nil:
771+
break
772+
704773
// If the sweeper is handling the second level transaction, wait for
705774
// the CSV and possible CLTV lock to expire, before sweeping the output
706775
// on the second-level.
@@ -774,6 +843,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
774843
h.htlcResolution.CsvDelay,
775844
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
776845
)
846+
777847
// Calculate the budget for this sweep.
778848
budget := calculateBudget(
779849
btcutil.Amount(inp.SignDesc().Output.Value),
@@ -811,6 +881,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
811881
case h.htlcResolution.SignedTimeoutTx != nil:
812882
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
813883
"delayed output", h, claimOutpoint)
884+
814885
sweepTx, err := waitForSpend(
815886
&claimOutpoint,
816887
h.htlcResolution.SweepSignDesc.Output.PkScript,
@@ -877,9 +948,11 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
877948

878949
// report returns a report on the resolution state of the contract.
879950
func (h *htlcTimeoutResolver) report() *ContractReport {
880-
// If the sign details are nil, the report will be created by handled
881-
// by the nursery.
882-
if h.htlcResolution.SignDetails == nil {
951+
// If we have a SignedTimeoutTx but no SignDetails, this is a local
952+
// commitment for a non-anchor channel, which was handled by the utxo
953+
// nursery.
954+
if h.htlcResolution.SignDetails == nil && h.
955+
htlcResolution.SignedTimeoutTx != nil {
883956
return nil
884957
}
885958

@@ -899,13 +972,20 @@ func (h *htlcTimeoutResolver) initReport() {
899972
)
900973
}
901974

975+
// If there's no timeout transaction, then we're already effectively in
976+
// level two.
977+
stage := uint32(1)
978+
if h.htlcResolution.SignedTimeoutTx == nil {
979+
stage = 2
980+
}
981+
902982
h.currentReport = ContractReport{
903983
Outpoint: h.htlcResolution.ClaimOutpoint,
904984
Type: ReportOutputOutgoingHtlc,
905985
Amount: finalAmt,
906986
MaturityHeight: h.htlcResolution.Expiry,
907987
LimboBalance: finalAmt,
908-
Stage: 1,
988+
Stage: stage,
909989
}
910990
}
911991

contractcourt/htlc_timeout_resolver_test.go

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -294,12 +294,13 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
294294
chainCfg := ChannelArbitratorConfig{
295295
ChainArbitratorConfig: ChainArbitratorConfig{
296296
Notifier: notifier,
297+
Sweeper: newMockSweeper(),
297298
PreimageDB: witnessBeacon,
298299
IncubateOutputs: func(wire.OutPoint,
299300
fn.Option[lnwallet.OutgoingHtlcResolution],
300301
fn.Option[lnwallet.IncomingHtlcResolution],
301-
uint32, fn.Option[int32],
302-
) error {
302+
uint32, fn.Option[int32]) error {
303+
303304
incubateChan <- struct{}{}
304305
return nil
305306
},
@@ -311,26 +312,28 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
311312
}
312313

313314
resolutionChan <- msgs[0]
315+
314316
return nil
315317
},
316318
Budget: *DefaultBudgetConfig(),
317319
QueryIncomingCircuit: func(circuit models.CircuitKey,
318320
) *models.CircuitKey {
321+
319322
return nil
320323
},
321324
},
322325
PutResolverReport: func(_ kvdb.RwTx,
323-
_ *channeldb.ResolverReport,
324-
) error {
326+
_ *channeldb.ResolverReport) error {
327+
325328
return nil
326329
},
327330
}
328331

329332
cfg := ResolverConfig{
330333
ChannelArbitratorConfig: chainCfg,
331334
Checkpoint: func(_ ContractResolver,
332-
reports ...*channeldb.ResolverReport,
333-
) error {
335+
reports ...*channeldb.ResolverReport) error {
336+
334337
checkPointChan <- struct{}{}
335338

336339
// Send all of our reports into the channel.
@@ -367,13 +370,15 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
367370

368371
if testCase.timeout {
369372
timeoutTxID := timeoutTx.TxHash()
370-
reports = append(reports, &channeldb.ResolverReport{
371-
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
373+
report := &channeldb.ResolverReport{
374+
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, //nolint:lll
372375
Amount: testHtlcAmt.ToSatoshis(),
373-
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
374-
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
376+
ResolverType: channeldb.ResolverTypeOutgoingHtlc, //nolint:lll
377+
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, //nolint:lll
375378
SpendTxID: &timeoutTxID,
376-
})
379+
}
380+
381+
reports = append(reports, report)
377382
}
378383
}
379384

@@ -391,10 +396,21 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
391396
}
392397
}()
393398

394-
// As the output isn't yet in the nursery, we expect that we
395-
// should receive an incubation request.
399+
// If this is a remote commit, then we expct the outputs should receive
400+
// an incubation request to go through the sweeper, otherwise the
401+
// nursery.
402+
var sweepChan chan input.Input
403+
if testCase.remoteCommit {
404+
mockSweeper, ok := resolver.Sweeper.(*mockSweeper)
405+
require.True(t, ok)
406+
sweepChan = mockSweeper.sweptInputs
407+
}
408+
409+
// The output should be offered to either the sweeper or
410+
// the nursery.
396411
select {
397412
case <-incubateChan:
413+
case <-sweepChan:
398414
case err := <-resolveErr:
399415
t.Fatalf("unable to resolve HTLC: %v", err)
400416
case <-time.After(time.Second * 5):
@@ -450,7 +466,6 @@ func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
450466
t.Fatalf("resolution not sent")
451467
}
452468
} else {
453-
454469
// Otherwise, the HTLC should now timeout. First, we
455470
// should get a resolution message with a populated
456471
// failure message.
@@ -559,10 +574,6 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
559574
}
560575

561576
checkpoints := []checkpoint{
562-
{
563-
// Output should be handed off to the nursery.
564-
incubating: true,
565-
},
566577
{
567578
// We send a confirmation the sweep tx from published
568579
// by the nursery.
@@ -594,7 +605,7 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
594605
// After the sweep has confirmed, we expect the
595606
// checkpoint to be resolved, and with the above
596607
// report.
597-
incubating: true,
608+
incubating: false,
598609
resolved: true,
599610
reports: []*channeldb.ResolverReport{
600611
claim,
@@ -849,9 +860,9 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
849860
)
850861
}
851862

852-
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
853-
// confirms, and the remote spends the output using the success tx, we
854-
// properly detect this and extract the preimage.
863+
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remote commitment
864+
// confirms, and the remote spends the output using the success tx, we properly
865+
// detect this and extract the preimage.
855866
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
856867
commitOutpoint := wire.OutPoint{Index: 2}
857868

@@ -895,10 +906,6 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
895906
}
896907

897908
checkpoints := []checkpoint{
898-
{
899-
// Output should be handed off to the nursery.
900-
incubating: true,
901-
},
902909
{
903910
// We send a confirmation for the remote's second layer
904911
// success transcation.
@@ -944,7 +951,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
944951
// After the sweep has confirmed, we expect the
945952
// checkpoint to be resolved, and with the above
946953
// report.
947-
incubating: true,
954+
incubating: false,
948955
resolved: true,
949956
reports: []*channeldb.ResolverReport{
950957
claim,
@@ -1321,8 +1328,8 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
13211328
}
13221329

13231330
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
1324-
checkpoints []checkpoint,
1325-
) {
1331+
checkpoints []checkpoint) {
1332+
13261333
t.Helper()
13271334

13281335
defer timeout()()

0 commit comments

Comments
 (0)