mirror of
https://github.com/lightninglabs/loop
synced 2024-11-04 06:00:21 +00:00
f0aff9b7bd
This commit extends SwapResponse and SwapStatus with np2wsh and p2wsh htlc output addresses to support both nested and native segwit htlcs in loop-in. Furthermore the commit adds support for native segwith loop-in htlcs. When the htlc is paid internally, as of this commit we'll use NP2WSH, otherwise users are free to select whether to pay the NP2WSH or the P2WSH htlc.
443 lines
9.7 KiB
Go
443 lines
9.7 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/lightninglabs/loop/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/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 := &swapConfig{
|
|
lnd: &ctx.lnd.LndServices,
|
|
store: ctx.store,
|
|
server: ctx.server,
|
|
}
|
|
|
|
swap, err := newLoopInSwap(
|
|
context.Background(), cfg,
|
|
height, &testLoopInRequest,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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 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.HtlcNP2WSH, 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 := &swapConfig{
|
|
lnd: &ctx.lnd.LndServices,
|
|
store: ctx.store,
|
|
server: ctx.server,
|
|
}
|
|
|
|
req := testLoopInRequest
|
|
if externalValue != 0 {
|
|
req.ExternalHtlc = true
|
|
}
|
|
|
|
s, err := newLoopInSwap(
|
|
context.Background(), cfg,
|
|
height, &req,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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
|
|
} 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,
|
|
}
|
|
|
|
// 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) {
|
|
t.Run("initiated", func(t *testing.T) {
|
|
testLoopInResume(t, loopdb.StateInitiated, false)
|
|
})
|
|
|
|
t.Run("initiated expired", func(t *testing.T) {
|
|
testLoopInResume(t, loopdb.StateInitiated, true)
|
|
})
|
|
|
|
t.Run("htlc published", func(t *testing.T) {
|
|
testLoopInResume(t, loopdb.StateHtlcPublished, false)
|
|
})
|
|
}
|
|
|
|
func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) {
|
|
defer test.Guard(t)()
|
|
|
|
ctx := newLoopInTestContext(t)
|
|
|
|
cfg := &swapConfig{
|
|
lnd: &ctx.lnd.LndServices,
|
|
store: ctx.store,
|
|
server: 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,
|
|
},
|
|
}
|
|
pendSwap := &loopdb.LoopIn{
|
|
Contract: contract,
|
|
Loop: loopdb.Loop{
|
|
Events: []*loopdb.LoopEvent{
|
|
{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: state,
|
|
},
|
|
},
|
|
},
|
|
Hash: testPreimage.Hash(),
|
|
},
|
|
}
|
|
|
|
htlc, err := swap.NewHtlc(
|
|
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
|
|
} else {
|
|
ctx.assertState(loopdb.StateHtlcPublished)
|
|
|
|
htlcTx.AddTxOut(&wire.TxOut{
|
|
PkScript: htlc.PkScript,
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|