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 }