Ethereum Virtual Machine in Elixir
Introduction
This week we, at Mana project, finally made all official EVM tests pass. I think it’s time to write a post describing its inner magic with code examples from our project. So every crypto alchemist1 interested in Ethereum can follow along.
Background
EVM
From Ethereum official website: Ethereum is a decentralized platform for applications that run exactly as programmed without any chance of fraud, censorship or third-party interference.
So what’s Ethereum Virtual Machine? To put it simply, it’s virtual machine that executes machine code compiled from high-level smart contracts programming languages. There are several languages that are compiled to EVM machine code: Solidity (similar to Javascript), LLL (Lisp Like Language) etc.
EVM is a simple stack machine. Its memory is a word-addressed byte array. The stack has a maximum size of 1024.
EVM can execute 132 operations that are divided into 11 categories:
-
Stop and Arithmetic Operation
Examples:
ADD
- adds two first items on stack saves the result and to first stack item.STOP
- halts execution.
-
Comparison & Bitwise Logic Operation
Examples:
LT
- less-than comparison.AND
- bitwise AND operation.
-
SHA3
Examples:
SHA3
- computes Keccak-256 hash.
-
Environmental Information
Examples:
ADDRESS
- gets address of a currently executing account.CALLDATACOPY
- copies input data in the current environment to memory.
-
Block Information
Examples:
NUMBER
- gets the block’s number.TIMESTAMP
- gets the block’s timestamp.
-
Stack, Memory, Storage and Flow Operation
Examples:
POP
- removes an item from stack.JUMP
- alters program counter.
-
Push Operation
Examples:
PUSH1
- places 1 byte item on a stack.PUSH17
- places 17 byte item on a stack.
-
Duplication Operation
Examples:
DUP1
- duplicates 1st stack item.DUP2
- duplicates 2st stack item..
-
Exchange Operations
Examples:
SWAP1
- exchanges 1st and 2nd stack items.SWAP16
- exchanges 1st and 17th stack items.
-
Logging Operations
Examples:
LOG0
- appends log record with no topicsLOG1
- appends log record with one topic.
-
System operations
Examples:
RETURN
- halts execution returning output data.CREATE
- Creates a new account with associated code.
Every operation consumes some amount of gas. Gas is the internal pricing for code execution in EVM. If not enough gas was provided then the execution halts with out of gas error.
How Ethereum’s yellow paper corresponds to programming code
This section is intended for acute readers that read Yellow Paper and want to know how our programming code in Elixir corresponds to execution model described in the paper. If you haven’t read Ethereum’s yellow paper, you can skip this section or return to it later.
Here’s excerpt from Ethereum’s yellow paper:
Let’s exemine programming code file `lib/evm/vm.ex’:
EVM.VM.run/2
- Ξ function from the paper.EVM.VM.exec/3
- X function from the paper.EVM.VM.cycle/3
- O function from the paper.EVM.Functions.is_exception_halt?/2
- Z function.
Examples
Prerequisites
To follow along executing code examples you need to clone our EVM implementation from GitHub:
git clone https://github.com/poanetwork/mana
Fetch dependencies:
./bin/setup
We should print debugging information to see how code executes.
Add these lines to lib/evm/vm.ex
file in cycle/3
method:
...
def cycle(machine_state, sub_state, exec_env) do
operation = MachineCode.current_operation(machine_state, exec_env)
inputs = Operation.inputs(operation, machine_state)
IO.puts("stack:")
IO.inspect(machine_state.stack)
IO.puts("operation: #{operation.sym}")
...
And start Elixir REPL:
cd apps/evm && iex -S mix
Code examples
So finally we came to examples. Examples illustrated here are taken from Ethereum’s official EVM tests.
Example 1
This test is located in VMTests/vmArithmeticTest/add3.json
in Ethereum tests repo.
Let’s create execution environment:
iex> env = %EVM.ExecEnv{
account_interface: %EVM.Interface.Mock.MockAccountInterface{
account_map: %{},
contract_result: %{gas: nil, output: nil, sub_state: nil}
},
address: 87579061662017136990230301793909925042452127430,
block_interface: %EVM.Interface.Mock.MockBlockInterface{
block_header: nil,
block_map: %{}
},
data: "",
gas_price: <<90, 243, 16, 122, 64, 0>>,
machine_code: <<96, 1, 96, 1, 1, 96, 1, 85>>,
originator: <<205, 23, 34, 242, 148, 125, 239, 76, 241, 68, 103, 157, 163,
156, 76, 50, 189, 195, 86, 129>>,
sender: 1170859069521887415590932569929099639409724315265,
stack_depth: 0,
value_in_wei: <<13, 224, 182, 179, 167, 100, 0, 0>>
}
The only field in env variable we are interested in is machine_code
. It is represented as a binary. Let’s decompile it in human readable form:
iex> env.machine_code |> EVM.MachineCode.decompile
[:push1, 1, :push1, 1, :add, :push1, 1, :sstore]
As we can see it places two ones to stack, adds them, places another one to stack and finally stores the second stack item to storage, storage index is the first stack item. Let’s execute it:
iex> EVM.VM.run(1000000, env)
stack:
[]
operation: push1
stack:
[1]
operation: push1
stack:
[1, 1]
operation: add
stack:
[2]
operation: push1
stack:
[1, 2]
operation: sstore
stack:
[]
{..., %EVM.SubState{logs: [], refund: 0, suicide_list: []},
%EVM.ExecEnv{
account_interface: %EVM.Interface.Mock.MockAccountInterface{
account_map: %{
87579061662017136990230301793909925042452127430 => %{
balance: 0,
nonce: 0,
storage: %{1 => 2}
}
},
contract_result: %{gas: nil, output: nil, sub_state: nil}
},
address: 87579061662017136990230301793909925042452127430,
block_interface: %EVM.Interface.Mock.MockBlockInterface{
block_header: nil,
block_map: %{}
},
data: "",
gas_price: <<90, 243, 16, 122, 64, 0>>,
machine_code: <<96, 0, 96, 0, 1, 96, 0, 85>>,
originator: <<205, 23, 34, 242, 148, 125, 239, 76, 241, 68, 103, 157, 163,
156, 76, 50, 189, 195, 86, 129>>,
sender: 1170859069521887415590932569929099639409724315265,
stack_depth: 0,
value_in_wei: <<13, 224, 182, 179, 167, 100, 0, 0>>
}, ""}
As you can see it executes as expected printing exact operations described above. Also note account storage now has new value storage: %{1 => 2}
.
Example 2
This test is located in VMTests/vmBitwiseLogicOperation/and5.json
in Ethereum tests repo.
Again let’s create execution environment:
iex> env = %EVM.ExecEnv{
account_interface: %EVM.Interface.Mock.MockAccountInterface{},
address: 87579061662017136990230301793909925042452127430,
block_interface: %EVM.Interface.Mock.MockBlockInterface{},
data: "",
gas_price: <<90, 243, 16, 122, 64, 0>>,
machine_code: <<127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 127, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238,
238, 238, 238, 239, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238,
238, 238, 238, 238, 238, 238, 22, 96, 0, 85>>,
originator: <<205, 23, 34, 242, 148, 125, 239, 76, 241, 68, 103, 157, 163,
156, 76, 50, 189, 195, 86, 129>>,
sender: 1170859069521887415590932569929099639409724315265,
stack_depth: 0,
value_in_wei: <<13, 224, 182, 179, 167, 100, 0, 0>>
}
Decompiled machine code:
iex> env.machine_code |> EVM.MachineCode.decompile |> IO.inspect(limit: :infinity)
[:push32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, :push32, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238,
238, 238, 239, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238,
238, 238, 238, 238, :and_, :push1, 0, :sstore]
It places 32 bytes to stack two times, then does bitwise AND
operation with them, and stores result under 0th storage index.
Let’s execute it:
iex> EVM.VM.run(100000, env)
stack:
[]
operation: push32
stack:
[115792089237316195423570985008687907853269984665640564039457584007913129639935]
operation: push32
stack:
[108072616621495115728666252674775380750164271619691439750117644576584916463342,
115792089237316195423570985008687907853269984665640564039457584007913129639935]
operation: and_
stack:
[108072616621495115728666252674775380750164271619691439750117644576584916463342]
operation: push1
stack:
[0,
108072616621495115728666252674775380750164271619691439750117644576584916463342]
operation: sstore
stack:
[]
operation: stop
{... , %EVM.SubState{logs: [], refund: 0, suicide_list: []},
%EVM.ExecEnv{
account_interface: %EVM.Interface.Mock.MockAccountInterface{
account_map: %{
87579061662017136990230301793909925042452127430 => %{
balance: 0,
nonce: 0,
storage: %{
0 => 108072616621495115728666252674775380750164271619691439750117644576584916463342
}
}
},
contract_result: %{gas: nil, output: nil, sub_state: nil}
},
address: 87579061662017136990230301793909925042452127430,
block_interface: %EVM.Interface.Mock.MockBlockInterface{
block_header: nil,
block_map: %{}
},
data: "",
gas_price: <<90, 243, 16, 122, 64, 0>>,
machine_code: <<127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 127, 238, 238, 238, 238, 238, 238, 238,
...>>,
originator: <<205, 23, 34, 242, 148, 125, 239, 76, 241, 68, 103, 157, 163,
156, 76, 50, 189, 195, 86, 129>>,
sender: 1170859069521887415590932569929099639409724315265,
stack_depth: 0,
value_in_wei: <<13, 224, 182, 179, 167, 100, 0, 0>>
}, ""}
In debug output we see that after second push32 operation stack items are 108072616621495115728666252674775380750164271619691439750117644576584916463342 and 115792089237316195423570985008687907853269984665640564039457584007913129639935. Let’s calculate bitwise AND
operation between them:
iex> use Bitwise
iex> 108072616621495115728666252674775380750164271619691439750117644576584916463342 &&& 115792089237316195423570985008687907853269984665640564039457584007913129639935
108072616621495115728666252674775380750164271619691439750117644576584916463342
Result is equal to storage value after EVM execution.
Example 3
This test is located in VMTests/vmPushDupSwapTest/dup9.json
in Ethereum tests repo.
Let’s create environment:
iex> env = %EVM.ExecEnv{
account_interface: %EVM.Interface.Mock.MockAccountInterface{},
address: 87579061662017136990230301793909925042452127430,
block_interface: %EVM.Interface.Mock.MockBlockInterface{},
data: "",
gas_price: <<90, 243, 16, 122, 64, 0>>,
machine_code: <<96, 9, 96, 8, 96, 7, 96, 6, 96, 5, 96, 4, 96, 3, 96, 2, 96, 1, 136, 96, 3,
85>>,
originator: <<205, 23, 34, 242, 148, 125, 239, 76, 241, 68, 103, 157, 163,
156, 76, 50, 189, 195, 86, 129>>,
sender: 1170859069521887415590932569929099639409724315265,
stack_depth: 0,
value_in_wei: <<13, 224, 182, 179, 167, 100, 0, 0>>
}
Decompiled code:
iex> env.machine_code |> EVM.MachineCode.decompile
[:push1, 9, :push1, 8, :push1, 7, :push1, 6, :push1, 5, :push1, 4, :push1, 3,
:push1, 2, :push1, 1, :dup9, :push1, 3, :sstore]
It places nine items to stack, then copies ninth stack and places it to stack.
Let’s execute it:
iex> EVM.VM.run(10000, env)
stack:
[]
operation: push1
stack:
'\t' # [9]
operation: push1
stack:
'\b\t' # [8, 9]
operation: push1
stack:
'\a\b\t' # [7, 8, 9]
operation: push1
stack:
[6, 7, 8, 9]
operation: push1
stack:
[5, 6, 7, 8, 9]
operation: push1
stack:
[4, 5, 6, 7, 8, 9]
operation: push1
stack:
[3, 4, 5, 6, 7, 8, 9]
operation: push1
stack:
[2, 3, 4, 5, 6, 7, 8, 9]
operation: push1
stack:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
operation: dup9
stack:
[9, 1, 2, 3, 4, 5, 6, 7, 8, 9]
As you can see it works as expected.
Conclusion
I hope this post was helpful in understanding Ethereum Virtual Machine. Currently we are working hard on our Ethereum client in Elixir.
See also
Footnotes
1 Elixir developer interested in blockchain technologies
Comments