mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-13 11:09:23 +01:00
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:
commit
04dde98edc
13 changed files with 647 additions and 210 deletions
|
@ -136,6 +136,9 @@ commitment when the channel was force closed.
|
|||
the `lncli addinvoice` command to instruct LND to include blinded paths in the
|
||||
invoice.
|
||||
|
||||
* Add the ability to [send to use multiple blinded payment
|
||||
paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.
|
||||
|
||||
## Testing
|
||||
## Database
|
||||
|
||||
|
|
|
@ -594,6 +594,10 @@ var allTestCases = []*lntest.TestCase{
|
|||
Name: "mpp to single blinded path",
|
||||
TestFunc: testMPPToSingleBlindedPath,
|
||||
},
|
||||
{
|
||||
Name: "mpp to multiple blinded paths",
|
||||
TestFunc: testMPPToMultipleBlindedPaths,
|
||||
},
|
||||
{
|
||||
Name: "route blinding dummy hops",
|
||||
TestFunc: testBlindedRouteDummyHops,
|
||||
|
|
|
@ -1229,3 +1229,166 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
|
|||
var (
|
||||
targetPubKey *route.Vertex
|
||||
routeHintEdges map[route.Vertex][]routing.AdditionalEdge
|
||||
blindedPmt *routing.BlindedPayment
|
||||
blindedPathSet *routing.BlindedPaymentPathSet
|
||||
|
||||
// finalCLTVDelta varies depending on whether we're sending to
|
||||
// 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
|
||||
// on whether it is using a blinded path or not.
|
||||
if len(in.BlindedPaymentPaths) > 0 {
|
||||
blindedPmt, err = parseBlindedPayment(in)
|
||||
blindedPathSet, err = parseBlindedPaymentPaths(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if blindedPmt.Features != nil {
|
||||
destinationFeatures = blindedPmt.Features.Clone()
|
||||
pathFeatures := blindedPathSet.Features()
|
||||
if pathFeatures != nil {
|
||||
destinationFeatures = pathFeatures.Clone()
|
||||
}
|
||||
} else {
|
||||
// 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,
|
||||
)
|
||||
},
|
||||
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
|
||||
CltvLimit: cltvLimit,
|
||||
DestFeatures: destinationFeatures,
|
||||
BlindedPayment: blindedPmt,
|
||||
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
|
||||
CltvLimit: cltvLimit,
|
||||
DestFeatures: destinationFeatures,
|
||||
BlindedPaymentPathSet: blindedPathSet,
|
||||
}
|
||||
|
||||
// Pass along an outgoing channel restriction if specified.
|
||||
|
@ -419,39 +420,24 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
|
|||
|
||||
return routing.NewRouteRequest(
|
||||
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
|
||||
customRecords, routeHintEdges, blindedPmt, finalCLTVDelta,
|
||||
customRecords, routeHintEdges, blindedPathSet,
|
||||
finalCLTVDelta,
|
||||
)
|
||||
}
|
||||
|
||||
func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
|
||||
*routing.BlindedPayment, error) {
|
||||
func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
|
||||
*routing.BlindedPaymentPathSet, error) {
|
||||
|
||||
if len(in.PubKey) != 0 {
|
||||
return nil, fmt.Errorf("target pubkey: %x should not be set "+
|
||||
"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 {
|
||||
return nil, errors.New("route hints and blinded path can't " +
|
||||
"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 {
|
||||
return nil, errors.New("final cltv delta should be " +
|
||||
"zero for blinded paths")
|
||||
|
@ -466,7 +452,21 @@ func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
|
|||
"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) (
|
||||
|
@ -1001,28 +1001,24 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
|||
payIntent.Metadata = payReq.Metadata
|
||||
|
||||
if len(payReq.BlindedPaymentPaths) > 0 {
|
||||
// NOTE: Currently we only choose a single payment path.
|
||||
// This will be updated in a future PR to handle
|
||||
// multiple blinded payment paths.
|
||||
path := payReq.BlindedPaymentPaths[0]
|
||||
if len(path.Hops) == 0 {
|
||||
return nil, fmt.Errorf("a blinded payment " +
|
||||
"must have at least 1 hop")
|
||||
pathSet, err := BuildBlindedPathSet(
|
||||
payReq.BlindedPaymentPaths,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payIntent.BlindedPathSet = pathSet
|
||||
|
||||
finalHop := path.Hops[len(path.Hops)-1]
|
||||
|
||||
payIntent.BlindedPayment = MarshalBlindedPayment(path)
|
||||
|
||||
// Replace the target node with the blinded public key
|
||||
// of the blinded path's final node.
|
||||
// Replace the target node with the target public key
|
||||
// of the blinded path set.
|
||||
copy(
|
||||
payIntent.Target[:],
|
||||
finalHop.BlindedNodePub.SerializeCompressed(),
|
||||
pathSet.TargetPubKey().SerializeCompressed(),
|
||||
)
|
||||
|
||||
if !path.Features.IsEmpty() {
|
||||
payIntent.DestFeatures = path.Features.Clone()
|
||||
pathFeatures := pathSet.Features()
|
||||
if !pathFeatures.IsEmpty() {
|
||||
payIntent.DestFeatures = pathFeatures.Clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -1163,9 +1159,29 @@ func (r *RouterBackend) extractIntentFromSendRequest(
|
|||
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.
|
||||
func MarshalBlindedPayment(
|
||||
func marshalBlindedPayment(
|
||||
path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
|
||||
|
||||
return &routing.BlindedPayment{
|
||||
|
|
|
@ -4,8 +4,10 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
)
|
||||
|
@ -25,6 +27,218 @@ var (
|
|||
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
|
||||
// payment along a blinded path.
|
||||
type BlindedPayment struct {
|
||||
|
@ -87,8 +301,11 @@ func (b *BlindedPayment) Validate() error {
|
|||
// 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
|
||||
// hints (both for intermediate hops and the final_cltv_delta for the receiving
|
||||
// node).
|
||||
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
|
||||
// node). The pseudoTarget, if provided, will be used to override the pub key
|
||||
// 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
|
||||
// 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
|
||||
|
@ -136,12 +353,12 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
|
|||
ToNodeFeatures: features,
|
||||
}
|
||||
|
||||
edge, err := NewBlindedEdge(edgePolicy, b, 0)
|
||||
lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
|
||||
if err != nil {
|
||||
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
|
||||
// is the introduction node and terminate at the second-last node
|
||||
|
@ -168,13 +385,24 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
|
|||
ToNodeFeatures: features,
|
||||
}
|
||||
|
||||
edge, err := NewBlindedEdge(edgePolicy, b, i)
|
||||
lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -128,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
|
|||
HtlcMaximum: htlcMax,
|
||||
Features: features,
|
||||
}
|
||||
hints, err := blindedPayment.toRouteHints()
|
||||
hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
|
||||
require.NoError(t, err)
|
||||
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.Equal(t, len(expected), len(actual))
|
||||
|
|
|
@ -136,9 +136,8 @@ type finalHopParams struct {
|
|||
// NOTE: If a non-nil blinded path is provided it is assumed to have been
|
||||
// validated by the caller.
|
||||
func newRoute(sourceVertex route.Vertex,
|
||||
pathEdges []*unifiedEdge, currentHeight uint32,
|
||||
finalHop finalHopParams, blindedPath *sphinx.BlindedPath) (
|
||||
*route.Route, error) {
|
||||
pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
|
||||
blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {
|
||||
|
||||
var (
|
||||
hops []*route.Hop
|
||||
|
@ -153,6 +152,8 @@ func newRoute(sourceVertex route.Vertex,
|
|||
// backwards below, this next hop gets closer and closer to the
|
||||
// sender of the payment.
|
||||
nextIncomingAmount lnwire.MilliSatoshi
|
||||
|
||||
blindedPayment *BlindedPayment
|
||||
)
|
||||
|
||||
pathLength := len(pathEdges)
|
||||
|
@ -161,6 +162,15 @@ func newRoute(sourceVertex route.Vertex,
|
|||
// payload for the hop this edge is leading to.
|
||||
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
|
||||
// in the route. The base case is the final hop which includes
|
||||
// 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.
|
||||
fee = 0
|
||||
|
||||
// Only include the final hop CLTV delta in the total
|
||||
// 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.
|
||||
if blindedPathSet == nil {
|
||||
totalTimeLock += uint32(finalHop.cltvDelta)
|
||||
} else {
|
||||
totalTimeLock += uint32(
|
||||
blindedPathSet.FinalCLTVDelta(),
|
||||
)
|
||||
}
|
||||
outgoingTimeLock = totalTimeLock
|
||||
|
||||
|
@ -240,7 +242,7 @@ func newRoute(sourceVertex route.Vertex,
|
|||
|
||||
metadata = finalHop.metadata
|
||||
|
||||
if blindedPath != nil {
|
||||
if blindedPathSet != nil {
|
||||
totalAmtMsatBlinded = finalHop.totalAmt
|
||||
}
|
||||
} 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
|
||||
// additional data to the route that is required for blinded forwarding.
|
||||
// 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 (
|
||||
inBlindedRoute bool
|
||||
dataIndex = 0
|
||||
|
||||
blindedPath = blindedPayment.BlindedPath
|
||||
numHops = len(blindedPath.BlindedHops)
|
||||
realFinal = blindedPath.BlindedHops[numHops-1].
|
||||
BlindedNodePub
|
||||
|
||||
introVertex = route.NewVertex(
|
||||
blindedPath.IntroductionPoint,
|
||||
)
|
||||
|
@ -332,6 +352,11 @@ func newRoute(sourceVertex route.Vertex,
|
|||
if i != len(hops)-1 {
|
||||
hop.AmtToForward = 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++
|
||||
|
@ -437,9 +462,9 @@ type RestrictParams struct {
|
|||
// the payee.
|
||||
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.
|
||||
BlindedPayment *BlindedPayment
|
||||
BlindedPaymentPathSet *BlindedPaymentPathSet
|
||||
}
|
||||
|
||||
// PathFindingConfig defines global parameters that control the trade-off in
|
||||
|
@ -1131,7 +1156,7 @@ type blindedPathRestrictions struct {
|
|||
// path.
|
||||
type blindedHop struct {
|
||||
vertex route.Vertex
|
||||
edgePolicy *models.CachedEdgePolicy
|
||||
channelID uint64
|
||||
edgeCapacity btcutil.Amount
|
||||
}
|
||||
|
||||
|
@ -1271,7 +1296,7 @@ func processNodeForBlindedPath(g Graph, node route.Vertex,
|
|||
|
||||
hop := blindedHop{
|
||||
vertex: channel.OtherNode,
|
||||
edgePolicy: channel.InPolicy,
|
||||
channelID: channel.ChannelID,
|
||||
edgeCapacity: channel.Capacity,
|
||||
}
|
||||
|
||||
|
@ -1365,9 +1390,11 @@ func getProbabilityBasedDist(weight int64, probability float64,
|
|||
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
||||
amount lnwire.MilliSatoshi) uint64 {
|
||||
|
||||
if r.BlindedPayment != nil {
|
||||
blindedPath := r.BlindedPayment.BlindedPath.BlindedHops
|
||||
blindedPoint := r.BlindedPayment.BlindedPath.BlindingPoint
|
||||
if r.BlindedPaymentPathSet != nil {
|
||||
paymentPath := r.BlindedPaymentPathSet.
|
||||
LargestLastHopPayloadPath()
|
||||
blindedPath := paymentPath.BlindedPath.BlindedHops
|
||||
blindedPoint := paymentPath.BlindedPath.BlindingPoint
|
||||
|
||||
encryptedData := blindedPath[len(blindedPath)-1].CipherText
|
||||
finalHop := route.Hop{
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
|
@ -3277,21 +3278,39 @@ func TestBlindedRouteConstruction(t *testing.T) {
|
|||
|
||||
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
|
||||
// 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
|
||||
// accordingly.
|
||||
blindedEdges, err := blindedPayment.toRouteHints()
|
||||
blindedEdges, err := blindedPayment.toRouteHints(
|
||||
fn.None[*btcec.PublicKey](),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
carolDaveEdge := blindedEdges[carolVertex][0]
|
||||
daveEveEdge := blindedEdges[daveBlindedVertex][0]
|
||||
|
||||
edges := []*unifiedEdge{
|
||||
{policy: aliceBobEdge},
|
||||
{policy: bobCarolEdge},
|
||||
{policy: carolDaveEdge.EdgePolicy()},
|
||||
{policy: daveEveEdge.EdgePolicy()},
|
||||
{
|
||||
policy: aliceBobEdge,
|
||||
},
|
||||
{
|
||||
policy: bobCarolEdge,
|
||||
blindedPayment: blindedPayment,
|
||||
},
|
||||
{
|
||||
policy: carolDaveEdge.EdgePolicy(),
|
||||
blindedPayment: blindedPayment,
|
||||
},
|
||||
{
|
||||
policy: daveEveEdge.EdgePolicy(),
|
||||
blindedPayment: blindedPayment,
|
||||
},
|
||||
}
|
||||
|
||||
// Total timelock for the route should include:
|
||||
|
@ -3382,7 +3401,7 @@ func TestBlindedRouteConstruction(t *testing.T) {
|
|||
|
||||
route, err := newRoute(
|
||||
sourceVertex, edges, currentHeight, finalHopParams,
|
||||
blindedPath,
|
||||
blindedPathSet,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRoute, route)
|
||||
|
@ -3409,31 +3428,38 @@ func TestLastHopPayloadSize(t *testing.T) {
|
|||
amtToForward = lnwire.MilliSatoshi(10000)
|
||||
finalHopExpiry int32 = 144
|
||||
|
||||
oneHopBlindedPayment = &BlindedPayment{
|
||||
BlindedPath: &sphinx.BlindedPath{
|
||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||
{
|
||||
CipherText: encrypedData,
|
||||
},
|
||||
oneHopPath = &sphinx.BlindedPath{
|
||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||
{
|
||||
CipherText: encrypedData,
|
||||
},
|
||||
BlindingPoint: blindedPoint,
|
||||
},
|
||||
BlindingPoint: blindedPoint,
|
||||
}
|
||||
twoHopBlindedPayment = &BlindedPayment{
|
||||
BlindedPath: &sphinx.BlindedPath{
|
||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||
{
|
||||
CipherText: encrypedData,
|
||||
},
|
||||
{
|
||||
CipherText: encrypedData,
|
||||
},
|
||||
|
||||
twoHopPath = &sphinx.BlindedPath{
|
||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||
{
|
||||
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 {
|
||||
name string
|
||||
restrictions *RestrictParams
|
||||
|
@ -3454,7 +3480,7 @@ func TestLastHopPayloadSize(t *testing.T) {
|
|||
{
|
||||
name: "Blinded final hop introduction point",
|
||||
restrictions: &RestrictParams{
|
||||
BlindedPayment: oneHopBlindedPayment,
|
||||
BlindedPaymentPathSet: oneHopBlindedPayment,
|
||||
},
|
||||
amount: amtToForward,
|
||||
finalHopExpiry: finalHopExpiry,
|
||||
|
@ -3462,7 +3488,7 @@ func TestLastHopPayloadSize(t *testing.T) {
|
|||
{
|
||||
name: "Blinded final hop of a two hop payment",
|
||||
restrictions: &RestrictParams{
|
||||
BlindedPayment: twoHopBlindedPayment,
|
||||
BlindedPaymentPathSet: twoHopBlindedPayment,
|
||||
},
|
||||
amount: amtToForward,
|
||||
finalHopExpiry: finalHopExpiry,
|
||||
|
@ -3490,12 +3516,11 @@ func TestLastHopPayloadSize(t *testing.T) {
|
|||
}
|
||||
|
||||
var finalHop route.Hop
|
||||
if tc.restrictions.BlindedPayment != nil {
|
||||
blindedPath := tc.restrictions.BlindedPayment.
|
||||
BlindedPath.BlindedHops
|
||||
|
||||
blindedPoint := tc.restrictions.BlindedPayment.
|
||||
BlindedPath.BlindingPoint
|
||||
if tc.restrictions.BlindedPaymentPathSet != nil {
|
||||
path := tc.restrictions.BlindedPaymentPathSet.
|
||||
LargestLastHopPayloadPath()
|
||||
blindedPath := path.BlindedPath.BlindedHops
|
||||
blindedPoint := path.BlindedPath.BlindingPoint
|
||||
|
||||
//nolint:lll
|
||||
finalHop = route.Hop{
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btclog"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
|
@ -206,13 +205,13 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if p.BlindedPayment != nil {
|
||||
if p.BlindedPathSet != nil {
|
||||
if len(edges) != 0 {
|
||||
return nil, fmt.Errorf("cannot have both route hints " +
|
||||
"and blinded path")
|
||||
}
|
||||
|
||||
edges, err = p.BlindedPayment.toRouteHints()
|
||||
edges, err = p.BlindedPathSet.ToRouteHints()
|
||||
if err != nil {
|
||||
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
|
||||
// MPP records.
|
||||
if p.payment.PaymentAddr == nil &&
|
||||
p.payment.BlindedPayment == nil {
|
||||
p.payment.BlindedPathSet == nil {
|
||||
|
||||
p.log.Debugf("not splitting because payment " +
|
||||
"address is unspecified")
|
||||
|
@ -407,11 +406,6 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||
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
|
||||
// this into a route by applying the time-lock and fee
|
||||
// requirements.
|
||||
|
@ -424,7 +418,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||
records: p.payment.DestCustomRecords,
|
||||
paymentAddr: p.payment.PaymentAddr,
|
||||
metadata: p.payment.Metadata,
|
||||
}, blindedPath,
|
||||
}, p.payment.BlindedPathSet,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -477,10 +477,10 @@ type RouteRequest struct {
|
|||
// in blinded payment.
|
||||
FinalExpiry uint16
|
||||
|
||||
// BlindedPayment contains an optional blinded path and parameters
|
||||
// used to reach a target node via a blinded path. This field is
|
||||
// BlindedPathSet contains a set of optional blinded paths and
|
||||
// parameters used to reach a target node blinded paths. This field is
|
||||
// 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
|
||||
|
@ -494,7 +494,7 @@ type RouteHints map[route.Vertex][]AdditionalEdge
|
|||
func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
||||
amount lnwire.MilliSatoshi, timePref float64,
|
||||
restrictions *RestrictParams, customRecords record.CustomSet,
|
||||
routeHints RouteHints, blindedPayment *BlindedPayment,
|
||||
routeHints RouteHints, blindedPathSet *BlindedPaymentPathSet,
|
||||
finalExpiry uint16) (*RouteRequest, error) {
|
||||
|
||||
var (
|
||||
|
@ -504,16 +504,8 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
|||
err error
|
||||
)
|
||||
|
||||
if blindedPayment != nil {
|
||||
if err := blindedPayment.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid blinded payment: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
introVertex := route.NewVertex(
|
||||
blindedPayment.BlindedPath.IntroductionPoint,
|
||||
)
|
||||
if source == introVertex {
|
||||
if blindedPathSet != nil {
|
||||
if blindedPathSet.IsIntroNode(source) {
|
||||
return nil, ErrSelfIntro
|
||||
}
|
||||
|
||||
|
@ -527,25 +519,15 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
|||
return nil, ErrExpiryAndBlinded
|
||||
}
|
||||
|
||||
// If we have a blinded path with 1 hop, the cltv expiry
|
||||
// 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
|
||||
}
|
||||
requestExpiry = blindedPathSet.FinalCLTVDelta()
|
||||
|
||||
requestHints, err = blindedPayment.toRouteHints()
|
||||
requestHints, err = blindedPathSet.ToRouteHints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
requestTarget, err := getTargetNode(target, blindedPayment)
|
||||
requestTarget, err := getTargetNode(target, blindedPathSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -559,15 +541,15 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
|
|||
CustomRecords: customRecords,
|
||||
RouteHints: requestHints,
|
||||
FinalExpiry: requestExpiry,
|
||||
BlindedPayment: blindedPayment,
|
||||
BlindedPathSet: blindedPathSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
|
||||
route.Vertex, error) {
|
||||
func getTargetNode(target *route.Vertex,
|
||||
blindedPathSet *BlindedPaymentPathSet) (route.Vertex, error) {
|
||||
|
||||
var (
|
||||
blinded = blindedPayment != nil
|
||||
blinded = blindedPathSet != nil
|
||||
targetSet = target != nil
|
||||
)
|
||||
|
||||
|
@ -576,18 +558,7 @@ func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
|
|||
return route.Vertex{}, ErrTargetAndBlinded
|
||||
|
||||
case blinded:
|
||||
// If we're dealing with an edge-case blinded path that just
|
||||
// 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
|
||||
return route.NewVertex(blindedPathSet.TargetPubKey()), nil
|
||||
|
||||
case targetSet:
|
||||
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
|
||||
// particular target destination to which it is able to send `amt` after
|
||||
// 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,
|
||||
cltvDelta: req.FinalExpiry,
|
||||
records: req.CustomRecords,
|
||||
}, req.blindedPath(),
|
||||
}, req.BlindedPathSet,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
@ -761,7 +722,7 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||
|
||||
hops = append(hops, &route.Hop{
|
||||
PubKeyBytes: path[j].vertex,
|
||||
ChannelID: path[j-1].edgePolicy.ChannelID,
|
||||
ChannelID: path[j-1].channelID,
|
||||
})
|
||||
|
||||
prevNode = path[j].vertex
|
||||
|
@ -926,14 +887,10 @@ type LightningPayment struct {
|
|||
// BlindedPayment field.
|
||||
RouteHints [][]zpay32.HopHint
|
||||
|
||||
// BlindedPayment holds the information about a blinded path to the
|
||||
// payment recipient. This is mutually exclusive to the RouteHints
|
||||
// BlindedPathSet holds the information about a set of blinded paths to
|
||||
// the payment recipient. This is mutually exclusive to the RouteHints
|
||||
// field.
|
||||
//
|
||||
// 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
|
||||
BlindedPathSet *BlindedPaymentPathSet
|
||||
|
||||
// OutgoingChannelIDs is the list of channels that are allowed for the
|
||||
// first hop. If nil, any channel may be used.
|
||||
|
|
|
@ -2223,11 +2223,6 @@ func TestNewRouteRequest(t *testing.T) {
|
|||
finalExpiry: unblindedCltv,
|
||||
err: ErrExpiryAndBlinded,
|
||||
},
|
||||
{
|
||||
name: "invalid blinded payment",
|
||||
blindedPayment: &BlindedPayment{},
|
||||
err: ErrNoBlindedPath,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
@ -2236,9 +2231,26 @@ func TestNewRouteRequest(t *testing.T) {
|
|||
t.Run(testCase.name, func(t *testing.T) {
|
||||
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(
|
||||
source, testCase.target, 1000, 0, nil, nil,
|
||||
testCase.routeHints, testCase.blindedPayment,
|
||||
testCase.routeHints, blindedPathInfo,
|
||||
testCase.finalExpiry,
|
||||
)
|
||||
require.ErrorIs(t, err, testCase.err)
|
||||
|
@ -2248,7 +2260,7 @@ func TestNewRouteRequest(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
require.Equal(t, req.Target, testCase.expectedTarget)
|
||||
require.Equal(t, req.Target, expectedTarget)
|
||||
require.Equal(
|
||||
t, req.FinalExpiry, testCase.expectedCltv,
|
||||
)
|
||||
|
|
32
rpcserver.go
32
rpcserver.go
|
@ -5105,7 +5105,7 @@ type rpcPaymentIntent struct {
|
|||
paymentAddr *[32]byte
|
||||
payReq []byte
|
||||
metadata []byte
|
||||
blindedPayment *routing.BlindedPayment
|
||||
blindedPathSet *routing.BlindedPaymentPathSet
|
||||
|
||||
destCustomRecords record.CustomSet
|
||||
|
||||
|
@ -5242,28 +5242,24 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
|
|||
payIntent.metadata = payReq.Metadata
|
||||
|
||||
if len(payReq.BlindedPaymentPaths) > 0 {
|
||||
// NOTE: Currently we only choose a single payment path.
|
||||
// This will be updated in a future PR to handle
|
||||
// multiple blinded payment paths.
|
||||
path := payReq.BlindedPaymentPaths[0]
|
||||
if len(path.Hops) == 0 {
|
||||
return payIntent, fmt.Errorf("a blinded " +
|
||||
"payment must have at least 1 hop")
|
||||
pathSet, err := routerrpc.BuildBlindedPathSet(
|
||||
payReq.BlindedPaymentPaths,
|
||||
)
|
||||
if err != nil {
|
||||
return payIntent, err
|
||||
}
|
||||
payIntent.blindedPathSet = pathSet
|
||||
|
||||
finalHop := path.Hops[len(path.Hops)-1]
|
||||
payIntent.blindedPayment =
|
||||
routerrpc.MarshalBlindedPayment(path)
|
||||
|
||||
// Replace the target node with the blinded public key
|
||||
// of the blinded path's final node.
|
||||
// Replace the destination node with the target public
|
||||
// key of the blinded path set.
|
||||
copy(
|
||||
payIntent.dest[:],
|
||||
finalHop.BlindedNodePub.SerializeCompressed(),
|
||||
pathSet.TargetPubKey().SerializeCompressed(),
|
||||
)
|
||||
|
||||
if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() {
|
||||
payIntent.destFeatures = path.Features.Clone()
|
||||
pathFeatures := pathSet.Features()
|
||||
if !pathFeatures.IsEmpty() {
|
||||
payIntent.destFeatures = pathFeatures.Clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5421,7 +5417,7 @@ func (r *rpcServer) dispatchPaymentIntent(
|
|||
DestFeatures: payIntent.destFeatures,
|
||||
PaymentAddr: payIntent.paymentAddr,
|
||||
Metadata: payIntent.metadata,
|
||||
BlindedPayment: payIntent.blindedPayment,
|
||||
BlindedPathSet: payIntent.blindedPathSet,
|
||||
|
||||
// Don't enable multi-part payments on the main rpc.
|
||||
// Users need to use routerrpc for that.
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
|
@ -21,6 +20,12 @@ const (
|
|||
// proposal](https://github.com/lightning/blips/pull/39) for a detailed
|
||||
// calculation.
|
||||
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 (
|
||||
|
@ -215,6 +220,12 @@ func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) {
|
|||
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)
|
||||
_, err = r.Read(encryptedData)
|
||||
if err != nil {
|
||||
|
@ -238,9 +249,9 @@ func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(hop.CipherText) > math.MaxUint16 {
|
||||
if len(hop.CipherText) > maxCipherTextLength {
|
||||
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{})
|
||||
|
|
Loading…
Add table
Reference in a new issue