mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-10-11 03:12:54 +02:00
routerrpc+routing: adapt payment session for multi shard send
Modifies the payment session to launch additional pathfinding attempts for lower amounts. If a single shot payment isn't possible, the goal is to try to complete the payment using multiple htlcs. In previous commits, the payment lifecycle has been prepared to deal with partial-amount routes returned from the payment session. It will query for additional shards if needed. Additionally a new rpc payment parameter is added that controls the maximum number of shards that will be used for the payment.
This commit is contained in:
@@ -2,6 +2,9 @@ package routing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// TestProbabilityExtrapolation tests that probabilities for tried channels are
|
||||
@@ -50,7 +53,7 @@ func TestProbabilityExtrapolation(t *testing.T) {
|
||||
// a specific number of attempts to safe-guard against accidental
|
||||
// modifications anywhere in the chain of components that is involved in
|
||||
// this test.
|
||||
attempts, err := ctx.testPayment()
|
||||
attempts, err := ctx.testPayment(1)
|
||||
if err != nil {
|
||||
t.Fatalf("payment failed: %v", err)
|
||||
}
|
||||
@@ -62,7 +65,7 @@ func TestProbabilityExtrapolation(t *testing.T) {
|
||||
// of data from other channels), all ten bad channels will be tried
|
||||
// first before switching to the paid channel.
|
||||
ctx.mcCfg.AprioriWeight = 1
|
||||
attempts, err = ctx.testPayment()
|
||||
attempts, err = ctx.testPayment(1)
|
||||
if err != nil {
|
||||
t.Fatalf("payment failed: %v", err)
|
||||
}
|
||||
@@ -70,3 +73,211 @@ func TestProbabilityExtrapolation(t *testing.T) {
|
||||
t.Fatalf("expected 11 attempts, but needed %v", len(attempts))
|
||||
}
|
||||
}
|
||||
|
||||
type mppSendTestCase struct {
|
||||
name string
|
||||
amt btcutil.Amount
|
||||
expectedAttempts int
|
||||
|
||||
// expectedSuccesses is a list of htlcs that made it to the receiver,
|
||||
// regardless of whether the final set became complete or not.
|
||||
expectedSuccesses []expectedHtlcSuccess
|
||||
|
||||
graph func(g *mockGraph)
|
||||
expectedFailure bool
|
||||
maxHtlcs uint32
|
||||
}
|
||||
|
||||
const (
|
||||
chanSourceIm1 = 13
|
||||
chanIm1Target = 32
|
||||
chanSourceIm2 = 14
|
||||
chanIm2Target = 42
|
||||
)
|
||||
|
||||
func onePathGraph(g *mockGraph) {
|
||||
// Create the following network of nodes:
|
||||
// source -> intermediate1 -> target
|
||||
|
||||
const im1NodeID = 3
|
||||
intermediate1 := newMockNode(im1NodeID)
|
||||
g.addNode(intermediate1)
|
||||
|
||||
g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, 200000)
|
||||
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
|
||||
}
|
||||
|
||||
func twoPathGraph(g *mockGraph) {
|
||||
// Create the following network of nodes:
|
||||
// source -> intermediate1 -> target
|
||||
// source -> intermediate2 -> target
|
||||
|
||||
const im1NodeID = 3
|
||||
intermediate1 := newMockNode(im1NodeID)
|
||||
g.addNode(intermediate1)
|
||||
|
||||
const im2NodeID = 4
|
||||
intermediate2 := newMockNode(im2NodeID)
|
||||
g.addNode(intermediate2)
|
||||
|
||||
g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, 200000)
|
||||
g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, 200000)
|
||||
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
|
||||
g.addChannel(chanIm2Target, targetNodeID, im2NodeID, 100000)
|
||||
}
|
||||
|
||||
var mppTestCases = []mppSendTestCase{
|
||||
// Test a two-path graph with sufficient liquidity. It is expected that
|
||||
// pathfinding will try first try to send the full amount via the two
|
||||
// available routes. When that fails, it will half the amount to 35k sat
|
||||
// and retry. That attempt reaches the target successfully. Then the
|
||||
// same route is tried again. Because the channel only had 50k sat, it
|
||||
// will fail. Finally the second route is tried for 35k and it succeeds
|
||||
// too. Mpp payment complete.
|
||||
{
|
||||
|
||||
name: "sufficient inbound",
|
||||
graph: twoPathGraph,
|
||||
amt: 70000,
|
||||
expectedAttempts: 5,
|
||||
expectedSuccesses: []expectedHtlcSuccess{
|
||||
{
|
||||
amt: 35000,
|
||||
chans: []uint64{chanSourceIm1, chanIm1Target},
|
||||
},
|
||||
{
|
||||
amt: 35000,
|
||||
chans: []uint64{chanSourceIm2, chanIm2Target},
|
||||
},
|
||||
},
|
||||
maxHtlcs: 1000,
|
||||
},
|
||||
|
||||
// Test that a cap on the max htlcs makes it impossible to pay.
|
||||
{
|
||||
name: "no splitting",
|
||||
graph: twoPathGraph,
|
||||
amt: 70000,
|
||||
expectedAttempts: 2,
|
||||
expectedSuccesses: []expectedHtlcSuccess{},
|
||||
expectedFailure: true,
|
||||
maxHtlcs: 1,
|
||||
},
|
||||
|
||||
// Test that an attempt is made to split the payment in multiple parts
|
||||
// that all use the same route if the full amount cannot be sent in a
|
||||
// single htlc. The sender is effectively probing the receiver's
|
||||
// incoming channel to see if it has sufficient balance. In this test
|
||||
// case, the endeavour fails.
|
||||
{
|
||||
|
||||
name: "one path split",
|
||||
graph: onePathGraph,
|
||||
amt: 70000,
|
||||
expectedAttempts: 7,
|
||||
expectedSuccesses: []expectedHtlcSuccess{
|
||||
{
|
||||
amt: 35000,
|
||||
chans: []uint64{chanSourceIm1, chanIm1Target},
|
||||
},
|
||||
{
|
||||
amt: 8750,
|
||||
chans: []uint64{chanSourceIm1, chanIm1Target},
|
||||
},
|
||||
},
|
||||
expectedFailure: true,
|
||||
maxHtlcs: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
// TestMppSend tests that a payment can be completed using multiple shards.
|
||||
func TestMppSend(t *testing.T) {
|
||||
for _, testCase := range mppTestCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testMppSend(t, &testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testMppSend(t *testing.T, testCase *mppSendTestCase) {
|
||||
ctx := newIntegratedRoutingContext(t)
|
||||
|
||||
g := ctx.graph
|
||||
testCase.graph(g)
|
||||
|
||||
ctx.amt = lnwire.NewMSatFromSatoshis(testCase.amt)
|
||||
|
||||
attempts, err := ctx.testPayment(testCase.maxHtlcs)
|
||||
switch {
|
||||
case err == nil && testCase.expectedFailure:
|
||||
t.Fatal("expected payment to fail")
|
||||
case err != nil && !testCase.expectedFailure:
|
||||
t.Fatal("expected payment to succeed")
|
||||
}
|
||||
|
||||
if len(attempts) != testCase.expectedAttempts {
|
||||
t.Fatalf("expected %v attempts, but needed %v",
|
||||
testCase.expectedAttempts, len(attempts),
|
||||
)
|
||||
}
|
||||
|
||||
assertSuccessAttempts(t, attempts, testCase.expectedSuccesses)
|
||||
}
|
||||
|
||||
// expectedHtlcSuccess describes an expected successful htlc attempt.
|
||||
type expectedHtlcSuccess struct {
|
||||
amt btcutil.Amount
|
||||
chans []uint64
|
||||
}
|
||||
|
||||
// equals matches the expectation with an actual attempt.
|
||||
func (e *expectedHtlcSuccess) equals(a htlcAttempt) bool {
|
||||
if a.route.TotalAmount !=
|
||||
lnwire.NewMSatFromSatoshis(e.amt) {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.route.Hops) != len(e.chans) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, h := range a.route.Hops {
|
||||
if h.ChannelID != e.chans[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// assertSuccessAttempts asserts that the set of successful htlc attempts
|
||||
// matches the given expectation.
|
||||
func assertSuccessAttempts(t *testing.T, attempts []htlcAttempt,
|
||||
expected []expectedHtlcSuccess) {
|
||||
|
||||
successCount := 0
|
||||
loop:
|
||||
for _, a := range attempts {
|
||||
if !a.success {
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
|
||||
for _, exp := range expected {
|
||||
if exp.equals(a) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("htlc success %v not found", a)
|
||||
}
|
||||
|
||||
if successCount != len(expected) {
|
||||
t.Fatalf("expected %v successful htlcs, but got %v",
|
||||
expected, successCount)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user