The Ethereum Virtual Machine
Contents
The term “virtual machine” is often applied to the virtualization of a real computer. A virtual machine must provide a software abstraction, respectively, of actual hardware, and of system calls and other kernel functionality.
The EVM operates in a much more limited domain: it is just a computation engine, and as such provides an abstraction of just computation and storage.
- The EVM has no scheduling capability, because execution ordering is organized externally to it—Ethereum clients run through verified block transactions to determine which smart contracts need executing and in which order.
- In this sense, the Ethereum world computer is single-threaded, like JavaScript.
- Neither does the EVM have any “system interface” handling or “hardware support”—there is no physical machine to interface with.
The EVM is the runtime environment for smart contracts in Ethereum.
- It is not only sandboxed but actually completely isolated, which means that code running inside the EVM has no access to network, filesystem or other processes. Smart contracts even have limited access to other smart contracts.
- It runs a special form of code called EVM bytecode.
- It handles smart contract deployment and execution.
- Simple value transfer transactions from one EOA to another don’t need to involve it, practically speaking, but everything else will involve a state update computed by the EVM.
- Contracts can create other contracts using a special opcode (i.e. they do not simply call the zero address as a transaction would). The only difference between these create calls and normal message calls is that the payload data is executed and the result stored as code and the caller / creator receives the address of the new contract on the stack.
- It is a quasi–Turing-complete state machine; “quasi” because all execution processes are limited to a finite number of computational steps by the amount of gas available for any given smart contract execution. As such, the halting problem is “solved”.
- It has a stack-based architecture, storing all in-memory values on a stack.
- It works with a word size of 256 bits (mainly to facilitate native hashing and elliptic curve operations).
The EVM has three areas where it can store data:
- Storage
- Each account has a data area called storage that is persistent between function calls and transactions.
- Storage is a key-value store that maps 256-bit words to 256-bit words.
- It is not possible to enumerate storage from within a contract, it is comparatively costly to read, and even more to initialize and modify storage.
- Minimize what you store in persistent storage to what the contract needs to run.
- Store data like derived calculations, caching, and aggregates outside of the contract.
- A contract can neither read nor write to any storage apart from its own.
- Memory
- A contract obtains a freshly cleared instance of memory for each message call.
- Memory is linear and can be addressed at byte level, but reads are limited to a width of 256 bits, while writes can be either 8 bits or 256 bits wide.
- Memory is expanded by a word (256-bit), when accessing (either reading or writing) a previously untouched memory word (i.e. any offset within a word).
- At the time of expansion, the cost in gas must be paid.
- Memory is more costly the larger it grows as it scales quadratically.
- The stack
- The EVM is not a register machine but a stack machine, so all computations are performed on a data area called the stack.
- It has a maximum size of 1024 elements and contains words of 256 bits.
- All operands are taken from the stack, and the result (where applicable) is often put back on the top of the stack.
- Access to the stack is limited to the top end in the following way:
- It is possible to copy one of the topmost 16 elements to the top of the stack or swap the topmost element with one of the 16 elements below it.
- All other operations take the topmost two (or one, or more, depending on the operation) elements from the stack and push the result onto the stack.
- It is not possible to just access arbitrary elements deeper in the stack without first removing the top of the stack.
- It is possible to move stack elements to storage or memory in order to get deeper access to the stack.
- An immutable program code ROM, loaded with the bytecode of the smart contract to be executed.
The address range between 1
and (including) 8
contains “precompiled contracts” that can be called as any other contract but their behaviour (and their gas consumption) is not defined by EVM code stored at that address (they do not contain code) but instead is implemented in the EVM execution environment itself.
- Different EVM-compatible chains might use a different set of precompiled contracts.
- It might also be possible that new precompiled contracts are added to the Ethereum main chain in the future, but you can reasonably expect them to always be in the range between
1
and0xffff
(inclusive). sha256
,ripemd160
,ecrecover
are implemented as precompiled contracts and only really exist after they receive the first message (although their contract code is hardcoded).- Messages to non-existing contracts are more expensive and thus the execution might run into an Out-of-Gas error. A workaround for this problem is to first send Wei (1 for example) to each of the contracts before you use them in your actual contracts.
- This is not an issue on the main or test net.
- Messages to non-existing contracts are more expensive and thus the execution might run into an Out-of-Gas error. A workaround for this problem is to first send Wei (1 for example) to each of the contracts before you use them in your actual contracts.
The EVM Instruction Set
The instruction set of the EVM is kept minimal in order to avoid incorrect or inconsistent implementations which could cause consensus problems.
- All instructions operate on the basic data type, 256-bit words or on slices of memory (or other byte arrays).
- The usual arithmetic, bit, logical and comparison operations are present.
- Conditional and unconditional jumps are possible.
- Contracts can access relevant properties of the current block like its number and timestamp.
- opcodes
Ethereum State
The Ethereum world state is at the top level. It’s a mapping of Ethereum addresses (160-bit values) to accounts.
Each Ethereum address represents an account that consists of:
- an ether balance (stored as the number of wei owned by the account);
- a nonce (representing the number of transactions successfully sent from this account if it is an EOA, or the number of contracts created by it if it is a contract account);
- the account’s storage (which is a permanent data store, only used by smart contracts);
- the account’s program code (again, only if the account is a smart contract account).
An EOA will always have no code and an empty storage.
Transaction execution:
- The EVM’s program code ROM is loaded with the code of the contract account being called, the program counter is set to zero, the storage is loaded from the contract account’s storage, the memory is set to all zeros, and all the block and environment variables are set.
- As code execution progresses, the gas supply is reduced according to the gas cost of the operations executed.
- If at any point the gas supply is reduced to zero we get an “Out of Gas” (OOG) exception; execution immediately halts and the transaction is abandoned. No changes to the Ethereum state are applied, except for the sender’s nonce being incremented anyway and their ether balance going down to pay the block’s beneficiary for the resources used to execute the code to the halting point.
- At this point, you can think of the EVM running on a sandboxed copy of the Ethereum world state, with this sandboxed version being discarded completely if execution cannot complete for whatever reason.
- If execution does complete successfully, then the real world state is updated, including any changes to the called contract’s storage data, any new contracts created, and any ether balance transfers that were initiated.
A smart contract can itself effectively initiate transactions, code execution is a recursive process.
- A contract can call other contracts, with each call resulting in another EVM being instantiated around the new target of the call.
- Each instantiation has its sandbox world state initialized from the sandbox of the EVM at the level above.
- Each instantiation is also given a specified amount of gas for its gas supply (not exceeding the amount of gas remaining in the level above), and so may itself halt with an exception due to being given too little gas to complete its execution. In such cases, the sandbox state is discarded, and execution returns to the EVM at the level above.
Bytecode
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract example {
address contractOwner;
constructor() {
contractOwner = msg.sender;
}
}
// docker run --rm -v $HOME/solsrc:/sources ethereum/solc:0.8.17-alpine -o /sources/output/example --opcodes --asm --bin /sources/Example.sol
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE
- Pushes the single byte
0x80
onto the stack; - Pushes the single byte
0x40
onto the stack; - Save
0x80
at the memory location0x40
.0x40
is removed from the stack and used as the first argument forMSTORE
;0x80
is removed from the stack and used as the second argument forMSTORE
.
CALLVALUE
pushes onto the top of the stack the amount of ether (measured in wei) sent with the message call that initiated this execution.
- Pushes the single byte
In order to create a new contract, a special transaction is needed that has its to
field set to the special 0x0
address and its data
field set to the contract’s initiation code (deployment code).
- When such a contract creation transaction is processed, the code for the new contract accounts is not the code in the
data
field of the transaction. Instead, an EVM is instantiated with the code in thedata
field of the transaction loaded into its program code ROM, and then the output of the execution of that deployment code is taken as the code for the new contract account. - This is so that new contracts can be programmatically initialized using the Ethereum world state at the time of deployment, setting values in the contract’s storage and even sending ether or creating further new contracts.
When compiling a contract offline, e.g., using solc
on the command line, you can either get the deployment bytecode or the runtime bytecode.
- The deployment bytecode is used for every aspect of the initialization of a new contract account, including the bytecode that will actually end up being executed when transactions call this new contract (i.e., the runtime bytecode) and the code to initialize everything based on the contract’s constructor.
solc --bin
- The runtime bytecode is entirely contained within the deployment bytecode.
- The runtime bytecode is exactly the bytecode that ends up being executed when the contract is called, and nothing more; it does not include the bytecode needed to initialize the contract during deployment.
- The runtime bytecode is a subset of the deployment bytecode.
solc --bin-runtime
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Faucet {
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount
require(withdraw_amount <= 100000000000000000);
// Send the amount to the address that requested it
msg.sender.transfer(withdraw_amount);
}
// Accept any incoming amount
function() public payable {}
}
// docker run --rm -v $HOME/solsrc:/sources ethereum/solc:0.8.17-alpine -o /sources/output/faucet --opcodes /sources/Faucet.sol
Disassembling the bytecode:
PUSH1 0x80 PUSH1 0x40 MSTORE
PUSH1 0x4 CALLDATASIZE LT PUSH2 0x22 JUMPI
PUSH1 0x4
places0x4
onto the top of the stack, which is otherwise empty.CALLDATASIZE
gets the size in bytes of the data sent with the transaction (known as the calldata) and pushes that number onto the stack.LT
checks whether the top item on the stack is less than the next item on the stack. In this case, it checks to see if the result ofCALLDATASIZE
is less than 4 bytes. LT pops the top two values off the stack and, if the transaction’s data field is less than 4 bytes, pushes1
onto it. Otherwise, it pushes0
.- Each function is identified by the first 4 bytes of its Keccak-256 hash. By placing the function’s name and what arguments it takes into a
keccak256
hash function, we can deduce its function identifier. - A function identifier is always 4 bytes long, so if the entire
data
field of the transaction sent to the contract is less than 4 bytes, then there’s no function with which the transaction could possibly be communicating, unless a fallback function is defined. - Because we implemented such a fallback function, the EVM jumps to this function when the calldata’s length is less than 4 bytes.
keccak256("withdraw(uint256)") = 0x2e1a7d4d 13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f
- Each function is identified by the first 4 bytes of its Keccak-256 hash. By placing the function’s name and what arguments it takes into a
- Assume the data field of the transaction sent to our contract was less than 4 bytes, then
1
is pushed onto the stack.- The
PUSH2 0x22
instruction pushes0x3f
onto the stack. JUMPI
stands for “jump if.”label
is0x22
, which is where fallback function lives in the smart contract.cond
is1
.jumpi(label, cond) // Jump to "label" if "cond" is true
- The
- Assume the data field of the transaction sent to our contract was less than 4 bytes, then
0
is pushed onto the stack.PUSH1 0xE0 SHR DUP1 PUSH4 0x2E1A7D4D EQ PUSH2 0x2E JUMPI
- When you send a transaction to an ABI-compatible smart contract, the transaction first interacts with that smart contract’s dispatcher. The dispatcher reads in the
data
field of the transaction and sends the relevant part to the appropriate function.
Message Calls
A transaction is a message that is sent from one account to another account. It can include binary data and Ether.
- If the target account contains code, that code is executed and the payload is provided as input data.
- If the target account is not set (the transaction does not have a recipient or the recipient is set to null), the transaction creates a new contract.
- The payload of such a contract creation transaction is taken to be EVM bytecode and executed.
- The output data of this execution is permanently stored as the code of the contract.
- While a contract is being created, its code is still empty. Because of that, you should not call back into the contract under construction until its constructor has finished executing.
The originator of the transaction has to pay gas_price * gas
up front to the EVM executor.
- If some gas is left after execution, it is refunded to the transaction originator.
- In case of an exception that reverts changes, already used up gas is not refunded.
The two types of accounts are treated equally by the EVM, regardless of whether or not the account stores code.
- Every account has a balance in Ether (in Wei) that can be modified by sending transactions that include Ether.
Smart contracts have limited access to other smart contracts.
Contracts can call other contracts or send Ether to non-contract accounts by the means of message calls.
- Message calls are similar to transactions, in that they have a source, a target, data payload, Ether, gas and return data.
- Every transaction consists of a top-level message call which in turn can create further message calls.
- A contract can decide how much of its remaining gas should be sent with the inner message call and how much it wants to retain.
- If an out-of-gas exception happens in the inner call (or any other exception), this will be signaled by an error value put onto the stack. In this case, only the gas sent together with the call is used up.
- In Solidity, the calling contract causes a manual exception by default in such situations, so that exceptions “bubble up” the call stack.
- If an out-of-gas exception happens in the inner call (or any other exception), this will be signaled by an error value put onto the stack. In this case, only the gas sent together with the call is used up.
- The called contract (which can be the same as the caller) will receive a freshly cleared instance of memory and has access to the call payload - which will be provided in a separate area called the calldata. After it has finished execution, it can return data which will be stored at a location in the caller’s memory preallocated by the caller.
- All such calls are fully synchronous.
- Calls are limited to a depth of 1024.
- It means that for more complex operations, loops should be preferred over recursive calls.
- Only 63/64th ( 63/64 rule ) of the gas can be forwarded in a message call, which causes a depth limit of a little less than 1000 in practice.
References