You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/loopout_test.go

596 lines
17 KiB
Go

package loop
import (
"context"
"errors"
"reflect"
"testing"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"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/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) {
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 = 5
// Initiate the swap.
req := *testRequest
req.OutgoingChanSet = loopdb.ChannelSet{2, 3}
initResult, err := newLoopOutSwap(
context.Background(), cfg, height, &req,
)
if err != nil {
t.Fatal(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,
}, height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
store.assertLoopOutStored()
state := <-statusChan
if state.State != loopdb.StateInitiated {
t.Fatal("unexpected state")
}
// 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.
if swapPayment.MaxParts != maxParts {
t.Fatalf("Expected %v parts, but got %v",
maxParts, swapPayment.MaxParts)
}
// Verify the outgoing channel set restriction.
if !reflect.DeepEqual(
[]uint64(req.OutgoingChanSet), swapPayment.OutgoingChanIds,
) {
t.Fatalf("Unexpected outgoing channel set")
}
// 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.
err = <-errChan
if err != context.Canceled {
t.Fatal(err)
}
}
// TestLateHtlcPublish tests that the client is not revealing the preimage if
// there are not enough blocks left.
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,
)
if err != nil {
t.Fatal(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,
}, height)
if err != nil {
log.Error(err)
}
errChan <- err
}()
store.assertLoopOutStored()
state := <-statusChan
if state.State != loopdb.StateInitiated {
t.Fatal("unexpected 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 <- int32(swap.CltvExpiry - 10)
signalSwapPaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
signalPrepaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
store.assertStoreFinished(loopdb.StateFailTimeout)
status := <-statusChan
if status.State != loopdb.StateFailTimeout {
t.Fatal("unexpected state")
}
err = <-errChan
if err != nil {
t.Fatal(err)
}
}
// TestCustomSweepConfTarget ensures we are able to sweep a Loop Out HTLC with a
// custom confirmation target.
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 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,
)
if err != nil {
t.Fatal(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,
}, 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
if state.State != loopdb.StateInitiated {
t.Fatal("unexpected 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.
<-ctx.Lnd.SignOutputRawChannel
cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed)
status := <-statusChan
if status.State != loopdb.StatePreimageRevealed {
t.Fatalf("expected state %v, got %v",
loopdb.StatePreimageRevealed, status.State)
}
// 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()
if sweepTx.TxIn[0].PreviousOutPoint.Hash != htlcTx.TxHash() {
t.Fatalf("expected sweep tx to spend %v, got %v",
htlcTx.TxHash(), sweepTx.TxIn[0].PreviousOutPoint)
}
// 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.EstimateFee(
context.Background(), expConfTarget,
)
if err != nil {
t.Fatalf("unable to retrieve fee estimate: %v", err)
}
minFee := feeRate.FeeForWeight(weight)
maxFee := btcutil.Amount(float64(minFee) * 1.1)
if fee < minFee && fee > maxFee {
t.Fatalf("expected sweep tx to have fee between %v-%v, "+
"got %v", minFee, maxFee, fee)
}
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.
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 <- int32(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
if status.State != loopdb.StateSuccess {
t.Fatalf("expected state %v, got %v", loopdb.StateSuccess,
status.State)
}
if err := <-errChan; err != nil {
t.Fatal(err)
}
}
// 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. We do this by starting with a high confirmation target with
// a high fee, and setting the default confirmation fee (which our swap will
// drop down to if it is not confirming in time) to a lower fee. This is not
// intuitive (lower confs having lower fees), but it allows up to mock fee
// changes.
func TestPreimagePush(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock(lnd)
// Start with a high confirmation delta which will have a very high fee
// attached to it.
testReq := *testRequest
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta - 1
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,
),
)
// We set the fee estimate for our default confirmation target very
// low, so that once we drop down to our default confs we will start
// trying to sweep the preimage.
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 1)
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,
}, 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
// Now, we notify the height at which the client will start using the
// default confirmation target. This has the effect of lowering our fees
// so that the client still start sweeping.
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta
blockEpochChan <- defaultConfTargetHeight
// This time when we tick the expiry chan, our fees are lower than the
// swap max, so we expect it to prompt a sweep.
expiryChan <- testTime
// Expect a signing request for the HTLC success transaction.
<-ctx.Lnd.SignOutputRawChannel
// 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()
// 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
// 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.
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)
}