github.com/klaytn/klaytn@v1.12.1/blockchain/spam_throttler.go (about)

     1  // Copyright 2021 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn 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 klaytn 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 klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package blockchain
    18  
    19  import (
    20  	"errors"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/rcrowley/go-metrics"
    25  
    26  	"github.com/klaytn/klaytn/blockchain/types"
    27  	"github.com/klaytn/klaytn/common"
    28  )
    29  
    30  // TODO-Klaytn: move these variables into TxPool when BlockChain struct contains a TxPool interface
    31  // spamThrottler need to be accessed by both of TxPool and BlockChain.
    32  var (
    33  	spamThrottler   *throttler = nil
    34  	spamThrottlerMu            = new(sync.RWMutex)
    35  )
    36  
    37  var (
    38  	thresholdGauge           = metrics.NewRegisteredGauge("txpool/throttler/threshold", nil)
    39  	candidateSizeGauge       = metrics.NewRegisteredGauge("txpool/throttler/candidate/size", nil)
    40  	throttledSizeGauge       = metrics.NewRegisteredGauge("txpool/throttler/throttled/size", nil)
    41  	allowedSizeGauge         = metrics.NewRegisteredGauge("txpool/throttler/allowed/size", nil)
    42  	throttlerUpdateTimeGauge = metrics.NewRegisteredGauge("txpool/throttler/update/time", nil)
    43  	throttlerDropCount       = metrics.NewRegisteredCounter("txpool/throttler/dropped/count", nil)
    44  )
    45  
    46  type throttler struct {
    47  	config *ThrottlerConfig
    48  
    49  	candidates map[common.Address]int  // throttle candidates with spam weight. Not for concurrent use
    50  	throttled  map[common.Address]int  // throttled addresses with throttle time. Requires mu.lock for concurrent use
    51  	allowed    map[common.Address]bool // white listed addresses. Requires mu.lock for concurrent use
    52  	mu         *sync.RWMutex           // mutex for throttled and allowed
    53  
    54  	threshold  int
    55  	throttleCh chan *types.Transaction
    56  	quitCh     chan struct{}
    57  }
    58  
    59  type ThrottlerConfig struct {
    60  	ActivateTxPoolSize uint `json:"activate_tx_pool_size"`
    61  	TargetFailRatio    uint `json:"target_fail_ratio"`
    62  	ThrottleTPS        uint `json:"throttle_tps"`
    63  	MaxCandidates      uint `json:"max_candidates"`
    64  
    65  	IncreaseWeight      int `json:"increase_weight"`
    66  	DecreaseWeight      int `json:"decrease_weight"`
    67  	InitialThreshold    int `json:"initial_threshold"`
    68  	MinimumThreshold    int `json:"minimum_threshold"`
    69  	ThresholdAdjustment int `json:"threshold_adjustment"`
    70  	ThrottleSeconds     int `json:"throttle_seconds"`
    71  }
    72  
    73  var DefaultSpamThrottlerConfig = &ThrottlerConfig{
    74  	ActivateTxPoolSize: 1000,
    75  	TargetFailRatio:    20,
    76  	ThrottleTPS:        100,   // len(throttleCh) = ThrottleTPS * 5. 32KB * 100 * 5 = 16MB
    77  	MaxCandidates:      10000, // (20 + 4)B * 10000 = 240KB
    78  
    79  	IncreaseWeight:      5,
    80  	DecreaseWeight:      1,
    81  	InitialThreshold:    500,
    82  	MinimumThreshold:    100,
    83  	ThresholdAdjustment: 5,
    84  	ThrottleSeconds:     300,
    85  }
    86  
    87  func GetSpamThrottler() *throttler {
    88  	spamThrottlerMu.RLock()
    89  	t := spamThrottler
    90  	spamThrottlerMu.RUnlock()
    91  	return t
    92  }
    93  
    94  func validateConfig(conf *ThrottlerConfig) error {
    95  	if conf == nil {
    96  		return errors.New("nil ThrottlerConfig")
    97  	}
    98  	if conf.TargetFailRatio > 100 {
    99  		return errors.New("invalid ThrottlerConfig. 0 <= TargetFailRatio <= 100")
   100  	}
   101  	if conf.InitialThreshold < conf.MinimumThreshold {
   102  		return errors.New("invalid ThrottlerConfig. MinimumThreshold <= InitialThreshold")
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // adjustThreshold adjusts the spam weight threshold of throttler in an adaptive way.
   109  func (t *throttler) adjustThreshold(ratio uint) {
   110  	var newThreshold int
   111  	// Decrease threshold if a fail ratio is bigger than target value to put more addresses in throttled map
   112  	if ratio > t.config.TargetFailRatio {
   113  		if t.threshold-t.config.ThresholdAdjustment > t.config.MinimumThreshold {
   114  			newThreshold = t.threshold - t.config.ThresholdAdjustment
   115  		} else {
   116  			// Set minimum threshold
   117  			newThreshold = t.config.MinimumThreshold
   118  		}
   119  
   120  		// Increase threshold if a fail ratio is smaller than target ratio until it exceeds InitialThreshold
   121  	} else {
   122  		if t.threshold+t.config.ThresholdAdjustment < t.config.InitialThreshold {
   123  			newThreshold = t.threshold + t.config.ThresholdAdjustment
   124  		} else {
   125  			// Set maximum threshold
   126  			newThreshold = t.config.InitialThreshold
   127  		}
   128  	}
   129  
   130  	t.threshold = newThreshold
   131  
   132  	// Update metrics
   133  	thresholdGauge.Update(int64(newThreshold))
   134  }
   135  
   136  // newAllowed generates a new allowed list of throttler.
   137  func (t *throttler) newAllowed(allowed []common.Address) {
   138  	t.mu.Lock()
   139  	defer t.mu.Unlock()
   140  
   141  	a := make(map[common.Address]bool, len(allowed))
   142  	for _, addr := range allowed {
   143  		a[addr] = true
   144  	}
   145  	t.allowed = a
   146  }
   147  
   148  // updateThrottled removes outdated addresses from the throttle list and adds new addresses to the list.
   149  func (t *throttler) updateThrottled(newThrottled []common.Address) {
   150  	var removeThrottled []common.Address
   151  	t.mu.Lock()
   152  	defer t.mu.Unlock()
   153  
   154  	// Decrease throttling remained time for all throttled addresses.
   155  	for addr, remained := range t.throttled {
   156  		t.throttled[addr] = remained - 1
   157  		if t.throttled[addr] < 0 {
   158  			removeThrottled = append(removeThrottled, addr)
   159  		}
   160  	}
   161  
   162  	// Remove throttled addresses from throttled map.
   163  	for _, addr := range removeThrottled {
   164  		delete(t.throttled, addr)
   165  	}
   166  
   167  	for _, addr := range newThrottled {
   168  		t.throttled[addr] = t.config.ThrottleSeconds
   169  	}
   170  
   171  	// Update metrics
   172  	throttledSizeGauge.Update(int64(len(t.throttled)))
   173  	allowedSizeGauge.Update(int64(len(t.allowed)))
   174  }
   175  
   176  // updateThrottlerState updates the throttle list by calculating spam weight of candidates.
   177  func (t *throttler) updateThrottlerState(txs types.Transactions, receipts types.Receipts) {
   178  	var removeCandidate []common.Address
   179  	var newThrottled []common.Address
   180  
   181  	startTime := time.Now()
   182  	numFailed := 0
   183  	failRatio := uint(0)
   184  	mapSize := uint(len(t.candidates))
   185  
   186  	// Increase spam weight of throttle candidates who generate failed txs.
   187  	for i, receipt := range receipts {
   188  		if receipt.Status != types.ReceiptStatusSuccessful {
   189  			numFailed++
   190  
   191  			toAddr := txs[i].To()
   192  			if toAddr == nil {
   193  				continue
   194  			}
   195  
   196  			weight := t.candidates[*toAddr]
   197  			if weight == 0 {
   198  				if mapSize >= t.config.MaxCandidates {
   199  					continue
   200  				}
   201  				mapSize++
   202  			}
   203  
   204  			t.candidates[*toAddr] = weight + t.config.IncreaseWeight
   205  		}
   206  	}
   207  
   208  	// Decrease spam weight for all candidates and update throttle lists in throttled.
   209  	for addr, weight := range t.candidates {
   210  		newWeight := weight - t.config.DecreaseWeight
   211  
   212  		switch {
   213  		case newWeight <= 0:
   214  			removeCandidate = append(removeCandidate, addr)
   215  
   216  		case newWeight > t.threshold:
   217  			removeCandidate = append(removeCandidate, addr)
   218  			newThrottled = append(newThrottled, addr)
   219  
   220  		default:
   221  			t.candidates[addr] = newWeight
   222  		}
   223  	}
   224  
   225  	// Remove throttle candidates from candidates map.
   226  	for _, addr := range removeCandidate {
   227  		delete(t.candidates, addr)
   228  	}
   229  
   230  	if len(receipts) != 0 {
   231  		failRatio = uint(100 * numFailed / len(receipts))
   232  	}
   233  
   234  	// Update throttled and threshold
   235  	t.updateThrottled(newThrottled)
   236  	t.adjustThreshold(failRatio)
   237  
   238  	// Update metrics
   239  	candidateSizeGauge.Update(int64(len(t.candidates)))
   240  	throttlerUpdateTimeGauge.Update(int64(time.Since(startTime)))
   241  }
   242  
   243  // classifyTxs classifies given txs into allowTxs and throttleTxs.
   244  // If to-address of tx is listed in the throttle list, it is classified as throttleTx.
   245  func (t *throttler) classifyTxs(txs types.Transactions) (types.Transactions, types.Transactions) {
   246  	allowTxs := txs[:0]
   247  	throttleTxs := txs[:0]
   248  
   249  	t.mu.RLock()
   250  	for _, tx := range txs {
   251  		if tx.To() != nil && t.throttled[*tx.To()] > 0 && t.allowed[*tx.To()] == false {
   252  			throttleTxs = append(throttleTxs, tx)
   253  		} else {
   254  			allowTxs = append(allowTxs, tx)
   255  		}
   256  	}
   257  	t.mu.RUnlock()
   258  
   259  	return allowTxs, throttleTxs
   260  }
   261  
   262  // SetAllowed resets the allowed list of throttler. The previous list will be abandoned.
   263  func (t *throttler) SetAllowed(list []common.Address) {
   264  	t.mu.Lock()
   265  	defer t.mu.Unlock()
   266  
   267  	t.allowed = make(map[common.Address]bool)
   268  	for _, addr := range list {
   269  		t.allowed[addr] = true
   270  	}
   271  }
   272  
   273  func (t *throttler) GetAllowed() []common.Address {
   274  	t.mu.RLock()
   275  	defer t.mu.RUnlock()
   276  
   277  	allowList := make([]common.Address, 0)
   278  	for addr := range t.allowed {
   279  		allowList = append(allowList, addr)
   280  	}
   281  	return allowList
   282  }
   283  
   284  func (t *throttler) GetThrottled() []common.Address {
   285  	t.mu.RLock()
   286  	defer t.mu.RUnlock()
   287  
   288  	throttledList := make([]common.Address, 0)
   289  	for addr := range t.throttled {
   290  		throttledList = append(throttledList, addr)
   291  	}
   292  	return throttledList
   293  }
   294  
   295  func (t *throttler) GetCandidates() map[common.Address]int {
   296  	t.mu.RLock()
   297  	defer t.mu.RUnlock()
   298  
   299  	return t.candidates
   300  }
   301  
   302  func (t *throttler) GetConfig() *ThrottlerConfig {
   303  	return t.config
   304  }