diff --git a/les/balance.go b/les/balance.go index 6f11daead2..1260a3114b 100644 --- a/les/balance.go +++ b/les/balance.go @@ -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 diff --git a/les/balance_test.go b/les/balance_test.go index 91032fc714..66c722e279 100644 --- a/les/balance_test.go +++ b/les/balance_test.go @@ -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 } diff --git a/les/clientpool.go b/les/clientpool.go index 80915317ca..5a5a0bcb6b 100644 --- a/les/clientpool.go +++ b/les/clientpool.go @@ -36,17 +36,15 @@ 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 - persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence - posBalanceCacheLimit = 8192 // the maximum number of cached items in positive balance queue - negBalanceCacheLimit = 8192 // the maximum number of cached items in negative balance queue - freeRatioTC = time.Hour // time constant of token supply control based on free service availability + defaultPosExpTC = 36000 // default time constant (in seconds) for exponentially reducing positive balance + defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance + 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 + persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence + posBalanceCacheLimit = 8192 // the maximum number of cached items in positive balance queue + negBalanceCacheLimit = 8192 // the maximum number of cached items in negative balance queue + freeRatioTC = time.Hour // time constant of token supply control based on free service availability // activeBias is applied to already connected clients So that // already connected client won't be kicked out very soon and we @@ -108,10 +106,10 @@ type clientPool struct { disableBias bool // Disable connection bias(used in testing) // fields in this group are protected by expLock - expLock sync.RWMutex - posExp, negExp, posExpTC, negExpTC uint64 - posExpTCi, negExpTCi float64 // already inverted (logMultiplier/time) - freeRatioLastUpdate mclock.AbsTime + expLock sync.RWMutex + posExpTC, negExpTC uint64 + posExp, negExp float64 + freeRatioLastUpdate mclock.AbsTime } // clientPoolPeer represents a client peer in the pool. @@ -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]) } diff --git a/les/clientpool_test.go b/les/clientpool_test.go index 94ccb33209..9629572e88 100644 --- a/les/clientpool_test.go +++ b/les/clientpool_test.go @@ -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) diff --git a/les/expiredvalue.go b/les/expiredvalue.go new file mode 100644 index 0000000000..a639ac38b3 --- /dev/null +++ b/les/expiredvalue.go @@ -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 . + +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 + } +} diff --git a/les/expiredvalue_test.go b/les/expiredvalue_test.go new file mode 100644 index 0000000000..450a55c319 --- /dev/null +++ b/les/expiredvalue_test.go @@ -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 . + +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) + } + } +}