github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/utils/expiredvalue.go (about) 1 // Copyright 2020 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package utils 18 19 import ( 20 "math" 21 "sync" 22 23 "github.com/cryptogateway/go-paymex/common/mclock" 24 ) 25 26 // ExpiredValue is a scalar value that is continuously expired (decreased 27 // exponentially) based on the provided logarithmic expiration offset value. 28 // 29 // The formula for value calculation is: base*2^(exp-logOffset). In order to 30 // simplify the calculation of ExpiredValue, its value is expressed in the form 31 // of an exponent with a base of 2. 32 // 33 // Also here is a trick to reduce a lot of calculations. In theory, when a value X 34 // decays over time and then a new value Y is added, the final result should be 35 // X*2^(exp-logOffset)+Y. However it's very hard to represent in memory. 36 // So the trick is using the idea of inflation instead of exponential decay. At this 37 // moment the temporary value becomes: X*2^exp+Y*2^logOffset_1, apply the exponential 38 // decay when we actually want to calculate the value. 39 // 40 // e.g. 41 // t0: V = 100 42 // t1: add 30, inflationary value is: 100 + 30/0.3, 0.3 is the decay coefficient 43 // t2: get value, decay coefficient is 0.2 now, final result is: 200*0.2 = 40 44 type ExpiredValue struct { 45 Base, Exp uint64 // rlp encoding works by default 46 } 47 48 // ExpirationFactor is calculated from logOffset. 1 <= Factor < 2 and Factor*2^Exp 49 // describes the multiplier applicable for additions and the divider for readouts. 50 // If logOffset changes slowly then it saves some expensive operations to not calculate 51 // them for each addition and readout but cache this intermediate form for some time. 52 // It is also useful for structures where multiple values are expired with the same 53 // Expirer. 54 type ExpirationFactor struct { 55 Exp uint64 56 Factor float64 57 } 58 59 // ExpFactor calculates ExpirationFactor based on logOffset 60 func ExpFactor(logOffset Fixed64) ExpirationFactor { 61 return ExpirationFactor{Exp: logOffset.ToUint64(), Factor: logOffset.Fraction().Pow2()} 62 } 63 64 // Value calculates the expired value based on a floating point base and integer 65 // power-of-2 exponent. This function should be used by multi-value expired structures. 66 func (e ExpirationFactor) Value(base float64, exp uint64) float64 { 67 return base / e.Factor * math.Pow(2, float64(int64(exp-e.Exp))) 68 } 69 70 // value calculates the value at the given moment. 71 func (e ExpiredValue) Value(logOffset Fixed64) uint64 { 72 offset := Uint64ToFixed64(e.Exp) - logOffset 73 return uint64(float64(e.Base) * offset.Pow2()) 74 } 75 76 // add adds a signed value at the given moment 77 func (e *ExpiredValue) Add(amount int64, logOffset Fixed64) int64 { 78 integer, frac := logOffset.ToUint64(), logOffset.Fraction() 79 factor := frac.Pow2() 80 base := factor * float64(amount) 81 if integer < e.Exp { 82 base /= math.Pow(2, float64(e.Exp-integer)) 83 } 84 if integer > e.Exp { 85 e.Base >>= (integer - e.Exp) 86 e.Exp = integer 87 } 88 if base >= 0 || uint64(-base) <= e.Base { 89 // The conversion from negative float64 to 90 // uint64 is undefined in golang, and doesn't 91 // work with ARMv8. More details at: 92 // https://github.com/golang/go/issues/43047 93 if base >= 0 { 94 e.Base += uint64(base) 95 } else { 96 e.Base -= uint64(-base) 97 } 98 return amount 99 } 100 net := int64(-float64(e.Base) / factor) 101 e.Base = 0 102 return net 103 } 104 105 // addExp adds another ExpiredValue 106 func (e *ExpiredValue) AddExp(a ExpiredValue) { 107 if e.Exp > a.Exp { 108 a.Base >>= (e.Exp - a.Exp) 109 } 110 if e.Exp < a.Exp { 111 e.Base >>= (a.Exp - e.Exp) 112 e.Exp = a.Exp 113 } 114 e.Base += a.Base 115 } 116 117 // subExp subtracts another ExpiredValue 118 func (e *ExpiredValue) SubExp(a ExpiredValue) { 119 if e.Exp > a.Exp { 120 a.Base >>= (e.Exp - a.Exp) 121 } 122 if e.Exp < a.Exp { 123 e.Base >>= (a.Exp - e.Exp) 124 e.Exp = a.Exp 125 } 126 if e.Base > a.Base { 127 e.Base -= a.Base 128 } else { 129 e.Base = 0 130 } 131 } 132 133 // IsZero returns true if the value is zero 134 func (e *ExpiredValue) IsZero() bool { 135 return e.Base == 0 136 } 137 138 // LinearExpiredValue is very similar with the expiredValue which the value 139 // will continuously expired. But the different part is it's expired linearly. 140 type LinearExpiredValue struct { 141 Offset uint64 // The latest time offset 142 Val uint64 // The remaining value, can never be negative 143 Rate mclock.AbsTime `rlp:"-"` // Expiration rate(by nanosecond), will ignored by RLP 144 } 145 146 // value calculates the value at the given moment. This function always has the 147 // assumption that the given timestamp shouldn't less than the recorded one. 148 func (e LinearExpiredValue) Value(now mclock.AbsTime) uint64 { 149 offset := uint64(now / e.Rate) 150 if e.Offset < offset { 151 diff := offset - e.Offset 152 if e.Val >= diff { 153 e.Val -= diff 154 } else { 155 e.Val = 0 156 } 157 } 158 return e.Val 159 } 160 161 // add adds a signed value at the given moment. This function always has the 162 // assumption that the given timestamp shouldn't less than the recorded one. 163 func (e *LinearExpiredValue) Add(amount int64, now mclock.AbsTime) uint64 { 164 offset := uint64(now / e.Rate) 165 if e.Offset < offset { 166 diff := offset - e.Offset 167 if e.Val >= diff { 168 e.Val -= diff 169 } else { 170 e.Val = 0 171 } 172 e.Offset = offset 173 } 174 if amount < 0 && uint64(-amount) > e.Val { 175 e.Val = 0 176 } else { 177 e.Val = uint64(int64(e.Val) + amount) 178 } 179 return e.Val 180 } 181 182 // ValueExpirer controls value expiration rate 183 type ValueExpirer interface { 184 SetRate(now mclock.AbsTime, rate float64) 185 SetLogOffset(now mclock.AbsTime, logOffset Fixed64) 186 LogOffset(now mclock.AbsTime) Fixed64 187 } 188 189 // Expirer changes logOffset with a linear rate which can be changed during operation. 190 // It is not thread safe, if access by multiple goroutines is needed then it should be 191 // encapsulated into a locked structure. 192 // Note that if neither SetRate nor SetLogOffset are used during operation then LogOffset 193 // is thread safe. 194 type Expirer struct { 195 lock sync.RWMutex 196 logOffset Fixed64 197 rate float64 198 lastUpdate mclock.AbsTime 199 } 200 201 // SetRate changes the expiration rate which is the inverse of the time constant in 202 // nanoseconds. 203 func (e *Expirer) SetRate(now mclock.AbsTime, rate float64) { 204 e.lock.Lock() 205 defer e.lock.Unlock() 206 207 dt := now - e.lastUpdate 208 if dt > 0 { 209 e.logOffset += Fixed64(logToFixedFactor * float64(dt) * e.rate) 210 } 211 e.lastUpdate = now 212 e.rate = rate 213 } 214 215 // SetLogOffset sets logOffset instantly. 216 func (e *Expirer) SetLogOffset(now mclock.AbsTime, logOffset Fixed64) { 217 e.lock.Lock() 218 defer e.lock.Unlock() 219 220 e.lastUpdate = now 221 e.logOffset = logOffset 222 } 223 224 // LogOffset returns the current logarithmic offset. 225 func (e *Expirer) LogOffset(now mclock.AbsTime) Fixed64 { 226 e.lock.RLock() 227 defer e.lock.RUnlock() 228 229 dt := now - e.lastUpdate 230 if dt <= 0 { 231 return e.logOffset 232 } 233 return e.logOffset + Fixed64(logToFixedFactor*float64(dt)*e.rate) 234 } 235 236 // fixedFactor is the fixed point multiplier factor used by Fixed64. 237 const fixedFactor = 0x1000000 238 239 // Fixed64 implements 64-bit fixed point arithmetic functions. 240 type Fixed64 int64 241 242 // Uint64ToFixed64 converts uint64 integer to Fixed64 format. 243 func Uint64ToFixed64(f uint64) Fixed64 { 244 return Fixed64(f * fixedFactor) 245 } 246 247 // float64ToFixed64 converts float64 to Fixed64 format. 248 func Float64ToFixed64(f float64) Fixed64 { 249 return Fixed64(f * fixedFactor) 250 } 251 252 // toUint64 converts Fixed64 format to uint64. 253 func (f64 Fixed64) ToUint64() uint64 { 254 return uint64(f64) / fixedFactor 255 } 256 257 // fraction returns the fractional part of a Fixed64 value. 258 func (f64 Fixed64) Fraction() Fixed64 { 259 return f64 % fixedFactor 260 } 261 262 var ( 263 logToFixedFactor = float64(fixedFactor) / math.Log(2) 264 fixedToLogFactor = math.Log(2) / float64(fixedFactor) 265 ) 266 267 // pow2Fixed returns the base 2 power of the fixed point value. 268 func (f64 Fixed64) Pow2() float64 { 269 return math.Exp(float64(f64) * fixedToLogFactor) 270 }