Understanding EVM Storage

Understanding EVM Storage

·

7 min read

EVM stands for Ethereum Virtual Machine. Thousands of nodes (systems run by individuals like ourselves) run a piece of software which is known as the EVM. The EVM is used to perform the operations and calculations as instructed by the smart contracts. These smart contracts are compiled into bytecode which is then run by the EVM.

Storage in EVM

In EVM, data can be stored in 6 different types which are as follows:

  1. Stack:
    A stack is a very simple data structure and to compare, imagine stack of books. That is similar to a visual representation of a stack. Each book that is added is put on top of the stack, and if you were to take the book then you will need to take it from the top of the stack, all the way down and that is how data is stored in a stack data structure.

The operations we perform on this data structure are push (add) and pop (remove) operations. The data that is pushed into the stack is of 32 bytes each (a byte consists of 2 bits each). In any stack, the top 2 items are used to interact with the opcodes. However, when data is larger than 32 bytes, it can’t be processed by stacks, for example string, array or struct.

  1. Memory:
    In a memory, we can write to specific locations (bits). EVM uses a reserved memory space to process that data and server it to stack in chunks of data that can be stored easily. Whenever the memory needs to perform operations such as abi-decoding or abi-encoding or hashing functions via keccak256, it uses the memory as a scratchpad and returns the final value. This allocation is provided from memory for the first 2 — 32 byte words (offset 0x00 and 0x20).

    0x40 is reserved as the home to the free memory pointer and it points to 0x80 which is because the free memory pointer points to the first spot in memory/word that is free and isn’t reserved for something else. So if we ever want to write to the memory, we can read the free memory pointer and safely write to it without overwriting any pre-existing data.

    Using memory is cheaper in terms of gas but it is not persistent and it is volatile which means that it gets erased if any external function call is made which makes it mutable. When we say this, it does not mean the memory cleans up but memory is specific to an execution instance so in the context in which a contract is called, a unique memory instance will exist for it. To conclude, EVM memory is specific to:
    1. a message call
    2. the execution instance of the called contract

    However, 0x60 is not used as it is always empty and filled with zeroes and the reason behind that is that it is used as an initial value for dynamic arrays as a ‘0’ value which is why it should never be written to which is why free memory pointer skips it and points to 0x80 which.

This has been done from a slight side of optimizing the code as well. If say a transaction reverts for a reason then for the initialization process of the memory — we have a limited number of words to load.

  1. Calldata:
    Calldata stores arbit bytes but the difference is that we can only read from Calldata but can't write anything to it. This is why, it is much more cheaper (in gas terms) to read data from here. It is used in cases where we can read something from Calldata to use in a transaction, this prevents the need to store the data into memory. If there is a parameter that we know we wouldn't need to modify then reading from Calldata is the way and it shouldn't be copied to memory unnecessarily.

  2. Storage:
    Storage is a 'permanent' space. It is used in cases where this stored data can be used for future operations. Writing data to storage is the most expensive operation in EVM in terms of gas. The content of storage can be changed with sendTransaction calls and since these calls change the state of the blockchain, the contract-level variables are also called state variables. A contract can never read/write to a storage other than its own unless contract B exposes a function that specifically allows it to do so.

    Initializing a storage slot costs about 20000 gas, editing the value at a storage costs about 5000 gas and deleting the value at a storage slot gives a refund of 15000 gas. A smart contract’s storage consists of 2²⁵⁶ slots, where each slot can contain values of size up to 32 bytes.

  3. Code:
    In the Ethereum ecosystem, we have 2 types of accounts:
    1. Externally Owned Account: these are operated by anyone with private keys
    2. Smart Contracts: deployed on the network, controlled by code

    The EVM stores the bytecode in a separate virtual read-only memory (ROM), which means that the instruction data stored in the code is immutable which also points that the data and variables stored in the code are read-only and immutable as well.

    To understand where is this stored, the Ethereum yellow paper states that this contract's code fragments are stored in the state database under their corresponding hashes:

    A diagram showing the make up of an account

    As the image from Ethereum shows above, the storage-hash and the code-hash are stored with the account itself and the actual bytecode is stored in a low-level database of the Ethereum client under a key which corresponds to the keccak256 hash of the contract bytecode.

    There are 2 reasons for this setup, one is to maximize the performance as when the nonce, balance or storageRoot of a smart contract changes, we need to rehash the 4 elements of the contract's account state again (nonce + balance + storageRoot + codeHash). If we used code, then we would have to perform a much heavy (and expensive) computation which can be provided by using codeHash, which never changes. Secondly, this is done to save space. In an instance where there are multiple use-cases for a bytecode, instead of storing the same bytecode at 10 different places, we just store the codeHash and store it under the smart contract address.

  4. Logs:
    Developers can also store the data in logs. Currently EVM provides 5 opcodes for emitting log events: LOG0, LOG1, LOG2, LOG3, LOG4. As specified in the Ethereum Yellow Paper:

    LOG0 can store no topic value whereas LOG4 can store 4 topic values which are 32-byte "words" - used to describe the change. The first part of a log consists of an array of 'topics' used to describe the event. First topic usually consists of the signature (keccak256 hash) of the name of the event that occured including the type of parameters. However, anonymous events do not hold such signatures.

    The second part of the log stores the data. The 'topics' are limited to 4*32 bytes whereas the data is not limited which gives us 2 things:
    1. Data in logs can be large and complicated such as arrays and strings
    2. Topics are searchable but data is not

    Currently the base cost for a logging operation is 375 gas. To top it all, every included topic cost an additional 375 gas with each byte of data costing another 8 gas each.

Compilation Process

While working with solidity, compiling a .sol file gets us a .abi and .bin file. Solidity is one of the EVM Languages (amongst Vyper, Yul, Cairo, Rust etc). Once compiled, those files are put through a solidity wrapper generator which generates a wrapper (an interface that can be used to integrate into our applications) that allows us to call the solidity code.

We use a web3 library that talks to our solidity code via the internet. Our solidity code runs inside EVM (in an Ethereum client - an instance of software running on a node in the network), on the Ethereum network. Here since the client is the one running it as well as processing the requests so the machine is itself the client and the server.


References:

  1. Owen Thrum's EVM Explanation

  2. Jean Cvllr : EVM Memory Blog

  3. Luit Hollander Event Logs Blog

Did you find this article valuable?

Support hexbyte by becoming a sponsor. Any amount is appreciated!