mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
5f22d97b51
Signed-off-by: fuyangpengqi <995764973@qq.com>
1351 lines
38 KiB
Go
1351 lines
38 KiB
Go
package autopilot
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type moreChansResp struct {
|
|
numMore uint32
|
|
amt btcutil.Amount
|
|
}
|
|
|
|
type moreChanArg struct {
|
|
chans []LocalChannel
|
|
balance btcutil.Amount
|
|
}
|
|
|
|
type mockConstraints struct {
|
|
moreChansResps chan moreChansResp
|
|
moreChanArgs chan moreChanArg
|
|
quit chan struct{}
|
|
}
|
|
|
|
func (m *mockConstraints) ChannelBudget(chans []LocalChannel,
|
|
balance btcutil.Amount) (btcutil.Amount, uint32) {
|
|
|
|
if m.moreChanArgs != nil {
|
|
moreChan := moreChanArg{
|
|
chans: chans,
|
|
balance: balance,
|
|
}
|
|
|
|
select {
|
|
case m.moreChanArgs <- moreChan:
|
|
case <-m.quit:
|
|
return 0, 0
|
|
}
|
|
}
|
|
|
|
select {
|
|
case resp := <-m.moreChansResps:
|
|
return resp.amt, resp.numMore
|
|
case <-m.quit:
|
|
return 0, 0
|
|
}
|
|
}
|
|
|
|
func (m *mockConstraints) MaxPendingOpens() uint16 {
|
|
return 10
|
|
}
|
|
|
|
func (m *mockConstraints) MinChanSize() btcutil.Amount {
|
|
return 1e7
|
|
}
|
|
func (m *mockConstraints) MaxChanSize() btcutil.Amount {
|
|
return 1e8
|
|
}
|
|
|
|
var _ AgentConstraints = (*mockConstraints)(nil)
|
|
|
|
type mockHeuristic struct {
|
|
nodeScoresResps chan map[NodeID]*NodeScore
|
|
nodeScoresArgs chan directiveArg
|
|
|
|
quit chan struct{}
|
|
}
|
|
|
|
type directiveArg struct {
|
|
graph ChannelGraph
|
|
amt btcutil.Amount
|
|
chans []LocalChannel
|
|
nodes map[NodeID]struct{}
|
|
}
|
|
|
|
func (m *mockHeuristic) Name() string {
|
|
return "mock"
|
|
}
|
|
|
|
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []LocalChannel,
|
|
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
|
|
map[NodeID]*NodeScore, error) {
|
|
|
|
if m.nodeScoresArgs != nil {
|
|
directive := directiveArg{
|
|
graph: g,
|
|
amt: chanSize,
|
|
chans: chans,
|
|
nodes: nodes,
|
|
}
|
|
|
|
select {
|
|
case m.nodeScoresArgs <- directive:
|
|
case <-m.quit:
|
|
return nil, errors.New("exiting")
|
|
}
|
|
}
|
|
|
|
select {
|
|
case resp := <-m.nodeScoresResps:
|
|
return resp, nil
|
|
case <-m.quit:
|
|
return nil, errors.New("exiting")
|
|
}
|
|
}
|
|
|
|
var _ AttachmentHeuristic = (*mockHeuristic)(nil)
|
|
|
|
type openChanIntent struct {
|
|
target *btcec.PublicKey
|
|
amt btcutil.Amount
|
|
private bool
|
|
}
|
|
|
|
type mockChanController struct {
|
|
openChanSignals chan openChanIntent
|
|
private bool
|
|
}
|
|
|
|
func (m *mockChanController) OpenChannel(target *btcec.PublicKey,
|
|
amt btcutil.Amount) error {
|
|
|
|
m.openChanSignals <- openChanIntent{
|
|
target: target,
|
|
amt: amt,
|
|
private: m.private,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockChanController) CloseChannel(chanPoint *wire.OutPoint) error {
|
|
return nil
|
|
}
|
|
|
|
var _ ChannelController = (*mockChanController)(nil)
|
|
|
|
type testContext struct {
|
|
constraints *mockConstraints
|
|
heuristic *mockHeuristic
|
|
chanController ChannelController
|
|
graph testGraph
|
|
agent *Agent
|
|
walletBalance btcutil.Amount
|
|
|
|
quit chan struct{}
|
|
sync.Mutex
|
|
}
|
|
|
|
func setup(t *testing.T, initialChans []LocalChannel) *testContext {
|
|
t.Helper()
|
|
|
|
// First, we'll create all the dependencies that we'll need in order to
|
|
// create the autopilot agent.
|
|
self, err := randKey()
|
|
require.NoError(t, err, "unable to generate key")
|
|
|
|
quit := make(chan struct{})
|
|
heuristic := &mockHeuristic{
|
|
nodeScoresArgs: make(chan directiveArg),
|
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
|
quit: quit,
|
|
}
|
|
constraints := &mockConstraints{
|
|
moreChansResps: make(chan moreChansResp),
|
|
moreChanArgs: make(chan moreChanArg),
|
|
quit: quit,
|
|
}
|
|
|
|
chanController := &mockChanController{
|
|
openChanSignals: make(chan openChanIntent, 10),
|
|
}
|
|
memGraph, _ := newMemChanGraph(t)
|
|
|
|
// We'll keep track of the funds available to the agent, to make sure
|
|
// it correctly uses this value when querying the ChannelBudget.
|
|
var availableFunds btcutil.Amount = 10 * btcutil.SatoshiPerBitcoin
|
|
|
|
ctx := &testContext{
|
|
constraints: constraints,
|
|
heuristic: heuristic,
|
|
chanController: chanController,
|
|
graph: memGraph,
|
|
walletBalance: availableFunds,
|
|
quit: quit,
|
|
}
|
|
|
|
// With the dependencies we created, we can now create the initial
|
|
// agent itself.
|
|
testCfg := Config{
|
|
Self: self,
|
|
Heuristic: heuristic,
|
|
ChanController: chanController,
|
|
WalletBalance: func() (btcutil.Amount, error) {
|
|
ctx.Lock()
|
|
defer ctx.Unlock()
|
|
return ctx.walletBalance, nil
|
|
},
|
|
ConnectToPeer: func(*btcec.PublicKey, []net.Addr) (bool, error) {
|
|
return false, nil
|
|
},
|
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
|
return nil
|
|
},
|
|
Graph: memGraph,
|
|
Constraints: constraints,
|
|
}
|
|
|
|
agent, err := New(testCfg, initialChans)
|
|
require.NoError(t, err, "unable to create agent")
|
|
ctx.agent = agent
|
|
|
|
// With the autopilot agent and all its dependencies we'll start the
|
|
// primary controller goroutine.
|
|
if err := agent.Start(); err != nil {
|
|
t.Fatalf("unable to start agent: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
// We must close quit before agent.Stop(), to make sure
|
|
// ChannelBudget won't block preventing the agent from exiting.
|
|
close(quit)
|
|
agent.Stop()
|
|
})
|
|
|
|
return ctx
|
|
}
|
|
|
|
// respondMoreChans consumes the moreChanArgs element and responds to the agent
|
|
// with the given moreChansResp.
|
|
func respondMoreChans(t *testing.T, testCtx *testContext, resp moreChansResp) {
|
|
t.Helper()
|
|
|
|
// The agent should now query the heuristic.
|
|
select {
|
|
case <-testCtx.constraints.moreChanArgs:
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// We'll send the response.
|
|
select {
|
|
case testCtx.constraints.moreChansResps <- resp:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("response wasn't sent in time")
|
|
}
|
|
}
|
|
|
|
// respondNodeScores consumes the nodeScoresArgs element and responds to the
|
|
// agent with the given node scores.
|
|
func respondNodeScores(t *testing.T, testCtx *testContext,
|
|
resp map[NodeID]*NodeScore) {
|
|
t.Helper()
|
|
|
|
// Send over an empty list of attachment directives, which should cause
|
|
// the agent to return to waiting on a new signal.
|
|
select {
|
|
case <-testCtx.heuristic.nodeScoresArgs:
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("node scores weren't queried in time")
|
|
}
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- resp:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("node scores were not sent in time")
|
|
}
|
|
}
|
|
|
|
// TestAgentChannelOpenSignal tests that upon receipt of a chanOpenUpdate, then
|
|
// agent modifies its local state accordingly, and reconsults the heuristic.
|
|
func TestAgentChannelOpenSignal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// We'll send an initial "no" response to advance the agent past its
|
|
// initial check.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// Next we'll signal a new channel being opened by the backing LN node,
|
|
// with a capacity of 1 BTC.
|
|
newChan := LocalChannel{
|
|
ChanID: randChanID(),
|
|
Balance: btcutil.SatoshiPerBitcoin,
|
|
}
|
|
testCtx.agent.OnChannelOpen(newChan)
|
|
|
|
// The agent should now query the heuristic in order to determine its
|
|
// next action as it local state has now been modified.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// At this point, the local state of the agent should
|
|
// have also been updated to reflect that the LN node
|
|
// now has an additional channel with one BTC.
|
|
if _, ok := testCtx.agent.chanState[newChan.ChanID]; !ok {
|
|
t.Fatalf("internal channel state wasn't updated")
|
|
}
|
|
|
|
// There shouldn't be a call to the Select method as we've returned
|
|
// "false" for NeedMoreChans above.
|
|
select {
|
|
|
|
// If this send success, then Select was erroneously called and the
|
|
// test should be failed.
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
|
t.Fatalf("Select was called but shouldn't have been")
|
|
|
|
// This is the correct path as Select should've be called.
|
|
default:
|
|
}
|
|
}
|
|
|
|
// TestAgentHeuristicUpdateSignal tests that upon notification about a
|
|
// heuristic update, the agent reconsults the heuristic.
|
|
func TestAgentHeuristicUpdateSignal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
pub, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to generate key")
|
|
|
|
// We'll send an initial "no" response to advance the agent past its
|
|
// initial check.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// Next we'll signal that one of the heuristics have been updated.
|
|
testCtx.agent.OnHeuristicUpdate(testCtx.heuristic)
|
|
|
|
// The update should trigger the agent to ask for a channel budget.so
|
|
// we'll respond that there is a budget for opening 1 more channel.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: 1 * btcutil.SatoshiPerBitcoin,
|
|
},
|
|
)
|
|
|
|
// At this point, the agent should now be querying the heuristic for
|
|
// scores. We'll respond.
|
|
nodeID := NewNodeID(pub)
|
|
scores := map[NodeID]*NodeScore{
|
|
nodeID: {
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
},
|
|
}
|
|
respondNodeScores(t, testCtx, scores)
|
|
|
|
// Finally, this should result in the agent opening a channel.
|
|
chanController := testCtx.chanController.(*mockChanController)
|
|
select {
|
|
case <-chanController.openChanSignals:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("channel not opened in time")
|
|
}
|
|
}
|
|
|
|
// A mockFailingChanController always fails to open a channel.
|
|
type mockFailingChanController struct {
|
|
}
|
|
|
|
func (m *mockFailingChanController) OpenChannel(target *btcec.PublicKey,
|
|
amt btcutil.Amount) error {
|
|
return errors.New("failure")
|
|
}
|
|
|
|
func (m *mockFailingChanController) CloseChannel(chanPoint *wire.OutPoint) error {
|
|
return nil
|
|
}
|
|
|
|
var _ ChannelController = (*mockFailingChanController)(nil)
|
|
|
|
// TestAgentChannelFailureSignal tests that if an autopilot channel fails to
|
|
// open, the agent is signalled to make a new decision.
|
|
func TestAgentChannelFailureSignal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
testCtx.chanController = &mockFailingChanController{}
|
|
|
|
node, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to add node")
|
|
|
|
// First ensure the agent will attempt to open a new channel. Return
|
|
// that we need more channels, and have 5BTC to use.
|
|
respondMoreChans(t, testCtx, moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin})
|
|
|
|
// At this point, the agent should now be querying the heuristic to
|
|
// request attachment directives, return a fake so the agent will
|
|
// attempt to open a channel.
|
|
var fakeDirective = &NodeScore{
|
|
NodeID: NewNodeID(node),
|
|
Score: 0.5,
|
|
}
|
|
|
|
respondNodeScores(
|
|
t, testCtx, map[NodeID]*NodeScore{
|
|
NewNodeID(node): fakeDirective,
|
|
},
|
|
)
|
|
|
|
// At this point the agent will attempt to create a channel and fail.
|
|
|
|
// Now ensure that the controller loop is re-executed.
|
|
respondMoreChans(t, testCtx, moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin})
|
|
respondNodeScores(t, testCtx, map[NodeID]*NodeScore{})
|
|
}
|
|
|
|
// TestAgentChannelCloseSignal ensures that once the agent receives an outside
|
|
// signal of a channel belonging to the backing LN node being closed, then it
|
|
// will query the heuristic to make its next decision.
|
|
func TestAgentChannelCloseSignal(t *testing.T) {
|
|
t.Parallel()
|
|
// We'll start the agent with two channels already being active.
|
|
initialChans := []LocalChannel{
|
|
{
|
|
ChanID: randChanID(),
|
|
Balance: btcutil.SatoshiPerBitcoin,
|
|
},
|
|
{
|
|
ChanID: randChanID(),
|
|
Balance: btcutil.SatoshiPerBitcoin * 2,
|
|
},
|
|
}
|
|
|
|
testCtx := setup(t, initialChans)
|
|
|
|
// We'll send an initial "no" response to advance the agent past its
|
|
// initial check.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// Next, we'll close both channels which should force the agent to
|
|
// re-query the heuristic.
|
|
testCtx.agent.OnChannelClose(initialChans[0].ChanID, initialChans[1].ChanID)
|
|
|
|
// The agent should now query the heuristic in order to determine its
|
|
// next action as it local state has now been modified.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// At this point, the local state of the agent should
|
|
// have also been updated to reflect that the LN node
|
|
// has no existing open channels.
|
|
if len(testCtx.agent.chanState) != 0 {
|
|
t.Fatalf("internal channel state wasn't updated")
|
|
}
|
|
|
|
// There shouldn't be a call to the Select method as we've returned
|
|
// "false" for NeedMoreChans above.
|
|
select {
|
|
|
|
// If this send success, then Select was erroneously called and the
|
|
// test should be failed.
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
|
t.Fatalf("Select was called but shouldn't have been")
|
|
|
|
// This is the correct path as Select should've be called.
|
|
default:
|
|
}
|
|
}
|
|
|
|
// TestAgentBalanceUpdate ensures that once the agent receives an
|
|
// outside signal concerning a balance update, then it will re-query the
|
|
// heuristic to determine its next action.
|
|
func TestAgentBalanceUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// We'll send an initial "no" response to advance the agent past its
|
|
// initial check.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// Next we'll send a new balance update signal to the agent, adding 5
|
|
// BTC to the amount of available funds.
|
|
testCtx.Lock()
|
|
testCtx.walletBalance += btcutil.SatoshiPerBitcoin * 5
|
|
testCtx.Unlock()
|
|
|
|
testCtx.agent.OnBalanceChange()
|
|
|
|
// The agent should now query the heuristic in order to determine its
|
|
// next action as it local state has now been modified.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// At this point, the local state of the agent should
|
|
// have also been updated to reflect that the LN node
|
|
// now has an additional 5BTC available.
|
|
if testCtx.agent.totalBalance != testCtx.walletBalance {
|
|
t.Fatalf("expected %v wallet balance "+
|
|
"instead have %v", testCtx.agent.totalBalance,
|
|
testCtx.walletBalance)
|
|
}
|
|
|
|
// There shouldn't be a call to the Select method as we've returned
|
|
// "false" for NeedMoreChans above.
|
|
select {
|
|
|
|
// If this send success, then Select was erroneously called and the
|
|
// test should be failed.
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
|
t.Fatalf("Select was called but shouldn't have been")
|
|
|
|
// This is the correct path as Select should've be called.
|
|
default:
|
|
}
|
|
}
|
|
|
|
// TestAgentImmediateAttach tests that if an autopilot agent is created, and it
|
|
// has enough funds available to create channels, then it does so immediately.
|
|
func TestAgentImmediateAttach(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
const numChans = 5
|
|
|
|
// We'll generate 5 mock directives so it can progress within its loop.
|
|
directives := make(map[NodeID]*NodeScore)
|
|
nodeKeys := make(map[NodeID]struct{})
|
|
for i := 0; i < numChans; i++ {
|
|
pub, err := testCtx.graph.addRandNode()
|
|
if err != nil {
|
|
t.Fatalf("unable to generate key: %v", err)
|
|
}
|
|
nodeID := NewNodeID(pub)
|
|
directives[nodeID] = &NodeScore{
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
}
|
|
nodeKeys[nodeID] = struct{}{}
|
|
}
|
|
// The very first thing the agent should do is query the NeedMoreChans
|
|
// method on the passed heuristic. So we'll provide it with a response
|
|
// that will kick off the main loop.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: numChans,
|
|
amt: 5 * btcutil.SatoshiPerBitcoin,
|
|
},
|
|
)
|
|
|
|
// At this point, the agent should now be querying the heuristic to
|
|
// requests attachment directives. With our fake directives created,
|
|
// we'll now send then to the agent as a return value for the Select
|
|
// function.
|
|
respondNodeScores(t, testCtx, directives)
|
|
|
|
// Finally, we should receive 5 calls to the OpenChannel method with
|
|
// the exact same parameters that we specified within the attachment
|
|
// directives.
|
|
chanController := testCtx.chanController.(*mockChanController)
|
|
for i := 0; i < numChans; i++ {
|
|
select {
|
|
case openChan := <-chanController.openChanSignals:
|
|
if openChan.amt != btcutil.SatoshiPerBitcoin {
|
|
t.Fatalf("invalid chan amt: expected %v, got %v",
|
|
btcutil.SatoshiPerBitcoin, openChan.amt)
|
|
}
|
|
nodeID := NewNodeID(openChan.target)
|
|
_, ok := nodeKeys[nodeID]
|
|
if !ok {
|
|
t.Fatalf("unexpected key: %v, not found",
|
|
nodeID)
|
|
}
|
|
delete(nodeKeys, nodeID)
|
|
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("channel not opened in time")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAgentPrivateChannels ensure that only requests for private channels are
|
|
// sent if set.
|
|
func TestAgentPrivateChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// The chanController should be initialized such that all of its open
|
|
// channel requests are for private channels.
|
|
testCtx.chanController.(*mockChanController).private = true
|
|
|
|
const numChans = 5
|
|
|
|
// We'll generate 5 mock directives so the pubkeys will be found in the
|
|
// agent's graph, and it can progress within its loop.
|
|
directives := make(map[NodeID]*NodeScore)
|
|
for i := 0; i < numChans; i++ {
|
|
pub, err := testCtx.graph.addRandNode()
|
|
if err != nil {
|
|
t.Fatalf("unable to generate key: %v", err)
|
|
}
|
|
directives[NewNodeID(pub)] = &NodeScore{
|
|
NodeID: NewNodeID(pub),
|
|
Score: 0.5,
|
|
}
|
|
}
|
|
|
|
// The very first thing the agent should do is query the NeedMoreChans
|
|
// method on the passed heuristic. So we'll provide it with a response
|
|
// that will kick off the main loop. We'll send over a response
|
|
// indicating that it should establish more channels, and give it a
|
|
// budget of 5 BTC to do so.
|
|
resp := moreChansResp{
|
|
numMore: numChans,
|
|
amt: 5 * btcutil.SatoshiPerBitcoin,
|
|
}
|
|
respondMoreChans(t, testCtx, resp)
|
|
|
|
// At this point, the agent should now be querying the heuristic to
|
|
// requests attachment directives. With our fake directives created,
|
|
// we'll now send then to the agent as a return value for the Select
|
|
// function.
|
|
respondNodeScores(t, testCtx, directives)
|
|
|
|
// Finally, we should receive 5 calls to the OpenChannel method, each
|
|
// specifying that it's for a private channel.
|
|
chanController := testCtx.chanController.(*mockChanController)
|
|
for i := 0; i < numChans; i++ {
|
|
select {
|
|
case openChan := <-chanController.openChanSignals:
|
|
if !openChan.private {
|
|
t.Fatal("expected open channel request to be private")
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("channel not opened in time")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAgentPendingChannelState ensures that the agent properly factors in its
|
|
// pending channel state when making decisions w.r.t if it needs more channels
|
|
// or not, and if so, who is eligible to open new channels to.
|
|
func TestAgentPendingChannelState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// We'll only return a single directive for a pre-chosen node.
|
|
nodeKey, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to generate key")
|
|
nodeID := NewNodeID(nodeKey)
|
|
nodeDirective := &NodeScore{
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
}
|
|
|
|
// Once again, we'll start by telling the agent as part of its first
|
|
// query, that it needs more channels and has 3 BTC available for
|
|
// attachment. We'll send over a response indicating that it should
|
|
// establish more channels, and give it a budget of 1 BTC to do so.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: btcutil.SatoshiPerBitcoin,
|
|
},
|
|
)
|
|
|
|
respondNodeScores(t, testCtx,
|
|
map[NodeID]*NodeScore{
|
|
nodeID: nodeDirective,
|
|
},
|
|
)
|
|
|
|
// A request to open the channel should've also been sent.
|
|
chanController := testCtx.chanController.(*mockChanController)
|
|
select {
|
|
case openChan := <-chanController.openChanSignals:
|
|
chanAmt := testCtx.constraints.MaxChanSize()
|
|
if openChan.amt != chanAmt {
|
|
t.Fatalf("invalid chan amt: expected %v, got %v",
|
|
chanAmt, openChan.amt)
|
|
}
|
|
if !openChan.target.IsEqual(nodeKey) {
|
|
t.Fatalf("unexpected key: expected %x, got %x",
|
|
nodeKey.SerializeCompressed(),
|
|
openChan.target.SerializeCompressed())
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("channel wasn't opened in time")
|
|
}
|
|
|
|
// Now, in order to test that the pending state was properly updated,
|
|
// we'll trigger a balance update in order to trigger a query to the
|
|
// heuristic.
|
|
testCtx.Lock()
|
|
testCtx.walletBalance += 0.4 * btcutil.SatoshiPerBitcoin
|
|
testCtx.Unlock()
|
|
|
|
testCtx.agent.OnBalanceChange()
|
|
|
|
// The heuristic should be queried, and the argument for the set of
|
|
// channels passed in should include the pending channels that
|
|
// should've been created above.
|
|
select {
|
|
// The request that we get should include a pending channel for the
|
|
// one that we just created, otherwise the agent isn't properly
|
|
// updating its internal state.
|
|
case req := <-testCtx.constraints.moreChanArgs:
|
|
chanAmt := testCtx.constraints.MaxChanSize()
|
|
if len(req.chans) != 1 {
|
|
t.Fatalf("should include pending chan in current "+
|
|
"state, instead have %v chans", len(req.chans))
|
|
}
|
|
if req.chans[0].Balance != chanAmt {
|
|
t.Fatalf("wrong chan balance: expected %v, got %v",
|
|
req.chans[0].Balance, chanAmt)
|
|
}
|
|
if req.chans[0].Node != nodeID {
|
|
t.Fatalf("wrong node ID: expected %x, got %x",
|
|
nodeID, req.chans[0].Node[:])
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("need more chans wasn't queried in time")
|
|
}
|
|
|
|
// We'll send across a response indicating that it *does* need more
|
|
// channels.
|
|
select {
|
|
case testCtx.constraints.moreChansResps <- moreChansResp{1, btcutil.SatoshiPerBitcoin}:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("need more chans wasn't queried in time")
|
|
}
|
|
|
|
// The response above should prompt the agent to make a query to the
|
|
// Select method. The arguments passed should reflect the fact that the
|
|
// node we have a pending channel to, should be ignored.
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
if len(req.chans) == 0 {
|
|
t.Fatalf("expected to skip %v nodes, instead "+
|
|
"skipping %v", 1, len(req.chans))
|
|
}
|
|
if req.chans[0].Node != nodeID {
|
|
t.Fatalf("pending node not included in skip arguments")
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
}
|
|
|
|
// TestAgentPendingOpenChannel ensures that the agent queries its heuristic once
|
|
// it detects a channel is pending open. This allows the agent to use its own
|
|
// change outputs that have yet to confirm for funding transactions.
|
|
func TestAgentPendingOpenChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// We'll send an initial "no" response to advance the agent past its
|
|
// initial check.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// Next, we'll signal that a new channel has been opened, but it is
|
|
// still pending.
|
|
testCtx.agent.OnChannelPendingOpen()
|
|
|
|
// The agent should now query the heuristic in order to determine its
|
|
// next action as its local state has now been modified.
|
|
respondMoreChans(t, testCtx, moreChansResp{0, 0})
|
|
|
|
// There shouldn't be a call to the Select method as we've returned
|
|
// "false" for NeedMoreChans above.
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
|
t.Fatalf("Select was called but shouldn't have been")
|
|
default:
|
|
}
|
|
}
|
|
|
|
// TestAgentOnNodeUpdates tests that the agent will wake up in response to the
|
|
// OnNodeUpdates signal. This is useful in ensuring that autopilot is always
|
|
// pulling in the latest graph updates into its decision making. It also
|
|
// prevents the agent from stalling after an initial attempt that finds no nodes
|
|
// in the graph.
|
|
func TestAgentOnNodeUpdates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
// We'll send an initial "yes" response to advance the agent past its
|
|
// initial check. This will cause it to try to get directives from an
|
|
// empty graph.
|
|
respondMoreChans(
|
|
t, testCtx,
|
|
moreChansResp{
|
|
numMore: 2,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// Send over an empty list of attachment directives, which should cause
|
|
// the agent to return to waiting on a new signal.
|
|
respondNodeScores(t, testCtx, map[NodeID]*NodeScore{})
|
|
|
|
// Simulate more nodes being added to the graph by informing the agent
|
|
// that we have node updates.
|
|
testCtx.agent.OnNodeUpdates()
|
|
|
|
// In response, the agent should wake up and see if it needs more
|
|
// channels. Since we haven't done anything, we will send the same
|
|
// response as before since we are still trying to open channels.
|
|
respondMoreChans(
|
|
t, testCtx,
|
|
moreChansResp{
|
|
numMore: 2,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// Again the agent should pull in the next set of attachment directives.
|
|
// It's not important that this list is also empty, so long as the node
|
|
// updates signal is causing the agent to make this attempt.
|
|
respondNodeScores(t, testCtx, map[NodeID]*NodeScore{})
|
|
}
|
|
|
|
// TestAgentSkipPendingConns asserts that the agent will not try to make
|
|
// duplicate connection requests to the same node, even if the attachment
|
|
// heuristic instructs the agent to do so. It also asserts that the agent
|
|
// stops tracking the pending connection once it finishes. Note that in
|
|
// practice, a failed connection would be inserted into the skip map passed to
|
|
// the attachment heuristic, though this does not assert that case.
|
|
func TestAgentSkipPendingConns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
connect := make(chan chan error)
|
|
testCtx.agent.cfg.ConnectToPeer = func(*btcec.PublicKey, []net.Addr) (bool, error) {
|
|
errChan := make(chan error)
|
|
|
|
select {
|
|
case connect <- errChan:
|
|
case <-testCtx.quit:
|
|
return false, errors.New("quit")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
return false, err
|
|
case <-testCtx.quit:
|
|
return false, errors.New("quit")
|
|
}
|
|
}
|
|
|
|
// We'll only return a single directive for a pre-chosen node.
|
|
nodeKey, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to generate key")
|
|
nodeID := NewNodeID(nodeKey)
|
|
nodeDirective := &NodeScore{
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
}
|
|
|
|
// We'll also add a second node to the graph, to keep the first one
|
|
// company.
|
|
nodeKey2, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to generate key")
|
|
nodeID2 := NewNodeID(nodeKey2)
|
|
|
|
// We'll send an initial "yes" response to advance the agent past its
|
|
// initial check. This will cause it to try to get directives from the
|
|
// graph.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// Both nodes should be part of the arguments.
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
if len(req.nodes) != 2 {
|
|
t.Fatalf("expected %v nodes, instead "+
|
|
"had %v", 2, len(req.nodes))
|
|
}
|
|
if _, ok := req.nodes[nodeID]; !ok {
|
|
t.Fatalf("node not included in arguments")
|
|
}
|
|
if _, ok := req.nodes[nodeID2]; !ok {
|
|
t.Fatalf("node not included in arguments")
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
|
|
// Respond with a scored directive. We skip node2 for now, implicitly
|
|
// giving it a zero-score.
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
|
NewNodeID(nodeKey): nodeDirective,
|
|
}:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// The agent should attempt connection to the node.
|
|
var errChan chan error
|
|
select {
|
|
case errChan = <-connect:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("agent did not attempt connection")
|
|
}
|
|
|
|
// Signal the agent to go again, now that we've tried to connect.
|
|
testCtx.agent.OnNodeUpdates()
|
|
|
|
// The heuristic again informs the agent that we need more channels.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// Since the node now has a pending connection, it should be skipped
|
|
// and not part of the nodes attempting to be scored.
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
if len(req.nodes) != 1 {
|
|
t.Fatalf("expected %v nodes, instead "+
|
|
"had %v", 1, len(req.nodes))
|
|
}
|
|
if _, ok := req.nodes[nodeID2]; !ok {
|
|
t.Fatalf("node not included in arguments")
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
|
|
// Respond with an emtpty score set.
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// The agent should not attempt any connection, since no nodes were
|
|
// scored.
|
|
select {
|
|
case <-connect:
|
|
t.Fatalf("agent should not have attempted connection")
|
|
case <-time.After(time.Second * 3):
|
|
}
|
|
|
|
// Now, timeout the original request, which should still be waiting for
|
|
// a response.
|
|
select {
|
|
case errChan <- fmt.Errorf("connection timeout"):
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("agent did not receive connection timeout")
|
|
}
|
|
|
|
// The agent will now retry since the last connection attempt failed.
|
|
// The heuristic again informs the agent that we need more channels.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// The node should now be marked as "failed", which should make it
|
|
// being skipped during scoring. Again check that it won't be among the
|
|
// score request.
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
if len(req.nodes) != 1 {
|
|
t.Fatalf("expected %v nodes, instead "+
|
|
"had %v", 1, len(req.nodes))
|
|
}
|
|
if _, ok := req.nodes[nodeID2]; !ok {
|
|
t.Fatalf("node not included in arguments")
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
|
|
// Send a directive for the second node.
|
|
nodeDirective2 := &NodeScore{
|
|
NodeID: nodeID2,
|
|
Score: 0.5,
|
|
}
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
|
nodeID2: nodeDirective2,
|
|
}:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// This time, the agent should try the connection to the second node.
|
|
select {
|
|
case <-connect:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("agent should have attempted connection")
|
|
}
|
|
}
|
|
|
|
// TestAgentQuitWhenPendingConns tests that we are able to stop the autopilot
|
|
// agent even though there are pending connections to nodes.
|
|
func TestAgentQuitWhenPendingConns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
connect := make(chan chan error)
|
|
|
|
testCtx.agent.cfg.ConnectToPeer = func(*btcec.PublicKey, []net.Addr) (bool, error) {
|
|
errChan := make(chan error)
|
|
|
|
select {
|
|
case connect <- errChan:
|
|
case <-testCtx.quit:
|
|
return false, errors.New("quit")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
return false, err
|
|
case <-testCtx.quit:
|
|
return false, errors.New("quit")
|
|
}
|
|
}
|
|
|
|
// We'll only return a single directive for a pre-chosen node.
|
|
nodeKey, err := testCtx.graph.addRandNode()
|
|
require.NoError(t, err, "unable to generate key")
|
|
nodeID := NewNodeID(nodeKey)
|
|
nodeDirective := &NodeScore{
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
}
|
|
|
|
// We'll send an initial "yes" response to advance the agent past its
|
|
// initial check. This will cause it to try to get directives from the
|
|
// graph.
|
|
respondMoreChans(t, testCtx,
|
|
moreChansResp{
|
|
numMore: 1,
|
|
amt: testCtx.walletBalance,
|
|
},
|
|
)
|
|
|
|
// Check the args.
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
if len(req.nodes) != 1 {
|
|
t.Fatalf("expected %v nodes, instead "+
|
|
"had %v", 1, len(req.nodes))
|
|
}
|
|
if _, ok := req.nodes[nodeID]; !ok {
|
|
t.Fatalf("node not included in arguments")
|
|
}
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
|
|
// Respond with a scored directive.
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
|
NewNodeID(nodeKey): nodeDirective,
|
|
}:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// The agent should attempt connection to the node.
|
|
select {
|
|
case <-connect:
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatalf("agent did not attempt connection")
|
|
}
|
|
|
|
// Make sure that we are able to stop the agent, even though there is a
|
|
// pending connection.
|
|
stopped := make(chan error)
|
|
go func() {
|
|
stopped <- testCtx.agent.Stop()
|
|
}()
|
|
|
|
select {
|
|
case err := <-stopped:
|
|
if err != nil {
|
|
t.Fatalf("error stopping agent: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("unable to stop agent")
|
|
}
|
|
}
|
|
|
|
// respondWithScores checks that the moreChansRequest contains what we expect,
|
|
// and responds with the given node scores.
|
|
func respondWithScores(t *testing.T, testCtx *testContext,
|
|
channelBudget btcutil.Amount, existingChans, newChans int,
|
|
nodeScores map[NodeID]*NodeScore) {
|
|
|
|
t.Helper()
|
|
|
|
select {
|
|
case testCtx.constraints.moreChansResps <- moreChansResp{
|
|
numMore: uint32(newChans),
|
|
amt: channelBudget,
|
|
}:
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// The agent should query for scores using the constraints returned
|
|
// above. We expect the agent to use the maximum channel size when
|
|
// opening channels.
|
|
chanSize := testCtx.constraints.MaxChanSize()
|
|
|
|
select {
|
|
case req := <-testCtx.heuristic.nodeScoresArgs:
|
|
// All nodes in the graph should be potential channel
|
|
// candidates.
|
|
if len(req.nodes) != len(nodeScores) {
|
|
t.Fatalf("expected %v nodes, instead had %v",
|
|
len(nodeScores), len(req.nodes))
|
|
}
|
|
|
|
// 'existingChans' is already open.
|
|
if len(req.chans) != existingChans {
|
|
t.Fatalf("expected %d existing channel, got %v",
|
|
existingChans, len(req.chans))
|
|
}
|
|
if req.amt != chanSize {
|
|
t.Fatalf("expected channel size of %v, got %v",
|
|
chanSize, req.amt)
|
|
}
|
|
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("select wasn't queried in time")
|
|
}
|
|
|
|
// Respond with the given scores.
|
|
select {
|
|
case testCtx.heuristic.nodeScoresResps <- nodeScores:
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("NodeScores wasn't queried in time")
|
|
}
|
|
}
|
|
|
|
// checkChannelOpens asserts that the channel controller attempts open the
|
|
// number of channels we expect, and with the exact total allocation.
|
|
func checkChannelOpens(t *testing.T, testCtx *testContext,
|
|
allocation btcutil.Amount, numChans int) []NodeID {
|
|
|
|
var nodes []NodeID
|
|
|
|
// The agent should attempt to open channels, totaling what we expect.
|
|
var totalAllocation btcutil.Amount
|
|
chanController := testCtx.chanController.(*mockChanController)
|
|
for i := 0; i < numChans; i++ {
|
|
select {
|
|
case openChan := <-chanController.openChanSignals:
|
|
totalAllocation += openChan.amt
|
|
|
|
testCtx.Lock()
|
|
testCtx.walletBalance -= openChan.amt
|
|
testCtx.Unlock()
|
|
|
|
nodes = append(nodes, NewNodeID(openChan.target))
|
|
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("channel not opened in time")
|
|
}
|
|
}
|
|
|
|
if totalAllocation != allocation {
|
|
t.Fatalf("expected agent to open channels totalling %v, "+
|
|
"instead was %v", allocation, totalAllocation)
|
|
}
|
|
|
|
// Finally, make sure the agent won't try opening more channels.
|
|
select {
|
|
case <-chanController.openChanSignals:
|
|
t.Fatalf("agent unexpectedly opened channel")
|
|
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
// TestAgentChannelSizeAllocation tests that the autopilot agent opens channel
|
|
// of size that stays within the channel budget and size restrictions.
|
|
func TestAgentChannelSizeAllocation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Total number of nodes in our mock graph.
|
|
const numNodes = 20
|
|
|
|
testCtx := setup(t, nil)
|
|
|
|
nodeScores := make(map[NodeID]*NodeScore)
|
|
for i := 0; i < numNodes; i++ {
|
|
nodeKey, err := testCtx.graph.addRandNode()
|
|
if err != nil {
|
|
t.Fatalf("unable to generate key: %v", err)
|
|
}
|
|
nodeID := NewNodeID(nodeKey)
|
|
nodeScores[nodeID] = &NodeScore{
|
|
NodeID: nodeID,
|
|
Score: 0.5,
|
|
}
|
|
}
|
|
|
|
// The agent should now query the heuristic in order to determine its
|
|
// next action as it local state has now been modified.
|
|
select {
|
|
case arg := <-testCtx.constraints.moreChanArgs:
|
|
if len(arg.chans) != 0 {
|
|
t.Fatalf("expected agent to have no channels open, "+
|
|
"had %v", len(arg.chans))
|
|
}
|
|
if arg.balance != testCtx.walletBalance {
|
|
t.Fatalf("expected agent to have %v balance, had %v",
|
|
testCtx.walletBalance, arg.balance)
|
|
}
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("heuristic wasn't queried in time")
|
|
}
|
|
|
|
// We'll return a response telling the agent to open 5 channels, with a
|
|
// total channel budget of 5 BTC.
|
|
var channelBudget btcutil.Amount = 5 * btcutil.SatoshiPerBitcoin
|
|
numExistingChannels := 0
|
|
numNewChannels := 5
|
|
respondWithScores(
|
|
t, testCtx, channelBudget, numExistingChannels,
|
|
numNewChannels, nodeScores,
|
|
)
|
|
|
|
// We expect the autopilot to have allocated all funds towards
|
|
// channels.
|
|
expectedAllocation := testCtx.constraints.MaxChanSize() * btcutil.Amount(numNewChannels)
|
|
nodes := checkChannelOpens(
|
|
t, testCtx, expectedAllocation, numNewChannels,
|
|
)
|
|
|
|
// Delete the selected nodes from our set of scores, to avoid scoring
|
|
// nodes we already have channels to.
|
|
for _, node := range nodes {
|
|
delete(nodeScores, node)
|
|
}
|
|
|
|
// TODO(halseth): this loop is a hack to ensure all the attempted
|
|
// channels are accounted for. This happens because the agent will
|
|
// query the ChannelBudget before all the pending channels are added to
|
|
// the map. Fix by adding them to the pending channels map before
|
|
// executing directives in goroutines?
|
|
waitForNumChans := func(expChans int) {
|
|
t.Helper()
|
|
|
|
var (
|
|
numChans int
|
|
balance btcutil.Amount
|
|
)
|
|
|
|
Loop:
|
|
for {
|
|
select {
|
|
case arg := <-testCtx.constraints.moreChanArgs:
|
|
numChans = len(arg.chans)
|
|
balance = arg.balance
|
|
|
|
// As long as the number of existing channels
|
|
// is below our expected number of channels,
|
|
// and the balance is not what we expect, we'll
|
|
// keep responding with "no more channels".
|
|
if numChans == expChans &&
|
|
balance == testCtx.walletBalance {
|
|
break Loop
|
|
}
|
|
|
|
select {
|
|
case testCtx.constraints.moreChansResps <- moreChansResp{0, 0}:
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("heuristic wasn't queried " +
|
|
"in time")
|
|
}
|
|
|
|
case <-time.After(time.Second * 3):
|
|
t.Fatalf("did not receive expected "+
|
|
"channels(%d) and balance(%d), "+
|
|
"instead got %d and %d", expChans,
|
|
testCtx.walletBalance, numChans,
|
|
balance)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for the agent to have 5 channels.
|
|
waitForNumChans(numNewChannels)
|
|
|
|
// Set the channel budget to 1.5 BTC.
|
|
channelBudget = btcutil.SatoshiPerBitcoin * 3 / 2
|
|
|
|
// We'll return a response telling the agent to open 3 channels, with a
|
|
// total channel budget of 1.5 BTC.
|
|
numExistingChannels = 5
|
|
numNewChannels = 3
|
|
respondWithScores(
|
|
t, testCtx, channelBudget, numExistingChannels,
|
|
numNewChannels, nodeScores,
|
|
)
|
|
|
|
// To stay within the budget, we expect the autopilot to open 2
|
|
// channels.
|
|
expectedAllocation = channelBudget
|
|
nodes = checkChannelOpens(t, testCtx, expectedAllocation, 2)
|
|
numExistingChannels = 7
|
|
|
|
for _, node := range nodes {
|
|
delete(nodeScores, node)
|
|
}
|
|
|
|
waitForNumChans(numExistingChannels)
|
|
|
|
// Finally check that we make maximum channels if we are well within
|
|
// our budget.
|
|
channelBudget = btcutil.SatoshiPerBitcoin * 5
|
|
numNewChannels = 2
|
|
respondWithScores(
|
|
t, testCtx, channelBudget, numExistingChannels,
|
|
numNewChannels, nodeScores,
|
|
)
|
|
|
|
// We now expect the autopilot to open 2 channels, and since it has
|
|
// more than enough balance within the budget, they should both be of
|
|
// maximum size.
|
|
expectedAllocation = testCtx.constraints.MaxChanSize() *
|
|
btcutil.Amount(numNewChannels)
|
|
|
|
checkChannelOpens(t, testCtx, expectedAllocation, numNewChannels)
|
|
}
|