diff --git a/CHANGELOG.md b/CHANGELOG.md index 6968f011..3706d19d 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 15e056cd..d50a56aa 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 47365687..b116fc1e 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 925774b3..d06e35a2 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 99181b21..ba001494 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 ed0f463b..3b0a7b2f 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 cd4258f0..93d3285a 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 53b7ced3..dc98e86a 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 49779c9c..feefa12c 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 75282274..d13586c2 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")