From 35f4ec84d15a14b3cdf97f4d298b40da38c3fc85 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 22 Nov 2018 23:18:08 +0100 Subject: [PATCH] autopilot/prefattach: use HeuristicConstraints This commit makes the pref attach heuristic and the agent use the HeuristicConstraints internally. --- autopilot/agent.go | 14 +++---- autopilot/agent_test.go | 60 ++++++++++++++++++++---------- autopilot/prefattach.go | 72 ++++++++---------------------------- autopilot/prefattach_test.go | 62 +++++++++++++++++++++++-------- pilot.go | 17 ++++++--- 5 files changed, 120 insertions(+), 105 deletions(-) diff --git a/autopilot/agent.go b/autopilot/agent.go index 6d888b35..12761db3 100644 --- a/autopilot/agent.go +++ b/autopilot/agent.go @@ -51,11 +51,9 @@ type Config struct { // within the graph. Graph ChannelGraph - // 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 + // Constraints is the set of constraints the autopilot must adhere to + // when opening channels. + Constraints *HeuristicConstraints // TODO(roasbeef): add additional signals from fee rates and revenue of // currently opened channels @@ -525,12 +523,12 @@ func (a *Agent) controller() { // connections succeed, we will they will be ignored and made // available to future heuristic selections. pendingMtx.Lock() - if uint16(len(pendingOpens)) >= a.cfg.MaxPendingOpens { + if uint16(len(pendingOpens)) >= a.cfg.Constraints.MaxPendingOpens { pendingMtx.Unlock() log.Debugf("Reached cap of %v pending "+ "channel opens, will retry "+ "after success/failure", - a.cfg.MaxPendingOpens) + a.cfg.Constraints.MaxPendingOpens) continue } @@ -587,7 +585,7 @@ func (a *Agent) controller() { // finished first. pendingMtx.Lock() if uint16(len(pendingOpens)) >= - a.cfg.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 diff --git a/autopilot/agent_test.go b/autopilot/agent_test.go index 16099eeb..3330ffd3 100644 --- a/autopilot/agent_test.go +++ b/autopilot/agent_test.go @@ -167,8 +167,10 @@ func TestAgentChannelOpenSignal(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} agent, err := New(testCfg, initialChans) @@ -288,8 +290,10 @@ func TestAgentChannelFailureSignal(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} @@ -389,8 +393,10 @@ func TestAgentChannelCloseSignal(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } // We'll start the agent with two channels already being active. @@ -503,8 +509,10 @@ func TestAgentBalanceUpdate(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} agent, err := New(testCfg, initialChans) @@ -608,8 +616,10 @@ func TestAgentImmediateAttach(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} agent, err := New(testCfg, initialChans) @@ -743,8 +753,10 @@ func TestAgentPrivateChannels(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } agent, err := New(cfg, nil) if err != nil { @@ -866,8 +878,10 @@ func TestAgentPendingChannelState(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} agent, err := New(testCfg, initialChans) @@ -1035,8 +1049,10 @@ func TestAgentPendingOpenChannel(t *testing.T) { WalletBalance: func() (btcutil.Amount, error) { return walletBalance, nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } agent, err := New(cfg, nil) if err != nil { @@ -1118,8 +1134,10 @@ func TestAgentOnNodeUpdates(t *testing.T) { WalletBalance: func() (btcutil.Amount, error) { return walletBalance, nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } agent, err := New(cfg, nil) if err != nil { @@ -1232,8 +1250,10 @@ func TestAgentSkipPendingConns(t *testing.T) { DisconnectPeer: func(*btcec.PublicKey) error { return nil }, - Graph: memGraph, - MaxPendingOpens: 10, + Graph: memGraph, + Constraints: &HeuristicConstraints{ + MaxPendingOpens: 10, + }, } initialChans := []Channel{} agent, err := New(testCfg, initialChans) diff --git a/autopilot/prefattach.go b/autopilot/prefattach.go index 44c428fe..85969992 100644 --- a/autopilot/prefattach.go +++ b/autopilot/prefattach.go @@ -22,28 +22,20 @@ import ( // // TODO(roasbeef): BA, with k=-3 type ConstrainedPrefAttachment struct { - minChanSize btcutil.Amount - maxChanSize btcutil.Amount - - chanLimit uint16 - - threshold float64 + constraints *HeuristicConstraints } // NewConstrainedPrefAttachment creates a new instance of a // ConstrainedPrefAttachment heuristics given bounds on allowed channel sizes, // and an allocation amount which is interpreted as a percentage of funds that // is to be committed to channels at all times. -func NewConstrainedPrefAttachment(minChanSize, maxChanSize btcutil.Amount, - chanLimit uint16, allocation float64) *ConstrainedPrefAttachment { +func NewConstrainedPrefAttachment( + cfg *HeuristicConstraints) *ConstrainedPrefAttachment { prand.Seed(time.Now().Unix()) return &ConstrainedPrefAttachment{ - minChanSize: minChanSize, - chanLimit: chanLimit, - maxChanSize: maxChanSize, - threshold: allocation, + constraints: cfg, } } @@ -61,45 +53,11 @@ var _ AttachmentHeuristic = (*ConstrainedPrefAttachment)(nil) func (p *ConstrainedPrefAttachment) NeedMoreChans(channels []Channel, funds btcutil.Amount) (btcutil.Amount, uint32, bool) { - // 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(p.chanLimit) { - return 0, 0, false - } - - // 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(p.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 < p.threshold - if !needMore { - return 0, 0, false - } - - // 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) * p.threshold) - fundsAvailable := targetAllocation - totalChanAllocation - return fundsAvailable, numAdditionalChans, true + // 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 @@ -150,7 +108,7 @@ func (p *ConstrainedPrefAttachment) Select(self *btcec.PublicKey, g ChannelGraph var directives []AttachmentDirective - if fundsAvailable < p.minChanSize { + if fundsAvailable < p.constraints.MinChanSize { return directives, nil } @@ -263,9 +221,9 @@ func (p *ConstrainedPrefAttachment) Select(self *btcec.PublicKey, g ChannelGraph // If we have enough available funds to distribute the maximum channel // size for each of the selected peers to attach to, then we'll // allocate the maximum amount to each peer. - case int64(fundsAvailable) >= numSelectedNodes*int64(p.maxChanSize): + case int64(fundsAvailable) >= numSelectedNodes*int64(p.constraints.MaxChanSize): for i := 0; i < int(numSelectedNodes); i++ { - directives[i].ChanAmt = p.maxChanSize + directives[i].ChanAmt = p.constraints.MaxChanSize } return directives, nil @@ -273,14 +231,14 @@ func (p *ConstrainedPrefAttachment) Select(self *btcec.PublicKey, g ChannelGraph // Otherwise, we'll greedily allocate our funds to the channels // successively until we run out of available funds, or can't create a // channel above the min channel size. - case int64(fundsAvailable) < numSelectedNodes*int64(p.maxChanSize): + case int64(fundsAvailable) < numSelectedNodes*int64(p.constraints.MaxChanSize): i := 0 - for fundsAvailable > p.minChanSize { + for fundsAvailable > p.constraints.MinChanSize { // We'll attempt to allocate the max channel size // initially. If we don't have enough funds to do this, // then we'll allocate the remainder of the funds // available to the channel. - delta := p.maxChanSize + delta := p.constraints.MaxChanSize if fundsAvailable-delta < 0 { delta = fundsAvailable } diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index d3440277..6a6dc44c 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -29,6 +29,13 @@ func TestConstrainedPrefAttachmentNeedMoreChan(t *testing.T) { threshold = 0.5 ) + constraints := &HeuristicConstraints{ + MinChanSize: minChanSize, + MaxChanSize: maxChanSize, + ChanLimit: chanLimit, + Allocation: threshold, + } + randChanID := func() lnwire.ShortChannelID { return lnwire.NewShortChanIDFromInt(uint64(prand.Int63())) } @@ -146,8 +153,7 @@ func TestConstrainedPrefAttachmentNeedMoreChan(t *testing.T) { }, } - prefAttach := NewConstrainedPrefAttachment(minChanSize, maxChanSize, - chanLimit, threshold) + prefAttach := NewConstrainedPrefAttachment(constraints) for i, testCase := range testCases { amtToAllocate, numMore, needMore := prefAttach.NeedMoreChans( @@ -236,14 +242,20 @@ func TestConstrainedPrefAttachmentSelectEmptyGraph(t *testing.T) { threshold = 0.5 ) + constraints := &HeuristicConstraints{ + MinChanSize: minChanSize, + MaxChanSize: maxChanSize, + ChanLimit: chanLimit, + Allocation: threshold, + } + // First, we'll generate a random key that represents "us", and create // a new instance of the heuristic with our set parameters. self, err := randKey() if err != nil { t.Fatalf("unable to generate self key: %v", err) } - prefAttach := NewConstrainedPrefAttachment(minChanSize, maxChanSize, - chanLimit, threshold) + prefAttach := NewConstrainedPrefAttachment(constraints) skipNodes := make(map[NodeID]struct{}) for _, graph := range chanGraphs { @@ -296,6 +308,12 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) { threshold = 0.5 ) + constraints := &HeuristicConstraints{ + MinChanSize: minChanSize, + MaxChanSize: maxChanSize, + ChanLimit: chanLimit, + Allocation: threshold, + } skipNodes := make(map[NodeID]struct{}) for _, graph := range chanGraphs { success := t.Run(graph.name, func(t1 *testing.T) { @@ -314,8 +332,7 @@ func TestConstrainedPrefAttachmentSelectTwoVertexes(t *testing.T) { if err != nil { t1.Fatalf("unable to generate self key: %v", err) } - prefAttach := NewConstrainedPrefAttachment(minChanSize, maxChanSize, - chanLimit, threshold) + prefAttach := NewConstrainedPrefAttachment(constraints) // For this set, we'll load the memory graph with two // nodes, and a random channel connecting them. @@ -386,6 +403,13 @@ func TestConstrainedPrefAttachmentSelectInsufficientFunds(t *testing.T) { threshold = 0.5 ) + constraints := &HeuristicConstraints{ + MinChanSize: minChanSize, + MaxChanSize: maxChanSize, + ChanLimit: chanLimit, + Allocation: threshold, + } + skipNodes := make(map[NodeID]struct{}) for _, graph := range chanGraphs { success := t.Run(graph.name, func(t1 *testing.T) { @@ -404,9 +428,7 @@ func TestConstrainedPrefAttachmentSelectInsufficientFunds(t *testing.T) { if err != nil { t1.Fatalf("unable to generate self key: %v", err) } - prefAttach := NewConstrainedPrefAttachment( - minChanSize, maxChanSize, chanLimit, threshold, - ) + prefAttach := NewConstrainedPrefAttachment(constraints) // Next, we'll attempt to select a set of candidates, // passing zero for the amount of wallet funds. This @@ -445,6 +467,13 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) { threshold = 0.5 ) + constraints := &HeuristicConstraints{ + MinChanSize: minChanSize, + MaxChanSize: maxChanSize, + ChanLimit: chanLimit, + Allocation: threshold, + } + skipNodes := make(map[NodeID]struct{}) for _, graph := range chanGraphs { success := t.Run(graph.name, func(t1 *testing.T) { @@ -463,9 +492,7 @@ func TestConstrainedPrefAttachmentSelectGreedyAllocation(t *testing.T) { if err != nil { t1.Fatalf("unable to generate self key: %v", err) } - prefAttach := NewConstrainedPrefAttachment( - minChanSize, maxChanSize, chanLimit, threshold, - ) + prefAttach := NewConstrainedPrefAttachment(constraints) const chanCapacity = btcutil.SatoshiPerBitcoin @@ -581,6 +608,13 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) { 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) { skipNodes := make(map[NodeID]struct{}) @@ -600,9 +634,7 @@ func TestConstrainedPrefAttachmentSelectSkipNodes(t *testing.T) { if err != nil { t1.Fatalf("unable to generate self key: %v", err) } - prefAttach := NewConstrainedPrefAttachment( - minChanSize, maxChanSize, chanLimit, threshold, - ) + prefAttach := NewConstrainedPrefAttachment(constraints) // Next, we'll create a simple topology of two nodes, // with a single channel connecting them. diff --git a/pilot.go b/pilot.go index c745082c..174452d7 100644 --- a/pilot.go +++ b/pilot.go @@ -85,12 +85,19 @@ var _ autopilot.ChannelController = (*chanController)(nil) func initAutoPilot(svr *server, cfg *autoPilotConfig) (*autopilot.Agent, error) { atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg)) + // Set up the constraints the autopilot heuristics must adhere to. + atplConstraints := &autopilot.HeuristicConstraints{ + MinChanSize: btcutil.Amount(cfg.MinChannelSize), + MaxChanSize: btcutil.Amount(cfg.MaxChannelSize), + ChanLimit: uint16(cfg.MaxChannels), + Allocation: cfg.Allocation, + MaxPendingOpens: 10, + } + // First, we'll create the preferential attachment heuristic, // initialized with the passed auto pilot configuration parameters. prefAttachment := autopilot.NewConstrainedPrefAttachment( - btcutil.Amount(cfg.MinChannelSize), - btcutil.Amount(cfg.MaxChannelSize), - uint16(cfg.MaxChannels), cfg.Allocation, + atplConstraints, ) // With the heuristic itself created, we can now populate the remainder @@ -107,8 +114,8 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) (*autopilot.Agent, error) WalletBalance: func() (btcutil.Amount, error) { return svr.cc.wallet.ConfirmedBalance(cfg.MinConfs) }, - Graph: autopilot.ChannelGraphFromDatabase(svr.chanDB.ChannelGraph()), - MaxPendingOpens: 10, + Graph: autopilot.ChannelGraphFromDatabase(svr.chanDB.ChannelGraph()), + Constraints: atplConstraints, ConnectToPeer: func(target *btcec.PublicKey, addrs []net.Addr) (bool, error) { // First, we'll check if we're already connected to the // target peer. If we are, we can exit early. Otherwise,