From ccbaf1fe050de9ca96db287b1b6b3445a4c7a18b Mon Sep 17 00:00:00 2001 From: Freddy Caceres Date: Mon, 8 Aug 2022 04:17:10 -0400 Subject: [PATCH] imp(tests): prune node integration tests (#1212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * first pass * extra comment * fixed pruned node tests. Fix getBalance on pruned. Fix BaseFee on pruned. * fix tests execution * check logs on tests * address pr comments * address comments * Update rpc/namespaces/ethereum/eth/api.go * update error msg check * fix lint * fix linter * fix linter * fix py lint * test lint * fix lint * pin golangcli version * pin golanci version * pin lint to version 0.48 * fix linter * fix last linter last file Co-authored-by: ramacarlucho Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> --- .github/workflows/lint.yml | 4 +- app/ante/doc.go | 3 +- app/export.go | 2 +- app/simulation_test.go | 2 +- client/keys/add.go | 14 +- cmd/config/config.go | 2 +- rpc/backend/backend.go | 2 +- rpc/namespaces/ethereum/debug/api.go | 2 +- rpc/namespaces/ethereum/eth/api.go | 7 +- .../ethereum/eth/filters/filters.go | 2 +- rpc/types/query_client.go | 6 +- tests/integration_tests/.isort.cfg | 9 ++ .../configs/pruned_node.jsonnet | 12 ++ .../{TestERC20.sol => TestERC20A.sol} | 0 tests/integration_tests/test_pruned_node.py | 142 ++++++++++++++++++ tests/integration_tests/utils.py | 57 +++++++ x/evm/keeper/keeper.go | 9 +- x/evm/keeper/state_transition.go | 12 +- x/evm/statedb/statedb.go | 4 +- x/evm/types/logs.go | 4 +- x/evm/types/tx_data.go | 5 +- x/feemarket/types/keys.go | 2 +- 22 files changed, 268 insertions(+), 34 deletions(-) create mode 100644 tests/integration_tests/.isort.cfg create mode 100644 tests/integration_tests/configs/pruned_node.jsonnet rename tests/integration_tests/contracts/contracts/{TestERC20.sol => TestERC20A.sol} (100%) create mode 100644 tests/integration_tests/test_pruned_node.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 60f94d2e..2fc36d89 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: go.sum - uses: golangci/golangci-lint-action@v3 with: - version: latest + version: v1.48.0 args: --timeout 10m github-token: ${{ secrets.github_token }} # Check only if there are differences in the source code @@ -59,7 +59,7 @@ jobs: PATTERNS: | **/**.py - run: | - nix-shell -I nixpkgs=./nix -p test-env --run "make lint-py" + nix-shell -I nixpkgs=./nix -p test-env --run "make lint-py" if: env.GIT_DIFF gomod2nix: name: Check gomod2nix.toml file is up to date diff --git a/app/ante/doc.go b/app/ante/doc.go index 73b56f74..d57d8f32 100644 --- a/app/ante/doc.go +++ b/app/ante/doc.go @@ -1,4 +1,5 @@ -/*Package ante defines the SDK auth module's AnteHandler as well as an internal +/* +Package ante defines the SDK auth module's AnteHandler as well as an internal AnteHandler for an Ethereum transaction (i.e MsgEthereumTx). During CheckTx, the transaction is passed through a series of diff --git a/app/export.go b/app/export.go index ea52f248..4016e2aa 100644 --- a/app/export.go +++ b/app/export.go @@ -62,7 +62,7 @@ func (app *EthermintApp) ExportAppStateAndValidators( // prepare for fresh start at zero height // NOTE zero height genesis is a temporary feature which will be deprecated -// in favor of export at a block height +// in favor of export at a block height func (app *EthermintApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []string) error { applyAllowedAddrs := false diff --git a/app/simulation_test.go b/app/simulation_test.go index 16a54af9..8a9731fe 100644 --- a/app/simulation_test.go +++ b/app/simulation_test.go @@ -154,7 +154,7 @@ func TestAppImportExport(t *testing.T) { fmt.Printf("importing genesis...\n") - // nolint: dogsled + //nolint: dogsled _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") require.NoError(t, err, "simulation setup failed") diff --git a/client/keys/add.go b/client/keys/add.go index 92605e7a..94104096 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -38,14 +38,16 @@ const ( mnemonicEntropySize = 256 ) -/* RunAddCmd +/* +RunAddCmd input - - bip39 mnemonic - - bip39 passphrase - - bip44 path - - local encryption password + - bip39 mnemonic + - bip39 passphrase + - bip44 path + - local encryption password + output - - armor encrypted private key (saved to file) + - armor encrypted private key (saved to file) */ func RunAddCmd(ctx client.Context, cmd *cobra.Command, args []string, inBuf *bufio.Reader) error { var err error diff --git a/cmd/config/config.go b/cmd/config/config.go index 8fcce595..bdd48757 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -40,7 +40,7 @@ func SetBech32Prefixes(config *sdk.Config) { func SetBip44CoinType(config *sdk.Config) { config.SetCoinType(ethermint.Bip44CoinType) config.SetPurpose(sdk.Purpose) // Shared - config.SetFullFundraiserPath(ethermint.BIP44HDPath) // nolint: staticcheck + config.SetFullFundraiserPath(ethermint.BIP44HDPath) //nolint: staticcheck } // RegisterDenoms registers the base and display denominations to the SDK. diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 550435c8..a3371f30 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -22,7 +22,7 @@ import ( ) // BackendI implements the Cosmos and EVM backend. -type BackendI interface { // nolint: revive +type BackendI interface { //nolint: revive CosmosBackend EVMBackend } diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index f4b91d9d..c46493f6 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -287,7 +287,7 @@ func (a *API) BlockProfile(file string, nsec uint) error { // CpuProfile turns on CPU profiling for nsec seconds and writes // profile data to file. -func (a *API) CpuProfile(file string, nsec uint) error { // nolint: golint, stylecheck, revive +func (a *API) 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 diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 92439932..444e6564 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -128,7 +128,7 @@ func (e *PublicAPI) ProtocolVersion() hexutil.Uint { } // ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config. -func (e *PublicAPI) ChainId() (*hexutil.Big, error) { // nolint +func (e *PublicAPI) ChainId() (*hexutil.Big, error) { //nolint e.logger.Debug("eth_chainId") // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config bn, err := e.backend.BlockNumber() @@ -294,6 +294,11 @@ func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.Bl 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 } diff --git a/rpc/namespaces/ethereum/eth/filters/filters.go b/rpc/namespaces/ethereum/eth/filters/filters.go index 63a6d82b..e01a0c04 100644 --- a/rpc/namespaces/ethereum/eth/filters/filters.go +++ b/rpc/namespaces/ethereum/eth/filters/filters.go @@ -48,7 +48,7 @@ func NewRangeFilter(logger log.Logger, backend Backend, begin, end int64, addres // 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 + var filtersBz [][][]byte //nolint: prealloc if len(addresses) > 0 { filter := make([][]byte, len(addresses)) for i, address := range addresses { diff --git a/rpc/types/query_client.go b/rpc/types/query_client.go index 6268e5f7..e0fa09f7 100644 --- a/rpc/types/query_client.go +++ b/rpc/types/query_client.go @@ -15,9 +15,9 @@ import ( ) // QueryClient defines a gRPC Client used for: -// - Transaction simulation -// - EVM module queries -// - Fee market module queries +// - Transaction simulation +// - EVM module queries +// - Fee market module queries type QueryClient struct { tx.ServiceClient evmtypes.QueryClient diff --git a/tests/integration_tests/.isort.cfg b/tests/integration_tests/.isort.cfg new file mode 100644 index 00000000..abc4929a --- /dev/null +++ b/tests/integration_tests/.isort.cfg @@ -0,0 +1,9 @@ +[settings] +# compatible with black +# https://black.readthedocs.io/en/stable/the_black_code_style.html +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 88 diff --git a/tests/integration_tests/configs/pruned_node.jsonnet b/tests/integration_tests/configs/pruned_node.jsonnet new file mode 100644 index 00000000..ffb6a830 --- /dev/null +++ b/tests/integration_tests/configs/pruned_node.jsonnet @@ -0,0 +1,12 @@ +local config = import 'default.jsonnet'; + +config { + 'ethermint_9000-1'+: { + 'app-config'+: { + pruning: 'everything', + 'state-sync'+: { + 'snapshot-interval': 0, + }, + }, + }, +} diff --git a/tests/integration_tests/contracts/contracts/TestERC20.sol b/tests/integration_tests/contracts/contracts/TestERC20A.sol similarity index 100% rename from tests/integration_tests/contracts/contracts/TestERC20.sol rename to tests/integration_tests/contracts/contracts/TestERC20A.sol diff --git a/tests/integration_tests/test_pruned_node.py b/tests/integration_tests/test_pruned_node.py new file mode 100644 index 00000000..09206c4c --- /dev/null +++ b/tests/integration_tests/test_pruned_node.py @@ -0,0 +1,142 @@ +from pathlib import Path + +import pytest +from eth_bloom import BloomFilter +from eth_utils import abi, big_endian_to_int +from hexbytes import HexBytes +from web3.datastructures import AttributeDict + +from .network import setup_custom_ethermint +from .utils import ( + ADDRS, + CONTRACTS, + KEYS, + deploy_contract, + sign_transaction, + w3_wait_for_new_blocks, +) + + +@pytest.fixture(scope="module") +def pruned(request, tmp_path_factory): + """start-cronos + params: enable_auto_deployment + """ + yield from setup_custom_ethermint( + tmp_path_factory.mktemp("pruned"), + 26900, + Path(__file__).parent / "configs/pruned_node.jsonnet", + ) + + +def test_pruned_node(pruned): + """ + test basic json-rpc apis works in pruned node + """ + w3 = pruned.w3 + erc20 = deploy_contract( + w3, + CONTRACTS["TestERC20A"], + key=KEYS["validator"], + ) + tx = erc20.functions.transfer(ADDRS["community"], 10).buildTransaction( + {"from": ADDRS["validator"]} + ) + signed = sign_transaction(w3, tx, KEYS["validator"]) + txhash = w3.eth.send_raw_transaction(signed.rawTransaction) + print("wait for prunning happens") + w3_wait_for_new_blocks(w3, 10) + + tx_receipt = w3.eth.get_transaction_receipt(txhash.hex()) + assert len(tx_receipt.logs) == 1 + expect_log = { + "address": erc20.address, + "topics": [ + HexBytes( + abi.event_signature_to_log_topic("Transfer(address,address,uint256)") + ), + HexBytes(b"\x00" * 12 + HexBytes(ADDRS["validator"])), + HexBytes(b"\x00" * 12 + HexBytes(ADDRS["community"])), + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000000a", + "transactionIndex": 0, + "logIndex": 0, + "removed": False, + } + assert expect_log.items() <= tx_receipt.logs[0].items() + + # check get_balance and eth_call don't work on pruned state + # we need to check error message here. + # `get_balance` returns unmarshallJson and thats not what it should + res = w3.eth.get_balance(ADDRS["validator"], "latest") + assert res > 0 + + pruned_res = pruned.w3.provider.make_request( + "eth_getBalance", [ADDRS["validator"], hex(tx_receipt.blockNumber)] + ) + assert "error" in pruned_res + assert ( + pruned_res["error"]["message"] == "couldn't fetch balance. Node state is pruned" + ) + + with pytest.raises(Exception): + erc20.caller(block_identifier=tx_receipt.blockNumber).balanceOf( + ADDRS["validator"] + ) + + # check block bloom + block = w3.eth.get_block(tx_receipt.blockNumber) + + assert "baseFeePerGas" in block + assert block.miner == "0x0000000000000000000000000000000000000000" + bloom = BloomFilter(big_endian_to_int(block.logsBloom)) + assert HexBytes(erc20.address) in bloom + for topic in expect_log["topics"]: + assert topic in bloom + + tx1 = w3.eth.get_transaction(txhash) + tx2 = w3.eth.get_transaction_by_block( + tx_receipt.blockNumber, tx_receipt.transactionIndex + ) + exp_tx = AttributeDict( + { + "from": "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2", + "gas": 51542, + "input": ( + "0xa9059cbb000000000000000000000000378c50d9264c63f3f92b806d4ee56e" + "9d86ffb3ec000000000000000000000000000000000000000000000000000000" + "000000000a" + ), + "nonce": 2, + "to": erc20.address, + "transactionIndex": 0, + "value": 0, + "type": "0x2", + "accessList": [], + "chainId": "0x2328", + } + ) + assert tx1 == tx2 + for name in exp_tx.keys(): + assert tx1[name] == tx2[name] == exp_tx[name] + + logs = w3.eth.get_logs( + { + "fromBlock": tx_receipt.blockNumber, + "toBlock": tx_receipt.blockNumber, + } + )[0] + assert ( + "address" in logs + and logs["address"] == "0x68542BD12B41F5D51D6282Ec7D91D7d0D78E4503" + ) + assert "topics" in logs and len(logs["topics"]) == 3 + assert logs["topics"][0] == HexBytes( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + assert logs["topics"][1] == HexBytes( + "0x00000000000000000000000057f96e6b86cdefdb3d412547816a82e3e0ebf9d2" + ) + assert logs["topics"][2] == HexBytes( + "0x000000000000000000000000378c50d9264c63f3f92b806d4ee56e9d86ffb3ec" + ) diff --git a/tests/integration_tests/utils.py b/tests/integration_tests/utils.py index 3074628d..d073d189 100644 --- a/tests/integration_tests/utils.py +++ b/tests/integration_tests/utils.py @@ -1,10 +1,44 @@ +import json import os import socket import time +from pathlib import Path +from dotenv import load_dotenv from eth_account import Account from web3._utils.transactions import fill_nonce, fill_transaction_defaults +load_dotenv(Path(__file__).parent.parent.parent / "scripts/.env") +Account.enable_unaudited_hdwallet_features() +ACCOUNTS = { + "validator": Account.from_mnemonic(os.getenv("VALIDATOR1_MNEMONIC")), + "community": Account.from_mnemonic(os.getenv("COMMUNITY_MNEMONIC")), + "signer1": Account.from_mnemonic(os.getenv("SIGNER1_MNEMONIC")), + "signer2": Account.from_mnemonic(os.getenv("SIGNER2_MNEMONIC")), +} +KEYS = {name: account.key for name, account in ACCOUNTS.items()} +ADDRS = {name: account.address for name, account in ACCOUNTS.items()} +ETHERMINT_ADDRESS_PREFIX = "ethm" +TEST_CONTRACTS = { + "TestERC20A": "TestERC20A.sol", +} + + +def contract_path(name, filename): + return ( + Path(__file__).parent + / "contracts/artifacts/contracts/" + / filename + / (name + ".json") + ) + + +CONTRACTS = { + **{ + name: contract_path(name, filename) for name, filename in TEST_CONTRACTS.items() + }, +} + Account.enable_unaudited_hdwallet_features() ACCOUNTS = { @@ -32,6 +66,29 @@ def wait_for_port(port, host="127.0.0.1", timeout=40.0): ) from ex +def w3_wait_for_new_blocks(w3, n): + begin_height = w3.eth.block_number + while True: + time.sleep(0.5) + cur_height = w3.eth.block_number + if cur_height - begin_height >= n: + break + + +def deploy_contract(w3, jsonfile, args=(), key=KEYS["validator"]): + """ + deploy contract and return the deployed contract instance + """ + acct = Account.from_key(key) + info = json.loads(jsonfile.read_text()) + contract = w3.eth.contract(abi=info["abi"], bytecode=info["bytecode"]) + tx = contract.constructor(*args).buildTransaction({"from": acct.address}) + txreceipt = send_transaction(w3, tx, key) + assert txreceipt.status == 1 + address = txreceipt.contractAddress + return w3.eth.contract(address=address, abi=info["abi"]) + + def fill_defaults(w3, tx): return fill_nonce(w3, fill_transaction_defaults(w3, tx)) diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 1ebb77b2..675b9588 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -283,8 +283,13 @@ func (k *Keeper) GetNonce(ctx sdk.Context, addr common.Address) uint64 { // GetBalance load account's balance of gas token func (k *Keeper) GetBalance(ctx sdk.Context, addr common.Address) *big.Int { cosmosAddr := sdk.AccAddress(addr.Bytes()) - params := k.GetParams(ctx) - coin := k.bankKeeper.GetBalance(ctx, cosmosAddr, params.EvmDenom) + evmDenom := "" + k.paramSpace.GetIfExists(ctx, types.ParamStoreKeyEVMDenom, &evmDenom) + // if node is pruned, params is empty. Return invalid value + if evmDenom == "" { + return big.NewInt(-1) + } + coin := k.bankKeeper.GetBalance(ctx, cosmosAddr, evmDenom) return coin.Amount.BigInt() } diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 60dd5b73..fcaceee9 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -178,7 +178,7 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { // ApplyTransaction runs and attempts to perform a state transition with the given transaction (i.e Message), that will // only be persisted (committed) to the underlying KVStore if the transaction does not fail. // -// Gas tracking +// # Gas tracking // // Ethereum consumes gas according to the EVM opcodes instead of general reads and writes to store. Because of this, the // state transition needs to ignore the SDK gas consumption mechanism defined by the GasKVStore and instead consume the @@ -309,18 +309,18 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t // If the message fails, the VM execution error with the reason will be returned to the client // and the transaction won't be committed to the store. // -// Reverted state +// # Reverted state // // The snapshot and rollback are supported by the `statedb.StateDB`. // -// Different Callers +// # Different Callers // // It's called in three scenarios: // 1. `ApplyTransaction`, in the transaction processing flow. // 2. `EthCall/EthEstimateGas` grpc query handler. // 3. Called by other native modules directly. // -// Prechecks and Preprocessing +// # Prechecks and Preprocessing // // All relevant state transition prechecks for the MsgEthereumTx are performed on the AnteHandler, // prior to running the transaction against the state. The prechecks run are the following: @@ -336,11 +336,11 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t // // 1. set up the initial access list (iff fork > Berlin) // -// Tracer parameter +// # Tracer parameter // // It should be a `vm.Tracer` object or nil, if pass `nil`, it'll create a default one based on keeper options. // -// Commit parameter +// # Commit parameter // // If commit is true, the `StateDB` will be committed, otherwise discarded. func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool, cfg *types.EVMConfig, txConfig statedb.TxConfig) (*types.MsgEthereumTxResponse, error) { diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index f8c892d2..e9b07f8a 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -250,8 +250,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) // CreateAccount is called during the EVM CREATE operation. The situation might arise that // a contract does the following: // -// 1. sends funds to sha(account ++ (nonce + 1)) -// 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) +// 1. sends funds to sha(account ++ (nonce + 1)) +// 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) // // Carrying over the balance ensures that Ether doesn't disappear. func (s *StateDB) CreateAccount(addr common.Address) { diff --git a/x/evm/types/logs.go b/x/evm/types/logs.go index b21bece6..f6ac5938 100644 --- a/x/evm/types/logs.go +++ b/x/evm/types/logs.go @@ -89,7 +89,7 @@ func (log *Log) ToEthereum() *ethtypes.Log { } func NewLogsFromEth(ethlogs []*ethtypes.Log) []*Log { - var logs []*Log // nolint: prealloc + var logs []*Log //nolint: prealloc for _, ethlog := range ethlogs { logs = append(logs, NewLogFromEth(ethlog)) } @@ -99,7 +99,7 @@ func NewLogsFromEth(ethlogs []*ethtypes.Log) []*Log { // LogsToEthereum casts the Ethermint Logs to a slice of Ethereum Logs. func LogsToEthereum(logs []*Log) []*ethtypes.Log { - var ethLogs []*ethtypes.Log // nolint: prealloc + var ethLogs []*ethtypes.Log //nolint: prealloc for i := range logs { ethLogs = append(ethLogs, logs[i].ToEthereum()) } diff --git a/x/evm/types/tx_data.go b/x/evm/types/tx_data.go index aa3b13c9..150d509c 100644 --- a/x/evm/types/tx_data.go +++ b/x/evm/types/tx_data.go @@ -71,8 +71,9 @@ func NewTxDataFromTx(tx *ethtypes.Transaction) (TxData, error) { // // CONTRACT: v value is either: // -// - {0,1} + CHAIN_ID * 2 + 35, if EIP155 is used -// - {0,1} + 27, otherwise +// - {0,1} + CHAIN_ID * 2 + 35, if EIP155 is used +// - {0,1} + 27, otherwise +// // Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md func DeriveChainID(v *big.Int) *big.Int { if v == nil || v.Sign() < 1 { diff --git a/x/feemarket/types/keys.go b/x/feemarket/types/keys.go index 0f355234..9e552601 100644 --- a/x/feemarket/types/keys.go +++ b/x/feemarket/types/keys.go @@ -19,7 +19,7 @@ const ( // prefix bytes for the feemarket persistent store const ( prefixBlockGasWanted = iota + 1 - deprecatedPrefixBaseFee // nolint + deprecatedPrefixBaseFee //nolint ) const (