スマートコントラクトをデプロイ、実行するときにEVMは何をやっているのか

どうもこんにちはpjです。今回は基本の基本ですがEVMについて書いていきます。

EVM

EVMはEthereumのスマートコントラクトを動かす仮想マシンです。solidityで書かれたコントラクトはコンパイルされ、EVM上で動きます。EVMは3種類のデータ構造を持っています。

  • stack : LIFOのデータ構造
  • memory : byte配列
  • storage : Key/Valueの形の永続的に保持されるデータ。gas使用量が大きい

これらを書き換えることでスマートコントラクトを実行します。

Gas

トランザクションを作成したり、EVMにコントラクトを実行させたりするときに支払う手数料のことです。
EVMを利用することにコストを加えることで、無制限にEVMを動かせ続けることを防いでいます。

EVMがコントラクトを実行する際、opcodeの単位で段階的に実行されていきます。そのopcodeそれぞれにgasの計算方法が決められており、それが実行されるたびにgasが消費されていきます。
トランザクション作成者(コントラクト実行者)ははじめに多めにgasを支払い(gasLimit)、コントラクト実行後に残ったgasが返ってきます。

またgasという言葉は、EVMの計算ステップの単位としても使われます。opcodeはその処理によって何 gas消費するか決められています。
例えば、トランザクションにデータを埋め込む際には、1 byteごとに5 gasかかります。

Gasの計算

実際に消費される手数料は次の式で求められます。

gas * gasPrice

ただし、ネットワークの状態によって決まるgasPriceです。

実際にEVMを動かす

準備

必要なものをインストールします。

brew install solidity solc

これで今回使うevmコマンドもインストールされます。

アセンブリを読む

今回動かすコントラクトはこちら

pragma solidity ^0.5.5;
contract None {
function none() public pure {
return;
}
}

見ての通り何もしないコントラクトです。次のコマンドでEVMアセンブリを出力します。

> solc none.sol --asm
======= none.sol:None =======
EVM assembly:
/* "none.sol":25:90 contract None {... */
mstore(0x40, 0x80)
callvalue
/* "--CODEGEN--":8:17 */
dup1
/* "--CODEGEN--":5:7 */
iszero
tag_1
jumpi
/* "--CODEGEN--":30:31 */
0x00
/* "--CODEGEN--":27:28 */
dup1
/* "--CODEGEN--":20:32 */
revert
/* "--CODEGEN--":5:7 */
tag_1:
/* "none.sol":25:90 contract None {... */
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop
sub_0: assembly {
/* "none.sol":25:90 contract None {... */
mstore(0x40, 0x80)
callvalue
/* "--CODEGEN--":8:17 */
dup1
/* "--CODEGEN--":5:7 */
iszero
tag_1
jumpi
/* "--CODEGEN--":30:31 */
0x00
/* "--CODEGEN--":27:28 */
dup1
/* "--CODEGEN--":20:32 */
revert
/* "--CODEGEN--":5:7 */
tag_1:
/* "none.sol":25:90 contract None {... */
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0xa877db9f
eq
tag_3
jumpi
tag_2:
0x00
dup1
revert
/* "none.sol":43:88 function none() public pure {... */
tag_3:
tag_4
tag_5
jump // in
tag_4:
stop
tag_5:
jump // out
auxdata: 0xa165627a7a723058203fac2ff90dfd1d67ed32362ddd3afb4e26c4341917988860838a47fdbc52ccf20029
}

また、auxdataというのはauxiliary dataの略で、EVMで実行されない補助データです。
検証に使う為にソースコードのfingerprintなどが入ります。bitcoinでいうOP_RETURNの後の部分のことだと思います。

ちなみにopcodeはEthereumのyellow paperで定められていますが、とても読みにくいので、Solidityのドキュメントを読むのがいいと思います。

デプロイ部分

EVM assembly:
mstore(0x40, 0x80) // memoryの0x40に0x80を入れる
callvalue // valueの値が入る
dup1 // stackの一番上をcopy
iszero // valueが0の時に1を返す
tag_1
jumpi // iszeroが1ならtag_1に飛ぶ
0x00
dup1
revert // stateをrevertしてmemoryを返す
tag_1:
pop // stackの一番上を削除
dataSize(sub_0) // sub_0(contractの中身)のサイズ
dup1
dataOffset(sub_0) // sub_0までのdataoffset
0x00
codecopy // stackの2番目を3番目のサイズ分だけmemoryにcopy
0x00 // memoryのoffset
return // memoryを返す
stop

はじめにmemoryの0x40 に値を入れているのは、Solidityの慣習でここは free memory pointer と呼ばれ、ここに入っている値以降のmemoryがfree memoryですよというということらしいです。
また、valueってのはこのContractを呼ぶ時の送金額ですね。

まとめると、valueが0の時に sub_0をメモリのコピーして変更されたstateを戻してメモリを返しています。

自分がチューターをしているブロックチェーン集中講座でもデプロイって具体的にどんなことをやっているのかわからないって人が多かったのですが、これを見ればすぐにわかりますね。

コントラクトの中身(sub_0)

sub_0: assembly {
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
jumpi(tag_2, lt(calldatasize, 0x04)) // inputのサイズが4より小さい時にtag_2にjump
shr(0xe0, calldataload(0x00)) // 0xe0 bitだけ0x00のデータを右にshiftする
dup1
0xa877db9f
eq // stackの1と2が同じ時に1, 他は0
tag_3
jumpi // eqが1の時にtag_3にjump
tag_2:
0x00
dup1
revert
tag_3:
tag_4
tag_5
jump // tag_5にjump
tag_4:
stop // 実行をやめる
tag_5:
jump // tag_4にjump
}

コントラクトの中身がこちらになります。inputのサイズが4より小さい時にはtag_2に飛ばされ、revertされます。
また、そうでない場合には 0x00 のデータが想定通りか確認され、想定通りならtag_5, tag_4と飛んでstopします。
今回は何もしないコントラクトなのでtag_3,4,5はjumpし合うだけになっています。

逐次実行

アセンブラはスタックなどの状態を想像しながら読まないといけないので辛いです。
ですが、EVM内部状態を表示しながら逐次実行してくれるevmコマンドがあるのでそっちでも実行してみます。

コントラクトをコンパイルしてバイナリにし、それを実行します。

> solc none.sol --bin -o .
> evm --debug --codefile None.bin run

出力はとても長くなるのではじめと終わりのスクショを載せます。

EVMの内部状態の変化やopcodeが実行されるたびにgasが消費されていくこと(defaultでgasLimitは10000000000)、最終的に sub_0 のバイナリがメモリに書き込まれていることが確認できます。

まとめ

今回は実際にコントラクトのデプロイや実行がどのようにEVMに解釈、実行されるのかを見ることで具体的に何が行われているかを見てきました。
自分は簡単なコントラクトしか書いたことがなく、具体的にEVMがどのように動いているのかがモヤモヤしていたのですが、今回でそれが晴れた気がします。

evmコマンドを用いることで自分の書いたコントラクトがEVMの内部状態を確認しながら実行することができ、EVMへの理解を深めることができます。
なので皆さんも是非自分の書いたコントラクトがどのように処理されていくのかを上の手順で確認しましょう。


お知らせ

■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。

HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo

■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。

ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/

■HashHubでは下記のポジションを積極採用中です!
・コミュニティマネージャー
・ブロックチェーン技術者・開発者
・ビジネスディベロップメント

詳細は下記Wantedlyのページをご覧ください。

Wantedly:https://www.wantedly.com/companies/hashhub/projects