From 1d82e12fcfb4b896fa090d64088c77aee755ea70 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 19 Dec 2018 14:54:53 +0100 Subject: [PATCH] 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. --- autopilot/agent.go | 8 +- autopilot/agent_constraints.go | 151 +++++++++++++++++++++++++++++ autopilot/agent_test.go | 101 +++++++++++-------- autopilot/heuristic_constraints.go | 76 --------------- autopilot/prefattach.go | 10 +- autopilot/prefattach_test.go | 79 ++++++++------- pilot.go | 14 +-- 7 files changed, 271 insertions(+), 168 deletions(-) create mode 100644 autopilot/agent_constraints.go delete mode 100644 autopilot/heuristic_constraints.go diff --git a/autopilot/agent.go b/autopilot/agent.go index 56038c01..d384abde 100644 --- a/autopilot/agent.go +++ b/autopilot/agent.go @@ -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 diff --git a/autopilot/agent_constraints.go b/autopilot/agent_constraints.go new file mode 100644 index 00000000..914a16ce --- /dev/null +++ b/autopilot/agent_constraints.go @@ -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 +} diff --git a/autopilot/agent_test.go b/autopilot/agent_test.go index 1a3c8d75..c975326b 100644 --- a/autopilot/agent_test.go +++ b/autopilot/agent_test.go @@ -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) diff --git a/autopilot/heuristic_constraints.go b/autopilot/heuristic_constraints.go deleted file mode 100644 index 916f8507..00000000 --- a/autopilot/heuristic_constraints.go +++ /dev/null @@ -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 -} diff --git a/autopilot/prefattach.go b/autopilot/prefattach.go index 65676307..3b5ee9a7 100644 --- a/autopilot/prefattach.go +++ b/autopilot/prefattach.go @@ -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 diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index f485ce7d..0fbf68e3 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -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) { diff --git a/pilot.go b/pilot.go index 9f352742..e68a3d2b 100644 --- a/pilot.go +++ b/pilot.go @@ -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.