The Ethereum Virtual Machine (EVM)
The Ethereum Virtual Machine (EVM) is a “quasi-turing complete” 256-bit virtual machine, one of the most important and integral components of the Ethereum network.
As EVM smart contract development has gradually improved, there has been ever increasing sophistication to the DApp applications — such as the recently popular Fomo3D game — launched on the network.
With this progression however, there are several barriers and issues restricting Ethereum DApp development which is ultimately hindering functionality and long term potential for this network.
Firstly, the Ethereum virtual machine (EVM) is not Turing complete
There is currently a common misconception that the Ethereum virtual machine is Turing-complete, however this is not the case.
Turing completeness means that any computable problem can be solved. The essence of a programming language or virtual machine is a Turing machine. If a programming language or virtual machine is Turing-complete, it means that it can do all the things that Turing can do, that is, it can solve all the computational problems without exception.
In the design of the Ethereum virtual machine, since the calculation of the instruction is constrained by gas, this limits the number of calculations that can be completed. This is a common cause of Turing incompleteness, because the loop, recursion, or computational bounds cause the program to terminate, so applications running on the EVM are subject to many restrictions therefore making the EVM not Turing-complete.
Secondly, the design of EVM is irrational
As development of DApps using smart contracts has increased, some of the flaws of the original design of the EVM have gradually emerged, with some of these leading to serious security concerns. On the Ethereum virtual machine, we believe that there are the following issues in the design and security levels:
1. Smart contract design level
- EVM lacks complete standard library support. Even the most basic string type support, which is very hard in EVM, such as string splicing, cutting, searching, etc — all need to be implemented by developers themselves. At the same time, the self-implemented class library may be too high in time and space complexity, resulting in consumption of a lot of unnecessary gas. Alternatively, the developer can borrow relevant class library code from the open source project itself, but this again will introduce more security issues and add to the complexity of contract code auditing which would make the process highly inefficient.
- DApps are difficult to debug and test. In addition to throwing OutOfGas exceptions, EVM does not return any information to the developer. It is impossible to print logs, make breakpoints, and single-step debugging. Although the event mechanism can partially improve the problem, the design of the event mechanism itself determines that it is not an elegant and easy to use debugging tool.
- Floating point numbers are not supported. Ethereum uses Wei as the smallest unit. Wei only has integers and does not support other granular measurements. This design creates a precision problem caused by the introduction of floating point numbers. In order to represent an ETH variable, there will be a lot of zeros after the variable, which makes the code maintenance extremely complicated. At the same time, it is undeniable that floating-point numbers still have great value in certain scenarios, and they cannot be abandoned completely.
- Contracts cannot be upgraded. Contract upgrades are a strong requirement in smart contract development. This is an issue that every contract developer must consider for their project. Contract upgrades can implement security patches for existing deployments as well as extend their usability, features and more. EVM does not support upgrades at all — which as a result means developers can only solve this problem by issuing new contracts — which is time consuming, costly and laborious.
2. Smart contract security level
- Overflow attack concerns. EVM’s safeMath library is not used by default. For example, when the developer calculates the solidity uint256 — if the final result is greater than the maximum value of uint256 — the overflow will be changed to a small number therefore creating an overflow vulnerability. Tokens such as BEC and SMT have suffered from overflow attacks, which have brought extremely serious consequences. An example of the overflow vulnerability in BEC is as follows:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { uint cnt = _receivers.length; uint256 amount = uint256(cnt) * _value; // overflow occurred here require(cnt > 0 && cnt <= 20); require(_value > 0 && balances[msg.sender] >= amount); // require is always established after overflow, generating a vulnerability
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;
}
- Re-entry attacks. A major feature of solidity is that you can call other external contracts, but when you send ETH to an external address or call an external contract, you need to submit an external call. If the external address is a malicious contract, the attacker can add malicious code to the fallback function. When this transfer then occurs, the fallback function is called to execute the malicious code. The malicious code executes the vulnerable function of the calling contract, causing the transfer to be resubmitted. The most serious re-entry attack occurred in the early days of Ethereum, the well-known DAO vulnerability. The following contract segment specifically demonstrates the re-entry attack:
contract weakContract { mapping (address => uint) public balances; function withdraw() { // Transfer the caller's balance out and set the caller's balance map to 0 //As long as the un-executed balance map is set to 0, you can always call msg.sender to transfer funds, and reenter here. if (!msg.sender.call.value(balances[msg.sender])()) { throw; } balances[msg.sender] = 0; } } contract attack{ weakContract public weak; // This is a fallback function. It will be triggered when an external call is transferred. It will always trigger the withdraw method of weak Contract to perform a re-entrant attack. function () payable { if (weak.balance >= msg.value) { weak.withdraw(); } } }
- Unexpected function executions. EVM does not strictly check function calls, and if the contract address is controllable as an incoming parameter, it may cause unexpected behavior as follows:
contract A { function withdraw(uint) returns (uint); } // When executing contract B, it will only check if contract A has a withdraw method, and if so, the contract will be called normally. // If the transferred parameter does not have the withdraw method, A's Fallback function will not be called, resulting in unexpected behavior contract B { function put(A a){ a.withdraw(42); }
In summary, EVM has several problems in design and security. Although the EVM team has developed a new contract development language — Vyper — it is still in the experimental stage and currently cannot be used. Currently, when Ethereum is used on a large scale, the accumulation of these security and design issues will ultimately lead to serious vulnerabilities for the network and its users.
The EOS Virtual Machine
EOS is another high profile blockchain application recently launched and following in Ethereum’s footsteps. It has its own set of intelligent contract engines based on WebAssembly. However, there are several obvious problems in EOS’s contract development:
- The account system is not easily accessible. EOS requires an existing account to create a new account, and only after creating an account, can a contract then be issued. Requiring a friend or third party with an EOS account is a poor solution and creates a barrier to the accessibility of this network. To create an account without a referral, you need to first purchase RAM, creating another financial barrier. After creating an account, you need to stake EOS in exchange for CPU usage time and net bandwidth to operate on the EOS network. These requirements are too convoluted and cumbersome for potential developers.
- RAM is expensive. Unlike bandwidth and CPU, there is no fixed exchange rate for RAM on the EOS network. Developers are exposed to the exchange rate risk between RAM and EOS and generally will not receive back the same amount of tokens they have staked. This also creates an issue of RAM “trading” — creating a opportunity for speculation which in turn removes predictability of cost for developers.
- Difficulties in development. Using C++ as a contract language greatly increases the barrier for contract development for the developer community. C++ itself is extremely complex, and it is necessary to call EOS’s C++ API to complete smart contract development. As a result, the skills and knowledge required for Dapp development on EOS network are quite extensive leading to a shrinking pool of developers who can build on this platform.
In the face of all these problems, EOS smart contract development is not very attractive to developers, and we have already seen it being one of reason for developers to abandon EOS for other projects.
The IOST Virtual Machine
At IOST, we believe that a successful virtual machine must deliver ease of use for developers, ensure robust security, as well as implement an elegant architectural design. In this respect, we aimed to solve the irrational design and security problems currently existing within EVM and EOS. After comparing the advantages and disadvantages of virtual machines such as EVM, EOS, C Lua, V8, etc., and based on the excellent performance of V8 on NodeJs and Chrome, we finally decided to build the IOST virtual machine based on V8.
IOST VMManager system architecture
The core of the V8VM architecture is VMManger, which has the following three functions:
- VM Entrance. It interfaces external requests from other modules, including RPC requests, block validation, Tx validation, etc. The work is handed off to VMWorker after pre-processing and formatting. It gives unified entrance for all requests.
- VMWorker lifecycle management. The number of workers (VMManager threads) are set dynamically based on system load resulting in efficient reuse. Within the workers, JavaScript hot launch and persistence of hotspot Sandbox snapshots help reduce frequent creation of VMs, and avoid heavy load in CPU and memory when the same code is loaded. This will increase the throughput of the system, allowing the IOST V8VM to achieve high performance even when processing contracts with high transaction volumes, such as fomo3D.
- Management of interface with State Database. This ensures atomicity (prevents partial updates to the database) of each IOST transaction, denying the entire transaction when there is an error of insufficient funds. At the same time, two-level cache is achieved in the State Database before being flushed to RocksDB. This ensures less access time for different versions of data, and optimized performance for temporary data.
IOST Sandbox System Architecture
As the carrier of the final execution of the JavaScript smart contract, the IOST Sandbox completes the call to the V8VM and the next package of Chrome V8, which is divided into the Compile and Execute phases:
Compile stage
Mainly for contract development and winding, there are two main functions:
- Contract Pack. A packaged smart contract, based on the webpack implementation, will package all the JavaScript code under the current contract project and automate the dependency installation making it possible for IOST V8VM to develop large contract projects. At the same time, IOST V8VM and Node.js module systems are fully compatible, and can seamlessly use methods such as require, module.exports and exports to give contract developers a native JavaScript development experience.
- Contract Snapshot. With V8’s snapshot technology, IOST’s virtual machine delivers improves performance by removing the initial default empty state. Real implementation only needs to de-serialize the snapshot to complete the execution, greatly improving the loading speed and execution speed of JavaScript.
Execute stage
For the real implementation of the chain contract, there are two main functions:
- LoadVM. To complete VM initialization, including generating Chrome V8 objects, setting system execution parameters, and importing related JavaScript class libraries, etc., thereby complete all preparations before smart contract execution. Some JavaScript class libraries are as follows:
- Execute. The final implementation of a JavaScript smart contract on IOST V8VM will open a separate thread execution contract and monitor the current execution state. If an exception occurs, the use of resources exceeds the limit or the execution time exceeds the maximum limit, the Terminate call to end the current contract execution will be made, thus returning an exception result.
IOST V8VM performance
A virtual machine that composes the core of a public blockchain infrastructure, must deliver exceptional performance to meet the demands and requirements of the network. At the beginning of the design and virtual machine selection, IOST regarded performance as one of the most important indicators.
Chrome V8 uses JIT, inline caching, lazy loading among others to implement JavaScript interpretation. Thanks to the high performance of Chrome V8, the JavaScript execution speed of IOST V8VM has been vastly improved. After testing the performance of EVM, EOS, C Lua and V8VM in recursive Fibonacci, memory copy, and complex CPU operations we observed the following results:
These results clearly showed IOST V8VM performs well in mainstream VM implementations. The above test contains the time for the virtual machine to start and load the configuration. It can be seen that the IOST V8VM direct cold boot also has a lot of performance advantages. Later, we will also join the VM object pool, LRU cache, etc. to improve the virtual machine CPU and memory usage to better enhance IOST’s ability to handle smart contracts.
Conclusion
Currently our test network is running the first version of the IOST V8VM virtual machine. In this first version, we achieved all the intended functionality as well as verifying a lot of design concepts such as voting, contract domain name, token features, etc.. Going forward, we will continue to improve the IOST V8VM with the following main focuses:
- Improved security across all layers and systems
- High performance and improving faster execution of contracts
- Ease of development and use including increasing and improving standard libraries
- Support large project construction, debugging, and a complete tool chain
More new features will be implemented in the test network update coming in the next few weeks. We are making great progress in our ultimate goal of deploying a industry leading virtual machine that out-competes and improves upon all current available networks and are extremely excited not only about this current development, but also the entire IOST ecosystem as a whole.
Appendix: Virtual Machine Benchmark Program
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: %vn”, 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)
}
}
}
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
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, ‘ ’);
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) )
V8 code
function fibonacci(cycles) { if (cycles == 0) return 0 if (cycles == 1) return 1 return fibonacci(cycles — 1) + fibonacci(cycles — 2) }
function strConcat(str, cycles)
{
let rs = ‘’
for (let i = 0; i < cycles; i++) {
rs += str
}
return rs
}
function calculate(cycles)
{
let rs = 0
for (let i = 0; i < cycles; i++) {
rs = rs + Math.pow(i, 3)
}
return rs
}
Source: Crypto New Media