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 }