github.com/lukso-network/go-ethereum@v1.8.22/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/ethereum/go-ethereum/common/mclock"
    27  	"github.com/ethereum/go-ethereum/common/prque"
    28  	"github.com/ethereum/go-ethereum/ethdb"
    29  	"github.com/ethereum/go-ethereum/log"
    30  	"github.com/ethereum/go-ethereum/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  }