Ethereum is a blockchain with a built-in Turing-complete programming language. Anyone can use Ethereum smart contracts to create decentralized applications.
“The Ethereum Virtual Machine (EVM) is the part of Ethereum that handles smart contract deployment and execution” (Antonopoulos and Wood, 2018).
The EVM features a stack-based architecture. To deploy a smart contract, all high-level Ethereum smart contract code must first be compiled into machine-readable code, known as bytecode. The EVM then processes this bytecode using a last-in-first-out (LIFO) stack structure. This operation resembles the Java Virtual Machine (JVM), where each instruction begins with a single-byte opcode followed by optional parameters, with values given in big-endian order (Scott, 2009).
This article’s primary goal is to explain the inner workings of Ethereum’s stack-based EVM. After covering these EVM basics, we will see how Ethereum is organically evolving toward a future powered by WebAssembly.
Understanding WebAssembly (Wasm)
WebAssembly (Wasm) is a new type of code that can run in modern web browsers. It enables new features and offers significant performance improvements. Wasm is designed as an efficient compilation target for low-level source languages like C, C++, and Rust (MDN Web Docs, 2019).
Ethereum WebAssembly (Ewasm)
Ethereum WebAssembly (Ewasm) is a deterministic smart contract execution engine built on the modern WebAssembly virtual machine. Ewasm is a leading candidate to replace the EVM and is part of the Ethereum 2.0 “Serenity” roadmap. There are even proposals to adopt Ewasm on the Ethereum mainnet (ewasm, 2019).
The current architecture of the EVM is one of the biggest obstacles to unlocking its raw performance (GitHub EIP48, 2019). For example, while the 256-bit word length facilitates native hash and elliptic curve operations (Antonopoulos and Wood, 2018), it also makes translating EVM opcodes into hardware instructions more complex than necessary. An architecture closer to hardware mapping would significantly improve Ethereum’s performance (GitHub EIP48, 2019).
Beyond performance enhancements, one of the Ewasm project’s design goals is to support smart contract development across multiple languages and tools, incorporating LLVM, C, C++, Rust, and JavaScript into the development cycle.
Theoretically, any language that can compile to Wasm can be used to write smart contracts, provided it implements the Ewasm Contract Interface (ECI) and the Ethereum Environment Interface (EEI) (ewasm, 2019).
Smart Contracts
To understand the EVM’s essential components, let’s explore how Ethereum smart contracts are created, compiled, and deployed using the original EVM architecture.
An Ethereum smart contract acts like an “autonomous agent” within the Ethereum execution environment. When triggered by a message or transaction, it always executes a specific code segment, directly controls its own Ether balance, and manages its key/value storage to track persistent variables (Buterin, 2013).
Each high-level smart contract source programming language, such as Solidity, Vyper, and Lity, maintains its own compiler. Smart contract source code can be compiled into various outputs, including but not limited to the Application Binary Interface (ABI), bytecode streams, and opcodes.
Compiling Smart Contracts
Before installing a compiler on your local machine, it’s advisable to try web-based compilers like SecondState’s BUIDL environment. This can save you a significant amount of time.
Let’s use the simple storage source code and compile it using SecondState’s BUIDL environment, as shown below.
Clicking the compile button immediately generates the smart contract’s ABI and bytecode.
If you prefer to perform this compilation via the command line using a locally installed compiler, refer to Appendix A at the end of this article.
Analyzing Deployment Bytecode
If we examine the first four instructions in the bytecode, we see the following:
60 80 60 40 5234
Referring to the mnemonic representations of these values as cited on page 30 of the Ethereum Yellow Paper, we find that the first instruction (60) is PUSH1.
δ
The column labeled δ to the right of the mnemonic indicates the number of items to be removed from the stack by the PUSH1 instruction—in this case, 0.
α
The next column, labeled α, represents the number of additional items to be placed on the stack by the PUSH1 instruction. Here, it is 1: a single byte, 0x80.
In the following example, we see that the first instruction (0x60) PUSH1 pushes the value 0x80 onto the stack, and the second instruction (0x60) PUSH1 pushes the value 0x40 onto the stack.
60 80 60 40…
PUSH1 0x80 PUSH1 0x40…
Moving further along the bytecode, we encounter the value 0x52. As shown in the Yellow Paper, this instruction has the mnemonic representation MSTORE.
60806040 52 34
Looking again at the δ column, we see that MSTORE consumes the top two items from the stack. In total, MSTORE consumes two items from the stack but returns zero items to it.
At this stage, it’s important to note that MSTORE is a memory operation tasked with saving a word to memory. Don’t be confused by the term “word” here.
The EVM has a word length of 256 bits (Antonopoulos and Wood, 2018).
This “word” isn’t a textual word; it could be an account address, for example.
MSTORE begins its operation by consuming the current entry at the top of the stack—an address specifying where the word will be stored in memory. In this case, the address location is 0x60. MSTORE then uses the next entry on the stack and saves it (0x80) to the pre-specified address (0x60). At this point, no entries remain on the stack.
The next instruction is (0x34), which has the mnemonic representation CALLVALUE.
60806040 5234
If we pay close attention to the column labeled α, we see that CALLVALUE will place one item on the stack as part of its standard operation. However, we just mentioned that the stack is currently empty, which raises the question: How does CALLVALUE obtain the data to place on the stack?
From the Yellow Paper, we see that all instructions with values in the 30s (0x30 to 0x3e) relate to environmental information. In this case, CALLVALUE obtains its required data from the message call responsible for executing this bytecode. Another example of environmental information is instruction 0x33, which has the mnemonic CALLER. The CALLER instruction automatically retrieves the address of the Ethereum account that initiated the bytecode execution.
Deployment vs. Runtime Bytecode
At this point, it’s essential to distinguish between deployment bytecode and runtime bytecode. If you review Appendix A.1 (obtaining deployment bytecode) and Appendix A.2 (obtaining runtime bytecode), you’ll notice that the returned bytecode results are not identical.
Runtime bytecode is the bytecode executed when calling a function of a deployed smart contract.
Deployment bytecode contains additional instructions relevant only to deployment.
Interestingly, runtime bytecode can always be viewed as a subset of the code that resides verbatim within the deployment bytecode, as illustrated below.
Analyzing Runtime Bytecode
Each smart contract function can be identified by a 4-byte function signature within the runtime bytecode. To calculate the function signature, we start with the function name. In our example, let’s use the function “set.”
In addition to the function name “set,” we include the function’s input parameter data types (comma-separated and enclosed in parentheses). In our simple case, we end up with the text “set(uint256).”
Note: When creating the function selector text, do not use any spaces.
With this information, we create the hexadecimal representation of the sha3 hash and truncate it to only 4 bytes. Here are examples in web3.js and web3.py.
selectorHash = "0x" + str(web3.toHex(web3.sha3(text="set(uint256)")))[2:10]
var selectorHash = web3.sha3("set(uint256)").substring(0,10)
Both commands return the following signature: 0x60fe47b1, which we can easily locate in the deployment and runtime bytecode data.
At this stage, we understand how each instruction in the bytecode is executed—fetching and obtaining information from the stack, then calling environmental and memory functions. With a solid understanding of how called code uses environmental information to perform state transition functions, let’s proceed to Ethereum’s specific Ewasm implementation details.
Ewasm Implementation
As mentioned earlier, smart contract source code can be compiled into various outputs. The path from high-level smart contract code to Ewasm is complex and can involve multiple compilation routes through different toolchains.
Developers at Second State recently built a Solidity-to-Ewasm compiler called Soll.
Second State completed a prototype demo of this challenging task ahead of two significant Ethereum conferences:
- Crosslink: A leading global conference for blockchain researchers and developers in Taipei.
- Devcon5: The international Ethereum developer conference in Osaka, Japan.
Crosslink (October 2019)
Second State developer Hung-Ying-Ting received an Ethereum Foundation grant for the Soll compiler project (photographed with Vitalik Buterin at the Crosslink event in Taipei).
Devcon5
For those who didn’t attend Devcon5, the following video overview demonstrates Soll’s operation: deploying and interacting with an Ethereum ERC20 Solidity smart contract on the new Ethereum Ewasm testnet.
https://www.youtube.com/watch?v=X-A6sP_HTy0
Devcon5 was an excellent venue for sharing the Second State team’s latest Ewasm developments. Following a formal presentation on the first day of the conference about Second State’s proposed solutions for Ethereum 1.X and Ethereum 2.0, an informal public demo and discussion took place.
Post-demo discussions with Christian Reitwiessner of the Solidity team and others highlighted the best directions for collaboration and reducing duplicate work among developers and software projects in the future Ewasm landscape.
After successfully prototyping the compilation from Solidity to LLVM to Ewasm, Second State, taking Christian’s valuable feedback into account, will focus on implementing a compilation path from Yul to LLVM to Ewasm.
Yul
Yul is an Ethereum-specific intermediate language. Future versions of the Ethereum Solidity compiler (and likely the Vyper compiler) will fully support Yul as an intermediate language.
Yul is designed to be usable for EVM 1.0, EVM 1.5, and Ewasm. Its core components include functions, blocks, variables, literals, for loops, if statements, switch statements, expressions, and assignments to variables (Solidity-Yul, 2019).
A backend or target is a translator from Yul to a specific bytecode. Each backend can expose functions prefixed with the backend’s name. Yul reserves the evm_ and ewasm_ prefixes for the two proposed backends (Solidity-Yul, 2019).
Substantial work has already been done in this area, so let’s briefly examine the known parts of the path from Solidity to Yul to Ewasm.
Compiling Solidity to Yul
The Solidity compiler has a special flag that can be used to compile Solidity source code into Yul’s intermediate representation (IR).
To demonstrate this functionality, we use the Ethereum Solidity compiler on the command line to compile the simple storage example from above into this YUL IR format.
As shown in the following code, we can use the IR flag to perform this task.
Compiling Yul to Ewasm
A simplified overview of the path from Yul includes the following steps.
Starting with EVM-Yul code, we need to:
Split each 256-bit (32-byte) variable into four separate 64-bit (8-byte) variables, noting endianness differences.
Create a library that implements each EVM opcode using built-in Ewasm equivalents as user-defined functions, and apply a regular optimizer.
More Solidity Compilation Examples
If we feed the above YUL IR code to the Solidity compiler, it can generate pretty-printed, binary-representation, and text-representation formats.
This can be achieved with the following command:
solc --strict-assembly --optimize ~/simple_storage/simple_storage_yul_ir.txt
The respective outputs are as follows:
An Example of Inner Workings
Christian’s code example from his Devcon5 demonstration shows the inner workings of the Ewasm-style compilation process. More specifically, it illustrates the work required to produce an equivalent of the MSTORE function. As shown in the demo using the original EVM, MSTORE accepts two parameters: first, a standard 32-byte address, and second, a 256-bit (32-byte) word. However, the following code shows how the original Solidity smart contract’s 256-bit variable is split into separate 64-bit variables. You’ll also notice that an endianness swap occurs.
1 2 3 4 5 6 | function mstore (x1, x2, x3, x4, y1, y2, y3, y4) { let pos := u256_to_i32ptr (x1, x2, x3, x4) i64.store (pos, endian_swap (x1)) i64.store (i64.add (pos, 8), endian_swap (x2)) i64.store (i64.add (pos, 16), endian_swap (x3)) i64.store (i64.add (pos, 24), endian_swap (x4)) } |
Available at: http://chriseth.github.io/notes/talks/yul_devcon5/#/11
Endianness
Swapping endianness (the byte order for storing and retrieving bytes from memory) is crucial when compiling for Ewasm for the following reason. The Ethereum Virtual Machine specification adopts big-endian byte order (Wood, 2014). However, the WebAssembly specification requires all values to be read and written in little-endian (webassembly.github.io, 2019).
As this important work progresses, other details are currently under consideration/discussion. For brevity, some of these are outlined in Appendix B below.
Conclusion
There are multiple paths between smart contract code and Ewasm. Using Yul will provide target endpoints for current Ethereum compilers and entry points for LLVM-to-Ewasm compilers. A compiler from Yul to LLVM to Ewasm will bring the fundamental advantages of Wasm/Ewasm to any Yul-compatible smart contract language, such as Solidity and Vyper.
Using Yul is a major win because it allows reusing almost all optimizer components (Reitwiessner, 2019).
Moreover, since other languages like Rust also use LLVM as their primary code backend (Rust-lang.github.io, 2019), the above toolchain path will open the door for other programming languages to become part of Ethereum’s Ewasm smart contract ecosystem.
👉 Explore advanced Ethereum development tools
Frequently Asked Questions
What is the main difference between EVM and Ewasm?
EVM is Ethereum's current virtual machine for executing smart contracts, using a stack-based architecture and specific bytecode. Ewasm is a new engine based on WebAssembly, designed to be more efficient, interoperable with multiple programming languages, and better suited for modern hardware.
Why is Ewasm considered important for Ethereum’s future?
Ewasm aims to significantly improve performance, enable smarter contract development using popular languages like C++ and Rust, and facilitate easier integration with existing developer tools. It is a key component of the Ethereum 2.0 upgrade, enhancing scalability and usability.
How does Yul fit into the Ewasm ecosystem?
Yul is an intermediate language that can be targeted by high-level smart contract languages like Solidity. It serves as a common layer that can be compiled to multiple backends, including both EVM and Ewasm, reducing duplication of effort and streamlining the development of new compilers.
Can existing Solidity smart contracts be ported to Ewasm?
Yes, through compilers like Soll, which translate Solidity code to Ewasm via LLVM or Yul. However, some adjustments may be needed for full compatibility, especially concerning storage handling and environmental interactions.
What are the benefits of using a WebAssembly-based virtual machine?
WebAssembly offers near-native execution speed, support for multiple programming languages, and a growing ecosystem of developer tools. These features make it easier to build complex, high-performance decentralized applications on Ethereum.
How does Ewasm handle deterministic execution?
Ewasm is a deterministic subset of WebAssembly, meaning it excludes any features that could lead to unpredictable behavior. Validator contracts are proposed to check and ensure that only deterministic code is deployed on the network.
Appendix A: Compiling the Simple Storage Smart Contract via Command Line
As mentioned in the article, compilation can be performed manually via the command line. For example, after installing Solidity, Vyper, or Lity, you will have access to the appropriate local compilation environment.
Here is an example of how to use SecondState’s Lity compiler to compile the simple storage smart contract for various outputs.
A.1 Obtain Deployment Bytecode
lityc --bin ~/simple_storage/simple_storage.sol
A.2 Obtain Runtime Bytecode
lityc --bin-runtime ~/simple_storage/simple_storage.sol
A.3 Obtain Opcodes
lityc --opcodes ~/simple_storage/simple_storage.sol
A.4 Obtain ABI
lityc --abi ~/simple_storage/simple_storage.sol
Appendix B: Topics Under Consideration/Discussion
- Interaction with the rest of EVM 1.x: Besides typical bytecode operations (stack, memory, and storage access), the original EVM can access account information (address and balance), block information, and current gas price (Antonopoulos and Wood, 2018). How Ewasm will interact with the rest of the EVM state (contract storage, Ethereum balances, etc.) is yet to be fully resolved under the Ethereum 1.x roadmap. One approach is to prevent Ewasm code from directly accessing EVM state but allow it to exchange input/output when called (Docs.ethhub.io, 2019).
- Deterministic behavior: Ewasm is a subset of Wasm, which has some non-deterministic features. As Ewasm evolves, a method is needed to reject any contract with non-deterministic characteristics (Docs.ethhub.io, 2019). Currently, this may be achieved using a sentinel contract—a system contract (part of genesis or enabled via hard fork) that validates contracts during deployment and handles invalid operations.
- Gas metering: Calculating gas for Ewasm contract deployment and interaction is not yet finalized. Proposed methods include automatic gas upper-bound estimation through static analysis of bytecode, calculating an upper bound for executed instructions (virtual gas) for code subsets (Docs.ethhub.io, 2019).
👉 Get more strategies for blockchain development
References
- Antonopoulos, A. and Wood, G. (2018). Mastering Ethereum, 1st Edition. O’Reilly Media.
- Docs.ethhub.io. (2019). Ethereum 1.x — EthHub.
- Ewasm. (2019).
- GitHub EIP48. (2019). Ethereum EIP48.
- MDN Web Docs. (2019).
- Reitwiessner, C. (2019). Yul, eWasm, and the Progress and Future Plans of Solidity.
- Rust-lang.github.io.
- Scott, M. (2009). Programming Language Pragmatics, Third Edition. Amsterdam: Elsevier/Morgan Kaufmann Pub.
- Solidity — Yul. (2019). Yul — Solidity 0.5.12 Documentation.
- Webassembly.github.io. (2019). WebAssembly Specification.
- Wood, G. (2014). Ethereum Yellow Paper. Ethereum: A Secure Decentralized Generalized Transaction Ledger.