|
7 | 7 | "time" |
8 | 8 |
|
9 | 9 | "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" |
10 | 13 | "github.com/lightningnetwork/lnd/lnrpc" |
11 | 14 | "github.com/lightningnetwork/lnd/lnwire" |
12 | 15 | paymentsdb "github.com/lightningnetwork/lnd/payments/db" |
@@ -764,3 +767,135 @@ func TestPrepareLspRouteHints(t *testing.T) { |
764 | 767 | require.Contains(t, err.Error(), "no public LSP nodes found") |
765 | 768 | }) |
766 | 769 | } |
| 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