Merge pull request #2341 from halseth/autopilot-weighted-heuristics

[autopilot] Decouple agent constraints from heuristics
This commit is contained in:
Olaoluwa Osuntokun 2019-01-08 20:15:57 -08:00 committed by GitHub
commit a0c0e8edc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 754 additions and 785 deletions

@ -57,7 +57,7 @@ type Config struct {
// Constraints is the set of constraints the autopilot must adhere to
// when opening channels.
Constraints *HeuristicConstraints
Constraints AgentConstraints
// TODO(roasbeef): add additional signals from fee rates and revenue of
// currently opened channels
@ -478,12 +478,21 @@ func (a *Agent) controller() {
a.pendingMtx.Unlock()
// Now that we've updated our internal state, we'll consult our
// channel attachment heuristic to determine if we should open
// up any additional channels or modify existing channels.
availableFunds, numChans, needMore := a.cfg.Heuristic.NeedMoreChans(
// channel attachment heuristic to determine if we can open
// up any additional channels while staying within our
// constraints.
availableFunds, numChans := a.cfg.Constraints.ChannelBudget(
totalChans, a.totalBalance,
)
if !needMore {
switch {
case numChans == 0:
continue
// If the amount is too small, we don't want to attempt opening
// another channel.
case availableFunds == 0:
continue
case availableFunds < a.cfg.Constraints.MinChanSize():
continue
}
@ -516,6 +525,7 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
// want to skip.
selfPubBytes := a.cfg.Self.SerializeCompressed()
nodes := make(map[NodeID]struct{})
addresses := make(map[NodeID][]net.Addr)
if err := a.cfg.Graph.ForEachNode(func(node Node) error {
nID := NodeID(node.PubKey())
@ -526,6 +536,14 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
return nil
}
// If the node has no known addresses, we cannot connect to it,
// so we'll skip it.
addrs := node.Addrs()
if len(addrs) == 0 {
return nil
}
addresses[nID] = addrs
// Additionally, if this node is in the blacklist, then
// we'll skip it.
if _, ok := nodesToSkip[nID]; ok {
@ -538,10 +556,16 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
return fmt.Errorf("unable to get graph nodes: %v", err)
}
// As channel size we'll use the maximum channel size available.
chanSize := a.cfg.Constraints.MaxChanSize()
if availableFunds-chanSize < 0 {
chanSize = availableFunds
}
// Use the heuristic to calculate a score for each node in the
// graph.
scores, err := a.cfg.Heuristic.NodeScores(
a.cfg.Graph, totalChans, availableFunds, nodes,
a.cfg.Graph, totalChans, chanSize, nodes,
)
if err != nil {
return fmt.Errorf("unable to calculate node scores : %v", err)
@ -549,14 +573,32 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
log.Debugf("Got scores for %d nodes", len(scores))
// Now use the score to make a weighted choice which
// nodes to attempt to open channels to.
chanCandidates, err := chooseN(numChans, scores)
// Now use the score to make a weighted choice which nodes to attempt
// to open channels to.
scores, err = chooseN(numChans, scores)
if err != nil {
return fmt.Errorf("Unable to make weighted choice: %v",
err)
}
chanCandidates := make(map[NodeID]*AttachmentDirective)
for nID := range scores {
// Add addresses to the candidates.
addrs := addresses[nID]
// If the node has no known addresses, we cannot connect to it,
// so we'll skip it.
if len(addrs) == 0 {
continue
}
chanCandidates[nID] = &AttachmentDirective{
NodeID: nID,
ChanAmt: chanSize,
Addrs: addrs,
}
}
if len(chanCandidates) == 0 {
log.Infof("No eligible candidates to connect to")
return nil
@ -573,11 +615,11 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
// available to future heuristic selections.
a.pendingMtx.Lock()
defer a.pendingMtx.Unlock()
if uint16(len(a.pendingOpens)) >= a.cfg.Constraints.MaxPendingOpens {
if uint16(len(a.pendingOpens)) >= a.cfg.Constraints.MaxPendingOpens() {
log.Debugf("Reached cap of %v pending "+
"channel opens, will retry "+
"after success/failure",
a.cfg.Constraints.MaxPendingOpens)
a.cfg.Constraints.MaxPendingOpens())
return nil
}
@ -642,7 +684,7 @@ func (a *Agent) executeDirective(directive AttachmentDirective) {
// first.
a.pendingMtx.Lock()
if uint16(len(a.pendingOpens)) >=
a.cfg.Constraints.MaxPendingOpens {
a.cfg.Constraints.MaxPendingOpens() {
// Since we've reached our max number of pending opens, we'll
// disconnect this peer and exit. However, if we were
// previously connected to them, then we'll make sure to

@ -0,0 +1,151 @@
package autopilot
import (
"github.com/btcsuite/btcutil"
)
// AgentConstraints is an interface the agent will query to determine what
// limits it will need to stay inside when opening channels.
type AgentConstraints interface {
// ChannelBudget should, given the passed parameters, return whether
// more channels can be be opened while still staying withing the set
// constraints. If the constraints allow us to open more channels, then
// the first return value will represent the amount of additional funds
// available towards creating channels. The second return value is the
// exact *number* of additional channels available.
ChannelBudget(chans []Channel, balance btcutil.Amount) (
btcutil.Amount, uint32)
// MaxPendingOpens returns the maximum number of pending channel
// establishment goroutines that can be lingering. We cap this value in
// order to control the level of parallelism caused by the autopilot
// agent.
MaxPendingOpens() uint16
// MinChanSize returns the smallest channel that the autopilot agent
// should create.
MinChanSize() btcutil.Amount
// MaxChanSize returns largest channel that the autopilot agent should
// create.
MaxChanSize() btcutil.Amount
}
// agenConstraints is an implementation of the AgentConstraints interface that
// indicate the constraints the autopilot agent must adhere to when opening
// channels.
type agentConstraints struct {
// minChanSize is the smallest channel that the autopilot agent should
// create.
minChanSize btcutil.Amount
// maxChanSize the largest channel that the autopilot agent should
// create.
maxChanSize btcutil.Amount
// chanLimit the maximum number of channels that should be created.
chanLimit uint16
// allocation the percentage of total funds that should be committed to
// automatic channel establishment.
allocation float64
// maxPendingOpens is the maximum number of pending channel
// establishment goroutines that can be lingering. We cap this value in
// order to control the level of parallelism caused by the autopilot
// agent.
maxPendingOpens uint16
}
// A compile time assertion to ensure agentConstraints satisfies the
// AgentConstraints interface.
var _ AgentConstraints = (*agentConstraints)(nil)
// NewConstraints returns a new AgentConstraints with the given limits.
func NewConstraints(minChanSize, maxChanSize btcutil.Amount, chanLimit,
maxPendingOpens uint16, allocation float64) AgentConstraints {
return &agentConstraints{
minChanSize: minChanSize,
maxChanSize: maxChanSize,
chanLimit: chanLimit,
allocation: allocation,
maxPendingOpens: maxPendingOpens,
}
}
// ChannelBudget should, given the passed parameters, return whether more
// channels can be be opened while still staying withing the set constraints.
// If the constraints allow us to open more channels, then the first return
// value will represent the amount of additional funds available towards
// creating channels. The second return value is the exact *number* of
// additional channels available.
//
// Note: part of the AgentConstraints interface.
func (h *agentConstraints) ChannelBudget(channels []Channel,
funds btcutil.Amount) (btcutil.Amount, uint32) {
// If we're already over our maximum allowed number of channels, then
// we'll instruct the controller not to create any more channels.
if len(channels) >= int(h.chanLimit) {
return 0, 0
}
// The number of additional channels that should be opened is the
// difference between the channel limit, and the number of channels we
// already have open.
numAdditionalChans := uint32(h.chanLimit) - uint32(len(channels))
// First, we'll tally up the total amount of funds that are currently
// present within the set of active channels.
var totalChanAllocation btcutil.Amount
for _, channel := range channels {
totalChanAllocation += channel.Capacity
}
// With this value known, we'll now compute the total amount of fund
// allocated across regular utxo's and channel utxo's.
totalFunds := funds + totalChanAllocation
// Once the total amount has been computed, we then calculate the
// fraction of funds currently allocated to channels.
fundsFraction := float64(totalChanAllocation) / float64(totalFunds)
// If this fraction is below our threshold, then we'll return true, to
// indicate the controller should call Select to obtain a candidate set
// of channels to attempt to open.
needMore := fundsFraction < h.allocation
if !needMore {
return 0, 0
}
// Now that we know we need more funds, we'll compute the amount of
// additional funds we should allocate towards channels.
targetAllocation := btcutil.Amount(float64(totalFunds) * h.allocation)
fundsAvailable := targetAllocation - totalChanAllocation
return fundsAvailable, numAdditionalChans
}
// MaxPendingOpens returns the maximum number of pending channel establishment
// goroutines that can be lingering. We cap this value in order to control the
// level of parallelism caused by the autopilot agent.
//
// Note: part of the AgentConstraints interface.
func (h *agentConstraints) MaxPendingOpens() uint16 {
return h.maxPendingOpens
}
// MinChanSize returns the smallest channel that the autopilot agent should
// create.
//
// Note: part of the AgentConstraints interface.
func (h *agentConstraints) MinChanSize() btcutil.Amount {
return h.minChanSize
}
// MaxChanSize returns largest channel that the autopilot agent should create.
//
// Note: part of the AgentConstraints interface.
func (h *agentConstraints) MaxChanSize() btcutil.Amount {
return h.maxChanSize
}

@ -0,0 +1,166 @@
package autopilot
import (
"testing"
"time"
prand "math/rand"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
)
func TestConstraintsChannelBudget(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
randChanID := func() lnwire.ShortChannelID {
return lnwire.NewShortChanIDFromInt(uint64(prand.Int63()))
}
testCases := []struct {
channels []Channel
walletAmt btcutil.Amount
needMore bool
amtAvailable btcutil.Amount
numMore uint32
}{
// Many available funds, but already have too many active open
// channels.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 10),
false,
0,
0,
},
// Ratio of funds in channels and total funds meets the
// threshold.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 2),
false,
0,
0,
},
// Ratio of funds in channels and total funds is below the
// threshold. We have 10 BTC allocated amongst channels and
// funds, atm. We're targeting 50%, so 5 BTC should be
// allocated. Only 1 BTC is atm, so 4 BTC should be
// recommended. We should also request 2 more channels as the
// limit is 3.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 9),
true,
btcutil.Amount(btcutil.SatoshiPerBitcoin * 4),
2,
},
// Ratio of funds in channels and total funds is below the
// threshold. We have 14 BTC total amongst the wallet's
// balance, and our currently opened channels. Since we're
// targeting a 50% allocation, we should commit 7 BTC. The
// current channels commit 4 BTC, so we should expected 3 BTC
// to be committed. We should only request a single additional
// channel as the limit is 3.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin * 3),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 10),
true,
btcutil.Amount(btcutil.SatoshiPerBitcoin * 3),
1,
},
// Ratio of funds in channels and total funds is above the
// threshold.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin),
false,
0,
0,
},
}
for i, testCase := range testCases {
amtToAllocate, numMore := constraints.ChannelBudget(
testCase.channels, testCase.walletAmt,
)
if amtToAllocate != testCase.amtAvailable {
t.Fatalf("test #%v: expected %v, got %v",
i, testCase.amtAvailable, amtToAllocate)
}
if numMore != testCase.numMore {
t.Fatalf("test #%v: expected %v, got %v",
i, testCase.numMore, numMore)
}
}
}

@ -1,7 +1,6 @@
package autopilot
import (
"bytes"
"errors"
"fmt"
"net"
@ -15,9 +14,8 @@ import (
)
type moreChansResp struct {
needMore bool
numMore uint32
amt btcutil.Amount
numMore uint32
amt btcutil.Amount
}
type moreChanArg struct {
@ -25,18 +23,14 @@ type moreChanArg struct {
balance btcutil.Amount
}
type mockHeuristic struct {
type mockConstraints struct {
moreChansResps chan moreChansResp
moreChanArgs chan moreChanArg
nodeScoresResps chan map[NodeID]*AttachmentDirective
nodeScoresArgs chan directiveArg
quit chan struct{}
quit chan struct{}
}
func (m *mockHeuristic) NeedMoreChans(chans []Channel,
balance btcutil.Amount) (btcutil.Amount, uint32, bool) {
func (m *mockConstraints) ChannelBudget(chans []Channel,
balance btcutil.Amount) (btcutil.Amount, uint32) {
if m.moreChanArgs != nil {
moreChan := moreChanArg{
@ -47,18 +41,38 @@ func (m *mockHeuristic) NeedMoreChans(chans []Channel,
select {
case m.moreChanArgs <- moreChan:
case <-m.quit:
return 0, 0, false
return 0, 0
}
}
select {
case resp := <-m.moreChansResps:
return resp.amt, resp.numMore, resp.needMore
return resp.amt, resp.numMore
case <-m.quit:
return 0, 0, false
return 0, 0
}
}
func (m *mockConstraints) MaxPendingOpens() uint16 {
return 10
}
func (m *mockConstraints) MinChanSize() btcutil.Amount {
return 0
}
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
@ -68,7 +82,7 @@ type directiveArg struct {
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []Channel,
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*AttachmentDirective, error) {
map[NodeID]*NodeScore, error) {
if m.nodeScoresArgs != nil {
directive := directiveArg{
@ -146,10 +160,14 @@ func TestAgentChannelOpenSignal(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent, 10),
}
@ -170,10 +188,8 @@ func TestAgentChannelOpenSignal(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -200,7 +216,7 @@ func TestAgentChannelOpenSignal(t *testing.T) {
// We'll send an initial "no" response to advance the agent past its
// initial check.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- moreChansResp{0, 0}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
@ -216,7 +232,7 @@ func TestAgentChannelOpenSignal(t *testing.T) {
// The agent should now query the heuristic in order to determine its
// next action as it local state has now been modified.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- 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.
@ -234,7 +250,7 @@ func TestAgentChannelOpenSignal(t *testing.T) {
// If this send success, then Select was erroneously called and the
// test should be failed.
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case 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.
@ -279,12 +295,20 @@ func TestAgentChannelFailureSignal(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockFailingChanController{}
memGraph, _, _ := newMemChanGraph()
node, err := memGraph.addRandNode()
if err != nil {
t.Fatalf("unable to add node: %v", err)
}
// With the dependencies we created, we can now create the initial
// agent itself.
@ -295,16 +319,15 @@ func TestAgentChannelFailureSignal(t *testing.T) {
WalletBalance: func() (btcutil.Amount, error) {
return 0, nil
},
// TODO: move address check to agent.
ConnectToPeer: func(*btcec.PublicKey, []net.Addr) (bool, error) {
return false, nil
},
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
@ -332,7 +355,7 @@ func TestAgentChannelFailureSignal(t *testing.T) {
// First ensure the agent will attempt to open a new channel. Return
// that we need more channels, and have 5BTC to use.
select {
case heuristic.moreChansResps <- moreChansResp{true, 1, 5 * btcutil.SatoshiPerBitcoin}:
case constraints.moreChansResps <- moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin}:
case <-time.After(time.Second * 10):
t.Fatal("heuristic wasn't queried in time")
}
@ -340,20 +363,14 @@ func TestAgentChannelFailureSignal(t *testing.T) {
// 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 = &AttachmentDirective{
NodeID: NewNodeID(self),
ChanAmt: btcutil.SatoshiPerBitcoin,
Addrs: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Score: 0.5,
var fakeDirective = &NodeScore{
NodeID: NewNodeID(node),
Score: 0.5,
}
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
NewNodeID(self): fakeDirective,
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
NewNodeID(node): fakeDirective,
}:
case <-time.After(time.Second * 10):
t.Fatal("heuristic wasn't queried in time")
@ -363,13 +380,13 @@ func TestAgentChannelFailureSignal(t *testing.T) {
// Now ensure that the controller loop is re-executed.
select {
case heuristic.moreChansResps <- moreChansResp{true, 1, 5 * btcutil.SatoshiPerBitcoin}:
case constraints.moreChansResps <- moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin}:
case <-time.After(time.Second * 10):
t.Fatal("heuristic wasn't queried in time")
}
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
case <-time.After(time.Second * 10):
t.Fatal("heuristic wasn't queried in time")
}
@ -390,10 +407,14 @@ func TestAgentChannelCloseSignal(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -414,10 +435,8 @@ func TestAgentChannelCloseSignal(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
// We'll start the agent with two channels already being active.
@ -455,7 +474,7 @@ func TestAgentChannelCloseSignal(t *testing.T) {
// We'll send an initial "no" response to advance the agent past its
// initial check.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- moreChansResp{0, 0}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
@ -467,7 +486,7 @@ func TestAgentChannelCloseSignal(t *testing.T) {
// The agent should now query the heuristic in order to determine its
// next action as it local state has now been modified.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- 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.
@ -485,7 +504,7 @@ func TestAgentChannelCloseSignal(t *testing.T) {
// If this send success, then Select was erroneously called and the
// test should be failed.
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case 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.
@ -508,10 +527,14 @@ func TestAgentBalanceUpdate(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -538,10 +561,8 @@ func TestAgentBalanceUpdate(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -568,7 +589,7 @@ func TestAgentBalanceUpdate(t *testing.T) {
// We'll send an initial "no" response to advance the agent past its
// initial check.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- moreChansResp{0, 0}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
@ -584,7 +605,7 @@ func TestAgentBalanceUpdate(t *testing.T) {
// The agent should now query the heuristic in order to determine its
// next action as it local state has now been modified.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- 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.
@ -604,7 +625,7 @@ func TestAgentBalanceUpdate(t *testing.T) {
// If this send success, then Select was erroneously called and the
// test should be failed.
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case 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.
@ -626,10 +647,14 @@ func TestAgentImmediateAttach(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -653,10 +678,8 @@ func TestAgentImmediateAttach(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -682,6 +705,21 @@ func TestAgentImmediateAttach(t *testing.T) {
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 := memGraph.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.
@ -690,41 +728,18 @@ func TestAgentImmediateAttach(t *testing.T) {
// We'll send over a response indicating that it should
// establish more channels, and give it a budget of 5 BTC to do
// so.
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: numChans,
amt: 5 * btcutil.SatoshiPerBitcoin,
case constraints.moreChansResps <- moreChansResp{
numMore: numChans,
amt: 5 * btcutil.SatoshiPerBitcoin,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// At this point, the agent should now be querying the heuristic to
// requests attachment directives. We'll generate 5 mock directives so
// it can progress within its loop.
directives := make(map[NodeID]*AttachmentDirective)
nodeKeys := make(map[NodeID]struct{})
for i := 0; i < numChans; i++ {
pub, err := randKey()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
nodeID := NewNodeID(pub)
directives[nodeID] = &AttachmentDirective{
NodeID: nodeID,
ChanAmt: btcutil.SatoshiPerBitcoin,
Addrs: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Score: 0.5,
}
nodeKeys[nodeID] = struct{}{}
}
// With our fake directives created, we'll now send then to the agent
// as a return value for the Select function.
// requests attachment directives. With our fake directives created,
// we'll now send then to the agent as a return value for the Select
// function.
select {
case heuristic.nodeScoresResps <- directives:
case <-time.After(time.Second * 10):
@ -769,10 +784,14 @@ func TestAgentPrivateChannels(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
// The chanController should be initialized such that all of its open
// channel requests are for private channels.
chanController := &mockChanController{
@ -799,10 +818,8 @@ func TestAgentPrivateChannels(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -827,45 +844,38 @@ func TestAgentPrivateChannels(t *testing.T) {
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 := memGraph.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{
needMore: true,
numMore: numChans,
amt: 5 * btcutil.SatoshiPerBitcoin,
numMore: numChans,
amt: 5 * btcutil.SatoshiPerBitcoin,
}
select {
case heuristic.moreChansResps <- resp:
case constraints.moreChansResps <- resp:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// At this point, the agent should now be querying the heuristic to
// requests attachment directives. We'll generate 5 mock directives so
// it can progress within its loop.
directives := make(map[NodeID]*AttachmentDirective)
for i := 0; i < numChans; i++ {
pub, err := randKey()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
directives[NewNodeID(pub)] = &AttachmentDirective{
NodeID: NewNodeID(pub),
ChanAmt: btcutil.SatoshiPerBitcoin,
Addrs: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Score: 0.5,
}
}
// With our fake directives created, we'll now send then to the agent
// as a return value for the Select function.
// requests attachment directives. With our fake directives created,
// we'll now send then to the agent as a return value for the Select
// function.
select {
case heuristic.nodeScoresResps <- directives:
case <-time.After(time.Second * 10):
@ -901,10 +911,14 @@ func TestAgentPendingChannelState(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -932,10 +946,8 @@ func TestAgentPendingChannelState(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -959,43 +971,34 @@ func TestAgentPendingChannelState(t *testing.T) {
// exiting.
defer close(quit)
// We'll only return a single directive for a pre-chosen node.
nodeKey, err := memGraph.addRandNode()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
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.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 1,
amt: btcutil.SatoshiPerBitcoin,
case constraints.moreChansResps <- moreChansResp{
numMore: 1,
amt: btcutil.SatoshiPerBitcoin,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
heuristic.moreChanArgs = make(chan moreChanArg)
// Next, the agent should deliver a query to the Select method of the
// heuristic. We'll only return a single directive for a pre-chosen
// node.
nodeKey, err := randKey()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
nodeID := NewNodeID(nodeKey)
nodeDirective := &AttachmentDirective{
NodeID: nodeID,
ChanAmt: 0.5 * btcutil.SatoshiPerBitcoin,
Addrs: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Score: 0.5,
}
constraints.moreChanArgs = make(chan moreChanArg)
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
nodeID: nodeDirective,
}:
case <-time.After(time.Second * 10):
@ -1007,9 +1010,10 @@ func TestAgentPendingChannelState(t *testing.T) {
// A request to open the channel should've also been sent.
select {
case openChan := <-chanController.openChanSignals:
if openChan.amt != nodeDirective.ChanAmt {
chanAmt := constraints.MaxChanSize()
if openChan.amt != chanAmt {
t.Fatalf("invalid chan amt: expected %v, got %v",
nodeDirective.ChanAmt, openChan.amt)
chanAmt, openChan.amt)
}
if !openChan.target.IsEqual(nodeKey) {
t.Fatalf("unexpected key: expected %x, got %x",
@ -1036,14 +1040,15 @@ func TestAgentPendingChannelState(t *testing.T) {
// 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 := <-heuristic.moreChanArgs:
case req := <-constraints.moreChanArgs:
chanAmt := 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].Capacity != nodeDirective.ChanAmt {
if req.chans[0].Capacity != chanAmt {
t.Fatalf("wrong chan capacity: expected %v, got %v",
req.chans[0].Capacity, nodeDirective.ChanAmt)
req.chans[0].Capacity, chanAmt)
}
if req.chans[0].Node != nodeID {
t.Fatalf("wrong node ID: expected %x, got %x",
@ -1056,7 +1061,7 @@ func TestAgentPendingChannelState(t *testing.T) {
// We'll send across a response indicating that it *does* need more
// channels.
select {
case heuristic.moreChansResps <- moreChansResp{true, 1, btcutil.SatoshiPerBitcoin}:
case constraints.moreChansResps <- moreChansResp{1, btcutil.SatoshiPerBitcoin}:
case <-time.After(time.Second * 10):
t.Fatalf("need more chans wasn't queried in time")
}
@ -1093,10 +1098,14 @@ func TestAgentPendingOpenChannel(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1114,10 +1123,8 @@ func TestAgentPendingOpenChannel(t *testing.T) {
WalletBalance: func() (btcutil.Amount, error) {
return walletBalance, nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -1143,7 +1150,7 @@ func TestAgentPendingOpenChannel(t *testing.T) {
// We'll send an initial "no" response to advance the agent past its
// initial check.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- moreChansResp{0, 0}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
@ -1155,7 +1162,7 @@ func TestAgentPendingOpenChannel(t *testing.T) {
// The agent should now query the heuristic in order to determine its
// next action as its local state has now been modified.
select {
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
case constraints.moreChansResps <- moreChansResp{0, 0}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
@ -1163,7 +1170,7 @@ func TestAgentPendingOpenChannel(t *testing.T) {
// There shouldn't be a call to the Select method as we've returned
// "false" for NeedMoreChans above.
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
t.Fatalf("Select was called but shouldn't have been")
default:
}
@ -1186,10 +1193,14 @@ func TestAgentOnNodeUpdates(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1207,10 +1218,8 @@ func TestAgentOnNodeUpdates(t *testing.T) {
WalletBalance: func() (btcutil.Amount, error) {
return walletBalance, nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -1237,10 +1246,9 @@ func TestAgentOnNodeUpdates(t *testing.T) {
// initial check. This will cause it to try to get directives from an
// empty graph.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 2,
amt: walletBalance,
case constraints.moreChansResps <- moreChansResp{
numMore: 2,
amt: walletBalance,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
@ -1249,7 +1257,7 @@ func TestAgentOnNodeUpdates(t *testing.T) {
// Send over an empty list of attachment directives, which should cause
// the agent to return to waiting on a new signal.
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
case <-time.After(time.Second * 10):
t.Fatalf("Select was not called but should have been")
}
@ -1262,10 +1270,9 @@ func TestAgentOnNodeUpdates(t *testing.T) {
// channels. Since we haven't done anything, we will send the same
// response as before since we are still trying to open channels.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 2,
amt: walletBalance,
case constraints.moreChansResps <- moreChansResp{
numMore: 2,
amt: walletBalance,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
@ -1275,7 +1282,7 @@ func TestAgentOnNodeUpdates(t *testing.T) {
// 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.
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
case <-time.After(time.Second * 10):
t.Fatalf("Select was not called but should have been")
}
@ -1299,10 +1306,15 @@ func TestAgentSkipPendingConns(t *testing.T) {
quit := make(chan struct{})
heuristic := &mockHeuristic{
moreChansResps: make(chan moreChansResp),
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
nodeScoresArgs: make(chan directiveArg),
nodeScoresResps: make(chan map[NodeID]*NodeScore),
quit: quit,
}
constraints := &mockConstraints{
moreChansResps: make(chan moreChansResp),
quit: quit,
}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1341,10 +1353,8 @@ func TestAgentSkipPendingConns(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -1368,45 +1378,65 @@ func TestAgentSkipPendingConns(t *testing.T) {
// exiting.
defer close(quit)
// We'll only return a single directive for a pre-chosen node.
nodeKey, err := memGraph.addRandNode()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
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 := memGraph.addRandNode()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
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.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 1,
amt: walletBalance,
case constraints.moreChansResps <- moreChansResp{
numMore: 1,
amt: walletBalance,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// Next, the agent should deliver a query to the Select method of the
// heuristic. We'll only return a single directive for a pre-chosen
// node.
nodeKey, err := randKey()
if err != nil {
t.Fatalf("unable to generate key: %v", err)
}
nodeDirective := &AttachmentDirective{
NodeID: NewNodeID(nodeKey),
ChanAmt: 0.5 * btcutil.SatoshiPerBitcoin,
Addrs: []net.Addr{
&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
},
},
Score: 0.5,
// Both nodes should be part of the arguments.
select {
case req := <-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 heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
case 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:
@ -1419,26 +1449,38 @@ func TestAgentSkipPendingConns(t *testing.T) {
// The heuristic again informs the agent that we need more channels.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 1,
amt: walletBalance,
case constraints.moreChansResps <- moreChansResp{
numMore: 1,
amt: walletBalance,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// Send a directive for the same node, which already has a pending conn.
// Since the node now has a pending connection, it should be skipped
// and not part of the nodes attempting to be scored.
select {
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
NewNodeID(nodeKey): nodeDirective,
}:
case req := <-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 heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// This time, the agent should skip trying to connect to the node with a
// pending connection.
// The agent should not attempt any connection, since no nodes were
// scored.
select {
case <-connect:
t.Fatalf("agent should not have attempted connection")
@ -1456,29 +1498,47 @@ func TestAgentSkipPendingConns(t *testing.T) {
// The agent will now retry since the last connection attempt failed.
// The heuristic again informs the agent that we need more channels.
select {
case heuristic.moreChansResps <- moreChansResp{
needMore: true,
numMore: 1,
amt: walletBalance,
case constraints.moreChansResps <- moreChansResp{
numMore: 1,
amt: walletBalance,
}:
case <-time.After(time.Second * 10):
t.Fatalf("heuristic wasn't queried in time")
}
// Send a directive for the same node, which already has a pending conn.
// 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 heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
NewNodeID(nodeKey): nodeDirective,
case req := <-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 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 since the peer has
// been removed from the pending map.
// This time, the agent should try the connection to the second node.
select {
case <-connect:
case <-time.After(time.Second * 10):
t.Fatalf("agent have attempted connection")
t.Fatalf("agent should have attempted connection")
}
}

@ -46,10 +46,10 @@ func weightedChoice(w []float64) (int, error) {
return 0, fmt.Errorf("unable to make choice")
}
// chooseN picks at random min[n, len(s)] nodes if from the
// AttachmentDirectives map, with a probability weighted by their score.
func chooseN(n uint32, s map[NodeID]*AttachmentDirective) (
map[NodeID]*AttachmentDirective, error) {
// chooseN picks at random min[n, len(s)] nodes if from the NodeScore map, with
// a probability weighted by their score.
func chooseN(n uint32, s map[NodeID]*NodeScore) (
map[NodeID]*NodeScore, error) {
// Keep track of the number of nodes not yet chosen, in addition to
// their scores and NodeIDs.
@ -65,7 +65,7 @@ func chooseN(n uint32, s map[NodeID]*AttachmentDirective) (
// Pick a weighted choice from the remaining nodes as long as there are
// nodes left, and we haven't already picked n.
chosen := make(map[NodeID]*AttachmentDirective)
chosen := make(map[NodeID]*NodeScore)
for len(chosen) < int(n) && rem > 0 {
choice, err := weightedChoice(scores)
if err == ErrNoPositive {

@ -173,7 +173,7 @@ func TestWeightedChoiceDistribution(t *testing.T) {
func TestChooseNEmptyMap(t *testing.T) {
t.Parallel()
nodes := map[NodeID]*AttachmentDirective{}
nodes := map[NodeID]*NodeScore{}
property := func(n uint32) bool {
res, err := chooseN(n, nodes)
if err != nil {
@ -191,12 +191,12 @@ func TestChooseNEmptyMap(t *testing.T) {
// candidateMapVarLen is a type we'll use to generate maps of various lengths
// up to 255 to be used during QuickTests.
type candidateMapVarLen map[NodeID]*AttachmentDirective
type candidateMapVarLen map[NodeID]*NodeScore
// Generate generates a value of type candidateMapVarLen to be used during
// QuickTests.
func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value {
nodes := make(map[NodeID]*AttachmentDirective)
nodes := make(map[NodeID]*NodeScore)
// To avoid creating huge maps, we restrict them to max uint8 len.
n := uint8(rand.Uint32())
@ -212,7 +212,7 @@ func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value {
var nID [33]byte
binary.BigEndian.PutUint32(nID[:], uint32(i))
nodes[nID] = &AttachmentDirective{
nodes[nID] = &NodeScore{
Score: s,
}
}
@ -226,7 +226,7 @@ func TestChooseNMinimum(t *testing.T) {
t.Parallel()
// Helper to count the number of positive scores in the given map.
numPositive := func(nodes map[NodeID]*AttachmentDirective) int {
numPositive := func(nodes map[NodeID]*NodeScore) int {
cnt := 0
for _, v := range nodes {
if v.Score > 0 {
@ -274,7 +274,7 @@ func TestChooseNSample(t *testing.T) {
const maxIterations = 100000
fifth := uint32(numNodes / 5)
nodes := make(map[NodeID]*AttachmentDirective)
nodes := make(map[NodeID]*NodeScore)
// we make 5 buckets of nodes: 0, 0.1, 0.2, 0.4 and 0.8 score. We want
// to check that zero scores never gets chosen, while a doubling the
@ -299,7 +299,7 @@ func TestChooseNSample(t *testing.T) {
var nID [33]byte
binary.BigEndian.PutUint32(nID[:], i)
nodes[nID] = &AttachmentDirective{
nodes[nID] = &NodeScore{
Score: s,
}
}

@ -1,76 +0,0 @@
package autopilot
import (
"github.com/btcsuite/btcutil"
)
// HeuristicConstraints is a struct that indicate the constraints an autopilot
// heuristic must adhere to when opening channels.
type HeuristicConstraints struct {
// MinChanSize is the smallest channel that the autopilot agent should
// create.
MinChanSize btcutil.Amount
// MaxChanSize the largest channel that the autopilot agent should
// create.
MaxChanSize btcutil.Amount
// ChanLimit the maximum number of channels that should be created.
ChanLimit uint16
// Allocation the percentage of total funds that should be committed to
// automatic channel establishment.
Allocation float64
// MaxPendingOpens is the maximum number of pending channel
// establishment goroutines that can be lingering. We cap this value in
// order to control the level of parallelism caused by the autopilot
// agent.
MaxPendingOpens uint16
}
// availableChans returns the funds and number of channels slots the autopilot
// has available towards new channels, and still be within the set constraints.
func (h *HeuristicConstraints) availableChans(channels []Channel,
funds btcutil.Amount) (btcutil.Amount, uint32) {
// If we're already over our maximum allowed number of channels, then
// we'll instruct the controller not to create any more channels.
if len(channels) >= int(h.ChanLimit) {
return 0, 0
}
// The number of additional channels that should be opened is the
// difference between the channel limit, and the number of channels we
// already have open.
numAdditionalChans := uint32(h.ChanLimit) - uint32(len(channels))
// First, we'll tally up the total amount of funds that are currently
// present within the set of active channels.
var totalChanAllocation btcutil.Amount
for _, channel := range channels {
totalChanAllocation += channel.Capacity
}
// With this value known, we'll now compute the total amount of fund
// allocated across regular utxo's and channel utxo's.
totalFunds := funds + totalChanAllocation
// Once the total amount has been computed, we then calculate the
// fraction of funds currently allocated to channels.
fundsFraction := float64(totalChanAllocation) / float64(totalFunds)
// If this fraction is below our threshold, then we'll return true, to
// indicate the controller should call Select to obtain a candidate set
// of channels to attempt to open.
needMore := fundsFraction < h.Allocation
if !needMore {
return 0, 0
}
// Now that we know we need more funds, we'll compute the amount of
// additional funds we should allocate towards channels.
targetAllocation := btcutil.Amount(float64(totalFunds) * h.Allocation)
fundsAvailable := targetAllocation - totalChanAllocation
return fundsAvailable, numAdditionalChans
}

@ -81,6 +81,18 @@ type ChannelGraph interface {
ForEachNode(func(Node) error) error
}
// NodeScore is a tuple mapping a NodeID to a score indicating the preference
// of opening a channel with it.
type NodeScore struct {
// NodeID is the serialized compressed pubkey of the node that is being
// scored.
NodeID NodeID
// Score is the score given by the heuristic for opening a channel of
// the given size to this node.
Score float64
}
// AttachmentDirective describes a channel attachment proscribed by an
// AttachmentHeuristic. It details to which node a channel should be created
// to, and also the parameters which should be used in the channel creation.
@ -98,10 +110,6 @@ type AttachmentDirective struct {
// Addrs is a list of addresses that the target peer may be reachable
// at.
Addrs []net.Addr
// Score is the score given by the heuristic for opening a channel of
// the given size to this node.
Score float64
}
// AttachmentHeuristic is one of the primary interfaces within this package.
@ -111,21 +119,11 @@ type AttachmentDirective struct {
// the interface is to allow an auto-pilot agent to decide if it needs more
// channels, and if so, which exact channels should be opened.
type AttachmentHeuristic interface {
// NeedMoreChans is a predicate that should return true if, given the
// passed parameters, and its internal state, more channels should be
// opened within the channel graph. If the heuristic decides that we do
// indeed need more channels, then the second argument returned will
// represent the amount of additional funds to be used towards creating
// channels. This method should also return the exact *number* of
// additional channels that are needed in order to converge towards our
// ideal state.
NeedMoreChans(chans []Channel, balance btcutil.Amount) (btcutil.Amount, uint32, bool)
// NodeScores is a method that given the current channel graph, current
// set of local channels and funds available, scores the given nodes
// according to the preference of opening a channel with them. The
// returned channel candidates maps the NodeID to an attachemnt
// directive containing a score and a channel size.
// NodeScores is a method that given the current channel graph and
// current set of local channels, scores the given nodes according to
// the preference of opening a channel of the given size with them. The
// returned channel candidates maps the NodeID to a NodeScore for the
// node.
//
// The scores will be in the range [0, M], where 0 indicates no
// improvement in connectivity if a channel is opened to this node,
@ -136,8 +134,8 @@ type AttachmentHeuristic interface {
// NOTE: A NodeID not found in the returned map is implicitly given a
// score of 0.
NodeScores(g ChannelGraph, chans []Channel,
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*AttachmentDirective, error)
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error)
}
// ChannelController is a simple interface that allows an auto-pilot agent to

@ -2,62 +2,35 @@ package autopilot
import (
prand "math/rand"
"net"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
)
// ConstrainedPrefAttachment is an implementation of the AttachmentHeuristic
// interface that implement a constrained non-linear preferential attachment
// heuristic. This means that given a threshold to allocate to automatic
// channel establishment, the heuristic will attempt to favor connecting to
// nodes which already have a set amount of links, selected by sampling from a
// power law distribution. The attachment is non-linear in that it favors
// nodes with a higher in-degree but less so that regular linear preferential
// attachment. As a result, this creates smaller and less clusters than regular
// linear preferential attachment.
// PrefAttachment is an implementation of the AttachmentHeuristic interface
// that implement a non-linear preferential attachment heuristic. This means
// that given a threshold to allocate to automatic channel establishment, the
// heuristic will attempt to favor connecting to nodes which already have a set
// amount of links, selected by sampling from a power law distribution. The
// attachment is non-linear in that it favors nodes with a higher in-degree but
// less so than regular linear preferential attachment. As a result, this
// creates smaller and less clusters than regular linear preferential
// attachment.
//
// TODO(roasbeef): BA, with k=-3
type ConstrainedPrefAttachment struct {
constraints *HeuristicConstraints
type PrefAttachment struct {
}
// NewConstrainedPrefAttachment creates a new instance of a
// ConstrainedPrefAttachment heuristics given bounds on allowed channel sizes,
// and an allocation amount which is interpreted as a percentage of funds that
// is to be committed to channels at all times.
func NewConstrainedPrefAttachment(
cfg *HeuristicConstraints) *ConstrainedPrefAttachment {
// NewPrefAttachment creates a new instance of a PrefAttachment heuristic.
func NewPrefAttachment() *PrefAttachment {
prand.Seed(time.Now().Unix())
return &ConstrainedPrefAttachment{
constraints: cfg,
}
return &PrefAttachment{}
}
// A compile time assertion to ensure ConstrainedPrefAttachment meets the
// A compile time assertion to ensure PrefAttachment meets the
// AttachmentHeuristic interface.
var _ AttachmentHeuristic = (*ConstrainedPrefAttachment)(nil)
// NeedMoreChans is a predicate that should return true if, given the passed
// parameters, and its internal state, more channels should be opened within
// the channel graph. If the heuristic decides that we do indeed need more
// channels, then the second argument returned will represent the amount of
// additional funds to be used towards creating channels.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (p *ConstrainedPrefAttachment) NeedMoreChans(channels []Channel,
funds btcutil.Amount) (btcutil.Amount, uint32, bool) {
// We'll try to open more channels as long as the constraints allow it.
availableFunds, availableChans := p.constraints.availableChans(
channels, funds,
)
return availableFunds, availableChans, availableChans > 0
}
var _ AttachmentHeuristic = (*PrefAttachment)(nil)
// NodeID is a simple type that holds an EC public key serialized in compressed
// format.
@ -70,9 +43,9 @@ func NewNodeID(pub *btcec.PublicKey) NodeID {
return n
}
// NodeScores is a method that given the current channel graph, current set of
// local channels and funds available, scores the given nodes according the the
// preference of opening a channel with them.
// NodeScores is a method that given the current channel graph and
// current set of local channels, scores the given nodes according to
// the preference of opening a channel of the given size with them.
//
// The heuristic employed by this method is one that attempts to promote a
// scale-free network globally, via local attachment preferences for new nodes
@ -87,16 +60,14 @@ func NewNodeID(pub *btcec.PublicKey) NodeID {
// given to nodes already having high connectivity in the graph.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*AttachmentDirective, error) {
func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
// Count the number of channels in the graph. We'll also count the
// number of channels as we go for the nodes we are interested in, and
// record their addresses found in the db.
// number of channels as we go for the nodes we are interested in.
var graphChans int
nodeChanNum := make(map[NodeID]int)
addresses := make(map[NodeID][]net.Addr)
if err := g.ForEachNode(func(n Node) error {
var nodeChans int
err := n.ForEachChannel(func(_ ChannelEdge) error {
@ -115,10 +86,8 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
return nil
}
// Otherwise we'll record the number of channels, and also
// populate the address in our channel candidates map.
// Otherwise we'll record the number of channels.
nodeChanNum[nID] = nodeChans
addresses[nID] = n.Addrs()
return nil
}); err != nil {
@ -139,16 +108,10 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
// For each node in the set of nodes, count their fraction of channels
// in the graph, and use that as the score.
candidates := make(map[NodeID]*AttachmentDirective)
candidates := make(map[NodeID]*NodeScore)
for nID, nodeChans := range nodeChanNum {
// As channel size we'll use the maximum channel size available.
chanSize := p.constraints.MaxChanSize
if fundsAvailable-chanSize < 0 {
chanSize = fundsAvailable
}
_, ok := existingPeers[nID]
addrs := addresses[nID]
switch {
@ -157,16 +120,6 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
case ok:
continue
// If the amount is too small, we don't want to attempt opening
// another channel.
case chanSize == 0 || chanSize < p.constraints.MinChanSize:
continue
// If the node has no addresses, we cannot connect to it, so we
// skip it for now, which implicitly gives it a score of 0.
case len(addrs) == 0:
continue
// If the node had no channels, we skip it, since it would have
// gotten a zero score anyway.
case nodeChans == 0:
@ -176,11 +129,9 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
// Otherwise we score the node according to its fraction of
// channels in the graph.
score := float64(nodeChans) / float64(graphChans)
candidates[nID] = &AttachmentDirective{
NodeID: nID,
ChanAmt: chanSize,
Addrs: addrs,
Score: score,
candidates[nID] = &NodeScore{
NodeID: nID,
Score: score,
}
}

@ -12,169 +12,8 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)
func TestConstrainedPrefAttachmentNeedMoreChan(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
randChanID := func() lnwire.ShortChannelID {
return lnwire.NewShortChanIDFromInt(uint64(prand.Int63()))
}
testCases := []struct {
channels []Channel
walletAmt btcutil.Amount
needMore bool
amtAvailable btcutil.Amount
numMore uint32
}{
// Many available funds, but already have too many active open
// channels.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(prand.Int31()),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 10),
false,
0,
0,
},
// Ratio of funds in channels and total funds meets the
// threshold.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 2),
false,
0,
0,
},
// Ratio of funds in channels and total funds is below the
// threshold. We have 10 BTC allocated amongst channels and
// funds, atm. We're targeting 50%, so 5 BTC should be
// allocated. Only 1 BTC is atm, so 4 BTC should be
// recommended. We should also request 2 more channels as the
// limit is 3.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 9),
true,
btcutil.Amount(btcutil.SatoshiPerBitcoin * 4),
2,
},
// Ratio of funds in channels and total funds is below the
// threshold. We have 14 BTC total amongst the wallet's
// balance, and our currently opened channels. Since we're
// targeting a 50% allocation, we should commit 7 BTC. The
// current channels commit 4 BTC, so we should expected 3 BTC
// to be committed. We should only request a single additional
// channel as the limit is 3.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin * 3),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin * 10),
true,
btcutil.Amount(btcutil.SatoshiPerBitcoin * 3),
1,
},
// Ratio of funds in channels and total funds is above the
// threshold.
{
[]Channel{
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
{
ChanID: randChanID(),
Capacity: btcutil.Amount(btcutil.SatoshiPerBitcoin),
},
},
btcutil.Amount(btcutil.SatoshiPerBitcoin),
false,
0,
0,
},
}
prefAttach := NewConstrainedPrefAttachment(constraints)
for i, testCase := range testCases {
amtToAllocate, numMore, needMore := prefAttach.NeedMoreChans(
testCase.channels, testCase.walletAmt,
)
if amtToAllocate != testCase.amtAvailable {
t.Fatalf("test #%v: expected %v, got %v",
i, testCase.amtAvailable, amtToAllocate)
}
if needMore != testCase.needMore {
t.Fatalf("test #%v: expected %v, got %v",
i, testCase.needMore, needMore)
}
if numMore != testCase.numMore {
t.Fatalf("test #%v: expected %v, got %v",
i, testCase.numMore, numMore)
}
}
}
type genGraphFunc func() (testGraph, func(), error)
type testGraph interface {
@ -232,24 +71,14 @@ var chanGraphs = []struct {
},
}
// TestConstrainedPrefAttachmentSelectEmptyGraph ensures that when passed an
// TestPrefAttachmentSelectEmptyGraph ensures that when passed an
// empty graph, the NodeSores function always returns a score of 0.
func TestConstrainedPrefAttachmentSelectEmptyGraph(t *testing.T) {
func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
prefAttach := NewConstrainedPrefAttachment(constraints)
prefAttach := NewPrefAttachment()
// Create a random public key, which we will query to get a score for.
pub, err := randKey()
@ -335,27 +164,18 @@ func completeGraph(t *testing.T, g testGraph, numNodes int) {
}
}
// TestConstrainedPrefAttachmentSelectTwoVertexes ensures that when passed a
// TestPrefAttachmentSelectTwoVertexes ensures that when passed a
// graph with only two eligible vertexes, then both are given the same score,
// and the funds are appropriately allocated across each peer.
func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
graph, cleanup, err := graph.genFunc()
@ -366,7 +186,7 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
defer cleanup()
}
prefAttach := NewConstrainedPrefAttachment(constraints)
prefAttach := NewPrefAttachment()
// For this set, we'll load the memory graph with two
// nodes, and a random channel connecting them.
@ -399,9 +219,8 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
// With the necessary state initialized, we'll now
// attempt to get our candidates channel score given
// the current state of the graph.
const walletFunds = btcutil.SatoshiPerBitcoin * 10
candidates, err := prefAttach.NodeScores(graph, nil,
walletFunds, nodes)
maxChanSize, nodes)
if err != nil {
t1.Fatalf("unable to select attachment "+
"directives: %v", err)
@ -428,15 +247,6 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
nodeID[:])
}
// As the number of funds available exceed the
// max channel size, both edges should consume
// the maximum channel size.
if candidate.ChanAmt != maxChanSize {
t1.Fatalf("max channel size should be "+
"allocated, instead %v was: ",
maxChanSize)
}
// Since each of the nodes has 1 channel, out
// of only one channel in the graph, we expect
// their score to be 0.5.
@ -446,11 +256,6 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
"to be %v, instead was %v",
expScore, candidate.Score)
}
if len(candidate.Addrs) == 0 {
t1.Fatalf("expected node to have " +
"available addresses, didn't")
}
}
})
if !success {
@ -459,98 +264,18 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
}
}
// TestConstrainedPrefAttachmentSelectInsufficientFunds ensures that if the
// balance of the backing wallet is below the set min channel size, then it
// never recommends candidates to attach to.
func TestConstrainedPrefAttachmentSelectInsufficientFunds(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
graph, cleanup, err := graph.genFunc()
if err != nil {
t1.Fatalf("unable to create graph: %v", err)
}
if cleanup != nil {
defer cleanup()
}
// Add 10 nodes to the graph, with channels between
// them.
completeGraph(t, graph, 10)
prefAttach := NewConstrainedPrefAttachment(constraints)
nodes := make(map[NodeID]struct{})
if err := graph.ForEachNode(func(n Node) error {
nodes[n.PubKey()] = struct{}{}
return nil
}); err != nil {
t1.Fatalf("unable to traverse graph: %v", err)
}
// With the necessary state initialized, we'll now
// attempt to get the score for our list of nodes,
// passing zero for the amount of wallet funds. This
// should return an all-zero score set.
scores, err := prefAttach.NodeScores(graph, nil,
0, nodes)
if err != nil {
t1.Fatalf("unable to select attachment "+
"directives: %v", err)
}
// Since all should be given a score of 0, the map
// should be empty.
if len(scores) != 0 {
t1.Fatalf("expected empty score map, "+
"instead got %v ", len(scores))
}
})
if !success {
break
}
}
}
// TestConstrainedPrefAttachmentSelectGreedyAllocation tests that if upon
// TestPrefAttachmentSelectGreedyAllocation tests that if upon
// returning node scores, the NodeScores method will attempt to greedily
// allocate all funds to each vertex (up to the max channel size).
func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
graph, cleanup, err := graph.genFunc()
@ -561,7 +286,7 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
defer cleanup()
}
prefAttach := NewConstrainedPrefAttachment(constraints)
prefAttach := NewPrefAttachment()
const chanCapacity = btcutil.SatoshiPerBitcoin
@ -622,9 +347,8 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
// 50/50 allocation, and have 3 BTC in channels. As a
// result, the heuristic should try to greedily
// allocate funds to channels.
const availableBalance = btcutil.SatoshiPerBitcoin * 2.5
scores, err := prefAttach.NodeScores(graph, nil,
availableBalance, nodes)
maxChanSize, nodes)
if err != nil {
t1.Fatalf("unable to select attachment "+
"directives: %v", err)
@ -642,17 +366,6 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
if candidate.Score == 0 {
t1.Fatalf("Expected non-zero score")
}
if candidate.ChanAmt != maxChanSize {
t1.Fatalf("expected recommendation "+
"of %v, instead got %v",
maxChanSize, candidate.ChanAmt)
}
if len(candidate.Addrs) == 0 {
t1.Fatalf("expected node to have " +
"available addresses, didn't")
}
}
// Imagine a few channels are being opened, and there's
@ -677,17 +390,6 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
if candidate.Score == 0 {
t1.Fatalf("Expected non-zero score")
}
if candidate.ChanAmt != remBalance {
t1.Fatalf("expected recommendation "+
"of %v, instead got %v",
remBalance, candidate.ChanAmt)
}
if len(candidate.Addrs) == 0 {
t1.Fatalf("expected node to have " +
"available addresses, didn't")
}
}
})
if !success {
@ -696,28 +398,18 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
}
}
// TestConstrainedPrefAttachmentSelectSkipNodes ensures that if a node was
// TestPrefAttachmentSelectSkipNodes ensures that if a node was
// already selected as a channel counterparty, then that node will get a score
// of zero during scoring.
func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
t.Parallel()
prand.Seed(time.Now().Unix())
const (
minChanSize = 0
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
chanLimit = 3
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
graph, cleanup, err := graph.genFunc()
@ -728,7 +420,7 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
defer cleanup()
}
prefAttach := NewConstrainedPrefAttachment(constraints)
prefAttach := NewPrefAttachment()
// Next, we'll create a simple topology of two nodes,
// with a single channel connecting them.
@ -753,9 +445,8 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
// With our graph created, we'll now get the scores for
// all nodes in the graph.
const availableBalance = btcutil.SatoshiPerBitcoin * 2.5
scores, err := prefAttach.NodeScores(graph, nil,
availableBalance, nodes)
maxChanSize, nodes)
if err != nil {
t1.Fatalf("unable to select attachment "+
"directives: %v", err)
@ -772,17 +463,6 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
if candidate.Score == 0 {
t1.Fatalf("Expected non-zero score")
}
if candidate.ChanAmt != maxChanSize {
t1.Fatalf("expected recommendation "+
"of %v, instead got %v",
maxChanSize, candidate.ChanAmt)
}
if len(candidate.Addrs) == 0 {
t1.Fatalf("expected node to have " +
"available addresses, didn't")
}
}
// We'll simulate a channel update by adding the nodes
@ -801,7 +481,7 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
// then all nodes should have a score of zero, since we
// already got channels to them.
scores, err = prefAttach.NodeScores(graph, chans,
availableBalance, nodes)
maxChanSize, nodes)
if err != nil {
t1.Fatalf("unable to select attachment "+
"directives: %v", err)

@ -87,20 +87,17 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg {
atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))
// Set up the constraints the autopilot heuristics must adhere to.
atplConstraints := &autopilot.HeuristicConstraints{
MinChanSize: btcutil.Amount(cfg.MinChannelSize),
MaxChanSize: btcutil.Amount(cfg.MaxChannelSize),
ChanLimit: uint16(cfg.MaxChannels),
Allocation: cfg.Allocation,
MaxPendingOpens: 10,
}
// First, we'll create the preferential attachment heuristic,
// initialized with the passed auto pilot configuration parameters.
prefAttachment := autopilot.NewConstrainedPrefAttachment(
atplConstraints,
atplConstraints := autopilot.NewConstraints(
btcutil.Amount(cfg.MinChannelSize),
btcutil.Amount(cfg.MaxChannelSize),
uint16(cfg.MaxChannels),
10,
cfg.Allocation,
)
// First, we'll create the preferential attachment heuristic.
prefAttachment := autopilot.NewPrefAttachment()
// With the heuristic itself created, we can now populate the remainder
// of the items that the autopilot agent needs to perform its duties.
self := svr.identityPriv.PubKey()