Merge pull request #8764 from ellemouton/rb-send-via-multi-path

[3/4] Route Blinding: send MPP over multiple blinded paths
This commit is contained in:
Olaoluwa Osuntokun 2024-07-31 19:21:49 -07:00 committed by GitHub
commit 04dde98edc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 647 additions and 210 deletions

View file

@ -136,6 +136,9 @@ commitment when the channel was force closed.
the `lncli addinvoice` command to instruct LND to include blinded paths in the the `lncli addinvoice` command to instruct LND to include blinded paths in the
invoice. invoice.
* Add the ability to [send to use multiple blinded payment
paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.
## Testing ## Testing
## Database ## Database

View file

@ -594,6 +594,10 @@ var allTestCases = []*lntest.TestCase{
Name: "mpp to single blinded path", Name: "mpp to single blinded path",
TestFunc: testMPPToSingleBlindedPath, TestFunc: testMPPToSingleBlindedPath,
}, },
{
Name: "mpp to multiple blinded paths",
TestFunc: testMPPToMultipleBlindedPaths,
},
{ {
Name: "route blinding dummy hops", Name: "route blinding dummy hops",
TestFunc: testBlindedRouteDummyHops, TestFunc: testBlindedRouteDummyHops,

View file

@ -1229,3 +1229,166 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
ht.AssertNumWaitingClose(hn, 0) ht.AssertNumWaitingClose(hn, 0)
} }
} }
// testMPPToMultipleBlindedPaths tests that a two-shard MPP payment can be sent
// over a multiple blinded paths. The following network is created where Dave
// is the recipient and Alice the sender. Dave will create an invoice containing
// two blinded paths: one with Bob at the intro node and one with Carol as the
// intro node. Channel liquidity will be set up in such a way that Alice will be
// forced to send one shared via the Bob->Dave route and one over the
// Carol->Dave route.
//
// --- Bob ---
// / \
// Alice Dave
// \ /
// --- Carol ---
func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
alice, bob := ht.Alice, ht.Bob
// Create a four-node context consisting of Alice, Bob and three new
// nodes.
dave := ht.NewNode("dave", []string{
"--routing.blinding.min-num-real-hops=1",
"--routing.blinding.num-hops=1",
})
carol := ht.NewNode("carol", nil)
// Connect nodes to ensure propagation of channels.
ht.EnsureConnected(alice, carol)
ht.EnsureConnected(alice, bob)
ht.EnsureConnected(carol, dave)
ht.EnsureConnected(bob, dave)
// Fund the new nodes.
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
ht.MineBlocksAndAssertNumTxes(1, 2)
const paymentAmt = btcutil.Amount(300000)
nodes := []*node.HarnessNode{alice, bob, carol, dave}
reqs := []*lntest.OpenChannelRequest{
{
Local: alice,
Remote: bob,
Param: lntest.OpenChannelParams{
Amt: paymentAmt * 2 / 3,
},
},
{
Local: alice,
Remote: carol,
Param: lntest.OpenChannelParams{
Amt: paymentAmt * 2 / 3,
},
},
{
Local: bob,
Remote: dave,
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
},
{
Local: carol,
Remote: dave,
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
},
}
channelPoints := ht.OpenMultiChannelsAsync(reqs)
// Make sure every node has heard every channel.
for _, hn := range nodes {
for _, cp := range channelPoints {
ht.AssertTopologyChannelOpen(hn, cp)
}
// Each node should have exactly 5 edges.
ht.AssertNumEdges(hn, len(channelPoints), false)
}
// Ok now make a payment that must be split to succeed.
// Make Dave create an invoice for Alice to pay
invoice := &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
Blind: true,
}
invoiceResp := dave.RPC.AddInvoice(invoice)
// Assert that two blinded paths are included in the invoice.
payReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
require.Len(ht, payReq.BlindedPaths, 2)
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: invoiceResp.PaymentRequest,
MaxParts: 10,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
}
payment := ht.SendPaymentAssertSettled(alice, sendReq)
preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
require.NoError(ht, err)
preimage, err := lntypes.MakePreimage(preimageBytes)
require.NoError(ht, err)
hash, err := lntypes.MakeHash(invoiceResp.RHash)
require.NoError(ht, err)
// Make sure we got the preimage.
require.True(ht, preimage.Matches(hash), "preimage doesn't match")
// Check that Alice split the payment in at least two shards. Because
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
// there is some non-determinism in the process. Depending on whether
// the new pathfinding round is started before or after the htlc is
// locked into the channel, different sharding may occur. Therefore we
// can only check if the number of shards isn't below the theoretical
// minimum.
succeeded := 0
for _, htlc := range payment.Htlcs {
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
succeeded++
}
}
const minExpectedShards = 2
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
"expected shards not reached")
// Make sure Dave show the invoice as settled for the full amount.
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
"incorrect payment amt")
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
"Invoice not settled")
settled := 0
for _, htlc := range inv.Htlcs {
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
settled++
}
}
require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
// Close all channels without mining the closing transactions.
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
ht.CloseChannelAssertPending(alice, channelPoints[1], false)
ht.CloseChannelAssertPending(bob, channelPoints[2], false)
ht.CloseChannelAssertPending(carol, channelPoints[3], false)
// Now mine a block to include all the closing transactions. (first
// iteration: no blinded paths)
ht.MineBlocksAndAssertNumTxes(1, 4)
// Assert that the channels are closed.
for _, hn := range nodes {
ht.AssertNumWaitingClose(hn, 0)
}
}

View file

@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
var ( var (
targetPubKey *route.Vertex targetPubKey *route.Vertex
routeHintEdges map[route.Vertex][]routing.AdditionalEdge routeHintEdges map[route.Vertex][]routing.AdditionalEdge
blindedPmt *routing.BlindedPayment blindedPathSet *routing.BlindedPaymentPathSet
// finalCLTVDelta varies depending on whether we're sending to // finalCLTVDelta varies depending on whether we're sending to
// a blinded route or an unblinded node. For blinded paths, // a blinded route or an unblinded node. For blinded paths,
@ -297,13 +297,14 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
// Validate that the fields provided in the request are sane depending // Validate that the fields provided in the request are sane depending
// on whether it is using a blinded path or not. // on whether it is using a blinded path or not.
if len(in.BlindedPaymentPaths) > 0 { if len(in.BlindedPaymentPaths) > 0 {
blindedPmt, err = parseBlindedPayment(in) blindedPathSet, err = parseBlindedPaymentPaths(in)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if blindedPmt.Features != nil { pathFeatures := blindedPathSet.Features()
destinationFeatures = blindedPmt.Features.Clone() if pathFeatures != nil {
destinationFeatures = pathFeatures.Clone()
} }
} else { } else {
// If we do not have a blinded path, a target pubkey must be // If we do not have a blinded path, a target pubkey must be
@ -387,10 +388,10 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
fromNode, toNode, amt, capacity, fromNode, toNode, amt, capacity,
) )
}, },
DestCustomRecords: record.CustomSet(in.DestCustomRecords), DestCustomRecords: record.CustomSet(in.DestCustomRecords),
CltvLimit: cltvLimit, CltvLimit: cltvLimit,
DestFeatures: destinationFeatures, DestFeatures: destinationFeatures,
BlindedPayment: blindedPmt, BlindedPaymentPathSet: blindedPathSet,
} }
// Pass along an outgoing channel restriction if specified. // Pass along an outgoing channel restriction if specified.
@ -419,39 +420,24 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
return routing.NewRouteRequest( return routing.NewRouteRequest(
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions, sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
customRecords, routeHintEdges, blindedPmt, finalCLTVDelta, customRecords, routeHintEdges, blindedPathSet,
finalCLTVDelta,
) )
} }
func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) ( func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
*routing.BlindedPayment, error) { *routing.BlindedPaymentPathSet, error) {
if len(in.PubKey) != 0 { if len(in.PubKey) != 0 {
return nil, fmt.Errorf("target pubkey: %x should not be set "+ return nil, fmt.Errorf("target pubkey: %x should not be set "+
"when blinded path is provided", in.PubKey) "when blinded path is provided", in.PubKey)
} }
if len(in.BlindedPaymentPaths) != 1 {
return nil, errors.New("query routes only supports a single " +
"blinded path")
}
blindedPath := in.BlindedPaymentPaths[0]
if len(in.RouteHints) > 0 { if len(in.RouteHints) > 0 {
return nil, errors.New("route hints and blinded path can't " + return nil, errors.New("route hints and blinded path can't " +
"both be set") "both be set")
} }
blindedPmt, err := unmarshalBlindedPayment(blindedPath)
if err != nil {
return nil, fmt.Errorf("parse blinded payment: %w", err)
}
if err := blindedPmt.Validate(); err != nil {
return nil, fmt.Errorf("invalid blinded path: %w", err)
}
if in.FinalCltvDelta != 0 { if in.FinalCltvDelta != 0 {
return nil, errors.New("final cltv delta should be " + return nil, errors.New("final cltv delta should be " +
"zero for blinded paths") "zero for blinded paths")
@ -466,7 +452,21 @@ func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
"be populated in blinded path") "be populated in blinded path")
} }
return blindedPmt, nil paths := make([]*routing.BlindedPayment, len(in.BlindedPaymentPaths))
for i, paymentPath := range in.BlindedPaymentPaths {
blindedPmt, err := unmarshalBlindedPayment(paymentPath)
if err != nil {
return nil, fmt.Errorf("parse blinded payment: %w", err)
}
if err := blindedPmt.Validate(); err != nil {
return nil, fmt.Errorf("invalid blinded path: %w", err)
}
paths[i] = blindedPmt
}
return routing.NewBlindedPaymentPathSet(paths)
} }
func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) ( func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
@ -1001,28 +1001,24 @@ func (r *RouterBackend) extractIntentFromSendRequest(
payIntent.Metadata = payReq.Metadata payIntent.Metadata = payReq.Metadata
if len(payReq.BlindedPaymentPaths) > 0 { if len(payReq.BlindedPaymentPaths) > 0 {
// NOTE: Currently we only choose a single payment path. pathSet, err := BuildBlindedPathSet(
// This will be updated in a future PR to handle payReq.BlindedPaymentPaths,
// multiple blinded payment paths. )
path := payReq.BlindedPaymentPaths[0] if err != nil {
if len(path.Hops) == 0 { return nil, err
return nil, fmt.Errorf("a blinded payment " +
"must have at least 1 hop")
} }
payIntent.BlindedPathSet = pathSet
finalHop := path.Hops[len(path.Hops)-1] // Replace the target node with the target public key
// of the blinded path set.
payIntent.BlindedPayment = MarshalBlindedPayment(path)
// Replace the target node with the blinded public key
// of the blinded path's final node.
copy( copy(
payIntent.Target[:], payIntent.Target[:],
finalHop.BlindedNodePub.SerializeCompressed(), pathSet.TargetPubKey().SerializeCompressed(),
) )
if !path.Features.IsEmpty() { pathFeatures := pathSet.Features()
payIntent.DestFeatures = path.Features.Clone() if !pathFeatures.IsEmpty() {
payIntent.DestFeatures = pathFeatures.Clone()
} }
} }
} else { } else {
@ -1163,9 +1159,29 @@ func (r *RouterBackend) extractIntentFromSendRequest(
return payIntent, nil return payIntent, nil
} }
// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a // BuildBlindedPathSet marshals a set of zpay32.BlindedPaymentPath and uses
// the result to build a new routing.BlindedPaymentPathSet.
func BuildBlindedPathSet(paths []*zpay32.BlindedPaymentPath) (
*routing.BlindedPaymentPathSet, error) {
marshalledPaths := make([]*routing.BlindedPayment, len(paths))
for i, path := range paths {
paymentPath := marshalBlindedPayment(path)
err := paymentPath.Validate()
if err != nil {
return nil, err
}
marshalledPaths[i] = paymentPath
}
return routing.NewBlindedPaymentPathSet(marshalledPaths)
}
// marshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
// routing.BlindedPayment. // routing.BlindedPayment.
func MarshalBlindedPayment( func marshalBlindedPayment(
path *zpay32.BlindedPaymentPath) *routing.BlindedPayment { path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
return &routing.BlindedPayment{ return &routing.BlindedPayment{

View file

@ -4,8 +4,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
) )
@ -25,6 +27,218 @@ var (
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum") ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
) )
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
// blinded paths provided by the recipient of a payment.
//
// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
// series, it will handle multiple paths.
type BlindedPaymentPathSet struct {
// paths is the set of blinded payment paths for a single payment.
// NOTE: For now this will always only have a single entry. By the end
// of this PR, it can hold multiple.
paths []*BlindedPayment
// targetPubKey is the ephemeral node pub key that we will inject into
// each path as the last hop. This is only for the sake of path finding.
// Once the path has been found, the original destination pub key is
// used again. In the edge case where there is only a single hop in the
// path (the introduction node is the destination node), then this will
// just be the introduction node's real public key.
targetPubKey *btcec.PublicKey
// features is the set of relay features available for the payment.
// This is extracted from the set of blinded payment paths. At the
// moment we require that all paths for the same payment have the
// same feature set.
features *lnwire.FeatureVector
// finalCLTV is the final hop's expiry delta of _any_ path in the set.
// For any multi-hop path, the final CLTV delta should be seen as zero
// since the final hop's final CLTV delta is accounted for in the
// accumulated path policy values. The only edge case is for when the
// final hop in the path is also the introduction node in which case
// that path's FinalCLTV must be the non-zero min CLTV of the final hop
// so that it is accounted for in path finding. For this reason, if
// we have any single path in the set with only one hop, then we throw
// away all the other paths. This should be fine to do since if there is
// a path where the intro node is also the destination node, then there
// isn't any need to try any other longer blinded path. In other words,
// if this value is non-zero, then there is only one path in this
// blinded path set and that path only has a single hop: the
// introduction node.
finalCLTV uint16
}
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
// BlindedPayments.
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
error) {
if len(paths) == 0 {
return nil, ErrNoBlindedPath
}
// For now, we assert that all the paths have the same set of features.
features := paths[0].Features
noFeatures := features == nil || features.IsEmpty()
for i := 1; i < len(paths); i++ {
noFeats := paths[i].Features == nil ||
paths[i].Features.IsEmpty()
if noFeatures && !noFeats {
return nil, fmt.Errorf("all blinded paths must have " +
"the same set of features")
}
if noFeatures {
continue
}
if !features.RawFeatureVector.Equals(
paths[i].Features.RawFeatureVector,
) {
return nil, fmt.Errorf("all blinded paths must have " +
"the same set of features")
}
}
// Derive an ephemeral target priv key that will be injected into each
// blinded path final hop.
targetPriv, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
targetPub := targetPriv.PubKey()
var (
pathSet = paths
finalCLTVDelta uint16
)
// If any provided blinded path only has a single hop (ie, the
// destination node is also the introduction node), then we discard all
// other paths since we know the real pub key of the destination node.
// We also then set the final CLTV delta to the path's delta since
// there are no other edge hints that will account for it. For a single
// hop path, there is also no need for the pseudo target pub key
// replacement, so our target pub key in this case just remains the
// real introduction node ID.
for _, path := range paths {
if len(path.BlindedPath.BlindedHops) != 1 {
continue
}
pathSet = []*BlindedPayment{path}
finalCLTVDelta = path.CltvExpiryDelta
targetPub = path.BlindedPath.IntroductionPoint
break
}
return &BlindedPaymentPathSet{
paths: pathSet,
targetPubKey: targetPub,
features: features,
finalCLTV: finalCLTVDelta,
}, nil
}
// TargetPubKey returns the public key to be used as the destination node's
// public key during pathfinding.
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
return s.targetPubKey
}
// Features returns the set of relay features available for the payment.
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
return s.features
}
// IntroNodeOnlyPath can be called if it is expected that the path set only
// contains a single payment path which itself only has one hop. It errors if
// this is not the case.
func (s *BlindedPaymentPathSet) IntroNodeOnlyPath() (*BlindedPayment, error) {
if len(s.paths) != 1 {
return nil, fmt.Errorf("expected only a single path in the "+
"blinded payment set, got %d", len(s.paths))
}
if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
return nil, fmt.Errorf("an intro node only path cannot have " +
"more than one hop")
}
return s.paths[0], nil
}
// IsIntroNode returns true if the given vertex is an introduction node for one
// of the paths in the blinded payment path set.
func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
for _, path := range s.paths {
introVertex := route.NewVertex(
path.BlindedPath.IntroductionPoint,
)
if source == introVertex {
return true
}
}
return false
}
// FinalCLTVDelta is the minimum CLTV delta to use for the final hop on the
// route. In most cases this will return zero since the value is accounted for
// in the path's accumulated CLTVExpiryDelta. Only in the edge case of the path
// set only including a single path which only includes an introduction node
// will this return a non-zero value.
func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
return s.finalCLTV
}
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
// largest last-hop payload. This is to be used for onion size estimation in
// path finding.
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
var (
largestPath *BlindedPayment
currentMax int
)
for _, path := range s.paths {
numHops := len(path.BlindedPath.BlindedHops)
lastHop := path.BlindedPath.BlindedHops[numHops-1]
if len(lastHop.CipherText) > currentMax {
largestPath = path
}
}
return largestPath
}
// ToRouteHints converts the blinded path payment set into a RouteHints map so
// that the blinded payment paths can be treated like route hints throughout the
// code base.
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
hints := make(RouteHints)
for _, path := range s.paths {
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
if err != nil {
return nil, err
}
for from, edges := range pathHints {
hints[from] = append(hints[from], edges...)
}
}
if len(hints) == 0 {
return nil, nil
}
return hints, nil
}
// BlindedPayment provides the path and payment parameters required to send a // BlindedPayment provides the path and payment parameters required to send a
// payment along a blinded path. // payment along a blinded path.
type BlindedPayment struct { type BlindedPayment struct {
@ -87,8 +301,11 @@ func (b *BlindedPayment) Validate() error {
// effectively the final_cltv_delta for the receiving introduction node). In // effectively the final_cltv_delta for the receiving introduction node). In
// the case of multiple blinded hops, CLTV delta is fully accounted for in the // the case of multiple blinded hops, CLTV delta is fully accounted for in the
// hints (both for intermediate hops and the final_cltv_delta for the receiving // hints (both for intermediate hops and the final_cltv_delta for the receiving
// node). // node). The pseudoTarget, if provided, will be used to override the pub key
func (b *BlindedPayment) toRouteHints() (RouteHints, error) { // of the destination node in the path.
func (b *BlindedPayment) toRouteHints(
pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
// If we just have a single hop in our blinded route, it just contains // If we just have a single hop in our blinded route, it just contains
// an introduction node (this is a valid path according to the spec). // an introduction node (this is a valid path according to the spec).
// Since we have the un-blinded node ID for the introduction node, we // Since we have the un-blinded node ID for the introduction node, we
@ -136,12 +353,12 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
ToNodeFeatures: features, ToNodeFeatures: features,
} }
edge, err := NewBlindedEdge(edgePolicy, b, 0) lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hints[fromNode] = []AdditionalEdge{edge} hints[fromNode] = []AdditionalEdge{lastEdge}
// Start at an offset of 1 because the first node in our blinded hops // Start at an offset of 1 because the first node in our blinded hops
// is the introduction node and terminate at the second-last node // is the introduction node and terminate at the second-last node
@ -168,13 +385,24 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
ToNodeFeatures: features, ToNodeFeatures: features,
} }
edge, err := NewBlindedEdge(edgePolicy, b, i) lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hints[fromNode] = []AdditionalEdge{edge} hints[fromNode] = []AdditionalEdge{lastEdge}
} }
pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
// For the very last hop on the path, switch out the ToNodePub
// for the pseudo target pub key.
lastEdge.policy.ToNodePubKey = func() route.Vertex {
return route.NewVertex(key)
}
// Then override the final hint with this updated edge.
hints[fromNode] = []AdditionalEdge{lastEdge}
})
return hints, nil return hints, nil
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -128,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
HtlcMaximum: htlcMax, HtlcMaximum: htlcMax,
Features: features, Features: features,
} }
hints, err := blindedPayment.toRouteHints() hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, hints) require.Nil(t, hints)
@ -183,7 +184,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
}, },
} }
actual, err := blindedPayment.toRouteHints() actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(expected), len(actual)) require.Equal(t, len(expected), len(actual))

View file

@ -136,9 +136,8 @@ type finalHopParams struct {
// NOTE: If a non-nil blinded path is provided it is assumed to have been // NOTE: If a non-nil blinded path is provided it is assumed to have been
// validated by the caller. // validated by the caller.
func newRoute(sourceVertex route.Vertex, func newRoute(sourceVertex route.Vertex,
pathEdges []*unifiedEdge, currentHeight uint32, pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
finalHop finalHopParams, blindedPath *sphinx.BlindedPath) ( blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {
*route.Route, error) {
var ( var (
hops []*route.Hop hops []*route.Hop
@ -153,6 +152,8 @@ func newRoute(sourceVertex route.Vertex,
// backwards below, this next hop gets closer and closer to the // backwards below, this next hop gets closer and closer to the
// sender of the payment. // sender of the payment.
nextIncomingAmount lnwire.MilliSatoshi nextIncomingAmount lnwire.MilliSatoshi
blindedPayment *BlindedPayment
) )
pathLength := len(pathEdges) pathLength := len(pathEdges)
@ -161,6 +162,15 @@ func newRoute(sourceVertex route.Vertex,
// payload for the hop this edge is leading to. // payload for the hop this edge is leading to.
edge := pathEdges[i].policy edge := pathEdges[i].policy
// If this is an edge from a blinded path and the
// blindedPayment variable has not been set yet, then set it now
// by extracting the corresponding blinded payment from the
// edge.
isBlindedEdge := pathEdges[i].blindedPayment != nil
if isBlindedEdge && blindedPayment == nil {
blindedPayment = pathEdges[i].blindedPayment
}
// We'll calculate the amounts, timelocks, and fees for each hop // We'll calculate the amounts, timelocks, and fees for each hop
// in the route. The base case is the final hop which includes // in the route. The base case is the final hop which includes
// their amount and timelocks. These values will accumulate // their amount and timelocks. These values will accumulate
@ -200,20 +210,12 @@ func newRoute(sourceVertex route.Vertex,
// reporting through RPC. Set to zero for the final hop. // reporting through RPC. Set to zero for the final hop.
fee = 0 fee = 0
// Only include the final hop CLTV delta in the total if blindedPathSet == nil {
// time lock value if this is not a route to a blinded
// path. For blinded paths, the total time-lock from the
// whole path will be deduced from the introduction
// node's CLTV delta. The exception is for the case
// where the final hop is the blinded path introduction
// node.
if blindedPath == nil ||
len(blindedPath.BlindedHops) == 1 {
// As this is the last hop, we'll use the
// specified final CLTV delta value instead of
// the value from the last link in the route.
totalTimeLock += uint32(finalHop.cltvDelta) totalTimeLock += uint32(finalHop.cltvDelta)
} else {
totalTimeLock += uint32(
blindedPathSet.FinalCLTVDelta(),
)
} }
outgoingTimeLock = totalTimeLock outgoingTimeLock = totalTimeLock
@ -240,7 +242,7 @@ func newRoute(sourceVertex route.Vertex,
metadata = finalHop.metadata metadata = finalHop.metadata
if blindedPath != nil { if blindedPathSet != nil {
totalAmtMsatBlinded = finalHop.totalAmt totalAmtMsatBlinded = finalHop.totalAmt
} }
} else { } else {
@ -300,11 +302,29 @@ func newRoute(sourceVertex route.Vertex,
// If we are creating a route to a blinded path, we need to add some // If we are creating a route to a blinded path, we need to add some
// additional data to the route that is required for blinded forwarding. // additional data to the route that is required for blinded forwarding.
// We do another pass on our edges to append this data. // We do another pass on our edges to append this data.
if blindedPath != nil { if blindedPathSet != nil {
// If the passed in BlindedPaymentPathSet is non-nil but no
// edge had a BlindedPayment attached, it means that the path
// chosen was an introduction-node-only path. So in this case,
// we can assume the relevant payment is the only one in the
// payment set.
if blindedPayment == nil {
var err error
blindedPayment, err = blindedPathSet.IntroNodeOnlyPath()
if err != nil {
return nil, err
}
}
var ( var (
inBlindedRoute bool inBlindedRoute bool
dataIndex = 0 dataIndex = 0
blindedPath = blindedPayment.BlindedPath
numHops = len(blindedPath.BlindedHops)
realFinal = blindedPath.BlindedHops[numHops-1].
BlindedNodePub
introVertex = route.NewVertex( introVertex = route.NewVertex(
blindedPath.IntroductionPoint, blindedPath.IntroductionPoint,
) )
@ -332,6 +352,11 @@ func newRoute(sourceVertex route.Vertex,
if i != len(hops)-1 { if i != len(hops)-1 {
hop.AmtToForward = 0 hop.AmtToForward = 0
hop.OutgoingTimeLock = 0 hop.OutgoingTimeLock = 0
} else {
// For the final hop, we swap out the pub key
// bytes to the original destination node pub
// key for that payment path.
hop.PubKeyBytes = route.NewVertex(realFinal)
} }
dataIndex++ dataIndex++
@ -437,9 +462,9 @@ type RestrictParams struct {
// the payee. // the payee.
Metadata []byte Metadata []byte
// BlindedPayment is necessary to determine the hop size of the // BlindedPaymentPathSet is necessary to determine the hop size of the
// last/exit hop. // last/exit hop.
BlindedPayment *BlindedPayment BlindedPaymentPathSet *BlindedPaymentPathSet
} }
// PathFindingConfig defines global parameters that control the trade-off in // PathFindingConfig defines global parameters that control the trade-off in
@ -1131,7 +1156,7 @@ type blindedPathRestrictions struct {
// path. // path.
type blindedHop struct { type blindedHop struct {
vertex route.Vertex vertex route.Vertex
edgePolicy *models.CachedEdgePolicy channelID uint64
edgeCapacity btcutil.Amount edgeCapacity btcutil.Amount
} }
@ -1271,7 +1296,7 @@ func processNodeForBlindedPath(g Graph, node route.Vertex,
hop := blindedHop{ hop := blindedHop{
vertex: channel.OtherNode, vertex: channel.OtherNode,
edgePolicy: channel.InPolicy, channelID: channel.ChannelID,
edgeCapacity: channel.Capacity, edgeCapacity: channel.Capacity,
} }
@ -1365,9 +1390,11 @@ func getProbabilityBasedDist(weight int64, probability float64,
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32, func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
amount lnwire.MilliSatoshi) uint64 { amount lnwire.MilliSatoshi) uint64 {
if r.BlindedPayment != nil { if r.BlindedPaymentPathSet != nil {
blindedPath := r.BlindedPayment.BlindedPath.BlindedHops paymentPath := r.BlindedPaymentPathSet.
blindedPoint := r.BlindedPayment.BlindedPath.BlindingPoint LargestLastHopPayloadPath()
blindedPath := paymentPath.BlindedPath.BlindedHops
blindedPoint := paymentPath.BlindedPath.BlindingPoint
encryptedData := blindedPath[len(blindedPath)-1].CipherText encryptedData := blindedPath[len(blindedPath)-1].CipherText
finalHop := route.Hop{ finalHop := route.Hop{

View file

@ -23,6 +23,7 @@ import (
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop" switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
@ -3277,21 +3278,39 @@ func TestBlindedRouteConstruction(t *testing.T) {
require.NoError(t, blindedPayment.Validate()) require.NoError(t, blindedPayment.Validate())
blindedPathSet, err := NewBlindedPaymentPathSet(
[]*BlindedPayment{blindedPayment},
)
require.NoError(t, err)
// Generate route hints from our blinded payment and a set of edges // Generate route hints from our blinded payment and a set of edges
// that make up the graph we'll give to route construction. The hints // that make up the graph we'll give to route construction. The hints
// map is keyed by source node, so we can retrieve our blinded edges // map is keyed by source node, so we can retrieve our blinded edges
// accordingly. // accordingly.
blindedEdges, err := blindedPayment.toRouteHints() blindedEdges, err := blindedPayment.toRouteHints(
fn.None[*btcec.PublicKey](),
)
require.NoError(t, err) require.NoError(t, err)
carolDaveEdge := blindedEdges[carolVertex][0] carolDaveEdge := blindedEdges[carolVertex][0]
daveEveEdge := blindedEdges[daveBlindedVertex][0] daveEveEdge := blindedEdges[daveBlindedVertex][0]
edges := []*unifiedEdge{ edges := []*unifiedEdge{
{policy: aliceBobEdge}, {
{policy: bobCarolEdge}, policy: aliceBobEdge,
{policy: carolDaveEdge.EdgePolicy()}, },
{policy: daveEveEdge.EdgePolicy()}, {
policy: bobCarolEdge,
blindedPayment: blindedPayment,
},
{
policy: carolDaveEdge.EdgePolicy(),
blindedPayment: blindedPayment,
},
{
policy: daveEveEdge.EdgePolicy(),
blindedPayment: blindedPayment,
},
} }
// Total timelock for the route should include: // Total timelock for the route should include:
@ -3382,7 +3401,7 @@ func TestBlindedRouteConstruction(t *testing.T) {
route, err := newRoute( route, err := newRoute(
sourceVertex, edges, currentHeight, finalHopParams, sourceVertex, edges, currentHeight, finalHopParams,
blindedPath, blindedPathSet,
) )
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedRoute, route) require.Equal(t, expectedRoute, route)
@ -3409,31 +3428,38 @@ func TestLastHopPayloadSize(t *testing.T) {
amtToForward = lnwire.MilliSatoshi(10000) amtToForward = lnwire.MilliSatoshi(10000)
finalHopExpiry int32 = 144 finalHopExpiry int32 = 144
oneHopBlindedPayment = &BlindedPayment{ oneHopPath = &sphinx.BlindedPath{
BlindedPath: &sphinx.BlindedPath{ BlindedHops: []*sphinx.BlindedHopInfo{
BlindedHops: []*sphinx.BlindedHopInfo{ {
{ CipherText: encrypedData,
CipherText: encrypedData,
},
}, },
BlindingPoint: blindedPoint,
}, },
BlindingPoint: blindedPoint,
} }
twoHopBlindedPayment = &BlindedPayment{
BlindedPath: &sphinx.BlindedPath{ twoHopPath = &sphinx.BlindedPath{
BlindedHops: []*sphinx.BlindedHopInfo{ BlindedHops: []*sphinx.BlindedHopInfo{
{ {
CipherText: encrypedData, CipherText: encrypedData,
}, },
{ {
CipherText: encrypedData, CipherText: encrypedData,
},
}, },
BlindingPoint: blindedPoint,
}, },
BlindingPoint: blindedPoint,
} }
) )
oneHopBlindedPayment, err := NewBlindedPaymentPathSet(
[]*BlindedPayment{{BlindedPath: oneHopPath}},
)
require.NoError(t, err)
twoHopBlindedPayment, err := NewBlindedPaymentPathSet(
[]*BlindedPayment{{BlindedPath: twoHopPath}},
)
require.NoError(t, err)
testCases := []struct { testCases := []struct {
name string name string
restrictions *RestrictParams restrictions *RestrictParams
@ -3454,7 +3480,7 @@ func TestLastHopPayloadSize(t *testing.T) {
{ {
name: "Blinded final hop introduction point", name: "Blinded final hop introduction point",
restrictions: &RestrictParams{ restrictions: &RestrictParams{
BlindedPayment: oneHopBlindedPayment, BlindedPaymentPathSet: oneHopBlindedPayment,
}, },
amount: amtToForward, amount: amtToForward,
finalHopExpiry: finalHopExpiry, finalHopExpiry: finalHopExpiry,
@ -3462,7 +3488,7 @@ func TestLastHopPayloadSize(t *testing.T) {
{ {
name: "Blinded final hop of a two hop payment", name: "Blinded final hop of a two hop payment",
restrictions: &RestrictParams{ restrictions: &RestrictParams{
BlindedPayment: twoHopBlindedPayment, BlindedPaymentPathSet: twoHopBlindedPayment,
}, },
amount: amtToForward, amount: amtToForward,
finalHopExpiry: finalHopExpiry, finalHopExpiry: finalHopExpiry,
@ -3490,12 +3516,11 @@ func TestLastHopPayloadSize(t *testing.T) {
} }
var finalHop route.Hop var finalHop route.Hop
if tc.restrictions.BlindedPayment != nil { if tc.restrictions.BlindedPaymentPathSet != nil {
blindedPath := tc.restrictions.BlindedPayment. path := tc.restrictions.BlindedPaymentPathSet.
BlindedPath.BlindedHops LargestLastHopPayloadPath()
blindedPath := path.BlindedPath.BlindedHops
blindedPoint := tc.restrictions.BlindedPayment. blindedPoint := path.BlindedPath.BlindingPoint
BlindedPath.BlindingPoint
//nolint:lll //nolint:lll
finalHop = route.Hop{ finalHop = route.Hop{

View file

@ -5,7 +5,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btclog" "github.com/btcsuite/btclog"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
@ -206,13 +205,13 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
return nil, err return nil, err
} }
if p.BlindedPayment != nil { if p.BlindedPathSet != nil {
if len(edges) != 0 { if len(edges) != 0 {
return nil, fmt.Errorf("cannot have both route hints " + return nil, fmt.Errorf("cannot have both route hints " +
"and blinded path") "and blinded path")
} }
edges, err = p.BlindedPayment.toRouteHints() edges, err = p.BlindedPathSet.ToRouteHints()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -342,7 +341,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
// can split. Split payments to blinded paths won't have // can split. Split payments to blinded paths won't have
// MPP records. // MPP records.
if p.payment.PaymentAddr == nil && if p.payment.PaymentAddr == nil &&
p.payment.BlindedPayment == nil { p.payment.BlindedPathSet == nil {
p.log.Debugf("not splitting because payment " + p.log.Debugf("not splitting because payment " +
"address is unspecified") "address is unspecified")
@ -407,11 +406,6 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
return nil, err return nil, err
} }
var blindedPath *sphinx.BlindedPath
if p.payment.BlindedPayment != nil {
blindedPath = p.payment.BlindedPayment.BlindedPath
}
// With the next candidate path found, we'll attempt to turn // With the next candidate path found, we'll attempt to turn
// this into a route by applying the time-lock and fee // this into a route by applying the time-lock and fee
// requirements. // requirements.
@ -424,7 +418,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
records: p.payment.DestCustomRecords, records: p.payment.DestCustomRecords,
paymentAddr: p.payment.PaymentAddr, paymentAddr: p.payment.PaymentAddr,
metadata: p.payment.Metadata, metadata: p.payment.Metadata,
}, blindedPath, }, p.payment.BlindedPathSet,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -477,10 +477,10 @@ type RouteRequest struct {
// in blinded payment. // in blinded payment.
FinalExpiry uint16 FinalExpiry uint16
// BlindedPayment contains an optional blinded path and parameters // BlindedPathSet contains a set of optional blinded paths and
// used to reach a target node via a blinded path. This field is // parameters used to reach a target node blinded paths. This field is
// mutually exclusive with the Target field. // mutually exclusive with the Target field.
BlindedPayment *BlindedPayment BlindedPathSet *BlindedPaymentPathSet
} }
// RouteHints is an alias type for a set of route hints, with the source node // RouteHints is an alias type for a set of route hints, with the source node
@ -494,7 +494,7 @@ type RouteHints map[route.Vertex][]AdditionalEdge
func NewRouteRequest(source route.Vertex, target *route.Vertex, func NewRouteRequest(source route.Vertex, target *route.Vertex,
amount lnwire.MilliSatoshi, timePref float64, amount lnwire.MilliSatoshi, timePref float64,
restrictions *RestrictParams, customRecords record.CustomSet, restrictions *RestrictParams, customRecords record.CustomSet,
routeHints RouteHints, blindedPayment *BlindedPayment, routeHints RouteHints, blindedPathSet *BlindedPaymentPathSet,
finalExpiry uint16) (*RouteRequest, error) { finalExpiry uint16) (*RouteRequest, error) {
var ( var (
@ -504,16 +504,8 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
err error err error
) )
if blindedPayment != nil { if blindedPathSet != nil {
if err := blindedPayment.Validate(); err != nil { if blindedPathSet.IsIntroNode(source) {
return nil, fmt.Errorf("invalid blinded payment: %w",
err)
}
introVertex := route.NewVertex(
blindedPayment.BlindedPath.IntroductionPoint,
)
if source == introVertex {
return nil, ErrSelfIntro return nil, ErrSelfIntro
} }
@ -527,25 +519,15 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
return nil, ErrExpiryAndBlinded return nil, ErrExpiryAndBlinded
} }
// If we have a blinded path with 1 hop, the cltv expiry requestExpiry = blindedPathSet.FinalCLTVDelta()
// will not be included in any hop hints (since we're just
// sending to the introduction node and need no blinded hints).
// In this case, we include it to make sure that the final
// cltv delta is accounted for (since it's part of the blinded
// delta). In the case of a multi-hop route, we set our final
// cltv to zero, since it's going to be accounted for in the
// delta for our hints.
if len(blindedPayment.BlindedPath.BlindedHops) == 1 {
requestExpiry = blindedPayment.CltvExpiryDelta
}
requestHints, err = blindedPayment.toRouteHints() requestHints, err = blindedPathSet.ToRouteHints()
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
requestTarget, err := getTargetNode(target, blindedPayment) requestTarget, err := getTargetNode(target, blindedPathSet)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -559,15 +541,15 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
CustomRecords: customRecords, CustomRecords: customRecords,
RouteHints: requestHints, RouteHints: requestHints,
FinalExpiry: requestExpiry, FinalExpiry: requestExpiry,
BlindedPayment: blindedPayment, BlindedPathSet: blindedPathSet,
}, nil }, nil
} }
func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) ( func getTargetNode(target *route.Vertex,
route.Vertex, error) { blindedPathSet *BlindedPaymentPathSet) (route.Vertex, error) {
var ( var (
blinded = blindedPayment != nil blinded = blindedPathSet != nil
targetSet = target != nil targetSet = target != nil
) )
@ -576,18 +558,7 @@ func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
return route.Vertex{}, ErrTargetAndBlinded return route.Vertex{}, ErrTargetAndBlinded
case blinded: case blinded:
// If we're dealing with an edge-case blinded path that just return route.NewVertex(blindedPathSet.TargetPubKey()), nil
// has an introduction node (first hop expected to be the intro
// hop), then we return the unblinded introduction node as our
// target.
hops := blindedPayment.BlindedPath.BlindedHops
if len(hops) == 1 {
return route.NewVertex(
blindedPayment.BlindedPath.IntroductionPoint,
), nil
}
return route.NewVertex(hops[len(hops)-1].BlindedNodePub), nil
case targetSet: case targetSet:
return *target, nil return *target, nil
@ -597,16 +568,6 @@ func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
} }
} }
// blindedPath returns the request's blinded path, which is set if the payment
// is to a blinded route.
func (r *RouteRequest) blindedPath() *sphinx.BlindedPath {
if r.BlindedPayment == nil {
return nil
}
return r.BlindedPayment.BlindedPath
}
// FindRoute attempts to query the ChannelRouter for the optimum path to a // FindRoute attempts to query the ChannelRouter for the optimum path to a
// particular target destination to which it is able to send `amt` after // particular target destination to which it is able to send `amt` after
// factoring in channel capacities and cumulative fees along the route. // factoring in channel capacities and cumulative fees along the route.
@ -664,7 +625,7 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
totalAmt: req.Amount, totalAmt: req.Amount,
cltvDelta: req.FinalExpiry, cltvDelta: req.FinalExpiry,
records: req.CustomRecords, records: req.CustomRecords,
}, req.blindedPath(), }, req.BlindedPathSet,
) )
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -761,7 +722,7 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
hops = append(hops, &route.Hop{ hops = append(hops, &route.Hop{
PubKeyBytes: path[j].vertex, PubKeyBytes: path[j].vertex,
ChannelID: path[j-1].edgePolicy.ChannelID, ChannelID: path[j-1].channelID,
}) })
prevNode = path[j].vertex prevNode = path[j].vertex
@ -926,14 +887,10 @@ type LightningPayment struct {
// BlindedPayment field. // BlindedPayment field.
RouteHints [][]zpay32.HopHint RouteHints [][]zpay32.HopHint
// BlindedPayment holds the information about a blinded path to the // BlindedPathSet holds the information about a set of blinded paths to
// payment recipient. This is mutually exclusive to the RouteHints // the payment recipient. This is mutually exclusive to the RouteHints
// field. // field.
// BlindedPathSet *BlindedPaymentPathSet
// NOTE: a recipient may provide multiple blinded payment paths in the
// same invoice. Currently, LND will only attempt to use the first one.
// A future PR will handle multiple blinded payment paths.
BlindedPayment *BlindedPayment
// OutgoingChannelIDs is the list of channels that are allowed for the // OutgoingChannelIDs is the list of channels that are allowed for the
// first hop. If nil, any channel may be used. // first hop. If nil, any channel may be used.

View file

@ -2223,11 +2223,6 @@ func TestNewRouteRequest(t *testing.T) {
finalExpiry: unblindedCltv, finalExpiry: unblindedCltv,
err: ErrExpiryAndBlinded, err: ErrExpiryAndBlinded,
}, },
{
name: "invalid blinded payment",
blindedPayment: &BlindedPayment{},
err: ErrNoBlindedPath,
},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@ -2236,9 +2231,26 @@ func TestNewRouteRequest(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
t.Parallel() t.Parallel()
var (
blindedPathInfo *BlindedPaymentPathSet
expectedTarget = testCase.expectedTarget
)
if testCase.blindedPayment != nil {
blindedPathInfo, err = NewBlindedPaymentPathSet(
[]*BlindedPayment{
testCase.blindedPayment,
},
)
require.NoError(t, err)
expectedTarget = route.NewVertex(
blindedPathInfo.TargetPubKey(),
)
}
req, err := NewRouteRequest( req, err := NewRouteRequest(
source, testCase.target, 1000, 0, nil, nil, source, testCase.target, 1000, 0, nil, nil,
testCase.routeHints, testCase.blindedPayment, testCase.routeHints, blindedPathInfo,
testCase.finalExpiry, testCase.finalExpiry,
) )
require.ErrorIs(t, err, testCase.err) require.ErrorIs(t, err, testCase.err)
@ -2248,7 +2260,7 @@ func TestNewRouteRequest(t *testing.T) {
return return
} }
require.Equal(t, req.Target, testCase.expectedTarget) require.Equal(t, req.Target, expectedTarget)
require.Equal( require.Equal(
t, req.FinalExpiry, testCase.expectedCltv, t, req.FinalExpiry, testCase.expectedCltv,
) )

View file

@ -5105,7 +5105,7 @@ type rpcPaymentIntent struct {
paymentAddr *[32]byte paymentAddr *[32]byte
payReq []byte payReq []byte
metadata []byte metadata []byte
blindedPayment *routing.BlindedPayment blindedPathSet *routing.BlindedPaymentPathSet
destCustomRecords record.CustomSet destCustomRecords record.CustomSet
@ -5242,28 +5242,24 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
payIntent.metadata = payReq.Metadata payIntent.metadata = payReq.Metadata
if len(payReq.BlindedPaymentPaths) > 0 { if len(payReq.BlindedPaymentPaths) > 0 {
// NOTE: Currently we only choose a single payment path. pathSet, err := routerrpc.BuildBlindedPathSet(
// This will be updated in a future PR to handle payReq.BlindedPaymentPaths,
// multiple blinded payment paths. )
path := payReq.BlindedPaymentPaths[0] if err != nil {
if len(path.Hops) == 0 { return payIntent, err
return payIntent, fmt.Errorf("a blinded " +
"payment must have at least 1 hop")
} }
payIntent.blindedPathSet = pathSet
finalHop := path.Hops[len(path.Hops)-1] // Replace the destination node with the target public
payIntent.blindedPayment = // key of the blinded path set.
routerrpc.MarshalBlindedPayment(path)
// Replace the target node with the blinded public key
// of the blinded path's final node.
copy( copy(
payIntent.dest[:], payIntent.dest[:],
finalHop.BlindedNodePub.SerializeCompressed(), pathSet.TargetPubKey().SerializeCompressed(),
) )
if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() { pathFeatures := pathSet.Features()
payIntent.destFeatures = path.Features.Clone() if !pathFeatures.IsEmpty() {
payIntent.destFeatures = pathFeatures.Clone()
} }
} }
@ -5421,7 +5417,7 @@ func (r *rpcServer) dispatchPaymentIntent(
DestFeatures: payIntent.destFeatures, DestFeatures: payIntent.destFeatures,
PaymentAddr: payIntent.paymentAddr, PaymentAddr: payIntent.paymentAddr,
Metadata: payIntent.metadata, Metadata: payIntent.metadata,
BlindedPayment: payIntent.blindedPayment, BlindedPathSet: payIntent.blindedPathSet,
// Don't enable multi-part payments on the main rpc. // Don't enable multi-part payments on the main rpc.
// Users need to use routerrpc for that. // Users need to use routerrpc for that.

View file

@ -4,7 +4,6 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"math"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
@ -21,6 +20,12 @@ const (
// proposal](https://github.com/lightning/blips/pull/39) for a detailed // proposal](https://github.com/lightning/blips/pull/39) for a detailed
// calculation. // calculation.
maxNumHopsPerPath = 7 maxNumHopsPerPath = 7
// maxCipherTextLength defines the largest cipher text size allowed.
// This is derived by using the `data_length` upper bound of 639 bytes
// and then assuming the case of a path with only a single hop (meaning
// the cipher text may be as large as possible).
maxCipherTextLength = 535
) )
var ( var (
@ -215,6 +220,12 @@ func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) {
return nil, err return nil, err
} }
if dataLen > maxCipherTextLength {
return nil, fmt.Errorf("a blinded hop cipher text blob may "+
"not exceed the maximum of %d bytes",
maxCipherTextLength)
}
encryptedData := make([]byte, dataLen) encryptedData := make([]byte, dataLen)
_, err = r.Read(encryptedData) _, err = r.Read(encryptedData)
if err != nil { if err != nil {
@ -238,9 +249,9 @@ func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error {
return err return err
} }
if len(hop.CipherText) > math.MaxUint16 { if len(hop.CipherText) > maxCipherTextLength {
return fmt.Errorf("encrypted recipient data can not exceed a "+ return fmt.Errorf("encrypted recipient data can not exceed a "+
"length of %d bytes", math.MaxUint16) "length of %d bytes", maxCipherTextLength)
} }
err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{}) err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{})