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