diff --git a/CHANGELOG.md b/CHANGELOG.md index 48120db3..8267196b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (deps) [tharsis#655](https://github.com/tharsis/ethermint/pull/665) Bump Cosmos SDK version to [`v0.44.2`](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.44.2). * (evm) [tharsis#650](https://github.com/tharsis/ethermint/pull/650) Fix panic when flattening the cache context in case transaction is reverted. * (rpc, test) [tharsis#608](https://github.com/tharsis/ethermint/pull/608) Fix rpc test. -* (evm) [tharsis#660](https://github.com/tharsis/ethermint/pull/660) Fix nil pointer panic in ApplyNativeMessage. +* (rpc) [tharsis#661](https://github.com/tharsis/ethermint/pull/661) Fix OOM bug when creating too many filters using JSON-RPC. +* (evm) [tharsis#660](https://github.com/tharsis/ethermint/pull/660) Fix `nil` pointer panic in `ApplyNativeMessage`. ## [v0.7.0] - 2021-10-07 diff --git a/rpc/ethereum/backend/backend.go b/rpc/ethereum/backend/backend.go index d2b91815..33b72ade 100644 --- a/rpc/ethereum/backend/backend.go +++ b/rpc/ethereum/backend/backend.go @@ -765,6 +765,11 @@ func (e *EVMBackend) RPCGasCap() uint64 { return e.cfg.JSONRPC.GasCap } +// RPCFilterCap is the limit for total number of filters that can be created +func (e *EVMBackend) RPCFilterCap() int32 { + return e.cfg.JSONRPC.FilterCap +} + // RPCMinGasPrice returns the minimum gas price for a transaction obtained from // the node config. If set value is 0, it will default to 20. diff --git a/rpc/ethereum/namespaces/eth/filters/api.go b/rpc/ethereum/namespaces/eth/filters/api.go index 8e1ff2f4..2922b603 100644 --- a/rpc/ethereum/namespaces/eth/filters/api.go +++ b/rpc/ethereum/namespaces/eth/filters/api.go @@ -36,6 +36,8 @@ type Backend interface { BloomStatus() (uint64, uint64) GetFilteredBlocks(from int64, to int64, bloomIndexes [][]BloomIV, filterAddresses bool) ([]int64, error) + + RPCFilterCap() int32 } // consider a filter inactive if it has not been polled for within deadline @@ -107,15 +109,20 @@ func (api *PublicFilterAPI) timeoutLoop() { // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newPendingTransactionFilter func (api *PublicFilterAPI) NewPendingTransactionFilter() rpc.ID { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return rpc.ID("error creating pending tx filter: max limit reached") + } + pendingTxSub, cancelSubs, err := api.events.SubscribePendingTxs() if err != nil { // wrap error on the ID return rpc.ID(fmt.Sprintf("error creating pending tx filter: %s", err.Error())) } - api.filtersMu.Lock() api.filters[pendingTxSub.ID()] = &filter{typ: filters.PendingTransactionsSubscription, deadline: time.NewTimer(deadline), hashes: make([]common.Hash, 0), s: pendingTxSub} - api.filtersMu.Unlock() go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) { defer cancelSubs() @@ -219,6 +226,13 @@ func (api *PublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Su // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter func (api *PublicFilterAPI) NewBlockFilter() rpc.ID { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return rpc.ID("error creating block filter: max limit reached") + } + headerSub, cancelSubs, err := api.events.SubscribeNewHeads() if err != nil { // wrap error on the ID @@ -228,9 +242,7 @@ func (api *PublicFilterAPI) NewBlockFilter() rpc.ID { // TODO: use events to get the base fee amount baseFee := big.NewInt(params.InitialBaseFee) - api.filtersMu.Lock() api.filters[headerSub.ID()] = &filter{typ: filters.BlocksSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: headerSub} - api.filtersMu.Unlock() go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) { defer cancelSubs() @@ -404,6 +416,13 @@ func (api *PublicFilterAPI) Logs(ctx context.Context, crit filters.FilterCriteri // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return rpc.ID(""), fmt.Errorf("error creating filter: max limit reached") + } + var ( filterID = rpc.ID("") err error @@ -416,9 +435,7 @@ func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, filterID = logsSub.ID() - api.filtersMu.Lock() api.filters[filterID] = &filter{typ: filters.LogsSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: logsSub} - api.filtersMu.Unlock() go func(eventCh <-chan coretypes.ResultEvent) { defer cancelSubs() diff --git a/server/config/config.go b/server/config/config.go index 110b2452..b1814b1f 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -28,6 +28,8 @@ const ( DefaultEVMTracer = "json" DefaultGasCap uint64 = 25000000 + + DefaultFilterCap int32 = 200 ) var evmTracers = []string{DefaultEVMTracer, "markdown", "struct", "access_list"} @@ -51,16 +53,18 @@ type EVMConfig struct { // JSONRPCConfig defines configuration for the EVM RPC server. type JSONRPCConfig struct { + // API defines a list of JSON-RPC namespaces that should be enabled + API []string `mapstructure:"api"` // Address defines the HTTP server to listen on Address string `mapstructure:"address"` // WsAddress defines the WebSocket server to listen on WsAddress string `mapstructure:"ws-address"` - // API defines a list of JSON-RPC namespaces that should be enabled - API []string `mapstructure:"api"` - // Enable defines if the EVM RPC server should be enabled. - Enable bool `mapstructure:"enable"` // GasCap is the global gas cap for eth-call variants. GasCap uint64 `mapstructure:"gas-cap"` + // FilterCap is the global cap for total number of filters that can be created. + FilterCap int32 `mapstructure:"filter-cap"` + // Enable defines if the EVM RPC server should be enabled. + Enable bool `mapstructure:"enable"` } // TLSConfig defines the certificate and matching private key for the server. @@ -145,6 +149,7 @@ func DefaultJSONRPCConfig() *JSONRPCConfig { Address: DefaultJSONRPCAddress, WsAddress: DefaultJSONRPCWsAddress, GasCap: DefaultGasCap, + FilterCap: DefaultFilterCap, } } @@ -154,6 +159,10 @@ func (c JSONRPCConfig) Validate() error { return errors.New("cannot enable JSON-RPC without defining any API namespace") } + if c.FilterCap < 0 { + return errors.New("JSON-RPC filter-cap cannot be negative") + } + // TODO: validate APIs seenAPIs := make(map[string]bool) for _, api := range c.API { @@ -207,6 +216,7 @@ func GetConfig(v *viper.Viper) Config { Address: v.GetString("json-rpc.address"), WsAddress: v.GetString("json-rpc.ws-address"), GasCap: v.GetUint64("json-rpc.gas-cap"), + FilterCap: v.GetInt32("json-rpc.filter-cap"), }, TLS: TLSConfig{ CertificatePath: v.GetString("tls.certificate-path"), diff --git a/server/config/toml.go b/server/config/toml.go index 6793e1da..73bf049c 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -35,6 +35,9 @@ api = "{{range $index, $elmt := .JSONRPC.API}}{{if $index}},{{$elmt}}{{else}}{{$ # GasCap sets a cap on gas that can be used in eth_call/estimateGas (0=infinite). Default: 25,000,000. gas-cap = {{ .JSONRPC.GasCap }} +# FilterCap sets the global cap for total number of filters that can be created +filter-cap = {{ .JSONRPC.FilterCap }} + ############################################################################### ### TLS Configuration ### ############################################################################### diff --git a/server/flags/flags.go b/server/flags/flags.go index fb8eaad0..1383e7c8 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -26,11 +26,12 @@ const ( // JSON-RPC flags const ( - JSONRPCEnable = "json-rpc.enable" - JSONRPCAPI = "json-rpc.api" - JSONRPCAddress = "json-rpc.address" - JSONWsAddress = "json-rpc.ws-address" - JSONRPCGasCap = "json-rpc.gas-cap" + JSONRPCEnable = "json-rpc.enable" + JSONRPCAPI = "json-rpc.api" + JSONRPCAddress = "json-rpc.address" + JSONWsAddress = "json-rpc.ws-address" + JSONRPCGasCap = "json-rpc.gas-cap" + JSONRPCFilterCap = "json-rpc.filter-cap" ) // EVM flags diff --git a/server/start.go b/server/start.go index 7a90c763..d71ee32f 100644 --- a/server/start.go +++ b/server/start.go @@ -156,6 +156,7 @@ which accepts a path for the resulting pprof file. cmd.Flags().String(srvflags.JSONRPCAddress, config.DefaultJSONRPCAddress, "the JSON-RPC server address to listen on") cmd.Flags().String(srvflags.JSONWsAddress, config.DefaultJSONRPCWsAddress, "the JSON-RPC WS server address to listen on") cmd.Flags().Uint64(srvflags.JSONRPCGasCap, config.DefaultGasCap, "Sets a cap on gas that can be used in eth_call/estimateGas (0=infinite)") + cmd.Flags().Int32(srvflags.JSONRPCFilterCap, config.DefaultFilterCap, "Sets the global cap for total number of filters that can be created") cmd.Flags().String(srvflags.EVMTracer, config.DefaultEVMTracer, "the EVM tracer type to collect execution traces from the EVM transaction execution (json|struct|access_list|markdown)")