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  }