Skip to content

Commit 62177f9

Browse files
authored
Merge pull request #1122 from starius/addinvoice-coverage
loop-ins: fix route hints passing, add test coverage
2 parents 807aa61 + 51bb2b6 commit 62177f9

8 files changed

Lines changed: 293 additions & 11 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ require (
2020
github.com/jessevdk/go-flags v1.4.0
2121
github.com/lib/pq v1.10.9
2222
github.com/lightninglabs/aperture v0.3.13-beta
23-
github.com/lightninglabs/lndclient v0.20.0-7
23+
github.com/lightninglabs/lndclient v0.20.0-8
2424
github.com/lightninglabs/loop/looprpc v1.0.7
2525
github.com/lightninglabs/loop/swapserverrpc v1.0.14
2626
github.com/lightninglabs/taproot-assets v0.7.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,8 +1116,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI
11161116
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
11171117
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7 h1:373o5lNr1udAdhcf5+zq/0dYpRtkvYLl8Lk6wG7I0DY=
11181118
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM=
1119-
github.com/lightninglabs/lndclient v0.20.0-7 h1:EA5QOjT9IJmcgybIuR4pmIXkj2GMpa/2PxOf6j4reWU=
1120-
github.com/lightninglabs/lndclient v0.20.0-7/go.mod h1:gBtIFPGmC2xIspGIv/G5+HiPSGJsFD8uIow7Oke1HFI=
1119+
github.com/lightninglabs/lndclient v0.20.0-8 h1:xymEVZjHcFoszZsJy3jyPNErY+YBCkxLDRV5ohynry4=
1120+
github.com/lightninglabs/lndclient v0.20.0-8/go.mod h1:AQTlloQUUK6OW6j9YRiA/7Sy09PXlyVxsvPo5bW0L6A=
11211121
github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q=
11221122
github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
11231123
github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs=

loopin_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"github.com/btcsuite/btcd/wire"
1111
"github.com/lightninglabs/lndclient"
1212
"github.com/lightninglabs/loop/loopdb"
13+
"github.com/lightninglabs/loop/swap"
1314
"github.com/lightninglabs/loop/test"
1415
"github.com/lightninglabs/loop/utils"
1516
"github.com/lightningnetwork/lnd/chainntnfs"
1617
"github.com/lightningnetwork/lnd/clock"
1718
invpkg "github.com/lightningnetwork/lnd/invoices"
1819
"github.com/lightningnetwork/lnd/lntypes"
1920
"github.com/lightningnetwork/lnd/routing/route"
21+
"github.com/lightningnetwork/lnd/zpay32"
2022
"github.com/stretchr/testify/require"
2123
)
2224

@@ -124,6 +126,39 @@ func TestLoopInSuccess(t *testing.T) {
124126
})
125127
}
126128

129+
// TestLoopInSwapInvoiceRouteHintsMatchProbe asserts that explicit route hints
130+
// are preserved on both loop-in invoices. The probe invoice already keeps the
131+
// requested hints, while the swap invoice currently loses them via the
132+
// lndclient AddInvoice wrapper.
133+
func TestLoopInSwapInvoiceRouteHintsMatchProbe(t *testing.T) {
134+
t.Parallel()
135+
136+
ctx := newLoopInTestContext(t)
137+
cfg := newSwapConfig(
138+
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
139+
clock.NewTestClock(time.Unix(123, 0)),
140+
)
141+
142+
req := testLoopInRequest
143+
req.RouteHints = testLoopInRouteHints()
144+
145+
_, err := newLoopInSwap(t.Context(), cfg, 600, &req)
146+
require.NoError(t, err)
147+
148+
_, swapRouteHints, _, _, err := swap.DecodeInvoice(
149+
ctx.lnd.ChainParams, ctx.server.swapInvoice,
150+
)
151+
require.NoError(t, err)
152+
153+
_, probeRouteHints, _, _, err := swap.DecodeInvoice(
154+
ctx.lnd.ChainParams, ctx.server.probeInvoice,
155+
)
156+
require.NoError(t, err)
157+
158+
test.RequireRouteHintsEqual(t, req.RouteHints, probeRouteHints)
159+
test.RequireRouteHintsEqual(t, probeRouteHints, swapRouteHints)
160+
}
161+
127162
func testLoopInSuccess(t *testing.T) {
128163
defer test.Guard(t)()
129164

@@ -233,6 +268,42 @@ func testLoopInSuccess(t *testing.T) {
233268
require.NoError(t, <-errChan)
234269
}
235270

271+
// testLoopInRouteHints returns deterministic explicit route hints that can be
272+
// encoded into loop-in invoices for regression tests.
273+
func testLoopInRouteHints() [][]zpay32.HopHint {
274+
_, pubKey1 := test.CreateKey(11)
275+
_, pubKey2 := test.CreateKey(12)
276+
_, pubKey3 := test.CreateKey(13)
277+
278+
return [][]zpay32.HopHint{
279+
{
280+
{
281+
NodeID: pubKey1,
282+
ChannelID: 1,
283+
FeeBaseMSat: 10,
284+
FeeProportionalMillionths: 20,
285+
CLTVExpiryDelta: 30,
286+
},
287+
{
288+
NodeID: pubKey2,
289+
ChannelID: 2,
290+
FeeBaseMSat: 11,
291+
FeeProportionalMillionths: 21,
292+
CLTVExpiryDelta: 31,
293+
},
294+
},
295+
{
296+
{
297+
NodeID: pubKey3,
298+
ChannelID: 3,
299+
FeeBaseMSat: 12,
300+
FeeProportionalMillionths: 22,
301+
CLTVExpiryDelta: 32,
302+
},
303+
},
304+
}
305+
}
306+
236307
// TestLoopInTimeout tests scenarios where the server doesn't sweep the htlc
237308
// and the client is forced to reclaim the funds using the timeout tx.
238309
func TestLoopInTimeout(t *testing.T) {

server_mock_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ type serverMock struct {
4040

4141
height int32
4242

43-
swapInvoice string
44-
swapHash lntypes.Hash
45-
prepayHash lntypes.Hash
43+
swapInvoice string
44+
probeInvoice string
45+
swapHash lntypes.Hash
46+
prepayHash lntypes.Hash
4647

4748
// preimagePush is a channel that preimage pushes are sent into.
4849
preimagePush chan lntypes.Preimage
@@ -157,7 +158,7 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err
157158
}
158159

159160
func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
160-
amount btcutil.Amount, _, _ [33]byte, swapInvoice, _ string,
161+
amount btcutil.Amount, _, _ [33]byte, swapInvoice, probeInvoice string,
161162
_ *route.Vertex, _ string) (*newLoopInResponse, error) {
162163

163164
_, receiverKey := test.CreateKey(101)
@@ -175,6 +176,7 @@ func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
175176
)
176177

177178
s.swapInvoice = swapInvoice
179+
s.probeInvoice = probeInvoice
178180
s.swapHash = swapHash
179181

180182
// Simulate the server paying the probe invoice and expect the client to

staticaddr/loopin/actions_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ import (
77
"time"
88

99
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
11+
"github.com/btcsuite/btcd/wire"
1012
"github.com/lightninglabs/lndclient"
1113
"github.com/lightninglabs/loop/fsm"
1214
"github.com/lightninglabs/loop/staticaddr/address"
1315
"github.com/lightninglabs/loop/staticaddr/deposit"
1416
"github.com/lightninglabs/loop/staticaddr/script"
1517
"github.com/lightninglabs/loop/staticaddr/version"
18+
"github.com/lightninglabs/loop/swap"
19+
"github.com/lightninglabs/loop/swapserverrpc"
1620
"github.com/lightninglabs/loop/test"
1721
"github.com/lightningnetwork/lnd/invoices"
1822
"github.com/lightningnetwork/lnd/lntypes"
23+
"github.com/lightningnetwork/lnd/zpay32"
1924
"github.com/stretchr/testify/require"
25+
"google.golang.org/grpc"
2026
)
2127

2228
// TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr ensures that an error from
@@ -123,6 +129,148 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) {
123129
}
124130
}
125131

132+
// TestInitHtlcActionPreservesRouteHints asserts that static-address loop-in
133+
// propagates explicit route hints into the encoded swap invoice sent to the
134+
// server. This currently fails because lndclient.AddInvoice drops route hints.
135+
func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
136+
t.Parallel()
137+
138+
mockLnd := test.NewMockLnd()
139+
_, serverKey := test.CreateKey(21)
140+
141+
server := &mockStaticAddressServer{
142+
response: testStaticAddressLoopInResponse(
143+
serverKey.SerializeCompressed(),
144+
),
145+
}
146+
147+
dep := &deposit.Deposit{
148+
OutPoint: wire.OutPoint{
149+
Hash: chainhash.Hash{1},
150+
Index: 0,
151+
},
152+
Value: 500_000,
153+
}
154+
155+
loopIn := &StaticAddressLoopIn{
156+
Deposits: []*deposit.Deposit{dep},
157+
DepositOutpoints: []string{dep.OutPoint.String()},
158+
SelectedAmount: dep.Value,
159+
QuotedSwapFee: 1_000,
160+
RouteHints: testStaticAddressRouteHints(),
161+
InitiationHeight: uint32(mockLnd.Height),
162+
InitiationTime: time.Now(),
163+
PaymentTimeoutSeconds: 3_600,
164+
}
165+
166+
f := &FSM{
167+
StateMachine: &fsm.StateMachine{},
168+
cfg: &Config{
169+
Server: server,
170+
DepositManager: &noopDepositManager{},
171+
LndClient: mockLnd.Client,
172+
WalletKit: mockLnd.WalletKit,
173+
ChainParams: mockLnd.ChainParams,
174+
Store: &mockStore{},
175+
ValidateLoopInContract: testValidateLoopInContract,
176+
MaxStaticAddrHtlcFeePercentage: 1,
177+
MaxStaticAddrHtlcBackupFeePercentage: 1,
178+
},
179+
loopIn: loopIn,
180+
}
181+
182+
event := f.InitHtlcAction(t.Context(), nil)
183+
require.Equal(t, OnHtlcInitiated, event)
184+
require.Nil(t, f.LastActionError)
185+
require.NotNil(t, server.request)
186+
187+
_, routeHints, _, _, err := swap.DecodeInvoice(
188+
mockLnd.ChainParams, server.request.SwapInvoice,
189+
)
190+
require.NoError(t, err)
191+
192+
test.RequireRouteHintsEqual(t, loopIn.RouteHints, routeHints)
193+
}
194+
195+
// mockStaticAddressServer captures static-address loop-in requests in tests.
196+
type mockStaticAddressServer struct {
197+
swapserverrpc.StaticAddressServerClient
198+
199+
request *swapserverrpc.ServerStaticAddressLoopInRequest
200+
response *swapserverrpc.ServerStaticAddressLoopInResponse
201+
}
202+
203+
// ServerStaticAddressLoopIn records the request and returns the prepared
204+
// response.
205+
func (m *mockStaticAddressServer) ServerStaticAddressLoopIn(
206+
_ context.Context, in *swapserverrpc.ServerStaticAddressLoopInRequest,
207+
_ ...grpc.CallOption) (*swapserverrpc.ServerStaticAddressLoopInResponse,
208+
error) {
209+
210+
m.request = in
211+
212+
return m.response, nil
213+
}
214+
215+
// testStaticAddressLoopInResponse returns a minimal successful server response
216+
// for InitHtlcAction tests.
217+
func testStaticAddressLoopInResponse(
218+
serverPubKey []byte) *swapserverrpc.ServerStaticAddressLoopInResponse {
219+
220+
signingInfo := &swapserverrpc.ServerHtlcSigningInfo{
221+
FeeRate: 1,
222+
}
223+
224+
return &swapserverrpc.ServerStaticAddressLoopInResponse{
225+
HtlcServerPubKey: serverPubKey,
226+
HtlcExpiry: 1_000,
227+
StandardHtlcInfo: signingInfo,
228+
HighFeeHtlcInfo: signingInfo,
229+
ExtremeFeeHtlcInfo: signingInfo,
230+
}
231+
}
232+
233+
// testStaticAddressRouteHints returns deterministic route hints for static
234+
// loop-in invoice regression tests.
235+
func testStaticAddressRouteHints() [][]zpay32.HopHint {
236+
_, pubKey1 := test.CreateKey(31)
237+
_, pubKey2 := test.CreateKey(32)
238+
_, pubKey3 := test.CreateKey(33)
239+
240+
return [][]zpay32.HopHint{
241+
{
242+
{
243+
NodeID: pubKey1,
244+
ChannelID: 11,
245+
FeeBaseMSat: 101,
246+
FeeProportionalMillionths: 201,
247+
CLTVExpiryDelta: 31,
248+
},
249+
{
250+
NodeID: pubKey2,
251+
ChannelID: 12,
252+
FeeBaseMSat: 102,
253+
FeeProportionalMillionths: 202,
254+
CLTVExpiryDelta: 32,
255+
},
256+
},
257+
{
258+
{
259+
NodeID: pubKey3,
260+
ChannelID: 13,
261+
FeeBaseMSat: 103,
262+
FeeProportionalMillionths: 203,
263+
CLTVExpiryDelta: 33,
264+
},
265+
},
266+
}
267+
}
268+
269+
// testValidateLoopInContract accepts all server contract parameters in tests.
270+
func testValidateLoopInContract(_ int32, _ int32) error {
271+
return nil
272+
}
273+
126274
// mockAddressManager is a minimal AddressManager implementation used by the
127275
// test FSM setup.
128276
type mockAddressManager struct {

test/invoices_mock.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,17 @@ func (s *mockInvoices) AddHoldInvoice(ctx context.Context,
8383
// Create and encode the payment request as a bech32 (zpay32) string.
8484
creationDate := time.Now()
8585

86-
payReq, err := zpay32.NewInvoice(
87-
s.lnd.ChainParams, *hash, creationDate,
86+
options := []func(*zpay32.Invoice){
8887
zpay32.Description(in.Memo),
8988
zpay32.CLTVExpiry(in.CltvExpiry),
9089
zpay32.Amount(in.Value),
90+
}
91+
for _, routeHint := range in.RouteHints {
92+
options = append(options, zpay32.RouteHint(routeHint))
93+
}
94+
95+
payReq, err := zpay32.NewInvoice(
96+
s.lnd.ChainParams, *hash, creationDate, options...,
9197
)
9298
if err != nil {
9399
return "", err

test/lightning_client_mock.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,17 @@ func (h *mockLightningClient) AddInvoice(ctx context.Context,
105105
// Create and encode the payment request as a bech32 (zpay32) string.
106106
creationDate := time.Now()
107107

108-
payReq, err := zpay32.NewInvoice(
109-
h.lnd.ChainParams, hash, creationDate,
108+
options := []func(*zpay32.Invoice){
110109
zpay32.Description(in.Memo),
111110
zpay32.CLTVExpiry(in.CltvExpiry),
112111
zpay32.Amount(in.Value),
112+
}
113+
for _, routeHint := range in.RouteHints {
114+
options = append(options, zpay32.RouteHint(routeHint))
115+
}
116+
117+
payReq, err := zpay32.NewInvoice(
118+
h.lnd.ChainParams, hash, creationDate, options...,
113119
)
114120
if err != nil {
115121
return lntypes.Hash{}, "", err

0 commit comments

Comments
 (0)