From e1f1077f4426fdde2cf01e8183c629af6280884c Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Wed, 15 Apr 2026 00:29:08 -0500 Subject: [PATCH] loop+test: add loop-in route hint regression --- loopin_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++ server_mock_test.go | 10 +++--- test/invoices_mock.go | 10 ++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/loopin_test.go b/loopin_test.go index 156f1d60..a1a09584 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -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) { diff --git a/server_mock_test.go b/server_mock_test.go index 4baceb64..0e8c029b 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -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 diff --git a/test/invoices_mock.go b/test/invoices_mock.go index 0174a925..7b0f309d 100644 --- a/test/invoices_mock.go +++ b/test/invoices_mock.go @@ -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