The basic architecture of Elrond is a plug-and-play type, where high level components connect/communicate with each other through well defined interfaces. A virtual machine which deploys and executes smart contracts in a specific language is a high level plug-able component in this architecture.
One such VM has to satisfy an interface and gets read-only access to the data existing in the blockchain through a set of hooks. Through this interface the VM gets access to cryptographic and hash functions as well. A smart contract can interact with the blockchain through a well-defined API. All outputs generated by the SmartContract (e.g. balance change), is saved in a so-called VMOutput at the VM’s level. After the complete execution of the smart contract the VM output is returned to the protocol. The protocol then does the basic verification and updates the state trie of the modified accounts.
Furthermore, reading from the blockchain is optimized by saving the data from an accessed account into a cache. At consequential reads through the blockchain hooks, the response comes directly from the cache.
Until now, Elrond had one VM component deployed with the protocol: a formally verified VM for the IELE language, generated by the K Framework with our Go backend. The language is great as it was created for blockchain smart contract purposes, the VM is formally verified meaning it is mathematically demonstrable that all behavior is deterministic.
But IELE is not a well known language for smart contracts, so we asked ourselves, why not support more languages, more VMs, each being optimal for different things. And because VMs in our architecture are already a modular component, we can create more such components, put them into a container and call the one which is needed for the job at any specific time. So having this in mind we decided to go on and search for the next VM to be integrated into Elrond.
We had a few goals in mind when choosing a VM and one or more languages to be supported by the Elrond protocol:
- Smart contract code should be easy to read and easy to write.
- Catch obvious bugs at compile-time, and it should have a linter for anything that is impossible to catch at compile-time.
- Compile to very compact bytecode to ensure that it’s cheap to deploy smart contracts.
- It should be easy to discover how to use the API
- It should integrate as well with the rest of the ecosystem as possible, so that the tools can be reused and avoid re-implementing code.
- Tooling support and determinism for general-purpose languages
And the choice was: WASM
WebAssembly (often shortened to Wasm) is an open standard that defines a portable binary code format for executable programs, and a corresponding textual assembly language, as well as interfaces for facilitating interactions between such programs and their host environment. The main goal of WebAssembly is to enable high performance applications on web pages, but the format is designed to be executed and integrated in other environments as well.
WebAssembly is a VM target that (unlike most virtual machines) generally tries to match the semantics of a CPU architecture like ARM or x86 rather than trying to match the semantics of the language that runs on it. As a result, many languages can compile to it unchanged. Since most programming languages are built to expose the semantics of the CPU at some level, having the VM emulate a CPU is a good lowest common denominator. It also allows us to use world-class optimising compilers that were built for ISAs like x86 and ARM — very important when you’re charging by the instruction. solc has an — optimize flag but as far as I can tell all that it does is remove unused jump targets from the list of allowed targets in the assembly’s metadata, which is more of a security feature than an optimisation.
One downside, however, is that these general-purpose languages were not built to write smart contracts in. That, however, can be solved with the right library.
Wasm expands the family of languages available to smart contract developers to include Rust, C/C++, C#, Typescript and much more.This means you can write smart contracts in whichever language you’re familiar with, compile it of WASM and even easily debug its WAT human readable format. Additional benefits to WebAssembly:
- Memory-safe, sandboxed, and platform-independent.
- 64 and 32-bit integer operation support that maps one-to-one with CPU instructions.
- Easily made deterministic by removing floating point operations
- Supported by the LLVM compiler infrastructure project, meaning that Wasm benefits from over a decade of LLVM’s compiler optimisation.
- Continually developed by major companies such as Google, Apple, Microsoft, Mozilla, and Facebook.
As for the implementation part we looked at existing integration and debated whether to use interpreters or compilers. One of the reasons of choosing WASM was speed: we wanted to have the fastest VM / engine out there. These engines are mostly written in C++ and our protocol is in GO, but with cgo those two can work well together.
We wanted to use WASM engines at their best, bare as they are, with as little overhead as possible, preferably none. The eWasm team did a great job of integrating WABT, Bynarien and WAVM through the heraVM into Ethereum and the code looked promising. The WASM engines were left as clean as they are, adding the definition of the blockchain hooks as defined functions at every smart contract run. (BTW this is a place of further improvement — make those calls part of the underlying system, do it only once, not at every run/deploy).
So we took the code and started to create our own adapter to it, one that supports the Elrond VM interface. But while writing the adapter we made several optimizations, in order for the smart contracts to run faster. A smart contract can access several “system” functions to read and write into the state of the blockchain, to manipulate the balance of the accounts. In our integration of hera, we bring these functions and data as close as possible to the running engine — having most of the data in the adapter directly.
So, the smart contracts make use of he blockchain hooks only when the adapter does not have the data the smart contract is asking for. If an account was touched through a smart contract run, all of its data is brought to the adapter. The next reads from that accounts will be resolved directly at the adapter — no more overhead of going all the way to the blockchain.
Furthermore, in Elrond the Virtual Machines only get read access to the blockchain, while any writing they do is made to a local cache, which is written to the state only at the end of the smart contract processing. Writing to these caches is super fast, and in the case of hera this happens directly in the adapter. The final writing into the blockchain state is done only by the protocol, after basic validation — keeping the system safe at all the time.
As hera implemented the Ethereum Environment Interface and we created an adapter over it, this means Elrond directly supports the EEI: smart contracts written for Ethereum can be deployed and ran in Elrond protocol as well (with a small modification: changing the address handling from 20 bytes to 32 bytes).
Our benchmarks for smart contracts running on the protocol using WASM:
- Fibonacci 32 is used in many of the open benchmarks in the space, so we did the same. We wrote a simple contract in C, compiled it to WASM, deployed it in the protocol and called it via transactions. The results: 16ms average processing time
- CPU calculate 8000 was tested as well. Compiled the same way as fibonacci 32. The resulting average processing time: 3.9ms
- String concat 10000: concatenating a 44 character long string 10000 times took an average of 6.7ms.
- Another test was running ERC20 contracts. Our test contained 100K ERC20 transfer transactions, running that batch several times. The average processing time for a transfer in an ERC20 contract is 0.29ms. This means in a single block, where the block creation limit is of 1 second, the protocol is capable of putting ~3350 ERC20 transactions (including validation, processing and commit). In Elrond a round is of 4 seconds, the first second is left for the leader to create a block, the next 3 seconds are for propagation, validation, signing.
How do we decide which VM is needed for a smart contract?
In the Elrond protocol the address of any account is of 32 bytes, so there is a big room to play with these bytes and add some meaning to some of them. In case of smart contract we have the following convention for addresses:
- first 8 bytes are zero
- the next 2 bytes define the Virtual Machine Identifier on which the smart contract will run
- Bytes 11–30 are equal with sha256(smart_contract_owner_address + nonce)[11:30]
- The last 2 bytes is the shard identifier
At deploy time, the owner of the smart contract sends a transaction to the reserved address 0x0000000000000000000000000000000. The protocol knows that this transaction is a special type: smart contract deployment. In the data field of the transaction the owner sends the byte code of the smart contract in hex and the virtual machine identifier (2 bytes) separated by the ‘@’ character. At deploy time, the address of the smart contract is calculated (sha256(smart_contract_owner_address + nonce)) and the code of the smart contract is saved into the state of that address.
Transactions that call smart contracts will contain the address of the deployed smart contract, the protocol will know from the address which VM it has to call, as the identifier is saved into the address. The function and the arguments for the smart contract call are saved in the data field as well, as a combined string where the first part is the function name followed by the arguments encoded in hex and separated by the ‘@’ character.
In case of hera, in order to make even the function calls compatible with Ethereum contracts, the adapter changes the hex encoded Elrond type input into Ethereum format: a byte array with the following mentions:
- first 4 bytes are the function selector (sha256(function)[0:4])
- each argument is translated from hex into a 32 bytes long array
- Further optimization of the integrated engines and creating a set of basic libraries with the most needed functionalities.
- But API and scripts are not enough, in order for a developer to feel good when creating a new application they need tools. Elrond will push forward and build/integrate several tools for developers, to make it easy to develop new and amazing dApps.
Elrond is built with security and speed in mind. We have shown transaction processing speeds of more than 50k TPS on our sharded architecture and a mature, fast VM is imperative to keep up with our protocol. Integrating WASM, a crucial component of the new decentralized web, is a step forward in achieving very high throughputs even for smart contracts transactions. The advantages of WASM don’t stop here: support for multiple languages, sandboxed environment, developed and maintained by major companies. Our super-fast, specialized adapter combined with the bare-boned WASM engines, have reached very high processing speeds.