From 66b46d4115eb4e6baaabf76be8cdf4dbe4e02af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Kunze=20K=C3=BCllmer?= <31522760+fedekunze@users.noreply.github.com> Date: Tue, 21 Jun 2022 17:51:37 +0200 Subject: [PATCH 1/4] imp(rpc): restrict unprotected txs on the node RPC --- rpc/apis.go | 33 +++++++++++++++++++-------------- rpc/backend/backend.go | 29 ++++++++++++++++------------- rpc/backend/evm_backend.go | 15 ++++++++++++++- server/config/config.go | 32 +++++++++++++++++++------------- server/config/toml.go | 4 ++++ server/flags/flags.go | 25 +++++++++++++------------ server/json_rpc.go | 4 +++- server/start.go | 1 + 8 files changed, 89 insertions(+), 54 deletions(-) diff --git a/rpc/apis.go b/rpc/apis.go index 15e056cd7c..433305e1a1 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, allowUnprotectedTxs 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..f009690fcd 100644 --- a/rpc/backend/evm_backend.go +++ b/rpc/backend/evm_backend.go @@ -638,7 +638,14 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e return common.Hash{}, err } - txHash := msg.AsTransaction().Hash() + ethTx := msg.AsTransaction() + txHash := ethTx.Hash() + + // 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") + } // Broadcast transaction in sync mode (default) // NOTE: If error is encountered on the node, the broadcast will not return an error @@ -956,3 +963,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/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") From aa4bf7a2b82e651110eaadd637daaf4ed0353587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Kunze=20K=C3=BCllmer?= <31522760+fedekunze@users.noreply.github.com> Date: Wed, 22 Jun 2022 10:38:01 +0200 Subject: [PATCH 2/4] lint --- rpc/apis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/apis.go b/rpc/apis.go index 433305e1a1..d50a56aada 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -105,7 +105,7 @@ func init() { }, } }, - TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { + TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool) []rpc.API { return []rpc.API{ { Namespace: TxPoolNamespace, From ed76d1e43479a0c5ab16821769521eb6c4ce9c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Kunze=20K=C3=BCllmer?= <31522760+fedekunze@users.noreply.github.com> Date: Wed, 22 Jun 2022 10:42:47 +0200 Subject: [PATCH 3/4] send raw transaction --- rpc/backend/evm_backend.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpc/backend/evm_backend.go b/rpc/backend/evm_backend.go index f009690fcd..d06e35a289 100644 --- a/rpc/backend/evm_backend.go +++ b/rpc/backend/evm_backend.go @@ -639,7 +639,6 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e } ethTx := msg.AsTransaction() - txHash := ethTx.Hash() // check the local node config in case unprotected txs are disabled if !b.UnprotectedAllowed() && !ethTx.Protected() { @@ -647,6 +646,8 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e 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 syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) From f536db3c13143b5418f655bcd3c829eb88b35d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Kunze=20K=C3=BCllmer?= <31522760+fedekunze@users.noreply.github.com> Date: Wed, 22 Jun 2022 12:42:21 +0200 Subject: [PATCH 4/4] c++ --- CHANGELOG.md | 1 + rpc/namespaces/ethereum/eth/api.go | 6 ++++++ 2 files changed, 7 insertions(+) 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/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())