github.com/decred/dcrlnd@v0.7.6/autopilot/agent_test.go (about) 1 package autopilot 2 3 import ( 4 "errors" 5 "fmt" 6 "net" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/decred/dcrd/dcrec/secp256k1/v4" 12 "github.com/decred/dcrd/dcrutil/v4" 13 "github.com/decred/dcrd/wire" 14 ) 15 16 type moreChansResp struct { 17 numMore uint32 18 amt dcrutil.Amount 19 } 20 21 type moreChanArg struct { 22 chans []LocalChannel 23 balance dcrutil.Amount 24 } 25 26 type mockConstraints struct { 27 moreChansResps chan moreChansResp 28 moreChanArgs chan moreChanArg 29 quit chan struct{} 30 } 31 32 func (m *mockConstraints) ChannelBudget(chans []LocalChannel, 33 balance dcrutil.Amount) (dcrutil.Amount, uint32) { 34 35 if m.moreChanArgs != nil { 36 moreChan := moreChanArg{ 37 chans: chans, 38 balance: balance, 39 } 40 41 select { 42 case m.moreChanArgs <- moreChan: 43 case <-m.quit: 44 return 0, 0 45 } 46 } 47 48 select { 49 case resp := <-m.moreChansResps: 50 return resp.amt, resp.numMore 51 case <-m.quit: 52 return 0, 0 53 } 54 } 55 56 func (m *mockConstraints) MaxPendingOpens() uint16 { 57 return 10 58 } 59 60 func (m *mockConstraints) MinChanSize() dcrutil.Amount { 61 return 1e7 62 } 63 func (m *mockConstraints) MaxChanSize() dcrutil.Amount { 64 return 1e8 65 } 66 67 var _ AgentConstraints = (*mockConstraints)(nil) 68 69 type mockHeuristic struct { 70 nodeScoresResps chan map[NodeID]*NodeScore 71 nodeScoresArgs chan directiveArg 72 73 quit chan struct{} 74 } 75 76 type directiveArg struct { 77 graph ChannelGraph 78 amt dcrutil.Amount 79 chans []LocalChannel 80 nodes map[NodeID]struct{} 81 } 82 83 func (m *mockHeuristic) Name() string { 84 return "mock" 85 } 86 87 func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []LocalChannel, 88 chanSize dcrutil.Amount, nodes map[NodeID]struct{}) ( 89 map[NodeID]*NodeScore, error) { 90 91 if m.nodeScoresArgs != nil { 92 directive := directiveArg{ 93 graph: g, 94 amt: chanSize, 95 chans: chans, 96 nodes: nodes, 97 } 98 99 select { 100 case m.nodeScoresArgs <- directive: 101 case <-m.quit: 102 return nil, errors.New("exiting") 103 } 104 } 105 106 select { 107 case resp := <-m.nodeScoresResps: 108 return resp, nil 109 case <-m.quit: 110 return nil, errors.New("exiting") 111 } 112 } 113 114 var _ AttachmentHeuristic = (*mockHeuristic)(nil) 115 116 type openChanIntent struct { 117 target *secp256k1.PublicKey 118 amt dcrutil.Amount 119 private bool 120 } 121 122 type mockChanController struct { 123 openChanSignals chan openChanIntent 124 private bool 125 } 126 127 func (m *mockChanController) OpenChannel(target *secp256k1.PublicKey, 128 amt dcrutil.Amount) error { 129 130 m.openChanSignals <- openChanIntent{ 131 target: target, 132 amt: amt, 133 private: m.private, 134 } 135 136 return nil 137 } 138 139 func (m *mockChanController) CloseChannel(chanPoint *wire.OutPoint) error { 140 return nil 141 } 142 143 var _ ChannelController = (*mockChanController)(nil) 144 145 type testContext struct { 146 constraints *mockConstraints 147 heuristic *mockHeuristic 148 chanController ChannelController 149 graph testGraph 150 agent *Agent 151 walletBalance dcrutil.Amount 152 153 quit chan struct{} 154 sync.Mutex 155 } 156 157 func setup(t *testing.T, initialChans []LocalChannel) (*testContext, func()) { 158 t.Helper() 159 160 // First, we'll create all the dependencies that we'll need in order to 161 // create the autopilot agent. 162 self, err := randKey() 163 if err != nil { 164 t.Fatalf("unable to generate key: %v", err) 165 } 166 167 quit := make(chan struct{}) 168 heuristic := &mockHeuristic{ 169 nodeScoresArgs: make(chan directiveArg), 170 nodeScoresResps: make(chan map[NodeID]*NodeScore), 171 quit: quit, 172 } 173 constraints := &mockConstraints{ 174 moreChansResps: make(chan moreChansResp), 175 moreChanArgs: make(chan moreChanArg), 176 quit: quit, 177 } 178 179 chanController := &mockChanController{ 180 openChanSignals: make(chan openChanIntent, 10), 181 } 182 memGraph, _, _ := newMemChanGraph() 183 184 // We'll keep track of the funds available to the agent, to make sure 185 // it correctly uses this value when querying the ChannelBudget. 186 var availableFunds dcrutil.Amount = 10 * dcrutil.AtomsPerCoin 187 188 ctx := &testContext{ 189 constraints: constraints, 190 heuristic: heuristic, 191 chanController: chanController, 192 graph: memGraph, 193 walletBalance: availableFunds, 194 quit: quit, 195 } 196 197 // With the dependencies we created, we can now create the initial 198 // agent itself. 199 testCfg := Config{ 200 Self: self, 201 Heuristic: heuristic, 202 ChanController: chanController, 203 WalletBalance: func() (dcrutil.Amount, error) { 204 ctx.Lock() 205 defer ctx.Unlock() 206 return ctx.walletBalance, nil 207 }, 208 ConnectToPeer: func(*secp256k1.PublicKey, []net.Addr) (bool, error) { 209 return false, nil 210 }, 211 DisconnectPeer: func(*secp256k1.PublicKey) error { 212 return nil 213 }, 214 Graph: memGraph, 215 Constraints: constraints, 216 } 217 218 agent, err := New(testCfg, initialChans) 219 if err != nil { 220 t.Fatalf("unable to create agent: %v", err) 221 } 222 ctx.agent = agent 223 224 // With the autopilot agent and all its dependencies we'll start the 225 // primary controller goroutine. 226 if err := agent.Start(); err != nil { 227 t.Fatalf("unable to start agent: %v", err) 228 } 229 230 cleanup := func() { 231 // We must close quit before agent.Stop(), to make sure 232 // ChannelBudget won't block preventing the agent from exiting. 233 close(quit) 234 agent.Stop() 235 } 236 237 return ctx, cleanup 238 } 239 240 // respondMoreChans consumes the moreChanArgs element and responds to the agent 241 // with the given moreChansResp. 242 func respondMoreChans(t *testing.T, testCtx *testContext, resp moreChansResp) { 243 t.Helper() 244 245 // The agent should now query the heuristic. 246 select { 247 case <-testCtx.constraints.moreChanArgs: 248 case <-time.After(time.Second * 3): 249 t.Fatalf("heuristic wasn't queried in time") 250 } 251 252 // We'll send the response. 253 select { 254 case testCtx.constraints.moreChansResps <- resp: 255 case <-time.After(time.Second * 10): 256 t.Fatalf("response wasn't sent in time") 257 } 258 } 259 260 // respondMoreChans consumes the nodeScoresArgs element and responds to the 261 // agent with the given node scores. 262 func respondNodeScores(t *testing.T, testCtx *testContext, 263 resp map[NodeID]*NodeScore) { 264 t.Helper() 265 266 // Send over an empty list of attachment directives, which should cause 267 // the agent to return to waiting on a new signal. 268 select { 269 case <-testCtx.heuristic.nodeScoresArgs: 270 case <-time.After(time.Second * 3): 271 t.Fatalf("node scores weren't queried in time") 272 } 273 select { 274 case testCtx.heuristic.nodeScoresResps <- resp: 275 case <-time.After(time.Second * 10): 276 t.Fatalf("node scores were not sent in time") 277 } 278 } 279 280 // TestAgentChannelOpenSignal tests that upon receipt of a chanOpenUpdate, then 281 // agent modifies its local state accordingly, and reconsults the heuristic. 282 func TestAgentChannelOpenSignal(t *testing.T) { 283 t.Parallel() 284 285 testCtx, cleanup := setup(t, nil) 286 defer cleanup() 287 288 // We'll send an initial "no" response to advance the agent past its 289 // initial check. 290 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 291 292 // Next we'll signal a new channel being opened by the backing LN node, 293 // with a capacity of 1 DCR. 294 newChan := LocalChannel{ 295 ChanID: randChanID(), 296 Balance: dcrutil.AtomsPerCoin, 297 } 298 testCtx.agent.OnChannelOpen(newChan) 299 300 // The agent should now query the heuristic in order to determine its 301 // next action as it local state has now been modified. 302 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 303 304 // At this point, the local state of the agent should 305 // have also been updated to reflect that the LN node 306 // now has an additional channel with one BTC. 307 if _, ok := testCtx.agent.chanState[newChan.ChanID]; !ok { 308 t.Fatalf("internal channel state wasn't updated") 309 } 310 311 // There shouldn't be a call to the Select method as we've returned 312 // "false" for NeedMoreChans above. 313 select { 314 315 // If this send success, then Select was erroneously called and the 316 // test should be failed. 317 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}: 318 t.Fatalf("Select was called but shouldn't have been") 319 320 // This is the correct path as Select should've be called. 321 default: 322 } 323 } 324 325 // TestAgentHeuristicUpdateSignal tests that upon notification about a 326 // heuristic update, the agent reconsults the heuristic. 327 func TestAgentHeuristicUpdateSignal(t *testing.T) { 328 t.Parallel() 329 330 testCtx, cleanup := setup(t, nil) 331 defer cleanup() 332 333 pub, err := testCtx.graph.addRandNode() 334 if err != nil { 335 t.Fatalf("unable to generate key: %v", err) 336 } 337 338 // We'll send an initial "no" response to advance the agent past its 339 // initial check. 340 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 341 342 // Next we'll signal that one of the heuristcs have been updated. 343 testCtx.agent.OnHeuristicUpdate(testCtx.heuristic) 344 345 // The update should trigger the agent to ask for a channel budget.so 346 // we'll respond that there is a budget for opening 1 more channel. 347 respondMoreChans(t, testCtx, 348 moreChansResp{ 349 numMore: 1, 350 amt: 1 * dcrutil.AtomsPerCoin, 351 }, 352 ) 353 354 // At this point, the agent should now be querying the heuristic for 355 // scores. We'll respond. 356 nodeID := NewNodeID(pub) 357 scores := map[NodeID]*NodeScore{ 358 nodeID: { 359 NodeID: nodeID, 360 Score: 0.5, 361 }, 362 } 363 respondNodeScores(t, testCtx, scores) 364 365 // Finally, this should result in the agent opening a channel. 366 chanController := testCtx.chanController.(*mockChanController) 367 select { 368 case <-chanController.openChanSignals: 369 case <-time.After(time.Second * 10): 370 t.Fatalf("channel not opened in time") 371 } 372 } 373 374 // A mockFailingChanController always fails to open a channel. 375 type mockFailingChanController struct { 376 } 377 378 func (m *mockFailingChanController) OpenChannel(target *secp256k1.PublicKey, 379 amt dcrutil.Amount) error { 380 return errors.New("failure") 381 } 382 383 func (m *mockFailingChanController) CloseChannel(chanPoint *wire.OutPoint) error { 384 return nil 385 } 386 387 var _ ChannelController = (*mockFailingChanController)(nil) 388 389 // TestAgentChannelFailureSignal tests that if an autopilot channel fails to 390 // open, the agent is signalled to make a new decision. 391 func TestAgentChannelFailureSignal(t *testing.T) { 392 t.Parallel() 393 394 testCtx, cleanup := setup(t, nil) 395 defer cleanup() 396 397 testCtx.chanController = &mockFailingChanController{} 398 399 node, err := testCtx.graph.addRandNode() 400 if err != nil { 401 t.Fatalf("unable to add node: %v", err) 402 } 403 404 // First ensure the agent will attempt to open a new channel. Return 405 // that we need more channels, and have 5BTC to use. 406 respondMoreChans(t, testCtx, moreChansResp{1, 5 * dcrutil.AtomsPerCoin}) 407 408 // At this point, the agent should now be querying the heuristic to 409 // request attachment directives, return a fake so the agent will 410 // attempt to open a channel. 411 var fakeDirective = &NodeScore{ 412 NodeID: NewNodeID(node), 413 Score: 0.5, 414 } 415 416 respondNodeScores( 417 t, testCtx, map[NodeID]*NodeScore{ 418 NewNodeID(node): fakeDirective, 419 }, 420 ) 421 422 // At this point the agent will attempt to create a channel and fail. 423 424 // Now ensure that the controller loop is re-executed. 425 respondMoreChans(t, testCtx, moreChansResp{1, 5 * dcrutil.AtomsPerCoin}) 426 respondNodeScores(t, testCtx, map[NodeID]*NodeScore{}) 427 } 428 429 // TestAgentChannelCloseSignal ensures that once the agent receives an outside 430 // signal of a channel belonging to the backing LN node being closed, then it 431 // will query the heuristic to make its next decision. 432 func TestAgentChannelCloseSignal(t *testing.T) { 433 t.Parallel() 434 435 // We'll start the agent with two channels already being active. 436 initialChans := []LocalChannel{ 437 { 438 ChanID: randChanID(), 439 Balance: dcrutil.AtomsPerCoin, 440 }, 441 { 442 ChanID: randChanID(), 443 Balance: dcrutil.AtomsPerCoin * 2, 444 }, 445 } 446 447 testCtx, cleanup := setup(t, initialChans) 448 defer cleanup() 449 450 // We'll send an initial "no" response to advance the agent past its 451 // initial check. 452 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 453 454 // Next, we'll close both channels which should force the agent to 455 // re-query the heuristic. 456 testCtx.agent.OnChannelClose(initialChans[0].ChanID, initialChans[1].ChanID) 457 458 // The agent should now query the heuristic in order to determine its 459 // next action as it local state has now been modified. 460 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 461 462 // At this point, the local state of the agent should 463 // have also been updated to reflect that the LN node 464 // has no existing open channels. 465 if len(testCtx.agent.chanState) != 0 { 466 t.Fatalf("internal channel state wasn't updated") 467 } 468 469 // There shouldn't be a call to the Select method as we've returned 470 // "false" for NeedMoreChans above. 471 select { 472 473 // If this send success, then Select was erroneously called and the 474 // test should be failed. 475 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}: 476 t.Fatalf("Select was called but shouldn't have been") 477 478 // This is the correct path as Select should've be called. 479 default: 480 } 481 } 482 483 // TestAgentBalanceUpdateIncrease ensures that once the agent receives an 484 // outside signal concerning a balance update, then it will re-query the 485 // heuristic to determine its next action. 486 func TestAgentBalanceUpdate(t *testing.T) { 487 t.Parallel() 488 489 testCtx, cleanup := setup(t, nil) 490 defer cleanup() 491 492 // We'll send an initial "no" response to advance the agent past its 493 // initial check. 494 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 495 496 // Next we'll send a new balance update signal to the agent, adding 5 497 // BTC to the amount of available funds. 498 testCtx.Lock() 499 testCtx.walletBalance += dcrutil.AtomsPerCoin * 5 500 testCtx.Unlock() 501 502 testCtx.agent.OnBalanceChange() 503 504 // The agent should now query the heuristic in order to determine its 505 // next action as it local state has now been modified. 506 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 507 508 // At this point, the local state of the agent should 509 // have also been updated to reflect that the LN node 510 // now has an additional 5BTC available. 511 if testCtx.agent.totalBalance != testCtx.walletBalance { 512 t.Fatalf("expected %v wallet balance "+ 513 "instead have %v", testCtx.agent.totalBalance, 514 testCtx.walletBalance) 515 } 516 517 // There shouldn't be a call to the Select method as we've returned 518 // "false" for NeedMoreChans above. 519 select { 520 521 // If this send success, then Select was erroneously called and the 522 // test should be failed. 523 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}: 524 t.Fatalf("Select was called but shouldn't have been") 525 526 // This is the correct path as Select should've be called. 527 default: 528 } 529 } 530 531 // TestAgentImmediateAttach tests that if an autopilot agent is created, and it 532 // has enough funds available to create channels, then it does so immediately. 533 func TestAgentImmediateAttach(t *testing.T) { 534 t.Parallel() 535 536 testCtx, cleanup := setup(t, nil) 537 defer cleanup() 538 539 const numChans = 5 540 541 // We'll generate 5 mock directives so it can progress within its loop. 542 directives := make(map[NodeID]*NodeScore) 543 nodeKeys := make(map[NodeID]struct{}) 544 for i := 0; i < numChans; i++ { 545 pub, err := testCtx.graph.addRandNode() 546 if err != nil { 547 t.Fatalf("unable to generate key: %v", err) 548 } 549 nodeID := NewNodeID(pub) 550 directives[nodeID] = &NodeScore{ 551 NodeID: nodeID, 552 Score: 0.5, 553 } 554 nodeKeys[nodeID] = struct{}{} 555 } 556 // The very first thing the agent should do is query the NeedMoreChans 557 // method on the passed heuristic. So we'll provide it with a response 558 // that will kick off the main loop. 559 respondMoreChans(t, testCtx, 560 moreChansResp{ 561 numMore: numChans, 562 amt: 5 * dcrutil.AtomsPerCoin, 563 }, 564 ) 565 566 // At this point, the agent should now be querying the heuristic to 567 // requests attachment directives. With our fake directives created, 568 // we'll now send then to the agent as a return value for the Select 569 // function. 570 respondNodeScores(t, testCtx, directives) 571 572 // Finally, we should receive 5 calls to the OpenChannel method with 573 // the exact same parameters that we specified within the attachment 574 // directives. 575 chanController := testCtx.chanController.(*mockChanController) 576 for i := 0; i < numChans; i++ { 577 select { 578 case openChan := <-chanController.openChanSignals: 579 if openChan.amt != dcrutil.AtomsPerCoin { 580 t.Fatalf("invalid chan amt: expected %v, got %v", 581 dcrutil.AtomsPerCoin, openChan.amt) 582 } 583 nodeID := NewNodeID(openChan.target) 584 _, ok := nodeKeys[nodeID] 585 if !ok { 586 t.Fatalf("unexpected key: %v, not found", 587 nodeID) 588 } 589 delete(nodeKeys, nodeID) 590 591 case <-time.After(time.Second * 10): 592 t.Fatalf("channel not opened in time") 593 } 594 } 595 } 596 597 // TestAgentPrivateChannels ensure that only requests for private channels are 598 // sent if set. 599 func TestAgentPrivateChannels(t *testing.T) { 600 t.Parallel() 601 602 testCtx, cleanup := setup(t, nil) 603 defer cleanup() 604 605 // The chanController should be initialized such that all of its open 606 // channel requests are for private channels. 607 testCtx.chanController.(*mockChanController).private = true 608 609 const numChans = 5 610 611 // We'll generate 5 mock directives so the pubkeys will be found in the 612 // agent's graph, and it can progress within its loop. 613 directives := make(map[NodeID]*NodeScore) 614 for i := 0; i < numChans; i++ { 615 pub, err := testCtx.graph.addRandNode() 616 if err != nil { 617 t.Fatalf("unable to generate key: %v", err) 618 } 619 directives[NewNodeID(pub)] = &NodeScore{ 620 NodeID: NewNodeID(pub), 621 Score: 0.5, 622 } 623 } 624 625 // The very first thing the agent should do is query the NeedMoreChans 626 // method on the passed heuristic. So we'll provide it with a response 627 // that will kick off the main loop. We'll send over a response 628 // indicating that it should establish more channels, and give it a 629 // budget of 5 DCR to do so. 630 resp := moreChansResp{ 631 numMore: numChans, 632 amt: 5 * dcrutil.AtomsPerCoin, 633 } 634 respondMoreChans(t, testCtx, resp) 635 636 // At this point, the agent should now be querying the heuristic to 637 // requests attachment directives. With our fake directives created, 638 // we'll now send then to the agent as a return value for the Select 639 // function. 640 respondNodeScores(t, testCtx, directives) 641 642 // Finally, we should receive 5 calls to the OpenChannel method, each 643 // specifying that it's for a private channel. 644 chanController := testCtx.chanController.(*mockChanController) 645 for i := 0; i < numChans; i++ { 646 select { 647 case openChan := <-chanController.openChanSignals: 648 if !openChan.private { 649 t.Fatal("expected open channel request to be private") 650 } 651 case <-time.After(10 * time.Second): 652 t.Fatal("channel not opened in time") 653 } 654 } 655 } 656 657 // TestAgentPendingChannelState ensures that the agent properly factors in its 658 // pending channel state when making decisions w.r.t if it needs more channels 659 // or not, and if so, who is eligible to open new channels to. 660 func TestAgentPendingChannelState(t *testing.T) { 661 t.Parallel() 662 663 testCtx, cleanup := setup(t, nil) 664 defer cleanup() 665 666 // We'll only return a single directive for a pre-chosen node. 667 nodeKey, err := testCtx.graph.addRandNode() 668 if err != nil { 669 t.Fatalf("unable to generate key: %v", err) 670 } 671 nodeID := NewNodeID(nodeKey) 672 nodeDirective := &NodeScore{ 673 NodeID: nodeID, 674 Score: 0.5, 675 } 676 677 // Once again, we'll start by telling the agent as part of its first 678 // query, that it needs more channels and has 3 BTC available for 679 // attachment. We'll send over a response indicating that it should 680 // establish more channels, and give it a budget of 1 BTC to do so. 681 respondMoreChans(t, testCtx, 682 moreChansResp{ 683 numMore: 1, 684 amt: dcrutil.AtomsPerCoin, 685 }, 686 ) 687 688 respondNodeScores(t, testCtx, 689 map[NodeID]*NodeScore{ 690 nodeID: nodeDirective, 691 }, 692 ) 693 694 // A request to open the channel should've also been sent. 695 chanController := testCtx.chanController.(*mockChanController) 696 select { 697 case openChan := <-chanController.openChanSignals: 698 chanAmt := testCtx.constraints.MaxChanSize() 699 if openChan.amt != chanAmt { 700 t.Fatalf("invalid chan amt: expected %v, got %v", 701 chanAmt, openChan.amt) 702 } 703 if !openChan.target.IsEqual(nodeKey) { 704 t.Fatalf("unexpected key: expected %x, got %x", 705 nodeKey.SerializeCompressed(), 706 openChan.target.SerializeCompressed()) 707 } 708 case <-time.After(time.Second * 10): 709 t.Fatalf("channel wasn't opened in time") 710 } 711 712 // Now, in order to test that the pending state was properly updated, 713 // we'll trigger a balance update in order to trigger a query to the 714 // heuristic. 715 testCtx.Lock() 716 testCtx.walletBalance += 0.4 * dcrutil.AtomsPerCoin 717 testCtx.Unlock() 718 719 testCtx.agent.OnBalanceChange() 720 721 // The heuristic should be queried, and the argument for the set of 722 // channels passed in should include the pending channels that 723 // should've been created above. 724 select { 725 // The request that we get should include a pending channel for the 726 // one that we just created, otherwise the agent isn't properly 727 // updating its internal state. 728 case req := <-testCtx.constraints.moreChanArgs: 729 chanAmt := testCtx.constraints.MaxChanSize() 730 if len(req.chans) != 1 { 731 t.Fatalf("should include pending chan in current "+ 732 "state, instead have %v chans", len(req.chans)) 733 } 734 if req.chans[0].Balance != chanAmt { 735 t.Fatalf("wrong chan balance: expected %v, got %v", 736 req.chans[0].Balance, chanAmt) 737 } 738 if req.chans[0].Node != nodeID { 739 t.Fatalf("wrong node ID: expected %x, got %x", 740 nodeID, req.chans[0].Node[:]) 741 } 742 case <-time.After(time.Second * 10): 743 t.Fatalf("need more chans wasn't queried in time") 744 } 745 746 // We'll send across a response indicating that it *does* need more 747 // channels. 748 select { 749 case testCtx.constraints.moreChansResps <- moreChansResp{1, dcrutil.AtomsPerCoin}: 750 case <-time.After(time.Second * 10): 751 t.Fatalf("need more chans wasn't queried in time") 752 } 753 754 // The response above should prompt the agent to make a query to the 755 // Select method. The arguments passed should reflect the fact that the 756 // node we have a pending channel to, should be ignored. 757 select { 758 case req := <-testCtx.heuristic.nodeScoresArgs: 759 if len(req.chans) == 0 { 760 t.Fatalf("expected to skip %v nodes, instead "+ 761 "skipping %v", 1, len(req.chans)) 762 } 763 if req.chans[0].Node != nodeID { 764 t.Fatalf("pending node not included in skip arguments") 765 } 766 case <-time.After(time.Second * 10): 767 t.Fatalf("select wasn't queried in time") 768 } 769 } 770 771 // TestAgentPendingOpenChannel ensures that the agent queries its heuristic once 772 // it detects a channel is pending open. This allows the agent to use its own 773 // change outputs that have yet to confirm for funding transactions. 774 func TestAgentPendingOpenChannel(t *testing.T) { 775 t.Parallel() 776 777 testCtx, cleanup := setup(t, nil) 778 defer cleanup() 779 780 // We'll send an initial "no" response to advance the agent past its 781 // initial check. 782 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 783 784 // Next, we'll signal that a new channel has been opened, but it is 785 // still pending. 786 testCtx.agent.OnChannelPendingOpen() 787 788 // The agent should now query the heuristic in order to determine its 789 // next action as its local state has now been modified. 790 respondMoreChans(t, testCtx, moreChansResp{0, 0}) 791 792 // There shouldn't be a call to the Select method as we've returned 793 // "false" for NeedMoreChans above. 794 select { 795 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}: 796 t.Fatalf("Select was called but shouldn't have been") 797 default: 798 } 799 } 800 801 // TestAgentOnNodeUpdates tests that the agent will wake up in response to the 802 // OnNodeUpdates signal. This is useful in ensuring that autopilot is always 803 // pulling in the latest graph updates into its decision making. It also 804 // prevents the agent from stalling after an initial attempt that finds no nodes 805 // in the graph. 806 func TestAgentOnNodeUpdates(t *testing.T) { 807 t.Parallel() 808 809 testCtx, cleanup := setup(t, nil) 810 defer cleanup() 811 812 // We'll send an initial "yes" response to advance the agent past its 813 // initial check. This will cause it to try to get directives from an 814 // empty graph. 815 respondMoreChans( 816 t, testCtx, 817 moreChansResp{ 818 numMore: 2, 819 amt: testCtx.walletBalance, 820 }, 821 ) 822 823 // Send over an empty list of attachment directives, which should cause 824 // the agent to return to waiting on a new signal. 825 respondNodeScores(t, testCtx, map[NodeID]*NodeScore{}) 826 827 // Simulate more nodes being added to the graph by informing the agent 828 // that we have node updates. 829 testCtx.agent.OnNodeUpdates() 830 831 // In response, the agent should wake up and see if it needs more 832 // channels. Since we haven't done anything, we will send the same 833 // response as before since we are still trying to open channels. 834 respondMoreChans( 835 t, testCtx, 836 moreChansResp{ 837 numMore: 2, 838 amt: testCtx.walletBalance, 839 }, 840 ) 841 842 // Again the agent should pull in the next set of attachment directives. 843 // It's not important that this list is also empty, so long as the node 844 // updates signal is causing the agent to make this attempt. 845 respondNodeScores(t, testCtx, map[NodeID]*NodeScore{}) 846 } 847 848 // TestAgentSkipPendingConns asserts that the agent will not try to make 849 // duplicate connection requests to the same node, even if the attachment 850 // heuristic instructs the agent to do so. It also asserts that the agent 851 // stops tracking the pending connection once it finishes. Note that in 852 // practice, a failed connection would be inserted into the skip map passed to 853 // the attachment heuristic, though this does not assert that case. 854 func TestAgentSkipPendingConns(t *testing.T) { 855 t.Parallel() 856 857 testCtx, cleanup := setup(t, nil) 858 defer cleanup() 859 860 connect := make(chan chan error) 861 testCtx.agent.cfg.ConnectToPeer = func(*secp256k1.PublicKey, []net.Addr) (bool, error) { 862 errChan := make(chan error) 863 864 select { 865 case connect <- errChan: 866 case <-testCtx.quit: 867 return false, errors.New("quit") 868 } 869 870 select { 871 case err := <-errChan: 872 return false, err 873 case <-testCtx.quit: 874 return false, errors.New("quit") 875 } 876 } 877 878 // We'll only return a single directive for a pre-chosen node. 879 nodeKey, err := testCtx.graph.addRandNode() 880 if err != nil { 881 t.Fatalf("unable to generate key: %v", err) 882 } 883 nodeID := NewNodeID(nodeKey) 884 nodeDirective := &NodeScore{ 885 NodeID: nodeID, 886 Score: 0.5, 887 } 888 889 // We'll also add a second node to the graph, to keep the first one 890 // company. 891 nodeKey2, err := testCtx.graph.addRandNode() 892 if err != nil { 893 t.Fatalf("unable to generate key: %v", err) 894 } 895 nodeID2 := NewNodeID(nodeKey2) 896 897 // We'll send an initial "yes" response to advance the agent past its 898 // initial check. This will cause it to try to get directives from the 899 // graph. 900 respondMoreChans(t, testCtx, 901 moreChansResp{ 902 numMore: 1, 903 amt: testCtx.walletBalance, 904 }, 905 ) 906 907 // Both nodes should be part of the arguments. 908 select { 909 case req := <-testCtx.heuristic.nodeScoresArgs: 910 if len(req.nodes) != 2 { 911 t.Fatalf("expected %v nodes, instead "+ 912 "had %v", 2, len(req.nodes)) 913 } 914 if _, ok := req.nodes[nodeID]; !ok { 915 t.Fatalf("node not included in arguments") 916 } 917 if _, ok := req.nodes[nodeID2]; !ok { 918 t.Fatalf("node not included in arguments") 919 } 920 case <-time.After(time.Second * 10): 921 t.Fatalf("select wasn't queried in time") 922 } 923 924 // Respond with a scored directive. We skip node2 for now, implicitly 925 // giving it a zero-score. 926 select { 927 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{ 928 NewNodeID(nodeKey): nodeDirective, 929 }: 930 case <-time.After(time.Second * 10): 931 t.Fatalf("heuristic wasn't queried in time") 932 } 933 934 // The agent should attempt connection to the node. 935 var errChan chan error 936 select { 937 case errChan = <-connect: 938 case <-time.After(time.Second * 10): 939 t.Fatalf("agent did not attempt connection") 940 } 941 942 // Signal the agent to go again, now that we've tried to connect. 943 testCtx.agent.OnNodeUpdates() 944 945 // The heuristic again informs the agent that we need more channels. 946 respondMoreChans(t, testCtx, 947 moreChansResp{ 948 numMore: 1, 949 amt: testCtx.walletBalance, 950 }, 951 ) 952 953 // Since the node now has a pending connection, it should be skipped 954 // and not part of the nodes attempting to be scored. 955 select { 956 case req := <-testCtx.heuristic.nodeScoresArgs: 957 if len(req.nodes) != 1 { 958 t.Fatalf("expected %v nodes, instead "+ 959 "had %v", 1, len(req.nodes)) 960 } 961 if _, ok := req.nodes[nodeID2]; !ok { 962 t.Fatalf("node not included in arguments") 963 } 964 case <-time.After(time.Second * 10): 965 t.Fatalf("select wasn't queried in time") 966 } 967 968 // Respond with an emtpty score set. 969 select { 970 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}: 971 case <-time.After(time.Second * 10): 972 t.Fatalf("heuristic wasn't queried in time") 973 } 974 975 // The agent should not attempt any connection, since no nodes were 976 // scored. 977 select { 978 case <-connect: 979 t.Fatalf("agent should not have attempted connection") 980 case <-time.After(time.Second * 3): 981 } 982 983 // Now, timeout the original request, which should still be waiting for 984 // a response. 985 select { 986 case errChan <- fmt.Errorf("connection timeout"): 987 case <-time.After(time.Second * 10): 988 t.Fatalf("agent did not receive connection timeout") 989 } 990 991 // The agent will now retry since the last connection attempt failed. 992 // The heuristic again informs the agent that we need more channels. 993 respondMoreChans(t, testCtx, 994 moreChansResp{ 995 numMore: 1, 996 amt: testCtx.walletBalance, 997 }, 998 ) 999 1000 // The node should now be marked as "failed", which should make it 1001 // being skipped during scoring. Again check that it won't be among the 1002 // score request. 1003 select { 1004 case req := <-testCtx.heuristic.nodeScoresArgs: 1005 if len(req.nodes) != 1 { 1006 t.Fatalf("expected %v nodes, instead "+ 1007 "had %v", 1, len(req.nodes)) 1008 } 1009 if _, ok := req.nodes[nodeID2]; !ok { 1010 t.Fatalf("node not included in arguments") 1011 } 1012 case <-time.After(time.Second * 10): 1013 t.Fatalf("select wasn't queried in time") 1014 } 1015 1016 // Send a directive for the second node. 1017 nodeDirective2 := &NodeScore{ 1018 NodeID: nodeID2, 1019 Score: 0.5, 1020 } 1021 select { 1022 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{ 1023 nodeID2: nodeDirective2, 1024 }: 1025 case <-time.After(time.Second * 10): 1026 t.Fatalf("heuristic wasn't queried in time") 1027 } 1028 1029 // This time, the agent should try the connection to the second node. 1030 select { 1031 case <-connect: 1032 case <-time.After(time.Second * 10): 1033 t.Fatalf("agent should have attempted connection") 1034 } 1035 } 1036 1037 // TestAgentQuitWhenPendingConns tests that we are able to stop the autopilot 1038 // agent even though there are pending connections to nodes. 1039 func TestAgentQuitWhenPendingConns(t *testing.T) { 1040 t.Parallel() 1041 1042 testCtx, cleanup := setup(t, nil) 1043 defer cleanup() 1044 1045 connect := make(chan chan error) 1046 1047 testCtx.agent.cfg.ConnectToPeer = func(*secp256k1.PublicKey, []net.Addr) (bool, error) { 1048 errChan := make(chan error) 1049 1050 select { 1051 case connect <- errChan: 1052 case <-testCtx.quit: 1053 return false, errors.New("quit") 1054 } 1055 1056 select { 1057 case err := <-errChan: 1058 return false, err 1059 case <-testCtx.quit: 1060 return false, errors.New("quit") 1061 } 1062 } 1063 1064 // We'll only return a single directive for a pre-chosen node. 1065 nodeKey, err := testCtx.graph.addRandNode() 1066 if err != nil { 1067 t.Fatalf("unable to generate key: %v", err) 1068 } 1069 nodeID := NewNodeID(nodeKey) 1070 nodeDirective := &NodeScore{ 1071 NodeID: nodeID, 1072 Score: 0.5, 1073 } 1074 1075 // We'll send an initial "yes" response to advance the agent past its 1076 // initial check. This will cause it to try to get directives from the 1077 // graph. 1078 respondMoreChans(t, testCtx, 1079 moreChansResp{ 1080 numMore: 1, 1081 amt: testCtx.walletBalance, 1082 }, 1083 ) 1084 1085 // Check the args. 1086 select { 1087 case req := <-testCtx.heuristic.nodeScoresArgs: 1088 if len(req.nodes) != 1 { 1089 t.Fatalf("expected %v nodes, instead "+ 1090 "had %v", 1, len(req.nodes)) 1091 } 1092 if _, ok := req.nodes[nodeID]; !ok { 1093 t.Fatalf("node not included in arguments") 1094 } 1095 case <-time.After(time.Second * 10): 1096 t.Fatalf("select wasn't queried in time") 1097 } 1098 1099 // Respond with a scored directive. 1100 select { 1101 case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{ 1102 NewNodeID(nodeKey): nodeDirective, 1103 }: 1104 case <-time.After(time.Second * 10): 1105 t.Fatalf("heuristic wasn't queried in time") 1106 } 1107 1108 // The agent should attempt connection to the node. 1109 select { 1110 case <-connect: 1111 case <-time.After(time.Second * 10): 1112 t.Fatalf("agent did not attempt connection") 1113 } 1114 1115 // Make sure that we are able to stop the agent, even though there is a 1116 // pending connection. 1117 stopped := make(chan error) 1118 go func() { 1119 stopped <- testCtx.agent.Stop() 1120 }() 1121 1122 select { 1123 case err := <-stopped: 1124 if err != nil { 1125 t.Fatalf("error stopping agent: %v", err) 1126 } 1127 case <-time.After(2 * time.Second): 1128 t.Fatalf("unable to stop agent") 1129 } 1130 } 1131 1132 // respondWithScores checks that the moreChansRequest contains what we expect, 1133 // and responds with the given node scores. 1134 func respondWithScores(t *testing.T, testCtx *testContext, 1135 channelBudget dcrutil.Amount, existingChans, newChans int, 1136 nodeScores map[NodeID]*NodeScore) { 1137 1138 t.Helper() 1139 1140 select { 1141 case testCtx.constraints.moreChansResps <- moreChansResp{ 1142 numMore: uint32(newChans), 1143 amt: channelBudget, 1144 }: 1145 case <-time.After(time.Second * 3): 1146 t.Fatalf("heuristic wasn't queried in time") 1147 } 1148 1149 // The agent should query for scores using the constraints returned 1150 // above. We expect the agent to use the maximum channel size when 1151 // opening channels. 1152 chanSize := testCtx.constraints.MaxChanSize() 1153 1154 select { 1155 case req := <-testCtx.heuristic.nodeScoresArgs: 1156 // All nodes in the graph should be potential channel 1157 // candidates. 1158 if len(req.nodes) != len(nodeScores) { 1159 t.Fatalf("expected %v nodes, instead had %v", 1160 len(nodeScores), len(req.nodes)) 1161 } 1162 1163 // 'existingChans' is already open. 1164 if len(req.chans) != existingChans { 1165 t.Fatalf("expected %d existing channel, got %v", 1166 existingChans, len(req.chans)) 1167 } 1168 if req.amt != chanSize { 1169 t.Fatalf("expected channel size of %v, got %v", 1170 chanSize, req.amt) 1171 } 1172 1173 case <-time.After(time.Second * 3): 1174 t.Fatalf("select wasn't queried in time") 1175 } 1176 1177 // Respond with the given scores. 1178 select { 1179 case testCtx.heuristic.nodeScoresResps <- nodeScores: 1180 case <-time.After(time.Second * 3): 1181 t.Fatalf("NodeScores wasn't queried in time") 1182 } 1183 } 1184 1185 // checkChannelOpens asserts that the channel controller attempts open the 1186 // number of channels we expect, and with the exact total allocation. 1187 func checkChannelOpens(t *testing.T, testCtx *testContext, 1188 allocation dcrutil.Amount, numChans int) []NodeID { 1189 1190 var nodes []NodeID 1191 1192 // The agent should attempt to open channels, totaling what we expect. 1193 var totalAllocation dcrutil.Amount 1194 chanController := testCtx.chanController.(*mockChanController) 1195 for i := 0; i < numChans; i++ { 1196 select { 1197 case openChan := <-chanController.openChanSignals: 1198 totalAllocation += openChan.amt 1199 1200 testCtx.Lock() 1201 testCtx.walletBalance -= openChan.amt 1202 testCtx.Unlock() 1203 1204 nodes = append(nodes, NewNodeID(openChan.target)) 1205 1206 case <-time.After(time.Second * 3): 1207 t.Fatalf("channel not opened in time") 1208 } 1209 } 1210 1211 if totalAllocation != allocation { 1212 t.Fatalf("expected agent to open channels totalling %v, "+ 1213 "instead was %v", allocation, totalAllocation) 1214 } 1215 1216 // Finally, make sure the agent won't try opening more channels. 1217 select { 1218 case <-chanController.openChanSignals: 1219 t.Fatalf("agent unexpectedly opened channel") 1220 1221 case <-time.After(50 * time.Millisecond): 1222 } 1223 1224 return nodes 1225 } 1226 1227 // TestAgentChannelSizeAllocation tests that the autopilot agent opens channel 1228 // of size that stays within the channel budget and size restrictions. 1229 func TestAgentChannelSizeAllocation(t *testing.T) { 1230 t.Parallel() 1231 1232 // Total number of nodes in our mock graph. 1233 const numNodes = 20 1234 1235 testCtx, cleanup := setup(t, nil) 1236 defer cleanup() 1237 1238 nodeScores := make(map[NodeID]*NodeScore) 1239 for i := 0; i < numNodes; i++ { 1240 nodeKey, err := testCtx.graph.addRandNode() 1241 if err != nil { 1242 t.Fatalf("unable to generate key: %v", err) 1243 } 1244 nodeID := NewNodeID(nodeKey) 1245 nodeScores[nodeID] = &NodeScore{ 1246 NodeID: nodeID, 1247 Score: 0.5, 1248 } 1249 } 1250 1251 // The agent should now query the heuristic in order to determine its 1252 // next action as it local state has now been modified. 1253 select { 1254 case arg := <-testCtx.constraints.moreChanArgs: 1255 if len(arg.chans) != 0 { 1256 t.Fatalf("expected agent to have no channels open, "+ 1257 "had %v", len(arg.chans)) 1258 } 1259 if arg.balance != testCtx.walletBalance { 1260 t.Fatalf("expectd agent to have %v balance, had %v", 1261 testCtx.walletBalance, arg.balance) 1262 } 1263 case <-time.After(time.Second * 3): 1264 t.Fatalf("heuristic wasn't queried in time") 1265 } 1266 1267 // We'll return a response telling the agent to open 5 channels, with a 1268 // total channel budget of 5 BTC. 1269 var channelBudget dcrutil.Amount = 5 * dcrutil.AtomsPerCoin 1270 numExistingChannels := 0 1271 numNewChannels := 5 1272 respondWithScores( 1273 t, testCtx, channelBudget, numExistingChannels, 1274 numNewChannels, nodeScores, 1275 ) 1276 1277 // We expect the autopilot to have allocated all funds towards 1278 // channels. 1279 expectedAllocation := testCtx.constraints.MaxChanSize() * dcrutil.Amount(numNewChannels) 1280 nodes := checkChannelOpens( 1281 t, testCtx, expectedAllocation, numNewChannels, 1282 ) 1283 1284 // Delete the selected nodes from our set of scores, to avoid scoring 1285 // nodes we already have channels to. 1286 for _, node := range nodes { 1287 delete(nodeScores, node) 1288 } 1289 1290 // TODO(halseth): this loop is a hack to ensure all the attempted 1291 // channels are accounted for. This happens because the agent will 1292 // query the ChannelBudget before all the pending channels are added to 1293 // the map. Fix by adding them to the pending channels map before 1294 // executing directives in goroutines? 1295 waitForNumChans := func(expChans int) { 1296 t.Helper() 1297 1298 var ( 1299 numChans int 1300 balance dcrutil.Amount 1301 ) 1302 1303 Loop: 1304 for { 1305 select { 1306 case arg := <-testCtx.constraints.moreChanArgs: 1307 numChans = len(arg.chans) 1308 balance = arg.balance 1309 1310 // As long as the number of existing channels 1311 // is below our expected number of channels, 1312 // and the balance is not what we expect, we'll 1313 // keep responding with "no more channels". 1314 if numChans == expChans && 1315 balance == testCtx.walletBalance { 1316 break Loop 1317 } 1318 1319 select { 1320 case testCtx.constraints.moreChansResps <- moreChansResp{0, 0}: 1321 case <-time.After(time.Second * 3): 1322 t.Fatalf("heuristic wasn't queried " + 1323 "in time") 1324 } 1325 1326 case <-time.After(time.Second * 3): 1327 t.Fatalf("did not receive expected "+ 1328 "channels(%d) and balance(%d), "+ 1329 "instead got %d and %d", expChans, 1330 testCtx.walletBalance, numChans, 1331 balance) 1332 } 1333 } 1334 } 1335 1336 // Wait for the agent to have 5 channels. 1337 waitForNumChans(numNewChannels) 1338 1339 // Set the channel budget to 1.5 BTC. 1340 channelBudget = dcrutil.AtomsPerCoin * 3 / 2 1341 1342 // We'll return a response telling the agent to open 3 channels, with a 1343 // total channel budget of 1.5 BTC. 1344 numExistingChannels = 5 1345 numNewChannels = 3 1346 respondWithScores( 1347 t, testCtx, channelBudget, numExistingChannels, 1348 numNewChannels, nodeScores, 1349 ) 1350 1351 // To stay within the budget, we expect the autopilot to open 2 1352 // channels. 1353 expectedAllocation = channelBudget 1354 nodes = checkChannelOpens(t, testCtx, expectedAllocation, 2) 1355 numExistingChannels = 7 1356 1357 for _, node := range nodes { 1358 delete(nodeScores, node) 1359 } 1360 1361 waitForNumChans(numExistingChannels) 1362 1363 // Finally check that we make maximum channels if we are well within 1364 // our budget. 1365 channelBudget = dcrutil.AtomsPerCoin * 5 1366 numNewChannels = 2 1367 respondWithScores( 1368 t, testCtx, channelBudget, numExistingChannels, 1369 numNewChannels, nodeScores, 1370 ) 1371 1372 // We now expect the autopilot to open 2 channels, and since it has 1373 // more than enough balance within the budget, they should both be of 1374 // maximum size. 1375 expectedAllocation = testCtx.constraints.MaxChanSize() * 1376 dcrutil.Amount(numNewChannels) 1377 1378 checkChannelOpens(t, testCtx, expectedAllocation, numNewChannels) 1379 }