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
|
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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
32
rpcserver.go
32
rpcserver.go
|
@ -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.
|
||||||
|
|
|
@ -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{})
|
||||||
|
|
Loading…
Add table
Reference in a new issue