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  }