Getting Deep Into EVM: How Ethereum Works Backstage

Fabrisde
6 min readNov 22, 2019

--

This post is a continuation of my Getting Deep Into Series started in an effort to provide a deeper understanding of the internal workings and other cool stuff about Ethereum and blockchain in general which you will not find easily on the web. Here are the previous parts of the Series in case you missed them:

In this part we are going to explore explain and describe in detail core behavior of the EVM. We will see how contracts are created, how message calls work, and take a look at everything related to data management, such as storage, memory, calldata, and the stack.

To better understand this article, you should be familiar with the basics of the Ethereum. If you are not, I highly recommend reading these posts first.

5 resources to get started with ethereum

Throughout this post we will illustrate some examples and demonstrations using sample contracts you can find in this repository. Please clone it, run npm install, and check it out before beginning.

Enjoy, and please do not hesitate to reach out with questions, suggestions or feedback.

EVM: 10,000 ft Perspective

Before diving into understanding how EVM works and seeing it working via code examples, let’s see where EVM fits in the Ethereum and what are its components. Don’t get scared by these diagrams because as soon as you are done reading this article you will be able to make a lot of sense out of these diagrams.

The below diagram shows where EVM fits into Ethereum.

The below diagram shows basic Architecture of EVM.

This below diagram shows how different parts of EVM interact with each other to make Ethereum do its magic.

We have seen what EVM looks like. Now it’s time to start understanding how these parts play a significant role in the way Ethereum works.

Ethereum Contracts

Basics

Smart contracts are just computer programs, and we can say that Ethereum contracts are smart contracts that run on the Ethereum Virtual Machine. The EVM is the sandboxed runtime and a completely isolated environment for smart contracts in Ethereum. This means that every smart contract running inside the EVM has no access to the network, file system, or other processes running on the computer hosting the VM.

As we already know, there are two kinds of accounts: contracts and external accounts. Every account is identified by an address, and all accounts share the same address space. The EVM handles addresses of 160-bit length.

Every account consists of a balance, a nonce, bytecode, and stored data (storage). However, there are some differences between these two kinds of accounts. For instance, the code and storage of external accounts are empty, while contract accounts store their bytecode and the merkle root hash of the entire state tree. Moreover, while external addresses have a corresponding private key, contract accounts don’t. The actions of contract accounts are controlled by the code they host in addition to regular cryptographic signing of every Ethereum transaction.

Creation

The creation of a contract is simply a transaction in which the receiver address is empty and its data field contains the compiled bytecode of the contract to be created (this makes sense — contracts can create contracts too). Let’s look at a quick example. Please open the directory of exercise 1; in it you will find a contract called MyContract with the following code:

pragma solidity ^0.4.21;contract MyContract {
event Log(address addr);
function MyContract() public {
emit Log(this);
}
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}

Let’s open a truffle console in develop mode running truffle develop. Once inside, follow the subsequent commands to deploy an instance of MyContract:

truffle(develop)> compile
truffle(develop)> sender = web3.eth.accounts[0]
truffle(develop)> opts = { from: sender, to: null, data: MyContract.bytecode, gas: 4600000 }
truffle(develop)> txHash = web3.eth.sendTransaction(opts)

We can check that our contract has been deployed successfully by running the following code:

truffle(develop)> receipt = web3.eth.getTransactionReceipt(txHash)
truffle(develop)> myContract = new MyContract(receipt.contractAddress)
truffle(develop)> myContract.add(10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

Let’s go deeper to analyze what we just did. The first thing that happens when a new contract is deployed to the Ethereum blockchain is that its account is created.¹ As you can see, we logged the address of the contract in the constructor in the example above. You can confirm this by checking that receipt.logs[0].data is the address of the contract-padded 32 bytes and that receipt.logs[0].topics is the keccak-256 hash of the string “Log(address)”.

As the next step, the data sent in with the transaction is executed as bytecode. This will initialize the state variables in storage, and determine the body of the contract being created. This process is executed only once during the lifecycle of a contract. The initialization code is not what is stored in the contract; it actually produces as its return value the bytecode to be stored. Bear in mind that after a contract account has been created, there is no way to change its code.²

Given the fact that the initialization process returns the code of the contract’s body to be stored, it makes sense that this code isn’t reachable from the constructor logic. For example, let’s take a look at the Impossible contract of exercise 1:

contract Impossible {
function Impossible() public {
this.test();
}
function test() public pure returns(uint256) {
return 2;
}
}

If you try to compile this contract, you will get a warning saying you’re referencing this within the constructor function, but it will compile. However, if you try to deploy a new instance, it will revert. This is because it makes no sense to attempt to run code that is not stored yet.³ On the other hand, we were able to access the address of the contract: the account exists, but it doesn’t have any code yet.

However, a code execution can produce other events, such as altering the storage, creating further accounts, or making further message calls. For example, let’s take a look at the AnotherContract code:

contract AnotherContract {
MyContract public myContract;
function AnotherContract() public {
myContract = new MyContract();
}
}

Let’s see how it works running the following commands inside a truffle console:

truffle(develop)> compile
truffle(develop)> sender = web3.eth.accounts[0]
truffle(develop)> opts = { from: sender, to: null, data: AnotherContract.bytecode, gas: 4600000 }
truffle(develop)> txHash = web3.eth.sendTransaction(opts)
truffle(develop)> receipt = web3.eth.getTransactionReceipt(txHash)
truffle(develop)> anotherContract = AnotherContract.at(receipt.contractAddress)
truffle(develop)> anotherContract.myContract().then(a => myContractAddress = a)
truffle(develop)> myContract = MyContract.at(myContractAddress)
truffle(develop)> myContract.add(10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

Additionally, contracts can be created using the CREATE opcode, which is what the Solidity new construct compiles down to. Both alternatives work the same way. Let’s continue exploring how message calls work.

Message Calls

Contracts can call other contracts through message calls. Every time a Solidity contract calls a function of another contract, it does so by producing a message call. Every call has a sender, a recipient, a payload, a value, and an amount of gas. The depth of message call is limited to less than 1024 levels.

--

--