mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #1122 from starius/addinvoice-coverage
loop-ins: fix route hints passing, add test coverage
This commit is contained in:
commit
62177f9d62
8 changed files with 293 additions and 11 deletions
2
go.mod
2
go.mod
|
|
@ -20,7 +20,7 @@ require (
|
|||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lightninglabs/aperture v0.3.13-beta
|
||||
github.com/lightninglabs/lndclient v0.20.0-7
|
||||
github.com/lightninglabs/lndclient v0.20.0-8
|
||||
github.com/lightninglabs/loop/looprpc v1.0.7
|
||||
github.com/lightninglabs/loop/swapserverrpc v1.0.14
|
||||
github.com/lightninglabs/taproot-assets v0.7.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1116,8 +1116,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI
|
|||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
|
||||
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7 h1:373o5lNr1udAdhcf5+zq/0dYpRtkvYLl8Lk6wG7I0DY=
|
||||
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM=
|
||||
github.com/lightninglabs/lndclient v0.20.0-7 h1:EA5QOjT9IJmcgybIuR4pmIXkj2GMpa/2PxOf6j4reWU=
|
||||
github.com/lightninglabs/lndclient v0.20.0-7/go.mod h1:gBtIFPGmC2xIspGIv/G5+HiPSGJsFD8uIow7Oke1HFI=
|
||||
github.com/lightninglabs/lndclient v0.20.0-8 h1:xymEVZjHcFoszZsJy3jyPNErY+YBCkxLDRV5ohynry4=
|
||||
github.com/lightninglabs/lndclient v0.20.0-8/go.mod h1:AQTlloQUUK6OW6j9YRiA/7Sy09PXlyVxsvPo5bW0L6A=
|
||||
github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q=
|
||||
github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
||||
github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs=
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
invpkg "github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -124,6 +126,39 @@ func TestLoopInSuccess(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestLoopInSwapInvoiceRouteHintsMatchProbe asserts that explicit route hints
|
||||
// are preserved on both loop-in invoices. The probe invoice already keeps the
|
||||
// requested hints, while the swap invoice currently loses them via the
|
||||
// lndclient AddInvoice wrapper.
|
||||
func TestLoopInSwapInvoiceRouteHintsMatchProbe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := newLoopInTestContext(t)
|
||||
cfg := newSwapConfig(
|
||||
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
|
||||
clock.NewTestClock(time.Unix(123, 0)),
|
||||
)
|
||||
|
||||
req := testLoopInRequest
|
||||
req.RouteHints = testLoopInRouteHints()
|
||||
|
||||
_, err := newLoopInSwap(t.Context(), cfg, 600, &req)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, swapRouteHints, _, _, err := swap.DecodeInvoice(
|
||||
ctx.lnd.ChainParams, ctx.server.swapInvoice,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, probeRouteHints, _, _, err := swap.DecodeInvoice(
|
||||
ctx.lnd.ChainParams, ctx.server.probeInvoice,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
test.RequireRouteHintsEqual(t, req.RouteHints, probeRouteHints)
|
||||
test.RequireRouteHintsEqual(t, probeRouteHints, swapRouteHints)
|
||||
}
|
||||
|
||||
func testLoopInSuccess(t *testing.T) {
|
||||
defer test.Guard(t)()
|
||||
|
||||
|
|
@ -233,6 +268,42 @@ func testLoopInSuccess(t *testing.T) {
|
|||
require.NoError(t, <-errChan)
|
||||
}
|
||||
|
||||
// testLoopInRouteHints returns deterministic explicit route hints that can be
|
||||
// encoded into loop-in invoices for regression tests.
|
||||
func testLoopInRouteHints() [][]zpay32.HopHint {
|
||||
_, pubKey1 := test.CreateKey(11)
|
||||
_, pubKey2 := test.CreateKey(12)
|
||||
_, pubKey3 := test.CreateKey(13)
|
||||
|
||||
return [][]zpay32.HopHint{
|
||||
{
|
||||
{
|
||||
NodeID: pubKey1,
|
||||
ChannelID: 1,
|
||||
FeeBaseMSat: 10,
|
||||
FeeProportionalMillionths: 20,
|
||||
CLTVExpiryDelta: 30,
|
||||
},
|
||||
{
|
||||
NodeID: pubKey2,
|
||||
ChannelID: 2,
|
||||
FeeBaseMSat: 11,
|
||||
FeeProportionalMillionths: 21,
|
||||
CLTVExpiryDelta: 31,
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
NodeID: pubKey3,
|
||||
ChannelID: 3,
|
||||
FeeBaseMSat: 12,
|
||||
FeeProportionalMillionths: 22,
|
||||
CLTVExpiryDelta: 32,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@ type serverMock struct {
|
|||
|
||||
height int32
|
||||
|
||||
swapInvoice string
|
||||
swapHash lntypes.Hash
|
||||
prepayHash lntypes.Hash
|
||||
swapInvoice string
|
||||
probeInvoice string
|
||||
swapHash lntypes.Hash
|
||||
prepayHash lntypes.Hash
|
||||
|
||||
// preimagePush is a channel that preimage pushes are sent into.
|
||||
preimagePush chan lntypes.Preimage
|
||||
|
|
@ -157,7 +158,7 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err
|
|||
}
|
||||
|
||||
func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
|
||||
amount btcutil.Amount, _, _ [33]byte, swapInvoice, _ string,
|
||||
amount btcutil.Amount, _, _ [33]byte, swapInvoice, probeInvoice string,
|
||||
_ *route.Vertex, _ string) (*newLoopInResponse, error) {
|
||||
|
||||
_, receiverKey := test.CreateKey(101)
|
||||
|
|
@ -175,6 +176,7 @@ func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
|
|||
)
|
||||
|
||||
s.swapInvoice = swapInvoice
|
||||
s.probeInvoice = probeInvoice
|
||||
s.swapHash = swapHash
|
||||
|
||||
// Simulate the server paying the probe invoice and expect the client to
|
||||
|
|
|
|||
|
|
@ -7,16 +7,22 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/staticaddr/version"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr ensures that an error from
|
||||
|
|
@ -123,6 +129,148 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestInitHtlcActionPreservesRouteHints asserts that static-address loop-in
|
||||
// propagates explicit route hints into the encoded swap invoice sent to the
|
||||
// server. This currently fails because lndclient.AddInvoice drops route hints.
|
||||
func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
_, serverKey := test.CreateKey(21)
|
||||
|
||||
server := &mockStaticAddressServer{
|
||||
response: testStaticAddressLoopInResponse(
|
||||
serverKey.SerializeCompressed(),
|
||||
),
|
||||
}
|
||||
|
||||
dep := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1},
|
||||
Index: 0,
|
||||
},
|
||||
Value: 500_000,
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
Deposits: []*deposit.Deposit{dep},
|
||||
DepositOutpoints: []string{dep.OutPoint.String()},
|
||||
SelectedAmount: dep.Value,
|
||||
QuotedSwapFee: 1_000,
|
||||
RouteHints: testStaticAddressRouteHints(),
|
||||
InitiationHeight: uint32(mockLnd.Height),
|
||||
InitiationTime: time.Now(),
|
||||
PaymentTimeoutSeconds: 3_600,
|
||||
}
|
||||
|
||||
f := &FSM{
|
||||
StateMachine: &fsm.StateMachine{},
|
||||
cfg: &Config{
|
||||
Server: server,
|
||||
DepositManager: &noopDepositManager{},
|
||||
LndClient: mockLnd.Client,
|
||||
WalletKit: mockLnd.WalletKit,
|
||||
ChainParams: mockLnd.ChainParams,
|
||||
Store: &mockStore{},
|
||||
ValidateLoopInContract: testValidateLoopInContract,
|
||||
MaxStaticAddrHtlcFeePercentage: 1,
|
||||
MaxStaticAddrHtlcBackupFeePercentage: 1,
|
||||
},
|
||||
loopIn: loopIn,
|
||||
}
|
||||
|
||||
event := f.InitHtlcAction(t.Context(), nil)
|
||||
require.Equal(t, OnHtlcInitiated, event)
|
||||
require.Nil(t, f.LastActionError)
|
||||
require.NotNil(t, server.request)
|
||||
|
||||
_, routeHints, _, _, err := swap.DecodeInvoice(
|
||||
mockLnd.ChainParams, server.request.SwapInvoice,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
test.RequireRouteHintsEqual(t, loopIn.RouteHints, routeHints)
|
||||
}
|
||||
|
||||
// mockStaticAddressServer captures static-address loop-in requests in tests.
|
||||
type mockStaticAddressServer struct {
|
||||
swapserverrpc.StaticAddressServerClient
|
||||
|
||||
request *swapserverrpc.ServerStaticAddressLoopInRequest
|
||||
response *swapserverrpc.ServerStaticAddressLoopInResponse
|
||||
}
|
||||
|
||||
// ServerStaticAddressLoopIn records the request and returns the prepared
|
||||
// response.
|
||||
func (m *mockStaticAddressServer) ServerStaticAddressLoopIn(
|
||||
_ context.Context, in *swapserverrpc.ServerStaticAddressLoopInRequest,
|
||||
_ ...grpc.CallOption) (*swapserverrpc.ServerStaticAddressLoopInResponse,
|
||||
error) {
|
||||
|
||||
m.request = in
|
||||
|
||||
return m.response, nil
|
||||
}
|
||||
|
||||
// testStaticAddressLoopInResponse returns a minimal successful server response
|
||||
// for InitHtlcAction tests.
|
||||
func testStaticAddressLoopInResponse(
|
||||
serverPubKey []byte) *swapserverrpc.ServerStaticAddressLoopInResponse {
|
||||
|
||||
signingInfo := &swapserverrpc.ServerHtlcSigningInfo{
|
||||
FeeRate: 1,
|
||||
}
|
||||
|
||||
return &swapserverrpc.ServerStaticAddressLoopInResponse{
|
||||
HtlcServerPubKey: serverPubKey,
|
||||
HtlcExpiry: 1_000,
|
||||
StandardHtlcInfo: signingInfo,
|
||||
HighFeeHtlcInfo: signingInfo,
|
||||
ExtremeFeeHtlcInfo: signingInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// testStaticAddressRouteHints returns deterministic route hints for static
|
||||
// loop-in invoice regression tests.
|
||||
func testStaticAddressRouteHints() [][]zpay32.HopHint {
|
||||
_, pubKey1 := test.CreateKey(31)
|
||||
_, pubKey2 := test.CreateKey(32)
|
||||
_, pubKey3 := test.CreateKey(33)
|
||||
|
||||
return [][]zpay32.HopHint{
|
||||
{
|
||||
{
|
||||
NodeID: pubKey1,
|
||||
ChannelID: 11,
|
||||
FeeBaseMSat: 101,
|
||||
FeeProportionalMillionths: 201,
|
||||
CLTVExpiryDelta: 31,
|
||||
},
|
||||
{
|
||||
NodeID: pubKey2,
|
||||
ChannelID: 12,
|
||||
FeeBaseMSat: 102,
|
||||
FeeProportionalMillionths: 202,
|
||||
CLTVExpiryDelta: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
NodeID: pubKey3,
|
||||
ChannelID: 13,
|
||||
FeeBaseMSat: 103,
|
||||
FeeProportionalMillionths: 203,
|
||||
CLTVExpiryDelta: 33,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testValidateLoopInContract accepts all server contract parameters in tests.
|
||||
func testValidateLoopInContract(_ int32, _ int32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockAddressManager is a minimal AddressManager implementation used by the
|
||||
// test FSM setup.
|
||||
type mockAddressManager struct {
|
||||
|
|
|
|||
|
|
@ -83,11 +83,17 @@ func (s *mockInvoices) AddHoldInvoice(ctx context.Context,
|
|||
// Create and encode the payment request as a bech32 (zpay32) string.
|
||||
creationDate := time.Now()
|
||||
|
||||
payReq, err := zpay32.NewInvoice(
|
||||
s.lnd.ChainParams, *hash, creationDate,
|
||||
options := []func(*zpay32.Invoice){
|
||||
zpay32.Description(in.Memo),
|
||||
zpay32.CLTVExpiry(in.CltvExpiry),
|
||||
zpay32.Amount(in.Value),
|
||||
}
|
||||
for _, routeHint := range in.RouteHints {
|
||||
options = append(options, zpay32.RouteHint(routeHint))
|
||||
}
|
||||
|
||||
payReq, err := zpay32.NewInvoice(
|
||||
s.lnd.ChainParams, *hash, creationDate, options...,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -105,11 +105,17 @@ func (h *mockLightningClient) AddInvoice(ctx context.Context,
|
|||
// Create and encode the payment request as a bech32 (zpay32) string.
|
||||
creationDate := time.Now()
|
||||
|
||||
payReq, err := zpay32.NewInvoice(
|
||||
h.lnd.ChainParams, hash, creationDate,
|
||||
options := []func(*zpay32.Invoice){
|
||||
zpay32.Description(in.Memo),
|
||||
zpay32.CLTVExpiry(in.CltvExpiry),
|
||||
zpay32.Amount(in.Value),
|
||||
}
|
||||
for _, routeHint := range in.RouteHints {
|
||||
options = append(options, zpay32.RouteHint(routeHint))
|
||||
}
|
||||
|
||||
payReq, err := zpay32.NewInvoice(
|
||||
h.lnd.ChainParams, hash, creationDate, options...,
|
||||
)
|
||||
if err != nil {
|
||||
return lntypes.Hash{}, "", err
|
||||
|
|
|
|||
49
test/route_hints.go
Normal file
49
test/route_hints.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// RequireRouteHintsEqual asserts that two route hint sets are identical.
|
||||
func RequireRouteHintsEqual(t testing.TB, expected,
|
||||
actual [][]zpay32.HopHint) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
require.Len(t, actual, len(expected))
|
||||
|
||||
for i := range expected {
|
||||
require.Len(t, actual[i], len(expected[i]))
|
||||
|
||||
for j := range expected[i] {
|
||||
expectedHint := expected[i][j]
|
||||
actualHint := actual[i][j]
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
expectedHint.NodeID.SerializeCompressed(),
|
||||
actualHint.NodeID.SerializeCompressed(),
|
||||
)
|
||||
require.Equal(
|
||||
t, expectedHint.ChannelID, actualHint.ChannelID,
|
||||
)
|
||||
require.Equal(
|
||||
t, expectedHint.FeeBaseMSat,
|
||||
actualHint.FeeBaseMSat,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
expectedHint.FeeProportionalMillionths,
|
||||
actualHint.FeeProportionalMillionths,
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
expectedHint.CLTVExpiryDelta,
|
||||
actualHint.CLTVExpiryDelta,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue