github.com/luckypickle/go-ethereum-vet@v1.14.2/les/freeclient.go (about) 1 // Copyright 2016 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 les implements the Light Ethereum Subprotocol. 18 package les 19 20 import ( 21 "io" 22 "math" 23 "sync" 24 "time" 25 26 "github.com/luckypickle/go-ethereum-vet/common/mclock" 27 "github.com/luckypickle/go-ethereum-vet/common/prque" 28 "github.com/luckypickle/go-ethereum-vet/ethdb" 29 "github.com/luckypickle/go-ethereum-vet/log" 30 "github.com/luckypickle/go-ethereum-vet/rlp" 31 ) 32 33 // freeClientPool implements a client database that limits the connection time 34 // of each client and manages accepting/rejecting incoming connections and even 35 // kicking out some connected clients. The pool calculates recent usage time 36 // for each known client (a value that increases linearly when the client is 37 // connected and decreases exponentially when not connected). Clients with lower 38 // recent usage are preferred, unknown nodes have the highest priority. Already 39 // connected nodes receive a small bias in their favor in order to avoid accepting 40 // and instantly kicking out clients. 41 // 42 // Note: the pool can use any string for client identification. Using signature 43 // keys for that purpose would not make sense when being known has a negative 44 // value for the client. Currently the LES protocol manager uses IP addresses 45 // (without port address) to identify clients. 46 type freeClientPool struct { 47 db ethdb.Database 48 lock sync.Mutex 49 clock mclock.Clock 50 closed bool 51 52 connectedLimit, totalLimit int 53 54 addressMap map[string]*freeClientPoolEntry 55 connPool, disconnPool *prque.Prque 56 startupTime mclock.AbsTime 57 logOffsetAtStartup int64 58 } 59 60 const ( 61 recentUsageExpTC = time.Hour // time constant of the exponential weighting window for "recent" server usage 62 fixedPointMultiplier = 0x1000000 // constant to convert logarithms to fixed point format 63 connectedBias = time.Minute // this bias is applied in favor of already connected clients in order to avoid kicking them out very soon 64 ) 65 66 // newFreeClientPool creates a new free client pool 67 func newFreeClientPool(db ethdb.Database, connectedLimit, totalLimit int, clock mclock.Clock) *freeClientPool { 68 pool := &freeClientPool{ 69 db: db, 70 clock: clock, 71 addressMap: make(map[string]*freeClientPoolEntry), 72 connPool: prque.New(poolSetIndex), 73 disconnPool: prque.New(poolSetIndex), 74 connectedLimit: connectedLimit, 75 totalLimit: totalLimit, 76 } 77 pool.loadFromDb() 78 return pool 79 } 80 81 func (f *freeClientPool) stop() { 82 f.lock.Lock() 83 f.closed = true 84 f.saveToDb() 85 f.lock.Unlock() 86 } 87 88 // connect should be called after a successful handshake. If the connection was 89 // rejected, there is no need to call disconnect. 90 // 91 // Note: the disconnectFn callback should not block. 92 func (f *freeClientPool) connect(address string, disconnectFn func()) bool { 93 f.lock.Lock() 94 defer f.lock.Unlock() 95 96 if f.closed { 97 return false 98 } 99 e := f.addressMap[address] 100 now := f.clock.Now() 101 var recentUsage int64 102 if e == nil { 103 e = &freeClientPoolEntry{address: address, index: -1} 104 f.addressMap[address] = e 105 } else { 106 if e.connected { 107 log.Debug("Client already connected", "address", address) 108 return false 109 } 110 recentUsage = int64(math.Exp(float64(e.logUsage-f.logOffset(now)) / fixedPointMultiplier)) 111 } 112 e.linUsage = recentUsage - int64(now) 113 // check whether (linUsage+connectedBias) is smaller than the highest entry in the connected pool 114 if f.connPool.Size() == f.connectedLimit { 115 i := f.connPool.PopItem().(*freeClientPoolEntry) 116 if e.linUsage+int64(connectedBias)-i.linUsage < 0 { 117 // kick it out and accept the new client 118 f.connPool.Remove(i.index) 119 f.calcLogUsage(i, now) 120 i.connected = false 121 f.disconnPool.Push(i, -i.logUsage) 122 log.Debug("Client kicked out", "address", i.address) 123 i.disconnectFn() 124 } else { 125 // keep the old client and reject the new one 126 f.connPool.Push(i, i.linUsage) 127 log.Debug("Client rejected", "address", address) 128 return false 129 } 130 } 131 f.disconnPool.Remove(e.index) 132 e.connected = true 133 e.disconnectFn = disconnectFn 134 f.connPool.Push(e, e.linUsage) 135 if f.connPool.Size()+f.disconnPool.Size() > f.totalLimit { 136 f.disconnPool.Pop() 137 } 138 log.Debug("Client accepted", "address", address) 139 return true 140 } 141 142 // disconnect should be called when a connection is terminated. If the disconnection 143 // was initiated by the pool itself using disconnectFn then calling disconnect is 144 // not necessary but permitted. 145 func (f *freeClientPool) disconnect(address string) { 146 f.lock.Lock() 147 defer f.lock.Unlock() 148 149 if f.closed { 150 return 151 } 152 e := f.addressMap[address] 153 now := f.clock.Now() 154 if !e.connected { 155 log.Debug("Client already disconnected", "address", address) 156 return 157 } 158 159 f.connPool.Remove(e.index) 160 f.calcLogUsage(e, now) 161 e.connected = false 162 f.disconnPool.Push(e, -e.logUsage) 163 log.Debug("Client disconnected", "address", address) 164 } 165 166 // logOffset calculates the time-dependent offset for the logarithmic 167 // representation of recent usage 168 func (f *freeClientPool) logOffset(now mclock.AbsTime) int64 { 169 // Note: fixedPointMultiplier acts as a multiplier here; the reason for dividing the divisor 170 // is to avoid int64 overflow. We assume that int64(recentUsageExpTC) >> fixedPointMultiplier. 171 logDecay := int64((time.Duration(now - f.startupTime)) / (recentUsageExpTC / fixedPointMultiplier)) 172 return f.logOffsetAtStartup + logDecay 173 } 174 175 // calcLogUsage converts recent usage from linear to logarithmic representation 176 // when disconnecting a peer or closing the client pool 177 func (f *freeClientPool) calcLogUsage(e *freeClientPoolEntry, now mclock.AbsTime) { 178 dt := e.linUsage + int64(now) 179 if dt < 1 { 180 dt = 1 181 } 182 e.logUsage = int64(math.Log(float64(dt))*fixedPointMultiplier) + f.logOffset(now) 183 } 184 185 // freeClientPoolStorage is the RLP representation of the pool's database storage 186 type freeClientPoolStorage struct { 187 LogOffset uint64 188 List []*freeClientPoolEntry 189 } 190 191 // loadFromDb restores pool status from the database storage 192 // (automatically called at initialization) 193 func (f *freeClientPool) loadFromDb() { 194 enc, err := f.db.Get([]byte("freeClientPool")) 195 if err != nil { 196 return 197 } 198 var storage freeClientPoolStorage 199 err = rlp.DecodeBytes(enc, &storage) 200 if err != nil { 201 log.Error("Failed to decode client list", "err", err) 202 return 203 } 204 f.logOffsetAtStartup = int64(storage.LogOffset) 205 f.startupTime = f.clock.Now() 206 for _, e := range storage.List { 207 log.Debug("Loaded free client record", "address", e.address, "logUsage", e.logUsage) 208 f.addressMap[e.address] = e 209 f.disconnPool.Push(e, -e.logUsage) 210 } 211 } 212 213 // saveToDb saves pool status to the database storage 214 // (automatically called during shutdown) 215 func (f *freeClientPool) saveToDb() { 216 now := f.clock.Now() 217 storage := freeClientPoolStorage{ 218 LogOffset: uint64(f.logOffset(now)), 219 List: make([]*freeClientPoolEntry, len(f.addressMap)), 220 } 221 i := 0 222 for _, e := range f.addressMap { 223 if e.connected { 224 f.calcLogUsage(e, now) 225 } 226 storage.List[i] = e 227 i++ 228 } 229 enc, err := rlp.EncodeToBytes(storage) 230 if err != nil { 231 log.Error("Failed to encode client list", "err", err) 232 } else { 233 f.db.Put([]byte("freeClientPool"), enc) 234 } 235 } 236 237 // freeClientPoolEntry represents a client address known by the pool. 238 // When connected, recent usage is calculated as linUsage + int64(clock.Now()) 239 // When disconnected, it is calculated as exp(logUsage - logOffset) where logOffset 240 // also grows linearly with time while the server is running. 241 // Conversion between linear and logarithmic representation happens when connecting 242 // or disconnecting the node. 243 // 244 // Note: linUsage and logUsage are values used with constantly growing offsets so 245 // even though they are close to each other at any time they may wrap around int64 246 // limits over time. Comparison should be performed accordingly. 247 type freeClientPoolEntry struct { 248 address string 249 connected bool 250 disconnectFn func() 251 linUsage, logUsage int64 252 index int 253 } 254 255 func (e *freeClientPoolEntry) EncodeRLP(w io.Writer) error { 256 return rlp.Encode(w, []interface{}{e.address, uint64(e.logUsage)}) 257 } 258 259 func (e *freeClientPoolEntry) DecodeRLP(s *rlp.Stream) error { 260 var entry struct { 261 Address string 262 LogUsage uint64 263 } 264 if err := s.Decode(&entry); err != nil { 265 return err 266 } 267 e.address = entry.Address 268 e.logUsage = int64(entry.LogUsage) 269 e.connected = false 270 e.index = -1 271 return nil 272 } 273 274 // poolSetIndex callback is used by both priority queues to set/update the index of 275 // the element in the queue. Index is needed to remove elements other than the top one. 276 func poolSetIndex(a interface{}, i int) { 277 a.(*freeClientPoolEntry).index = i 278 }