diff --git a/CHANGELOG.md b/CHANGELOG.md index 6968f01133..3706d19d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### API Breaking +* (rpc) [\#1143](https://github.com/evmos/ethermint/pull/1143) Restrict unprotected txs on the node JSON-RPC configuration. * (all) [\#1137](https://github.com/evmos/ethermint/pull/1137) Rename go module to `evmos/ethermint` ### Improvements diff --git a/rpc/apis.go b/rpc/apis.go index 15e056cd7c..d50a56aada 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -44,16 +44,21 @@ const ( ) // APICreator creates the JSON-RPC API implementations. -type APICreator = func(*server.Context, client.Context, *rpcclient.WSClient) []rpc.API +type APICreator = func( + ctx *server.Context, + clientCtx client.Context, + tendermintWebsocketClient *rpcclient.WSClient, + allowUnprotectedTxs bool, +) []rpc.API // apiCreators defines the JSON-RPC API namespaces. var apiCreators map[string]APICreator func init() { apiCreators = map[string]APICreator{ - EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc.API { + EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { nonceLock := new(types.AddrLocker) - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) return []rpc.API{ { Namespace: EthNamespace, @@ -69,7 +74,7 @@ func init() { }, } }, - Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient) []rpc.API { + Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient, bool) []rpc.API { return []rpc.API{ { Namespace: Web3Namespace, @@ -79,7 +84,7 @@ func init() { }, } }, - NetNamespace: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { + NetNamespace: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, _ bool) []rpc.API { return []rpc.API{ { Namespace: NetNamespace, @@ -89,8 +94,8 @@ func init() { }, } }, - PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) return []rpc.API{ { Namespace: PersonalNamespace, @@ -100,7 +105,7 @@ func init() { }, } }, - TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient) []rpc.API { + TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool) []rpc.API { return []rpc.API{ { Namespace: TxPoolNamespace, @@ -110,8 +115,8 @@ func init() { }, } }, - DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) return []rpc.API{ { Namespace: DebugNamespace, @@ -121,8 +126,8 @@ func init() { }, } }, - MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) return []rpc.API{ { Namespace: MinerNamespace, @@ -136,12 +141,12 @@ func init() { } // GetRPCAPIs returns the list of all APIs -func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, selectedAPIs []string) []rpc.API { +func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, selectedAPIs []string) []rpc.API { var apis []rpc.API for _, ns := range selectedAPIs { if creator, ok := apiCreators[ns]; ok { - apis = append(apis, creator(ctx, clientCtx, tmWSClient)...) + apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs)...) } else { ctx.Logger.Error("invalid namespace value", "namespace", ns) } diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 4736568745..b116fc1ed6 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -44,6 +44,7 @@ type EVMBackend interface { RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether. + UnprotectedAllowed() bool RPCMinGasPrice() int64 SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) @@ -86,16 +87,17 @@ var _ BackendI = (*Backend)(nil) // Backend implements the BackendI interface type Backend struct { - ctx context.Context - clientCtx client.Context - queryClient *types.QueryClient // gRPC query client - logger log.Logger - chainID *big.Int - cfg config.Config + ctx context.Context + clientCtx client.Context + queryClient *types.QueryClient // gRPC query client + logger log.Logger + chainID *big.Int + cfg config.Config + allowUnprotectedTxs bool } // NewBackend creates a new Backend instance for cosmos and ethereum namespaces -func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context) *Backend { +func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context, allowUnprotectedTxs bool) *Backend { chainID, err := ethermint.ParseChainID(clientCtx.ChainID) if err != nil { panic(err) @@ -104,11 +106,12 @@ func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context appConf := config.GetConfig(ctx.Viper) return &Backend{ - ctx: context.Background(), - clientCtx: clientCtx, - queryClient: types.NewQueryClient(clientCtx), - logger: logger.With("module", "backend"), - chainID: chainID, - cfg: appConf, + ctx: context.Background(), + clientCtx: clientCtx, + queryClient: types.NewQueryClient(clientCtx), + logger: logger.With("module", "backend"), + chainID: chainID, + cfg: appConf, + allowUnprotectedTxs: allowUnprotectedTxs, } } diff --git a/rpc/backend/evm_backend.go b/rpc/backend/evm_backend.go index 925774b3a6..d06e35a289 100644 --- a/rpc/backend/evm_backend.go +++ b/rpc/backend/evm_backend.go @@ -638,7 +638,15 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e return common.Hash{}, err } - txHash := msg.AsTransaction().Hash() + ethTx := msg.AsTransaction() + + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + txHash := ethTx.Hash() // Broadcast transaction in sync mode (default) // NOTE: If error is encountered on the node, the broadcast will not return an error @@ -956,3 +964,9 @@ func (b *Backend) GetEthereumMsgsFromTendermintBlock(resBlock *tmrpctypes.Result return result } + +// UnprotectedAllowed returns the node configuration value for allowing +// unprotected transactions (i.e not replay-protected) +func (b Backend) UnprotectedAllowed() bool { + return b.allowUnprotectedTxs +} diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 99181b2168..ba001494c0 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -525,6 +525,12 @@ func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) return common.Hash{}, err } + // check the local node config in case unprotected txs are disabled + if !e.backend.UnprotectedAllowed() && !tx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + ethereumTx := &evmtypes.MsgEthereumTx{} if err := ethereumTx.FromEthereumTx(tx); err != nil { e.logger.Error("transaction converting failed", "error", err.Error()) diff --git a/server/config/config.go b/server/config/config.go index ed0f463bd0..3b0a7b2f28 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -47,6 +47,8 @@ const ( DefaultHTTPTimeout = 30 * time.Second DefaultHTTPIdleTimeout = 120 * time.Second + // DefaultAllowUnprotectedTxs value is false + DefaultAllowUnprotectedTxs = false ) var evmTracers = []string{"json", "markdown", "struct", "access_list"} @@ -98,6 +100,9 @@ type JSONRPCConfig struct { HTTPTimeout time.Duration `mapstructure:"http-timeout"` // HTTPIdleTimeout is the idle timeout of http json-rpc server. HTTPIdleTimeout time.Duration `mapstructure:"http-idle-timeout"` + // AllowUnprotectedTxs restricts unprotected (non EIP155 signed) transactions to be submitted via + // the node's RPC when global parameter is disabled. + AllowUnprotectedTxs bool `mapstructure:"allow-unprotected-txs"` } // TLSConfig defines the certificate and matching private key for the server. @@ -183,19 +188,20 @@ func GetAPINamespaces() []string { // DefaultJSONRPCConfig returns an EVM config with the JSON-RPC API enabled by default func DefaultJSONRPCConfig() *JSONRPCConfig { return &JSONRPCConfig{ - Enable: true, - API: GetDefaultAPINamespaces(), - Address: DefaultJSONRPCAddress, - WsAddress: DefaultJSONRPCWsAddress, - GasCap: DefaultGasCap, - EVMTimeout: DefaultEVMTimeout, - TxFeeCap: DefaultTxFeeCap, - FilterCap: DefaultFilterCap, - FeeHistoryCap: DefaultFeeHistoryCap, - BlockRangeCap: DefaultBlockRangeCap, - LogsCap: DefaultLogsCap, - HTTPTimeout: DefaultHTTPTimeout, - HTTPIdleTimeout: DefaultHTTPIdleTimeout, + Enable: true, + API: GetDefaultAPINamespaces(), + Address: DefaultJSONRPCAddress, + WsAddress: DefaultJSONRPCWsAddress, + GasCap: DefaultGasCap, + EVMTimeout: DefaultEVMTimeout, + TxFeeCap: DefaultTxFeeCap, + FilterCap: DefaultFilterCap, + FeeHistoryCap: DefaultFeeHistoryCap, + BlockRangeCap: DefaultBlockRangeCap, + LogsCap: DefaultLogsCap, + HTTPTimeout: DefaultHTTPTimeout, + HTTPIdleTimeout: DefaultHTTPIdleTimeout, + AllowUnprotectedTxs: DefaultAllowUnprotectedTxs, } } diff --git a/server/config/toml.go b/server/config/toml.go index cd4258f06a..93d3285add 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -62,6 +62,10 @@ http-timeout = "{{ .JSONRPC.HTTPTimeout }}" # HTTPIdleTimeout is the idle timeout of http json-rpc server. http-idle-timeout = "{{ .JSONRPC.HTTPIdleTimeout }}" +# AllowUnprotectedTxs restricts unprotected (non EIP155 signed) transactions to be submitted via +# the node's RPC when the global parameter is disabled. +allow-unprotected-txs = {{ .JSONRPC.AllowUnprotectedTxs }} + ############################################################################### ### TLS Configuration ### ############################################################################### diff --git a/server/flags/flags.go b/server/flags/flags.go index 53b7ced3a8..dc98e86ada 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -32,18 +32,19 @@ 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" - JSONRPCEVMTimeout = "json-rpc.evm-timeout" - JSONRPCTxFeeCap = "json-rpc.txfee-cap" - JSONRPCFilterCap = "json-rpc.filter-cap" - JSONRPCLogsCap = "json-rpc.logs-cap" - JSONRPCBlockRangeCap = "json-rpc.block-range-cap" - JSONRPCHTTPTimeout = "json-rpc.http-timeout" - JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout" + JSONRPCEnable = "json-rpc.enable" + JSONRPCAPI = "json-rpc.api" + JSONRPCAddress = "json-rpc.address" + JSONWsAddress = "json-rpc.ws-address" + JSONRPCGasCap = "json-rpc.gas-cap" + JSONRPCEVMTimeout = "json-rpc.evm-timeout" + JSONRPCTxFeeCap = "json-rpc.txfee-cap" + JSONRPCFilterCap = "json-rpc.filter-cap" + JSONRPCLogsCap = "json-rpc.logs-cap" + JSONRPCBlockRangeCap = "json-rpc.block-range-cap" + JSONRPCHTTPTimeout = "json-rpc.http-timeout" + JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout" + JSONRPCAllowUnprotectedTxs = "json-rpc.allow-unprotected-txs" ) // EVM flags diff --git a/server/json_rpc.go b/server/json_rpc.go index 49779c9cdf..feefa12cb2 100644 --- a/server/json_rpc.go +++ b/server/json_rpc.go @@ -36,8 +36,10 @@ func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEn rpcServer := ethrpc.NewServer() + allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs rpcAPIArr := config.JSONRPC.API - apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, rpcAPIArr) + + apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, rpcAPIArr) for _, api := range apis { if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { diff --git a/server/start.go b/server/start.go index 75282274a2..d13586c258 100644 --- a/server/start.go +++ b/server/start.go @@ -162,6 +162,7 @@ which accepts a path for the resulting pprof file. cmd.Flags().Duration(srvflags.JSONRPCEVMTimeout, config.DefaultEVMTimeout, "Sets a timeout used for eth_call (0=infinite)") cmd.Flags().Duration(srvflags.JSONRPCHTTPTimeout, config.DefaultHTTPTimeout, "Sets a read/write timeout for json-rpc http server (0=infinite)") cmd.Flags().Duration(srvflags.JSONRPCHTTPIdleTimeout, config.DefaultHTTPIdleTimeout, "Sets a idle timeout for json-rpc http server (0=infinite)") + cmd.Flags().Bool(srvflags.JSONRPCAllowUnprotectedTxs, config.DefaultAllowUnprotectedTxs, "Allow for unprotected (non EIP155 signed) transactions to be submitted via the node's RPC when the global parameter is disabled") cmd.Flags().Int32(srvflags.JSONRPCLogsCap, config.DefaultLogsCap, "Sets the max number of results can be returned from single `eth_getLogs` query") cmd.Flags().Int32(srvflags.JSONRPCBlockRangeCap, config.DefaultBlockRangeCap, "Sets the max block range allowed for `eth_getLogs` query")