diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index 21136feed1..0cafd0a617 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -8,6 +8,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ethtypes "github.com/ethereum/go-ethereum/core/types" + + ethermint "github.com/okex/okexchain/app/types" + "github.com/okex/okexchain/x/evm/types" ) // BeginBlock sets the block hash -> block height map for the previous block height @@ -29,18 +32,31 @@ 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()) + epoch, err := ethermint.ParseChainID(ctx.ChainID()) + if err != nil { + panic(err) + } + + // set the hash for the current height and epoch + // 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, epoch.Uint64(), 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) + _, err = k.Commit(ctx, true) if 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 b75e847e79..969c87c3af 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, epoch, height uint64) (common.Hash, bool) { + return k.CommitStateDB.WithContext(ctx).GetHeightHash(epoch, height) +} + +// SetHeightHash sets the block header hash associated with a given height. +func (k Keeper) SetHeightHash(ctx sdk.Context, epoch, height uint64, hash common.Hash) { + k.CommitStateDB.WithContext(ctx).SetHeightHash(epoch, 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 df30b2745a..233038ef2b 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 3acad54f50..e24df4ca85 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 a5e7d665a6..cffafc8112 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -27,8 +27,21 @@ 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) + bytes(epoch) +// This ordering facilitates the iteration by height for the EVM GetHashFn +// queries. The epoch (i.e chain version) needs to be stored in case a software +// upgrade resets the height to 0. +func HeightHashKey(epoch, height uint64) []byte { + epochBz := sdk.Uint64ToBigEndian(epoch) + heightBz := sdk.Uint64ToBigEndian(height) + return append(heightBz, epochBz...) +} + // 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 b92504113b..7a2191b129 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -47,11 +47,58 @@ 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 previous chain epoch number (i.e previous chain version) +func GetHashFn(ctx sdk.Context, csdb *CommitStateDB, chainEpoch uint64) vm.GetHashFunc { + return func(height uint64) common.Hash { + var ( + hash common.Hash + found bool + ) + + 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. + hash, found = csdb.WithContext(ctx).GetHeightHash(chainEpoch, height) + } + + if found { + return hash + } + + // Case 3: iterate over the past chain epochs and check if there was a previous epoch with the requested + // height. This case applies when a chain upgrades to a non-zero height. + // Eg: chainID ethermint-1, epoch number: 1, final height: 100 --> chainID ethermint-2, epoch number: 2, initial height: 101 + hash, found = csdb.WithContext(ctx).FindHeightHash(height) + if !found { + // return an empty hash if the hash wasn't found + return common.Hash{} + } + + return 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, st.ChainID.Uint64()), Origin: st.Sender, Coinbase: common.Address{}, // there's no benefitiary since we're not mining BlockNumber: big.NewInt(ctx.BlockHeight()), @@ -133,8 +180,9 @@ 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) csdb.SetNonce(st.Sender, csdb.GetNonce(st.Sender)+1) ret, leftOverGas, err = evm.Call(senderRef, *st.Recipient, st.Payload, gasLimit, st.Amount) @@ -221,3 +269,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 de31ec58bf..cb35a0dc8b 100644 --- a/x/evm/types/state_transition_test.go +++ b/x/evm/types/state_transition_test.go @@ -3,16 +3,124 @@ package types_test import ( "math/big" + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/okex/okexchain/app/crypto/ethsecp256k1" ethermint "github.com/okex/okexchain/app/types" "github.com/okex/okexchain/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, 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, + }, + { + "valid hash, case 3", + 1, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-2", + Height: 100, + ValidatorsHash: []byte("val_hash"), + }, + ) + hash := types.HashFromContext(suite.ctx) + suite.stateDB.WithContext(suite.ctx).SetHeightHash(2, 1, hash) + }, + false, + }, + { + "empty hash, case 3", + 1000, + func() { + suite.ctx = suite.ctx.WithBlockHeader( + abci.Header{ + ChainID: "ethermint-2", + 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, suite.chainEpoch)(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 +212,52 @@ func (suite *StateDBTestSuite) TestTransitionDb() { }, false, }, + { + "call disabled", + func() { + params := types.NewParams(ethermint.NativeToken, 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.NativeToken, 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.NativeToken}, } diff --git a/x/evm/types/statedb.go b/x/evm/types/statedb.go index 7bfe9d9577..8f059e7103 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/okex/okexchain/x/params" - emint "github.com/okex/okexchain/app/types" + ethermint "github.com/okex/okexchain/app/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(epoch, height uint64, hash ethcmn.Hash) { + store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash) + key := HeightHashKey(epoch, 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(epoch, height uint64) (ethcmn.Hash, bool) { + store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash) + key := HeightHashKey(epoch, height) + bz := store.Get(key) + if len(bz) == 0 { + return ethcmn.Hash{}, false + } + + return ethcmn.BytesToHash(bz), true +} + // 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 } } } @@ -921,6 +940,28 @@ func (csdb *CommitStateDB) RawDump() ethstate.Dump { return ethstate.Dump{} } +// FindHeightHash iterates over all the hashes stored for a given height. The function will always +// return the hash for the latest chain epoch at the requested height. If there's no hash stored for +// the height, the function will return not found. +func (csdb *CommitStateDB) FindHeightHash(height uint64) (ethcmn.Hash, bool) { + store := csdb.ctx.KVStore(csdb.storeKey) + // use the height as the prefix iterator to iterate on all the epochs + prefix := append(KeyPrefixHeightHash, sdk.Uint64ToBigEndian(height)...) + // use the reverse iterator to iterate in descending order (i.e from latest epoch to earliest). + iterator := sdk.KVStoreReversePrefixIterator(store, prefix) + defer iterator.Close() + + if !iterator.Valid() { + // not found + return ethcmn.Hash{}, false + } + + // NOTE: if the store has a hash for the requested height, then we know that the first + // element will be the one from the latest epoch (due to the reverse/descending iteration). + // Thus it's safe to return the hash directly here. + return ethcmn.BytesToHash(iterator.Value()), true +} + type preimageEntry struct { // hash key of the preimage entry hash ethcmn.Hash diff --git a/x/evm/types/statedb_test.go b/x/evm/types/statedb_test.go index 20f80a40f8..4c18e83abc 100644 --- a/x/evm/types/statedb_test.go +++ b/x/evm/types/statedb_test.go @@ -27,6 +27,7 @@ type StateDBTestSuite struct { ctx sdk.Context app *app.OKExChainApp + chainEpoch uint64 stateDB *types.CommitStateDB address ethcmn.Address stateObject types.StateObject @@ -40,7 +41,8 @@ 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.chainEpoch = 1 suite.stateDB = suite.app.EvmKeeper.CommitStateDB.WithContext(suite.ctx) privkey, err := ethsecp256k1.GenerateKey() @@ -67,6 +69,19 @@ func (suite *StateDBTestSuite) TestParams() { suite.Require().Equal(newParams, params) } +func (suite *StateDBTestSuite) TestGetHeightHash() { + hash, found := suite.stateDB.GetHeightHash(0, 0) + suite.Require().False(found) + suite.Require().Equal(ethcmn.Hash{}.String(), hash.String()) + + expHash := ethcmn.BytesToHash([]byte("hash")) + suite.stateDB.SetHeightHash(0, 10, expHash) + + hash, found = suite.stateDB.GetHeightHash(0, 10) + suite.Require().True(found) + suite.Require().Equal(expHash.String(), hash.String()) +} + func (suite *StateDBTestSuite) TestBloomFilter() { // Prepare db for logs tHash := ethcmn.BytesToHash([]byte{0x1}) @@ -713,3 +728,28 @@ func (suite *StateDBTestSuite) TestCommitStateDB_AccessList() { suite.Require().True(addrIn) suite.Require().True(slotIn) } + +func (suite *StateDBTestSuite) TestFindHeightHash() { + hash, found := suite.stateDB.FindHeightHash(10) + suite.Require().False(found) + suite.Require().Equal(ethcmn.Hash{}.String(), hash.String()) + + epoch0 := ethcmn.BytesToHash([]byte("hash_epoch0")) + epoch1 := ethcmn.BytesToHash([]byte("hash_epoch1")) + epoch2 := ethcmn.BytesToHash([]byte("hash_epoch2")) + + suite.stateDB.SetHeightHash(0, 10, epoch0) + suite.stateDB.SetHeightHash(1, 10, epoch1) + + // should match the latest epoch hash + hash, found = suite.stateDB.FindHeightHash(10) + suite.Require().True(found) + suite.Require().Equal(epoch1.String(), hash.String()) + + suite.stateDB.SetHeightHash(2, 10, epoch2) + + // should match the latest epoch hash + hash, found = suite.stateDB.FindHeightHash(10) + suite.Require().True(found) + suite.Require().Equal(epoch2.String(), hash.String()) +} diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 27e5dca498..c86e233161 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, + } +}