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  }