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
This commit is contained in:
Federico Kunze 2020-12-07 17:09:09 -03:00 committed by GitHub
parent 7efd10eb21
commit c4a3c0a96e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 300 additions and 15 deletions

View File

@ -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

View File

@ -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()})

View File

@ -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)
}

View File

@ -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.

View File

@ -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)

View File

@ -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())
}

View File

@ -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))

View File

@ -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())
}

View File

@ -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: &ethcmn.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: &ethcmn.Hash{},
Sender: suite.address,
Simulate: suite.ctx.IsCheckTx(),
},
false,
},
{
"nil gas price",
func() {
suite.stateDB.SetParams(types.DefaultParams())
invalidGas := sdk.DecCoins{
{Denom: ethermint.AttoPhoton},
}

View File

@ -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, &params)
@ -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, &params)
@ -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
}
}
}

View File

@ -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})

View File

@ -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,
}
}