mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-29 02:00:54 +02:00
multi: send MPP payment to blinded path
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.
This commit is contained in:
@ -67,12 +67,12 @@ var (
|
|||||||
|
|
||||||
// ErrValueMismatch is returned if we try to register a non-MPP attempt
|
// ErrValueMismatch is returned if we try to register a non-MPP attempt
|
||||||
// with an amount that doesn't match the payment amount.
|
// 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")
|
"amount")
|
||||||
|
|
||||||
// ErrValueExceedsAmt is returned if we try to register an attempt that
|
// ErrValueExceedsAmt is returned if we try to register an attempt that
|
||||||
// would take the total sent amount above the payment amount.
|
// 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")
|
"amount")
|
||||||
|
|
||||||
// ErrNonMPPayment is returned if we try to register an MPP attempt for
|
// ErrNonMPPayment is returned if we try to register an MPP attempt for
|
||||||
@ -83,6 +83,17 @@ var (
|
|||||||
// a payment that already has an MPP attempt registered.
|
// a payment that already has an MPP attempt registered.
|
||||||
ErrMPPayment = errors.New("payment has MPP attempts")
|
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
|
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
|
||||||
// shard where the payment address doesn't match existing shards.
|
// shard where the payment address doesn't match existing shards.
|
||||||
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
|
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
|
||||||
@ -96,7 +107,7 @@ var (
|
|||||||
// attempt to a payment that has at least one of its HTLCs settled.
|
// attempt to a payment that has at least one of its HTLCs settled.
|
||||||
ErrPaymentPendingSettled = errors.New("payment has settled htlcs")
|
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.
|
// to a payment that already has a failure reason.
|
||||||
ErrPaymentPendingFailed = errors.New("payment has failure reason")
|
ErrPaymentPendingFailed = errors.New("payment has failure reason")
|
||||||
|
|
||||||
@ -334,12 +345,48 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the final hop has encrypted data, 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
|
// Make sure any existing shards match the new one with regards
|
||||||
// to MPP options.
|
// to MPP options.
|
||||||
mpp := attempt.Route.FinalHop().MPP
|
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() {
|
for _, h := range payment.InFlightHTLCs() {
|
||||||
hMpp := h.Route.FinalHop().MPP
|
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 {
|
switch {
|
||||||
// We tried to register a non-MPP attempt for a MPP
|
// We tried to register a non-MPP attempt for a MPP
|
||||||
// payment.
|
// payment.
|
||||||
@ -367,9 +414,10 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this is a non-MPP attempt, it must match the total amount
|
// 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()
|
amt := attempt.Route.ReceiverAmt()
|
||||||
if mpp == nil && amt != payment.Info.Value {
|
if !isBlinded && mpp == nil && amt != payment.Info.Value {
|
||||||
return ErrValueMismatch
|
return ErrValueMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,6 +586,10 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "on chain to blinded",
|
Name: "on chain to blinded",
|
||||||
TestFunc: testErrorHandlingOnChainFailure,
|
TestFunc: testErrorHandlingOnChainFailure,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "mpp to single blinded path",
|
||||||
|
TestFunc: testMPPToSingleBlindedPath,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "removetx",
|
Name: "removetx",
|
||||||
TestFunc: testRemoveTx,
|
TestFunc: testRemoveTx,
|
||||||
|
@ -878,3 +878,176 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) {
|
|||||||
ht.CloseChannel(testCase.carol, testCase.channels[2])
|
ht.CloseChannel(testCase.carol, testCase.channels[2])
|
||||||
testCase.cancel()
|
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-real-hops=1",
|
||||||
|
"--invoices.blinding.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.MineBlocksAndAssertNumTxes(1, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.MineBlocksAndAssertNumTxes(1, 5)
|
||||||
|
|
||||||
|
// Assert that the channels are closed.
|
||||||
|
for _, hn := range nodes {
|
||||||
|
ht.AssertNumWaitingClose(hn, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -338,8 +338,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
switch {
|
switch {
|
||||||
case err == errNoPathFound:
|
case err == errNoPathFound:
|
||||||
// Don't split if this is a legacy payment without mpp
|
// Don't split if this is a legacy payment without mpp
|
||||||
// record.
|
// record. If it has a blinded path though, then we
|
||||||
if p.payment.PaymentAddr == nil {
|
// 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 " +
|
p.log.Debugf("not splitting because payment " +
|
||||||
"address is unspecified")
|
"address is unspecified")
|
||||||
|
|
||||||
@ -357,7 +361,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
!destFeatures.HasFeature(lnwire.AMPOptional) {
|
!destFeatures.HasFeature(lnwire.AMPOptional) {
|
||||||
|
|
||||||
p.log.Debug("not splitting because " +
|
p.log.Debug("not splitting because " +
|
||||||
"destination doesn't declare MPP or AMP")
|
"destination doesn't declare MPP or " +
|
||||||
|
"AMP")
|
||||||
|
|
||||||
return nil, errNoPathFound
|
return nil, errNoPathFound
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user