Merge branch '0-18-4-branch-rc1-9062' into 0-18-4-branch-rc1

This commit is contained in:
Oliver Gugger
2024-11-08 08:52:31 +01:00
2 changed files with 391 additions and 277 deletions

View File

@@ -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,
} }
} }

View File

@@ -69,11 +69,31 @@ func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error {
return nil return nil
} }
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all type htlcTimeoutTestCase struct {
// variations of possible local+remote spends. // name is a human readable description of the test case.
func TestHtlcTimeoutResolver(t *testing.T) { name string
t.Parallel()
// remoteCommit denotes if the commitment broadcast was the remote
// commitment or not.
remoteCommit bool
// timeout denotes if the HTLC should be let timeout, or if the "remote"
// party should sweep it on-chain. This also affects what type of
// resolution message we expect.
timeout bool
// txToBroadcast is a function closure that should generate the
// transaction that should spend the HTLC output. Test authors can use
// this to customize the witness used when spending to trigger various
// redemption cases.
txToBroadcast func() (*wire.MsgTx, error)
// outcome is the resolver outcome that we expect to be reported once
// the contract is fully resolved.
outcome channeldb.ResolverOutcome
}
func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var ( var (
@@ -105,29 +125,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
}, },
} }
testCases := []struct { return []htlcTimeoutTestCase{
// name is a human readable description of the test case.
name string
// remoteCommit denotes if the commitment broadcast was the
// remote commitment or not.
remoteCommit bool
// timeout denotes if the HTLC should be let timeout, or if the
// "remote" party should sweep it on-chain. This also affects
// what type of resolution message we expect.
timeout bool
// txToBroadcast is a function closure that should generate the
// transaction that should spend the HTLC output. Test authors
// can use this to customize the witness used when spending to
// trigger various redemption cases.
txToBroadcast func() (*wire.MsgTx, error)
// outcome is the resolver outcome that we expect to be reported
// once the contract is fully resolved.
outcome channeldb.ResolverOutcome
}{
// Remote commitment is broadcast, we time out the HTLC on // Remote commitment is broadcast, we time out the HTLC on
// chain, and should expect a fail HTLC resolution. // chain, and should expect a fail HTLC resolution.
{ {
@@ -149,7 +147,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// immediately if the witness is already set // immediately if the witness is already set
// correctly. // correctly.
if reflect.DeepEqual( if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness, templateTx.TxIn[0].Witness,
witness,
) { ) {
return templateTx, nil return templateTx, nil
@@ -219,7 +218,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// immediately if the witness is already set // immediately if the witness is already set
// correctly. // correctly.
if reflect.DeepEqual( if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness, templateTx.TxIn[0].Witness,
witness,
) { ) {
return templateTx, nil return templateTx, nil
@@ -253,7 +253,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// immediately if the witness is already set // immediately if the witness is already set
// correctly. // correctly.
if reflect.DeepEqual( if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness, templateTx.TxIn[0].Witness,
witness,
) { ) {
return templateTx, nil return templateTx, nil
@@ -265,17 +266,25 @@ func TestHtlcTimeoutResolver(t *testing.T) {
outcome: channeldb.ResolverOutcomeClaimed, outcome: channeldb.ResolverOutcomeClaimed,
}, },
} }
}
func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var fakePreimage lntypes.Preimage
fakeSignDesc := &input.SignDescriptor{
Output: &wire.TxOut{},
}
copy(fakePreimage[:], fakePreimageBytes)
notifier := &mock.ChainNotifier{ notifier := &mock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch), EpochChan: make(chan *chainntnfs.BlockEpoch),
SpendChan: make(chan *chainntnfs.SpendDetail), SpendChan: make(chan *chainntnfs.SpendDetail),
ConfChan: make(chan *chainntnfs.TxConfirmation), ConfChan: make(chan *chainntnfs.TxConfirmation),
} }
witnessBeacon := newMockWitnessBeacon() witnessBeacon := newMockWitnessBeacon()
for _, testCase := range testCases {
t.Logf("Running test case: %v", testCase.name)
checkPointChan := make(chan struct{}, 1) checkPointChan := make(chan struct{}, 1)
incubateChan := make(chan struct{}, 1) incubateChan := make(chan struct{}, 1)
resolutionChan := make(chan ResolutionMsg, 1) resolutionChan := make(chan ResolutionMsg, 1)
@@ -285,6 +294,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
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],
@@ -302,10 +312,13 @@ func TestHtlcTimeoutResolver(t *testing.T) {
} }
resolutionChan <- msgs[0] resolutionChan <- msgs[0]
return nil return nil
}, },
Budget: *DefaultBudgetConfig(), Budget: *DefaultBudgetConfig(),
QueryIncomingCircuit: func(circuit models.CircuitKey) *models.CircuitKey { QueryIncomingCircuit: func(circuit models.CircuitKey,
) *models.CircuitKey {
return nil return nil
}, },
}, },
@@ -357,13 +370,15 @@ func TestHtlcTimeoutResolver(t *testing.T) {
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)
} }
} }
@@ -381,10 +396,21 @@ func TestHtlcTimeoutResolver(t *testing.T) {
} }
}() }()
// At 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):
@@ -440,7 +466,6 @@ func TestHtlcTimeoutResolver(t *testing.T) {
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.
@@ -503,6 +528,19 @@ func TestHtlcTimeoutResolver(t *testing.T) {
t.Fatalf("resolver should be marked as resolved") t.Fatalf("resolver should be marked as resolved")
} }
} }
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
// variations of possible local+remote spends.
func TestHtlcTimeoutResolver(t *testing.T) {
t.Parallel()
testCases := genHtlcTimeoutTestCases()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
testHtlcTimeoutResolver(t, testCase)
})
}
} }
// NOTE: the following tests essentially checks many of the same scenarios as // NOTE: the following tests essentially checks many of the same scenarios as
@@ -536,15 +574,12 @@ 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.
preCheckpoint: func(ctx *htlcResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error { _ bool) error {
// The nursery will create and publish a sweep // The nursery will create and publish a sweep
// tx. // tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
@@ -570,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,
@@ -653,6 +688,7 @@ func TestHtlcTimeoutSecondStage(t *testing.T) {
// that our sweep succeeded. // that our sweep succeeded.
preCheckpoint: func(ctx *htlcResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error { _ bool) error {
// The nursery will publish the timeout tx. // The nursery will publish the timeout tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: timeoutTx, SpendingTx: timeoutTx,
@@ -824,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}
@@ -870,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.
@@ -919,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,
@@ -1298,6 +1330,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()
defer timeout()() defer timeout()()
// We first run the resolver from start to finish, ensuring it gets // We first run the resolver from start to finish, ensuring it gets