mirror of https://github.com/ethereum/go-ethereum
les: move client pool to les/vflux/server (#22495)
* les: move client pool to les/vflux/server * les/vflux/server: un-expose NodeBalance, remove unused fn, fix bugs * tests/fuzzers/vflux: add ClientPool fuzzer * les/vflux/server: fixed balance tests * les: rebase fix * les/vflux/server: fixed more bugs * les/vflux/server: unexported NodeStateMachine fields and flags * les/vflux/server: unexport all internal components and functions * les/vflux/server: fixed priorityPool test * les/vflux/server: polish balance * les/vflux/server: fixed mutex locking error * les/vflux/server: priorityPool bug fixed * common/prque: make Prque wrap-around priority handling optional * les/vflux/server: rename funcs, small optimizations * les/vflux/server: fixed timeUntil * les/vflux/server: separated balance.posValue and negValue * les/vflux/server: polish setup * les/vflux/server: enforce capacity curve monotonicity * les/vflux/server: simplified requestCapacity * les/vflux/server: requestCapacity with target range, no iterations in SetCapacity * les/vflux/server: minor changes * les/vflux/server: moved default factors to balanceTracker * les/vflux/server: set inactiveFlag in priorityPool * les/vflux/server: moved related metrics to vfs package * les/vflux/client: make priorityPool temp state logic cleaner * les/vflux/server: changed log.Crit to log.Error * add vflux fuzzer to oss-fuzz Co-authored-by: rjl493456442 <garyrong0905@gmail.com>pull/22634/head
parent
e275b1a293
commit
2d89fe0883
@ -1,453 +0,0 @@ |
|||||||
// Copyright 2019 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 les |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock" |
|
||||||
"github.com/ethereum/go-ethereum/ethdb" |
|
||||||
"github.com/ethereum/go-ethereum/les/utils" |
|
||||||
"github.com/ethereum/go-ethereum/les/vflux" |
|
||||||
vfs "github.com/ethereum/go-ethereum/les/vflux/server" |
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/nodestate" |
|
||||||
"github.com/ethereum/go-ethereum/rlp" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance
|
|
||||||
|
|
||||||
// defaultConnectedBias is applied to already connected clients So that
|
|
||||||
// already connected client won't be kicked out very soon and we
|
|
||||||
// can ensure all connected clients can have enough time to request
|
|
||||||
// or sync some data.
|
|
||||||
//
|
|
||||||
// todo(rjl493456442) make it configurable. It can be the option of
|
|
||||||
// free trial time!
|
|
||||||
defaultConnectedBias = time.Minute * 3 |
|
||||||
inactiveTimeout = time.Second * 10 |
|
||||||
) |
|
||||||
|
|
||||||
// clientPool implements a client database that assigns a priority to each client
|
|
||||||
// based on a positive and negative balance. Positive balance is externally assigned
|
|
||||||
// to prioritized clients and is decreased with connection time and processed
|
|
||||||
// requests (unless the price factors are zero). If the positive balance is zero
|
|
||||||
// then negative balance is accumulated.
|
|
||||||
//
|
|
||||||
// Balance tracking and priority calculation for connected clients is done by
|
|
||||||
// balanceTracker. activeQueue ensures that clients with the lowest positive or
|
|
||||||
// highest negative balance get evicted when the total capacity allowance is full
|
|
||||||
// and new clients with a better balance want to connect.
|
|
||||||
//
|
|
||||||
// Already connected nodes receive a small bias in their favor in order to avoid
|
|
||||||
// accepting and instantly kicking out clients. In theory, we try to ensure that
|
|
||||||
// each client can have several minutes of connection time.
|
|
||||||
//
|
|
||||||
// Balances of disconnected clients are stored in nodeDB including positive balance
|
|
||||||
// and negative banalce. Boeth positive balance and negative balance will decrease
|
|
||||||
// exponentially. If the balance is low enough, then the record will be dropped.
|
|
||||||
type clientPool struct { |
|
||||||
vfs.BalanceTrackerSetup |
|
||||||
vfs.PriorityPoolSetup |
|
||||||
lock sync.Mutex |
|
||||||
clock mclock.Clock |
|
||||||
closed bool |
|
||||||
removePeer func(enode.ID) |
|
||||||
synced func() bool |
|
||||||
ns *nodestate.NodeStateMachine |
|
||||||
pp *vfs.PriorityPool |
|
||||||
bt *vfs.BalanceTracker |
|
||||||
|
|
||||||
defaultPosFactors, defaultNegFactors vfs.PriceFactors |
|
||||||
posExpTC, negExpTC uint64 |
|
||||||
minCap uint64 // The minimal capacity value allowed for any client
|
|
||||||
connectedBias time.Duration |
|
||||||
capLimit uint64 |
|
||||||
} |
|
||||||
|
|
||||||
// clientPoolPeer represents a client peer in the pool.
|
|
||||||
// Positive balances are assigned to node key while negative balances are assigned
|
|
||||||
// to freeClientId. Currently network IP address without port is used because
|
|
||||||
// clients have a limited access to IP addresses while new node keys can be easily
|
|
||||||
// generated so it would be useless to assign a negative value to them.
|
|
||||||
type clientPoolPeer interface { |
|
||||||
Node() *enode.Node |
|
||||||
freeClientId() string |
|
||||||
updateCapacity(uint64) |
|
||||||
freeze() |
|
||||||
allowInactive() bool |
|
||||||
} |
|
||||||
|
|
||||||
// clientInfo defines all information required by clientpool.
|
|
||||||
type clientInfo struct { |
|
||||||
node *enode.Node |
|
||||||
address string |
|
||||||
peer clientPoolPeer |
|
||||||
connected, priority bool |
|
||||||
connectedAt mclock.AbsTime |
|
||||||
balance *vfs.NodeBalance |
|
||||||
} |
|
||||||
|
|
||||||
// newClientPool creates a new client pool
|
|
||||||
func newClientPool(ns *nodestate.NodeStateMachine, lesDb ethdb.Database, minCap uint64, connectedBias time.Duration, clock mclock.Clock, removePeer func(enode.ID), synced func() bool) *clientPool { |
|
||||||
pool := &clientPool{ |
|
||||||
ns: ns, |
|
||||||
BalanceTrackerSetup: balanceTrackerSetup, |
|
||||||
PriorityPoolSetup: priorityPoolSetup, |
|
||||||
clock: clock, |
|
||||||
minCap: minCap, |
|
||||||
connectedBias: connectedBias, |
|
||||||
removePeer: removePeer, |
|
||||||
synced: synced, |
|
||||||
} |
|
||||||
pool.bt = vfs.NewBalanceTracker(ns, balanceTrackerSetup, lesDb, clock, &utils.Expirer{}, &utils.Expirer{}) |
|
||||||
pool.pp = vfs.NewPriorityPool(ns, priorityPoolSetup, clock, minCap, connectedBias, 4) |
|
||||||
|
|
||||||
// set default expiration constants used by tests
|
|
||||||
// Note: server overwrites this if token sale is active
|
|
||||||
pool.bt.SetExpirationTCs(0, defaultNegExpTC) |
|
||||||
|
|
||||||
ns.SubscribeState(pool.InactiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { |
|
||||||
if newState.Equals(pool.InactiveFlag) { |
|
||||||
ns.AddTimeout(node, pool.InactiveFlag, inactiveTimeout) |
|
||||||
} |
|
||||||
if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.InactiveFlag.Or(pool.PriorityFlag)) { |
|
||||||
ns.SetStateSub(node, pool.InactiveFlag, nodestate.Flags{}, 0) // remove timeout
|
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
ns.SubscribeState(pool.ActiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { |
|
||||||
c, _ := ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c == nil { |
|
||||||
return |
|
||||||
} |
|
||||||
c.priority = newState.HasAll(pool.PriorityFlag) |
|
||||||
if newState.Equals(pool.ActiveFlag) { |
|
||||||
cap, _ := ns.GetField(node, pool.CapacityField).(uint64) |
|
||||||
if cap > minCap { |
|
||||||
pool.pp.RequestCapacity(node, minCap, 0, true) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
ns.SubscribeState(pool.InactiveFlag.Or(pool.ActiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { |
|
||||||
if oldState.IsEmpty() { |
|
||||||
clientConnectedMeter.Mark(1) |
|
||||||
log.Debug("Client connected", "id", node.ID()) |
|
||||||
} |
|
||||||
if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.ActiveFlag) { |
|
||||||
clientActivatedMeter.Mark(1) |
|
||||||
log.Debug("Client activated", "id", node.ID()) |
|
||||||
} |
|
||||||
if oldState.Equals(pool.ActiveFlag) && newState.Equals(pool.InactiveFlag) { |
|
||||||
clientDeactivatedMeter.Mark(1) |
|
||||||
log.Debug("Client deactivated", "id", node.ID()) |
|
||||||
c, _ := ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c == nil || !c.peer.allowInactive() { |
|
||||||
pool.removePeer(node.ID()) |
|
||||||
} |
|
||||||
} |
|
||||||
if newState.IsEmpty() { |
|
||||||
clientDisconnectedMeter.Mark(1) |
|
||||||
log.Debug("Client disconnected", "id", node.ID()) |
|
||||||
pool.removePeer(node.ID()) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
var totalConnected uint64 |
|
||||||
ns.SubscribeField(pool.CapacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { |
|
||||||
oldCap, _ := oldValue.(uint64) |
|
||||||
newCap, _ := newValue.(uint64) |
|
||||||
totalConnected += newCap - oldCap |
|
||||||
totalConnectedGauge.Update(int64(totalConnected)) |
|
||||||
c, _ := ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c != nil { |
|
||||||
c.peer.updateCapacity(newCap) |
|
||||||
} |
|
||||||
}) |
|
||||||
return pool |
|
||||||
} |
|
||||||
|
|
||||||
// stop shuts the client pool down
|
|
||||||
func (f *clientPool) stop() { |
|
||||||
f.lock.Lock() |
|
||||||
f.closed = true |
|
||||||
f.lock.Unlock() |
|
||||||
f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { |
|
||||||
// enforces saving all balances in BalanceTracker
|
|
||||||
f.disconnectNode(node) |
|
||||||
}) |
|
||||||
f.bt.Stop() |
|
||||||
} |
|
||||||
|
|
||||||
// connect should be called after a successful handshake. If the connection was
|
|
||||||
// rejected, there is no need to call disconnect.
|
|
||||||
func (f *clientPool) connect(peer clientPoolPeer) (uint64, error) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
// Short circuit if clientPool is already closed.
|
|
||||||
if f.closed { |
|
||||||
return 0, fmt.Errorf("Client pool is already closed") |
|
||||||
} |
|
||||||
// Dedup connected peers.
|
|
||||||
node, freeID := peer.Node(), peer.freeClientId() |
|
||||||
if f.ns.GetField(node, clientInfoField) != nil { |
|
||||||
log.Debug("Client already connected", "address", freeID, "id", node.ID().String()) |
|
||||||
return 0, fmt.Errorf("Client already connected address=%s id=%s", freeID, node.ID().String()) |
|
||||||
} |
|
||||||
now := f.clock.Now() |
|
||||||
c := &clientInfo{ |
|
||||||
node: node, |
|
||||||
address: freeID, |
|
||||||
peer: peer, |
|
||||||
connected: true, |
|
||||||
connectedAt: now, |
|
||||||
} |
|
||||||
f.ns.SetField(node, clientInfoField, c) |
|
||||||
f.ns.SetField(node, connAddressField, freeID) |
|
||||||
if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*vfs.NodeBalance); c.balance == nil { |
|
||||||
f.disconnect(peer) |
|
||||||
return 0, nil |
|
||||||
} |
|
||||||
c.balance.SetPriceFactors(f.defaultPosFactors, f.defaultNegFactors) |
|
||||||
|
|
||||||
f.ns.SetState(node, f.InactiveFlag, nodestate.Flags{}, 0) |
|
||||||
var allowed bool |
|
||||||
f.ns.Operation(func() { |
|
||||||
_, allowed = f.pp.RequestCapacity(node, f.minCap, f.connectedBias, true) |
|
||||||
}) |
|
||||||
if allowed { |
|
||||||
return f.minCap, nil |
|
||||||
} |
|
||||||
if !peer.allowInactive() { |
|
||||||
f.disconnect(peer) |
|
||||||
} |
|
||||||
return 0, nil |
|
||||||
} |
|
||||||
|
|
||||||
// setConnectedBias sets the connection bias, which is applied to already connected clients
|
|
||||||
// So that already connected client won't be kicked out very soon and we can ensure all
|
|
||||||
// connected clients can have enough time to request or sync some data.
|
|
||||||
func (f *clientPool) setConnectedBias(bias time.Duration) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
f.connectedBias = bias |
|
||||||
f.pp.SetActiveBias(bias) |
|
||||||
} |
|
||||||
|
|
||||||
// disconnect should be called when a connection is terminated. If the disconnection
|
|
||||||
// was initiated by the pool itself using disconnectFn then calling disconnect is
|
|
||||||
// not necessary but permitted.
|
|
||||||
func (f *clientPool) disconnect(p clientPoolPeer) { |
|
||||||
f.disconnectNode(p.Node()) |
|
||||||
} |
|
||||||
|
|
||||||
// disconnectNode removes node fields and flags related to connected status
|
|
||||||
func (f *clientPool) disconnectNode(node *enode.Node) { |
|
||||||
f.ns.SetField(node, connAddressField, nil) |
|
||||||
f.ns.SetField(node, clientInfoField, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// setDefaultFactors sets the default price factors applied to subsequently connected clients
|
|
||||||
func (f *clientPool) setDefaultFactors(posFactors, negFactors vfs.PriceFactors) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
f.defaultPosFactors = posFactors |
|
||||||
f.defaultNegFactors = negFactors |
|
||||||
} |
|
||||||
|
|
||||||
// capacityInfo returns the total capacity allowance, the total capacity of connected
|
|
||||||
// clients and the total capacity of connected and prioritized clients
|
|
||||||
func (f *clientPool) capacityInfo() (uint64, uint64, uint64) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
// total priority active cap will be supported when the token issuer module is added
|
|
||||||
_, activeCap := f.pp.Active() |
|
||||||
return f.capLimit, activeCap, 0 |
|
||||||
} |
|
||||||
|
|
||||||
// setLimits sets the maximum number and total capacity of connected clients,
|
|
||||||
// dropping some of them if necessary.
|
|
||||||
func (f *clientPool) setLimits(totalConn int, totalCap uint64) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
f.capLimit = totalCap |
|
||||||
f.pp.SetLimits(uint64(totalConn), totalCap) |
|
||||||
} |
|
||||||
|
|
||||||
// setCapacity sets the assigned capacity of a connected client
|
|
||||||
func (f *clientPool) setCapacity(node *enode.Node, freeID string, capacity uint64, bias time.Duration, setCap bool) (uint64, error) { |
|
||||||
c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c == nil { |
|
||||||
if setCap { |
|
||||||
return 0, fmt.Errorf("client %064x is not connected", node.ID()) |
|
||||||
} |
|
||||||
c = &clientInfo{node: node} |
|
||||||
f.ns.SetField(node, clientInfoField, c) |
|
||||||
f.ns.SetField(node, connAddressField, freeID) |
|
||||||
if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*vfs.NodeBalance); c.balance == nil { |
|
||||||
log.Error("BalanceField is missing", "node", node.ID()) |
|
||||||
return 0, fmt.Errorf("BalanceField of %064x is missing", node.ID()) |
|
||||||
} |
|
||||||
defer func() { |
|
||||||
f.ns.SetField(node, connAddressField, nil) |
|
||||||
f.ns.SetField(node, clientInfoField, nil) |
|
||||||
}() |
|
||||||
} |
|
||||||
var ( |
|
||||||
minPriority int64 |
|
||||||
allowed bool |
|
||||||
) |
|
||||||
f.ns.Operation(func() { |
|
||||||
if !setCap || c.priority { |
|
||||||
// check clientInfo.priority inside Operation to ensure thread safety
|
|
||||||
minPriority, allowed = f.pp.RequestCapacity(node, capacity, bias, setCap) |
|
||||||
} |
|
||||||
}) |
|
||||||
if allowed { |
|
||||||
return 0, nil |
|
||||||
} |
|
||||||
missing := c.balance.PosBalanceMissing(minPriority, capacity, bias) |
|
||||||
if missing < 1 { |
|
||||||
// ensure that we never return 0 missing and insufficient priority error
|
|
||||||
missing = 1 |
|
||||||
} |
|
||||||
return missing, errNoPriority |
|
||||||
} |
|
||||||
|
|
||||||
// setCapacityLocked is the equivalent of setCapacity used when f.lock is already locked
|
|
||||||
func (f *clientPool) setCapacityLocked(node *enode.Node, freeID string, capacity uint64, minConnTime time.Duration, setCap bool) (uint64, error) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
return f.setCapacity(node, freeID, capacity, minConnTime, setCap) |
|
||||||
} |
|
||||||
|
|
||||||
// forClients calls the supplied callback for either the listed node IDs or all connected
|
|
||||||
// nodes. It passes a valid clientInfo to the callback and ensures that the necessary
|
|
||||||
// fields and flags are set in order for BalanceTracker and PriorityPool to work even if
|
|
||||||
// the node is not connected.
|
|
||||||
func (f *clientPool) forClients(ids []enode.ID, cb func(client *clientInfo)) { |
|
||||||
f.lock.Lock() |
|
||||||
defer f.lock.Unlock() |
|
||||||
|
|
||||||
if len(ids) == 0 { |
|
||||||
f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { |
|
||||||
c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c != nil { |
|
||||||
cb(c) |
|
||||||
} |
|
||||||
}) |
|
||||||
} else { |
|
||||||
for _, id := range ids { |
|
||||||
node := f.ns.GetNode(id) |
|
||||||
if node == nil { |
|
||||||
node = enode.SignNull(&enr.Record{}, id) |
|
||||||
} |
|
||||||
c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c != nil { |
|
||||||
cb(c) |
|
||||||
} else { |
|
||||||
c = &clientInfo{node: node} |
|
||||||
f.ns.SetField(node, clientInfoField, c) |
|
||||||
f.ns.SetField(node, connAddressField, "") |
|
||||||
if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*vfs.NodeBalance); c.balance != nil { |
|
||||||
cb(c) |
|
||||||
} else { |
|
||||||
log.Error("BalanceField is missing") |
|
||||||
} |
|
||||||
f.ns.SetField(node, connAddressField, nil) |
|
||||||
f.ns.SetField(node, clientInfoField, nil) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// serveCapQuery serves a vflux capacity query. It receives multiple token amount values
|
|
||||||
// and a bias time value. For each given token amount it calculates the maximum achievable
|
|
||||||
// capacity in case the amount is added to the balance.
|
|
||||||
func (f *clientPool) serveCapQuery(id enode.ID, freeID string, data []byte) []byte { |
|
||||||
var req vflux.CapacityQueryReq |
|
||||||
if rlp.DecodeBytes(data, &req) != nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
if l := len(req.AddTokens); l == 0 || l > vflux.CapacityQueryMaxLen { |
|
||||||
return nil |
|
||||||
} |
|
||||||
result := make(vflux.CapacityQueryReply, len(req.AddTokens)) |
|
||||||
if !f.synced() { |
|
||||||
capacityQueryZeroMeter.Mark(1) |
|
||||||
reply, _ := rlp.EncodeToBytes(&result) |
|
||||||
return reply |
|
||||||
} |
|
||||||
|
|
||||||
node := f.ns.GetNode(id) |
|
||||||
if node == nil { |
|
||||||
node = enode.SignNull(&enr.Record{}, id) |
|
||||||
} |
|
||||||
c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) |
|
||||||
if c == nil { |
|
||||||
c = &clientInfo{node: node} |
|
||||||
f.ns.SetField(node, clientInfoField, c) |
|
||||||
f.ns.SetField(node, connAddressField, freeID) |
|
||||||
defer func() { |
|
||||||
f.ns.SetField(node, connAddressField, nil) |
|
||||||
f.ns.SetField(node, clientInfoField, nil) |
|
||||||
}() |
|
||||||
if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*vfs.NodeBalance); c.balance == nil { |
|
||||||
log.Error("BalanceField is missing", "node", node.ID()) |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
// use vfs.CapacityCurve to answer request for multiple newly bought token amounts
|
|
||||||
curve := f.pp.GetCapacityCurve().Exclude(id) |
|
||||||
bias := time.Second * time.Duration(req.Bias) |
|
||||||
if f.connectedBias > bias { |
|
||||||
bias = f.connectedBias |
|
||||||
} |
|
||||||
pb, _ := c.balance.GetBalance() |
|
||||||
for i, addTokens := range req.AddTokens { |
|
||||||
add := addTokens.Int64() |
|
||||||
result[i] = curve.MaxCapacity(func(capacity uint64) int64 { |
|
||||||
return c.balance.EstimatePriority(capacity, add, 0, bias, false) / int64(capacity) |
|
||||||
}) |
|
||||||
if add <= 0 && uint64(-add) >= pb && result[i] > f.minCap { |
|
||||||
result[i] = f.minCap |
|
||||||
} |
|
||||||
if result[i] < f.minCap { |
|
||||||
result[i] = 0 |
|
||||||
} |
|
||||||
} |
|
||||||
// add first result to metrics (don't care about priority client multi-queries yet)
|
|
||||||
if result[0] == 0 { |
|
||||||
capacityQueryZeroMeter.Mark(1) |
|
||||||
} else { |
|
||||||
capacityQueryNonZeroMeter.Mark(1) |
|
||||||
} |
|
||||||
reply, _ := rlp.EncodeToBytes(&result) |
|
||||||
return reply |
|
||||||
} |
|
@ -0,0 +1,335 @@ |
|||||||
|
// Copyright 2019 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 server |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/mclock" |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/ethereum/go-ethereum/les/utils" |
||||||
|
"github.com/ethereum/go-ethereum/les/vflux" |
||||||
|
"github.com/ethereum/go-ethereum/log" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enode" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/nodestate" |
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrNotConnected = errors.New("client not connected") |
||||||
|
ErrNoPriority = errors.New("priority too low to raise capacity") |
||||||
|
ErrCantFindMaximum = errors.New("Unable to find maximum allowed capacity") |
||||||
|
) |
||||||
|
|
||||||
|
// ClientPool implements a client database that assigns a priority to each client
|
||||||
|
// based on a positive and negative balance. Positive balance is externally assigned
|
||||||
|
// to prioritized clients and is decreased with connection time and processed
|
||||||
|
// requests (unless the price factors are zero). If the positive balance is zero
|
||||||
|
// then negative balance is accumulated.
|
||||||
|
//
|
||||||
|
// Balance tracking and priority calculation for connected clients is done by
|
||||||
|
// balanceTracker. PriorityQueue ensures that clients with the lowest positive or
|
||||||
|
// highest negative balance get evicted when the total capacity allowance is full
|
||||||
|
// and new clients with a better balance want to connect.
|
||||||
|
//
|
||||||
|
// Already connected nodes receive a small bias in their favor in order to avoid
|
||||||
|
// accepting and instantly kicking out clients. In theory, we try to ensure that
|
||||||
|
// each client can have several minutes of connection time.
|
||||||
|
//
|
||||||
|
// Balances of disconnected clients are stored in nodeDB including positive balance
|
||||||
|
// and negative banalce. Boeth positive balance and negative balance will decrease
|
||||||
|
// exponentially. If the balance is low enough, then the record will be dropped.
|
||||||
|
type ClientPool struct { |
||||||
|
*priorityPool |
||||||
|
*balanceTracker |
||||||
|
|
||||||
|
setup *serverSetup |
||||||
|
clock mclock.Clock |
||||||
|
closed bool |
||||||
|
ns *nodestate.NodeStateMachine |
||||||
|
synced func() bool |
||||||
|
|
||||||
|
lock sync.RWMutex |
||||||
|
connectedBias time.Duration |
||||||
|
|
||||||
|
minCap uint64 // the minimal capacity value allowed for any client
|
||||||
|
capReqNode *enode.Node // node that is requesting capacity change; only used inside NSM operation
|
||||||
|
} |
||||||
|
|
||||||
|
// clientPeer represents a peer in the client pool. None of the callbacks should block.
|
||||||
|
type clientPeer interface { |
||||||
|
Node() *enode.Node |
||||||
|
FreeClientId() string // unique id for non-priority clients (typically a prefix of the network address)
|
||||||
|
InactiveAllowance() time.Duration // disconnection timeout for inactive non-priority peers
|
||||||
|
UpdateCapacity(newCap uint64, requested bool) // signals a capacity update (requested is true if it is a result of a SetCapacity call on the given peer
|
||||||
|
Disconnect() // initiates disconnection (Unregister should always be called)
|
||||||
|
} |
||||||
|
|
||||||
|
// NewClientPool creates a new client pool
|
||||||
|
func NewClientPool(balanceDb ethdb.KeyValueStore, minCap uint64, connectedBias time.Duration, clock mclock.Clock, synced func() bool) *ClientPool { |
||||||
|
setup := newServerSetup() |
||||||
|
ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup) |
||||||
|
cp := &ClientPool{ |
||||||
|
priorityPool: newPriorityPool(ns, setup, clock, minCap, connectedBias, 4, 100), |
||||||
|
balanceTracker: newBalanceTracker(ns, setup, balanceDb, clock, &utils.Expirer{}, &utils.Expirer{}), |
||||||
|
setup: setup, |
||||||
|
ns: ns, |
||||||
|
clock: clock, |
||||||
|
minCap: minCap, |
||||||
|
connectedBias: connectedBias, |
||||||
|
synced: synced, |
||||||
|
} |
||||||
|
|
||||||
|
ns.SubscribeState(nodestate.MergeFlags(setup.activeFlag, setup.inactiveFlag, setup.priorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { |
||||||
|
if newState.Equals(setup.inactiveFlag) { |
||||||
|
// set timeout for non-priority inactive client
|
||||||
|
var timeout time.Duration |
||||||
|
if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { |
||||||
|
timeout = c.InactiveAllowance() |
||||||
|
} |
||||||
|
if timeout > 0 { |
||||||
|
ns.AddTimeout(node, setup.inactiveFlag, timeout) |
||||||
|
} else { |
||||||
|
// Note: if capacity is immediately available then priorityPool will set the active
|
||||||
|
// flag simultaneously with removing the inactive flag and therefore this will not
|
||||||
|
// initiate disconnection
|
||||||
|
ns.SetStateSub(node, nodestate.Flags{}, setup.inactiveFlag, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
if oldState.Equals(setup.inactiveFlag) && newState.Equals(setup.inactiveFlag.Or(setup.priorityFlag)) { |
||||||
|
ns.SetStateSub(node, setup.inactiveFlag, nodestate.Flags{}, 0) // priority gained; remove timeout
|
||||||
|
} |
||||||
|
if newState.Equals(setup.activeFlag) { |
||||||
|
// active with no priority; limit capacity to minCap
|
||||||
|
cap, _ := ns.GetField(node, setup.capacityField).(uint64) |
||||||
|
if cap > minCap { |
||||||
|
cp.requestCapacity(node, minCap, minCap, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
if newState.Equals(nodestate.Flags{}) { |
||||||
|
if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { |
||||||
|
c.Disconnect() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
ns.SubscribeField(setup.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { |
||||||
|
if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { |
||||||
|
newCap, _ := newValue.(uint64) |
||||||
|
c.UpdateCapacity(newCap, node == cp.capReqNode) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// add metrics
|
||||||
|
cp.ns.SubscribeState(nodestate.MergeFlags(cp.setup.activeFlag, cp.setup.inactiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { |
||||||
|
if oldState.IsEmpty() && !newState.IsEmpty() { |
||||||
|
clientConnectedMeter.Mark(1) |
||||||
|
} |
||||||
|
if !oldState.IsEmpty() && newState.IsEmpty() { |
||||||
|
clientDisconnectedMeter.Mark(1) |
||||||
|
} |
||||||
|
if oldState.HasNone(cp.setup.activeFlag) && oldState.HasAll(cp.setup.activeFlag) { |
||||||
|
clientActivatedMeter.Mark(1) |
||||||
|
} |
||||||
|
if oldState.HasAll(cp.setup.activeFlag) && oldState.HasNone(cp.setup.activeFlag) { |
||||||
|
clientDeactivatedMeter.Mark(1) |
||||||
|
} |
||||||
|
_, connected := cp.Active() |
||||||
|
totalConnectedGauge.Update(int64(connected)) |
||||||
|
}) |
||||||
|
return cp |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts the client pool. Should be called before Register/Unregister.
|
||||||
|
func (cp *ClientPool) Start() { |
||||||
|
cp.ns.Start() |
||||||
|
} |
||||||
|
|
||||||
|
// Stop shuts the client pool down. The clientPeer interface callbacks will not be called
|
||||||
|
// after Stop. Register calls will return nil.
|
||||||
|
func (cp *ClientPool) Stop() { |
||||||
|
cp.balanceTracker.stop() |
||||||
|
cp.ns.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
// Register registers the peer into the client pool. If the peer has insufficient
|
||||||
|
// priority and remains inactive for longer than the allowed timeout then it will be
|
||||||
|
// disconnected by calling the Disconnect function of the clientPeer interface.
|
||||||
|
func (cp *ClientPool) Register(peer clientPeer) ConnectedBalance { |
||||||
|
cp.ns.SetField(peer.Node(), cp.setup.clientField, peerWrapper{peer}) |
||||||
|
balance, _ := cp.ns.GetField(peer.Node(), cp.setup.balanceField).(*nodeBalance) |
||||||
|
return balance |
||||||
|
} |
||||||
|
|
||||||
|
// Unregister removes the peer from the client pool
|
||||||
|
func (cp *ClientPool) Unregister(peer clientPeer) { |
||||||
|
cp.ns.SetField(peer.Node(), cp.setup.clientField, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// setConnectedBias sets the connection bias, which is applied to already connected clients
|
||||||
|
// So that already connected client won't be kicked out very soon and we can ensure all
|
||||||
|
// connected clients can have enough time to request or sync some data.
|
||||||
|
func (cp *ClientPool) SetConnectedBias(bias time.Duration) { |
||||||
|
cp.lock.Lock() |
||||||
|
cp.connectedBias = bias |
||||||
|
cp.setActiveBias(bias) |
||||||
|
cp.lock.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// SetCapacity sets the assigned capacity of a connected client
|
||||||
|
func (cp *ClientPool) SetCapacity(node *enode.Node, reqCap uint64, bias time.Duration, requested bool) (capacity uint64, err error) { |
||||||
|
cp.lock.RLock() |
||||||
|
if cp.connectedBias > bias { |
||||||
|
bias = cp.connectedBias |
||||||
|
} |
||||||
|
cp.lock.RUnlock() |
||||||
|
|
||||||
|
cp.ns.Operation(func() { |
||||||
|
balance, _ := cp.ns.GetField(node, cp.setup.balanceField).(*nodeBalance) |
||||||
|
if balance == nil { |
||||||
|
err = ErrNotConnected |
||||||
|
return |
||||||
|
} |
||||||
|
capacity, _ = cp.ns.GetField(node, cp.setup.capacityField).(uint64) |
||||||
|
if capacity == 0 { |
||||||
|
// if the client is inactive then it has insufficient priority for the minimal capacity
|
||||||
|
// (will be activated automatically with minCap when possible)
|
||||||
|
return |
||||||
|
} |
||||||
|
if reqCap < cp.minCap { |
||||||
|
// can't request less than minCap; switching between 0 (inactive state) and minCap is
|
||||||
|
// performed by the server automatically as soon as necessary/possible
|
||||||
|
reqCap = cp.minCap |
||||||
|
} |
||||||
|
if reqCap > cp.minCap && cp.ns.GetState(node).HasNone(cp.setup.priorityFlag) { |
||||||
|
err = ErrNoPriority |
||||||
|
return |
||||||
|
} |
||||||
|
if reqCap == capacity { |
||||||
|
return |
||||||
|
} |
||||||
|
if requested { |
||||||
|
// mark the requested node so that the UpdateCapacity callback can signal
|
||||||
|
// whether the update is the direct result of a SetCapacity call on the given node
|
||||||
|
cp.capReqNode = node |
||||||
|
defer func() { |
||||||
|
cp.capReqNode = nil |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
var minTarget, maxTarget uint64 |
||||||
|
if reqCap > capacity { |
||||||
|
// Estimate maximum available capacity at the current priority level and request
|
||||||
|
// the estimated amount.
|
||||||
|
// Note: requestCapacity could find the highest available capacity between the
|
||||||
|
// current and the requested capacity but it could cost a lot of iterations with
|
||||||
|
// fine step adjustment if the requested capacity is very high. By doing a quick
|
||||||
|
// estimation of the maximum available capacity based on the capacity curve we
|
||||||
|
// can limit the number of required iterations.
|
||||||
|
curve := cp.getCapacityCurve().exclude(node.ID()) |
||||||
|
maxTarget = curve.maxCapacity(func(capacity uint64) int64 { |
||||||
|
return balance.estimatePriority(capacity, 0, 0, bias, false) |
||||||
|
}) |
||||||
|
if maxTarget <= capacity { |
||||||
|
return |
||||||
|
} |
||||||
|
if maxTarget > reqCap { |
||||||
|
maxTarget = reqCap |
||||||
|
} |
||||||
|
// Specify a narrow target range that allows a limited number of fine step
|
||||||
|
// iterations
|
||||||
|
minTarget = maxTarget - maxTarget/20 |
||||||
|
if minTarget < capacity { |
||||||
|
minTarget = capacity |
||||||
|
} |
||||||
|
} else { |
||||||
|
minTarget, maxTarget = reqCap, reqCap |
||||||
|
} |
||||||
|
if newCap := cp.requestCapacity(node, minTarget, maxTarget, bias); newCap >= minTarget && newCap <= maxTarget { |
||||||
|
capacity = newCap |
||||||
|
return |
||||||
|
} |
||||||
|
// we should be able to find the maximum allowed capacity in a few iterations
|
||||||
|
log.Error("Unable to find maximum allowed capacity") |
||||||
|
err = ErrCantFindMaximum |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// serveCapQuery serves a vflux capacity query. It receives multiple token amount values
|
||||||
|
// and a bias time value. For each given token amount it calculates the maximum achievable
|
||||||
|
// capacity in case the amount is added to the balance.
|
||||||
|
func (cp *ClientPool) serveCapQuery(id enode.ID, freeID string, data []byte) []byte { |
||||||
|
var req vflux.CapacityQueryReq |
||||||
|
if rlp.DecodeBytes(data, &req) != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
if l := len(req.AddTokens); l == 0 || l > vflux.CapacityQueryMaxLen { |
||||||
|
return nil |
||||||
|
} |
||||||
|
result := make(vflux.CapacityQueryReply, len(req.AddTokens)) |
||||||
|
if !cp.synced() { |
||||||
|
capacityQueryZeroMeter.Mark(1) |
||||||
|
reply, _ := rlp.EncodeToBytes(&result) |
||||||
|
return reply |
||||||
|
} |
||||||
|
|
||||||
|
bias := time.Second * time.Duration(req.Bias) |
||||||
|
cp.lock.RLock() |
||||||
|
if cp.connectedBias > bias { |
||||||
|
bias = cp.connectedBias |
||||||
|
} |
||||||
|
cp.lock.RUnlock() |
||||||
|
|
||||||
|
// use capacityCurve to answer request for multiple newly bought token amounts
|
||||||
|
curve := cp.getCapacityCurve().exclude(id) |
||||||
|
cp.BalanceOperation(id, freeID, func(balance AtomicBalanceOperator) { |
||||||
|
pb, _ := balance.GetBalance() |
||||||
|
for i, addTokens := range req.AddTokens { |
||||||
|
add := addTokens.Int64() |
||||||
|
result[i] = curve.maxCapacity(func(capacity uint64) int64 { |
||||||
|
return balance.estimatePriority(capacity, add, 0, bias, false) / int64(capacity) |
||||||
|
}) |
||||||
|
if add <= 0 && uint64(-add) >= pb && result[i] > cp.minCap { |
||||||
|
result[i] = cp.minCap |
||||||
|
} |
||||||
|
if result[i] < cp.minCap { |
||||||
|
result[i] = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
// add first result to metrics (don't care about priority client multi-queries yet)
|
||||||
|
if result[0] == 0 { |
||||||
|
capacityQueryZeroMeter.Mark(1) |
||||||
|
} else { |
||||||
|
capacityQueryNonZeroMeter.Mark(1) |
||||||
|
} |
||||||
|
reply, _ := rlp.EncodeToBytes(&result) |
||||||
|
return reply |
||||||
|
} |
||||||
|
|
||||||
|
// Handle implements Service
|
||||||
|
func (cp *ClientPool) Handle(id enode.ID, address string, name string, data []byte) []byte { |
||||||
|
switch name { |
||||||
|
case vflux.CapacityQueryName: |
||||||
|
return cp.serveCapQuery(id, address, data) |
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
// Copyright 2021 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 server |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/ethereum/go-ethereum/metrics" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
totalConnectedGauge = metrics.NewRegisteredGauge("vflux/server/totalConnected", nil) |
||||||
|
|
||||||
|
clientConnectedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/connected", nil) |
||||||
|
clientActivatedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/activated", nil) |
||||||
|
clientDeactivatedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/deactivated", nil) |
||||||
|
clientDisconnectedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/disconnected", nil) |
||||||
|
|
||||||
|
capacityQueryZeroMeter = metrics.NewRegisteredMeter("vflux/server/capQueryZero", nil) |
||||||
|
capacityQueryNonZeroMeter = metrics.NewRegisteredMeter("vflux/server/capQueryNonZero", nil) |
||||||
|
) |
@ -0,0 +1,59 @@ |
|||||||
|
// Copyright 2021 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 server |
||||||
|
|
||||||
|
import ( |
||||||
|
"reflect" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/nodestate" |
||||||
|
) |
||||||
|
|
||||||
|
type peerWrapper struct{ clientPeer } // the NodeStateMachine type system needs this wrapper
|
||||||
|
|
||||||
|
// serverSetup is a wrapper of the node state machine setup, which contains
|
||||||
|
// all the created flags and fields used in the vflux server side.
|
||||||
|
type serverSetup struct { |
||||||
|
setup *nodestate.Setup |
||||||
|
clientField nodestate.Field // Field contains the client peer handler
|
||||||
|
|
||||||
|
// Flags and fields controlled by balance tracker. BalanceTracker
|
||||||
|
// is responsible for setting/deleting these flags or fields.
|
||||||
|
priorityFlag nodestate.Flags // Flag is set if the node has a positive balance
|
||||||
|
updateFlag nodestate.Flags // Flag is set whenever the node balance is changed(priority changed)
|
||||||
|
balanceField nodestate.Field // Field contains the client balance for priority calculation
|
||||||
|
|
||||||
|
// Flags and fields controlled by priority queue. Priority queue
|
||||||
|
// is responsible for setting/deleting these flags or fields.
|
||||||
|
activeFlag nodestate.Flags // Flag is set if the node is active
|
||||||
|
inactiveFlag nodestate.Flags // Flag is set if the node is inactive
|
||||||
|
capacityField nodestate.Field // Field contains the capacity of the node
|
||||||
|
queueField nodestate.Field // Field contains the infomration in the priority queue
|
||||||
|
} |
||||||
|
|
||||||
|
// newServerSetup initializes the setup for state machine and returns the flags/fields group.
|
||||||
|
func newServerSetup() *serverSetup { |
||||||
|
setup := &serverSetup{setup: &nodestate.Setup{}} |
||||||
|
setup.clientField = setup.setup.NewField("client", reflect.TypeOf(peerWrapper{})) |
||||||
|
setup.priorityFlag = setup.setup.NewFlag("priority") |
||||||
|
setup.updateFlag = setup.setup.NewFlag("update") |
||||||
|
setup.balanceField = setup.setup.NewField("balance", reflect.TypeOf(&nodeBalance{})) |
||||||
|
setup.activeFlag = setup.setup.NewFlag("active") |
||||||
|
setup.inactiveFlag = setup.setup.NewFlag("inactive") |
||||||
|
setup.capacityField = setup.setup.NewField("capacity", reflect.TypeOf(uint64(0))) |
||||||
|
setup.queueField = setup.setup.NewField("queue", reflect.TypeOf(&ppNodeInfo{})) |
||||||
|
return setup |
||||||
|
} |
@ -0,0 +1,289 @@ |
|||||||
|
// Copyright 2021 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 vflux |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"io" |
||||||
|
"math" |
||||||
|
"math/big" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/mclock" |
||||||
|
"github.com/ethereum/go-ethereum/ethdb/memorydb" |
||||||
|
"github.com/ethereum/go-ethereum/les/vflux" |
||||||
|
vfs "github.com/ethereum/go-ethereum/les/vflux/server" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enode" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enr" |
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
) |
||||||
|
|
||||||
|
type fuzzer struct { |
||||||
|
peers [256]*clientPeer |
||||||
|
disconnectList []*clientPeer |
||||||
|
input io.Reader |
||||||
|
exhausted bool |
||||||
|
activeCount, activeCap uint64 |
||||||
|
maxCount, maxCap uint64 |
||||||
|
} |
||||||
|
|
||||||
|
type clientPeer struct { |
||||||
|
fuzzer *fuzzer |
||||||
|
node *enode.Node |
||||||
|
freeID string |
||||||
|
timeout time.Duration |
||||||
|
|
||||||
|
balance vfs.ConnectedBalance |
||||||
|
capacity uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func (p *clientPeer) Node() *enode.Node { |
||||||
|
return p.node |
||||||
|
} |
||||||
|
|
||||||
|
func (p *clientPeer) FreeClientId() string { |
||||||
|
return p.freeID |
||||||
|
} |
||||||
|
|
||||||
|
func (p *clientPeer) InactiveAllowance() time.Duration { |
||||||
|
return p.timeout |
||||||
|
} |
||||||
|
|
||||||
|
func (p *clientPeer) UpdateCapacity(newCap uint64, requested bool) { |
||||||
|
p.fuzzer.activeCap -= p.capacity |
||||||
|
if p.capacity != 0 { |
||||||
|
p.fuzzer.activeCount-- |
||||||
|
} |
||||||
|
p.capacity = newCap |
||||||
|
p.fuzzer.activeCap += p.capacity |
||||||
|
if p.capacity != 0 { |
||||||
|
p.fuzzer.activeCount++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *clientPeer) Disconnect() { |
||||||
|
p.fuzzer.disconnectList = append(p.fuzzer.disconnectList, p) |
||||||
|
p.fuzzer.activeCap -= p.capacity |
||||||
|
if p.capacity != 0 { |
||||||
|
p.fuzzer.activeCount-- |
||||||
|
} |
||||||
|
p.capacity = 0 |
||||||
|
p.balance = nil |
||||||
|
} |
||||||
|
|
||||||
|
func newFuzzer(input []byte) *fuzzer { |
||||||
|
f := &fuzzer{ |
||||||
|
input: bytes.NewReader(input), |
||||||
|
} |
||||||
|
for i := range f.peers { |
||||||
|
f.peers[i] = &clientPeer{ |
||||||
|
fuzzer: f, |
||||||
|
node: enode.SignNull(new(enr.Record), enode.ID{byte(i)}), |
||||||
|
freeID: string([]byte{byte(i)}), |
||||||
|
timeout: f.randomDelay(), |
||||||
|
} |
||||||
|
} |
||||||
|
return f |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) read(size int) []byte { |
||||||
|
out := make([]byte, size) |
||||||
|
if _, err := f.input.Read(out); err != nil { |
||||||
|
f.exhausted = true |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomByte() byte { |
||||||
|
d := f.read(1) |
||||||
|
return d[0] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomBool() bool { |
||||||
|
d := f.read(1) |
||||||
|
return d[0]&1 == 1 |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomInt(max int) int { |
||||||
|
if max == 0 { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
if max <= 256 { |
||||||
|
return int(f.randomByte()) % max |
||||||
|
} |
||||||
|
var a uint16 |
||||||
|
if err := binary.Read(f.input, binary.LittleEndian, &a); err != nil { |
||||||
|
f.exhausted = true |
||||||
|
} |
||||||
|
return int(a % uint16(max)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomTokenAmount(signed bool) int64 { |
||||||
|
x := uint64(f.randomInt(65000)) |
||||||
|
x = x * x * x * x |
||||||
|
|
||||||
|
if signed && (x&1) == 1 { |
||||||
|
if x <= math.MaxInt64 { |
||||||
|
return -int64(x) |
||||||
|
} |
||||||
|
return math.MinInt64 |
||||||
|
} |
||||||
|
if x <= math.MaxInt64 { |
||||||
|
return int64(x) |
||||||
|
} |
||||||
|
return math.MaxInt64 |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomDelay() time.Duration { |
||||||
|
delay := f.randomByte() |
||||||
|
if delay < 128 { |
||||||
|
return time.Duration(delay) * time.Second |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) randomFactors() vfs.PriceFactors { |
||||||
|
return vfs.PriceFactors{ |
||||||
|
TimeFactor: float64(f.randomByte()) / 25500, |
||||||
|
CapacityFactor: float64(f.randomByte()) / 255, |
||||||
|
RequestFactor: float64(f.randomByte()) / 255, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) connectedBalanceOp(balance vfs.ConnectedBalance) { |
||||||
|
switch f.randomInt(3) { |
||||||
|
case 0: |
||||||
|
balance.RequestServed(uint64(f.randomTokenAmount(false))) |
||||||
|
case 1: |
||||||
|
balance.SetPriceFactors(f.randomFactors(), f.randomFactors()) |
||||||
|
case 2: |
||||||
|
balance.GetBalance() |
||||||
|
balance.GetRawBalance() |
||||||
|
balance.GetPriceFactors() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *fuzzer) atomicBalanceOp(balance vfs.AtomicBalanceOperator) { |
||||||
|
switch f.randomInt(3) { |
||||||
|
case 0: |
||||||
|
balance.AddBalance(f.randomTokenAmount(true)) |
||||||
|
case 1: |
||||||
|
balance.SetBalance(uint64(f.randomTokenAmount(false)), uint64(f.randomTokenAmount(false))) |
||||||
|
case 2: |
||||||
|
balance.GetBalance() |
||||||
|
balance.GetRawBalance() |
||||||
|
balance.GetPriceFactors() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func FuzzClientPool(input []byte) int { |
||||||
|
if len(input) > 10000 { |
||||||
|
return -1 |
||||||
|
} |
||||||
|
f := newFuzzer(input) |
||||||
|
if f.exhausted { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
clock := &mclock.Simulated{} |
||||||
|
db := memorydb.New() |
||||||
|
pool := vfs.NewClientPool(db, 10, f.randomDelay(), clock, func() bool { return true }) |
||||||
|
pool.Start() |
||||||
|
defer pool.Stop() |
||||||
|
|
||||||
|
count := 0 |
||||||
|
for !f.exhausted && count < 1000 { |
||||||
|
count++ |
||||||
|
switch f.randomInt(11) { |
||||||
|
case 0: |
||||||
|
i := int(f.randomByte()) |
||||||
|
f.peers[i].balance = pool.Register(f.peers[i]) |
||||||
|
case 1: |
||||||
|
i := int(f.randomByte()) |
||||||
|
f.peers[i].Disconnect() |
||||||
|
case 2: |
||||||
|
f.maxCount = uint64(f.randomByte()) |
||||||
|
f.maxCap = uint64(f.randomByte()) |
||||||
|
f.maxCap *= f.maxCap |
||||||
|
pool.SetLimits(f.maxCount, f.maxCap) |
||||||
|
case 3: |
||||||
|
pool.SetConnectedBias(f.randomDelay()) |
||||||
|
case 4: |
||||||
|
pool.SetDefaultFactors(f.randomFactors(), f.randomFactors()) |
||||||
|
case 5: |
||||||
|
pool.SetExpirationTCs(uint64(f.randomInt(50000)), uint64(f.randomInt(50000))) |
||||||
|
case 6: |
||||||
|
if _, err := pool.SetCapacity(f.peers[f.randomByte()].node, uint64(f.randomByte()), f.randomDelay(), f.randomBool()); err == vfs.ErrCantFindMaximum { |
||||||
|
panic(nil) |
||||||
|
} |
||||||
|
case 7: |
||||||
|
if balance := f.peers[f.randomByte()].balance; balance != nil { |
||||||
|
f.connectedBalanceOp(balance) |
||||||
|
} |
||||||
|
case 8: |
||||||
|
pool.BalanceOperation(f.peers[f.randomByte()].node.ID(), f.peers[f.randomByte()].freeID, func(balance vfs.AtomicBalanceOperator) { |
||||||
|
count := f.randomInt(4) |
||||||
|
for i := 0; i < count; i++ { |
||||||
|
f.atomicBalanceOp(balance) |
||||||
|
} |
||||||
|
}) |
||||||
|
case 9: |
||||||
|
pool.TotalTokenAmount() |
||||||
|
pool.GetExpirationTCs() |
||||||
|
pool.Active() |
||||||
|
pool.Limits() |
||||||
|
pool.GetPosBalanceIDs(f.peers[f.randomByte()].node.ID(), f.peers[f.randomByte()].node.ID(), f.randomInt(100)) |
||||||
|
case 10: |
||||||
|
req := vflux.CapacityQueryReq{ |
||||||
|
Bias: uint64(f.randomByte()), |
||||||
|
AddTokens: make([]vflux.IntOrInf, f.randomInt(vflux.CapacityQueryMaxLen+1)), |
||||||
|
} |
||||||
|
for i := range req.AddTokens { |
||||||
|
v := vflux.IntOrInf{Type: uint8(f.randomInt(4))} |
||||||
|
if v.Type < 2 { |
||||||
|
v.Value = *big.NewInt(f.randomTokenAmount(false)) |
||||||
|
} |
||||||
|
req.AddTokens[i] = v |
||||||
|
} |
||||||
|
reqEnc, err := rlp.EncodeToBytes(&req) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
p := int(f.randomByte()) |
||||||
|
if p < len(reqEnc) { |
||||||
|
reqEnc[p] = f.randomByte() |
||||||
|
} |
||||||
|
pool.Handle(f.peers[f.randomByte()].node.ID(), f.peers[f.randomByte()].freeID, vflux.CapacityQueryName, reqEnc) |
||||||
|
} |
||||||
|
|
||||||
|
for _, peer := range f.disconnectList { |
||||||
|
pool.Unregister(peer) |
||||||
|
} |
||||||
|
f.disconnectList = nil |
||||||
|
if d := f.randomDelay(); d > 0 { |
||||||
|
clock.Run(d) |
||||||
|
} |
||||||
|
//fmt.Println(f.activeCount, f.maxCount, f.activeCap, f.maxCap)
|
||||||
|
if activeCount, activeCap := pool.Active(); activeCount != f.activeCount || activeCap != f.activeCap { |
||||||
|
panic(nil) |
||||||
|
} |
||||||
|
if f.activeCount > f.maxCount || f.activeCap > f.maxCap { |
||||||
|
panic(nil) |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
// Copyright 2020 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 main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/tests/fuzzers/vflux" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
if len(os.Args) != 2 { |
||||||
|
fmt.Fprintf(os.Stderr, "Usage: debug <file>\n") |
||||||
|
fmt.Fprintf(os.Stderr, "Example\n") |
||||||
|
fmt.Fprintf(os.Stderr, " $ debug ../crashers/4bbef6857c733a87ecf6fd8b9e7238f65eb9862a\n") |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
crasher := os.Args[1] |
||||||
|
data, err := ioutil.ReadFile(crasher) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "error loading crasher %v: %v", crasher, err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
vflux.FuzzClientPool(data) |
||||||
|
} |
Loading…
Reference in new issue