github.com/decred/dcrlnd@v0.7.6/routing/integrated_routing_test.go (about)

     1  package routing
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/davecgh/go-spew/spew"
     8  	"github.com/decred/dcrd/dcrutil/v4"
     9  	"github.com/decred/dcrlnd/lnwire"
    10  	"github.com/stretchr/testify/require"
    11  )
    12  
    13  // TestProbabilityExtrapolation tests that probabilities for tried channels are
    14  // extrapolated to untried channels. This is a way to improve pathfinding
    15  // success by steering away from bad nodes.
    16  func TestProbabilityExtrapolation(t *testing.T) {
    17  	ctx := newIntegratedRoutingContext(t)
    18  
    19  	// Create the following network of nodes:
    20  	// source -> expensiveNode (charges routing fee) -> target
    21  	// source -> intermediate1 (free routing) -> intermediate(1-10) (free routing) -> target
    22  	g := ctx.graph
    23  
    24  	const expensiveNodeID = 3
    25  	expensiveNode := newMockNode(expensiveNodeID)
    26  	expensiveNode.baseFee = 10000
    27  	g.addNode(expensiveNode)
    28  
    29  	g.addChannel(100, sourceNodeID, expensiveNodeID, 100000)
    30  	g.addChannel(101, targetNodeID, expensiveNodeID, 100000)
    31  
    32  	const intermediate1NodeID = 4
    33  	intermediate1 := newMockNode(intermediate1NodeID)
    34  	g.addNode(intermediate1)
    35  	g.addChannel(102, sourceNodeID, intermediate1NodeID, 100000)
    36  
    37  	for i := 0; i < 10; i++ {
    38  		imNodeID := byte(10 + i)
    39  		imNode := newMockNode(imNodeID)
    40  		g.addNode(imNode)
    41  		g.addChannel(uint64(200+i), imNodeID, targetNodeID, 100000)
    42  		g.addChannel(uint64(300+i), imNodeID, intermediate1NodeID, 100000)
    43  
    44  		// The channels from intermediate1 all have insufficient balance.
    45  		g.nodes[intermediate1.pubkey].channels[imNode.pubkey].balance = 0
    46  	}
    47  
    48  	// It is expected that pathfinding will try to explore the routes via
    49  	// intermediate1 first, because those are free. But as failures happen,
    50  	// the node probability of intermediate1 will go down in favor of the
    51  	// paid route via expensiveNode.
    52  	//
    53  	// The exact number of attempts required is dependent on mission control
    54  	// config. For this test, it would have been enough to only assert that
    55  	// we are not trying all routes via intermediate1. However, we do assert
    56  	// a specific number of attempts to safe-guard against accidental
    57  	// modifications anywhere in the chain of components that is involved in
    58  	// this test.
    59  	attempts, err := ctx.testPayment(1)
    60  	if err != nil {
    61  		t.Fatalf("payment failed: %v", err)
    62  	}
    63  	if len(attempts) != 5 {
    64  		t.Fatalf("expected 5 attempts, but needed %v", len(attempts))
    65  	}
    66  
    67  	// If we use a static value for the node probability (no extrapolation
    68  	// of data from other channels), all ten bad channels will be tried
    69  	// first before switching to the paid channel.
    70  	ctx.mcCfg.AprioriWeight = 1
    71  	attempts, err = ctx.testPayment(1)
    72  	if err != nil {
    73  		t.Fatalf("payment failed: %v", err)
    74  	}
    75  	if len(attempts) != 11 {
    76  		t.Fatalf("expected 11 attempts, but needed %v", len(attempts))
    77  	}
    78  }
    79  
    80  type mppSendTestCase struct {
    81  	name             string
    82  	amt              dcrutil.Amount
    83  	expectedAttempts int
    84  
    85  	// expectedSuccesses is a list of htlcs that made it to the receiver,
    86  	// regardless of whether the final set became complete or not.
    87  	expectedSuccesses []expectedHtlcSuccess
    88  
    89  	graph           func(g *mockGraph)
    90  	expectedFailure bool
    91  	maxParts        uint32
    92  	maxShardSize    dcrutil.Amount
    93  }
    94  
    95  const (
    96  	chanSourceIm1 = 13
    97  	chanIm1Target = 32
    98  	chanSourceIm2 = 14
    99  	chanIm2Target = 42
   100  )
   101  
   102  func onePathGraph(g *mockGraph) {
   103  	// Create the following network of nodes:
   104  	// source -> intermediate1 -> target
   105  
   106  	const im1NodeID = 3
   107  	intermediate1 := newMockNode(im1NodeID)
   108  	g.addNode(intermediate1)
   109  
   110  	g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, 200000)
   111  	g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
   112  }
   113  
   114  func twoPathGraph(g *mockGraph, capacityOut, capacityIn dcrutil.Amount) {
   115  	// Create the following network of nodes:
   116  	// source -> intermediate1 -> target
   117  	// source -> intermediate2 -> target
   118  
   119  	const im1NodeID = 3
   120  	intermediate1 := newMockNode(im1NodeID)
   121  	g.addNode(intermediate1)
   122  
   123  	const im2NodeID = 4
   124  	intermediate2 := newMockNode(im2NodeID)
   125  	g.addNode(intermediate2)
   126  
   127  	g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, capacityOut)
   128  	g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, capacityOut)
   129  	g.addChannel(chanIm1Target, targetNodeID, im1NodeID, capacityIn)
   130  	g.addChannel(chanIm2Target, targetNodeID, im2NodeID, capacityIn)
   131  }
   132  
   133  var mppTestCases = []mppSendTestCase{
   134  	// Test a two-path graph with sufficient liquidity. It is expected that
   135  	// pathfinding will try first try to send the full amount via the two
   136  	// available routes. When that fails, it will half the amount to 35k sat
   137  	// and retry. That attempt reaches the target successfully. Then the
   138  	// same route is tried again. Because the channel only had 50k sat, it
   139  	// will fail. Finally the second route is tried for 35k and it succeeds
   140  	// too. Mpp payment complete.
   141  	{
   142  
   143  		name: "sufficient inbound",
   144  		graph: func(g *mockGraph) {
   145  			twoPathGraph(g, 200000, 100000)
   146  		},
   147  		amt:              70000,
   148  		expectedAttempts: 5,
   149  		expectedSuccesses: []expectedHtlcSuccess{
   150  			{
   151  				amt:   35000,
   152  				chans: []uint64{chanSourceIm1, chanIm1Target},
   153  			},
   154  			{
   155  				amt:   35000,
   156  				chans: []uint64{chanSourceIm2, chanIm2Target},
   157  			},
   158  		},
   159  		maxParts: 1000,
   160  	},
   161  
   162  	// Test that a cap on the max htlcs makes it impossible to pay.
   163  	{
   164  		name: "no splitting",
   165  		graph: func(g *mockGraph) {
   166  			twoPathGraph(g, 200000, 100000)
   167  		},
   168  		amt:               70000,
   169  		expectedAttempts:  2,
   170  		expectedSuccesses: []expectedHtlcSuccess{},
   171  		expectedFailure:   true,
   172  		maxParts:          1,
   173  	},
   174  
   175  	// Test that an attempt is made to split the payment in multiple parts
   176  	// that all use the same route if the full amount cannot be sent in a
   177  	// single htlc. The sender is effectively probing the receiver's
   178  	// incoming channel to see if it has sufficient balance. In this test
   179  	// case, the endeavour fails.
   180  	{
   181  
   182  		name:             "one path split",
   183  		graph:            onePathGraph,
   184  		amt:              70000,
   185  		expectedAttempts: 7,
   186  		expectedSuccesses: []expectedHtlcSuccess{
   187  			{
   188  				amt:   35000,
   189  				chans: []uint64{chanSourceIm1, chanIm1Target},
   190  			},
   191  			{
   192  				amt:   8750,
   193  				chans: []uint64{chanSourceIm1, chanIm1Target},
   194  			},
   195  		},
   196  		expectedFailure: true,
   197  		maxParts:        1000,
   198  	},
   199  
   200  	// Test that no attempts are made if the total local balance is
   201  	// insufficient.
   202  	{
   203  		name: "insufficient total balance",
   204  		graph: func(g *mockGraph) {
   205  			twoPathGraph(g, 100000, 500000)
   206  		},
   207  		amt:              300000,
   208  		expectedAttempts: 0,
   209  		expectedFailure:  true,
   210  		maxParts:         10,
   211  	},
   212  
   213  	// Test that if maxShardSize is set, then all attempts are below the
   214  	// max shard size, yet still sum up to the total payment amount. A
   215  	// payment of 30k satoshis with a max shard size of 10k satoshis should
   216  	// produce 3 payments of 10k sats each.
   217  	{
   218  		name:             "max shard size clamping",
   219  		graph:            onePathGraph,
   220  		amt:              30_000,
   221  		expectedAttempts: 3,
   222  		expectedSuccesses: []expectedHtlcSuccess{
   223  			{
   224  				amt:   10_000,
   225  				chans: []uint64{chanSourceIm1, chanIm1Target},
   226  			},
   227  			{
   228  				amt:   10_000,
   229  				chans: []uint64{chanSourceIm1, chanIm1Target},
   230  			},
   231  			{
   232  				amt:   10_000,
   233  				chans: []uint64{chanSourceIm1, chanIm1Target},
   234  			},
   235  		},
   236  		maxParts:     1000,
   237  		maxShardSize: 10_000,
   238  	},
   239  }
   240  
   241  // TestMppSend tests that a payment can be completed using multiple shards.
   242  func TestMppSend(t *testing.T) {
   243  	for _, testCase := range mppTestCases {
   244  		testCase := testCase
   245  
   246  		t.Run(testCase.name, func(t *testing.T) {
   247  			testMppSend(t, &testCase)
   248  		})
   249  	}
   250  }
   251  
   252  func testMppSend(t *testing.T, testCase *mppSendTestCase) {
   253  	ctx := newIntegratedRoutingContext(t)
   254  
   255  	g := ctx.graph
   256  	testCase.graph(g)
   257  
   258  	ctx.amt = lnwire.NewMAtomsFromAtoms(testCase.amt)
   259  
   260  	if testCase.maxShardSize != 0 {
   261  		shardAmt := lnwire.NewMAtomsFromAtoms(testCase.maxShardSize)
   262  		ctx.maxShardAmt = &shardAmt
   263  	}
   264  
   265  	attempts, err := ctx.testPayment(testCase.maxParts)
   266  	switch {
   267  	case err == nil && testCase.expectedFailure:
   268  		t.Fatal("expected payment to fail")
   269  	case err != nil && !testCase.expectedFailure:
   270  		t.Fatal("expected payment to succeed")
   271  	}
   272  
   273  	if len(attempts) != testCase.expectedAttempts {
   274  		t.Fatalf("expected %v attempts, but needed %v",
   275  			testCase.expectedAttempts, len(attempts),
   276  		)
   277  	}
   278  
   279  	assertSuccessAttempts(t, attempts, testCase.expectedSuccesses)
   280  }
   281  
   282  // expectedHtlcSuccess describes an expected successful htlc attempt.
   283  type expectedHtlcSuccess struct {
   284  	amt   dcrutil.Amount
   285  	chans []uint64
   286  }
   287  
   288  // equals matches the expectation with an actual attempt.
   289  func (e *expectedHtlcSuccess) equals(a htlcAttempt) bool {
   290  	if a.route.TotalAmount !=
   291  		lnwire.NewMAtomsFromAtoms(e.amt) {
   292  
   293  		return false
   294  	}
   295  
   296  	if len(a.route.Hops) != len(e.chans) {
   297  		return false
   298  	}
   299  
   300  	for i, h := range a.route.Hops {
   301  		if h.ChannelID != e.chans[i] {
   302  			return false
   303  		}
   304  	}
   305  
   306  	return true
   307  }
   308  
   309  // assertSuccessAttempts asserts that the set of successful htlc attempts
   310  // matches the given expectation.
   311  func assertSuccessAttempts(t *testing.T, attempts []htlcAttempt,
   312  	expected []expectedHtlcSuccess) {
   313  
   314  	successCount := 0
   315  loop:
   316  	for _, a := range attempts {
   317  		if !a.success {
   318  			continue
   319  		}
   320  
   321  		successCount++
   322  
   323  		for _, exp := range expected {
   324  			if exp.equals(a) {
   325  				continue loop
   326  			}
   327  		}
   328  
   329  		t.Fatalf("htlc success %v not found", a)
   330  	}
   331  
   332  	if successCount != len(expected) {
   333  		t.Fatalf("expected %v successful htlcs, but got %v",
   334  			expected, successCount)
   335  	}
   336  }
   337  
   338  // TestPaymentAddrOnlyNoSplit tests that if the dest of a payment only has the
   339  // payment addr feature bit set, then we won't attempt to split payments.
   340  func TestPaymentAddrOnlyNoSplit(t *testing.T) {
   341  	t.Parallel()
   342  
   343  	// First, we'll create the routing context, then create a simple two
   344  	// path graph where the sender has two paths to the destination.
   345  	ctx := newIntegratedRoutingContext(t)
   346  
   347  	// We'll have a basic graph with 2 mil sats of capacity, with 1 mil
   348  	// sats available on either end.
   349  	const chanSize = 2_000_000
   350  	twoPathGraph(ctx.graph, chanSize, chanSize)
   351  
   352  	payAddrOnlyFeatures := []lnwire.FeatureBit{
   353  		lnwire.TLVOnionPayloadOptional,
   354  		lnwire.PaymentAddrOptional,
   355  	}
   356  
   357  	// We'll make a payment of 1.5 mil satoshis our single chan sizes,
   358  	// which should cause a split attempt _if_ we had MPP bits activated.
   359  	// However, we only have the payment addr on, so we shouldn't split at
   360  	// all.
   361  	//
   362  	// We'll set a non-zero value for max parts as well, which should be
   363  	// ignored.
   364  	const maxParts = 5
   365  	ctx.amt = lnwire.NewMAtomsFromAtoms(1_500_000)
   366  
   367  	attempts, err := ctx.testPayment(maxParts, payAddrOnlyFeatures...)
   368  	require.NotNil(
   369  		t,
   370  		err,
   371  		fmt.Sprintf("expected path finding to fail instead made "+
   372  			"attempts: %v", spew.Sdump(attempts)),
   373  	)
   374  
   375  	// The payment should have failed since we need to split in order to
   376  	// route a payment to the destination, but they don't actually support
   377  	// MPP.
   378  	require.Equal(t, err.Error(), errNoPathFound.Error())
   379  }