contractcourt: add Launch method to htlc success resolver

This commit breaks the `Resolve` into two parts - the first part is
moved into a `Launch` method that handles sending sweep requests, and
the second part remains in `Resolve` which handles waiting for the
spend. Since we are using both utxo nursery and sweeper at the same
time, to make sure this change doesn't break the existing behavior, we
implement the `Launch` as following,
- zero-fee htlc - handled by the sweeper
- direct output from the remote commit - handled by the sweeper
- legacy htlc - handled by the utxo nursery
This commit is contained in:
yyforyongyu 2024-07-16 07:24:45 +08:00
parent 913f5d4657
commit cf105e67f4
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
2 changed files with 153 additions and 122 deletions

View File

@ -10,8 +10,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models"
@ -116,139 +114,60 @@ func (h *htlcSuccessResolver) ResolverKey() []byte {
// anymore. Every HTLC has already passed through the incoming contest resolver
// and in there the invoice was already marked as settled.
//
// TODO(roasbeef): create multi to batch
//
// NOTE: Part of the ContractResolver interface.
//
// TODO(yy): refactor the interface method to return an error only.
func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
var err error
switch {
// If we're already resolved, then we can exit early.
if h.resolved {
return nil, nil
}
case h.resolved:
h.log.Errorf("already resolved")
// If we don't have a success transaction, then this means that this is
// an output on the remote party's commitment transaction.
if h.isRemoteCommitOutput() {
return h.resolveRemoteCommitOutput()
}
// Otherwise this an output on our own commitment, and we must start by
// broadcasting the second-level success transaction.
secondLevelOutpoint, err := h.broadcastSuccessTx()
if err != nil {
return nil, err
}
// To wrap this up, we'll wait until the second-level transaction has
// been spent, then fully resolve the contract.
return nil, h.resolveSuccessTxOutput(*secondLevelOutpoint)
}
// broadcastSuccessTx handles an HTLC output on our local commitment by
// broadcasting the second-level success transaction. It returns the ultimate
// outpoint of the second-level tx, that we must wait to be spent for the
// resolver to be fully resolved.
func (h *htlcSuccessResolver) broadcastSuccessTx() (
*wire.OutPoint, error) {
// If we have non-nil SignDetails, this means that have a 2nd level
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
// (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. We use
// the checkpointed outputIncubating field to determine if we already
// swept the HTLC output into the second level transaction.
if h.isZeroFeeOutput() {
return h.broadcastReSignedSuccessTx()
}
// Otherwise we'll publish the second-level transaction directly and
// offer the resolution to the nursery to handle.
log.Infof("%T(%x): broadcasting second-layer transition tx: %v",
h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx))
// We'll now broadcast the second layer transaction so we can kick off
// the claiming process.
err := h.resolveLegacySuccessTx()
if err != nil {
return nil, err
}
return &h.htlcResolution.ClaimOutpoint, nil
}
// broadcastReSignedSuccessTx handles the case where we have non-nil
// SignDetails, and offers the second level transaction to the Sweeper, that
// will re-sign it and attach fees at will.
func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (*wire.OutPoint,
error) {
// Keep track of the tx spending the HTLC output on the commitment, as
// this will be the confirmed second-level tx we'll ultimately sweep.
var commitSpend *chainntnfs.SpendDetail
// We will have to let the sweeper re-sign the success tx and wait for
// it to confirm, if we haven't already.
if !h.outputIncubating {
err := h.sweepSuccessTx()
if err != nil {
return nil, err
}
// If this is an output on the remote party's commitment transaction,
// use the direct-spend path to sweep the htlc.
case h.isRemoteCommitOutput():
err = h.resolveRemoteCommitOutput()
// If this is an output on our commitment transaction using post-anchor
// channel type, it will be handled by the sweeper.
case h.isZeroFeeOutput():
err = h.resolveSuccessTx()
if err != nil {
return nil, err
}
// If this is an output on our own commitment using pre-anchor channel
// type, we will publish the success tx and offer the output to the
// nursery.
default:
err = h.resolveLegacySuccessTx()
}
// 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, waitForSpend will return immediately.
commitSpend, err := waitForSpend(
&h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint,
h.htlcResolution.SignDetails.SignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
err = h.sweepSuccessTxOutput()
if err != nil {
return nil, err
}
// Will return this outpoint, when this is spent the resolver is fully
// resolved.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
return op, nil
return nil, err
}
// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote
// commitment with the preimage. In this case we can sweep the output directly,
// and don't have to broadcast a second-level transaction.
func (h *htlcSuccessResolver) resolveRemoteCommitOutput() (
ContractResolver, error) {
err := h.sweepRemoteCommitOutput()
if err != nil {
return nil, err
}
func (h *htlcSuccessResolver) resolveRemoteCommitOutput() error {
h.log.Info("waiting for direct-preimage spend of the htlc to confirm")
// Wait for the direct-preimage HTLC sweep tx to confirm.
//
// TODO(yy): use the result chan returned from `SweepInput`.
sweepTxDetails, err := waitForSpend(
&h.htlcResolution.ClaimOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
return err
}
// TODO(yy): should also update the `RecoveredBalance` and
// `LimboBalance` like other paths?
// Checkpoint the resolver, and write the outcome to disk.
return nil, h.checkpointClaim(sweepTxDetails.SpenderTxHash)
return h.checkpointClaim(sweepTxDetails.SpenderTxHash)
}
// checkpointClaim checkpoints the success resolver with the reports it needs.
@ -316,6 +235,9 @@ func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash) error {
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcSuccessResolver) Stop() {
h.log.Debugf("stopping...")
defer h.log.Debugf("stopped")
close(h.quit)
}
@ -809,3 +731,47 @@ func (h *htlcSuccessResolver) resolveSuccessTxOutput(op wire.OutPoint) error {
return h.checkpointClaim(spend.SpenderTxHash)
}
// Launch creates an input based on the details of the incoming htlc resolution
// and offers it to the sweeper.
func (h *htlcSuccessResolver) Launch() error {
if h.launched {
h.log.Tracef("already launched")
return nil
}
h.log.Debugf("launching resolver...")
h.launched = true
switch {
// If we're already resolved, then we can exit early.
case h.resolved:
h.log.Errorf("already resolved")
return nil
// If this is an output on the remote party's commitment transaction,
// use the direct-spend path.
case h.isRemoteCommitOutput():
return h.sweepRemoteCommitOutput()
// If this is an anchor type channel, we now sweep either the
// second-level success tx or the output from the second-level success
// tx.
case h.isZeroFeeOutput():
// If the second-level success tx has already been swept, we
// can go ahead and sweep its output.
if h.outputIncubating {
return h.sweepSuccessTxOutput()
}
// Otherwise, sweep the second level tx.
return h.sweepSuccessTx()
// If this is a legacy channel type, the output is handled by the
// nursery via the Resolve so we do nothing here.
//
// TODO(yy): handle the legacy output by offering it to the sweeper.
default:
return nil
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -20,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
var testHtlcAmt = lnwire.MilliSatoshi(200000)
@ -39,6 +41,15 @@ type htlcResolverTestContext struct {
t *testing.T
}
func newHtlcResolverTestContextFromReader(t *testing.T,
newResolver func(htlc channeldb.HTLC,
cfg ResolverConfig) ContractResolver) *htlcResolverTestContext {
ctx := newHtlcResolverTestContext(t, newResolver)
return ctx
}
func newHtlcResolverTestContext(t *testing.T,
newResolver func(htlc channeldb.HTLC,
cfg ResolverConfig) ContractResolver) *htlcResolverTestContext {
@ -133,6 +144,7 @@ func newHtlcResolverTestContext(t *testing.T,
func (i *htlcResolverTestContext) resolve() {
// Start resolver.
i.resolverResultChan = make(chan resolveResult, 1)
go func() {
nextResolver, err := i.resolver.Resolve()
i.resolverResultChan <- resolveResult{
@ -192,6 +204,7 @@ func TestHtlcSuccessSingleStage(t *testing.T) {
// sweeper.
details := &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpentOutPoint: &htlcOutpoint,
SpenderTxHash: &sweepTxid,
}
ctx.notifier.SpendChan <- details
@ -215,8 +228,8 @@ func TestHtlcSuccessSingleStage(t *testing.T) {
)
}
// TestSecondStageResolution tests successful sweep of a second stage htlc
// claim, going through the Nursery.
// TestHtlcSuccessSecondStageResolution tests successful sweep of a second
// stage htlc claim, going through the Nursery.
func TestHtlcSuccessSecondStageResolution(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
@ -279,6 +292,7 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpentOutPoint: &htlcOutpoint,
SpenderTxHash: &sweepHash,
}
@ -302,6 +316,8 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) {
// TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with
// non-nil SignDetails will offer the second-level transaction to the sweeper
// for re-signing.
//
//nolint:ll
func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
@ -399,7 +415,20 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
_ bool) error {
resolver := ctx.resolver.(*htlcSuccessResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
var (
inp input.Input
ok bool
)
select {
case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs:
require.True(t, ok)
case <-time.After(1 * time.Second):
t.Fatal("expected input to be swept")
}
op := inp.OutPoint()
if op != commitOutpoint {
return fmt.Errorf("outpoint %v swept, "+
@ -412,6 +441,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
SpenderTxHash: &reSignedHash,
SpenderInputIndex: 1,
SpendingHeight: 10,
SpentOutPoint: &commitOutpoint,
}
return nil
},
@ -434,13 +464,37 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
SpenderTxHash: &reSignedHash,
SpenderInputIndex: 1,
SpendingHeight: 10,
SpentOutPoint: &commitOutpoint,
}
}
// We expect it to sweep the second-level
// transaction we notfied about above.
resolver := ctx.resolver.(*htlcSuccessResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
// Mock `waitForSpend` to return the commit
// spend.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: reSignedSuccessTx,
SpenderTxHash: &reSignedHash,
SpenderInputIndex: 1,
SpendingHeight: 10,
SpentOutPoint: &commitOutpoint,
}
var (
inp input.Input
ok bool
)
select {
case inp, ok = <-resolver.Sweeper.(*mockSweeper).sweptInputs:
require.True(t, ok)
case <-time.After(1 * time.Second):
t.Fatal("expected input to be swept")
}
op := inp.OutPoint()
exp := wire.OutPoint{
Hash: reSignedHash,
@ -457,6 +511,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
SpendingHeight: 14,
SpentOutPoint: &op,
}
return nil
@ -504,11 +559,14 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
// for the next portion of the test.
ctx := newHtlcResolverTestContext(t,
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
return &htlcSuccessResolver{
r := &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlc: htlc,
htlcResolution: resolution,
}
r.initLogger("htlcSuccessResolver")
return r
},
)
@ -606,7 +664,12 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
checkpointedState = append(checkpointedState, b.Bytes())
nextCheckpoint++
checkpointChan <- struct{}{}
select {
case checkpointChan <- struct{}{}:
case <-time.After(1 * time.Second):
t.Fatal("checkpoint timeout")
}
return nil
}
@ -617,6 +680,8 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
// preCheckpoint logic if needed.
resumed := true
for i, cp := range expectedCheckpoints {
t.Logf("Running checkpoint %d", i)
if cp.preCheckpoint != nil {
if err := cp.preCheckpoint(ctx, resumed); err != nil {
t.Fatalf("failure at stage %d: %v", i, err)
@ -625,15 +690,15 @@ func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
resumed = false
// Wait for the resolver to have checkpointed its state.
<-checkpointChan
select {
case <-checkpointChan:
case <-time.After(1 * time.Second):
t.Fatalf("resolver did not checkpoint at stage %d", i)
}
}
// Wait for the resolver to fully complete.
ctx.waitForResult()
if nextCheckpoint < len(expectedCheckpoints) {
t.Fatalf("not all checkpoints hit")
}
return checkpointedState
}