From 86889fb3dad54a8935e4729b881b777fb2f12c0e Mon Sep 17 00:00:00 2001 From: MalteHerrmann <42640438+MalteHerrmann@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:53:43 +0200 Subject: [PATCH] imp(rpc): Add server implementation (#22) --- .clconfig.json | 3 +- .golangci.yml | 1 - CHANGELOG.md | 1 + ethereum/eip712/eip712_test.go | 8 - rpc/backend/backend.go | 2 +- rpc/backend/node_info.go | 2 +- rpc/backend/node_info_test.go | 2 +- rpc/websockets.go | 2 +- server/config/config.go | 434 ++++++++++++++++++++++ server/config/config_test.go | 70 ++++ server/config/toml.go | 100 +++++ server/flags/flags.go | 100 +++++ server/indexer_cmd.go | 120 ++++++ server/indexer_service.go | 111 ++++++ server/json_rpc.go | 114 ++++++ server/start.go | 659 +++++++++++++++++++++++++++++++++ server/util.go | 129 +++++++ testutil/tx/eth.go | 2 +- 18 files changed, 1845 insertions(+), 15 deletions(-) create mode 100644 server/config/config.go create mode 100644 server/config/config_test.go create mode 100644 server/config/toml.go create mode 100644 server/flags/flags.go create mode 100644 server/indexer_cmd.go create mode 100644 server/indexer_service.go create mode 100644 server/json_rpc.go create mode 100644 server/start.go create mode 100644 server/util.go diff --git a/.clconfig.json b/.clconfig.json index 6b3dbb9..ed09764 100644 --- a/.clconfig.json +++ b/.clconfig.json @@ -18,6 +18,7 @@ "precompiles", "proto", "rpc", + "server", "staking-precompile", "tests", "types" @@ -48,4 +49,4 @@ }, "legacy_version": null, "target_repo": "https://github.com/evmos/os" -} \ No newline at end of file +} diff --git a/.golangci.yml b/.golangci.yml index 1d9b0d3..2b7c505 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,7 +21,6 @@ linters: - gosimple - govet - ineffassign - # - lll TODO: enable - misspell - nakedret - prealloc diff --git a/CHANGELOG.md b/CHANGELOG.md index 6153d2d..250953d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog was created using the `clu` binary ### Improvements +- (server) [#22](https://github.com/evmos/os/pull/22) Add server implementation. - (rpc) [#21](https://github.com/evmos/os/pull/21) Add RPC and indexer types. - (types) [#20](https://github.com/evmos/os/pull/20) Add crypto and encoding packages. - (types) [#19](https://github.com/evmos/os/pull/19) Add required wallet types for integration. diff --git a/ethereum/eip712/eip712_test.go b/ethereum/eip712/eip712_test.go index cbedc7c..18c22d1 100644 --- a/ethereum/eip712/eip712_test.go +++ b/ethereum/eip712/eip712_test.go @@ -5,9 +5,6 @@ import ( "fmt" "testing" - // TODO: this import is required for the tests to pass, remove after porting the crypto package - evmoseip712 "github.com/evmos/evmos/v19/ethereum/eip712" - "cosmossdk.io/math" chainparams "cosmossdk.io/simapp/params" "github.com/cosmos/cosmos-sdk/client" @@ -74,11 +71,6 @@ func (suite *EIP712TestSuite) SetupTest() { sdk.GetConfig().SetBech32PrefixForAccount(config.Bech32Prefix, "") eip712.SetEncodingConfig(suite.config) - - // TODO: this is required for the tests to pass because the encoding is called from the original - // Evmos repo during one function from the crypto/... package. - // TODO: remove once it's ported to the new repo - evmoseip712.SetEncodingConfig(suite.config) } // createTestAddress creates random test addresses for messages diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 0d4fd91..93a42a1 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -20,9 +20,9 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/evmos/evmos/v19/server/config" evmtypes "github.com/evmos/evmos/v19/x/evm/types" rpctypes "github.com/evmos/os/rpc/types" + "github.com/evmos/os/server/config" evmostypes "github.com/evmos/os/types" ) diff --git a/rpc/backend/node_info.go b/rpc/backend/node_info.go index 33df4b3..8fa06ca 100644 --- a/rpc/backend/node_info.go +++ b/rpc/backend/node_info.go @@ -22,10 +22,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" - "github.com/evmos/evmos/v19/server/config" evmtypes "github.com/evmos/evmos/v19/x/evm/types" "github.com/evmos/os/crypto/ethsecp256k1" rpctypes "github.com/evmos/os/rpc/types" + "github.com/evmos/os/server/config" "github.com/evmos/os/testutil" ) diff --git a/rpc/backend/node_info_test.go b/rpc/backend/node_info_test.go index 5d60815..4f0829f 100644 --- a/rpc/backend/node_info_test.go +++ b/rpc/backend/node_info_test.go @@ -280,7 +280,7 @@ func (suite *BackendTestSuite) TestSetEtherbase() { // queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) // RegisterStatus(client) // RegisterValidatorAccount(queryClient, suite.acc) - // c := sdk.NewDecCoin(types.AttoEvmos, math.NewIntFromBigInt(big.NewInt(1))) + // c := sdk.NewDecCoin(testutil.ExampleAttoDenom, math.NewIntFromBigInt(big.NewInt(1))) // suite.backend.cfg.SetMinGasPrices(sdk.DecCoins{c}) // delAddr, _ := suite.backend.GetCoinbase() // account, _ := suite.backend.clientCtx.AccountRetriever.GetAccount(suite.backend.clientCtx, delAddr) diff --git a/rpc/websockets.go b/rpc/websockets.go index ebeaeb7..52c9997 100644 --- a/rpc/websockets.go +++ b/rpc/websockets.go @@ -29,11 +29,11 @@ import ( rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" tmtypes "github.com/cometbft/cometbft/types" - "github.com/evmos/evmos/v19/server/config" evmtypes "github.com/evmos/evmos/v19/x/evm/types" "github.com/evmos/os/rpc/ethereum/pubsub" rpcfilters "github.com/evmos/os/rpc/namespaces/ethereum/eth/filters" "github.com/evmos/os/rpc/types" + "github.com/evmos/os/server/config" ) type WebsocketsServer interface { diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..d4671f5 --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,434 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package config + +import ( + "errors" + "fmt" + "path" + "time" + + "github.com/spf13/viper" + + "github.com/cometbft/cometbft/libs/strings" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/server/config" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/crypto-org-chain/cronos/memiavl" + memiavlcfg "github.com/crypto-org-chain/cronos/store/config" +) + +const ( + // DefaultAPIEnable is the default value for the parameter that defines if the cosmos REST API server is enabled + DefaultAPIEnable = false + + // DefaultGRPCEnable is the default value for the parameter that defines if the gRPC server is enabled + DefaultGRPCEnable = false + + // DefaultGRPCWebEnable is the default value for the parameter that defines if the gRPC web server is enabled + DefaultGRPCWebEnable = false + + // DefaultJSONRPCEnable is the default value for the parameter that defines if the JSON-RPC server is enabled + DefaultJSONRPCEnable = false + + // DefaultRosettaEnable is the default value for the parameter that defines if the Rosetta API server is enabled + DefaultRosettaEnable = false + + // DefaultTelemetryEnable is the default value for the parameter that defines if the telemetry is enabled + DefaultTelemetryEnable = false + + // DefaultGRPCAddress is the default address the gRPC server binds to. + DefaultGRPCAddress = "0.0.0.0:9900" + + // DefaultJSONRPCAddress is the default address the JSON-RPC server binds to. + DefaultJSONRPCAddress = "127.0.0.1:8545" + + // DefaultJSONRPCWsAddress is the default address the JSON-RPC WebSocket server binds to. + DefaultJSONRPCWsAddress = "127.0.0.1:8546" + + // DefaultJsonRPCMetricsAddress is the default address the JSON-RPC Metrics server binds to. + DefaultJSONRPCMetricsAddress = "127.0.0.1:6065" + + // DefaultEVMTracer is the default vm.Tracer type + DefaultEVMTracer = "" + + // DefaultFixRevertGasRefundHeight is the default height at which to overwrite gas refund + DefaultFixRevertGasRefundHeight = 0 + + // DefaultMaxTxGasWanted is the default gas wanted for each eth tx returned in ante handler in check tx mode + DefaultMaxTxGasWanted = 0 + + // DefaultGasCap is the default cap on gas that can be used in eth_call/estimateGas + DefaultGasCap uint64 = 25000000 + + // DefaultJSONRPCAllowInsecureUnlock is true + DefaultJSONRPCAllowInsecureUnlock bool = true + + // DefaultFilterCap is the default cap for total number of filters that can be created + DefaultFilterCap int32 = 200 + + // DefaultFeeHistoryCap is the default cap for total number of blocks that can be fetched + DefaultFeeHistoryCap int32 = 100 + + // DefaultLogsCap is the default cap of results returned from single 'eth_getLogs' query + DefaultLogsCap int32 = 10000 + + // DefaultBlockRangeCap is the default cap of block range allowed for 'eth_getLogs' query + DefaultBlockRangeCap int32 = 10000 + + // DefaultEVMTimeout is the default timeout for eth_call + DefaultEVMTimeout = 5 * time.Second + + // DefaultTxFeeCap is the default tx-fee cap for sending a transaction + DefaultTxFeeCap float64 = 1.0 + + // DefaultHTTPTimeout is the default read/write timeout of the http json-rpc server + DefaultHTTPTimeout = 30 * time.Second + + // DefaultHTTPIdleTimeout is the default idle timeout of the http json-rpc server + DefaultHTTPIdleTimeout = 120 * time.Second + + // DefaultAllowUnprotectedTxs value is false + DefaultAllowUnprotectedTxs = false + + // DefaultMaxOpenConnections represents the amount of open connections (unlimited = 0) + DefaultMaxOpenConnections = 0 + + // DefaultGasAdjustment value to use as default in gas-adjustment flag + DefaultGasAdjustment = 1.2 + + // ============================ + // MemIAVL + // ============================ + + // DefaultMemIAVLEnable is the default value that defines if memIAVL is enabled + DefaultMemIAVLEnable = false + + // DefaultZeroCopy is the default value that defines if + // the zero-copied slices must be retained beyond current block's execution + // the sdk address cache will be disabled if zero-copy is enabled + DefaultZeroCopy = false + + // DefaultAsyncCommitBuffer value to use as default for the size of + // asynchronous commit queue when using memIAVL + DefaultAsyncCommitBuffer = 0 + + // DefaultSnapshotKeepRecent default value for how many old snapshots + // (excluding the latest one) should be kept after new snapshots + // when using memIAVL + DefaultSnapshotKeepRecent = 1 +) + +var evmTracers = []string{"json", "markdown", "struct", "access_list"} + +// Config defines the server's top level configuration. It includes the default app config +// from the SDK as well as the EVM configuration to enable the JSON-RPC APIs. +type Config struct { + config.Config `mapstructure:",squash"` + + EVM EVMConfig `mapstructure:"evm"` + JSONRPC JSONRPCConfig `mapstructure:"json-rpc"` + TLS TLSConfig `mapstructure:"tls"` + + MemIAVL MemIAVLConfig `mapstructure:"memiavl"` +} + +// EVMConfig defines the application configuration values for the EVM. +type EVMConfig struct { + // Tracer defines vm.Tracer type that the EVM will use if the node is run in + // trace mode. Default: 'json'. + Tracer string `mapstructure:"tracer"` + // MaxTxGasWanted defines the gas wanted for each eth tx returned in ante handler in check tx mode. + MaxTxGasWanted uint64 `mapstructure:"max-tx-gas-wanted"` +} + +// 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"` + // GasCap is the global gas cap for eth-call variants. + GasCap uint64 `mapstructure:"gas-cap"` + // AllowInsecureUnlock toggles if account unlocking is enabled when account-related RPCs are exposed by http. + AllowInsecureUnlock bool `mapstructure:"allow-insecure-unlock"` + // EVMTimeout is the global timeout for eth-call. + EVMTimeout time.Duration `mapstructure:"evm-timeout"` + // TxFeeCap is the global tx-fee cap for send transaction + TxFeeCap float64 `mapstructure:"txfee-cap"` + // FilterCap is the global cap for total number of filters that can be created. + FilterCap int32 `mapstructure:"filter-cap"` + // FeeHistoryCap is the global cap for total number of blocks that can be fetched + FeeHistoryCap int32 `mapstructure:"feehistory-cap"` + // Enable defines if the EVM RPC server should be enabled. + Enable bool `mapstructure:"enable"` + // LogsCap defines the max number of results can be returned from single `eth_getLogs` query. + LogsCap int32 `mapstructure:"logs-cap"` + // BlockRangeCap defines the max block range allowed for `eth_getLogs` query. + BlockRangeCap int32 `mapstructure:"block-range-cap"` + // HTTPTimeout is the read/write timeout of http json-rpc server. + 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"` + // MaxOpenConnections sets the maximum number of simultaneous connections + // for the server listener. + MaxOpenConnections int `mapstructure:"max-open-connections"` + // EnableIndexer defines if enable the custom indexer service. + EnableIndexer bool `mapstructure:"enable-indexer"` + // MetricsAddress defines the metrics server to listen on + MetricsAddress string `mapstructure:"metrics-address"` + // FixRevertGasRefundHeight defines the upgrade height for fix of revert gas refund logic when transaction reverted + FixRevertGasRefundHeight int64 `mapstructure:"fix-revert-gas-refund-height"` +} + +// TLSConfig defines the certificate and matching private key for the server. +type TLSConfig struct { + // CertificatePath the file path for the certificate .pem file + CertificatePath string `mapstructure:"certificate-path"` + // KeyPath the file path for the key .pem file + KeyPath string `mapstructure:"key-path"` +} + +// MemIAVLConfig defines the configuration for memIAVL. +type MemIAVLConfig struct { + memiavlcfg.MemIAVLConfig +} + +// AppConfig helps to override default appConfig template and configs. +// return "", nil if no custom configuration is required for the application. +func AppConfig(denom string) (string, interface{}) { + // Optionally allow the chain developer to overwrite the SDK's default + // server config. + customAppConfig := DefaultConfig() + + // The SDK's default minimum gas price is set to "" (empty value) inside + // app.toml. If left empty by validators, the node will halt on startup. + // However, the chain developer can set a default app.toml value for their + // validators here. + // + // In summary: + // - if you leave srvCfg.MinGasPrices = "", all validators MUST tweak their + // own app.toml config, + // - if you set srvCfg.MinGasPrices non-empty, validators CAN tweak their + // own app.toml to override, or use this default value. + // + // In evmos, we set the min gas prices to 0. + if denom != "" { + customAppConfig.Config.MinGasPrices = "0" + denom + } + + customAppTemplate := config.DefaultConfigTemplate + + DefaultEVMConfigTemplate + + memiavlcfg.DefaultConfigTemplate + + return customAppTemplate, *customAppConfig +} + +// DefaultConfig returns server's default configuration. +func DefaultConfig() *Config { + defaultSDKConfig := config.DefaultConfig() + defaultSDKConfig.API.Enable = DefaultAPIEnable + defaultSDKConfig.GRPC.Enable = DefaultGRPCEnable + defaultSDKConfig.GRPCWeb.Enable = DefaultGRPCWebEnable + defaultSDKConfig.Rosetta.Enable = DefaultRosettaEnable + defaultSDKConfig.Telemetry.Enabled = DefaultTelemetryEnable + + return &Config{ + Config: *defaultSDKConfig, + EVM: *DefaultEVMConfig(), + JSONRPC: *DefaultJSONRPCConfig(), + TLS: *DefaultTLSConfig(), + MemIAVL: *DefaultMemIAVLConfig(), + } +} + +// DefaultEVMConfig returns the default EVM configuration +func DefaultEVMConfig() *EVMConfig { + return &EVMConfig{ + Tracer: DefaultEVMTracer, + MaxTxGasWanted: DefaultMaxTxGasWanted, + } +} + +// Validate returns an error if the tracer type is invalid. +func (c EVMConfig) Validate() error { + if c.Tracer != "" && !strings.StringInSlice(c.Tracer, evmTracers) { + return fmt.Errorf("invalid tracer type %s, available types: %v", c.Tracer, evmTracers) + } + + return nil +} + +// GetDefaultAPINamespaces returns the default list of JSON-RPC namespaces that should be enabled +func GetDefaultAPINamespaces() []string { + return []string{"eth", "net", "web3"} +} + +// GetAPINamespaces returns the all the available JSON-RPC API namespaces. +func GetAPINamespaces() []string { + return []string{"web3", "eth", "personal", "net", "txpool", "debug", "miner"} +} + +// DefaultJSONRPCConfig returns an EVM config with the JSON-RPC API enabled by default +func DefaultJSONRPCConfig() *JSONRPCConfig { + return &JSONRPCConfig{ + Enable: false, + API: GetDefaultAPINamespaces(), + Address: DefaultJSONRPCAddress, + WsAddress: DefaultJSONRPCWsAddress, + GasCap: DefaultGasCap, + AllowInsecureUnlock: DefaultJSONRPCAllowInsecureUnlock, + EVMTimeout: DefaultEVMTimeout, + TxFeeCap: DefaultTxFeeCap, + FilterCap: DefaultFilterCap, + FeeHistoryCap: DefaultFeeHistoryCap, + BlockRangeCap: DefaultBlockRangeCap, + LogsCap: DefaultLogsCap, + HTTPTimeout: DefaultHTTPTimeout, + HTTPIdleTimeout: DefaultHTTPIdleTimeout, + AllowUnprotectedTxs: DefaultAllowUnprotectedTxs, + MaxOpenConnections: DefaultMaxOpenConnections, + EnableIndexer: false, + MetricsAddress: DefaultJSONRPCMetricsAddress, + FixRevertGasRefundHeight: DefaultFixRevertGasRefundHeight, + } +} + +// Validate returns an error if the JSON-RPC configuration fields are invalid. +func (c JSONRPCConfig) Validate() error { + if c.Enable && len(c.API) == 0 { + 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") + } + + if c.FeeHistoryCap <= 0 { + return errors.New("JSON-RPC feehistory-cap cannot be negative or 0") + } + + if c.TxFeeCap < 0 { + return errors.New("JSON-RPC tx fee cap cannot be negative") + } + + if c.EVMTimeout < 0 { + return errors.New("JSON-RPC EVM timeout duration cannot be negative") + } + + if c.LogsCap < 0 { + return errors.New("JSON-RPC logs cap cannot be negative") + } + + if c.BlockRangeCap < 0 { + return errors.New("JSON-RPC block range cap cannot be negative") + } + + if c.HTTPTimeout < 0 { + return errors.New("JSON-RPC HTTP timeout duration cannot be negative") + } + + if c.HTTPIdleTimeout < 0 { + return errors.New("JSON-RPC HTTP idle timeout duration cannot be negative") + } + + // check for duplicates + seenAPIs := make(map[string]bool) + for _, api := range c.API { + if seenAPIs[api] { + return fmt.Errorf("repeated API namespace '%s'", api) + } + + seenAPIs[api] = true + } + + return nil +} + +// DefaultTLSConfig returns the default TLS configuration +func DefaultTLSConfig() *TLSConfig { + return &TLSConfig{ + CertificatePath: "", + KeyPath: "", + } +} + +// Validate returns an error if the TLS certificate and key file extensions are invalid. +func (c TLSConfig) Validate() error { + certExt := path.Ext(c.CertificatePath) + + if c.CertificatePath != "" && certExt != ".pem" { + return fmt.Errorf("invalid extension %s for certificate path %s, expected '.pem'", certExt, c.CertificatePath) + } + + keyExt := path.Ext(c.KeyPath) + + if c.KeyPath != "" && keyExt != ".pem" { + return fmt.Errorf("invalid extension %s for key path %s, expected '.pem'", keyExt, c.KeyPath) + } + + return nil +} + +// DefaultMemIAVLConfig returns the default MemIAVL configuration +func DefaultMemIAVLConfig() *MemIAVLConfig { + return &MemIAVLConfig{memiavlcfg.MemIAVLConfig{ + Enable: DefaultMemIAVLEnable, + ZeroCopy: DefaultZeroCopy, + AsyncCommitBuffer: DefaultAsyncCommitBuffer, + SnapshotKeepRecent: DefaultSnapshotKeepRecent, + SnapshotInterval: memiavl.DefaultSnapshotInterval, + CacheSize: memiavlcfg.DefaultCacheSize, + }} +} + +// Validate returns an error if the MemIAVL configuration fields are invalid. +func (c MemIAVLConfig) Validate() error { + // AsyncCommitBuffer can be -1, which means synchronous commit + if c.AsyncCommitBuffer < -1 { + return errors.New("AsyncCommitBuffer cannot be negative") + } + + if c.CacheSize < 0 { + return errors.New("CacheSize cannot be negative") + } + + return nil +} + +// GetConfig returns a fully parsed Config object. +func GetConfig(v *viper.Viper) (Config, error) { + conf := DefaultConfig() + if err := v.Unmarshal(conf); err != nil { + return Config{}, fmt.Errorf("error extracting app config: %w", err) + } + return *conf, nil +} + +// ValidateBasic returns an error any of the application configuration fields are invalid +func (c Config) ValidateBasic() error { + if err := c.EVM.Validate(); err != nil { + return errorsmod.Wrapf(errortypes.ErrAppConfig, "invalid evm config value: %s", err.Error()) + } + + if err := c.JSONRPC.Validate(); err != nil { + return errorsmod.Wrapf(errortypes.ErrAppConfig, "invalid json-rpc config value: %s", err.Error()) + } + + if err := c.TLS.Validate(); err != nil { + return errorsmod.Wrapf(errortypes.ErrAppConfig, "invalid tls config value: %s", err.Error()) + } + + if err := c.MemIAVL.Validate(); err != nil { + return errorsmod.Wrapf(errortypes.ErrAppConfig, "invalid memIAVL config value: %s", err.Error()) + } + + return c.Config.ValidateBasic() +} diff --git a/server/config/config_test.go b/server/config/config_test.go new file mode 100644 index 0000000..a43cf1c --- /dev/null +++ b/server/config/config_test.go @@ -0,0 +1,70 @@ +package config + +import ( + "fmt" + "reflect" + "testing" + + "github.com/evmos/os/testutil" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + require.False(t, cfg.JSONRPC.Enable) + require.Equal(t, cfg.JSONRPC.Address, DefaultJSONRPCAddress) + require.Equal(t, cfg.JSONRPC.WsAddress, DefaultJSONRPCWsAddress) +} + +func TestGetConfig(t *testing.T) { + tests := []struct { + name string + args func() *viper.Viper + want func() Config + wantErr bool + }{ + { + "test unmarshal embedded structs", + func() *viper.Viper { + v := viper.New() + v.Set("minimum-gas-prices", fmt.Sprintf("100%s", testutil.ExampleAttoDenom)) + return v + }, + func() Config { + cfg := DefaultConfig() + cfg.MinGasPrices = fmt.Sprintf("100%s", testutil.ExampleAttoDenom) + return *cfg + }, + false, + }, + { + "test unmarshal EVMConfig", + func() *viper.Viper { + v := viper.New() + v.Set("evm.tracer", "struct") + return v + }, + func() Config { + cfg := DefaultConfig() + require.NotEqual(t, "struct", cfg.EVM.Tracer) + cfg.EVM.Tracer = "struct" + return *cfg + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetConfig(tt.args()) + if (err != nil) != tt.wantErr { + t.Errorf("GetConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want()) { + t.Errorf("GetConfig() got = %v, want %v", got, tt.want()) + } + }) + } +} diff --git a/server/config/toml.go b/server/config/toml.go new file mode 100644 index 0000000..e42d871 --- /dev/null +++ b/server/config/toml.go @@ -0,0 +1,100 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package config + +// DefaultEVMConfigTemplate defines the configuration template for the EVM RPC configuration. +const DefaultEVMConfigTemplate = ` +############################################################################### +### EVM Configuration ### +############################################################################### + +[evm] + +# Tracer defines the 'vm.Tracer' type that the EVM will use when the node is run in +# debug mode. To enable tracing use the '--evm.tracer' flag when starting your node. +# Valid types are: json|struct|access_list|markdown +tracer = "{{ .EVM.Tracer }}" + +# MaxTxGasWanted defines the gas wanted for each eth tx returned in ante handler in check tx mode. +max-tx-gas-wanted = {{ .EVM.MaxTxGasWanted }} + +############################################################################### +### JSON RPC Configuration ### +############################################################################### + +[json-rpc] + +# Enable defines if the JSONRPC server should be enabled. +enable = {{ .JSONRPC.Enable }} + +# Address defines the EVM RPC HTTP server address to bind to. +address = "{{ .JSONRPC.Address }}" + +# Address defines the EVM WebSocket server address to bind to. +ws-address = "{{ .JSONRPC.WsAddress }}" + +# API defines a list of JSON-RPC namespaces that should be enabled +# Example: "eth,txpool,personal,net,debug,web3" +api = "{{range $index, $elmt := .JSONRPC.API}}{{if $index}},{{$elmt}}{{else}}{{$elmt}}{{end}}{{end}}" + +# GasCap sets a cap on gas that can be used in eth_call/estimateGas (0=infinite). Default: 25,000,000. +gas-cap = {{ .JSONRPC.GasCap }} + +# Allow insecure account unlocking when account-related RPCs are exposed by http +allow-insecure-unlock = {{ .JSONRPC.AllowInsecureUnlock }} + +# EVMTimeout is the global timeout for eth_call. Default: 5s. +evm-timeout = "{{ .JSONRPC.EVMTimeout }}" + +# TxFeeCap is the global tx-fee cap for send transaction. Default: 1eth. +txfee-cap = {{ .JSONRPC.TxFeeCap }} + +# FilterCap sets the global cap for total number of filters that can be created +filter-cap = {{ .JSONRPC.FilterCap }} + +# FeeHistoryCap sets the global cap for total number of blocks that can be fetched +feehistory-cap = {{ .JSONRPC.FeeHistoryCap }} + +# LogsCap defines the max number of results can be returned from single 'eth_getLogs' query. +logs-cap = {{ .JSONRPC.LogsCap }} + +# BlockRangeCap defines the max block range allowed for 'eth_getLogs' query. +block-range-cap = {{ .JSONRPC.BlockRangeCap }} + +# HTTPTimeout is the read/write timeout of http json-rpc server. +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 }} + +# MaxOpenConnections sets the maximum number of simultaneous connections +# for the server listener. +max-open-connections = {{ .JSONRPC.MaxOpenConnections }} + +# EnableIndexer enables the custom transaction indexer for the EVM (ethereum transactions). +enable-indexer = {{ .JSONRPC.EnableIndexer }} + +# MetricsAddress defines the EVM Metrics server address to bind to. Pass --metrics in CLI to enable +# Prometheus metrics path: /debug/metrics/prometheus +metrics-address = "{{ .JSONRPC.MetricsAddress }}" + +# Upgrade height for fix of revert gas refund logic when transaction reverted. +fix-revert-gas-refund-height = {{ .JSONRPC.FixRevertGasRefundHeight }} + +############################################################################### +### TLS Configuration ### +############################################################################### + +[tls] + +# Certificate path defines the cert.pem file path for the TLS configuration. +certificate-path = "{{ .TLS.CertificatePath }}" + +# Key path defines the key.pem file path for the TLS configuration. +key-path = "{{ .TLS.KeyPath }}" +` diff --git a/server/flags/flags.go b/server/flags/flags.go new file mode 100644 index 0000000..8dcd06b --- /dev/null +++ b/server/flags/flags.go @@ -0,0 +1,100 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package flags + +import ( + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Tendermint/cosmos-sdk full-node start flags +const ( + WithTendermint = "with-tendermint" + Address = "address" + Transport = "transport" + TraceStore = "trace-store" + CPUProfile = "cpu-profile" + // The type of database for application and snapshots databases + AppDBBackend = "app-db-backend" +) + +// GRPC-related flags. +const ( + GRPCOnly = "grpc-only" + GRPCEnable = "grpc.enable" + GRPCAddress = "grpc.address" + GRPCWebEnable = "grpc-web.enable" + GRPCWebAddress = "grpc-web.address" +) + +// Cosmos API flags +const ( + RPCEnable = "api.enable" + EnabledUnsafeCors = "api.enabled-unsafe-cors" +) + +// 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" + JSONRPCAllowInsecureUnlock = "json-rpc.allow-insecure-unlock" + 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" + JSONRPCMaxOpenConnections = "json-rpc.max-open-connections" + JSONRPCEnableIndexer = "json-rpc.enable-indexer" + // JSONRPCEnableMetrics enables EVM RPC metrics server. + // Set to `metrics` which is hardcoded flag from go-ethereum. + // https://github.com/ethereum/go-ethereum/blob/master/metrics/metrics.go#L35-L55 + JSONRPCEnableMetrics = "metrics" + JSONRPCFixRevertGasRefundHeight = "json-rpc.fix-revert-gas-refund-height" +) + +// EVM flags +const ( + EVMTracer = "evm.tracer" + EVMMaxTxGasWanted = "evm.max-tx-gas-wanted" +) + +// TLS flags +const ( + TLSCertPath = "tls.certificate-path" + TLSKeyPath = "tls.key-path" +) + +// AddTxFlags adds common flags for commands to post tx +func AddTxFlags(cmd *cobra.Command) (*cobra.Command, error) { + cmd.PersistentFlags().String(flags.FlagChainID, "", "Specify Chain ID for sending Tx") + cmd.PersistentFlags().String(flags.FlagFrom, "", "Name or address of private key with which to sign") + cmd.PersistentFlags().String(flags.FlagFees, "", "Fees to pay along with transaction; eg: 10aevmos") + cmd.PersistentFlags().String(flags.FlagGasPrices, "", "Gas prices to determine the transaction fee (e.g. 10aevmos)") + cmd.PersistentFlags().String(flags.FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") //nolint:lll + cmd.PersistentFlags().Float64(flags.FlagGasAdjustment, flags.DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ") //nolint:lll + cmd.PersistentFlags().StringP(flags.FlagBroadcastMode, "b", flags.BroadcastSync, "Transaction broadcasting mode (sync|async)") + cmd.PersistentFlags().String(flags.FlagKeyringBackend, keyring.BackendOS, "Select keyring's backend") + + // --gas can accept integers and "simulate" + // cmd.PersistentFlags().Var(&flags.GasFlagVar, "gas", fmt.Sprintf( + // "gas limit to set per-transaction; set to %q to calculate required gas automatically (default %d)", + // flags.GasFlagAuto, flags.DefaultGasLimit, + // )) + + // viper.BindPFlag(flags.FlagTrustNode, cmd.Flags().Lookup(flags.FlagTrustNode)) + if err := viper.BindPFlag(flags.FlagNode, cmd.PersistentFlags().Lookup(flags.FlagNode)); err != nil { + return nil, err + } + if err := viper.BindPFlag(flags.FlagKeyringBackend, cmd.PersistentFlags().Lookup(flags.FlagKeyringBackend)); err != nil { + return nil, err + } + return cmd, nil +} diff --git a/server/indexer_cmd.go b/server/indexer_cmd.go new file mode 100644 index 0000000..5a65b59 --- /dev/null +++ b/server/indexer_cmd.go @@ -0,0 +1,120 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package server + +import ( + "fmt" + + tmnode "github.com/cometbft/cometbft/node" + sm "github.com/cometbft/cometbft/state" + tmstore "github.com/cometbft/cometbft/store" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "github.com/evmos/os/indexer" + "github.com/spf13/cobra" +) + +func NewIndexTxCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "index-eth-tx [backward|forward]", + Short: "Index historical eth txs", + Long: `Index historical eth txs, it only support two traverse direction to avoid creating gaps in the indexer db if using arbitrary block ranges: + - backward: index the blocks from the first indexed block to the earliest block in the chain, if indexer db is empty, start from the latest block. + - forward: index the blocks from the latest indexed block to latest block in the chain. + + When start the node, the indexer start from the latest indexed block to avoid creating gap. + Backward mode should be used most of the time, so the latest indexed block is always up-to-date. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + direction := args[0] + if direction != "backward" && direction != "forward" { + return fmt.Errorf("unknown index direction, expect: backward|forward, got: %s", direction) + } + + cfg := serverCtx.Config + home := cfg.RootDir + logger := serverCtx.Logger + idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(serverCtx.Viper)) + if err != nil { + logger.Error("failed to open evm indexer DB", "error", err.Error()) + return err + } + idxer := indexer.NewKVIndexer(idxDB, logger.With("module", "evmindex"), clientCtx) + + // open local tendermint db, because the local rpc won't be available. + tmdb, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return err + } + blockStore := tmstore.NewBlockStore(tmdb) + + stateDB, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "state", Config: cfg}) + if err != nil { + return err + } + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: cfg.Storage.DiscardABCIResponses, + }) + + indexBlock := func(height int64) error { + blk := blockStore.LoadBlock(height) + if blk == nil { + return fmt.Errorf("block not found %d", height) + } + resBlk, err := stateStore.LoadABCIResponses(height) + if err != nil { + return err + } + if err := idxer.IndexBlock(blk, resBlk.DeliverTxs); err != nil { + return err + } + fmt.Println(height) + return nil + } + + switch args[0] { + case "backward": + first, err := idxer.FirstIndexedBlock() + if err != nil { + return err + } + if first == -1 { + // start from the latest block if indexer db is empty + first = blockStore.Height() + } + for i := first - 1; i > 0; i-- { + if err := indexBlock(i); err != nil { + return err + } + } + case "forward": + latest, err := idxer.LastIndexedBlock() + if err != nil { + return err + } + if latest == -1 { + // start from genesis if empty + latest = 0 + } + for i := latest + 1; i <= blockStore.Height(); i++ { + if err := indexBlock(i); err != nil { + return err + } + } + default: + return fmt.Errorf("unknown direction %s", args[0]) + } + + return nil + }, + } + return cmd +} diff --git a/server/indexer_service.go b/server/indexer_service.go new file mode 100644 index 0000000..18df859 --- /dev/null +++ b/server/indexer_service.go @@ -0,0 +1,111 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package server + +import ( + "context" + "time" + + "github.com/cometbft/cometbft/libs/service" + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/cometbft/cometbft/types" + evmostypes "github.com/evmos/os/types" +) + +const ( + ServiceName = "EVMIndexerService" + + NewBlockWaitTimeout = 60 * time.Second +) + +// EVMIndexerService indexes transactions for json-rpc service. +type EVMIndexerService struct { + service.BaseService + + txIdxr evmostypes.EVMTxIndexer + client rpcclient.Client +} + +// NewEVMIndexerService returns a new service instance. +func NewEVMIndexerService( + txIdxr evmostypes.EVMTxIndexer, + client rpcclient.Client, +) *EVMIndexerService { + is := &EVMIndexerService{txIdxr: txIdxr, client: client} + is.BaseService = *service.NewBaseService(nil, ServiceName, is) + return is +} + +// OnStart implements service.Service by subscribing for new blocks +// and indexing them by events. +func (eis *EVMIndexerService) OnStart() error { + ctx := context.Background() + status, err := eis.client.Status(ctx) + if err != nil { + return err + } + latestBlock := status.SyncInfo.LatestBlockHeight + newBlockSignal := make(chan struct{}, 1) + + // Use SubscribeUnbuffered here to ensure both subscriptions does not get + // canceled due to not pulling messages fast enough. Cause this might + // sometimes happen when there are no other subscribers. + blockHeadersChan, err := eis.client.Subscribe( + ctx, + ServiceName, + types.QueryForEvent(types.EventNewBlockHeader).String(), + 0) + if err != nil { + return err + } + + go func() { + for { + msg := <-blockHeadersChan + eventDataHeader := msg.Data.(types.EventDataNewBlockHeader) + if eventDataHeader.Header.Height > latestBlock { + latestBlock = eventDataHeader.Header.Height + // notify + select { + case newBlockSignal <- struct{}{}: + default: + } + } + } + }() + + lastBlock, err := eis.txIdxr.LastIndexedBlock() + if err != nil { + return err + } + if lastBlock == -1 { + lastBlock = latestBlock + } + for { + if latestBlock <= lastBlock { + // nothing to index. wait for signal of new block + select { + case <-newBlockSignal: + case <-time.After(NewBlockWaitTimeout): + } + continue + } + for i := lastBlock + 1; i <= latestBlock; i++ { + block, err := eis.client.Block(ctx, &i) + if err != nil { + eis.Logger.Error("failed to fetch block", "height", i, "err", err) + break + } + blockResult, err := eis.client.BlockResults(ctx, &i) + if err != nil { + eis.Logger.Error("failed to fetch block result", "height", i, "err", err) + break + } + if err := eis.txIdxr.IndexBlock(block.Block, blockResult.TxsResults); err != nil { + eis.Logger.Error("failed to index block", "height", i, "err", err) + } + lastBlock = blockResult.Height + } + } +} diff --git a/server/json_rpc.go b/server/json_rpc.go new file mode 100644 index 0000000..0eac135 --- /dev/null +++ b/server/json_rpc.go @@ -0,0 +1,114 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package server + +import ( + "net/http" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/types" + ethlog "github.com/ethereum/go-ethereum/log" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/evmos/os/rpc" + "github.com/evmos/os/server/config" + evmostypes "github.com/evmos/os/types" + "github.com/gorilla/mux" + "github.com/rs/cors" +) + +// StartJSONRPC starts the JSON-RPC server +func StartJSONRPC(ctx *server.Context, + clientCtx client.Context, + tmRPCAddr, + tmEndpoint string, + config *config.Config, + indexer evmostypes.EVMTxIndexer, +) (*http.Server, chan struct{}, error) { + tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) + + logger := ctx.Logger.With("module", "geth") + ethlog.Root().SetHandler(ethlog.FuncHandler(func(r *ethlog.Record) error { + switch r.Lvl { + case ethlog.LvlTrace, ethlog.LvlDebug: + logger.Debug(r.Msg, r.Ctx...) + case ethlog.LvlInfo, ethlog.LvlWarn: + logger.Info(r.Msg, r.Ctx...) + case ethlog.LvlError, ethlog.LvlCrit: + logger.Error(r.Msg, r.Ctx...) + } + return nil + })) + + rpcServer := ethrpc.NewServer() + + allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs + rpcAPIArr := config.JSONRPC.API + + apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, indexer, rpcAPIArr) + + for _, api := range apis { + if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { + ctx.Logger.Error( + "failed to register service in JSON RPC namespace", + "namespace", api.Namespace, + "service", api.Service, + ) + return nil, nil, err + } + } + + r := mux.NewRouter() + r.HandleFunc("/", rpcServer.ServeHTTP).Methods("POST") + + handlerWithCors := cors.Default() + if config.API.EnableUnsafeCORS { + handlerWithCors = cors.AllowAll() + } + + httpSrv := &http.Server{ + Addr: config.JSONRPC.Address, + Handler: handlerWithCors.Handler(r), + ReadHeaderTimeout: config.JSONRPC.HTTPTimeout, + ReadTimeout: config.JSONRPC.HTTPTimeout, + WriteTimeout: config.JSONRPC.HTTPTimeout, + IdleTimeout: config.JSONRPC.HTTPIdleTimeout, + } + httpSrvDone := make(chan struct{}, 1) + + ln, err := Listen(httpSrv.Addr, config) + if err != nil { + return nil, nil, err + } + + errCh := make(chan error) + go func() { + ctx.Logger.Info("Starting JSON-RPC server", "address", config.JSONRPC.Address) + if err := httpSrv.Serve(ln); err != nil { + if err == http.ErrServerClosed { + close(httpSrvDone) + return + } + + ctx.Logger.Error("failed to start JSON-RPC server", "error", err.Error()) + errCh <- err + } + }() + + select { + case err := <-errCh: + ctx.Logger.Error("failed to boot JSON-RPC server", "error", err.Error()) + return nil, nil, err + case <-time.After(types.ServerStartTime): // assume JSON RPC server started successfully + } + + ctx.Logger.Info("Starting JSON WebSocket server", "address", config.JSONRPC.WsAddress) + + // allocate separate WS connection to Tendermint + tmWsClient = ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) + wsSrv := rpc.NewWebsocketsServer(clientCtx, ctx.Logger, tmWsClient, config) + wsSrv.Start() + return httpSrv, httpSrvDone, nil +} diff --git a/server/start.go b/server/start.go new file mode 100644 index 0000000..fe6827e --- /dev/null +++ b/server/start.go @@ -0,0 +1,659 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package server + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "runtime/pprof" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/telemetry" + + "github.com/spf13/cobra" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + dbm "github.com/cometbft/cometbft-db" + abciserver "github.com/cometbft/cometbft/abci/server" + tcmd "github.com/cometbft/cometbft/cmd/cometbft/commands" + tmos "github.com/cometbft/cometbft/libs/os" + "github.com/cometbft/cometbft/node" + "github.com/cometbft/cometbft/p2p" + pvm "github.com/cometbft/cometbft/privval" + "github.com/cometbft/cometbft/proxy" + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/cometbft/cometbft/rpc/client/local" + + "cosmossdk.io/tools/rosetta" + crgserver "cosmossdk.io/tools/rosetta/lib/server" + + ethmetricsexp "github.com/ethereum/go-ethereum/metrics/exp" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/api" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" + servergrpc "github.com/cosmos/cosmos-sdk/server/grpc" + "github.com/cosmos/cosmos-sdk/server/types" + pruningtypes "github.com/cosmos/cosmos-sdk/store/pruning/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/evmos/evmos/v19/cmd/evmosd/opendb" + "github.com/evmos/os/indexer" + ethdebug "github.com/evmos/os/rpc/namespaces/ethereum/debug" + "github.com/evmos/os/server/config" + srvflags "github.com/evmos/os/server/flags" + evmostypes "github.com/evmos/os/types" +) + +// DBOpener is a function to open `application.db`, potentially with customized options. +type DBOpener func(opts types.AppOptions, rootDir string, backend dbm.BackendType) (dbm.DB, error) + +// StartOptions defines options that can be customized in `StartCmd` +type StartOptions struct { + AppCreator types.AppCreator + DefaultNodeHome string + DBOpener DBOpener +} + +// NewDefaultStartOptions use the default db opener provided in tm-db. +func NewDefaultStartOptions(appCreator types.AppCreator, defaultNodeHome string) StartOptions { + return StartOptions{ + AppCreator: appCreator, + DefaultNodeHome: defaultNodeHome, + DBOpener: opendb.OpenDB, + } +} + +// StartCmd runs the service passed in, either stand-alone or in-process with +// Tendermint. +func StartCmd(opts StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "start", + Short: "Run the full node", + Long: `Run the full node application with Tendermint in or out of process. By +default, the application will run with Tendermint in process. + +Pruning options can be provided via the '--pruning' flag or alternatively with '--pruning-keep-recent', +'pruning-keep-every', and 'pruning-interval' together. + +For '--pruning' the options are as follows: + +default: the last 100 states are kept in addition to every 500th state; pruning at 10 block intervals +nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +everything: all saved states will be deleted, storing only the current state; pruning at 10 block intervals +custom: allow pruning options to be manually specified through 'pruning-keep-recent', 'pruning-keep-every', and 'pruning-interval' + +Node halting configurations exist in the form of two flags: '--halt-height' and '--halt-time'. During +the ABCI Commit phase, the node will check if the current block height is greater than or equal to +the halt-height or if the current block time is greater than or equal to the halt-time. If so, the +node will attempt to gracefully shutdown and the block will not be committed. In addition, the node +will not be able to commit subsequent blocks. + +For profiling and benchmarking purposes, CPU profiling can be enabled via the '--cpu-profile' flag +which accepts a path for the resulting pprof file. +`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + + // Bind flags to the Context's Viper so the app construction can set + // options accordingly. + err := serverCtx.Viper.BindPFlags(cmd.Flags()) + if err != nil { + return err + } + + _, err = server.GetPruningOptionsFromFlags(serverCtx.Viper) + return err + }, + RunE: func(cmd *cobra.Command, _ []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + withTM, _ := cmd.Flags().GetBool(srvflags.WithTendermint) + if !withTM { + serverCtx.Logger.Info("starting ABCI without Tendermint") + return startStandAlone(serverCtx, opts) + } + + serverCtx.Logger.Info("Unlocking keyring") + + // fire unlock precess for keyring + keyringBackend, _ := cmd.Flags().GetString(flags.FlagKeyringBackend) + if keyringBackend == keyring.BackendFile { + _, err = clientCtx.Keyring.List() + if err != nil { + return err + } + } + + serverCtx.Logger.Info("starting ABCI with Tendermint") + + // amino is needed here for backwards compatibility of REST routes + err = startInProcess(serverCtx, clientCtx, opts) + errCode, ok := err.(server.ErrorCode) + if !ok { + return err + } + + serverCtx.Logger.Debug(fmt.Sprintf("received quit signal: %d", errCode.Code)) + return nil + }, + } + + cmd.Flags().String(flags.FlagHome, opts.DefaultNodeHome, "The application home directory") + cmd.Flags().Bool(srvflags.WithTendermint, true, "Run abci app embedded in-process with tendermint") + cmd.Flags().String(srvflags.Address, "tcp://0.0.0.0:26658", "Listen address") + cmd.Flags().String(srvflags.Transport, "socket", "Transport protocol: socket, grpc") + cmd.Flags().String(srvflags.TraceStore, "", "Enable KVStore tracing to an output file") + cmd.Flags().String(server.FlagMinGasPrices, "", "Minimum gas prices to accept for transactions; Any fee in a tx must meet this minimum (e.g. 20000000000aevmos)") //nolint:lll + cmd.Flags().IntSlice(server.FlagUnsafeSkipUpgrades, []int{}, "Skip a set of upgrade heights to continue the old binary") + cmd.Flags().Uint64(server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node") + cmd.Flags().Uint64(server.FlagHaltTime, 0, "Minimum block time (in Unix seconds) at which to gracefully halt the chain and shutdown the node") + cmd.Flags().Bool(server.FlagInterBlockCache, true, "Enable inter-block caching") + cmd.Flags().String(srvflags.CPUProfile, "", "Enable CPU profiling and write to the provided file") + cmd.Flags().Bool(server.FlagTrace, false, "Provide full stack traces for errors in ABCI Log") + cmd.Flags().String(server.FlagPruning, pruningtypes.PruningOptionDefault, "Pruning strategy (default|nothing|everything|custom)") + cmd.Flags().Uint64(server.FlagPruningKeepRecent, 0, "Number of recent heights to keep on disk (ignored if pruning is not 'custom')") + cmd.Flags().Uint64(server.FlagPruningInterval, 0, "Height interval at which pruned heights are removed from disk (ignored if pruning is not 'custom')") //nolint:lll + cmd.Flags().Uint(server.FlagInvCheckPeriod, 0, "Assert registered invariants every N blocks") + cmd.Flags().Uint64(server.FlagMinRetainBlocks, 0, "Minimum block height offset during ABCI commit to prune Tendermint blocks") + cmd.Flags().String(srvflags.AppDBBackend, "", "The type of database for application and snapshots databases") + + cmd.Flags().Bool(srvflags.GRPCOnly, false, "Start the node in gRPC query only mode without Tendermint process") + cmd.Flags().Bool(srvflags.GRPCEnable, config.DefaultGRPCEnable, "Define if the gRPC server should be enabled") + cmd.Flags().String(srvflags.GRPCAddress, serverconfig.DefaultGRPCAddress, "the gRPC server address to listen on") + cmd.Flags().Bool(srvflags.GRPCWebEnable, config.DefaultGRPCWebEnable, "Define if the gRPC-Web server should be enabled. (Note: gRPC must also be enabled.)") + cmd.Flags().String(srvflags.GRPCWebAddress, serverconfig.DefaultGRPCWebAddress, "The gRPC-Web server address to listen on") + + cmd.Flags().Bool(srvflags.RPCEnable, config.DefaultAPIEnable, "Defines if Cosmos-sdk REST server should be enabled") + cmd.Flags().Bool(srvflags.EnabledUnsafeCors, false, "Defines if CORS should be enabled (unsafe - use it at your own risk)") + + cmd.Flags().Bool(srvflags.JSONRPCEnable, config.DefaultJSONRPCEnable, "Define if the JSON-RPC server should be enabled") + cmd.Flags().StringSlice(srvflags.JSONRPCAPI, config.GetDefaultAPINamespaces(), "Defines a list of JSON-RPC namespaces that should be enabled") + 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 unit is aevmos (0=infinite)") //nolint:lll + cmd.Flags().Bool(srvflags.JSONRPCAllowInsecureUnlock, config.DefaultJSONRPCAllowInsecureUnlock, "Allow insecure account unlocking when account-related RPCs are exposed by http") //nolint:lll + cmd.Flags().Float64(srvflags.JSONRPCTxFeeCap, config.DefaultTxFeeCap, "Sets a cap on transaction fee that can be sent via the RPC APIs (1 = default 1 evmos)") //nolint:lll + cmd.Flags().Int32(srvflags.JSONRPCFilterCap, config.DefaultFilterCap, "Sets the global cap for total number of filters that can be created") + 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") //nolint:lll + 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().Int(srvflags.JSONRPCMaxOpenConnections, config.DefaultMaxOpenConnections, "Sets the maximum number of simultaneous connections for the server listener") //nolint:lll + cmd.Flags().Bool(srvflags.JSONRPCEnableIndexer, false, "Enable the custom tx indexer for json-rpc") + cmd.Flags().Bool(srvflags.JSONRPCEnableMetrics, false, "Define if EVM rpc metrics server should be enabled") + + 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)") //nolint:lll + cmd.Flags().Uint64(srvflags.EVMMaxTxGasWanted, config.DefaultMaxTxGasWanted, "the gas wanted for each eth tx returned in ante handler in check tx mode") //nolint:lll + + cmd.Flags().String(srvflags.TLSCertPath, "", "the cert.pem file path for the server TLS configuration") + cmd.Flags().String(srvflags.TLSKeyPath, "", "the key.pem file path for the server TLS configuration") + + cmd.Flags().Uint64(server.FlagStateSyncSnapshotInterval, 0, "State sync snapshot interval") + cmd.Flags().Uint32(server.FlagStateSyncSnapshotKeepRecent, 2, "State sync snapshot to keep") + + // add support for all Tendermint-specific command line options + tcmd.AddNodeFlags(cmd) + return cmd +} + +func startStandAlone(ctx *server.Context, opts StartOptions) error { + addr := ctx.Viper.GetString(srvflags.Address) + transport := ctx.Viper.GetString(srvflags.Transport) + home := ctx.Viper.GetString(flags.FlagHome) + + db, err := opts.DBOpener(ctx.Viper, home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + return err + } + + defer func() { + if err := db.Close(); err != nil { + ctx.Logger.Error("error closing db", "error", err.Error()) + } + }() + + traceWriterFile := ctx.Viper.GetString(srvflags.TraceStore) + traceWriter, err := openTraceWriter(traceWriterFile) + if err != nil { + return err + } + + app := opts.AppCreator(ctx.Logger, db, traceWriter, ctx.Viper) + + config, err := config.GetConfig(ctx.Viper) + if err != nil { + ctx.Logger.Error("failed to get server config", "error", err.Error()) + return err + } + + if err := config.ValidateBasic(); err != nil { + ctx.Logger.Error("invalid server config", "error", err.Error()) + return err + } + + _, err = startTelemetry(config) + if err != nil { + return err + } + + svr, err := abciserver.NewServer(addr, transport, app) + if err != nil { + return fmt.Errorf("error creating listener: %v", err) + } + + svr.SetLogger(ctx.Logger.With("server", "abci")) + + err = svr.Start() + if err != nil { + tmos.Exit(err.Error()) + } + + defer func() { + if err = svr.Stop(); err != nil { + tmos.Exit(err.Error()) + } + }() + + // Wait for SIGINT or SIGTERM signal + return server.WaitForQuitSignals() +} + +// legacyAminoCdc is used for the legacy REST API +func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOptions) (err error) { + cfg := ctx.Config + home := cfg.RootDir + logger := ctx.Logger + + if cpuProfile := ctx.Viper.GetString(srvflags.CPUProfile); cpuProfile != "" { + fp, err := ethdebug.ExpandHome(cpuProfile) + if err != nil { + ctx.Logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) + return err + } + + f, err := os.Create(fp) + if err != nil { + return err + } + + ctx.Logger.Info("starting CPU profiler", "profile", cpuProfile) + if err := pprof.StartCPUProfile(f); err != nil { + return err + } + + defer func() { + ctx.Logger.Info("stopping CPU profiler", "profile", cpuProfile) + pprof.StopCPUProfile() + if err := f.Close(); err != nil { + logger.Error("failed to close CPU profiler file", "error", err.Error()) + } + }() + } + + db, err := opts.DBOpener(ctx.Viper, home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + logger.Error("failed to open DB", "error", err.Error()) + return err + } + + defer func() { + if err := db.Close(); err != nil { + ctx.Logger.With("error", err).Error("error closing db") + } + }() + + traceWriterFile := ctx.Viper.GetString(srvflags.TraceStore) + traceWriter, err := openTraceWriter(traceWriterFile) + if err != nil { + logger.Error("failed to open trace writer", "error", err.Error()) + return err + } + + config, err := config.GetConfig(ctx.Viper) + if err != nil { + logger.Error("failed to get server config", "error", err.Error()) + return err + } + + if err := config.ValidateBasic(); err != nil { + logger.Error("invalid server config", "error", err.Error()) + return err + } + + app := opts.AppCreator(ctx.Logger, db, traceWriter, ctx.Viper) + + nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) + if err != nil { + logger.Error("failed load or gen node key", "error", err.Error()) + return err + } + + genDocProvider := node.DefaultGenesisDocProviderFunc(cfg) + + var ( + tmNode *node.Node + gRPCOnly = ctx.Viper.GetBool(srvflags.GRPCOnly) + ) + + if gRPCOnly { + logger.Info("starting node in query only mode; Tendermint is disabled") + config.GRPC.Enable = true + config.JSONRPC.EnableIndexer = false + } else { + logger.Info("starting node with ABCI Tendermint in-process") + + tmNode, err = node.NewNode( + cfg, + pvm.LoadOrGenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()), + nodeKey, + proxy.NewLocalClientCreator(app), + genDocProvider, + node.DefaultDBProvider, + node.DefaultMetricsProvider(cfg.Instrumentation), + ctx.Logger.With("server", "node"), + ) + if err != nil { + logger.Error("failed init node", "error", err.Error()) + return err + } + + if err := tmNode.Start(); err != nil { + logger.Error("failed start tendermint server", "error", err.Error()) + return err + } + + defer func() { + if tmNode.IsRunning() { + _ = tmNode.Stop() + } + }() + } + + // Add the tx service to the gRPC router. We only need to register this + // service if API or gRPC or JSONRPC is enabled, and avoid doing so in the general + // case, because it spawns a new local tendermint RPC client. + if (config.API.Enable || config.GRPC.Enable || config.JSONRPC.Enable || config.JSONRPC.EnableIndexer) && tmNode != nil { + clientCtx = clientCtx.WithClient(local.New(tmNode)) + + app.RegisterTxService(clientCtx) + app.RegisterTendermintService(clientCtx) + app.RegisterNodeService(clientCtx) + } + + metrics, err := startTelemetry(config) + if err != nil { + return err + } + + // Enable metrics if JSONRPC is enabled and --metrics is passed + // Flag not added in config to avoid user enabling in config without passing in CLI + if config.JSONRPC.Enable && ctx.Viper.GetBool(srvflags.JSONRPCEnableMetrics) { + ethmetricsexp.Setup(config.JSONRPC.MetricsAddress) + } + + var idxer evmostypes.EVMTxIndexer + if config.JSONRPC.EnableIndexer { + idxDB, err := OpenIndexerDB(home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + logger.Error("failed to open evm indexer DB", "error", err.Error()) + return err + } + + idxLogger := ctx.Logger.With("indexer", "evm") + idxer = indexer.NewKVIndexer(idxDB, idxLogger, clientCtx) + indexerService := NewEVMIndexerService(idxer, clientCtx.Client.(rpcclient.Client)) + indexerService.SetLogger(idxLogger) + + errCh := make(chan error) + go func() { + if err := indexerService.Start(); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(types.ServerStartTime): // assume server started successfully + } + } + + if config.API.Enable || config.JSONRPC.Enable { + genDoc, err := genDocProvider() + if err != nil { + return err + } + + clientCtx = clientCtx. + WithHomeDir(home). + WithChainID(genDoc.ChainID) + + // Set `GRPCClient` to `clientCtx` to enjoy concurrent grpc query. + // only use it if gRPC server is enabled. + if config.GRPC.Enable { + _, port, err := net.SplitHostPort(config.GRPC.Address) + if err != nil { + return errorsmod.Wrapf(err, "invalid grpc address %s", config.GRPC.Address) + } + + maxSendMsgSize := config.GRPC.MaxSendMsgSize + if maxSendMsgSize == 0 { + maxSendMsgSize = serverconfig.DefaultGRPCMaxSendMsgSize + } + + maxRecvMsgSize := config.GRPC.MaxRecvMsgSize + if maxRecvMsgSize == 0 { + maxRecvMsgSize = serverconfig.DefaultGRPCMaxRecvMsgSize + } + + grpcAddress := fmt.Sprintf("127.0.0.1:%s", port) + + // If grpc is enabled, configure grpc client for grpc gateway and json-rpc. + grpcClient, err := grpc.NewClient( + grpcAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.ForceCodec(codec.NewProtoCodec(clientCtx.InterfaceRegistry).GRPCCodec()), + grpc.MaxCallRecvMsgSize(maxRecvMsgSize), + grpc.MaxCallSendMsgSize(maxSendMsgSize), + ), + ) + if err != nil { + return err + } + + clientCtx = clientCtx.WithGRPCClient(grpcClient) + ctx.Logger.Debug("gRPC client assigned to client context", "address", grpcAddress) + } + } + + var apiSrv *api.Server + if config.API.Enable { + apiSrv = api.New(clientCtx, ctx.Logger.With("server", "api")) + app.RegisterAPIRoutes(apiSrv, config.API) + + if config.Telemetry.Enabled { + apiSrv.SetTelemetry(metrics) + } + + errCh := make(chan error) + go func() { + if err := apiSrv.Start(config.Config); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(types.ServerStartTime): // assume server started successfully + } + + defer apiSrv.Close() + } + + var ( + grpcSrv *grpc.Server + grpcWebSrv *http.Server + ) + + if config.GRPC.Enable { + grpcSrv, err = servergrpc.StartGRPCServer(clientCtx, app, config.GRPC) + if err != nil { + return err + } + defer grpcSrv.Stop() + if config.GRPCWeb.Enable { + grpcWebSrv, err = servergrpc.StartGRPCWeb(grpcSrv, config.Config) + if err != nil { + ctx.Logger.Error("failed to start grpc-web http server", "error", err.Error()) + return err + } + + defer func() { + if err := grpcWebSrv.Close(); err != nil { + logger.Error("failed to close the grpc-web http server", "error", err.Error()) + } + }() + } + } + + var ( + httpSrv *http.Server + httpSrvDone chan struct{} + ) + + if config.JSONRPC.Enable { + genDoc, err := genDocProvider() + if err != nil { + return err + } + + clientCtx := clientCtx.WithChainID(genDoc.ChainID) + + tmEndpoint := "/websocket" + tmRPCAddr := cfg.RPC.ListenAddress + httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config, idxer) + if err != nil { + return err + } + defer func() { + shutdownCtx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + if err := httpSrv.Shutdown(shutdownCtx); err != nil { + logger.Error("HTTP server shutdown produced a warning", "error", err.Error()) + } else { + logger.Info("HTTP server shut down, waiting 5 sec") + select { + case <-time.Tick(5 * time.Second): + case <-httpSrvDone: + } + } + }() + } + + // At this point it is safe to block the process if we're in query only mode as + // we do not need to start Rosetta or handle any Tendermint related processes. + if gRPCOnly { + // wait for signal capture and gracefully return + return server.WaitForQuitSignals() + } + + var rosettaSrv crgserver.Server + if config.Rosetta.Enable { + offlineMode := config.Rosetta.Offline + + // If GRPC is not enabled rosetta cannot work in online mode, so it works in + // offline mode. + if !config.GRPC.Enable { + offlineMode = true + } + + minGasPrices, err := sdk.ParseDecCoins(config.MinGasPrices) + if err != nil { + ctx.Logger.Error("failed to parse minimum-gas-prices", "error", err.Error()) + return err + } + + conf := &rosetta.Config{ + Blockchain: config.Rosetta.Blockchain, + Network: config.Rosetta.Network, + TendermintRPC: ctx.Config.RPC.ListenAddress, + GRPCEndpoint: config.GRPC.Address, + Addr: config.Rosetta.Address, + Retries: config.Rosetta.Retries, + Offline: offlineMode, + GasToSuggest: config.Rosetta.GasToSuggest, + EnableFeeSuggestion: config.Rosetta.EnableFeeSuggestion, + GasPrices: minGasPrices.Sort(), + Codec: clientCtx.Codec.(*codec.ProtoCodec), + InterfaceRegistry: clientCtx.InterfaceRegistry, + } + + rosettaSrv, err = rosetta.ServerFromConfig(conf) + if err != nil { + return err + } + + errCh := make(chan error) + go func() { + if err := rosettaSrv.Start(); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(types.ServerStartTime): // assume server started successfully + } + } + // Wait for SIGINT or SIGTERM signal + return server.WaitForQuitSignals() +} + +// OpenIndexerDB opens the custom eth indexer db, using the same db backend as the main app +func OpenIndexerDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) { + dataDir := filepath.Join(rootDir, "data") + return dbm.NewDB("evmindexer", backendType, dataDir) +} + +func openTraceWriter(traceWriterFile string) (w io.Writer, err error) { + if traceWriterFile == "" { + return + } + + filePath := filepath.Clean(traceWriterFile) + return os.OpenFile( + filePath, + os.O_WRONLY|os.O_APPEND|os.O_CREATE, + 0o600, + ) +} + +func startTelemetry(cfg config.Config) (*telemetry.Metrics, error) { + if !cfg.Telemetry.Enabled { + return nil, nil + } + return telemetry.New(cfg.Telemetry) +} diff --git a/server/util.go b/server/util.go new file mode 100644 index 0000000..f970980 --- /dev/null +++ b/server/util.go @@ -0,0 +1,129 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package server + +import ( + "net" + "net/http" + "time" + + "github.com/evmos/os/server/config" + "github.com/gorilla/mux" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/spf13/cobra" + "golang.org/x/net/netutil" + + sdkserver "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/version" + + tmcmd "github.com/cometbft/cometbft/cmd/cometbft/commands" + tmlog "github.com/cometbft/cometbft/libs/log" + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" +) + +// AddCommands adds server commands +func AddCommands( + rootCmd *cobra.Command, + opts StartOptions, + appExport types.AppExporter, + addStartFlags types.ModuleInitFlags, +) { + tendermintCmd := &cobra.Command{ + Use: "tendermint", + Short: "Tendermint subcommands", + } + + tendermintCmd.AddCommand( + sdkserver.ShowNodeIDCmd(), + sdkserver.ShowValidatorCmd(), + sdkserver.ShowAddressCmd(), + sdkserver.VersionCmd(), + tmcmd.ResetAllCmd, + tmcmd.ResetStateCmd, + sdkserver.BootstrapStateCmd(opts.AppCreator), + ) + + startCmd := StartCmd(opts) + addStartFlags(startCmd) + + rootCmd.AddCommand( + startCmd, + tendermintCmd, + sdkserver.ExportCmd(appExport, opts.DefaultNodeHome), + version.NewVersionCommand(), + sdkserver.NewRollbackCmd(opts.AppCreator, opts.DefaultNodeHome), + + // custom tx indexer command + NewIndexTxCmd(), + ) +} + +func ConnectTmWS(tmRPCAddr, tmEndpoint string, logger tmlog.Logger) *rpcclient.WSClient { + tmWsClient, err := rpcclient.NewWS(tmRPCAddr, tmEndpoint, + rpcclient.MaxReconnectAttempts(256), + rpcclient.ReadWait(120*time.Second), + rpcclient.WriteWait(120*time.Second), + rpcclient.PingPeriod(50*time.Second), + rpcclient.OnReconnect(func() { + logger.Debug("EVM RPC reconnects to Tendermint WS", "address", tmRPCAddr+tmEndpoint) + }), + ) + + if err != nil { + logger.Error( + "Tendermint WS client could not be created", + "address", tmRPCAddr+tmEndpoint, + "error", err, + ) + } else if err := tmWsClient.OnStart(); err != nil { + logger.Error( + "Tendermint WS client could not start", + "address", tmRPCAddr+tmEndpoint, + "error", err, + ) + } + + return tmWsClient +} + +func MountGRPCWebServices( + router *mux.Router, + grpcWeb *grpcweb.WrappedGrpcServer, + grpcResources []string, + logger tmlog.Logger, +) { + for _, res := range grpcResources { + logger.Info("[GRPC Web] HTTP POST mounted", "resource", res) + + s := router.Methods("POST").Subrouter() + s.HandleFunc(res, func(resp http.ResponseWriter, req *http.Request) { + if grpcWeb.IsGrpcWebSocketRequest(req) { + grpcWeb.HandleGrpcWebsocketRequest(resp, req) + return + } + + if grpcWeb.IsGrpcWebRequest(req) { + grpcWeb.HandleGrpcWebRequest(resp, req) + return + } + }) + } +} + +// Listen starts a net.Listener on the tcp network on the given address. +// If there is a specified MaxOpenConnections in the config, it will also set the limitListener. +func Listen(addr string, config *config.Config) (net.Listener, error) { + if addr == "" { + addr = ":http" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + if config.JSONRPC.MaxOpenConnections > 0 { + ln = netutil.LimitListener(ln, config.JSONRPC.MaxOpenConnections) + } + return ln, err +} diff --git a/testutil/tx/eth.go b/testutil/tx/eth.go index 783aef5..d350c17 100644 --- a/testutil/tx/eth.go +++ b/testutil/tx/eth.go @@ -19,8 +19,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/evmos/evmos/v19/app" - "github.com/evmos/evmos/v19/server/config" evmtypes "github.com/evmos/evmos/v19/x/evm/types" + "github.com/evmos/os/server/config" "github.com/evmos/os/testutil" )