Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3/4] Route Blinding: send MPP over multiple blinded paths #8764

Open
wants to merge 7 commits into
base: elle-rb-receives-2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/release-notes/release-notes-0.18.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@

* [Generate and send to](https://github.com/lightningnetwork/lnd/pull/8735) an
invoice with blinded paths.

* Add the ability to [send to use multiple blinded payment
paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.

## Testing
## Database
Expand Down
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,10 @@ var allTestCases = []*lntest.TestCase{
Name: "mpp to single blinded path",
TestFunc: testMPPToSingleBlindedPath,
},
{
Name: "mpp to multiple blinded paths",
TestFunc: testMPPToMultipleBlindedPaths,
},
{
Name: "removetx",
TestFunc: testRemoveTx,
Expand Down
163 changes: 163 additions & 0 deletions itest/lnd_route_blinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,166 @@ func testMPPToSingleBlindedPath(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{
"--invoices.blinding.min-num-hops=1",
"--invoices.blinding.max-num-hops=1",
})
carol := ht.NewNode("carol", nil)

// Connect nodes to ensure propagation of channels.
ht.EnsureConnected(alice, carol)
ht.EnsureConnected(alice, carol)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.MineBlocks(1)

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 slit 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.MineBlocks(1)

// Assert that the channels are closed.
for _, hn := range nodes {
ht.AssertNumWaitingClose(hn, 0)
}
}
108 changes: 62 additions & 46 deletions lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand All @@ -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) (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down
Loading
Loading