POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Kevin Tan

本記事は、原著者の許諾のもとに翻訳・掲載しております。

文:Li HaifengIOST シニアエンジニア)

Li Haifengは元Tencentシニアエンジニアで、起業家として成功しています。Tencent NewsとTiantian Expressの再構築を主導し、150,000,000を超えるデイリービジターを獲得しました。現在、IOSTの仮想マシンのリードエンジニアです。

ブロックチェーンの開発者であれば誰もが、スマートコントラクト機能だけでなく、 仮想マシンの設計 とパフォーマンスが主流のDAppの導入に欠かせないことを認識しています。結果として、これらのコア機能を実装することが、主要な公開のブロックチェーンネットワーク間の競争と差別化の核心になっています。どのネットワークが成功し、最終的に現実に成功を収め、ユーザを獲得して成長していくかどうかを左右するのは、これらの実装なのです。

先週、 IOST はテストネットワークの2回目のイテレーション、 Everest v1.0 を公開しました。JavaScriptによるスマートコントラクトのプログラミング、スマートコントラクト Domain Name System(DNS)と多版型同時実行制御(MVCC)をサポートしているほか、このアップデートにはV8エンジンに基づく新しい仮想マシンのデプロイも含まれています。

以下の技術記事では、イーサリアムとEOSをそれらの仮想マシン開発の面から分析・評価します。そして、競合のネットワークが直面している問題に対処することを目指して選択した、IOSTの実装と設計の概要を説明します。

イーサリアム仮想マシン(EVM)

イーサリアム 仮想マシン(EVM)は、「疑似的にチューリング完全」な256ビット仮想マシンで、イーサリアムネットワークに欠かせない重要なコンポーネントの1つです。

EVMスマートコントラクトの開発の改善が進むにつれ、最近人気のFomo3Dゲームなど、ネットワーク上でローンチされるDAppアプリケーションはかなり高度になってきています。

しかし、この進化には、イーサリアムDAppの開発を制限するいくつかの障壁と問題が伴い、いずれこのネットワークの機能性と長期的な可能性を妨げることになりそうです。

第一に、イーサリアム仮想マシン(EVM)はチューリング完全ではありません

イーサリアム仮想マシンはチューリング完全だと考えている人が多いのですが、誤解です。

チューリング完全性とは、計算可能な問題は必ず解決できるという状態を意味します。プログラミング言語、または仮想マシンの本質はチューリングマシンです。プログラミング言語、または仮想マシンがチューリング完全であるなら、チューリングが行うことなら何でもできます。つまり、例外なくすべての計算問題を解決できます。

イーサリアム仮想マシンの設計では、インストラクションの計算にガスの制約がかかるため、完了できる計算の数に制限があります。これはよくチューリング不完全の原因になります。なぜなら、ループ、再帰、または計算上の境界がプログラムを終了させるため、EVMで実行中のアプリケーションには多くの制約がかかりがちで、結局EVMはチューリング不完全になってしまうのです。

第二に、EVMの設計は不合理です

スマートコントラクトを利用したDAppsの開発が増加するにつれてEVMの元の設計の欠陥が出現しており、その中にはセキュリティ上の懸念が大きいものもあります。イーサリアム仮想マシンには、設計レベルとセキュリティレベルにおいて次の問題があると考えています。

1. スマートコントラクトの設計レベル

  • EVMには標準ライブラリの完全なサポートがありません。 文字列スプライシング、切り取り、検索など、最も基本的な文字列型のサポートもEVMでは非常に難しく、開発者がそれぞれ実装しなければなりません。同時に、自前で実装したクラスのライブラリは時間と空間の複雑さが高すぎるため、大量のガスを無駄に消費する可能性があります。その代わりに、オープンソースプロジェクトそのものから関連するクラスのライブラリコードを借りられますが、これもセキュリティ上の問題を招き、コントラクトコード監査の複雑さが増し、結果的にプロセスが非効率的になります。
  • DAppsはデバッグとテストが困難です。 OutOfGasの例外を投げることに加えて、EVMは開発者に何も情報を返しません。ログの出力、ブレークポイントの作成、シングルステップのデバッグは不可能です。イベントメカニズムが部分的に問題を解消するかもしれませんが、そもそもイベントメカニズムの設計を見れば、エレガントかつ使いやすいデバッグツールでないことは分かります。
  • 浮動小数点数がサポートされていません。 イーサリアムが使用する最小単位はWeiです。 Weiは整数のみで、その他の細かい測定はサポートしていません。この設計では、浮動小数点数が入った場合に細かい問題が生じます。ETH変数を表現する際、変数の後にゼロがたくさんあるため、コードのメンテナンスが非常に煩雑になります。同時に、浮動小数点数は特定のシナリオでは依然として大きな価値があるのは事実で、完全に捨て去ることはできないでしょう。
  • コントラクトのアップグレードはできません。 コントラクトのアップグレードは、スマートコントラクト開発ではほとんど必須です。これは、すべてのコントラクト開発者がプロジェクトのために検討しなければならない問題です。コントラクトのアップグレードは、既存のデプロイのセキュリティパッチを実装するだけでなく、そのユーザビリティや機能などの拡張も可能にします。EVMはアップグレードをまったくサポートしていません。結果として開発者は、新しいコントラクトを発行しない限り、この問題を解決することはできません。時間もコストもかかる上に面倒です。

2. スマートコントラクトのセキュリティレベル

  • オーバーフロー攻撃に対する懸念があります。 EVMのsafeMathライブラリは初期設定では使われません。たとえば開発者がSolidityのuint256を計算する際、最終結果がuint256の最大値より大きい場合、オーバーフローは少数に変更されてオーバーフローの脆弱性が生じます。BECやSMTといったトークンはオーバーフロー攻撃を受けており、きわめて深刻な結果を招いています。BECのオーバーフローの脆弱性の一例を以下に挙げます。
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; } ``` リエントリー攻撃の危険があります。Solidityの主な特徴は、他の外部コントラクトを呼び出せることです。しかしETHを外部アドレスに送信する、または外部コントラクトを呼び出す際は、外部呼び出しを送信する必要があります。外部アドレスが悪意のあるコントラクトだった場合、攻撃者はfallback関数に悪意のあるコードを追加する可能性があります。この転送の際にfallback関数が呼び出され、悪意のあるコードが実行されます。悪意のあるコードは呼び出したコントラクトの脆弱な関数を実行し、転送を再送信してしまいます。イーサリアムの初期に起こった非常に深刻なリエントリー攻撃、DAOの脆弱性としてよく知られています。次のコントラクトセグメントが、具体的にリエントリー攻撃を起こす部分です。 ``` 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();
}
}
}
  • 予期しない関数を実行する可能性があります。 EVMは関数の呼び出しを厳密にチェックしません。コントラクトアドレスを受信パラメータとして制御できる場合、次のような予期しない動作を起こす場合があります。
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);
}

つまり、EVMには設計とセキュリティにいくつかの問題があるということです。EVMチームは新しいコントラクト開発言語Vyperを開発していますが、まだ実験段階であり、現時点では使用できません。 今イーサリアムを大規模に利用した場合、これらのセキュリティおよび設計上の問題が積み重なり、ネットワークおよびそのユーザに深刻な脆弱性をもたらすでしょう。

EOS仮想マシン

EOSは、最近登場してイーサリアムの軌跡を追いかけている、もうひとつの注目すべきブロックチェーンアプリケーションです。WebAssemblyをベースとした独自のインテリジェントなコントラクトエンジン一式を持っています。しかし、EOSのコントラクト開発にはいくつか明らかな問題点が見られます。

  • アカウントシステムに容易にアクセスできません。 EOSで新規アカウントを作成するには既存のアカウントが必要で、アカウントを作成して初めてコントラクトを発行できます。EOSアカウントを持っている友人や第三者がいなければならないというのは貧弱なソリューションであり、このネットワークのアクセシビリティの障壁になっています。紹介なしでアカウントを作成する場合は、最初にRAMを購入する必要があり、今度は経済的な障壁です。アカウント作成後、EOSネットワーク上のオペレーションに要するCPU使用時間とネット帯域幅と引き換えに、EOSをステークする必要があります。これらの要件は、利用を検討する開発者にとっては複雑過ぎて扱いにくいものです。
  • RAMは高価です。 帯域幅やCPUとは異なり、EOSネットワーク上のRAMには固定の交換レートがありません。開発者は、RAMとEOS間の交換リスクに晒されます。一般的にステークしたのと同じ量のトークンを受け取ることはないのです。これはまた、RAM「取引」の問題を生み出します。投機の機会が発生する代わりに開発者がコストを予測することが不可能になります。
  • 開発の難易度が高いです。 コントラクト言語にC ++を使用しているため、開発者コミュニティのコントラクト開発の障壁が大幅に高まります。そもそもC ++が非常に複雑で、スマートコントラクト開発を行う際はEOSのC ++ APIを呼び出す必要があります。EOSネットワーク上のDApp開発にきわめて広い範囲のスキルと知識が求められるため、このプラットフォームを使って構築できる開発者は減りつつあります。

これらの問題を考えると、EOSスマートコントラクトの開発は開発者にとって魅力に欠けると言えます。実際それが、開発者たちがEOSを止め、他のプロジェクトを選ぶ一因になっているのを見てきています。

IOST仮想マシン

IOST では、成功につながる仮想マシンとは、洗練されたアーキテクチャ設計を実装するだけでなく、開発者にとって使いやすく、セキュリティが堅牢であるべきと考えています。その点から、現状のEVMとEOS内に内在する不合理な設計とセキュリティの問題を解決することを目指しました。EVM、EOS、C Lua、V8をはじめとする仮想マシンの利点と欠点を比較した結果、NodeJsとChromeのV8の優れたパフォーマンスを基盤に、V8ベースでIOST仮想マシンを構築することにしました。

IOST VMManagerシステムアーキテクチャ

V8VMアーキテクチャのコアはVMMangerです。VMMangerには次の3つの機能があります。

  • VMエントランス。 これは、RPCリクエスト、ブロック認証、Tx認証などを含む他のモジュールからの外部リクエストの仲立ちをします。プリプロセッシングおよびフォーマット設定後にVMWorkerに処理が渡されます。全リクエストを一括で処理する入り口です。
  • VMWorkerのライフサイクル管理。 Worker(VMManagerスレッド)の数がシステムの負荷に基づいて動的に設定されるため、効率よく再利用できます。Workerの中では、JavaScriptホットローンチとホットスポットサンドボックスのスナップショットの永続性によってVMが頻繁に作成されないようにし、同じコードがロードされた時にCPUとメモリに負荷がかかるのを防ぎます。これにより、システムのスループットが向上し、fomo3Dなどトランザクション量の多いコントラクトを処理する場合でもIOST V8VMが高いパフォーマンスを実現します。
  • 状態データベースによるインターフェース管理。 これにより、確実に各IOSTトランザクションの原子性(データベースの部分的な更新を防止)を保ち、資金不足エラーが発生した場合にトランザクション全体を拒否します。同時に、 RocksDB にフラッシュする前に、状態データベースで2レベルのキャッシュを行います。すると、さまざまなバージョンのデータに対するアクセス時間が短くなり、一時データのパフォーマンスを最適化できます。


IOST仮想マシンのアーキテクチャ
注釈:(5列のブロックとして、左から1~5列)
(第1列、上から)
PRCリクエスト
ブロック認証
Tx認証

(第2列、上から)
リクエスト
レスポンス

(第3列、上から)
VMManager
VMWorker
V8VM
サンドボックス
V8Engine
通常の結果 例外
VMWorker
VMWorker

(第4列、上から)
レスポンス
リクエスト

(第5列、上から)
状態ストレージ
メモリーキャッシュ
ローカルキャッシュ
MVCCDB
フラッシュ
RocksDB

IOSTサンドボックスシステムアーキテクチャ

JavaScriptスマートコントラクトの最終実行のキャリアとして、IOSTサンドボックスはV8VMとChrome V8の次のパッケージへの呼び出しを完了させます。これは以下のようにコンパイルと実行のフェーズに分かれています。

コンパイル段階

基本的にコントラクト開発とワインディングに使う下記の2つの主要機能があります。

  • コントラクトパック。 スマートコントラクトのパッケージです。Webpackの実装に基づき、現行のコントラクトプロジェクト下にあるすべてのJavaScriptコードをパッケージ化し、依存インストールを自動化して、IOST V8VMによる大規模なコントラクトプロジェクト開発を可能にします。また、IOST V8VMとNode.jsモジュールシステムには完全な互換性があるので、コントラクト開発者はrequire、module.exports、exportsなどのメソッドをシームレスに使用して、ネイティブのJavaScript開発環境と同じように作業ができます。
  • コントラクトスナップショット。 IOSTの仮想マシンは、V8のスナップショット技術を用いて初期設定の初めの空の状態を取り除くことでパフォーマンスを向上させます。実際の実装では、スナップショットをデシリアライズして実行を完了するだけで、JavaScriptの読み込み速度と実行速度が飛躍的に上がります。

実行段階

チェーンコントラクトの実際の実装については、下記の2つの主要機能があります。

  • LoadVM。 Chrome V8オブジェクトの生成、システム実行パラメータの設定、関連するJavaScriptクラスライブラリのインポートなどを含めてVMを初期化すれば、スマートコントラクトの実行前にすべての準備を完了できます。たとえば、次のようなJavaScriptクラスライブラリです。


*注釈:(行ごとに左列から右列へ)
クラスライブラリ  特徴
Blockchain: ブロックチェーン関連の機能。転送、出金、現在のブロックや現在のtx情報の取得など。
Event: イベントは実装されており、JavaScriptコントラクト内のイベントの呼び出しは、ワインディング完了後に呼び戻される。
NativeModule: クラスNode.jsのモジュールシステム。モジュールキャッシュ、モジュールプリコンパイル、ループ呼び出しなど。
Storage: JavaScriptは状態データベースの読み込みと書き出しを行い、コントラクトが実行されなかったり、例外が起こったりした場合にロールバックを完了させる。*

  • 実行。 IOST V8VM上のJavaScriptスマートコントラクトの最終の実装で分離したスレッド実行コントラクトを開き、現行の実行状態を監視します。例外が発生した場合、リソースの使用量や実行時間が限度を超えた場合、Terminateを呼び出して実行中のコントラクトを終了し、例外の結果を返します。


注釈:(5列のブロックとして、左から1~5列)
(第1列、上から)
コンパイル段階
実行段階

(第2列、上から)
コントラクト
コンパイルしたコントラクト

(第3列、上から)
コントラクトパック

(第4列、上から)
コントラクトスナップショット

(第5列、上から)
コンパイルしたコントラクト
実行
例外をチェック
リソースの使用量をチェック
ランタイムをチェック
通常の結果 終了

IOST V8VMのパフォーマンス

パブリックブロックチェーンのインフラストラクチャの核を構成する仮想マシンは、並外れたパフォーマンスを発揮し、ネットワークが求める条件を満たさなければなりません。設計と仮想マシンの選択に着手した際、IOSTはパフォーマンスを最重要の指標に据えていました。

Chrome V8は、JIT、インラインキャッシング、遅延ロードなどを使ってJavaScriptを解釈実行します。Chrome V8の高性能のおかげで、IOST V8VMのJavaScript実行速度ははるかに向上しました。再帰的フィボナッチ、メモリコピー、複雑なCPU操作でEVM、EOS、C Lua、V8VMのパフォーマンスをテストした結果、以下の結果が得られました。


注釈:(最左列のみ、上から)
CPU処理速度(8000)
フィボナッチ(32)
文字連結(10000)

以上の結果から、主流のVM実装でIOST V8VMが成果を上げていることが明らかになりました。上記のテストには、仮想マシンが設定を開始し、ロードする時間が含まれています。 IOST V8VMの直のコールドブートにも大きなパフォーマンス上の利点があることが分かります。今後、VMオブジェクトプール、LRUキャッシュなども接続して仮想マシンのCPUとメモリの使用率を向上させ、IOSTのスマートコントラクト処理能力をさらに向上させていきます。

まとめ

現在、私たちのテストネットワークは、IOST V8VM仮想マシンの最初のバージョンを実行しています。この最初のバージョンでは、意図した機能をすべて実現し、投票、コントラクトドメインネーム、トークン機能などの多くの設計概念を実証しました。引き続き、次の機能に焦点を当ててIOST V8VMの開発を進めます。

  • 全てのレイヤーおよびシステムのセキュリティ向上
  • 高パフォーマンスで飛躍的に速いコントラクトの実行処理
  • 標準ライブラリの増設、改良を含め、より開発、利用がしやすい設計
  • 大規模なプロジェクト構築、デバッグ、完全なツールチェーンのサポート

これから数週間のうちに、テストネットワークのアップデートによってさらに多くの新機能を実装する予定です。私たちは、現時点で展開されているあらゆるネットワークを凌駕し、業界をリードする仮想マシンをデプロイするという最終目標に近づいています。この開発だけでなく包括的なIOSTエコシステムの未来も非常に楽しみです。

付録:仮想マシンのベンチマークプログラム

EVMのコード

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)
}
}
}

Luaのコード

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のコード

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) )

V8のコード

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
}