Skip to content

Commit

Permalink
multi: send MPP payment to blinded path
Browse files Browse the repository at this point in the history
Make various sender side adjustments so that a sender is able to send an
MP payment to a single blinded path without actually including an MPP
record in the payment.
  • Loading branch information
ellemouton committed May 7, 2024
1 parent f03b5fc commit 50c39ed
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 8 deletions.
58 changes: 53 additions & 5 deletions channeldb/payment_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ var (

// ErrValueMismatch is returned if we try to register a non-MPP attempt
// with an amount that doesn't match the payment amount.
ErrValueMismatch = errors.New("attempted value doesn't match payment" +
ErrValueMismatch = errors.New("attempted value doesn't match payment " +
"amount")

// ErrValueExceedsAmt is returned if we try to register an attempt that
// would take the total sent amount above the payment amount.
ErrValueExceedsAmt = errors.New("attempted value exceeds payment" +
ErrValueExceedsAmt = errors.New("attempted value exceeds payment " +
"amount")

// ErrNonMPPayment is returned if we try to register an MPP attempt for
Expand All @@ -83,6 +83,17 @@ var (
// a payment that already has an MPP attempt registered.
ErrMPPayment = errors.New("payment has MPP attempts")

// ErrMPPRecordInBlindedPayment is returned if we try to register an
// attempt with an MPP record for a payment to a blinded path.
ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " +
"contain MPP records")

// ErrBlindedPaymentTotalAmountMismatch is returned if we try to
// register an HTLC shard to a blinded route where the total amount
// doesn't match existing shards.
ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " +
"total amount mismatch")

// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
// shard where the payment address doesn't match existing shards.
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
Expand All @@ -96,7 +107,7 @@ var (
// attempt to a payment that has at least one of its HTLCs settled.
ErrPaymentPendingSettled = errors.New("payment has settled htlcs")

// ErrPaymentAlreadyFailed is returned when we try to add a new attempt
// ErrPaymentPendingFailed is returned when we try to add a new attempt
// to a payment that already has a failure reason.
ErrPaymentPendingFailed = errors.New("payment has failure reason")

Expand Down Expand Up @@ -334,12 +345,48 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
return err
}

// If the final hop has a non-nil blinding point, then we know
// this is a blinded payment. In blinded payments, MPP records
// are not set for split payments and the recipient is
// responsible for using a consistent PathID across the
// various encrypted data payloads that we received from them
// for this payment. All we need to check is that the total
// amount field for each HTLC in the split payment is correct.
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0

// Make sure any existing shards match the new one with regards
// to MPP options.
mpp := attempt.Route.FinalHop().MPP

// MPP records should not be set for attempts to blinded paths.
if isBlinded && mpp != nil {
return ErrMPPRecordInBlindedPayment
}

for _, h := range payment.InFlightHTLCs() {
hMpp := h.Route.FinalHop().MPP

// If this is a blinded payment, then no existing HTLCs
// should have MPP records.
if isBlinded && hMpp != nil {
return ErrMPPRecordInBlindedPayment
}

// If this is a blinded payment, then we just need to
// check that the TotalAmtMsat field for this shard
// is equal to that of any other shard in the same
// payment.
if isBlinded {
if attempt.Route.FinalHop().TotalAmtMsat !=
h.Route.FinalHop().TotalAmtMsat {

//nolint:lll
return ErrBlindedPaymentTotalAmountMismatch
}

continue
}

switch {
// We tried to register a non-MPP attempt for a MPP
// payment.
Expand Down Expand Up @@ -367,9 +414,10 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
}

// If this is a non-MPP attempt, it must match the total amount
// exactly.
// exactly. Note that a blinded payment is considered an MPP
// attempt.
amt := attempt.Route.ReceiverAmt()
if mpp == nil && amt != payment.Info.Value {
if !isBlinded && mpp == nil && amt != payment.Info.Value {
return ErrValueMismatch
}

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 @@ -586,6 +586,10 @@ var allTestCases = []*lntest.TestCase{
Name: "on chain to blinded",
TestFunc: testErrorHandlingOnChainFailure,
},
{
Name: "mpp to single blinded path",
TestFunc: testMPPToSingleBlindedPath,
},
{
Name: "removetx",
TestFunc: testRemoveTx,
Expand Down
173 changes: 173 additions & 0 deletions itest/lnd_route_blinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,3 +869,176 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) {
ht.CloseChannel(testCase.carol, testCase.channels[2])
testCase.cancel()
}

// testMPPToSingleBlindedPath tests that a two-shard MPP payment can be sent
// over a single blinded path.
// The following graph is created where Dave is the destination node, and he
// will choose Carol as the introduction node. The channel capacities are set in
// such a way that Alice will have to split the payment to dave over both the
// A->B->C-D and A->E->C->D routes.
//
// ---- Bob ---
// / \
// Alice Carol --- Dave
// \ /
// ---- Eve ---
func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
// Create a five-node context consisting of Alice, Bob and three new
// nodes.
alice, bob := ht.Alice, ht.Bob

// Restrict Dave so that he only ever chooses the Carol->Dave path for
// a blinded route.
dave := ht.NewNode("dave", []string{
"--invoices.blinding.min-num-hops=1",
"--invoices.blinding.max-num-hops=1",
})
carol := ht.NewNode("carol", nil)
eve := ht.NewNode("eve", nil)

// Connect nodes to ensure propagation of channels.
ht.EnsureConnected(alice, bob)
ht.EnsureConnected(alice, eve)
ht.EnsureConnected(carol, bob)
ht.EnsureConnected(carol, eve)
ht.EnsureConnected(carol, dave)

// Send coins to the nodes and mine 1 blocks to confirm them.
for i := 0; i < 2; i++ {
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, eve)
ht.MineBlocks(1)
}

const paymentAmt = btcutil.Amount(300000)

nodes := []*node.HarnessNode{alice, bob, carol, dave, eve}
reqs := []*lntest.OpenChannelRequest{
{
Local: alice,
Remote: bob,
Param: lntest.OpenChannelParams{
Amt: paymentAmt * 2 / 3,
},
},
{
Local: alice,
Remote: eve,
Param: lntest.OpenChannelParams{
Amt: paymentAmt * 2 / 3,
},
},
{
Local: bob,
Remote: carol,
Param: lntest.OpenChannelParams{
Amt: paymentAmt * 2,
},
},
{
Local: eve,
Remote: carol,
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 about 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)
}

// Make Dave create an invoice with a blinded path for Alice to pay.
invoice := &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
Blind: true,
}
invoiceResp := dave.RPC.AddInvoice(invoice)

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(eve, channelPoints[3], false)
ht.CloseChannelAssertPending(carol, channelPoints[4], false)

// Now mine a block to include all the closing transactions.
ht.MineBlocks(1)

// Assert that the channels are closed.
for _, hn := range nodes {
ht.AssertNumWaitingClose(hn, 0)
}
}
11 changes: 8 additions & 3 deletions routing/payment_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
switch {
case err == errNoPathFound:
// Don't split if this is a legacy payment without mpp
// record.
if p.payment.PaymentAddr == nil {
// record. If it has a blinded path though, then we
// can split. Split payments to blinded paths won't have
// MPP records.
if p.payment.PaymentAddr == nil &&
p.payment.BlindedPayment == nil {

p.log.Debugf("not splitting because payment " +
"address is unspecified")

Expand All @@ -345,7 +349,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
!destFeatures.HasFeature(lnwire.AMPOptional) {

p.log.Debug("not splitting because " +
"destination doesn't declare MPP or AMP")
"destination doesn't declare MPP or " +
"AMP")

return nil, errNoPathFound
}
Expand Down

0 comments on commit 50c39ed

Please sign in to comment.