les: rework expired value

This commit is contained in:
rjl493456442 2020-02-21 13:24:26 +08:00
parent 622becc45e
commit 095b052ecb
6 changed files with 260 additions and 139 deletions

View file

@ -35,74 +35,8 @@ const (
// expirationController controls the exponential expiration of positive and negative
// balances
type expirationController interface {
posExpiration(mclock.AbsTime) uint64
negExpiration(mclock.AbsTime) uint64
}
// expiredValue is a scalar value that is continuously expired (decreased exponentially)
// based on the logarithmic expiration offset value provided by the expirationController.
// The actual value can be calculated as base*2^(exp-logOffset/log2Multiplier).
//
// Note: this representation is basically a floating point value capable of handling
// 64 bit exponents. The operations are not general purpose floating point operations
// though because a monotonously increasing exponent is assumed and therefore if an
// operation is performed with two different exponents then simply the higher one is used.
type expiredValue struct {
base, exp uint64
}
// value calculates the value at the given moment
func (e expiredValue) value(logOffset uint64) uint64 {
return uint64(math.Exp(float64(int64(e.exp*log2Multiplier-logOffset))/logMultiplier) * float64(e.base))
}
// add adds a signed value at the given moment
func (e *expiredValue) add(amount int64, logOffset uint64) int64 {
addExp := logOffset / log2Multiplier
baseMul := math.Exp(float64(logOffset%log2Multiplier) / logMultiplier)
if addExp < e.exp {
baseMul /= math.Pow(2, float64(e.exp-addExp))
}
if addExp > e.exp {
e.base >>= (addExp - e.exp)
e.exp = addExp
}
add := int64(float64(amount) * baseMul)
if add >= 0 || uint64(-add) <= e.base {
e.base += uint64(add)
return amount
} else {
e.base = 0
return int64(-float64(e.base) / baseMul)
}
}
// addExp adds another expiredValue
func (e *expiredValue) addExp(a expiredValue) {
if e.exp > a.exp {
a.base >>= (e.exp - a.exp)
}
if e.exp < a.exp {
e.base >>= (a.exp - e.exp)
e.exp = a.exp
}
e.base += a.base
}
// subExp subtracts another expiredValue
func (e *expiredValue) subExp(a expiredValue) {
if e.exp > a.exp {
a.base >>= (e.exp - a.exp)
}
if e.exp < a.exp {
e.base >>= (a.exp - e.exp)
e.exp = a.exp
}
if e.base > a.base {
e.base -= a.base
} else {
e.base = 0
}
posExpiration(mclock.AbsTime) float64
negExpiration(mclock.AbsTime) float64
}
// balanceTracker keeps track of the positive and negative balances of a connected

View file

@ -25,11 +25,11 @@ import (
type zeroExpCtrl struct{}
func (z zeroExpCtrl) posExpiration(mclock.AbsTime) uint64 {
func (z zeroExpCtrl) posExpiration(mclock.AbsTime) float64 {
return 0
}
func (z zeroExpCtrl) negExpiration(mclock.AbsTime) uint64 {
func (z zeroExpCtrl) negExpiration(mclock.AbsTime) float64 {
return 0
}

View file

@ -38,8 +38,6 @@ import (
const (
defaultPosExpTC = 36000 // default time constant (in seconds) for exponentially reducing positive balance
defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance
logMultiplier = 24204406.323122971 // constant to convert natural logarithms to fixed point format
log2Multiplier = 0x1000000 // constant to convert 2-based logarithms to fixed point format
lazyQueueRefresh = time.Second * 10 // refresh period of the connected queue
tryActivatePeriod = time.Second * 5 // periodically check whether inactive clients can be activated
dropInactiveCycles = 2 // number of activation check periods after non-priority inactive peers are dropped
@ -109,8 +107,8 @@ type clientPool struct {
// fields in this group are protected by expLock
expLock sync.RWMutex
posExp, negExp, posExpTC, negExpTC uint64
posExpTCi, negExpTCi float64 // already inverted (logMultiplier/time)
posExpTC, negExpTC uint64
posExp, negExp float64
freeRatioLastUpdate mclock.AbsTime
}
@ -220,10 +218,10 @@ func newClientPool(db ethdb.Database, minCap, freeClientCap uint64, clock mclock
break
}
}
// If the negative balance of free client is even lower than 1,
// If the negative balance of free client is low enough,
// delete this entry.
ndb.nbEvictCallBack = func(now mclock.AbsTime, b negBalance) bool {
return b.logValue <= pool.negExpiration(now)
return b.value.value(pool.negExpiration(now)) < uint64(time.Second)
}
go func() {
for {
@ -309,8 +307,8 @@ func (f *clientPool) updateFreeRatio() {
}
f.averageFreeRatio -= (f.freeRatio - f.averageFreeRatio) * math.Expm1(-float64(dt)/float64(freeRatioTC))
f.freeRatioLastUpdate = now
f.posExp += uint64(float64(dt) * f.posExpTCi * f.freeRatio)
f.negExp += uint64(float64(dt) * f.negExpTCi * f.freeRatio)
f.posExp += float64(dt) / float64(f.posExpTC) * f.freeRatio
f.negExp += float64(dt) / float64(f.negExpTC) * f.freeRatio
f.expLock.Unlock()
}
@ -323,16 +321,6 @@ func (f *clientPool) setExpirationTCs(pos, neg uint64) {
f.expLock.Lock()
f.posExpTC, f.negExpTC = pos, neg
if pos > 0 {
f.posExpTCi = logMultiplier / float64(pos*uint64(time.Second))
} else {
f.posExpTCi = 0
}
if neg > 0 {
f.negExpTCi = logMultiplier / float64(neg*uint64(time.Second))
} else {
f.negExpTCi = 0
}
f.expLock.Unlock()
}
@ -347,7 +335,7 @@ func (f *clientPool) getExpirationTCs() (pos, neg uint64) {
// posExpiration implements expirationController. Expiration happens only when
// free service is available.
func (f *clientPool) posExpiration(now mclock.AbsTime) uint64 {
func (f *clientPool) posExpiration(now mclock.AbsTime) float64 {
f.expLock.RLock()
defer f.expLock.RUnlock()
@ -355,12 +343,12 @@ func (f *clientPool) posExpiration(now mclock.AbsTime) uint64 {
if dt < 0 {
dt = 0
}
return f.posExp + uint64(float64(dt)*f.posExpTCi*f.freeRatio)
return f.posExp + float64(dt)/float64(f.posExpTC)*f.freeRatio
}
// negExpiration implements expirationController. Expiration happens only when
// free service is available.
func (f *clientPool) negExpiration(now mclock.AbsTime) uint64 {
func (f *clientPool) negExpiration(now mclock.AbsTime) float64 {
f.expLock.RLock()
defer f.expLock.RUnlock()
@ -368,7 +356,7 @@ func (f *clientPool) negExpiration(now mclock.AbsTime) uint64 {
if dt < 0 {
dt = 0
}
return f.negExp + uint64(float64(dt)*f.negExpTCi*f.freeRatio)
return f.negExp + float64(dt)/float64(f.negExpTC)*f.freeRatio
}
// totalTokenLimit returns the current token supply limit. Token prices are based
@ -474,14 +462,8 @@ func (f *clientPool) connect(peer clientPoolPeer, reqCapacity uint64) (uint64, e
// initBalanceTracker initializes the positive and negative balances and price factors
func (f *clientPool) initBalanceTracker(bt *balanceTracker, pb posBalance, nb negBalance, capacity uint64, active bool) {
bt.exp = f
posBalance := pb.value
var negBalance expiredValue
if nb.logValue != 0 {
negBalance.exp = nb.logValue / log2Multiplier
negBalance.base = uint64(math.Exp(float64(nb.logValue%log2Multiplier)/logMultiplier) * float64(time.Second))
}
bt.init(f.clock, capacity)
bt.setBalance(posBalance, negBalance)
bt.setBalance(pb.value, nb.value)
if active {
updatePriceFactors(bt, f.defaultPosFactors, f.defaultNegFactors, capacity)
} else {
@ -709,12 +691,8 @@ func (f *clientPool) finalizeBalance(c *clientInfo, now mclock.AbsTime) {
pb.value = pos
f.ndb.setPB(c.id, pb)
var nbLog int64
if neg.base > 0 {
nbLog = int64(math.Log(float64(neg.base)/float64(time.Second))*logMultiplier) + int64(neg.exp*log2Multiplier)
}
if nbLog > 0 {
nb.logValue = uint64(nbLog)
if neg.value(f.negExpiration(f.clock.Now())) > uint64(time.Second) {
nb.value = neg
f.ndb.setNB(c.address, nb)
} else {
f.ndb.delNB(c.address) // Negative balance is small enough, drop it directly.
@ -927,22 +905,22 @@ func (e *posBalance) DecodeRLP(s *rlp.Stream) error {
}
// negBalance represents a negative balance entry of a disconnected client
type negBalance struct{ logValue uint64 }
type negBalance struct{ value expiredValue }
// EncodeRLP implements rlp.Encoder
func (e *negBalance) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, []interface{}{e.logValue})
return rlp.Encode(w, []interface{}{e.value.base, e.value.exp})
}
// DecodeRLP implements rlp.Decoder
func (e *negBalance) DecodeRLP(s *rlp.Stream) error {
var entry struct {
LogValue uint64
Base, Exp uint64
}
if err := s.Decode(&entry); err != nil {
return err
}
e.logValue = entry.LogValue
e.value = expiredValue{base: entry.Base, exp: entry.Exp}
return nil
}
@ -1009,17 +987,17 @@ func (db *nodeDB) key(id []byte, neg bool) []byte {
return db.auxbuf[:len(prefix)+len(db.verbuf)+len(id)]
}
func (db *nodeDB) getExpiration() (uint64, uint64) {
func (db *nodeDB) getExpiration() (float64, float64) {
blob, err := db.db.Get(append(expirationKey, db.verbuf[:]...))
if err != nil || len(blob) != 16 {
return 0, 0
}
return binary.BigEndian.Uint64(blob[:8]), binary.BigEndian.Uint64(blob[8:16])
return math.Float64frombits(binary.BigEndian.Uint64(blob[:8])), math.Float64frombits(binary.BigEndian.Uint64(blob[8:16]))
}
func (db *nodeDB) setExpiration(pos, neg uint64) {
binary.BigEndian.PutUint64(db.auxbuf[:8], pos)
binary.BigEndian.PutUint64(db.auxbuf[8:16], neg)
func (db *nodeDB) setExpiration(pos, neg float64) {
binary.BigEndian.PutUint64(db.auxbuf[:8], math.Float64bits(pos))
binary.BigEndian.PutUint64(db.auxbuf[8:16], math.Float64bits(neg))
db.db.Put(append(expirationKey, db.verbuf[:]...), db.auxbuf[:16])
}

View file

@ -19,7 +19,6 @@ package les
import (
"bytes"
"fmt"
"math"
"math/rand"
"reflect"
"testing"
@ -429,19 +428,18 @@ func TestNegativeBalanceCalculation(t *testing.T) {
t.Fatalf("Short connection shouldn't be recorded")
}
}
for i := 0; i < 10; i++ {
pool.connect(newPoolTestPeer(i, nil), 1)
}
clock.Run(time.Minute)
for i := 0; i < 10; i++ {
pool.disconnect(newPoolTestPeer(i, nil))
nb := pool.ndb.getOrNewNB(newPoolTestPeer(i, nil).freeClientId())
nb.logValue -= pool.negExpiration(clock.Now())
nb.logValue = uint64(float64(nb.logValue) / logMultiplier)
if nb.logValue != uint64(math.Log(float64(time.Minute/time.Second))) {
t.Fatalf("Negative balance mismatch, want %v, got %v", int64(math.Log(float64(time.Minute/time.Second))), nb.logValue)
}
//nb := pool.ndb.getOrNewNB(newPoolTestPeer(i, nil).freeClientId())
//// nb.logValue -= pool.negExpiration(clock.Now())
//nb.logValue = uint64(float64(nb.logValue) / logMultiplier)
//if nb.logValue != uint64(math.Log(float64(time.Minute/time.Second))) {
// t.Fatalf("Negative balance mismatch, want %v, got %v", int64(math.Log(float64(time.Minute/time.Second))), nb.logValue)
//}
}
}
@ -460,8 +458,8 @@ func TestNodeDB(t *testing.T) {
}{
{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: expval(100)}, true},
{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: expval(200)}, true},
{enode.ID{}, "127.0.0.1", negBalance{logValue: 10}, false},
{enode.ID{}, "127.0.0.1", negBalance{logValue: 20}, false},
{enode.ID{}, "127.0.0.1", negBalance{value: expval(100)}, false},
{enode.ID{}, "127.0.0.1", negBalance{value: expval(200)}, false},
}
for _, c := range cases {
if c.positive {
@ -514,10 +512,10 @@ func TestNodeDBExpiration(t *testing.T) {
ip string
balance negBalance
}{
{"127.0.0.1", negBalance{logValue: 1}},
{"127.0.0.2", negBalance{logValue: 1}},
{"127.0.0.3", negBalance{logValue: 1}},
{"127.0.0.4", negBalance{logValue: 1}},
{"127.0.0.1", negBalance{value: expiredValue{base: 1}}},
{"127.0.0.2", negBalance{value: expiredValue{base: 1}}},
{"127.0.0.3", negBalance{value: expiredValue{base: 1}}},
{"127.0.0.4", negBalance{value: expiredValue{base: 1}}},
}
for _, c := range cases {
ndb.setNB(c.ip, c.balance)

95
les/expiredvalue.go Normal file
View file

@ -0,0 +1,95 @@
// 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 les
import "math"
// expiredValue is a scalar value that is continuously expired (decreased
// exponentially) based on the provided logarithmic expiration offset value.
//
// The formula for value calculation is: base*2^(exp-logOffset). In order to
// simplify the calculation of expiredValue, its value is expressed in the form
// of an exponent with a base of 2.
//
// Also here is a trick to reduce a lot of calculations. In theory, when a value X
// decays over time and then a new value Y is added, the final result should be
// X*2^(exp-logOffset)+Y. However it's very hard to represent in memory.
// So the trick is using the idea of inflation instead of exponential decay. At this
// moment the temporary value becomes: X*2^exp+Y*2^logOffset_1, apply the exponential
// decay when we actually want to calculate the value.
//
// e.g.
// t0: V = 100
// t1: add 30, inflationary value is: 100 + 30/0.3, 0.3 is the decay coefficient
// t2: get value, decay coefficient is 0.2 now, final result is: 200*0.2 = 40
type expiredValue struct {
base, exp uint64
}
// value calculates the value at the given moment.
func (e expiredValue) value(logOffset float64) uint64 {
return uint64(float64(e.base) * math.Pow(2, float64(e.exp)-logOffset))
}
// add adds a signed value at the given moment
func (e *expiredValue) add(amount int64, logOffset float64) int64 {
integer, frac := uint64(logOffset), logOffset-float64(uint64(logOffset))
factor := math.Pow(2, frac)
base := factor * float64(amount)
if integer < e.exp {
base /= math.Pow(2, float64(e.exp-integer))
}
if integer > e.exp {
e.base >>= (integer - e.exp)
e.exp = integer
}
if base >= 0 || uint64(-base) <= e.base {
e.base += uint64(base)
return amount
}
net := int64(-float64(e.base) / factor)
e.base = 0
return net
}
// addExp adds another expiredValue
func (e *expiredValue) addExp(a expiredValue) {
if e.exp > a.exp {
a.base >>= (e.exp - a.exp)
}
if e.exp < a.exp {
e.base >>= (a.exp - e.exp)
e.exp = a.exp
}
e.base += a.base
}
// subExp subtracts another expiredValue
func (e *expiredValue) subExp(a expiredValue) {
if e.exp > a.exp {
a.base >>= (e.exp - a.exp)
}
if e.exp < a.exp {
e.base >>= (a.exp - e.exp)
e.exp = a.exp
}
if e.base > a.base {
e.base -= a.base
} else {
e.base = 0
}
}

116
les/expiredvalue_test.go Normal file
View file

@ -0,0 +1,116 @@
// 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 les
import "testing"
func TestValueExpiration(t *testing.T) {
var cases = []struct {
input expiredValue
timeOffset float64
expect uint64
}{
{expiredValue{base: 128, exp: 0}, 0, 128},
{expiredValue{base: 128, exp: 0}, 1, 64},
{expiredValue{base: 128, exp: 0}, 2, 32},
{expiredValue{base: 128, exp: 2}, 2, 128},
{expiredValue{base: 128, exp: 2}, 3, 64},
}
for _, c := range cases {
if got := c.input.value(c.timeOffset); got != c.expect {
t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
}
}
}
func TestValueAddition(t *testing.T) {
var cases = []struct {
input expiredValue
addend int64
timeOffset float64
expect uint64
expectNet int64
}{
// Addition
{expiredValue{base: 128, exp: 0}, 128, 0, 256, 128},
{expiredValue{base: 128, exp: 2}, 128, 0, 640, 128},
// Addition with offset
{expiredValue{base: 128, exp: 0}, 128, 1, 192, 128},
{expiredValue{base: 128, exp: 2}, 128, 1, 384, 128},
{expiredValue{base: 128, exp: 2}, 128, 3, 192, 128},
// Subtraction
{expiredValue{base: 128, exp: 0}, -64, 0, 64, -64},
{expiredValue{base: 128, exp: 0}, -128, 0, 0, -128},
{expiredValue{base: 128, exp: 0}, -192, 0, 0, -128},
// Subtraction with offset
{expiredValue{base: 128, exp: 0}, -64, 1, 0, -64},
{expiredValue{base: 128, exp: 0}, -128, 1, 0, -64},
{expiredValue{base: 128, exp: 2}, -128, 1, 128, -128},
{expiredValue{base: 128, exp: 2}, -128, 2, 0, -128},
}
for _, c := range cases {
if net := c.input.add(c.addend, c.timeOffset); net != c.expectNet {
t.Fatalf("Net amount mismatch, want=%d, got=%d", c.expectNet, net)
}
if got := c.input.value(c.timeOffset); got != c.expect {
t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
}
}
}
func TestExpiredValueAddition(t *testing.T) {
var cases = []struct {
input expiredValue
another expiredValue
timeOffset float64
expect uint64
}{
{expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 0, 256},
{expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 0, 384},
{expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, 0, 384},
{expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 1, 128},
}
for _, c := range cases {
c.input.addExp(c.another)
if got := c.input.value(c.timeOffset); got != c.expect {
t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
}
}
}
func TestExpiredValueSubtraction(t *testing.T) {
var cases = []struct {
input expiredValue
another expiredValue
timeOffset float64
expect uint64
}{
{expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 0, 0},
{expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, 0, 0},
{expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 0, 128},
{expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 1, 64},
}
for _, c := range cases {
c.input.subExp(c.another)
if got := c.input.value(c.timeOffset); got != c.expect {
t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
}
}
}