From 83da4ad1f39940a444d6623cb5b1870460bf1450 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Mon, 6 May 2024 00:20:23 -0500 Subject: [PATCH] feat(eth-rpc): RPC backend, Ethereum tracer, KV indexer, and RPC APIs (#1861) * Squashed commit of the following: commit 045cf6d27f7718260dbd5b0af1a9817b135438bd Merge: db032103 ffcea960 Author: Unique-Divine Date: Sun May 5 15:21:16 2024 -0500 Merge branch 'main' into ud/evm-rpc commit db0321037450d60d9a136a30f569098f58c71af8 Author: Unique-Divine Date: Sun May 5 15:18:59 2024 -0500 test(eth-rpc): more tests commit 14f8a2c57c5d66cfea4af2f29b00fba48c398560 Author: Unique-Divine Date: Sun Apr 28 17:21:18 2024 -0500 test(eth): more tests commit 433780d6619ff5117d69737cf29f69f2b7670cfb Merge: 4f28f8f8 29e2acd0 Author: Unique-Divine Date: Sun Apr 28 14:53:34 2024 -0500 Merge branch 'main' into ud/evm-rpc commit 4f28f8f87f505cc4dd7a27096a949a14c044bd9b Author: Unique-Divine Date: Sun Apr 28 14:51:54 2024 -0500 test(eth): eip712 more tests commit bfac1e821190d01b5addf0406cb28b36cf774370 Author: Unique-Divine Date: Sun Apr 28 14:29:27 2024 -0500 refactor(eth): ethtypes -> eth commit b32d3852b02849114f627a4be8d6f077fd2259b6 Author: Unique-Divine Date: Sun Apr 28 14:24:26 2024 -0500 refactor(eth): evm/types -> evm commit a336983f355f3677670a5a7247558ba98bbc8ac9 Author: Unique-Divine Date: Sat Apr 27 15:58:05 2024 -0500 refactor(eth): rpc/types -> rpc commit c3a62155f8e0ef2cf071ebf9ed03c68aeaeadcae Merge: fe913dc6 31bb3bb6 Author: Unique-Divine Date: Sat Apr 27 13:13:37 2024 -0500 Merge branch 'ud/evm' into ud/evm-rpc commit 31bb3bb6a6698ca17952ad87029ca5a2d490d87d Merge: 1a050612 072de031 Author: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Sat Apr 27 20:12:14 2024 +0200 Merge branch 'main' into ud/evm commit fe913dc65d02e5640649f7fefc4b61f6c6336017 Author: Unique-Divine Date: Sat Apr 27 09:10:03 2024 -0500 test,refactor(eth): remove unnecessary nesting + more tests commit 372df12665567ad049ea57a008890ebdfbf9e6fc Author: Unique-Divine Date: Sat Apr 27 07:41:04 2024 -0500 ci: add CODECOV_TOKEN env var to secrets commit ac0701a0070537285fc54491d2db94e2926d232a Author: Unique-Divine Date: Sat Apr 27 07:26:57 2024 -0500 test(eth-rpc): more tests for types dir commit 176b6c62834222b25aa0e7e29c50f5f7e46d21dd Author: Unique-Divine Date: Fri Apr 26 20:54:04 2024 -0500 chore: linter commit 36730c53215c0923486dfbaa50aa298f4a3d12d4 Author: Unique-Divine Date: Fri Apr 26 20:48:58 2024 -0500 feat(eth-rpc): Conversion types and functions between Ethereum txs and blocks and Tendermint ones. commit 1a05061254b4d42102f97d24d13e99fa74787aa3 Author: Unique-Divine Date: Fri Apr 26 08:11:57 2024 +0200 feat(eth-pubsub): Implement in-memory EventBus for real-time topic management and event distribution * feat(eth-rpc): RPC backend, Ethereum tracer, KV indexer, and RPC APIs * fix(indexer): register eth and evm types during test setup --- .github/codecov.yml | 4 +- CHANGELOG.md | 1 + app/ante/fixed_gas_test.go | 20 +- app/app.go | 6 +- app/appconst/appconst.go | 44 + app/modules.go | 12 +- app/server/config/server_config.go | 459 +++++ cmd/nibid/cmd/root.go | 3 +- eth/assert_test.go | 6 +- eth/codec.go | 37 + eth/encoding/config_test.go | 6 +- eth/indexer.go | 20 + eth/indexer/kv_indexer.go | 231 +++ eth/indexer/kv_indexer_test.go | 199 ++ eth/rpc/backend/account_info.go | 223 +++ eth/rpc/backend/account_info_test.go | 412 +++++ eth/rpc/backend/backend.go | 178 ++ eth/rpc/backend/backend_suite_test.go | 201 ++ eth/rpc/backend/blocks.go | 515 ++++++ eth/rpc/backend/blocks_test.go | 1629 +++++++++++++++++ eth/rpc/backend/call_tx.go | 417 +++++ eth/rpc/backend/call_tx_test.go | 502 +++++ eth/rpc/backend/chain_info.go | 238 +++ eth/rpc/backend/chain_info_test.go | 350 ++++ eth/rpc/backend/client_test.go | 286 +++ eth/rpc/backend/evm_query_client_test.go | 324 ++++ eth/rpc/backend/filters.go | 37 + eth/rpc/backend/filters_test.go | 123 ++ eth/rpc/backend/mocks/client.go | 887 +++++++++ eth/rpc/backend/mocks/evm_query_client.go | 393 ++++ eth/rpc/backend/node_info.go | 341 ++++ eth/rpc/backend/node_info_test.go | 335 ++++ eth/rpc/backend/sign_tx.go | 156 ++ eth/rpc/backend/sign_tx_test.go | 270 +++ eth/rpc/backend/tracing.go | 211 +++ eth/rpc/backend/tracing_test.go | 265 +++ eth/rpc/backend/tx_info.go | 420 +++++ eth/rpc/backend/tx_info_test.go | 671 +++++++ eth/rpc/backend/utils.go | 302 +++ eth/rpc/backend/utils_test.go | 52 + eth/rpc/rpc.go | 18 +- eth/rpc/rpcapi/apis.go | 195 ++ eth/rpc/rpcapi/debugapi/api.go | 338 ++++ eth/rpc/rpcapi/debugapi/trace.go | 84 + eth/rpc/rpcapi/debugapi/trace_fallback.go | 36 + eth/rpc/rpcapi/debugapi/utils.go | 61 + eth/rpc/rpcapi/eth_api.go | 585 ++++++ eth/rpc/rpcapi/filtersapi/api.go | 646 +++++++ eth/rpc/rpcapi/filtersapi/filter_system.go | 311 ++++ .../rpcapi/filtersapi/filter_system_test.go | 73 + eth/rpc/rpcapi/filtersapi/filters.go | 268 +++ eth/rpc/rpcapi/filtersapi/subscription.go | 63 + eth/rpc/rpcapi/filtersapi/utils.go | 107 ++ eth/rpc/rpcapi/miner_api.go | 94 + eth/rpc/rpcapi/net_api.go | 60 + eth/rpc/rpcapi/personal_api.go | 207 +++ eth/rpc/rpcapi/txpool_api.go | 54 + eth/rpc/rpcapi/web3_api.go | 27 + eth/rpc/rpcapi/websockets.go | 704 +++++++ eth/rpc/types.go | 4 +- go.mod | 6 +- go.sum | 15 + x/common/testutil/sample.go | 19 - x/common/testutil/testapp/testapp.go | 3 +- x/evm/evmtest/eth.go | 20 +- x/{common/testutil => evm/evmtest}/signer.go | 2 +- x/evm/json_tx_args.go | 249 +++ x/evm/json_tx_args_test.go | 290 +++ x/evm/logs.go | 128 ++ x/evm/logs_test.go | 201 ++ x/evm/msg.go | 59 + x/evm/msg_test.go | 70 +- x/evm/params.go | 5 +- x/evm/tx_test.go | 4 +- x/evm/vmtracer.go | 111 ++ .../cli/gen_pricefeeder_delegation_test.go | 3 +- 76 files changed, 15834 insertions(+), 72 deletions(-) create mode 100644 app/appconst/appconst.go create mode 100644 app/server/config/server_config.go create mode 100644 eth/indexer.go create mode 100644 eth/indexer/kv_indexer.go create mode 100644 eth/indexer/kv_indexer_test.go create mode 100644 eth/rpc/backend/account_info.go create mode 100644 eth/rpc/backend/account_info_test.go create mode 100644 eth/rpc/backend/backend.go create mode 100644 eth/rpc/backend/backend_suite_test.go create mode 100644 eth/rpc/backend/blocks.go create mode 100644 eth/rpc/backend/blocks_test.go create mode 100644 eth/rpc/backend/call_tx.go create mode 100644 eth/rpc/backend/call_tx_test.go create mode 100644 eth/rpc/backend/chain_info.go create mode 100644 eth/rpc/backend/chain_info_test.go create mode 100644 eth/rpc/backend/client_test.go create mode 100644 eth/rpc/backend/evm_query_client_test.go create mode 100644 eth/rpc/backend/filters.go create mode 100644 eth/rpc/backend/filters_test.go create mode 100644 eth/rpc/backend/mocks/client.go create mode 100644 eth/rpc/backend/mocks/evm_query_client.go create mode 100644 eth/rpc/backend/node_info.go create mode 100644 eth/rpc/backend/node_info_test.go create mode 100644 eth/rpc/backend/sign_tx.go create mode 100644 eth/rpc/backend/sign_tx_test.go create mode 100644 eth/rpc/backend/tracing.go create mode 100644 eth/rpc/backend/tracing_test.go create mode 100644 eth/rpc/backend/tx_info.go create mode 100644 eth/rpc/backend/tx_info_test.go create mode 100644 eth/rpc/backend/utils.go create mode 100644 eth/rpc/backend/utils_test.go create mode 100644 eth/rpc/rpcapi/apis.go create mode 100644 eth/rpc/rpcapi/debugapi/api.go create mode 100644 eth/rpc/rpcapi/debugapi/trace.go create mode 100644 eth/rpc/rpcapi/debugapi/trace_fallback.go create mode 100644 eth/rpc/rpcapi/debugapi/utils.go create mode 100644 eth/rpc/rpcapi/eth_api.go create mode 100644 eth/rpc/rpcapi/filtersapi/api.go create mode 100644 eth/rpc/rpcapi/filtersapi/filter_system.go create mode 100644 eth/rpc/rpcapi/filtersapi/filter_system_test.go create mode 100644 eth/rpc/rpcapi/filtersapi/filters.go create mode 100644 eth/rpc/rpcapi/filtersapi/subscription.go create mode 100644 eth/rpc/rpcapi/filtersapi/utils.go create mode 100644 eth/rpc/rpcapi/miner_api.go create mode 100644 eth/rpc/rpcapi/net_api.go create mode 100644 eth/rpc/rpcapi/personal_api.go create mode 100644 eth/rpc/rpcapi/txpool_api.go create mode 100644 eth/rpc/rpcapi/web3_api.go create mode 100644 eth/rpc/rpcapi/websockets.go rename x/{common/testutil => evm/evmtest}/signer.go (98%) create mode 100644 x/evm/json_tx_args.go create mode 100644 x/evm/json_tx_args_test.go create mode 100644 x/evm/logs.go create mode 100644 x/evm/logs_test.go create mode 100644 x/evm/vmtracer.go diff --git a/.github/codecov.yml b/.github/codecov.yml index 29d4eb759..8e55c3df0 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -21,14 +21,14 @@ coverage: status: project: default: - if_not_found: success target: 66% + if_not_found: success threshold: 1% # Allow coverage to drop by X%, posting a success status. # removed_code_behavior: Takes values [off, removals_only, adjust_base] removed_code_behavior: adjust_base patch: default: - target: 69% + target: 0% comment: # this is a top-level key layout: " diff, flags, files" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd46d909..15f2e74fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1841](https://github.com/NibiruChain/nibiru/pull/1841) - feat(eth): Collections encoders for bytes, Ethereum addresses, and Ethereum hashes - [#1855](https://github.com/NibiruChain/nibiru/pull/1855) - feat(eth-pubsub): Implement in-memory EventBus for real-time topic management and event distribution - [#1856](https://github.com/NibiruChain/nibiru/pull/1856) - feat(eth-rpc): Conversion types and functions between Ethereum txs and blocks and Tendermint ones. +- [#1861](https://github.com/NibiruChain/nibiru/pull/1861) - feat(eth-rpc): RPC backend, Ethereum tracer, KV indexer, and RPC APIs #### Dapp modules: perp, spot, oracle, etc diff --git a/app/ante/fixed_gas_test.go b/app/ante/fixed_gas_test.go index 30d9f0bef..a8ea5c2a6 100644 --- a/app/ante/fixed_gas_test.go +++ b/app/ante/fixed_gas_test.go @@ -12,8 +12,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/app/ante" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/common/testutil/testapp" oracletypes "github.com/NibiruChain/nibiru/x/oracle/types" @@ -64,7 +64,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, }, expectedGas: 1042, @@ -76,7 +76,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, &oracletypes.MsgAggregateExchangeRatePrevote{ Hash: "", @@ -99,7 +99,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, }, expectedGas: 1042, @@ -111,7 +111,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, &oracletypes.MsgAggregateExchangeRateVote{ Salt: "dummySalt", @@ -171,7 +171,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, &oracletypes.MsgAggregateExchangeRatePrevote{ Hash: "", @@ -188,12 +188,12 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 100)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 100)), }, &types.MsgSend{ FromAddress: addr.String(), ToAddress: addr.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 200)), + Amount: sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 200)), }, }, expectedGas: 62288, @@ -208,7 +208,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder() // msg and signatures - feeAmount := sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 150)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 150)) gasLimit := testdata.NewTestGasLimit() suite.txBuilder.SetFeeAmount(feeAmount) suite.txBuilder.SetGasLimit(gasLimit) @@ -224,7 +224,7 @@ func (suite *AnteTestSuite) TestOraclePostPriceTransactionsHaveFixedPrice() { err = testapp.FundAccount( suite.app.BankKeeper, suite.ctx, addr, - sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 1000)), + sdk.NewCoins(sdk.NewInt64Coin(appconst.BondDenom, 1000)), ) suite.Require().NoError(err) diff --git a/app/app.go b/app/app.go index 2f8de8dff..f580830f7 100644 --- a/app/app.go +++ b/app/app.go @@ -49,10 +49,8 @@ import ( ) const ( - AccountAddressPrefix = "nibi" - appName = "Nibiru" - BondDenom = "unibi" - DisplayDenom = "NIBI" + appName = "Nibiru" + DisplayDenom = "NIBI" ) var ( diff --git a/app/appconst/appconst.go b/app/appconst/appconst.go new file mode 100644 index 000000000..9e8810da4 --- /dev/null +++ b/app/appconst/appconst.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package appconst + +import ( + "fmt" + "runtime" +) + +const ( + BinaryName = "nibiru" + BondDenom = "unibi" + // AccountAddressPrefix: Bech32 prefix for Nibiru accounts. + AccountAddressPrefix = "nibi" +) + +// Runtime version vars +var ( + AppVersion = "" + GitCommit = "" + BuildDate = "" + + GoVersion = "" + GoArch = "" +) + +func init() { + if len(AppVersion) == 0 { + AppVersion = "dev" + } + + GoVersion = runtime.Version() + GoArch = runtime.GOARCH +} + +func Version() string { + return fmt.Sprintf( + "Version %s (%s)\nCompiled at %s using Go %s (%s)", + AppVersion, + GitCommit, + BuildDate, + GoVersion, + GoArch, + ) +} diff --git a/app/modules.go b/app/modules.go index c64155dad..933246cb5 100644 --- a/app/modules.go +++ b/app/modules.go @@ -17,6 +17,8 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" "github.com/cosmos/cosmos-sdk/x/staking" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/NibiruChain/nibiru/app/appconst" ) // BankModule defines a custom wrapper around the x/bank module's AppModuleBasic @@ -29,13 +31,13 @@ type BankModule struct { func (BankModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { denomMetadata := banktypes.Metadata{ Description: "The native staking token of the Nibiru network.", - Base: BondDenom, + Base: appconst.BondDenom, Name: DisplayDenom, Display: DisplayDenom, Symbol: DisplayDenom, DenomUnits: []*banktypes.DenomUnit{ { - Denom: BondDenom, + Denom: appconst.BondDenom, Exponent: 0, Aliases: []string{ "micronibi", @@ -65,7 +67,7 @@ var _ module.HasGenesisBasics = (*StakingModule)(nil) // DefaultGenesis returns custom Nibiru x/staking module genesis state. func (StakingModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { genState := stakingtypes.DefaultGenesisState() - genState.Params.BondDenom = BondDenom + genState.Params.BondDenom = appconst.BondDenom genState.Params.MinCommissionRate = sdk.MustNewDecFromStr("0.05") return cdc.MustMarshalJSON(genState) } @@ -99,7 +101,7 @@ type CrisisModule struct { // DefaultGenesis returns custom Nibiru x/crisis module genesis state. func (CrisisModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { genState := crisistypes.DefaultGenesisState() - genState.ConstantFee = sdk.NewCoin(BondDenom, genState.ConstantFee.Amount) + genState.ConstantFee = sdk.NewCoin(appconst.BondDenom, genState.ConstantFee.Amount) return cdc.MustMarshalJSON(genState) } @@ -113,7 +115,7 @@ type GovModule struct { func (GovModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { genState := govtypes.DefaultGenesisState() genState.Params.MinDeposit = sdk.NewCoins( - sdk.NewCoin(BondDenom, govtypes.DefaultMinDepositTokens)) + sdk.NewCoin(appconst.BondDenom, govtypes.DefaultMinDepositTokens)) return cdc.MustMarshalJSON(genState) } diff --git a/app/server/config/server_config.go b/app/server/config/server_config.go new file mode 100644 index 000000000..5b34d7a77 --- /dev/null +++ b/app/server/config/server_config.go @@ -0,0 +1,459 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +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" +) + +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 + + // 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 + + // 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 +) + +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"` +} + +// 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"` + // 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"` +} + +// 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. + if denom != "" { + customAppConfig.Config.MinGasPrices = "0" + denom + } + + customAppTemplate := config.DefaultConfigTemplate + 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(), + } +} + +// 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, + 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 +} + +// 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()) + } + + return c.Config.ValidateBasic() +} + +// DefaultConfigTemplate defines the configuration template for the EVM RPC configuration +const DefaultConfigTemplate = ` +############################################################################### +### 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 gRPC 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 }} + +# 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/cmd/nibid/cmd/root.go b/cmd/nibid/cmd/root.go index 25bb4a4f6..c739b9a71 100644 --- a/cmd/nibid/cmd/root.go +++ b/cmd/nibid/cmd/root.go @@ -5,6 +5,7 @@ import ( "io" "os" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/sudo/cli" dbm "github.com/cometbft/cometbft-db" @@ -35,7 +36,7 @@ import ( // main function. func NewRootCmd() (*cobra.Command, app.EncodingConfig) { encodingConfig := app.MakeEncodingConfig() - app.SetPrefixes(app.AccountAddressPrefix) + app.SetPrefixes(appconst.AccountAddressPrefix) initClientCtx := client.Context{}. WithCodec(encodingConfig.Marshaler). diff --git a/eth/assert_test.go b/eth/assert_test.go index 020c37197..792ef7213 100644 --- a/eth/assert_test.go +++ b/eth/assert_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/NibiruChain/nibiru/eth" - "github.com/NibiruChain/nibiru/x/common/testutil" + "github.com/NibiruChain/nibiru/x/evm/evmtest" ) func TestIsEmptyHash(t *testing.T) { @@ -72,7 +72,7 @@ func TestValidateAddress(t *testing.T) { "zero address", common.Address{}.String(), false, }, { - "valid address", testutil.NewEthAddr().Hex(), false, + "valid address", evmtest.NewEthAddr().Hex(), false, }, } @@ -103,7 +103,7 @@ func TestValidateNonZeroAddress(t *testing.T) { "zero address", common.Address{}.String(), true, }, { - "valid address", testutil.NewEthAddr().Hex(), false, + "valid address", evmtest.NewEthAddr().Hex(), false, }, } diff --git a/eth/codec.go b/eth/codec.go index 09c43b661..2a45817a6 100644 --- a/eth/codec.go +++ b/eth/codec.go @@ -2,9 +2,33 @@ package eth import ( + fmt "fmt" + "math/big" + "strings" + + sdkmath "cosmossdk.io/math" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdktx "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/NibiruChain/nibiru/app/appconst" +) + +const ( + EthBaseDenom = appconst.BondDenom + // EIP155ChainID_Testnet: Chain ID for a testnet Nibiru following the + // format proposed by Vitalik in EIP155. + EIP155ChainID_Testnet = "nibirutest_420" + + DefaultGasPrice = 20 + + // ProtocolVersion: Latest supported version of the Ethereum protocol. + // Matches the message types and expected APIs. + // As of April, 2024, the highest protocol version on Ethereum mainnet is + // "eth/68". + // See https://github.com/ethereum/devp2p/blob/master/caps/eth.md#change-log + // for the historical summary of each version. + ProtocolVersion = 65 ) // RegisterInterfaces registers the tendermint concrete client-related @@ -37,3 +61,16 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { &ExtensionOptionDynamicFeeTx{}, ) } + +func ParseEIP155ChainIDNumber(chainId string) *big.Int { + parts := strings.Split(chainId, "_") + int, ok := sdkmath.NewIntFromString(parts[len(parts)-1]) + if !ok { + err := fmt.Errorf( + "failed to parse EIP155 chain ID number from chain ID: \"%s\"", + chainId, + ) + panic(err) + } + return int.BigInt() +} diff --git a/eth/encoding/config_test.go b/eth/encoding/config_test.go index 94982cd09..40f58901c 100644 --- a/eth/encoding/config_test.go +++ b/eth/encoding/config_test.go @@ -10,13 +10,13 @@ import ( "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/eth/encoding" - "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" ) func TestTxEncoding(t *testing.T) { - addr, key := testutil.PrivKeyEth() - signer := testutil.NewSigner(key) + addr, key := evmtest.PrivKeyEth() + signer := evmtest.NewSigner(key) ethTxParams := evm.EvmTxArgs{ ChainID: big.NewInt(1), diff --git a/eth/indexer.go b/eth/indexer.go new file mode 100644 index 000000000..bf2dda109 --- /dev/null +++ b/eth/indexer.go @@ -0,0 +1,20 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package eth + +import ( + abci "github.com/cometbft/cometbft/abci/types" + tmtypes "github.com/cometbft/cometbft/types" + "github.com/ethereum/go-ethereum/common" +) + +// EVMTxIndexer defines the interface of custom eth tx indexer. +type EVMTxIndexer interface { + // LastIndexedBlock returns -1 if indexer db is empty + LastIndexedBlock() (int64, error) + IndexBlock(*tmtypes.Block, []*abci.ResponseDeliverTx) error + + // GetByTxHash returns nil if tx not found. + GetByTxHash(common.Hash) (*TxResult, error) + // GetByBlockAndIndex returns nil if tx not found. + GetByBlockAndIndex(int64, int32) (*TxResult, error) +} diff --git a/eth/indexer/kv_indexer.go b/eth/indexer/kv_indexer.go new file mode 100644 index 000000000..cb3054aef --- /dev/null +++ b/eth/indexer/kv_indexer.go @@ -0,0 +1,231 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package indexer + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" + tmtypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +const ( + KeyPrefixTxHash = 1 + KeyPrefixTxIndex = 2 + + // TxIndexKeyLength is the length of tx-index key + TxIndexKeyLength = 1 + 8 + 8 +) + +var _ eth.EVMTxIndexer = &KVIndexer{} + +// KVIndexer implements a eth tx indexer on a KV db. +type KVIndexer struct { + db dbm.DB + logger log.Logger + clientCtx client.Context +} + +// NewKVIndexer creates the KVIndexer +func NewKVIndexer(db dbm.DB, logger log.Logger, clientCtx client.Context) *KVIndexer { + return &KVIndexer{db, logger, clientCtx} +} + +// IndexBlock index all the eth txs in a block through the following steps: +// - Iterates over all of the Txs in Block +// - Parses eth Tx infos from cosmos-sdk events for every TxResult +// - Iterates over all the messages of the Tx +// - Builds and stores a indexer.TxResult based on parsed events for every message +func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.ResponseDeliverTx) error { + height := block.Header.Height + + batch := kv.db.NewBatch() + defer batch.Close() + + // record index of valid eth tx during the iteration + var ethTxIndex int32 + for txIndex, tx := range block.Txs { + result := txResults[txIndex] + if !rpc.TxSuccessOrExpectedFailure(result) { + continue + } + + tx, err := kv.clientCtx.TxConfig.TxDecoder()(tx) + if err != nil { + kv.logger.Error("Fail to decode tx", "err", err, "block", height, "txIndex", txIndex) + continue + } + + if !isEthTx(tx) { + continue + } + + txs, err := rpc.ParseTxResult(result, tx) + if err != nil { + kv.logger.Error("Fail to parse event", "err", err, "block", height, "txIndex", txIndex) + continue + } + + var cumulativeGasUsed uint64 + for msgIndex, msg := range tx.GetMsgs() { + ethMsg := msg.(*evm.MsgEthereumTx) + txHash := common.HexToHash(ethMsg.Hash) + + txResult := eth.TxResult{ + Height: height, + TxIndex: uint32(txIndex), + MsgIndex: uint32(msgIndex), + EthTxIndex: ethTxIndex, + } + if result.Code != abci.CodeTypeOK { + // exceeds block gas limit scenario, set gas used to gas limit because that's what's charged by ante handler. + // some old versions don't emit any events, so workaround here directly. + txResult.GasUsed = ethMsg.GetGas() + txResult.Failed = true + } else { + parsedTx := txs.GetTxByMsgIndex(msgIndex) + if parsedTx == nil { + kv.logger.Error("msg index not found in events", "msgIndex", msgIndex) + continue + } + if parsedTx.EthTxIndex >= 0 && parsedTx.EthTxIndex != ethTxIndex { + kv.logger.Error("eth tx index don't match", "expect", ethTxIndex, "found", parsedTx.EthTxIndex) + } + txResult.GasUsed = parsedTx.GasUsed + txResult.Failed = parsedTx.Failed + } + + cumulativeGasUsed += txResult.GasUsed + txResult.CumulativeGasUsed = cumulativeGasUsed + ethTxIndex++ + + if err := saveTxResult(kv.clientCtx.Codec, batch, txHash, &txResult); err != nil { + return errorsmod.Wrapf(err, "IndexBlock %d", height) + } + } + } + if err := batch.Write(); err != nil { + return errorsmod.Wrapf(err, "IndexBlock %d, write batch", block.Height) + } + return nil +} + +// LastIndexedBlock returns the latest indexed block number, returns -1 if db is empty +func (kv *KVIndexer) LastIndexedBlock() (int64, error) { + return LoadLastBlock(kv.db) +} + +// FirstIndexedBlock returns the first indexed block number, returns -1 if db is empty +func (kv *KVIndexer) FirstIndexedBlock() (int64, error) { + return LoadFirstBlock(kv.db) +} + +// GetByTxHash finds eth tx by eth tx hash +func (kv *KVIndexer) GetByTxHash(hash common.Hash) (*eth.TxResult, error) { + bz, err := kv.db.Get(TxHashKey(hash)) + if err != nil { + return nil, errorsmod.Wrapf(err, "GetByTxHash %s", hash.Hex()) + } + if len(bz) == 0 { + return nil, fmt.Errorf("tx not found, hash: %s", hash.Hex()) + } + var txKey eth.TxResult + if err := kv.clientCtx.Codec.Unmarshal(bz, &txKey); err != nil { + return nil, errorsmod.Wrapf(err, "GetByTxHash %s", hash.Hex()) + } + return &txKey, nil +} + +// GetByBlockAndIndex finds eth tx by block number and eth tx index +func (kv *KVIndexer) GetByBlockAndIndex(blockNumber int64, txIndex int32) (*eth.TxResult, error) { + bz, err := kv.db.Get(TxIndexKey(blockNumber, txIndex)) + if err != nil { + return nil, errorsmod.Wrapf(err, "GetByBlockAndIndex %d %d", blockNumber, txIndex) + } + if len(bz) == 0 { + return nil, fmt.Errorf("tx not found, block: %d, eth-index: %d", blockNumber, txIndex) + } + return kv.GetByTxHash(common.BytesToHash(bz)) +} + +// TxHashKey returns the key for db entry: `tx hash -> tx result struct` +func TxHashKey(hash common.Hash) []byte { + return append([]byte{KeyPrefixTxHash}, hash.Bytes()...) +} + +// TxIndexKey returns the key for db entry: `(block number, tx index) -> tx hash` +func TxIndexKey(blockNumber int64, txIndex int32) []byte { + bz1 := sdk.Uint64ToBigEndian(uint64(blockNumber)) + bz2 := sdk.Uint64ToBigEndian(uint64(txIndex)) + return append(append([]byte{KeyPrefixTxIndex}, bz1...), bz2...) +} + +// LoadLastBlock returns the latest indexed block number, returns -1 if db is empty +func LoadLastBlock(db dbm.DB) (int64, error) { + it, err := db.ReverseIterator([]byte{KeyPrefixTxIndex}, []byte{KeyPrefixTxIndex + 1}) + if err != nil { + return 0, errorsmod.Wrap(err, "LoadLastBlock") + } + defer it.Close() + if !it.Valid() { + return -1, nil + } + return parseBlockNumberFromKey(it.Key()) +} + +// LoadFirstBlock loads the first indexed block, returns -1 if db is empty +func LoadFirstBlock(db dbm.DB) (int64, error) { + it, err := db.Iterator([]byte{KeyPrefixTxIndex}, []byte{KeyPrefixTxIndex + 1}) + if err != nil { + return 0, errorsmod.Wrap(err, "LoadFirstBlock") + } + defer it.Close() + if !it.Valid() { + return -1, nil + } + return parseBlockNumberFromKey(it.Key()) +} + +// isEthTx check if the tx is an eth tx +func isEthTx(tx sdk.Tx) bool { + extTx, ok := tx.(authante.HasExtensionOptionsTx) + if !ok { + return false + } + opts := extTx.GetExtensionOptions() + if len(opts) != 1 || opts[0].GetTypeUrl() != "/eth.evm.v1.ExtensionOptionsEthereumTx" { + return false + } + return true +} + +// saveTxResult index the txResult into the kv db batch +func saveTxResult(codec codec.Codec, batch dbm.Batch, txHash common.Hash, txResult *eth.TxResult) error { + bz := codec.MustMarshal(txResult) + if err := batch.Set(TxHashKey(txHash), bz); err != nil { + return errorsmod.Wrap(err, "set tx-hash key") + } + if err := batch.Set(TxIndexKey(txResult.Height, txResult.EthTxIndex), txHash.Bytes()); err != nil { + return errorsmod.Wrap(err, "set tx-index key") + } + return nil +} + +func parseBlockNumberFromKey(key []byte) (int64, error) { + if len(key) != TxIndexKeyLength { + return 0, fmt.Errorf("wrong tx index key length, expect: %d, got: %d", TxIndexKeyLength, len(key)) + } + + return int64(sdk.BigEndianToUint64(key[1:9])), nil +} diff --git a/eth/indexer/kv_indexer_test.go b/eth/indexer/kv_indexer_test.go new file mode 100644 index 000000000..98033dff1 --- /dev/null +++ b/eth/indexer/kv_indexer_test.go @@ -0,0 +1,199 @@ +package indexer_test + +import ( + "math/big" + "testing" + + "cosmossdk.io/simapp/params" + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + tmlog "github.com/cometbft/cometbft/libs/log" + tmtypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + evmenc "github.com/NibiruChain/nibiru/eth/encoding" + "github.com/NibiruChain/nibiru/eth/indexer" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func TestKVIndexer(t *testing.T) { + priv, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + signer := evmtest.NewSigner(priv) + ethSigner := gethcore.LatestSignerForChainID(nil) + + to := common.BigToAddress(big.NewInt(1)) + ethTxParams := evm.EvmTxArgs{ + Nonce: 0, + To: &to, + Amount: big.NewInt(1000), + GasLimit: 21000, + } + tx := evm.NewTx(ðTxParams) + tx.From = from.Hex() + require.NoError(t, tx.Sign(ethSigner, signer)) + txHash := tx.AsTransaction().Hash() + + encCfg := MakeEncodingConfig() + eth.RegisterInterfaces(encCfg.InterfaceRegistry) + evm.RegisterInterfaces(encCfg.InterfaceRegistry) + clientCtx := client.Context{}. + WithTxConfig(encCfg.TxConfig). + WithCodec(encCfg.Codec) + + // build cosmos-sdk wrapper tx + tmTx, err := tx.BuildTx(clientCtx.TxConfig.NewTxBuilder(), eth.EthBaseDenom) + require.NoError(t, err) + txBz, err := clientCtx.TxConfig.TxEncoder()(tmTx) + require.NoError(t, err) + + // build an invalid wrapper tx + builder := clientCtx.TxConfig.NewTxBuilder() + require.NoError(t, builder.SetMsgs(tx)) + tmTx2 := builder.GetTx() + txBz2, err := clientCtx.TxConfig.TxEncoder()(tmTx2) + require.NoError(t, err) + + testCases := []struct { + name string + block *tmtypes.Block + blockResult []*abci.ResponseDeliverTx + expSuccess bool + }{ + { + "success, format 1", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + true, + }, + { + "success, format 2", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + }}, + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + true, + }, + { + "success, exceed block gas limit", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 11, + Log: "out of gas in location: block gas meter; gasWanted: 21000", + Events: []abci.Event{}, + }, + }, + true, + }, + { + "fail, failed eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 15, + Log: "nonce mismatch", + Events: []abci.Event{}, + }, + }, + false, + }, + { + "fail, invalid events", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{}, + }, + }, + false, + }, + { + "fail, not eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz2}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{}, + }, + }, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := dbm.NewMemDB() + idxer := indexer.NewKVIndexer(db, tmlog.NewNopLogger(), clientCtx) + + err = idxer.IndexBlock(tc.block, tc.blockResult) + require.NoError(t, err) + if !tc.expSuccess { + first, err := idxer.FirstIndexedBlock() + require.NoError(t, err) + require.Equal(t, int64(-1), first) + + last, err := idxer.LastIndexedBlock() + require.NoError(t, err) + require.Equal(t, int64(-1), last) + } else { + first, err := idxer.FirstIndexedBlock() + require.NoError(t, err) + require.Equal(t, tc.block.Header.Height, first) + + last, err := idxer.LastIndexedBlock() + require.NoError(t, err) + require.Equal(t, tc.block.Header.Height, last) + + res1, err := idxer.GetByTxHash(txHash) + require.NoError(t, err) + require.NotNil(t, res1) + res2, err := idxer.GetByBlockAndIndex(1, 0) + require.NoError(t, err) + require.Equal(t, res1, res2) + } + }) + } +} + +// MakeEncodingConfig creates the EncodingConfig +func MakeEncodingConfig() params.EncodingConfig { + return evmenc.MakeConfig(app.ModuleBasics) +} diff --git a/eth/rpc/backend/account_info.go b/eth/rpc/backend/account_info.go new file mode 100644 index 000000000..c4f09b608 --- /dev/null +++ b/eth/rpc/backend/account_info.go @@ -0,0 +1,223 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "fmt" + "math" + "math/big" + + errorsmod "cosmossdk.io/errors" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// GetCode returns the contract code at the given address and block number. +func (b *Backend) GetCode( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, +) (hexutil.Bytes, error) { + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evm.QueryCodeRequest{ + Address: address.String(), + } + + res, err := b.queryClient.Code(rpc.NewContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + return res.Code, nil +} + +// GetProof returns an account object with proof and any storage proofs +func (b *Backend) GetProof( + address common.Address, + storageKeys []string, + blockNrOrHash rpc.BlockNumberOrHash, +) (*rpc.AccountResult, error) { + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + + height := blockNum.Int64() + + _, err = b.TendermintBlockByNumber(blockNum) + if err != nil { + // the error message imitates geth behavior + return nil, errors.New("header not found") + } + ctx := rpc.NewContextWithHeight(height) + + // if the height is equal to zero, meaning the query condition of the block is either "pending" or "latest" + if height == 0 { + bn, err := b.BlockNumber() + if err != nil { + return nil, err + } + + if bn > math.MaxInt64 { + return nil, fmt.Errorf("not able to query block number greater than MaxInt64") + } + + height = int64(bn) //#nosec G701 -- checked for int overflow already + } + + clientCtx := b.clientCtx.WithHeight(height) + + // query storage proofs + storageProofs := make([]rpc.StorageResult, len(storageKeys)) + + for i, key := range storageKeys { + hexKey := common.HexToHash(key) + valueBz, proof, err := b.queryClient.GetProof(clientCtx, evm.StoreKey, evm.StateKey(address, hexKey.Bytes())) + if err != nil { + return nil, err + } + + storageProofs[i] = rpc.StorageResult{ + Key: key, + Value: (*hexutil.Big)(new(big.Int).SetBytes(valueBz)), + Proof: GetHexProofs(proof), + } + } + + // query EVM account + req := &evm.QueryAccountRequest{ + Address: address.String(), + } + + res, err := b.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) + if err != nil { + return nil, err + } + + balance, ok := sdkmath.NewIntFromString(res.Balance) + if !ok { + return nil, errors.New("invalid balance") + } + + return &rpc.AccountResult{ + Address: address, + AccountProof: GetHexProofs(proof), + Balance: (*hexutil.Big)(balance.BigInt()), + CodeHash: common.HexToHash(res.CodeHash), + Nonce: hexutil.Uint64(res.Nonce), + // NOTE: The StorageHash is blank. Consider whether this is useful in the + // future. Currently, all storage is handles by persistent and transient + // `sdk.KVStore` objects. + StorageHash: common.Hash{}, + StorageProof: storageProofs, + }, nil +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evm.QueryStorageRequest{ + Address: address.String(), + Key: key, + } + + res, err := b.queryClient.Storage(rpc.NewContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + value := common.HexToHash(res.Value) + return value.Bytes(), nil +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (b *Backend) GetBalance(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) { + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evm.QueryBalanceRequest{ + Address: address.String(), + } + + _, err = b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + res, err := b.queryClient.Balance(rpc.NewContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + val, ok := sdkmath.NewIntFromString(res.Balance) + if !ok { + return nil, errors.New("invalid balance") + } + + // balance can only be negative in case of pruned node + if val.IsNegative() { + return nil, errors.New("couldn't fetch balance. Node state is pruned") + } + + return (*hexutil.Big)(val.BigInt()), nil +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (b *Backend) GetTransactionCount(address common.Address, blockNum rpc.BlockNumber) (*hexutil.Uint64, error) { + n := hexutil.Uint64(0) + bn, err := b.BlockNumber() + if err != nil { + return &n, err + } + height := blockNum.Int64() + + currentHeight := int64(bn) //#nosec G701 -- checked for int overflow already + if height > currentHeight { + return &n, errorsmod.Wrapf( + sdkerrors.ErrInvalidHeight, + "cannot query with height in the future (current: %d, queried: %d); please provide a valid height", + currentHeight, height, + ) + } + // Get nonce (sequence) from account + from := sdk.AccAddress(address.Bytes()) + accRet := b.clientCtx.AccountRetriever + + err = accRet.EnsureExists(b.clientCtx, from) + if err != nil { + // account doesn't exist yet, return 0 + return &n, nil + } + + includePending := blockNum == rpc.EthPendingBlockNumber + nonce, err := b.getAccountNonce(address, includePending, blockNum.Int64(), b.logger) + if err != nil { + return nil, err + } + + n = hexutil.Uint64(nonce) + return &n, nil +} diff --git a/eth/rpc/backend/account_info_test.go b/eth/rpc/backend/account_info_test.go new file mode 100644 index 000000000..64531bdec --- /dev/null +++ b/eth/rpc/backend/account_info_test.go @@ -0,0 +1,412 @@ +package backend + +import ( + "fmt" + "math/big" + + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *BackendSuite) TestGetCode() { + blockNr := rpc.NewBlockNumber(big.NewInt(1)) + contractCode := []byte( + "0xef616c92f3cfc9e92dc270d6acff9cea213cecc7020a76ee4395af09bdceb4837a1ebdb5735e11e7d3adb6104e0c3ac55180b4ddf5e54d022cc5e8837f6a4f971b", + ) + + testCases := []struct { + name string + addr common.Address + blockNrOrHash rpc.BlockNumberOrHash + registerMock func(common.Address) + expPass bool + expCode hexutil.Bytes + }{ + { + "fail - BlockHash and BlockNumber are both nil ", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{}, + func(addr common.Address) {}, + false, + nil, + }, + { + "fail - query client errors on getting Code", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(addr common.Address) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterCodeError(queryClient, addr) + }, + false, + nil, + }, + { + "pass", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(addr common.Address) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterCode(queryClient, addr, contractCode) + }, + true, + contractCode, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset + tc.registerMock(tc.addr) + + code, err := s.backend.GetCode(tc.addr, tc.blockNrOrHash) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expCode, code) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetProof() { + blockNrInvalid := rpc.NewBlockNumber(big.NewInt(1)) + blockNr := rpc.NewBlockNumber(big.NewInt(4)) + address1 := evmtest.NewEthAddr() + + testCases := []struct { + name string + addr common.Address + storageKeys []string + blockNrOrHash rpc.BlockNumberOrHash + registerMock func(rpc.BlockNumber, common.Address) + expPass bool + expAccRes *rpc.AccountResult + }{ + { + "fail - BlockNumeber = 1 (invalidBlockNumber)", + address1, + []string{}, + rpc.BlockNumberOrHash{BlockNumber: &blockNrInvalid}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterAccount(queryClient, addr, blockNrInvalid.Int64()) + }, + false, + &rpc.AccountResult{}, + }, + { + "fail - Block doesn't exist", + address1, + []string{}, + rpc.BlockNumberOrHash{BlockNumber: &blockNrInvalid}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, bn.Int64()) + }, + false, + &rpc.AccountResult{}, + }, + { + "pass", + address1, + []string{"0x0"}, + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + s.backend.ctx = rpc.NewContextWithHeight(bn.Int64()) + + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterAccount(queryClient, addr, bn.Int64()) + + // Use the IAVL height if a valid tendermint height is passed in. + iavlHeight := bn.Int64() + RegisterABCIQueryWithOptions( + client, + bn.Int64(), + "store/evm/key", + evm.StateKey(address1, common.HexToHash("0x0").Bytes()), + tmrpcclient.ABCIQueryOptions{Height: iavlHeight, Prove: true}, + ) + RegisterABCIQueryWithOptions( + client, + bn.Int64(), + "store/acc/key", + authtypes.AddressStoreKey(sdk.AccAddress(address1.Bytes())), + tmrpcclient.ABCIQueryOptions{Height: iavlHeight, Prove: true}, + ) + }, + true, + &rpc.AccountResult{ + Address: address1, + AccountProof: []string{""}, + Balance: (*hexutil.Big)(big.NewInt(0)), + CodeHash: common.HexToHash(""), + Nonce: 0x0, + StorageHash: common.Hash{}, + StorageProof: []rpc.StorageResult{ + { + Key: "0x0", + Value: (*hexutil.Big)(big.NewInt(2)), + Proof: []string{""}, + }, + }, + }, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() + tc.registerMock(*tc.blockNrOrHash.BlockNumber, tc.addr) + + accRes, err := s.backend.GetProof(tc.addr, tc.storageKeys, tc.blockNrOrHash) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expAccRes, accRes) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetStorageAt() { + blockNr := rpc.NewBlockNumber(big.NewInt(1)) + + testCases := []struct { + name string + addr common.Address + key string + blockNrOrHash rpc.BlockNumberOrHash + registerMock func(common.Address, string, string) + expPass bool + expStorage hexutil.Bytes + }{ + { + "fail - BlockHash and BlockNumber are both nil", + evmtest.NewEthAddr(), + "0x0", + rpc.BlockNumberOrHash{}, + func(addr common.Address, key string, storage string) {}, + false, + nil, + }, + { + "fail - query client errors on getting Storage", + evmtest.NewEthAddr(), + "0x0", + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(addr common.Address, key string, storage string) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStorageAtError(queryClient, addr, key) + }, + false, + nil, + }, + { + "pass", + evmtest.NewEthAddr(), + "0x0", + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(addr common.Address, key string, storage string) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStorageAt(queryClient, addr, key, storage) + }, + true, + hexutil.Bytes{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() + tc.registerMock(tc.addr, tc.key, tc.expStorage.String()) + + storage, err := s.backend.GetStorageAt(tc.addr, tc.key, tc.blockNrOrHash) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expStorage, storage) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetEvmGasBalance() { + blockNr := rpc.NewBlockNumber(big.NewInt(1)) + + testCases := []struct { + name string + addr common.Address + blockNrOrHash rpc.BlockNumberOrHash + registerMock func(rpc.BlockNumber, common.Address) + expPass bool + expBalance *hexutil.Big + }{ + { + "fail - BlockHash and BlockNumber are both nil", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{}, + func(bn rpc.BlockNumber, addr common.Address) { + }, + false, + nil, + }, + { + "fail - tendermint client failed to get block", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, bn.Int64()) + }, + false, + nil, + }, + { + "fail - query client failed to get balance", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBalanceError(queryClient, addr, bn.Int64()) + }, + false, + nil, + }, + { + "fail - invalid balance", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBalanceInvalid(queryClient, addr, bn.Int64()) + }, + false, + nil, + }, + { + "fail - pruned node state", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBalanceNegative(queryClient, addr, bn.Int64()) + }, + false, + nil, + }, + { + "pass", + evmtest.NewEthAddr(), + rpc.BlockNumberOrHash{BlockNumber: &blockNr}, + func(bn rpc.BlockNumber, addr common.Address) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, bn.Int64(), nil) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBalance(queryClient, addr, bn.Int64()) + }, + true, + (*hexutil.Big)(big.NewInt(1)), + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() + + // avoid nil pointer reference + if tc.blockNrOrHash.BlockNumber != nil { + tc.registerMock(*tc.blockNrOrHash.BlockNumber, tc.addr) + } + + balance, err := s.backend.GetBalance(tc.addr, tc.blockNrOrHash) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expBalance, balance) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionCount() { + testCases := []struct { + name string + accExists bool + blockNum rpc.BlockNumber + registerMock func(common.Address, rpc.BlockNumber) + expPass bool + expTxCount hexutil.Uint64 + }{ + { + "pass - account doesn't exist", + false, + rpc.NewBlockNumber(big.NewInt(1)), + func(addr common.Address, bn rpc.BlockNumber) { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + }, + true, + hexutil.Uint64(0), + }, + { + "fail - block height is in the future", + false, + rpc.NewBlockNumber(big.NewInt(10000)), + func(addr common.Address, bn rpc.BlockNumber) { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + }, + false, + hexutil.Uint64(0), + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() + + addr := evmtest.NewEthAddr() + if tc.accExists { + addr = common.BytesToAddress(s.acc.Bytes()) + } + + tc.registerMock(addr, tc.blockNum) + + txCount, err := s.backend.GetTransactionCount(addr, tc.blockNum) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expTxCount, *txCount) + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go new file mode 100644 index 000000000..c6344efe6 --- /dev/null +++ b/eth/rpc/backend/backend.go @@ -0,0 +1,178 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "context" + "math/big" + "time" + + "github.com/cometbft/cometbft/libs/log" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + params "github.com/ethereum/go-ethereum/params" + gethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/NibiruChain/nibiru/app/server/config" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// BackendI implements the Cosmos and EVM backend. +type BackendI interface { //nolint: revive + CosmosBackend + EVMBackend +} + +// CosmosBackend: Currently unused. Backend functionality for the shared +// "cosmos" RPC namespace. Implements [BackendI] in combination with [EVMBackend]. +// TODO: feat(eth): Implement the cosmos JSON-RPC defined by Wallet Connect V2: +// https://docs.walletconnect.com/2.0/json-rpc/cosmos. +type CosmosBackend interface { + // TODO: GetAccounts() + // TODO: SignDirect() + // TODO: SignAmino() +} + +// EVMBackend implements the functionality shared within ethereum namespaces +// as defined by EIP-1474: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md +// Implemented by Backend. +type EVMBackend interface { + // Node specific queries + Accounts() ([]common.Address, error) + Syncing() (interface{}, error) + SetEtherbase(etherbase common.Address) bool + SetGasPrice(gasPrice hexutil.Big) bool + ImportRawKey(privkey, password string) (common.Address, error) + ListAccounts() ([]common.Address, error) + NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, error) + UnprotectedAllowed() bool + RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection + RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection + RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether. + RPCMinGasPrice() int64 + + // Sign Tx + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + SendTransaction(args evm.JsonTxArgs) (common.Hash, error) + SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) + + // Blocks Info + BlockNumber() (hexutil.Uint64, error) + GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpc.BlockNumber) *hexutil.Uint + TendermintBlockByNumber(blockNum rpc.BlockNumber) (*tmrpctypes.ResultBlock, error) + TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) + TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) + BlockNumberFromTendermint(blockNrOrHash rpc.BlockNumberOrHash) (rpc.BlockNumber, error) + BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) + EthMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evm.MsgEthereumTx + BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (gethcore.Bloom, error) + HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, error) + HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) + RPCBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool) (map[string]interface{}, error) + EthBlockByNumber(blockNum rpc.BlockNumber) (*gethcore.Block, error) + EthBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*gethcore.Block, error) + + // Account Info + GetCode(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) + GetBalance(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) + GetStorageAt(address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) + GetProof(address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*rpc.AccountResult, error) + GetTransactionCount(address common.Address, blockNum rpc.BlockNumber) (*hexutil.Uint64, error) + + // Chain Info + ChainID() (*hexutil.Big, error) + ChainConfig() *params.ChainConfig + // TODO: feat: Dynamic fees + // GlobalMinGasPrice() (math.LegacyDec, error) + BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) + CurrentHeader() (*gethcore.Header, error) + PendingTransactions() ([]*sdk.Tx, error) + GetCoinbase() (sdk.AccAddress, error) + FeeHistory(blockCount gethrpc.DecimalOrHex, lastBlock gethrpc.BlockNumber, rewardPercentiles []float64) (*rpc.FeeHistoryResult, error) + SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) + + // Tx Info + GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, error) + GetTxByEthHash(txHash common.Hash) (*eth.TxResult, error) + GetTxByTxIndex(height int64, txIndex uint) (*eth.TxResult, error) + GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) + GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) + + // Send Transaction + Resend(args evm.JsonTxArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SetTxDefaults(args evm.JsonTxArgs) (evm.JsonTxArgs, error) + EstimateGas(args evm.JsonTxArgs, blockNrOptional *rpc.BlockNumber) (hexutil.Uint64, error) + DoCall(args evm.JsonTxArgs, blockNr rpc.BlockNumber) (*evm.MsgEthereumTxResponse, error) + GasPrice() (*hexutil.Big, error) + + // Filter API + GetLogs(hash common.Hash) ([][]*gethcore.Log, error) + GetLogsByHeight(height *int64) ([][]*gethcore.Log, error) + BloomStatus() (uint64, uint64) + + // Tracing + TraceTransaction(hash common.Hash, config *evm.TraceConfig) (interface{}, error) + TraceBlock( + height rpc.BlockNumber, + config *evm.TraceConfig, + block *tmrpctypes.ResultBlock, + ) ([]*evm.TxTraceResult, error) +} + +var _ BackendI = (*Backend)(nil) + +// Backend implements the BackendI interface +type Backend struct { + ctx context.Context + clientCtx client.Context + queryClient *rpc.QueryClient // gRPC query client + logger log.Logger + chainID *big.Int + cfg config.Config + allowUnprotectedTxs bool + indexer eth.EVMTxIndexer +} + +// NewBackend creates a new Backend instance for cosmos and ethereum namespaces +func NewBackend( + ctx *server.Context, + logger log.Logger, + clientCtx client.Context, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, +) *Backend { + chainID, err := eth.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + + appConf, err := config.GetConfig(ctx.Viper) + if err != nil { + panic(err) + } + + return &Backend{ + ctx: context.Background(), + clientCtx: clientCtx, + queryClient: rpc.NewQueryClient(clientCtx), + logger: logger.With("module", "backend"), + chainID: chainID, + cfg: appConf, + allowUnprotectedTxs: allowUnprotectedTxs, + indexer: indexer, + } +} diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go new file mode 100644 index 000000000..e77d53c45 --- /dev/null +++ b/eth/rpc/backend/backend_suite_test.go @@ -0,0 +1,201 @@ +package backend + +import ( + "bufio" + "math/big" + "os" + "path/filepath" + "testing" + + dbm "github.com/cometbft/cometbft-db" + + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/crypto/hd" + "github.com/NibiruChain/nibiru/eth/encoding" + "github.com/NibiruChain/nibiru/eth/indexer" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +type BackendSuite struct { + suite.Suite + + backend *Backend + from common.Address + acc sdk.AccAddress + signer keyring.Signer +} + +func TestBackendSuite(t *testing.T) { + suite.Run(t, new(BackendSuite)) +} + +const ChainID = eth.EIP155ChainID_Testnet + "-1" + +// SetupTest is executed before every BackendTestSuite test +func (s *BackendSuite) SetupTest() { + ctx := server.NewDefaultContext() + ctx.Viper.Set("telemetry.global-labels", []interface{}{}) + + baseDir := s.T().TempDir() + nodeDirName := "node" + clientDir := filepath.Join(baseDir, nodeDirName, "nibirucli") + keyRing, err := s.generateTestKeyring(clientDir) + if err != nil { + panic(err) + } + + // Create Account with set sequence + s.acc = sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + accounts := map[string]client.TestAccount{} + accounts[s.acc.String()] = client.TestAccount{ + Address: s.acc, + Num: uint64(1), + Seq: uint64(1), + } + + from, priv := evmtest.PrivKeyEth() + s.from = from + s.signer = evmtest.NewSigner(priv) + s.Require().NoError(err) + + encCfg := encoding.MakeConfig(app.ModuleBasics) + evm.RegisterInterfaces(encCfg.InterfaceRegistry) + eth.RegisterInterfaces(encCfg.InterfaceRegistry) + clientCtx := client.Context{}.WithChainID(ChainID). + WithHeight(1). + WithTxConfig(encCfg.TxConfig). + WithKeyringDir(clientDir). + WithKeyring(keyRing). + WithAccountRetriever(client.TestAccountRetriever{Accounts: accounts}) + + allowUnprotectedTxs := false + idxer := indexer.NewKVIndexer(dbm.NewMemDB(), ctx.Logger, clientCtx) + + s.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, idxer) + s.backend.cfg.JSONRPC.GasCap = 0 + s.backend.cfg.JSONRPC.EVMTimeout = 0 + s.backend.queryClient.QueryClient = mocks.NewEVMQueryClient(s.T()) + s.backend.clientCtx.Client = mocks.NewClient(s.T()) + s.backend.ctx = rpc.NewContextWithHeight(1) + + s.backend.clientCtx.Codec = encCfg.Codec +} + +// buildEthereumTx returns an example legacy Ethereum transaction +func (s *BackendSuite) buildEthereumTx() (*evm.MsgEthereumTx, []byte) { + ethTxParams := evm.EvmTxArgs{ + ChainID: s.backend.chainID, + Nonce: uint64(0), + To: &common.Address{}, + Amount: big.NewInt(0), + GasLimit: 100000, + GasPrice: big.NewInt(1), + } + msgEthereumTx := evm.NewTx(ðTxParams) + + // A valid msg should have empty `From` + msgEthereumTx.From = s.from.Hex() + + txBuilder := s.backend.clientCtx.TxConfig.NewTxBuilder() + err := txBuilder.SetMsgs(msgEthereumTx) + s.Require().NoError(err) + + bz, err := s.backend.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + s.Require().NoError(err) + return msgEthereumTx, bz +} + +// buildFormattedBlock returns a formatted block for testing +func (s *BackendSuite) buildFormattedBlock( + blockRes *tmrpctypes.ResultBlockResults, + resBlock *tmrpctypes.ResultBlock, + fullTx bool, + tx *evm.MsgEthereumTx, + validator sdk.AccAddress, + baseFee *big.Int, +) map[string]interface{} { + header := resBlock.Block.Header + gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) + gasUsed := new(big.Int).SetUint64(uint64(blockRes.TxsResults[0].GasUsed)) + + root := common.Hash{}.Bytes() + receipt := gethcore.NewReceipt(root, false, gasUsed.Uint64()) + bloom := gethcore.CreateBloom(gethcore.Receipts{receipt}) + + ethRPCTxs := []interface{}{} + if tx != nil { + if fullTx { + rpcTx, err := rpc.NewRPCTxFromEthTx( + tx.AsTransaction(), + common.BytesToHash(header.Hash()), + uint64(header.Height), + uint64(0), + baseFee, + s.backend.chainID, + ) + s.Require().NoError(err) + ethRPCTxs = []interface{}{rpcTx} + } else { + ethRPCTxs = []interface{}{common.HexToHash(tx.Hash)} + } + } + + return rpc.FormatBlock( + header, + resBlock.Block.Size(), + gasLimit, + gasUsed, + ethRPCTxs, + bloom, + common.BytesToAddress(validator.Bytes()), + baseFee, + ) +} + +func (s *BackendSuite) generateTestKeyring(clientDir string) (keyring.Keyring, error) { + buf := bufio.NewReader(os.Stdin) + encCfg := encoding.MakeConfig(app.ModuleBasics) + return keyring.New( + sdk.KeyringServiceName(), // appName + keyring.BackendTest, // backend + clientDir, // rootDir + buf, // userInput + encCfg.Codec, // codec + []keyring.Option{hd.EthSecp256k1Option()}..., + ) +} + +func (s *BackendSuite) signAndEncodeEthTx(msgEthereumTx *evm.MsgEthereumTx) []byte { + from, priv := evmtest.PrivKeyEth() + signer := evmtest.NewSigner(priv) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + + ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) + msgEthereumTx.From = from.String() + err := msgEthereumTx.Sign(ethSigner, signer) + s.Require().NoError(err) + + tx, err := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), eth.EthBaseDenom) + s.Require().NoError(err) + + txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() + txBz, err := txEncoder(tx) + s.Require().NoError(err) + + return txBz +} diff --git a/eth/rpc/backend/blocks.go b/eth/rpc/backend/blocks.go new file mode 100644 index 000000000..1e5f1fc3e --- /dev/null +++ b/eth/rpc/backend/blocks.go @@ -0,0 +1,515 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "fmt" + "math" + "math/big" + "strconv" + + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// BlockNumber returns the current block number in abci app state. Because abci +// app state could lag behind from tendermint latest block, it's more stable for +// the client to use the latest block number in abci app state than tendermint +// rpc. +func (b *Backend) BlockNumber() (hexutil.Uint64, error) { + // do any grpc query, ignore the response and use the returned block height + var header metadata.MD + _, err := b.queryClient.Params(b.ctx, &evm.QueryParamsRequest{}, grpc.Header(&header)) + if err != nil { + return hexutil.Uint64(0), err + } + + blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) + if headerLen := len(blockHeightHeader); headerLen != 1 { + return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1) + } + + height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse block height: %w", err) + } + + if height > math.MaxInt64 { + return 0, fmt.Errorf("block height %d is greater than max uint64", height) + } + + return hexutil.Uint64(height), nil +} + +// GetBlockByNumber returns the JSON-RPC compatible Ethereum block identified by +// block number. Depending on fullTx it either returns the full transaction +// objects or if false only the hashes of the transactions. +func (b *Backend) GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, nil + } + + // return if requested block height is greater than the current one + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + b.logger.Debug("failed to fetch block result from Tendermint", "height", blockNum, "error", err.Error()) + return nil, nil + } + + res, err := b.RPCBlockFromTendermintBlock(resBlock, blockRes, fullTx) + if err != nil { + b.logger.Debug("GetEthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) + return nil, err + } + + return res, nil +} + +// GetBlockByHash returns the JSON-RPC compatible Ethereum block identified by +// hash. +func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + resBlock, err := b.TendermintBlockByHash(hash) + if err != nil { + return nil, err + } + + if resBlock == nil { + // block not found + return nil, nil + } + + blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + b.logger.Debug("failed to fetch block result from Tendermint", "block-hash", hash.String(), "error", err.Error()) + return nil, nil + } + + res, err := b.RPCBlockFromTendermintBlock(resBlock, blockRes, fullTx) + if err != nil { + b.logger.Debug("GetEthBlockFromTendermint failed", "hash", hash, "error", err.Error()) + return nil, err + } + + return res, nil +} + +// GetBlockTransactionCountByHash returns the number of Ethereum transactions in +// the block identified by hash. +func (b *Backend) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) + if !ok { + b.logger.Error("invalid rpc client") + } + + block, err := sc.BlockByHash(b.ctx, hash.Bytes()) + if err != nil { + b.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) + return nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "hash", hash.Hex()) + return nil + } + + return b.GetBlockTransactionCount(block) +} + +// GetBlockTransactionCountByNumber returns the number of Ethereum transactions +// in the block identified by number. +func (b *Backend) GetBlockTransactionCountByNumber(blockNum rpc.BlockNumber) *hexutil.Uint { + block, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + b.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) + return nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "height", blockNum.Int64()) + return nil + } + + return b.GetBlockTransactionCount(block) +} + +// GetBlockTransactionCount returns the number of Ethereum transactions in a +// given block. +func (b *Backend) GetBlockTransactionCount(block *tmrpctypes.ResultBlock) *hexutil.Uint { + blockRes, err := b.TendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + return nil + } + + ethMsgs := b.EthMsgsFromTendermintBlock(block, blockRes) + n := hexutil.Uint(len(ethMsgs)) + return &n +} + +// TendermintBlockByNumber returns a Tendermint-formatted block for a given +// block number +func (b *Backend) TendermintBlockByNumber(blockNum rpc.BlockNumber) (*tmrpctypes.ResultBlock, error) { + height := blockNum.Int64() + if height <= 0 { + // fetch the latest block number from the app state, more accurate than the tendermint block store state. + n, err := b.BlockNumber() + if err != nil { + return nil, err + } + height = int64(n) //#nosec G701 -- checked for int overflow already + } + resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + if err != nil { + b.logger.Debug("tendermint client failed to get block", "height", height, "error", err.Error()) + return nil, err + } + + if resBlock.Block == nil { + b.logger.Debug("TendermintBlockByNumber block not found", "height", height) + return nil, nil + } + + return resBlock, nil +} + +// TendermintBlockResultByNumber returns a Tendermint-formatted block result +// by block number +func (b *Backend) TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) { + sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + return sc.BlockResults(b.ctx, height) +} + +// TendermintBlockByHash returns a Tendermint-formatted block by block number +func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { + sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + resBlock, err := sc.BlockByHash(b.ctx, blockHash.Bytes()) + if err != nil { + b.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error()) + return nil, err + } + + if resBlock == nil || resBlock.Block == nil { + b.logger.Debug("TendermintBlockByHash block not found", "blockHash", blockHash.Hex()) + return nil, nil + } + + return resBlock, nil +} + +// BlockNumberFromTendermint returns the BlockNumber from BlockNumberOrHash +func (b *Backend) BlockNumberFromTendermint(blockNrOrHash rpc.BlockNumberOrHash) (rpc.BlockNumber, error) { + switch { + case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: + return rpc.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") + case blockNrOrHash.BlockHash != nil: + blockNumber, err := b.BlockNumberFromTendermintByHash(*blockNrOrHash.BlockHash) + if err != nil { + return rpc.EthEarliestBlockNumber, err + } + return rpc.NewBlockNumber(blockNumber), nil + case blockNrOrHash.BlockNumber != nil: + return *blockNrOrHash.BlockNumber, nil + default: + return rpc.EthEarliestBlockNumber, nil + } +} + +// BlockNumberFromTendermintByHash returns the block height of given block hash +func (b *Backend) BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) { + resBlock, err := b.TendermintBlockByHash(blockHash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) + } + return big.NewInt(resBlock.Block.Height), nil +} + +// EthMsgsFromTendermintBlock returns all real MsgEthereumTxs from a +// Tendermint block. It also ensures consistency over the correct txs indexes +// across RPC endpoints +func (b *Backend) EthMsgsFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, +) []*evm.MsgEthereumTx { + var result []*evm.MsgEthereumTx + block := resBlock.Block + + txResults := blockRes.TxsResults + + for i, tx := range block.Txs { + // Check if tx exists on EVM by cross checking with blockResults: + // - Include unsuccessful tx that exceeds block gas limit + // - Include unsuccessful tx that failed when committing changes to stateDB + // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit + if !rpc.TxSuccessOrExpectedFailure(txResults[i]) { + b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) + continue + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(tx) + if err != nil { + b.logger.Debug("failed to decode transaction in block", "height", block.Height, "error", err.Error()) + continue + } + + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + continue + } + + ethMsg.Hash = ethMsg.AsTransaction().Hash().Hex() + result = append(result, ethMsg) + } + } + + return result +} + +// HeaderByNumber returns the block header identified by height. +func (b *Backend) HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + if resBlock == nil { + return nil, errors.Errorf("block not found for height %d", blockNum) + } + + blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, fmt.Errorf("block result not found for height %d. %w", resBlock.Block.Height, err) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) + } + + ethHeader := rpc.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + return ethHeader, nil +} + +// HeaderByHash returns the block header identified by hash. +func (b *Backend) HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) { + resBlock, err := b.TendermintBlockByHash(blockHash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) + } + + blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, errors.Errorf("block result not found for height %d", resBlock.Block.Height) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) + } + + ethHeader := rpc.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + return ethHeader, nil +} + +// BlockBloom query block bloom filter from block results +func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (gethcore.Bloom, error) { + for _, event := range blockRes.EndBlockEvents { + if event.Type != evm.EventTypeBlockBloom { + continue + } + + for _, attr := range event.Attributes { + if attr.Key == evm.AttributeKeyEthereumBloom { + return gethcore.BytesToBloom([]byte(attr.Value)), nil + } + } + } + return gethcore.Bloom{}, errors.New("block bloom event is not found") +} + +// RPCBlockFromTendermintBlock returns a JSON-RPC compatible Ethereum block from a +// given Tendermint block and its block result. +func (b *Backend) RPCBlockFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, + fullTx bool, +) (map[string]interface{}, error) { + ethRPCTxs := []interface{}{} + block := resBlock.Block + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Height, "error", err) + } + + msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) + for txIndex, ethMsg := range msgs { + if !fullTx { + hash := common.HexToHash(ethMsg.Hash) + ethRPCTxs = append(ethRPCTxs, hash) + continue + } + + tx := ethMsg.AsTransaction() + height := uint64(block.Height) //#nosec G701 -- checked for int overflow already + index := uint64(txIndex) //#nosec G701 -- checked for int overflow already + rpcTx, err := rpc.NewRPCTxFromEthTx( + tx, + common.BytesToHash(block.Hash()), + height, + index, + baseFee, + b.chainID, + ) + if err != nil { + b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) + continue + } + ethRPCTxs = append(ethRPCTxs, rpcTx) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) + } + + req := &evm.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), + } + + var validatorAccAddr sdk.AccAddress + + ctx := rpc.NewContextWithHeight(block.Height) + res, err := b.queryClient.ValidatorAccount(ctx, req) + if err != nil { + b.logger.Debug( + "failed to query validator operator address", + "height", block.Height, + "cons-address", req.ConsAddress, + "error", err.Error(), + ) + // use zero address as the validator operator address + validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) + } else { + validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) + if err != nil { + return nil, err + } + } + + validatorAddr := common.BytesToAddress(validatorAccAddr) + + gasLimit, err := rpc.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) + if err != nil { + b.logger.Error("failed to query consensus params", "error", err.Error()) + } + + gasUsed := uint64(0) + + for _, txsResult := range blockRes.TxsResults { + // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 + if ShouldIgnoreGasUsed(txsResult) { + // block gas limit has exceeded, other txs must have failed with same reason. + break + } + gasUsed += uint64(txsResult.GetGasUsed()) // #nosec G701 -- checked for int overflow already + } + + formattedBlock := rpc.FormatBlock( + block.Header, block.Size(), + gasLimit, new(big.Int).SetUint64(gasUsed), + ethRPCTxs, bloom, validatorAddr, baseFee, + ) + return formattedBlock, nil +} + +// EthBlockByNumber returns the Ethereum Block identified by number. +func (b *Backend) EthBlockByNumber(blockNum rpc.BlockNumber) (*gethcore.Block, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + if resBlock == nil { + // block not found + return nil, fmt.Errorf("block not found for height %d", blockNum) + } + + blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) + } + + return b.EthBlockFromTendermintBlock(resBlock, blockRes) +} + +// EthBlockFromTendermintBlock returns an Ethereum Block type from Tendermint block +// EthBlockFromTendermintBlock +func (b *Backend) EthBlockFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, +) (*gethcore.Block, error) { + block := resBlock.Block + height := block.Height + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByNumber BlockBloom failed", "height", height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle error for pruned node and log + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", height, "error", err) + } + + ethHeader := rpc.EthHeaderFromTendermint(block.Header, bloom, baseFee) + msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) + + txs := make([]*gethcore.Transaction, len(msgs)) + for i, ethMsg := range msgs { + txs[i] = ethMsg.AsTransaction() + } + + // TODO: add tx receipts + ethBlock := gethcore.NewBlock(ethHeader, txs, nil, nil, trie.NewStackTrie(nil)) + return ethBlock, nil +} diff --git a/eth/rpc/backend/blocks_test.go b/eth/rpc/backend/blocks_test.go new file mode 100644 index 000000000..e415bf334 --- /dev/null +++ b/eth/rpc/backend/blocks_test.go @@ -0,0 +1,1629 @@ +package backend + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + + "github.com/cometbft/cometbft/abci/types" + cmtrpc "github.com/cometbft/cometbft/rpc/core/types" + cmt "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *BackendSuite) TestBlockNumber() { + testCases := []struct { + name string + registerMock func() + wantBlockNum hexutil.Uint64 + wantPass bool + }{ + { + name: "fail - invalid block header height", + registerMock: func() { + var header metadata.MD + height := int64(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsInvalidHeight(queryClient, &header, height) + }, + wantBlockNum: 0x0, + wantPass: false, + }, + { + name: "fail - invalid block header", + registerMock: func() { + var header metadata.MD + height := int64(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsInvalidHeader(queryClient, &header, height) + }, + wantBlockNum: 0x0, + wantPass: false, + }, + { + name: "pass - app state header height 1", + registerMock: func() { + var header metadata.MD + height := int64(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, height) + }, + wantBlockNum: 0x1, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + blockNumber, err := s.backend.BlockNumber() + + if tc.wantPass { + s.Require().NoError(err) + s.Require().Equal(tc.wantBlockNum, blockNumber) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetBlockByNumber() { + var ( + blockRes *cmtrpc.ResultBlockResults + resBlock *cmtrpc.ResultBlock + ) + msgEthereumTx, bz := s.buildEthereumTx() + + testCases := []struct { + name string + blockNumber rpc.BlockNumber + fullTx bool + baseFee *big.Int + validator sdk.AccAddress + ethTx *evm.MsgEthereumTx + ethTxBz []byte + registerMock func(rpc.BlockNumber, math.Int, sdk.AccAddress, []byte) + wantNoop bool + wantPass bool + }{ + { + name: "pass - tendermint block not found", + blockNumber: rpc.BlockNumber(1), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + ethTx: nil, + ethTxBz: nil, + registerMock: func(blockNum rpc.BlockNumber, _ math.Int, _ sdk.AccAddress, _ []byte) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, height) + }, + wantNoop: true, + wantPass: true, + }, + { + name: "pass - block not found (e.g. request block height that is greater than current one)", + blockNumber: rpc.BlockNumber(1), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + ethTx: nil, + ethTxBz: nil, + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockNotFound(client, height) + }, + wantNoop: true, + wantPass: true, + }, + { + name: "pass - block results error", + blockNumber: rpc.BlockNumber(1), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + ethTx: nil, + ethTxBz: nil, + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlock(client, height, txBz) + RegisterBlockResultsError(client, blockNum.Int64()) + }, + wantNoop: true, + wantPass: true, + }, + { + name: "pass - without tx", + blockNumber: rpc.BlockNumber(1), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + ethTx: nil, + ethTxBz: nil, + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlock(client, height, txBz) + blockRes, _ = RegisterBlockResults(client, blockNum.Int64()) + RegisterConsensusParams(client, height) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + }, + wantNoop: false, + wantPass: true, + }, + { + name: "pass - with tx", + blockNumber: rpc.BlockNumber(1), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + ethTx: msgEthereumTx, + ethTxBz: bz, + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlock(client, height, txBz) + blockRes, _ = RegisterBlockResults(client, blockNum.Int64()) + RegisterConsensusParams(client, height) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + }, + wantNoop: false, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(tc.blockNumber, math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.ethTxBz) + + block, err := s.backend.GetBlockByNumber(tc.blockNumber, tc.fullTx) + + if tc.wantPass { + if tc.wantNoop { + s.Require().Nil(block) + } else { + expBlock := s.buildFormattedBlock( + blockRes, + resBlock, + tc.fullTx, + tc.ethTx, + tc.validator, + tc.baseFee, + ) + s.Require().Equal(expBlock, block) + } + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetBlockByHash() { + var ( + blockRes *cmtrpc.ResultBlockResults + resBlock *cmtrpc.ResultBlock + ) + msgEthereumTx, bz := s.buildEthereumTx() + + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + + testCases := []struct { + name string + hash common.Hash + fullTx bool + baseFee *big.Int + validator sdk.AccAddress + tx *evm.MsgEthereumTx + txBz []byte + registerMock func( + common.Hash, math.Int, sdk.AccAddress, []byte) + wantNoop bool + wantPass bool + }{ + { + name: "fail - tendermint failed to get block", + hash: common.BytesToHash(block.Hash()), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + tx: nil, + txBz: nil, + registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, hash, txBz) + }, + wantNoop: false, + wantPass: false, + }, + { + name: "noop - tendermint blockres not found", + hash: common.BytesToHash(block.Hash()), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + tx: nil, + txBz: nil, + registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashNotFound(client, hash, txBz) + }, + wantNoop: true, + wantPass: true, + }, + { + name: "noop - tendermint failed to fetch block result", + hash: common.BytesToHash(block.Hash()), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + tx: nil, + txBz: nil, + registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, hash, txBz) + + RegisterBlockResultsError(client, height) + }, + wantNoop: true, + wantPass: true, + }, + { + name: "pass - without tx", + hash: common.BytesToHash(block.Hash()), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + tx: nil, + txBz: nil, + registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, hash, txBz) + + blockRes, _ = RegisterBlockResults(client, height) + RegisterConsensusParams(client, height) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + }, + wantNoop: false, + wantPass: true, + }, + { + name: "pass - with tx", + hash: common.BytesToHash(block.Hash()), + fullTx: true, + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + tx: msgEthereumTx, + txBz: bz, + registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, hash, txBz) + + blockRes, _ = RegisterBlockResults(client, height) + RegisterConsensusParams(client, height) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + }, + wantNoop: false, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(tc.hash, math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.txBz) + + block, err := s.backend.GetBlockByHash(tc.hash, tc.fullTx) + + if tc.wantPass { + if tc.wantNoop { + s.Require().Nil(block) + } else { + expBlock := s.buildFormattedBlock( + blockRes, + resBlock, + tc.fullTx, + tc.tx, + tc.validator, + tc.baseFee, + ) + s.Require().Equal(expBlock, block) + } + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetBlockTransactionCountByHash() { + _, bz := s.buildEthereumTx() + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + hash common.Hash + registerMock func(common.Hash) + wantCount hexutil.Uint + wantPass bool + }{ + { + name: "fail - block not found", + hash: common.BytesToHash(emptyBlock.Hash()), + registerMock: func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, hash, nil) + }, + wantCount: hexutil.Uint(0), + wantPass: false, + }, + { + name: "fail - tendermint client failed to get block result", + hash: common.BytesToHash(emptyBlock.Hash()), + registerMock: func(hash common.Hash) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, nil) + s.Require().NoError(err) + RegisterBlockResultsError(client, height) + }, + wantCount: hexutil.Uint(0), + wantPass: false, + }, + { + name: "pass - block without tx", + hash: common.BytesToHash(emptyBlock.Hash()), + registerMock: func(hash common.Hash) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, height) + s.Require().NoError(err) + }, + wantCount: hexutil.Uint(0), + wantPass: true, + }, + { + name: "pass - block with tx", + hash: common.BytesToHash(block.Hash()), + registerMock: func(hash common.Hash) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, bz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, height) + s.Require().NoError(err) + }, + wantCount: hexutil.Uint(1), + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.hash) + count := s.backend.GetBlockTransactionCountByHash(tc.hash) + if tc.wantPass { + s.Require().Equal(tc.wantCount, *count) + } else { + s.Require().Nil(count) + } + }) + } +} + +func (s *BackendSuite) TestGetBlockTransactionCountByNumber() { + _, bz := s.buildEthereumTx() + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + blockNum rpc.BlockNumber + registerMock func(rpc.BlockNumber) + wantCount hexutil.Uint + wantPass bool + }{ + { + name: "fail - block not found", + blockNum: rpc.BlockNumber(emptyBlock.Height), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, height) + }, + wantCount: hexutil.Uint(0), + wantPass: false, + }, + { + name: "fail - tendermint client failed to get block result", + blockNum: rpc.BlockNumber(emptyBlock.Height), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, nil) + s.Require().NoError(err) + RegisterBlockResultsError(client, height) + }, + wantCount: hexutil.Uint(0), + wantPass: false, + }, + { + name: "pass - block without tx", + blockNum: rpc.BlockNumber(emptyBlock.Height), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, height) + s.Require().NoError(err) + }, + wantCount: hexutil.Uint(0), + wantPass: true, + }, + { + name: "pass - block with tx", + blockNum: rpc.BlockNumber(block.Height), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, bz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, height) + s.Require().NoError(err) + }, + wantCount: hexutil.Uint(1), + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.blockNum) + count := s.backend.GetBlockTransactionCountByNumber(tc.blockNum) + if tc.wantPass { + s.Require().Equal(tc.wantCount, *count) + } else { + s.Require().Nil(count) + } + }) + } +} + +func (s *BackendSuite) TestTendermintBlockByNumber() { + var expResultBlock *cmtrpc.ResultBlock + + testCases := []struct { + name string + blockNumber rpc.BlockNumber + registerMock func(rpc.BlockNumber) + wantBlockFound bool + wantPass bool + }{ + { + name: "fail - client error", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, height) + }, + wantBlockFound: false, + wantPass: false, + }, + { + name: "noop - block not found", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockNotFound(client, height) + s.Require().NoError(err) + }, + wantBlockFound: false, + wantPass: true, + }, + { + name: "fail - blockNum < 0 with app state height error", + blockNumber: rpc.BlockNumber(-1), + registerMock: func(_ rpc.BlockNumber) { + var header metadata.MD + appHeight := int64(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsError(queryClient, &header, appHeight) + }, + wantBlockFound: false, + wantPass: false, + }, + { + name: "pass - blockNum < 0 with app state height >= 1", + blockNumber: rpc.BlockNumber(-1), + registerMock: func(blockNum rpc.BlockNumber) { + var header metadata.MD + appHeight := int64(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, appHeight) + + tmHeight := appHeight + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, tmHeight, nil) + }, + wantBlockFound: true, + wantPass: true, + }, + { + name: "pass - blockNum = 0 (defaults to blockNum = 1 due to a difference between tendermint heights and geth heights)", + blockNumber: rpc.BlockNumber(0), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, height, nil) + }, + wantBlockFound: true, + wantPass: true, + }, + { + name: "pass - blockNum = 1", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, height, nil) + }, + wantBlockFound: true, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.blockNumber) + resultBlock, err := s.backend.TendermintBlockByNumber(tc.blockNumber) + + if tc.wantPass { + s.Require().NoError(err) + + if !tc.wantBlockFound { + s.Require().Nil(resultBlock) + } else { + s.Require().Equal(expResultBlock, resultBlock) + s.Require().Equal(expResultBlock.Block.Header.Height, resultBlock.Block.Header.Height) + } + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestTendermintBlockResultByNumber() { + var expBlockRes *cmtrpc.ResultBlockResults + + testCases := []struct { + name string + blockNumber int64 + registerMock func(int64) + wantPass bool + }{ + { + name: "fail", + blockNumber: 1, + registerMock: func(blockNum int64) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockResultsError(client, blockNum) + }, + wantPass: false, + }, + { + name: "pass", + blockNumber: 1, + registerMock: func(blockNum int64) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockResults(client, blockNum) + s.Require().NoError(err) + expBlockRes = &cmtrpc.ResultBlockResults{ + Height: blockNum, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + } + }, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(tc.blockNumber) + + blockRes, err := s.backend.TendermintBlockResultByNumber(&tc.blockNumber) //#nosec G601 -- fine for tests + + if tc.wantPass { + s.Require().NoError(err) + s.Require().Equal(expBlockRes, blockRes) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestBlockNumberFromTendermint() { + var resBlock *cmtrpc.ResultBlock + + _, bz := s.buildEthereumTx() + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + blockNum := rpc.NewBlockNumber(big.NewInt(block.Height)) + blockHash := common.BytesToHash(block.Hash()) + + testCases := []struct { + name string + blockNum *rpc.BlockNumber + hash *common.Hash + registerMock func(*common.Hash) + wantPass bool + }{ + { + "error - without blockHash or blockNum", + nil, + nil, + func(hash *common.Hash) {}, + false, + }, + { + "error - with blockHash, tendermint client failed to get block", + nil, + &blockHash, + func(hash *common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, *hash, bz) + }, + false, + }, + { + "pass - with blockHash", + nil, + &blockHash, + func(hash *common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, *hash, bz) + }, + true, + }, + { + "pass - without blockHash & with blockNumber", + &blockNum, + nil, + func(hash *common.Hash) {}, + true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + blockNrOrHash := rpc.BlockNumberOrHash{ + BlockNumber: tc.blockNum, + BlockHash: tc.hash, + } + + tc.registerMock(tc.hash) + blockNum, err := s.backend.BlockNumberFromTendermint(blockNrOrHash) + + if tc.wantPass { + s.Require().NoError(err) + if tc.hash == nil { + s.Require().Equal(*tc.blockNum, blockNum) + } else { + expHeight := rpc.NewBlockNumber(big.NewInt(resBlock.Block.Height)) + s.Require().Equal(expHeight, blockNum) + } + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestBlockNumberFromTendermintByHash() { + var resBlock *cmtrpc.ResultBlock + + _, bz := s.buildEthereumTx() + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + hash common.Hash + registerMock func(common.Hash) + wantPass bool + }{ + { + "fail - tendermint client failed to get block", + common.BytesToHash(block.Hash()), + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, hash, bz) + }, + false, + }, + { + "pass - block without tx", + common.BytesToHash(emptyBlock.Hash()), + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, hash, bz) + }, + true, + }, + { + "pass - block with tx", + common.BytesToHash(block.Hash()), + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + resBlock, _ = RegisterBlockByHash(client, hash, bz) + }, + true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.hash) + blockNum, err := s.backend.BlockNumberFromTendermintByHash(tc.hash) + if tc.wantPass { + expHeight := big.NewInt(resBlock.Block.Height) + s.Require().NoError(err) + s.Require().Equal(expHeight, blockNum) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestBlockBloom() { + testCases := []struct { + name string + blockRes *cmtrpc.ResultBlockResults + wantBlockBloom gethcore.Bloom + wantPass bool + }{ + { + "fail - empty block result", + &cmtrpc.ResultBlockResults{}, + gethcore.Bloom{}, + false, + }, + { + "fail - non block bloom event type", + &cmtrpc.ResultBlockResults{ + EndBlockEvents: []types.Event{{Type: evm.EventTypeEthereumTx}}, + }, + gethcore.Bloom{}, + false, + }, + { + "fail - nonblock bloom attribute key", + &cmtrpc.ResultBlockResults{ + EndBlockEvents: []types.Event{ + { + Type: evm.EventTypeBlockBloom, + Attributes: []types.EventAttribute{ + {Key: evm.AttributeKeyEthereumTxHash}, + }, + }, + }, + }, + gethcore.Bloom{}, + false, + }, + { + "pass - block bloom attribute key", + &cmtrpc.ResultBlockResults{ + EndBlockEvents: []types.Event{ + { + Type: evm.EventTypeBlockBloom, + Attributes: []types.EventAttribute{ + {Key: evm.AttributeKeyEthereumBloom}, + }, + }, + }, + }, + gethcore.Bloom{}, + true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + blockBloom, err := s.backend.BlockBloom(tc.blockRes) + + if tc.wantPass { + s.Require().NoError(err) + s.Require().Equal(tc.wantBlockBloom, blockBloom) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetEthBlockFromTendermint() { + msgEthereumTx, bz := s.buildEthereumTx() + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + baseFee *big.Int + validator sdk.AccAddress + height int64 + resBlock *cmtrpc.ResultBlock + blockRes *cmtrpc.ResultBlockResults + fullTx bool + registerMock func(math.Int, sdk.AccAddress, int64) + wantTxs bool + wantPass bool + }{ + { + name: "pass - block without tx", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(common.Address{}.Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{Block: emptyBlock}, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: false, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: false, + wantPass: true, + }, + { + name: "pass - block with tx - with BaseFee error", + baseFee: nil, + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: true, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFeeError(queryClient) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: true, + wantPass: true, + }, + { + name: "pass - block with tx - with ValidatorAccount error", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(common.Address{}.Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: true, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccountError(queryClient) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: true, + wantPass: true, + }, + { + name: "pass - block with tx - with ConsensusParams error - BlockMaxGas defaults to max uint32", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: true, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParamsError(client, height) + }, + wantTxs: true, + wantPass: true, + }, + { + name: "pass - block with tx - with ShouldIgnoreGasUsed - empty txs", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{ + { + Code: 11, + GasUsed: 0, + Log: "no block gas left to run tx: out of gas", + }, + }, + }, + fullTx: true, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: false, + wantPass: true, + }, + { + name: "pass - block with tx - non fullTx", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: false, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: true, + wantPass: true, + }, + { + name: "pass - block with tx", + baseFee: math.NewInt(1).BigInt(), + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + height: int64(1), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + fullTx: true, + registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterConsensusParams(client, height) + }, + wantTxs: true, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.height) + + block, err := s.backend.RPCBlockFromTendermintBlock(tc.resBlock, tc.blockRes, tc.fullTx) + + var expBlock map[string]interface{} + header := tc.resBlock.Block.Header + gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) + gasUsed := new(big.Int).SetUint64(uint64(tc.blockRes.TxsResults[0].GasUsed)) + + root := common.Hash{}.Bytes() + receipt := gethcore.NewReceipt(root, false, gasUsed.Uint64()) + bloom := gethcore.CreateBloom(gethcore.Receipts{receipt}) + + ethRPCTxs := []interface{}{} + + if tc.wantTxs { + if tc.fullTx { + rpcTx, err := rpc.NewRPCTxFromEthTx( + msgEthereumTx.AsTransaction(), + common.BytesToHash(header.Hash()), + uint64(header.Height), + uint64(0), + tc.baseFee, + s.backend.chainID, + ) + s.Require().NoError(err) + ethRPCTxs = []interface{}{rpcTx} + } else { + ethRPCTxs = []interface{}{common.HexToHash(msgEthereumTx.Hash)} + } + } + + expBlock = rpc.FormatBlock( + header, + tc.resBlock.Block.Size(), + gasLimit, + gasUsed, + ethRPCTxs, + bloom, + common.BytesToAddress(tc.validator.Bytes()), + tc.baseFee, + ) + + if tc.wantPass { + s.Require().Equal(expBlock, block) + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestEthMsgsFromTendermintBlock() { + msgEthereumTx, bz := s.buildEthereumTx() + + testCases := []struct { + name string + resBlock *cmtrpc.ResultBlock + blockRes *cmtrpc.ResultBlockResults + wantMsgs []*evm.MsgEthereumTx + }{ + { + "tx in not included in block - unsuccessful tx without ExceedBlockGasLimit error", + &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + &cmtrpc.ResultBlockResults{ + TxsResults: []*types.ResponseDeliverTx{ + { + Code: 1, + }, + }, + }, + []*evm.MsgEthereumTx(nil), + }, + { + "tx included in block - unsuccessful tx with ExceedBlockGasLimit error", + &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + &cmtrpc.ResultBlockResults{ + TxsResults: []*types.ResponseDeliverTx{ + { + Code: 1, + Log: rpc.ErrExceedBlockGasLimit, + }, + }, + }, + []*evm.MsgEthereumTx{msgEthereumTx}, + }, + { + "pass", + &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + &cmtrpc.ResultBlockResults{ + TxsResults: []*types.ResponseDeliverTx{ + { + Code: 0, + Log: rpc.ErrExceedBlockGasLimit, + }, + }, + }, + []*evm.MsgEthereumTx{msgEthereumTx}, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + msgs := s.backend.EthMsgsFromTendermintBlock(tc.resBlock, tc.blockRes) + s.Require().Equal(tc.wantMsgs, msgs) + }) + } +} + +func (s *BackendSuite) TestHeaderByNumber() { + var expResultBlock *cmtrpc.ResultBlock + + _, bz := s.buildEthereumTx() + + testCases := []struct { + name string + blockNumber rpc.BlockNumber + baseFee *big.Int + registerMock func(rpc.BlockNumber, math.Int) + wantPass bool + }{ + { + name: "fail - tendermint client failed to get block", + blockNumber: rpc.BlockNumber(1), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, height) + }, + wantPass: false, + }, + { + name: "fail - block not found for height", + blockNumber: rpc.BlockNumber(1), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockNotFound(client, height) + s.Require().NoError(err) + }, + wantPass: false, + }, + { + name: "fail - block not found for height", + blockNumber: rpc.BlockNumber(1), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, nil) + s.Require().NoError(err) + RegisterBlockResultsError(client, height) + }, + wantPass: false, + }, + { + name: "pass - without Base Fee, failed to fetch from prunned block", + blockNumber: rpc.BlockNumber(1), + baseFee: nil, + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, height, nil) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFeeError(queryClient) + }, + wantPass: true, + }, + { + name: "pass - blockNum = 1, without tx", + blockNumber: rpc.BlockNumber(1), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, height, nil) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + wantPass: true, + }, + { + name: "pass - blockNum = 1, with tx", + blockNumber: rpc.BlockNumber(1), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlock(client, height, bz) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.blockNumber, math.NewIntFromBigInt(tc.baseFee)) + header, err := s.backend.HeaderByNumber(tc.blockNumber) + + if tc.wantPass { + expHeader := rpc.EthHeaderFromTendermint(expResultBlock.Block.Header, gethcore.Bloom{}, tc.baseFee) + s.Require().NoError(err) + s.Require().Equal(expHeader, header) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestHeaderByHash() { + var expResultBlock *cmtrpc.ResultBlock + + _, bz := s.buildEthereumTx() + block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + hash common.Hash + baseFee *big.Int + registerMock func(common.Hash, math.Int) + wantPass bool + }{ + { + name: "fail - tendermint client failed to get block", + hash: common.BytesToHash(block.Hash()), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(hash common.Hash, baseFee math.Int) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, hash, bz) + }, + wantPass: false, + }, + { + name: "fail - block not found for height", + hash: common.BytesToHash(block.Hash()), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(hash common.Hash, baseFee math.Int) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashNotFound(client, hash, bz) + }, + wantPass: false, + }, + { + name: "fail - block not found for height", + hash: common.BytesToHash(block.Hash()), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(hash common.Hash, baseFee math.Int) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, bz) + s.Require().NoError(err) + RegisterBlockResultsError(client, height) + }, + wantPass: false, + }, + { + name: "pass - without Base Fee, failed to fetch from prunned block", + hash: common.BytesToHash(block.Hash()), + baseFee: nil, + registerMock: func(hash common.Hash, baseFee math.Int) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlockByHash(client, hash, bz) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFeeError(queryClient) + }, + wantPass: true, + }, + { + name: "pass - blockNum = 1, without tx", + hash: common.BytesToHash(emptyBlock.Hash()), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(hash common.Hash, baseFee math.Int) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlockByHash(client, hash, nil) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + wantPass: true, + }, + { + name: "pass - with tx", + hash: common.BytesToHash(block.Hash()), + baseFee: math.NewInt(1).BigInt(), + registerMock: func(hash common.Hash, baseFee math.Int) { + height := int64(1) + client := s.backend.clientCtx.Client.(*mocks.Client) + expResultBlock, _ = RegisterBlockByHash(client, hash, bz) + _, err := RegisterBlockResults(client, height) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + wantPass: true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + + tc.registerMock(tc.hash, math.NewIntFromBigInt(tc.baseFee)) + header, err := s.backend.HeaderByHash(tc.hash) + + if tc.wantPass { + expHeader := rpc.EthHeaderFromTendermint(expResultBlock.Block.Header, gethcore.Bloom{}, tc.baseFee) + s.Require().NoError(err) + s.Require().Equal(expHeader, header) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestEthBlockByNumber() { + msgEthereumTx, bz := s.buildEthereumTx() + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + blockNumber rpc.BlockNumber + registerMock func(rpc.BlockNumber) + expEthBlock *gethcore.Block + wantPass bool + }{ + { + name: "fail - tendermint client failed to get block", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, height) + }, + expEthBlock: nil, + wantPass: false, + }, + { + name: "fail - block result not found for height", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, nil) + s.Require().NoError(err) + RegisterBlockResultsError(client, blockNum.Int64()) + }, + expEthBlock: nil, + wantPass: false, + }, + { + name: "pass - block without tx", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, blockNum.Int64()) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + baseFee := math.NewInt(1) + RegisterBaseFee(queryClient, baseFee) + }, + expEthBlock: gethcore.NewBlock( + rpc.EthHeaderFromTendermint( + emptyBlock.Header, + gethcore.Bloom{}, + math.NewInt(1).BigInt(), + ), + []*gethcore.Transaction{}, + nil, + nil, + nil, + ), + wantPass: true, + }, + { + name: "pass - block with tx", + blockNumber: rpc.BlockNumber(1), + registerMock: func(blockNum rpc.BlockNumber) { + height := blockNum.Int64() + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, height, bz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, blockNum.Int64()) + s.Require().NoError(err) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + baseFee := math.NewInt(1) + RegisterBaseFee(queryClient, baseFee) + }, + expEthBlock: gethcore.NewBlock( + rpc.EthHeaderFromTendermint( + emptyBlock.Header, + gethcore.Bloom{}, + math.NewInt(1).BigInt(), + ), + []*gethcore.Transaction{msgEthereumTx.AsTransaction()}, + nil, + nil, + trie.NewStackTrie(nil), + ), + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(tc.blockNumber) + + ethBlock, err := s.backend.EthBlockByNumber(tc.blockNumber) + + if tc.wantPass { + s.Require().NoError(err) + s.Require().Equal(tc.expEthBlock.Header(), ethBlock.Header()) + s.Require().Equal(tc.expEthBlock.Uncles(), ethBlock.Uncles()) + s.Require().Equal(tc.expEthBlock.ReceiptHash(), ethBlock.ReceiptHash()) + for i, tx := range tc.expEthBlock.Transactions() { + s.Require().Equal(tx.Data(), ethBlock.Transactions()[i].Data()) + } + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestEthBlockFromTendermintBlock() { + msgEthereumTx, bz := s.buildEthereumTx() + emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) + + testCases := []struct { + name string + baseFee *big.Int + resBlock *cmtrpc.ResultBlock + blockRes *cmtrpc.ResultBlockResults + registerMock func(math.Int, int64) + expEthBlock *gethcore.Block + wantPass bool + }{ + { + name: "pass - block without tx", + baseFee: math.NewInt(1).BigInt(), + resBlock: &cmtrpc.ResultBlock{ + Block: emptyBlock, + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + }, + registerMock: func(baseFee math.Int, blockNum int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + expEthBlock: gethcore.NewBlock( + rpc.EthHeaderFromTendermint( + emptyBlock.Header, + gethcore.Bloom{}, + math.NewInt(1).BigInt(), + ), + []*gethcore.Transaction{}, + nil, + nil, + nil, + ), + wantPass: true, + }, + { + name: "pass - block with tx", + baseFee: math.NewInt(1).BigInt(), + resBlock: &cmtrpc.ResultBlock{ + Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), + }, + blockRes: &cmtrpc.ResultBlockResults{ + Height: 1, + TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + EndBlockEvents: []types.Event{ + { + Type: evm.EventTypeBlockBloom, + Attributes: []types.EventAttribute{ + {Key: evm.AttributeKeyEthereumBloom}, + }, + }, + }, + }, + registerMock: func(baseFee math.Int, blockNum int64) { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + expEthBlock: gethcore.NewBlock( + rpc.EthHeaderFromTendermint( + emptyBlock.Header, + gethcore.Bloom{}, + math.NewInt(1).BigInt(), + ), + []*gethcore.Transaction{msgEthereumTx.AsTransaction()}, + nil, + nil, + trie.NewStackTrie(nil), + ), + wantPass: true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(math.NewIntFromBigInt(tc.baseFee), tc.blockRes.Height) + + ethBlock, err := s.backend.EthBlockFromTendermintBlock(tc.resBlock, tc.blockRes) + + if tc.wantPass { + s.Require().NoError(err) + s.Require().Equal(tc.expEthBlock.Header(), ethBlock.Header()) + s.Require().Equal(tc.expEthBlock.Uncles(), ethBlock.Uncles()) + s.Require().Equal(tc.expEthBlock.ReceiptHash(), ethBlock.ReceiptHash()) + for i, tx := range tc.expEthBlock.Transactions() { + s.Require().Equal(tx.Data(), ethBlock.Transactions()[i].Data()) + } + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/call_tx.go b/eth/rpc/backend/call_tx.go new file mode 100644 index 000000000..b207f7b66 --- /dev/null +++ b/eth/rpc/backend/call_tx.go @@ -0,0 +1,417 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// Resend accepts an existing transaction and a new gas price and limit. It will remove +// the given transaction from the pool and reinsert it with the new gas price and limit. +func (b *Backend) Resend(args evm.JsonTxArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { + if args.Nonce == nil { + return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") + } + + args, err := b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } + + // The signer used should always be the 'latest' known one because we expect + // signers to be backwards-compatible with old transactions. + eip155ChainID, err := eth.ParseChainID(b.clientCtx.ChainID) + if err != nil { + return common.Hash{}, err + } + + cfg := b.ChainConfig() + if cfg == nil { + cfg = evm.DefaultChainConfig().EthereumConfig(eip155ChainID) + } + + signer := gethcore.LatestSigner(cfg) + + matchTx := args.ToTransaction().AsTransaction() + + // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. + price := matchTx.GasPrice() + if gasPrice != nil { + price = gasPrice.ToInt() + } + gas := matchTx.Gas() + if gasLimit != nil { + gas = uint64(*gasLimit) + } + if err := rpc.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { + return common.Hash{}, err + } + + pending, err := b.PendingTransactions() + if err != nil { + return common.Hash{}, err + } + + for _, tx := range pending { + p, err := evm.UnwrapEthereumMsg(tx, common.Hash{}) + if err != nil { + // not valid ethereum tx + continue + } + + pTx := p.AsTransaction() + + wantSigHash := signer.Hash(matchTx) + pFrom, err := gethcore.Sender(signer, pTx) + if err != nil { + continue + } + + if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { + // Match. Re-sign and send the transaction. + if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { + args.GasPrice = gasPrice + } + if gasLimit != nil && *gasLimit != 0 { + args.Gas = gasLimit + } + + return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice + } + } + + return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) +} + +// SendRawTransaction send a raw Ethereum transaction. +func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + // RLP decode raw transaction bytes + tx := &gethcore.Transaction{} + if err := tx.UnmarshalBinary(data); err != nil { + b.logger.Error("transaction decoding failed", "error", err.Error()) + return common.Hash{}, err + } + + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !tx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + ethereumTx := &evm.MsgEthereumTx{} + if err := ethereumTx.FromEthereumTx(tx); err != nil { + b.logger.Error("transaction converting failed", "error", err.Error()) + return common.Hash{}, err + } + + if err := ethereumTx.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return common.Hash{}, err + } + + // Query params to use the EVM denomination + res, err := b.queryClient.QueryClient.Params(b.ctx, &evm.QueryParamsRequest{}) + if err != nil { + b.logger.Error("failed to query evm params", "error", err.Error()) + return common.Hash{}, err + } + + cosmosTx, err := ethereumTx.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) + if err != nil { + b.logger.Error("failed to build cosmos tx", "error", err.Error()) + return common.Hash{}, err + } + + // Encode transaction by default Tx encoder + txBytes, err := b.clientCtx.TxConfig.TxEncoder()(cosmosTx) + if err != nil { + b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) + return common.Hash{}, err + } + + txHash := ethereumTx.AsTransaction().Hash() + + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Error("failed to broadcast tx", "error", err.Error()) + return txHash, err + } + + return txHash, nil +} + +// SetTxDefaults populates tx message with default values in case they are not +// provided on the args +func (b *Backend) SetTxDefaults(args evm.JsonTxArgs) (evm.JsonTxArgs, error) { + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return args, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + + head, _ := b.CurrentHeader() // #nosec G703 -- no need to check error cause we're already checking that head == nil + if head == nil { + return args, errors.New("latest header is nil") + } + + // If user specifies both maxPriorityfee and maxFee, then we do not + // need to consult the chain for defaults. It's definitely a London tx. + if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { + // In this clause, user left some fields unspecified. + if head.BaseFee != nil && args.GasPrice == nil { + if args.MaxPriorityFeePerGas == nil { + tip, err := b.SuggestGasTipCap(head.BaseFee) + if err != nil { + return args, err + } + args.MaxPriorityFeePerGas = (*hexutil.Big)(tip) + } + + if args.MaxFeePerGas == nil { + gasFeeCap := new(big.Int).Add( + (*big.Int)(args.MaxPriorityFeePerGas), + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap) + } + + if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { + return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) + } + } else { + if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil { + return args, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") + } + + if args.GasPrice == nil { + price, err := b.SuggestGasTipCap(head.BaseFee) + if err != nil { + return args, err + } + if head.BaseFee != nil { + // The legacy tx gas price suggestion should not add 2x base fee + // because all fees are consumed, so it would result in a spiral + // upwards. + price.Add(price, head.BaseFee) + } + args.GasPrice = (*hexutil.Big)(price) + } + } + } else { + // Both maxPriorityfee and maxFee set by caller. Sanity-check their internal relation + if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { + return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) + } + } + + if args.Value == nil { + args.Value = new(hexutil.Big) + } + if args.Nonce == nil { + // get the nonce from the account retriever + // ignore error in case tge account doesn't exist yet + nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) // #nosec G703s + args.Nonce = (*hexutil.Uint64)(&nonce) + } + + if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { + return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data") + } + + if args.To == nil { + // Contract creation + var input []byte + if args.Data != nil { + input = *args.Data + } else if args.Input != nil { + input = *args.Input + } + + if len(input) == 0 { + return args, errors.New("contract creation without any data provided") + } + } + + if args.Gas == nil { + // For backwards-compatibility reason, we try both input and data + // but input is preferred. + input := args.Input + if input == nil { + input = args.Data + } + + callArgs := evm.JsonTxArgs{ + From: args.From, + To: args.To, + Gas: args.Gas, + GasPrice: args.GasPrice, + MaxFeePerGas: args.MaxFeePerGas, + MaxPriorityFeePerGas: args.MaxPriorityFeePerGas, + Value: args.Value, + Data: input, + AccessList: args.AccessList, + ChainID: args.ChainID, + Nonce: args.Nonce, + } + + blockNr := rpc.NewBlockNumber(big.NewInt(0)) + estimated, err := b.EstimateGas(callArgs, &blockNr) + if err != nil { + return args, err + } + args.Gas = &estimated + b.logger.Debug("estimate gas usage automatically", "gas", args.Gas) + } + + if args.ChainID == nil { + args.ChainID = (*hexutil.Big)(b.chainID) + } + + return args, nil +} + +// EstimateGas returns an estimate of gas usage for the given smart contract call. +func (b *Backend) EstimateGas(args evm.JsonTxArgs, blockNrOptional *rpc.BlockNumber) (hexutil.Uint64, error) { + blockNr := rpc.EthPendingBlockNumber + if blockNrOptional != nil { + blockNr = *blockNrOptional + } + + bz, err := json.Marshal(&args) + if err != nil { + return 0, err + } + + header, err := b.TendermintBlockByNumber(blockNr) + if err != nil { + // the error message imitates geth behavior + return 0, errors.New("header not found") + } + + req := evm.EthCallRequest{ + Args: bz, + GasCap: b.RPCGasCap(), + ProposerAddress: sdk.ConsAddress(header.Block.ProposerAddress), + ChainId: b.chainID.Int64(), + } + + // 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. + res, err := b.queryClient.EstimateGas(rpc.NewContextWithHeight(blockNr.Int64()), &req) + if err != nil { + return 0, err + } + return hexutil.Uint64(res.Gas), nil +} + +// DoCall performs a simulated call operation through the evmtypes. It returns the +// estimated gas used on the operation or an error if fails. +func (b *Backend) DoCall( + args evm.JsonTxArgs, blockNr rpc.BlockNumber, +) (*evm.MsgEthereumTxResponse, error) { + bz, err := json.Marshal(&args) + if err != nil { + return nil, err + } + header, err := b.TendermintBlockByNumber(blockNr) + if err != nil { + // the error message imitates geth behavior + return nil, errors.New("header not found") + } + + req := evm.EthCallRequest{ + Args: bz, + GasCap: b.RPCGasCap(), + ProposerAddress: sdk.ConsAddress(header.Block.ProposerAddress), + ChainId: b.chainID.Int64(), + } + + // 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 := rpc.NewContextWithHeight(blockNr.Int64()) + timeout := b.RPCEVMTimeout() + + // Setup context so it may be canceled the call has completed + // or, in case of unmetered gas, setup a context with a timeout. + var cancel context.CancelFunc + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + + // 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) + if err != nil { + return nil, err + } + + if res.Failed() { + if res.VmError != vm.ErrExecutionReverted.Error() { + return nil, status.Error(codes.Internal, res.VmError) + } + return nil, evm.NewExecErrorWithReason(res.Ret) + } + + return res, nil +} + +// GasPrice returns the current gas price based on Ethermint's gas price oracle. +func (b *Backend) GasPrice() (*hexutil.Big, error) { + var ( + result *big.Int + err error + ) + + head, err := b.CurrentHeader() + if err != nil { + return nil, err + } + + if head.BaseFee != nil { + result, err = b.SuggestGasTipCap(head.BaseFee) + if err != nil { + return nil, err + } + result = result.Add(result, head.BaseFee) + } else { + result = big.NewInt(b.RPCMinGasPrice()) + } + + // return at least GlobalMinGasPrice + minGasPrice, err := b.GlobalMinGasPrice() + if err != nil { + return nil, err + } + minGasPriceInt := minGasPrice.TruncateInt().BigInt() + if result.Cmp(minGasPriceInt) < 0 { + result = minGasPriceInt + } + + return (*hexutil.Big)(result), nil +} diff --git a/eth/rpc/backend/call_tx_test.go b/eth/rpc/backend/call_tx_test.go new file mode 100644 index 000000000..dd55c1500 --- /dev/null +++ b/eth/rpc/backend/call_tx_test.go @@ -0,0 +1,502 @@ +package backend + +import ( + "encoding/json" + "fmt" + "math/big" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *BackendSuite) TestResend() { + txNonce := (hexutil.Uint64)(1) + baseFee := math.NewInt(1) + gasPrice := new(hexutil.Big) + toAddr := evmtest.NewEthAddr() + chainID := (*hexutil.Big)(s.backend.chainID) + callArgs := evm.JsonTxArgs{ + From: nil, + To: &toAddr, + Gas: nil, + GasPrice: nil, + MaxFeePerGas: gasPrice, + MaxPriorityFeePerGas: gasPrice, + Value: gasPrice, + Nonce: &txNonce, + Input: nil, + Data: nil, + AccessList: nil, + ChainID: chainID, + } + + testCases := []struct { + name string + registerMock func() + args evm.JsonTxArgs + gasPrice *hexutil.Big + gasLimit *hexutil.Uint64 + expHash common.Hash + expPass bool + }{ + { + "fail - Missing transaction nonce", + func() {}, + evm.JsonTxArgs{ + Nonce: nil, + }, + nil, + nil, + common.Hash{}, + false, + }, + { + "pass - Can't set Tx defaults BaseFee disabled", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFeeDisabled(queryClient) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + ChainID: callArgs.ChainID, + }, + nil, + nil, + common.Hash{}, + true, + }, + { + "pass - Can't set Tx defaults", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + }, + nil, + nil, + common.Hash{}, + true, + }, + { + "pass - MaxFeePerGas is nil", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFeeDisabled(queryClient) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + MaxPriorityFeePerGas: nil, + GasPrice: nil, + MaxFeePerGas: nil, + }, + nil, + nil, + common.Hash{}, + true, + }, + { + "fail - GasPrice and (MaxFeePerGas or MaxPriorityPerGas specified)", + func() {}, + evm.JsonTxArgs{ + Nonce: &txNonce, + MaxPriorityFeePerGas: nil, + GasPrice: gasPrice, + MaxFeePerGas: gasPrice, + }, + nil, + nil, + common.Hash{}, + false, + }, + { + "fail - Block error", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + RegisterBlockError(client, 1) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + }, + nil, + nil, + common.Hash{}, + false, + }, + { + "pass - MaxFeePerGas is nil", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + GasPrice: nil, + MaxPriorityFeePerGas: gasPrice, + MaxFeePerGas: gasPrice, + ChainID: callArgs.ChainID, + }, + nil, + nil, + common.Hash{}, + true, + }, + { + "pass - Chain Id is nil", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + MaxPriorityFeePerGas: gasPrice, + ChainID: nil, + }, + nil, + nil, + common.Hash{}, + true, + }, + { + "fail - Pending transactions error", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + RegisterEstimateGas(queryClient, callArgs) + RegisterParams(queryClient, &header, 1) + RegisterParamsWithoutHeader(queryClient, 1) + RegisterUnconfirmedTxsError(client, nil) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + To: &toAddr, + MaxFeePerGas: gasPrice, + MaxPriorityFeePerGas: gasPrice, + Value: gasPrice, + Gas: nil, + ChainID: callArgs.ChainID, + }, + gasPrice, + nil, + common.Hash{}, + false, + }, + { + "fail - Not Ethereum txs", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + RegisterEstimateGas(queryClient, callArgs) + RegisterParams(queryClient, &header, 1) + RegisterParamsWithoutHeader(queryClient, 1) + RegisterUnconfirmedTxsEmpty(client, nil) + }, + evm.JsonTxArgs{ + Nonce: &txNonce, + To: &toAddr, + MaxFeePerGas: gasPrice, + MaxPriorityFeePerGas: gasPrice, + Value: gasPrice, + Gas: nil, + ChainID: callArgs.ChainID, + }, + gasPrice, + nil, + common.Hash{}, + false, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + hash, err := s.backend.Resend(tc.args, tc.gasPrice, tc.gasLimit) + + if tc.expPass { + s.Require().Equal(tc.expHash, hash) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSendRawTransaction() { + ethTx, bz := s.buildEthereumTx() + + // Sign the ethTx + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) + err := ethTx.Sign(ethSigner, s.signer) + s.Require().NoError(err) + + rlpEncodedBz, _ := rlp.EncodeToBytes(ethTx.AsTransaction()) + cosmosTx, _ := ethTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) + txBytes, _ := s.backend.clientCtx.TxConfig.TxEncoder()(cosmosTx) + + testCases := []struct { + name string + registerMock func() + rawTx []byte + expHash common.Hash + expPass bool + }{ + { + "fail - empty bytes", + func() {}, + []byte{}, + common.Hash{}, + false, + }, + { + "fail - no RLP encoded bytes", + func() {}, + bz, + common.Hash{}, + false, + }, + { + "fail - unprotected transactions", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.allowUnprotectedTxs = false + RegisterParamsWithoutHeaderError(queryClient, 1) + }, + rlpEncodedBz, + common.Hash{}, + false, + }, + { + "fail - failed to get evm params", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.allowUnprotectedTxs = true + RegisterParamsWithoutHeaderError(queryClient, 1) + }, + rlpEncodedBz, + common.Hash{}, + false, + }, + { + "fail - failed to broadcast transaction", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.allowUnprotectedTxs = true + RegisterParamsWithoutHeader(queryClient, 1) + RegisterBroadcastTxError(client, txBytes) + }, + rlpEncodedBz, + common.HexToHash(ethTx.Hash), + false, + }, + { + "pass - Gets the correct transaction hash of the eth transaction", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.allowUnprotectedTxs = true + RegisterParamsWithoutHeader(queryClient, 1) + RegisterBroadcastTx(client, txBytes) + }, + rlpEncodedBz, + common.HexToHash(ethTx.Hash), + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + hash, err := s.backend.SendRawTransaction(tc.rawTx) + + if tc.expPass { + s.Require().Equal(tc.expHash, hash) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestDoCall() { + _, bz := s.buildEthereumTx() + gasPrice := (*hexutil.Big)(big.NewInt(1)) + toAddr := evmtest.NewEthAddr() + chainID := (*hexutil.Big)(s.backend.chainID) + callArgs := evm.JsonTxArgs{ + From: nil, + To: &toAddr, + Gas: nil, + GasPrice: nil, + MaxFeePerGas: gasPrice, + MaxPriorityFeePerGas: gasPrice, + Value: gasPrice, + Input: nil, + Data: nil, + AccessList: nil, + ChainID: chainID, + } + argsBz, err := json.Marshal(callArgs) + s.Require().NoError(err) + + testCases := []struct { + name string + registerMock func() + blockNum rpc.BlockNumber + callArgs evm.JsonTxArgs + expEthTx *evm.MsgEthereumTxResponse + expPass bool + }{ + { + "fail - Invalid request", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, bz) + s.Require().NoError(err) + RegisterEthCallError(queryClient, &evm.EthCallRequest{Args: argsBz, ChainId: s.backend.chainID.Int64()}) + }, + rpc.BlockNumber(1), + callArgs, + &evm.MsgEthereumTxResponse{}, + false, + }, + { + "pass - Returned transaction response", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, bz) + s.Require().NoError(err) + RegisterEthCall(queryClient, &evm.EthCallRequest{Args: argsBz, ChainId: s.backend.chainID.Int64()}) + }, + rpc.BlockNumber(1), + callArgs, + &evm.MsgEthereumTxResponse{}, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + msgEthTx, err := s.backend.DoCall(tc.callArgs, tc.blockNum) + + if tc.expPass { + s.Require().Equal(tc.expEthTx, msgEthTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGasPrice() { + defaultGasPrice := (*hexutil.Big)(big.NewInt(1)) + + testCases := []struct { + name string + registerMock func() + expGas *hexutil.Big + expPass bool + }{ + { + "pass - get the default gas price", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, math.NewInt(1)) + }, + defaultGasPrice, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + gasPrice, err := s.backend.GasPrice() + if tc.expPass { + s.Require().Equal(tc.expGas, gasPrice) + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/chain_info.go b/eth/rpc/backend/chain_info.go new file mode 100644 index 000000000..3936d5416 --- /dev/null +++ b/eth/rpc/backend/chain_info.go @@ -0,0 +1,238 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "fmt" + "math/big" + + sdkmath "cosmossdk.io/math" + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + gethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/pkg/errors" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// ChainID is the EIP-155 replay-protection chain id for the current ethereum chain config. +func (b *Backend) ChainID() (*hexutil.Big, error) { + eip155ChainID, err := eth.ParseChainID(b.clientCtx.ChainID) + if err != nil { + panic(err) + } + // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config + bn, err := b.BlockNumber() + if err != nil { + b.logger.Debug("failed to fetch latest block number", "error", err.Error()) + return (*hexutil.Big)(eip155ChainID), nil + } + + if config := b.ChainConfig(); config.IsEIP155(new(big.Int).SetUint64(uint64(bn))) { + return (*hexutil.Big)(config.ChainID), nil + } + + return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block") +} + +// ChainConfig returns the latest ethereum chain configuration +func (b *Backend) ChainConfig() *params.ChainConfig { + params, err := b.queryClient.Params(b.ctx, &evm.QueryParamsRequest{}) + if err != nil { + return nil + } + + return params.Params.ChainConfig.EthereumConfig(b.chainID) +} + +// BaseFee returns the base fee tracked by the Fee Market module. +// If the base fee is not enabled globally, the query returns nil. +// If the London hard fork is not activated at the current height, the query will +// return nil. +func (b *Backend) BaseFee( + blockRes *tmrpctypes.ResultBlockResults, +) (baseFee *big.Int, err error) { + // return BaseFee if London hard fork is activated and feemarket is enabled + res, err := b.queryClient.BaseFee(rpc.NewContextWithHeight(blockRes.Height), &evm.QueryBaseFeeRequest{}) + if err != nil || res.BaseFee == nil { + baseFee = nil + // TODO: feat: dynamic fee handling on events + return baseFee, nil + } + return res.BaseFee.BigInt(), nil +} + +// CurrentHeader returns the latest block header +// This will return error as per node configuration +// if the ABCI responses are discarded ('discard_abci_responses' config param) +func (b *Backend) CurrentHeader() (*gethcore.Header, error) { + return b.HeaderByNumber(rpc.EthLatestBlockNumber) +} + +// PendingTransactions returns the transactions that are in the transaction pool +// and have a from address that is one of the accounts this node manages. +func (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { + mc, ok := b.clientCtx.Client.(tmrpcclient.MempoolClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + + res, err := mc.UnconfirmedTxs(b.ctx, nil) + if err != nil { + return nil, err + } + + result := make([]*sdk.Tx, 0, len(res.Txs)) + for _, txBz := range res.Txs { + tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) + if err != nil { + return nil, err + } + result = append(result, &tx) + } + + return result, nil +} + +// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase). +func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { + node, err := b.clientCtx.GetNode() + if err != nil { + return nil, err + } + + status, err := node.Status(b.ctx) + if err != nil { + return nil, err + } + + req := &evm.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), + } + + res, err := b.queryClient.ValidatorAccount(b.ctx, req) + if err != nil { + return nil, err + } + + address, _ := sdk.AccAddressFromBech32(res.AccountAddress) // #nosec G703 + return address, nil +} + +// FeeHistory returns data relevant for fee estimation based on the specified range of blocks. +func (b *Backend) FeeHistory( + userBlockCount gethrpc.DecimalOrHex, // number blocks to fetch, maximum is 100 + lastBlock gethrpc.BlockNumber, // the block to start search , to oldest + rewardPercentiles []float64, // percentiles to fetch reward +) (*rpc.FeeHistoryResult, error) { + blockEnd := int64(lastBlock) //#nosec G701 -- checked for int overflow already + + if blockEnd < 0 { + blockNumber, err := b.BlockNumber() + if err != nil { + return nil, err + } + blockEnd = int64(blockNumber) //#nosec G701 -- checked for int overflow already + } + + blocks := int64(userBlockCount) // #nosec G701 -- checked for int overflow already + maxBlockCount := int64(b.cfg.JSONRPC.FeeHistoryCap) // #nosec G701 -- checked for int overflow already + if blocks > maxBlockCount { + return nil, fmt.Errorf("FeeHistory user block count %d higher than %d", blocks, maxBlockCount) + } + + if blockEnd+1 < blocks { + blocks = blockEnd + 1 + } + // Ensure not trying to retrieve before genesis. + blockStart := blockEnd + 1 - blocks + oldestBlock := (*hexutil.Big)(big.NewInt(blockStart)) + + // prepare space + reward := make([][]*hexutil.Big, blocks) + rewardCount := len(rewardPercentiles) + for i := 0; i < int(blocks); i++ { + reward[i] = make([]*hexutil.Big, rewardCount) + } + + thisBaseFee := make([]*hexutil.Big, blocks+1) + thisGasUsedRatio := make([]float64, blocks) + + // rewards should only be calculated if reward percentiles were included + calculateRewards := rewardCount != 0 + + // fetch block + for blockID := blockStart; blockID <= blockEnd; blockID++ { + index := int32(blockID - blockStart) // #nosec G701 + // tendermint block + tendermintblock, err := b.TendermintBlockByNumber(rpc.BlockNumber(blockID)) + if tendermintblock == nil { + return nil, err + } + + // eth block + ethBlock, err := b.GetBlockByNumber(rpc.BlockNumber(blockID), true) + if ethBlock == nil { + return nil, err + } + + // tendermint block result + tendermintBlockResult, err := b.TendermintBlockResultByNumber(&tendermintblock.Block.Height) + if tendermintBlockResult == nil { + b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) + return nil, err + } + + oneFeeHistory := rpc.OneFeeHistory{} + err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) + if err != nil { + return nil, err + } + + // copy + thisBaseFee[index] = (*hexutil.Big)(oneFeeHistory.BaseFee) + thisBaseFee[index+1] = (*hexutil.Big)(oneFeeHistory.NextBaseFee) + thisGasUsedRatio[index] = oneFeeHistory.GasUsedRatio + if calculateRewards { + for j := 0; j < rewardCount; j++ { + reward[index][j] = (*hexutil.Big)(oneFeeHistory.Reward[j]) + if reward[index][j] == nil { + reward[index][j] = (*hexutil.Big)(big.NewInt(0)) + } + } + } + } + + feeHistory := rpc.FeeHistoryResult{ + OldestBlock: oldestBlock, + BaseFee: thisBaseFee, + GasUsedRatio: thisGasUsedRatio, + } + + if calculateRewards { + feeHistory.Reward = reward + } + + return &feeHistory, nil +} + +// SuggestGasTipCap: Not yet supported. Returns 0 as the suggested tip cap. After +// implementing tx prioritization, this function can come to life. +func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { + maxDelta := big.NewInt(0) + return maxDelta, nil +} + +func DefaultMinGasPrice() sdkmath.LegacyDec { return sdkmath.LegacyZeroDec() } + +// GlobalMinGasPrice returns the minimum gas price for all nodes. This is +// distinct from the individual configuration set by the validator set. +func (b *Backend) GlobalMinGasPrice() (sdkmath.LegacyDec, error) { + // TODO: feat(eth): dynamic fees + return DefaultMinGasPrice(), nil +} diff --git a/eth/rpc/backend/chain_info_test.go b/eth/rpc/backend/chain_info_test.go new file mode 100644 index 000000000..c1267cd0d --- /dev/null +++ b/eth/rpc/backend/chain_info_test.go @@ -0,0 +1,350 @@ +package backend + +import ( + "fmt" + "math/big" + + sdkmath "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/common/hexutil" + ethrpc "github.com/ethereum/go-ethereum/rpc" + + "google.golang.org/grpc/metadata" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cometbft/cometbft/abci/types" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *BackendSuite) TestBaseFee() { + baseFee := sdkmath.NewInt(1) + + testCases := []struct { + name string + blockRes *tmrpctypes.ResultBlockResults + registerMock func() + expBaseFee *big.Int + expPass bool + }{ + // TODO: test(eth): Test base fee query after it's enabled. + // { + // "fail - grpc BaseFee error", + // &tmrpctypes.ResultBlockResults{Height: 1}, + // func() { + // queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + // RegisterBaseFeeError(queryClient) + // }, + // nil, + // false, + // }, + { + name: "pass - grpc BaseFee error - with non feemarket block event", + blockRes: &tmrpctypes.ResultBlockResults{ + Height: 1, + BeginBlockEvents: []types.Event{ + { + Type: evm.EventTypeBlockBloom, + }, + }, + }, + registerMock: func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFeeDisabled(queryClient) + }, + expBaseFee: nil, + expPass: true, + }, + { + name: "pass - base fee or london fork not enabled", + blockRes: &tmrpctypes.ResultBlockResults{Height: 1}, + registerMock: func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFeeDisabled(queryClient) + }, + expBaseFee: nil, + expPass: true, + }, + { + "pass", + &tmrpctypes.ResultBlockResults{Height: 1}, + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterBaseFee(queryClient, baseFee) + }, + baseFee.BigInt(), + true, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + baseFee, err := s.backend.BaseFee(tc.blockRes) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expBaseFee, baseFee) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestChainId() { + expChainIDNumber := eth.ParseEIP155ChainIDNumber(eth.EIP155ChainID_Testnet) + expChainID := (*hexutil.Big)(expChainIDNumber) + testCases := []struct { + name string + registerMock func() + expChainID *hexutil.Big + expPass bool + }{ + { + "pass - block is at or past the EIP-155 replay-protection fork block, return chainID from config ", + func() { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsInvalidHeight(queryClient, &header, int64(1)) + }, + expChainID, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + chainID, err := s.backend.ChainID() + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expChainID, chainID) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetCoinbase() { + validatorAcc := sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + testCases := []struct { + name string + registerMock func() + accAddr sdk.AccAddress + expPass bool + }{ + { + "fail - Can't retrieve status from node", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterStatusError(client) + }, + validatorAcc, + false, + }, + { + "fail - Can't query validator account", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStatus(client) + RegisterValidatorAccountError(queryClient) + }, + validatorAcc, + false, + }, + { + "pass - Gets coinbase account", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStatus(client) + RegisterValidatorAccount(queryClient, validatorAcc) + }, + validatorAcc, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + accAddr, err := s.backend.GetCoinbase() + + if tc.expPass { + s.Require().Equal(tc.accAddr, accAddr) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSuggestGasTipCap() { + testCases := []struct { + name string + registerMock func() + baseFee *big.Int + expGasTipCap *big.Int + expPass bool + }{ + { + "pass - London hardfork not enabled or feemarket not enabled ", + func() {}, + nil, + big.NewInt(0), + true, + }, + { + "pass - Gets the suggest gas tip cap ", + func() {}, + nil, + big.NewInt(0), + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + maxDelta, err := s.backend.SuggestGasTipCap(tc.baseFee) + + if tc.expPass { + s.Require().Equal(tc.expGasTipCap, maxDelta) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestFeeHistory() { + testCases := []struct { + name string + registerMock func(validator sdk.AccAddress) + userBlockCount ethrpc.DecimalOrHex + latestBlock ethrpc.BlockNumber + expFeeHistory *rpc.FeeHistoryResult + validator sdk.AccAddress + expPass bool + }{ + { + "fail - can't get params ", + func(validator sdk.AccAddress) { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.cfg.JSONRPC.FeeHistoryCap = 0 + RegisterParamsError(queryClient, &header, ethrpc.BlockNumber(1).Int64()) + }, + 1, + -1, + nil, + nil, + false, + }, + { + "fail - user block count higher than max block count ", + func(validator sdk.AccAddress) { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + s.backend.cfg.JSONRPC.FeeHistoryCap = 0 + RegisterParams(queryClient, &header, ethrpc.BlockNumber(1).Int64()) + }, + 1, + -1, + nil, + nil, + false, + }, + { + "fail - Tendermint block fetching error ", + func(validator sdk.AccAddress) { + client := s.backend.clientCtx.Client.(*mocks.Client) + s.backend.cfg.JSONRPC.FeeHistoryCap = 2 + RegisterBlockError(client, ethrpc.BlockNumber(1).Int64()) + }, + 1, + 1, + nil, + nil, + false, + }, + { + "fail - Eth block fetching error", + func(validator sdk.AccAddress) { + client := s.backend.clientCtx.Client.(*mocks.Client) + s.backend.cfg.JSONRPC.FeeHistoryCap = 2 + _, err := RegisterBlock(client, ethrpc.BlockNumber(1).Int64(), nil) + s.Require().NoError(err) + RegisterBlockResultsError(client, 1) + }, + 1, + 1, + nil, + nil, + true, + }, + { + name: "pass - Valid FeeHistoryResults object", + registerMock: func(validator sdk.AccAddress) { + baseFee := sdkmath.NewInt(1) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + s.backend.cfg.JSONRPC.FeeHistoryCap = 2 + blockHeight := int64(1) + _, err := RegisterBlock(client, blockHeight, nil) + s.Require().NoError(err) + + _, err = RegisterBlockResults(client, blockHeight) + s.Require().NoError(err) + + RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) + RegisterConsensusParams(client, blockHeight) + + header := new(metadata.MD) + RegisterParams(queryClient, header, blockHeight) + RegisterParamsWithoutHeader(queryClient, blockHeight) + }, + userBlockCount: 1, + latestBlock: 1, + expFeeHistory: &rpc.FeeHistoryResult{ + OldestBlock: (*hexutil.Big)(big.NewInt(1)), + BaseFee: []*hexutil.Big{(*hexutil.Big)(big.NewInt(1)), (*hexutil.Big)(big.NewInt(1))}, + GasUsedRatio: []float64{0}, + Reward: [][]*hexutil.Big{{(*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0))}}, + }, + validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + expPass: true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock(tc.validator) + + feeHistory, err := s.backend.FeeHistory(tc.userBlockCount, tc.latestBlock, []float64{25, 50, 75, 100}) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(feeHistory, tc.expFeeHistory) + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/client_test.go b/eth/rpc/backend/client_test.go new file mode 100644 index 000000000..21f22ce4d --- /dev/null +++ b/eth/rpc/backend/client_test.go @@ -0,0 +1,286 @@ +package backend + +import ( + "context" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/bytes" + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cometbft/cometbft/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" +) + +// Client defines a mocked object that implements the Tendermint JSON-RPC Client +// interface. It allows for performing Client queries without having to run a +// Tendermint RPC Client server. +// +// To use a mock method it has to be registered in a given test. +var _ tmrpcclient.Client = &mocks.Client{} + +// Tx Search +func RegisterTxSearch(client *mocks.Client, query string, txBz []byte) { + resulTxs := []*tmrpctypes.ResultTx{{Tx: txBz}} + client.On("TxSearch", rpc.NewContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). + Return(&tmrpctypes.ResultTxSearch{Txs: resulTxs, TotalCount: 1}, nil) +} + +func RegisterTxSearchEmpty(client *mocks.Client, query string) { + client.On("TxSearch", rpc.NewContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). + Return(&tmrpctypes.ResultTxSearch{}, nil) +} + +func RegisterTxSearchError(client *mocks.Client, query string) { + client.On("TxSearch", rpc.NewContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Broadcast Tx +func RegisterBroadcastTx(client *mocks.Client, tx types.Tx) { + client.On("BroadcastTxSync", context.Background(), tx). + Return(&tmrpctypes.ResultBroadcastTx{}, nil) +} + +func RegisterBroadcastTxError(client *mocks.Client, tx types.Tx) { + client.On("BroadcastTxSync", context.Background(), tx). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Unconfirmed Transactions +func RegisterUnconfirmedTxs(client *mocks.Client, limit *int, txs []types.Tx) { + client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). + Return(&tmrpctypes.ResultUnconfirmedTxs{Txs: txs}, nil) +} + +func RegisterUnconfirmedTxsEmpty(client *mocks.Client, limit *int) { + client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). + Return(&tmrpctypes.ResultUnconfirmedTxs{ + Txs: make([]types.Tx, 2), + }, nil) +} + +func RegisterUnconfirmedTxsError(client *mocks.Client, limit *int) { + client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Status +func RegisterStatus(client *mocks.Client) { + client.On("Status", rpc.NewContextWithHeight(1)). + Return(&tmrpctypes.ResultStatus{}, nil) +} + +func RegisterStatusError(client *mocks.Client) { + client.On("Status", rpc.NewContextWithHeight(1)). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Block +func RegisterBlockMultipleTxs( + client *mocks.Client, + height int64, + txs []types.Tx, +) (*tmrpctypes.ResultBlock, error) { + block := types.MakeBlock(height, txs, nil, nil) + block.ChainID = ChainID + resBlock := &tmrpctypes.ResultBlock{Block: block} + client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + return resBlock, nil +} + +func RegisterBlock( + client *mocks.Client, + height int64, + tx []byte, +) (*tmrpctypes.ResultBlock, error) { + // without tx + if tx == nil { + emptyBlock := types.MakeBlock(height, []types.Tx{}, nil, nil) + emptyBlock.ChainID = ChainID + resBlock := &tmrpctypes.ResultBlock{Block: emptyBlock} + client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + return resBlock, nil + } + + // with tx + block := types.MakeBlock(height, []types.Tx{tx}, nil, nil) + block.ChainID = ChainID + resBlock := &tmrpctypes.ResultBlock{Block: block} + client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + return resBlock, nil +} + +// Block returns error +func RegisterBlockError(client *mocks.Client, height int64) { + client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Block not found +func RegisterBlockNotFound( + client *mocks.Client, + height int64, +) (*tmrpctypes.ResultBlock, error) { + client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(&tmrpctypes.ResultBlock{Block: nil}, nil) + + return &tmrpctypes.ResultBlock{Block: nil}, nil +} + +func TestRegisterBlock(t *testing.T) { + client := mocks.NewClient(t) + height := rpc.BlockNumber(1).Int64() + _, err := RegisterBlock(client, height, nil) + require.NoError(t, err) + + res, err := client.Block(rpc.NewContextWithHeight(height), &height) + + emptyBlock := types.MakeBlock(height, []types.Tx{}, nil, nil) + emptyBlock.ChainID = ChainID + resBlock := &tmrpctypes.ResultBlock{Block: emptyBlock} + require.Equal(t, resBlock, res) + require.NoError(t, err) +} + +// ConsensusParams +func RegisterConsensusParams(client *mocks.Client, height int64) { + consensusParams := types.DefaultConsensusParams() + client.On("ConsensusParams", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(&tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, nil) +} + +func RegisterConsensusParamsError(client *mocks.Client, height int64) { + client.On("ConsensusParams", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(nil, errortypes.ErrInvalidRequest) +} + +func TestRegisterConsensusParams(t *testing.T) { + client := mocks.NewClient(t) + height := int64(1) + RegisterConsensusParams(client, height) + + res, err := client.ConsensusParams(rpc.NewContextWithHeight(height), &height) + consensusParams := types.DefaultConsensusParams() + require.Equal(t, &tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, res) + require.NoError(t, err) +} + +// BlockResults + +func RegisterBlockResultsWithEventLog(client *mocks.Client, height int64) (*tmrpctypes.ResultBlockResults, error) { + res := &tmrpctypes.ResultBlockResults{ + Height: height, + TxsResults: []*abci.ResponseDeliverTx{ + {Code: 0, GasUsed: 0, Events: []abci.Event{{ + Type: evm.EventTypeTxLog, + Attributes: []abci.EventAttribute{{ + Key: evm.AttributeKeyTxLog, + Value: "{\"test\": \"hello\"}", // TODO refactor the value to unmarshall to a evmtypes.Log struct successfully + Index: true, + }}, + }}}, + }, + } + client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(res, nil) + return res, nil +} + +func RegisterBlockResults( + client *mocks.Client, + height int64, +) (*tmrpctypes.ResultBlockResults, error) { + res := &tmrpctypes.ResultBlockResults{ + Height: height, + TxsResults: []*abci.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + } + + client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(res, nil) + return res, nil +} + +func RegisterBlockResultsError(client *mocks.Client, height int64) { + client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). + Return(nil, errortypes.ErrInvalidRequest) +} + +func TestRegisterBlockResults(t *testing.T) { + client := mocks.NewClient(t) + height := int64(1) + _, err := RegisterBlockResults(client, height) + require.NoError(t, err) + + res, err := client.BlockResults(rpc.NewContextWithHeight(height), &height) + expRes := &tmrpctypes.ResultBlockResults{ + Height: height, + TxsResults: []*abci.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, + } + require.Equal(t, expRes, res) + require.NoError(t, err) +} + +// BlockByHash +func RegisterBlockByHash( + client *mocks.Client, + _ common.Hash, + tx []byte, +) (*tmrpctypes.ResultBlock, error) { + block := types.MakeBlock(1, []types.Tx{tx}, nil, nil) + resBlock := &tmrpctypes.ResultBlock{Block: block} + + client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). + Return(resBlock, nil) + return resBlock, nil +} + +func RegisterBlockByHashError(client *mocks.Client, _ common.Hash, _ []byte) { + client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). + Return(nil, errortypes.ErrInvalidRequest) +} + +func RegisterBlockByHashNotFound(client *mocks.Client, _ common.Hash, _ []byte) { + client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). + Return(nil, nil) +} + +func RegisterABCIQueryWithOptions(client *mocks.Client, height int64, path string, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions) { + client.On("ABCIQueryWithOptions", context.Background(), path, data, opts). + Return(&tmrpctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte{2}, // TODO replace with data.Bytes(), + Height: height, + }, + }, nil) +} + +func RegisterABCIQueryWithOptionsError(clients *mocks.Client, path string, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions) { + clients.On("ABCIQueryWithOptions", context.Background(), path, data, opts). + Return(nil, errortypes.ErrInvalidRequest) +} + +func RegisterABCIQueryAccount(clients *mocks.Client, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions, acc client.Account) { + baseAccount := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence()) + accAny, _ := codectypes.NewAnyWithValue(baseAccount) + accResponse := authtypes.QueryAccountResponse{Account: accAny} + respBz, _ := accResponse.Marshal() + clients.On("ABCIQueryWithOptions", context.Background(), "/cosmos.auth.v1beta1.Query/Account", data, opts). + Return(&tmrpctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: respBz, + Height: 1, + }, + }, nil) +} diff --git a/eth/rpc/backend/evm_query_client_test.go b/eth/rpc/backend/evm_query_client_test.go new file mode 100644 index 000000000..3e1b31624 --- /dev/null +++ b/eth/rpc/backend/evm_query_client_test.go @@ -0,0 +1,324 @@ +package backend + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "testing" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + "github.com/ethereum/go-ethereum/common" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +// QueryClient defines a mocked object that implements the ethermint GRPC +// QueryClient interface. It allows for performing QueryClient queries without having +// to run a ethermint GRPC server. +// +// To use a mock method it has to be registered in a given test. +var _ evm.QueryClient = &mocks.EVMQueryClient{} + +var TEST_CHAIN_ID_NUMBER = eth.ParseEIP155ChainIDNumber(eth.EIP155ChainID_Testnet).Int64() + +// TraceTransaction +func RegisterTraceTransactionWithPredecessors( + queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, predecessors []*evm.MsgEthereumTx, +) { + data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} + queryClient.On("TraceTx", rpc.NewContextWithHeight(1), + &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, Predecessors: predecessors, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + Return(&evm.QueryTraceTxResponse{Data: data}, nil) +} + +func RegisterTraceTransaction( + queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, +) { + data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} + queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + Return(&evm.QueryTraceTxResponse{Data: data}, nil) +} + +func RegisterTraceTransactionError( + queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, +) { + queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER}). + Return(nil, errortypes.ErrInvalidRequest) +} + +// TraceBlock +func RegisterTraceBlock( + queryClient *mocks.EVMQueryClient, txs []*evm.MsgEthereumTx, +) { + data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} + queryClient.On("TraceBlock", rpc.NewContextWithHeight(1), + &evm.QueryTraceBlockRequest{Txs: txs, BlockNumber: 1, TraceConfig: &evm.TraceConfig{}, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + Return(&evm.QueryTraceBlockResponse{Data: data}, nil) +} + +func RegisterTraceBlockError(queryClient *mocks.EVMQueryClient) { + queryClient.On("TraceBlock", rpc.NewContextWithHeight(1), &evm.QueryTraceBlockRequest{}). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Params +func RegisterParams( + queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, +) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). + Return(&evm.QueryParamsResponse{}, nil). + Run(func(args mock.Arguments) { + // If Params call is successful, also update the header height + arg := args.Get(2).(grpc.HeaderCallOption) + h := metadata.MD{} + h.Set(grpctypes.GRPCBlockHeightHeader, fmt.Sprint(height)) + *arg.HeaderAddr = h + }) +} + +func RegisterParamsWithoutHeader( + queryClient *mocks.EVMQueryClient, height int64, +) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}). + Return(&evm.QueryParamsResponse{Params: evm.DefaultParams()}, nil) +} + +func RegisterParamsInvalidHeader( + queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, +) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). + Return(&evm.QueryParamsResponse{}, nil). + Run(func(args mock.Arguments) { + // If Params call is successful, also update the header height + arg := args.Get(2).(grpc.HeaderCallOption) + h := metadata.MD{} + *arg.HeaderAddr = h + }) +} + +func RegisterParamsInvalidHeight(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). + Return(&evm.QueryParamsResponse{}, nil). + Run(func(args mock.Arguments) { + // If Params call is successful, also update the header height + arg := args.Get(2).(grpc.HeaderCallOption) + h := metadata.MD{} + h.Set(grpctypes.GRPCBlockHeightHeader, "invalid") + *arg.HeaderAddr = h + }) +} + +func RegisterParamsWithoutHeaderError(queryClient *mocks.EVMQueryClient, height int64) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Params returns error +func RegisterParamsError( + queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, +) { + queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). + Return(nil, errortypes.ErrInvalidRequest) +} + +func TestRegisterParams(t *testing.T) { + var header metadata.MD + queryClient := mocks.NewEVMQueryClient(t) + + height := int64(1) + RegisterParams(queryClient, &header, height) + + _, err := queryClient.Params(rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(&header)) + require.NoError(t, err) + blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) + headerHeight, err := strconv.ParseInt(blockHeightHeader[0], 10, 64) + require.NoError(t, err) + require.Equal(t, height, headerHeight) +} + +func TestRegisterParamsError(t *testing.T) { + queryClient := mocks.NewEVMQueryClient(t) + RegisterBaseFeeError(queryClient) + _, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) + require.Error(t, err) +} + +// ETH Call +func RegisterEthCall( + queryClient *mocks.EVMQueryClient, request *evm.EthCallRequest, +) { + ctx, _ := context.WithCancel(rpc.NewContextWithHeight(1)) //nolint + queryClient.On("EthCall", ctx, request). + Return(&evm.MsgEthereumTxResponse{}, nil) +} + +func RegisterEthCallError( + queryClient *mocks.EVMQueryClient, request *evm.EthCallRequest, +) { + ctx, _ := context.WithCancel(rpc.NewContextWithHeight(1)) //nolint + queryClient.On("EthCall", ctx, request). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Estimate Gas +func RegisterEstimateGas( + queryClient *mocks.EVMQueryClient, args evm.JsonTxArgs, +) { + bz, _ := json.Marshal(args) + queryClient.On("EstimateGas", rpc.NewContextWithHeight(1), &evm.EthCallRequest{Args: bz, ChainId: args.ChainID.ToInt().Int64()}). + Return(&evm.EstimateGasResponse{}, nil) +} + +// BaseFee +func RegisterBaseFee( + queryClient *mocks.EVMQueryClient, baseFee math.Int, +) { + queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). + Return(&evm.QueryBaseFeeResponse{BaseFee: &baseFee}, nil) +} + +// Base fee returns error +func RegisterBaseFeeError(queryClient *mocks.EVMQueryClient) { + queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). + Return(&evm.QueryBaseFeeResponse{}, evm.ErrInvalidBaseFee) +} + +// Base fee not enabled +func RegisterBaseFeeDisabled(queryClient *mocks.EVMQueryClient) { + queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). + Return(&evm.QueryBaseFeeResponse{}, nil) +} + +func TestRegisterBaseFee(t *testing.T) { + baseFee := math.NewInt(1) + queryClient := mocks.NewEVMQueryClient(t) + RegisterBaseFee(queryClient, baseFee) + res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) + require.Equal(t, &evm.QueryBaseFeeResponse{BaseFee: &baseFee}, res) + require.NoError(t, err) +} + +func TestRegisterBaseFeeError(t *testing.T) { + queryClient := mocks.NewEVMQueryClient(t) + RegisterBaseFeeError(queryClient) + res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) + require.Equal(t, &evm.QueryBaseFeeResponse{}, res) + require.Error(t, err) +} + +func TestRegisterBaseFeeDisabled(t *testing.T) { + queryClient := mocks.NewEVMQueryClient(t) + RegisterBaseFeeDisabled(queryClient) + res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) + require.Equal(t, &evm.QueryBaseFeeResponse{}, res) + require.NoError(t, err) +} + +// ValidatorAccount +func RegisterValidatorAccount( + queryClient *mocks.EVMQueryClient, validator sdk.AccAddress, +) { + queryClient.On("ValidatorAccount", rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}). + Return(&evm.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil) +} + +func RegisterValidatorAccountError(queryClient *mocks.EVMQueryClient) { + queryClient.On("ValidatorAccount", rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}). + Return(nil, status.Error(codes.InvalidArgument, "empty request")) +} + +func TestRegisterValidatorAccount(t *testing.T) { + queryClient := mocks.NewEVMQueryClient(t) + + validator := sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + RegisterValidatorAccount(queryClient, validator) + res, err := queryClient.ValidatorAccount(rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}) + require.Equal(t, &evm.QueryValidatorAccountResponse{AccountAddress: validator.String()}, res) + require.NoError(t, err) +} + +// Code +func RegisterCode( + queryClient *mocks.EVMQueryClient, addr common.Address, code []byte, +) { + queryClient.On("Code", rpc.NewContextWithHeight(1), &evm.QueryCodeRequest{Address: addr.String()}). + Return(&evm.QueryCodeResponse{Code: code}, nil) +} + +func RegisterCodeError(queryClient *mocks.EVMQueryClient, addr common.Address) { + queryClient.On("Code", rpc.NewContextWithHeight(1), &evm.QueryCodeRequest{Address: addr.String()}). + Return(nil, errortypes.ErrInvalidRequest) +} + +// Storage +func RegisterStorageAt( + queryClient *mocks.EVMQueryClient, addr common.Address, + key string, storage string, +) { + queryClient.On("Storage", rpc.NewContextWithHeight(1), &evm.QueryStorageRequest{Address: addr.String(), Key: key}). + Return(&evm.QueryStorageResponse{Value: storage}, nil) +} + +func RegisterStorageAtError( + queryClient *mocks.EVMQueryClient, addr common.Address, key string, +) { + queryClient.On("Storage", rpc.NewContextWithHeight(1), &evm.QueryStorageRequest{Address: addr.String(), Key: key}). + Return(nil, errortypes.ErrInvalidRequest) +} + +func RegisterAccount( + queryClient *mocks.EVMQueryClient, addr common.Address, height int64, +) { + queryClient.On("Account", rpc.NewContextWithHeight(height), &evm.QueryAccountRequest{Address: addr.String()}). + Return(&evm.QueryAccountResponse{ + Balance: "0", + CodeHash: "", + Nonce: 0, + }, + nil, + ) +} + +// Balance +func RegisterBalance( + queryClient *mocks.EVMQueryClient, addr common.Address, height int64, +) { + queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). + Return(&evm.QueryBalanceResponse{Balance: "1"}, nil) +} + +func RegisterBalanceInvalid( + queryClient *mocks.EVMQueryClient, addr common.Address, height int64, +) { + queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). + Return(&evm.QueryBalanceResponse{Balance: "invalid"}, nil) +} + +func RegisterBalanceNegative( + queryClient *mocks.EVMQueryClient, addr common.Address, height int64, +) { + queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). + Return(&evm.QueryBalanceResponse{Balance: "-1"}, nil) +} + +func RegisterBalanceError( + queryClient *mocks.EVMQueryClient, addr common.Address, height int64, +) { + queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). + Return(nil, errortypes.ErrInvalidRequest) +} diff --git a/eth/rpc/backend/filters.go b/eth/rpc/backend/filters.go new file mode 100644 index 000000000..244cda0e5 --- /dev/null +++ b/eth/rpc/backend/filters.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" +) + +// GetLogs returns all the logs from all the ethereum transactions in a block. +func (b *Backend) GetLogs(hash common.Hash) ([][]*gethcore.Log, error) { + resBlock, err := b.TendermintBlockByHash(hash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", hash) + } + return b.GetLogsByHeight(&resBlock.Block.Header.Height) +} + +// GetLogsByHeight returns all the logs from all the ethereum transactions in a block. +func (b *Backend) GetLogsByHeight(height *int64) ([][]*gethcore.Log, error) { + // NOTE: we query the state in case the tx result logs are not persisted after an upgrade. + blockRes, err := b.TendermintBlockResultByNumber(height) + if err != nil { + return nil, err + } + + return GetLogsFromBlockResults(blockRes) +} + +// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained +// by the chain indexer. +func (b *Backend) BloomStatus() (uint64, uint64) { + return 4096, 0 +} diff --git a/eth/rpc/backend/filters_test.go b/eth/rpc/backend/filters_test.go new file mode 100644 index 000000000..831a2c1bf --- /dev/null +++ b/eth/rpc/backend/filters_test.go @@ -0,0 +1,123 @@ +package backend + +import ( + "encoding/json" + + tmtypes "github.com/cometbft/cometbft/types" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" +) + +func (s *BackendSuite) TestGetLogs() { + _, bz := s.buildEthereumTx() + block := tmtypes.MakeBlock(1, []tmtypes.Tx{bz}, nil, nil) + logs := make([]*evm.Log, 0, 1) + var log evm.Log + err := json.Unmarshal([]byte("{\"test\": \"hello\"}"), &log) // TODO refactor this to unmarshall to a log struct successfully + s.Require().NoError(err) + + logs = append(logs, &log) + + testCases := []struct { + name string + registerMock func(hash common.Hash) + blockHash common.Hash + expLogs [][]*gethcore.Log + expPass bool + }{ + { + "fail - no block with that hash", + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashNotFound(client, hash, bz) + }, + common.Hash{}, + nil, + false, + }, + { + "fail - error fetching block by hash", + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, hash, bz) + }, + common.Hash{}, + nil, + false, + }, + { + "fail - error getting block results", + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, bz) + s.Require().NoError(err) + RegisterBlockResultsError(client, 1) + }, + common.Hash{}, + nil, + false, + }, + { + "success - getting logs with block hash", + func(hash common.Hash) { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, hash, bz) + s.Require().NoError(err) + _, err = RegisterBlockResultsWithEventLog(client, rpc.BlockNumber(1).Int64()) + s.Require().NoError(err) + }, + common.BytesToHash(block.Hash()), + [][]*gethcore.Log{evm.LogsToEthereum(logs)}, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + tc.registerMock(tc.blockHash) + logs, err := s.backend.GetLogs(tc.blockHash) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expLogs, logs) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestBloomStatus() { + testCases := []struct { + name string + registerMock func() + expResult uint64 + expPass bool + }{ + { + "pass - returns the BloomBitsBlocks and the number of processed sections maintained", + func() {}, + 4096, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + tc.registerMock() + bloom, _ := s.backend.BloomStatus() + + if tc.expPass { + s.Require().Equal(tc.expResult, bloom) + } + }) + } +} diff --git a/eth/rpc/backend/mocks/client.go b/eth/rpc/backend/mocks/client.go new file mode 100644 index 000000000..2fabaa113 --- /dev/null +++ b/eth/rpc/backend/mocks/client.go @@ -0,0 +1,887 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + bytes "github.com/cometbft/cometbft/libs/bytes" + client "github.com/cometbft/cometbft/rpc/client" + + context "context" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + + log "github.com/cometbft/cometbft/libs/log" + + mock "github.com/stretchr/testify/mock" + + types "github.com/cometbft/cometbft/types" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// ABCIInfo provides a mock function with given fields: _a0 +func (_m *Client) ABCIInfo(_a0 context.Context) (*coretypes.ResultABCIInfo, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultABCIInfo + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultABCIInfo); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ABCIQuery provides a mock function with given fields: ctx, path, data +func (_m *Client) ABCIQuery(ctx context.Context, path string, data bytes.HexBytes) (*coretypes.ResultABCIQuery, error) { + ret := _m.Called(ctx, path, data) + + var r0 *coretypes.ResultABCIQuery + if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes) *coretypes.ResultABCIQuery); ok { + r0 = rf(ctx, path, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIQuery) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes) error); ok { + r1 = rf(ctx, path, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ABCIQueryWithOptions provides a mock function with given fields: ctx, path, data, opts +func (_m *Client) ABCIQueryWithOptions(ctx context.Context, path string, data bytes.HexBytes, opts client.ABCIQueryOptions) (*coretypes.ResultABCIQuery, error) { + ret := _m.Called(ctx, path, data, opts) + + var r0 *coretypes.ResultABCIQuery + if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) *coretypes.ResultABCIQuery); ok { + r0 = rf(ctx, path, data, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIQuery) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) error); ok { + r1 = rf(ctx, path, data, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Block provides a mock function with given fields: ctx, height +func (_m *Client) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultBlock + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlock); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlock) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockByHash provides a mock function with given fields: ctx, hash +func (_m *Client) BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) { + ret := _m.Called(ctx, hash) + + var r0 *coretypes.ResultBlock + if rf, ok := ret.Get(0).(func(context.Context, []byte) *coretypes.ResultBlock); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlock) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockResults provides a mock function with given fields: ctx, height +func (_m *Client) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultBlockResults + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlockResults); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockResults) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockSearch provides a mock function with given fields: ctx, query, page, perPage, orderBy +func (_m *Client) BlockSearch(ctx context.Context, query string, page *int, perPage *int, orderBy string) (*coretypes.ResultBlockSearch, error) { + ret := _m.Called(ctx, query, page, perPage, orderBy) + + var r0 *coretypes.ResultBlockSearch + if rf, ok := ret.Get(0).(func(context.Context, string, *int, *int, string) *coretypes.ResultBlockSearch); ok { + r0 = rf(ctx, query, page, perPage, orderBy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockSearch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *int, *int, string) error); ok { + r1 = rf(ctx, query, page, perPage, orderBy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockchainInfo provides a mock function with given fields: ctx, minHeight, maxHeight +func (_m *Client) BlockchainInfo(ctx context.Context, minHeight int64, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { + ret := _m.Called(ctx, minHeight, maxHeight) + + var r0 *coretypes.ResultBlockchainInfo + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *coretypes.ResultBlockchainInfo); ok { + r0 = rf(ctx, minHeight, maxHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockchainInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok { + r1 = rf(ctx, minHeight, maxHeight) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastEvidence provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastEvidence(_a0 context.Context, _a1 types.Evidence) (*coretypes.ResultBroadcastEvidence, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastEvidence + if rf, ok := ret.Get(0).(func(context.Context, types.Evidence) *coretypes.ResultBroadcastEvidence); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastEvidence) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Evidence) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxAsync provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxAsync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxCommit provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxCommit(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTxCommit, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTxCommit + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTxCommit); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTxCommit) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxSync provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxSync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckTx provides a mock function with given fields: _a0, _a1 +func (_m *Client) CheckTx(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultCheckTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultCheckTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultCheckTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultCheckTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Commit provides a mock function with given fields: ctx, height +func (_m *Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultCommit + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultCommit); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultCommit) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsensusParams provides a mock function with given fields: ctx, height +func (_m *Client) ConsensusParams(ctx context.Context, height *int64) (*coretypes.ResultConsensusParams, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultConsensusParams + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultConsensusParams); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultConsensusParams) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsensusState provides a mock function with given fields: _a0 +func (_m *Client) ConsensusState(_a0 context.Context) (*coretypes.ResultConsensusState, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultConsensusState + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultConsensusState); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultConsensusState) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DumpConsensusState provides a mock function with given fields: _a0 +func (_m *Client) DumpConsensusState(_a0 context.Context) (*coretypes.ResultDumpConsensusState, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultDumpConsensusState + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultDumpConsensusState); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultDumpConsensusState) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Genesis provides a mock function with given fields: _a0 +func (_m *Client) Genesis(_a0 context.Context) (*coretypes.ResultGenesis, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultGenesis + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultGenesis); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultGenesis) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenesisChunked provides a mock function with given fields: _a0, _a1 +func (_m *Client) GenesisChunked(_a0 context.Context, _a1 uint) (*coretypes.ResultGenesisChunk, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultGenesisChunk + if rf, ok := ret.Get(0).(func(context.Context, uint) *coretypes.ResultGenesisChunk); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultGenesisChunk) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Header provides a mock function with given fields: ctx, height +func (_m *Client) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultHeader + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultHeader); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHeader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HeaderByHash provides a mock function with given fields: ctx, hash +func (_m *Client) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { + ret := _m.Called(ctx, hash) + + var r0 *coretypes.ResultHeader + if rf, ok := ret.Get(0).(func(context.Context, bytes.HexBytes) *coretypes.ResultHeader); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHeader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, bytes.HexBytes) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Health provides a mock function with given fields: _a0 +func (_m *Client) Health(_a0 context.Context) (*coretypes.ResultHealth, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultHealth + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultHealth); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHealth) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsRunning provides a mock function with given fields: +func (_m *Client) IsRunning() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NetInfo provides a mock function with given fields: _a0 +func (_m *Client) NetInfo(_a0 context.Context) (*coretypes.ResultNetInfo, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultNetInfo + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultNetInfo); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultNetInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NumUnconfirmedTxs provides a mock function with given fields: _a0 +func (_m *Client) NumUnconfirmedTxs(_a0 context.Context) (*coretypes.ResultUnconfirmedTxs, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultUnconfirmedTxs + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultUnconfirmedTxs); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OnReset provides a mock function with given fields: +func (_m *Client) OnReset() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OnStart provides a mock function with given fields: +func (_m *Client) OnStart() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OnStop provides a mock function with given fields: +func (_m *Client) OnStop() { + _m.Called() +} + +// Quit provides a mock function with given fields: +func (_m *Client) Quit() <-chan struct{} { + ret := _m.Called() + + var r0 <-chan struct{} + if rf, ok := ret.Get(0).(func() <-chan struct{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan struct{}) + } + } + + return r0 +} + +// Reset provides a mock function with given fields: +func (_m *Client) Reset() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetLogger provides a mock function with given fields: _a0 +func (_m *Client) SetLogger(_a0 log.Logger) { + _m.Called(_a0) +} + +// Start provides a mock function with given fields: +func (_m *Client) Start() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Status provides a mock function with given fields: _a0 +func (_m *Client) Status(_a0 context.Context) (*coretypes.ResultStatus, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultStatus + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultStatus); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Stop provides a mock function with given fields: +func (_m *Client) Stop() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// String provides a mock function with given fields: +func (_m *Client) String() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Subscribe provides a mock function with given fields: ctx, subscriber, query, outCapacity +func (_m *Client) Subscribe(ctx context.Context, subscriber string, query string, outCapacity ...int) (<-chan coretypes.ResultEvent, error) { + _va := make([]interface{}, len(outCapacity)) + for _i := range outCapacity { + _va[_i] = outCapacity[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, subscriber, query) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 <-chan coretypes.ResultEvent + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...int) <-chan coretypes.ResultEvent); ok { + r0 = rf(ctx, subscriber, query, outCapacity...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan coretypes.ResultEvent) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...int) error); ok { + r1 = rf(ctx, subscriber, query, outCapacity...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Tx provides a mock function with given fields: ctx, hash, prove +func (_m *Client) Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) { + ret := _m.Called(ctx, hash, prove) + + var r0 *coretypes.ResultTx + if rf, ok := ret.Get(0).(func(context.Context, []byte, bool) *coretypes.ResultTx); ok { + r0 = rf(ctx, hash, prove) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, []byte, bool) error); ok { + r1 = rf(ctx, hash, prove) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxSearch provides a mock function with given fields: ctx, query, prove, page, perPage, orderBy +func (_m *Client) TxSearch(ctx context.Context, query string, prove bool, page *int, perPage *int, orderBy string) (*coretypes.ResultTxSearch, error) { + ret := _m.Called(ctx, query, prove, page, perPage, orderBy) + + var r0 *coretypes.ResultTxSearch + if rf, ok := ret.Get(0).(func(context.Context, string, bool, *int, *int, string) *coretypes.ResultTxSearch); ok { + r0 = rf(ctx, query, prove, page, perPage, orderBy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultTxSearch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bool, *int, *int, string) error); ok { + r1 = rf(ctx, query, prove, page, perPage, orderBy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnconfirmedTxs provides a mock function with given fields: ctx, limit +func (_m *Client) UnconfirmedTxs(ctx context.Context, limit *int) (*coretypes.ResultUnconfirmedTxs, error) { + ret := _m.Called(ctx, limit) + + var r0 *coretypes.ResultUnconfirmedTxs + if rf, ok := ret.Get(0).(func(context.Context, *int) *coretypes.ResultUnconfirmedTxs); ok { + r0 = rf(ctx, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int) error); ok { + r1 = rf(ctx, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Unsubscribe provides a mock function with given fields: ctx, subscriber, query +func (_m *Client) Unsubscribe(ctx context.Context, subscriber string, query string) error { + ret := _m.Called(ctx, subscriber, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, subscriber, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnsubscribeAll provides a mock function with given fields: ctx, subscriber +func (_m *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { + ret := _m.Called(ctx, subscriber) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, subscriber) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Validators provides a mock function with given fields: ctx, height, page, perPage +func (_m *Client) Validators(ctx context.Context, height *int64, page *int, perPage *int) (*coretypes.ResultValidators, error) { + ret := _m.Called(ctx, height, page, perPage) + + var r0 *coretypes.ResultValidators + if rf, ok := ret.Get(0).(func(context.Context, *int64, *int, *int) *coretypes.ResultValidators); ok { + r0 = rf(ctx, height, page, perPage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultValidators) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64, *int, *int) error); ok { + r1 = rf(ctx, height, page, perPage) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t mockConstructorTestingTNewClient) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/eth/rpc/backend/mocks/evm_query_client.go b/eth/rpc/backend/mocks/evm_query_client.go new file mode 100644 index 000000000..3e06374e6 --- /dev/null +++ b/eth/rpc/backend/mocks/evm_query_client.go @@ -0,0 +1,393 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// EVMQueryClient is an autogenerated mock type for the EVMQueryClient type +type EVMQueryClient struct { + mock.Mock +} + +// Account provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) Account(ctx context.Context, in *evm.QueryAccountRequest, opts ...grpc.CallOption) (*evm.QueryAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryAccountRequest, ...grpc.CallOption) *evm.QueryAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Balance provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) Balance(ctx context.Context, in *evm.QueryBalanceRequest, opts ...grpc.CallOption) (*evm.QueryBalanceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryBalanceResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBalanceRequest, ...grpc.CallOption) *evm.QueryBalanceResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryBalanceResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryBalanceRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BaseFee provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) BaseFee(ctx context.Context, in *evm.QueryBaseFeeRequest, opts ...grpc.CallOption) (*evm.QueryBaseFeeResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryBaseFeeResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBaseFeeRequest, ...grpc.CallOption) *evm.QueryBaseFeeResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryBaseFeeResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryBaseFeeRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Code provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) Code(ctx context.Context, in *evm.QueryCodeRequest, opts ...grpc.CallOption) (*evm.QueryCodeResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryCodeResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryCodeRequest, ...grpc.CallOption) *evm.QueryCodeResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryCodeResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryCodeRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CosmosAccount provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) CosmosAccount(ctx context.Context, in *evm.QueryCosmosAccountRequest, opts ...grpc.CallOption) (*evm.QueryCosmosAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryCosmosAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryCosmosAccountRequest, ...grpc.CallOption) *evm.QueryCosmosAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryCosmosAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryCosmosAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EstimateGas provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) EstimateGas(ctx context.Context, in *evm.EthCallRequest, opts ...grpc.CallOption) (*evm.EstimateGasResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.EstimateGasResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) *evm.EstimateGasResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.EstimateGasResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EthCall provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) EthCall(ctx context.Context, in *evm.EthCallRequest, opts ...grpc.CallOption) (*evm.MsgEthereumTxResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.MsgEthereumTxResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) *evm.MsgEthereumTxResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.MsgEthereumTxResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Params provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) Params(ctx context.Context, in *evm.QueryParamsRequest, opts ...grpc.CallOption) (*evm.QueryParamsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryParamsResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryParamsRequest, ...grpc.CallOption) *evm.QueryParamsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryParamsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryParamsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) Storage(ctx context.Context, in *evm.QueryStorageRequest, opts ...grpc.CallOption) (*evm.QueryStorageResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryStorageResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryStorageRequest, ...grpc.CallOption) *evm.QueryStorageResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryStorageResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryStorageRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TraceBlock provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) TraceBlock(ctx context.Context, in *evm.QueryTraceBlockRequest, opts ...grpc.CallOption) (*evm.QueryTraceBlockResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryTraceBlockResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceBlockRequest, ...grpc.CallOption) *evm.QueryTraceBlockResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryTraceBlockResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryTraceBlockRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TraceTx provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) TraceTx(ctx context.Context, in *evm.QueryTraceTxRequest, opts ...grpc.CallOption) (*evm.QueryTraceTxResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryTraceTxResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) *evm.QueryTraceTxResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryTraceTxResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ValidatorAccount provides a mock function with given fields: ctx, in, opts +func (_m *EVMQueryClient) ValidatorAccount(ctx context.Context, in *evm.QueryValidatorAccountRequest, opts ...grpc.CallOption) (*evm.QueryValidatorAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *evm.QueryValidatorAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryValidatorAccountRequest, ...grpc.CallOption) *evm.QueryValidatorAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm.QueryValidatorAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryValidatorAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewEVMQueryClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewEVMQueryClient creates a new instance of EVMQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEVMQueryClient(t mockConstructorTestingTNewEVMQueryClient) *EVMQueryClient { + mock := &EVMQueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/eth/rpc/backend/node_info.go b/eth/rpc/backend/node_info.go new file mode 100644 index 000000000..e99cee5d9 --- /dev/null +++ b/eth/rpc/backend/node_info.go @@ -0,0 +1,341 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "fmt" + "math/big" + "time" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + tmtypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + sdkcrypto "github.com/cosmos/cosmos-sdk/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdkconfig "github.com/cosmos/cosmos-sdk/server/config" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/NibiruChain/nibiru/app/server/config" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// Accounts returns the list of accounts available to this node. +func (b *Backend) Accounts() ([]common.Address, error) { + addresses := make([]common.Address, 0) // return [] instead of nil if empty + + infos, err := b.clientCtx.Keyring.List() + if err != nil { + return addresses, err + } + + for _, info := range infos { + pubKey, err := info.GetPubKey() + if err != nil { + return nil, err + } + addressBytes := pubKey.Address().Bytes() + addresses = append(addresses, common.BytesToAddress(addressBytes)) + } + + return addresses, nil +} + +// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (b *Backend) Syncing() (interface{}, error) { + status, err := b.clientCtx.Client.Status(b.ctx) + if err != nil { + return false, err + } + + if !status.SyncInfo.CatchingUp { + return false, nil + } + + return map[string]interface{}{ + "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), + "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), + // "highestBlock": nil, // NA + // "pulledStates": nil, // NA + // "knownStates": nil, // NA + }, nil +} + +// SetEtherbase sets the etherbase of the miner +func (b *Backend) SetEtherbase(etherbase common.Address) bool { + delAddr, err := b.GetCoinbase() + if err != nil { + b.logger.Debug("failed to get coinbase address", "error", err.Error()) + return false + } + + withdrawAddr := sdk.AccAddress(etherbase.Bytes()) + msg := distributiontypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr) + + if err := msg.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return false + } + + // Assemble transaction from fields + builder, ok := b.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + if !ok { + b.logger.Debug("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error()) + return false + } + + err = builder.SetMsgs(msg) + if err != nil { + b.logger.Error("builder.SetMsgs failed", "error", err.Error()) + return false + } + + // Fetch minimun gas price to calculate fees using the configuration. + minGasPrices := b.cfg.GetMinGasPrices() + if len(minGasPrices) == 0 || minGasPrices.Empty() { + b.logger.Debug("the minimun fee is not set") + return false + } + minGasPriceValue := minGasPrices[0].Amount + denom := minGasPrices[0].Denom + + delCommonAddr := common.BytesToAddress(delAddr.Bytes()) + nonce, err := b.GetTransactionCount(delCommonAddr, rpc.EthPendingBlockNumber) + if err != nil { + b.logger.Debug("failed to get nonce", "error", err.Error()) + return false + } + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(b.clientCtx.ChainID). + WithKeybase(b.clientCtx.Keyring). + WithTxConfig(b.clientCtx.TxConfig). + WithSequence(uint64(*nonce)). + WithGasAdjustment(1.25) + + _, gas, err := tx.CalculateGas(b.clientCtx, txFactory, msg) + if err != nil { + b.logger.Debug("failed to calculate gas", "error", err.Error()) + return false + } + + txFactory = txFactory.WithGas(gas) + + value := new(big.Int).SetUint64(gas * minGasPriceValue.Ceil().TruncateInt().Uint64()) + fees := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(value))} + builder.SetFeeAmount(fees) + builder.SetGasLimit(gas) + + keyInfo, err := b.clientCtx.Keyring.KeyByAddress(delAddr) + if err != nil { + b.logger.Debug("failed to get the wallet address using the keyring", "error", err.Error()) + return false + } + + if err := tx.Sign(txFactory, keyInfo.Name, builder, false); err != nil { + b.logger.Debug("failed to sign tx", "error", err.Error()) + return false + } + + // Encode transaction by default Tx encoder + txEncoder := b.clientCtx.TxConfig.TxEncoder() + txBytes, err := txEncoder(builder.GetTx()) + if err != nil { + b.logger.Debug("failed to encode eth tx using default encoder", "error", err.Error()) + return false + } + + tmHash := common.BytesToHash(tmtypes.Tx(txBytes).Hash()) + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Debug("failed to broadcast tx", "error", err.Error()) + return false + } + + b.logger.Debug("broadcasted tx to set miner withdraw address (etherbase)", "hash", tmHash.String()) + return true +} + +// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory. +// The name of the key will have the format "personal_", where is the total number of +// keys stored on the keyring. +// +// NOTE: The key will be both armored and encrypted using the same passphrase. +func (b *Backend) ImportRawKey(privkey, password string) (common.Address, error) { + priv, err := crypto.HexToECDSA(privkey) + if err != nil { + return common.Address{}, err + } + + privKey := ðsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)} + + addr := sdk.AccAddress(privKey.PubKey().Address().Bytes()) + ethereumAddr := common.BytesToAddress(addr) + + // return if the key has already been imported + if _, err := b.clientCtx.Keyring.KeyByAddress(addr); err == nil { + return ethereumAddr, nil + } + + // ignore error as we only care about the length of the list + list, _ := b.clientCtx.Keyring.List() // #nosec G703 + privKeyName := fmt.Sprintf("personal_%d", len(list)) + + armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType) + + if err := b.clientCtx.Keyring.ImportPrivKey(privKeyName, armor, password); err != nil { + return common.Address{}, err + } + + b.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String()) + + return ethereumAddr, nil +} + +// ListAccounts will return a list of addresses for accounts this node manages. +func (b *Backend) ListAccounts() ([]common.Address, error) { + addrs := []common.Address{} + + list, err := b.clientCtx.Keyring.List() + if err != nil { + return nil, err + } + + for _, info := range list { + pubKey, err := info.GetPubKey() + if err != nil { + return nil, err + } + addrs = append(addrs, common.BytesToAddress(pubKey.Address())) + } + + return addrs, nil +} + +// NewAccount will create a new account and returns the address for the new account. +func (b *Backend) NewMnemonic(uid string, + _ keyring.Language, + hdPath, + bip39Passphrase string, + algo keyring.SignatureAlgo, +) (*keyring.Record, error) { + info, _, err := b.clientCtx.Keyring.NewMnemonic(uid, keyring.English, hdPath, bip39Passphrase, algo) + if err != nil { + return nil, err + } + return info, err +} + +// SetGasPrice sets the minimum accepted gas price for the miner. +// NOTE: this function accepts only integers to have the same interface than go-eth +// to use float values, the gas prices must be configured using the configuration file +func (b *Backend) SetGasPrice(gasPrice hexutil.Big) bool { + appConf, err := config.GetConfig(b.clientCtx.Viper) + if err != nil { + b.logger.Debug("could not get the server config", "error", err.Error()) + return false + } + + var unit string + minGasPrices := appConf.GetMinGasPrices() + + // fetch the base denom from the sdk Config in case it's not currently defined on the node config + if len(minGasPrices) == 0 || minGasPrices.Empty() { + var err error + unit, err = sdk.GetBaseDenom() + if err != nil { + b.logger.Debug("could not get the denom of smallest unit registered", "error", err.Error()) + return false + } + } else { + unit = minGasPrices[0].Denom + } + + c := sdk.NewDecCoin(unit, sdkmath.NewIntFromBigInt(gasPrice.ToInt())) + + appConf.SetMinGasPrices(sdk.DecCoins{c}) + sdkconfig.WriteConfigFile(b.clientCtx.Viper.ConfigFileUsed(), appConf) + b.logger.Info("Your configuration file was modified. Please RESTART your node.", "gas-price", c.String()) + return true +} + +// UnprotectedAllowed returns the node configuration value for allowing +// unprotected transactions (i.e not replay-protected) +func (b Backend) UnprotectedAllowed() bool { + return b.allowUnprotectedTxs +} + +// RPCGasCap is the global gas cap for eth-call variants. +func (b *Backend) RPCGasCap() uint64 { + return b.cfg.JSONRPC.GasCap +} + +// RPCEVMTimeout is the global evm timeout for eth-call variants. +func (b *Backend) RPCEVMTimeout() time.Duration { + return b.cfg.JSONRPC.EVMTimeout +} + +// RPCGasCap is the global gas cap for eth-call variants. +func (b *Backend) RPCTxFeeCap() float64 { + return b.cfg.JSONRPC.TxFeeCap +} + +// RPCFilterCap is the limit for total number of filters that can be created +func (b *Backend) RPCFilterCap() int32 { + return b.cfg.JSONRPC.FilterCap +} + +// RPCFeeHistoryCap is the limit for total number of blocks that can be fetched +func (b *Backend) RPCFeeHistoryCap() int32 { + return b.cfg.JSONRPC.FeeHistoryCap +} + +// RPCLogsCap defines the max number of results can be returned from single `eth_getLogs` query. +func (b *Backend) RPCLogsCap() int32 { + return b.cfg.JSONRPC.LogsCap +} + +// RPCBlockRangeCap defines the max block range allowed for `eth_getLogs` query. +func (b *Backend) RPCBlockRangeCap() int32 { + return b.cfg.JSONRPC.BlockRangeCap +} + +// RPCMinGasPrice returns the minimum gas price for a transaction obtained from +// the node config. If set value is 0, it will default to 20. + +func (b *Backend) RPCMinGasPrice() int64 { + evmParams, err := b.queryClient.Params(b.ctx, &evm.QueryParamsRequest{}) + if err != nil { + return eth.DefaultGasPrice + } + + minGasPrice := b.cfg.GetMinGasPrices() + amt := minGasPrice.AmountOf(evmParams.Params.EvmDenom).TruncateInt64() + if amt == 0 { + return eth.DefaultGasPrice + } + + return amt +} diff --git a/eth/rpc/backend/node_info_test.go b/eth/rpc/backend/node_info_test.go new file mode 100644 index 000000000..87fe46b63 --- /dev/null +++ b/eth/rpc/backend/node_info_test.go @@ -0,0 +1,335 @@ +package backend + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/spf13/viper" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" +) + +func (s *BackendSuite) TestRPCMinGasPrice() { + testCases := []struct { + name string + registerMock func() + expMinGasPrice int64 + expPass bool + }{ + { + "pass - default gas price", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeaderError(queryClient, 1) + }, + eth.DefaultGasPrice, + true, + }, + { + "pass - min gas price is 0", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + }, + eth.DefaultGasPrice, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + minPrice := s.backend.RPCMinGasPrice() + if tc.expPass { + s.Require().Equal(tc.expMinGasPrice, minPrice) + } else { + s.Require().NotEqual(tc.expMinGasPrice, minPrice) + } + }) + } +} + +func (s *BackendSuite) TestSetGasPrice() { + defaultGasPrice := (*hexutil.Big)(big.NewInt(1)) + testCases := []struct { + name string + registerMock func() + gasPrice hexutil.Big + expOutput bool + }{ + { + "pass - cannot get server config", + func() { + s.backend.clientCtx.Viper = viper.New() + }, + *defaultGasPrice, + false, + }, + { + "pass - cannot find coin denom", + func() { + s.backend.clientCtx.Viper = viper.New() + s.backend.clientCtx.Viper.Set("telemetry.global-labels", []interface{}{}) + }, + *defaultGasPrice, + false, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + output := s.backend.SetGasPrice(tc.gasPrice) + s.Require().Equal(tc.expOutput, output) + }) + } +} + +// TODO: Combine these 2 into one test since the code is identical +func (s *BackendSuite) TestListAccounts() { + testCases := []struct { + name string + registerMock func() + expAddr []common.Address + expPass bool + }{ + { + "pass - returns empty address", + func() {}, + []common.Address{}, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + output, err := s.backend.ListAccounts() + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expAddr, output) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestAccounts() { + testCases := []struct { + name string + registerMock func() + expAddr []common.Address + expPass bool + }{ + { + "pass - returns empty address", + func() {}, + []common.Address{}, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + output, err := s.backend.Accounts() + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expAddr, output) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSyncing() { + testCases := []struct { + name string + registerMock func() + expResponse interface{} + expPass bool + }{ + { + "fail - Can't get status", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterStatusError(client) + }, + false, + false, + }, + { + "pass - Node not catching up", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterStatus(client) + }, + false, + true, + }, + { + "pass - Node is catching up", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterStatus(client) + status, _ := client.Status(s.backend.ctx) + status.SyncInfo.CatchingUp = true + }, + map[string]interface{}{ + "startingBlock": hexutil.Uint64(0), + "currentBlock": hexutil.Uint64(0), + }, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + output, err := s.backend.Syncing() + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expResponse, output) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSetEtherbase() { + testCases := []struct { + name string + registerMock func() + etherbase common.Address + expResult bool + }{ + { + "pass - Failed to get coinbase address", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterStatusError(client) + }, + common.Address{}, + false, + }, + { + "pass - the minimum fee is not set", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStatus(client) + RegisterValidatorAccount(queryClient, s.acc) + }, + common.Address{}, + false, + }, + { + "fail - error querying for account", + func() { + var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterStatus(client) + RegisterValidatorAccount(queryClient, s.acc) + RegisterParams(queryClient, &header, 1) + c := sdk.NewDecCoin(eth.EthBaseDenom, math.NewIntFromBigInt(big.NewInt(1))) + s.backend.cfg.SetMinGasPrices(sdk.DecCoins{c}) + delAddr, _ := s.backend.GetCoinbase() + // account, _ := suite.backend.clientCtx.AccountRetriever.GetAccount(suite.backend.clientCtx, delAddr) + delCommonAddr := common.BytesToAddress(delAddr.Bytes()) + request := &authtypes.QueryAccountRequest{Address: sdk.AccAddress(delCommonAddr.Bytes()).String()} + requestMarshal, _ := request.Marshal() + RegisterABCIQueryWithOptionsError( + client, + "/cosmos.auth.v1beta1.Query/Account", + requestMarshal, + tmrpcclient.ABCIQueryOptions{Height: int64(1), Prove: false}, + ) + }, + common.Address{}, + false, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + output := s.backend.SetEtherbase(tc.etherbase) + + s.Require().Equal(tc.expResult, output) + }) + } +} + +func (s *BackendSuite) TestImportRawKey() { + priv, _ := ethsecp256k1.GenerateKey() + privHex := common.Bytes2Hex(priv.Bytes()) + pubAddr := common.BytesToAddress(priv.PubKey().Address().Bytes()) + + testCases := []struct { + name string + registerMock func() + privKey string + password string + expAddr common.Address + expPass bool + }{ + { + "fail - not a valid private key", + func() {}, + "", + "", + common.Address{}, + false, + }, + { + "pass - returning correct address", + func() {}, + privHex, + "", + pubAddr, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + output, err := s.backend.ImportRawKey(tc.privKey, tc.password) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expAddr, output) + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/sign_tx.go b/eth/rpc/backend/sign_tx.go new file mode 100644 index 000000000..806df48d4 --- /dev/null +++ b/eth/rpc/backend/sign_tx.go @@ -0,0 +1,156 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "errors" + "fmt" + "math/big" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// SendTransaction sends transaction based on received args using Node's key to sign it +func (b *Backend) SendTransaction(args evm.JsonTxArgs) (common.Hash, error) { + // Look up the wallet containing the requested signer + _, err := b.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.GetFrom().Bytes())) + if err != nil { + b.logger.Error("failed to find key in keyring", "address", args.GetFrom(), "error", err.Error()) + return common.Hash{}, fmt.Errorf("failed to find key in the node's keyring; %s; %s", keystore.ErrNoMatch, err.Error()) + } + + if args.ChainID != nil && (b.chainID).Cmp((*big.Int)(args.ChainID)) != 0 { + return common.Hash{}, fmt.Errorf("chainId does not match node's (have=%v, want=%v)", args.ChainID, (*hexutil.Big)(b.chainID)) + } + + args, err = b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } + + bn, err := b.BlockNumber() + if err != nil { + b.logger.Debug("failed to fetch latest block number", "error", err.Error()) + return common.Hash{}, err + } + + signer := gethcore.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) + + // LegacyTx derives chainID from the signature. To make sure the msg.ValidateBasic makes + // the corresponding chainID validation, we need to sign the transaction before calling it + + // Sign transaction + msg := args.ToTransaction() + if err := msg.Sign(signer, b.clientCtx.Keyring); err != nil { + b.logger.Debug("failed to sign tx", "error", err.Error()) + return common.Hash{}, err + } + + if err := msg.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return common.Hash{}, err + } + + // Query params to use the EVM denomination + res, err := b.queryClient.QueryClient.Params(b.ctx, &evm.QueryParamsRequest{}) + if err != nil { + b.logger.Error("failed to query evm params", "error", err.Error()) + return common.Hash{}, err + } + + // Assemble transaction from fields + tx, err := msg.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) + if err != nil { + b.logger.Error("build cosmos tx failed", "error", err.Error()) + return common.Hash{}, err + } + + // Encode transaction by default Tx encoder + txEncoder := b.clientCtx.TxConfig.TxEncoder() + txBytes, err := txEncoder(tx) + if err != nil { + b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) + return common.Hash{}, err + } + + ethTx := msg.AsTransaction() + + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + txHash := ethTx.Hash() + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Error("failed to broadcast tx", "error", err.Error()) + return txHash, err + } + + // Return transaction hash + return txHash, nil +} + +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (b *Backend) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + from := sdk.AccAddress(address.Bytes()) + + _, err := b.clientCtx.Keyring.KeyByAddress(from) + if err != nil { + b.logger.Error("failed to find key in keyring", "address", address.String()) + return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) + } + + // Sign the requested hash with the wallet + signature, _, err := b.clientCtx.Keyring.SignByAddress(from, data) + if err != nil { + b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) + return nil, err + } + + signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil +} + +// SignTypedData signs EIP-712 conformant typed data +func (b *Backend) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { + from := sdk.AccAddress(address.Bytes()) + + _, err := b.clientCtx.Keyring.KeyByAddress(from) + if err != nil { + b.logger.Error("failed to find key in keyring", "address", address.String()) + return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) + } + + sigHash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + return nil, err + } + + // Sign the requested hash with the wallet + signature, _, err := b.clientCtx.Keyring.SignByAddress(from, sigHash) + if err != nil { + b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) + return nil, err + } + + signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil +} diff --git a/eth/rpc/backend/sign_tx_test.go b/eth/rpc/backend/sign_tx_test.go new file mode 100644 index 000000000..7141bf5bb --- /dev/null +++ b/eth/rpc/backend/sign_tx_test.go @@ -0,0 +1,270 @@ +package backend + +import ( + "fmt" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + goethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *BackendSuite) TestSendTransaction() { + gasPrice := new(hexutil.Big) + gas := hexutil.Uint64(1) + zeroGas := hexutil.Uint64(0) + toAddr := evmtest.NewEthAddr() + priv, _ := ethsecp256k1.GenerateKey() + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + nonce := hexutil.Uint64(1) + baseFee := math.NewInt(1) + callArgsDefault := evm.JsonTxArgs{ + From: &from, + To: &toAddr, + GasPrice: gasPrice, + Gas: &gas, + Nonce: &nonce, + } + + hash := common.Hash{} + + testCases := []struct { + name string + registerMock func() + args evm.JsonTxArgs + expHash common.Hash + expPass bool + }{ + { + "fail - Can't find account in Keyring", + func() {}, + evm.JsonTxArgs{}, + hash, + false, + }, + { + "fail - Block error can't set Tx defaults", + func() { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + s.Require().NoError(err) + RegisterParams(queryClient, &header, 1) + RegisterBlockError(client, 1) + }, + callArgsDefault, + hash, + false, + }, + { + "fail - Cannot validate transaction gas set to 0", + func() { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + s.Require().NoError(err) + RegisterParams(queryClient, &header, 1) + _, err = RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + RegisterParamsWithoutHeader(queryClient, 1) + }, + evm.JsonTxArgs{ + From: &from, + To: &toAddr, + GasPrice: gasPrice, + Gas: &zeroGas, + Nonce: &nonce, + }, + hash, + false, + }, + { + "fail - Cannot broadcast transaction", + func() { + client, txBytes := broadcastTx(s, priv, baseFee, callArgsDefault) + RegisterBroadcastTxError(client, txBytes) + }, + callArgsDefault, + common.Hash{}, + false, + }, + { + "pass - Return the transaction hash", + func() { + client, txBytes := broadcastTx(s, priv, baseFee, callArgsDefault) + RegisterBroadcastTx(client, txBytes) + }, + callArgsDefault, + hash, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + if tc.expPass { + // Sign the transaction and get the hash + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) + msg := callArgsDefault.ToTransaction() + err := msg.Sign(ethSigner, s.backend.clientCtx.Keyring) + s.Require().NoError(err) + tc.expHash = msg.AsTransaction().Hash() + } + responseHash, err := s.backend.SendTransaction(tc.args) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expHash, responseHash) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSign() { + from, priv := evmtest.PrivKeyEth() + testCases := []struct { + name string + registerMock func() + fromAddr common.Address + inputBz hexutil.Bytes + expPass bool + }{ + { + "fail - can't find key in Keyring", + func() {}, + from, + nil, + false, + }, + { + "pass - sign nil data", + func() { + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + s.Require().NoError(err) + }, + from, + nil, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + responseBz, err := s.backend.Sign(tc.fromAddr, tc.inputBz) + if tc.expPass { + signature, _, err := s.backend.clientCtx.Keyring.SignByAddress((sdk.AccAddress)(from.Bytes()), tc.inputBz) + signature[goethcrypto.RecoveryIDOffset] += 27 + s.Require().NoError(err) + s.Require().Equal((hexutil.Bytes)(signature), responseBz) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestSignTypedData() { + from, priv := evmtest.PrivKeyEth() + testCases := []struct { + name string + registerMock func() + fromAddr common.Address + inputTypedData apitypes.TypedData + expPass bool + }{ + { + "fail - can't find key in Keyring", + func() {}, + from, + apitypes.TypedData{}, + false, + }, + { + "fail - empty TypeData", + func() { + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + s.Require().NoError(err) + }, + from, + apitypes.TypedData{}, + false, + }, + // TODO: Generate a TypedData msg + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + responseBz, err := s.backend.SignTypedData(tc.fromAddr, tc.inputTypedData) + + if tc.expPass { + sigHash, _, _ := apitypes.TypedDataAndHash(tc.inputTypedData) + signature, _, err := s.backend.clientCtx.Keyring.SignByAddress((sdk.AccAddress)(from.Bytes()), sigHash) + signature[goethcrypto.RecoveryIDOffset] += 27 + s.Require().NoError(err) + s.Require().Equal((hexutil.Bytes)(signature), responseBz) + } else { + s.Require().Error(err) + } + }) + } +} + +func broadcastTx( + s *BackendSuite, + priv *ethsecp256k1.PrivKey, + baseFee math.Int, + callArgsDefault evm.JsonTxArgs, +) (client *mocks.Client, txBytes []byte) { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client = s.backend.clientCtx.Client.(*mocks.Client) + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + _ = s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + RegisterParams(queryClient, &header, 1) + _, err := RegisterBlock(client, 1, nil) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, baseFee) + RegisterParamsWithoutHeader(queryClient, 1) + ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) + msg := callArgsDefault.ToTransaction() + err = msg.Sign(ethSigner, s.backend.clientCtx.Keyring) + s.Require().NoError(err) + tx, _ := msg.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) + txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() + txBytes, _ = txEncoder(tx) + return client, txBytes +} diff --git a/eth/rpc/backend/tracing.go b/eth/rpc/backend/tracing.go new file mode 100644 index 000000000..eb823d823 --- /dev/null +++ b/eth/rpc/backend/tracing.go @@ -0,0 +1,211 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "encoding/json" + "fmt" + "math" + + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// 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 *evm.TraceConfig) (interface{}, error) { + // Get transaction by hash + transaction, err := b.GetTxByEthHash(hash) + if err != nil { + b.logger.Debug("tx not found", "hash", hash) + return nil, err + } + + // check if block number is 0 + if transaction.Height == 0 { + return nil, errors.New("genesis is not traceable") + } + + blk, err := b.TendermintBlockByNumber(rpc.BlockNumber(transaction.Height)) + if err != nil { + b.logger.Debug("block not found", "height", transaction.Height) + return nil, err + } + + // check tx index is not out of bound + if len(blk.Block.Txs) > math.MaxUint32 { + return nil, fmt.Errorf("tx count %d is overfloing", len(blk.Block.Txs)) + } + txsLen := uint32(len(blk.Block.Txs)) // #nosec G701 -- checked for int overflow already + if txsLen < transaction.TxIndex { + b.logger.Debug("tx index out of bounds", "index", transaction.TxIndex, "hash", hash.String(), "height", blk.Block.Height) + return nil, fmt.Errorf("transaction not included in block %v", blk.Block.Height) + } + + var predecessors []*evm.MsgEthereumTx + for _, txBz := range blk.Block.Txs[:transaction.TxIndex] { + tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) + if err != nil { + b.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) + continue + } + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + continue + } + + predecessors = append(predecessors, ethMsg) + } + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(blk.Block.Txs[transaction.TxIndex]) + if err != nil { + b.logger.Debug("tx not found", "hash", hash) + return nil, err + } + + // add predecessor messages in current cosmos tx + index := int(transaction.MsgIndex) // #nosec G701 + for i := 0; i < index; i++ { + ethMsg, ok := tx.GetMsgs()[i].(*evm.MsgEthereumTx) + if !ok { + continue + } + predecessors = append(predecessors, ethMsg) + } + + ethMessage, ok := tx.GetMsgs()[transaction.MsgIndex].(*evm.MsgEthereumTx) + if !ok { + b.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) + return nil, fmt.Errorf("invalid transaction type %T", tx) + } + + nc, ok := b.clientCtx.Client.(tmrpcclient.NetworkClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + + cp, err := nc.ConsensusParams(b.ctx, &blk.Block.Height) + if err != nil { + return nil, err + } + + traceTxRequest := evm.QueryTraceTxRequest{ + Msg: ethMessage, + Predecessors: predecessors, + BlockNumber: blk.Block.Height, + BlockTime: blk.Block.Time, + BlockHash: common.Bytes2Hex(blk.BlockID.Hash), + ProposerAddress: sdk.ConsAddress(blk.Block.ProposerAddress), + ChainId: b.chainID.Int64(), + BlockMaxGas: cp.ConsensusParams.Block.MaxGas, + } + + if config != nil { + traceTxRequest.TraceConfig = config + } + + // minus one to get the context of block beginning + contextHeight := transaction.Height - 1 + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + traceResult, err := b.queryClient.TraceTx(rpc.NewContextWithHeight(contextHeight), &traceTxRequest) + if err != nil { + return nil, err + } + + // Response format is unknown due to custom tracer config param + // More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered + var decodedResult interface{} + err = json.Unmarshal(traceResult.Data, &decodedResult) + if err != nil { + return nil, err + } + + return decodedResult, nil +} + +// TraceBlock configures a new tracer according to the provided configuration, and +// executes all the transactions contained within. The return value will be one item +// per transaction, dependent on the requested tracer. +func (b *Backend) TraceBlock(height rpc.BlockNumber, + config *evm.TraceConfig, + block *tmrpctypes.ResultBlock, +) ([]*evm.TxTraceResult, error) { + txs := block.Block.Txs + txsLength := len(txs) + + if txsLength == 0 { + // If there are no transactions return empty array + return []*evm.TxTraceResult{}, nil + } + + txDecoder := b.clientCtx.TxConfig.TxDecoder() + + var txsMessages []*evm.MsgEthereumTx + for i, tx := range txs { + decodedTx, err := txDecoder(tx) + if err != nil { + b.logger.Error("failed to decode transaction", "hash", txs[i].Hash(), "error", err.Error()) + continue + } + + for _, msg := range decodedTx.GetMsgs() { + ethMessage, ok := msg.(*evm.MsgEthereumTx) + if !ok { + // Just considers Ethereum transactions + continue + } + txsMessages = append(txsMessages, ethMessage) + } + } + + // minus one to get the context at the beginning of the block + contextHeight := height - 1 + if contextHeight < 1 { + // 0 is a special value for `ContextWithHeight`. + contextHeight = 1 + } + ctxWithHeight := rpc.NewContextWithHeight(int64(contextHeight)) + + nc, ok := b.clientCtx.Client.(tmrpcclient.NetworkClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + + cp, err := nc.ConsensusParams(b.ctx, &block.Block.Height) + if err != nil { + return nil, err + } + + traceBlockRequest := &evm.QueryTraceBlockRequest{ + Txs: txsMessages, + TraceConfig: config, + BlockNumber: block.Block.Height, + BlockTime: block.Block.Time, + BlockHash: common.Bytes2Hex(block.BlockID.Hash), + ProposerAddress: sdk.ConsAddress(block.Block.ProposerAddress), + ChainId: b.chainID.Int64(), + BlockMaxGas: cp.ConsensusParams.Block.MaxGas, + } + + res, err := b.queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) + if err != nil { + return nil, err + } + + decodedResults := make([]*evm.TxTraceResult, txsLength) + if err := json.Unmarshal(res.Data, &decodedResults); err != nil { + return nil, err + } + + return decodedResults, nil +} diff --git a/eth/rpc/backend/tracing_test.go b/eth/rpc/backend/tracing_test.go new file mode 100644 index 000000000..53f7b9f85 --- /dev/null +++ b/eth/rpc/backend/tracing_test.go @@ -0,0 +1,265 @@ +package backend + +import ( + "fmt" + + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + tmlog "github.com/cometbft/cometbft/libs/log" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/crypto" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/eth/indexer" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" +) + +func (s *BackendSuite) TestTraceTransaction() { + msgEthereumTx, _ := s.buildEthereumTx() + msgEthereumTx2, _ := s.buildEthereumTx() + + txHash := msgEthereumTx.AsTransaction().Hash() + txHash2 := msgEthereumTx2.AsTransaction().Hash() + + priv, _ := ethsecp256k1.GenerateKey() + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsWithoutHeader(queryClient, 1) + + armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") + _ = s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") + + ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) + + txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() + + msgEthereumTx.From = from.String() + _ = msgEthereumTx.Sign(ethSigner, s.signer) + + tx, _ := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) + txBz, _ := txEncoder(tx) + + msgEthereumTx2.From = from.String() + _ = msgEthereumTx2.Sign(ethSigner, s.signer) + + tx2, _ := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) + txBz2, _ := txEncoder(tx2) + + testCases := []struct { + name string + registerMock func() + block *types.Block + responseBlock []*abci.ResponseDeliverTx + expResult interface{} + expPass bool + }{ + { + "fail - tx not found", + func() {}, + &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + nil, + false, + }, + { + "fail - block not found", + func() { + // var header metadata.MD + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, 1) + }, + &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + map[string]interface{}{"test": "hello"}, + false, + }, + { + "pass - transaction found in a block with multiple transactions", + func() { + var ( + queryClient = s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client = s.backend.clientCtx.Client.(*mocks.Client) + height int64 = 1 + ) + _, err := RegisterBlockMultipleTxs(client, height, []types.Tx{txBz, txBz2}) + s.Require().NoError(err) + RegisterTraceTransactionWithPredecessors(queryClient, msgEthereumTx, []*evm.MsgEthereumTx{msgEthereumTx}) + RegisterConsensusParams(client, height) + }, + &types.Block{Header: types.Header{Height: 1, ChainID: ChainID}, Data: types.Data{Txs: []types.Tx{txBz, txBz2}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash2.Hex()}, + {Key: "txIndex", Value: "1"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + map[string]interface{}{"test": "hello"}, + true, + }, + { + "pass - transaction found", + func() { + var ( + queryClient = s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client = s.backend.clientCtx.Client.(*mocks.Client) + height int64 = 1 + ) + _, err := RegisterBlock(client, height, txBz) + s.Require().NoError(err) + RegisterTraceTransaction(queryClient, msgEthereumTx) + RegisterConsensusParams(client, height) + }, + &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + map[string]interface{}{"test": "hello"}, + true, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + db := dbm.NewMemDB() + s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) + + err := s.backend.indexer.IndexBlock(tc.block, tc.responseBlock) + s.Require().NoError(err) + txResult, err := s.backend.TraceTransaction(txHash, nil) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expResult, txResult) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestTraceBlock() { + msgEthTx, bz := s.buildEthereumTx() + emptyBlock := types.MakeBlock(1, []types.Tx{}, nil, nil) + emptyBlock.ChainID = ChainID + filledBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) + filledBlock.ChainID = ChainID + resBlockEmpty := tmrpctypes.ResultBlock{Block: emptyBlock, BlockID: emptyBlock.LastBlockID} + resBlockFilled := tmrpctypes.ResultBlock{Block: filledBlock, BlockID: filledBlock.LastBlockID} + + testCases := []struct { + name string + registerMock func() + expTraceResults []*evm.TxTraceResult + resBlock *tmrpctypes.ResultBlock + config *evm.TraceConfig + expPass bool + }{ + { + "pass - no transaction returning empty array", + func() {}, + []*evm.TxTraceResult{}, + &resBlockEmpty, + &evm.TraceConfig{}, + true, + }, + { + "fail - cannot unmarshal data", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterTraceBlock(queryClient, []*evm.MsgEthereumTx{msgEthTx}) + RegisterConsensusParams(client, 1) + }, + []*evm.TxTraceResult{}, + &resBlockFilled, + &evm.TraceConfig{}, + false, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("case %s", tc.name), func() { + s.SetupTest() // reset test and queries + tc.registerMock() + + traceResults, err := s.backend.TraceBlock(1, tc.config, tc.resBlock) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(tc.expTraceResults, traceResults) + } else { + s.Require().Error(err) + } + }) + } +} diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go new file mode 100644 index 000000000..afbf523ca --- /dev/null +++ b/eth/rpc/backend/tx_info.go @@ -0,0 +1,420 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "fmt" + "math" + "math/big" + + errorsmod "cosmossdk.io/errors" + + tmrpcclient "github.com/cometbft/cometbft/rpc/client" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash +func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, error) { + res, err := b.GetTxByEthHash(txHash) + hexTx := txHash.Hex() + + if err != nil { + return b.getTransactionByHashPending(txHash) + } + + block, err := b.TendermintBlockByNumber(rpc.BlockNumber(res.Height)) + if err != nil { + return nil, err + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex]) + if err != nil { + return nil, err + } + + // the `res.MsgIndex` is inferred from tx index, should be within the bound. + msg, ok := tx.GetMsgs()[res.MsgIndex].(*evm.MsgEthereumTx) + if !ok { + return nil, errors.New("invalid ethereum tx") + } + + blockRes, err := b.TendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + b.logger.Debug("block result not found", "height", block.Block.Height, "error", err.Error()) + return nil, nil + } + + if res.EthTxIndex == -1 { + // Fallback to find tx index by iterating all valid eth transactions + msgs := b.EthMsgsFromTendermintBlock(block, blockRes) + for i := range msgs { + if msgs[i].Hash == hexTx { + if i > math.MaxInt32 { + return nil, errors.New("tx index overflow") + } + res.EthTxIndex = int32(i) //#nosec G701 -- checked for int overflow already + break + } + } + } + // if we still unable to find the eth tx index, return error, shouldn't happen. + if res.EthTxIndex == -1 { + return nil, errors.New("can't find index of ethereum tx") + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", blockRes.Height, "error", err) + } + + height := uint64(res.Height) //#nosec G701 -- checked for int overflow already + index := uint64(res.EthTxIndex) //#nosec G701 -- checked for int overflow already + return rpc.NewRPCTxFromMsg( + msg, + common.BytesToHash(block.BlockID.Hash.Bytes()), + height, + index, + baseFee, + b.chainID, + ) +} + +// getTransactionByHashPending find pending tx from mempool +func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpc.EthTxJsonRPC, error) { + hexTx := txHash.Hex() + // try to find tx in mempool + txs, err := b.PendingTransactions() + if err != nil { + b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + for _, tx := range txs { + msg, err := evm.UnwrapEthereumMsg(tx, txHash) + if err != nil { + // not ethereum tx + continue + } + + if msg.Hash == hexTx { + // use zero block values since it's not included in a block yet + rpctx, err := rpc.NewRPCTxFromMsg( + msg, + common.Hash{}, + uint64(0), + uint64(0), + nil, + b.chainID, + ) + if err != nil { + return nil, err + } + return rpctx, nil + } + } + + b.logger.Debug("tx not found", "hash", hexTx) + return nil, nil +} + +// GetGasUsed returns gasUsed from transaction, patching to +// price * gas in the event the tx is reverted. +func (b *Backend) GetGasUsed(res *eth.TxResult, price *big.Int, gas uint64) uint64 { + if res.Failed && res.Height < b.cfg.JSONRPC.FixRevertGasRefundHeight { + return new(big.Int).Mul(price, new(big.Int).SetUint64(gas)).Uint64() + } + return res.GasUsed +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + hexTx := hash.Hex() + b.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) + + res, err := b.GetTxByEthHash(hash) + if err != nil { + b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + resBlock, err := b.TendermintBlockByNumber(rpc.BlockNumber(res.Height)) + if err != nil { + b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) + return nil, nil + } + tx, err := b.clientCtx.TxConfig.TxDecoder()(resBlock.Block.Txs[res.TxIndex]) + if err != nil { + b.logger.Debug("decoding failed", "error", err.Error()) + return nil, fmt.Errorf("failed to decode tx: %w", err) + } + ethMsg := tx.GetMsgs()[res.MsgIndex].(*evm.MsgEthereumTx) + + txData, err := evm.UnpackTxData(ethMsg.Data) + if err != nil { + b.logger.Error("failed to unpack tx data", "error", err.Error()) + return nil, err + } + + cumulativeGasUsed := uint64(0) + blockRes, err := b.TendermintBlockResultByNumber(&res.Height) + if err != nil { + b.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error()) + return nil, nil + } + for _, txResult := range blockRes.TxsResults[0:res.TxIndex] { + cumulativeGasUsed += uint64(txResult.GasUsed) // #nosec G701 -- checked for int overflow already + } + cumulativeGasUsed += res.CumulativeGasUsed + + var status hexutil.Uint + if res.Failed { + status = hexutil.Uint(gethcore.ReceiptStatusFailed) + } else { + status = hexutil.Uint(gethcore.ReceiptStatusSuccessful) + } + chainID, err := b.ChainID() + if err != nil { + return nil, err + } + + from, err := ethMsg.GetSender(chainID.ToInt()) + if err != nil { + return nil, err + } + + // parse tx logs from events + msgIndex := int(res.MsgIndex) // #nosec G701 -- checked for int overflow already + logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex) + if err != nil { + b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) + } + + if res.EthTxIndex == -1 { + // Fallback to find tx index by iterating all valid eth transactions + msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) + for i := range msgs { + if msgs[i].Hash == hexTx { + res.EthTxIndex = int32(i) // #nosec G701 + break + } + } + } + // return error if still unable to find the eth tx index + if res.EthTxIndex == -1 { + return nil, errors.New("can't find index of ethereum tx") + } + + receipt := map[string]interface{}{ + // Consensus fields: These fields are defined by the Yellow Paper + "status": status, + "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), + "logsBloom": gethcore.BytesToBloom(gethcore.LogsBloom(logs)), + "logs": logs, + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + "transactionHash": hash, + "contractAddress": nil, + "gasUsed": hexutil.Uint64(b.GetGasUsed(res, txData.GetGasPrice(), txData.GetGas())), + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), + "blockNumber": hexutil.Uint64(res.Height), + "transactionIndex": hexutil.Uint64(res.EthTxIndex), + + // sender and receiver (contract or EOA) addreses + "from": from, + "to": txData.GetTo(), + "type": hexutil.Uint(ethMsg.AsTransaction().Type()), + } + + if logs == nil { + receipt["logs"] = [][]*gethcore.Log{} + } + + // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation + if txData.GetTo() == nil { + receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) + } + + if dynamicTx, ok := txData.(*evm.DynamicFeeTx); ok { + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // tolerate the error for pruned node. + b.logger.Error("fetch basefee failed, node is pruned?", "height", res.Height, "error", err) + } else { + receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.EffectiveGasPrice(baseFee)) + } + } + + return receipt, nil +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (b *Backend) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) { + b.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) + sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) + if !ok { + return nil, errors.New("invalid rpc client") + } + + block, err := sc.BlockByHash(b.ctx, hash.Bytes()) + if err != nil { + b.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) + return nil, nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "hash", hash.Hex()) + return nil, nil + } + + return b.GetTransactionByBlockAndIndex(block, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) { + b.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + + block, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + b.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) + return nil, nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "height", blockNum.Int64()) + return nil, nil + } + + return b.GetTransactionByBlockAndIndex(block, idx) +} + +// GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash +// TODO: Don't need to convert once hashing is fixed on Tendermint +// https://github.com/cometbft/cometbft/issues/6539 +func (b *Backend) GetTxByEthHash(hash common.Hash) (*eth.TxResult, error) { + if b.indexer != nil { + return b.indexer.GetByTxHash(hash) + } + + // fallback to tendermint tx indexer + query := fmt.Sprintf("%s.%s='%s'", evm.TypeMsgEthereumTx, evm.AttributeKeyEthereumTxHash, hash.Hex()) + txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpc.ParsedTxs) *rpc.ParsedTx { + return txs.GetTxByHash(hash) + }) + if err != nil { + return nil, errorsmod.Wrapf(err, "GetTxByEthHash %s", hash.Hex()) + } + return txResult, nil +} + +// GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs +func (b *Backend) GetTxByTxIndex(height int64, index uint) (*eth.TxResult, error) { + int32Index := int32(index) // #nosec G701 -- checked for int overflow already + if b.indexer != nil { + return b.indexer.GetByBlockAndIndex(height, int32Index) + } + + // fallback to tendermint tx indexer + query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", + height, evm.TypeMsgEthereumTx, + evm.AttributeKeyTxIndex, index, + ) + txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpc.ParsedTxs) *rpc.ParsedTx { + return txs.GetTxByTxIndex(int(index)) // #nosec G701 -- checked for int overflow already + }) + if err != nil { + return nil, errorsmod.Wrapf(err, "GetTxByTxIndex %d %d", height, index) + } + return txResult, nil +} + +// queryTendermintTxIndexer query tx in tendermint tx indexer +func (b *Backend) queryTendermintTxIndexer(query string, txGetter func(*rpc.ParsedTxs) *rpc.ParsedTx) (*eth.TxResult, error) { + resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") + if err != nil { + return nil, err + } + if len(resTxs.Txs) == 0 { + return nil, errors.New("ethereum tx not found") + } + txResult := resTxs.Txs[0] + if !rpc.TxSuccessOrExpectedFailure(&txResult.TxResult) { + return nil, errors.New("invalid ethereum tx") + } + + var tx sdk.Tx + if txResult.TxResult.Code != 0 { + // it's only needed when the tx exceeds block gas limit + tx, err = b.clientCtx.TxConfig.TxDecoder()(txResult.Tx) + if err != nil { + return nil, fmt.Errorf("invalid ethereum tx") + } + } + + return rpc.ParseTxIndexerResult(txResult, tx, txGetter) +} + +// GetTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. +func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) { + blockRes, err := b.TendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + return nil, nil + } + + var msg *evm.MsgEthereumTx + // find in tx indexer + res, err := b.GetTxByTxIndex(block.Block.Height, uint(idx)) + if err == nil { + tx, err := b.clientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex]) + if err != nil { + b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + + var ok bool + // msgIndex is inferred from tx events, should be within bound. + msg, ok = tx.GetMsgs()[res.MsgIndex].(*evm.MsgEthereumTx) + if !ok { + b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + } else { + i := int(idx) // #nosec G701 + ethMsgs := b.EthMsgsFromTendermintBlock(block, blockRes) + if i >= len(ethMsgs) { + b.logger.Debug("block txs index out of bound", "index", i) + return nil, nil + } + + msg = ethMsgs[i] + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Block.Height, "error", err) + } + + height := uint64(block.Block.Height) // #nosec G701 -- checked for int overflow already + index := uint64(idx) // #nosec G701 -- checked for int overflow already + return rpc.NewRPCTxFromMsg( + msg, + common.BytesToHash(block.Block.Hash()), + height, + index, + baseFee, + b.chainID, + ) +} diff --git a/eth/rpc/backend/tx_info_test.go b/eth/rpc/backend/tx_info_test.go new file mode 100644 index 000000000..beb708047 --- /dev/null +++ b/eth/rpc/backend/tx_info_test.go @@ -0,0 +1,671 @@ +package backend + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + tmlog "github.com/cometbft/cometbft/libs/log" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cometbft/cometbft/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "google.golang.org/grpc/metadata" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/indexer" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend/mocks" + "github.com/NibiruChain/nibiru/x/evm" +) + +func (s *BackendSuite) TestGetTransactionByHash() { + msgEthereumTx, _ := s.buildEthereumTx() + txHash := msgEthereumTx.AsTransaction().Hash() + + txBz := s.signAndEncodeEthTx(msgEthereumTx) + block := &types.Block{Header: types.Header{Height: 1, ChainID: "test"}, Data: types.Data{Txs: []types.Tx{txBz}}} + responseDeliver := []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: ""}, + }}, + }, + }, + } + + rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) + + testCases := []struct { + name string + registerMock func() + tx *evm.MsgEthereumTx + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "fail - Block error", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, 1) + }, + msgEthereumTx, + rpcTransaction, + false, + }, + { + "fail - Block Result error", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlock(client, 1, txBz) + s.Require().NoError(err) + RegisterBlockResultsError(client, 1) + }, + msgEthereumTx, + nil, + true, + }, + { + "pass - Base fee error", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, txBz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFeeError(queryClient) + }, + msgEthereumTx, + rpcTransaction, + true, + }, + { + "pass - Transaction found and returned", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, txBz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, math.NewInt(1)) + }, + msgEthereumTx, + rpcTransaction, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + db := dbm.NewMemDB() + s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) + err := s.backend.indexer.IndexBlock(block, responseDeliver) + s.Require().NoError(err) + + rpcTx, err := s.backend.GetTransactionByHash(common.HexToHash(tc.tx.Hash)) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionsByHashPending() { + msgEthereumTx, bz := s.buildEthereumTx() + rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) + + testCases := []struct { + name string + registerMock func() + tx *evm.MsgEthereumTx + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "fail - Pending transactions returns error", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterUnconfirmedTxsError(client, nil) + }, + msgEthereumTx, + nil, + true, + }, + { + "fail - Tx not found return nil", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterUnconfirmedTxs(client, nil, nil) + }, + msgEthereumTx, + nil, + true, + }, + { + "pass - Tx found and returned", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterUnconfirmedTxs(client, nil, types.Txs{bz}) + }, + msgEthereumTx, + rpcTransaction, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + rpcTx, err := s.backend.getTransactionByHashPending(common.HexToHash(tc.tx.Hash)) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTxByEthHash() { + msgEthereumTx, bz := s.buildEthereumTx() + rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) + + testCases := []struct { + name string + registerMock func() + tx *evm.MsgEthereumTx + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "fail - Indexer disabled can't find transaction", + func() { + s.backend.indexer = nil + client := s.backend.clientCtx.Client.(*mocks.Client) + query := fmt.Sprintf("%s.%s='%s'", evm.TypeMsgEthereumTx, evm.AttributeKeyEthereumTxHash, common.HexToHash(msgEthereumTx.Hash).Hex()) + RegisterTxSearch(client, query, bz) + }, + msgEthereumTx, + rpcTransaction, + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + rpcTx, err := s.backend.GetTxByEthHash(common.HexToHash(tc.tx.Hash)) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionByBlockHashAndIndex() { + _, bz := s.buildEthereumTx() + + testCases := []struct { + name string + registerMock func() + blockHash common.Hash + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "pass - block not found", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockByHashError(client, common.Hash{}, bz) + }, + common.Hash{}, + nil, + true, + }, + { + "pass - Block results error", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockByHash(client, common.Hash{}, bz) + s.Require().NoError(err) + RegisterBlockResultsError(client, 1) + }, + common.Hash{}, + nil, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + rpcTx, err := s.backend.GetTransactionByBlockHashAndIndex(tc.blockHash, 1) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionByBlockAndIndex() { + msgEthTx, bz := s.buildEthereumTx() + + defaultBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) + defaultResponseDeliverTx := []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: common.HexToHash(msgEthTx.Hash).Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: ""}, + }}, + }, + }, + } + + txFromMsg, _ := rpc.NewRPCTxFromMsg( + msgEthTx, + common.BytesToHash(defaultBlock.Hash().Bytes()), + 1, + 0, + big.NewInt(1), + s.backend.chainID, + ) + testCases := []struct { + name string + registerMock func() + block *tmrpctypes.ResultBlock + idx hexutil.Uint + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "pass - block txs index out of bound", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockResults(client, 1) + s.Require().NoError(err) + }, + &tmrpctypes.ResultBlock{Block: types.MakeBlock(1, []types.Tx{bz}, nil, nil)}, + 1, + nil, + true, + }, + { + "pass - Can't fetch base fee", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFeeError(queryClient) + }, + &tmrpctypes.ResultBlock{Block: defaultBlock}, + 0, + txFromMsg, + true, + }, + { + "pass - Gets Tx by transaction index", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + db := dbm.NewMemDB() + s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) + txBz := s.signAndEncodeEthTx(msgEthTx) + block := &types.Block{Header: types.Header{Height: 1, ChainID: "test"}, Data: types.Data{Txs: []types.Tx{txBz}}} + err := s.backend.indexer.IndexBlock(block, defaultResponseDeliverTx) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, math.NewInt(1)) + }, + &tmrpctypes.ResultBlock{Block: defaultBlock}, + 0, + txFromMsg, + true, + }, + { + "pass - returns the Ethereum format transaction by the Ethereum hash", + func() { + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, math.NewInt(1)) + }, + &tmrpctypes.ResultBlock{Block: defaultBlock}, + 0, + txFromMsg, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + rpcTx, err := s.backend.GetTransactionByBlockAndIndex(tc.block, tc.idx) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionByBlockNumberAndIndex() { + msgEthTx, bz := s.buildEthereumTx() + defaultBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) + txFromMsg, _ := rpc.NewRPCTxFromMsg( + msgEthTx, + common.BytesToHash(defaultBlock.Hash().Bytes()), + 1, + 0, + big.NewInt(1), + s.backend.chainID, + ) + testCases := []struct { + name string + registerMock func() + blockNum rpc.BlockNumber + idx hexutil.Uint + expRPCTx *rpc.EthTxJsonRPC + expPass bool + }{ + { + "fail - block not found return nil", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterBlockError(client, 1) + }, + 0, + 0, + nil, + true, + }, + { + "pass - returns the transaction identified by block number and index", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + _, err := RegisterBlock(client, 1, bz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + RegisterBaseFee(queryClient, math.NewInt(1)) + }, + 0, + 0, + txFromMsg, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + rpcTx, err := s.backend.GetTransactionByBlockNumberAndIndex(tc.blockNum, tc.idx) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(rpcTx, tc.expRPCTx) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionByTxIndex() { + _, bz := s.buildEthereumTx() + + testCases := []struct { + name string + registerMock func() + height int64 + index uint + expTxResult *eth.TxResult + expPass bool + }{ + { + "fail - Ethereum tx with query not found", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + s.backend.indexer = nil + RegisterTxSearch(client, "tx.height=0 AND ethereum_tx.txIndex=0", bz) + }, + 0, + 0, + ð.TxResult{}, + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + txResults, err := s.backend.GetTxByTxIndex(tc.height, tc.index) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(txResults, tc.expTxResult) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestQueryTendermintTxIndexer() { + testCases := []struct { + name string + registerMock func() + txGetter func(*rpc.ParsedTxs) *rpc.ParsedTx + query string + expTxResult *eth.TxResult + expPass bool + }{ + { + "fail - Ethereum tx with query not found", + func() { + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterTxSearchEmpty(client, "") + }, + func(txs *rpc.ParsedTxs) *rpc.ParsedTx { + return &rpc.ParsedTx{} + }, + "", + ð.TxResult{}, + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + txResults, err := s.backend.queryTendermintTxIndexer(tc.query, tc.txGetter) + + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(txResults, tc.expTxResult) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *BackendSuite) TestGetTransactionReceipt() { + msgEthereumTx, _ := s.buildEthereumTx() + txHash := msgEthereumTx.AsTransaction().Hash() + + txBz := s.signAndEncodeEthTx(msgEthereumTx) + + testCases := []struct { + name string + registerMock func() + tx *evm.MsgEthereumTx + block *types.Block + blockResult []*abci.ResponseDeliverTx + expTxReceipt map[string]interface{} + expPass bool + }{ + { + "fail - Receipts do not match", + func() { + var header metadata.MD + queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + client := s.backend.clientCtx.Client.(*mocks.Client) + RegisterParams(queryClient, &header, 1) + RegisterParamsWithoutHeader(queryClient, 1) + _, err := RegisterBlock(client, 1, txBz) + s.Require().NoError(err) + _, err = RegisterBlockResults(client, 1) + s.Require().NoError(err) + }, + msgEthereumTx, + &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, + []*abci.ResponseDeliverTx{ + { + Code: 0, + Events: []abci.Event{ + {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: "ethereumTxHash", Value: txHash.Hex()}, + {Key: "txIndex", Value: "0"}, + {Key: "amount", Value: "1000"}, + {Key: "txGasUsed", Value: "21000"}, + {Key: "txHash", Value: ""}, + {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, + }}, + }, + }, + }, + map[string]interface{}(nil), + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() // reset + tc.registerMock() + + db := dbm.NewMemDB() + s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) + err := s.backend.indexer.IndexBlock(tc.block, tc.blockResult) + s.Require().NoError(err) + + txReceipt, err := s.backend.GetTransactionReceipt(common.HexToHash(tc.tx.Hash)) + if tc.expPass { + s.Require().NoError(err) + s.Require().Equal(txReceipt, tc.expTxReceipt) + } else { + s.Require().NotEqual(txReceipt, tc.expTxReceipt) + } + }) + } +} + +func (s *BackendSuite) TestGetGasUsed() { + origin := s.backend.cfg.JSONRPC.FixRevertGasRefundHeight + testCases := []struct { + name string + fixRevertGasRefundHeight int64 + txResult *eth.TxResult + price *big.Int + gas uint64 + exp uint64 + }{ + { + "success txResult", + 1, + ð.TxResult{ + Height: 1, + Failed: false, + GasUsed: 53026, + }, + new(big.Int).SetUint64(0), + 0, + 53026, + }, + { + "fail txResult before cap", + 2, + ð.TxResult{ + Height: 1, + Failed: true, + GasUsed: 53026, + }, + new(big.Int).SetUint64(200000), + 5000000000000, + 1000000000000000000, + }, + { + "fail txResult after cap", + 2, + ð.TxResult{ + Height: 3, + Failed: true, + GasUsed: 53026, + }, + new(big.Int).SetUint64(200000), + 5000000000000, + 53026, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.backend.cfg.JSONRPC.FixRevertGasRefundHeight = tc.fixRevertGasRefundHeight + s.Require().Equal(tc.exp, s.backend.GetGasUsed(tc.txResult, tc.price, tc.gas)) + s.backend.cfg.JSONRPC.FixRevertGasRefundHeight = origin + }) + } +} diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go new file mode 100644 index 000000000..4f93b0ea4 --- /dev/null +++ b/eth/rpc/backend/utils.go @@ -0,0 +1,302 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package backend + +import ( + "encoding/json" + "fmt" + "math/big" + "sort" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc" + gethcore "github.com/ethereum/go-ethereum/core/types" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + + "github.com/cometbft/cometbft/proto/tendermint/crypto" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +type txGasAndReward struct { + gasUsed uint64 + reward *big.Int +} + +type sortGasAndReward []txGasAndReward + +func (s sortGasAndReward) Len() int { return len(s) } +func (s sortGasAndReward) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortGasAndReward) Less(i, j int) bool { + return s[i].reward.Cmp(s[j].reward) < 0 +} + +// getAccountNonce returns the account nonce for the given account address. +// If the pending value is true, it will iterate over the mempool (pending) +// txs in order to compute and return the pending tx sequence. +// Todo: include the ability to specify a blockNumber +func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) { + queryClient := authtypes.NewQueryClient(b.clientCtx) + adr := sdk.AccAddress(accAddr.Bytes()).String() + ctx := rpc.NewContextWithHeight(height) + res, err := queryClient.Account(ctx, &authtypes.QueryAccountRequest{Address: adr}) + if err != nil { + st, ok := status.FromError(err) + // treat as account doesn't exist yet + if ok && st.Code() == codes.NotFound { + return 0, nil + } + return 0, err + } + var acc authtypes.AccountI + if err := b.clientCtx.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { + return 0, err + } + + nonce := acc.GetSequence() + + if !pending { + return nonce, nil + } + + // the account retriever doesn't include the uncommitted transactions on the nonce so we need to + // to manually add them. + pendingTxs, err := b.PendingTransactions() + if err != nil { + logger.Error("failed to fetch pending transactions", "error", err.Error()) + return nonce, nil + } + + // add the uncommitted txs to the nonce counter + // only supports `MsgEthereumTx` style tx + for _, tx := range pendingTxs { + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + // not ethereum tx + break + } + + sender, err := ethMsg.GetSender(b.chainID) + if err != nil { + continue + } + if sender == accAddr { + nonce++ + } + } + } + + return nonce, nil +} + +// output: targetOneFeeHistory +func (b *Backend) processBlock( + tendermintBlock *tmrpctypes.ResultBlock, + ethBlock *map[string]interface{}, + rewardPercentiles []float64, + tendermintBlockResult *tmrpctypes.ResultBlockResults, + targetOneFeeHistory *rpc.OneFeeHistory, +) error { + blockHeight := tendermintBlock.Block.Height + blockBaseFee, err := b.BaseFee(tendermintBlockResult) + if err != nil { + return err + } + + // set basefee + targetOneFeeHistory.BaseFee = blockBaseFee + cfg := b.ChainConfig() + if cfg.IsLondon(big.NewInt(blockHeight + 1)) { + header, err := b.CurrentHeader() + if err != nil { + return err + } + targetOneFeeHistory.NextBaseFee = misc.CalcBaseFee(cfg, header) + } else { + targetOneFeeHistory.NextBaseFee = new(big.Int) + } + // set gas used ratio + gasLimitUint64, ok := (*ethBlock)["gasLimit"].(hexutil.Uint64) + if !ok { + return fmt.Errorf("invalid gas limit type: %T", (*ethBlock)["gasLimit"]) + } + + gasUsedBig, ok := (*ethBlock)["gasUsed"].(*hexutil.Big) + if !ok { + return fmt.Errorf("invalid gas used type: %T", (*ethBlock)["gasUsed"]) + } + + gasusedfloat, _ := new(big.Float).SetInt(gasUsedBig.ToInt()).Float64() + + if gasLimitUint64 <= 0 { + return fmt.Errorf("gasLimit of block height %d should be bigger than 0 , current gaslimit %d", blockHeight, gasLimitUint64) + } + + gasUsedRatio := gasusedfloat / float64(gasLimitUint64) + blockGasUsed := gasusedfloat + targetOneFeeHistory.GasUsedRatio = gasUsedRatio + + rewardCount := len(rewardPercentiles) + targetOneFeeHistory.Reward = make([]*big.Int, rewardCount) + for i := 0; i < rewardCount; i++ { + targetOneFeeHistory.Reward[i] = big.NewInt(0) + } + + // check tendermintTxs + tendermintTxs := tendermintBlock.Block.Txs + tendermintTxResults := tendermintBlockResult.TxsResults + tendermintTxCount := len(tendermintTxs) + + var sorter sortGasAndReward + + for i := 0; i < tendermintTxCount; i++ { + eachTendermintTx := tendermintTxs[i] + eachTendermintTxResult := tendermintTxResults[i] + + tx, err := b.clientCtx.TxConfig.TxDecoder()(eachTendermintTx) + if err != nil { + b.logger.Debug("failed to decode transaction in block", "height", blockHeight, "error", err.Error()) + continue + } + txGasUsed := uint64(eachTendermintTxResult.GasUsed) // #nosec G701 + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + continue + } + tx := ethMsg.AsTransaction() + reward := tx.EffectiveGasTipValue(blockBaseFee) + if reward == nil { + reward = big.NewInt(0) + } + sorter = append(sorter, txGasAndReward{gasUsed: txGasUsed, reward: reward}) + } + } + + // return an all zero row if there are no transactions to gather data from + ethTxCount := len(sorter) + if ethTxCount == 0 { + return nil + } + + sort.Sort(sorter) + + var txIndex int + sumGasUsed := sorter[0].gasUsed + + for i, p := range rewardPercentiles { + thresholdGasUsed := uint64(blockGasUsed * p / 100) // #nosec G701 + for sumGasUsed < thresholdGasUsed && txIndex < ethTxCount-1 { + txIndex++ + sumGasUsed += sorter[txIndex].gasUsed + } + targetOneFeeHistory.Reward[i] = sorter[txIndex].reward + } + + return nil +} + +// AllTxLogsFromEvents parses all ethereum logs from cosmos events +func AllTxLogsFromEvents(events []abci.Event) ([][]*gethcore.Log, error) { + allLogs := make([][]*gethcore.Log, 0, 4) + for _, event := range events { + if event.Type != evm.EventTypeTxLog { + continue + } + + logs, err := ParseTxLogsFromEvent(event) + if err != nil { + return nil, err + } + + allLogs = append(allLogs, logs) + } + return allLogs, nil +} + +// TxLogsFromEvents parses ethereum logs from cosmos events for specific msg index +func TxLogsFromEvents(events []abci.Event, msgIndex int) ([]*gethcore.Log, error) { + for _, event := range events { + if event.Type != evm.EventTypeTxLog { + continue + } + + if msgIndex > 0 { + // not the eth tx we want + msgIndex-- + continue + } + + return ParseTxLogsFromEvent(event) + } + return nil, fmt.Errorf("eth tx logs not found for message index %d", msgIndex) +} + +// ParseTxLogsFromEvent parse tx logs from one event +func ParseTxLogsFromEvent(event abci.Event) ([]*gethcore.Log, error) { + logs := make([]*evm.Log, 0, len(event.Attributes)) + for _, attr := range event.Attributes { + if attr.Key != evm.AttributeKeyTxLog { + continue + } + + var log evm.Log + if err := json.Unmarshal([]byte(attr.Value), &log); err != nil { + return nil, err + } + + logs = append(logs, &log) + } + return evm.LogsToEthereum(logs), nil +} + +// ShouldIgnoreGasUsed returns true if the gasUsed in result should be ignored +// workaround for issue: https://github.com/cosmos/cosmos-sdk/issues/10832 +func ShouldIgnoreGasUsed(res *abci.ResponseDeliverTx) bool { + return res.GetCode() == 11 && strings.Contains(res.GetLog(), "no block gas left to run tx: out of gas") +} + +// GetLogsFromBlockResults returns the list of event logs from the tendermint block result response +func GetLogsFromBlockResults(blockRes *tmrpctypes.ResultBlockResults) ([][]*gethcore.Log, error) { + blockLogs := [][]*gethcore.Log{} + for _, txResult := range blockRes.TxsResults { + logs, err := AllTxLogsFromEvents(txResult.Events) + if err != nil { + return nil, err + } + + blockLogs = append(blockLogs, logs...) + } + return blockLogs, nil +} + +// GetHexProofs returns list of hex data of proof op +func GetHexProofs(proof *crypto.ProofOps) []string { + if proof == nil { + return []string{""} + } + proofs := []string{} + // check for proof + for _, p := range proof.Ops { + proof := "" + if len(p.Data) > 0 { + proof = hexutil.Encode(p.Data) + } + proofs = append(proofs, proof) + } + return proofs +} diff --git a/eth/rpc/backend/utils_test.go b/eth/rpc/backend/utils_test.go new file mode 100644 index 000000000..4c7c14a74 --- /dev/null +++ b/eth/rpc/backend/utils_test.go @@ -0,0 +1,52 @@ +package backend + +import ( + "fmt" + + "github.com/cometbft/cometbft/proto/tendermint/crypto" +) + +func mookProofs(num int, withData bool) *crypto.ProofOps { + var proofOps *crypto.ProofOps + if num > 0 { + proofOps = new(crypto.ProofOps) + for i := 0; i < num; i++ { + proof := crypto.ProofOp{} + if withData { + proof.Data = []byte("\n\031\n\003KEY\022\005VALUE\032\013\010\001\030\001 \001*\003\000\002\002") + } + proofOps.Ops = append(proofOps.Ops, proof) + } + } + return proofOps +} + +func (s *BackendSuite) TestGetHexProofs() { + defaultRes := []string{""} + testCases := []struct { + name string + proof *crypto.ProofOps + exp []string + }{ + { + "no proof provided", + mookProofs(0, false), + defaultRes, + }, + { + "no proof data provided", + mookProofs(1, false), + defaultRes, + }, + { + "valid proof provided", + mookProofs(1, true), + []string{"0x0a190a034b4559120556414c55451a0b0801180120012a03000202"}, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.Require().Equal(tc.exp, GetHexProofs(tc.proof)) + }) + } +} diff --git a/eth/rpc/rpc.go b/eth/rpc/rpc.go index 5eaa9380b..88b9f579d 100644 --- a/eth/rpc/rpc.go +++ b/eth/rpc/rpc.go @@ -155,29 +155,29 @@ func FormatBlock( return result } -// NewRpcTxFromMsg returns a transaction that will serialize to the RPC +// NewRPCTxFromMsg returns a transaction that will serialize to the RPC // representation, with the given location metadata set (if available). -func NewRpcTxFromMsg( +func NewRPCTxFromMsg( msg *evm.MsgEthereumTx, blockHash gethcommon.Hash, blockNumber, index uint64, baseFee *big.Int, chainID *big.Int, -) (*RPCTransaction, error) { +) (*EthTxJsonRPC, error) { tx := msg.AsTransaction() - return NewRpcTxFromEthTx(tx, blockHash, blockNumber, index, baseFee, chainID) + return NewRPCTxFromEthTx(tx, blockHash, blockNumber, index, baseFee, chainID) } // NewTransactionFromData returns a transaction that will serialize to the RPC // representation, with the given location metadata set (if available). -func NewRpcTxFromEthTx( +func NewRPCTxFromEthTx( tx *gethcore.Transaction, blockHash gethcommon.Hash, blockNumber, index uint64, baseFee *big.Int, chainID *big.Int, -) (*RPCTransaction, error) { +) (*EthTxJsonRPC, error) { // Determine the signer. For replay-protected transactions, use the most // permissive signer, because we assume that signers are backwards-compatible // with old transactions. For non-protected transactions, the homestead @@ -191,7 +191,7 @@ func NewRpcTxFromEthTx( } from, _ := gethcore.Sender(signer, tx) // #nosec G703 v, r, s := tx.RawSignatureValues() - result := &RPCTransaction{ + result := &EthTxJsonRPC{ Type: hexutil.Uint64(tx.Type()), From: from, Gas: hexutil.Uint64(tx.Gas()), @@ -263,8 +263,8 @@ func TxStateDBCommitError(res *abci.ResponseDeliverTx) bool { return strings.Contains(res.Log, ErrStateDBCommit) } -// TxSucessOrExpectedFailure returns true if the transaction was successful +// TxSuccessOrExpectedFailure returns true if the transaction was successful // or if it failed with an ExceedBlockGasLimit error or TxStateDBCommitError error -func TxSucessOrExpectedFailure(res *abci.ResponseDeliverTx) bool { +func TxSuccessOrExpectedFailure(res *abci.ResponseDeliverTx) bool { return res.Code == 0 || TxExceedBlockGasLimit(res) || TxStateDBCommitError(res) } diff --git a/eth/rpc/rpcapi/apis.go b/eth/rpc/rpcapi/apis.go new file mode 100644 index 000000000..336674c1e --- /dev/null +++ b/eth/rpc/rpcapi/apis.go @@ -0,0 +1,195 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + + "github.com/ethereum/go-ethereum/rpc" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc/backend" + "github.com/NibiruChain/nibiru/eth/rpc/rpcapi/debugapi" + "github.com/NibiruChain/nibiru/eth/rpc/rpcapi/filtersapi" + + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" +) + +// RPC namespaces and API version +const ( + // Cosmos namespaces + NamespaceCosmos = "cosmos" + + // Ethereum namespaces + NamespaceWeb3 = "web3" + NamespaceEth = "eth" + NamespacePersonal = "personal" + NamespaceNet = "net" + NamespaceTxPool = "txpool" + NamespaceDebug = "debug" + NamespaceMiner = "miner" + + apiVersion = "1.0" +) + +func EthereumNamespaces() []string { + return []string{ + NamespaceWeb3, + NamespaceEth, + NamespacePersonal, + NamespaceNet, + NamespaceTxPool, + NamespaceDebug, + NamespaceMiner, + } +} + +// APICreator creates the JSON-RPC API implementations. +type APICreator = func( + ctx *server.Context, + clientCtx client.Context, + tendermintWebsocketClient *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, +) []rpc.API + +// apiCreators defines the JSON-RPC API namespaces. +var apiCreators map[string]APICreator + +func init() { + apiCreators = map[string]APICreator{ + NamespaceEth: func(ctx *server.Context, + clientCtx client.Context, + tmWSClient *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, + ) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + return []rpc.API{ + { + Namespace: NamespaceEth, + Version: apiVersion, + Service: NewImplEthAPI(ctx.Logger, evmBackend), + Public: true, + }, + { + Namespace: NamespaceEth, + Version: apiVersion, + Service: filtersapi.NewImplFiltersAPI(ctx.Logger, clientCtx, tmWSClient, evmBackend), + Public: true, + }, + } + }, + NamespaceWeb3: func(*server.Context, client.Context, *rpcclient.WSClient, bool, eth.EVMTxIndexer) []rpc.API { + return []rpc.API{ + { + Namespace: NamespaceWeb3, + Version: apiVersion, + Service: NewImplWeb3API(), + Public: true, + }, + } + }, + NamespaceNet: func(_ *server.Context, clientCtx client.Context, _ *rpcclient.WSClient, _ bool, _ eth.EVMTxIndexer) []rpc.API { + return []rpc.API{ + { + Namespace: NamespaceNet, + Version: apiVersion, + Service: NewImplNetAPI(clientCtx), + Public: true, + }, + } + }, + NamespacePersonal: func(ctx *server.Context, + clientCtx client.Context, + _ *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, + ) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + return []rpc.API{ + { + Namespace: NamespacePersonal, + Version: apiVersion, + Service: NewImplPersonalAPI(ctx.Logger, evmBackend), + Public: false, + }, + } + }, + NamespaceTxPool: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient, _ bool, _ eth.EVMTxIndexer) []rpc.API { + return []rpc.API{ + { + Namespace: NamespaceTxPool, + Version: apiVersion, + Service: NewImplTxPoolAPI(ctx.Logger), + Public: true, + }, + } + }, + NamespaceDebug: func(ctx *server.Context, + clientCtx client.Context, + _ *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, + ) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + return []rpc.API{ + { + Namespace: NamespaceDebug, + Version: apiVersion, + Service: debugapi.NewImplDebugAPI(ctx, evmBackend), + Public: true, + }, + } + }, + NamespaceMiner: func(ctx *server.Context, + clientCtx client.Context, + _ *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, + ) []rpc.API { + evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, indexer) + return []rpc.API{ + { + Namespace: NamespaceMiner, + Version: apiVersion, + Service: NewImplMinerAPI(ctx, evmBackend), + Public: false, + }, + } + }, + } +} + +// GetRPCAPIs returns the list of all APIs +func GetRPCAPIs(ctx *server.Context, + clientCtx client.Context, + tmWSClient *rpcclient.WSClient, + allowUnprotectedTxs bool, + indexer eth.EVMTxIndexer, + selectedAPIs []string, +) []rpc.API { + var apis []rpc.API + + for _, ns := range selectedAPIs { + if creator, ok := apiCreators[ns]; ok { + apis = append(apis, creator(ctx, clientCtx, tmWSClient, allowUnprotectedTxs, indexer)...) + } else { + ctx.Logger.Error("invalid namespace value", "namespace", ns) + } + } + + return apis +} + +// RegisterAPINamespace registers a new API namespace with the API creator. +// This function fails if the namespace is already registered. +func RegisterAPINamespace(ns string, creator APICreator) error { + if _, ok := apiCreators[ns]; ok { + return fmt.Errorf("duplicated api namespace %s", ns) + } + apiCreators[ns] = creator + return nil +} diff --git a/eth/rpc/rpcapi/debugapi/api.go b/eth/rpc/rpcapi/debugapi/api.go new file mode 100644 index 000000000..7b0bf65d6 --- /dev/null +++ b/eth/rpc/rpcapi/debugapi/api.go @@ -0,0 +1,338 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package debugapi + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "runtime" // #nosec G702 + "runtime/debug" + "runtime/pprof" + "sync" + "time" + + "github.com/davecgh/go-spew/spew" + + "github.com/NibiruChain/nibiru/x/evm" + + stderrors "github.com/pkg/errors" + + "github.com/cosmos/cosmos-sdk/server" + + "github.com/cometbft/cometbft/libs/log" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend" +) + +// HandlerT keeps track of the cpu profiler and trace execution +type HandlerT struct { + cpuFilename string + cpuFile io.WriteCloser + mu sync.Mutex + traceFilename string + traceFile io.WriteCloser +} + +// DebugAPI is the collection of tracing APIs exposed over the private debugging +// endpoint. +type DebugAPI struct { + ctx *server.Context + logger log.Logger + backend backend.EVMBackend + handler *HandlerT +} + +// NewImplDebugAPI creates a new API definition for the tracing methods of the +// Ethereum service. +func NewImplDebugAPI( + ctx *server.Context, + backend backend.EVMBackend, +) *DebugAPI { + return &DebugAPI{ + ctx: ctx, + logger: ctx.Logger.With("module", "debug"), + backend: backend, + handler: new(HandlerT), + } +} + +// TraceTransaction returns the structured logs created during the execution of EVM +// and returns them as a JSON object. +func (a *DebugAPI) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (interface{}, error) { + a.logger.Debug("debug_traceTransaction", "hash", hash) + return a.backend.TraceTransaction(hash, config) +} + +// TraceBlockByNumber returns the structured logs created during the execution of +// EVM and returns them as a JSON object. +func (a *DebugAPI) TraceBlockByNumber(height rpc.BlockNumber, config *evm.TraceConfig) ([]*evm.TxTraceResult, error) { + a.logger.Debug("debug_traceBlockByNumber", "height", height) + if height == 0 { + return nil, errors.New("genesis is not traceable") + } + // Get Tendermint Block + resBlock, err := a.backend.TendermintBlockByNumber(height) + if err != nil { + a.logger.Debug("get block failed", "height", height, "error", err.Error()) + return nil, err + } + + return a.backend.TraceBlock(rpc.BlockNumber(resBlock.Block.Height), config, resBlock) +} + +// TraceBlockByHash returns the structured logs created during the execution of +// EVM and returns them as a JSON object. +func (a *DebugAPI) TraceBlockByHash(hash common.Hash, config *evm.TraceConfig) ([]*evm.TxTraceResult, error) { + a.logger.Debug("debug_traceBlockByHash", "hash", hash) + // Get Tendermint Block + resBlock, err := a.backend.TendermintBlockByHash(hash) + if err != nil { + a.logger.Debug("get block failed", "hash", hash.Hex(), "error", err.Error()) + return nil, err + } + + if resBlock == nil || resBlock.Block == nil { + a.logger.Debug("block not found", "hash", hash.Hex()) + return nil, errors.New("block not found") + } + + return a.backend.TraceBlock(rpc.BlockNumber(resBlock.Block.Height), config, resBlock) +} + +// BlockProfile turns on goroutine profiling for nsec seconds and writes profile data to +// file. It uses a profile rate of 1 for most accurate information. If a different rate is +// desired, set the rate and write the profile manually. +func (a *DebugAPI) BlockProfile(file string, nsec uint) error { + a.logger.Debug("debug_blockProfile", "file", file, "nsec", nsec) + runtime.SetBlockProfileRate(1) + defer runtime.SetBlockProfileRate(0) + + time.Sleep(time.Duration(nsec) * time.Second) + return writeProfile("block", file, a.logger) +} + +// CpuProfile turns on CPU profiling for nsec seconds and writes +// profile data to file. +func (a *DebugAPI) CpuProfile(file string, nsec uint) error { //nolint: golint, stylecheck, revive + a.logger.Debug("debug_cpuProfile", "file", file, "nsec", nsec) + if err := a.StartCPUProfile(file); err != nil { + return err + } + time.Sleep(time.Duration(nsec) * time.Second) + return a.StopCPUProfile() +} + +// GcStats returns GC statistics. +func (a *DebugAPI) GcStats() *debug.GCStats { + a.logger.Debug("debug_gcStats") + s := new(debug.GCStats) + debug.ReadGCStats(s) + return s +} + +// GoTrace turns on tracing for nsec seconds and writes +// trace data to file. +func (a *DebugAPI) GoTrace(file string, nsec uint) error { + a.logger.Debug("debug_goTrace", "file", file, "nsec", nsec) + if err := a.StartGoTrace(file); err != nil { + return err + } + time.Sleep(time.Duration(nsec) * time.Second) + return a.StopGoTrace() +} + +// MemStats returns detailed runtime memory statistics. +func (a *DebugAPI) MemStats() *runtime.MemStats { + a.logger.Debug("debug_memStats") + s := new(runtime.MemStats) + runtime.ReadMemStats(s) + return s +} + +// SetBlockProfileRate sets the rate of goroutine block profile data collection. +// rate 0 disables block profiling. +func (a *DebugAPI) SetBlockProfileRate(rate int) { + a.logger.Debug("debug_setBlockProfileRate", "rate", rate) + runtime.SetBlockProfileRate(rate) +} + +// Stacks returns a printed representation of the stacks of all goroutines. +func (a *DebugAPI) Stacks() string { + a.logger.Debug("debug_stacks") + buf := new(bytes.Buffer) + err := pprof.Lookup("goroutine").WriteTo(buf, 2) + if err != nil { + a.logger.Error("Failed to create stacks", "error", err.Error()) + } + return buf.String() +} + +// StartCPUProfile turns on CPU profiling, writing to the given file. +func (a *DebugAPI) StartCPUProfile(file string) error { + a.logger.Debug("debug_startCPUProfile", "file", file) + a.handler.mu.Lock() + defer a.handler.mu.Unlock() + + switch { + case isCPUProfileConfigurationActivated(a.ctx): + a.logger.Debug("CPU profiling already in progress using the configuration file") + return errors.New("CPU profiling already in progress using the configuration file") + case a.handler.cpuFile != nil: + a.logger.Debug("CPU profiling already in progress") + return errors.New("CPU profiling already in progress") + default: + fp, err := ExpandHome(file) + if err != nil { + a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) + return err + } + f, err := os.Create(fp) + if err != nil { + a.logger.Debug("failed to create CPU profile file", "error", err.Error()) + return err + } + if err := pprof.StartCPUProfile(f); err != nil { + a.logger.Debug("cpu profiling already in use", "error", err.Error()) + if err := f.Close(); err != nil { + a.logger.Debug("failed to close cpu profile file") + return stderrors.Wrap(err, "failed to close cpu profile file") + } + return err + } + + a.logger.Info("CPU profiling started", "profile", file) + a.handler.cpuFile = f + a.handler.cpuFilename = file + return nil + } +} + +// StopCPUProfile stops an ongoing CPU profile. +func (a *DebugAPI) StopCPUProfile() error { + a.logger.Debug("debug_stopCPUProfile") + a.handler.mu.Lock() + defer a.handler.mu.Unlock() + + switch { + case isCPUProfileConfigurationActivated(a.ctx): + a.logger.Debug("CPU profiling already in progress using the configuration file") + return errors.New("CPU profiling already in progress using the configuration file") + case a.handler.cpuFile != nil: + a.logger.Info("Done writing CPU profile", "profile", a.handler.cpuFilename) + pprof.StopCPUProfile() + if err := a.handler.cpuFile.Close(); err != nil { + a.logger.Debug("failed to close cpu file") + return stderrors.Wrap(err, "failed to close cpu file") + } + a.handler.cpuFile = nil + a.handler.cpuFilename = "" + return nil + default: + a.logger.Debug("CPU profiling not in progress") + return errors.New("CPU profiling not in progress") + } +} + +// WriteBlockProfile writes a goroutine blocking profile to the given file. +func (a *DebugAPI) WriteBlockProfile(file string) error { + a.logger.Debug("debug_writeBlockProfile", "file", file) + return writeProfile("block", file, a.logger) +} + +// WriteMemProfile writes an allocation profile to the given file. +// Note that the profiling rate cannot be set through the API, +// it must be set on the command line. +func (a *DebugAPI) WriteMemProfile(file string) error { + a.logger.Debug("debug_writeMemProfile", "file", file) + return writeProfile("heap", file, a.logger) +} + +// MutexProfile turns on mutex profiling for nsec seconds and writes profile data to file. +// It uses a profile rate of 1 for most accurate information. If a different rate is +// desired, set the rate and write the profile manually. +func (a *DebugAPI) MutexProfile(file string, nsec uint) error { + a.logger.Debug("debug_mutexProfile", "file", file, "nsec", nsec) + runtime.SetMutexProfileFraction(1) + time.Sleep(time.Duration(nsec) * time.Second) + defer runtime.SetMutexProfileFraction(0) + return writeProfile("mutex", file, a.logger) +} + +// SetMutexProfileFraction sets the rate of mutex profiling. +func (a *DebugAPI) SetMutexProfileFraction(rate int) { + a.logger.Debug("debug_setMutexProfileFraction", "rate", rate) + runtime.SetMutexProfileFraction(rate) +} + +// WriteMutexProfile writes a goroutine blocking profile to the given file. +func (a *DebugAPI) WriteMutexProfile(file string) error { + a.logger.Debug("debug_writeMutexProfile", "file", file) + return writeProfile("mutex", file, a.logger) +} + +// FreeOSMemory forces a garbage collection. +func (a *DebugAPI) FreeOSMemory() { + a.logger.Debug("debug_freeOSMemory") + debug.FreeOSMemory() +} + +// SetGCPercent sets the garbage collection target percentage. It returns the previous +// setting. A negative value disables GC. +func (a *DebugAPI) SetGCPercent(v int) int { + a.logger.Debug("debug_setGCPercent", "percent", v) + return debug.SetGCPercent(v) +} + +// GetHeaderRlp retrieves the RLP encoded for of a single header. +func (a *DebugAPI) GetHeaderRlp(number uint64) (hexutil.Bytes, error) { + header, err := a.backend.HeaderByNumber(rpc.BlockNumber(number)) + if err != nil { + return nil, err + } + + return rlp.EncodeToBytes(header) +} + +// GetBlockRlp retrieves the RLP encoded for of a single block. +func (a *DebugAPI) GetBlockRlp(number uint64) (hexutil.Bytes, error) { + block, err := a.backend.EthBlockByNumber(rpc.BlockNumber(number)) + if err != nil { + return nil, err + } + + return rlp.EncodeToBytes(block) +} + +// PrintBlock retrieves a block and returns its pretty printed form. +func (a *DebugAPI) PrintBlock(number uint64) (string, error) { + block, err := a.backend.EthBlockByNumber(rpc.BlockNumber(number)) + if err != nil { + return "", err + } + + return spew.Sdump(block), nil +} + +// SeedHash retrieves the seed hash of a block. +func (a *DebugAPI) SeedHash(number uint64) (string, error) { + _, err := a.backend.HeaderByNumber(rpc.BlockNumber(number)) + if err != nil { + return "", err + } + + return fmt.Sprintf("0x%x", ethash.SeedHash(number)), nil +} + +// IntermediateRoots executes a block, and returns a list +// of intermediate roots: the stateroot after each transaction. +func (a *DebugAPI) IntermediateRoots(hash common.Hash, _ *evm.TraceConfig) ([]common.Hash, error) { + a.logger.Debug("debug_intermediateRoots", "hash", hash) + return ([]common.Hash)(nil), nil +} diff --git a/eth/rpc/rpcapi/debugapi/trace.go b/eth/rpc/rpcapi/debugapi/trace.go new file mode 100644 index 000000000..8dce68952 --- /dev/null +++ b/eth/rpc/rpcapi/debugapi/trace.go @@ -0,0 +1,84 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build go1.5 +// +build go1.5 + +package debugapi + +import ( + "errors" + "os" + "runtime/trace" + + stderrors "github.com/pkg/errors" +) + +// StartGoTrace turns on tracing, writing to the given file. +func (a *DebugAPI) StartGoTrace(file string) error { + a.logger.Debug("debug_startGoTrace", "file", file) + a.handler.mu.Lock() + defer a.handler.mu.Unlock() + + if a.handler.traceFile != nil { + a.logger.Debug("trace already in progress") + return errors.New("trace already in progress") + } + fp, err := ExpandHome(file) + if err != nil { + a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) + return err + } + f, err := os.Create(fp) + if err != nil { + a.logger.Debug("failed to create go trace file", "error", err.Error()) + return err + } + if err := trace.Start(f); err != nil { + a.logger.Debug("Go tracing already started", "error", err.Error()) + if err := f.Close(); err != nil { + a.logger.Debug("failed to close trace file") + return stderrors.Wrap(err, "failed to close trace file") + } + + return err + } + a.handler.traceFile = f + a.handler.traceFilename = file + a.logger.Info("Go tracing started", "dump", a.handler.traceFilename) + return nil +} + +// StopGoTrace stops an ongoing trace. +func (a *DebugAPI) StopGoTrace() error { + a.logger.Debug("debug_stopGoTrace") + a.handler.mu.Lock() + defer a.handler.mu.Unlock() + + trace.Stop() + if a.handler.traceFile == nil { + a.logger.Debug("trace not in progress") + return errors.New("trace not in progress") + } + a.logger.Info("Done writing Go trace", "dump", a.handler.traceFilename) + if err := a.handler.traceFile.Close(); err != nil { + a.logger.Debug("failed to close trace file") + return stderrors.Wrap(err, "failed to close trace file") + } + a.handler.traceFile = nil + a.handler.traceFilename = "" + return nil +} diff --git a/eth/rpc/rpcapi/debugapi/trace_fallback.go b/eth/rpc/rpcapi/debugapi/trace_fallback.go new file mode 100644 index 000000000..8f4c6caa2 --- /dev/null +++ b/eth/rpc/rpcapi/debugapi/trace_fallback.go @@ -0,0 +1,36 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build !go1.5 +// +build !go1.5 + +// no-op implementation of tracing methods for Go < 1.5. + +package debug + +import ( + "errors" +) + +func (*API) StartGoTrace(string file) error { + a.logger.Debug("debug_stopGoTrace", "file", file) + return errors.New("tracing is not supported on Go < 1.5") +} + +func (*API) StopGoTrace() error { + a.logger.Debug("debug_stopGoTrace") + return errors.New("tracing is not supported on Go < 1.5") +} diff --git a/eth/rpc/rpcapi/debugapi/utils.go b/eth/rpc/rpcapi/debugapi/utils.go new file mode 100644 index 000000000..95fbad86e --- /dev/null +++ b/eth/rpc/rpcapi/debugapi/utils.go @@ -0,0 +1,61 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package debugapi + +import ( + "os" + "os/user" + "path/filepath" + "runtime/pprof" + "strings" + + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/server" +) + +// isCPUProfileConfigurationActivated: Checks if the "cpu-profile" flag was set +func isCPUProfileConfigurationActivated(ctx *server.Context) bool { + // TODO: use same constants as server/start.go + // constant declared in start.go cannot be imported (cyclical dependency) + const flagCPUProfile = "cpu-profile" + if cpuProfile := ctx.Viper.GetString(flagCPUProfile); cpuProfile != "" { + return true + } + return false +} + +// ExpandHome expands home directory in file paths. +// ~someuser/tmp will not be expanded. +func ExpandHome(p string) (string, error) { + if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { + usr, err := user.Current() + if err != nil { + return p, err + } + home := usr.HomeDir + p = home + p[1:] + } + return filepath.Clean(p), nil +} + +// writeProfile writes the data to a file +func writeProfile(name, file string, log log.Logger) error { + p := pprof.Lookup(name) + log.Info("Writing profile records", "count", p.Count(), "type", name, "dump", file) + fp, err := ExpandHome(file) + if err != nil { + return err + } + f, err := os.Create(fp) + if err != nil { + return err + } + + if err := p.WriteTo(f, 0); err != nil { + if err := f.Close(); err != nil { + return err + } + return err + } + + return f.Close() +} diff --git a/eth/rpc/rpcapi/eth_api.go b/eth/rpc/rpcapi/eth_api.go new file mode 100644 index 000000000..1283e47b6 --- /dev/null +++ b/eth/rpc/rpcapi/eth_api.go @@ -0,0 +1,585 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/cometbft/cometbft/libs/log" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/eth/rpc/backend" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/x/evm" +) + +// Ethereum API: Allows connection to a full node of the Nibiru blockchain +// network via Nibiru EVM. Developers can interact with on-chain EVM data and +// send different types of transactions to the network by utilizing the endpoints +// provided by the API. The API follows a JSON-RPC standard. If not otherwise +// specified, the interface is derived from the Alchemy Ethereum API: +// https://docs.alchemy.com/alchemy/apis/ethereum +type IEthAPI interface { + // Getting Blocks + // + // Retrieves information from a particular block in the blockchain. + BlockNumber() (hexutil.Uint64, error) + GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpc.BlockNumber) *hexutil.Uint + + // Reading Transactions + // + // Retrieves information on the state data for addresses regardless of whether + // it is a user or a smart contract. + GetTransactionByHash(hash common.Hash) (*rpc.EthTxJsonRPC, error) + GetTransactionCount(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) + GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) + // eth_getBlockReceipts + + // Writing Transactions + // + // Allows developers to both send ETH from one address to another, write data + // on-chain, and interact with smart contracts. + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SendTransaction(args evm.JsonTxArgs) (common.Hash, error) + // eth_sendPrivateTransaction + // eth_cancel PrivateTransaction + + // Account Information + // + // Returns information regarding an address's stored on-chain data. + Accounts() ([]common.Address, error) + GetBalance( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, + ) (*hexutil.Big, error) + GetStorageAt( + address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash, + ) (hexutil.Bytes, error) + GetCode( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, + ) (hexutil.Bytes, error) + GetProof( + address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash, + ) (*rpc.AccountResult, error) + + // EVM/Smart Contract Execution + // + // Allows developers to read data from the blockchain which includes executing + // smart contracts. However, no data is published to the Ethereum network. + Call( + args evm.JsonTxArgs, blockNrOrHash rpc.BlockNumberOrHash, _ *rpc.StateOverride, + ) (hexutil.Bytes, error) + + // Chain Information + // + // Returns information on the Ethereum network and internal settings. + ProtocolVersion() hexutil.Uint + GasPrice() (*hexutil.Big, error) + EstimateGas( + args evm.JsonTxArgs, blockNrOptional *rpc.BlockNumber, + ) (hexutil.Uint64, error) + FeeHistory( + blockCount gethrpc.DecimalOrHex, lastBlock gethrpc.BlockNumber, rewardPercentiles []float64, + ) (*rpc.FeeHistoryResult, error) + MaxPriorityFeePerGas() (*hexutil.Big, error) + ChainId() (*hexutil.Big, error) + + // Getting Uncles + // + // Returns information on uncle blocks are which are network rejected blocks + // and replaced by a canonical block instead. + GetUncleByBlockHashAndIndex( + hash common.Hash, idx hexutil.Uint, + ) map[string]interface{} + GetUncleByBlockNumberAndIndex( + number, idx hexutil.Uint, + ) map[string]interface{} + GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint + GetUncleCountByBlockNumber(blockNum rpc.BlockNumber) hexutil.Uint + + // Proof of Work + Hashrate() hexutil.Uint64 + Mining() bool + + // Other + Syncing() (interface{}, error) + Coinbase() (string, error) + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + GetTransactionLogs(txHash common.Hash) ([]*gethcore.Log, error) + SignTypedData( + address common.Address, typedData apitypes.TypedData, + ) (hexutil.Bytes, error) + FillTransaction( + args evm.JsonTxArgs, + ) (*rpc.SignTransactionResult, error) + Resend( + ctx context.Context, args evm.JsonTxArgs, + gasPrice *hexutil.Big, gasLimit *hexutil.Uint64, + ) (common.Hash, error) + GetPendingTransactions() ([]*rpc.EthTxJsonRPC, error) + // eth_signTransaction (on Ethereum.org) + // eth_getCompilers (on Ethereum.org) + // eth_compileSolidity (on Ethereum.org) + // eth_compileLLL (on Ethereum.org) + // eth_compileSerpent (on Ethereum.org) + // eth_getWork (on Ethereum.org) + // eth_submitWork (on Ethereum.org) + // eth_submitHashrate (on Ethereum.org) +} + +var _ IEthAPI = (*EthAPI)(nil) + +// EthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type EthAPI struct { + ctx context.Context + logger log.Logger + backend backend.EVMBackend +} + +// NewImplEthAPI creates an instance of the public ETH Web3 API. +func NewImplEthAPI(logger log.Logger, backend backend.EVMBackend) *EthAPI { + api := &EthAPI{ + ctx: context.Background(), + logger: logger.With("client", "json-rpc"), + backend: backend, + } + + return api +} + +// -------------------------------------------------------------------------- +// Blocks +// -------------------------------------------------------------------------- + +// BlockNumber returns the current block number. +func (e *EthAPI) BlockNumber() (hexutil.Uint64, error) { + e.logger.Debug("eth_blockNumber") + return e.backend.BlockNumber() +} + +// GetBlockByNumber returns the block identified by number. +func (e *EthAPI) GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) + return e.backend.GetBlockByNumber(ethBlockNum, fullTx) +} + +// GetBlockByHash returns the block identified by hash. +func (e *EthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) + return e.backend.GetBlockByHash(hash, fullTx) +} + +// -------------------------------------------------------------------------- +// Read Txs +// -------------------------------------------------------------------------- + +// GetTransactionByHash returns the transaction identified by hash. +func (e *EthAPI) GetTransactionByHash(hash common.Hash) (*rpc.EthTxJsonRPC, error) { + e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex()) + return e.backend.GetTransactionByHash(hash) +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (e *EthAPI) GetTransactionCount( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, +) (*hexutil.Uint64, error) { + e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) + blockNum, err := e.backend.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + return e.backend.GetTransactionCount(address, blockNum) +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (e *EthAPI) GetTransactionReceipt( + hash common.Hash, +) (map[string]interface{}, error) { + hexTx := hash.Hex() + e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) + return e.backend.GetTransactionReceipt(hash) +} + +// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. +func (e *EthAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) + return e.backend.GetBlockTransactionCountByHash(hash) +} + +// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. +func (e *EthAPI) GetBlockTransactionCountByNumber( + blockNum rpc.BlockNumber, +) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64()) + return e.backend.GetBlockTransactionCountByNumber(blockNum) +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (e *EthAPI) GetTransactionByBlockHashAndIndex( + hash common.Hash, idx hexutil.Uint, +) (*rpc.EthTxJsonRPC, error) { + e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) + return e.backend.GetTransactionByBlockHashAndIndex(hash, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (e *EthAPI) GetTransactionByBlockNumberAndIndex( + blockNum rpc.BlockNumber, idx hexutil.Uint, +) (*rpc.EthTxJsonRPC, error) { + e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + return e.backend.GetTransactionByBlockNumberAndIndex(blockNum, idx) +} + +// -------------------------------------------------------------------------- +// Write Txs +// -------------------------------------------------------------------------- + +// SendRawTransaction send a raw Ethereum transaction. +func (e *EthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + e.logger.Debug("eth_sendRawTransaction", "length", len(data)) + return e.backend.SendRawTransaction(data) +} + +// SendTransaction sends an Ethereum transaction. +func (e *EthAPI) SendTransaction( + txArgs evm.JsonTxArgs, +) (common.Hash, error) { + e.logger.Debug("eth_sendTransaction", "args", txArgs.String()) + return e.backend.SendTransaction(txArgs) +} + +// -------------------------------------------------------------------------- +// Account Information +// -------------------------------------------------------------------------- + +// Accounts returns the list of accounts available to this node. +func (e *EthAPI) Accounts() ([]common.Address, error) { + e.logger.Debug("eth_accounts") + return e.backend.Accounts() +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (e *EthAPI) GetBalance( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, +) (*hexutil.Big, error) { + e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash) + return e.backend.GetBalance(address, blockNrOrHash) +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (e *EthAPI) GetStorageAt( + address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash, +) (hexutil.Bytes, error) { + e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash) + return e.backend.GetStorageAt(address, key, blockNrOrHash) +} + +// GetCode returns the contract code at the given address and block number. +func (e *EthAPI) GetCode( + address common.Address, blockNrOrHash rpc.BlockNumberOrHash, +) (hexutil.Bytes, error) { + e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash) + return e.backend.GetCode(address, blockNrOrHash) +} + +// GetProof returns an account object with proof and any storage proofs +func (e *EthAPI) GetProof(address common.Address, + storageKeys []string, + blockNrOrHash rpc.BlockNumberOrHash, +) (*rpc.AccountResult, error) { + e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash) + return e.backend.GetProof(address, storageKeys, blockNrOrHash) +} + +// -------------------------------------------------------------------------- +// EVM/Smart Contract Execution +// -------------------------------------------------------------------------- + +// Call performs a raw contract call. +func (e *EthAPI) Call(args evm.JsonTxArgs, + blockNrOrHash rpc.BlockNumberOrHash, + _ *rpc.StateOverride, +) (hexutil.Bytes, error) { + e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash) + + blockNum, err := e.backend.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + data, err := e.backend.DoCall(args, blockNum) + if err != nil { + return []byte{}, err + } + + return (hexutil.Bytes)(data.Ret), nil +} + +// -------------------------------------------------------------------------- +// Event Logs +// -------------------------------------------------------------------------- +// FILTER API at ./filters/api.go + +// -------------------------------------------------------------------------- +// Chain Information +// -------------------------------------------------------------------------- + +// ProtocolVersion returns the supported Ethereum protocol version. +func (e *EthAPI) ProtocolVersion() hexutil.Uint { + e.logger.Debug("eth_protocolVersion") + return hexutil.Uint(eth.ProtocolVersion) +} + +// GasPrice returns the current gas price based on Ethermint's gas price oracle. +func (e *EthAPI) GasPrice() (*hexutil.Big, error) { + e.logger.Debug("eth_gasPrice") + return e.backend.GasPrice() +} + +// EstimateGas returns an estimate of gas usage for the given smart contract call. +func (e *EthAPI) EstimateGas( + args evm.JsonTxArgs, blockNrOptional *rpc.BlockNumber, +) (hexutil.Uint64, error) { + e.logger.Debug("eth_estimateGas") + return e.backend.EstimateGas(args, blockNrOptional) +} + +func (e *EthAPI) FeeHistory(blockCount gethrpc.DecimalOrHex, + lastBlock gethrpc.BlockNumber, + rewardPercentiles []float64, +) (*rpc.FeeHistoryResult, error) { + e.logger.Debug("eth_feeHistory") + return e.backend.FeeHistory(blockCount, lastBlock, rewardPercentiles) +} + +// MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee +// transactions. +func (e *EthAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { + e.logger.Debug("eth_maxPriorityFeePerGas") + head, err := e.backend.CurrentHeader() + if err != nil { + return nil, err + } + tipcap, err := e.backend.SuggestGasTipCap(head.BaseFee) + if err != nil { + return nil, err + } + return (*hexutil.Big)(tipcap), nil +} + +// ChainId is the EIP-155 replay-protection chain id for the current ethereum +// chain config. +func (e *EthAPI) ChainId() (*hexutil.Big, error) { //nolint + e.logger.Debug("eth_chainId") + return e.backend.ChainID() +} + +// -------------------------------------------------------------------------- +// Uncles +// -------------------------------------------------------------------------- + +// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. +// Always returns nil. +func (e *EthAPI) GetUncleByBlockHashAndIndex( + _ common.Hash, _ hexutil.Uint, +) map[string]interface{} { + return nil +} + +// GetUncleByBlockNumberAndIndex returns the uncle identified by number and +// index. Always returns nil. +func (e *EthAPI) GetUncleByBlockNumberAndIndex( + _, _ hexutil.Uint, +) map[string]interface{} { + return nil +} + +// GetUncleCountByBlockHash returns the number of uncles in the block identified +// by hash. Always zero. +func (e *EthAPI) GetUncleCountByBlockHash(_ common.Hash) hexutil.Uint { + return 0 +} + +// GetUncleCountByBlockNumber returns the number of uncles in the block +// identified by number. Always zero. +func (e *EthAPI) GetUncleCountByBlockNumber(_ rpc.BlockNumber) hexutil.Uint { + return 0 +} + +// -------------------------------------------------------------------------- +// Proof of Work +// -------------------------------------------------------------------------- + +// Hashrate returns the current node's hashrate. Always 0. +func (e *EthAPI) Hashrate() hexutil.Uint64 { + e.logger.Debug("eth_hashrate") + return 0 +} + +// Mining returns whether or not this node is currently mining. Always false. +func (e *EthAPI) Mining() bool { + e.logger.Debug("eth_mining") + return false +} + +// -------------------------------------------------------------------------- +// Other +// -------------------------------------------------------------------------- + +// Syncing returns false in case the node is currently not syncing with the +// network. It can be up to date or has not yet received the latest block headers +// from its pears. In case it is synchronizing: +// +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (e *EthAPI) Syncing() (interface{}, error) { + e.logger.Debug("eth_syncing") + return e.backend.Syncing() +} + +// Coinbase is the address that staking rewards will be send to (alias for Etherbase). +func (e *EthAPI) Coinbase() (string, error) { + e.logger.Debug("eth_coinbase") + + coinbase, err := e.backend.GetCoinbase() + if err != nil { + return "", err + } + ethAddr := common.BytesToAddress(coinbase.Bytes()) + return ethAddr.Hex(), nil +} + +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (e *EthAPI) Sign( + address common.Address, data hexutil.Bytes, +) (hexutil.Bytes, error) { + e.logger.Debug("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data)) + return e.backend.Sign(address, data) +} + +// GetTransactionLogs returns the logs given a transaction hash. +func (e *EthAPI) GetTransactionLogs(txHash common.Hash) ([]*gethcore.Log, error) { + e.logger.Debug("eth_getTransactionLogs", "hash", txHash) + + hexTx := txHash.Hex() + res, err := e.backend.GetTxByEthHash(txHash) + if err != nil { + e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + if res.Failed { + // failed, return empty logs + return nil, nil + } + + resBlockResult, err := e.backend.TendermintBlockResultByNumber(&res.Height) + if err != nil { + e.logger.Debug("block result not found", "number", res.Height, "error", err.Error()) + return nil, nil + } + + // parse tx logs from events + index := int(res.MsgIndex) // #nosec G701 + return backend.TxLogsFromEvents(resBlockResult.TxsResults[res.TxIndex].Events, index) +} + +// SignTypedData signs EIP-712 conformant typed data +func (e *EthAPI) SignTypedData( + address common.Address, typedData apitypes.TypedData, +) (hexutil.Bytes, error) { + e.logger.Debug( + "eth_signTypedData", "address", address.Hex(), "data", typedData, + ) + return e.backend.SignTypedData(address, typedData) +} + +// FillTransaction fills the defaults (nonce, gas, gasPrice or 1559 fields) +// on a given unsigned transaction, and returns it to the caller for further +// processing (signing + broadcast). +func (e *EthAPI) FillTransaction( + args evm.JsonTxArgs, +) (*rpc.SignTransactionResult, error) { + // Set some sanity defaults and terminate on failure + args, err := e.backend.SetTxDefaults(args) + if err != nil { + return nil, err + } + + // Assemble the transaction and obtain rlp + tx := args.ToTransaction().AsTransaction() + + data, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + + return &rpc.SignTransactionResult{ + Raw: data, + Tx: tx, + }, nil +} + +// Resend accepts an existing transaction and a new gas price and limit. It will +// remove the given transaction from the pool and reinsert it with the new gas +// price and limit. +func (e *EthAPI) Resend(_ context.Context, + args evm.JsonTxArgs, + gasPrice *hexutil.Big, + gasLimit *hexutil.Uint64, +) (common.Hash, error) { + e.logger.Debug("eth_resend", "args", args.String()) + return e.backend.Resend(args, gasPrice, gasLimit) +} + +// GetPendingTransactions returns the transactions that are in the transaction +// pool and have a from address that is one of the accounts this node manages. +func (e *EthAPI) GetPendingTransactions() ([]*rpc.EthTxJsonRPC, error) { + e.logger.Debug("eth_getPendingTransactions") + + txs, err := e.backend.PendingTransactions() + if err != nil { + return nil, err + } + + result := make([]*rpc.EthTxJsonRPC, 0, len(txs)) + for _, tx := range txs { + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + // not valid ethereum tx + break + } + + rpctx, err := rpc.NewRPCTxFromMsg( + ethMsg, + common.Hash{}, + uint64(0), + uint64(0), + nil, + e.backend.ChainConfig().ChainID, + ) + if err != nil { + return nil, err + } + + result = append(result, rpctx) + } + } + + return result, nil +} diff --git a/eth/rpc/rpcapi/filtersapi/api.go b/eth/rpc/rpcapi/filtersapi/api.go new file mode 100644 index 000000000..bb9f5ea69 --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/api.go @@ -0,0 +1,646 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package filtersapi + +import ( + "context" + "fmt" + "math/big" + "sync" + "time" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/NibiruChain/nibiru/eth/rpc" + + "github.com/cometbft/cometbft/libs/log" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// IFilterAPI +type IFilterAPI interface { + NewPendingTransactionFilter() gethrpc.ID + NewBlockFilter() gethrpc.ID + NewFilter(criteria filters.FilterCriteria) (gethrpc.ID, error) + GetFilterChanges(id gethrpc.ID) (interface{}, error) + GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*gethcore.Log, error) + UninstallFilter(id gethrpc.ID) bool + GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*gethcore.Log, error) +} + +// IFilterEthBackend defines the methods requided by the PublicFilterAPI backend +type IFilterEthBackend interface { + GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) + HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, error) + HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) + TendermintBlockByHash(hash common.Hash) (*coretypes.ResultBlock, error) + TendermintBlockResultByNumber(height *int64) (*coretypes.ResultBlockResults, error) + GetLogs(blockHash common.Hash) ([][]*gethcore.Log, error) + GetLogsByHeight(*int64) ([][]*gethcore.Log, error) + BlockBloom(blockRes *coretypes.ResultBlockResults) (gethcore.Bloom, error) + + BloomStatus() (uint64, uint64) + + RPCFilterCap() int32 + RPCLogsCap() int32 + RPCBlockRangeCap() int32 +} + +// consider a filter inactive if it has not been polled for within deadlineForInactivity +func deadlineForInactivity() time.Duration { return 5 * time.Minute } + +// filter is a helper struct that holds meta information over the filter type and +// associated subscription in the event system. +type filter struct { + typ filters.Type + deadline *time.Timer // filter is inactive when deadline triggers + hashes []common.Hash + crit filters.FilterCriteria + logs []*gethcore.Log + s *Subscription // associated subscription in event system +} + +// FiltersAPI offers support to create and manage filters. This will allow +// external clients to retrieve various information related to the Ethereum +// protocol such as blocks, transactions and logs. +type FiltersAPI struct { + logger log.Logger + clientCtx client.Context + backend IFilterEthBackend + events *EventSystem + filtersMu sync.Mutex + filters map[gethrpc.ID]*filter +} + +// NewImplFiltersAPI returns a new PublicFilterAPI instance. +func NewImplFiltersAPI(logger log.Logger, clientCtx client.Context, tmWSClient *rpcclient.WSClient, backend IFilterEthBackend) *FiltersAPI { + logger = logger.With("api", "filter") + api := &FiltersAPI{ + logger: logger, + clientCtx: clientCtx, + backend: backend, + filters: make(map[gethrpc.ID]*filter), + events: NewEventSystem(logger, tmWSClient), + } + + go api.timeoutLoop() + + return api +} + +// timeoutLoop runs every 5 minutes and deletes filters that have not been recently used. +// Tt is started when the api is created. +func (api *FiltersAPI) timeoutLoop() { + ticker := time.NewTicker(deadlineForInactivity()) + defer ticker.Stop() + + for { + <-ticker.C + api.filtersMu.Lock() + // #nosec G705 + for id, f := range api.filters { + select { + case <-f.deadline.C: + f.s.Unsubscribe(api.events) + delete(api.filters, id) + default: + continue + } + } + api.filtersMu.Unlock() + } +} + +// NewPendingTransactionFilter creates a filter that fetches pending transaction +// hashes as transactions enter the pending state. +// +// It is part of the filter package because this filter can be used through the +// `eth_getFilterChanges` polling method that is also used for log filters. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newPendingTransactionFilter +func (api *FiltersAPI) NewPendingTransactionFilter() gethrpc.ID { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return gethrpc.ID("error creating pending tx filter: max limit reached") + } + + pendingTxSub, cancelSubs, err := api.events.SubscribePendingTxs() + if err != nil { + // wrap error on the ID + return gethrpc.ID(fmt.Sprintf("error creating pending tx filter: %s", err.Error())) + } + + api.filters[pendingTxSub.ID()] = &filter{ + typ: filters.PendingTransactionsSubscription, + deadline: time.NewTimer(deadlineForInactivity()), + hashes: make([]common.Hash, 0), + s: pendingTxSub, + } + + go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) { + defer cancelSubs() + + for { + select { + case ev, ok := <-txsCh: + if !ok { + api.filtersMu.Lock() + delete(api.filters, pendingTxSub.ID()) + api.filtersMu.Unlock() + return + } + + data, ok := ev.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + tx, err := api.clientCtx.TxConfig.TxDecoder()(data.Tx) + if err != nil { + api.logger.Debug("fail to decode tx", "error", err.Error()) + continue + } + + api.filtersMu.Lock() + if f, found := api.filters[pendingTxSub.ID()]; found { + for _, msg := range tx.GetMsgs() { + ethTx, ok := msg.(*evm.MsgEthereumTx) + if ok { + f.hashes = append(f.hashes, ethTx.AsTransaction().Hash()) + } + } + } + api.filtersMu.Unlock() + case <-errCh: + api.filtersMu.Lock() + delete(api.filters, pendingTxSub.ID()) + api.filtersMu.Unlock() + } + } + }(pendingTxSub.eventCh, pendingTxSub.Err()) + + return pendingTxSub.ID() +} + +// NewPendingTransactions creates a subscription that is triggered each time a +// transaction enters the transaction pool and was signed from one of the +// transactions this nodes manages. +func (api *FiltersAPI) NewPendingTransactions(ctx context.Context) (*gethrpc.Subscription, error) { + notifier, supported := gethrpc.NotifierFromContext(ctx) + if !supported { + return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported + } + + rpcSub := notifier.CreateSubscription() + + ctx, cancelFn := context.WithTimeout(context.Background(), deadlineForInactivity()) + defer cancelFn() + + api.events.WithContext(ctx) + + pendingTxSub, cancelSubs, err := api.events.SubscribePendingTxs() + if err != nil { + return nil, err + } + + go func(txsCh <-chan coretypes.ResultEvent) { + defer cancelSubs() + + for { + select { + case ev, ok := <-txsCh: + if !ok { + api.filtersMu.Lock() + delete(api.filters, pendingTxSub.ID()) + api.filtersMu.Unlock() + return + } + + data, ok := ev.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + tx, err := api.clientCtx.TxConfig.TxDecoder()(data.Tx) + if err != nil { + api.logger.Debug("fail to decode tx", "error", err.Error()) + continue + } + + for _, msg := range tx.GetMsgs() { + ethTx, ok := msg.(*evm.MsgEthereumTx) + if ok { + _ = notifier.Notify(rpcSub.ID, ethTx.AsTransaction().Hash()) // #nosec G703 + } + } + case <-rpcSub.Err(): + pendingTxSub.Unsubscribe(api.events) + return + case <-notifier.Closed(): + pendingTxSub.Unsubscribe(api.events) + return + } + } + }(pendingTxSub.eventCh) + + return rpcSub, err +} + +// NewBlockFilter creates a filter that fetches blocks that are imported into the +// chain. It is part of the filter package since polling goes with +// eth_getFilterChanges. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter +func (api *FiltersAPI) NewBlockFilter() gethrpc.ID { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return gethrpc.ID("error creating block filter: max limit reached") + } + + headerSub, cancelSubs, err := api.events.SubscribeNewHeads() + if err != nil { + // wrap error on the ID + return gethrpc.ID(fmt.Sprintf("error creating block filter: %s", err.Error())) + } + + api.filters[headerSub.ID()] = &filter{typ: filters.BlocksSubscription, deadline: time.NewTimer(deadlineForInactivity()), hashes: []common.Hash{}, s: headerSub} + + go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) { + defer cancelSubs() + + for { + select { + case ev, ok := <-headersCh: + if !ok { + api.filtersMu.Lock() + delete(api.filters, headerSub.ID()) + api.filtersMu.Unlock() + return + } + + data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + api.filtersMu.Lock() + if f, found := api.filters[headerSub.ID()]; found { + f.hashes = append(f.hashes, common.BytesToHash(data.Header.Hash())) + } + api.filtersMu.Unlock() + case <-errCh: + api.filtersMu.Lock() + delete(api.filters, headerSub.ID()) + api.filtersMu.Unlock() + return + } + } + }(headerSub.eventCh, headerSub.Err()) + + return headerSub.ID() +} + +// NewHeads send a notification each time a new (header) block is appended to the +// chain. +func (api *FiltersAPI) NewHeads(ctx context.Context) (*gethrpc.Subscription, error) { + notifier, supported := gethrpc.NotifierFromContext(ctx) + if !supported { + return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported + } + + api.events.WithContext(ctx) + rpcSub := notifier.CreateSubscription() + + headersSub, cancelSubs, err := api.events.SubscribeNewHeads() + if err != nil { + return &gethrpc.Subscription{}, err + } + + go func(headersCh <-chan coretypes.ResultEvent) { + defer cancelSubs() + + for { + select { + case ev, ok := <-headersCh: + if !ok { + headersSub.Unsubscribe(api.events) + return + } + + data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + var baseFee *big.Int = nil + // TODO: fetch bloom from events + header := rpc.EthHeaderFromTendermint(data.Header, gethcore.Bloom{}, baseFee) + _ = notifier.Notify(rpcSub.ID, header) // #nosec G703 + case <-rpcSub.Err(): + headersSub.Unsubscribe(api.events) + return + case <-notifier.Closed(): + headersSub.Unsubscribe(api.events) + return + } + } + }(headersSub.eventCh) + + return rpcSub, err +} + +// Logs creates a subscription that fires for all new log that match the given +// filter criteria. +func (api *FiltersAPI) Logs(ctx context.Context, crit filters.FilterCriteria) (*gethrpc.Subscription, error) { + notifier, supported := gethrpc.NotifierFromContext(ctx) + if !supported { + return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported + } + + api.events.WithContext(ctx) + rpcSub := notifier.CreateSubscription() + + logsSub, cancelSubs, err := api.events.SubscribeLogs(crit) + if err != nil { + return &gethrpc.Subscription{}, err + } + + go func(logsCh <-chan coretypes.ResultEvent) { + defer cancelSubs() + + for { + select { + case ev, ok := <-logsCh: + if !ok { + logsSub.Unsubscribe(api.events) + return + } + + // filter only events from EVM module txs + _, isMsgEthereumTx := ev.Events[evm.TypeMsgEthereumTx] + + if !isMsgEthereumTx { + // ignore transaction as it's not from the evm module + return + } + + // get transaction result data + dataTx, ok := ev.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + txResponse, err := evm.DecodeTxResponse(dataTx.TxResult.Result.Data) + if err != nil { + api.logger.Error("fail to decode tx response", "error", err) + return + } + + logs := FilterLogs(evm.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + + for _, log := range logs { + _ = notifier.Notify(rpcSub.ID, log) // #nosec G703 + } + case <-rpcSub.Err(): // client send an unsubscribe request + logsSub.Unsubscribe(api.events) + return + case <-notifier.Closed(): // connection dropped + logsSub.Unsubscribe(api.events) + return + } + } + }(logsSub.eventCh) + + return rpcSub, err +} + +// NewFilter creates a new filter and returns the filter id. It can be +// used to retrieve logs when the state changes. This method cannot be +// used to fetch logs that are already stored in the state. +// +// Default criteria for the from and to block are "latest". +// Using "latest" as block number will return logs for mined blocks. +// Using "pending" as block number returns logs for not yet mined (pending) blocks. +// In case logs are removed (chain reorg) previously returned logs are returned +// again but with the removed property set to true. +// +// In case "fromBlock" > "toBlock" an error is returned. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter +func (api *FiltersAPI) NewFilter(criteria filters.FilterCriteria) (gethrpc.ID, error) { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if len(api.filters) >= int(api.backend.RPCFilterCap()) { + return gethrpc.ID(""), fmt.Errorf("error creating filter: max limit reached") + } + + var ( + filterID = gethrpc.ID("") + err error + ) + + logsSub, cancelSubs, err := api.events.SubscribeLogs(criteria) + if err != nil { + return gethrpc.ID(""), err + } + + filterID = logsSub.ID() + + api.filters[filterID] = &filter{ + typ: filters.LogsSubscription, + crit: criteria, + deadline: time.NewTimer(deadlineForInactivity()), + hashes: []common.Hash{}, + s: logsSub, + } + + go func(eventCh <-chan coretypes.ResultEvent) { + defer cancelSubs() + + for { + select { + case ev, ok := <-eventCh: + if !ok { + api.filtersMu.Lock() + delete(api.filters, filterID) + api.filtersMu.Unlock() + return + } + dataTx, ok := ev.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + txResponse, err := evm.DecodeTxResponse(dataTx.TxResult.Result.Data) + if err != nil { + api.logger.Error("fail to decode tx response", "error", err) + return + } + + logs := FilterLogs(evm.LogsToEthereum(txResponse.Logs), criteria.FromBlock, criteria.ToBlock, criteria.Addresses, criteria.Topics) + + api.filtersMu.Lock() + if f, found := api.filters[filterID]; found { + f.logs = append(f.logs, logs...) + } + api.filtersMu.Unlock() + case <-logsSub.Err(): + api.filtersMu.Lock() + delete(api.filters, filterID) + api.filtersMu.Unlock() + return + } + } + }(logsSub.eventCh) + + return filterID, err +} + +// GetLogs returns logs matching the given argument that are stored within the state. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs +func (api *FiltersAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*gethcore.Log, error) { + var filter *Filter + if crit.BlockHash != nil { + // Block filter requested, construct a single-shot filter + filter = NewBlockFilter(api.logger, api.backend, crit) + } else { + // Convert the RPC block numbers into internal representations + begin := gethrpc.LatestBlockNumber.Int64() + if crit.FromBlock != nil { + begin = crit.FromBlock.Int64() + } + end := gethrpc.LatestBlockNumber.Int64() + if crit.ToBlock != nil { + end = crit.ToBlock.Int64() + } + // Construct the range filter + filter = NewRangeFilter(api.logger, api.backend, begin, end, crit.Addresses, crit.Topics) + } + + // Run the filter and return all the logs + logs, err := filter.Logs(ctx, int(api.backend.RPCLogsCap()), int64(api.backend.RPCBlockRangeCap())) + if err != nil { + return nil, err + } + + return returnLogs(logs), err +} + +// UninstallFilter removes the filter with the given filter id. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter +func (api *FiltersAPI) UninstallFilter(id gethrpc.ID) bool { + api.filtersMu.Lock() + f, found := api.filters[id] + if found { + delete(api.filters, id) + } + api.filtersMu.Unlock() + + if !found { + return false + } + f.s.Unsubscribe(api.events) + return true +} + +// GetFilterLogs returns the logs for the filter with the given id. +// If the filter could not be found an empty array of logs is returned. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs +func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*gethcore.Log, error) { + api.filtersMu.Lock() + f, found := api.filters[id] + api.filtersMu.Unlock() + + if !found { + return returnLogs(nil), fmt.Errorf("filter %s not found", id) + } + + if f.typ != filters.LogsSubscription { + return returnLogs(nil), fmt.Errorf("filter %s doesn't have a LogsSubscription type: got %d", id, f.typ) + } + + var filter *Filter + if f.crit.BlockHash != nil { + // Block filter requested, construct a single-shot filter + filter = NewBlockFilter(api.logger, api.backend, f.crit) + } else { + // Convert the RPC block numbers into internal representations + begin := gethrpc.LatestBlockNumber.Int64() + if f.crit.FromBlock != nil { + begin = f.crit.FromBlock.Int64() + } + end := gethrpc.LatestBlockNumber.Int64() + if f.crit.ToBlock != nil { + end = f.crit.ToBlock.Int64() + } + // Construct the range filter + filter = NewRangeFilter(api.logger, api.backend, begin, end, f.crit.Addresses, f.crit.Topics) + } + // Run the filter and return all the logs + logs, err := filter.Logs(ctx, int(api.backend.RPCLogsCap()), int64(api.backend.RPCBlockRangeCap())) + if err != nil { + return nil, err + } + return returnLogs(logs), nil +} + +// GetFilterChanges returns the logs for the filter with the given id since +// last time it was called. This can be used for polling. +// +// For pending transaction and block filters the result is []common.Hash. +// (pending)Log filters return []Log. +// +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges +func (api *FiltersAPI) GetFilterChanges(id gethrpc.ID) (interface{}, error) { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + f, found := api.filters[id] + if !found { + return nil, fmt.Errorf("filter %s not found", id) + } + + if !f.deadline.Stop() { + // timer expired but filter is not yet removed in timeout loop + // receive timer value and reset timer + <-f.deadline.C + } + f.deadline.Reset(deadlineForInactivity()) + + switch f.typ { + case filters.PendingTransactionsSubscription, filters.BlocksSubscription: + hashes := f.hashes + f.hashes = nil + return returnHashes(hashes), nil + case filters.LogsSubscription, filters.MinedAndPendingLogsSubscription: + logs := make([]*gethcore.Log, len(f.logs)) + copy(logs, f.logs) + f.logs = []*gethcore.Log{} + return returnLogs(logs), nil + default: + return nil, fmt.Errorf("invalid filter %s type %d", id, f.typ) + } +} diff --git a/eth/rpc/rpcapi/filtersapi/filter_system.go b/eth/rpc/rpcapi/filtersapi/filter_system.go new file mode 100644 index 000000000..5d91c0963 --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/filter_system.go @@ -0,0 +1,311 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package filtersapi + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/pkg/errors" + + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/libs/log" + tmquery "github.com/cometbft/cometbft/libs/pubsub/query" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/eth/rpc/pubsub" + "github.com/NibiruChain/nibiru/x/evm" +) + +var ( + txEvents = tmtypes.QueryForEvent(tmtypes.EventTx).String() + evmEvents = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", + tmtypes.EventTypeKey, + tmtypes.EventTx, + sdk.EventTypeMessage, + sdk.AttributeKeyModule, evm.ModuleName)).String() + headerEvents = tmtypes.QueryForEvent(tmtypes.EventNewBlockHeader).String() +) + +// EventSystem creates subscriptions, processes events and broadcasts them to the +// subscription which match the subscription criteria using the Tendermint's RPC client. +type EventSystem struct { + logger log.Logger + ctx context.Context + tmWSClient *rpcclient.WSClient + + // light client mode + lightMode bool + + index filterIndex + topicChans map[string]chan<- coretypes.ResultEvent + indexMux *sync.RWMutex + + // Channels + install chan *Subscription // install filter for event notification + uninstall chan *Subscription // remove filter for event notification + eventBus pubsub.EventBus +} + +// NewEventSystem creates a new manager that listens for event on the given mux, +// parses and filters them. It uses the all map to retrieve filter changes. The +// work loop holds its own index that is used to forward events to filters. +// +// The returned manager has a loop that needs to be stopped with the Stop function +// or by stopping the given mux. +func NewEventSystem(logger log.Logger, tmWSClient *rpcclient.WSClient) *EventSystem { + index := make(filterIndex) + for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { + index[i] = make(map[gethrpc.ID]*Subscription) + } + + es := &EventSystem{ + logger: logger, + ctx: context.Background(), + tmWSClient: tmWSClient, + lightMode: false, + index: index, + topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), + indexMux: new(sync.RWMutex), + install: make(chan *Subscription), + uninstall: make(chan *Subscription), + eventBus: pubsub.NewEventBus(), + } + + go es.eventLoop() + go es.consumeEvents() + return es +} + +// WithContext sets a new context to the EventSystem. This is required to set a timeout context when +// a new filter is intantiated. +func (es *EventSystem) WithContext(ctx context.Context) { + es.ctx = ctx +} + +// subscribe performs a new event subscription to a given Tendermint event. +// The subscription creates a unidirectional receive event channel to receive the ResultEvent. +func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, pubsub.UnsubscribeFunc, error) { + var ( + err error + cancelFn context.CancelFunc + ) + + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + existingSubs := es.eventBus.Topics() + for _, topic := range existingSubs { + if topic == sub.event { + eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) + if err != nil { + err := errors.Wrapf(err, "failed to subscribe to topic: %s", sub.event) + return nil, nil, err + } + + sub.eventCh = eventCh + return sub, unsubFn, nil + } + } + + switch sub.typ { + case filters.LogsSubscription: + err = es.tmWSClient.Subscribe(ctx, sub.event) + case filters.BlocksSubscription: + err = es.tmWSClient.Subscribe(ctx, sub.event) + case filters.PendingTransactionsSubscription: + err = es.tmWSClient.Subscribe(ctx, sub.event) + default: + err = fmt.Errorf("invalid filter subscription type %d", sub.typ) + } + + if err != nil { + sub.err <- err + return nil, nil, err + } + + // wrap events in a go routine to prevent blocking + es.install <- sub + <-sub.installed + + eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to subscribe to topic after installed: %s", sub.event) + } + + sub.eventCh = eventCh + return sub, unsubFn, nil +} + +// SubscribeLogs creates a subscription that will write all logs matching the +// given criteria to the given logs channel. Default value for the from and to +// block is "latest". If the fromBlock > toBlock an error is returned. +func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { + var from, to gethrpc.BlockNumber + if crit.FromBlock == nil { + from = gethrpc.LatestBlockNumber + } else { + from = gethrpc.BlockNumber(crit.FromBlock.Int64()) + } + if crit.ToBlock == nil { + to = gethrpc.LatestBlockNumber + } else { + to = gethrpc.BlockNumber(crit.ToBlock.Int64()) + } + + switch { + // only interested in new mined logs, mined logs within a specific block range, or + // logs from a specific block number to new mined blocks + case (from == gethrpc.LatestBlockNumber && to == gethrpc.LatestBlockNumber), + (from >= 0 && to >= 0 && to >= from), + (from >= 0 && to == gethrpc.LatestBlockNumber): + return es.subscribeLogs(crit) + + default: + return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to) + } +} + +// subscribeLogs creates a subscription that will write all logs matching the +// given criteria to the given logs channel. +func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { + sub := &Subscription{ + id: gethrpc.NewID(), + typ: filters.LogsSubscription, + event: evmEvents, + logsCrit: crit, + created: time.Now().UTC(), + logs: make(chan []*gethcore.Log), + installed: make(chan struct{}, 1), + err: make(chan error, 1), + } + return es.subscribe(sub) +} + +// SubscribeNewHeads subscribes to new block headers events. +func (es EventSystem) SubscribeNewHeads() (*Subscription, pubsub.UnsubscribeFunc, error) { + sub := &Subscription{ + id: gethrpc.NewID(), + typ: filters.BlocksSubscription, + event: headerEvents, + created: time.Now().UTC(), + headers: make(chan *gethcore.Header), + installed: make(chan struct{}, 1), + err: make(chan error, 1), + } + return es.subscribe(sub) +} + +// SubscribePendingTxs subscribes to new pending transactions events from the mempool. +func (es EventSystem) SubscribePendingTxs() (*Subscription, pubsub.UnsubscribeFunc, error) { + sub := &Subscription{ + id: gethrpc.NewID(), + typ: filters.PendingTransactionsSubscription, + event: txEvents, + created: time.Now().UTC(), + hashes: make(chan []common.Hash), + installed: make(chan struct{}, 1), + err: make(chan error, 1), + } + return es.subscribe(sub) +} + +type filterIndex map[filters.Type]map[gethrpc.ID]*Subscription + +// eventLoop (un)installs filters and processes mux events. +func (es *EventSystem) eventLoop() { + for { + select { + case f := <-es.install: + es.indexMux.Lock() + es.index[f.typ][f.id] = f + ch := make(chan coretypes.ResultEvent) + if err := es.eventBus.AddTopic(f.event, ch); err != nil { + es.logger.Error("failed to add event topic to event bus", "topic", f.event, "error", err.Error()) + } else { + es.topicChans[f.event] = ch + } + es.indexMux.Unlock() + close(f.installed) + case f := <-es.uninstall: + es.indexMux.Lock() + delete(es.index[f.typ], f.id) + + var channelInUse bool + // #nosec G705 + for _, sub := range es.index[f.typ] { + if sub.event == f.event { + channelInUse = true + break + } + } + + // remove topic only when channel is not used by other subscriptions + if !channelInUse { + if err := es.tmWSClient.Unsubscribe(es.ctx, f.event); err != nil { + es.logger.Error("failed to unsubscribe from query", "query", f.event, "error", err.Error()) + } + + ch, ok := es.topicChans[f.event] + if ok { + es.eventBus.RemoveTopic(f.event) + close(ch) + delete(es.topicChans, f.event) + } + } + + es.indexMux.Unlock() + close(f.err) + } + } +} + +func (es *EventSystem) consumeEvents() { + for { + for rpcResp := range es.tmWSClient.ResponsesCh { + var ev coretypes.ResultEvent + + if rpcResp.Error != nil { + time.Sleep(5 * time.Second) + continue + } else if err := tmjson.Unmarshal(rpcResp.Result, &ev); err != nil { + es.logger.Error("failed to JSON unmarshal ResponsesCh result event", "error", err.Error()) + continue + } + + if len(ev.Query) == 0 { + // skip empty responses + continue + } + + es.indexMux.RLock() + ch, ok := es.topicChans[ev.Query] + es.indexMux.RUnlock() + if !ok { + es.logger.Debug("channel for subscription not found", "topic", ev.Query) + es.logger.Debug("list of available channels", "channels", es.eventBus.Topics()) + continue + } + + // gracefully handle lagging subscribers + t := time.NewTimer(time.Second) + select { + case <-t.C: + es.logger.Debug("dropped event during lagging subscription", "topic", ev.Query) + case ch <- ev: + } + } + + time.Sleep(time.Second) + } +} diff --git a/eth/rpc/rpcapi/filtersapi/filter_system_test.go b/eth/rpc/rpcapi/filtersapi/filter_system_test.go new file mode 100644 index 000000000..d55b2c188 --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/filter_system_test.go @@ -0,0 +1,73 @@ +package filtersapi + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/NibiruChain/nibiru/eth/rpc/pubsub" +) + +func makeSubscription(id, event string) *Subscription { + return &Subscription{ + id: rpc.ID(id), + typ: filters.LogsSubscription, + event: event, + created: time.Now(), + logs: make(chan []*gethcore.Log), + hashes: make(chan []common.Hash), + headers: make(chan *gethcore.Header), + installed: make(chan struct{}), + eventCh: make(chan coretypes.ResultEvent), + err: make(chan error), + } +} + +func TestFilterSystem(t *testing.T) { + index := make(filterIndex) + for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { + index[i] = make(map[rpc.ID]*Subscription) + } + es := &EventSystem{ + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)), + ctx: context.Background(), + lightMode: false, + index: index, + topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), + indexMux: new(sync.RWMutex), + install: make(chan *Subscription), + uninstall: make(chan *Subscription), + eventBus: pubsub.NewEventBus(), + } + go es.eventLoop() + + event := "event" + sub := makeSubscription("1", event) + es.install <- sub + <-sub.installed + ch, ok := es.topicChans[sub.event] + if !ok { + t.Error("expect topic channel exist") + } + + sub = makeSubscription("2", event) + es.install <- sub + <-sub.installed + newCh, ok := es.topicChans[sub.event] + if !ok { + t.Error("expect topic channel exist") + } + + if newCh != ch { + t.Error("expect topic channel unchanged") + } +} diff --git a/eth/rpc/rpcapi/filtersapi/filters.go b/eth/rpc/rpcapi/filtersapi/filters.go new file mode 100644 index 000000000..4710dff8c --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/filters.go @@ -0,0 +1,268 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package filtersapi + +import ( + "context" + "encoding/binary" + "fmt" + "math/big" + + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/backend" + + "github.com/cometbft/cometbft/libs/log" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/filters" +) + +// BloomIV represents the bit indexes and value inside the bloom filter that belong +// to some key. +type BloomIV struct { + I [3]uint + V [3]byte +} + +// Filter can be used to retrieve and filter logs. +type Filter struct { + logger log.Logger + backend IFilterEthBackend + criteria filters.FilterCriteria + + bloomFilters [][]BloomIV // Filter the system is matching for +} + +// NewBlockFilter creates a new filter which directly inspects the contents of +// a block to figure out whether it is interesting or not. +func NewBlockFilter(logger log.Logger, backend IFilterEthBackend, criteria filters.FilterCriteria) *Filter { + // Create a generic filter and convert it into a block filter + return newFilter(logger, backend, criteria, nil) +} + +// NewRangeFilter creates a new filter which uses a bloom filter on blocks to +// figure out whether a particular block is interesting or not. +func NewRangeFilter(logger log.Logger, backend IFilterEthBackend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { + // Flatten the address and topic filter clauses into a single bloombits filter + // system. Since the bloombits are not positional, nil topics are permitted, + // which get flattened into a nil byte slice. + var filtersBz [][][]byte //nolint: prealloc + if len(addresses) > 0 { + filter := make([][]byte, len(addresses)) + for i, address := range addresses { + filter[i] = address.Bytes() + } + filtersBz = append(filtersBz, filter) + } + + for _, topicList := range topics { + filter := make([][]byte, len(topicList)) + for i, topic := range topicList { + filter[i] = topic.Bytes() + } + filtersBz = append(filtersBz, filter) + } + + // Create a generic filter and convert it into a range filter + criteria := filters.FilterCriteria{ + FromBlock: big.NewInt(begin), + ToBlock: big.NewInt(end), + Addresses: addresses, + Topics: topics, + } + + return newFilter(logger, backend, criteria, createBloomFilters(filtersBz, logger)) +} + +// newFilter returns a new Filter +func newFilter(logger log.Logger, backend IFilterEthBackend, criteria filters.FilterCriteria, bloomFilters [][]BloomIV) *Filter { + return &Filter{ + logger: logger, + backend: backend, + criteria: criteria, + bloomFilters: bloomFilters, + } +} + +const ( + maxToOverhang = 600 +) + +// Logs searches the blockchain for matching log entries, returning all from the +// first block that contains matches, updating the start of the filter accordingly. +func (f *Filter) Logs(_ context.Context, logLimit int, blockLimit int64) ([]*gethcore.Log, error) { + logs := []*gethcore.Log{} + var err error + + // If we're doing singleton block filtering, execute and return + if f.criteria.BlockHash != nil && *f.criteria.BlockHash != (common.Hash{}) { + resBlock, err := f.backend.TendermintBlockByHash(*f.criteria.BlockHash) + if err != nil { + return nil, fmt.Errorf("failed to fetch header by hash %s: %w", f.criteria.BlockHash, err) + } + + blockRes, err := f.backend.TendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + f.logger.Debug("failed to fetch block result from Tendermint", "height", resBlock.Block.Height, "error", err.Error()) + return nil, nil + } + + bloom, err := f.backend.BlockBloom(blockRes) + if err != nil { + return nil, err + } + + return f.blockLogs(blockRes, bloom) + } + + // Figure out the limits of the filter range + header, err := f.backend.HeaderByNumber(rpc.EthLatestBlockNumber) + if err != nil { + return nil, fmt.Errorf("failed to fetch header by number (latest): %w", err) + } + + if header == nil || header.Number == nil { + f.logger.Debug("header not found or has no number") + return nil, nil + } + + head := header.Number.Int64() + if f.criteria.FromBlock.Int64() < 0 { + f.criteria.FromBlock = big.NewInt(head) + } else if f.criteria.FromBlock.Int64() == 0 { + f.criteria.FromBlock = big.NewInt(1) + } + if f.criteria.ToBlock.Int64() < 0 { + f.criteria.ToBlock = big.NewInt(head) + } else if f.criteria.ToBlock.Int64() == 0 { + f.criteria.ToBlock = big.NewInt(1) + } + + if f.criteria.ToBlock.Int64()-f.criteria.FromBlock.Int64() > blockLimit { + return nil, fmt.Errorf("maximum [from, to] blocks distance: %d", blockLimit) + } + + // check bounds + if f.criteria.FromBlock.Int64() > head { + return []*gethcore.Log{}, nil + } else if f.criteria.ToBlock.Int64() > head+maxToOverhang { + f.criteria.ToBlock = big.NewInt(head + maxToOverhang) + } + + from := f.criteria.FromBlock.Int64() + to := f.criteria.ToBlock.Int64() + + for height := from; height <= to; height++ { + blockRes, err := f.backend.TendermintBlockResultByNumber(&height) + if err != nil { + f.logger.Debug("failed to fetch block result from Tendermint", "height", height, "error", err.Error()) + return nil, nil + } + + bloom, err := f.backend.BlockBloom(blockRes) + if err != nil { + return nil, err + } + + filtered, err := f.blockLogs(blockRes, bloom) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch block by number %d", height) + } + + // check logs limit + if len(logs)+len(filtered) > logLimit { + return nil, fmt.Errorf("query returned more than %d results", logLimit) + } + logs = append(logs, filtered...) + } + return logs, nil +} + +// blockLogs returns the logs matching the filter criteria within a single block. +func (f *Filter) blockLogs(blockRes *tmrpctypes.ResultBlockResults, bloom gethcore.Bloom) ([]*gethcore.Log, error) { + if !bloomFilter(bloom, f.criteria.Addresses, f.criteria.Topics) { + return []*gethcore.Log{}, nil + } + + logsList, err := backend.GetLogsFromBlockResults(blockRes) + if err != nil { + return []*gethcore.Log{}, errors.Wrapf(err, "failed to fetch logs block number %d", blockRes.Height) + } + + unfiltered := make([]*gethcore.Log, 0) + for _, logs := range logsList { + unfiltered = append(unfiltered, logs...) + } + + logs := FilterLogs(unfiltered, nil, nil, f.criteria.Addresses, f.criteria.Topics) + if len(logs) == 0 { + return []*gethcore.Log{}, nil + } + + return logs, nil +} + +func createBloomFilters(filters [][][]byte, logger log.Logger) [][]BloomIV { + bloomFilters := make([][]BloomIV, 0) + for _, filter := range filters { + // Gather the bit indexes of the filter rule, special casing the nil filter + if len(filter) == 0 { + continue + } + bloomIVs := make([]BloomIV, len(filter)) + + // Transform the filter rules (the addresses and topics) to the bloom index and value arrays + // So it can be used to compare with the bloom of the block header. If the rule has any nil + // clauses. The rule will be ignored. + for i, clause := range filter { + if clause == nil { + bloomIVs = nil + break + } + + iv, err := calcBloomIVs(clause) + if err != nil { + bloomIVs = nil + logger.Error("calcBloomIVs error: %v", err) + break + } + + bloomIVs[i] = iv + } + // Accumulate the filter rules if no nil rule was within + if bloomIVs != nil { + bloomFilters = append(bloomFilters, bloomIVs) + } + } + return bloomFilters +} + +// calcBloomIVs returns BloomIV for the given data, +// revised from https://github.com/ethereum/go-ethereum/blob/401354976bb44f0ad4455ca1e0b5c0dc31d9a5f5/core/types/bloom9.go#L139 +func calcBloomIVs(data []byte) (BloomIV, error) { + hashbuf := make([]byte, 6) + biv := BloomIV{} + + sha := crypto.NewKeccakState() + sha.Reset() + if _, err := sha.Write(data); err != nil { + return BloomIV{}, err + } + if _, err := sha.Read(hashbuf); err != nil { + return BloomIV{}, err + } + + // The actual bits to flip + biv.V[0] = byte(1 << (hashbuf[1] & 0x7)) + biv.V[1] = byte(1 << (hashbuf[3] & 0x7)) + biv.V[2] = byte(1 << (hashbuf[5] & 0x7)) + // The indices for the bytes to OR in + biv.I[0] = gethcore.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf)&0x7ff)>>3) - 1 + biv.I[1] = gethcore.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[2:])&0x7ff)>>3) - 1 + biv.I[2] = gethcore.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[4:])&0x7ff)>>3) - 1 + + return biv, nil +} diff --git a/eth/rpc/rpcapi/filtersapi/subscription.go b/eth/rpc/rpcapi/filtersapi/subscription.go new file mode 100644 index 000000000..fc2ac0f91 --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/subscription.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package filtersapi + +import ( + "time" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/rpc" +) + +// Subscription defines a wrapper for the private subscription +type Subscription struct { + id rpc.ID + typ filters.Type + event string + created time.Time + logsCrit filters.FilterCriteria + logs chan []*gethcore.Log + hashes chan []common.Hash + headers chan *gethcore.Header + installed chan struct{} // closed when the filter is installed + eventCh <-chan coretypes.ResultEvent + err chan error +} + +// ID returns the underlying subscription RPC identifier. +func (s Subscription) ID() rpc.ID { + return s.id +} + +// Unsubscribe from the current subscription to Tendermint Websocket. It sends an error to the +// subscription error channel if unsubscribe fails. +func (s *Subscription) Unsubscribe(es *EventSystem) { + go func() { + uninstallLoop: + for { + // write uninstall request and consume logs/hashes. This prevents + // the eventLoop broadcast method to deadlock when writing to the + // filter event channel while the subscription loop is waiting for + // this method to return (and thus not reading these events). + select { + case es.uninstall <- s: + break uninstallLoop + case <-s.logs: + case <-s.hashes: + case <-s.headers: + } + } + }() +} + +// Err returns the error channel +func (s *Subscription) Err() <-chan error { + return s.err +} + +// Event returns the tendermint result event channel +func (s *Subscription) Event() <-chan coretypes.ResultEvent { + return s.eventCh +} diff --git a/eth/rpc/rpcapi/filtersapi/utils.go b/eth/rpc/rpcapi/filtersapi/utils.go new file mode 100644 index 000000000..47d27de2e --- /dev/null +++ b/eth/rpc/rpcapi/filtersapi/utils.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package filtersapi + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" +) + +// FilterLogs creates a slice of logs matching the given criteria. +// [] -> anything +// [A] -> A in first position of log topics, anything after +// [null, B] -> anything in first position, B in second position +// [A, B] -> A in first position and B in second position +// [[A, B], [A, B]] -> A or B in first position, A or B in second position +func FilterLogs(logs []*gethcore.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*gethcore.Log { + var ret []*gethcore.Log +Logs: + for _, log := range logs { + if fromBlock != nil && fromBlock.Int64() >= 0 && fromBlock.Uint64() > log.BlockNumber { + continue + } + if toBlock != nil && toBlock.Int64() >= 0 && toBlock.Uint64() < log.BlockNumber { + continue + } + if len(addresses) > 0 && !includes(addresses, log.Address) { + continue + } + // If the to filtered topics is greater than the amount of topics in logs, skip. + if len(topics) > len(log.Topics) { + continue + } + for i, sub := range topics { + match := len(sub) == 0 // empty rule set == wildcard + for _, topic := range sub { + if log.Topics[i] == topic { + match = true + break + } + } + if !match { + continue Logs + } + } + ret = append(ret, log) + } + return ret +} + +func includes(addresses []common.Address, a common.Address) bool { + for _, addr := range addresses { + if addr == a { + return true + } + } + + return false +} + +// https://github.com/ethereum/go-ethereum/blob/v1.10.14/eth/filters/filter.go#L321 +func bloomFilter(bloom gethcore.Bloom, addresses []common.Address, topics [][]common.Hash) bool { + if len(addresses) > 0 { + var included bool + for _, addr := range addresses { + if gethcore.BloomLookup(bloom, addr) { + included = true + break + } + } + if !included { + return false + } + } + + for _, sub := range topics { + included := len(sub) == 0 // empty rule set == wildcard + for _, topic := range sub { + if gethcore.BloomLookup(bloom, topic) { + included = true + break + } + } + if !included { + return false + } + } + return true +} + +// returnHashes is a helper that will return an empty hash array case the given hash array is nil, +// otherwise the given hashes array is returned. +func returnHashes(hashes []common.Hash) []common.Hash { + if hashes == nil { + return []common.Hash{} + } + return hashes +} + +// returnLogs is a helper that will return an empty log array in case the given logs array is nil, +// otherwise the given logs array is returned. +func returnLogs(logs []*gethcore.Log) []*gethcore.Log { + if logs == nil { + return []*gethcore.Log{} + } + return logs +} diff --git a/eth/rpc/rpcapi/miner_api.go b/eth/rpc/rpcapi/miner_api.go new file mode 100644 index 000000000..298d8c4a5 --- /dev/null +++ b/eth/rpc/rpcapi/miner_api.go @@ -0,0 +1,94 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "errors" + + "github.com/cosmos/cosmos-sdk/server" + + "github.com/NibiruChain/nibiru/eth/rpc/backend" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/cometbft/cometbft/libs/log" +) + +// MinerAPI is the private miner prefixed set of APIs in the Miner JSON-RPC spec. +type MinerAPI struct { + ctx *server.Context + logger log.Logger + backend backend.EVMBackend +} + +// NewImplMinerAPI creates an instance of the Miner API. +func NewImplMinerAPI( + ctx *server.Context, + backend backend.EVMBackend, +) *MinerAPI { + return &MinerAPI{ + ctx: ctx, + logger: ctx.Logger.With("api", "miner"), + backend: backend, + } +} + +// SetEtherbase sets the etherbase of the miner +func (api *MinerAPI) SetEtherbase(etherbase common.Address) bool { + api.logger.Debug("miner_setEtherbase") + return api.backend.SetEtherbase(etherbase) +} + +// SetGasPrice sets the minimum accepted gas price for the miner. +func (api *MinerAPI) SetGasPrice(gasPrice hexutil.Big) bool { + api.logger.Info(api.ctx.Viper.ConfigFileUsed()) + return api.backend.SetGasPrice(gasPrice) +} + +// ------------------------------------------------ +// Unsupported functions on the Miner API +// ------------------------------------------------ + +// GetHashrate returns the current hashrate for local CPU miner and remote miner. +// Unsupported in Ethermint +func (api *MinerAPI) GetHashrate() uint64 { + api.logger.Debug("miner_getHashrate") + api.logger.Debug("Unsupported rpc function: miner_getHashrate") + return 0 +} + +// SetExtra sets the extra data string that is included when this miner mines a block. +// Unsupported in Ethermint +func (api *MinerAPI) SetExtra(_ string) (bool, error) { + api.logger.Debug("miner_setExtra") + api.logger.Debug("Unsupported rpc function: miner_setExtra") + return false, errors.New("unsupported rpc function: miner_setExtra") +} + +// SetGasLimit sets the gaslimit to target towards during mining. +// Unsupported in Ethermint +func (api *MinerAPI) SetGasLimit(_ hexutil.Uint64) bool { + api.logger.Debug("miner_setGasLimit") + api.logger.Debug("Unsupported rpc function: miner_setGasLimit") + return false +} + +// Start starts the miner with the given number of threads. If threads is nil, +// the number of workers started is equal to the number of logical CPUs that are +// usable by this process. If mining is already running, this method adjust the +// number of threads allowed to use and updates the minimum price required by the +// transaction pool. +// Unsupported in Ethermint +func (api *MinerAPI) Start(_ *int) error { + api.logger.Debug("miner_start") + api.logger.Debug("Unsupported rpc function: miner_start") + return errors.New("unsupported rpc function: miner_start") +} + +// Stop terminates the miner, both at the consensus engine level as well as at +// the block creation level. +// Unsupported in Ethermint +func (api *MinerAPI) Stop() { + api.logger.Debug("miner_stop") + api.logger.Debug("Unsupported rpc function: miner_stop") +} diff --git a/eth/rpc/rpcapi/net_api.go b/eth/rpc/rpcapi/net_api.go new file mode 100644 index 000000000..9325a19ad --- /dev/null +++ b/eth/rpc/rpcapi/net_api.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "context" + "fmt" + + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/cosmos/cosmos-sdk/client" + + "github.com/NibiruChain/nibiru/eth" +) + +// NetAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type NetAPI struct { + networkVersion uint64 + // TODO: epic: test(eth-rpc): "github.com/NibiruChain/nibiru/x/common/testutil/cli" + // Validator.RPCClient should be used to test APIs that depend on the CometBFT + // RPC client. + tmClient rpcclient.Client +} + +// NewImplNetAPI creates an instance of the public Net Web3 API. +func NewImplNetAPI(clientCtx client.Context) *NetAPI { + // parse the chainID from a integer string + chainIDEpoch, err := eth.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + + return &NetAPI{ + networkVersion: chainIDEpoch.Uint64(), + tmClient: clientCtx.Client.(rpcclient.Client), + } +} + +// Version returns the current ethereum protocol version. +func (s *NetAPI) Version() string { + return fmt.Sprintf("%d", s.networkVersion) +} + +// Listening returns if client is actively listening for network connections. +func (s *NetAPI) Listening() bool { + ctx := context.Background() + netInfo, err := s.tmClient.NetInfo(ctx) + if err != nil { + return false + } + return netInfo.Listening +} + +// PeerCount returns the number of peers currently connected to the client. +func (s *NetAPI) PeerCount() int { + ctx := context.Background() + netInfo, err := s.tmClient.NetInfo(ctx) + if err != nil { + return 0 + } + return len(netInfo.Peers) +} diff --git a/eth/rpc/rpcapi/personal_api.go b/eth/rpc/rpcapi/personal_api.go new file mode 100644 index 000000000..1ea0f6f52 --- /dev/null +++ b/eth/rpc/rpcapi/personal_api.go @@ -0,0 +1,207 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/NibiruChain/nibiru/app/appconst" + "github.com/NibiruChain/nibiru/eth/rpc/backend" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/eth/crypto/hd" + + "github.com/cometbft/cometbft/libs/log" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// PersonalAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PersonalAPI struct { + backend backend.EVMBackend + logger log.Logger + hdPathIter eth.HDPathIterator +} + +// NewImplPersonalAPI creates an instance of the public Personal Eth API. +func NewImplPersonalAPI( + logger log.Logger, + backend backend.EVMBackend, +) *PersonalAPI { + cfg := sdk.GetConfig() + basePath := cfg.GetFullBIP44Path() + + iterator, err := eth.NewHDPathIterator(basePath, true) + if err != nil { + panic(err) + } + + return &PersonalAPI{ + logger: logger.With("api", "personal"), + hdPathIter: iterator, + backend: backend, + } +} + +// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory. +// The name of the key will have the format "personal_", where is the total number of +// keys stored on the keyring. +// +// NOTE: The key will be both armored and encrypted using the same passphrase. +func (api *PersonalAPI) ImportRawKey(privkey, password string) (common.Address, error) { + api.logger.Debug("personal_importRawKey") + return api.backend.ImportRawKey(privkey, password) +} + +// ListAccounts will return a list of addresses for accounts this node manages. +func (api *PersonalAPI) ListAccounts() ([]common.Address, error) { + api.logger.Debug("personal_listAccounts") + return api.backend.ListAccounts() +} + +// LockAccount will lock the account associated with the given address when it's unlocked. +// It removes the key corresponding to the given address from the API's local keys. +func (api *PersonalAPI) LockAccount(address common.Address) bool { + api.logger.Debug("personal_lockAccount", "address", address.String()) + api.logger.Info("personal_lockAccount not supported") + // TODO: Not supported. See underlying issue https://github.com/99designs/keyring/issues/85 + return false +} + +// NewAccount will create a new account and returns the address for the new +// account. +func (api *PersonalAPI) NewAccount(password string) (common.Address, error) { + api.logger.Debug("personal_newAccount") + + name := "key_" + time.Now().UTC().Format(time.RFC3339) + + // create the mnemonic and save the account + hdPath := api.hdPathIter() + + info, err := api.backend.NewMnemonic(name, keyring.English, hdPath.String(), password, hd.EthSecp256k1) + if err != nil { + return common.Address{}, err + } + + pubKey, err := info.GetPubKey() + if err != nil { + return common.Address{}, err + } + addr := common.BytesToAddress(pubKey.Address().Bytes()) + api.logger.Info("Your new key was generated", "address", addr.String()) + + binPath := fmt.Sprintf("%s/.%s/%s", + os.Getenv("HOME"), appconst.BinaryName, name) + api.logger.Info("Please backup your key file!", "path", binPath) + api.logger.Info("Please remember your password!") + return addr, nil +} + +// UnlockAccount will unlock the account associated with the given address with +// the given password for duration seconds. It returns an indication if the +// account was unlocked. +func (api *PersonalAPI) UnlockAccount( + _ context.Context, addr common.Address, _ string, _ *uint64, +) (isUnlocked bool, err error) { + api.logger.Debug("personal_unlockAccount", "address", addr.String()) + // TODO: feat(eth-rpc): Implement a way to lock and unlock the keyring + // securely on the Ethereum "peronsal" RPC namespace. + return false, nil // Not yet supported. +} + +// SendTransaction will create a transaction from the given arguments and +// tries to sign it with the key associated with args.To. If the given password isn't +// able to decrypt the key it fails. +func (api *PersonalAPI) SendTransaction( + _ context.Context, args evm.JsonTxArgs, _ string, +) (common.Hash, error) { + api.logger.Debug("personal_sendTransaction", "address", args.To.String()) + return api.backend.SendTransaction(args) +} + +// Sign calculates an Ethereum ECDSA signature for: +// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)) +// +// Note, the produced signature conforms to the secp256k1 curve R, S and V values, +// where the V value will be 27 or 28 for legacy reasons. +// +// The key used to calculate the signature is decrypted with the given password. +// +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign +func (api *PersonalAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, _ string) (hexutil.Bytes, error) { + api.logger.Debug("personal_sign", "data", data, "address", addr.String()) + return api.backend.Sign(addr, data) +} + +// EcRecover returns the address for the account that was used to create the signature. +// Note, this function is compatible with eth_sign and personal_sign. As such it recovers +// the address of: +// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message}) +// addr = ecrecover(hash, signature) +// +// Note, the signature must conform to the secp256k1 curve R, S and V values, where +// the V value must be 27 or 28 for legacy reasons. +// +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove +func (api *PersonalAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) { + api.logger.Debug("personal_ecRecover", "data", data, "sig", sig) + + if len(sig) != crypto.SignatureLength { + return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength) + } + + if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 { + return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") + } + + sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1 + + pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig) + if err != nil { + return common.Address{}, err + } + + return crypto.PubkeyToAddress(*pubkey), nil +} + +// Unpair deletes a pairing between wallet and ethermint. +func (api *PersonalAPI) Unpair(_ context.Context, url, pin string) error { + api.logger.Debug("personal_unpair", "url", url, "pin", pin) + api.logger.Info("personal_unpair for smartcard wallet not supported") + // TODO: Smartcard wallet not supported yet, refer to: https://github.com/ethereum/go-ethereum/blob/master/accounts/scwallet/README.md + return fmt.Errorf("smartcard wallet not supported yet") +} + +// InitializeWallet initializes a new wallet at the provided URL, by generating and returning a new private key. +func (api *PersonalAPI) InitializeWallet(_ context.Context, url string) (string, error) { + api.logger.Debug("personal_initializeWallet", "url", url) + api.logger.Info("personal_initializeWallet for smartcard wallet not supported") + // TODO: Smartcard wallet not supported yet, refer to: https://github.com/ethereum/go-ethereum/blob/master/accounts/scwallet/README.md + return "", fmt.Errorf("smartcard wallet not supported yet") +} + +// RawWallet is a JSON representation of an accounts.Wallet interface, with its +// data contents extracted into plain fields. +type RawWallet struct { + URL string `json:"url"` + Status string `json:"status"` + Failure string `json:"failure,omitempty"` + Accounts []accounts.Account `json:"accounts,omitempty"` +} + +// ListWallets will return a list of wallets this node manages. +func (api *PersonalAPI) ListWallets() []RawWallet { + api.logger.Debug("personal_ListWallets") + api.logger.Info("currently wallet level that manages accounts is not supported") + return ([]RawWallet)(nil) +} diff --git a/eth/rpc/rpcapi/txpool_api.go b/eth/rpc/rpcapi/txpool_api.go new file mode 100644 index 000000000..ab2bc0055 --- /dev/null +++ b/eth/rpc/rpcapi/txpool_api.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "github.com/cometbft/cometbft/libs/log" + + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/NibiruChain/nibiru/eth/rpc" +) + +// TxPoolAPI offers and API for the transaction pool. It only operates on data +// that is non-confidential. +type TxPoolAPI struct { + logger log.Logger +} + +// NewImplTxPoolAPI creates a new tx pool service that gives information about the transaction pool. +func NewImplTxPoolAPI(logger log.Logger) *TxPoolAPI { + return &TxPoolAPI{ + logger: logger.With("module", "txpool"), + } +} + +// Content returns the transactions contained within the transaction pool +func (api *TxPoolAPI) Content() ( + map[string]map[string]map[string]*rpc.EthTxJsonRPC, error, +) { + api.logger.Debug("txpool_content") + content := map[string]map[string]map[string]*rpc.EthTxJsonRPC{ + "pending": make(map[string]map[string]*rpc.EthTxJsonRPC), + "queued": make(map[string]map[string]*rpc.EthTxJsonRPC), + } + return content, nil +} + +// Inspect returns the content of the transaction pool and flattens it into an +func (api *TxPoolAPI) Inspect() (map[string]map[string]map[string]string, error) { + api.logger.Debug("txpool_inspect") + content := map[string]map[string]map[string]string{ + "pending": make(map[string]map[string]string), + "queued": make(map[string]map[string]string), + } + return content, nil +} + +// Status returns the number of pending and queued transaction in the pool. +func (api *TxPoolAPI) Status() map[string]hexutil.Uint { + api.logger.Debug("txpool_status") + return map[string]hexutil.Uint{ + "pending": hexutil.Uint(0), + "queued": hexutil.Uint(0), + } +} diff --git a/eth/rpc/rpcapi/web3_api.go b/eth/rpc/rpcapi/web3_api.go new file mode 100644 index 000000000..1a2c521d6 --- /dev/null +++ b/eth/rpc/rpcapi/web3_api.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + appconst "github.com/NibiruChain/nibiru/app/appconst" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +// APIWeb3 is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec. +type APIWeb3 struct{} + +// NewImplWeb3API creates an instance of the Web3 API. +func NewImplWeb3API() *APIWeb3 { + return &APIWeb3{} +} + +// ClientVersion returns the client version in the Web3 user agent format. +func (a *APIWeb3) ClientVersion() string { + return appconst.Version() +} + +// Sha3 returns the keccak-256 hash of the passed-in input. +func (a *APIWeb3) Sha3(input string) hexutil.Bytes { + return crypto.Keccak256(hexutil.Bytes(input)) +} diff --git a/eth/rpc/rpcapi/websockets.go b/eth/rpc/rpcapi/websockets.go new file mode 100644 index 000000000..c168b9054 --- /dev/null +++ b/eth/rpc/rpcapi/websockets.go @@ -0,0 +1,704 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math/big" + "net" + "net/http" + "strconv" + "sync" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/params" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/cometbft/cometbft/libs/log" + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/NibiruChain/collections" + + "github.com/NibiruChain/nibiru/app/server/config" + "github.com/NibiruChain/nibiru/eth/rpc" + "github.com/NibiruChain/nibiru/eth/rpc/pubsub" + rpcfilters "github.com/NibiruChain/nibiru/eth/rpc/rpcapi/filtersapi" + "github.com/NibiruChain/nibiru/x/evm" +) + +type WebsocketsServer interface { + Start() +} + +type SubscriptionResponseJSON struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result"` + ID float64 `json:"id"` +} + +type SubscriptionNotification struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params *SubscriptionResult `json:"params"` +} + +type SubscriptionResult struct { + Subscription gethrpc.ID `json:"subscription"` + Result interface{} `json:"result"` +} + +type ErrorResponseJSON struct { + Jsonrpc string `json:"jsonrpc"` + Error *ErrorMessageJSON `json:"error"` + ID *big.Int `json:"id"` +} + +type ErrorMessageJSON struct { + Code *big.Int `json:"code"` + Message string `json:"message"` +} + +type websocketsServer struct { + rpcAddr string // listen address of rest-server + wsAddr string // listen address of ws server + certFile string + keyFile string + api *pubSubAPI + logger log.Logger +} + +func NewWebsocketsServer( + clientCtx client.Context, + logger log.Logger, + tmWSClient *rpcclient.WSClient, + cfg *config.Config, +) WebsocketsServer { + logger = logger.With("api", "websocket-server") + _, port, _ := net.SplitHostPort(cfg.JSONRPC.Address) // #nosec G703 + + return &websocketsServer{ + rpcAddr: "localhost:" + port, // FIXME: this shouldn't be hardcoded to localhost + wsAddr: cfg.JSONRPC.WsAddress, + certFile: cfg.TLS.CertificatePath, + keyFile: cfg.TLS.KeyPath, + api: newPubSubAPI(clientCtx, logger, tmWSClient), + logger: logger, + } +} + +func (s *websocketsServer) Start() { + ws := mux.NewRouter() + ws.Handle("/", s) + + go func() { + var err error + if s.certFile == "" || s.keyFile == "" { + //#nosec G114 -- http functions have no support for timeouts + err = http.ListenAndServe(s.wsAddr, ws) + } else { + //#nosec G114 -- http functions have no support for timeouts + err = http.ListenAndServeTLS(s.wsAddr, s.certFile, s.keyFile, ws) + } + + if err != nil { + if err == http.ErrServerClosed { + return + } + + s.logger.Error("failed to start HTTP server for WS", "error", err.Error()) + } + }() +} + +func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + s.logger.Debug("websocket upgrade failed", "error", err.Error()) + return + } + + s.readLoop(&wsConn{ + mux: new(sync.Mutex), + conn: conn, + }) +} + +func (s *websocketsServer) sendErrResponse(wsConn *wsConn, msg string) { + res := &ErrorResponseJSON{ + Jsonrpc: "2.0", + Error: &ErrorMessageJSON{ + Code: big.NewInt(-32600), + Message: msg, + }, + ID: nil, + } + + _ = wsConn.WriteJSON(res) // #nosec G703 +} + +type wsConn struct { + conn *websocket.Conn + mux *sync.Mutex +} + +func (w *wsConn) WriteJSON(v interface{}) error { + w.mux.Lock() + defer w.mux.Unlock() + + return w.conn.WriteJSON(v) +} + +func (w *wsConn) Close() error { + w.mux.Lock() + defer w.mux.Unlock() + + return w.conn.Close() +} + +func (w *wsConn) ReadMessage() (messageType int, p []byte, err error) { + // not protected by write mutex + + return w.conn.ReadMessage() +} + +func (s *websocketsServer) readLoop(wsConn *wsConn) { + // subscriptions of current connection + subscriptions := make(map[gethrpc.ID]pubsub.UnsubscribeFunc) + defer func() { + // cancel all subscriptions when connection closed + // #nosec G705 + for _, unsubFn := range subscriptions { + unsubFn() + } + }() + + for { + _, mb, err := wsConn.ReadMessage() + if err != nil { + _ = wsConn.Close() // #nosec G703 + s.logger.Error("read message error, breaking read loop", "error", err.Error()) + return + } + + if isBatch(mb) { + if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + continue + } + + var msg map[string]interface{} + if err = json.Unmarshal(mb, &msg); err != nil { + s.sendErrResponse(wsConn, err.Error()) + continue + } + + // check if method == eth_subscribe or eth_unsubscribe + method, ok := msg["method"].(string) + if !ok { + // otherwise, call the usual rpc server to respond + if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + + continue + } + + var connID float64 + switch id := msg["id"].(type) { + case string: + connID, err = strconv.ParseFloat(id, 64) + case float64: + connID = id + default: + err = fmt.Errorf("unknown type") + } + if err != nil { + s.sendErrResponse( + wsConn, + fmt.Errorf("invalid type for connection ID: %T", msg["id"]).Error(), + ) + continue + } + + switch method { + case "eth_subscribe": + params, ok := s.getParamsAndCheckValid(msg, wsConn) + if !ok { + continue + } + + subID := gethrpc.NewID() + unsubFn, err := s.api.subscribe(wsConn, subID, params) + if err != nil { + s.sendErrResponse(wsConn, err.Error()) + continue + } + subscriptions[subID] = unsubFn + + res := &SubscriptionResponseJSON{ + Jsonrpc: "2.0", + ID: connID, + Result: subID, + } + + if err := wsConn.WriteJSON(res); err != nil { + break + } + case "eth_unsubscribe": + params, ok := s.getParamsAndCheckValid(msg, wsConn) + if !ok { + continue + } + + id, ok := params[0].(string) + if !ok { + s.sendErrResponse(wsConn, "invalid parameters") + continue + } + + subID := gethrpc.ID(id) + unsubFn, ok := subscriptions[subID] + if ok { + delete(subscriptions, subID) + unsubFn() + } + + res := &SubscriptionResponseJSON{ + Jsonrpc: "2.0", + ID: connID, + Result: ok, + } + + if err := wsConn.WriteJSON(res); err != nil { + break + } + default: + // otherwise, call the usual rpc server to respond + if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + } + } +} + +// tcpGetAndSendResponse sends error response to client if params is invalid +func (s *websocketsServer) getParamsAndCheckValid(msg map[string]interface{}, wsConn *wsConn) ([]interface{}, bool) { + params, ok := msg["params"].([]interface{}) + if !ok { + s.sendErrResponse(wsConn, "invalid parameters") + return nil, false + } + + if len(params) == 0 { + s.sendErrResponse(wsConn, "empty parameters") + return nil, false + } + + return params, true +} + +// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response +// to the client over websockets +func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) error { + req, err := http.NewRequestWithContext(context.Background(), "POST", "http://"+s.rpcAddr, bytes.NewBuffer(mb)) + if err != nil { + return errors.Wrap(err, "Could not build request") + } + + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errors.Wrap(err, "Could not perform request") + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "could not read body from response") + } + + var wsSend interface{} + err = json.Unmarshal(body, &wsSend) + if err != nil { + return errors.Wrap(err, "failed to unmarshal rest-server response") + } + + return wsConn.WriteJSON(wsSend) +} + +// pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec +type pubSubAPI struct { + events *rpcfilters.EventSystem + logger log.Logger + clientCtx client.Context +} + +// newPubSubAPI creates an instance of the ethereum PubSub API. +func newPubSubAPI(clientCtx client.Context, logger log.Logger, tmWSClient *rpcclient.WSClient) *pubSubAPI { + logger = logger.With("module", "websocket-client") + return &pubSubAPI{ + events: rpcfilters.NewEventSystem(logger, tmWSClient), + logger: logger, + clientCtx: clientCtx, + } +} + +func (api *pubSubAPI) subscribe(wsConn *wsConn, subID gethrpc.ID, params []interface{}) (pubsub.UnsubscribeFunc, error) { + method, ok := params[0].(string) + if !ok { + return nil, errors.New("invalid parameters") + } + + switch method { + case "newHeads": + // TODO: handle extra params + return api.subscribeNewHeads(wsConn, subID) + case "logs": + if len(params) > 1 { + return api.subscribeLogs(wsConn, subID, params[1]) + } + return api.subscribeLogs(wsConn, subID, nil) + case "newPendingTransactions": + return api.subscribePendingTransactions(wsConn, subID) + case "syncing": + return api.subscribeSyncing(wsConn, subID) + default: + return nil, errors.Errorf("unsupported method %s", method) + } +} + +func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID gethrpc.ID) (pubsub.UnsubscribeFunc, error) { + sub, unsubFn, err := api.events.SubscribeNewHeads() + if err != nil { + return nil, errors.Wrap(err, "error creating block filter") + } + + // TODO: use events + baseFee := big.NewInt(params.InitialBaseFee) + + go func() { + headersCh := sub.Event() + errCh := sub.Err() + for { + select { + case event, ok := <-headersCh: + if !ok { + return + } + + data, ok := event.Data.(tmtypes.EventDataNewBlockHeader) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data)) + continue + } + + header := rpc.EthHeaderFromTendermint(data.Header, gethcore.Bloom{}, baseFee) + + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: subID, + Result: header, + }, + } + + err = wsConn.WriteJSON(res) + if err != nil { + api.logger.Error("error writing header, will drop peer", "error", err.Error()) + + try(func() { + if err != websocket.ErrCloseSent { + _ = wsConn.Close() // #nosec G703 + } + }, api.logger, "closing websocket peer sub") + } + case err, ok := <-errCh: + if !ok { + return + } + api.logger.Debug("dropping NewHeads WebSocket subscription", "subscription-id", subID, "error", err.Error()) + } + } + }() + + return unsubFn, nil +} + +func try(fn func(), l log.Logger, desc string) { + defer func() { + if x := recover(); x != nil { + if err, ok := x.(error); ok { + // debug.PrintStack() + l.Debug("panic during "+desc, "error", err.Error()) + return + } + + l.Debug(fmt.Sprintf("panic during %s: %+v", desc, x)) + return + } + }() + + fn() +} + +func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra interface{}) (pubsub.UnsubscribeFunc, error) { + crit := filters.FilterCriteria{} + + if extra != nil { + params, ok := extra.(map[string]interface{}) + if !ok { + err := errors.New("invalid criteria") + api.logger.Debug("invalid criteria", "type", fmt.Sprintf("%T", extra)) + return nil, err + } + + if params["address"] != nil { + address, isString := params["address"].(string) + addresses, isSlice := params["address"].([]interface{}) + if !isString && !isSlice { + err := errors.New("invalid addresses; must be address or array of addresses") + api.logger.Debug("invalid addresses", "type", fmt.Sprintf("%T", params["address"])) + return nil, err + } + + if ok { + crit.Addresses = []common.Address{common.HexToAddress(address)} + } + + if isSlice { + crit.Addresses = []common.Address{} + for _, addr := range addresses { + address, ok := addr.(string) + if !ok { + err := errors.New("invalid address") + api.logger.Debug("invalid address", "type", fmt.Sprintf("%T", addr)) + return nil, err + } + + crit.Addresses = append(crit.Addresses, common.HexToAddress(address)) + } + } + } + + if params["topics"] != nil { + topics, ok := params["topics"].([]interface{}) + if !ok { + err := errors.Errorf("invalid topics: %s", topics) + api.logger.Error("invalid topics", "type", fmt.Sprintf("%T", topics)) + return nil, err + } + + crit.Topics = make([][]common.Hash, len(topics)) + + addCritTopic := func(topicIdx int, topic interface{}) error { + tstr, ok := topic.(string) + if !ok { + err := errors.Errorf("invalid topic: %s", topic) + api.logger.Error("invalid topic", "type", fmt.Sprintf("%T", topic)) + return err + } + + crit.Topics[topicIdx] = []common.Hash{common.HexToHash(tstr)} + return nil + } + + for topicIdx, subtopics := range topics { + if subtopics == nil { + continue + } + + // in case we don't have list, but a single topic value + if topic, ok := subtopics.(string); ok { + if err := addCritTopic(topicIdx, topic); err != nil { + return nil, err + } + + continue + } + + // in case we actually have a list of subtopics + subtopicsList, ok := subtopics.([]interface{}) + if !ok { + err := errors.New("invalid subtopics") + api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopics)) + return nil, err + } + + subtopicsCollect := make([]common.Hash, len(subtopicsList)) + for idx, subtopic := range subtopicsList { + tstr, ok := subtopic.(string) + if !ok { + err := errors.Errorf("invalid subtopic: %s", subtopic) + api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopic)) + return nil, err + } + + subtopicsCollect[idx] = common.HexToHash(tstr) + } + + crit.Topics[topicIdx] = subtopicsCollect + } + } + } + + sub, unsubFn, err := api.events.SubscribeLogs(crit) + if err != nil { + api.logger.Error("failed to subscribe logs", "error", err.Error()) + return nil, err + } + + go func() { + ch := sub.Event() + errCh := sub.Err() + for { + select { + case event, ok := <-ch: + if !ok { + return + } + + dataTx, ok := event.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data)) + continue + } + + txResponse, err := evm.DecodeTxResponse(dataTx.TxResult.Result.Data) + if err != nil { + api.logger.Error("failed to decode tx response", "error", err.Error()) + return + } + + logs := rpcfilters.FilterLogs(evm.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + if len(logs) == 0 { + continue + } + + for _, ethLog := range logs { + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: subID, + Result: ethLog, + }, + } + + err = wsConn.WriteJSON(res) + if err != nil { + try(func() { + if err != websocket.ErrCloseSent { + _ = wsConn.Close() // #nosec G703 + } + }, api.logger, "closing websocket peer sub") + } + } + case err, ok := <-errCh: + if !ok { + return + } + api.logger.Debug("dropping Logs WebSocket subscription", "subscription-id", subID, "error", err.Error()) + } + } + }() + + return unsubFn, nil +} + +func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn, subID gethrpc.ID) (pubsub.UnsubscribeFunc, error) { + sub, unsubFn, err := api.events.SubscribePendingTxs() + if err != nil { + return nil, errors.Wrap(err, "error creating block filter: %s") + } + + go func() { + txsCh := sub.Event() + errCh := sub.Err() + for { + select { + case ev := <-txsCh: + data, ok := ev.Data.(tmtypes.EventDataTx) + if !ok { + api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) + continue + } + + fmt.Printf("data.Tx: %s\n", collections.HumanizeBytes(data.Tx)) + ethTxs, err := rpc.RawTxToEthTx(api.clientCtx, data.Tx) + if err != nil { + // not ethereum tx + continue + } + + for _, ethTx := range ethTxs { + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: subID, + Result: ethTx.Hash, + }, + } + + err = wsConn.WriteJSON(res) + if err != nil { + api.logger.Debug("error writing header, will drop peer", "error", err.Error()) + + try(func() { + if err != websocket.ErrCloseSent { + _ = wsConn.Close() // #nosec G703 + } + }, api.logger, "closing websocket peer sub") + } + } + case err, ok := <-errCh: + if !ok { + return + } + api.logger.Debug("dropping PendingTransactions WebSocket subscription", subID, "error", err.Error()) + } + } + }() + + return unsubFn, nil +} + +func (api *pubSubAPI) subscribeSyncing(_ *wsConn, _ gethrpc.ID) (pubsub.UnsubscribeFunc, error) { + return nil, errors.New("syncing subscription is not implemented") +} + +// copy from github.com/ethereum/go-ethereum/rpc/json.go +// isBatch returns true when the first non-whitespace characters is '[' +func isBatch(raw []byte) bool { + for _, c := range raw { + // skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt) + if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d { + continue + } + return c == '[' + } + return false +} diff --git a/eth/rpc/types.go b/eth/rpc/types.go index 5dbe28189..dfcec77ab 100644 --- a/eth/rpc/types.go +++ b/eth/rpc/types.go @@ -32,8 +32,8 @@ type StorageResult struct { Proof []string `json:"proof"` } -// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction -type RPCTransaction struct { +// EthTxJsonRPC represents a transaction that will serialize to the RPC representation of a transaction +type EthTxJsonRPC struct { BlockHash *common.Hash `json:"blockHash"` BlockNumber *hexutil.Big `json:"blockNumber"` From common.Address `json:"from"` diff --git a/go.mod b/go.mod index 5c3bd31b4..1e2344fdc 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,8 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 + github.com/gorilla/websocket v1.5.0 golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb golang.org/x/text v0.14.0 ) @@ -97,7 +99,6 @@ require ( github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect github.com/creachadair/taskgroup v0.4.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect @@ -107,6 +108,7 @@ require ( github.com/docker/distribution v2.8.2+incompatible // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect + github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/getsentry/sentry-go v0.23.0 // indirect @@ -132,7 +134,6 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect @@ -179,6 +180,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.7.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rjeczalik/notify v0.9.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/cors v1.8.3 // indirect github.com/rs/zerolog v1.32.0 // indirect diff --git a/go.sum b/go.sum index f4e501bad..6834666e6 100644 --- a/go.sum +++ b/go.sum @@ -348,6 +348,7 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -441,9 +442,11 @@ github.com/cosmos/ledger-cosmos-go v0.12.4 h1:drvWt+GJP7Aiw550yeb3ON/zsrgW0jgh5s github.com/cosmos/ledger-cosmos-go v0.12.4/go.mod h1:fjfVWRf++Xkygt9wzCsjEBdjcf7wiiY35fv3ctT+k4M= github.com/cosmos/rosetta-sdk-go v0.10.0 h1:E5RhTruuoA7KTIXUcMicL76cffyeoyvNybzUGSKFTcM= github.com/cosmos/rosetta-sdk-go v0.10.0/go.mod h1:SImAZkb96YbwvoRkzSMQB6noNJXFgWl/ENIznEoYQI4= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/taskgroup v0.4.2 h1:jsBLdAJE42asreGss2xZGZ8fJra7WtwnHWeJFxv2Li8= github.com/creachadair/taskgroup v0.4.2/go.mod h1:qiXUOSrbwAY3u0JPGTzObbE3yf9hcXHDKBZ2ZjpCbgM= @@ -519,6 +522,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -605,6 +609,7 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= @@ -766,6 +771,7 @@ github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIv github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -814,6 +820,7 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -835,6 +842,7 @@ github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19y github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -969,6 +977,7 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -1109,6 +1118,7 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1122,8 +1132,10 @@ github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -1237,8 +1249,10 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= @@ -1249,6 +1263,7 @@ github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPyS github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/x/common/testutil/sample.go b/x/common/testutil/sample.go index 64a18e3d4..fc23896f8 100644 --- a/x/common/testutil/sample.go +++ b/x/common/testutil/sample.go @@ -6,11 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/store" "github.com/cosmos/cosmos-sdk/store/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - - "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -39,20 +34,6 @@ func PrivKey() (*secp256k1.PrivKey, sdk.AccAddress) { return privKey, sdk.AccAddress(addr) } -// PrivKeyEth returns an Ethereum private key and corresponding Eth address. -func PrivKeyEth() (common.Address, *ethsecp256k1.PrivKey) { - privkey, _ := ethsecp256k1.GenerateKey() - privKeyE, _ := privkey.ToECDSA() - ethAddr := crypto.PubkeyToAddress(privKeyE.PublicKey) - return ethAddr, privkey -} - -// NewEthAddr generates an Ethereum address. -func NewEthAddr() common.Address { - addr, _ := PrivKeyEth() - return addr -} - // PrivKeyAddressPairs generates (deterministically) a total of n private keys // and addresses. func PrivKeyAddressPairs(n int) (keys []cryptotypes.PrivKey, addrs []sdk.AccAddress) { diff --git a/x/common/testutil/testapp/testapp.go b/x/common/testutil/testapp/testapp.go index 7f3b75dad..4f27bc1e8 100644 --- a/x/common/testutil/testapp/testapp.go +++ b/x/common/testutil/testapp/testapp.go @@ -14,6 +14,7 @@ import ( bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/common/asset" "github.com/NibiruChain/nibiru/x/common/denoms" "github.com/NibiruChain/nibiru/x/common/testutil" @@ -170,7 +171,7 @@ func FundModuleAccount( // addresses rather than cosmos ones (for Gaia). func EnsureNibiruPrefix() { csdkConfig := sdk.GetConfig() - nibiruPrefix := app.AccountAddressPrefix + nibiruPrefix := appconst.AccountAddressPrefix if csdkConfig.GetBech32AccountAddrPrefix() != nibiruPrefix { app.SetPrefixes(nibiruPrefix) } diff --git a/x/evm/evmtest/eth.go b/x/evm/evmtest/eth.go index ba6358ee6..54f312dbf 100644 --- a/x/evm/evmtest/eth.go +++ b/x/evm/evmtest/eth.go @@ -8,24 +8,40 @@ import ( cmt "github.com/cometbft/cometbft/types" "github.com/stretchr/testify/assert" + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" "github.com/NibiruChain/nibiru/eth/encoding" "github.com/cosmos/cosmos-sdk/client" + gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/eth" - "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/evm" ) +// NewEthAddr generates an Ethereum address. +func NewEthAddr() gethcommon.Address { + addr, _ := PrivKeyEth() + return addr +} + +// PrivKeyEth returns an Ethereum private key and corresponding Eth address. +func PrivKeyEth() (gethcommon.Address, *ethsecp256k1.PrivKey) { + privkey, _ := ethsecp256k1.GenerateKey() + privKeyE, _ := privkey.ToECDSA() + ethAddr := crypto.PubkeyToAddress(privKeyE.PublicKey) + return ethAddr, privkey +} + // NewEthTxMsg: Helper that returns a valid instance of [*evm.MsgEthereumTx]. func NewEthTxMsg() *evm.MsgEthereumTx { return NewEthTxMsgs(1)[0] } func NewEthTxMsgs(count uint64) (ethTxMsgs []*evm.MsgEthereumTx) { - ethAddr := testutil.NewEthAddr() + ethAddr := NewEthAddr() startIdx := uint64(1) for nonce := startIdx; nonce-startIdx < count; nonce++ { ethTxMsgs = append(ethTxMsgs, evm.NewTx(&evm.EvmTxArgs{ diff --git a/x/common/testutil/signer.go b/x/evm/evmtest/signer.go similarity index 98% rename from x/common/testutil/signer.go rename to x/evm/evmtest/signer.go index d9f1cf387..25fb451fc 100644 --- a/x/common/testutil/signer.go +++ b/x/evm/evmtest/signer.go @@ -1,4 +1,4 @@ -package testutil +package evmtest import ( "fmt" diff --git a/x/evm/json_tx_args.go b/x/evm/json_tx_args.go new file mode 100644 index 000000000..14fde2da3 --- /dev/null +++ b/x/evm/json_tx_args.go @@ -0,0 +1,249 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evm + +import ( + "errors" + "fmt" + "math/big" + + sdkmath "cosmossdk.io/math" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + geth "github.com/ethereum/go-ethereum/core/types" +) + +// JsonTxArgs represents the arguments to construct a new transaction +// or a message call using JSON-RPC. +// Duplicate struct definition since geth struct is in internal package +// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.10.4/internal/ethapi/transaction_args.go#L36 +type JsonTxArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` + Value *hexutil.Big `json:"value"` + Nonce *hexutil.Uint64 `json:"nonce"` + + // We accept "data" and "input" for backwards-compatibility reasons. + // "input" is the newer name and should be preferred by clients. + // Issue detail: https://github.com/ethereum/go-ethereum/issues/15628 + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` + + // Introduced by AccessListTxType transaction. + AccessList *geth.AccessList `json:"accessList,omitempty"` + ChainID *hexutil.Big `json:"chainId,omitempty"` +} + +// String return the struct in a string format +func (args *JsonTxArgs) String() string { + // Todo: There is currently a bug with hexutil.Big when the value its nil, printing would trigger an exception + return fmt.Sprintf("TransactionArgs{From:%v, To:%v, Gas:%v,"+ + " Nonce:%v, Data:%v, Input:%v, AccessList:%v}", + args.From, + args.To, + args.Gas, + args.Nonce, + args.Data, + args.Input, + args.AccessList) +} + +// ToTransaction converts the arguments to an ethereum transaction. +// This assumes that setTxDefaults has been called. +func (args *JsonTxArgs) ToTransaction() *MsgEthereumTx { + var ( + chainID, value, gasPrice, maxFeePerGas, maxPriorityFeePerGas sdkmath.Int + gas, nonce uint64 + from, to string + ) + + // Set sender address or use zero address if none specified. + if args.ChainID != nil { + chainID = sdkmath.NewIntFromBigInt(args.ChainID.ToInt()) + } + + if args.Nonce != nil { + nonce = uint64(*args.Nonce) + } + + if args.Gas != nil { + gas = uint64(*args.Gas) + } + + if args.GasPrice != nil { + gasPrice = sdkmath.NewIntFromBigInt(args.GasPrice.ToInt()) + } + + if args.MaxFeePerGas != nil { + maxFeePerGas = sdkmath.NewIntFromBigInt(args.MaxFeePerGas.ToInt()) + } + + if args.MaxPriorityFeePerGas != nil { + maxPriorityFeePerGas = sdkmath.NewIntFromBigInt(args.MaxPriorityFeePerGas.ToInt()) + } + + if args.Value != nil { + value = sdkmath.NewIntFromBigInt(args.Value.ToInt()) + } + + if args.To != nil { + to = args.To.Hex() + } + + var data TxData + switch { + case args.MaxFeePerGas != nil: + al := AccessList{} + if args.AccessList != nil { + al = NewAccessList(args.AccessList) + } + + data = &DynamicFeeTx{ + To: to, + ChainID: &chainID, + Nonce: nonce, + GasLimit: gas, + GasFeeCap: &maxFeePerGas, + GasTipCap: &maxPriorityFeePerGas, + Amount: &value, + Data: args.GetData(), + Accesses: al, + } + case args.AccessList != nil: + data = &AccessListTx{ + To: to, + ChainID: &chainID, + Nonce: nonce, + GasLimit: gas, + GasPrice: &gasPrice, + Amount: &value, + Data: args.GetData(), + Accesses: NewAccessList(args.AccessList), + } + default: + data = &LegacyTx{ + To: to, + Nonce: nonce, + GasLimit: gas, + GasPrice: &gasPrice, + Amount: &value, + Data: args.GetData(), + } + } + + anyData, err := PackTxData(data) + if err != nil { + return nil + } + + if args.From != nil { + from = args.From.Hex() + } + + msg := MsgEthereumTx{ + Data: anyData, + From: from, + } + msg.Hash = msg.AsTransaction().Hash().Hex() + return &msg +} + +// ToMessage converts the arguments to the Message type used by the core evm. +// This assumes that setTxDefaults has been called. +func (args *JsonTxArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (geth.Message, error) { + // Reject invalid combinations of pre- and post-1559 fee styles + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return geth.Message{}, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + + // Set sender address or use zero address if none specified. + addr := args.GetFrom() + + // Set default gas & gas price if none were set + gas := globalGasCap + if gas == 0 { + gas = uint64(math.MaxUint64 / 2) + } + if args.Gas != nil { + gas = uint64(*args.Gas) + } + if globalGasCap != 0 && globalGasCap < gas { + gas = globalGasCap + } + var ( + gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int + ) + if baseFee == nil { + // If there's no basefee, then it must be a non-1559 execution + gasPrice = new(big.Int) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + gasFeeCap, gasTipCap = gasPrice, gasPrice + } else { + // A basefee is provided, necessitating 1559-type execution + if args.GasPrice != nil { + // User specified the legacy gas field, convert to 1559 gas typing + gasPrice = args.GasPrice.ToInt() + gasFeeCap, gasTipCap = gasPrice, gasPrice + } else { + // User specified 1559 gas feilds (or none), use those + gasFeeCap = new(big.Int) + if args.MaxFeePerGas != nil { + gasFeeCap = args.MaxFeePerGas.ToInt() + } + gasTipCap = new(big.Int) + if args.MaxPriorityFeePerGas != nil { + gasTipCap = args.MaxPriorityFeePerGas.ToInt() + } + // Backfill the legacy gasPrice for EVM execution, unless we're all zeroes + gasPrice = new(big.Int) + if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 { + gasPrice = math.BigMin(new(big.Int).Add(gasTipCap, baseFee), gasFeeCap) + } + } + } + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + data := args.GetData() + var accessList geth.AccessList + if args.AccessList != nil { + accessList = *args.AccessList + } + + nonce := uint64(0) + if args.Nonce != nil { + nonce = uint64(*args.Nonce) + } + + msg := geth.NewMessage(addr, args.To, nonce, value, gas, gasPrice, gasFeeCap, gasTipCap, data, accessList, true) + return msg, nil +} + +// GetFrom retrieves the transaction sender address. +func (args *JsonTxArgs) GetFrom() common.Address { + if args.From == nil { + return common.Address{} + } + return *args.From +} + +// GetData retrieves the transaction calldata. Input field is preferred. +func (args *JsonTxArgs) GetData() []byte { + if args.Input != nil { + return *args.Input + } + if args.Data != nil { + return *args.Data + } + return nil +} diff --git a/x/evm/json_tx_args_test.go b/x/evm/json_tx_args_test.go new file mode 100644 index 000000000..31d73b6f5 --- /dev/null +++ b/x/evm/json_tx_args_test.go @@ -0,0 +1,290 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evm_test + +import ( + "fmt" + "math/big" + + ethcommon "github.com/ethereum/go-ethereum/common" + ethcoretypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +func (suite *TxDataTestSuite) TestTxArgsString() { + testCases := []struct { + name string + txArgs evm.JsonTxArgs + expectedString string + }{ + { + "empty tx args", + evm.JsonTxArgs{}, + "TransactionArgs{From:, To:, Gas:, Nonce:, Data:, Input:, AccessList:}", + }, + { + "tx args with fields", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + Nonce: &suite.hexUint64, + Input: &suite.hexInputBytes, + Data: &suite.hexDataBytes, + AccessList: ðcoretypes.AccessList{}, + }, + fmt.Sprintf("TransactionArgs{From:%v, To:%v, Gas:%v, Nonce:%v, Data:%v, Input:%v, AccessList:%v}", + &suite.addr, + &suite.addr, + &suite.hexUint64, + &suite.hexUint64, + &suite.hexDataBytes, + &suite.hexInputBytes, + ðcoretypes.AccessList{}), + }, + } + for _, tc := range testCases { + outputString := tc.txArgs.String() + suite.Require().Equal(outputString, tc.expectedString) + } +} + +func (suite *TxDataTestSuite) TestConvertTxArgsEthTx() { + testCases := []struct { + name string + txArgs evm.JsonTxArgs + }{ + { + "empty tx args", + evm.JsonTxArgs{}, + }, + { + "no nil args", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: &suite.hexBigInt, + MaxPriorityFeePerGas: &suite.hexBigInt, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + }, + { + "max fee per gas nil, but access list not nil", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: nil, + MaxPriorityFeePerGas: &suite.hexBigInt, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + }, + } + for _, tc := range testCases { + res := tc.txArgs.ToTransaction() + suite.Require().NotNil(res) + } +} + +func (suite *TxDataTestSuite) TestToMessageEVM() { + testCases := []struct { + name string + txArgs evm.JsonTxArgs + globalGasCap uint64 + baseFee *big.Int + expError bool + }{ + { + "empty tx args", + evm.JsonTxArgs{}, + uint64(0), + nil, + false, + }, + { + "specify gasPrice and (maxFeePerGas or maxPriorityFeePerGas)", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: &suite.hexBigInt, + MaxPriorityFeePerGas: &suite.hexBigInt, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + uint64(0), + nil, + true, + }, + { + "non-1559 execution, zero gas cap", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: nil, + MaxPriorityFeePerGas: nil, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + uint64(0), + nil, + false, + }, + { + "non-1559 execution, nonzero gas cap", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: nil, + MaxPriorityFeePerGas: nil, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + uint64(1), + nil, + false, + }, + { + "1559-type execution, nil gas price", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: nil, + MaxFeePerGas: &suite.hexBigInt, + MaxPriorityFeePerGas: &suite.hexBigInt, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + uint64(1), + suite.bigInt, + false, + }, + { + "1559-type execution, non-nil gas price", + evm.JsonTxArgs{ + From: &suite.addr, + To: &suite.addr, + Gas: &suite.hexUint64, + GasPrice: &suite.hexBigInt, + MaxFeePerGas: nil, + MaxPriorityFeePerGas: nil, + Value: &suite.hexBigInt, + Nonce: &suite.hexUint64, + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + AccessList: ðcoretypes.AccessList{{Address: suite.addr, StorageKeys: []ethcommon.Hash{{0}}}}, + ChainID: &suite.hexBigInt, + }, + uint64(1), + suite.bigInt, + false, + }, + } + for _, tc := range testCases { + res, err := tc.txArgs.ToMessage(tc.globalGasCap, tc.baseFee) + + if tc.expError { + suite.Require().NotNil(err) + } else { + suite.Require().Nil(err) + suite.Require().NotNil(res) + } + } +} + +func (suite *TxDataTestSuite) TestGetFrom() { + testCases := []struct { + name string + txArgs evm.JsonTxArgs + expAddress ethcommon.Address + }{ + { + "empty from field", + evm.JsonTxArgs{}, + ethcommon.Address{}, + }, + { + "non-empty from field", + evm.JsonTxArgs{ + From: &suite.addr, + }, + suite.addr, + }, + } + for _, tc := range testCases { + retrievedAddress := tc.txArgs.GetFrom() + suite.Require().Equal(retrievedAddress, tc.expAddress) + } +} + +func (suite *TxDataTestSuite) TestGetData() { + testCases := []struct { + name string + txArgs evm.JsonTxArgs + expectedOutput []byte + }{ + { + "empty input and data fields", + evm.JsonTxArgs{ + Data: nil, + Input: nil, + }, + nil, + }, + { + "empty input field, non-empty data field", + evm.JsonTxArgs{ + Data: &suite.hexDataBytes, + Input: nil, + }, + []byte("data"), + }, + { + "non-empty input and data fields", + evm.JsonTxArgs{ + Data: &suite.hexDataBytes, + Input: &suite.hexInputBytes, + }, + []byte("input"), + }, + } + for _, tc := range testCases { + retrievedData := tc.txArgs.GetData() + suite.Require().Equal(retrievedData, tc.expectedOutput) + } +} diff --git a/x/evm/logs.go b/x/evm/logs.go new file mode 100644 index 000000000..b96de5d8a --- /dev/null +++ b/x/evm/logs.go @@ -0,0 +1,128 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evm + +import ( + "errors" + "fmt" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/eth" +) + +// NewTransactionLogs creates a new NewTransactionLogs instance. +func NewTransactionLogs(hash gethcommon.Hash, logs []*Log) TransactionLogs { + return TransactionLogs{ + Hash: hash.String(), + Logs: logs, + } +} + +// NewTransactionLogsFromEth creates a new NewTransactionLogs instance using []*ethtypes.Log. +func NewTransactionLogsFromEth(hash gethcommon.Hash, ethlogs []*gethcore.Log) TransactionLogs { + return TransactionLogs{ + Hash: hash.String(), + Logs: NewLogsFromEth(ethlogs), + } +} + +// Validate performs a basic validation of a GenesisAccount fields. +func (tx TransactionLogs) Validate() error { + if eth.IsEmptyHash(tx.Hash) { + return fmt.Errorf("hash cannot be the empty %s", tx.Hash) + } + + for i, log := range tx.Logs { + if log == nil { + return fmt.Errorf("log %d cannot be nil", i) + } + if err := log.Validate(); err != nil { + return fmt.Errorf("invalid log %d: %w", i, err) + } + if log.TxHash != tx.Hash { + return fmt.Errorf("log tx hash mismatch (%s ≠ %s)", log.TxHash, tx.Hash) + } + } + return nil +} + +// EthLogs returns the Ethereum type Logs from the Transaction Logs. +func (tx TransactionLogs) EthLogs() []*gethcore.Log { + return LogsToEthereum(tx.Logs) +} + +// Validate performs a basic validation of an ethereum Log fields. +func (log *Log) Validate() error { + if err := eth.ValidateAddress(log.Address); err != nil { + return fmt.Errorf("invalid log address %w", err) + } + if eth.IsEmptyHash(log.BlockHash) { + return fmt.Errorf("block hash cannot be the empty %s", log.BlockHash) + } + if log.BlockNumber == 0 { + return errors.New("block number cannot be zero") + } + if eth.IsEmptyHash(log.TxHash) { + return fmt.Errorf("tx hash cannot be the empty %s", log.TxHash) + } + return nil +} + +// ToEthereum returns the Ethereum type Log from a Ethermint proto compatible Log. +func (log *Log) ToEthereum() *gethcore.Log { + topics := make([]gethcommon.Hash, len(log.Topics)) + for i, topic := range log.Topics { + topics[i] = gethcommon.HexToHash(topic) + } + + return &gethcore.Log{ + Address: gethcommon.HexToAddress(log.Address), + Topics: topics, + Data: log.Data, + BlockNumber: log.BlockNumber, + TxHash: gethcommon.HexToHash(log.TxHash), + TxIndex: uint(log.TxIndex), + Index: uint(log.Index), + BlockHash: gethcommon.HexToHash(log.BlockHash), + Removed: log.Removed, + } +} + +func NewLogsFromEth(ethlogs []*gethcore.Log) []*Log { + var logs []*Log //nolint: prealloc + for _, ethlog := range ethlogs { + logs = append(logs, NewLogFromEth(ethlog)) + } + + return logs +} + +// LogsToEthereum casts the Ethermint Logs to a slice of Ethereum Logs. +func LogsToEthereum(logs []*Log) []*gethcore.Log { + var ethLogs []*gethcore.Log //nolint: prealloc + for i := range logs { + ethLogs = append(ethLogs, logs[i].ToEthereum()) + } + return ethLogs +} + +// NewLogFromEth creates a new Log instance from a Ethereum type Log. +func NewLogFromEth(log *gethcore.Log) *Log { + topics := make([]string, len(log.Topics)) + for i, topic := range log.Topics { + topics[i] = topic.String() + } + + return &Log{ + Address: log.Address.String(), + Topics: topics, + Data: log.Data, + BlockNumber: log.BlockNumber, + TxHash: log.TxHash.String(), + TxIndex: uint64(log.TxIndex), + Index: uint64(log.Index), + BlockHash: log.BlockHash.String(), + Removed: log.Removed, + } +} diff --git a/x/evm/logs_test.go b/x/evm/logs_test.go new file mode 100644 index 000000000..123ddcf17 --- /dev/null +++ b/x/evm/logs_test.go @@ -0,0 +1,201 @@ +package evm_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/x/evm" + evmtest "github.com/NibiruChain/nibiru/x/evm/evmtest" + + "github.com/ethereum/go-ethereum/common" +) + +func TestTransactionLogsValidate(t *testing.T) { + addr := evmtest.NewEthAddr().String() + + testCases := []struct { + name string + txLogs evm.TransactionLogs + expPass bool + }{ + { + "valid log", + evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{ + { + Address: addr, + Topics: []string{common.BytesToHash([]byte("topic")).String()}, + Data: []byte("data"), + BlockNumber: 1, + TxHash: common.BytesToHash([]byte("tx_hash")).String(), + TxIndex: 1, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + Index: 1, + Removed: false, + }, + }, + }, + true, + }, + { + "empty hash", + evm.TransactionLogs{ + Hash: common.Hash{}.String(), + }, + false, + }, + { + "nil log", + evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{nil}, + }, + false, + }, + { + "invalid log", + evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{{}}, + }, + false, + }, + { + "hash mismatch log", + evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{ + { + Address: addr, + Topics: []string{common.BytesToHash([]byte("topic")).String()}, + Data: []byte("data"), + BlockNumber: 1, + TxHash: common.BytesToHash([]byte("other_hash")).String(), + TxIndex: 1, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + Index: 1, + Removed: false, + }, + }, + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + err := tc.txLogs.Validate() + if tc.expPass { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestValidateLog(t *testing.T) { + addr := evmtest.NewEthAddr().String() + + testCases := []struct { + name string + log *evm.Log + expPass bool + }{ + { + "valid log", + &evm.Log{ + Address: addr, + Topics: []string{common.BytesToHash([]byte("topic")).String()}, + Data: []byte("data"), + BlockNumber: 1, + TxHash: common.BytesToHash([]byte("tx_hash")).String(), + TxIndex: 1, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + Index: 1, + Removed: false, + }, + true, + }, + { + "empty log", &evm.Log{}, false, + }, + { + "zero address", + &evm.Log{ + Address: common.Address{}.String(), + }, + false, + }, + { + "empty block hash", + &evm.Log{ + Address: addr, + BlockHash: common.Hash{}.String(), + }, + false, + }, + { + "zero block number", + &evm.Log{ + Address: addr, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + BlockNumber: 0, + }, + false, + }, + { + "empty tx hash", + &evm.Log{ + Address: addr, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + BlockNumber: 1, + TxHash: common.Hash{}.String(), + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + err := tc.log.Validate() + if tc.expPass { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestConversionFunctions(t *testing.T) { + addr := evmtest.NewEthAddr().String() + + txLogs := evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{ + { + Address: addr, + Topics: []string{common.BytesToHash([]byte("topic")).String()}, + Data: []byte("data"), + BlockNumber: 1, + TxHash: common.BytesToHash([]byte("tx_hash")).String(), + TxIndex: 1, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + Index: 1, + Removed: false, + }, + }, + } + + // convert valid log to eth logs and back (and validate) + conversionLogs := evm.NewTransactionLogsFromEth(common.BytesToHash([]byte("tx_hash")), txLogs.EthLogs()) + conversionErr := conversionLogs.Validate() + + // create new transaction logs as copy of old valid one (and validate) + copyLogs := evm.NewTransactionLogs(common.BytesToHash([]byte("tx_hash")), txLogs.Logs) + copyErr := copyLogs.Validate() + + require.Nil(t, conversionErr) + require.Nil(t, copyErr) +} diff --git a/x/evm/msg.go b/x/evm/msg.go index 9cb96fb58..5f0c2017b 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -6,6 +6,8 @@ import ( "fmt" "math/big" + "github.com/cosmos/gogoproto/proto" + sdkmath "cosmossdk.io/math" errorsmod "cosmossdk.io/errors" @@ -387,3 +389,60 @@ func (m *MsgUpdateParams) ValidateBasic() error { func (m MsgUpdateParams) GetSignBytes() []byte { return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&m)) } + +// UnwrapEthereumMsg extracts MsgEthereumTx from wrapping sdk.Tx +func UnwrapEthereumMsg(tx *sdk.Tx, ethHash common.Hash) (*MsgEthereumTx, error) { + if tx == nil { + return nil, fmt.Errorf("invalid tx: nil") + } + + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("invalid tx type: %T", tx) + } + txHash := ethMsg.AsTransaction().Hash() + ethMsg.Hash = txHash.Hex() + if txHash == ethHash { + return ethMsg, nil + } + } + + return nil, fmt.Errorf("eth tx not found: %s", ethHash) +} + +// EncodeTransactionLogs encodes TransactionLogs slice into a protobuf-encoded +// byte slice. +func EncodeTransactionLogs(res *TransactionLogs) ([]byte, error) { + return proto.Marshal(res) +} + +// DecodeTransactionLogs decodes an protobuf-encoded byte slice into +// TransactionLogs +func DecodeTransactionLogs(data []byte) (TransactionLogs, error) { + var logs TransactionLogs + err := proto.Unmarshal(data, &logs) + if err != nil { + return TransactionLogs{}, err + } + return logs, nil +} + +// DecodeTxResponse decodes an protobuf-encoded byte slice into TxResponse +func DecodeTxResponse(in []byte) (*MsgEthereumTxResponse, error) { + var txMsgData sdk.TxMsgData + if err := proto.Unmarshal(in, &txMsgData); err != nil { + return nil, err + } + + if len(txMsgData.MsgResponses) == 0 { + return &MsgEthereumTxResponse{}, nil + } + + var res MsgEthereumTxResponse + if err := proto.Unmarshal(txMsgData.MsgResponses[0].Value, &res); err != nil { + return nil, errorsmod.Wrap(err, "failed to unmarshal tx response message data") + } + + return &res, nil +} diff --git a/x/evm/msg_test.go b/x/evm/msg_test.go index 3bb0d33c9..0bdaa24c9 100644 --- a/x/evm/msg_test.go +++ b/x/evm/msg_test.go @@ -20,10 +20,12 @@ import ( "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/eth/encoding" - "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" ) type MsgsSuite struct { @@ -43,11 +45,11 @@ func TestMsgsSuite(t *testing.T) { } func (s *MsgsSuite) SetupTest() { - from, privFrom := testutil.PrivKeyEth() + from, privFrom := evmtest.PrivKeyEth() - s.signer = testutil.NewSigner(privFrom) + s.signer = evmtest.NewSigner(privFrom) s.from = from - s.to = testutil.NewEthAddr() + s.to = evmtest.NewEthAddr() s.chainID = big.NewInt(1) s.hundredBigInt = big.NewInt(100) @@ -921,3 +923,63 @@ func assertEqualTxs(orig *gethcore.Transaction, cpy *gethcore.Transaction) error } return nil } + +func (s *MsgsSuite) TestUnwrapEthererumMsg() { + _, err := evm.UnwrapEthereumMsg(nil, common.Hash{}) + s.NotNil(err) + + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + clientCtx := client.Context{}.WithTxConfig(encodingConfig.TxConfig) + builder, _ := clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + + tx := builder.GetTx().(sdk.Tx) + _, err = evm.UnwrapEthereumMsg(&tx, common.Hash{}) + s.NotNil(err) + + evmTxParams := &evm.EvmTxArgs{ + ChainID: big.NewInt(1), + Nonce: 0, + To: &common.Address{}, + Amount: big.NewInt(0), + GasLimit: 0, + GasPrice: big.NewInt(0), + Input: []byte{}, + } + + msg := evm.NewTx(evmTxParams) + err = builder.SetMsgs(msg) + s.Nil(err) + + tx = builder.GetTx().(sdk.Tx) + unwrappedMsg, err := evm.UnwrapEthereumMsg(&tx, msg.AsTransaction().Hash()) + s.Nil(err) + s.Equal(unwrappedMsg, msg) +} + +func (s *MsgsSuite) TestTransactionLogsEncodeDecode() { + addr := evmtest.NewEthAddr().String() + + txLogs := evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).String(), + Logs: []*evm.Log{ + { + Address: addr, + Topics: []string{common.BytesToHash([]byte("topic")).String()}, + Data: []byte("data"), + BlockNumber: 1, + TxHash: common.BytesToHash([]byte("tx_hash")).String(), + TxIndex: 1, + BlockHash: common.BytesToHash([]byte("block_hash")).String(), + Index: 1, + Removed: false, + }, + }, + } + + txLogsEncoded, encodeErr := evm.EncodeTransactionLogs(&txLogs) + s.Nil(encodeErr) + + txLogsEncodedDecoded, decodeErr := evm.DecodeTransactionLogs(txLogsEncoded) + s.Nil(decodeErr) + s.Equal(txLogs, txLogsEncodedDecoded) +} diff --git a/x/evm/params.go b/x/evm/params.go index 0318bb0c2..8112b61e0 100644 --- a/x/evm/params.go +++ b/x/evm/params.go @@ -20,9 +20,12 @@ import ( "github.com/NibiruChain/nibiru/eth" ) -var ( +const ( // DefaultEVMDenom defines the default EVM denomination DefaultEVMDenom = "unibi" +) + +var ( // DefaultAllowUnprotectedTxs rejects all unprotected txs (i.e false) DefaultAllowUnprotectedTxs = false // DefaultEnableCreate enables contract creation (i.e true) diff --git a/x/evm/tx_test.go b/x/evm/tx_test.go index 6567b4f99..929a599c2 100644 --- a/x/evm/tx_test.go +++ b/x/evm/tx_test.go @@ -10,8 +10,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" "github.com/stretchr/testify/suite" ) @@ -45,7 +45,7 @@ func (suite *TxDataTestSuite) SetupTest() { suite.sdkMinusOneInt = sdkmath.NewInt(-1) suite.invalidAddr = "123456" - suite.addr = testutil.NewEthAddr() + suite.addr = evmtest.NewEthAddr() suite.hexAddr = suite.addr.Hex() suite.hexDataBytes = hexutil.Bytes([]byte("data")) diff --git a/x/evm/vmtracer.go b/x/evm/vmtracer.go new file mode 100644 index 000000000..5438d778d --- /dev/null +++ b/x/evm/vmtracer.go @@ -0,0 +1,111 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evm + +import ( + "math/big" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/params" +) + +const ( + TracerAccessList = "access_list" + TracerJSON = "json" + TracerStruct = "struct" + TracerMarkdown = "markdown" +) + +// NewTracer creates a new Logger tracer to collect execution traces from an +// EVM transaction. +func NewTracer(tracer string, msg core.Message, cfg *params.ChainConfig, height int64) vm.EVMLogger { + // TODO: enable additional log configuration + logCfg := &logger.Config{ + Debug: true, + } + + // FIXME: inconsistent logging between stdout and stderr + switch tracer { + case TracerAccessList: + preCompiles := vm.DefaultActivePrecompiles(cfg.Rules(big.NewInt(height), cfg.MergeNetsplitBlock != nil)) + return logger.NewAccessListTracer(msg.AccessList(), msg.From(), *msg.To(), preCompiles) + case TracerJSON: + return logger.NewJSONLogger(logCfg, os.Stderr) + case TracerMarkdown: + return logger.NewMarkdownLogger(logCfg, os.Stdout) + case TracerStruct: + return logger.NewStructLogger(logCfg) + default: + return NewNoOpTracer() + } +} + +// TxTraceResult is the result of a single transaction trace during a block trace. +type TxTraceResult struct { + Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer +} + +var _ vm.EVMLogger = &NoOpTracer{} + +// NoOpTracer is an empty implementation of vm.Tracer interface +type NoOpTracer struct{} + +// NewNoOpTracer creates a no-op vm.Tracer +func NewNoOpTracer() *NoOpTracer { + return &NoOpTracer{} +} + +// CaptureStart implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureStart(env *vm.EVM, + from common.Address, + to common.Address, + create bool, + input []byte, + gas uint64, + value *big.Int) { +} + +// CaptureState implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +// CaptureFault implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnd implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureEnd(output []byte, gasUsed uint64, tm time.Duration, err error) {} + +// CaptureEnter implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +// CaptureExit implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureExit(output []byte, gasUsed uint64, err error) {} + +// CaptureTxStart implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureTxStart(gasLimit uint64) {} + +// CaptureTxEnd implements vm.Tracer interface +// +//nolint:revive // allow unused parameters to indicate expected signature +func (dt NoOpTracer) CaptureTxEnd(restGas uint64) {} diff --git a/x/oracle/client/cli/gen_pricefeeder_delegation_test.go b/x/oracle/client/cli/gen_pricefeeder_delegation_test.go index be4a98254..04dbb081b 100644 --- a/x/oracle/client/cli/gen_pricefeeder_delegation_test.go +++ b/x/oracle/client/cli/gen_pricefeeder_delegation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/common/testutil" "github.com/NibiruChain/nibiru/x/oracle/client/cli" @@ -13,7 +14,7 @@ import ( ) func TestAddGenesisPricefeederDelegation(t *testing.T) { - app.SetPrefixes(app.AccountAddressPrefix) + app.SetPrefixes(appconst.AccountAddressPrefix) tests := []struct { name string