mirror of
https://github.com/lightninglabs/loop
synced 2024-11-04 06:00:21 +00:00
491 lines
11 KiB
Go
491 lines
11 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
"github.com/lightninglabs/loop/swap"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
)
|
|
|
|
var (
|
|
testLoopInRequest = LoopInRequest{
|
|
Amount: btcutil.Amount(50000),
|
|
MaxSwapFee: btcutil.Amount(1000),
|
|
HtlcConfTarget: 2,
|
|
}
|
|
)
|
|
|
|
// TestLoopInSuccess tests the success scenario where the swap completes the
|
|
// happy flow.
|
|
func TestLoopInSuccess(t *testing.T) {
|
|
defer test.Guard(t)()
|
|
|
|
ctx := newLoopInTestContext(t)
|
|
|
|
height := int32(600)
|
|
|
|
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server)
|
|
|
|
initResult, err := newLoopInSwap(
|
|
context.Background(), cfg,
|
|
height, &testLoopInRequest,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
swap := initResult.swap
|
|
|
|
ctx.store.assertLoopInStored()
|
|
|
|
errChan := make(chan error)
|
|
go func() {
|
|
err := swap.execute(context.Background(), ctx.cfg, height)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
errChan <- err
|
|
}()
|
|
|
|
ctx.assertState(loopdb.StateInitiated)
|
|
|
|
ctx.assertState(loopdb.StateHtlcPublished)
|
|
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
|
|
// Expect htlc to be published.
|
|
htlcTx := <-ctx.lnd.SendOutputsChannel
|
|
|
|
// Expect the same state to be written again with the htlc tx hash.
|
|
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
require.NotNil(t, state.HtlcTxHash)
|
|
|
|
// Expect register for htlc conf.
|
|
<-ctx.lnd.RegisterConfChannel
|
|
<-ctx.lnd.RegisterConfChannel
|
|
|
|
// Confirm htlc.
|
|
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
|
|
Tx: &htlcTx,
|
|
}
|
|
|
|
// Client starts listening for spend of htlc.
|
|
<-ctx.lnd.RegisterSpendChannel
|
|
|
|
// Client starts listening for swap invoice updates.
|
|
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
|
|
if subscription.Hash != ctx.server.swapHash {
|
|
t.Fatal("client subscribing to wrong invoice")
|
|
}
|
|
|
|
// Server has already paid invoice before spending the htlc. Signal
|
|
// settled.
|
|
subscription.Update <- lndclient.InvoiceUpdate{
|
|
State: channeldb.ContractSettled,
|
|
AmtPaid: 49000,
|
|
}
|
|
|
|
// Swap is expected to move to the state InvoiceSettled
|
|
ctx.assertState(loopdb.StateInvoiceSettled)
|
|
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
|
|
|
|
// Server spends htlc.
|
|
successTx := wire.MsgTx{}
|
|
successTx.AddTxIn(&wire.TxIn{
|
|
Witness: [][]byte{{}, {}, {}},
|
|
})
|
|
|
|
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
|
|
SpendingTx: &successTx,
|
|
SpenderInputIndex: 0,
|
|
}
|
|
|
|
ctx.assertState(loopdb.StateSuccess)
|
|
ctx.store.assertLoopInState(loopdb.StateSuccess)
|
|
|
|
err = <-errChan
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestLoopInTimeout tests scenarios where the server doesn't sweep the htlc
|
|
// and the client is forced to reclaim the funds using the timeout tx.
|
|
func TestLoopInTimeout(t *testing.T) {
|
|
testAmt := int64(testLoopInRequest.Amount)
|
|
t.Run("internal htlc", func(t *testing.T) {
|
|
testLoopInTimeout(t, swap.HtlcP2WSH, 0)
|
|
})
|
|
|
|
outputTypes := []swap.HtlcOutputType{swap.HtlcP2WSH, swap.HtlcNP2WSH}
|
|
|
|
for _, outputType := range outputTypes {
|
|
outputType := outputType
|
|
t.Run(outputType.String(), func(t *testing.T) {
|
|
t.Run("external htlc", func(t *testing.T) {
|
|
testLoopInTimeout(t, outputType, testAmt)
|
|
})
|
|
|
|
t.Run("external amount too high", func(t *testing.T) {
|
|
testLoopInTimeout(t, outputType, testAmt+1)
|
|
})
|
|
|
|
t.Run("external amount too low", func(t *testing.T) {
|
|
testLoopInTimeout(t, outputType, testAmt-1)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func testLoopInTimeout(t *testing.T,
|
|
outputType swap.HtlcOutputType, externalValue int64) {
|
|
|
|
defer test.Guard(t)()
|
|
|
|
ctx := newLoopInTestContext(t)
|
|
|
|
height := int32(600)
|
|
|
|
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server)
|
|
|
|
req := testLoopInRequest
|
|
if externalValue != 0 {
|
|
req.ExternalHtlc = true
|
|
}
|
|
|
|
initResult, err := newLoopInSwap(
|
|
context.Background(), cfg,
|
|
height, &req,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := initResult.swap
|
|
|
|
ctx.store.assertLoopInStored()
|
|
|
|
errChan := make(chan error)
|
|
go func() {
|
|
err := s.execute(context.Background(), ctx.cfg, height)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
errChan <- err
|
|
}()
|
|
|
|
ctx.assertState(loopdb.StateInitiated)
|
|
|
|
ctx.assertState(loopdb.StateHtlcPublished)
|
|
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
|
|
var htlcTx wire.MsgTx
|
|
if externalValue == 0 {
|
|
// Expect htlc to be published.
|
|
htlcTx = <-ctx.lnd.SendOutputsChannel
|
|
|
|
// Expect the same state to be written again with the htlc tx hash.
|
|
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
require.NotNil(t, state.HtlcTxHash)
|
|
} else {
|
|
// Create an external htlc publish tx.
|
|
var pkScript []byte
|
|
if outputType == swap.HtlcNP2WSH {
|
|
pkScript = s.htlcNP2WSH.PkScript
|
|
} else {
|
|
pkScript = s.htlcP2WSH.PkScript
|
|
}
|
|
htlcTx = wire.MsgTx{
|
|
TxOut: []*wire.TxOut{
|
|
{
|
|
PkScript: pkScript,
|
|
Value: externalValue,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Expect register for htlc conf.
|
|
<-ctx.lnd.RegisterConfChannel
|
|
<-ctx.lnd.RegisterConfChannel
|
|
|
|
// Confirm htlc.
|
|
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
|
|
Tx: &htlcTx,
|
|
}
|
|
|
|
// Assert that the swap is failed in case of an invalid amount.
|
|
invalidAmt := externalValue != 0 && externalValue != int64(req.Amount)
|
|
if invalidAmt {
|
|
ctx.assertState(loopdb.StateFailIncorrectHtlcAmt)
|
|
ctx.store.assertLoopInState(loopdb.StateFailIncorrectHtlcAmt)
|
|
|
|
err = <-errChan
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Client starts listening for spend of htlc.
|
|
<-ctx.lnd.RegisterSpendChannel
|
|
|
|
// Client starts listening for swap invoice updates.
|
|
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
|
|
if subscription.Hash != ctx.server.swapHash {
|
|
t.Fatal("client subscribing to wrong invoice")
|
|
}
|
|
|
|
// Let htlc expire.
|
|
ctx.blockEpochChan <- s.LoopInContract.CltvExpiry
|
|
|
|
// Expect a signing request for the htlc tx output value.
|
|
signReq := <-ctx.lnd.SignOutputRawChannel
|
|
if signReq.SignDescriptors[0].Output.Value != htlcTx.TxOut[0].Value {
|
|
|
|
t.Fatal("invalid signing amount")
|
|
}
|
|
|
|
// Expect timeout tx to be published.
|
|
timeoutTx := <-ctx.lnd.TxPublishChannel
|
|
|
|
// Confirm timeout tx.
|
|
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
|
|
SpendingTx: timeoutTx,
|
|
SpenderInputIndex: 0,
|
|
}
|
|
|
|
// Now that timeout tx has confirmed, the client should be able to
|
|
// safely cancel the swap invoice.
|
|
<-ctx.lnd.FailInvoiceChannel
|
|
|
|
// Signal the the invoice was canceled.
|
|
subscription.Update <- lndclient.InvoiceUpdate{
|
|
State: channeldb.ContractCanceled,
|
|
}
|
|
|
|
ctx.assertState(loopdb.StateFailTimeout)
|
|
ctx.store.assertLoopInState(loopdb.StateFailTimeout)
|
|
|
|
err = <-errChan
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestLoopInResume tests resuming swaps in various states.
|
|
func TestLoopInResume(t *testing.T) {
|
|
storedVersion := []loopdb.ProtocolVersion{
|
|
loopdb.ProtocolVersionUnrecorded,
|
|
loopdb.ProtocolVersionHtlcV2,
|
|
}
|
|
|
|
htlcVersion := []swap.ScriptVersion{
|
|
swap.HtlcV1,
|
|
swap.HtlcV2,
|
|
}
|
|
|
|
for i, version := range storedVersion {
|
|
version := version
|
|
scriptVersion := htlcVersion[i]
|
|
|
|
t.Run(version.String(), func(t *testing.T) {
|
|
t.Run("initiated", func(t *testing.T) {
|
|
testLoopInResume(
|
|
t, loopdb.StateInitiated, false,
|
|
version, scriptVersion,
|
|
)
|
|
})
|
|
|
|
t.Run("initiated expired", func(t *testing.T) {
|
|
testLoopInResume(
|
|
t, loopdb.StateInitiated, true,
|
|
version, scriptVersion,
|
|
)
|
|
})
|
|
|
|
t.Run("htlc published", func(t *testing.T) {
|
|
testLoopInResume(
|
|
t, loopdb.StateHtlcPublished, false,
|
|
version, scriptVersion,
|
|
)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
|
|
storedVersion loopdb.ProtocolVersion, scriptVersion swap.ScriptVersion) {
|
|
|
|
defer test.Guard(t)()
|
|
|
|
ctx := newLoopInTestContext(t)
|
|
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server)
|
|
|
|
senderKey := [33]byte{4}
|
|
receiverKey := [33]byte{5}
|
|
|
|
contract := &loopdb.LoopInContract{
|
|
HtlcConfTarget: 2,
|
|
SwapContract: loopdb.SwapContract{
|
|
Preimage: testPreimage,
|
|
AmountRequested: 100000,
|
|
CltvExpiry: 744,
|
|
ReceiverKey: receiverKey,
|
|
SenderKey: senderKey,
|
|
MaxSwapFee: 60000,
|
|
MaxMinerFee: 50000,
|
|
ProtocolVersion: storedVersion,
|
|
},
|
|
}
|
|
pendSwap := &loopdb.LoopIn{
|
|
Contract: contract,
|
|
Loop: loopdb.Loop{
|
|
Events: []*loopdb.LoopEvent{
|
|
{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: state,
|
|
},
|
|
},
|
|
},
|
|
Hash: testPreimage.Hash(),
|
|
},
|
|
}
|
|
|
|
htlc, err := swap.NewHtlc(
|
|
scriptVersion, contract.CltvExpiry, contract.SenderKey,
|
|
contract.ReceiverKey, testPreimage.Hash(), swap.HtlcNP2WSH,
|
|
cfg.lnd.ChainParams,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = ctx.store.CreateLoopIn(testPreimage.Hash(), contract)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
swap, err := resumeLoopInSwap(
|
|
context.Background(), cfg,
|
|
pendSwap,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var height int32
|
|
if expired {
|
|
height = 740
|
|
} else {
|
|
height = 600
|
|
}
|
|
|
|
errChan := make(chan error)
|
|
go func() {
|
|
err := swap.execute(context.Background(), ctx.cfg, height)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
errChan <- err
|
|
}()
|
|
|
|
defer func() {
|
|
err = <-errChan
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.lnd.SendPaymentChannel:
|
|
t.Fatal("unexpected payment sent")
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case <-ctx.lnd.SendOutputsChannel:
|
|
t.Fatal("unexpected tx published")
|
|
default:
|
|
}
|
|
}()
|
|
|
|
var htlcTx wire.MsgTx
|
|
if state == loopdb.StateInitiated {
|
|
ctx.assertState(loopdb.StateInitiated)
|
|
|
|
if expired {
|
|
ctx.assertState(loopdb.StateFailTimeout)
|
|
return
|
|
}
|
|
|
|
ctx.assertState(loopdb.StateHtlcPublished)
|
|
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
|
|
// Expect htlc to be published.
|
|
htlcTx = <-ctx.lnd.SendOutputsChannel
|
|
|
|
// Expect the same state to be written again with the htlc tx
|
|
// hash.
|
|
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
|
|
require.NotNil(t, state.HtlcTxHash)
|
|
} else {
|
|
ctx.assertState(loopdb.StateHtlcPublished)
|
|
|
|
htlcTx.AddTxOut(&wire.TxOut{
|
|
PkScript: htlc.PkScript,
|
|
Value: int64(contract.AmountRequested),
|
|
})
|
|
}
|
|
|
|
// Expect register for htlc conf.
|
|
<-ctx.lnd.RegisterConfChannel
|
|
<-ctx.lnd.RegisterConfChannel
|
|
|
|
// Confirm htlc.
|
|
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
|
|
Tx: &htlcTx,
|
|
}
|
|
|
|
// Client starts listening for spend of htlc.
|
|
<-ctx.lnd.RegisterSpendChannel
|
|
|
|
// Client starts listening for swap invoice updates.
|
|
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
|
|
if subscription.Hash != testPreimage.Hash() {
|
|
t.Fatal("client subscribing to wrong invoice")
|
|
}
|
|
|
|
// Server has already paid invoice before spending the htlc. Signal
|
|
// settled.
|
|
subscription.Update <- lndclient.InvoiceUpdate{
|
|
State: channeldb.ContractSettled,
|
|
AmtPaid: 49000,
|
|
}
|
|
|
|
// Swap is expected to move to the state InvoiceSettled
|
|
ctx.assertState(loopdb.StateInvoiceSettled)
|
|
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
|
|
|
|
// Server spends htlc.
|
|
successTx := wire.MsgTx{}
|
|
successTx.AddTxIn(&wire.TxIn{
|
|
Witness: [][]byte{{}, {}, {}},
|
|
})
|
|
successTxHash := successTx.TxHash()
|
|
|
|
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
|
|
SpendingTx: &successTx,
|
|
SpenderTxHash: &successTxHash,
|
|
SpenderInputIndex: 0,
|
|
}
|
|
|
|
ctx.assertState(loopdb.StateSuccess)
|
|
}
|