This is a read note of Mastering Ethereum Ch13: The Ethereum Virtual Machine. The EVM is the part of Ethereum that 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. At a high level, the EVM running on the Ethereum blockchain can be thought of as a global decentralized computer containing millions of executable objects, each with its own permanent data store.
The EVM 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.
The EVM 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) and has several addressable data components:
- An immutable program code ROM, loaded with the bytecode of the smart contract to be executed.
- A volatile memory, with every location explicitly initialized to zero
- A permanent storage that is part of the Ethereum state, also zero-initialized
2 The EVM Instruction Set (Bytecode Operations)
The EVM instruction set offers most of the operations you might expect, including:
- Arithmetic and bitwise logic operations
- Execution context inquiries
- Stack, memory, and storage access
- Control flow operations
- Logging, calling, and other operators
In addition to the typical bytecode operations, the EVM also has access to account information (e.g., address and balance), environment operations, block information (e.g., block number and current gas price) and block operations.
3 Ethereum State
The job of the EVM is to update the Ethereum state by computing valid state transitions as a result of smart contract code execution, as defined by the Ethereum protocol. This aspect leads to the description of Ethereum as a transaction-based state machine, which reflects the fact that external actors (i.e., account holders and miners) initiate state transitions by creating, accepting, and ordering transactions. It is useful at this point to consider what constitutes the Ethereum state.
At the top level, we have the Ethereum world state. The world state is a mapping of Ethereum addresses (160-bit values) to accounts. At the lower level, each Ethereum address represents an account comprising
- 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) and 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.
When a transaction results in smart contract code execution, an EVM is instantiated with all the information required in relation to the current block being created and the specific transaction being processed.
3 Contract Deployment Code
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. When such a contract creation transaction is processed, the code for the new contract account is not the code in the
data field of the transaction. Instead, an EVM is instantiated with the code in the data 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. The deployent bytecode includes the runtime bytecode.
The runtime bytecode, on the other hand, is exactly the bytecode that ends up being executed when the new contract is called, and nothing more; it does not include the bytecode needed to initialize the contract during deployment.