12 min read
Ethereum RPC clients return hexadecimal data, and to humans, this can be hard to interpret without decoding. If you want to understand your blockchain RPC responses quicker, check out the Translate Transaction add-on from the QuickNode Marketplace, which translates hexadecimal values from RPC calls like debug_traceTransaction
into more human-readable format.
Overview
Ethereum settles billions of dollars of value per day (source), and developers building smart contracts need to ensure their smart contracts function as intended. With the complexity of smart contracts growing, this can require devs to dive deeper than just Solidity code but also understand how EVM operates. This guide will provide you with a deeper understanding of how the EVM executes bytecode (opcodes), specifically diving into different EVM components and showing you how a contract is compiled down to bytecode and how a transaction is executed on the EVM step-by-step.
Let's get started!
What You Will Need
Basic Understanding of EVM and Ethereum Smart Contracts. Recommended Reading:
What You Will Do
- Learn about internal components of the EVM (Stack, Memory, Storage, etc.)
- Learn about opcodes and how they execute on the EVM
What is a Virtual Machine (VM)?
Let's briefly recap Virtual Machines (VM) and how they tie into the Ethereum Virtual Machine (EVM). Virtual machines are some pieces of software running that emulate a physical computer and its associated hardware specs. Virtual machines are most commonly accessed via platforms like AWS and GCP, and the software (e.g., Ubuntu, Linux) and hardware (e.g., x64, arm) will vary according to user use-case. The benefits of VMs include the ability for users in any part of the globe to mimic physical machines without needing to actually see or deal with physical hardware.
The VM's lowest level of operations is done via machine code (binary 0's and 1's) which are represented by opcodes like ADD
, PUSH
, POP
, etc., which do some different types of operations (arithmetic, conditionals, etc.). Most developers do not directly use these opcodes today, but others who use languages like Assembly are familiar with it.
What is the EVM (Ethereum Virtual Machine)?
The Ethereum Virtual Machine (EVM) is a virtual machine for the Ethereum blockchain that executes opcodes as defined in its yellow paper (page 28). Many of these opcodes are similar to the ones you would see on a VM, but there are Ethereum-specific opcodes (we'll cover later) that enable the environment and runtime of smart contracts.
The EVM is contained within the Ethereum execution client (i.e., Geth, Nethermind, and Reth, who all have their own EVM implementations), and its role is to execute EVM code and transaction calldata, then update the world state of Ethereum (Storage
, which we'll cover later). The EVM uses a Stack-based machine architecture consisting of components:
Stack
: A list of 32-byte items utilized for holding the inputs and outputs of smart contract instructionsMemory
: Temporary data that is used during contract executionStorage
: Mapping of 32-byte slots to 32-byte values containing keys likenonce
,balance
,code hash
, andstorage hash
EVM code
: Stored in the code is persistent as part of a contract account state fieldProgram Counter (PC)
: A pointer that instructs the EVM which opcode instruction should be executed nextGas
: Every opcode incurs a predetermined fee for its execution, denominated in some gas amount (in wei)
Some of these components are volatile in nature and are only available during transaction runtime, while others are non-volatile and immutable for use in storage and keeping track of state changes. The diagram below demonstrates this architecture.
EVM Architecture (source: Ethereum.org)
Ethereum State Transition Function
It's important to note that all open-source Ethereum client software must adhere to the Ethereum spec (as defined in the yellow paper), ensuring that every transaction execution (given the same state) on every client produces the same result. This is a deterministic output feature of the EVM that allows for transaction predictability and is derived from Ethereum's state transition function (Y(S, T)= S')
, where given old state (S) + a new set of state inputs (T) = you get (S') (new state). In a real-world example, this is similar to how you have block n (which at the time is the latest block), then block n+1 is proposed (with a list of transactions that have state changes), and when block n+1 is mined (becoming the latest block), you have the new state which includes the old state + any new state changes.
In the next section, we'll deepen our understanding of the internal components that make up the EVM.
Stack
The Stack enables the EVM to execute code and transaction instructions, ultimately updating the world state of Ethereum. The Stack is part of the volatile machine state of the EVM, so its operations are only persistent during transaction execution. The Stack is also limited in its operations and computing, so any operations exceeding the limits will result in a Stack error, causing a transaction to revert. In size, the Stack is limited to 1024 elements in total and uses words, which are 256-bit, 32-byte chunks of data. The Stack cannot store values such as arrays, strings, or mappings, so other components like Memory
can be used to store them for reference during transaction execution. A common error developers run into when dealing with the Stack is stack overflow/underflow
and not enough gas
. This occurs when there is an exception on the Stack due to an incorrect amount of items on the Stack or insufficient gas to complete the computation.
The Stack is designed in a Last-In First-Out (LIFO) data format and executes different bytecodes (opcodes) by popping values off the stack, executing the bytecode, and then pushing them back on the Stack. Basic operations of the Stack include opcode instructions like PUSH1
, ADD
, MUL
, POP
(and more), which all rely on the Stack to perform. The Stack can also access other parts of the VM, such as Memory and Storage (i.e., world state), which reference opcodes such as MSTORE
and SSTORE
, both of which we'll cover later.
source: Ethereum illustrated
Memory
Memory on the EVM is linear and volatile, meaning that this data is not persistent across transactions, only during a transaction's runtime. While the costs to store memory during runtime increase quadratically with size, it is still cheaper to use than Storage, which persists in its state. Memory is limited to 256-bit (32-byte) "words" (also known as some chunk of data) and can be accessed with opcodes such as MSTORE
and MLOAD
(and others). Memory allows you to do Stack operations that are longer than 32-bytes but still limited to the maximum gas computation a transaction can endure. As mentioned, memory is most commonly used to store values that cannot be stored in the Stack, such as arrays and strings.
source: Ethereum illustrated
Storage
The world state (aka Storage) of Ethereum is persistent, unlike the Stack and Memory, which are volatile. Storage consists of a keystore of 256-bit to 256-bit values that contain different states of an account (smart contract) such as balance
, nonce
, storage hash
and code hash
. Accounts that are externally owned (EOA; e.g., non-custodial wallets) only contain balance and nonce key stores. Storage uses a modified Merkle Patricia Trie data structure to store accounts (EOA and smart contracts) via hashing and reducing the stored data to a single root hash. This hash represents a mapping between account addresses and account states (e.g., balance, nonce, storage hash, code hash).
The Storage
component of the EVM enables the state to be persistent and modifiable, enabling use cases like updating the token balance of an account. Storage can be accessed via a message call; this is with methods such as eth_call, which is one of the RPC methods on Ethereum implementations like Geth. The cost of Storage on Ethereum is paid via transaction execution, consisting of fees to validators (which can vary on network activity) and the predetermined gas costs for each opcode. Since Storage on Ethereum can get expensive, others look for solutions like IPFS and AWS to store data (e.g., NFT Metadata). Check out our IPFS guides to learn how to pin data on the IPFS network via a UI or API.
source: Ethereum illustrated
The maximum contract size that can be deployed (stored) on Ethereum is 24,576 bytes (24KB); however, this should not be confused with the limit of data that you can store on the smart contract (which is only limited by gas, not size).
EVM Code
Smart contracts are written in higher-level languages like Solidity and Vyper and eventually get compiled down to bytecode (EVM Code
). This bytecode (smart contract instruction) is stored in Storage
(in the account state fields) and can be accessed by other smart contracts and EOAs to read or interact with.
Program Counter (PC)
With the smart contract instructions from the compiled EVM code, the Program Counter
tracks the EVM instruction (opcode) to execute next, ensuring operations are done in the correct sequence.
For example, if the bytecode of a smart contract is 20 bytes long, you should expect to have 20 lines, and each line will be an opcode instruction. You can see how this looks visually with a site like https://evm.codes/
Gas
Gas
can be thought of as the fuel needed for transactions to be executed via the EVM. It is a unit that measures the computational effort required to execute operations. Each opcode executed by the EVM consumes a certain amount of gas, predetermined by the Ethereum yellow paper. The implementation of gas prevents spam (DDos) and ensures that resources are used efficiently.
Now that we've touched on the components of the EVM let's tie these together with opcodes in the next section.
What are EVM Opcodes?
Bytecode, as seen in a contract deployment or transaction calldata, translates to different EVM opcodes and instructs EVM components (e.g., Stack, Memory, Storage) on what to do.
For example, here are some of the more common opcodes you would similarly see in a VM that are also implemented on the EVM:
- Arithmetic Opcodes:
ADD
,MUL
,DIV
(and more) allow for the computation of arithmetic operations - Control Flow Opcodes:
JUMP
,JUMPI
(and more) enable conditional and unconditional branching - Memory and Storage Management Opcodes
MLOAD
,SLOAD
,SSTORE
(and more): There are different opcodes depending on the instruction, such asSSTORE
,SLOAD
,MSTORE
,MLOAD
- and several more
The opcodes above are common to see in other machines like x64, however, Ethereum has introduced new opcodes that are specific to the Ethereum environment itself:
CREATE
: This opcode creates a new smart contract based on the code provided. This uses a larger gas amount and is variable depending on the bytecode being deployed.BLOCKHASH
: This opcode is the hash of one of the 256 most recent complete blocksBALANCE
: Returns the balance of a given accountCALL
: A system operation which consists of a message-call (read operation) into an account- and several more (Ethereum opcodes are more down the list)
Each opcode has some gas associated with its execution, and the cost of this gas will vary depending on the operation being executed. For example, writing to contract storage costs (i.e., SSTORE
) more than other less complex opcodes (i.e., ADD
).
A full list of EVM opcodes can be found in the Ethereum yellow Paper. In the next section, we'll cover how a higher-level smart contract is compiled using bytecode.
From Source Code to Bytecode
Let's walk through the cycle of a Solidity smart contract being compiled down to bytecode and how that bytecode translates to opcodes.
For example, say we have a simple storage contract in Solidity that looks like this:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
To compile this in EVM-readable format, the compiler takes the code from the .sol (Solidity) file and generates .bin (Binary) and abi files, which hold the compiled bytecode and smart contract interface. The contract above does not have any constructor arguments, but if it did, this one-time initialization bytecode would be added to calldata within the contract deployment transaction. This bytecode is different than the bytecode (runtime bytecode) which is stored at a contract's storage space as it contains the constructor bytecode as well.
The hexadecimal bytecode representing this compiled contract will look like this:
6080604052348015600e575f80fd5b506101438061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c806360fe47b1146100385780636d4ce63c14610054575b5f80fd5b610052600480360381019061004d91906100ba565b610072565b005b61005c61007b565b60405161006991906100f4565b60405180910390f35b805f8190555050565b5f8054905090565b5f80fd5b5f819050919050565b61009981610087565b81146100a3575f80fd5b50565b5f813590506100b481610090565b92915050565b5f602082840312156100cf576100ce610083565b5b5f6100dc848285016100a6565b91505092915050565b6100ee81610087565b82525050565b5f6020820190506101075f8301846100e5565b9291505056fea2646970667358221220fe2a712e6758ca6e067fd552b99e33f169a13afa9b0c54fdd2e92518f3aa766764736f6c63430008190033
Note that the exact bytecode output can vary based on the compiler version and compiler optimization settings
This compiled bytecode includes all the smart contract logic, including its functions, conditionals, loops, and state modifications. When an EOA or smart contract interacts with this contract, this bytecode is interpreted by the EVM as opcode instructions; for example, the first byte (60
) translates to the first opcode instruction. You can look up opcodes here: https://ethervm.io/ and see that 60
translates to PUSH1, which pushes the next 1-byte value onto the Stack (which is 80). Therefore, Program Counter (PC) keeps track of each opcode in an array/list fashion so the Stack knows what to execute next. Thefore, the first opcode instruction translates to PUSH 80
on top of the Stack.
The bytecode above translates into Opcode instructions below, which represent the sequence of operations that the EVM will perform when executing this contract.
[00] PUSH1 80 // Push 1-byte of value 80 on the stack
[02] PUSH1 40 // Push 1-byte of value 40 on the stack
[04] MSTORE // Memory store
[05] CALLVALUE // Get deposited value from call
[06] DUP1
[07] ISZERO // A conditional opcode
[08] PUSH2 0010 // Push 2-bytes
[0b] JUMPI // Jump to another location on the stack
[0c] PUSH1 00
[0e] DUP1 // Duplicate 1-byte
[0f] REVERT // Stop execution
[10] JUMPDEST
...
...
[138]
You can copy/paste the Solidity contract above into evm.codes to see the bytecode and full opcode instructions.
In the next section, let's cover the transaction lifecycle and how transaction calldata can be interpreted and how the EVM executes it.
Understanding EVM Transaction Execution
A transaction includes various fields such as the nonce, gas price, gas limit, to (recipient address), value (amount of Ether to send), and data (calldata). Calldata is a crucial part of interacting with smart contracts as the data is encoded and contains the function to call and its input arguments.
Now, at a high level, a transaction including a smart contract interaction looks like this:
to: 0x6b175474e89094c44da98b954eedeac495271d0f
from: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
value: 0x0
data: 0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c
gasPrice: 500000
gasLimit: 210000
The process of the EVM executing this transaction will follow this flow:
- EVM checks the hashed payload (RLP) of a transaction object, decoding values like the recipient, value and payload
- Then confirms the nonce and signature is valid by looking at the transaction's v,r,s values (i.e.,
v
is the recovery id andr
ands
are outputs of an ECDSA signature. More info here), making sure the signature is associated with the sending account owner - Then the EVM creates an empty memory space and Stack context to perform Stack operations
- The EVM then executes each opcode instruction in the bytecode by following the Program Counter, sequentially executing each opcode instruction line by line and then storing the results in the world state
Note the EVM can also return logs, which is a strict WRITE space of the EVM that outputs logs
Let's dive a bit more into how this payload is formatted.
When interacting with smart contracts, the first four bytes of the data specify the function identifier (a hash of the function signature), and the rest of the data represents the encoded arguments. The example value provided demonstrates a call to the set
method of the storage contract, which is encoded in hexadecimal payload as follows:
0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c
The first four bytes in the example above 60fe47b1
(the 0x is appended to the front) represent the function selector of the set()
function. The remaining bytecode refers to its argument (uint; unsigned integer) of hex 0000000000000000000000000000000000000000000000000000000000010f2c
in 32 bytes (padded), which translates to some uint amount. If you go back to the full bytecode of the contract and search for the function selector (60fe47b1), you'll see it in the opcode instructions and corresponds to EVM to execute the PUSH4
(push 4-bytes) opcode (as seen in the yellow paper).
Final Thoughts
That's it for this guide, if you'd like to see a deeper dive into EVM opcodes, leave some feedback below for a part 2! Also check out the list of resources.
Drop us a line in Discord, or give us a follow on Twitter to stay up to date on all the latest information!
Additional Resources
Check out the following resources to strengthen your understanding of Ethereum and EVM bytecode.
- Ethereum Yellow Paper
- EVM.Codes
- Porosity (popular open source decompiler)
- QuickNode Marketplace: Translate Transaction add-on
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.