diff --git a/CHANGELOG.md b/CHANGELOG.md index d249a64b28..8b56bbe787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### Features + +* (rpc) [#1603](https://github.com/evmos/ethermint/pull/1603) Support multi gRPC query clients serve with old binary. + ### State Machine Breaking * (deps) [#1168](https://github.com/evmos/ethermint/pull/1168) Upgrade Cosmos SDK to [`v0.46.6`] diff --git a/rpc/apis.go b/rpc/apis.go index e297721cf8..3c3fe4e7c6 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -35,6 +35,7 @@ import ( ethermint "github.com/evmos/ethermint/types" rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + "google.golang.org/grpc" ) // RPC namespaces and API version @@ -60,6 +61,7 @@ const ( type APICreator = func( ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, tendermintWebsocketClient *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, @@ -72,11 +74,12 @@ func init() { apiCreators = map[string]APICreator{ EthNamespace: func(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, ) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, backupGRPCClientConns, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: EthNamespace, @@ -92,7 +95,14 @@ func init() { }, } }, - Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient, bool, ethermint.EVMTxIndexer) []rpc.API { + Web3Namespace: func( + *server.Context, + client.Context, + map[[2]int]*grpc.ClientConn, + *rpcclient.WSClient, + bool, + ethermint.EVMTxIndexer, + ) []rpc.API { return []rpc.API{ { Namespace: Web3Namespace, @@ -102,7 +112,14 @@ func init() { }, } }, - NetNamespace: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, _ bool, _ ethermint.EVMTxIndexer) []rpc.API { + NetNamespace: func( + _ *server.Context, + clientCtx client.Context, + _ map[[2]int]*grpc.ClientConn, + _ *rpcclient.WSClient, + _ bool, + _ ethermint.EVMTxIndexer, + ) []rpc.API { return []rpc.API{ { Namespace: NetNamespace, @@ -114,11 +131,12 @@ func init() { }, PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, ) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, backupGRPCClientConns, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: PersonalNamespace, @@ -128,7 +146,14 @@ func init() { }, } }, - TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool, _ ethermint.EVMTxIndexer) []rpc.API { + TxPoolNamespace: func( + ctx *server.Context, + _ client.Context, + _ map[[2]int]*grpc.ClientConn, + _ *rpcclient.WSClient, + _ bool, + _ ethermint.EVMTxIndexer, + ) []rpc.API { return []rpc.API{ { Namespace: TxPoolNamespace, @@ -140,11 +165,12 @@ func init() { }, DebugNamespace: func(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, ) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, backupGRPCClientConns, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: DebugNamespace, @@ -156,11 +182,12 @@ func init() { }, MinerNamespace: func(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, _ *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, ) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, backupGRPCClientConns, allowUnprotectedTxs, indexer) return []rpc.API{ { Namespace: MinerNamespace, @@ -176,6 +203,7 @@ func init() { // GetRPCAPIs returns the list of all APIs func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, @@ -185,7 +213,7 @@ func GetRPCAPIs(ctx *server.Context, for _, ns := range selectedAPIs { if creator, ok := apiCreators[ns]; ok { - apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs, indexer)...) + apis = append(apis, creator(ctx, clientCtx, backupGRPCClientConns, tmWSClient, allowUnprotectedTxs, indexer)...) } else { ctx.Logger.Error("invalid namespace value", "namespace", ns) } diff --git a/rpc/backend/account_info.go b/rpc/backend/account_info.go index 21a483aa51..9f11a6baf1 100644 --- a/rpc/backend/account_info.go +++ b/rpc/backend/account_info.go @@ -42,8 +42,9 @@ func (b *Backend) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNu req := &evmtypes.QueryCodeRequest{ Address: address.String(), } - - res, err := b.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req) + height := blockNum.Int64() + queryClient := b.getGrpcClient(height) + res, err := queryClient.Code(rpctypes.ContextWithHeight(height), req) if err != nil { return nil, err } @@ -84,10 +85,10 @@ func (b *Backend) GetProof(address common.Address, storageKeys []string, blockNr // query storage proofs storageProofs := make([]rpctypes.StorageResult, len(storageKeys)) - + queryClient := b.getGrpcClient(height) for i, key := range storageKeys { hexKey := common.HexToHash(key) - valueBz, proof, err := b.queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) + valueBz, proof, err := queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) if err != nil { return nil, err } @@ -104,14 +105,14 @@ func (b *Backend) GetProof(address common.Address, storageKeys []string, blockNr Address: address.String(), } - res, err := b.queryClient.Account(ctx, req) + res, err := queryClient.Account(ctx, req) if err != nil { return nil, err } // query account proofs accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes())) - _, proof, err := b.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) + _, proof, err := queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) if err != nil { return nil, err } @@ -143,8 +144,9 @@ func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash Address: address.String(), Key: key, } - - res, err := b.queryClient.Storage(rpctypes.ContextWithHeight(blockNum.Int64()), req) + height := blockNum.Int64() + queryClient := b.getGrpcClient(height) + res, err := queryClient.Storage(rpctypes.ContextWithHeight(height), req) if err != nil { return nil, err } @@ -169,7 +171,7 @@ func (b *Backend) GetBalance(address common.Address, blockNrOrHash rpctypes.Bloc return nil, err } - res, err := b.queryClient.Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req) + res, err := b.getGrpcClient(blockNum.Int64()).Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req) if err != nil { return nil, err } diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 09709e7bce..bba26e650e 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -24,6 +24,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -34,8 +35,10 @@ import ( "github.com/evmos/ethermint/server/config" ethermint "github.com/evmos/ethermint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" "github.com/tendermint/tendermint/libs/log" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + "google.golang.org/grpc" ) // BackendI implements the Cosmos and EVM backend. @@ -149,6 +152,7 @@ type Backend struct { ctx context.Context clientCtx client.Context queryClient *rpctypes.QueryClient // gRPC query client + backupQueryClients map[[2]int]*rpctypes.QueryClient logger log.Logger chainID *big.Int cfg config.Config @@ -161,6 +165,7 @@ func NewBackend( ctx *server.Context, logger log.Logger, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, allowUnprotectedTxs bool, indexer ethermint.EVMTxIndexer, ) *Backend { @@ -174,14 +179,23 @@ func NewBackend( panic(err) } - return &Backend{ + backend := &Backend{ ctx: context.Background(), clientCtx: clientCtx, queryClient: rpctypes.NewQueryClient(clientCtx), + backupQueryClients: make(map[[2]int]*rpctypes.QueryClient), logger: logger.With("module", "backend"), chainID: chainID, cfg: appConf, allowUnprotectedTxs: allowUnprotectedTxs, indexer: indexer, } + for key, conn := range backupGRPCClientConns { + backend.backupQueryClients[key] = &rpctypes.QueryClient{ + ServiceClient: tx.NewServiceClient(conn), + QueryClient: evmtypes.NewQueryClient(conn), + FeeMarket: feemarkettypes.NewQueryClient(conn), + } + } + return backend } diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go index 66320f27e5..882ba825d3 100644 --- a/rpc/backend/backend_suite_test.go +++ b/rpc/backend/backend_suite_test.go @@ -27,6 +27,7 @@ import ( rpctypes "github.com/evmos/ethermint/rpc/types" "github.com/evmos/ethermint/tests" evmtypes "github.com/evmos/ethermint/x/evm/types" + "google.golang.org/grpc" ) type BackendTestSuite struct { @@ -79,7 +80,7 @@ func (suite *BackendTestSuite) SetupTest() { allowUnprotectedTxs := false idxer := indexer.NewKVIndexer(dbm.NewMemDB(), ctx.Logger, clientCtx) - suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, idxer) + suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, make(map[[2]int]*grpc.ClientConn), allowUnprotectedTxs, idxer) suite.backend.queryClient.QueryClient = mocks.NewEVMQueryClient(suite.T()) suite.backend.clientCtx.Client = mocks.NewClient(suite.T()) suite.backend.queryClient.FeeMarket = mocks.NewFeeMarketQueryClient(suite.T()) diff --git a/rpc/backend/blocks.go b/rpc/backend/blocks.go index c4f09d7009..0d3b65f188 100644 --- a/rpc/backend/blocks.go +++ b/rpc/backend/blocks.go @@ -42,6 +42,7 @@ import ( func (b *Backend) BlockNumber() (hexutil.Uint64, error) { // do any grpc query, ignore the response and use the returned block height var header metadata.MD + // use latest queryClient to get block height _, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}, grpc.Header(&header)) if err != nil { return hexutil.Uint64(0), err @@ -412,7 +413,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( var validatorAccAddr sdk.AccAddress ctx := rpctypes.ContextWithHeight(block.Height) - res, err := b.queryClient.ValidatorAccount(ctx, req) + res, err := b.getGrpcClient(block.Height).ValidatorAccount(ctx, req) if err != nil { b.logger.Debug( "failed to query validator operator address", diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go index 7967d06fe2..bbafdb0bf2 100644 --- a/rpc/backend/call_tx.go +++ b/rpc/backend/call_tx.go @@ -142,6 +142,7 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { } // Query params to use the EVM denomination + // use latest queryClient in send res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) if err != nil { b.logger.Error("failed to query evm params", "error", err.Error()) @@ -332,6 +333,7 @@ func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rp // From ContextWithHeight: if the provided height is 0, // it will return an empty context and the gRPC query will use // the latest block height for querying. + // use latest queryClient to estimate res, err := b.queryClient.EstimateGas(rpctypes.ContextWithHeight(blockNr.Int64()), &req) if err != nil { return 0, err @@ -364,7 +366,8 @@ func (b *Backend) DoCall( // From ContextWithHeight: if the provided height is 0, // it will return an empty context and the gRPC query will use // the latest block height for querying. - ctx := rpctypes.ContextWithHeight(blockNr.Int64()) + height := blockNr.Int64() + ctx := rpctypes.ContextWithHeight(height) timeout := b.RPCEVMTimeout() // Setup context so it may be canceled the call has completed @@ -379,8 +382,7 @@ func (b *Backend) DoCall( // Make sure the context is canceled when the call has completed // this makes sure resources are cleaned up. defer cancel() - - res, err := b.queryClient.EthCall(ctx, &req) + res, err := b.getGrpcClient(height).EthCall(ctx, &req) if err != nil { return nil, err } diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go index bd78e1d1d6..1bcc90984f 100644 --- a/rpc/backend/chain_info.go +++ b/rpc/backend/chain_info.go @@ -54,6 +54,7 @@ func (b *Backend) ChainID() (*hexutil.Big, error) { // ChainConfig returns the latest ethereum chain configuration func (b *Backend) ChainConfig() *params.ChainConfig { + // use latest queryClient to get config params, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) if err != nil { return nil @@ -64,6 +65,7 @@ func (b *Backend) ChainConfig() *params.ChainConfig { // GlobalMinGasPrice returns MinGasPrice param from FeeMarket func (b *Backend) GlobalMinGasPrice() (sdk.Dec, error) { + // use latest queryClient to get MinGasPrice res, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) if err != nil { return sdk.ZeroDec(), err @@ -77,7 +79,9 @@ func (b *Backend) GlobalMinGasPrice() (sdk.Dec, error) { // return nil. func (b *Backend) BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) { // return BaseFee if London hard fork is activated and feemarket is enabled - res, err := b.queryClient.BaseFee(rpctypes.ContextWithHeight(blockRes.Height), &evmtypes.QueryBaseFeeRequest{}) + height := blockRes.Height + queryClient := b.getGrpcClient(height) + res, err := queryClient.BaseFee(rpctypes.ContextWithHeight(height), &evmtypes.QueryBaseFeeRequest{}) if err != nil || res.BaseFee == nil { // we can't tell if it's london HF not enabled or the state is pruned, // in either case, we'll fallback to parsing from begin blocker event, @@ -143,7 +147,7 @@ func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { req := &evmtypes.QueryValidatorAccountRequest{ ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), } - + // use latest queryClient to get coinbase res, err := b.queryClient.ValidatorAccount(b.ctx, req) if err != nil { return nil, err @@ -258,7 +262,7 @@ func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { // london hardfork not enabled or feemarket not enabled return big.NewInt(0), nil } - + // use latest queryClient to get gas info params, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) if err != nil { return nil, err diff --git a/rpc/backend/node_info.go b/rpc/backend/node_info.go index f46c907009..c26e5a6e7d 100644 --- a/rpc/backend/node_info.go +++ b/rpc/backend/node_info.go @@ -339,6 +339,7 @@ func (b *Backend) RPCBlockRangeCap() int32 { // the node config. If set value is 0, it will default to 20. func (b *Backend) RPCMinGasPrice() int64 { + // use latest queryClient to get gas price evmParams, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) if err != nil { return ethermint.DefaultGasPrice diff --git a/rpc/backend/sign_tx.go b/rpc/backend/sign_tx.go index 0c16da0c34..6b173625a7 100644 --- a/rpc/backend/sign_tx.go +++ b/rpc/backend/sign_tx.go @@ -72,6 +72,7 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e } // Query params to use the EVM denomination + // use latest queryClient in send res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) if err != nil { b.logger.Error("failed to query evm params", "error", err.Error()) diff --git a/rpc/backend/tracing.go b/rpc/backend/tracing.go index b784c53e5e..e2182e532b 100644 --- a/rpc/backend/tracing.go +++ b/rpc/backend/tracing.go @@ -28,6 +28,17 @@ import ( tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" ) +func (b *Backend) getGrpcClient(height int64) *rpctypes.QueryClient { + for blocks, client := range b.backupQueryClients { + // b1-b2 -> g1 + // b3-b4 -> g2 + if int64(blocks[0]) <= height && int64(blocks[1]) >= height { + return client + } + } + return b.queryClient +} + // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { @@ -113,7 +124,8 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi // 0 is a special value in `ContextWithHeight` contextHeight = 1 } - traceResult, err := b.queryClient.TraceTx(rpctypes.ContextWithHeight(contextHeight), &traceTxRequest) + queryClient := b.getGrpcClient(contextHeight) + traceResult, err := queryClient.TraceTx(rpctypes.ContextWithHeight(contextHeight), &traceTxRequest) if err != nil { return nil, err } @@ -189,8 +201,8 @@ func (b *Backend) TraceBlock(height rpctypes.BlockNumber, ProposerAddress: sdk.ConsAddress(block.Block.ProposerAddress), ChainId: b.chainID.Int64(), } - - res, err := b.queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) + queryClient := b.getGrpcClient(int64(contextHeight)) + res, err := queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) if err != nil { return nil, err } diff --git a/server/config/config.go b/server/config/config.go index 0d2b433c71..591b93e557 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -16,6 +16,7 @@ package config import ( + "encoding/json" "errors" "fmt" "path" @@ -138,6 +139,8 @@ type JSONRPCConfig struct { 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"` + // A list of grpc address with block range + BackupGRPCBlockAddressBlockRange map[[2]int]string `mapstructure:"backup-grpc-address-block-range"` } // TLSConfig defines the certificate and matching private key for the server. @@ -223,24 +226,25 @@ 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, - AllowUnprotectedTxs: DefaultAllowUnprotectedTxs, - MaxOpenConnections: DefaultMaxOpenConnections, - EnableIndexer: false, - MetricsAddress: DefaultJSONRPCMetricsAddress, - FixRevertGasRefundHeight: DefaultFixRevertGasRefundHeight, + 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, + MaxOpenConnections: DefaultMaxOpenConnections, + EnableIndexer: false, + MetricsAddress: DefaultJSONRPCMetricsAddress, + FixRevertGasRefundHeight: DefaultFixRevertGasRefundHeight, + BackupGRPCBlockAddressBlockRange: make(map[[2]int]string), } } @@ -327,6 +331,18 @@ func GetConfig(v *viper.Viper) (Config, error) { return Config{}, err } + data := make(map[string][2]int) + raw := v.GetString("json-rpc.backup-grpc-address-block-range") + if len(raw) > 0 { + err = json.Unmarshal([]byte(raw), &data) + if err != nil { + return Config{}, err + } + } + backupGRPCBlockAddressBlockRange := make(map[[2]int]string) + for k, v := range data { + backupGRPCBlockAddressBlockRange[v] = k + } return Config{ Config: cfg, EVM: EVMConfig{ @@ -334,23 +350,24 @@ func GetConfig(v *viper.Viper) (Config, error) { MaxTxGasWanted: v.GetUint64("evm.max-tx-gas-wanted"), }, JSONRPC: JSONRPCConfig{ - Enable: v.GetBool("json-rpc.enable"), - API: v.GetStringSlice("json-rpc.api"), - Address: v.GetString("json-rpc.address"), - WsAddress: v.GetString("json-rpc.ws-address"), - GasCap: v.GetUint64("json-rpc.gas-cap"), - FilterCap: v.GetInt32("json-rpc.filter-cap"), - FeeHistoryCap: v.GetInt32("json-rpc.feehistory-cap"), - TxFeeCap: v.GetFloat64("json-rpc.txfee-cap"), - EVMTimeout: v.GetDuration("json-rpc.evm-timeout"), - LogsCap: v.GetInt32("json-rpc.logs-cap"), - BlockRangeCap: v.GetInt32("json-rpc.block-range-cap"), - HTTPTimeout: v.GetDuration("json-rpc.http-timeout"), - HTTPIdleTimeout: v.GetDuration("json-rpc.http-idle-timeout"), - MaxOpenConnections: v.GetInt("json-rpc.max-open-connections"), - EnableIndexer: v.GetBool("json-rpc.enable-indexer"), - MetricsAddress: v.GetString("json-rpc.metrics-address"), - FixRevertGasRefundHeight: v.GetInt64("json-rpc.fix-revert-gas-refund-height"), + Enable: v.GetBool("json-rpc.enable"), + API: v.GetStringSlice("json-rpc.api"), + Address: v.GetString("json-rpc.address"), + WsAddress: v.GetString("json-rpc.ws-address"), + GasCap: v.GetUint64("json-rpc.gas-cap"), + FilterCap: v.GetInt32("json-rpc.filter-cap"), + FeeHistoryCap: v.GetInt32("json-rpc.feehistory-cap"), + TxFeeCap: v.GetFloat64("json-rpc.txfee-cap"), + EVMTimeout: v.GetDuration("json-rpc.evm-timeout"), + LogsCap: v.GetInt32("json-rpc.logs-cap"), + BlockRangeCap: v.GetInt32("json-rpc.block-range-cap"), + HTTPTimeout: v.GetDuration("json-rpc.http-timeout"), + HTTPIdleTimeout: v.GetDuration("json-rpc.http-idle-timeout"), + MaxOpenConnections: v.GetInt("json-rpc.max-open-connections"), + EnableIndexer: v.GetBool("json-rpc.enable-indexer"), + MetricsAddress: v.GetString("json-rpc.metrics-address"), + FixRevertGasRefundHeight: v.GetInt64("json-rpc.fix-revert-gas-refund-height"), + BackupGRPCBlockAddressBlockRange: backupGRPCBlockAddressBlockRange, }, TLS: TLSConfig{ CertificatePath: v.GetString("tls.certificate-path"), diff --git a/server/config/toml.go b/server/config/toml.go index a61f110dc7..d83e472c26 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -95,6 +95,10 @@ metrics-address = "{{ .JSONRPC.MetricsAddress }}" # Upgrade height for fix of revert gas refund logic when transaction reverted. fix-revert-gas-refund-height = {{ .JSONRPC.FixRevertGasRefundHeight }} +# A list of backup grpc address with block range +# Example: "0.0.0.0:26113" = [0, 20] +backup-grpc-address-block-range = "{{ "{" }}{{ range $k, $v := .JSONRPC.BackupGRPCBlockAddressBlockRange }}\"{{ $v }}\": [{{index $k 0 }}, {{ index $k 1}}]{{ end }}{{ "}" }}" + ############################################################################### ### TLS Configuration ### ############################################################################### diff --git a/server/flags/flags.go b/server/flags/flags.go index 5f3d9c7112..e9a06f960e 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -68,8 +68,9 @@ const ( // 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" + JSONRPCEnableMetrics = "metrics" + JSONRPCFixRevertGasRefundHeight = "json-rpc.fix-revert-gas-refund-height" + JSONRPCBackupGRPCBlockAddressBlockRange = "json-rpc.backup-grpc-address-block-range" ) // EVM flags diff --git a/server/json_rpc.go b/server/json_rpc.go index 7934a30c77..00e307c3d2 100644 --- a/server/json_rpc.go +++ b/server/json_rpc.go @@ -31,11 +31,13 @@ import ( "github.com/evmos/ethermint/server/config" ethermint "github.com/evmos/ethermint/types" + "google.golang.org/grpc" ) // StartJSONRPC starts the JSON-RPC server func StartJSONRPC(ctx *server.Context, clientCtx client.Context, + backupGRPCClientConns map[[2]int]*grpc.ClientConn, tmRPCAddr, tmEndpoint string, config *config.Config, @@ -61,7 +63,7 @@ func StartJSONRPC(ctx *server.Context, allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs rpcAPIArr := config.JSONRPC.API - apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, indexer, rpcAPIArr) + apis := rpc.GetRPCAPIs(ctx, clientCtx, backupGRPCClientConns, tmWsClient, allowUnprotectedTxs, indexer, 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 74a81d4815..38b193be32 100644 --- a/server/start.go +++ b/server/start.go @@ -210,6 +210,7 @@ which accepts a path for the resulting pprof file. 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.JSONRPCBackupGRPCBlockAddressBlockRange, "", "Define if backup grpc and block range is available") 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 @@ -287,6 +288,14 @@ func startStandAlone(ctx *server.Context, opts StartOptions) error { return server.WaitForQuitSignals() } +func parseGrpcAddress(address string) (string, error) { + _, port, err := net.SplitHostPort(address) + if err != nil { + return "", errorsmod.Wrapf(err, "invalid grpc address %s", address) + } + return fmt.Sprintf("127.0.0.1:%s", port), nil +} + // legacyAminoCdc is used for the legacy REST API func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOptions) (err error) { cfg := ctx.Config @@ -450,6 +459,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt } } + backupGRPCClientConns := make(map[[2]int]*grpc.ClientConn) if config.API.Enable || config.JSONRPC.Enable { genDoc, err := genDocProvider() if err != nil { @@ -463,9 +473,9 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt // 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) + grpcAddress, err := parseGrpcAddress(config.GRPC.Address) if err != nil { - return errorsmod.Wrapf(err, "invalid grpc address %s", config.GRPC.Address) + return err } maxSendMsgSize := config.GRPC.MaxSendMsgSize @@ -478,8 +488,6 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt 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.Dial( grpcAddress, @@ -496,6 +504,27 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt clientCtx = clientCtx.WithGRPCClient(grpcClient) ctx.Logger.Debug("gRPC client assigned to client context", "address", grpcAddress) + + grpcBlockAddresses := config.JSONRPC.BackupGRPCBlockAddressBlockRange + for k, address := range grpcBlockAddresses { + grpcAddr, err := parseGrpcAddress(address) + if err != nil { + return err + } + c, err := grpc.Dial( + grpcAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.ForceCodec(codec.NewProtoCodec(clientCtx.InterfaceRegistry).GRPCCodec()), + grpc.MaxCallRecvMsgSize(maxRecvMsgSize), + grpc.MaxCallSendMsgSize(maxSendMsgSize), + ), + ) + if err != nil { + return err + } + backupGRPCClientConns[k] = c + } } } @@ -565,7 +594,8 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt tmEndpoint := "/websocket" tmRPCAddr := cfg.RPC.ListenAddress - httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config, idxer) + + httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, backupGRPCClientConns, tmRPCAddr, tmEndpoint, &config, idxer) if err != nil { return err } diff --git a/tests/integration_tests/configs/cache-access-list-ethermintd.nix b/tests/integration_tests/configs/cache-access-list-ethermintd.nix new file mode 100644 index 0000000000..c9f2196ea6 --- /dev/null +++ b/tests/integration_tests/configs/cache-access-list-ethermintd.nix @@ -0,0 +1,13 @@ +let + pkgs = import ../../../nix { }; + current = pkgs.callPackage ../../../. { }; + patched = current.overrideAttrs (oldAttrs: rec { + patches = oldAttrs.patches or [ ] ++ [ + ./cache-access-list-ethermintd.patch + ]; + }); +in +pkgs.linkFarm "cache-access-list-ethermintd" [ + { name = "genesis"; path = patched; } + { name = "integration-test-patch"; path = current; } +] \ No newline at end of file diff --git a/tests/integration_tests/configs/cache-access-list-ethermintd.patch b/tests/integration_tests/configs/cache-access-list-ethermintd.patch new file mode 100644 index 0000000000..624bf8cdec --- /dev/null +++ b/tests/integration_tests/configs/cache-access-list-ethermintd.patch @@ -0,0 +1,218 @@ +diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go +index 59a367871..55d49a098 100644 +--- a/x/evm/keeper/grpc_query.go ++++ b/x/evm/keeper/grpc_query.go +@@ -390,6 +390,34 @@ func (k Keeper) EstimateGas(c context.Context, req *types.EthCallRequest) (*type + return &types.EstimateGasResponse{Gas: hi}, nil + } + ++// GetTxTraceResultForTx returns statedb with cached address list when need patch ++func (k Keeper) GetTxTraceResultForTx( ++ ctx sdk.Context, ++ tx *types.MsgEthereumTx, ++ signer ethtypes.Signer, ++ cfg *statedb.EVMConfig, ++ txConfig statedb.TxConfig, ++ lastDB *statedb.StateDB, ++) (*statedb.StateDB, error) { ++ ethTx := tx.AsTransaction() ++ msg, err := ethTx.AsMessage(signer, cfg.BaseFee) ++ if err != nil { ++ return lastDB, err ++ } ++ txConfig.TxHash = ethTx.Hash() ++ stateDB := statedb.New(ctx, &k, txConfig) ++ if lastDB != nil { ++ stateDB.SetAddressToAccessList(lastDB.GetAddressToAccessList()) ++ } ++ lastDB = stateDB ++ rsp, err := k.ApplyMessageWithStateDB(ctx, msg, types.NewNoOpTracer(), true, cfg, txConfig, stateDB) ++ if err != nil { ++ return lastDB, err ++ } ++ txConfig.LogIndex += uint(len(rsp.Logs)) ++ return lastDB, nil ++} ++ + // TraceTx configures a new tracer according to the provided configuration, and + // executes the given message in the provided environment. The return value will + // be tracer dependent. +@@ -421,22 +449,13 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to load evm config: %s", err.Error()) + } +- signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) +- ++ height := ctx.BlockHeight() ++ signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(height)) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) ++ var lastDB *statedb.StateDB + for i, tx := range req.Predecessors { +- ethTx := tx.AsTransaction() +- msg, err := ethTx.AsMessage(signer, cfg.BaseFee) +- if err != nil { +- continue +- } +- txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) +- rsp, err := k.ApplyMessageWithConfig(ctx, msg, types.NewNoOpTracer(), true, cfg, txConfig) +- if err != nil { +- continue +- } +- txConfig.LogIndex += uint(len(rsp.Logs)) ++ lastDB, _ = k.GetTxTraceResultForTx(ctx, tx, signer, cfg, txConfig, lastDB) + } + + tx := req.Msg.AsTransaction() +@@ -450,8 +469,11 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ + // ignore error. default to no traceConfig + _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) + } +- +- result, _, err := k.traceTx(ctx, cfg, txConfig, signer, tx, req.TraceConfig, false, tracerConfig) ++ stateDB := statedb.New(ctx, &k, txConfig) ++ if lastDB != nil { ++ stateDB.SetAddressToAccessList(lastDB.GetAddressToAccessList()) ++ } ++ result, _, err := k.traceTx(ctx, cfg, txConfig, stateDB, signer, tx, req.TraceConfig, false, tracerConfig) + if err != nil { + // error will be returned with detail status from traceTx + return nil, err +@@ -467,6 +489,35 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ + }, nil + } + ++// GetTxTraceResultForBlock returns TxTraceResult and ++// statedb with cached address list when need patch and ++func (k Keeper) GetTxTraceResultForBlock( ++ ctx sdk.Context, ++ tx *types.MsgEthereumTx, ++ signer ethtypes.Signer, ++ cfg *statedb.EVMConfig, ++ txConfig statedb.TxConfig, ++ traceConfig *types.TraceConfig, ++ lastDB *statedb.StateDB, ++) (*statedb.StateDB, *types.TxTraceResult) { ++ result := new(types.TxTraceResult) ++ ethTx := tx.AsTransaction() ++ txConfig.TxHash = ethTx.Hash() ++ stateDB := statedb.New(ctx, &k, txConfig) ++ if lastDB != nil { ++ stateDB.SetAddressToAccessList(lastDB.GetAddressToAccessList()) ++ } ++ lastDB = stateDB ++ traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, stateDB, signer, ethTx, traceConfig, true, nil) ++ if err != nil { ++ result.Error = err.Error() ++ } else { ++ txConfig.LogIndex = logIndex ++ result.Result = traceResult ++ } ++ return lastDB, result ++} ++ + // TraceBlock configures a new tracer according to the provided configuration, and + // executes the given message in the provided environment for all the transactions in the queried block. + // The return value will be tracer dependent. +@@ -499,24 +550,18 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) + if err != nil { + return nil, status.Error(codes.Internal, "failed to load evm config") + } +- signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) ++ height := ctx.BlockHeight() ++ signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(height)) + txsLength := len(req.Txs) + results := make([]*types.TxTraceResult, 0, txsLength) + + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) ++ var lastDB *statedb.StateDB + for i, tx := range req.Txs { +- result := types.TxTraceResult{} +- ethTx := tx.AsTransaction() +- txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) +- traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) +- if err != nil { +- result.Error = err.Error() +- } else { +- txConfig.LogIndex = logIndex +- result.Result = traceResult +- } +- results = append(results, &result) ++ var result *types.TxTraceResult ++ lastDB, result = k.GetTxTraceResultForBlock(ctx, tx, signer, cfg, txConfig, req.TraceConfig, lastDB) ++ results = append(results, result) + } + + resultData, err := json.Marshal(results) +@@ -534,6 +579,7 @@ func (k *Keeper) traceTx( + ctx sdk.Context, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, ++ stateDB *statedb.StateDB, + signer ethtypes.Signer, + tx *ethtypes.Transaction, + traceConfig *types.TraceConfig, +@@ -602,7 +648,7 @@ func (k *Keeper) traceTx( + } + }() + +- res, err := k.ApplyMessageWithConfig(ctx, msg, tracer, commitMessage, cfg, txConfig) ++ res, err := k.ApplyMessageWithStateDB(ctx, msg, tracer, commitMessage, cfg, txConfig, stateDB) + if err != nil { + return nil, 0, status.Error(codes.Internal, err.Error()) + } +diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go +index ad90dba5f..894498b86 100644 +--- a/x/evm/keeper/state_transition.go ++++ b/x/evm/keeper/state_transition.go +@@ -316,6 +316,17 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, + commit bool, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, ++) (*types.MsgEthereumTxResponse, error) { ++ return k.ApplyMessageWithStateDB(ctx, msg, tracer, commit, cfg, txConfig, nil) ++} ++ ++func (k *Keeper) ApplyMessageWithStateDB(ctx sdk.Context, ++ msg core.Message, ++ tracer vm.EVMLogger, ++ commit bool, ++ cfg *statedb.EVMConfig, ++ txConfig statedb.TxConfig, ++ stateDB *statedb.StateDB, + ) (*types.MsgEthereumTxResponse, error) { + var ( + ret []byte // return bytes from evm execution +@@ -329,7 +340,9 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, + return nil, errorsmod.Wrap(types.ErrCallDisabled, "failed to call contract") + } + +- stateDB := statedb.New(ctx, k, txConfig) ++ if stateDB == nil { ++ stateDB = statedb.New(ctx, k, txConfig) ++ } + evm := k.NewEVM(ctx, msg, cfg, tracer, stateDB) + + leftoverGas := msg.Gas() +diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go +index 753ec7616..911a44e1f 100644 +--- a/x/evm/statedb/statedb.go ++++ b/x/evm/statedb/statedb.go +@@ -396,6 +396,16 @@ func (s *StateDB) AddAddressToAccessList(addr common.Address) { + } + } + ++// GetAddressToAccessList return full access list ++func (s *StateDB) GetAddressToAccessList() *accessList { ++ return s.accessList ++} ++ ++// SetAddressToAccessList overwrite with new access list ++func (s *StateDB) SetAddressToAccessList(accessList *accessList) { ++ s.accessList = accessList ++} ++ + // AddSlotToAccessList adds the given (address, slot)-tuple to the access list + func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + addrMod, slotMod := s.accessList.AddSlot(addr, slot) diff --git a/tests/integration_tests/configs/default.jsonnet b/tests/integration_tests/configs/default.jsonnet index a8425405ca..dbb82fadd0 100644 --- a/tests/integration_tests/configs/default.jsonnet +++ b/tests/integration_tests/configs/default.jsonnet @@ -19,7 +19,7 @@ 'feehistory-cap': 100, 'block-range-cap': 10000, 'logs-cap': 10000, - 'fix-revert-gas-refund-height': 1, + 'backup-grpc-address-block-range': '', //'{{"0.0.0.0:26113": [0, 20]}}', }, }, validators: [{ diff --git a/tests/integration_tests/network.py b/tests/integration_tests/network.py index c638e2a563..0e75e37c65 100644 --- a/tests/integration_tests/network.py +++ b/tests/integration_tests/network.py @@ -26,27 +26,26 @@ def __init__(self, base_dir, chain_binary=DEFAULT_CHAIN_BINARY): def copy(self): return Ethermint(self.base_dir) - @property def w3_http_endpoint(self, i=0): port = ports.evmrpc_port(self.base_port(i)) return f"http://localhost:{port}" - @property def w3_ws_endpoint(self, i=0): port = ports.evmrpc_ws_port(self.base_port(i)) return f"ws://localhost:{port}" @property - def w3(self, i=0): + def w3(self): if self._w3 is None: - if self._use_websockets: - self._w3 = web3.Web3( - web3.providers.WebsocketProvider(self.w3_ws_endpoint) - ) - else: - self._w3 = web3.Web3(web3.providers.HTTPProvider(self.w3_http_endpoint)) + self._w3 = self.node_w3(0) return self._w3 + def node_w3(self, i=0): + if self._use_websockets: + return web3.Web3(web3.providers.WebsocketProvider(self.w3_ws_endpoint(i))) + else: + return web3.Web3(web3.providers.HTTPProvider(self.w3_http_endpoint(i))) + def base_port(self, i): return self.config["validators"][i]["base_port"] diff --git a/tests/integration_tests/test_patch.py b/tests/integration_tests/test_patch.py new file mode 100644 index 0000000000..7c06d8bbb8 --- /dev/null +++ b/tests/integration_tests/test_patch.py @@ -0,0 +1,202 @@ +import configparser +import json +import re +import subprocess +from pathlib import Path + +import pytest +import requests +from pystarport import ports +from pystarport.cluster import SUPERVISOR_CONFIG_FILE + +from .network import setup_custom_ethermint +from .utils import ( + ADDRS, + CONTRACTS, + KEYS, + deploy_contract, + send_transaction, + sign_transaction, + supervisorctl, + wait_for_new_blocks, + wait_for_port, +) + + +def init_cosmovisor(home): + cosmovisor = home / "cosmovisor" + cosmovisor.mkdir() + (cosmovisor / "patched").symlink_to("../../../patched") + (cosmovisor / "genesis").symlink_to("./patched/genesis") + + +def post_init(path, base_port, config): + """ + prepare cosmovisor for each node + """ + chain_id = "ethermint_9000-1" + cfg = json.loads((path / chain_id / "config.json").read_text()) + for i, _ in enumerate(cfg["validators"]): + home = path / chain_id / f"node{i}" + init_cosmovisor(home) + + # patch supervisord ini config + ini_path = path / chain_id / SUPERVISOR_CONFIG_FILE + ini = configparser.RawConfigParser() + ini.read(ini_path) + reg = re.compile(rf"^program:{chain_id}-node(\d+)") + for section in ini.sections(): + m = reg.match(section) + if m: + i = m.group(1) + ini[section].update( + { + "command": f"cosmovisor start --home %(here)s/node{i}", + "environment": ( + f"DAEMON_NAME=ethermintd,DAEMON_HOME=%(here)s/node{i}" + ), + } + ) + with ini_path.open("w") as fp: + ini.write(fp) + + +@pytest.fixture(scope="module") +def custom_ethermint(tmp_path_factory): + path = tmp_path_factory.mktemp("patch") + cmd = [ + "nix-build", + Path(__file__).parent / "configs/cache-access-list-ethermintd.nix", + "-o", + path / "patched", + ] + print(*cmd) + subprocess.run(cmd, check=True) + # init with patch binary + yield from setup_custom_ethermint( + path, + 27000, + Path(__file__).parent / "configs/cosmovisor.jsonnet", + post_init=post_init, + chain_binary=str(path / "patched/genesis/bin/ethermintd"), + wait_port=True, + ) + + +def multi_transfer(w3, contract, sender, key, receiver): + amt = 100 + txhashes = [] + nonce = w3.eth.get_transaction_count(sender) + for i in range(2): + tx = contract.functions.transfer(sender, amt).build_transaction( + { + "from": receiver, + "nonce": nonce + i, + } + ) + signed = sign_transaction(w3, tx, key) + txhash = w3.eth.send_raw_transaction(signed.rawTransaction) + txhashes.append(txhash) + return txhashes + + +def test_patch(custom_ethermint): + cli = custom_ethermint.cosmos_cli() + w3 = custom_ethermint.w3 + validator = ADDRS["validator"] + community = ADDRS["community"] + contract, _ = deploy_contract(w3, CONTRACTS["TestERC20A"]) + amt = 3000 + # fund community + params = {"from": validator} + tx = contract.functions.transfer(community, amt).build_transaction(params) + receipt = send_transaction(w3, tx) + assert receipt.status == 1 + + sleep = 0.1 + target_height = wait_for_new_blocks(cli, 1, sleep) + txhashes = multi_transfer(w3, contract, validator, KEYS["validator"], community) + txhashes += multi_transfer(w3, contract, community, KEYS["community"], validator) + + for txhash in txhashes: + receipt = w3.eth.wait_for_transaction_receipt(txhash) + assert receipt.status == 1 + + nodes = [0, 1] + (wait_for_new_blocks(custom_ethermint.cosmos_cli(n), 1, sleep) for n in nodes) + + params = { + "method": "debug_traceTransaction", + "id": 1, + "jsonrpc": "2.0", + } + gas = 29506 + diff = 2000 + + def assert_debug_tx(i, all_eq=False): + results = [] + for txhash in txhashes: + params["params"] = [txhash.hex()] + rsp = requests.post(custom_ethermint.w3_http_endpoint(i), json=params) + assert rsp.status_code == 200 + result = rsp.json()["result"]["gas"] + results.append(result) + for i, result in enumerate(results): + if all_eq: + assert result == gas + else: + # costs less gas when cache access list + assert result == gas if i % 2 == 0 else result == gas - diff + + (assert_debug_tx(n) for n in nodes) + + base_dir = custom_ethermint.base_dir + for n in nodes: + supervisorctl(base_dir / "../tasks.ini", "stop", f"ethermint_9000-1-node{n}") + + procs = [] + + def append_proc(log, cmd): + with (base_dir / log).open("a") as logfile: + procs.append( + subprocess.Popen( + cmd, + stdout=logfile, + stderr=subprocess.STDOUT, + ) + ) + + path = Path(custom_ethermint.chain_binary).parent.parent.parent + grpc_port1 = ports.grpc_port(custom_ethermint.base_port(1)) + + for blk_end in [target_height - 1, target_height]: + try: + append_proc( + "node1.log", + [ + f"{str(path)}/genesis/bin/ethermintd", + "start", + "--home", + base_dir / "node1", + ], + ) + append_proc( + "node0.log", + [ + f"{str(path)}/integration-test-patch/bin/ethermintd", + "start", + "--json-rpc.backup-grpc-address-block-range", + f'{{"0.0.0.0:{grpc_port1}": [0, {blk_end}]}}', + "--home", + base_dir / "node0", + ], + ) + for n in nodes: + wait_for_port(ports.evmrpc_port(custom_ethermint.base_port(n))) + assert_debug_tx(0, blk_end != target_height) + assert_debug_tx(1) + finally: + for proc in procs: + proc.terminate() + proc.wait() + procs = [] diff --git a/testutil/network/util.go b/testutil/network/util.go index 03123cfbeb..709c397627 100644 --- a/testutil/network/util.go +++ b/testutil/network/util.go @@ -46,6 +46,7 @@ import ( "github.com/evmos/ethermint/server" evmtypes "github.com/evmos/ethermint/x/evm/types" + "google.golang.org/grpc" ) func startInProcess(cfg Config, val *Validator) error { @@ -145,12 +146,15 @@ func startInProcess(cfg Config, val *Validator) error { tmEndpoint := "/websocket" tmRPCAddr := val.RPCAddress - - val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, nil) + gprcClients := map[[2]int]*grpc.ClientConn{ + {0, 1}: val.ClientCtx.GRPCClient, + } + jsonrpc, jsonrpcDone, err := server.StartJSONRPC(val.Ctx, val.ClientCtx, gprcClients, tmRPCAddr, tmEndpoint, val.AppConfig, nil) if err != nil { return err } - + val.jsonrpc = jsonrpc + val.jsonrpcDone = jsonrpcDone address := fmt.Sprintf("http://%s", val.AppConfig.JSONRPC.Address) val.JSONRPCClient, err = ethclient.Dial(address)