前置き
先日、自分の勉強もかねてEVMの簡易実装であるToyEVMを作りました。
そのときにEVMについて結構調べましたので調べた内容をまとめたものを記事にしました。
🚗 ToyEVM
よかったらスターお願いします。
📟 Ethereum Virtual Machine について
EVMはスタックベース、ビッグエンディアン形式の1ワード256bitとするバーチャルマシンであり、Ethereumブロックチェーン上でスマートコントラクトを実行されるために作られたものです。
256bitスタックベースのVMなのでuint256といった一般的な高級言語では定義されていない整数型をよく利用します。
浮動小数点型はサポートされていないため、1ETHはEVMの内部では1000000000weiという単位で取り扱われます。1weiがETHの最小単位です。
スマートコントラクトはトランザクションを受け取って計算やさらなるトランザクションを発行するためにEVMバイトコードを実行する以外は普通のアカウントと変わりません。
トランザクションは0バイト以上の、コントラクトやほかの情報とどのように作用するか決めたりするためのデータを持っています。
コントラクトの実行はバイトコードの先頭から開始されます。
バイトコードはオペランドを持つPUSH命令を除いて1バイトの固定長です。
ほかのスタックベースのVMと同様に、オペコードはスタックの先頭からオペランドをPOPして実行結果をスタックにPUSHします。
🌏 EthereumのステートとEVM
EthereumのステートはWorld stateとAccount Stateのおおまかに2つに分けられます。
EVMはトランザクションの実行を通してこのステートを変更する役割を持ちます。
これらの状態はToyEVMにおいてはstate.rsで実装されています。
World State
EthereumアドレスからAccount Stateへのマッピングです。
Account State
Ethereumの残高、nonce、storage(後述するデータ領域)、コントラクトコードから構成されます。
nonceは、EOAではそのアカウントが実行したトランザクションの総数、コントラクトアカウントでは作成されたコントラクトの数を表します。
ステートへの反映
トランザクション実行時には、EVMのインスタンスが作成されその中でコントラクトのEVMバイトコードを実行します。
DBのトランザクションと同様に、Ethereumへのステートの反映はEVMインスタンスでトランザクションを完遂して初めて行われます。
途中でgas不足などによりトランザクションが中断した場合はEthereumへのステート反映は行われることはありません。
📦 コントラクトのデプロイ
新しいコントラクトをEthereum上にデプロイするには、デプロイのための特別なトランザクションが必要になってきます。
デプロイ用のトランザクションでは、トランザクションのtoフィールドをアドレス0x0に設定し、データフィールドをデプロイ用のEVMバイトコードにする必要があります。
このバイトコードはデプロイするEVMバイトコードとは異なったものとなっています。
例えば、6005600401
というEVMバイトコードをデプロイするとするとします。
これは逆アセンブルすると以下のようなコードになります。
PUSH1 05 // 6005
PUSH1 04 // 6004
ADD // 01
このバイトコードをデプロイする際に実行されるバイトコードは以下のように600580600b6000396000f36005600401
なり元のバイトコードとは異なったものになっています。
// デプロイ用のバイトコード
PUSH1 05 // 6005
DUP1 // 80
PUSH1 0b // 600b
PUSH1 00 // 6000
CODECOPY // 39
PUSH1 00 // 6000
RETURN // f3
// 本体のコード
PUSH1 05 // 6005
PUSH1 04 // 6004
ADD // 01
このデプロイ用のコードでは、最初にデプロイされるバイトコードの長さをスタックにpushしています。(PUSH1 05)
そしてCODECOPYで0b-0b+05つまりデプロイされるバイトコードをメモリの先頭にコピーし、それをreturnで返しているため、デプロイを行うコード(600580600b6000396000f36005600401
)の返り値はデプロイされるコード(6005600401
)となります。
この返り値がコントラクトのコードとしてEthereum上に登録されます。
🎮 コントラクトの実行
コントラクトはユーザーのためにインターフェースとなるABIを公開しています。
コントラクトを実行するために、ユーザーは任意のwei(Ethereumの最小単位)とABIに基づいたデータをもつトランザクションを発行します。トランザクションはコントラクトをどのように実行するかという情報に加えてほかの付随情報も持っています。
トランザクションの実行では主に以下の4つのデータ領域を操作します。
- Call Data
- Stack
- Memory
- Storage
Call Data
この領域に関連するオペコード: CALLDATALOAD, CALLDATASIZE, CALLDATACOPY
トランザクションに付随されるデータ(分かりやすく言えば引数など)を格納したデータ領域です。
スタック上のオペランドを含めると命令全体で4byteになることもあります。
Stack
この領域に関連するオペコード: PUSH1, DUP1, SWAP1, POP
EVMは1層1層が256bitのスタックを実装しており、一般的なスタックベースのプログラミング言語同様に、ローカル変数、関数の引数、リターン先のアドレスを保持しています。
リターン先のアドレスとほかの変数を区別する方法は少々変わっていますがここでは割愛します。
Memory
この領域に関連するオペコード: MLOAD, MSTORE, MSTORE8
メモリは1バイトの大きさの配列でスマートコントラクトが実行される間、データを保持する揮発性のデータ領域です。
Storage
この領域に関連するオペコード: SLOAD, SSTORE
ストレージは、HashMap型の不揮発性のデータ領域です。
コントラクトのインスタンス変数とハッシュマップはストレージに保存され、web3.eth.getStorageAt(address, key)
でアクセスすることができます。
その他の命令
LOG命令はブロック内にログを記録することで軽量クライアントによる検証を効率的に行えるようにします。
CALL, CREATE命令はコントラクトにほかのコントラクトのアクションを引き起こしたり、新しいコントラクトを作らせる命令です。
RETURN命令は、メモリからデータのまとまりを返り値として返す命令であり、SUICIDEはコントラクトを消去して、たまっていた資産を特定のアドレスに返す役割を持った命令です。
☁️ Gas
GasはWeiと同様にEthereumにおける基本単位です。
トランザクションの実行、EVMでのオペコード実行など、Ethereum上で計算リソースを利用する際の基本単位がGasです。
Gasはマイナーへの仕事の対価という側面と、DOS攻撃に対する防御策という側面を持っています。
Gasはどのように使われるか
トランザクションの実行の際にEVMにはユーザーが指定した分のGasが与えられ、実行が開始されます。
Gasはオペコードの実行ごとに減少していきます。どのオペコードがどれだけGasを消費するかはすでに決まっています。(Ethereum Virtual Machine についてのOpcode一覧参照)
Gasが足りていた場合
コントラクトコードの実行に消費したGasは手数料としてイーサに変換されマイナーに報酬として支払われます。
余ったガスは送信者のもとに送り返されます。
Gasが足りなかった場合
供給されたGasがコントラクトコードを実行するのに必要なGasより少なかった場合、例外(Out Of Gas)が発生します。
トランザクションが中断されるためEthereumへのステート反映は生じませんが、マイナーの計算資源を利用したことには変わりはないのでトランザクションを送信した人はその分のGasを消費することになります。
📑 Opcode一覧
Value | Mnemonic | Gas Used | Subset | Removed from stack | Added to stack | Notes | Formula Notes |
---|---|---|---|---|---|---|---|
0x00 | STOP | 0 | zero | 0 | 0 | Halts execution. | |
0x01 | ADD | 3 | verylow | 2 | 1 | Addition operation | |
0x02 | MUL | 5 | low | 2 | 1 | Multiplication operation. | |
0x03 | SUB | 3 | verylow | 2 | 1 | Subtraction operation. | |
0x04 | DIV | 5 | low | 2 | 1 | Integer division operation. | |
0x05 | SDIV | 5 | low | 2 | 1 | Signed integer division operation (truncated). | |
0x06 | MOD | 5 | low | 2 | 1 | Modulo remainder operation | |
0x07 | SMOD | 5 | low | 2 | 1 | Signed modulo remainder operation. | |
0x08 | ADDMOD | 8 | mid | 3 | 1 | Modulo addition operation. | |
0x09 | MULMOD | 8 | mid | 3 | 1 | Modulo multiplication operation. | |
0x0a | EXP | (exp == 0) ? 10 : (10 + 10 * (1 + log256(exp))) | 2 | 1 | Exponential operation. | "If exponent is 0 | |
0x0b | SIGNEXTEND | 5 | low | 2 | 1 | Extend length of two’s complement signed integer. | |
0x10 | LT | 3 | verylow | 2 | 1 | Less-than comparison. | |
0x11 | GT | 3 | verylow | 2 | 1 | Greater-than comparison. | |
0x12 | SLT | 3 | verylow | 2 | 1 | Signed less-than comparison. | |
0x13 | SGT | 3 | verylow | 2 | 1 | Signed greater-than comparison. | |
0x14 | EQ | 3 | verylow | 2 | 1 | Equality comparison. | |
0x15 | ISZERO | 3 | verylow | 1 | 1 | Simple not operator. | |
0x16 | AND | 3 | verylow | 2 | 1 | Bitwise AND operation. | |
0x17 | OR | 3 | verylow | 2 | 1 | Bitwise OR operation | |
0x18 | XOR | 3 | verylow | 2 | 1 | Bitwise XOR operation. | |
0x19 | NOT | 3 | verylow | 1 | 1 | Bitwise NOT operation. | |
0x1a | BYTE | 3 | verylow | 2 | 1 | Retrieve single byte from word | |
0x20 | SHA3 | 30 + 6 * (size of input in words) | 2 | 1 | Compute Keccak-256 hash. | 30 is the paid for the operation plus 6 paid for each word (rounded up) for the input data. | |
0x30 | ADDRESS | 2 | base | 0 | 1 | Get address of currently executing account. | |
0x31 | BALANCE | 400 | 1 | 1 | Get balance of the given account. | ||
0x32 | ORIGIN | 2 | base | 0 | 1 | Get execution origination address. | |
0x33 | CALLER | 2 | base | 0 | 1 | Get caller address. | |
0x34 | CALLVALUE | 2 | base | 0 | 1 | Get deposited value by the instruction/transaction responsible for this execution. | |
0x35 | CALLDATALOAD | 3 | verylow | 1 | 1 | Get input data of current environment. | |
0x36 | CALLDATASIZE | 2 | base | 0 | 1 | Get size of input data in current environment. | |
0x37 | CALLDATACOPY | "2 + 3 * (number of words copied | rounded up)" | 3 | 0 | Copy input data in current environment to memory. | |
0x38 | CODESIZE | 2 | base | 0 | 1 | Get size of code running in current environment. | |
0x39 | CODECOPY | "2 + 3 * (number of words copied | rounded up)" | 3 | 0 | Copy code running in current environment to memory. | |
0x3a | GASPRICE | 2 | base | 0 | 1 | Get price of gas in current environment. | |
0x3b | EXTCODESIZE | 700 | extcode | 1 | 1 | Get size of an account’s code. | |
0x3c | EXTCODECOPY | "700 + 3 * (number of words copied | rounded up)" | 4 | 0 | Copy an account’s code to memory. | |
0x40 | BLOCKHASH | 20 | 1 | 1 | Get the hash of one of the 256 most recent complete blocks. | ||
0x41 | COINBASE | 2 | base | 0 | 1 | Get the block’s beneficiary address. | |
0x42 | TIMESTAMP | 2 | base | 0 | 1 | Get the block’s timestamp. | |
0x43 | NUMBER | 2 | base | 0 | 1 | Get the block’s number. | |
0x44 | DIFFICULTY | 2 | base | 0 | 1 | Get the block’s difficulty. | |
0x45 | GASLIMIT | 2 | base | 0 | 1 | Get the block’s gas limit. | |
0x50 | POP | 2 | base | 1 | 0 | Remove item from stack. | |
0x51 | MLOAD | 3 | verylow | 1 | 1 | Load word from memory. | |
0x52 | MSTORE | 3 | verylow | 2 | 0 | Save word to memory | |
0x53 | MSTORE8 | 3 | verylow | 2 | 0 | Save byte to memory. | |
0x54 | SLOAD | 200 | 1 | 1 | Load word from storage | ||
0x55 | SSTORE | ((value != 0) && (storage_location == 0)) ? 20000 : 5000 | 1 | 1 | Save word to storage. | 20000 is paid when storage value is set to non-zero from zero. 5000 is paid when the storage value's zeroness remains unchanged or is set to zero. | |
0x56 | JUMP | 8 | mid | 1 | 0 | Alter the program counter | |
0x57 | JUMPI | 10 | high | 2 | 0 | Conditionally alter the program counter. | |
0x58 | PC | 2 | base | 0 | 1 | Get the value of the program counter prior to the increment corresponding to this instruction. | |
0x59 | MSIZE | 2 | base | 0 | 1 | Get the size of active memory in bytes. | |
0x5a | GAS | 2 | base | 0 | 1 | "Get the amount of available gas | including the corresponding reduction for the cost of this instruction." |
0x5b | JUMPDEST | 1 | 0 | 0 | Mark a valid destination for jumps | ||
0x60 -- 0x7f | PUSH* | 3 | verylow | 0 | 1 | Place * byte item on stack. 0 < * <= 32 | |
0x80 -- 0x8f | DUP* | 3 | verylow | * | * + 1 | Duplicate *th stack item. 0 < * <= 16 | |
0x90 -- 0x9f | SWAP* | 3 | verylow | * + 1 | * + 1 | Exchange 1st and (* + 1)th stack items. | |
0xa0 | LOG0 | 375 + 8 * (number of bytes in log data) | 2 | 0 | Append log record with no topics. | 375 is paid for operation plus 8 for each byte in data to be logged. | |
0xa1 | LOG1 | 375 + 8 * (number of bytes in log data) + 375 | 3 | 0 | Append log record with one topic. | 375 is paid for operation plus 8 for each byte in data to be logged plus 375 for the 1 topic to be logged. | |
0xa2 | LOG2 | 375 + 8 * (number of bytes in log data) + 2 * 375 | 4 | 0 | Append log record with two topics. | 375 is paid for operation plus 8 for each byte in data to be logged plus 2 * 375 for the 2 topics to be logged. | |
0xa3 | LOG3 | 375 + 8 * (number of bytes in log data) + 3 * 375 | 5 | 0 | Append log record with three topics. | 375 is paid for operation plus 8 for each byte in data to be logged plus 3 * 375 for the 3 topics to be logged. | |
0xa4 | LOG4 | 375 + 8 * (number of bytes in log data) + 4 * 375 | 6 | 0 | Append log record with four topics. | 375 is paid for operation plus 8 for each byte in data to be logged plus 4 * 375 for the 4 topics to be logged. | |
0xf0 | CREATE | 32000 | 3 | 1 | Create a new account with associated code. | ||
0xf1 | CALL | Complex -- see yellow paper Appendix H | 7 | 1 | Message-call into an account. | ||
0xf2 | CALLCODE | Complex -- see yellow paper Appendix H | 7 | 1 | Message-call into this account with an alternative account’s code. | ||
0xf3 | RETURN | 0 | zero | 2 | 0 | Halt execution returning output data. | |
0xf4 | DELEGATECALL | Complex -- see yellow paper Appendix H | 6 | 1 | "Message-call into this account with an alternative account’s code | but persisting the current values for sender and value." | |
0xfe | INVALID | NA | NA | NA | Designated invalid instruction. | ||
0xff | SELFDESTRUCT | 5000 + ((create_new_account) ? 25000 : 0) | 1 | 0 | Halt execution and register account for later deletion | 5000 for the operation plus 25000 if a new account is also created. A refund of 24000 gas is also added to the refund counter for self-destructing the account. |
※ opcode-gas-costs_EIP-150_revision-1e18248_2017-04-12.csvから移植したものです
さいごに
冒頭でも紹介しましたが、今回調べたことを使ってEVMの簡易実装であるToyEVMをRustを使って作りました。
オペコードなどまだまだ不十分なところがありますが、もし自分でEVMを実装したいという人がいたらワークフローなど参考になると思います。