From c4a3c0a96e1851944487e12a73acf36f3cf37dcf Mon Sep 17 00:00:00 2001 From: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Date: Mon, 7 Dec 2020 17:09:09 -0300 Subject: [PATCH] evm: implement vm.GetHashFn (#620) * evm: implement vm.GetHashFn * check nil case * test * handle 3 cases * use switch statement * stateDB tests * abci changes * fix LGTM issue * final tests * changelog * remove epoch * update test * clean test * rm epoch --- CHANGELOG.md | 1 + app/export.go | 1 - x/evm/keeper/abci.go | 15 ++- x/evm/keeper/keeper.go | 15 +++ x/evm/keeper/keeper_test.go | 2 +- x/evm/types/journal_test.go | 2 +- x/evm/types/key.go | 10 ++ x/evm/types/state_transition.go | 53 ++++++++++- x/evm/types/state_transition_test.go | 135 +++++++++++++++++++++++++++ x/evm/types/statedb.go | 31 ++++-- x/evm/types/statedb_test.go | 13 ++- x/evm/types/utils.go | 37 ++++++++ 12 files changed, 300 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d39f0b7f..f5877c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes * (evm) [\#621](https://github.com/cosmos/ethermint/issues/621) EVM `GenesisAccount` fields now share the same format as the auth module `Account`. +* (evm) [\#618](https://github.com/cosmos/ethermint/issues/618) Add missing EVM `Context` `GetHash` field that retrieves a the header hash from a given block height. * (app) [\#617](https://github.com/cosmos/ethermint/issues/617) Fix genesis export functionality. ## [v0.3.1] - 2020-11-24 diff --git a/app/export.go b/app/export.go index dfb7d17d..b324e8fa 100644 --- a/app/export.go +++ b/app/export.go @@ -28,7 +28,6 @@ func NewDefaultGenesisState() simapp.GenesisState { func (app *EthermintApp) ExportAppStateAndValidators( forZeroHeight bool, jailWhiteList []string, ) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { - // Creates context with current height and checks txs for ctx to be usable by start of next block ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index 21136fee..896a6657 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -8,6 +8,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/cosmos/ethermint/x/evm/types" ) // BeginBlock sets the block hash -> block height map for the previous block height @@ -29,18 +31,25 @@ func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { // EndBlock updates the accounts and commits state objects to the KV Store, while // deleting the empty ones. It also sets the bloom filers for the request block to -// the store. The EVM end block loginc doesn't update the validator set, thus it returns +// the store. The EVM end block logic doesn't update the validator set, thus it returns // an empty slice. func (k Keeper) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate { // Gas costs are handled within msg handler so costs should be ignored ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + // Set the hash for the current height. + // NOTE: we set the hash here instead of on BeginBlock in order to set the final block prior to + // an upgrade. If we set it on BeginBlock the last block from prior to the upgrade wouldn't be + // included on the store. + hash := types.HashFromContext(ctx) + k.SetHeightHash(ctx, uint64(ctx.BlockHeight()), hash) + // Update account balances before committing other parts of state k.UpdateAccounts(ctx) // Commit state objects to KV store - _, err := k.Commit(ctx, true) - if err != nil { + if _, err := k.Commit(ctx, true); err != nil { + k.Logger(ctx).Error("failed to commit state objects", "error", err, "height", ctx.BlockHeight()) panic(err) } diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 1b49ec3a..8b42d96d 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -88,6 +88,21 @@ func (k Keeper) SetBlockHash(ctx sdk.Context, hash []byte, height int64) { store.Set(hash, bz) } +// ---------------------------------------------------------------------------- +// Epoch Height -> hash mapping functions +// Required by EVM context's GetHashFunc +// ---------------------------------------------------------------------------- + +// GetHeightHash returns the block header hash associated with a given block height and chain epoch number. +func (k Keeper) GetHeightHash(ctx sdk.Context, height uint64) common.Hash { + return k.CommitStateDB.WithContext(ctx).GetHeightHash(height) +} + +// SetHeightHash sets the block header hash associated with a given height. +func (k Keeper) SetHeightHash(ctx sdk.Context, height uint64, hash common.Hash) { + k.CommitStateDB.WithContext(ctx).SetHeightHash(height, hash) +} + // ---------------------------------------------------------------------------- // Block bloom bits mapping functions // Required by Web3 API. diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 09943fe9..b9ff9b3c 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -42,7 +42,7 @@ func (suite *KeeperTestSuite) SetupTest() { checkTx := false suite.app = app.Setup(checkTx) - suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "3", Time: time.Now().UTC()}) + suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "ethermint-3", Time: time.Now().UTC()}) suite.querier = keeper.NewQuerier(suite.app.EvmKeeper) suite.address = ethcmn.HexToAddress(addrHex) diff --git a/x/evm/types/journal_test.go b/x/evm/types/journal_test.go index a07f6a9b..1041ec74 100644 --- a/x/evm/types/journal_test.go +++ b/x/evm/types/journal_test.go @@ -124,7 +124,7 @@ func (suite *JournalTestSuite) setup() { evmSubspace := paramsKeeper.Subspace(types.DefaultParamspace).WithKeyTable(ParamKeyTable()) ak := auth.NewAccountKeeper(cdc, authKey, authSubspace, ethermint.ProtoAccount) - suite.ctx = sdk.NewContext(cms, abci.Header{ChainID: "8"}, false, tmlog.NewNopLogger()) + suite.ctx = sdk.NewContext(cms, abci.Header{ChainID: "ethermint-8"}, false, tmlog.NewNopLogger()) suite.stateDB = NewCommitStateDB(suite.ctx, storeKey, evmSubspace, ak).WithContext(suite.ctx) suite.stateDB.SetParams(DefaultParams()) } diff --git a/x/evm/types/key.go b/x/evm/types/key.go index a5e7d665..6b53d909 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -27,8 +27,18 @@ var ( KeyPrefixCode = []byte{0x04} KeyPrefixStorage = []byte{0x05} KeyPrefixChainConfig = []byte{0x06} + KeyPrefixHeightHash = []byte{0x07} ) +// HeightHashKey returns the key for the given chain epoch and height. +// The key will be composed in the following order: +// key = prefix + bytes(height) +// This ordering facilitates the iteration by height for the EVM GetHashFn +// queries. +func HeightHashKey(height uint64) []byte { + return sdk.Uint64ToBigEndian(height) +} + // BloomKey defines the store key for a block Bloom func BloomKey(height int64) []byte { return sdk.Uint64ToBigEndian(uint64(height)) diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go index 8e0c5acf..3e8ece42 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -47,11 +47,42 @@ type ExecutionResult struct { GasInfo GasInfo } -func (st StateTransition) newEVM(ctx sdk.Context, csdb *CommitStateDB, gasLimit uint64, gasPrice *big.Int, config ChainConfig) *vm.EVM { +// GetHashFn implements vm.GetHashFunc for Ethermint. It handles 3 cases: +// 1. The requested height matches the current height from context (and thus same epoch number) +// 2. The requested height is from an previous height from the same chain epoch +// 3. The requested height is from a height greater than the latest one +func GetHashFn(ctx sdk.Context, csdb *CommitStateDB) vm.GetHashFunc { + return func(height uint64) common.Hash { + switch { + case ctx.BlockHeight() == int64(height): + // Case 1: The requested height matches the one from the context so we can retrieve the header + // hash directly from the context. + return HashFromContext(ctx) + + case ctx.BlockHeight() > int64(height): + // Case 2: if the chain is not the current height we need to retrieve the hash from the store for the + // current chain epoch. This only applies if the current height is greater than the requested height. + return csdb.WithContext(ctx).GetHeightHash(height) + + default: + // Case 3: heights greater than the current one returns an empty hash. + return common.Hash{} + } + } +} + +func (st StateTransition) newEVM( + ctx sdk.Context, + csdb *CommitStateDB, + gasLimit uint64, + gasPrice *big.Int, + config ChainConfig, +) *vm.EVM { // Create context for evm context := vm.Context{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, + GetHash: GetHashFn(ctx, csdb), Origin: st.Sender, Coinbase: common.Address{}, // there's no benefitiary since we're not mining BlockNumber: big.NewInt(ctx.BlockHeight()), @@ -134,7 +165,7 @@ func (st StateTransition) TransitionDb(ctx sdk.Context, config ChainConfig) (*Ex recipientLog = fmt.Sprintf("contract address %s", contractAddress.String()) default: if !params.EnableCall { - return nil, ErrCreateDisabled + return nil, ErrCallDisabled } // Increment the nonce for the next transaction (just for evm state transition) @@ -223,3 +254,21 @@ func (st StateTransition) TransitionDb(ctx sdk.Context, config ChainConfig) (*Ex return executionResult, nil } + +// HashFromContext returns the Ethereum Header hash from the context's Tendermint +// block header. +func HashFromContext(ctx sdk.Context) common.Hash { + // cast the ABCI header to tendermint Header type + tmHeader := AbciHeaderToTendermint(ctx.BlockHeader()) + + // get the Tendermint block hash from the current header + tmBlockHash := tmHeader.Hash() + + // NOTE: if the validator set hash is missing the hash will be returned as nil, + // so we need to check for this case to prevent a panic when calling Bytes() + if tmBlockHash == nil { + return common.Hash{} + } + + return common.BytesToHash(tmBlockHash.Bytes()) +} diff --git a/x/evm/types/state_transition_test.go b/x/evm/types/state_transition_test.go index c6577c98..b9e36463 100644 --- a/x/evm/types/state_transition_test.go +++ b/x/evm/types/state_transition_test.go @@ -3,16 +3,108 @@ package types_test import ( "math/big" + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ethermint/crypto/ethsecp256k1" ethermint "github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/x/evm/types" + "github.com/ethereum/go-ethereum/common" ethcmn "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" ) +func (suite *StateDBTestSuite) TestGetHashFn() { + testCase := []struct { + name string + height uint64 + malleate func() + expEmptyHash bool + }{ + { + "valid hash, case 1", + 1, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-1", + Height: 1, + ValidatorsHash: []byte("val_hash"), + }, + ) + }, + false, + }, + { + "case 1, nil tendermint hash", + 1, + func() {}, + true, + }, + { + "valid hash, case 2", + 1, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-1", + Height: 100, + ValidatorsHash: []byte("val_hash"), + }, + ) + hash := types.HashFromContext(suite.ctx) + suite.stateDB.WithContext(suite.ctx).SetHeightHash(1, hash) + }, + false, + }, + { + "height not found, case 2", + 1, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-1", + Height: 100, + ValidatorsHash: []byte("val_hash"), + }, + ) + }, + true, + }, + { + "empty hash, case 3", + 1000, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-1", + Height: 100, + ValidatorsHash: []byte("val_hash"), + }, + ) + }, + true, + }, + } + + for _, tc := range testCase { + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + tc.malleate() + + hash := types.GetHashFn(suite.ctx, suite.stateDB)(tc.height) + if tc.expEmptyHash { + suite.Require().Equal(common.Hash{}.String(), hash.String()) + } else { + suite.Require().NotEqual(common.Hash{}.String(), hash.String()) + } + }) + } +} + func (suite *StateDBTestSuite) TestTransitionDb() { suite.stateDB.SetNonce(suite.address, 123) @@ -104,9 +196,52 @@ func (suite *StateDBTestSuite) TestTransitionDb() { }, false, }, + { + "call disabled", + func() { + params := types.NewParams(ethermint.AttoPhoton, true, false) + suite.stateDB.SetParams(params) + }, + types.StateTransition{ + AccountNonce: 123, + Price: big.NewInt(10), + GasLimit: 11, + Recipient: &recipient, + Amount: big.NewInt(50), + Payload: []byte("data"), + ChainID: big.NewInt(1), + Csdb: suite.stateDB, + TxHash: ðcmn.Hash{}, + Sender: suite.address, + Simulate: suite.ctx.IsCheckTx(), + }, + false, + }, + { + "create disabled", + func() { + params := types.NewParams(ethermint.AttoPhoton, false, true) + suite.stateDB.SetParams(params) + }, + types.StateTransition{ + AccountNonce: 123, + Price: big.NewInt(10), + GasLimit: 11, + Recipient: nil, + Amount: big.NewInt(50), + Payload: []byte("data"), + ChainID: big.NewInt(1), + Csdb: suite.stateDB, + TxHash: ðcmn.Hash{}, + Sender: suite.address, + Simulate: suite.ctx.IsCheckTx(), + }, + false, + }, { "nil gas price", func() { + suite.stateDB.SetParams(types.DefaultParams()) invalidGas := sdk.DecCoins{ {Denom: ethermint.AttoPhoton}, } diff --git a/x/evm/types/statedb.go b/x/evm/types/statedb.go index 688bb7c5..ac867142 100644 --- a/x/evm/types/statedb.go +++ b/x/evm/types/statedb.go @@ -10,7 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params" - emint "github.com/cosmos/ethermint/types" + ethermint "github.com/cosmos/ethermint/types" ethcmn "github.com/ethereum/go-ethereum/common" ethstate "github.com/ethereum/go-ethereum/core/state" @@ -107,7 +107,7 @@ func NewCommitStateDB( } } -// WithContext returns a Database with an updated sdk context +// WithContext returns a Database with an updated SDK context func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB { csdb.ctx = ctx return csdb @@ -117,6 +117,13 @@ func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB { // Setters // ---------------------------------------------------------------------------- +// SetHeightHash sets the block header hash associated with a given height. +func (csdb *CommitStateDB) SetHeightHash(height uint64, hash ethcmn.Hash) { + store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash) + key := HeightHashKey(height) + store.Set(key, hash.Bytes()) +} + // SetParams sets the evm parameters to the param space. func (csdb *CommitStateDB) SetParams(params Params) { csdb.paramSpace.SetParamSet(csdb.ctx, ¶ms) @@ -286,6 +293,18 @@ func (csdb *CommitStateDB) SlotInAccessList(addr ethcmn.Address, slot ethcmn.Has // Getters // ---------------------------------------------------------------------------- +// GetHeightHash returns the block header hash associated with a given block height and chain epoch number. +func (csdb *CommitStateDB) GetHeightHash(height uint64) ethcmn.Hash { + store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash) + key := HeightHashKey(height) + bz := store.Get(key) + if len(bz) == 0 { + return ethcmn.Hash{} + } + + return ethcmn.BytesToHash(bz) +} + // GetParams returns the total set of evm parameters. func (csdb *CommitStateDB) GetParams() (params Params) { csdb.paramSpace.GetParamSet(csdb.ctx, ¶ms) @@ -680,7 +699,7 @@ func (csdb *CommitStateDB) Reset(_ ethcmn.Hash) error { func (csdb *CommitStateDB) UpdateAccounts() { for _, stateEntry := range csdb.stateObjects { currAcc := csdb.accountKeeper.GetAccount(csdb.ctx, sdk.AccAddress(stateEntry.address.Bytes())) - emintAcc, ok := currAcc.(*emint.EthAccount) + ethermintAcc, ok := currAcc.(*ethermint.EthAccount) if !ok { continue } @@ -688,12 +707,12 @@ func (csdb *CommitStateDB) UpdateAccounts() { evmDenom := csdb.GetParams().EvmDenom balance := sdk.Coin{ Denom: evmDenom, - Amount: emintAcc.GetCoins().AmountOf(evmDenom), + Amount: ethermintAcc.GetCoins().AmountOf(evmDenom), } if stateEntry.stateObject.Balance() != balance.Amount.BigInt() && balance.IsValid() || - stateEntry.stateObject.Nonce() != emintAcc.GetSequence() { - stateEntry.stateObject.account = emintAcc + stateEntry.stateObject.Nonce() != ethermintAcc.GetSequence() { + stateEntry.stateObject.account = ethermintAcc } } } diff --git a/x/evm/types/statedb_test.go b/x/evm/types/statedb_test.go index b0ccdeaa..9c9eaadd 100644 --- a/x/evm/types/statedb_test.go +++ b/x/evm/types/statedb_test.go @@ -40,7 +40,7 @@ func (suite *StateDBTestSuite) SetupTest() { checkTx := false suite.app = app.Setup(checkTx) - suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) + suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "ethermint-1"}) suite.stateDB = suite.app.EvmKeeper.CommitStateDB.WithContext(suite.ctx) privkey, err := ethsecp256k1.GenerateKey() @@ -67,6 +67,17 @@ func (suite *StateDBTestSuite) TestParams() { suite.Require().Equal(newParams, params) } +func (suite *StateDBTestSuite) TestGetHeightHash() { + hash := suite.stateDB.GetHeightHash(0) + suite.Require().Equal(ethcmn.Hash{}.String(), hash.String()) + + expHash := ethcmn.BytesToHash([]byte("hash")) + suite.stateDB.SetHeightHash(10, expHash) + + hash = suite.stateDB.GetHeightHash(10) + suite.Require().Equal(expHash.String(), hash.String()) +} + func (suite *StateDBTestSuite) TestBloomFilter() { // Prepare db for logs tHash := ethcmn.BytesToHash([]byte{0x1}) diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 871e68b3..8f06eed3 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -8,6 +8,10 @@ import ( "github.com/pkg/errors" "golang.org/x/crypto/sha3" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + "github.com/tendermint/tendermint/version" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -152,3 +156,36 @@ func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, erro return addr, nil } + +// AbciHeaderToTendermint is a util function to parse a tendermint ABCI Header to +// tendermint types Header. +func AbciHeaderToTendermint(header abci.Header) tmtypes.Header { + return tmtypes.Header{ + Version: version.Consensus{ + Block: version.Protocol(header.Version.Block), + App: version.Protocol(header.Version.App), + }, + ChainID: header.ChainID, + Height: header.Height, + Time: header.Time, + + LastBlockID: tmtypes.BlockID{ + Hash: header.LastBlockId.Hash, + PartsHeader: tmtypes.PartSetHeader{ + Total: int(header.LastBlockId.PartsHeader.Total), + Hash: header.LastBlockId.PartsHeader.Hash, + }, + }, + LastCommitHash: header.LastCommitHash, + DataHash: header.DataHash, + + ValidatorsHash: header.ValidatorsHash, + NextValidatorsHash: header.NextValidatorsHash, + ConsensusHash: header.ConsensusHash, + AppHash: header.AppHash, + LastResultsHash: header.LastResultsHash, + + EvidenceHash: header.EvidenceHash, + ProposerAddress: header.ProposerAddress, + } +}