Skip to content
This repository has been archived by the owner on Nov 30, 2021. It is now read-only.

evm: implement vm.GetHashFn #620

Merged
merged 20 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion app/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()})

Expand Down
15 changes: 12 additions & 3 deletions x/evm/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down
15 changes: 15 additions & 0 deletions x/evm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion x/evm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion x/evm/types/journal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
10 changes: 10 additions & 0 deletions x/evm/types/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
53 changes: 51 additions & 2 deletions x/evm/types/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: why don't we use csdb.WithContext(ctx).GetHeightHash(height) here as well?


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()),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}
135 changes: 135 additions & 0 deletions x/evm/types/state_transition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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},
}
Expand Down
Loading