7a800f98f6
* les/utils: Limiter * les/utils: dropped prior weight vs variable cost logic, using fixed weights * les/utils: always create node selector in addressGroup * les/utils: renamed request weight to request cost * les/utils: simplified and improved the DoS penalty mechanism * les/utils: minor fixes * les/utils: made selection weight calculation nicer * les/utils: fixed linter warning * les/utils: more precise and reliable probabilistic test * les/utils: fixed linter warning
184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
// Copyright 2016 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"math"
|
|
"math/rand"
|
|
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
type (
|
|
// WeightedRandomSelect is capable of weighted random selection from a set of items
|
|
WeightedRandomSelect struct {
|
|
root *wrsNode
|
|
idx map[WrsItem]int
|
|
wfn WeightFn
|
|
}
|
|
WrsItem interface{}
|
|
WeightFn func(interface{}) uint64
|
|
)
|
|
|
|
// NewWeightedRandomSelect returns a new WeightedRandomSelect structure
|
|
func NewWeightedRandomSelect(wfn WeightFn) *WeightedRandomSelect {
|
|
return &WeightedRandomSelect{root: &wrsNode{maxItems: wrsBranches}, idx: make(map[WrsItem]int), wfn: wfn}
|
|
}
|
|
|
|
// Update updates an item's weight, adds it if it was non-existent or removes it if
|
|
// the new weight is zero. Note that explicitly updating decreasing weights is not necessary.
|
|
func (w *WeightedRandomSelect) Update(item WrsItem) {
|
|
w.setWeight(item, w.wfn(item))
|
|
}
|
|
|
|
// Remove removes an item from the set
|
|
func (w *WeightedRandomSelect) Remove(item WrsItem) {
|
|
w.setWeight(item, 0)
|
|
}
|
|
|
|
// IsEmpty returns true if the set is empty
|
|
func (w *WeightedRandomSelect) IsEmpty() bool {
|
|
return w.root.sumCost == 0
|
|
}
|
|
|
|
// setWeight sets an item's weight to a specific value (removes it if zero)
|
|
func (w *WeightedRandomSelect) setWeight(item WrsItem, weight uint64) {
|
|
if weight > math.MaxInt64-w.root.sumCost {
|
|
// old weight is still included in sumCost, remove and check again
|
|
w.setWeight(item, 0)
|
|
if weight > math.MaxInt64-w.root.sumCost {
|
|
log.Error("WeightedRandomSelect overflow", "sumCost", w.root.sumCost, "new weight", weight)
|
|
weight = math.MaxInt64 - w.root.sumCost
|
|
}
|
|
}
|
|
idx, ok := w.idx[item]
|
|
if ok {
|
|
w.root.setWeight(idx, weight)
|
|
if weight == 0 {
|
|
delete(w.idx, item)
|
|
}
|
|
} else {
|
|
if weight != 0 {
|
|
if w.root.itemCnt == w.root.maxItems {
|
|
// add a new level
|
|
newRoot := &wrsNode{sumCost: w.root.sumCost, itemCnt: w.root.itemCnt, level: w.root.level + 1, maxItems: w.root.maxItems * wrsBranches}
|
|
newRoot.items[0] = w.root
|
|
newRoot.weights[0] = w.root.sumCost
|
|
w.root = newRoot
|
|
}
|
|
w.idx[item] = w.root.insert(item, weight)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Choose randomly selects an item from the set, with a chance proportional to its
|
|
// current weight. If the weight of the chosen element has been decreased since the
|
|
// last stored value, returns it with a newWeight/oldWeight chance, otherwise just
|
|
// updates its weight and selects another one
|
|
func (w *WeightedRandomSelect) Choose() WrsItem {
|
|
for {
|
|
if w.root.sumCost == 0 {
|
|
return nil
|
|
}
|
|
val := uint64(rand.Int63n(int64(w.root.sumCost)))
|
|
choice, lastWeight := w.root.choose(val)
|
|
weight := w.wfn(choice)
|
|
if weight != lastWeight {
|
|
w.setWeight(choice, weight)
|
|
}
|
|
if weight >= lastWeight || uint64(rand.Int63n(int64(lastWeight))) < weight {
|
|
return choice
|
|
}
|
|
}
|
|
}
|
|
|
|
const wrsBranches = 8 // max number of branches in the wrsNode tree
|
|
|
|
// wrsNode is a node of a tree structure that can store WrsItems or further wrsNodes.
|
|
type wrsNode struct {
|
|
items [wrsBranches]interface{}
|
|
weights [wrsBranches]uint64
|
|
sumCost uint64
|
|
level, itemCnt, maxItems int
|
|
}
|
|
|
|
// insert recursively inserts a new item to the tree and returns the item index
|
|
func (n *wrsNode) insert(item WrsItem, weight uint64) int {
|
|
branch := 0
|
|
for n.items[branch] != nil && (n.level == 0 || n.items[branch].(*wrsNode).itemCnt == n.items[branch].(*wrsNode).maxItems) {
|
|
branch++
|
|
if branch == wrsBranches {
|
|
panic(nil)
|
|
}
|
|
}
|
|
n.itemCnt++
|
|
n.sumCost += weight
|
|
n.weights[branch] += weight
|
|
if n.level == 0 {
|
|
n.items[branch] = item
|
|
return branch
|
|
}
|
|
var subNode *wrsNode
|
|
if n.items[branch] == nil {
|
|
subNode = &wrsNode{maxItems: n.maxItems / wrsBranches, level: n.level - 1}
|
|
n.items[branch] = subNode
|
|
} else {
|
|
subNode = n.items[branch].(*wrsNode)
|
|
}
|
|
subIdx := subNode.insert(item, weight)
|
|
return subNode.maxItems*branch + subIdx
|
|
}
|
|
|
|
// setWeight updates the weight of a certain item (which should exist) and returns
|
|
// the change of the last weight value stored in the tree
|
|
func (n *wrsNode) setWeight(idx int, weight uint64) uint64 {
|
|
if n.level == 0 {
|
|
oldWeight := n.weights[idx]
|
|
n.weights[idx] = weight
|
|
diff := weight - oldWeight
|
|
n.sumCost += diff
|
|
if weight == 0 {
|
|
n.items[idx] = nil
|
|
n.itemCnt--
|
|
}
|
|
return diff
|
|
}
|
|
branchItems := n.maxItems / wrsBranches
|
|
branch := idx / branchItems
|
|
diff := n.items[branch].(*wrsNode).setWeight(idx-branch*branchItems, weight)
|
|
n.weights[branch] += diff
|
|
n.sumCost += diff
|
|
if weight == 0 {
|
|
n.itemCnt--
|
|
}
|
|
return diff
|
|
}
|
|
|
|
// choose recursively selects an item from the tree and returns it along with its weight
|
|
func (n *wrsNode) choose(val uint64) (WrsItem, uint64) {
|
|
for i, w := range n.weights {
|
|
if val < w {
|
|
if n.level == 0 {
|
|
return n.items[i].(WrsItem), n.weights[i]
|
|
}
|
|
return n.items[i].(*wrsNode).choose(val)
|
|
}
|
|
val -= w
|
|
}
|
|
panic(nil)
|
|
}
|