From 14f8a2c57c5d66cfea4af2f29b00fba48c398560 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Sun, 28 Apr 2024 17:21:18 -0500 Subject: [PATCH] test(eth): more tests --- x/evm/genesis.go | 42 ++ x/evm/genesis_test.go | 228 ++++++++ x/evm/msg_test.go | 923 +++++++++++++++++++++++++++++++++ x/evm/{storage.go => state.go} | 8 +- x/evm/state_test.go | 92 ++++ 5 files changed, 1289 insertions(+), 4 deletions(-) create mode 100644 x/evm/genesis.go create mode 100644 x/evm/genesis_test.go create mode 100644 x/evm/msg_test.go rename x/evm/{storage.go => state.go} (85%) create mode 100644 x/evm/state_test.go diff --git a/x/evm/genesis.go b/x/evm/genesis.go new file mode 100644 index 000000000..e93c6cedd --- /dev/null +++ b/x/evm/genesis.go @@ -0,0 +1,42 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evm + +import ( + "fmt" + + "github.com/NibiruChain/nibiru/eth" +) + +// Validate performs a basic validation of a GenesisAccount fields. +func (ga GenesisAccount) Validate() error { + if err := eth.ValidateAddress(ga.Address); err != nil { + return err + } + return ga.Storage.Validate() +} + +// DefaultGenesisState sets default evm genesis state with empty accounts and default params and +// chain config values. +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Accounts: []GenesisAccount{}, + Params: DefaultParams(), + } +} + +// Validate performs basic genesis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + seenAccounts := make(map[string]bool) + for _, acc := range gs.Accounts { + if seenAccounts[acc.Address] { + return fmt.Errorf("duplicated genesis account %s", acc.Address) + } + if err := acc.Validate(); err != nil { + return fmt.Errorf("invalid genesis account %s: %w", acc.Address, err) + } + seenAccounts[acc.Address] = true + } + + return gs.Params.Validate() +} diff --git a/x/evm/genesis_test.go b/x/evm/genesis_test.go new file mode 100644 index 000000000..7c1e76a70 --- /dev/null +++ b/x/evm/genesis_test.go @@ -0,0 +1,228 @@ +package evm_test + +import ( + "testing" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/x/evm" +) + +type GenesisSuite struct { + suite.Suite + + address string + hash gethcommon.Hash + code string +} + +func (s *GenesisSuite) SetupTest() { + priv, err := ethsecp256k1.GenerateKey() + s.Require().NoError(err) + + s.address = gethcommon.BytesToAddress(priv.PubKey().Address().Bytes()).String() + s.hash = gethcommon.BytesToHash([]byte("hash")) + s.code = gethcommon.Bytes2Hex([]byte{1, 2, 3}) +} + +func TestGenesisSuite(t *testing.T) { + suite.Run(t, new(GenesisSuite)) +} + +func (s *GenesisSuite) TestValidateGenesisAccount() { + testCases := []struct { + name string + genAcc evm.GenesisAccount + expPass bool + }{ + { + name: "valid genesis account", + genAcc: evm.GenesisAccount{ + Address: s.address, + Code: s.code, + Storage: evm.Storage{ + evm.NewStateFromEthHashes(s.hash, s.hash), + }, + }, + expPass: true, + }, + { + name: "empty account address bytes", + genAcc: evm.GenesisAccount{ + Address: "", + Code: s.code, + Storage: evm.Storage{ + evm.NewStateFromEthHashes(s.hash, s.hash), + }, + }, + expPass: false, + }, + { + name: "empty code bytes", + genAcc: evm.GenesisAccount{ + Address: s.address, + Code: "", + Storage: evm.Storage{ + evm.NewStateFromEthHashes(s.hash, s.hash), + }, + }, + expPass: true, + }, + } + + for _, tc := range testCases { + tc := tc + err := tc.genAcc.Validate() + if tc.expPass { + s.Require().NoError(err, tc.name) + } else { + s.Require().Error(err, tc.name) + } + } +} + +func (s *GenesisSuite) TestValidateGenesis() { + testCases := []struct { + name string + genState *evm.GenesisState + expPass bool + }{ + { + name: "default", + genState: evm.DefaultGenesisState(), + expPass: true, + }, + { + name: "valid genesis", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: s.address, + + Code: s.code, + Storage: evm.Storage{ + {Key: s.hash.String()}, + }, + }, + }, + Params: evm.DefaultParams(), + }, + expPass: true, + }, + { + name: "empty genesis", + genState: &evm.GenesisState{}, + expPass: false, + }, + { + name: "copied genesis", + genState: &evm.GenesisState{ + Accounts: evm.DefaultGenesisState().Accounts, + Params: evm.DefaultGenesisState().Params, + }, + expPass: true, + }, + { + name: "invalid genesis", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: gethcommon.Address{}.String(), + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis account", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: "123456", + + Code: s.code, + Storage: evm.Storage{ + {Key: s.hash.String()}, + }, + }, + }, + Params: evm.DefaultParams(), + }, + expPass: false, + }, + { + name: "duplicated genesis account", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: s.address, + + Code: s.code, + Storage: evm.Storage{ + evm.NewStateFromEthHashes(s.hash, s.hash), + }, + }, + { + Address: s.address, + + Code: s.code, + Storage: evm.Storage{ + evm.NewStateFromEthHashes(s.hash, s.hash), + }, + }, + }, + }, + expPass: false, + }, + { + name: "duplicated tx log", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: s.address, + + Code: s.code, + Storage: evm.Storage{ + {Key: s.hash.String()}, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid tx log", + genState: &evm.GenesisState{ + Accounts: []evm.GenesisAccount{ + { + Address: s.address, + + Code: s.code, + Storage: evm.Storage{ + {Key: s.hash.String()}, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid params", + genState: &evm.GenesisState{ + Params: evm.Params{}, + }, + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.genState.Validate() + if tc.expPass { + s.Require().NoError(err, tc.name) + continue + } + s.Require().Error(err, tc.name) + } +} diff --git a/x/evm/msg_test.go b/x/evm/msg_test.go new file mode 100644 index 000000000..3bb0d33c9 --- /dev/null +++ b/x/evm/msg_test.go @@ -0,0 +1,923 @@ +package evm_test + +import ( + "fmt" + "math" + "math/big" + "reflect" + "strings" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth/encoding" + "github.com/NibiruChain/nibiru/x/common/testutil" + "github.com/NibiruChain/nibiru/x/evm" +) + +type MsgsSuite struct { + suite.Suite + + signer keyring.Signer + from common.Address + to common.Address + chainID *big.Int + hundredBigInt *big.Int + + clientCtx client.Context +} + +func TestMsgsSuite(t *testing.T) { + suite.Run(t, new(MsgsSuite)) +} + +func (s *MsgsSuite) SetupTest() { + from, privFrom := testutil.PrivKeyEth() + + s.signer = testutil.NewSigner(privFrom) + s.from = from + s.to = testutil.NewEthAddr() + s.chainID = big.NewInt(1) + s.hundredBigInt = big.NewInt(100) + + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + s.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) +} + +func (s *MsgsSuite) TestMsgEthereumTx_Constructor() { + evmTx := &evm.EvmTxArgs{ + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + } + msg := evm.NewTx(evmTx) + + // suite.Require().Equal(msg.Data.To, suite.to.Hex()) + s.Require().Equal(msg.Route(), evm.RouterKey) + s.Require().Equal(msg.Type(), evm.TypeMsgEthereumTx) + // suite.Require().NotNil(msg.To()) + s.Require().Equal(msg.GetMsgs(), []sdk.Msg{msg}) + s.Require().Panics(func() { msg.GetSigners() }) + s.Require().Panics(func() { msg.GetSignBytes() }) + + evmTx2 := &evm.EvmTxArgs{ + Nonce: 0, + GasLimit: 100000, + Input: []byte("test"), + } + msg = evm.NewTx(evmTx2) + s.Require().NotNil(msg) +} + +func (s *MsgsSuite) TestMsgEthereumTx_BuildTx() { + evmTx := &evm.EvmTxArgs{ + Nonce: 0, + To: &s.to, + GasLimit: 100000, + GasPrice: big.NewInt(1), + GasFeeCap: big.NewInt(1), + GasTipCap: big.NewInt(0), + Input: []byte("test"), + } + testCases := []struct { + name string + msg *evm.MsgEthereumTx + expError bool + }{ + { + "build tx - pass", + evm.NewTx(evmTx), + false, + }, + { + "build tx - fail: nil data", + evm.NewTx(evmTx), + true, + }, + } + + for _, tc := range testCases { + if strings.Contains(tc.name, "nil data") { + tc.msg.Data = nil + } + + tx, err := tc.msg.BuildTx(s.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) + if tc.expError { + s.Require().Error(err) + } else { + s.Require().NoError(err) + + s.Require().Empty(tx.GetMemo()) + s.Require().Empty(tx.GetTimeoutHeight()) + s.Require().Equal(uint64(100000), tx.GetGas()) + s.Require().Equal(sdk.NewCoins(sdk.NewCoin(evm.DefaultEVMDenom, sdkmath.NewInt(100000))), tx.GetFee()) + } + } +} + +func invalidAddr() string { return "0x0000" } + +func (s *MsgsSuite) TestMsgEthereumTx_ValidateBasic() { + var ( + hundredInt = big.NewInt(100) + validChainID = big.NewInt(9000) + zeroInt = big.NewInt(0) + minusOneInt = big.NewInt(-1) + //nolint:all + exp_2_255 = new(big.Int).Exp(big.NewInt(2), big.NewInt(255), nil) + ) + testCases := []struct { + msg string + to string + amount *big.Int + gasLimit uint64 + gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int + from string + accessList *gethcore.AccessList + chainID *big.Int + expectPass bool + errMsg string + }{ + { + msg: "pass with recipient - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: true, + }, + { + msg: "pass with recipient - AccessList Tx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: true, + }, + { + msg: "pass with recipient - DynamicFee Tx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: hundredInt, + gasTipCap: zeroInt, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: true, + }, + { + msg: "pass contract - Legacy Tx", + to: "", + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: true, + }, + { + msg: "maxInt64 gas limit overflow", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: math.MaxInt64 + 1, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "gas limit must be less than math.MaxInt64", + }, + { + msg: "nil amount - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: nil, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: true, + }, + { + msg: "negative amount - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: minusOneInt, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "amount cannot be negative", + }, + { + msg: "zero gas limit - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 0, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "gas limit must not be zero", + }, + { + msg: "nil gas price - Legacy Tx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: nil, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "gas price cannot be nil", + }, + { + msg: "negative gas price - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: minusOneInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "gas price cannot be negative", + }, + { + msg: "zero gas price - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: true, + }, + { + msg: "invalid from address - Legacy Tx", + to: s.to.Hex(), + from: invalidAddr(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "invalid from address", + }, + { + msg: "out of bound gas fee - Legacy Tx", + to: s.to.Hex(), + from: s.from.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: exp_2_255, + gasFeeCap: nil, + gasTipCap: nil, + chainID: validChainID, + expectPass: false, + errMsg: "out of bound", + }, + { + msg: "nil amount - AccessListTx", + to: s.to.Hex(), + amount: nil, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: true, + }, + { + msg: "negative amount - AccessListTx", + to: s.to.Hex(), + amount: minusOneInt, + gasLimit: 1000, + gasPrice: hundredInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: false, + errMsg: "amount cannot be negative", + }, + { + msg: "zero gas limit - AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 0, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: false, + errMsg: "gas limit must not be zero", + }, + { + msg: "nil gas price - AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: nil, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: false, + errMsg: "cannot be nil: invalid gas price", + }, + { + msg: "negative gas price - AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: minusOneInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: false, + errMsg: "gas price cannot be negative", + }, + { + msg: "zero gas price - AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: true, + }, + { + msg: "invalid from address - AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + from: invalidAddr(), + accessList: &gethcore.AccessList{}, + chainID: validChainID, + expectPass: false, + errMsg: "invalid from address", + }, + { + msg: "chain ID not set on AccessListTx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: nil, + expectPass: false, + errMsg: "chain ID must be present on AccessList txs", + }, + { + msg: "nil tx.Data - AccessList Tx", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + expectPass: false, + errMsg: "failed to unpack tx data", + }, + { + msg: "happy, valid chain ID", + to: s.to.Hex(), + amount: hundredInt, + gasLimit: 1000, + gasPrice: zeroInt, + gasFeeCap: nil, + gasTipCap: nil, + accessList: &gethcore.AccessList{}, + chainID: hundredInt, + expectPass: true, + errMsg: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.msg, func() { + to := common.HexToAddress(tc.to) + evmTx := &evm.EvmTxArgs{ + ChainID: tc.chainID, + Nonce: 1, + To: &to, + Amount: tc.amount, + GasLimit: tc.gasLimit, + GasPrice: tc.gasPrice, + GasFeeCap: tc.gasFeeCap, + Accesses: tc.accessList, + } + tx := evm.NewTx(evmTx) + tx.From = tc.from + + // apply nil assignment here to test ValidateBasic function instead of NewTx + if strings.Contains(tc.msg, "nil tx.Data") { + tx.Data = nil + } + + // for legacy_Tx need to sign tx because the chainID is derived + // from signature + if tc.accessList == nil && tc.from == s.from.Hex() { + ethSigner := gethcore.LatestSignerForChainID(tc.chainID) + err := tx.Sign(ethSigner, s.signer) + s.Require().NoError(err) + } + + err := tx.ValidateBasic() + + if tc.expectPass { + s.Require().NoError(err) + } else { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + } + }) + } +} + +func (s *MsgsSuite) TestMsgEthereumTx_ValidateBasicAdvanced() { + hundredInt := big.NewInt(100) + evmTx := &evm.EvmTxArgs{ + ChainID: hundredInt, + Nonce: 1, + Amount: big.NewInt(10), + GasLimit: 100000, + GasPrice: big.NewInt(150), + GasFeeCap: big.NewInt(200), + } + + testCases := []struct { + msg string + msgBuilder func() *evm.MsgEthereumTx + expectPass bool + }{ + { + "fails - invalid tx hash", + func() *evm.MsgEthereumTx { + msg := evm.NewTx(evmTx) + msg.Hash = "0x00" + return msg + }, + false, + }, + { + "fails - invalid size", + func() *evm.MsgEthereumTx { + msg := evm.NewTx(evmTx) + msg.Size_ = 1 + return msg + }, + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.msg, func() { + err := tc.msgBuilder().ValidateBasic() + if tc.expectPass { + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *MsgsSuite) TestMsgEthereumTx_Sign() { + testCases := []struct { + msg string + txParams *evm.EvmTxArgs + ethSigner gethcore.Signer + malleate func(tx *evm.MsgEthereumTx) + expectPass bool + }{ + { + "pass - EIP2930 signer", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + Accesses: &gethcore.AccessList{}, + }, + gethcore.NewEIP2930Signer(s.chainID), + func(tx *evm.MsgEthereumTx) { tx.From = s.from.Hex() }, + true, + }, + { + "pass - EIP155 signer", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + }, + gethcore.NewEIP155Signer(s.chainID), + func(tx *evm.MsgEthereumTx) { tx.From = s.from.Hex() }, + true, + }, + { + "pass - Homestead signer", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + }, + gethcore.HomesteadSigner{}, + func(tx *evm.MsgEthereumTx) { tx.From = s.from.Hex() }, + true, + }, + { + "pass - Frontier signer", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + }, + gethcore.FrontierSigner{}, + func(tx *evm.MsgEthereumTx) { tx.From = s.from.Hex() }, + true, + }, + { + "no from address ", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + Accesses: &gethcore.AccessList{}, + }, + gethcore.NewEIP2930Signer(s.chainID), + func(tx *evm.MsgEthereumTx) { tx.From = "" }, + false, + }, + { + "from address ≠ signer address", + &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 100000, + Input: []byte("test"), + Accesses: &gethcore.AccessList{}, + }, + gethcore.NewEIP2930Signer(s.chainID), + func(tx *evm.MsgEthereumTx) { tx.From = s.to.Hex() }, + false, + }, + } + + for i, tc := range testCases { + tx := evm.NewTx(tc.txParams) + tc.malleate(tx) + err := tx.Sign(tc.ethSigner, s.signer) + if tc.expectPass { + s.Require().NoError(err, "valid test %d failed: %s", i, tc.msg) + + sender, err := tx.GetSender(s.chainID) + s.Require().NoError(err, tc.msg) + s.Require().Equal(tx.From, sender.Hex(), tc.msg) + } else { + s.Require().Error(err, "invalid test %d passed: %s", i, tc.msg) + } + } +} + +func (s *MsgsSuite) TestMsgEthereumTx_Getters() { + evmTx := &evm.EvmTxArgs{ + ChainID: s.chainID, + Nonce: 0, + To: &s.to, + GasLimit: 50, + GasPrice: s.hundredBigInt, + Accesses: &gethcore.AccessList{}, + } + testCases := []struct { + name string + ethSigner gethcore.Signer + exp *big.Int + }{ + { + "get fee - pass", + + gethcore.NewEIP2930Signer(s.chainID), + big.NewInt(5000), + }, + { + "get fee - fail: nil data", + gethcore.NewEIP2930Signer(s.chainID), + nil, + }, + { + "get effective fee - pass", + + gethcore.NewEIP2930Signer(s.chainID), + big.NewInt(5000), + }, + { + "get effective fee - fail: nil data", + gethcore.NewEIP2930Signer(s.chainID), + nil, + }, + { + "get gas - pass", + gethcore.NewEIP2930Signer(s.chainID), + big.NewInt(50), + }, + { + "get gas - fail: nil data", + gethcore.NewEIP2930Signer(s.chainID), + big.NewInt(0), + }, + } + + var fee, effFee *big.Int + for _, tc := range testCases { + tx := evm.NewTx(evmTx) + if strings.Contains(tc.name, "nil data") { + tx.Data = nil + } + switch { + case strings.Contains(tc.name, "get fee"): + fee = tx.GetFee() + s.Require().Equal(tc.exp, fee) + case strings.Contains(tc.name, "get effective fee"): + effFee = tx.GetEffectiveFee(big.NewInt(0)) + s.Require().Equal(tc.exp, effFee) + case strings.Contains(tc.name, "get gas"): + gas := tx.GetGas() + s.Require().Equal(tc.exp.Uint64(), gas) + } + } +} + +func (s *MsgsSuite) TestFromEthereumTx() { + privkey, _ := ethsecp256k1.GenerateKey() + ethPriv, err := privkey.ToECDSA() + s.Require().NoError(err) + + // 10^80 is more than 256 bits + //nolint:all + exp_10_80 := new(big.Int).Mul(big.NewInt(1), new(big.Int).Exp(big.NewInt(10), big.NewInt(80), nil)) + + testCases := []struct { + msg string + expectPass bool + buildTx func() *gethcore.Transaction + }{ + {"success, normal tx", true, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.AccessListTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: big.NewInt(10), + GasPrice: big.NewInt(1), + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewEIP2930Signer(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + {"success, DynamicFeeTx", true, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.DynamicFeeTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: big.NewInt(10), + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewLondonSigner(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + {"fail, value bigger than 256bits - AccessListTx", false, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.AccessListTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: exp_10_80, + GasPrice: big.NewInt(1), + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewEIP2930Signer(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + {"fail, gas price bigger than 256bits - AccessListTx", false, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.AccessListTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: big.NewInt(1), + GasPrice: exp_10_80, + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewEIP2930Signer(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + {"fail, value bigger than 256bits - LegacyTx", false, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.LegacyTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: exp_10_80, + GasPrice: big.NewInt(1), + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewEIP2930Signer(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + {"fail, gas price bigger than 256bits - LegacyTx", false, func() *gethcore.Transaction { + tx := gethcore.NewTx(&gethcore.LegacyTx{ + Nonce: 0, + Data: nil, + To: &s.to, + Value: big.NewInt(1), + GasPrice: exp_10_80, + Gas: 21000, + }) + tx, err := gethcore.SignTx(tx, gethcore.NewEIP2930Signer(s.chainID), ethPriv) + s.Require().NoError(err) + return tx + }}, + } + + for _, tc := range testCases { + ethTx := tc.buildTx() + tx := &evm.MsgEthereumTx{} + err := tx.FromEthereumTx(ethTx) + if tc.expectPass { + s.Require().NoError(err) + + // round-trip test + s.Require().NoError(assertEqualTxs(tx.AsTransaction(), ethTx)) + } else { + s.Require().Error(err) + } + } +} + +// TestTxEncoding tests serializing/de-serializing to/from rlp and JSON. +// adapted from go-ethereum +func (s *MsgsSuite) TestTxEncoding() { + key, err := crypto.GenerateKey() + if err != nil { + s.T().Fatalf("could not generate key: %v", err) + } + var ( + signer = gethcore.NewEIP2930Signer(common.Big1) + addr = common.HexToAddress("0x0000000000000000000000000000000000000001") + recipient = common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87") + accesses = gethcore.AccessList{{Address: addr, StorageKeys: []common.Hash{{0}}}} + ) + for i := uint64(0); i < 500; i++ { + var txdata gethcore.TxData + switch i % 5 { + case 0: + // Legacy tx. + txdata = &gethcore.LegacyTx{ + Nonce: i, + To: &recipient, + Gas: 1, + GasPrice: big.NewInt(2), + Data: []byte("abcdef"), + } + case 1: + // Legacy tx contract creation. + txdata = &gethcore.LegacyTx{ + Nonce: i, + Gas: 1, + GasPrice: big.NewInt(2), + Data: []byte("abcdef"), + } + case 2: + // Tx with non-zero access list. + txdata = &gethcore.AccessListTx{ + ChainID: big.NewInt(1), + Nonce: i, + To: &recipient, + Gas: 123457, + GasPrice: big.NewInt(10), + AccessList: accesses, + Data: []byte("abcdef"), + } + case 3: + // Tx with empty access list. + txdata = &gethcore.AccessListTx{ + ChainID: big.NewInt(1), + Nonce: i, + To: &recipient, + Gas: 123457, + GasPrice: big.NewInt(10), + Data: []byte("abcdef"), + } + case 4: + // Contract creation with access list. + txdata = &gethcore.AccessListTx{ + ChainID: big.NewInt(1), + Nonce: i, + Gas: 123457, + GasPrice: big.NewInt(10), + AccessList: accesses, + } + } + tx, err := gethcore.SignNewTx(key, signer, txdata) + if err != nil { + s.T().Fatalf("could not sign transaction: %v", err) + } + // RLP + parsedTx, err := encodeDecodeBinary(tx) + if err != nil { + s.T().Fatal(err) + } + err = assertEqualTxs(parsedTx.AsTransaction(), tx) + s.Require().NoError(err) + } +} + +func encodeDecodeBinary(tx *gethcore.Transaction) (*evm.MsgEthereumTx, error) { + data, err := tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("rlp encoding failed: %v", err) + } + parsedTx := &evm.MsgEthereumTx{} + if err := parsedTx.UnmarshalBinary(data); err != nil { + return nil, fmt.Errorf("rlp decoding failed: %v", err) + } + return parsedTx, nil +} + +func assertEqualTxs(orig *gethcore.Transaction, cpy *gethcore.Transaction) error { + // compare nonce, price, gaslimit, recipient, amount, payload, V, R, S + if want, got := orig.Hash(), cpy.Hash(); want != got { + return fmt.Errorf("parsed tx differs from original tx, want %v, got %v", want, got) + } + if want, got := orig.ChainId(), cpy.ChainId(); want.Cmp(got) != 0 { + return fmt.Errorf("invalid chain id, want %d, got %d", want, got) + } + if orig.AccessList() != nil { + if !reflect.DeepEqual(orig.AccessList(), cpy.AccessList()) { + return fmt.Errorf("access list wrong") + } + } + return nil +} diff --git a/x/evm/storage.go b/x/evm/state.go similarity index 85% rename from x/evm/storage.go rename to x/evm/state.go index 228e0db6f..892e260ab 100644 --- a/x/evm/storage.go +++ b/x/evm/state.go @@ -48,8 +48,8 @@ func (s Storage) Copy() Storage { return cpy } -// Validate performs a basic validation of the State fields. -// NOTE: state value can be empty +// Validate performs a basic validation of the State fields. Note that [State] +// can be empty. func (s State) Validate() error { if strings.TrimSpace(s.Key) == "" { return errorsmod.Wrap(ErrInvalidState, "state key hash cannot be blank") @@ -58,8 +58,8 @@ func (s State) Validate() error { return nil } -// NewState creates a new State instance -func NewState(key, value common.Hash) State { +// NewStateFromEthHashes creates a [State] struct from Eth hashes. +func NewStateFromEthHashes(key, value common.Hash) State { return State{ Key: key.String(), Value: value.String(), diff --git a/x/evm/state_test.go b/x/evm/state_test.go new file mode 100644 index 000000000..482ef512a --- /dev/null +++ b/x/evm/state_test.go @@ -0,0 +1,92 @@ +package evm + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" +) + +type SuiteStorage struct { + suite.Suite +} + +func TestSuiteStorage(t *testing.T) { + suite.Run(t, new(SuiteStorage)) +} + +func (s *SuiteStorage) TestStorageString() { + storage := Storage{NewStateFromEthHashes(common.BytesToHash([]byte("key")), common.BytesToHash([]byte("value")))} + str := "key:\"0x00000000000000000000000000000000000000000000000000000000006b6579\" value:\"0x00000000000000000000000000000000000000000000000000000076616c7565\" \n" + s.Equal(str, storage.String()) +} + +func (s *SuiteStorage) TestStorageValidate() { + testCases := []struct { + name string + storage Storage + wantPass bool + }{ + { + name: "valid storage", + storage: Storage{ + NewStateFromEthHashes(common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{1, 2, 3})), + }, + wantPass: true, + }, + { + name: "empty storage key bytes", + storage: Storage{ + {Key: ""}, + }, + wantPass: false, + }, + { + name: "duplicated storage key", + storage: Storage{ + {Key: common.BytesToHash([]byte{1, 2, 3}).String()}, + {Key: common.BytesToHash([]byte{1, 2, 3}).String()}, + }, + wantPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + err := tc.storage.Validate() + if tc.wantPass { + s.NoError(err, tc.name) + } else { + s.Error(err, tc.name) + } + } +} + +func (s *SuiteStorage) TestStorageCopy() { + testCases := []struct { + name string + storage Storage + }{ + { + "single storage", + Storage{ + NewStateFromEthHashes(common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{1, 2, 3})), + }, + }, + { + "empty storage key value bytes", + Storage{ + {Key: common.Hash{}.String(), Value: common.Hash{}.String()}, + }, + }, + { + "empty storage", + Storage{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Require().Equal(tc.storage, tc.storage.Copy(), tc.name) + } +}