autopilot: define AgentConstraints

To decouple the autopilot heuristic from the constraints, we start by
abstracting them behind an interface to make them easier to mock. We
also rename them HeuristicConstraints->AgentConstraints to make it clear
that they are now constraints the agent must adhere to.
This commit is contained in:
Johan T. Halseth 2018-12-19 14:54:53 +01:00
parent 21460c9e67
commit 1d82e12fcf
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
7 changed files with 271 additions and 168 deletions

@ -57,7 +57,7 @@ type Config struct {
// Constraints is the set of constraints the autopilot must adhere to
// when opening channels.
Constraints *HeuristicConstraints
Constraints AgentConstraints
// TODO(roasbeef): add additional signals from fee rates and revenue of
// currently opened channels
@ -573,11 +573,11 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
// available to future heuristic selections.
a.pendingMtx.Lock()
defer a.pendingMtx.Unlock()
if uint16(len(a.pendingOpens)) >= a.cfg.Constraints.MaxPendingOpens {
if uint16(len(a.pendingOpens)) >= a.cfg.Constraints.MaxPendingOpens() {
log.Debugf("Reached cap of %v pending "+
"channel opens, will retry "+
"after success/failure",
a.cfg.Constraints.MaxPendingOpens)
a.cfg.Constraints.MaxPendingOpens())
return nil
}
@ -642,7 +642,7 @@ func (a *Agent) executeDirective(directive AttachmentDirective) {
// first.
a.pendingMtx.Lock()
if uint16(len(a.pendingOpens)) >=
a.cfg.Constraints.MaxPendingOpens {
a.cfg.Constraints.MaxPendingOpens() {
// Since we've reached our max number of pending opens, we'll
// disconnect this peer and exit. However, if we were
// previously connected to them, then we'll make sure to

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

@ -25,6 +25,27 @@ type moreChanArg struct {
balance btcutil.Amount
}
type mockConstraints struct {
}
func (m *mockConstraints) ChannelBudget(chans []Channel,
balance btcutil.Amount) (btcutil.Amount, uint32) {
return 1e8, 10
}
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 {
moreChansResps chan moreChansResp
moreChanArgs chan moreChanArg
@ -150,6 +171,8 @@ func TestAgentChannelOpenSignal(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent, 10),
}
@ -170,10 +193,8 @@ func TestAgentChannelOpenSignal(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -283,6 +304,8 @@ func TestAgentChannelFailureSignal(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockFailingChanController{}
memGraph, _, _ := newMemChanGraph()
@ -301,10 +324,8 @@ func TestAgentChannelFailureSignal(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
@ -394,6 +415,8 @@ func TestAgentChannelCloseSignal(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -414,10 +437,8 @@ func TestAgentChannelCloseSignal(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
// We'll start the agent with two channels already being active.
@ -512,6 +533,8 @@ func TestAgentBalanceUpdate(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -538,10 +561,8 @@ func TestAgentBalanceUpdate(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -630,6 +651,8 @@ func TestAgentImmediateAttach(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -653,10 +676,8 @@ func TestAgentImmediateAttach(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -773,6 +794,8 @@ func TestAgentPrivateChannels(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
// The chanController should be initialized such that all of its open
// channel requests are for private channels.
chanController := &mockChanController{
@ -799,10 +822,8 @@ func TestAgentPrivateChannels(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -905,6 +926,8 @@ func TestAgentPendingChannelState(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -932,10 +955,8 @@ func TestAgentPendingChannelState(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)
@ -1097,6 +1118,8 @@ func TestAgentPendingOpenChannel(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1114,10 +1137,8 @@ func TestAgentPendingOpenChannel(t *testing.T) {
WalletBalance: func() (btcutil.Amount, error) {
return walletBalance, nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -1190,6 +1211,8 @@ func TestAgentOnNodeUpdates(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1207,10 +1230,8 @@ func TestAgentOnNodeUpdates(t *testing.T) {
WalletBalance: func() (btcutil.Amount, error) {
return walletBalance, nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
agent, err := New(cfg, nil)
if err != nil {
@ -1303,6 +1324,8 @@ func TestAgentSkipPendingConns(t *testing.T) {
nodeScoresResps: make(chan map[NodeID]*AttachmentDirective),
quit: quit,
}
constraints := &mockConstraints{}
chanController := &mockChanController{
openChanSignals: make(chan openChanIntent),
}
@ -1341,10 +1364,8 @@ func TestAgentSkipPendingConns(t *testing.T) {
DisconnectPeer: func(*btcec.PublicKey) error {
return nil
},
Graph: memGraph,
Constraints: &HeuristicConstraints{
MaxPendingOpens: 10,
},
Graph: memGraph,
Constraints: constraints,
}
initialChans := []Channel{}
agent, err := New(testCfg, initialChans)

@ -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
}

@ -21,7 +21,7 @@ import (
//
// TODO(roasbeef): BA, with k=-3
type ConstrainedPrefAttachment struct {
constraints *HeuristicConstraints
constraints AgentConstraints
}
// NewConstrainedPrefAttachment creates a new instance of a
@ -29,7 +29,7 @@ type ConstrainedPrefAttachment struct {
// 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 {
cfg AgentConstraints) *ConstrainedPrefAttachment {
prand.Seed(time.Now().Unix())
@ -53,7 +53,7 @@ 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(
availableFunds, availableChans := p.constraints.ChannelBudget(
channels, funds,
)
return availableFunds, availableChans, availableChans > 0
@ -142,7 +142,7 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
candidates := make(map[NodeID]*AttachmentDirective)
for nID, nodeChans := range nodeChanNum {
// As channel size we'll use the maximum channel size available.
chanSize := p.constraints.MaxChanSize
chanSize := p.constraints.MaxChanSize()
if fundsAvailable-chanSize < 0 {
chanSize = fundsAvailable
}
@ -159,7 +159,7 @@ func (p *ConstrainedPrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
// If the amount is too small, we don't want to attempt opening
// another channel.
case chanSize == 0 || chanSize < p.constraints.MinChanSize:
case chanSize == 0 || chanSize < p.constraints.MinChanSize():
continue
// If the node has no addresses, we cannot connect to it, so we

@ -29,12 +29,13 @@ func TestConstrainedPrefAttachmentNeedMoreChan(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
randChanID := func() lnwire.ShortChannelID {
return lnwire.NewShortChanIDFromInt(uint64(prand.Int63()))
@ -242,12 +243,13 @@ func TestConstrainedPrefAttachmentSelectEmptyGraph(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
prefAttach := NewConstrainedPrefAttachment(constraints)
@ -350,12 +352,14 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
graph, cleanup, err := graph.genFunc()
@ -474,12 +478,13 @@ func TestConstrainedPrefAttachmentSelectInsufficientFunds(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
@ -544,12 +549,13 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {
@ -711,12 +717,13 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) {
threshold = 0.5
)
constraints := &HeuristicConstraints{
MinChanSize: minChanSize,
MaxChanSize: maxChanSize,
ChanLimit: chanLimit,
Allocation: threshold,
}
constraints := NewConstraints(
minChanSize,
maxChanSize,
chanLimit,
0,
threshold,
)
for _, graph := range chanGraphs {
success := t.Run(graph.name, func(t1 *testing.T) {

@ -87,13 +87,13 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg {
atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))
// Set up the constraints the autopilot heuristics must adhere to.
atplConstraints := &autopilot.HeuristicConstraints{
MinChanSize: btcutil.Amount(cfg.MinChannelSize),
MaxChanSize: btcutil.Amount(cfg.MaxChannelSize),
ChanLimit: uint16(cfg.MaxChannels),
Allocation: cfg.Allocation,
MaxPendingOpens: 10,
}
atplConstraints := autopilot.NewConstraints(
btcutil.Amount(cfg.MinChannelSize),
btcutil.Amount(cfg.MaxChannelSize),
uint16(cfg.MaxChannels),
10,
cfg.Allocation,
)
// First, we'll create the preferential attachment heuristic,
// initialized with the passed auto pilot configuration parameters.