github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/builder/collection/rate_limiter.go (about) 1 package collection 2 3 import ( 4 "math" 5 6 "github.com/onflow/flow-go/model/flow" 7 ) 8 9 // rateLimiter implements payer-based rate limiting. See Config for details. 10 type rateLimiter struct { 11 12 // maximum rate of transactions/payer/collection (from Config) 13 rate float64 14 // Derived fields based on the rate. Only one will be used: 15 // - if rate >= 1, then txPerBlock is set to the number of transactions allowed per payer per block 16 // - if rate < 1, then blocksPerTx is set to the number of consecutive blocks in which one transaction per payer is allowed 17 txPerBlock uint 18 blocksPerTx uint64 19 20 // set of unlimited payer address (from Config) 21 unlimited map[flow.Address]struct{} 22 // height of the collection we are building 23 height uint64 24 25 // for each payer, height of latest collection in which a transaction for 26 // which they were payer was included 27 latestCollectionHeight map[flow.Address]uint64 28 // number of transactions included in the currently built block per payer 29 txIncludedCount map[flow.Address]uint 30 } 31 32 func newRateLimiter(conf Config, height uint64) *rateLimiter { 33 limiter := &rateLimiter{ 34 rate: conf.MaxPayerTransactionRate, 35 unlimited: conf.UnlimitedPayers, 36 height: height, 37 latestCollectionHeight: make(map[flow.Address]uint64), 38 txIncludedCount: make(map[flow.Address]uint), 39 } 40 if limiter.rate >= 1 { 41 limiter.txPerBlock = uint(math.Floor(limiter.rate)) 42 } else { 43 limiter.blocksPerTx = uint64(math.Ceil(1 / limiter.rate)) 44 } 45 return limiter 46 } 47 48 // note the existence and height of a transaction in an ancestor collection. 49 func (limiter *rateLimiter) addAncestor(height uint64, tx *flow.TransactionBody) { 50 51 // skip tracking payers if we aren't rate-limiting or are configured 52 // to allow multiple transactions per payer per collection 53 if limiter.rate >= 1 || limiter.rate <= 0 { 54 return 55 } 56 57 latest := limiter.latestCollectionHeight[tx.Payer] 58 if height >= latest { 59 limiter.latestCollectionHeight[tx.Payer] = height 60 } 61 } 62 63 // note that we have added a transaction to the collection under construction. 64 func (limiter *rateLimiter) transactionIncluded(tx *flow.TransactionBody) { 65 limiter.txIncludedCount[tx.Payer]++ 66 } 67 68 // applies the rate limiting rules, returning whether the transaction should be 69 // omitted from the collection under construction. 70 func (limiter *rateLimiter) shouldRateLimit(tx *flow.TransactionBody) bool { 71 72 payer := tx.Payer 73 74 // skip rate limiting if it is turned off or the payer is unlimited 75 _, isUnlimited := limiter.unlimited[payer] 76 if limiter.rate <= 0 || isUnlimited { 77 return false 78 } 79 80 // if rate >=1, we only consider the current collection and rate limit once 81 // the number of transactions for the payer exceeds rate 82 if limiter.rate >= 1 { 83 if limiter.txIncludedCount[payer] >= limiter.txPerBlock { 84 return true 85 } 86 } 87 88 // if rate < 1, we need to look back to see when a transaction by this payer 89 // was most recently included - we rate limit if the # of collections since 90 // the payer's last transaction is less than ceil(1/rate) 91 if limiter.rate < 1 { 92 93 // rate limit if we've already include a transaction for this payer, we allow 94 // AT MOST one transaction per payer in a given collection 95 if limiter.txIncludedCount[payer] > 0 { 96 return true 97 } 98 99 // otherwise, check whether sufficiently many empty collection 100 // have been built since the last transaction from the payer 101 102 latestHeight, hasLatest := limiter.latestCollectionHeight[payer] 103 // if there is no recent transaction, don't rate limit 104 if !hasLatest { 105 return false 106 } 107 108 if limiter.height-latestHeight < limiter.blocksPerTx { 109 return true 110 } 111 } 112 113 return false 114 }