イーサリアムバーチャルマシン(EVM)の欠陥と欠点(前)

こんにちは。ヒツジノブナガです。

QtumのCTOであるJordan Earlzのブログを漁っていたところ、面白い記事を見つけました。

(参考記事)The faults and shortcomings of the EVM

技術者向けで難しい記事ですが、勉強になりましたので、今日は上記記事を翻訳して紹介したいと思います。

めちゃくちゃ長いので3回に分けます。
(以下、翻訳)

*わたしは非技術者なので翻訳には誤りがあると思います。もし内容や表現に誤りがあれば教えていただけると幸いです。

 

イーサリアムバーチャルマシン(Ethereum Virtual Machine:EVM)の欠陥と欠点

最初に自己紹介すると、私はQtumの共同設立者です。Qtumは基本的にEthereum仮想マシン(EVM)を採用し、(他の多くの暗号通貨と同様)EVMをEthereumではないブロックチェーンに載せるプロジェクトです。

EVMをブロックチェーンに載せる途中で、私はEVMについて学ぶ気が失せてしまいました。何が学習欲を失わせてしまったのでしょうか?まあ、一言で言えば私はEVMが好きではありません。私は個人的には、EVMは非実用的な実装であって実用に向かないと考えています。

免責条項として、私はEVMの問題を修正するため、Qtumに別のVMを追加することを検討したいと考えています。

(翻訳者注:彼は最近この問題解決のためのVM(x86VM)をリリースしました。)
(参考記事)【技術者向け】Qtum、X86ヴァーチャルマシンをリリース

簡潔に EVMのポイントは何であるか、なぜEVMが作られたのかを見ていきましょう。
設計理念によれば、EVMは以下の目的で設計されています。

  1. シンプリシティ
  2. 決定論
  3. コンパクトなバイトコードサイズ
  4. ブロックチェーンの特殊化
  5. シンプルなセキュリティ(←え?)
  6. 最適化可能

文書を読むとEVMの考え方はかなり分かります。ではEVMのどこが間違っているのでしょうか?それは今日の技術やパラダイムでうまくいくように作られていないところです。とてもよいデザインですが、現実世界では非常に使いにくいのです。以下、主に欠点について述べますが、EVMの特徴について語っていきましょう。

256ビットの整数しか使えない

最新のプロセッサでは、高速で効率的な整数のために4つの選択肢があります。

  1. 8ビット整数
  2. 16ビット整数
  3. 32ビット整数
  4. 64ビット整数

もちろん、場合によっては32ビットが16ビットより速く、少なくともx86 8ビット整数は完全にサポートされていません(ネイティブ除算も乗算もありませんが)。整数演算に必要なサイクル数を保証するもので、キャッシュミスやメモリレイテンシーを含まない場合は数ナノ秒で測定されます。とにかく、これらは近代的なプロセッサーが「ネイティブに」使用する整数のサイズであり、無関係の操作を必要とする翻訳やその他のものがないと言うだけで十分です。

EVMは速度と効率を最適化するためのもので、整数サイズの選択肢は以下のとおりです。

  • 256ビットの整数

参考までに、x86アセンブリに2つの32ビット整数を追加する方法を説明します(つまり、お使いのPCに搭載されているプロセッサ)

mov eax, dword [number1]
add eax, dword [number2]

お使いのプロセッサが64ビット対応であると仮定して、x86アセンブリに2つの64ビット整数を追加する方法は次のとおりです。

mov rax, qword [number1]
add rax, qword [number2]

32ビットx86コンピュータに2つの256ビット整数を追加する方法は次のとおりです。

mov eax, dword [number]
add dword [number2], eax
mov eax, dword [number1+4]
adc dword [number2+4], eax
mov eax, dword [number1+8]
adc dword [number2+8], eax
mov eax, dword [number1+12]
adc dword [number2+12], eax
mov eax, dword [number1+16]
adc dword [number2+16], eax
mov eax, dword [number1+20]
adc dword [number2+20], eax
mov eax, dword [number1+24]
adc dword [number2+24], eax
mov eax, dword [number1+28]
adc dword [number2+28], eax

しかし、256ビット整数を64ビットx86コンピュータに追加すると少し良くなります。

mov rax, qword [number]
add qword [number2], rax
mov rax, qword [number1+8]
adc qword [number2+8], rax
mov rax, qword [number1+16]
adc qword [number2+16], rax
mov rax, qword [number1+24]
adc qword [number2+24], rax

とにかく、256ビット整数での作業は、プロセッサによってネイティブにサポートされている整数長で作業するよりもはるかに複雑で低速です。
EVMはこの設計を採用しており、他の整数サイズで動作するためのオペコードを追加するよりも、256ビットの整数だけをサポートするほうがはるかに簡単です。 唯一の非256ビット演算は、メモリから1〜32バイトのデータを引き出すための一連のプッシュ命令と、8ビット整数で動作するいくつかの命令です。

したがって、すべての操作に256ビット整数を使用するのは効率的ではないのです。

「4バイトまたは8バイトのワードはアドレスを格納するにはあまりにも制限があり、暗号計算には大きな値があり、無制限の値では安全なガスモデルを作るのは難しい」という意見もあるかもしれません。

確かに1つの操作で2つのアドレスを比較できることはかなりクールです。 ただし、32ビットモード(SSEやその他の最適化なし)でx86で同じことを行う方法は次のとおりです。

mov esi, [address1]
mov edi, [address2]
mov ecx, 32 / 4
repe cmpsd 
jne not_equal
; if reach here, then they're equal

address1とaddress2がハードコードされたアドレスであると仮定すると、それは約6 + 5 + 5 = 16バイトのオペコードであるか、アドレスがスタック内にある場合、6 + 3 + 3 = 12バイトのオペコードのようなものです。

大きな整数サイズのもう1つの正当性は、「暗号演算の大きな値」です。しかし、数か月前に読んだことですが、アドレスもしくはハッシュが一致するかどうかを比較することを含まない256ビット整数のユースケースがあるという問題が判明しました。カスタム暗号化は、パブリックブロックチェーンで実行するには非常に高価です。私はgithubで1時間以上検索して、暗号化して定義したいと思うSolidityのスマートコントラクトを見つけようとしましたが、何も見つかりませんでした。

現代のコンピュータでは、暗号化のほとんどのタイプが遅く複雑になることを余儀なくされています。これは、ガスコスト(一般的なアルゴリズムをSolidityに移植することを努力している人はいません)のために公開されているEthereumブロックチェーンで実行するのは経済的ではないことを意味します。

ガス価格が問題にならないプライベートブロックチェーンは存在します。しかし、独自のブロックチェーンを所有している場合は、遅いEVMコントラクトの一部としてプライベートチェーンを用いる必要はありません。C ++、Go、または任意の数の実際のプログラミング言語を使用して、プリコンパイルされたネイティブコードで暗号化を実装するスマートコントラクトです。だから、EVMが256ビット整数だけをサポートしていることにまったく根拠はありません。

256ビットの整数のみを扱っていることは根本的なEVMの問題だと思います。ただし、それほど目立たない領域にはもっと多くの潜在的な問題があります。

EVMのメモリモデル

EVMには、データを置くことができる3つの主要な場所があります

  1. スタック
  2. 一時(temporary)メモリ
  3. 永久(permanent)メモリ

スタックには一定の制限があるため、非常に高価な永続メモリの代わりに一時メモリを使用する必要があります。 EVMに割り当て命令などはありません。EVMを書いてメモリを要求します。これはかなり賢いように思えるかもしれませんが、非常に不吉です。

たとえば、アドレス0xを10000書き込むと、契約では64Kワード(つまり256ビットワードの64K)のメモリが割り当てられ、64Kワードのメモリをすべて使用した場合と同様のガスコストがかかります。

この問題の簡単な回避策として、最後に使用したメモリアドレスを追跡し、必要なときにアドレスを増やしていくことです。一度にたくさんのメモリを必要とし、それ以上メモリを必要としない限り、この策はうまく動作します。

たとえば、100ワードのメモリを使用するクレイジーアルゴリズムを実行したとします。あなたはそれを割り当て、メモリを使い、それを何とかして100ワードのメモリでペイしてください…そして、あなたはその機能を終了します。

いま100ワードのメモリをしようしていますが、もう1ワードのメモリの追加が必要なので、別の単語を割り当てるとします。このときメモリを解放する方法はありません。理論的には、メモリの最後のスペースを追跡していた特別なポインタを減らすことができますが、メモリのブロック全体が再び参照されることはなく、安全に再利用できることがわかっている場合にのみ機能します。それらの100ワードのうち、50ワードと90ワードが必要な場合は、それらをスタックのような別の場所にコピーしてからそのメモリを解放する必要があります。これを支援するツールはEVMから提供されていません(メモリ断片化)。各関数が割り当てられていてグローバルにアクセス可能なメモリを使用していないことを確認するのはあなた次第であり、そのメモリを再利用して何かが吟味プロセスを経た場合、コントラクトは今や重大な状態破損のバグになりえます。

だからあなたは、メモリリユースバグのより大きな階層(クラス)に展開する、もしくはすでに必要以上に割り当てメモリに多額のガスを払うのどちらかしか選択することができません。

さらに、メモリの割り当ては線形のコストではありません。 100ワードのメモリを割り当てて、さらに1ワードを割り当てると、プログラムの開始時にそのメモリの第1ワードを割り当てるよりも、はるかに高価になります。この側面は、大幅にガスコストを削減するために、より多くのコントラクトバグまで自分自身を開くことと比較して、安全であることの経済的コストを大幅に増幅します。

では、なぜメモリを使い、なぜスタックを使用しないのかというと、まあ、スタックはばかげていて制限があるからです。

(次回記事に続きます)

スポンサーリンク