diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4d1021e..a7b773d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1873](https://github.com/NibiruChain/nibiru/pull/1873) - feat(evm): keeper collections and grpc query impls for EthAccount, NibiruAccount - [#xxxx](https://github.com/NibiruChain/nibiru/pull/xxxx) - feat(evm): keeper logic for ValidatorAccount, BaseFee, contract bytecode, EthCall, etc. - [#1889](https://github.com/NibiruChain/nibiru/pull/1889) - feat: implemented basic evm tx methods +- [#1883](https://github.com/NibiruChain/nibiru/pull/1883) - feat(evm): keeper logic, Ante handlers, EthCall, and EVM transactions. +- [#1887](https://github.com/NibiruChain/nibiru/pull/1887) - test(evm): eth api integration test suite #### Dapp modules: perp, spot, oracle, etc diff --git a/app/app.go b/app/app.go index b6e5eec31..e00bff067 100644 --- a/app/app.go +++ b/app/app.go @@ -8,8 +8,6 @@ import ( "os" "path/filepath" - "github.com/cosmos/cosmos-sdk/types/mempool" - wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" @@ -20,6 +18,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/log" tmos "github.com/cometbft/cometbft/libs/os" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" _ "github.com/cosmos/cosmos-sdk/client/docs/statik" @@ -34,6 +33,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/version" authante "github.com/cosmos/cosmos-sdk/x/auth/ante" @@ -42,9 +42,11 @@ import ( capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" "github.com/cosmos/cosmos-sdk/x/crisis" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/cosmos/ibc-go/v7/testing/types" + "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/rakyll/statik/fs" @@ -139,6 +141,13 @@ func NewNibiruApp( legacyAmino := encodingConfig.Amino interfaceRegistry := encodingConfig.InterfaceRegistry txConfig := encodingConfig.TxConfig + baseAppOptions = append(baseAppOptions, func(app *baseapp.BaseApp) { + mp := mempool.NoOpMempool{} + app.SetMempool(mp) + handler := baseapp.NewDefaultProposalHandler(mp, app) + app.SetPrepareProposal(handler.PrepareProposalHandler()) + app.SetProcessProposal(handler.ProcessProposalHandler()) + }) baseAppOptions = append(baseAppOptions, func(app *baseapp.BaseApp) { mp := mempool.NoOpMempool{} diff --git a/app/appconst/appconst.go b/app/appconst/appconst.go index 03964e07d..6360a6e78 100644 --- a/app/appconst/appconst.go +++ b/app/appconst/appconst.go @@ -55,3 +55,36 @@ func RuntimeVersion() string { GoArch, ) } + +// EIP 155 Chain IDs exported for tests. +const ( + ETH_CHAIN_ID_MAINNET int64 = 420 + ETH_CHAIN_ID_LOCAL int64 = 256 + ETH_CHAIN_ID_DEVNET int64 = 500 + ETH_CHAIN_ID_DEFAULT int64 = 3000 +) + +var knownEthChainIDMap = map[string]int64{ + "cataclysm-1": ETH_CHAIN_ID_MAINNET, + "nibiru-localnet-0": ETH_CHAIN_ID_LOCAL, + "nibiru-localnet-1": ETH_CHAIN_ID_LOCAL, + "nibiru-localnet-2": ETH_CHAIN_ID_LOCAL, + "nibiru-testnet-0": ETH_CHAIN_ID_DEVNET, + "nibiru-testnet-1": ETH_CHAIN_ID_DEVNET, + "nibiru-testnet-2": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-0": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-1": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-2": ETH_CHAIN_ID_DEVNET, +} + +// GetEthChainID: Maps the given chain ID from the block's `sdk.Context` to an +// EVM Chain ID (`*big.Int`). +func GetEthChainID(ctxChainID string) (ethChainID *big.Int) { + ethChainIdInt, found := knownEthChainIDMap[ctxChainID] + if !found { + ethChainID = big.NewInt(ETH_CHAIN_ID_DEFAULT) + } else { + ethChainID = big.NewInt(ethChainIdInt) + } + return ethChainID +} diff --git a/app/appconst/appconst_test.go b/app/appconst/appconst_test.go new file mode 100644 index 000000000..ba680ce91 --- /dev/null +++ b/app/appconst/appconst_test.go @@ -0,0 +1,49 @@ +package appconst_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app/appconst" +) + +type TestSuite struct { + suite.Suite +} + +func TestSuite_RunAll(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) TestGetEthChainID() { + s.Run("mainnet", func() { + s.EqualValues( + big.NewInt(appconst.ETH_CHAIN_ID_MAINNET), + appconst.GetEthChainID("cataclysm-1"), + ) + }) + s.Run("localnet", func() { + s.EqualValues( + big.NewInt(appconst.ETH_CHAIN_ID_LOCAL), + appconst.GetEthChainID("nibiru-localnet-0"), + ) + }) + s.Run("devnet", func() { + want := big.NewInt(appconst.ETH_CHAIN_ID_DEVNET) + given := "nibiru-testnet-1" + s.EqualValues(want, appconst.GetEthChainID(given)) + + given = "nibiru-devnet-2" + s.EqualValues(want, appconst.GetEthChainID(given)) + }) + s.Run("else", func() { + want := big.NewInt(appconst.ETH_CHAIN_ID_DEFAULT) + for _, given := range []string{ + "foo", "bloop-blap", "not a chain ID", "", "0x12345", + } { + s.EqualValues(want, appconst.GetEthChainID(given)) + } + }) +} diff --git a/app/evmante_eth.go b/app/evmante_eth.go index a178d918c..1f020094b 100644 --- a/app/evmante_eth.go +++ b/app/evmante_eth.go @@ -21,18 +21,18 @@ import ( ) var ( - _ sdk.AnteDecorator = (*EthGasConsumeDecorator)(nil) - _ sdk.AnteDecorator = (*EthAccountVerificationDecorator)(nil) + _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) + _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) ) -// EthAccountVerificationDecorator validates an account balance checks -type EthAccountVerificationDecorator struct { +// AnteDecVerifyEthAcc validates an account balance checks +type AnteDecVerifyEthAcc struct { AppKeepers } -// NewEthAccountVerificationDecorator creates a new EthAccountVerificationDecorator -func NewEthAccountVerificationDecorator(k AppKeepers) EthAccountVerificationDecorator { - return EthAccountVerificationDecorator{ +// NewAnteDecVerifyEthAcc creates a new EthAccountVerificationDecorator +func NewAnteDecVerifyEthAcc(k AppKeepers) AnteDecVerifyEthAcc { + return AnteDecVerifyEthAcc{ AppKeepers: k, } } @@ -43,7 +43,7 @@ func NewEthAccountVerificationDecorator(k AppKeepers) EthAccountVerificationDeco // - any of the msgs is not a MsgEthereumTx // - from address is empty // - account balance is lower than the transaction cost -func (avd EthAccountVerificationDecorator) AnteHandle( +func (avd AnteDecVerifyEthAcc) AnteHandle( ctx sdk.Context, tx sdk.Tx, simulate bool, @@ -90,9 +90,9 @@ func (avd EthAccountVerificationDecorator) AnteHandle( return next(ctx, tx, simulate) } -// EthGasConsumeDecorator validates enough intrinsic gas for the transaction and +// AnteDecEthGasConsume validates enough intrinsic gas for the transaction and // gas consumption. -type EthGasConsumeDecorator struct { +type AnteDecEthGasConsume struct { AppKeepers // bankKeeper anteutils.BankKeeper // distributionKeeper anteutils.DistributionKeeper @@ -101,12 +101,12 @@ type EthGasConsumeDecorator struct { maxGasWanted uint64 } -// NewEthGasConsumeDecorator creates a new EthGasConsumeDecorator -func NewEthGasConsumeDecorator( +// NewAnteDecEthGasConsume creates a new EthGasConsumeDecorator +func NewAnteDecEthGasConsume( keepers AppKeepers, maxGasWanted uint64, -) EthGasConsumeDecorator { - return EthGasConsumeDecorator{ +) AnteDecEthGasConsume { + return AnteDecEthGasConsume{ AppKeepers: keepers, maxGasWanted: maxGasWanted, } @@ -129,7 +129,7 @@ func NewEthGasConsumeDecorator( // - transaction or block gas meter runs out of gas // - sets the gas meter limit // - gas limit is greater than the block gas meter limit -func (egcd EthGasConsumeDecorator) AnteHandle( +func (egcd AnteDecEthGasConsume) AnteHandle( ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, ) (sdk.Context, error) { gasWanted := uint64(0) @@ -242,7 +242,7 @@ func (egcd EthGasConsumeDecorator) AnteHandle( // deductFee checks if the fee payer has enough funds to pay for the fees and deducts them. // If the spendable balance is not enough, it tries to claim enough staking rewards to cover the fees. -func (egcd EthGasConsumeDecorator) deductFee(ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress) error { +func (egcd AnteDecEthGasConsume) deductFee(ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress) error { if fees.IsZero() { return nil } @@ -335,14 +335,14 @@ func (ctd CanTransferDecorator) AnteHandle( return next(ctx, tx, simulate) } -// EthIncrementSenderSequenceDecorator increments the sequence of the signers. -type EthIncrementSenderSequenceDecorator struct { +// AnteDecEthIncrementSenderSequence increments the sequence of the signers. +type AnteDecEthIncrementSenderSequence struct { AppKeepers } -// NewEthIncrementSenderSequenceDecorator creates a new EthIncrementSenderSequenceDecorator. -func NewEthIncrementSenderSequenceDecorator(k AppKeepers) EthIncrementSenderSequenceDecorator { - return EthIncrementSenderSequenceDecorator{ +// NewAnteDecEthIncrementSenderSequence creates a new EthIncrementSenderSequenceDecorator. +func NewAnteDecEthIncrementSenderSequence(k AppKeepers) AnteDecEthIncrementSenderSequence { + return AnteDecEthIncrementSenderSequence{ AppKeepers: k, } } @@ -350,7 +350,7 @@ func NewEthIncrementSenderSequenceDecorator(k AppKeepers) EthIncrementSenderSequ // AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a // contract creation, the nonce will be incremented during the transaction execution and not within // this AnteHandler decorator. -func (issd EthIncrementSenderSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { +func (issd AnteDecEthIncrementSenderSequence) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { diff --git a/app/evmante_handler.go b/app/evmante_handler.go index 0d958f363..2ab2557ff 100644 --- a/app/evmante_handler.go +++ b/app/evmante_handler.go @@ -20,10 +20,10 @@ func NewAnteHandlerEVM( NewEthMinGasPriceDecorator(k), NewEthValidateBasicDecorator(k), NewEthSigVerificationDecorator(k), - NewEthAccountVerificationDecorator(k), + NewAnteDecVerifyEthAcc(k), NewCanTransferDecorator(k), - NewEthGasConsumeDecorator(k, options.MaxTxGasWanted), - NewEthIncrementSenderSequenceDecorator(k), + NewAnteDecEthGasConsume(k, options.MaxTxGasWanted), + NewAnteDecEthIncrementSenderSequence(k), NewGasWantedDecorator(k), // emit eth tx hash and index at the very last ante handler. NewEthEmitEventDecorator(k), diff --git a/app/evmante_test.go b/app/evmante_test.go new file mode 100644 index 000000000..dfcdb9e23 --- /dev/null +++ b/app/evmante_test.go @@ -0,0 +1,101 @@ +package app_test + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +var NextNoOpAnteHandler sdk.AnteHandler = func( + ctx sdk.Context, tx sdk.Tx, simulate bool, +) (newCtx sdk.Context, err error) { + return ctx, nil +} + +func (s *TestSuite) TestAnteDecoratorVerifyEthAcc_CheckTx() { + happyCreateContractTx := func(deps *evmtest.TestDeps) *evm.MsgEthereumTx { + ethContractCreationTxParams := &evm.EvmTxArgs{ + ChainID: deps.Chain.EvmKeeper.EthChainID(deps.Ctx), + Nonce: 1, + Amount: big.NewInt(10), + GasLimit: 1000, + GasPrice: big.NewInt(1), + } + tx := evm.NewTx(ethContractCreationTxParams) + tx.From = deps.Sender.EthAddr.Hex() + return tx + } + + testCases := []struct { + name string + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) *evm.MsgEthereumTx + wantErr string + }{ + { + name: "happy: sender with funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + gasLimit := new(big.Int).SetUint64( + gethparams.TxGasContractCreation + 500, + ) + // Force account to be a smart contract + sdb.AddBalance(deps.Sender.EthAddr, gasLimit) + }, + txSetup: happyCreateContractTx, + wantErr: "", + }, + { + name: "sad: sender has insufficient gas balance", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {}, + txSetup: happyCreateContractTx, + wantErr: "sender balance < tx cost", + }, + { + name: "sad: sender cannot be a contract -> no contract bytecode", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + // Force account to be a smart contract + sdb.SetCode(deps.Sender.EthAddr, []byte("evm bytecode stuff")) + }, + txSetup: happyCreateContractTx, + wantErr: "sender is not EOA", + }, + { + name: "sad: invalid tx", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {}, + txSetup: func(deps *evmtest.TestDeps) *evm.MsgEthereumTx { + return new(evm.MsgEthereumTx) + }, + wantErr: "failed to unpack tx data", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewAnteDecVerifyEthAcc(deps.Chain.AppKeepers) + + tc.beforeTxSetup(&deps, stateDB) + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} diff --git a/eth/chain_id.go b/eth/chain_id.go index 9613e4bd4..23b0b4897 100644 --- a/eth/chain_id.go +++ b/eth/chain_id.go @@ -37,12 +37,20 @@ func IsValidChainID(chainID string) bool { return nibiruEvmChainId.MatchString(chainID) } -// TODO: feat(eth): Make ParseEthChainID compatible with existing testnets. - // ParseEthChainID parses a string chain identifier's epoch to an +// Ethereum-compatible chain-id in *big.Int format. +// +// This function uses Nibiru's map of chain IDs defined in Nibiru/app/appconst +// rather than the regex of EIP155, which is implemented by +// ParseEthChainIDStrict. +func ParseEthChainID(chainID string) (*big.Int, error) { + return appconst.GetEthChainID(chainID), nil +} + +// ParseEthChainIDStrict parses a string chain identifier's epoch to an // Ethereum-compatible chain-id in *big.Int format. The function returns an error // if the chain-id has an invalid format -func ParseEthChainID(chainID string) (*big.Int, error) { +func ParseEthChainIDStrict(chainID string) (*big.Int, error) { evmChainID, exists := appconst.EVMChainIDs[chainID] if exists { return evmChainID, nil diff --git a/eth/chain_id_test.go b/eth/chain_id_test.go index f6e079c21..408ef2790 100644 --- a/eth/chain_id_test.go +++ b/eth/chain_id_test.go @@ -2,6 +2,7 @@ package eth import ( "math/big" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -31,7 +32,7 @@ func TestParseChainID_Happy(t *testing.T) { } for _, tc := range testCases { - chainIDEpoch, err := ParseEthChainID(tc.chainID) + chainIDEpoch, err := ParseEthChainIDStrict(tc.chainID) require.NoError(t, err, tc.name) var errMsg string = "" if err != nil { @@ -39,5 +40,85 @@ func TestParseChainID_Happy(t *testing.T) { } assert.NoError(t, err, tc.name, errMsg) require.Equal(t, tc.expInt, chainIDEpoch, tc.name) + require.True(t, IsValidChainID(tc.chainID)) + } +} + +func TestParseChainID_Sad(t *testing.T) { + testCases := []struct { + name string + chainID string + }{ + { + chainID: "chain_1_1", + name: "invalid chain-id, double underscore", + }, + { + chainID: "-", + name: "invalid chain-id, dash only", + }, + { + chainID: "-1", + name: "invalid chain-id, undefined identifier and EIP155", + }, + { + chainID: "_1-1", + name: "invalid chain-id, undefined identifier", + }, + { + chainID: "NIBIRU_1-1", + name: "invalid chain-id, uppercases", + }, + { + chainID: "Nibiru_1-1", + name: "invalid chain-id, mixed cases", + }, + { + chainID: "$&*#!_1-1", + name: "invalid chain-id, special chars", + }, + { + chainID: "nibiru_001-1", + name: "invalid eip155 chain-id, cannot start with 0", + }, + { + chainID: "nibiru_0x212-1", + name: "invalid eip155 chain-id, cannot invalid base", + }, + { + chainID: "nibiru_1-0x212", + name: "invalid eip155 chain-id, cannot invalid base", + }, + { + chainID: "nibiru_nibiru_9000-1", + name: "invalid eip155 chain-id, non-integer", + }, + { + chainID: "nibiru_-", + name: "invalid epoch, undefined", + }, + { + chainID: " ", + name: "blank chain ID", + }, + { + chainID: "", + name: "empty chain ID", + }, + { + chainID: "_-", + name: "empty content for chain id, eip155 and epoch numbers", + }, + { + chainID: "nibiru_" + strings.Repeat("1", 45) + "-1", + name: "long chain-id", + }, + } + + for _, tc := range testCases { + chainIDEpoch, err := ParseEthChainIDStrict(tc.chainID) + require.Error(t, err, tc.name) + require.Nil(t, chainIDEpoch) + require.False(t, IsValidChainID(tc.chainID), tc.name) } } diff --git a/eth/eip712/eip712_test.go b/eth/eip712/eip712_test.go index b0ef4f62c..885f292a5 100644 --- a/eth/eip712/eip712_test.go +++ b/eth/eip712/eip712_test.go @@ -48,7 +48,6 @@ import ( const ( msgsFieldName = "msgs" - baseDenom = "anibi" TESTNET_CHAIN_ID = "nibiru_9000" ) @@ -81,7 +80,7 @@ func TestEIP712TestSuite(t *testing.T) { func (suite *EIP712TestSuite) SetupTest() { suite.config = encoding.MakeConfig(app.ModuleBasics) suite.clientCtx = client.Context{}.WithTxConfig(suite.config.TxConfig) - suite.denom = baseDenom + suite.denom = evm.DefaultEVMDenom sdk.GetConfig().SetBech32PrefixForAccount(ethclient.Bech32Prefix, "") eip712.SetEncodingConfig(suite.config) @@ -273,6 +272,18 @@ func (suite *EIP712TestSuite) TestEIP712() { msgs: []sdk.Msg{}, expectSuccess: false, }, + { + title: "Success - Invalid ChainID uses default", + chainID: "invalidchainid", + msgs: []sdk.Msg{ + govtypes.NewMsgVote( + suite.createTestAddress(), + 5, + govtypes.OptionNo, + ), + }, + expectSuccess: true, + }, { title: "Fails - Includes TimeoutHeight", msgs: []sdk.Msg{ diff --git a/eth/rpc/backend/chain_info_test.go b/eth/rpc/backend/chain_info_test.go index aa80ada9c..cc533257c 100644 --- a/eth/rpc/backend/chain_info_test.go +++ b/eth/rpc/backend/chain_info_test.go @@ -99,7 +99,8 @@ func (s *BackendSuite) TestBaseFee() { } func (s *BackendSuite) TestChainId() { - expChainIDNumber := eth.ParseEIP155ChainIDNumber(eth.EIP155ChainID_Testnet) + expChainIDNumber, err := eth.ParseEthChainID(eth.EIP155ChainID_Testnet) + s.Require().NoError(err) expChainID := (*hexutil.Big)(expChainIDNumber) testCases := []struct { name string diff --git a/go.mod b/go.mod index a5a516f40..c8090fe61 100644 --- a/go.mod +++ b/go.mod @@ -50,9 +50,12 @@ require ( require ( cosmossdk.io/collections v0.4.0 + cosmossdk.io/tools/rosetta v0.2.1 github.com/davecgh/go-spew v1.1.1 github.com/gorilla/websocket v1.5.0 + github.com/rs/cors v1.8.3 golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb + golang.org/x/net v0.23.0 golang.org/x/text v0.14.0 ) @@ -66,7 +69,6 @@ require ( cosmossdk.io/core v0.10.0 // indirect cosmossdk.io/depinject v1.0.0-alpha.4 // indirect cosmossdk.io/log v1.3.1 // indirect - cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect @@ -113,6 +115,7 @@ require ( 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/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect github.com/getsentry/sentry-go v0.23.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-gonic/gin v1.9.1 // indirect @@ -150,8 +153,10 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huandu/skiplist v1.2.0 // indirect + github.com/huin/goupnp v1.0.3 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect @@ -184,12 +189,12 @@ require ( 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 github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect @@ -208,7 +213,6 @@ require ( go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index 77a205df5..20381a81f 100644 --- a/go.sum +++ b/go.sum @@ -422,8 +422,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= -github.com/cosmos/cosmos-db v1.0.0 h1:EVcQZ+qYag7W6uorBKFPvX6gRjw6Uq2hIh4hCWjuQ0E= -github.com/cosmos/cosmos-db v1.0.0/go.mod h1:iBvi1TtqaedwLdcrZVYRSSCb6eSy61NLj4UNmdIgs0U= github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAKs= github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= github.com/cosmos/cosmos-proto v1.0.0-beta.4 h1:aEL7tU/rLOmxZQ9z4i7mzxcLbSCY48OdY7lIWTLG7oU= @@ -543,6 +541,7 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= @@ -1188,6 +1187,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= diff --git a/x/common/error.go b/x/common/error.go index 2f640ce50..de3e4acac 100644 --- a/x/common/error.go +++ b/x/common/error.go @@ -184,9 +184,7 @@ func CombineErrorsFromStrings(strs ...string) (err error) { return CombineErrors(errs...) } -func ErrNilGrpcMsg() error { - return grpcstatus.Errorf(grpccodes.InvalidArgument, "nil msg") -} +var ErrNilGrpcMsg = grpcstatus.Errorf(grpccodes.InvalidArgument, "nil msg") // ErrNotImplemented: Represents an function error value. func ErrNotImplemented() error { return fmt.Errorf("fn not implemented yet") } diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index 348ff0e48..a2f1096c5 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -3,8 +3,13 @@ package evmtest import ( "fmt" + "math/big" + gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/x/evm" ) type GethTxType = uint8 @@ -47,7 +52,80 @@ func NewEthTxUnsigned( typedTxData.Nonce = nonce ethCoreTx = gethcore.NewTx(typedTxData) default: - return ethCoreTx, fmt.Errorf("received unknown tx type in NewCoreTx") + return ethCoreTx, fmt.Errorf("received unknown tx type in NewEthTxUnsigned") } return ethCoreTx, err } + +func TxTemplateAccessListTx() *gethcore.AccessListTx { + return &gethcore.AccessListTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} +func TxTemplateLegacyTx() *gethcore.LegacyTx { + return &gethcore.LegacyTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} +func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { + return &gethcore.DynamicFeeTx{ + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(2), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func NewEthTxMsgFromTxData( + deps *TestDeps, + txType GethTxType, + innerTxData []byte, + nonce uint64, + accessList gethcore.AccessList, +) (*evm.MsgEthereumTx, error) { + if innerTxData == nil { + innerTxData = []byte{} + } + + var ethCoreTx *gethcore.Transaction + switch txType { + case gethcore.LegacyTxType: + innerTx := TxTemplateLegacyTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.AccessListTxType: + innerTx := TxTemplateAccessListTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.DynamicFeeTxType: + innerTx := TxTemplateDynamicFeeTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + default: + return nil, fmt.Errorf( + "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) + } + + ethTxMsg := new(evm.MsgEthereumTx) + if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { + return ethTxMsg, err + } + + ethTxMsg.From = deps.Sender.EthAddr.Hex() + return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) +} diff --git a/x/evm/keeper/evm_state.go b/x/evm/keeper/evm_state.go index ffc45e3fe..715105978 100644 --- a/x/evm/keeper/evm_state.go +++ b/x/evm/keeper/evm_state.go @@ -173,3 +173,13 @@ func (state EvmState) CalcBloomFromLogs( func (k Keeper) ResetTransientGasUsed(ctx sdk.Context) { k.EvmState.BlockGasUsed.Set(ctx, 0) } + +// GetAccNonce returns the sequence number of an account, returns 0 if not exists. +func (k *Keeper) GetAccNonce(ctx sdk.Context, addr gethcommon.Address) uint64 { + nibiruAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, nibiruAddr) + if acct == nil { + return 0 + } + return acct.GetSequence() +} diff --git a/x/evm/keeper/gas_fees_test.go b/x/evm/keeper/gas_fees_test.go new file mode 100644 index 000000000..d9ba98a66 --- /dev/null +++ b/x/evm/keeper/gas_fees_test.go @@ -0,0 +1,2 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper_test diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 6289d43c2..d2a798356 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -4,23 +4,36 @@ package keeper import ( "context" "encoding/json" + "errors" "fmt" "math/big" + "time" + "github.com/ethereum/go-ethereum/common/hexutil" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm/statedb" + grpccodes "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" + sdkmath "cosmossdk.io/math" + storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + gethparams "github.com/ethereum/go-ethereum/params" - "github.com/NibiruChain/nibiru/x/common" "github.com/NibiruChain/nibiru/x/evm" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" ) // Compile-time interface assertion @@ -155,17 +168,12 @@ func (k Keeper) BaseFee( goCtx context.Context, _ *evm.QueryBaseFeeRequest, ) (*evm.QueryBaseFeeResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - params := k.GetParams(ctx) ethCfg := params.ChainConfig.EthereumConfig(k.EthChainID(ctx)) - baseFee := k.GetBaseFee(ctx, ethCfg) - - res := &evm.QueryBaseFeeResponse{} - if baseFee != nil { - aux := sdkmath.NewIntFromBigInt(baseFee) - res.BaseFee = &aux - } - return res, nil + baseFee := sdkmath.NewIntFromBigInt(k.GetBaseFee(ctx, ethCfg)) + return &evm.QueryBaseFeeResponse{ + BaseFee: &baseFee, + }, nil } // Storage: Implements the gRPC query for "/eth.evm.v1.Query/Storage". @@ -263,32 +271,30 @@ func (k Keeper) Params( func (k Keeper) EthCall( goCtx context.Context, req *evm.EthCallRequest, ) (*evm.MsgEthereumTxResponse, error) { - if req == nil { - return nil, status.Error(codes.InvalidArgument, "empty request") + if err := req.Validate(); err != nil { + return nil, err } + ctx := sdk.UnwrapSDKContext(goCtx) var args evm.JsonTxArgs err := json.Unmarshal(req.Args, &args) if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) - } - chainID, err := getChainID(ctx, req.ChainId) - if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) } - cfg, err := k.EVMConfig(ctx, GetProposerAddress(ctx, req.ProposerAddress), chainID) + chainID := k.EthChainID(ctx) + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } // ApplyMessageWithConfig expect correct nonce set in msg - nonce := k.GetNonce(ctx, args.GetFrom()) + nonce := k.GetAccNonce(ctx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) } txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash())) @@ -296,7 +302,7 @@ func (k Keeper) EthCall( // pass false to not commit StateDB res, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig) if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } return res, nil @@ -307,12 +313,11 @@ func (k Keeper) EthCall( func (k Keeper) EstimateGas( goCtx context.Context, req *evm.EthCallRequest, ) (*evm.EstimateGasResponse, error) { - // TODO: feat(evm): impl query EstimateGas return k.EstimateGasForEvmCallType(goCtx, req, evm.CallTypeRPC) } // EstimateGasForEvmCallType estimates the gas cost of a transaction. This can be -// called with the "eth_estimateGas" JSON-RPC method or an smart contract query. +// called with the "eth_estimateGas" JSON-RPC method or smart contract query. // // When [EstimateGas] is called from the JSON-RPC client, we need to reset the // gas meter before simulating the transaction (tx) to have an accurate gas @@ -328,10 +333,146 @@ func (k Keeper) EstimateGas( func (k Keeper) EstimateGasForEvmCallType( goCtx context.Context, req *evm.EthCallRequest, fromType evm.CallType, ) (*evm.EstimateGasResponse, error) { - // TODO: feat(evm): impl query EstimateGasForEvmCallType - return &evm.EstimateGasResponse{ - Gas: 220000, // TODO: replace with real gas calc - }, nil + if err := req.Validate(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + chainID := k.EthChainID(ctx) + + if req.GasCap < gethparams.TxGas { + return nil, grpcstatus.Errorf(grpccodes.InvalidArgument, "gas cap cannot be lower than %d", gethparams.TxGas) + } + + var args evm.JsonTxArgs + err := json.Unmarshal(req.Args, &args) + if err != nil { + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) + } + + // Binary search the gas requirement, as it may be higher than the amount used + var ( + lo = gethparams.TxGas - 1 + hi uint64 + gasCap uint64 + ) + + // Determine the highest gas limit can be used during the estimation. + if args.Gas != nil && uint64(*args.Gas) >= gethparams.TxGas { + hi = uint64(*args.Gas) + } else { + // Query block gas limit + params := ctx.ConsensusParams() + if params != nil && params.Block != nil && params.Block.MaxGas > 0 { + hi = uint64(params.Block.MaxGas) + } else { + hi = req.GasCap + } + } + + // TODO: Recap the highest gas limit with account's available balance. + // Recap the highest gas allowance with specified gascap. + if req.GasCap != 0 && hi > req.GasCap { + hi = req.GasCap + } + + gasCap = hi + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") + } + + // ApplyMessageWithConfig expect correct nonce set in msg + nonce := k.GetAccNonce(ctx, args.GetFrom()) + args.Nonce = (*hexutil.Uint64)(&nonce) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + // convert the tx args to an ethereum message + msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + // NOTE: the errors from the executable below should be consistent with + // go-ethereum, so we don't wrap them with the gRPC status code Create a + // helper to check if a gas allowance results in an executable transaction. + executable := func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { + // update the message with the new gas value + msg = gethcore.NewMessage( + msg.From(), + msg.To(), + msg.Nonce(), + msg.Value(), + gas, + msg.GasPrice(), + msg.GasFeeCap(), + msg.GasTipCap(), + msg.Data(), + msg.AccessList(), + msg.IsFake(), + ) + + tmpCtx := ctx + if fromType == evm.CallTypeRPC { + tmpCtx, _ = ctx.CacheContext() + + acct := k.GetAccount(tmpCtx, msg.From()) + + from := msg.From() + if acct == nil { + acc := k.accountKeeper.NewAccountWithAddress(tmpCtx, from[:]) + k.accountKeeper.SetAccount(tmpCtx, acc) + acct = statedb.NewEmptyAccount() + } + // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce + acct.Nonce = nonce + 1 + err = k.SetAccount(tmpCtx, from, *acct) + if err != nil { + return true, nil, err + } + // resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions + gasMeter := eth.NewInfiniteGasMeterWithLimit(msg.Gas()) + tmpCtx = tmpCtx.WithGasMeter(gasMeter). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + } + // pass false to not commit StateDB + rsp, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return len(rsp.VmError) > 0, rsp, nil + } + + // Execute the binary search and hone in on an executable gas limit + hi, err = evm.BinSearch(lo, hi, executable) + if err != nil { + return nil, err + } + + // Reject the transaction as invalid if it still fails at the highest allowance + if hi == gasCap { + failed, result, err := executable(hi) + if err != nil { + return nil, err + } + + if failed { + if result != nil && result.VmError != vm.ErrOutOfGas.Error() { + if result.VmError == vm.ErrExecutionReverted.Error() { + return nil, evm.NewExecErrorWithReason(result.Ret) + } + return nil, errors.New(result.VmError) + } + // Otherwise, the specified gas cap is too low + return nil, fmt.Errorf("gas required exceeds allowance (%d)", gasCap) + } + } + return &evm.EstimateGasResponse{Gas: hi}, nil } // TraceTx configures a new tracer according to the provided configuration, and @@ -340,12 +481,98 @@ func (k Keeper) EstimateGasForEvmCallType( func (k Keeper) TraceTx( goCtx context.Context, req *evm.QueryTraceTxRequest, ) (*evm.QueryTraceTxResponse, error) { - // TODO: feat(evm): impl query TraceTx + if err := req.Validate(); err != nil { + return nil, err + } + + // get the context of block beginning + contextHeight := req.BlockNumber + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + + ctx := sdk.UnwrapSDKContext(goCtx) + ctx = ctx.WithBlockHeight(contextHeight) + ctx = ctx.WithBlockTime(req.BlockTime) + ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) + + // to get the base fee we only need the block max gas in the consensus params + ctx = ctx.WithConsensusParams(&cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, + }) + + chainID := k.EthChainID(ctx) + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Errorf(grpccodes.Internal, "failed to load evm config: %s", err.Error()) + } + + // compute and use base fee of the height that is being traced + baseFee := k.GetBaseFee(ctx, cfg.ChainConfig) + if baseFee != nil { + cfg.BaseFee = baseFee + } + + signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + // gas used at this point corresponds to GetProposerAddress & CalculateBaseFee + // need to reset gas meter per transaction to be consistent with tx execution + // and avoid stacking the gas used of every predecessor in the same gas meter + + for i, tx := range req.Predecessors { + ethTx := tx.AsTransaction() + msg, err := ethTx.AsMessage(signer, cfg.BaseFee) + if err != nil { + continue + } + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + // reset gas meter for each transaction + ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + rsp, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig) + if err != nil { + continue + } + txConfig.LogIndex += uint(len(rsp.Logs)) + } + + tx := req.Msg.AsTransaction() + txConfig.TxHash = tx.Hash() + if len(req.Predecessors) > 0 { + txConfig.TxIndex++ + } + + var tracerConfig json.RawMessage + if req.TraceConfig != nil && req.TraceConfig.TracerJsonConfig != "" { + // ignore error. default to no traceConfig + _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) + } + + result, _, err := k.TraceEthTxMsg(ctx, cfg, txConfig, signer, tx, req.TraceConfig, false, tracerConfig) + if err != nil { + // error will be returned with detail status from traceTx + return nil, err + } + + resultData, err := json.Marshal(result) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + return &evm.QueryTraceTxResponse{ - Data: []byte{}, - }, common.ErrNotImplementedGprc() + Data: resultData, + }, nil } +// Re-export of the default tracer timeout from go-ethereum. +// See "geth/eth/tracers/api.go". +const DefaultGethTraceTimeout = 5 * time.Second + // TraceBlock: Implements the gRPC query for "/eth.evm.v1.Query/TraceBlock". // Configures a Nibiru EVM tracer that is used to "trace" and analyze // the execution of transactions within a given block. Block information is read @@ -354,18 +581,167 @@ func (k Keeper) TraceTx( func (k Keeper) TraceBlock( goCtx context.Context, req *evm.QueryTraceBlockRequest, ) (*evm.QueryTraceBlockResponse, error) { - // TODO: feat(evm): impl query TraceBlock + if err := req.Validate(); err != nil { + return nil, err + } + + // get the context of block beginning + contextHeight := req.BlockNumber + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + + ctx := sdk.UnwrapSDKContext(goCtx) + ctx = ctx.WithBlockHeight(contextHeight) + ctx = ctx.WithBlockTime(req.BlockTime) + ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) + + // to get the base fee we only need the block max gas in the consensus params + ctx = ctx.WithConsensusParams(&cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, + }) + + chainID := k.EthChainID(ctx) + + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") + } + + // compute and use base fee of height that is being traced + baseFee := k.GetBaseFeeNoCfg(ctx) + if baseFee != nil { + cfg.BaseFee = baseFee + } + + signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + txsLength := len(req.Txs) + results := make([]*evm.TxTraceResult, 0, txsLength) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + for i, tx := range req.Txs { + result := evm.TxTraceResult{} + ethTx := tx.AsTransaction() + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + traceResult, logIndex, err := k.TraceEthTxMsg(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) + if err != nil { + result.Error = err.Error() + } else { + txConfig.LogIndex = logIndex + result.Result = traceResult + } + results = append(results, &result) + } + + resultData, err := json.Marshal(results) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + return &evm.QueryTraceBlockResponse{ - Data: []byte{}, - }, common.ErrNotImplementedGprc() + Data: resultData, + }, nil } -// getChainID parse chainID from current context if not provided -func getChainID(ctx sdk.Context, chainID int64) (*big.Int, error) { - if chainID == 0 { - return eth.ParseEthChainID(ctx.ChainID()) +// TraceEthTxMsg do trace on one transaction, it returns a tuple: (traceResult, +// nextLogIndex, error). +func (k *Keeper) TraceEthTxMsg( + ctx sdk.Context, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, + signer gethcore.Signer, + tx *gethcore.Transaction, + traceConfig *evm.TraceConfig, + commitMessage bool, + tracerJSONConfig json.RawMessage, +) (*interface{}, uint, error) { + // Assemble the structured logger or the JavaScript tracer + var ( + tracer tracers.Tracer + overrides *gethparams.ChainConfig + err error + timeout = DefaultGethTraceTimeout + ) + msg, err := tx.AsMessage(signer, cfg.BaseFee) + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + if traceConfig == nil { + traceConfig = &evm.TraceConfig{} + } + + if traceConfig.Overrides != nil { + overrides = traceConfig.Overrides.EthereumConfig(cfg.ChainConfig.ChainID) } - return big.NewInt(chainID), nil + + logConfig := logger.Config{ + EnableMemory: traceConfig.EnableMemory, + DisableStorage: traceConfig.DisableStorage, + DisableStack: traceConfig.DisableStack, + EnableReturnData: traceConfig.EnableReturnData, + Debug: traceConfig.Debug, + Limit: int(traceConfig.Limit), + Overrides: overrides, + } + + tracer = logger.NewStructLogger(&logConfig) + + tCtx := &tracers.Context{ + BlockHash: txConfig.BlockHash, + TxIndex: int(txConfig.TxIndex), + TxHash: txConfig.TxHash, + } + + if traceConfig.Tracer != "" { + if tracer, err = tracers.New(traceConfig.Tracer, tCtx, tracerJSONConfig); err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + } + + // Define a meaningful timeout of a single transaction trace + if traceConfig.Timeout != "" { + if timeout, err = time.ParseDuration(traceConfig.Timeout); err != nil { + return nil, 0, grpcstatus.Errorf(grpccodes.InvalidArgument, "timeout value: %s", err.Error()) + } + } + + // Handle timeouts and RPC cancellations + deadlineCtx, cancel := context.WithTimeout(ctx.Context(), timeout) + defer cancel() + + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + } + }() + + // In order to be on in sync with the tx execution gas meter, + // we need to: + // 1. Reset GasMeter with InfiniteGasMeterWithLimit + // 2. Setup an empty KV gas config for gas to be calculated by opcodes + // and not kvstore actions + // 3. Setup an empty transient KV gas config for transient gas to be + // calculated by opcodes + ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + res, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig) + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + var result interface{} + result, err = tracer.GetResult() + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + return &result, txConfig.LogIndex + uint(len(res.Logs)), nil } // GetProposerAddress returns current block proposer's address when provided proposer address is empty. diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 4515c06be..79b0aad8a 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -18,7 +18,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/evm" ) @@ -90,11 +90,7 @@ func (k *Keeper) GetEvmGasBalance(ctx sdk.Context, addr gethcommon.Address) *big } func (k Keeper) EthChainID(ctx sdk.Context) *big.Int { - ethChainID, err := eth.ParseEthChainID(ctx.ChainID()) - if err != nil { - panic(err) - } - return ethChainID + return appconst.GetEthChainID(ctx.ChainID()) } // AddToBlockGasUsed accumulate gas used by each eth msgs included in current @@ -125,6 +121,14 @@ func (k Keeper) GetBaseFee( return big.NewInt(0) } +func (k Keeper) GetBaseFeeNoCfg( + ctx sdk.Context, +) *big.Int { + ethChainId := k.EthChainID(ctx) + ethCfg := k.GetParams(ctx).ChainConfig.EthereumConfig(ethChainId) + return k.GetBaseFee(ctx, ethCfg) +} + // Logger returns a module-specific logger. func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", evm.ModuleName) diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index ab679506c..caa757d22 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -3,11 +3,7 @@ package keeper_test import ( "testing" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" - - "github.com/NibiruChain/nibiru/x/common" - "github.com/NibiruChain/nibiru/x/common/testutil/testapp" ) type KeeperSuite struct { @@ -19,22 +15,3 @@ func TestKeeperSuite(t *testing.T) { s := new(KeeperSuite) suite.Run(t, s) } - -func (s *KeeperSuite) TestQuerier() { - chain, ctx := testapp.NewNibiruTestAppAndContext() - goCtx := sdk.WrapSDKContext(ctx) - k := chain.EvmKeeper - for _, testCase := range []func() error{ - func() error { - _, err := k.TraceTx(goCtx, nil) - return err - }, - func() error { - _, err := k.TraceBlock(goCtx, nil) - return err - }, - } { - err := testCase() - s.Require().ErrorContains(err, common.ErrNotImplemented().Error()) - } -} diff --git a/x/evm/keeper/msg_ethereum_tx_test.go b/x/evm/keeper/msg_ethereum_tx_test.go index 102157500..ee8682f38 100644 --- a/x/evm/keeper/msg_ethereum_tx_test.go +++ b/x/evm/keeper/msg_ethereum_tx_test.go @@ -2,8 +2,10 @@ package keeper_test import ( "math/big" + "strconv" "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" gethparams "github.com/ethereum/go-ethereum/params" "github.com/NibiruChain/nibiru/x/evm/evmtest" @@ -76,52 +78,55 @@ func (s *KeeperSuite) TestMsgEthereumTx_SimpleTransfer() { scenario func() }{ { - name: "happy: deploy contract, sufficient gas limit", + name: "happy: AccessListTx", scenario: func() { deps := evmtest.NewTestDeps() ethAcc := deps.Sender - s.T().Log("create eth tx msg, increase gas limit") - gasLimit := new(big.Int).SetUint64( - gethparams.TxGasContractCreation + 100_000, + s.T().Log("create eth tx msg") + var innerTxData []byte = nil + var accessList gethcore.AccessList = nil + ethTxMsg, err := evmtest.NewEthTxMsgFromTxData( + &deps, + gethcore.AccessListTxType, + innerTxData, + deps.StateDB().GetNonce(ethAcc.EthAddr), + accessList, ) - args := evmtest.ArgsCreateContract{ - EthAcc: ethAcc, - EthChainIDInt: deps.K.EthChainID(deps.Ctx), - GasPrice: big.NewInt(1), - Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), - GasLimit: gasLimit, - } - ethTxMsg, err := evmtest.CreateContractTxMsg(args) s.NoError(err) - s.Require().NoError(ethTxMsg.ValidateBasic()) - s.Equal(ethTxMsg.GetGas(), gasLimit.Uint64()) resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) - s.Require().NoError(err, "resp: %s\nblock header: %s", resp, deps.Ctx.BlockHeader().ProposerAddress) + s.Require().NoError(err) + + gasUsed := strconv.FormatUint(resp.GasUsed, 10) + wantGasUsed := strconv.FormatUint(gethparams.TxGas, 10) + s.Equal(gasUsed, wantGasUsed) }, }, { - name: "sad: deploy contract, exceed gas limit", + name: "happy: LegacyTx", scenario: func() { deps := evmtest.NewTestDeps() ethAcc := deps.Sender - s.T().Log("create eth tx msg, default create contract gas") - gasLimit := gethparams.TxGasContractCreation - args := evmtest.ArgsCreateContract{ - EthAcc: ethAcc, - EthChainIDInt: deps.K.EthChainID(deps.Ctx), - GasPrice: big.NewInt(1), - Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), - } - ethTxMsg, err := evmtest.CreateContractTxMsg(args) + s.T().Log("create eth tx msg") + var innerTxData []byte = nil + var accessList gethcore.AccessList = nil + ethTxMsg, err := evmtest.NewEthTxMsgFromTxData( + &deps, + gethcore.LegacyTxType, + innerTxData, + deps.StateDB().GetNonce(ethAcc.EthAddr), + accessList, + ) s.NoError(err) - s.Require().NoError(ethTxMsg.ValidateBasic()) - s.Equal(ethTxMsg.GetGas(), gasLimit) resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) - s.Require().ErrorContains(err, core.ErrIntrinsicGas.Error(), "resp: %s\nblock header: %s", resp, deps.Ctx.BlockHeader().ProposerAddress) + s.Require().NoError(err) + + gasUsed := strconv.FormatUint(resp.GasUsed, 10) + wantGasUsed := strconv.FormatUint(gethparams.TxGas, 10) + s.Equal(gasUsed, wantGasUsed) }, }, } diff --git a/x/evm/msg.go b/x/evm/msg.go index ba8a9e830..d67ba6a97 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -449,3 +449,26 @@ func DecodeTxResponse(in []byte) (*MsgEthereumTxResponse, error) { } var EmptyCodeHash = crypto.Keccak256(nil) + +// BinSearch executes the binary search and hone in on an executable gas limit +func BinSearch( + lo, hi uint64, executable func(uint64) (bool, *MsgEthereumTxResponse, error), +) (uint64, error) { + for lo+1 < hi { + mid := (hi + lo) / 2 + failed, _, err := executable(mid) + // If this errors, there was a consensus error, and the provided message + // call or tx will never be accepted, regardless of how high we set the + // gas limit. + // Return the error directly, don't struggle any more. + if err != nil { + return 0, err + } + if failed { + lo = mid + } else { + hi = mid + } + } + return hi, nil +} diff --git a/x/evm/query.go b/x/evm/query.go index 79235797d..f6d9cfde6 100644 --- a/x/evm/query.go +++ b/x/evm/query.go @@ -31,7 +31,7 @@ func (m QueryTraceBlockRequest) UnpackInterfaces(unpacker codectypes.AnyUnpacker func (req *QueryEthAccountRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { return status.Error( @@ -43,7 +43,7 @@ func (req *QueryEthAccountRequest) Validate() error { func (req *QueryNibiruAccountRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -72,7 +72,7 @@ func (req *QueryValidatorAccountRequest) Validate() ( func (req *QueryBalanceRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -86,7 +86,7 @@ func (req *QueryBalanceRequest) Validate() error { func (req *QueryStorageRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { return status.Error( @@ -99,7 +99,7 @@ func (req *QueryStorageRequest) Validate() error { func (req *QueryCodeRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -113,14 +113,14 @@ func (req *QueryCodeRequest) Validate() error { func (req *EthCallRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } return nil } func (req *QueryTraceTxRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if req.TraceConfig != nil && req.TraceConfig.Limit < 0 { @@ -131,7 +131,7 @@ func (req *QueryTraceTxRequest) Validate() error { func (req *QueryTraceBlockRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if req.TraceConfig != nil && req.TraceConfig.Limit < 0 { diff --git a/x/evm/query_test.go b/x/evm/query_test.go index f32a9704b..7c6e54992 100644 --- a/x/evm/query_test.go +++ b/x/evm/query_test.go @@ -53,6 +53,6 @@ func (s *TestSuite) TestNilQueries() { }, } { err := testCase() - s.Require().ErrorContains(err, common.ErrNilGrpcMsg().Error()) + s.Require().ErrorContains(err, common.ErrNilGrpcMsg.Error()) } } diff --git a/x/tokenfactory/keeper/msg_server.go b/x/tokenfactory/keeper/msg_server.go index 5a56b5c50..0b82ddec7 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -13,7 +13,7 @@ import ( var _ types.MsgServer = (*Keeper)(nil) -var errNilMsg error = common.ErrNilGrpcMsg() +var errNilMsg error = common.ErrNilGrpcMsg func (k Keeper) CreateDenom( goCtx context.Context, txMsg *types.MsgCreateDenom,