mirror of
https://github.com/lightninglabs/loop
synced 2024-11-04 06:00:21 +00:00
1018 lines
30 KiB
Go
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 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 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)
|
|
}
|