2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-11 13:11:12 +00:00
loop/loopout_test.go
sputn1ck 960a78be3d
protocol: set musig2 to be the stable version
This commit changes the stable protocol version to be the Musig2 protocol.
The experimental version is set to the stable version
in order for the flag to still work if a user has it set.
2023-04-25 17:26:01 +02:00

1018 lines
30 KiB
Go

package loop
import (
"context"
"errors"
"math"
"testing"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
)
// TestLoopOutPaymentParameters tests the first part of the loop out process up
// to the point where the off-chain payments are made.
func TestLoopOutPaymentParameters(t *testing.T) {
t.Run("stable protocol", func(t *testing.T) {
testLoopOutPaymentParameters(t)
})
t.Run("experimental protocol", func(t *testing.T) {
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
testLoopOutPaymentParameters(t)
})
}
// TestLoopOutPaymentParameters tests the first part of the loop out process up
// to the point where the off-chain payments are made.
func testLoopOutPaymentParameters(t *testing.T) {
defer test.Guard(t)()
// Set up test context objects.
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
store := newStoreMock(t)
expiryChan := make(chan time.Time)
timerFactory := func(_ time.Duration) <-chan time.Time {
return expiryChan
}
height := int32(600)
cfg := &swapConfig{
lnd: &lnd.LndServices,
store: store,
server: server,
}
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
const maxParts = uint32(5)
chanSet := loopdb.ChannelSet{2, 3}
// Initiate the swap.
req := *testRequest
req.OutgoingChanSet = chanSet
initResult, err := newLoopOutSwap(
context.Background(), cfg, height, &req,
)
require.NoError(t, err)
swap := initResult.swap
// Execute the swap in its own goroutine.
errChan := make(chan error)
swapCtx, cancel := context.WithCancel(context.Background())
go func() {
err := swap.execute(swapCtx, &executeConfig{
statusChan: statusChan,
sweeper: sweeper,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
loopOutMaxParts: maxParts,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail,
}, height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
store.assertLoopOutStored()
state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State)
// Check that the SwapInfo contains the outgoing chan set
require.Equal(t, chanSet, state.OutgoingChanSet)
// Check that the SwapInfo does not contain a last hop
require.Nil(t, state.LastHop)
// Intercept the swap and prepay payments. Order is undefined.
payments := []test.RouterPaymentChannelMessage{
<-ctx.Lnd.RouterSendPaymentChannel,
<-ctx.Lnd.RouterSendPaymentChannel,
}
// Find the swap payment.
var swapPayment test.RouterPaymentChannelMessage
for _, p := range payments {
if p.Invoice == swap.SwapInvoice {
swapPayment = p
}
}
// Assert that it is sent as a multi-part payment.
require.Equal(t, maxParts, swapPayment.MaxParts)
// Verify the outgoing channel set restriction.
require.Equal(
t, []uint64(req.OutgoingChanSet), swapPayment.OutgoingChanIds,
)
// Swap is expected to register for confirmation of the htlc. Assert
// this to prevent a blocked channel in the mock.
ctx.AssertRegisterConf(false, defaultConfirmations)
// Cancel the swap. There is nothing else we need to assert. The payment
// parameters don't play a role in the remainder of the swap process.
cancel()
// Expect the swap to signal that it was cancelled.
require.Equal(t, context.Canceled, <-errChan)
}
// TestLateHtlcPublish tests that the client is not revealing the preimage if
// there are not enough blocks left.
func TestLateHtlcPublish(t *testing.T) {
t.Run("stable protocol", func(t *testing.T) {
testLateHtlcPublish(t)
})
t.Run("experimental protocol", func(t *testing.T) {
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
testLateHtlcPublish(t)
})
}
func testLateHtlcPublish(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
store := newStoreMock(t)
expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time {
return expiryChan
}
height := int32(600)
cfg := newSwapConfig(&lnd.LndServices, store, server)
testRequest.Expiry = height + testLoopOutMinOnChainCltvDelta
initResult, err := newLoopOutSwap(
context.Background(), cfg, height, testRequest,
)
require.NoError(t, err)
swap := initResult.swap
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan,
sweeper: sweeper,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail,
}, height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
store.assertLoopOutStored()
status := <-statusChan
require.Equal(t, loopdb.StateInitiated, status.State)
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf
ctx.AssertRegisterConf(false, defaultConfirmations)
// // Wait too long before publishing htlc.
blockEpochChan <- swap.CltvExpiry - 10
signalSwapPaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
signalPrepaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
store.assertStoreFinished(loopdb.StateFailTimeout)
status = <-statusChan
require.Equal(t, loopdb.StateFailTimeout, status.State)
require.NoError(t, <-errChan)
}
// TestCustomSweepConfTarget ensures we are able to sweep a Loop Out HTLC with a
// custom confirmation target.
func TestCustomSweepConfTarget(t *testing.T) {
t.Run("stable protocol", func(t *testing.T) {
testCustomSweepConfTarget(t)
})
t.Run("experimental protocol", func(t *testing.T) {
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
testCustomSweepConfTarget(t)
})
}
func testCustomSweepConfTarget(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
// Use the highest sweep confirmation target before we attempt to use
// the default.
testReq := *testRequest
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta - 1
// Set on-chain HTLC CLTV.
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
// Set up custom fee estimates such that the lower confirmation target
// yields a much higher fee rate.
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, 250)
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server,
)
initResult, err := newLoopOutSwap(
context.Background(), cfg, ctx.Lnd.Height, &testReq,
)
require.NoError(t, err)
swap := initResult.swap
// Set up the required dependencies to execute the swap.
//
// TODO: create test context similar to loopInTestContext.
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time {
return expiryChan
}
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
sweeper: sweeper,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail,
}, ctx.Lnd.Height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
// The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored()
state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State)
// We'll then pay both the swap and prepay invoice, which should trigger
// the server to publish the on-chain HTLC.
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
signalSwapPaymentResult(nil)
signalPrepaymentResult(nil)
// Notify the confirmation notification for the HTLC.
ctx.AssertRegisterConf(false, defaultConfirmations)
blockEpochChan <- ctx.Lnd.Height + 1
htlcTx := wire.NewMsgTx(2)
htlcTx.AddTxOut(&wire.TxOut{
Value: int64(swap.AmountRequested),
PkScript: swap.htlc.PkScript,
})
ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for
// preimage push tracking.
trackPayment := ctx.AssertTrackPayment()
expiryChan <- time.Now()
// Expect a signing request for the HTLC success transaction.
if !IsTaprootSwap(&swap.SwapContract) {
<-ctx.Lnd.SignOutputRawChannel
}
cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed)
status := <-statusChan
require.Equal(t, loopdb.StatePreimageRevealed, status.State)
// When using taproot htlcs the flow is different as we do reveal the
// preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts.
if IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
// Try MuSig2 signing first and fail it so that we go for a
// normal sweep.
for i := 0; i < maxMusigSweepRetries; i++ {
expiryChan <- time.Now()
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
<-ctx.Lnd.SignOutputRawChannel
}
// assertSweepTx performs some sanity checks on a sweep transaction to
// ensure it was constructed correctly.
assertSweepTx := func(expConfTarget int32) *wire.MsgTx {
t.Helper()
sweepTx := ctx.ReceiveTx()
require.Equal(
t, htlcTx.TxHash(),
sweepTx.TxIn[0].PreviousOutPoint.Hash,
)
// The fee used for the sweep transaction is an estimate based
// on the maximum witness size, so we should expect to see a
// lower fee when using the actual witness size of the
// transaction.
fee := btcutil.Amount(
htlcTx.TxOut[0].Value - sweepTx.TxOut[0].Value,
)
weight := blockchain.GetTransactionWeight(btcutil.NewTx(sweepTx))
feeRate, err := ctx.Lnd.WalletKit.EstimateFeeRate(
context.Background(), expConfTarget,
)
require.NoError(t, err, "unable to retrieve fee estimate")
minFee := feeRate.FeeForWeight(weight)
// Just an estimate that works to sanity check fee upper bound.
maxFee := btcutil.Amount(float64(minFee) * 1.5)
require.GreaterOrEqual(t, fee, minFee)
require.LessOrEqual(t, fee, maxFee)
return sweepTx
}
// The sweep should have a fee that corresponds to the custom
// confirmation target.
_ = assertSweepTx(testReq.SweepConfTarget)
// Once we have published an on chain sweep, we expect a preimage to
// have been pushed to our server.
if !IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// Now that we have pushed our preimage to the sever, we send an update
// indicating that our off chain htlc is settled. We do this so that
// we don't have to keep consuming preimage pushes from our server mock
// for every sweep attempt.
trackPayment.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_SUCCEEDED,
}
// We'll then notify the height at which we begin using the default
// confirmation target.
defaultConfTargetHeight := ctx.Lnd.Height +
testLoopOutMinOnChainCltvDelta - DefaultSweepConfTargetDelta
blockEpochChan <- defaultConfTargetHeight
expiryChan <- time.Now()
// Expect another signing request.
<-ctx.Lnd.SignOutputRawChannel
// We should expect to see another sweep using the higher fee since the
// spend hasn't been confirmed yet.
sweepTx := assertSweepTx(DefaultSweepConfTarget)
// Notify the spend so that the swap reaches its final state.
ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
status = <-statusChan
require.Equal(t, loopdb.StateSuccess, status.State)
require.NoError(t, <-errChan)
}
// TestPreimagePush tests or logic that decides whether to push our preimage to
// the server. First, we test the case where we have not yet disclosed our
// preimage with a sweep, so we do not want to push our preimage yet. Next, we
// broadcast a sweep attempt and push our preimage to the server. In this stage
// we mock a server failure by not sending a settle update for our payment.
// Finally, we make a last sweep attempt, push the preimage (because we have
// not detected our settle) and settle the off chain htlc, indicating that the
// server successfully settled using the preimage push. In this test, we need
// to start with a fee rate that will be too high, then progress to an
// acceptable one.
func TestPreimagePush(t *testing.T) {
t.Run("stable protocol", func(t *testing.T) {
testPreimagePush(t)
})
t.Run("experimental protocol", func(t *testing.T) {
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
testPreimagePush(t)
})
}
func testPreimagePush(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
testReq := *testRequest
testReq.SweepConfTarget = 10
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
// We set our mock fee estimate for our target sweep confs to be our
// max miner fee *2, so that our fee will definitely be above what we
// are willing to pay, and we will not sweep.
ctx.Lnd.SetFeeEstimate(
testReq.SweepConfTarget, chainfee.SatPerKWeight(
testReq.MaxMinerFee*2,
),
)
cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server,
)
initResult, err := newLoopOutSwap(
context.Background(), cfg, ctx.Lnd.Height, &testReq,
)
require.NoError(t, err)
swap := initResult.swap
// Set up the required dependencies to execute the swap.
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
expiryChan := make(chan time.Time)
timerFactory := func(_ time.Duration) <-chan time.Time {
return expiryChan
}
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
sweeper: sweeper,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail,
}, ctx.Lnd.Height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
// The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored()
state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State)
// We'll then pay both the swap and prepay invoice, which should trigger
// the server to publish the on-chain HTLC.
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
signalSwapPaymentResult(nil)
signalPrepaymentResult(nil)
// Notify the confirmation notification for the HTLC.
ctx.AssertRegisterConf(false, defaultConfirmations)
blockEpochChan <- ctx.Lnd.Height + 1
htlcTx := wire.NewMsgTx(2)
htlcTx.AddTxOut(&wire.TxOut{
Value: int64(swap.AmountRequested),
PkScript: swap.htlc.PkScript,
})
ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for
// preimage push tracking.
trackPayment := ctx.AssertTrackPayment()
// Tick the expiry channel, we are still using our client confirmation
// target at this stage which has fees higher than our max acceptable
// fee. We do not expect a sweep attempt at this point. Since our
// preimage is not revealed, we also do not expect a preimage push.
expiryChan <- testTime
// When using taproot htlcs the flow is different as we do reveal the
// preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts.
if IsTaprootSwap(&swap.SwapContract) {
cfg.store.(*storeMock).assertLoopOutState(
loopdb.StatePreimageRevealed,
)
status := <-statusChan
require.Equal(
t, status.State, loopdb.StatePreimageRevealed,
)
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
// Try MuSig2 signing first and fail it so that we go for a
// normal sweep.
for i := 0; i < maxMusigSweepRetries; i++ {
expiryChan <- time.Now()
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
<-ctx.Lnd.SignOutputRawChannel
// We expect the sweep tx to have been published.
ctx.ReceiveTx()
}
// Since we don't have a reliable mechanism to non-intrusively avoid
// races by setting the fee estimate too soon, let's sleep here a bit
// to ensure the first sweep fails.
time.Sleep(500 * time.Millisecond)
// Now we decrease our fees for the swap's confirmation target to less
// than the maximum miner fee.
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
testReq.MaxMinerFee/2,
))
// Now when we report a new block and tick our expiry fee timer, and
// fees are acceptably low so we expect our sweep to be published.
blockEpochChan <- ctx.Lnd.Height + 2
expiryChan <- testTime
if IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// Expect a signing request for the HTLC success transaction.
<-ctx.Lnd.SignOutputRawChannel
if !IsTaprootSwap(&swap.SwapContract) {
// This is the first time we have swept, so we expect our
// preimage revealed state to be set.
cfg.store.(*storeMock).assertLoopOutState(
loopdb.StatePreimageRevealed,
)
status := <-statusChan
require.Equal(
t, status.State, loopdb.StatePreimageRevealed,
)
}
// We expect the sweep tx to have been published.
ctx.ReceiveTx()
if !IsTaprootSwap(&swap.SwapContract) {
// Once we have published an on chain sweep, we expect a
// preimage to have been pushed to the server after the sweep.
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// To mock a server failure, we do not send a payment settled update
// for our off chain payment yet. We also do not confirm our sweep on
// chain yet so we can test our preimage push retry logic. Instead, we
// tick the expiry chan again to prompt another sweep.
expiryChan <- testTime
if IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// We expect another signing request for out sweep, and publish of our
// sweep transaction.
<-ctx.Lnd.SignOutputRawChannel
ctx.ReceiveTx()
// Since we have not yet been notified of an off chain settle, and we
// have attempted to sweep again, we expect another preimage push
// attempt.
if !IsTaprootSwap(&swap.SwapContract) {
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
}
// This time, we send a payment succeeded update into our payment stream
// to reflect that the server received our preimage push and settled off
// chain.
trackPayment.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_SUCCEEDED,
}
// We tick one last time, this time expecting a sweep but no preimage
// push. The test's mocked preimage channel is un-buffered, so our test
// would hang if we pushed the preimage here.
expiryChan <- testTime
<-ctx.Lnd.SignOutputRawChannel
sweepTx := ctx.ReceiveTx()
// Finally, we put this swap out of its misery and notify a successful
// spend our our sweepTx and assert that the swap succeeds.
ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
status := <-statusChan
require.Equal(
t, status.State, loopdb.StateSuccess,
)
require.NoError(t, <-errChan)
}
// TestFailedOffChainCancelation tests sending of a cancelation message to
// the server when a swap fails due to off-chain routing.
func TestFailedOffChainCancelation(t *testing.T) {
t.Run("stable protocol", func(t *testing.T) {
testFailedOffChainCancelation(t)
})
t.Run("experimental protocol", func(t *testing.T) {
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
testFailedOffChainCancelation(t)
})
}
func testFailedOffChainCancelation(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
testReq := *testRequest
testReq.Expiry = lnd.Height + 20
cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server,
)
initResult, err := newLoopOutSwap(
context.Background(), cfg, lnd.Height, &testReq,
)
require.NoError(t, err)
swap := initResult.swap
// Set up the required dependencies to execute the swap.
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
expiryChan := make(chan time.Time)
timerFactory := func(_ time.Duration) <-chan time.Time {
return expiryChan
}
errChan := make(chan error)
go func() {
cfg := &executeConfig{
statusChan: statusChan,
sweeper: sweeper,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigFail,
}
err := swap.execute(context.Background(), cfg, ctx.Lnd.Height)
errChan <- err
}()
// The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored()
state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State)
// Assert that we register for htlc confirmation notifications.
ctx.AssertRegisterConf(false, defaultConfirmations)
// We expect prepayment and invoice to be dispatched, order is unknown.
pmt1 := <-ctx.Lnd.RouterSendPaymentChannel
pmt2 := <-ctx.Lnd.RouterSendPaymentChannel
failUpdate := lndclient.PaymentStatus{
State: lnrpc.Payment_FAILED,
FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_ERROR,
Htlcs: []*lndclient.HtlcAttempt{
{
// Include a non-failed htlc to test that we
// only report failed htlcs.
Status: lnrpc.HTLCAttempt_IN_FLIGHT,
},
// Add one htlc that failed within the server's
// infrastructure.
{
Status: lnrpc.HTLCAttempt_FAILED,
Route: &lnrpc.Route{
Hops: []*lnrpc.Hop{
{}, {}, {},
},
},
Failure: &lndclient.HtlcFailure{
FailureSourceIndex: 1,
},
},
// Add one htlc that failed in the network at wide.
{
Status: lnrpc.HTLCAttempt_FAILED,
Route: &lnrpc.Route{
Hops: []*lnrpc.Hop{
{}, {}, {}, {}, {},
},
},
Failure: &lndclient.HtlcFailure{
FailureSourceIndex: 1,
},
},
},
}
successUpdate := lndclient.PaymentStatus{
State: lnrpc.Payment_SUCCEEDED,
}
// We want to fail our swap payment and succeed the prepush, so we send
// a failure update to the payment that has the larger amount.
if pmt1.Amount > pmt2.Amount {
pmt1.TrackPaymentMessage.Updates <- failUpdate
pmt2.TrackPaymentMessage.Updates <- successUpdate
} else {
pmt1.TrackPaymentMessage.Updates <- successUpdate
pmt2.TrackPaymentMessage.Updates <- failUpdate
}
invoice, err := zpay32.Decode(
swap.LoopOutContract.SwapInvoice, lnd.ChainParams,
)
require.NoError(t, err)
require.NotNil(t, invoice.PaymentAddr)
swapCancelation := &outCancelDetails{
hash: swap.hash,
paymentAddr: *invoice.PaymentAddr,
metadata: routeCancelMetadata{
paymentType: paymentTypeInvoice,
failureReason: failUpdate.FailureReason,
attempts: []uint32{
2,
math.MaxUint32,
},
},
}
server.assertSwapCanceled(t, swapCancelation)
// Finally, the swap should be recorded with failed off chain timeout.
cfg.store.(*storeMock).assertLoopOutState(
loopdb.StateFailOffchainPayments,
)
state = <-statusChan
require.Equal(t, state.State, loopdb.StateFailOffchainPayments)
require.NoError(t, <-errChan)
}
// TestLoopOutMuSig2Sweep tests the loop out sweep flow when the MuSig2 signing
// process is successful.
func TestLoopOutMuSig2Sweep(t *testing.T) {
defer test.Guard(t)()
// TODO(bhandras): remove when MuSig2 is default.
loopdb.EnableExperimentalProtocol()
defer loopdb.ResetCurrentProtocolVersion()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
testReq := *testRequest
testReq.SweepConfTarget = 10
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
// We set our mock fee estimate for our target sweep confs to be our
// max miner fee * 2. With MuSig2 we still expect that the client will
// publish the sweep but with the fee clamped to the maximum allowed
// miner fee as the preimage is revealed before the sweep txn is
// published.
ctx.Lnd.SetFeeEstimate(
testReq.SweepConfTarget, chainfee.SatPerKWeight(
testReq.MaxMinerFee*2,
),
)
cfg := newSwapConfig(
&lnd.LndServices, newStoreMock(t), server,
)
initResult, err := newLoopOutSwap(
context.Background(), cfg, ctx.Lnd.Height, &testReq,
)
require.NoError(t, err)
swap := initResult.swap
// Set up the required dependencies to execute the swap.
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
expiryChan := make(chan time.Time)
timerFactory := func(_ time.Duration) <-chan time.Time {
return expiryChan
}
errChan := make(chan error)
// Mock a successful signature verify to make sure we don't fail
// creating the MuSig2 sweep.
mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash,
sig []byte) error {
return nil
}
go func() {
err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
sweeper: sweeper,
cancelSwap: server.CancelLoopOutSwap,
verifySchnorrSig: mockVerifySchnorrSigSuccess,
}, ctx.Lnd.Height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
// The swap should be found in its initial state.
cfg.store.(*storeMock).assertLoopOutStored()
state := <-statusChan
require.Equal(t, loopdb.StateInitiated, state.State)
// We'll then pay both the swap and prepay invoice, which should trigger
// the server to publish the on-chain HTLC.
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
signalSwapPaymentResult(nil)
signalPrepaymentResult(nil)
// Notify the confirmation notification for the HTLC.
ctx.AssertRegisterConf(false, defaultConfirmations)
blockEpochChan <- ctx.Lnd.Height + 1
htlcTx := wire.NewMsgTx(2)
htlcTx.AddTxOut(&wire.TxOut{
Value: int64(swap.AmountRequested),
PkScript: swap.htlc.PkScript,
})
ctx.NotifyConf(htlcTx)
// The client should then register for a spend of the HTLC and attempt
// to sweep it using the custom confirmation target.
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
// Assert that we made a query to track our payment, as required for
// preimage push tracking.
trackPayment := ctx.AssertTrackPayment()
// Tick the expiry channel, we are still using our client confirmation
// target at this stage which has fees higher than our max acceptable
// fee. We do not expect a sweep attempt at this point. Since our
// preimage is not revealed, we also do not expect a preimage push.
expiryChan <- testTime
// When using taproot htlcs the flow is different as we do reveal the
// preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts.
cfg.store.(*storeMock).assertLoopOutState(
loopdb.StatePreimageRevealed,
)
status := <-statusChan
require.Equal(
t, status.State, loopdb.StatePreimageRevealed,
)
preimage := <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
// We expect the sweep tx to have been published.
ctx.ReceiveTx()
// Since we don't have a reliable mechanism to non-intrusively avoid
// races by setting the fee estimate too soon, let's sleep here a bit
// to ensure the first sweep fails.
time.Sleep(500 * time.Millisecond)
// Now we decrease our fees for the swap's confirmation target to less
// than the maximum miner fee.
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
testReq.MaxMinerFee/2,
))
// Now when we report a new block and tick our expiry fee timer, and
// fees are acceptably low so we expect our sweep to be published.
blockEpochChan <- ctx.Lnd.Height + 2
expiryChan <- testTime
preimage = <-server.preimagePush
require.Equal(t, swap.Preimage, preimage)
// We expect the sweep tx to have been published.
sweepTx := ctx.ReceiveTx()
// This time, we send a payment succeeded update into our payment stream
// to reflect that the server received our preimage push and settled off
// chain.
trackPayment.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_SUCCEEDED,
}
// Make sure our sweep tx has a single witness indicating keyspend.
require.Len(t, sweepTx.TxIn[0].Witness, 1)
// Finally, we put this swap out of its misery and notify a successful
// spend our our sweepTx and assert that the swap succeeds.
ctx.NotifySpend(sweepTx, 0)
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
status = <-statusChan
require.Equal(t, status.State, loopdb.StateSuccess)
require.NoError(t, <-errChan)
}