Merge pull request #1122 from starius/addinvoice-coverage

loop-ins: fix route hints passing, add test coverage
This commit is contained in:
Boris Nagaev 2026-05-01 00:17:01 -05:00 committed by GitHub
commit 62177f9d62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 293 additions and 11 deletions

2
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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) {

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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
View 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,
)
}
}
}