Skip to content

Commit a9d8ae2

Browse files
authored
Merge pull request #10834 from lightningnetwork/backport-10771-to-v0.21.x-branch
[v0.21.x-branch] Backport #10771: routerrpc: isolate LSP route fee probes
2 parents c3291fd + c709ec5 commit a9d8ae2

2 files changed

Lines changed: 168 additions & 3 deletions

File tree

lnrpc/routerrpc/router_server.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,25 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
512512
func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
513513
timeout uint32) (*RouteFeeResponse, error) {
514514

515+
return s.probePaymentRequestWithSender(
516+
ctx, paymentRequest, timeout, s.sendProbePayment,
517+
)
518+
}
519+
520+
// probePaymentSender dispatches a probe payment request and returns the
521+
// resulting fee estimate. It exists as a test seam so tests can inject a stub
522+
// sender and inspect generated probe requests without running the payment
523+
// lifecycle.
524+
type probePaymentSender func(context.Context,
525+
*SendPaymentRequest) (*RouteFeeResponse, error)
526+
527+
// probePaymentRequestWithSender contains the implementation of
528+
// probePaymentRequest. The sender is injected so tests can inspect generated
529+
// probe requests without invoking the full payment lifecycle.
530+
func (s *Server) probePaymentRequestWithSender(ctx context.Context,
531+
paymentRequest string, timeout uint32,
532+
sendProbePayment probePaymentSender) (*RouteFeeResponse, error) {
533+
515534
payReq, err := zpay32.Decode(
516535
paymentRequest, s.cfg.RouterBackend.ActiveNetParams,
517536
)
@@ -564,7 +583,8 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
564583
probeRequest.Dest)
565584

566585
probeRequest.RouteHints = invoicesrpc.CreateRPCRouteHints(hints)
567-
return s.sendProbePayment(ctx, probeRequest)
586+
587+
return sendProbePayment(ctx, probeRequest)
568588
}
569589

570590
// If the heuristic indicates an LSP, we filter and group route hints by
@@ -600,6 +620,16 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
600620

601621
lspHint := group.LspHopHint
602622

623+
// Each LSP probe must use a unique payment hash, otherwise the
624+
// payment lifecycle will treat later probes as attempts on the
625+
// first probe's payment and reuse its payment-level parameters.
626+
var lspPaymentHash lntypes.Hash
627+
_, err := crand.Read(lspPaymentHash[:])
628+
if err != nil {
629+
return nil, fmt.Errorf("cannot generate random probe "+
630+
"preimage: %w", err)
631+
}
632+
603633
log.Infof("Probing LSP with destination: %v", lspKey)
604634

605635
// Create a new probe request for this LSP.
@@ -609,7 +639,7 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
609639
MaxParts: probeRequest.MaxParts,
610640
AllowSelfPayment: probeRequest.AllowSelfPayment,
611641
AmtMsat: amtMsat,
612-
PaymentHash: probeRequest.PaymentHash,
642+
PaymentHash: lspPaymentHash[:],
613643
FeeLimitSat: probeRequest.FeeLimitSat,
614644
FinalCltvDelta: int32(lspHint.CLTVExpiryDelta),
615645
DestFeatures: probeRequest.DestFeatures,
@@ -640,7 +670,7 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
640670
lspProbeRequest.AmtMsat += int64(hopFee)
641671

642672
// Dispatch the payment probe for this LSP.
643-
resp, err := s.sendProbePayment(ctx, lspProbeRequest)
673+
resp, err := sendProbePayment(ctx, lspProbeRequest)
644674
if err != nil {
645675
log.Warnf("Failed to probe LSP %v: %v", lspKey, err)
646676
continue

lnrpc/routerrpc/router_server_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"time"
88

99
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
11+
"github.com/btcsuite/btcd/chaincfg"
12+
"github.com/btcsuite/btcd/chaincfg/chainhash"
1013
"github.com/lightningnetwork/lnd/lnrpc"
1114
"github.com/lightningnetwork/lnd/lnwire"
1215
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
@@ -764,3 +767,135 @@ func TestPrepareLspRouteHints(t *testing.T) {
764767
require.Contains(t, err.Error(), "no public LSP nodes found")
765768
})
766769
}
770+
771+
// TestProbePaymentRequestUsesUniqueHashPerLSP verifies that each LSP probe is
772+
// isolated from the other probes by using a distinct payment hash.
773+
func TestProbePaymentRequestUsesUniqueHashPerLSP(t *testing.T) {
774+
// Arrange: create three public LSPs and an invoice whose private
775+
// destination can be reached through any of them.
776+
destPrivKey, err := btcec.NewPrivateKey()
777+
require.NoError(t, err)
778+
779+
bobPrivKey, err := btcec.NewPrivateKey()
780+
require.NoError(t, err)
781+
bobPubKey := bobPrivKey.PubKey()
782+
783+
evePrivKey, err := btcec.NewPrivateKey()
784+
require.NoError(t, err)
785+
evePubKey := evePrivKey.PubKey()
786+
787+
davePrivKey, err := btcec.NewPrivateKey()
788+
require.NoError(t, err)
789+
davePubKey := davePrivKey.PubKey()
790+
791+
bobVertex := route.NewVertex(bobPubKey)
792+
eveVertex := route.NewVertex(evePubKey)
793+
daveVertex := route.NewVertex(davePubKey)
794+
795+
publicNodes := map[route.Vertex]struct{}{
796+
bobVertex: {},
797+
eveVertex: {},
798+
daveVertex: {},
799+
}
800+
hasNode := func(nodePub route.Vertex) (bool, error) {
801+
_, ok := publicNodes[nodePub]
802+
803+
return ok, nil
804+
}
805+
806+
server := &Server{
807+
cfg: &Config{
808+
RouterBackend: &RouterBackend{
809+
ActiveNetParams: &chaincfg.RegressionNetParams,
810+
HasNode: hasNode,
811+
},
812+
},
813+
}
814+
815+
lspHint := func(pubKey *btcec.PublicKey, chanID uint64,
816+
cltv uint16) zpay32.HopHint {
817+
818+
return zpay32.HopHint{
819+
NodeID: pubKey,
820+
ChannelID: chanID,
821+
FeeBaseMSat: uint32(chanID * 1_000),
822+
FeeProportionalMillionths: uint32(chanID),
823+
CLTVExpiryDelta: cltv,
824+
}
825+
}
826+
827+
bobHint := lspHint(bobPubKey, 1, 100)
828+
eveHint := lspHint(evePubKey, 2, 200)
829+
daveHint := lspHint(davePubKey, 3, 120)
830+
831+
var paymentHash [32]byte
832+
paymentHash[0] = 1
833+
invoice, err := zpay32.NewInvoice(
834+
&chaincfg.RegressionNetParams, paymentHash, time.Unix(1, 0),
835+
zpay32.Amount(lnwire.MilliSatoshi(100_000)),
836+
zpay32.Description("multi lsp probe"),
837+
zpay32.Destination(destPrivKey.PubKey()),
838+
zpay32.RouteHint([]zpay32.HopHint{bobHint}),
839+
zpay32.RouteHint([]zpay32.HopHint{eveHint}),
840+
zpay32.RouteHint([]zpay32.HopHint{daveHint}),
841+
)
842+
require.NoError(t, err)
843+
844+
signer := zpay32.MessageSigner{
845+
SignCompact: func(msg []byte) ([]byte, error) {
846+
hash := chainhash.HashB(msg)
847+
848+
return ecdsa.SignCompact(destPrivKey, hash, true), nil
849+
},
850+
}
851+
payReq, err := invoice.Encode(signer)
852+
require.NoError(t, err)
853+
854+
seenHashes := make(map[[32]byte]struct{})
855+
probedDests := make(map[route.Vertex]struct{})
856+
expectedCltv := map[route.Vertex]int32{
857+
bobVertex: int32(bobHint.CLTVExpiryDelta),
858+
eveVertex: int32(eveHint.CLTVExpiryDelta),
859+
daveVertex: int32(daveHint.CLTVExpiryDelta),
860+
}
861+
862+
sendProbe := func(_ context.Context,
863+
req *SendPaymentRequest) (*RouteFeeResponse, error) {
864+
865+
require.Len(t, req.PaymentHash, 32)
866+
867+
var reqHash [32]byte
868+
copy(reqHash[:], req.PaymentHash)
869+
_, hashExists := seenHashes[reqHash]
870+
require.False(t, hashExists, "reused payment hash")
871+
seenHashes[reqHash] = struct{}{}
872+
873+
var dest route.Vertex
874+
copy(dest[:], req.Dest)
875+
probedDests[dest] = struct{}{}
876+
877+
require.Equal(t, expectedCltv[dest], req.FinalCltvDelta)
878+
879+
return &RouteFeeResponse{
880+
RoutingFeeMsat: int64(req.FinalCltvDelta),
881+
TimeLockDelay: int64(req.FinalCltvDelta),
882+
FailureReason: lnrpc.
883+
PaymentFailureReason_FAILURE_REASON_NONE,
884+
}, nil
885+
}
886+
887+
// Act: estimate the route fee with a stubbed probe sender that records
888+
// the generated per-LSP probe requests.
889+
_, err = server.probePaymentRequestWithSender(
890+
t.Context(), payReq, 1, sendProbe,
891+
)
892+
893+
// Assert: all LSPs were probed, each probe had a unique payment hash,
894+
// and the probe request kept the CLTV delta for its target LSP.
895+
require.NoError(t, err)
896+
require.Len(t, seenHashes, MaxLspsToProbe)
897+
require.Len(t, probedDests, MaxLspsToProbe)
898+
require.Contains(t, probedDests, bobVertex)
899+
require.Contains(t, probedDests, eveVertex)
900+
require.Contains(t, probedDests, daveVertex)
901+
}

0 commit comments

Comments
 (0)