github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/lespay/client/requestbasket.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 client
    18  
    19  import (
    20  	"io"
    21  
    22  	"github.com/cryptogateway/go-paymex/les/utils"
    23  	"github.com/cryptogateway/go-paymex/rlp"
    24  )
    25  
    26  const basketFactor = 1000000 // reference basket amount and value scale factor
    27  
    28  // referenceBasket keeps track of global request usage statistics and the usual prices
    29  // of each used request type relative to each other. The amounts in the basket are scaled
    30  // up by basketFactor because of the exponential expiration of long-term statistical data.
    31  // Values are scaled so that the sum of all amounts and the sum of all values are equal.
    32  //
    33  // reqValues represent the internal relative value estimates for each request type and are
    34  // calculated as value / amount. The average reqValue of all used requests is 1.
    35  // In other words: SUM(refBasket[type].amount * reqValue[type]) = SUM(refBasket[type].amount)
    36  type referenceBasket struct {
    37  	basket    requestBasket
    38  	reqValues []float64 // contents are read only, new slice is created for each update
    39  }
    40  
    41  // serverBasket collects served request amount and value statistics for a single server.
    42  //
    43  // Values are gradually transferred to the global reference basket with a long time
    44  // constant so that each server basket represents long term usage and price statistics.
    45  // When the transferred part is added to the reference basket the values are scaled so
    46  // that their sum equals the total value calculated according to the previous reqValues.
    47  // The ratio of request values coming from the server basket represent the pricing of
    48  // the specific server and modify the global estimates with a weight proportional to
    49  // the amount of service provided by the server.
    50  type serverBasket struct {
    51  	basket   requestBasket
    52  	rvFactor float64
    53  }
    54  
    55  type (
    56  	// requestBasket holds amounts and values for each request type.
    57  	// These values are exponentially expired (see utils.ExpiredValue). The power of 2
    58  	// exponent is applicable to all values within.
    59  	requestBasket struct {
    60  		items []basketItem
    61  		exp   uint64
    62  	}
    63  	// basketItem holds amount and value for a single request type. Value is the total
    64  	// relative request value accumulated for served requests while amount is the counter
    65  	// for each request type.
    66  	// Note that these values are both scaled up by basketFactor because of the exponential
    67  	// expiration.
    68  	basketItem struct {
    69  		amount, value uint64
    70  	}
    71  )
    72  
    73  // setExp sets the power of 2 exponent of the structure, scaling base values (the amounts
    74  // and request values) up or down if necessary.
    75  func (b *requestBasket) setExp(exp uint64) {
    76  	if exp > b.exp {
    77  		shift := exp - b.exp
    78  		for i, item := range b.items {
    79  			item.amount >>= shift
    80  			item.value >>= shift
    81  			b.items[i] = item
    82  		}
    83  		b.exp = exp
    84  	}
    85  	if exp < b.exp {
    86  		shift := b.exp - exp
    87  		for i, item := range b.items {
    88  			item.amount <<= shift
    89  			item.value <<= shift
    90  			b.items[i] = item
    91  		}
    92  		b.exp = exp
    93  	}
    94  }
    95  
    96  // init initializes a new server basket with the given service vector size (number of
    97  // different request types)
    98  func (s *serverBasket) init(size int) {
    99  	if s.basket.items == nil {
   100  		s.basket.items = make([]basketItem, size)
   101  	}
   102  }
   103  
   104  // add adds the give type and amount of requests to the basket. Cost is calculated
   105  // according to the server's own cost table.
   106  func (s *serverBasket) add(reqType, reqAmount uint32, reqCost uint64, expFactor utils.ExpirationFactor) {
   107  	s.basket.setExp(expFactor.Exp)
   108  	i := &s.basket.items[reqType]
   109  	i.amount += uint64(float64(uint64(reqAmount)*basketFactor) * expFactor.Factor)
   110  	i.value += uint64(float64(reqCost) * s.rvFactor * expFactor.Factor)
   111  }
   112  
   113  // updateRvFactor updates the request value factor that scales server costs into the
   114  // local value dimensions.
   115  func (s *serverBasket) updateRvFactor(rvFactor float64) {
   116  	s.rvFactor = rvFactor
   117  }
   118  
   119  // transfer decreases amounts and values in the basket with the given ratio and
   120  // moves the removed amounts into a new basket which is returned and can be added
   121  // to the global reference basket.
   122  func (s *serverBasket) transfer(ratio float64) requestBasket {
   123  	res := requestBasket{
   124  		items: make([]basketItem, len(s.basket.items)),
   125  		exp:   s.basket.exp,
   126  	}
   127  	for i, v := range s.basket.items {
   128  		ta := uint64(float64(v.amount) * ratio)
   129  		tv := uint64(float64(v.value) * ratio)
   130  		if ta > v.amount {
   131  			ta = v.amount
   132  		}
   133  		if tv > v.value {
   134  			tv = v.value
   135  		}
   136  		s.basket.items[i] = basketItem{v.amount - ta, v.value - tv}
   137  		res.items[i] = basketItem{ta, tv}
   138  	}
   139  	return res
   140  }
   141  
   142  // init initializes the reference basket with the given service vector size (number of
   143  // different request types)
   144  func (r *referenceBasket) init(size int) {
   145  	r.reqValues = make([]float64, size)
   146  	r.normalize()
   147  	r.updateReqValues()
   148  }
   149  
   150  // add adds the transferred part of a server basket to the reference basket while scaling
   151  // value amounts so that their sum equals the total value calculated according to the
   152  // previous reqValues.
   153  func (r *referenceBasket) add(newBasket requestBasket) {
   154  	r.basket.setExp(newBasket.exp)
   155  	// scale newBasket to match service unit value
   156  	var (
   157  		totalCost  uint64
   158  		totalValue float64
   159  	)
   160  	for i, v := range newBasket.items {
   161  		totalCost += v.value
   162  		totalValue += float64(v.amount) * r.reqValues[i]
   163  	}
   164  	if totalCost > 0 {
   165  		// add to reference with scaled values
   166  		scaleValues := totalValue / float64(totalCost)
   167  		for i, v := range newBasket.items {
   168  			r.basket.items[i].amount += v.amount
   169  			r.basket.items[i].value += uint64(float64(v.value) * scaleValues)
   170  		}
   171  	}
   172  	r.updateReqValues()
   173  }
   174  
   175  // updateReqValues recalculates reqValues after adding transferred baskets. Note that
   176  // values should be normalized first.
   177  func (r *referenceBasket) updateReqValues() {
   178  	r.reqValues = make([]float64, len(r.reqValues))
   179  	for i, b := range r.basket.items {
   180  		if b.amount > 0 {
   181  			r.reqValues[i] = float64(b.value) / float64(b.amount)
   182  		} else {
   183  			r.reqValues[i] = 0
   184  		}
   185  	}
   186  }
   187  
   188  // normalize ensures that the sum of values equal the sum of amounts in the basket.
   189  func (r *referenceBasket) normalize() {
   190  	var sumAmount, sumValue uint64
   191  	for _, b := range r.basket.items {
   192  		sumAmount += b.amount
   193  		sumValue += b.value
   194  	}
   195  	add := float64(int64(sumAmount-sumValue)) / float64(sumValue)
   196  	for i, b := range r.basket.items {
   197  		b.value += uint64(int64(float64(b.value) * add))
   198  		r.basket.items[i] = b
   199  	}
   200  }
   201  
   202  // reqValueFactor calculates the request value factor applicable to the server with
   203  // the given announced request cost list
   204  func (r *referenceBasket) reqValueFactor(costList []uint64) float64 {
   205  	var (
   206  		totalCost  float64
   207  		totalValue uint64
   208  	)
   209  	for i, b := range r.basket.items {
   210  		totalCost += float64(costList[i]) * float64(b.amount) // use floats to avoid overflow
   211  		totalValue += b.value
   212  	}
   213  	if totalCost < 1 {
   214  		return 0
   215  	}
   216  	return float64(totalValue) * basketFactor / totalCost
   217  }
   218  
   219  // EncodeRLP implements rlp.Encoder
   220  func (b *basketItem) EncodeRLP(w io.Writer) error {
   221  	return rlp.Encode(w, []interface{}{b.amount, b.value})
   222  }
   223  
   224  // DecodeRLP implements rlp.Decoder
   225  func (b *basketItem) DecodeRLP(s *rlp.Stream) error {
   226  	var item struct {
   227  		Amount, Value uint64
   228  	}
   229  	if err := s.Decode(&item); err != nil {
   230  		return err
   231  	}
   232  	b.amount, b.value = item.Amount, item.Value
   233  	return nil
   234  }
   235  
   236  // EncodeRLP implements rlp.Encoder
   237  func (r *requestBasket) EncodeRLP(w io.Writer) error {
   238  	return rlp.Encode(w, []interface{}{r.items, r.exp})
   239  }
   240  
   241  // DecodeRLP implements rlp.Decoder
   242  func (r *requestBasket) DecodeRLP(s *rlp.Stream) error {
   243  	var enc struct {
   244  		Items []basketItem
   245  		Exp   uint64
   246  	}
   247  	if err := s.Decode(&enc); err != nil {
   248  		return err
   249  	}
   250  	r.items, r.exp = enc.Items, enc.Exp
   251  	return nil
   252  }
   253  
   254  // convertMapping converts a basket loaded from the database into the current format.
   255  // If the available request types and their mapping into the service vector differ from
   256  // the one used when saving the basket then this function reorders old fields and fills
   257  // in previously unknown fields by scaling up amounts and values taken from the
   258  // initialization basket.
   259  func (r requestBasket) convertMapping(oldMapping, newMapping []string, initBasket requestBasket) requestBasket {
   260  	nameMap := make(map[string]int)
   261  	for i, name := range oldMapping {
   262  		nameMap[name] = i
   263  	}
   264  	rc := requestBasket{items: make([]basketItem, len(newMapping))}
   265  	var scale, oldScale, newScale float64
   266  	for i, name := range newMapping {
   267  		if ii, ok := nameMap[name]; ok {
   268  			rc.items[i] = r.items[ii]
   269  			oldScale += float64(initBasket.items[i].amount) * float64(initBasket.items[i].amount)
   270  			newScale += float64(rc.items[i].amount) * float64(initBasket.items[i].amount)
   271  		}
   272  	}
   273  	if oldScale > 1e-10 {
   274  		scale = newScale / oldScale
   275  	} else {
   276  		scale = 1
   277  	}
   278  	for i, name := range newMapping {
   279  		if _, ok := nameMap[name]; !ok {
   280  			rc.items[i].amount = uint64(float64(initBasket.items[i].amount) * scale)
   281  			rc.items[i].value = uint64(float64(initBasket.items[i].value) * scale)
   282  		}
   283  	}
   284  	return rc
   285  }