diff --git a/les/balance.go b/les/balance.go index 98ae239b06..97fc0d76ee 100644 --- a/les/balance.go +++ b/les/balance.go @@ -35,8 +35,8 @@ const ( // expirationController controls the exponential expiration of positive and negative // balances type expirationController interface { - posExpiration(mclock.AbsTime) float64 - negExpiration(mclock.AbsTime) float64 + posExpiration(mclock.AbsTime) fixed64 + negExpiration(mclock.AbsTime) fixed64 } // priceFactors determine the pricing policy (may apply either to positive or diff --git a/les/balance_test.go b/les/balance_test.go index 200ed0fd12..69d8caf95b 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) float64 { +func (z zeroExpCtrl) posExpiration(mclock.AbsTime) fixed64 { return 0 } -func (z zeroExpCtrl) negExpiration(mclock.AbsTime) float64 { +func (z zeroExpCtrl) negExpiration(mclock.AbsTime) fixed64 { return 0 } diff --git a/les/clientdb.go b/les/clientdb.go index a9905a5091..b1beeac915 100644 --- a/les/clientdb.go +++ b/les/clientdb.go @@ -20,7 +20,6 @@ import ( "bytes" "encoding/binary" "io" - "math" "time" "github.com/ethereum/go-ethereum/common" @@ -138,18 +137,18 @@ func (db *nodeDB) key(id []byte, neg bool) []byte { return append(prefix, id...) } -func (db *nodeDB) getExpiration() (float64, float64) { +func (db *nodeDB) getExpiration() (fixed64, fixed64) { blob, err := db.db.Get(expirationKey) if err != nil || len(blob) != 16 { return 0, 0 } - return math.Float64frombits(binary.BigEndian.Uint64(blob[:8])), math.Float64frombits(binary.BigEndian.Uint64(blob[8:16])) + return fixed64(binary.BigEndian.Uint64(blob[:8])), fixed64(binary.BigEndian.Uint64(blob[8:16])) } -func (db *nodeDB) setExpiration(pos, neg float64) { +func (db *nodeDB) setExpiration(pos, neg fixed64) { var buff [16]byte - binary.BigEndian.PutUint64(buff[:8], math.Float64bits(pos)) - binary.BigEndian.PutUint64(buff[8:16], math.Float64bits(neg)) + binary.BigEndian.PutUint64(buff[:8], uint64(pos)) + binary.BigEndian.PutUint64(buff[8:16], uint64(neg)) db.db.Put(expirationKey, buff[:16]) } diff --git a/les/clientdb_test.go b/les/clientdb_test.go index 188bb0d31f..ac2181fb66 100644 --- a/les/clientdb_test.go +++ b/les/clientdb_test.go @@ -67,7 +67,7 @@ func TestNodeDB(t *testing.T) { } } } - posExp, negExp := 100.1234, 200.5678 + posExp, negExp := fixed64(1000), fixed64(2000) ndb.setExpiration(posExp, negExp) if pos, neg := ndb.getExpiration(); pos != posExp || neg != negExp { t.Fatalf("Expiration mismatch, want %v / %v, got %v / %v", posExp, negExp, pos, neg) diff --git a/les/clientpool.go b/les/clientpool.go index b139d1d898..385367035c 100644 --- a/les/clientpool.go +++ b/les/clientpool.go @@ -95,10 +95,11 @@ type clientPool struct { disableBias bool // Disable connection bias(used in testing) // fields in this group are protected by expLock - expLock sync.RWMutex - posExpTC, negExpTC uint64 - posExp, negExp float64 - freeRatioLastUpdate mclock.AbsTime + expLock sync.RWMutex + posExpTC, negExpTC uint64 + posExp, negExp fixed64 + posExpTCi, negExpTCi float64 // already inverted (logMultiplier/time) + freeRatioLastUpdate mclock.AbsTime } // clientPoolPeer represents a client peer in the pool. @@ -201,7 +202,7 @@ func newClientPool(db ethdb.Database, minCap, freeClientCap uint64, clock mclock // and both of these decay exponentially over time. Delete them if the // value is small enough. ndb.evictCallBack = func(now mclock.AbsTime, neg bool, b tokenBalance) bool { - var expiration float64 + var expiration fixed64 if neg { expiration = pool.negExpiration(now) } else { @@ -292,13 +293,8 @@ func (f *clientPool) updateFreeRatio() { f.averageFreeRatio -= (f.freeRatio - f.averageFreeRatio) * math.Expm1(-float64(dt)/float64(freeRatioTC)) f.freeRatioLastUpdate = now - dt /= mclock.AbsTime(time.Second) - if f.posExpTC != 0 { - f.posExp += float64(dt) / float64(f.posExpTC) * f.freeRatio - } - if f.negExpTC != 0 { - f.negExp += float64(dt) / float64(f.negExpTC) * f.freeRatio - } + f.posExp += fixed64(float64(dt) * f.posExpTCi * f.freeRatio) + f.negExp += fixed64(float64(dt) * f.negExpTCi * f.freeRatio) f.expLock.Unlock() } @@ -311,6 +307,16 @@ func (f *clientPool) setExpirationTCs(pos, neg uint64) { f.expLock.Lock() f.posExpTC, f.negExpTC = pos, neg + if pos > 0 { + f.posExpTCi = fixedFactor / float64(pos*uint64(time.Second)) + } else { + f.posExpTCi = 0 + } + if neg > 0 { + f.negExpTCi = fixedFactor / float64(neg*uint64(time.Second)) + } else { + f.negExpTCi = 0 + } f.expLock.Unlock() } @@ -325,7 +331,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) float64 { +func (f *clientPool) posExpiration(now mclock.AbsTime) fixed64 { f.expLock.RLock() defer f.expLock.RUnlock() @@ -337,12 +343,12 @@ func (f *clientPool) posExpiration(now mclock.AbsTime) float64 { dt = 0 } dt /= mclock.AbsTime(time.Second) - return f.posExp + float64(dt)/float64(f.posExpTC)*f.freeRatio + return f.posExp + fixed64(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) float64 { +func (f *clientPool) negExpiration(now mclock.AbsTime) fixed64 { f.expLock.RLock() defer f.expLock.RUnlock() @@ -354,7 +360,7 @@ func (f *clientPool) negExpiration(now mclock.AbsTime) float64 { dt = 0 } dt /= mclock.AbsTime(time.Second) - return f.negExp + float64(dt)/float64(f.negExpTC)*f.freeRatio + return f.negExp + fixed64(float64(dt)/float64(f.negExpTC)*f.freeRatio) } // totalTokenLimit returns the current token supply limit. Token prices are based @@ -686,7 +692,7 @@ func (f *clientPool) finalizeBalance(c *clientInfo, now mclock.AbsTime) { for index, value := range []expiredValue{pos, neg} { var ( id []byte - expiration float64 + expiration fixed64 ) neg := index == 1 if !neg { diff --git a/les/expiredvalue.go b/les/expiredvalue.go index a639ac38b3..6ce3638c25 100644 --- a/les/expiredvalue.go +++ b/les/expiredvalue.go @@ -41,14 +41,15 @@ type expiredValue struct { } // 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)) +func (e expiredValue) value(logOffset fixed64) uint64 { + offset := uint64ToFixed64(e.exp) - logOffset + return uint64(float64(e.base) * offset.pow2Fixed()) } // 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) +func (e *expiredValue) add(amount int64, logOffset fixed64) int64 { + integer, frac := logOffset.toUint64(), logOffset.fraction() + factor := frac.pow2Fixed() base := factor * float64(amount) if integer < e.exp { base /= math.Pow(2, float64(e.exp-integer)) @@ -93,3 +94,35 @@ func (e *expiredValue) subExp(a expiredValue) { e.base = 0 } } + +// fixedFactor is the factor used by fixed64 +const fixedFactor = 0x1000000 + +// fixed64 is a float64 wrapper that uses integer arithmetic +// to avoid precision loss in floating-point arithmetic. +type fixed64 int64 + +// uint64ToFixed64 converts uint64 integer to fixed64 format. +func uint64ToFixed64(f uint64) fixed64 { + return fixed64(f * fixedFactor) +} + +// float64ToFixed64 converts float64 to fixed64 format. +func float64ToFixed64(f float64) fixed64 { + return fixed64(f * fixedFactor) +} + +// toUint64 converts fixed64 format to uint64. +func (f64 fixed64) toUint64() uint64 { + return uint64(f64) / fixedFactor +} + +// fraction returns the fraction of the fixed64. +func (f64 fixed64) fraction() fixed64 { + return f64 % fixedFactor +} + +// pow2Fixed returns the 2 based pow of the fixed value. +func (f64 fixed64) pow2Fixed() float64 { + return math.Pow(2, float64(f64)/fixedFactor) +} diff --git a/les/expiredvalue_test.go b/les/expiredvalue_test.go index 450a55c319..a59510a702 100644 --- a/les/expiredvalue_test.go +++ b/les/expiredvalue_test.go @@ -21,14 +21,14 @@ import "testing" func TestValueExpiration(t *testing.T) { var cases = []struct { input expiredValue - timeOffset float64 + timeOffset fixed64 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}, + {expiredValue{base: 128, exp: 0}, uint64ToFixed64(0), 128}, + {expiredValue{base: 128, exp: 0}, uint64ToFixed64(1), 64}, + {expiredValue{base: 128, exp: 0}, uint64ToFixed64(2), 32}, + {expiredValue{base: 128, exp: 2}, uint64ToFixed64(2), 128}, + {expiredValue{base: 128, exp: 2}, uint64ToFixed64(3), 64}, } for _, c := range cases { if got := c.input.value(c.timeOffset); got != c.expect { @@ -41,29 +41,29 @@ func TestValueAddition(t *testing.T) { var cases = []struct { input expiredValue addend int64 - timeOffset float64 + timeOffset fixed64 expect uint64 expectNet int64 }{ // Addition - {expiredValue{base: 128, exp: 0}, 128, 0, 256, 128}, - {expiredValue{base: 128, exp: 2}, 128, 0, 640, 128}, + {expiredValue{base: 128, exp: 0}, 128, uint64ToFixed64(0), 256, 128}, + {expiredValue{base: 128, exp: 2}, 128, uint64ToFixed64(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}, + {expiredValue{base: 128, exp: 0}, 128, uint64ToFixed64(1), 192, 128}, + {expiredValue{base: 128, exp: 2}, 128, uint64ToFixed64(1), 384, 128}, + {expiredValue{base: 128, exp: 2}, 128, uint64ToFixed64(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}, + {expiredValue{base: 128, exp: 0}, -64, uint64ToFixed64(0), 64, -64}, + {expiredValue{base: 128, exp: 0}, -128, uint64ToFixed64(0), 0, -128}, + {expiredValue{base: 128, exp: 0}, -192, uint64ToFixed64(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}, + {expiredValue{base: 128, exp: 0}, -64, uint64ToFixed64(1), 0, -64}, + {expiredValue{base: 128, exp: 0}, -128, uint64ToFixed64(1), 0, -64}, + {expiredValue{base: 128, exp: 2}, -128, uint64ToFixed64(1), 128, -128}, + {expiredValue{base: 128, exp: 2}, -128, uint64ToFixed64(2), 0, -128}, } for _, c := range cases { if net := c.input.add(c.addend, c.timeOffset); net != c.expectNet { @@ -79,13 +79,13 @@ func TestExpiredValueAddition(t *testing.T) { var cases = []struct { input expiredValue another expiredValue - timeOffset float64 + timeOffset fixed64 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}, + {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(0), 256}, + {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(0), 384}, + {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, uint64ToFixed64(0), 384}, + {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(1), 128}, } for _, c := range cases { c.input.addExp(c.another) @@ -99,13 +99,13 @@ func TestExpiredValueSubtraction(t *testing.T) { var cases = []struct { input expiredValue another expiredValue - timeOffset float64 + timeOffset fixed64 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}, + {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(0), 0}, + {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, uint64ToFixed64(0), 0}, + {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(0), 128}, + {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, uint64ToFixed64(1), 64}, } for _, c := range cases { c.input.subExp(c.another)