package autopilot import ( "bytes" "fmt" prand "math/rand" "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil" ) // ConstrainedPrefAttachment is an implementation of the AttachmentHeuristic // interface that implement a constrained non-linear preferential attachment // heuristic. This means that given a threshold to allocate to automatic // channel establishment, the heuristic will attempt to favor connecting to // nodes which already have a set amount of links, selected by sampling from a // power law distribution. The attachment is non-linear in that it favors // nodes with a higher in-degree but less so that regular linear preferential // attachment. As a result, this creates smaller and less clusters than regular // linear preferential attachment. // // TODO(roasbeef): BA, with k=-3 type ConstrainedPrefAttachment struct { minChanSize btcutil.Amount maxChanSize btcutil.Amount chanLimit uint16 threshold float64 } // 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 { prand.Seed(time.Now().Unix()) return &ConstrainedPrefAttachment{ minChanSize: minChanSize, chanLimit: chanLimit, maxChanSize: maxChanSize, threshold: allocation, } } // A compile time assertion to ensure ConstrainedPrefAttachment meets the // AttachmentHeuristic interface. var _ AttachmentHeuristic = (*ConstrainedPrefAttachment)(nil) // NeedMoreChans is a predicate that should return true if, given the passed // parameters, and its internal state, more channels should be opened within // the channel graph. If the heuristic decides that we do indeed need more // channels, then the second argument returned will represent the amount of // additional funds to be used towards creating channels. // // NOTE: This is a part of the AttachmentHeuristic interface. func (p *ConstrainedPrefAttachment) NeedMoreChans(channels []Channel, funds btcutil.Amount) (btcutil.Amount, uint32, bool) { // 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 } // NodeID is a simple type that holds an EC public key serialized in compressed // format. type NodeID [33]byte // NewNodeID creates a new nodeID from a passed public key. func NewNodeID(pub *btcec.PublicKey) NodeID { var n NodeID copy(n[:], pub.SerializeCompressed()) return n } // shuffleCandidates shuffles the set of candidate nodes for preferential // attachment in order to break any ordering already enforced by the sorted // order of the public key for each node. To shuffle the set of candidates, we // use a version of the Fisher–Yates shuffle algorithm. func shuffleCandidates(candidates []Node) []Node { shuffledNodes := make([]Node, len(candidates)) perm := prand.Perm(len(candidates)) for i, v := range perm { shuffledNodes[v] = candidates[i] } return shuffledNodes } // Select returns a candidate set of attachment directives that should be // executed based on the current internal state, the state of the channel // graph, the set of nodes we should exclude, and the amount of funds // available. The heuristic employed by this method is one that attempts to // promote a scale-free network globally, via local attachment preferences for // new nodes joining the network with an amount of available funds to be // allocated to channels. Specifically, we consider the degree of each node // (and the flow in/out of the node available via its open channels) and // utilize the Barabási–Albert model to drive our recommended attachment // heuristics. If implemented globally for each new participant, this results // in a channel graph that is scale-free and follows a power law distribution // with k=-3. // // NOTE: This is a part of the AttachmentHeuristic interface. func (p *ConstrainedPrefAttachment) Select(self *btcec.PublicKey, g ChannelGraph, fundsAvailable btcutil.Amount, numNewChans uint32, skipNodes map[NodeID]struct{}) ([]AttachmentDirective, error) { // TODO(roasbeef): rename? var directives []AttachmentDirective if fundsAvailable < p.minChanSize { return directives, nil } selfPubBytes := self.SerializeCompressed() // We'll continue our attachment loop until we've exhausted the current // amount of available funds. visited := make(map[NodeID]struct{}) for i := uint32(0); i < numNewChans; i++ { // selectionSlice will be used to randomly select a node // according to a power law distribution. For each connected // edge, we'll add an instance of the node to this slice. Thus, // for a given node, the probability that we'll attach to it // is: k_i / sum(k_j), where k_i is the degree of the target // node, and k_j is the degree of all other nodes i != j. This // implements the classic Barabási–Albert model for // preferential attachment. var selectionSlice []Node // For each node, and each channel that the node has, we'll add // an instance of that node to the selection slice above. // This'll slice where the frequency of each node is equivalent // to the number of channels that connect to it. // // TODO(roasbeef): add noise to make adversarially resistant? if err := g.ForEachNode(func(node Node) error { nID := NodeID(node.PubKey()) // Once a node has already been attached to, we'll // ensure that it isn't factored into any further // decisions within this round. if _, ok := visited[nID]; ok { return nil } // If we come across ourselves, them we'll continue in // order to avoid attempting to make a channel with // ourselves. if bytes.Equal(nID[:], selfPubBytes) { return nil } // Additionally, if this node is in the blacklist, then // we'll skip it. if _, ok := skipNodes[nID]; ok { return nil } // For initial bootstrap purposes, if a node doesn't // have any channels, then we'll ensure that it has at // least one item in the selection slice. // // TODO(roasbeef): make conditional? selectionSlice = append(selectionSlice, node) // For each active channel the node has, we'll add an // additional channel to the selection slice to // increase their weight. if err := node.ForEachChannel(func(channel ChannelEdge) error { selectionSlice = append(selectionSlice, node) return nil }); err != nil { return err } return nil }); err != nil { return nil, err } // If no nodes at all were accumulated, then we'll exit early // as there are no eligible candidates. if len(selectionSlice) == 0 { break } // Given our selection slice, we'll now generate a random index // into this slice. The node we select will be recommended by // us to create a channel to. candidates := shuffleCandidates(selectionSlice) selectedIndex := prand.Int31n(int32(len(candidates))) selectedNode := candidates[selectedIndex] // TODO(roasbeef): cap on num channels to same participant? // With the node selected, we'll add this (node, amount) tuple // to out set of recommended directives. pubBytes := selectedNode.PubKey() pub, err := btcec.ParsePubKey(pubBytes[:], btcec.S256()) if err != nil { return nil, err } directives = append(directives, AttachmentDirective{ // TODO(roasbeef): need curve? NodeKey: &btcec.PublicKey{ X: pub.X, Y: pub.Y, }, NodeID: NewNodeID(pub), Addrs: selectedNode.Addrs(), }) // With the node selected, we'll add it to the set of visited // nodes to avoid attaching to it again. visited[NodeID(pubBytes)] = struct{}{} } numSelectedNodes := int64(len(directives)) switch { // 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): for i := 0; i < int(numSelectedNodes); i++ { directives[i].ChanAmt = p.maxChanSize } return directives, nil // 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): i := 0 for fundsAvailable > p.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 if fundsAvailable-delta < 0 { delta = fundsAvailable } directives[i].ChanAmt = delta fundsAvailable -= delta i++ } // We'll slice the initial set of directives to properly // reflect the amount of funds we were able to allocate. return directives[:i:i], nil default: return nil, fmt.Errorf("err") } }