evm
Abstract
Since Ethereum's inception in 2015, the ability to manage digital assets via smart contracts has attracted a large developer community who build decentralized applications on the Ethereum Virtual Machine (EVM). This community continuously creates extensive tooling and introduces standards, increasing the adoption rate of EVM-compatible technology.
However, the growth of EVM-based chains (e.g., Ethereum) has exposed several scalability challenges, known as the decentralization, security, and scalability trilemma. Developers face high gas fees, slow transaction speeds, and throughput, and chain-specific governance that can only undergo slow changes due to its broad range of deployed applications.
The x/evm
module addresses these concerns by providing an EVM-familiar environment on a high-throughput, scalable Proof-of-Stake blockchain. Built as a Cosmos SDK module, it allows for the deployment of smart contracts, interaction with the EVM state machine, and the use of EVM tooling. It can be deployed on Cosmos application-specific blockchains, alleviating concerns with high transaction throughput via Tendermint Core, fast transaction finality, and horizontal scalability via IBC.
Contents
Module Architecture
NOTE:: Please check this document as prerequisite reading if you are unfamiliar with the general module structure from the SDK modules.
evm/
├── client
│ └── cli
│ ├── query.go # CLI query commands for the module
│ └── tx.go # CLI transaction commands for the module
├── keeper
│ ├── keeper.go # ABCI BeginBlock and EndBlock logic
│ ├── keeper.go # Store keeper that handles the business logic of the module and has access to a specific subtree of the state tree.
│ ├── params.go # Parameter getter and setter
│ ├── querier.go # State query functions
│ └── statedb.go # Functions from types/statedb with a passed in sdk.Context
├── types
│ ├── chain_config.go
│ ├── codec.go # Type registration for encoding
│ ├── errors.go # Module-specific errors
│ ├── events.go # Events exposed to the Tendermint PubSub/Websocket
│ ├── genesis.go # Genesis state for the module
│ ├── journal.go # Ethereum Journal of state transitions
│ ├── keys.go # Store keys and utility functions
│ ├── logs.go # Types for persisting Ethereum tx logs on state after chain upgrades
│ ├── msg.go # EVM module transaction messages
│ ├── params.go # Module parameters that can be customized with governance parameter change proposals
│ ├── state_object.go # EVM state object
│ ├── statedb.go # Implementation of the StateDb interface
│ ├── storage.go # Implementation of the Ethereum state storage map using arrays to prevent non-determinism
│ └── tx_data.go # Ethereum transaction data types
├── genesis.go # ABCI InitGenesis and ExportGenesis functionality
├── handler.go # Message routing
└── module.go # Module setup for the module manager
Concepts
EVM
A processing engine known as the Ethereum Virtual Machine (EVM) is maintained by thousands of connected computers (nodes) that are running an Ethereum client. As a virtual computer (VM), the EVM is in charge of deterministically computing state changes regardless of its surroundings (hardware and OS). This implies that each node must obtain the exact same outcome from a transaction (tx) with the same initial state.
The Ethereum Virtual Machine (EVM) is regarded as the component of the Ethereum protocol that manages the deployment and execution of smart contracts. To distinguish clearly:
- The Ethereum protocol describes a blockchain on which all Ethereum accounts and smart contracts are stored. At any given block in the chain, it only has one canonical state (a data structure that maintains all accounts).
- The EVM, on the other hand, is the state machine that establishes the procedures for calculating a new valid state from one block to the next. Since the EVM is an isolated runtime, no external APIs or processes on the network or file system can be accessed by code executing inside the EVM.
The EVM is implemented as a Cosmos SDK module by the x/evm
module.
By sending Ethereum txs and executing their enclosing messages on the specified state to cause a state transition, it enables users to communicate with the EVM.
State
The Ethereum state is a data structure that maintains all of the chain's accounts and is implemented as a Merkle Patricia Tree. This data structure is altered by the EVM, creating a new state with a new state root. As a result, Ethereum may be thought of as a state chain that changes states by executing transactions in a block using the EVM. The block header (parent hash, block number, time stamp, nonce, receipts, etc.) of a new block of text messages can be used to describe it.
Accounts
At a specific address, you can keep two different sorts of accounts in state:
- Externally Owned Account (EOA): Has nonce (tx counter) and balance
- Smart Contract: Has nonce, balance, (immutable) code hash, storage root (another Merkle Patricia Trie)
Similar to conventional blockchain accounts, smart contracts also store executable code in an Ethereum-specific binary format called EVM bytecode. They are often written in a high-level Ethereum language, such Solidity, which is then translated into EVM bytecode and deployed on the blockchain by submitting a transaction with an Ethereum client.
Architecture
Operating as a stack-based machine is the EVM. Its principal architectural elements are as follows:
- Virtual ROM: contract code is pulled into this read only memory when processing txs
- Machine state (volatile): changes as the EVM runs and is wiped clean after processing each tx
- Program counter (PC)
- Gas: keeps track of how much gas is used
- Stack and Memory: compute state changes
- Access to account storage (persistent)
State Transitions with Smart Contracts
A public ABI, or a list of supported methods a user can interact with a contract, is typically exposed by smart contracts. A user will send a tx carrying any quantity of gas and a data payload formatted in accordance with the ABI, identifying the type of interaction and any other parameters, to interact with a contract and initiate a state change. The EVM executes the EVM bytecode for the smart contracts using the tx payload when the tx is received.
Executing EVM bytecode
The EVM bytecode that comprises a contract contains basic operations such as add, multiply, store, and others, which are referred to as Opcodes. The execution of each Opcode requires gas, which must be paid for using the transaction. The EVM is therefore considered quasi-turing complete, as it allows for any arbitrary computation, but the number of computations that can be performed during contract execution is limited to the amount of gas provided in the transaction. The gas cost of each Opcode, which can be found on the EVM website, reflects the cost of running these operations on actual computer hardware, such as ADD = 3gas
and SSTORE = 100gas
. To calculate the gas consumption of a transaction, the gas cost is multiplied by the gas price, which can fluctuate depending on the demand of the network at the time. If the network is under heavy load, you may have to pay a higher gas price to get your transaction executed. If the gas limit is exceeded (out of gas exception), no changes to the Ethereum state are applied, except that the sender's nonce increments and their balance decreases to pay for wasting the EVM's time.
Smart contracts can also call other smart contracts, which creates a new instance of the EVM, including a new stack and memory, for each call. The sandbox state is passed down to the next EVM, and if the gas runs out, all state changes are discarded. Otherwise, they are kept.
Please see the following for more reading:
Humans.ai as Geth implementation
Humans.ai is built upon the Cosmos SDK and incorporates the Ethereum protocol, specifically the EVM (Ethereum Virtual Machine), which is executed through the go-ethereum module. This implementation enables Humans.ai to perform state transitions in the same way as Geth, an Ethereum node. Additionally, Humans.ai supports the standard Ethereum JSON-RPC APIs, making it compatible with Web3 and the EVM, just like Geth.
JSON-RPC
Remote procedure call (RPC) protocol JSON-RPC is stateless and compact. This specification's main purpose is to describe a number of data structures and the guidelines for processing them. It is transport agnostic in that the concepts can be applied within a single process, via sockets, via HTTP, or in a wide range of message-passing systems. As a data format, it employs JSON (RFC 4627).
JSON-RPC Example: eth_call
You can execute messages against contracts using the JSON-RPC method 'eth_call'.
Typically, in order to include a transaction in the mempool, you must submit it to a Geth node. After that, nodes chatter among themselves, and ultimately the transaction is included in a block and processed.
However, eth_call
enables you to submit information to a contract and observe the results without committing to a transaction.
The steps involved in calling the endpoint in the Geth implementation are basically as follows:
- The
eth_call
request is transformed to call thefunc (s *PublicBlockchainAPI) Call()
function using theeth
namespace Call()
is given the transaction arguments, the block to call against and optional arguments that modify the state to call against. It then callsDoCall()
.DoCall()
transforms the arguments into aethtypes.message
, instantiates an EVM and applies the message withcore.ApplyMessage
ApplyMessage()
calls the state transitionTransitionDb()
TransitionDb()
eitherCreate()
s a new contract orCall()
s a contractevm.Call()
runs the interpreterevm.interpreter.Run()
to execute the message. If the execution fails, the state is reverted to a snapshot taken before the execution and gas is consumed.Run()
performs a loop to execute the opcodes.
Similar to this, the Humans.ai implementation makes use of the Cosmos SDK's gRPC query client:
eth_call
request is transformed to call thefunc (e *PublicAPI) Call
function using theeth
namespaceCall()
callsdoCall()
doCall()
transforms the arguments into aEthCallRequest
and callsEthCall()
using the query client of the evm module.EthCall()
transforms the arguments into aethtypes.message
and calls `ApplyMessageWithConfig()ApplyMessageWithConfig()
instantiates an EVM and eitherCreate()
s a new contract orCall()
s a contract using the Geth implementation.
StateDB
The StateDB
interface from go-ethereum is a representation of an EVM database for comprehensive state querying.
This interface, which the Keeper
implements in the x/evm
module, allows EVM state transitions.
This interface's implementation is what makes Humans.ai EVM compliant.
Consensus Engine
The application utilizes the x/evm
module to communicate with the Tendermint Core consensus engine via the Application Blockchain Interface (ABCI), forming a comprehensive blockchain system that integrates business logic and decentralized data storage.
Ethereum transactions submitted to the x/evm
module are subject to the consensus process before they are executed and modify the application state.
To gain a deeper understanding of state transitions, it is recommended to explore the fundamentals of the Tendermint consensus engine.
Transaction Logs
The Ethereum Log
s from the state machine execution are included in every x/evm
transaction result and are used by the JSON-RPC Web3 server for filter querying and processing EVM Hooks.
The transaction logs are kept in the temporary store while the transaction is being completed and released through Cosmos events thereafter. They are accessible via gRPC and JSON-RPC queries.
Block Bloom
The Bloom filter value for each block, denominated as 'Bloom', can be leveraged for filter queries.
The Bloom value associated with a block is housed in the transient store and subsequently disseminated through a Cosmos event during the EndBlock
procedure.
It can be queried through either gRPC or JSON-RPC.
👉 Note: Transaction Logs and Block Blooms are not persisted after upgrades because they are not kept on state. After upgrades, a user needs to use an archival node to get legacy chain events.
State
This section provides an overview of the items kept in the x/evm
module state, as well as features inherited from the go-ethereum StateDB
interface and implemented through the Keeper and the state implementation at genesis.
State Objects
The following items are maintained in state by the x/evm
module:
State
Description | Key | Value | Store | |
---|---|---|---|---|
Code | Smart contract bytecode | []byte{1} + []byte(address) | []byte{code} | KV |
Storage | Smart contract storage | []byte{2} + [32]byte{key} | [32]byte(value) | KV |
Block Bloom | Block bloom filter, used to accumulate the bloom filter of current block, emitted to events at end blocker. | []byte{1} + []byte(tx.Hash) | protobuf([]Log) | Transient |
Tx Index | Index of current transaction in current block. | []byte{2} | BigEndian(uint64) | Transient |
Log Size | Number of the logs emitted so far in current block. Used to decide the log index of following logs. | []byte{3} | BigEndian(uint64) | Transient |
Gas Used | Amount of gas used by ethereum messages of current cosmos-sdk tx, it's necessary when cosmos-sdk tx contains multiple ethereum messages. | []byte{4} | BigEndian(uint64) | Transient |
StateDB
For comprehensive state querying of both contracts and accounts, the StateDB
interface in the x/evm/statedb
module implements an EVM database.
StateDB
s are used in the Ethereum protocol to store anything found in the IAVL tree and handle caching and storing nested states.
// github.com/ethereum/go-ethereum/core/vm/interface.go
type StateDB interface {
CreateAccount(common.Address)
SubBalance(common.Address, *big.Int)
AddBalance(common.Address, *big.Int)
GetBalance(common.Address) *big.Int
GetNonce(common.Address) uint64
SetNonce(common.Address, uint64)
GetCodeHash(common.Address) common.Hash
GetCode(common.Address) []byte
SetCode(common.Address, []byte)
GetCodeSize(common.Address) int
AddRefund(uint64)
SubRefund(uint64)
GetRefund() uint64
GetCommittedState(common.Address, common.Hash) common.Hash
GetState(common.Address, common.Hash) common.Hash
SetState(common.Address, common.Hash, common.Hash)
Suicide(common.Address) bool
HasSuicided(common.Address) bool
// Exist reports whether the given account exists in state.
// Notably this should also return true for suicided accounts.
Exist(common.Address) bool
// Empty returns whether the given account is empty. Empty
// is defined according to EIP161 (balance = nonce = code = 0).
Empty(common.Address) bool
PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList)
AddressInAccessList(addr common.Address) bool
SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool)
// AddAddressToAccessList adds the given address to the access list. This operation is safe to perform
// even if the feature/fork is not active yet
AddAddressToAccessList(addr common.Address)
// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform
// even if the feature/fork is not active yet
AddSlotToAccessList(addr common.Address, slot common.Hash)
RevertToSnapshot(int)
Snapshot() int
AddLog(*types.Log)
AddPreimage(common.Hash, []byte)
ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error
}
The StateDB
in the x/evm
offers the following features:
CRUD of Ethereum accounts
You can create instances of EthAccount
from a given address and store them in the AccountKeeper
using the createAccount()
function. If an account with the same address already exists, this function will reset any existing code and storage associated with that address.
The balance of an account can be managed using the BankKeeper
. You can retrieve the balance with GetBalance()
, add to the balance with AddBalance()
, and subtract from the balance with SubBalance()
.
GetBalance()
returns the balance of the specified address in the specified denomination. The denomination is determined by the module parameters.AddBalance()
adds a specified amount to the balance of the specified address. The denomination is determined by the module parameters.SubBalance()
subtracts a specified amount from the balance of the specified address. The denomination is determined by the module parameters. This function does nothing if the amount is negative or the user doesn't have enough funds.
The nonce (or transaction sequence) of an account can be retrieved using the auth module from the AccountKeeper
.
GetNonce()
retrieves the account with the specified address and returns its nonce. If the account doesn't exist, this function does nothing.SetNonce()
sets the specified nonce as the sequence of the address' account. If the account doesn't exist, a new one will be created.
The smart contract bytecode is stored in the EVMKeeper
and can be queried using GetCodeHash()
, GetCode()
, and GetCodeSize()
. You can also update the code using the SetCode()
function.
GetCodeHash()
retrieves the account from the store and returns its code hash. If the account doesn't exist or is not an EthAccount type, this function returns an empty code hash.GetCode()
returns the code byte array associated with the specified address. If the code hash from the account is empty, this function returns nil.SetCode()
stores the code byte array in the application KVStore and sets the code hash to the specified account. The code is deleted from the store if it is empty.GetCodeSize()
returns the size of the contract code associated with this object, or zero if none.
Gas refunds need to be tracked and stored in a separate variable to add/subtract it from/to the gas used value after the EVM execution has finalized. The refund value is cleared on every transaction and at the end of every block.
AddRefund()
adds a specified amount of gas to the in-memory refund value.SubRefund()
subtracts a specified amount of gas from the in-memory refund value. This function will panic if the gas amount is greater than the current refund.GetRefund()
returns the amount of gas available for return after the transaction execution finalizes. This value is reset to zero on every transaction.
The state is stored in the EVMKeeper
and can be queried using GetCommittedState()
, GetState()
, and updated using SetState()
.
GetCommittedState()
returns the value set in store for the specified key hash. If the key is not registered, this function returns the empty hash.GetState()
returns the in-memory dirty state for the specified key hash. If the key is not found, this function loads the committed value from the KVStore.SetState()
sets the specified hashes (key, value) to the state. If the value hash is empty, this function deletes the key from the state. The new value is kept in the dirty state at first and will be committed to the KVStore at the end.
Accounts can also be set to a suicide state. When a contract commits suicide, the account is marked as suicided, and the account balance is cleared.
Suicide()
marks the specified account as suicided and clears its account balance.HasSuicided()
queries the in-memory flag to check if the account has been marked as suicided in the current transaction. Accounts that are suicided will be returned as non-nil during queries and "cleared" after the block has been committed.
You can check if an account exists using Exist()
and Empty()
.
Exist()
returns true if the specified account is present in the store or has been designated as suicide.Empty()
returns true if the address meets the following conditions:- nonce is 0
- balance amount for evm denom is 0
- account code hash is empty
EIP2930 functionality
Introduces a new transaction type that includes an access list, which is a list of addresses and storage keys that the transaction plans to access. The access list is stored in memory and discarded after the transaction has been committed.
PrepareAccessList()
method handles the preparation steps for executing a state transition with regard to both EIP-2929 and EIP-2930. This method should only be called if Yolov3/Berlin/2929+2930 is applicable at the current number. It performs the following functions:- Add sender to access list (EIP-2929)
- Add destination to access list (EIP-2929)
- Add precompiles to access list (EIP-2929)
- Add the contents of the optional tx access list (EIP-2930)
AddressInAccessList()
returns true if the address is registered.SlotInAccessList()
checks if the address and the slots are registered in the access list.AddAddressToAccessList()
adds the given address to the access list. If the address is already in the access list, this function performs a no-op.AddSlotToAccessList()
adds the given (address, slot) to the access list. If the address and slot are already in the access list, this function performs a no-op.
Snapshot state and Revert functionality
The Ethereum Virtual Machine (EVM) utilizes state-reverting exceptions to handle errors that occur during the execution of smart contracts. When such an exception occurs, it reverses all changes made to the state since the current call (including all sub-calls) and allows the caller to handle the error without propagating it further.
Snapshot()
creates a new snapshot and returns the identifier.RevertToSnapshot(rev)
undo all the modifications up to the snapshot identified asrev
.
Humans.ai has adapted the journal implementation from go-ethereum to incorporate this functionality. The adapted implementation utilizes a list of journal logs to record all state modification operations. A snapshot is defined by a unique ID and an index in the log list. To revert to a snapshot, the implementation undoes the journal logs after the snapshot index in reverse order.
Ethereum Transaction logs
With AddLog()
you can append the given Ethereum Log
to the list of logs
associated with the transaction hash kept in the current state.
This function also fills in the tx hash, block hash, tx index and log index fields before setting the log to store.
Keeper
The EVM module Keeper
grants access to the EVM module state
and implements statedb.Keeper
interface to support the StateDB
implementation.
The Keeper contains a store key that allows the DB
to write to a concrete subtree of the multistore that is only accessible by the EVM module.
Instead of using a trie and database for querying and persistence (the StateDB
implementation),
Humans.ai uses the Cosmos KVStore
(key-value store) and Cosmos SDK Keeper
to facilitate state transitions.
To support the interface functionality, it imports 4 module Keepers:
auth
: CRUD accountsbank
: accounting (supply) and CRUD of balancesstaking
: query historical headersfee market
: EIP1559 base fee for processingDynamicFeeTx
after theLondon
hard fork has been activated on theChainConfig
parameters
type Keeper struct {
// Protobuf codec
cdc codec.BinaryCodec
// Store key required for the EVM Prefix KVStore. It is required by:
// - storing account's Storage State
// - storing account's Code
// - storing Bloom filters by block height. Needed for the Web3 API.
// For the full list, check the module specification
storeKey sdk.StoreKey
// key to access the transient store, which is reset on every block during Commit
transientKey sdk.StoreKey
// module specific parameter space that can be configured through governance
paramSpace paramtypes.Subspace
// access to account state
accountKeeper types.AccountKeeper
// update balance and accounting operations with coins
bankKeeper types.BankKeeper
// access historical headers for EVM state transition execution
stakingKeeper types.StakingKeeper
// fetch EIP1559 base fee and parameters
feeMarketKeeper types.FeeMarketKeeper
// chain ID number obtained from the context's chain id
eip155ChainID *big.Int
// Tracer used to collect execution traces from the EVM transaction execution
tracer string
// 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
// EVM Hooks for tx post-processing
hooks types.EvmHooks
}
Genesis State
The x/evm
module GenesisState
defines the state necessary for initializing the chain from a previous exported height.
It contains the GenesisAccounts
and the module parameters
type GenesisState struct {
// accounts is an array containing the ethereum genesis accounts.
Accounts []GenesisAccount `protobuf:"bytes,1,rep,name=accounts,proto3" json:"accounts"`
// params defines all the parameters of the module.
Params Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params"`
}
Genesis Accounts
The GenesisAccount
type corresponds to an adaptation of the Ethereum GenesisAccount
type.
It defines an account to be initialized in the genesis state.
Its main difference is that the one on Humans.ai uses a custom Storage
type
that uses a slice instead of maps for the evm State
(due to non-determinism),
and that it doesn't contain the private key field.
It is also important to note that since the auth
module on the Cosmos SDK manages the account state,
the Address
field must correspond to an existing EthAccount
that is stored in the auth
's module Keeper
(i.e AccountKeeper
).
Addresses use the EIP55 hex format
on genesis.json
.
type GenesisAccount struct {
// address defines an ethereum hex formated address of an account
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
// code defines the hex bytes of the account code.
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
// storage defines the set of state key values for the account.
Storage Storage `protobuf:"bytes,3,rep,name=storage,proto3,castrepeated=Storage" json:"storage"`
}
State Transitions
The x/evm
module allows for users to submit Ethereum transactions (Tx
)
and execute their containing messages to evoke state transitions on the given state.
Users submit transactions client-side to broadcast it to the network. When the transaction is included in a block during consensus, it is executed server-side. We highly recommend to understand the basics of the Tendermint consensus engine to understand the State Transitions in detail.
Client-Side
👉 This is based on the eth_sendTransaction
JSON-RPC
- A user submits a transaction via one of the available JSON-RPC endpoints
using an Ethereum-compatible client or wallet (eg Metamask, WalletConnect, Ledger, etc):
a. eth (public) namespace:
eth_sendTransaction
eth_sendRawTransaction
b. personal (private) namespace:personal_sendTransaction
- An instance of
MsgEthereumTx
is created after populating the RPC transaction usingSetTxDefaults
to fill missing tx arguments with default values - The
Tx
fields are validated (stateless) usingValidateBasic()
- The
Tx
is signed using the key associated with the sender address and the latest ethereum hard fork (London
,Berlin
, etc) from theChainConfig
- The
Tx
is built from the msg fields using the Cosmos Config builder - The
Tx
is broadcast in sync mode to ensure to wait for aCheckTx
execution response. Transactions are validated by the application usingCheckTx()
, before being added to the mempool of the consensus engine. - JSON-RPC user receives a response with the
RLP
hash of the transaction fields. This hash is different from the default hash used by SDK Transactions that calculates thesha256
hash of the transaction bytes.
Server-Side
Once a block (containing the Tx
) has been committed during consensus,
it is applied to the application in a series of ABCI msgs server-side.
Each Tx
is handled by the application by calling RunTx
.
After a stateless validation on each sdk.Msg
in the Tx
,
the AnteHandler
confirms whether the Tx
is an Ethereum or SDK transaction.
As an Ethereum transaction it's containing msgs are then handled
by the x/evm
module to update the application's state.
AnteHandler
The anteHandler
is run for every transaction.
It checks if the Tx
is an Ethereum transaction and routes it to an internal ante handler.
Here, Tx
s are handled using EthereumTx extension options to process them differently than normal Cosmos SDK transactions.
The antehandler
runs through a series of options and their AnteHandle
functions for each Tx
:
EthSetUpContextDecorator()
is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption by setting the gas meter to infiniteEthValidateBasicDecorator(evmKeeper)
validates the fields of an Ethereum type CosmosTx
msgEthSigVerificationDecorator(evmKeeper)
validates that the registered chain id is the same as the one on the message, and that the signer address matches the one defined on the message. It's not skipped for RecheckTx, because it setFrom
address which is critical from other ante handler to work. Failure in RecheckTx will prevent tx to be included into block, especially when CheckTx succeed, in which case user won't see the error message.EthAccountVerificationDecorator(ak, bankKeeper, evmKeeper)
will verify, that the sender balance is greater than the total transaction cost. The account will be set to store if it doesn't exist, i.e cannot be found on store. This AnteHandler decorator will fail if:- any of the msgs is not a MsgEthereumTx
- from address is empty
- account balance is lower than the transaction cost
EthNonceVerificationDecorator(ak)
validates that the transaction nonces are valid and equivalent to the sender account’s current nonce.EthGasConsumeDecorator(evmKeeper)
validates that the Ethereum tx message has enough to cover intrinsic gas (during CheckTx only) and that the sender has enough balance to pay for the gas cost. Intrinsic gas for a transaction is the amount of gas that the transaction uses before the transaction is executed. The gas is a constant value plus any cost incurred by additional bytes of data supplied with the transaction. This AnteHandler decorator will fail if:- the transaction contains more than one message
- the message is not a MsgEthereumTx
- sender account cannot be found
- transaction's gas limit is lower than the intrinsic gas
- user doesn't have enough balance to deduct the transaction fees (gas_limit * gas_price)
- transaction or block gas meter runs out of gas
CanTransferDecorator(evmKeeper, feeMarketKeeper)
creates an EVM from the message and calls the BlockContext CanTransfer function to see if the address can execute the transaction.EthIncrementSenderSequenceDecorator(ak)
handles incrementing the sequence of the signer (i.e sender). If the transaction is a contract creation, the nonce will be incremented during the transaction execution and not within this AnteHandler decorator.
The options authante.NewMempoolFeeDecorator()
, authante.NewTxTimeoutHeightDecorator()
and authante.NewValidateMemoDecorator(ak)
are the same as for a Cosmos Tx
.
Click here for more on the anteHandler
.
EVM module
After authentication through the antehandler
,
each sdk.Msg
(in this case MsgEthereumTx
) in the Tx
is delivered to the Msg Handler in the x/evm
module
and runs through the following the steps:
- Convert
Msg
to an ethereumTx
type - Apply
Tx
withEVMConfig
and attempt to perform a state transition, that will only be persisted (committed) to the underlying KVStore if the transaction does not fail:- Confirm that
EVMConfig
is created - Create the ethereum signer using chain config value from
EVMConfig
- Set the ethereum transaction hash to the (impermanent) transient store so that it's also available on the StateDB functions
- Generate a new EVM instance
- Confirm that EVM params for contract creation (
EnableCreate
) and contract execution (EnableCall
) are enabled - Apply message. If
To
address isnil
, create new contract using code as deployment code. Else call contract at given address with the given input as parameters - Calculate gas used by the evm operation
- Confirm that
- If
Tx
applied successfully- Execute EVM
Tx
postprocessing hooks. If hooks return error, revert the wholeTx
- Refund gas according to Ethereum gas accounting rules
- Update block bloom filter value using the logs generated from the tx
- Emit SDK events for the transaction fields and tx logs
- Execute EVM
Transactions
This section defines the sdk.Msg
concrete types that result in the state transitions defined on the previous section.
MsgEthereumTx
An EVM state transition can be achieved by using the MsgEthereumTx
.
This message encapsulates an Ethereum transaction data (TxData
) as a sdk.Msg
.
It contains the necessary transaction data fields.
Note, that the MsgEthereumTx
implements both the sdk.Msg
and sdk.Tx
interfaces.
Normally, SDK messages only implement the former, while the latter is a group of messages bundled together.
type MsgEthereumTx struct {
// inner transaction data
Data *types.Any `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
// DEPRECATED: encoded storage size of the transaction
Size_ float64 `protobuf:"fixed64,2,opt,name=size,proto3" json:"-"`
// transaction hash in hex format
Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty" rlp:"-"`
// ethereum signer address in hex format. This address value is checked
// against the address derived from the signature (V, R, S) using the
// secp256k1 elliptic curve
From string `protobuf:"bytes,4,opt,name=from,proto3" json:"from,omitempty"`
}
This message field validation is expected to fail if:
From
field is defined and the address is invalidTxData
stateless validation fails
The transaction execution is expected to fail if:
- Any of the custom
AnteHandler
Ethereum decorators checks fail:- Minimum gas amount requirements for transaction
- Tx sender account doesn't exist or hasn't enough balance for fees
- Account sequence doesn't match the transaction
Data.AccountNonce
- Message signature verification fails
- EVM contract creation (i.e
evm.Create
) fails, orevm.Call
fails
Conversion
The MsgEthreumTx
can be converted to the go-ethereum Transaction
and Message
types
in order to create and call evm contracts.
// AsTransaction creates an Ethereum Transaction type from the msg fields
func (msg MsgEthereumTx) AsTransaction() *ethtypes.Transaction {
txData, err := UnpackTxData(msg.Data)
if err != nil {
return nil
}
return ethtypes.NewTx(txData.AsEthereumData())
}
// AsMessage returns the transaction as a core.Message.
func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) {
msg := Message{
nonce: tx.Nonce(),
gasLimit: tx.Gas(),
gasPrice: new(big.Int).Set(tx.GasPrice()),
gasFeeCap: new(big.Int).Set(tx.GasFeeCap()),
gasTipCap: new(big.Int).Set(tx.GasTipCap()),
to: tx.To(),
amount: tx.Value(),
data: tx.Data(),
accessList: tx.AccessList(),
isFake: false,
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
msg.gasPrice = math.BigMin(msg.gasPrice.Add(msg.gasTipCap, baseFee), msg.gasFeeCap)
}
var err error
msg.from, err = Sender(s, tx)
return msg, err
}
Signing
In order for the signature verification to be valid, the TxData
must contain the v | r | s
values from the Signer
.
Sign calculates a secp256k1 ECDSA signature and signs the transaction.
It takes a keyring signer and the chainID to sign an Ethereum transaction according to EIP155 standard.
This method mutates the transaction as it populates the V, R, S fields of the Transaction's Signature.
The function will fail if the sender address is not defined for the msg
or if the sender is not registered on the keyring.
// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It
// takes a keyring signer and the chainID to sign an Ethereum transaction according to
// EIP155 standard.
// This method mutates the transaction as it populates the V, R, S
// fields of the Transaction's Signature.
// The function will fail if the sender address is not defined for the msg or if
// the sender is not registered on the keyring
func (msg *MsgEthereumTx) Sign(ethSigner ethtypes.Signer, keyringSigner keyring.Signer) error {
from := msg.GetFrom()
if from.Empty() {
return fmt.Errorf("sender address not defined for message")
}
tx := msg.AsTransaction()
txHash := ethSigner.Hash(tx)
sig, _, err := keyringSigner.SignByAddress(from, txHash.Bytes())
if err != nil {
return err
}
tx, err = tx.WithSignature(ethSigner, sig)
if err != nil {
return err
}
msg.FromEthereumTx(tx)
return nil
}
TxData
The MsgEthereumTx
supports the 3 valid Ethereum transaction data types from go-ethereum:
LegacyTx
, AccessListTx
and DynamicFeeTx
.
These types are defined as protobuf messages
and packed into a proto.Any
interface type in the MsgEthereumTx
field.
LegacyTx
: EIP-155 transaction typeDynamicFeeTx
: EIP-1559 transaction type. Enabled by London hard fork blockAccessListTx
: EIP-2930 transaction type. Enabled by Berlin hard fork block
LegacyTx
The transaction data of regular Ethereum transactions.
type LegacyTx struct {
// nonce corresponds to the account nonce (transaction sequence).
Nonce uint64 `protobuf:"varint,1,opt,name=nonce,proto3" json:"nonce,omitempty"`
// gas price defines the value for each gas unit
GasPrice *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,2,opt,name=gas_price,json=gasPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"gas_price,omitempty"`
// gas defines the gas limit defined for the transaction.
GasLimit uint64 `protobuf:"varint,3,opt,name=gas,proto3" json:"gas,omitempty"`
// hex formatted address of the recipient
To string `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"`
// value defines the unsigned integer value of the transaction amount.
Amount *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,5,opt,name=value,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"value,omitempty"`
// input defines the data payload bytes of the transaction.
Data []byte `protobuf:"bytes,6,opt,name=data,proto3" json:"data,omitempty"`
// v defines the signature value
V []byte `protobuf:"bytes,7,opt,name=v,proto3" json:"v,omitempty"`
// r defines the signature value
R []byte `protobuf:"bytes,8,opt,name=r,proto3" json:"r,omitempty"`
// s define the signature value
S []byte `protobuf:"bytes,9,opt,name=s,proto3" json:"s,omitempty"`
}
This message field validation is expected to fail if:
GasPrice
is invalid (nil
, negative or out of int256 bound)Fee
(gasprice * gaslimit) is invalidAmount
is invalid (negative or out of int256 bound)To
address is invalid (non valid ethereum hex address)
DynamicFeeTx
The transaction data of EIP-1559 dynamic fee transactions.
type DynamicFeeTx struct {
// destination EVM chain ID
ChainID *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"chainID"`
// nonce corresponds to the account nonce (transaction sequence).
Nonce uint64 `protobuf:"varint,2,opt,name=nonce,proto3" json:"nonce,omitempty"`
// gas tip cap defines the max value for the gas tip
GasTipCap *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,3,opt,name=gas_tip_cap,json=gasTipCap,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"gas_tip_cap,omitempty"`
// gas fee cap defines the max value for the gas fee
GasFeeCap *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,4,opt,name=gas_fee_cap,json=gasFeeCap,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"gas_fee_cap,omitempty"`
// gas defines the gas limit defined for the transaction.
GasLimit uint64 `protobuf:"varint,5,opt,name=gas,proto3" json:"gas,omitempty"`
// hex formatted address of the recipient
To string `protobuf:"bytes,6,opt,name=to,proto3" json:"to,omitempty"`
// value defines the the transaction amount.
Amount *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,7,opt,name=value,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"value,omitempty"`
// input defines the data payload bytes of the transaction.
Data []byte `protobuf:"bytes,8,opt,name=data,proto3" json:"data,omitempty"`
Accesses AccessList `protobuf:"bytes,9,rep,name=accesses,proto3,castrepeated=AccessList" json:"accessList"`
// v defines the signature value
V []byte `protobuf:"bytes,10,opt,name=v,proto3" json:"v,omitempty"`
// r defines the signature value
R []byte `protobuf:"bytes,11,opt,name=r,proto3" json:"r,omitempty"`
// s define the signature value
S []byte `protobuf:"bytes,12,opt,name=s,proto3" json:"s,omitempty"`
}
This message field validation is expected to fail if:
GasTipCap
is invalid (nil
, negative or overflows int256)GasFeeCap
is invalid (nil
, negative or overflows int256)GasFeeCap
is less thanGasTipCap
Fee
(gas price * gas limit) is invalid (overflows int256)Amount
is invalid (negative or overflows int256)To
address is invalid (non-valid ethereum hex address)ChainID
isnil
AccessListTx
The transaction data of EIP-2930 access list transactions.
type AccessListTx struct {
// destination EVM chain ID
ChainID *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"chainID"`
// nonce corresponds to the account nonce (transaction sequence).
Nonce uint64 `protobuf:"varint,2,opt,name=nonce,proto3" json:"nonce,omitempty"`
// gas price defines the value for each gas unit
GasPrice *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,3,opt,name=gas_price,json=gasPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"gas_price,omitempty"`
// gas defines the gas limit defined for the transaction.
GasLimit uint64 `protobuf:"varint,4,opt,name=gas,proto3" json:"gas,omitempty"`
// hex formatted address of the recipient
To string `protobuf:"bytes,5,opt,name=to,proto3" json:"to,omitempty"`
// value defines the unsigned integer value of the transaction amount.
Amount *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,6,opt,name=value,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"value,omitempty"`
// input defines the data payload bytes of the transaction.
Data []byte `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"`
Accesses AccessList `protobuf:"bytes,8,rep,name=accesses,proto3,castrepeated=AccessList" json:"accessList"`
// v defines the signature value
V []byte `protobuf:"bytes,9,opt,name=v,proto3" json:"v,omitempty"`
// r defines the signature value
R []byte `protobuf:"bytes,10,opt,name=r,proto3" json:"r,omitempty"`
// s define the signature value
S []byte `protobuf:"bytes,11,opt,name=s,proto3" json:"s,omitempty"`
}
This message field validation is expected to fail if:
GasPrice
is invalid (nil
, negative or overflows int256)Fee
(gas price * gas limit) is invalid (overflows int256)Amount
is invalid (negative or overflows int256)To
address is invalid (non-valid ethereum hex address)ChainID
isnil
ABCI
The Application Blockchain Interface (ABCI) allows the application to interact with the Tendermint Consensus engine.
The application maintains several ABCI connections with Tendermint.
The most relevant for the x/evm
is the Consensus connection at Commit.
This connection is responsible for block execution and calls the functions InitChain
(containing InitGenesis
), BeginBlock
, DeliverTx
, EndBlock
, Commit
.
InitChain
is only called the first time a new blockchain is started
and DeliverTx
is called for each transaction in the block.
InitGenesis
InitGenesis
initializes the EVM module genesis state by setting the GenesisState
fields to the store.
In particular, it sets the parameters and genesis accounts (state and code).
ExportGenesis
The ExportGenesis
ABCI function exports the genesis state of the EVM module.
In particular, it retrieves all the accounts with their bytecode, balance and storage, the transaction logs,
and the EVM parameters and chain configuration.
BeginBlock
The EVM module BeginBlock
logic is executed prior to handling the state transitions from the transactions.
The main objective of this function is to:
- Set the context for the current block so that the block header, store, gas meter, etc.
are available to the
Keeper
once one of theStateDB
functions are called during EVM state transitions. - Set the EIP155
ChainID
number (obtained from the full chain-id), in case it hasn't been set before duringInitChain
EndBlock
The EVM module EndBlock
logic occurs after executing all the state transitions from the transactions.
The main objective of this function is to:
- Emit Block bloom events
- This is due for web3 compatibility as the Ethereum headers contain this type as a field. The JSON-RPC service uses this event query to construct an Ethereum header from a Tendermint header.
- The block bloom filter value is obtained from the transient store and then emitted
Hooks
The x/evm
module implements an EvmHooks
interface that extend and customize the Tx
processing logic externally.
This supports EVM contracts to call native cosmos modules by
- defining a log signature and emitting the specific log from the smart contract,
- recognizing those logs in the native tx processing code, and
- converting them to native module calls.
To do this, the interface includes a PostTxProcessing
hook that registers custom Tx
hooks in the EvmKeeper
.
These Tx
hooks are processed after the EVM state transition is finalized and doesn't fail.
Note that there are no default hooks implemented in the EVM module.
type EvmHooks interface {
// Must be called after tx is processed successfully, if return an error, the whole transaction is reverted.
PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error
}
PostTxProcessing
PostTxProcessing
is only called after an EVM transaction finished successfully
and delegates the call to underlying hooks.
If no hook has been registered, this function returns with a nil
error.
func (k *Keeper) PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error {
if k.hooks == nil {
return nil
}
return k.hooks.PostTxProcessing(k.Ctx(), msg, receipt)
}
It's executed in the same cache context as the EVM transaction,
if it returns an error, the whole EVM transaction is reverted,
if the hook implementor doesn't want to revert the tx, they can always return nil
instead.
The error returned by the hooks is translated to a VM error failed to process native logs
,
the detailed error message is stored in the return value.
The message is sent to native modules asynchronously, there's no way for the caller to catch and recover the error.
Events
The x/evm
module emits the Cosmos SDK events after a state execution.
The EVM module emits events of the relevant transaction fields, as well as the transaction logs (ethereum events).
MsgEthereumTx
Type | Attribute Key | Attribute Value |
---|---|---|
ethereum_tx | "amount" | {amount} |
ethereum_tx | "recipient" | {hex_address} |
ethereum_tx | "contract" | {hex_address} |
ethereum_tx | "txHash" | {tendermint_hex_hash} |
ethereum_tx | "ethereumTxHash" | {hex_hash} |
ethereum_tx | "txIndex" | {tx_index} |
ethereum_tx | "txGasUsed" | {gas_used} |
tx_log | "txLog" | {tx_log} |
message | "sender" | {eth_address} |
message | "action" | "ethereum" |
message | "module" | "evm" |
Additionally, the EVM module emits an event during EndBlock
for the filter query block bloom.
ABCI
Type | Attribute Key | Attribute Value |
---|---|---|
block_bloom | "bloom" | string(bloomBytes) |
Parameters
The evm module contains the following parameters:
Params
Key | Type | Default Value |
---|---|---|
EVMDenom | string | "aheart" |
EnableCreate | bool | true |
EnableCall | bool | true |
ExtraEIPs | []int | TBD |
ChainConfig | ChainConfig | See ChainConfig |
EVM denom
The evm denomination parameter defines the token denomination used on the EVM state transitions and gas consumption for EVM messages.
For example, on Ethereum, the evm_denom
would be ETH
.
In the case of Humans.ai, the default denomination is the atto heart.
In terms of precision, HEART
and ETH
share the same value,
i.e. 1 HEART = 10^18 atto heart
and 1 ETH = 10^18 wei
.
Note: SDK applications that want to import the EVM module as a dependency
will need to set their own evm_denom
(i.e not "aheart"
).
Enable Create
The enable create parameter toggles state transitions that use the vm.Create
function.
When the parameter is disabled, it will prevent all contract creation functionality.
Enable Transfer
The enable transfer toggles state transitions that use the vm.Call
function.
When the parameter is disabled, it will prevent transfers between accounts and executing a smart contract call.
Extra EIPs
The extra EIPs parameter defines the set of activateable Ethereum Improvement Proposals (EIPs)
on the Ethereum VM Config
that apply custom jump tables.
NOTE: some of these EIPs are already enabled by the chain configuration, depending on the hard fork number.
The supported activateable EIPS are:
Chain Config
The ChainConfig
is a protobuf wrapper type
that contains the same fields as the go-ethereum ChainConfig
parameters,
but using *sdk.Int
types instead of *big.Int
.
By default, all block configuration fields but ConstantinopleBlock
, are enabled at genesis (height 0).
ChainConfig Defaults
Name | Default Value |
---|---|
HomesteadBlock | 0 |
DAOForkBlock | 0 |
DAOForkSupport | true |
EIP150Block | 0 |
EIP150Hash | 0x0000000000000000000000000000000000000000000000000000000000000000 |
EIP155Block | 0 |
EIP158Block | 0 |
ByzantiumBlock | 0 |
ConstantinopleBlock | 0 |
PetersburgBlock | 0 |
IstanbulBlock | 0 |
MuirGlacierBlock | 0 |
BerlinBlock | 0 |
LondonBlock | 0 |
ArrowGlacierBlock | 0 |
GrayGlacierBlock | 0 |
MergeNetsplitBlock | 0 |
ShanghaiBlock | 0 |
CancunBlock. | 0 |
Client
A user can query and interact with the evm
module using the CLI, JSON-RPC, gRPC or REST.
CLI
Find below a list of humansd
commands added with the x/evm
module.
You can obtain the full list by using the humansd -h
command.
Queries
The query
commands allow users to query evm
state.
code
Allows users to query the smart contract code at a given address.
humansd query evm code ADDRESS [flags]
# Example
$ humansd query evm code 0x7bf7b17da59880d9bcca24915679668db75f9397
# Output
code: "0xef616c92f3cfc9e92dc270d6acff9cea213cecc7020a76ee4395af09bdceb4837a1ebdb5735e11e7d3adb6104e0c3ac55180b4ddf5e54d022cc5e8837f6a4f971b"
storage
Allows users to query storage for an account with a given key and height.
humansd query evm storage ADDRESS KEY [flags]
# Example
$ humansd query evm storage 0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0 0 --height 0
# Output
value: "0x0000000000000000000000000000000000000000000000000000000000000000"
Transactions
The tx
commands allow users to interact with the evm
module.
raw
Allows users to build cosmos transactions from raw ethereum transaction.
humansd tx evm raw TX_HEX [flags]
# Example
$ humansd tx evm raw 0xf9ff74c86aefeb5f6019d77280bbb44fb695b4d45cfe97e6eed7acd62905f4a85034d5c68ed25a2e7a8eeb9baf1b84
# Output
value: "0x0000000000000000000000000000000000000000000000000000000000000000"
JSON-RPC
For an overview on the JSON-RPC methods and namespaces supported on Humans.ai, please refer to JSON-RPC methods.
gRPC
Queries
Verb | Method | Description |
---|---|---|
gRPC | ethermint.evm.v1.Query/Account | Get an Ethereum account |
gRPC | ethermint.evm.v1.Query/CosmosAccount | Get an Ethereum account's Cosmos Address |
gRPC | ethermint.evm.v1.Query/ValidatorAccount | Get an Ethereum account's from a validator consensus Address |
gRPC | ethermint.evm.v1.Query/Balance | Get the balance of a the EVM denomination for a single EthAccount. |
gRPC | ethermint.evm.v1.Query/Storage | Get the balance of all coins for a single account |
gRPC | ethermint.evm.v1.Query/Code | Get the balance of all coins for a single account |
gRPC | ethermint.evm.v1.Query/Params | Get the parameters of x/evm module |
gRPC | ethermint.evm.v1.Query/EthCall | Implements the eth_call rpc api |
gRPC | ethermint.evm.v1.Query/EstimateGas | Implements the eth_estimateGas rpc api |
gRPC | ethermint.evm.v1.Query/TraceTx | Implements the debug_traceTransaction rpc api |
gRPC | ethermint.evm.v1.Query/TraceBlock | Implements the debug_traceBlockByNumber and debug_traceBlockByHash rpc api |
GET | /ethermint/evm/v1/account/{address} | Get an Ethereum account |
GET | /ethermint/evm/v1/cosmos_account/{address} | Get an Ethereum account's Cosmos Address |
GET | /ethermint/evm/v1/validator_account/{cons_address} | Get an Ethereum account's from a validator consensus Address |
GET | /ethermint/evm/v1/balances/{address} | Get the balance of a the EVM denomination for a single EthAccount. |
GET | /ethermint/evm/v1/storage/{address}/{key} | Get the balance of all coins for a single account |
GET | /ethermint/evm/v1/codes/{address} | Get the balance of all coins for a single account |
GET | /ethermint/evm/v1/params | Get the parameters of x/evm module |
GET | /ethermint/evm/v1/eth_call | Implements the eth_call rpc api |
GET | /ethermint/evm/v1/estimate_gas | Implements the eth_estimateGas rpc api |
GET | /ethermint/evm/v1/trace_tx | Implements the debug_traceTransaction rpc api |
GET | /ethermint/evm/v1/trace_block | Implements the debug_traceBlockByNumber and debug_traceBlockByHash rpc api |
Transactions
Verb | Method | Description |
---|---|---|
gRPC | ethermint.evm.v1.Msg/EthereumTx | Submit an Ethereum transactions |
POST | /ethermint/evm/v1/ethereum_tx | Submit an Ethereum transactions |