evm: implement ADR-002 EVM Hooks (#417)

* Allow evm to call native modules through logs

Closes #416

comment

add txHash parameter

review suggestions

add hooks test

* Update x/evm/keeper/hooks.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* Update x/evm/keeper/hooks_test.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* Update x/evm/keeper/keeper.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* Update x/evm/keeper/keeper.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* Update x/evm/keeper/keeper.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

* use table tests

* update adr comment

* update adr

* changelog

* Update CHANGELOG.md

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
yihuang 2021-09-02 20:36:33 +08:00 committed by GitHub
parent b76d024225
commit 089afe41a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 161 additions and 3 deletions

View File

@ -48,6 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
### Features ### Features
* (evm) [tharsis#469](https://github.com/tharsis/ethermint/pull/469) Support [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) * (evm) [tharsis#469](https://github.com/tharsis/ethermint/pull/469) Support [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)
* (evm) [tharsis#417](https://github.com/tharsis/ethermint/pull/417) Add `EvmHooks` for tx post-processing
### Bug Fixes ### Bug Fixes

View File

@ -49,7 +49,7 @@ func (k *EvmKeeper) SetHooks(eh types.EvmHooks) *Keeper;
The EVM state transition method `ApplyTransaction` should be changed like this: The EVM state transition method `ApplyTransaction` should be changed like this:
```golang ```golang
// Create cached context which convers both the tx processing and post processing // Need to create a snapshot explicitly to cover both tx processing and post processing logic
revision := k.Snapshot() revision := k.Snapshot()
res, err := k.ApplyMessage(evm, msg, ethCfg, false) res, err := k.ApplyMessage(evm, msg, ethCfg, false)
@ -186,12 +186,12 @@ The proposed ADR is backward compatible.
### Negative ### Negative
- It's possible that some contracts accidentally define a log with the same signature and cause an unintentional result. - On the use case of native call: It's possible that some contracts accidentally define a log with the same signature and cause an unintentional result.
To mitigate this, the implementor could whitelist contracts that are allowed to invoke native calls. To mitigate this, the implementor could whitelist contracts that are allowed to invoke native calls.
### Neutral ### Neutral
- The contract can only call native modules asynchronously, which means it can neither get the result nor handle the error. - On the use case of native call: The contract can only call native modules asynchronously, which means it can neither get the result nor handle the error.
## Further Discussions ## Further Discussions

31
x/evm/keeper/hooks.go Normal file
View File

@ -0,0 +1,31 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/tharsis/ethermint/x/evm/types"
)
var (
_ types.EvmHooks = MultiEvmHooks{}
)
// MultiEvmHooks combine multiple evm hooks, all hook functions are run in array sequence
type MultiEvmHooks []types.EvmHooks
// NewMultiEvmHooks combine multiple evm hooks
func NewMultiEvmHooks(hooks ...types.EvmHooks) MultiEvmHooks {
return hooks
}
// PostTxProcessing delegate the call to underlying hooks
func (mh MultiEvmHooks) PostTxProcessing(ctx sdk.Context, txHash ethcmn.Hash, logs []*ethtypes.Log) error {
for i := range mh {
if err := mh[i].PostTxProcessing(ctx, txHash, logs); err != nil {
return sdkerrors.Wrapf(err, "EVM hook %T failed", mh[i])
}
}
return nil
}

View File

@ -0,0 +1,76 @@
package keeper_test
import (
"errors"
"math/big"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/tharsis/ethermint/x/evm/keeper"
"github.com/tharsis/ethermint/x/evm/types"
)
// LogRecordHook records all the logs
type LogRecordHook struct {
Logs []*ethtypes.Log
}
func (dh *LogRecordHook) PostTxProcessing(ctx sdk.Context, txHash common.Hash, logs []*ethtypes.Log) error {
dh.Logs = logs
return nil
}
// FailureHook always fail
type FailureHook struct{}
func (dh FailureHook) PostTxProcessing(ctx sdk.Context, txHash common.Hash, logs []*ethtypes.Log) error {
return errors.New("post tx processing failed")
}
func (suite *KeeperTestSuite) TestEvmHooks() {
testCases := []struct {
msg string
setupHook func() types.EvmHooks
expFunc func(hook types.EvmHooks, result error)
}{
{
"log collect hook",
func() types.EvmHooks {
return &LogRecordHook{}
},
func(hook types.EvmHooks, result error) {
suite.Require().NoError(result)
suite.Require().Equal(1, len((hook.(*LogRecordHook).Logs)))
},
},
{
"always fail hook",
func() types.EvmHooks {
return &FailureHook{}
},
func(hook types.EvmHooks, result error) {
suite.Require().Error(result)
},
},
}
for _, tc := range testCases {
suite.SetupTest()
hook := tc.setupHook()
suite.app.EvmKeeper.SetHooks(keeper.NewMultiEvmHooks(hook))
k := suite.app.EvmKeeper
txHash := common.BigToHash(big.NewInt(1))
k.SetTxHashTransient(txHash)
k.AddLog(&ethtypes.Log{
Topics: []common.Hash{},
Address: suite.address,
})
logs := k.GetTxLogs(txHash)
result := k.PostTxProcessing(txHash, logs)
tc.expFunc(hook, result)
}
}

View File

@ -54,6 +54,9 @@ type Keeper struct {
// trace EVM state transition execution. This value is obtained from the `--trace` flag. // trace EVM state transition execution. This value is obtained from the `--trace` flag.
// For more info check https://geth.ethereum.org/docs/dapp/tracing // For more info check https://geth.ethereum.org/docs/dapp/tracing
debug bool debug bool
// EVM Hooks for tx post-processing
hooks types.EvmHooks
} }
// NewKeeper generates new evm module keeper // NewKeeper generates new evm module keeper
@ -416,3 +419,21 @@ func (k Keeper) ResetAccount(addr common.Address) {
k.DeleteCode(addr) k.DeleteCode(addr)
k.DeleteAccountStorage(addr) k.DeleteAccountStorage(addr)
} }
// SetHooks sets the hooks for the EVM module
func (k *Keeper) SetHooks(eh types.EvmHooks) *Keeper {
if k.hooks != nil {
panic("cannot set evm hooks twice")
}
k.hooks = eh
return k
}
// PostTxProcessing delegate the call to the hooks. If no hook has been registered, this function returns with a `nil` error
func (k *Keeper) PostTxProcessing(txHash common.Hash, logs []*ethtypes.Log) error {
if k.hooks == nil {
return nil
}
return k.hooks.PostTxProcessing(k.Ctx(), txHash, logs)
}

View File

@ -175,6 +175,9 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT
panic("context stack shouldn't be dirty before apply message") panic("context stack shouldn't be dirty before apply message")
} }
// Contains the tx processing and post processing in same scope
revision := k.Snapshot()
// pass false to execute in real mode, which do actual gas refunding // pass false to execute in real mode, which do actual gas refunding
res, err := k.ApplyMessage(evm, msg, ethCfg, false) res, err := k.ApplyMessage(evm, msg, ethCfg, false)
if err != nil { if err != nil {
@ -183,6 +186,17 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT
res.Hash = txHash.Hex() res.Hash = txHash.Hex()
logs := k.GetTxLogs(txHash) logs := k.GetTxLogs(txHash)
if !res.Failed() {
// Only call hooks if tx executed successfully.
if err = k.PostTxProcessing(txHash, logs); err != nil {
// If hooks return error, revert the whole tx.
k.RevertToSnapshot(revision)
res.VmError = types.ErrPostTxProcessing.Error()
k.Logger(ctx).Error("tx post processing failed", "error", err)
}
}
if len(logs) > 0 { if len(logs) > 0 {
res.Logs = types.NewLogsFromEth(logs) res.Logs = types.NewLogsFromEth(logs)
// Update transient block bloom filter // Update transient block bloom filter

View File

@ -32,6 +32,10 @@ const (
codeErrInvalidBaseFee codeErrInvalidBaseFee
) )
var (
ErrPostTxProcessing = errors.New("failed to execute post processing")
)
var ( var (
// ErrInvalidState returns an error resulting from an invalid Storage State. // ErrInvalidState returns an error resulting from an invalid Storage State.
ErrInvalidState = sdkerrors.Register(ModuleName, codeErrInvalidState, "invalid storage state") ErrInvalidState = sdkerrors.Register(ModuleName, codeErrInvalidState, "invalid storage state")

View File

@ -4,6 +4,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
) )
// AccountKeeper defines the expected account keeper interface // AccountKeeper defines the expected account keeper interface
@ -33,3 +35,12 @@ type StakingKeeper interface {
GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool) GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool)
GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, found bool) GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, found bool)
} }
// Event Hooks
// These can be utilized to customize evm transaction processing.
// EvmHooks event hooks for evm tx processing
type EvmHooks interface {
// Must be called after tx is processed successfully, if return an error, the whole transaction is reverted.
PostTxProcessing(ctx sdk.Context, txHash ethcmn.Hash, logs []*ethtypes.Log) error
}