From 9f8f0dd7234ba8891a0602d7dbf3a44424761334 Mon Sep 17 00:00:00 2001 From: HuangYi Date: Mon, 9 Aug 2021 10:05:01 +0800 Subject: [PATCH] Allow evm to call native modules through logs Closes #416 comment add txHash parameter review suggestions add hooks test --- x/evm/keeper/hooks.go | 33 +++++++++++++++ x/evm/keeper/hooks_test.go | 72 ++++++++++++++++++++++++++++++++ x/evm/keeper/keeper.go | 21 ++++++++++ x/evm/keeper/keeper_test.go | 28 ------------- x/evm/keeper/state_transition.go | 14 +++++++ x/evm/types/errors.go | 4 ++ x/evm/types/interfaces.go | 11 +++++ 7 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 x/evm/keeper/hooks.go create mode 100644 x/evm/keeper/hooks_test.go diff --git a/x/evm/keeper/hooks.go b/x/evm/keeper/hooks.go new file mode 100644 index 0000000000..6c8f6e714c --- /dev/null +++ b/x/evm/keeper/hooks.go @@ -0,0 +1,33 @@ +package keeper + +import ( + "reflect" + + 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 %s failed", reflect.TypeOf(mh[i])) + } + } + return nil +} diff --git a/x/evm/keeper/hooks_test.go b/x/evm/keeper/hooks_test.go new file mode 100644 index 0000000000..8a4876cfa1 --- /dev/null +++ b/x/evm/keeper/hooks_test.go @@ -0,0 +1,72 @@ +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" + + ethermint "github.com/tharsis/ethermint/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() { + suite.SetupTest() + suite.Commit() + + logRecordHook := LogRecordHook{} + suite.app.EvmKeeper.SetHooks(keeper.NewMultiEvmHooks(&logRecordHook)) + + k := suite.app.EvmKeeper + + txHash := common.BigToHash(big.NewInt(1)) + + amt := sdk.Coins{ethermint.NewPhotonCoinInt64(100)} + err := suite.app.BankKeeper.MintCoins(suite.ctx, types.ModuleName, amt) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, suite.address.Bytes(), amt) + suite.Require().NoError(err) + + k.SetTxHashTransient(txHash) + k.AddLog(ðtypes.Log{ + Topics: []common.Hash{}, + Address: suite.address, + }) + + logs := k.GetTxLogs(txHash) + suite.Require().Equal(1, len(logs)) + + err = k.PostTxProcessing(txHash, logs) + suite.Require().NoError(err) + + suite.Require().Equal(1, len(logRecordHook.Logs)) +} + +func (suite *KeeperTestSuite) TestHookFailure() { + suite.SetupTest() + k := suite.app.EvmKeeper + + // Test failure hook + suite.app.EvmKeeper.SetHooks(keeper.NewMultiEvmHooks(FailureHook{})) + err := k.PostTxProcessing(common.Hash{}, nil) + suite.Require().Error(err) +} diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 6af67b44ff..cbae3a8682 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -54,6 +54,9 @@ type Keeper struct { // trace EVM state transition execution. This value is obtained from the `--trace` flag. // For more info check https://geth.ethereum.org/docs/dapp/tracing debug bool + + // EvmHooks + hooks types.EvmHooks } // NewKeeper generates new evm module keeper @@ -416,3 +419,21 @@ func (k Keeper) ResetAccount(addr common.Address) { k.DeleteCode(addr) k.DeleteAccountStorage(addr) } + +// SetHooks sets the hooks for governance +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 +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) +} diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 9fb0f4e584..aa177977a1 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -14,11 +14,8 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" - "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" - authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/tharsis/ethermint/app" @@ -150,13 +147,6 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) { suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) suite.appCodec = encodingConfig.Marshaler - - // mint some tokens to coinbase address - _, bankKeeper := suite.initKeepersWithmAccPerms() - require.NoError(t, err) - initCoin := sdk.NewCoins(sdk.NewCoin(suite.EvmDenom(), testTokens)) - err = simapp.FundAccount(bankKeeper, suite.ctx, acc.GetAddress(), initCoin) - require.NoError(t, err) } func (suite *KeeperTestSuite) SetupTest() { @@ -187,24 +177,6 @@ func (suite *KeeperTestSuite) Commit() { suite.queryClient = types.NewQueryClient(queryHelper) } -// initKeepersWithmAccPerms construct a bank keeper that can mint tokens out of thin air -func (suite *KeeperTestSuite) initKeepersWithmAccPerms() (authkeeper.AccountKeeper, bankkeeper.BaseKeeper) { - maccPerms := app.GetMaccPerms() - - maccPerms[authtypes.Burner] = []string{authtypes.Burner} - maccPerms[authtypes.Minter] = []string{authtypes.Minter} - authKeeper := authkeeper.NewAccountKeeper( - suite.appCodec, suite.app.GetKey(types.StoreKey), suite.app.GetSubspace(types.ModuleName), - authtypes.ProtoBaseAccount, maccPerms, - ) - keeper := bankkeeper.NewBaseKeeper( - suite.appCodec, suite.app.GetKey(types.StoreKey), authKeeper, - suite.app.GetSubspace(types.ModuleName), map[string]bool{}, - ) - - return authKeeper, keeper -} - // DeployTestContract deploy a test erc20 contract and returns the contract address func (suite *KeeperTestSuite) DeployTestContract(t require.TestingT, owner common.Address, supply *big.Int) common.Address { ctx := sdk.WrapSDKContext(suite.ctx) diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 077b05269b..77d08c2ff1 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -175,6 +175,9 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT 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 res, err := k.ApplyMessage(evm, msg, ethCfg, false) if err != nil { @@ -183,6 +186,17 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT res.Hash = txHash.Hex() 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 { res.Logs = types.NewLogsFromEth(logs) // Update transient block bloom filter diff --git a/x/evm/types/errors.go b/x/evm/types/errors.go index 09cefc220c..6c56c7efa2 100644 --- a/x/evm/types/errors.go +++ b/x/evm/types/errors.go @@ -32,6 +32,10 @@ const ( codeErrInvalidBaseFee ) +var ( + ErrPostTxProcessing = errors.New("failed to execute post processing") +) + var ( // ErrInvalidState returns an error resulting from an invalid Storage State. ErrInvalidState = sdkerrors.Register(ModuleName, codeErrInvalidState, "invalid storage state") diff --git a/x/evm/types/interfaces.go b/x/evm/types/interfaces.go index b45089e7ff..11da575800 100644 --- a/x/evm/types/interfaces.go +++ b/x/evm/types/interfaces.go @@ -4,6 +4,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/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 @@ -33,3 +35,12 @@ type StakingKeeper interface { GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, 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, if failed, the whole evm transaction is reverted. + PostTxProcessing(ctx sdk.Context, txHash ethcmn.Hash, logs []*ethtypes.Log) error +}