Skip to content

Commit

Permalink
Merge PR: evm: implement vm.GetHashFn (#475)
Browse files Browse the repository at this point in the history
* evm: implement vm.GetHashFn
  • Loading branch information
zhongqiuwood authored Dec 7, 2020
1 parent ec04c81 commit 0e61c65
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 13 deletions.
20 changes: 18 additions & 2 deletions x/evm/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

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, 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.
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
13 changes: 13 additions & 0 deletions x/evm/types/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
70 changes: 68 additions & 2 deletions x/evm/types/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}
151 changes: 151 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,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)

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

0 comments on commit 0e61c65

Please sign in to comment.