Skip to content

Commit 903c8fc

Browse files
committed
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 c898b68 commit 903c8fc

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
@@ -538,7 +538,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
538538
return err
539539
}
540540

541-
// TODO(yy): checkpoint here?
542541
return err
543542
}
544543

@@ -562,6 +561,59 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
562561
return h.Checkpoint(h)
563562
}
564563

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

585-
// If we have no SignDetails, and we haven't already sent the output to
586-
// the utxo nursery, then we'll do so now.
637+
// If this is a remote commitment there's no second level timeout txn,
638+
// and we can just send this directly to the sweeper.
639+
case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating:
640+
if err := h.sweepDirectHtlcOutput(immediate); err != nil {
641+
log.Errorf("Sending direct spend to sweeper: %v", err)
642+
643+
return nil, err
644+
}
645+
646+
// If we have a SignedTimeoutTx but no SignDetails, this is a local
647+
// commitment for a non-anchor channel, so we'll send it to the utxo
648+
// nursery.
587649
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
588650
if err := h.sendSecondLevelTxLegacy(); err != nil {
589651
log.Errorf("Sending timeout tx to nursery: %v", err)
@@ -690,6 +752,13 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
690752
)
691753

692754
switch {
755+
756+
// If we swept an HTLC directly off the remote party's commitment
757+
// transaction, then we can exit here as there's no second level sweep
758+
// to do.
759+
case h.htlcResolution.SignedTimeoutTx == nil:
760+
break
761+
693762
// If the sweeper is handling the second level transaction, wait for
694763
// the CSV and possible CLTV lock to expire, before sweeping the output
695764
// on the second-level.
@@ -763,6 +832,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
763832
h.htlcResolution.CsvDelay,
764833
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
765834
)
835+
766836
// Calculate the budget for this sweep.
767837
budget := calculateBudget(
768838
btcutil.Amount(inp.SignDesc().Output.Value),
@@ -800,6 +870,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
800870
case h.htlcResolution.SignedTimeoutTx != nil:
801871
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
802872
"delayed output", h, claimOutpoint)
873+
803874
sweepTx, err := waitForSpend(
804875
&claimOutpoint,
805876
h.htlcResolution.SweepSignDesc.Output.PkScript,
@@ -866,9 +937,11 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
866937

867938
// report returns a report on the resolution state of the contract.
868939
func (h *htlcTimeoutResolver) report() *ContractReport {
869-
// If the sign details are nil, the report will be created by handled
870-
// by the nursery.
871-
if h.htlcResolution.SignDetails == nil {
940+
// If we have a SignedTimeoutTx but no SignDetails, this is a local
941+
// commitment for a non-anchor channel, which was handled by the utxo
942+
// nursery.
943+
if h.htlcResolution.SignDetails == nil && h.
944+
htlcResolution.SignedTimeoutTx != nil {
872945
return nil
873946
}
874947

@@ -888,13 +961,20 @@ func (h *htlcTimeoutResolver) initReport() {
888961
)
889962
}
890963

964+
// If there's no timeout transaction, then we're already effectively in
965+
// level two.
966+
stage := uint32(1)
967+
if h.htlcResolution.SignedTimeoutTx == nil {
968+
stage = 2
969+
}
970+
891971
h.currentReport = ContractReport{
892972
Outpoint: h.htlcResolution.ClaimOutpoint,
893973
Type: ReportOutputOutgoingHtlc,
894974
Amount: finalAmt,
895975
MaturityHeight: h.htlcResolution.Expiry,
896976
LimboBalance: finalAmt,
897-
Stage: 1,
977+
Stage: stage,
898978
}
899979
}
900980

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)