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 }