Skip to main content

IOST V8VM Design

· 13 min read
Sherwin Li
IOST Core Engineer

The Ethereum Virtual Machine (EVM) is a "quasi-Turing-complete" 256-bit virtual machine and one of the most important components of the Ethereum network. Since Ethereum's launch, EVM-based smart-contract development has matured, giving rise to many DApps such as CryptoKitties and the recently popular Fomo3D. Virtually every blockchain developer recognizes the importance of smart contracts and virtual machines, so VM usability and ease of contract development have become a key battleground among public chains — and a decisive factor in how far a chain can ultimately go.

1. The EVM is not Turing-complete

A common misconception online is that the EVM is Turing-complete. It is not.

A Turing machine is a mathematical model proposed by Alan Turing in 1936. It consists of an infinite tape, a finite alphabet, a read/write head, a state register, and a finite instruction set. Starting from an initial configuration, the head moves and writes step by step according to the instruction set until the state becomes halt. In computing, every problem we study is a computational problem; Turing-completeness means that any computable problem can be solved. A programming language or virtual machine is essentially a Turing machine, and Turing-completeness implies it can do anything a Turing machine can — i.e. it can solve every computable problem.

In the EVM, however, every instruction's execution is bounded by gas, which limits the number of computations that can be performed. This is a common cause of Turing-incompleteness: loops, recursion, and computations are all bounded, so program termination is guaranteed. The set of programs that can run on the EVM is therefore restricted, and the EVM is not Turing-complete.

2. Questionable EVM design decisions

As the earliest quasi-Turing-complete VM, the EVM pioneered smart-contract-oriented DApp development. As blockchain applications have spread, however, some of the EVM's original design choices have started to show — a few of them severe enough to cause security incidents. At the VM layer alone, we see the following issues across design and security:

2.1 Smart-contract design

  • Lack of standard library support: The EVM has very limited standard-library support. Even basic string handling is awkward — concatenation, slicing and searching all have to be implemented by the developer. The consequence is that developers have to deal with all sorts of incidental work instead of focusing on their actual business logic. Hand-rolled libraries can be expensive in time and space (burning unnecessary gas), and borrowing code from open-source projects introduces additional security risks and makes auditing harder.
  • Hard to debug and test: Beyond the OutOfGas exception, the EVM gives developers no feedback. There is no logging, no breakpoints, and no step-by-step debugging. The event mechanism partially mitigates this but was never designed as a proper debugging tool.
  • No floating point: The EVM does not support floats. Ethereum uses Wei as the smallest unit and only deals with integers. This avoids floating-point precision issues, but in practice developers end up appending many zeros after their eth variables, which makes code extremely awkward to maintain. Floating point genuinely is useful in some scenarios and shouldn't have been ruled out wholesale.
  • Contracts can't be upgraded: The EVM does not support contract upgrades. Upgradeability is a hard requirement for serious smart-contract development — for hot-patching security issues, extending features, and so on. Without it, developers can only deploy a new contract, which is time-consuming and laborious.

2.2 Smart-contract security

  • Overflow attacks: The EVM does not use SafeMath by default. When Solidity arithmetic on uint256 exceeds the maximum value, the result wraps around to a small number — opening an overflow vulnerability. Tokens such as BEC and SMT have all suffered overflow attacks with devastating consequences. The BEC vulnerability looked like this:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value; // overflow occurs here
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount); // after overflow, require always succeeds — vulnerability triggered

balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
  • Reentrancy attacks: One of Solidity's key features is the ability to call other contracts, but sending ETH to an external address or invoking an external contract requires a callout. If the target is a malicious contract, the attacker can place malicious code in its fallback function. When a transfer happens, the fallback executes the malicious code, which re-enters the vulnerable function on the calling contract and re-triggers the transfer. The most infamous reentrancy attack was the DAO hack on early Ethereum. The snippet below illustrates the pattern:
contract weakContract {
mapping (address => uint) public balances;
...... // other contract code
function withdraw() {
// transfer the caller's balance out, then zero out the balance map
// !!! as long as the balance hasn't been zeroed, msg.sender can keep being called for transfers — reentrancy happens here
if (!msg.sender.call.value(balances[msg.sender])()) {
throw;
}
balances[msg.sender] = 0;
}
..... // other contract code
}

contract attack{
weakContract public weak;
...... // other code
// This is the fallback function, triggered by external transfers. It keeps invoking weakContract.withdraw to perform the reentrancy attack.
function () payable {
if (weak.balance >= msg.value) {
weak.withdraw();
}
}
...... // other code
}
  • Unexpected function execution: The EVM does not strictly check function calls. If a contract address is passed as a parameter that the caller can control, unexpected behavior can occur:
contract A {
function withdraw(uint) returns (uint);
}

// When B runs, it only checks whether A has a `withdraw` method, and calls it if so.
// If the incoming `a` does not have a `withdraw` method, its fallback function is invoked instead — unexpected behavior.

contract B {
function put(A a){ a.withdraw(42);
}

3. Visible issues with EOS, another high-profile public chain

EOS, the other big public-chain story after Ethereum, has its own WebAssembly-based smart-contract engine. EOS contract development, however, has several visible issues:

  • Unfriendly account system: Creating an account is hard. You can only publish a contract after creating an account, and creating one requires an existing EOS account — finding a friend or third party who already has one is non-trivial for most people. Creating an account also requires buying RAM (so it literally costs money), and using a third party introduces custody risk. After the account is created, you also need to stake EOS to obtain CPU time and net bandwidth before you can do anything on the network. These steps are excessive for a developer.
  • RAM price: RAM is expensive. Contracts have to use RAM, and EOS introduced a RAM market to let users trade it. Even though RAM can be bought and sold, speculators have driven prices up significantly.
  • High development difficulty: Using C++ as the contract language raises the bar substantially. C++ itself is already complex, and developers also need to call the EOS.IO C++ API on top of it — demanding significant skill from each developer.

Given these issues, EOS smart-contract development isn't particularly attractive — and may even be a reason developers walk away from EOS.

4. Enter the IOST virtual machine

We believe a good virtual machine has to be both elegantly designed and easy and safe to use. After studying the strengths and weaknesses of EVM, EOS, C Lua, V8 and other VMs, we addressed many of the EVM's and EOS's design issues at their root and — given V8's strong showing in Node.js and Chrome — ended up building the IOST virtual machine on V8.

4.1 IOST V8VM architecture and design

The core of the V8VM architecture is VMManager, which has three primary responsibilities:

  • VM entry point: receives requests from other modules — RPC requests, block validation, transaction validation, and so on — preprocesses and formats them, then dispatches them to VMWorkers.
  • VMWorker lifecycle management: sizes the worker pool dynamically based on system load and reuses workers. Inside each worker we implement JavaScript hot-start and hot Sandbox snapshot persistence, which avoids repeatedly creating VMs and re-loading the same code under high load — keeping resource usage low while dramatically increasing throughput. As a result, IOST V8VM handles workloads like Fomo3D — with massive concurrent users hitting a single contract — comfortably.
  • State database interaction: guarantees atomicity for every IOST transaction by rolling back the entire transaction when a contract errors out or runs out of gas. The State database uses two levels of in-memory cache before flushing to RocksDB.

4.2 Sandbox core design

The Sandbox is the actual executor of JavaScript smart contracts. It sits below the V8VM and wraps Chrome V8 directly. It has two phases — Compile and Execute:

Compile phase

This phase is geared toward contract development and on-chain deployment, with two main responsibilities:

  • Contract Pack: bundles the smart contract. Built on webpack, it packages all JavaScript code under the current contract project and runs dependency installation automatically — making it possible to build large contract projects on IOST V8VM. The IOST V8VM is also fully compatible with the Node.js module system: require, module.exports and exports all work, giving contract authors a native JavaScript developer experience.
  • Contract Snapshot: uses V8's snapshot feature to compile JavaScript code ahead of time. The compiled code makes Chrome V8 isolate and context creation much faster — at actual execution time the engine just deserializes the snapshot, dramatically improving load and execution speed.

Execute phase

This phase covers the actual on-chain execution of contracts, with two main responsibilities:

  • LoadVM: initializes the VM — creates the Chrome V8 instance, applies execution parameters, imports the JavaScript standard libraries, and so on. Some of the standard libraries:
LibraryPurpose
BlockchainA Node.js-style module system: module cache, module pre-compilation, cyclic-import handling, etc.
EventJavaScript reads/writes to the State database, with rollback on contract failure or exception.
NativeModuleBlockchain-related primitives: transfer, withdraw, and queries for the current block / current transaction.
StorageEvent implementation: events emitted from inside JavaScript contracts are delivered as callbacks after the transaction is included on-chain.
  • Execute: the final execution step. IOST V8VM runs the contract on a dedicated thread and monitors execution state. If an exception is thrown, resource limits are exceeded, or execution time crosses the cap, it calls Terminate to stop the contract and returns an error result.

4.3 IOST V8VM performance

As the most fundamental piece of infrastructure on a public chain, the virtual machine has to perform. Performance was one of our top design priorities from day one, including during VM selection.

Chrome V8 uses JIT compilation, inline caching, lazy loading, and other techniques to make JavaScript fast. Thanks to V8, IOST V8VM JavaScript execution speed has improved by orders of magnitude. We benchmarked recursive fibonacci, memory copy, and complex CPU computation across EVM, EOS, C Lua, and V8VM:

  • Test environment
PropertyConfiguration
SystemAWS EC2
CPU2 CPU
Memory8 GB
  • Results
evmlua ceos.binaryeneos.wavmIOST V8VM
cpu calculate (8000)92 ms6.34 ms3.34 ms10.28 ms6.26 ms
fibonacci (32)8.25 s470 ms5.5 s541 ms45 ms
string concat (10000)2.18 s370 ms316 ms96.3 ms9.3 ms

In practice, IOST V8VM holds up well against mainstream VM implementations. The numbers above include VM startup and config-loading time, so IOST V8VM has a clear edge even on cold start. We plan to add a VM object pool, LRU caching, and similar optimizations to improve CPU and memory utilization further and push IOST's smart-contract processing capability higher.

4.4 Closing notes

We have shipped the first version of the IOST V8VM and delivered every feature originally planned. As we iterate, we'll prioritize security and ease of use, and invest in three areas:

  • High performance — faster contract execution
  • A friendlier developer experience — more and better-quality standard libraries
  • First-class support for large projects — building, debugging, and a complete toolchain

The first IOST V8VM also let us validate several ideas — voting, contract names, tokens and more. Additional new features will roll out incrementally in upcoming testnet releases.

Appendix: VM benchmark programs

    1. EVM code
package evm

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
)

var bm *Benchmark

func init() {
key, err := crypto.GenerateKey()

auth := bind.NewKeyedTransactor(key)
gAlloc := map[common.Address]core.GenesisAccount{
auth.From: {Balance: big.NewInt(1000000)},
}
sim := backends.NewSimulatedBackend(gAlloc)

_, _, bm, err = DeployBenchmark(auth, sim)

if err != nil {
panic(err)
}
sim.Commit()
}

func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := bm.Fibonacci(nil, big.NewInt(32))
if err != nil {
b.Fatalf("fibonacci run error: %v\n", err)
}
}
}

func BenchmarkStrConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := bm.StrConcat(nil, "This is vm benchmark, tell me who is slower", big.NewInt(10000))
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkCalculate(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := bm.Calculate(nil, big.NewInt(5000))
if err != nil {
b.Fatal(err)
}
}
}
    1. Lua code
function fibonacci(number)
if number == 0
then
return 0
end

if number == 1
then
return 1
end

return fibonacci(number - 1) + fibonacci(number - 2)
end

function strConcat(str, cycles)
local result = ""
for i = 1, cycles do
result = result .. str
end

return result
end

function calculate(cycles)
local rs = 0
for i = 0, cycles-1 do
rs = rs + math.pow(i, 5)
end

return rs
end
    1. EOS code
class fibonacci : public eosio::contract {
public:
using contract::contract;

/// @abi action
void calcn(int64_t n) {
int64_t r = calc(n);
print(r);
}
int calc( int64_t n ) {
if (n < 0)
{
return -1;
}
if (n == 0 || n == 1)
{
return n;
}
return calc(n - 1) + calc(n - 2);
}
};

EOSIO_ABI( fibonacci, (calcn) )


class stringadd : public eosio::contract {
public:
using contract::contract;

/// @abi action
void calcn(std::string s, int64_t cycles) {
std::string ss(s.size() * cycles, '\0');
int32_t k = 0;
for (int i = 0; i < cycles; ++i)
{
for (int j = 0; j < s.size(); ++j)
{
ss[k++] = s[j];
}
}
print(ss);
}
};

EOSIO_ABI( stringadd, (calcn) )


class calculate : public eosio::contract {
public:
using contract::contract;

/// @abi action
void calcn(uint64_t cycles) {
uint64_t rs = 0;
for (uint64_t i = 0; i < cycles; ++i)
{
rs = rs + i * i * i * i * i;
}
print(rs);
}
};

EOSIO_ABI( calculate, (calcn) )