diff --git a/CHANGELOG.md b/CHANGELOG.md index c1510246..56e49d1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### State Machine Breaking +* (evm) [\#1272](https://github.com/evmos/ethermint/pull/1272) Implement modular interface for the EVM. * (deps) [\#1159](https://github.com/evmos/ethermint/pull/1159) Bump Geth version to `v1.10.19`. * (deps) [#1167](https://github.com/evmos/ethermint/pull/1167) Bump ibc-go to [`v4.0.0-rc2`](https://github.com/cosmos/ibc-go/releases/tag/v4.0.0-rc2) * (ante) [#1176](https://github.com/evmos/ethermint/pull/1176) Fix invalid tx hashes; Remove `Size_` field and validate `Hash`/`From` fields in ante handler, diff --git a/app/ante/eth.go b/app/ante/eth.go index 98dfef4a..f688c4e7 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -305,7 +305,7 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate // check that caller has enough balance to cover asset transfer for **topmost** call // NOTE: here the gas consumed is from the context with the infinite gas meter - if coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { + if coreMsg.Value().Sign() > 0 && !evm.Context().CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInsufficientFunds, "failed to transfer %s from address %s using the EVM block context transfer function", diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index a39e7b2c..0a2cb855 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/evmos/ethermint/x/evm/statedb" evmtypes "github.com/evmos/ethermint/x/evm/types" + evm "github.com/evmos/ethermint/x/evm/vm" feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" ) @@ -26,7 +27,7 @@ type EVMKeeper interface { statedb.Keeper DynamicFeeEVMKeeper - NewEVM(ctx sdk.Context, msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM + NewEVM(ctx sdk.Context, msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) evm.EVM DeductTxCostsFromUserBalance( ctx sdk.Context, msgEthTx evmtypes.MsgEthereumTx, txData evmtypes.TxData, denom string, homestead, istanbul, london bool, ) (fees sdk.Coins, priority int64, err error) diff --git a/app/app.go b/app/app.go index 158bf7a3..959f0817 100644 --- a/app/app.go +++ b/app/app.go @@ -106,6 +106,7 @@ import ( "github.com/evmos/ethermint/x/evm" evmkeeper "github.com/evmos/ethermint/x/evm/keeper" evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/evmos/ethermint/x/evm/vm/geth" "github.com/evmos/ethermint/x/feemarket" feemarketkeeper "github.com/evmos/ethermint/x/feemarket/keeper" feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" @@ -396,7 +397,7 @@ func NewEthermintApp( app.EvmKeeper = evmkeeper.NewKeeper( appCodec, keys[evmtypes.StoreKey], tkeys[evmtypes.TransientKey], app.GetSubspace(evmtypes.ModuleName), app.AccountKeeper, app.BankKeeper, app.StakingKeeper, app.FeeMarketKeeper, - tracer, + nil, geth.NewEVM, tracer, ) // Create IBC Keeper diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 675b9588..e5375ddb 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -19,6 +19,7 @@ import ( ethermint "github.com/evmos/ethermint/types" "github.com/evmos/ethermint/x/evm/statedb" "github.com/evmos/ethermint/x/evm/types" + evm "github.com/evmos/ethermint/x/evm/vm" ) // Keeper grants access to the EVM module state and implements the go-ethereum StateDB interface. @@ -54,14 +55,25 @@ type Keeper struct { // EVM Hooks for tx post-processing hooks types.EvmHooks + + // custom stateless precompiled smart contracts + customPrecompiles evm.PrecompiledContracts + + // evm constructor function + evmConstructor evm.Constructor } // NewKeeper generates new evm module keeper func NewKeeper( cdc codec.BinaryCodec, - storeKey, transientKey storetypes.StoreKey, paramSpace paramtypes.Subspace, - ak types.AccountKeeper, bankKeeper types.BankKeeper, sk types.StakingKeeper, + storeKey, transientKey storetypes.StoreKey, + paramSpace paramtypes.Subspace, + ak types.AccountKeeper, + bankKeeper types.BankKeeper, + sk types.StakingKeeper, fmk types.FeeMarketKeeper, + customPrecompiles evm.PrecompiledContracts, + evmConstructor evm.Constructor, tracer string, ) *Keeper { // ensure evm module account is set @@ -76,15 +88,17 @@ func NewKeeper( // NOTE: we pass in the parameter space to the CommitStateDB in order to use custom denominations for the EVM operations return &Keeper{ - cdc: cdc, - paramSpace: paramSpace, - accountKeeper: ak, - bankKeeper: bankKeeper, - stakingKeeper: sk, - feeMarketKeeper: fmk, - storeKey: storeKey, - transientKey: transientKey, - tracer: tracer, + cdc: cdc, + paramSpace: paramSpace, + accountKeeper: ak, + bankKeeper: bankKeeper, + stakingKeeper: sk, + feeMarketKeeper: fmk, + storeKey: storeKey, + transientKey: transientKey, + customPrecompiles: customPrecompiles, + evmConstructor: evmConstructor, + tracer: tracer, } } diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 4942fc61..87979f90 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -16,6 +16,7 @@ import ( ethermint "github.com/evmos/ethermint/types" "github.com/evmos/ethermint/x/evm/statedb" "github.com/evmos/ethermint/x/evm/types" + evm "github.com/evmos/ethermint/x/evm/vm" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -77,7 +78,7 @@ func (k *Keeper) NewEVM( cfg *types.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB, -) *vm.EVM { +) evm.EVM { blockCtx := vm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, @@ -95,7 +96,7 @@ func (k *Keeper) NewEVM( tracer = k.Tracer(ctx, msg, cfg.ChainConfig) } vmConfig := k.VMConfig(ctx, msg, cfg, tracer) - return vm.NewEVM(blockCtx, txCtx, stateDB, cfg.ChainConfig, vmConfig) + return k.evmConstructor(blockCtx, txCtx, stateDB, cfg.ChainConfig, vmConfig, k.customPrecompiles) } // VMConfig creates an EVM configuration from the debug setting and the extra EIPs enabled on the @@ -366,17 +367,19 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, evm := k.NewEVM(ctx, msg, cfg, tracer, stateDB) leftoverGas := msg.Gas() + // Allow the tracer captures the tx level events, mainly the gas consumption. - if evm.Config.Debug { - evm.Config.Tracer.CaptureTxStart(leftoverGas) + vmCfg := evm.Config() + if vmCfg.Debug { + vmCfg.Tracer.CaptureTxStart(leftoverGas) defer func() { - evm.Config.Tracer.CaptureTxEnd(leftoverGas) + vmCfg.Tracer.CaptureTxEnd(leftoverGas) }() } sender := vm.AccountRef(msg.From()) contractCreation := msg.To() == nil - isLondon := cfg.ChainConfig.IsLondon(evm.Context.BlockNumber) + isLondon := cfg.ChainConfig.IsLondon(evm.Context().BlockNumber) intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, cfg.ChainConfig, contractCreation) if err != nil { @@ -394,7 +397,7 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, // access list preparation is moved from ante handler to here, because it's needed when `ApplyMessage` is called // under contexts where ante handlers are not run, for example `eth_call` and `eth_estimateGas`. if rules := cfg.ChainConfig.Rules(big.NewInt(ctx.BlockHeight()), cfg.ChainConfig.MergeNetsplitBlock != nil); rules.IsBerlin { - stateDB.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) + stateDB.PrepareAccessList(msg.From(), msg.To(), evm.ActivePrecompiles(rules), msg.AccessList()) } if contractCreation { diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go index 8dd7ce72..ae016a2c 100644 --- a/x/evm/statedb/interfaces.go +++ b/x/evm/statedb/interfaces.go @@ -3,8 +3,19 @@ package statedb import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" ) +// ExtStateDB defines an extension to the interface provided by the go-ethereum +// codebase to support additional state transition functionalities. In particular +// it supports appending a new entry to the state journal through +// AppendJournalEntry so that the state can be reverted after running +// stateful precompiled contracts. +type ExtStateDB interface { + vm.StateDB + AppendJournalEntry(JournalEntry) +} + // Keeper provide underlying storage of StateDB type Keeper interface { // Read methods diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go index c3f692f9..49f61c00 100644 --- a/x/evm/statedb/journal.go +++ b/x/evm/statedb/journal.go @@ -24,21 +24,21 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// journalEntry is a modification entry in the state change journal that can be -// reverted on demand. -type journalEntry interface { - // revert undoes the changes introduced by this journal entry. - revert(*StateDB) +// JournalEntry is a modification entry in the state change journal that can be +// Reverted on demand. +type JournalEntry interface { + // Revert undoes the changes introduced by this journal entry. + Revert(*StateDB) - // dirtied returns the Ethereum address modified by this journal entry. - dirtied() *common.Address + // Dirtied returns the Ethereum address modified by this journal entry. + Dirtied() *common.Address } // journal contains the list of state modifications applied since the last state // commit. These are tracked to be able to be reverted in the case of an execution // exception or request for reversal. type journal struct { - entries []journalEntry // Current changes tracked by the journal + entries []JournalEntry // Current changes tracked by the journal dirties map[common.Address]int // Dirty accounts and the number of changes } @@ -64,22 +64,22 @@ func (j *journal) sortedDirties() []common.Address { } // append inserts a new modification entry to the end of the change journal. -func (j *journal) append(entry journalEntry) { +func (j *journal) append(entry JournalEntry) { j.entries = append(j.entries, entry) - if addr := entry.dirtied(); addr != nil { + if addr := entry.Dirtied(); addr != nil { j.dirties[*addr]++ } } -// revert undoes a batch of journalled modifications along with any reverted +// Revert undoes a batch of journalled modifications along with any Reverted // dirty handling too. -func (j *journal) revert(statedb *StateDB, snapshot int) { +func (j *journal) Revert(statedb *StateDB, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // Undo the changes made by the operation - j.entries[i].revert(statedb) + j.entries[i].Revert(statedb) // Drop any dirty tracking induced by the change - if addr := j.entries[i].dirtied(); addr != nil { + if addr := j.entries[i].Dirtied(); addr != nil { if j.dirties[*addr]--; j.dirties[*addr] == 0 { delete(j.dirties, *addr) } @@ -141,23 +141,23 @@ type ( } ) -func (ch createObjectChange) revert(s *StateDB) { +func (ch createObjectChange) Revert(s *StateDB) { delete(s.stateObjects, *ch.account) } -func (ch createObjectChange) dirtied() *common.Address { +func (ch createObjectChange) Dirtied() *common.Address { return ch.account } -func (ch resetObjectChange) revert(s *StateDB) { +func (ch resetObjectChange) Revert(s *StateDB) { s.setStateObject(ch.prev) } -func (ch resetObjectChange) dirtied() *common.Address { +func (ch resetObjectChange) Dirtied() *common.Address { return nil } -func (ch suicideChange) revert(s *StateDB) { +func (ch suicideChange) Revert(s *StateDB) { obj := s.getStateObject(*ch.account) if obj != nil { obj.suicided = ch.prev @@ -165,59 +165,59 @@ func (ch suicideChange) revert(s *StateDB) { } } -func (ch suicideChange) dirtied() *common.Address { +func (ch suicideChange) Dirtied() *common.Address { return ch.account } -func (ch balanceChange) revert(s *StateDB) { +func (ch balanceChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setBalance(ch.prev) } -func (ch balanceChange) dirtied() *common.Address { +func (ch balanceChange) Dirtied() *common.Address { return ch.account } -func (ch nonceChange) revert(s *StateDB) { +func (ch nonceChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) } -func (ch nonceChange) dirtied() *common.Address { +func (ch nonceChange) Dirtied() *common.Address { return ch.account } -func (ch codeChange) revert(s *StateDB) { +func (ch codeChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } -func (ch codeChange) dirtied() *common.Address { +func (ch codeChange) Dirtied() *common.Address { return ch.account } -func (ch storageChange) revert(s *StateDB) { +func (ch storageChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) } -func (ch storageChange) dirtied() *common.Address { +func (ch storageChange) Dirtied() *common.Address { return ch.account } -func (ch refundChange) revert(s *StateDB) { +func (ch refundChange) Revert(s *StateDB) { s.refund = ch.prev } -func (ch refundChange) dirtied() *common.Address { +func (ch refundChange) Dirtied() *common.Address { return nil } -func (ch addLogChange) revert(s *StateDB) { +func (ch addLogChange) Revert(s *StateDB) { s.logs = s.logs[:len(s.logs)-1] } -func (ch addLogChange) dirtied() *common.Address { +func (ch addLogChange) Dirtied() *common.Address { return nil } -func (ch accessListAddAccountChange) revert(s *StateDB) { +func (ch accessListAddAccountChange) Revert(s *StateDB) { /* One important invariant here, is that whenever a (addr, slot) is added, if the addr is not already present, the add causes two journal entries: @@ -230,14 +230,14 @@ func (ch accessListAddAccountChange) revert(s *StateDB) { s.accessList.DeleteAddress(*ch.address) } -func (ch accessListAddAccountChange) dirtied() *common.Address { +func (ch accessListAddAccountChange) Dirtied() *common.Address { return nil } -func (ch accessListAddSlotChange) revert(s *StateDB) { +func (ch accessListAddSlotChange) Revert(s *StateDB) { s.accessList.DeleteSlot(*ch.address, *ch.slot) } -func (ch accessListAddSlotChange) dirtied() *common.Address { +func (ch accessListAddSlotChange) Dirtied() *common.Address { return nil } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index e9b07f8a..ee8cd058 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -429,7 +429,7 @@ func (s *StateDB) RevertToSnapshot(revid int) { snapshot := s.validRevisions[idx].journalIndex // Replay the journal to undo changes and remove invalidated snapshots - s.journal.revert(s, snapshot) + s.journal.Revert(s, snapshot) s.validRevisions = s.validRevisions[:idx] } diff --git a/x/evm/vm/geth/geth.go b/x/evm/vm/geth/geth.go new file mode 100644 index 00000000..259559ce --- /dev/null +++ b/x/evm/vm/geth/geth.go @@ -0,0 +1,79 @@ +package geth + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + + evm "github.com/evmos/ethermint/x/evm/vm" +) + +var ( + _ evm.EVM = (*EVM)(nil) + _ evm.Constructor = NewEVM +) + +// EVM is the wrapper for the go-ethereum EVM. +type EVM struct { + *vm.EVM +} + +// NewEVM defines the constructor function for the go-ethereum (geth) EVM. It uses +// the default precompiled contracts and the EVM concrete implementation from +// geth. +func NewEVM( + blockCtx vm.BlockContext, + txCtx vm.TxContext, + stateDB vm.StateDB, + chainConfig *params.ChainConfig, + config vm.Config, + _ evm.PrecompiledContracts, // unused +) evm.EVM { + return &EVM{ + EVM: vm.NewEVM(blockCtx, txCtx, stateDB, chainConfig, config), + } +} + +// Context returns the EVM's Block Context +func (e EVM) Context() vm.BlockContext { + return e.EVM.Context +} + +// TxContext returns the EVM's Tx Context +func (e EVM) TxContext() vm.TxContext { + return e.EVM.TxContext +} + +// Config returns the configuration options for the EVM. +func (e EVM) Config() vm.Config { + return e.EVM.Config +} + +// Precompile returns the precompiled contract associated with the given address +// and the current chain configuration. If the contract cannot be found it returns +// nil. +func (e EVM) Precompile(addr common.Address) (p vm.PrecompiledContract, found bool) { + precompiles := GetPrecompiles(e.ChainConfig(), e.EVM.Context.BlockNumber) + p, found = precompiles[addr] + return p, found +} + +// ActivePrecompiles returns a list of all the active precompiled contract addresses +// for the current chain configuration. +func (EVM) ActivePrecompiles(rules params.Rules) []common.Address { + return vm.ActivePrecompiles(rules) +} + +// RunPrecompiledContract runs a stateless precompiled contract and ignores the address and +// value arguments. It uses the RunPrecompiledContract function from the geth vm package. +func (EVM) RunPrecompiledContract( + p evm.StatefulPrecompiledContract, + _ common.Address, // address arg is unused + input []byte, + suppliedGas uint64, + _ *big.Int, // value arg is unused +) (ret []byte, remainingGas uint64, err error) { + return vm.RunPrecompiledContract(p, input, suppliedGas) +} diff --git a/x/evm/vm/geth/precompiles.go b/x/evm/vm/geth/precompiles.go new file mode 100644 index 00000000..b7c0e66f --- /dev/null +++ b/x/evm/vm/geth/precompiles.go @@ -0,0 +1,27 @@ +package geth + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + + evm "github.com/evmos/ethermint/x/evm/vm" +) + +// GetPrecompiles returns all the precompiled contracts defined given the +// current chain configuration and block height. +func GetPrecompiles(cfg *params.ChainConfig, blockNumber *big.Int) evm.PrecompiledContracts { + var precompiles evm.PrecompiledContracts + switch { + case cfg.IsBerlin(blockNumber): + precompiles = vm.PrecompiledContractsBerlin + case cfg.IsIstanbul(blockNumber): + precompiles = vm.PrecompiledContractsIstanbul + case cfg.IsByzantium(blockNumber): + precompiles = vm.PrecompiledContractsByzantium + default: + precompiles = vm.PrecompiledContractsHomestead + } + return precompiles +} diff --git a/x/evm/vm/interface.go b/x/evm/vm/interface.go new file mode 100644 index 00000000..d83a4254 --- /dev/null +++ b/x/evm/vm/interface.go @@ -0,0 +1,66 @@ +package vm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// PrecompiledContracts defines a map of address -> precompiled contract +type PrecompiledContracts map[common.Address]vm.PrecompiledContract + +type StatefulPrecompiledContract interface { + vm.PrecompiledContract + RunStateful(evm EVM, addr common.Address, input []byte, value *big.Int) (ret []byte, err error) +} + +// EVM defines the interface for the Ethereum Virtual Machine used by the EVM module. +type EVM interface { + Config() vm.Config + Context() vm.BlockContext + TxContext() vm.TxContext + + Reset(txCtx vm.TxContext, statedb vm.StateDB) + Cancel() + Cancelled() bool //nolint + Interpreter() *vm.EVMInterpreter + Call(caller vm.ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) + CallCode(caller vm.ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) + DelegateCall(caller vm.ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) + StaticCall(caller vm.ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) + Create(caller vm.ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) + Create2( + caller vm.ContractRef, + code []byte, + gas uint64, + endowment *big.Int, + salt *uint256.Int) ( + ret []byte, contractAddr common.Address, leftOverGas uint64, err error, + ) + ChainConfig() *params.ChainConfig + + ActivePrecompiles(rules params.Rules) []common.Address + Precompile(addr common.Address) (vm.PrecompiledContract, bool) + RunPrecompiledContract( + p StatefulPrecompiledContract, + addr common.Address, + input []byte, + suppliedGas uint64, + value *big.Int) ( + ret []byte, remainingGas uint64, err error, + ) +} + +// Constructor defines the function used to instantiate the EVM on +// each state transition. +type Constructor func( + blockCtx vm.BlockContext, + txCtx vm.TxContext, + stateDB vm.StateDB, + chainConfig *params.ChainConfig, + config vm.Config, + customPrecompiles PrecompiledContracts, +) EVM