mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-13 11:09:23 +01:00
Merge pull request #2341 from halseth/autopilot-weighted-heuristics
[autopilot] Decouple agent constraints from heuristics
This commit is contained in:
commit
a0c0e8edc9
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
|
// Constraints is the set of constraints the autopilot must adhere to
|
||||||
// when opening channels.
|
// when opening channels.
|
||||||
Constraints *HeuristicConstraints
|
Constraints AgentConstraints
|
||||||
|
|
||||||
// TODO(roasbeef): add additional signals from fee rates and revenue of
|
// TODO(roasbeef): add additional signals from fee rates and revenue of
|
||||||
// currently opened channels
|
// currently opened channels
|
||||||
|
@ -478,12 +478,21 @@ func (a *Agent) controller() {
|
||||||
a.pendingMtx.Unlock()
|
a.pendingMtx.Unlock()
|
||||||
|
|
||||||
// Now that we've updated our internal state, we'll consult our
|
// Now that we've updated our internal state, we'll consult our
|
||||||
// channel attachment heuristic to determine if we should open
|
// channel attachment heuristic to determine if we can open
|
||||||
// up any additional channels or modify existing channels.
|
// up any additional channels while staying within our
|
||||||
availableFunds, numChans, needMore := a.cfg.Heuristic.NeedMoreChans(
|
// constraints.
|
||||||
|
availableFunds, numChans := a.cfg.Constraints.ChannelBudget(
|
||||||
totalChans, a.totalBalance,
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,6 +525,7 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
|
||||||
// want to skip.
|
// want to skip.
|
||||||
selfPubBytes := a.cfg.Self.SerializeCompressed()
|
selfPubBytes := a.cfg.Self.SerializeCompressed()
|
||||||
nodes := make(map[NodeID]struct{})
|
nodes := make(map[NodeID]struct{})
|
||||||
|
addresses := make(map[NodeID][]net.Addr)
|
||||||
if err := a.cfg.Graph.ForEachNode(func(node Node) error {
|
if err := a.cfg.Graph.ForEachNode(func(node Node) error {
|
||||||
nID := NodeID(node.PubKey())
|
nID := NodeID(node.PubKey())
|
||||||
|
|
||||||
|
@ -526,6 +536,14 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
|
||||||
return nil
|
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
|
// Additionally, if this node is in the blacklist, then
|
||||||
// we'll skip it.
|
// we'll skip it.
|
||||||
if _, ok := nodesToSkip[nID]; ok {
|
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)
|
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
|
// Use the heuristic to calculate a score for each node in the
|
||||||
// graph.
|
// graph.
|
||||||
scores, err := a.cfg.Heuristic.NodeScores(
|
scores, err := a.cfg.Heuristic.NodeScores(
|
||||||
a.cfg.Graph, totalChans, availableFunds, nodes,
|
a.cfg.Graph, totalChans, chanSize, nodes,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to calculate node scores : %v", err)
|
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))
|
log.Debugf("Got scores for %d nodes", len(scores))
|
||||||
|
|
||||||
// Now use the score to make a weighted choice which
|
// Now use the score to make a weighted choice which nodes to attempt
|
||||||
// nodes to attempt to open channels to.
|
// to open channels to.
|
||||||
chanCandidates, err := chooseN(numChans, scores)
|
scores, err = chooseN(numChans, scores)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Unable to make weighted choice: %v",
|
return fmt.Errorf("Unable to make weighted choice: %v",
|
||||||
err)
|
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 {
|
if len(chanCandidates) == 0 {
|
||||||
log.Infof("No eligible candidates to connect to")
|
log.Infof("No eligible candidates to connect to")
|
||||||
return nil
|
return nil
|
||||||
|
@ -573,11 +615,11 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
|
||||||
// available to future heuristic selections.
|
// available to future heuristic selections.
|
||||||
a.pendingMtx.Lock()
|
a.pendingMtx.Lock()
|
||||||
defer a.pendingMtx.Unlock()
|
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 "+
|
log.Debugf("Reached cap of %v pending "+
|
||||||
"channel opens, will retry "+
|
"channel opens, will retry "+
|
||||||
"after success/failure",
|
"after success/failure",
|
||||||
a.cfg.Constraints.MaxPendingOpens)
|
a.cfg.Constraints.MaxPendingOpens())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +684,7 @@ func (a *Agent) executeDirective(directive AttachmentDirective) {
|
||||||
// first.
|
// first.
|
||||||
a.pendingMtx.Lock()
|
a.pendingMtx.Lock()
|
||||||
if uint16(len(a.pendingOpens)) >=
|
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
|
// Since we've reached our max number of pending opens, we'll
|
||||||
// disconnect this peer and exit. However, if we were
|
// disconnect this peer and exit. However, if we were
|
||||||
// previously connected to them, then we'll make sure to
|
// previously connected to them, then we'll make sure to
|
||||||
|
|
151
autopilot/agent_constraints.go
Normal file
151
autopilot/agent_constraints.go
Normal file
|
@ -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
|
||||||
|
}
|
166
autopilot/agent_constraints_test.go
Normal file
166
autopilot/agent_constraints_test.go
Normal file
|
@ -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
|
package autopilot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
@ -15,9 +14,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type moreChansResp struct {
|
type moreChansResp struct {
|
||||||
needMore bool
|
numMore uint32
|
||||||
numMore uint32
|
amt btcutil.Amount
|
||||||
amt btcutil.Amount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type moreChanArg struct {
|
type moreChanArg struct {
|
||||||
|
@ -25,18 +23,14 @@ type moreChanArg struct {
|
||||||
balance btcutil.Amount
|
balance btcutil.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockHeuristic struct {
|
type mockConstraints struct {
|
||||||
moreChansResps chan moreChansResp
|
moreChansResps chan moreChansResp
|
||||||
moreChanArgs chan moreChanArg
|
moreChanArgs chan moreChanArg
|
||||||
|
quit chan struct{}
|
||||||
nodeScoresResps chan map[NodeID]*AttachmentDirective
|
|
||||||
nodeScoresArgs chan directiveArg
|
|
||||||
|
|
||||||
quit chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockHeuristic) NeedMoreChans(chans []Channel,
|
func (m *mockConstraints) ChannelBudget(chans []Channel,
|
||||||
balance btcutil.Amount) (btcutil.Amount, uint32, bool) {
|
balance btcutil.Amount) (btcutil.Amount, uint32) {
|
||||||
|
|
||||||
if m.moreChanArgs != nil {
|
if m.moreChanArgs != nil {
|
||||||
moreChan := moreChanArg{
|
moreChan := moreChanArg{
|
||||||
|
@ -47,18 +41,38 @@ func (m *mockHeuristic) NeedMoreChans(chans []Channel,
|
||||||
select {
|
select {
|
||||||
case m.moreChanArgs <- moreChan:
|
case m.moreChanArgs <- moreChan:
|
||||||
case <-m.quit:
|
case <-m.quit:
|
||||||
return 0, 0, false
|
return 0, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case resp := <-m.moreChansResps:
|
case resp := <-m.moreChansResps:
|
||||||
return resp.amt, resp.numMore, resp.needMore
|
return resp.amt, resp.numMore
|
||||||
case <-m.quit:
|
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 {
|
type directiveArg struct {
|
||||||
graph ChannelGraph
|
graph ChannelGraph
|
||||||
amt btcutil.Amount
|
amt btcutil.Amount
|
||||||
|
@ -68,7 +82,7 @@ type directiveArg struct {
|
||||||
|
|
||||||
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []Channel,
|
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []Channel,
|
||||||
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
|
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
|
||||||
map[NodeID]*AttachmentDirective, error) {
|
map[NodeID]*NodeScore, error) {
|
||||||
|
|
||||||
if m.nodeScoresArgs != nil {
|
if m.nodeScoresArgs != nil {
|
||||||
directive := directiveArg{
|
directive := directiveArg{
|
||||||
|
@ -146,10 +160,14 @@ func TestAgentChannelOpenSignal(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent, 10),
|
openChanSignals: make(chan openChanIntent, 10),
|
||||||
}
|
}
|
||||||
|
@ -170,10 +188,8 @@ func TestAgentChannelOpenSignal(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
agent, err := New(testCfg, initialChans)
|
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
|
// We'll send an initial "no" response to advance the agent past its
|
||||||
// initial check.
|
// initial check.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// The agent should now query the heuristic in order to determine its
|
||||||
// next action as it local state has now been modified.
|
// next action as it local state has now been modified.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
// At this point, the local state of the agent should
|
// At this point, the local state of the agent should
|
||||||
// have also been updated to reflect that the LN node
|
// have also been updated to reflect that the LN node
|
||||||
// now has an additional channel with one BTC.
|
// 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
|
// If this send success, then Select was erroneously called and the
|
||||||
// test should be failed.
|
// 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")
|
t.Fatalf("Select was called but shouldn't have been")
|
||||||
|
|
||||||
// This is the correct path as Select should've be called.
|
// This is the correct path as Select should've be called.
|
||||||
|
@ -279,12 +295,20 @@ func TestAgentChannelFailureSignal(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockFailingChanController{}
|
chanController := &mockFailingChanController{}
|
||||||
memGraph, _, _ := newMemChanGraph()
|
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
|
// With the dependencies we created, we can now create the initial
|
||||||
// agent itself.
|
// agent itself.
|
||||||
|
@ -295,16 +319,15 @@ func TestAgentChannelFailureSignal(t *testing.T) {
|
||||||
WalletBalance: func() (btcutil.Amount, error) {
|
WalletBalance: func() (btcutil.Amount, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
},
|
},
|
||||||
|
// TODO: move address check to agent.
|
||||||
ConnectToPeer: func(*btcec.PublicKey, []net.Addr) (bool, error) {
|
ConnectToPeer: func(*btcec.PublicKey, []net.Addr) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
},
|
},
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
|
@ -332,7 +355,7 @@ func TestAgentChannelFailureSignal(t *testing.T) {
|
||||||
// First ensure the agent will attempt to open a new channel. Return
|
// First ensure the agent will attempt to open a new channel. Return
|
||||||
// that we need more channels, and have 5BTC to use.
|
// that we need more channels, and have 5BTC to use.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{true, 1, 5 * btcutil.SatoshiPerBitcoin}:
|
case constraints.moreChansResps <- moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatal("heuristic wasn't queried in time")
|
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
|
// At this point, the agent should now be querying the heuristic to
|
||||||
// request attachment directives, return a fake so the agent will
|
// request attachment directives, return a fake so the agent will
|
||||||
// attempt to open a channel.
|
// attempt to open a channel.
|
||||||
var fakeDirective = &AttachmentDirective{
|
var fakeDirective = &NodeScore{
|
||||||
NodeID: NewNodeID(self),
|
NodeID: NewNodeID(node),
|
||||||
ChanAmt: btcutil.SatoshiPerBitcoin,
|
Score: 0.5,
|
||||||
Addrs: []net.Addr{
|
|
||||||
&net.TCPAddr{
|
|
||||||
IP: bytes.Repeat([]byte("a"), 16),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Score: 0.5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
||||||
NewNodeID(self): fakeDirective,
|
NewNodeID(node): fakeDirective,
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatal("heuristic wasn't queried in time")
|
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.
|
// Now ensure that the controller loop is re-executed.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{true, 1, 5 * btcutil.SatoshiPerBitcoin}:
|
case constraints.moreChansResps <- moreChansResp{1, 5 * btcutil.SatoshiPerBitcoin}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatal("heuristic wasn't queried in time")
|
t.Fatal("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatal("heuristic wasn't queried in time")
|
t.Fatal("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
@ -390,10 +407,14 @@ func TestAgentChannelCloseSignal(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -414,10 +435,8 @@ func TestAgentChannelCloseSignal(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll start the agent with two channels already being active.
|
// 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
|
// We'll send an initial "no" response to advance the agent past its
|
||||||
// initial check.
|
// initial check.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// The agent should now query the heuristic in order to determine its
|
||||||
// next action as it local state has now been modified.
|
// next action as it local state has now been modified.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
// At this point, the local state of the agent should
|
// At this point, the local state of the agent should
|
||||||
// have also been updated to reflect that the LN node
|
// have also been updated to reflect that the LN node
|
||||||
// has no existing open channels.
|
// 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
|
// If this send success, then Select was erroneously called and the
|
||||||
// test should be failed.
|
// 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")
|
t.Fatalf("Select was called but shouldn't have been")
|
||||||
|
|
||||||
// This is the correct path as Select should've be called.
|
// This is the correct path as Select should've be called.
|
||||||
|
@ -508,10 +527,14 @@ func TestAgentBalanceUpdate(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -538,10 +561,8 @@ func TestAgentBalanceUpdate(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
agent, err := New(testCfg, initialChans)
|
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
|
// We'll send an initial "no" response to advance the agent past its
|
||||||
// initial check.
|
// initial check.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// The agent should now query the heuristic in order to determine its
|
||||||
// next action as it local state has now been modified.
|
// next action as it local state has now been modified.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
// At this point, the local state of the agent should
|
// At this point, the local state of the agent should
|
||||||
// have also been updated to reflect that the LN node
|
// have also been updated to reflect that the LN node
|
||||||
// now has an additional 5BTC available.
|
// 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
|
// If this send success, then Select was erroneously called and the
|
||||||
// test should be failed.
|
// 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")
|
t.Fatalf("Select was called but shouldn't have been")
|
||||||
|
|
||||||
// This is the correct path as Select should've be called.
|
// This is the correct path as Select should've be called.
|
||||||
|
@ -626,10 +647,14 @@ func TestAgentImmediateAttach(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -653,10 +678,8 @@ func TestAgentImmediateAttach(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
agent, err := New(testCfg, initialChans)
|
agent, err := New(testCfg, initialChans)
|
||||||
|
@ -682,6 +705,21 @@ func TestAgentImmediateAttach(t *testing.T) {
|
||||||
|
|
||||||
const numChans = 5
|
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
|
// 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
|
// method on the passed heuristic. So we'll provide it with a response
|
||||||
// that will kick off the main loop.
|
// 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
|
// We'll send over a response indicating that it should
|
||||||
// establish more channels, and give it a budget of 5 BTC to do
|
// establish more channels, and give it a budget of 5 BTC to do
|
||||||
// so.
|
// so.
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: numChans,
|
||||||
numMore: numChans,
|
amt: 5 * btcutil.SatoshiPerBitcoin,
|
||||||
amt: 5 * btcutil.SatoshiPerBitcoin,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, the agent should now be querying the heuristic to
|
// At this point, the agent should now be querying the heuristic to
|
||||||
// requests attachment directives. We'll generate 5 mock directives so
|
// requests attachment directives. With our fake directives created,
|
||||||
// it can progress within its loop.
|
// we'll now send then to the agent as a return value for the Select
|
||||||
directives := make(map[NodeID]*AttachmentDirective)
|
// function.
|
||||||
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.
|
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- directives:
|
case heuristic.nodeScoresResps <- directives:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
|
@ -769,10 +784,14 @@ func TestAgentPrivateChannels(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
// The chanController should be initialized such that all of its open
|
// The chanController should be initialized such that all of its open
|
||||||
// channel requests are for private channels.
|
// channel requests are for private channels.
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
|
@ -799,10 +818,8 @@ func TestAgentPrivateChannels(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
agent, err := New(cfg, nil)
|
agent, err := New(cfg, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -827,45 +844,38 @@ func TestAgentPrivateChannels(t *testing.T) {
|
||||||
|
|
||||||
const numChans = 5
|
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
|
// 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
|
// 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
|
// that will kick off the main loop. We'll send over a response
|
||||||
// indicating that it should establish more channels, and give it a
|
// indicating that it should establish more channels, and give it a
|
||||||
// budget of 5 BTC to do so.
|
// budget of 5 BTC to do so.
|
||||||
resp := moreChansResp{
|
resp := moreChansResp{
|
||||||
needMore: true,
|
numMore: numChans,
|
||||||
numMore: numChans,
|
amt: 5 * btcutil.SatoshiPerBitcoin,
|
||||||
amt: 5 * btcutil.SatoshiPerBitcoin,
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- resp:
|
case constraints.moreChansResps <- resp:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, the agent should now be querying the heuristic to
|
// At this point, the agent should now be querying the heuristic to
|
||||||
// requests attachment directives. We'll generate 5 mock directives so
|
// requests attachment directives. With our fake directives created,
|
||||||
// it can progress within its loop.
|
// we'll now send then to the agent as a return value for the Select
|
||||||
directives := make(map[NodeID]*AttachmentDirective)
|
// function.
|
||||||
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.
|
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- directives:
|
case heuristic.nodeScoresResps <- directives:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
|
@ -901,10 +911,14 @@ func TestAgentPendingChannelState(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -932,10 +946,8 @@ func TestAgentPendingChannelState(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
agent, err := New(testCfg, initialChans)
|
agent, err := New(testCfg, initialChans)
|
||||||
|
@ -959,43 +971,34 @@ func TestAgentPendingChannelState(t *testing.T) {
|
||||||
// exiting.
|
// exiting.
|
||||||
defer close(quit)
|
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
|
// 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
|
// query, that it needs more channels and has 3 BTC available for
|
||||||
// attachment. We'll send over a response indicating that it should
|
// 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.
|
// establish more channels, and give it a budget of 1 BTC to do so.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 1,
|
||||||
numMore: 1,
|
amt: btcutil.SatoshiPerBitcoin,
|
||||||
amt: btcutil.SatoshiPerBitcoin,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
heuristic.moreChanArgs = make(chan moreChanArg)
|
constraints.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,
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
||||||
nodeID: nodeDirective,
|
nodeID: nodeDirective,
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
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.
|
// A request to open the channel should've also been sent.
|
||||||
select {
|
select {
|
||||||
case openChan := <-chanController.openChanSignals:
|
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",
|
t.Fatalf("invalid chan amt: expected %v, got %v",
|
||||||
nodeDirective.ChanAmt, openChan.amt)
|
chanAmt, openChan.amt)
|
||||||
}
|
}
|
||||||
if !openChan.target.IsEqual(nodeKey) {
|
if !openChan.target.IsEqual(nodeKey) {
|
||||||
t.Fatalf("unexpected key: expected %x, got %x",
|
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
|
// The request that we get should include a pending channel for the
|
||||||
// one that we just created, otherwise the agent isn't properly
|
// one that we just created, otherwise the agent isn't properly
|
||||||
// updating its internal state.
|
// updating its internal state.
|
||||||
case req := <-heuristic.moreChanArgs:
|
case req := <-constraints.moreChanArgs:
|
||||||
|
chanAmt := constraints.MaxChanSize()
|
||||||
if len(req.chans) != 1 {
|
if len(req.chans) != 1 {
|
||||||
t.Fatalf("should include pending chan in current "+
|
t.Fatalf("should include pending chan in current "+
|
||||||
"state, instead have %v chans", len(req.chans))
|
"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",
|
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 {
|
if req.chans[0].Node != nodeID {
|
||||||
t.Fatalf("wrong node ID: expected %x, got %x",
|
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
|
// We'll send across a response indicating that it *does* need more
|
||||||
// channels.
|
// channels.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{true, 1, btcutil.SatoshiPerBitcoin}:
|
case constraints.moreChansResps <- moreChansResp{1, btcutil.SatoshiPerBitcoin}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("need more chans wasn't queried in time")
|
t.Fatalf("need more chans wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
@ -1093,10 +1098,14 @@ func TestAgentPendingOpenChannel(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -1114,10 +1123,8 @@ func TestAgentPendingOpenChannel(t *testing.T) {
|
||||||
WalletBalance: func() (btcutil.Amount, error) {
|
WalletBalance: func() (btcutil.Amount, error) {
|
||||||
return walletBalance, nil
|
return walletBalance, nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
agent, err := New(cfg, nil)
|
agent, err := New(cfg, nil)
|
||||||
if err != 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
|
// We'll send an initial "no" response to advance the agent past its
|
||||||
// initial check.
|
// initial check.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// The agent should now query the heuristic in order to determine its
|
||||||
// next action as its local state has now been modified.
|
// next action as its local state has now been modified.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{false, 0, 0}:
|
case constraints.moreChansResps <- moreChansResp{0, 0}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// There shouldn't be a call to the Select method as we've returned
|
||||||
// "false" for NeedMoreChans above.
|
// "false" for NeedMoreChans above.
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
||||||
t.Fatalf("Select was called but shouldn't have been")
|
t.Fatalf("Select was called but shouldn't have been")
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
@ -1186,10 +1193,14 @@ func TestAgentOnNodeUpdates(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -1207,10 +1218,8 @@ func TestAgentOnNodeUpdates(t *testing.T) {
|
||||||
WalletBalance: func() (btcutil.Amount, error) {
|
WalletBalance: func() (btcutil.Amount, error) {
|
||||||
return walletBalance, nil
|
return walletBalance, nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
agent, err := New(cfg, nil)
|
agent, err := New(cfg, nil)
|
||||||
if err != 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
|
// initial check. This will cause it to try to get directives from an
|
||||||
// empty graph.
|
// empty graph.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 2,
|
||||||
numMore: 2,
|
amt: walletBalance,
|
||||||
amt: walletBalance,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// Send over an empty list of attachment directives, which should cause
|
||||||
// the agent to return to waiting on a new signal.
|
// the agent to return to waiting on a new signal.
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("Select was not called but should have been")
|
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
|
// channels. Since we haven't done anything, we will send the same
|
||||||
// response as before since we are still trying to open channels.
|
// response as before since we are still trying to open channels.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 2,
|
||||||
numMore: 2,
|
amt: walletBalance,
|
||||||
amt: walletBalance,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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
|
// 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.
|
// updates signal is causing the agent to make this attempt.
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{}:
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("Select was not called but should have been")
|
t.Fatalf("Select was not called but should have been")
|
||||||
}
|
}
|
||||||
|
@ -1299,10 +1306,15 @@ func TestAgentSkipPendingConns(t *testing.T) {
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
heuristic := &mockHeuristic{
|
heuristic := &mockHeuristic{
|
||||||
moreChansResps: make(chan moreChansResp),
|
nodeScoresArgs: make(chan directiveArg),
|
||||||
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
|
nodeScoresResps: make(chan map[NodeID]*NodeScore),
|
||||||
quit: quit,
|
quit: quit,
|
||||||
}
|
}
|
||||||
|
constraints := &mockConstraints{
|
||||||
|
moreChansResps: make(chan moreChansResp),
|
||||||
|
quit: quit,
|
||||||
|
}
|
||||||
|
|
||||||
chanController := &mockChanController{
|
chanController := &mockChanController{
|
||||||
openChanSignals: make(chan openChanIntent),
|
openChanSignals: make(chan openChanIntent),
|
||||||
}
|
}
|
||||||
|
@ -1341,10 +1353,8 @@ func TestAgentSkipPendingConns(t *testing.T) {
|
||||||
DisconnectPeer: func(*btcec.PublicKey) error {
|
DisconnectPeer: func(*btcec.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Graph: memGraph,
|
Graph: memGraph,
|
||||||
Constraints: &HeuristicConstraints{
|
Constraints: constraints,
|
||||||
MaxPendingOpens: 10,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
initialChans := []Channel{}
|
initialChans := []Channel{}
|
||||||
agent, err := New(testCfg, initialChans)
|
agent, err := New(testCfg, initialChans)
|
||||||
|
@ -1368,45 +1378,65 @@ func TestAgentSkipPendingConns(t *testing.T) {
|
||||||
// exiting.
|
// exiting.
|
||||||
defer close(quit)
|
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
|
// 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
|
// initial check. This will cause it to try to get directives from the
|
||||||
// graph.
|
// graph.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 1,
|
||||||
numMore: 1,
|
amt: walletBalance,
|
||||||
amt: walletBalance,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, the agent should deliver a query to the Select method of the
|
// Both nodes should be part of the arguments.
|
||||||
// heuristic. We'll only return a single directive for a pre-chosen
|
select {
|
||||||
// node.
|
case req := <-heuristic.nodeScoresArgs:
|
||||||
nodeKey, err := randKey()
|
if len(req.nodes) != 2 {
|
||||||
if err != nil {
|
t.Fatalf("expected %v nodes, instead "+
|
||||||
t.Fatalf("unable to generate key: %v", err)
|
"had %v", 2, len(req.nodes))
|
||||||
}
|
}
|
||||||
nodeDirective := &AttachmentDirective{
|
if _, ok := req.nodes[nodeID]; !ok {
|
||||||
NodeID: NewNodeID(nodeKey),
|
t.Fatalf("node not included in arguments")
|
||||||
ChanAmt: 0.5 * btcutil.SatoshiPerBitcoin,
|
}
|
||||||
Addrs: []net.Addr{
|
if _, ok := req.nodes[nodeID2]; !ok {
|
||||||
&net.TCPAddr{
|
t.Fatalf("node not included in arguments")
|
||||||
IP: bytes.Repeat([]byte("a"), 16),
|
}
|
||||||
},
|
case <-time.After(time.Second * 10):
|
||||||
},
|
t.Fatalf("select wasn't queried in time")
|
||||||
Score: 0.5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respond with a scored directive. We skip node2 for now, implicitly
|
||||||
|
// giving it a zero-score.
|
||||||
select {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
|
case heuristic.nodeScoresResps <- map[NodeID]*NodeScore{
|
||||||
NewNodeID(nodeKey): nodeDirective,
|
NewNodeID(nodeKey): nodeDirective,
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The agent should attempt connection to the node.
|
||||||
var errChan chan error
|
var errChan chan error
|
||||||
select {
|
select {
|
||||||
case errChan = <-connect:
|
case errChan = <-connect:
|
||||||
|
@ -1419,26 +1449,38 @@ func TestAgentSkipPendingConns(t *testing.T) {
|
||||||
|
|
||||||
// The heuristic again informs the agent that we need more channels.
|
// The heuristic again informs the agent that we need more channels.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 1,
|
||||||
numMore: 1,
|
amt: walletBalance,
|
||||||
amt: walletBalance,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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 {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
|
case req := <-heuristic.nodeScoresArgs:
|
||||||
NewNodeID(nodeKey): nodeDirective,
|
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):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This time, the agent should skip trying to connect to the node with a
|
// The agent should not attempt any connection, since no nodes were
|
||||||
// pending connection.
|
// scored.
|
||||||
select {
|
select {
|
||||||
case <-connect:
|
case <-connect:
|
||||||
t.Fatalf("agent should not have attempted connection")
|
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 agent will now retry since the last connection attempt failed.
|
||||||
// The heuristic again informs the agent that we need more channels.
|
// The heuristic again informs the agent that we need more channels.
|
||||||
select {
|
select {
|
||||||
case heuristic.moreChansResps <- moreChansResp{
|
case constraints.moreChansResps <- moreChansResp{
|
||||||
needMore: true,
|
numMore: 1,
|
||||||
numMore: 1,
|
amt: walletBalance,
|
||||||
amt: walletBalance,
|
|
||||||
}:
|
}:
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
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 {
|
select {
|
||||||
case heuristic.nodeScoresResps <- map[NodeID]*AttachmentDirective{
|
case req := <-heuristic.nodeScoresArgs:
|
||||||
NewNodeID(nodeKey): nodeDirective,
|
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):
|
case <-time.After(time.Second * 10):
|
||||||
t.Fatalf("heuristic wasn't queried in time")
|
t.Fatalf("heuristic wasn't queried in time")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This time, the agent should try the connection since the peer has
|
// This time, the agent should try the connection to the second node.
|
||||||
// been removed from the pending map.
|
|
||||||
select {
|
select {
|
||||||
case <-connect:
|
case <-connect:
|
||||||
case <-time.After(time.Second * 10):
|
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")
|
return 0, fmt.Errorf("unable to make choice")
|
||||||
}
|
}
|
||||||
|
|
||||||
// chooseN picks at random min[n, len(s)] nodes if from the
|
// chooseN picks at random min[n, len(s)] nodes if from the NodeScore map, with
|
||||||
// AttachmentDirectives map, with a probability weighted by their score.
|
// a probability weighted by their score.
|
||||||
func chooseN(n uint32, s map[NodeID]*AttachmentDirective) (
|
func chooseN(n uint32, s map[NodeID]*NodeScore) (
|
||||||
map[NodeID]*AttachmentDirective, error) {
|
map[NodeID]*NodeScore, error) {
|
||||||
|
|
||||||
// Keep track of the number of nodes not yet chosen, in addition to
|
// Keep track of the number of nodes not yet chosen, in addition to
|
||||||
// their scores and NodeIDs.
|
// 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
|
// Pick a weighted choice from the remaining nodes as long as there are
|
||||||
// nodes left, and we haven't already picked n.
|
// 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 {
|
for len(chosen) < int(n) && rem > 0 {
|
||||||
choice, err := weightedChoice(scores)
|
choice, err := weightedChoice(scores)
|
||||||
if err == ErrNoPositive {
|
if err == ErrNoPositive {
|
||||||
|
|
|
@ -173,7 +173,7 @@ func TestWeightedChoiceDistribution(t *testing.T) {
|
||||||
func TestChooseNEmptyMap(t *testing.T) {
|
func TestChooseNEmptyMap(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
nodes := map[NodeID]*AttachmentDirective{}
|
nodes := map[NodeID]*NodeScore{}
|
||||||
property := func(n uint32) bool {
|
property := func(n uint32) bool {
|
||||||
res, err := chooseN(n, nodes)
|
res, err := chooseN(n, nodes)
|
||||||
if err != nil {
|
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
|
// candidateMapVarLen is a type we'll use to generate maps of various lengths
|
||||||
// up to 255 to be used during QuickTests.
|
// 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
|
// Generate generates a value of type candidateMapVarLen to be used during
|
||||||
// QuickTests.
|
// QuickTests.
|
||||||
func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value {
|
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.
|
// To avoid creating huge maps, we restrict them to max uint8 len.
|
||||||
n := uint8(rand.Uint32())
|
n := uint8(rand.Uint32())
|
||||||
|
@ -212,7 +212,7 @@ func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||||
|
|
||||||
var nID [33]byte
|
var nID [33]byte
|
||||||
binary.BigEndian.PutUint32(nID[:], uint32(i))
|
binary.BigEndian.PutUint32(nID[:], uint32(i))
|
||||||
nodes[nID] = &AttachmentDirective{
|
nodes[nID] = &NodeScore{
|
||||||
Score: s,
|
Score: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,7 +226,7 @@ func TestChooseNMinimum(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Helper to count the number of positive scores in the given map.
|
// 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
|
cnt := 0
|
||||||
for _, v := range nodes {
|
for _, v := range nodes {
|
||||||
if v.Score > 0 {
|
if v.Score > 0 {
|
||||||
|
@ -274,7 +274,7 @@ func TestChooseNSample(t *testing.T) {
|
||||||
const maxIterations = 100000
|
const maxIterations = 100000
|
||||||
fifth := uint32(numNodes / 5)
|
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
|
// 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
|
// 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
|
var nID [33]byte
|
||||||
binary.BigEndian.PutUint32(nID[:], i)
|
binary.BigEndian.PutUint32(nID[:], i)
|
||||||
nodes[nID] = &AttachmentDirective{
|
nodes[nID] = &NodeScore{
|
||||||
Score: s,
|
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
|
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
|
// AttachmentDirective describes a channel attachment proscribed by an
|
||||||
// AttachmentHeuristic. It details to which node a channel should be created
|
// AttachmentHeuristic. It details to which node a channel should be created
|
||||||
// to, and also the parameters which should be used in the channel creation.
|
// 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
|
// Addrs is a list of addresses that the target peer may be reachable
|
||||||
// at.
|
// at.
|
||||||
Addrs []net.Addr
|
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.
|
// 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
|
// 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.
|
// channels, and if so, which exact channels should be opened.
|
||||||
type AttachmentHeuristic interface {
|
type AttachmentHeuristic interface {
|
||||||
// NeedMoreChans is a predicate that should return true if, given the
|
// NodeScores is a method that given the current channel graph and
|
||||||
// passed parameters, and its internal state, more channels should be
|
// current set of local channels, scores the given nodes according to
|
||||||
// opened within the channel graph. If the heuristic decides that we do
|
// the preference of opening a channel of the given size with them. The
|
||||||
// indeed need more channels, then the second argument returned will
|
// returned channel candidates maps the NodeID to a NodeScore for the
|
||||||
// represent the amount of additional funds to be used towards creating
|
// node.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// The scores will be in the range [0, M], where 0 indicates no
|
// The scores will be in the range [0, M], where 0 indicates no
|
||||||
// improvement in connectivity if a channel is opened to this node,
|
// 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
|
// NOTE: A NodeID not found in the returned map is implicitly given a
|
||||||
// score of 0.
|
// score of 0.
|
||||||
NodeScores(g ChannelGraph, chans []Channel,
|
NodeScores(g ChannelGraph, chans []Channel,
|
||||||
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
|
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
|
||||||
map[NodeID]*AttachmentDirective, error)
|
map[NodeID]*NodeScore, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelController is a simple interface that allows an auto-pilot agent to
|
// ChannelController is a simple interface that allows an auto-pilot agent to
|
||||||
|
|
|
@ -2,62 +2,35 @@ package autopilot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
prand "math/rand"
|
prand "math/rand"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConstrainedPrefAttachment is an implementation of the AttachmentHeuristic
|
// PrefAttachment is an implementation of the AttachmentHeuristic interface
|
||||||
// interface that implement a constrained non-linear preferential attachment
|
// that implement a non-linear preferential attachment heuristic. This means
|
||||||
// heuristic. This means that given a threshold to allocate to automatic
|
// that given a threshold to allocate to automatic channel establishment, the
|
||||||
// channel establishment, the heuristic will attempt to favor connecting to
|
// heuristic will attempt to favor connecting to nodes which already have a set
|
||||||
// nodes which already have a set amount of links, selected by sampling from a
|
// amount of links, selected by sampling from a power law distribution. The
|
||||||
// power law distribution. The attachment is non-linear in that it favors
|
// attachment is non-linear in that it favors nodes with a higher in-degree but
|
||||||
// nodes with a higher in-degree but less so that regular linear preferential
|
// less so than regular linear preferential attachment. As a result, this
|
||||||
// attachment. As a result, this creates smaller and less clusters than regular
|
// creates smaller and less clusters than regular linear preferential
|
||||||
// linear preferential attachment.
|
// attachment.
|
||||||
//
|
//
|
||||||
// TODO(roasbeef): BA, with k=-3
|
// TODO(roasbeef): BA, with k=-3
|
||||||
type ConstrainedPrefAttachment struct {
|
type PrefAttachment struct {
|
||||||
constraints *HeuristicConstraints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConstrainedPrefAttachment creates a new instance of a
|
// NewPrefAttachment creates a new instance of a PrefAttachment heuristic.
|
||||||
// ConstrainedPrefAttachment heuristics given bounds on allowed channel sizes,
|
func NewPrefAttachment() *PrefAttachment {
|
||||||
// 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 {
|
|
||||||
|
|
||||||
prand.Seed(time.Now().Unix())
|
prand.Seed(time.Now().Unix())
|
||||||
|
return &PrefAttachment{}
|
||||||
return &ConstrainedPrefAttachment{
|
|
||||||
constraints: cfg,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compile time assertion to ensure ConstrainedPrefAttachment meets the
|
// A compile time assertion to ensure PrefAttachment meets the
|
||||||
// AttachmentHeuristic interface.
|
// AttachmentHeuristic interface.
|
||||||
var _ AttachmentHeuristic = (*ConstrainedPrefAttachment)(nil)
|
var _ AttachmentHeuristic = (*PrefAttachment)(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeID is a simple type that holds an EC public key serialized in compressed
|
// NodeID is a simple type that holds an EC public key serialized in compressed
|
||||||
// format.
|
// format.
|
||||||
|
@ -70,9 +43,9 @@ func NewNodeID(pub *btcec.PublicKey) NodeID {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeScores is a method that given the current channel graph, current set of
|
// NodeScores is a method that given the current channel graph and
|
||||||
// local channels and funds available, scores the given nodes according the the
|
// current set of local channels, scores the given nodes according to
|
||||||
// preference of opening a channel with them.
|
// 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
|
// The heuristic employed by this method is one that attempts to promote a
|
||||||
// scale-free network globally, via local attachment preferences for new nodes
|
// 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.
|
// given to nodes already having high connectivity in the graph.
|
||||||
//
|
//
|
||||||
// NOTE: This is a part of the AttachmentHeuristic interface.
|
// NOTE: This is a part of the AttachmentHeuristic interface.
|
||||||
func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
|
func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
|
||||||
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
|
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
|
||||||
map[NodeID]*AttachmentDirective, error) {
|
map[NodeID]*NodeScore, error) {
|
||||||
|
|
||||||
// Count the number of channels in the graph. We'll also count the
|
// 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
|
// number of channels as we go for the nodes we are interested in.
|
||||||
// record their addresses found in the db.
|
|
||||||
var graphChans int
|
var graphChans int
|
||||||
nodeChanNum := make(map[NodeID]int)
|
nodeChanNum := make(map[NodeID]int)
|
||||||
addresses := make(map[NodeID][]net.Addr)
|
|
||||||
if err := g.ForEachNode(func(n Node) error {
|
if err := g.ForEachNode(func(n Node) error {
|
||||||
var nodeChans int
|
var nodeChans int
|
||||||
err := n.ForEachChannel(func(_ ChannelEdge) error {
|
err := n.ForEachChannel(func(_ ChannelEdge) error {
|
||||||
|
@ -115,10 +86,8 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we'll record the number of channels, and also
|
// Otherwise we'll record the number of channels.
|
||||||
// populate the address in our channel candidates map.
|
|
||||||
nodeChanNum[nID] = nodeChans
|
nodeChanNum[nID] = nodeChans
|
||||||
addresses[nID] = n.Addrs()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != 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
|
// For each node in the set of nodes, count their fraction of channels
|
||||||
// in the graph, and use that as the score.
|
// in the graph, and use that as the score.
|
||||||
candidates := make(map[NodeID]*AttachmentDirective)
|
candidates := make(map[NodeID]*NodeScore)
|
||||||
for nID, nodeChans := range nodeChanNum {
|
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]
|
_, ok := existingPeers[nID]
|
||||||
addrs := addresses[nID]
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
|
@ -157,16 +120,6 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
|
||||||
case ok:
|
case ok:
|
||||||
continue
|
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
|
// If the node had no channels, we skip it, since it would have
|
||||||
// gotten a zero score anyway.
|
// gotten a zero score anyway.
|
||||||
case nodeChans == 0:
|
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
|
// Otherwise we score the node according to its fraction of
|
||||||
// channels in the graph.
|
// channels in the graph.
|
||||||
score := float64(nodeChans) / float64(graphChans)
|
score := float64(nodeChans) / float64(graphChans)
|
||||||
candidates[nID] = &AttachmentDirective{
|
candidates[nID] = &NodeScore{
|
||||||
NodeID: nID,
|
NodeID: nID,
|
||||||
ChanAmt: chanSize,
|
Score: score,
|
||||||
Addrs: addrs,
|
|
||||||
Score: score,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,169 +12,8 @@ import (
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"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 genGraphFunc func() (testGraph, func(), error)
|
||||||
|
|
||||||
type testGraph interface {
|
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.
|
// empty graph, the NodeSores function always returns a score of 0.
|
||||||
func TestConstrainedPrefAttachmentSelectEmptyGraph(t *testing.T) {
|
func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
minChanSize = 0
|
|
||||||
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
||||||
chanLimit = 3
|
|
||||||
threshold = 0.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constraints := &HeuristicConstraints{
|
prefAttach := NewPrefAttachment()
|
||||||
MinChanSize: minChanSize,
|
|
||||||
MaxChanSize: maxChanSize,
|
|
||||||
ChanLimit: chanLimit,
|
|
||||||
Allocation: threshold,
|
|
||||||
}
|
|
||||||
|
|
||||||
prefAttach := NewConstrainedPrefAttachment(constraints)
|
|
||||||
|
|
||||||
// Create a random public key, which we will query to get a score for.
|
// Create a random public key, which we will query to get a score for.
|
||||||
pub, err := randKey()
|
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,
|
// graph with only two eligible vertexes, then both are given the same score,
|
||||||
// and the funds are appropriately allocated across each peer.
|
// and the funds are appropriately allocated across each peer.
|
||||||
func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
prand.Seed(time.Now().Unix())
|
prand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minChanSize = 0
|
|
||||||
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
||||||
chanLimit = 3
|
|
||||||
threshold = 0.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constraints := &HeuristicConstraints{
|
|
||||||
MinChanSize: minChanSize,
|
|
||||||
MaxChanSize: maxChanSize,
|
|
||||||
ChanLimit: chanLimit,
|
|
||||||
Allocation: threshold,
|
|
||||||
}
|
|
||||||
for _, graph := range chanGraphs {
|
for _, graph := range chanGraphs {
|
||||||
success := t.Run(graph.name, func(t1 *testing.T) {
|
success := t.Run(graph.name, func(t1 *testing.T) {
|
||||||
graph, cleanup, err := graph.genFunc()
|
graph, cleanup, err := graph.genFunc()
|
||||||
|
@ -366,7 +186,7 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
prefAttach := NewConstrainedPrefAttachment(constraints)
|
prefAttach := NewPrefAttachment()
|
||||||
|
|
||||||
// For this set, we'll load the memory graph with two
|
// For this set, we'll load the memory graph with two
|
||||||
// nodes, and a random channel connecting them.
|
// nodes, and a random channel connecting them.
|
||||||
|
@ -399,9 +219,8 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
// With the necessary state initialized, we'll now
|
// With the necessary state initialized, we'll now
|
||||||
// attempt to get our candidates channel score given
|
// attempt to get our candidates channel score given
|
||||||
// the current state of the graph.
|
// the current state of the graph.
|
||||||
const walletFunds = btcutil.SatoshiPerBitcoin * 10
|
|
||||||
candidates, err := prefAttach.NodeScores(graph, nil,
|
candidates, err := prefAttach.NodeScores(graph, nil,
|
||||||
walletFunds, nodes)
|
maxChanSize, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1.Fatalf("unable to select attachment "+
|
t1.Fatalf("unable to select attachment "+
|
||||||
"directives: %v", err)
|
"directives: %v", err)
|
||||||
|
@ -428,15 +247,6 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
nodeID[:])
|
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
|
// Since each of the nodes has 1 channel, out
|
||||||
// of only one channel in the graph, we expect
|
// of only one channel in the graph, we expect
|
||||||
// their score to be 0.5.
|
// their score to be 0.5.
|
||||||
|
@ -446,11 +256,6 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
"to be %v, instead was %v",
|
"to be %v, instead was %v",
|
||||||
expScore, candidate.Score)
|
expScore, candidate.Score)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(candidate.Addrs) == 0 {
|
|
||||||
t1.Fatalf("expected node to have " +
|
|
||||||
"available addresses, didn't")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if !success {
|
if !success {
|
||||||
|
@ -459,98 +264,18 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConstrainedPrefAttachmentSelectInsufficientFunds ensures that if the
|
// TestPrefAttachmentSelectGreedyAllocation tests that if upon
|
||||||
// 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
|
|
||||||
// returning node scores, the NodeScores method will attempt to greedily
|
// returning node scores, the NodeScores method will attempt to greedily
|
||||||
// allocate all funds to each vertex (up to the max channel size).
|
// allocate all funds to each vertex (up to the max channel size).
|
||||||
func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
prand.Seed(time.Now().Unix())
|
prand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minChanSize = 0
|
|
||||||
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
||||||
chanLimit = 3
|
|
||||||
threshold = 0.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constraints := &HeuristicConstraints{
|
|
||||||
MinChanSize: minChanSize,
|
|
||||||
MaxChanSize: maxChanSize,
|
|
||||||
ChanLimit: chanLimit,
|
|
||||||
Allocation: threshold,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, graph := range chanGraphs {
|
for _, graph := range chanGraphs {
|
||||||
success := t.Run(graph.name, func(t1 *testing.T) {
|
success := t.Run(graph.name, func(t1 *testing.T) {
|
||||||
graph, cleanup, err := graph.genFunc()
|
graph, cleanup, err := graph.genFunc()
|
||||||
|
@ -561,7 +286,7 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
prefAttach := NewConstrainedPrefAttachment(constraints)
|
prefAttach := NewPrefAttachment()
|
||||||
|
|
||||||
const chanCapacity = btcutil.SatoshiPerBitcoin
|
const chanCapacity = btcutil.SatoshiPerBitcoin
|
||||||
|
|
||||||
|
@ -622,9 +347,8 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
||||||
// 50/50 allocation, and have 3 BTC in channels. As a
|
// 50/50 allocation, and have 3 BTC in channels. As a
|
||||||
// result, the heuristic should try to greedily
|
// result, the heuristic should try to greedily
|
||||||
// allocate funds to channels.
|
// allocate funds to channels.
|
||||||
const availableBalance = btcutil.SatoshiPerBitcoin * 2.5
|
|
||||||
scores, err := prefAttach.NodeScores(graph, nil,
|
scores, err := prefAttach.NodeScores(graph, nil,
|
||||||
availableBalance, nodes)
|
maxChanSize, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1.Fatalf("unable to select attachment "+
|
t1.Fatalf("unable to select attachment "+
|
||||||
"directives: %v", err)
|
"directives: %v", err)
|
||||||
|
@ -642,17 +366,6 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
||||||
if candidate.Score == 0 {
|
if candidate.Score == 0 {
|
||||||
t1.Fatalf("Expected non-zero score")
|
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
|
// Imagine a few channels are being opened, and there's
|
||||||
|
@ -677,17 +390,6 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
|
||||||
if candidate.Score == 0 {
|
if candidate.Score == 0 {
|
||||||
t1.Fatalf("Expected non-zero score")
|
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 {
|
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
|
// already selected as a channel counterparty, then that node will get a score
|
||||||
// of zero during scoring.
|
// of zero during scoring.
|
||||||
func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
|
func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
prand.Seed(time.Now().Unix())
|
prand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minChanSize = 0
|
|
||||||
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
maxChanSize = btcutil.Amount(btcutil.SatoshiPerBitcoin)
|
||||||
chanLimit = 3
|
|
||||||
threshold = 0.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constraints := &HeuristicConstraints{
|
|
||||||
MinChanSize: minChanSize,
|
|
||||||
MaxChanSize: maxChanSize,
|
|
||||||
ChanLimit: chanLimit,
|
|
||||||
Allocation: threshold,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, graph := range chanGraphs {
|
for _, graph := range chanGraphs {
|
||||||
success := t.Run(graph.name, func(t1 *testing.T) {
|
success := t.Run(graph.name, func(t1 *testing.T) {
|
||||||
graph, cleanup, err := graph.genFunc()
|
graph, cleanup, err := graph.genFunc()
|
||||||
|
@ -728,7 +420,7 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
prefAttach := NewConstrainedPrefAttachment(constraints)
|
prefAttach := NewPrefAttachment()
|
||||||
|
|
||||||
// Next, we'll create a simple topology of two nodes,
|
// Next, we'll create a simple topology of two nodes,
|
||||||
// with a single channel connecting them.
|
// 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
|
// With our graph created, we'll now get the scores for
|
||||||
// all nodes in the graph.
|
// all nodes in the graph.
|
||||||
const availableBalance = btcutil.SatoshiPerBitcoin * 2.5
|
|
||||||
scores, err := prefAttach.NodeScores(graph, nil,
|
scores, err := prefAttach.NodeScores(graph, nil,
|
||||||
availableBalance, nodes)
|
maxChanSize, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1.Fatalf("unable to select attachment "+
|
t1.Fatalf("unable to select attachment "+
|
||||||
"directives: %v", err)
|
"directives: %v", err)
|
||||||
|
@ -772,17 +463,6 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
|
||||||
if candidate.Score == 0 {
|
if candidate.Score == 0 {
|
||||||
t1.Fatalf("Expected non-zero score")
|
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
|
// 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
|
// then all nodes should have a score of zero, since we
|
||||||
// already got channels to them.
|
// already got channels to them.
|
||||||
scores, err = prefAttach.NodeScores(graph, chans,
|
scores, err = prefAttach.NodeScores(graph, chans,
|
||||||
availableBalance, nodes)
|
maxChanSize, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1.Fatalf("unable to select attachment "+
|
t1.Fatalf("unable to select attachment "+
|
||||||
"directives: %v", err)
|
"directives: %v", err)
|
||||||
|
|
21
pilot.go
21
pilot.go
|
@ -87,20 +87,17 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg {
|
||||||
atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))
|
atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))
|
||||||
|
|
||||||
// Set up the constraints the autopilot heuristics must adhere to.
|
// Set up the constraints the autopilot heuristics must adhere to.
|
||||||
atplConstraints := &autopilot.HeuristicConstraints{
|
atplConstraints := autopilot.NewConstraints(
|
||||||
MinChanSize: btcutil.Amount(cfg.MinChannelSize),
|
btcutil.Amount(cfg.MinChannelSize),
|
||||||
MaxChanSize: btcutil.Amount(cfg.MaxChannelSize),
|
btcutil.Amount(cfg.MaxChannelSize),
|
||||||
ChanLimit: uint16(cfg.MaxChannels),
|
uint16(cfg.MaxChannels),
|
||||||
Allocation: cfg.Allocation,
|
10,
|
||||||
MaxPendingOpens: 10,
|
cfg.Allocation,
|
||||||
}
|
|
||||||
|
|
||||||
// First, we'll create the preferential attachment heuristic,
|
|
||||||
// initialized with the passed auto pilot configuration parameters.
|
|
||||||
prefAttachment := autopilot.NewConstrainedPrefAttachment(
|
|
||||||
atplConstraints,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// First, we'll create the preferential attachment heuristic.
|
||||||
|
prefAttachment := autopilot.NewPrefAttachment()
|
||||||
|
|
||||||
// With the heuristic itself created, we can now populate the remainder
|
// With the heuristic itself created, we can now populate the remainder
|
||||||
// of the items that the autopilot agent needs to perform its duties.
|
// of the items that the autopilot agent needs to perform its duties.
|
||||||
self := svr.identityPriv.PubKey()
|
self := svr.identityPriv.PubKey()
|
||||||
|
|
Loading…
Add table
Reference in a new issue