github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/lespay/server/balance_tracker.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 server
    18  
    19  import (
    20  	"reflect"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/cryptogateway/go-paymex/common/mclock"
    25  	"github.com/cryptogateway/go-paymex/ethdb"
    26  	"github.com/cryptogateway/go-paymex/les/utils"
    27  	"github.com/cryptogateway/go-paymex/p2p/enode"
    28  	"github.com/cryptogateway/go-paymex/p2p/nodestate"
    29  )
    30  
    31  const (
    32  	posThreshold             = 1000000         // minimum positive balance that is persisted in the database
    33  	negThreshold             = 1000000         // minimum negative balance that is persisted in the database
    34  	persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence
    35  )
    36  
    37  // BalanceTrackerSetup contains node state flags and fields used by BalanceTracker
    38  type BalanceTrackerSetup struct {
    39  	// controlled by PriorityPool
    40  	PriorityFlag, UpdateFlag nodestate.Flags
    41  	BalanceField             nodestate.Field
    42  	// external connections
    43  	connAddressField, capacityField nodestate.Field
    44  }
    45  
    46  // NewBalanceTrackerSetup creates a new BalanceTrackerSetup and initializes the fields
    47  // and flags controlled by BalanceTracker
    48  func NewBalanceTrackerSetup(setup *nodestate.Setup) BalanceTrackerSetup {
    49  	return BalanceTrackerSetup{
    50  		// PriorityFlag is set if the node has a positive balance
    51  		PriorityFlag: setup.NewFlag("priorityNode"),
    52  		// UpdateFlag set and then immediately reset if the balance has been updated and
    53  		// therefore priority is suddenly changed
    54  		UpdateFlag: setup.NewFlag("balanceUpdate"),
    55  		// BalanceField contains the NodeBalance struct which implements nodePriority,
    56  		// allowing on-demand priority calculation and future priority estimation
    57  		BalanceField: setup.NewField("balance", reflect.TypeOf(&NodeBalance{})),
    58  	}
    59  }
    60  
    61  // Connect sets the fields used by BalanceTracker as an input
    62  func (bts *BalanceTrackerSetup) Connect(connAddressField, capacityField nodestate.Field) {
    63  	bts.connAddressField = connAddressField
    64  	bts.capacityField = capacityField
    65  }
    66  
    67  // BalanceTracker tracks positive and negative balances for connected nodes.
    68  // After connAddressField is set externally, a NodeBalance is created and previous
    69  // balance values are loaded from the database. Both balances are exponentially expired
    70  // values. Costs are deducted from the positive balance if present, otherwise added to
    71  // the negative balance. If the capacity is non-zero then a time cost is applied
    72  // continuously while individual request costs are applied immediately.
    73  // The two balances are translated into a single priority value that also depends
    74  // on the actual capacity.
    75  type BalanceTracker struct {
    76  	BalanceTrackerSetup
    77  	clock              mclock.Clock
    78  	lock               sync.Mutex
    79  	ns                 *nodestate.NodeStateMachine
    80  	ndb                *nodeDB
    81  	posExp, negExp     utils.ValueExpirer
    82  	posExpTC, negExpTC uint64
    83  
    84  	active, inactive utils.ExpiredValue
    85  	balanceTimer     *utils.UpdateTimer
    86  	quit             chan struct{}
    87  }
    88  
    89  // NewBalanceTracker creates a new BalanceTracker
    90  func NewBalanceTracker(ns *nodestate.NodeStateMachine, setup BalanceTrackerSetup, db ethdb.KeyValueStore, clock mclock.Clock, posExp, negExp utils.ValueExpirer) *BalanceTracker {
    91  	ndb := newNodeDB(db, clock)
    92  	bt := &BalanceTracker{
    93  		ns:                  ns,
    94  		BalanceTrackerSetup: setup,
    95  		ndb:                 ndb,
    96  		clock:               clock,
    97  		posExp:              posExp,
    98  		negExp:              negExp,
    99  		balanceTimer:        utils.NewUpdateTimer(clock, time.Second*10),
   100  		quit:                make(chan struct{}),
   101  	}
   102  	bt.ndb.forEachBalance(false, func(id enode.ID, balance utils.ExpiredValue) bool {
   103  		bt.inactive.AddExp(balance)
   104  		return true
   105  	})
   106  
   107  	ns.SubscribeField(bt.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
   108  		n, _ := ns.GetField(node, bt.BalanceField).(*NodeBalance)
   109  		if n == nil {
   110  			return
   111  		}
   112  
   113  		ov, _ := oldValue.(uint64)
   114  		nv, _ := newValue.(uint64)
   115  		if ov == 0 && nv != 0 {
   116  			n.activate()
   117  		}
   118  		if nv != 0 {
   119  			n.setCapacity(nv)
   120  		}
   121  		if ov != 0 && nv == 0 {
   122  			n.deactivate()
   123  		}
   124  	})
   125  	ns.SubscribeField(bt.connAddressField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
   126  		if newValue != nil {
   127  			ns.SetFieldSub(node, bt.BalanceField, bt.newNodeBalance(node, newValue.(string)))
   128  		} else {
   129  			ns.SetStateSub(node, nodestate.Flags{}, bt.PriorityFlag, 0)
   130  			if b, _ := ns.GetField(node, bt.BalanceField).(*NodeBalance); b != nil {
   131  				b.deactivate()
   132  			}
   133  			ns.SetFieldSub(node, bt.BalanceField, nil)
   134  		}
   135  	})
   136  
   137  	// The positive and negative balances of clients are stored in database
   138  	// and both of these decay exponentially over time. Delete them if the
   139  	// value is small enough.
   140  	bt.ndb.evictCallBack = bt.canDropBalance
   141  
   142  	go func() {
   143  		for {
   144  			select {
   145  			case <-clock.After(persistExpirationRefresh):
   146  				now := clock.Now()
   147  				bt.ndb.setExpiration(posExp.LogOffset(now), negExp.LogOffset(now))
   148  			case <-bt.quit:
   149  				return
   150  			}
   151  		}
   152  	}()
   153  	return bt
   154  }
   155  
   156  // Stop saves expiration offset and unsaved node balances and shuts BalanceTracker down
   157  func (bt *BalanceTracker) Stop() {
   158  	now := bt.clock.Now()
   159  	bt.ndb.setExpiration(bt.posExp.LogOffset(now), bt.negExp.LogOffset(now))
   160  	close(bt.quit)
   161  	bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
   162  		if n, ok := bt.ns.GetField(node, bt.BalanceField).(*NodeBalance); ok {
   163  			n.lock.Lock()
   164  			n.storeBalance(true, true)
   165  			n.lock.Unlock()
   166  			bt.ns.SetField(node, bt.BalanceField, nil)
   167  		}
   168  	})
   169  	bt.ndb.close()
   170  }
   171  
   172  // TotalTokenAmount returns the current total amount of service tokens in existence
   173  func (bt *BalanceTracker) TotalTokenAmount() uint64 {
   174  	bt.lock.Lock()
   175  	defer bt.lock.Unlock()
   176  
   177  	bt.balanceTimer.Update(func(_ time.Duration) bool {
   178  		bt.active = utils.ExpiredValue{}
   179  		bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
   180  			if n, ok := bt.ns.GetField(node, bt.BalanceField).(*NodeBalance); ok {
   181  				pos, _ := n.GetRawBalance()
   182  				bt.active.AddExp(pos)
   183  			}
   184  		})
   185  		return true
   186  	})
   187  	total := bt.active
   188  	total.AddExp(bt.inactive)
   189  	return total.Value(bt.posExp.LogOffset(bt.clock.Now()))
   190  }
   191  
   192  // GetPosBalanceIDs lists node IDs with an associated positive balance
   193  func (bt *BalanceTracker) GetPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) {
   194  	return bt.ndb.getPosBalanceIDs(start, stop, maxCount)
   195  }
   196  
   197  // SetExpirationTCs sets positive and negative token expiration time constants.
   198  // Specified in seconds, 0 means infinite (no expiration).
   199  func (bt *BalanceTracker) SetExpirationTCs(pos, neg uint64) {
   200  	bt.lock.Lock()
   201  	defer bt.lock.Unlock()
   202  
   203  	bt.posExpTC, bt.negExpTC = pos, neg
   204  	now := bt.clock.Now()
   205  	if pos > 0 {
   206  		bt.posExp.SetRate(now, 1/float64(pos*uint64(time.Second)))
   207  	} else {
   208  		bt.posExp.SetRate(now, 0)
   209  	}
   210  	if neg > 0 {
   211  		bt.negExp.SetRate(now, 1/float64(neg*uint64(time.Second)))
   212  	} else {
   213  		bt.negExp.SetRate(now, 0)
   214  	}
   215  }
   216  
   217  // GetExpirationTCs returns the current positive and negative token expiration
   218  // time constants
   219  func (bt *BalanceTracker) GetExpirationTCs() (pos, neg uint64) {
   220  	bt.lock.Lock()
   221  	defer bt.lock.Unlock()
   222  
   223  	return bt.posExpTC, bt.negExpTC
   224  }
   225  
   226  // newNodeBalance loads balances from the database and creates a NodeBalance instance
   227  // for the given node. It also sets the PriorityFlag and adds balanceCallbackZero if
   228  // the node has a positive balance.
   229  // Note: this function should run inside a NodeStateMachine operation
   230  func (bt *BalanceTracker) newNodeBalance(node *enode.Node, negBalanceKey string) *NodeBalance {
   231  	pb := bt.ndb.getOrNewBalance(node.ID().Bytes(), false)
   232  	nb := bt.ndb.getOrNewBalance([]byte(negBalanceKey), true)
   233  	n := &NodeBalance{
   234  		bt:          bt,
   235  		node:        node,
   236  		connAddress: negBalanceKey,
   237  		balance:     balance{pos: pb, neg: nb},
   238  		initTime:    bt.clock.Now(),
   239  		lastUpdate:  bt.clock.Now(),
   240  	}
   241  	for i := range n.callbackIndex {
   242  		n.callbackIndex[i] = -1
   243  	}
   244  	if n.checkPriorityStatus() {
   245  		n.bt.ns.SetStateSub(n.node, n.bt.PriorityFlag, nodestate.Flags{}, 0)
   246  	}
   247  	return n
   248  }
   249  
   250  // storeBalance stores either a positive or a negative balance in the database
   251  func (bt *BalanceTracker) storeBalance(id []byte, neg bool, value utils.ExpiredValue) {
   252  	if bt.canDropBalance(bt.clock.Now(), neg, value) {
   253  		bt.ndb.delBalance(id, neg) // balance is small enough, drop it directly.
   254  	} else {
   255  		bt.ndb.setBalance(id, neg, value)
   256  	}
   257  }
   258  
   259  // canDropBalance tells whether a positive or negative balance is below the threshold
   260  // and therefore can be dropped from the database
   261  func (bt *BalanceTracker) canDropBalance(now mclock.AbsTime, neg bool, b utils.ExpiredValue) bool {
   262  	if neg {
   263  		return b.Value(bt.negExp.LogOffset(now)) <= negThreshold
   264  	}
   265  	return b.Value(bt.posExp.LogOffset(now)) <= posThreshold
   266  }
   267  
   268  // updateTotalBalance adjusts the total balance after executing given callback.
   269  func (bt *BalanceTracker) updateTotalBalance(n *NodeBalance, callback func() bool) {
   270  	bt.lock.Lock()
   271  	defer bt.lock.Unlock()
   272  
   273  	n.lock.Lock()
   274  	defer n.lock.Unlock()
   275  
   276  	original, active := n.balance.pos, n.active
   277  	if !callback() {
   278  		return
   279  	}
   280  	if active {
   281  		bt.active.SubExp(original)
   282  	} else {
   283  		bt.inactive.SubExp(original)
   284  	}
   285  	if n.active {
   286  		bt.active.AddExp(n.balance.pos)
   287  	} else {
   288  		bt.inactive.AddExp(n.balance.pos)
   289  	}
   290  }