The Bright Future of WebAssembly Smart Contracts — Part 1
When it comes to smart contracts there are two competing technologies. The original EVM smart contracts enjoy wide support and adoption, while WebAssembly is an emerging standard that promises higher performance and has great support in the Polkadot space. In this article, we analyze and compare both technologies.
At Pendulum we strive to provide the best experience for our ecosystem developers. In this comparison, we will focus on performance. Higher performance means more transactions per second, i.e., higher scalability. Most smart contracts are rather simple and only require a few computational resources. However, as more advanced DeFi applications with blockchain interoperability and a dense network of bridges are developed, the complexity of smart contracts will increase. For example, executing a Merkle proof of a foreign blockchain that uses an exotic hashing algorithm is a heavy load for a smart contract engine.
Keep Interpreting and Nobody Explodes
Developers write smart contracts using a smart contract language, the most well-known being Solidity. They then translate it into the actual smart contract by a tool called compiler. The smart contract is a sequence of very basic instructions like “load this value” or “add these two numbers”. This sequence of instructions is called bytecode. The set of instructions that the compiler can use to build up the bytecode and their precise behavior are predefined in a virtual machine standard. In this article we will consider two such virtual machines, the Ethereum Virtual Machine (EVM) and WebAssembly (Wasm).
The developer will upload the bytecode to a blockchain where it will eventually be executed by an executor, which is a part of the blockchain validator nodes. There are two different approaches of how to execute the bytecode:
- Interpret: This is the basic approach where the executor follows instruction by instruction of the bytecode and performs the behavior defined by the instruction. For example if the executor identifies that the next instruction is the “add two numbers” instructions, then it will actually add the two specified numbers. If you ever used a simple console emulator to play old console games on a PC, this is precisely what the emulator does.
- Compile: A more sophisticated executor would first translate the bytecode again into machine code that can be directly executed on the blockchain node target machine, i.e., another compilation step. This allows us to greatly optimize the resulting code and can give a tremendous performance boost as we will see.
Obviously a compiling executor is desirable because of the increased performance. However, compilers are prone to compiler bombs: the optimization steps a compiler performs can be quite complex and even for small smart contracts can take an unexpectedly long time. The computational resources of blockchain validator nodes are heavily limited and everyone using them needs to pay a fair amount of gas up front. An attacker could cleverly hand craft a small smart contract that leads to a compiler bomb. This would cost only a small amount of gas but leads to a denial of service of the validator nodes. Additionally compilers have the downside that they first have to compile the code — which takes some amount of time even without compiler bombs — whereas interpreters can start to execute the code immediately.
There is ongoing research for one-pass compilers that are in the middle ground: they are not prone to compiler bombs but the compiled code is not as efficient.
Rich Choice of Alternatives
For our performance analysis we considered the following smart contract languages:
- Solidity: This is clearly the language with the highest adoption. Developers can profit from a huge readily available library of smart contracts.
- Ink!: This is Parity’s solution to writing smart contracts for Substrate-based blockchains. It is an early stage language based on Rust and can be compiled to Wasm through the standard Rust toolchain.
- Wasm Text Format (Wat): In addition to its binary bytecode format, Wasm also has a text format so that a developer can directly write a smart contract in WebAssembly. This format is reminiscent of conventional assembly languages for CPUs. This can be tedious for larger smart contracts but can be useful to get the last bit of performance out of a smart contract.
We considered three executors:
- Go Ethereum (Geth): is the most widespread Ethereum client and is an executor for EVM bytecode.
- EVM pallet: is a component for Substrate based blockchains that executes EVM bytecode.
- Contracts pallet: is a component for Substrate based blockchains that executes Wasm smart contracts.
Since Pendulum uses Substrate, we are particularly interested in comparing the performance of the EVM and contracts pallets. We used Geth as a yardstick comparison.
If you follow the arrows in the diagram above starting at the smart contract languages and ending at the executors, then there are 5 possible combinations. We compared the performance of each of these combinations.
Additionally, we implemented each of our benchmark projects as a Substrate pallet. A pallet is a piece of software that behaves similar to a smart contract and is also compiled to Wasm but has some crucial differences:
- Unlike smart contracts, arbitrary users cannot upload a pallet to the blockchain. Instead, the pallet is directly integrated as a fixed part of the runtime by the blockchain maintainers.
- Because this makes pallets trusted code there is no danger of compiler bombs and therefore Substrate executes the pallet’s Wasm code through a much more efficient executor that compiles instead of interprets the bytecode. Since there is only a limited number of runtimes and they are known well ahead of time, they only need to be compiled once and can then be cached.
Wasm is the Performance Power House
We measured the execution time of a variety of benchmark projects. The complete code is available on GitHub. In this article we will only focus on two projects:
- Odd Numbers: the smart contract takes one argument n and will compute the product of the first n odd numbers (modulo 2**64).
- SHA-512: the smart contract takes two arguments n and i and will iterate SHA-512 i times on input n, i.e., it will compute SHA(SHA(…(n)…)).
While these are not smart contracts that you will find in the wild, they are useful to measure raw performance. The performance is shown in the bar chart below. Note that the execution time of the pallet is not visible because it is so low (about 1 millisecond only).
A bridge between blockchains requires the implementation of the logic of a foreign blockchain as a smart contract. This logic comprises cryptographic primitives (hashes and signatures). This is evident if the bridge smart contract implements a chain relay and relies on transaction inclusion proofs. Validating such a proof requires the calculation of about 10 hash functions, depending on the blocksize. Different blockchains use different hash functions and if the utilized hash function does not happen to be a precompiled smart contract (for the EVM), then the computationally complex hash function needs to be implemented as a smart contract, a true resource bottleneck. For that reason, we suppose that the SHA-512 benchmark is a significant performance indicator.
The bar chart makes it clear that Wasm is vastly superior to EVM. What is surprising is that it is even more performant to compile Solidity code via Solang to Wasm, even when compared to the mature EVM engine Geth.
The chart also indicates that it can be worthwhile to implement a smart contract directly in Wasm text format if performance is crucial. Our SHA-512 implementation is 3 times faster than its implementation in ink! It is almost 20 times faster than the EVM implementation executed through the EVM pallet.
Note also how big the performance gain comes from implementing the contract logic as a pallet and executing it through a Wasm compiler instead of interpreter. Pallets are also more efficient than smart contracts because they do not require gas metering and because they are compiled ahead of time. The pallet is over 1000 times faster than the ink! smart contract using precisely the same Rust code. That is the reason why we implement our Spacewalk bridge as a pallet and not as a smart contract.
Also the size of a compiled contract has an impact on the performance. It requires storage, gas fees and can lead to bottlenecks in Polkadot where a relay chain validator node needs to first fetch a smart contract from the parachain when it needs to execute it. The following chart shows the sizes of the compiled smart contracts of our SHA-512 benchmark. The difference in size between compiled ink! and Solidity smart contracts depends on the concrete implementation and therefore the chart can be misleading. However, it is evident that in our benchmark the current version of Solang generates larger smart contracts than the Solidity compiler, which is mostly due to the fact that Solang is not as optimized yet.
Forkless Updates and Layered Wasm Execution
One defining feature of Substrate is that it allows updating a blockchain runtime in a forkless way. It achieves this by encoding the complete runtime itself as Wasm bytecode and stores the currently valid version on the blockchain. A Substrate chain will switch from a native version of the runtime to its Wasm version after its first upgrade.
The Wasm version of the runtime is less performant than the native version. Since the contracts and EVM pallets are part of the runtime, they will also execute smart contracts slowlier. Thankfully the Wasm runtime is run by a Wasm executor that compiles the Wasm code so that the performance hit is not so high. We measured that Wasm smart contracts (executed through the contracts pallet) will take about 60% more time to execute whereas EVM smart contracts (executed through the EVM pallet) will take about 120% more time.
This widens the gap between executing EVM smart contracts and executing Wasm smart contracts in a Substrate chain even further. We suppose that the lower performance hit for Wasm smart contracts is due to the fact that running Wasm inside Wasm is more efficient than running EVM inside Wasm.
Fixed logic in a Substrate chain (i.e., logic that does not need to be supplied by users) should be implemented as a pallet instead of a smart contract. For smart contracts it turns out that Wasm can be vastly more efficient than EVM — we measured a performance difference between both technologies of up to a factor of 23.
For our benchmarks, even for Solidity smart contracts, it is more performant to compile them to Wasm bytecode via Solang.
Our benchmarks compared raw computational performance. In real life situations, smart contracts would also perform I/O tasks like reading from and writing to the blockchain state. These tasks take the same amount of time independently of what technology the smart contract is built on. Therefore the performance differences for smart contracts with I/O would be much less pronounced.
Thanks to Alexander Theißen for many helpful comments.