diff --git a/api/utils.go b/api/utils.go index 1dece5af9..aeadef217 100644 --- a/api/utils.go +++ b/api/utils.go @@ -132,6 +132,8 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect return zero, err case errors.Is(err, core.ErrInsufficientFunds): return zero, err + case errors.Is(err, errs.ErrRateLimit): + return zero, err default: collector.ApiErrorOccurred() log.Error().Err(err).Msg("api error") diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 52aa19dc3..f6160faa2 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -280,4 +280,6 @@ func init() { Cmd.Flags().StringVar(&cfg.ProfilerHost, "profiler-host", "localhost", "Host for the Profiler server") Cmd.Flags().IntVar(&cfg.ProfilerPort, "profiler-port", 6060, "Port for the Profiler server") Cmd.Flags().StringVar(&txStateValidation, "tx-state-validation", "tx-seal", "Sets the transaction validation mechanism. It can validate using the local state index, or wait for the outer Flow transaction to seal. Available values ('local-index' / 'tx-seal'), defaults to 'tx-seal'.") + Cmd.Flags().Uint64Var(&cfg.TxRequestLimit, "tx-request-limit", 0, "Number of transaction submissions to allow per the specified interval.") + Cmd.Flags().DurationVar(&cfg.TxRequestLimitDuration, "tx-request-limit-duration", time.Second*3, "Time interval upon which to enforce transaction submission rate limiting.") } diff --git a/config/config.go b/config/config.go index 570258d20..612f53d4e 100644 --- a/config/config.go +++ b/config/config.go @@ -89,4 +89,9 @@ type Config struct { // TxStateValidation sets the transaction validation mechanism. It can validate // using the local state index, or wait for the outer Flow transaction to seal. TxStateValidation string + // TxRequestLimit is the number of transaction submissions to allow per interval. + TxRequestLimit uint64 + // TxRequestLimitDuration is the time interval upon which to enforce transaction submission + // rate limiting. + TxRequestLimitDuration time.Duration } diff --git a/services/requester/requester.go b/services/requester/requester.go index 866d68e2c..be2dfa1ed 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -21,6 +21,8 @@ import ( "github.com/onflow/go-ethereum/core/txpool" "github.com/onflow/go-ethereum/core/types" "github.com/rs/zerolog" + "github.com/sethvargo/go-limiter" + "github.com/sethvargo/go-limiter/memorystore" "golang.org/x/sync/errgroup" "github.com/onflow/flow-evm-gateway/config" @@ -109,6 +111,7 @@ type EVM struct { evmSigner types.Signer validationOptions *txpool.ValidationOptions collector metrics.Collector + rateLimiter limiter.Store } func NewEVM( @@ -165,6 +168,16 @@ func NewEVM( MinTip: new(big.Int), } + rateLimiter, err := memorystore.New( + &memorystore.Config{ + Tokens: config.TxRequestLimit, + Interval: config.TxRequestLimitDuration, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create TX rate limiter: %w", err) + } + evm := &EVM{ registerStore: registerStore, client: client, @@ -177,6 +190,7 @@ func NewEVM( validationOptions: validationOptions, collector: collector, keystore: keystore, + rateLimiter: rateLimiter, } return evm, nil @@ -197,6 +211,17 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, return common.Hash{}, fmt.Errorf("failed to derive the sender: %w", err) } + if e.config.TxRequestLimit > 0 { + _, _, _, ok, err := e.rateLimiter.Take(ctx, from.Hex()) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to check rate limit: %w", err) + } + if !ok { + e.collector.RequestRateLimited("SendRawTransaction") + return common.Hash{}, errs.ErrRateLimit + } + } + if tx.GasPrice().Cmp(e.config.GasPrice) < 0 { return common.Hash{}, errs.NewTxGasPriceTooLowError(e.config.GasPrice) }