imp(rpc): restrict unprotected txs on the node RPC (#1143)

* imp(rpc): restrict unprotected txs on the node RPC

* lint

* send raw transaction

* c++
This commit is contained in:
Federico Kunze Küllmer 2022-06-22 12:50:36 +02:00 committed by GitHub
parent 8817e8052a
commit 3b852f723e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 97 additions and 54 deletions

View File

@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
### API Breaking ### 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` * (all) [\#1137](https://github.com/evmos/ethermint/pull/1137) Rename go module to `evmos/ethermint`
### Improvements ### Improvements

View File

@ -44,16 +44,21 @@ const (
) )
// APICreator creates the JSON-RPC API implementations. // 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. // apiCreators defines the JSON-RPC API namespaces.
var apiCreators map[string]APICreator var apiCreators map[string]APICreator
func init() { func init() {
apiCreators = map[string]APICreator{ 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) nonceLock := new(types.AddrLocker)
evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs)
return []rpc.API{ return []rpc.API{
{ {
Namespace: EthNamespace, 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{ return []rpc.API{
{ {
Namespace: Web3Namespace, 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{ return []rpc.API{
{ {
Namespace: NetNamespace, Namespace: NetNamespace,
@ -89,8 +94,8 @@ func init() {
}, },
} }
}, },
PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API {
evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs)
return []rpc.API{ return []rpc.API{
{ {
Namespace: PersonalNamespace, 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{ return []rpc.API{
{ {
Namespace: TxPoolNamespace, Namespace: TxPoolNamespace,
@ -110,8 +115,8 @@ func init() {
}, },
} }
}, },
DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API {
evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs)
return []rpc.API{ return []rpc.API{
{ {
Namespace: DebugNamespace, Namespace: DebugNamespace,
@ -121,8 +126,8 @@ func init() {
}, },
} }
}, },
MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API {
evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs)
return []rpc.API{ return []rpc.API{
{ {
Namespace: MinerNamespace, Namespace: MinerNamespace,
@ -136,12 +141,12 @@ func init() {
} }
// GetRPCAPIs returns the list of all APIs // 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 var apis []rpc.API
for _, ns := range selectedAPIs { for _, ns := range selectedAPIs {
if creator, ok := apiCreators[ns]; ok { if creator, ok := apiCreators[ns]; ok {
apis = append(apis, creator(ctx, clientCtx, tmWSClient)...) apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs)...)
} else { } else {
ctx.Logger.Error("invalid namespace value", "namespace", ns) ctx.Logger.Error("invalid namespace value", "namespace", ns)
} }

View File

@ -44,6 +44,7 @@ type EVMBackend interface {
RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection
RPCEVMTimeout() time.Duration // global timeout 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. RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether.
UnprotectedAllowed() bool
RPCMinGasPrice() int64 RPCMinGasPrice() int64
SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error)
@ -86,16 +87,17 @@ var _ BackendI = (*Backend)(nil)
// Backend implements the BackendI interface // Backend implements the BackendI interface
type Backend struct { type Backend struct {
ctx context.Context ctx context.Context
clientCtx client.Context clientCtx client.Context
queryClient *types.QueryClient // gRPC query client queryClient *types.QueryClient // gRPC query client
logger log.Logger logger log.Logger
chainID *big.Int chainID *big.Int
cfg config.Config cfg config.Config
allowUnprotectedTxs bool
} }
// NewBackend creates a new Backend instance for cosmos and ethereum namespaces // 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) chainID, err := ethermint.ParseChainID(clientCtx.ChainID)
if err != nil { if err != nil {
panic(err) panic(err)
@ -104,11 +106,12 @@ func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context
appConf := config.GetConfig(ctx.Viper) appConf := config.GetConfig(ctx.Viper)
return &Backend{ return &Backend{
ctx: context.Background(), ctx: context.Background(),
clientCtx: clientCtx, clientCtx: clientCtx,
queryClient: types.NewQueryClient(clientCtx), queryClient: types.NewQueryClient(clientCtx),
logger: logger.With("module", "backend"), logger: logger.With("module", "backend"),
chainID: chainID, chainID: chainID,
cfg: appConf, cfg: appConf,
allowUnprotectedTxs: allowUnprotectedTxs,
} }
} }

View File

@ -638,7 +638,15 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e
return common.Hash{}, err 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) // Broadcast transaction in sync mode (default)
// NOTE: If error is encountered on the node, the broadcast will not return an error // 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 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
}

View File

@ -525,6 +525,12 @@ func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error)
return common.Hash{}, err 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{} ethereumTx := &evmtypes.MsgEthereumTx{}
if err := ethereumTx.FromEthereumTx(tx); err != nil { if err := ethereumTx.FromEthereumTx(tx); err != nil {
e.logger.Error("transaction converting failed", "error", err.Error()) e.logger.Error("transaction converting failed", "error", err.Error())

View File

@ -47,6 +47,8 @@ const (
DefaultHTTPTimeout = 30 * time.Second DefaultHTTPTimeout = 30 * time.Second
DefaultHTTPIdleTimeout = 120 * time.Second DefaultHTTPIdleTimeout = 120 * time.Second
// DefaultAllowUnprotectedTxs value is false
DefaultAllowUnprotectedTxs = false
) )
var evmTracers = []string{"json", "markdown", "struct", "access_list"} var evmTracers = []string{"json", "markdown", "struct", "access_list"}
@ -98,6 +100,9 @@ type JSONRPCConfig struct {
HTTPTimeout time.Duration `mapstructure:"http-timeout"` HTTPTimeout time.Duration `mapstructure:"http-timeout"`
// HTTPIdleTimeout is the idle timeout of http json-rpc server. // HTTPIdleTimeout is the idle timeout of http json-rpc server.
HTTPIdleTimeout time.Duration `mapstructure:"http-idle-timeout"` 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. // 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 // DefaultJSONRPCConfig returns an EVM config with the JSON-RPC API enabled by default
func DefaultJSONRPCConfig() *JSONRPCConfig { func DefaultJSONRPCConfig() *JSONRPCConfig {
return &JSONRPCConfig{ return &JSONRPCConfig{
Enable: true, Enable: true,
API: GetDefaultAPINamespaces(), API: GetDefaultAPINamespaces(),
Address: DefaultJSONRPCAddress, Address: DefaultJSONRPCAddress,
WsAddress: DefaultJSONRPCWsAddress, WsAddress: DefaultJSONRPCWsAddress,
GasCap: DefaultGasCap, GasCap: DefaultGasCap,
EVMTimeout: DefaultEVMTimeout, EVMTimeout: DefaultEVMTimeout,
TxFeeCap: DefaultTxFeeCap, TxFeeCap: DefaultTxFeeCap,
FilterCap: DefaultFilterCap, FilterCap: DefaultFilterCap,
FeeHistoryCap: DefaultFeeHistoryCap, FeeHistoryCap: DefaultFeeHistoryCap,
BlockRangeCap: DefaultBlockRangeCap, BlockRangeCap: DefaultBlockRangeCap,
LogsCap: DefaultLogsCap, LogsCap: DefaultLogsCap,
HTTPTimeout: DefaultHTTPTimeout, HTTPTimeout: DefaultHTTPTimeout,
HTTPIdleTimeout: DefaultHTTPIdleTimeout, HTTPIdleTimeout: DefaultHTTPIdleTimeout,
AllowUnprotectedTxs: DefaultAllowUnprotectedTxs,
} }
} }

View File

@ -62,6 +62,10 @@ http-timeout = "{{ .JSONRPC.HTTPTimeout }}"
# HTTPIdleTimeout is the idle timeout of http json-rpc server. # HTTPIdleTimeout is the idle timeout of http json-rpc server.
http-idle-timeout = "{{ .JSONRPC.HTTPIdleTimeout }}" 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 ### ### TLS Configuration ###
############################################################################### ###############################################################################

View File

@ -32,18 +32,19 @@ const (
// JSON-RPC flags // JSON-RPC flags
const ( const (
JSONRPCEnable = "json-rpc.enable" JSONRPCEnable = "json-rpc.enable"
JSONRPCAPI = "json-rpc.api" JSONRPCAPI = "json-rpc.api"
JSONRPCAddress = "json-rpc.address" JSONRPCAddress = "json-rpc.address"
JSONWsAddress = "json-rpc.ws-address" JSONWsAddress = "json-rpc.ws-address"
JSONRPCGasCap = "json-rpc.gas-cap" JSONRPCGasCap = "json-rpc.gas-cap"
JSONRPCEVMTimeout = "json-rpc.evm-timeout" JSONRPCEVMTimeout = "json-rpc.evm-timeout"
JSONRPCTxFeeCap = "json-rpc.txfee-cap" JSONRPCTxFeeCap = "json-rpc.txfee-cap"
JSONRPCFilterCap = "json-rpc.filter-cap" JSONRPCFilterCap = "json-rpc.filter-cap"
JSONRPCLogsCap = "json-rpc.logs-cap" JSONRPCLogsCap = "json-rpc.logs-cap"
JSONRPCBlockRangeCap = "json-rpc.block-range-cap" JSONRPCBlockRangeCap = "json-rpc.block-range-cap"
JSONRPCHTTPTimeout = "json-rpc.http-timeout" JSONRPCHTTPTimeout = "json-rpc.http-timeout"
JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout" JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout"
JSONRPCAllowUnprotectedTxs = "json-rpc.allow-unprotected-txs"
) )
// EVM flags // EVM flags

View File

@ -36,8 +36,10 @@ func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEn
rpcServer := ethrpc.NewServer() rpcServer := ethrpc.NewServer()
allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs
rpcAPIArr := config.JSONRPC.API rpcAPIArr := config.JSONRPC.API
apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, rpcAPIArr)
apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, rpcAPIArr)
for _, api := range apis { for _, api := range apis {
if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil {

View File

@ -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.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.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().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.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") cmd.Flags().Int32(srvflags.JSONRPCBlockRangeCap, config.DefaultBlockRangeCap, "Sets the max block range allowed for `eth_getLogs` query")