アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

Solidity Assembly入門 ~ Function Selector ~

今回の記事はSolidity Assembly入門という連載記事の第5回目です。

この連載ではSolidityのコードをコンパイルした時に生成されるopcodeについて解説していきます。
この連載ではSolidityのコードをデバッグするのに必要な知識を得られることを目的としています。
前回の記事はこちら。
y-nakajo.hatenablog.com

第5回目の今回は、Solidityの関数をEVM上でどのように表現されているのかについて説明します。公式ドキュメントはこちらです。
Solidity Document#Function Selector
Solidity Document#Functions

EVM上でのFunction

EVM上、つまりSolidityのソースコードコンパイルして生成されたOPCODE上でのfunctionは関数のIDとして扱われる4bytesのfuntionIdとjump命令によって表現されます。

functionId

functionIdは関数の名前と引数の型を文字列とし、その文字列のkeccak256ハッシュ関数で生成されたハッシュ値の先頭4byteです。
コードで表現すると以下ような感じです。

bytes4(keccak256("fun(uint256)")

functionIdに関係するのは関数の名前と引数の型のみです。関数の修飾子(view や public payableなど)は影響しません。またmodifierも影響しません。つまり、func(uint256 _val) publicfunc(uint256 _val) public returns(uint)は同じfunctionIdになります。

Function Selector

外部から関数を呼び出すときに、関数の実態を探し出し実行する処理のことです。
Function Selectorはコンパイル後に生成されるopcodesの先頭に近い位置に配置されます。
Function SelectorのOPCODESは次のような定型処理になります。

DUP1
PUSH4 <functionId>
EQ
PUSH2 <program counter address>
JUMPI

上記の定型コードがpublic / externalな関数の数だけ生成されます。program counter addressは関数の実態処理が記述されているコード位置を示します。functionIdは前述した関数のIDです。このOPCODESの動作の詳細については後述します。

transactionから関数が実行されるメカニズム

次は、EthereumのtransactionをトリガーにしてContractの関数が実行されるメカニズムについて説明します。
このメカニズムの重要なポイントが前述したfunctionIdFunction Selectorとなります。

Contractの関数を呼び出すためのTransaction構造

web3.eth.contract#methodsなどを使って、Contractの関数を呼び出すtransactionを作成した場合は、必ずtransactionのdata部の先頭4byteに対象となる関数のfunctionIdが埋め込まれます。
EVMではこのdata部を引数として受け取り、解析することで実行すべき関数を探索します。

FunctionIdのセパレート処理

ContractのOPCODESでは一番最初にTransaction(=Message)から渡された引数の中からfunctionIdのみを切り出すための処理が実行されます。その処理もまた定型化されており、以下のOPCODESとなります。

005 PUSH1 04
007 CALLDATASIZE
008 LT
009 PUSH2 <error end program counter>
012 JUMPI
013 PUSH1 00
015 CALLDATALOAD
016 PUSH29 0100000000000000000000000000000000000000000000000000000000
046 SWAP1
047 DIV
048 PUSH4 ffffffff
053 AND

簡単に処理内容を解説します。
1. transactionから渡されたデータのサイズが4byteより大きいかチェックする(最低でも32byteはあるはずなので)

005 PUSH1 04
007 CALLDATASIZE
008 LT

2. 4byte未満の時は処理をエラー終了するので、エラー終了処理の位置にjumpする

009 PUSH2 <error end program counter>
012 JUMPI

3. transactionから渡されたデータを先頭から32byte分ロードする

013 PUSH1 00
015 CALLDATALOAD

4. transactionから渡されたデータのうち先頭4byteだけを残す

016 PUSH29 0100000000000000000000000000000000000000000000000000000000
046 SWAP1
047 DIV
048 PUSH4 ffffffff
053 AND

これで、最終的にstackの先頭にfunctionIdを示す4bytesのデータが残ります。

Functionの実行

前述の処理でstackの先頭にはfunctionIdが残っているため、このfunctionIdを使って関数の本体の位置にjumpします。このときにFunction SelectorのOPCODESが実行されます。public / externalな関数を3つ持つContractの場合は具体的に以下のようなOPCODESになります。

054 DUP1
055 PUSH4 6057361d
060 EQ
061 PUSH2 005c
064 JUMPI

065 DUP1
066 PUSH4 99130cc4
071 EQ
072 PUSH2 0089
075 JUMPI

076 DUP1
077 PUSH4 9fa6dd35
082 EQ
083 PUSH2 00b4
086 JUMPI

087 JUMPDEST
088 PUSH1 00
090 DUP1
091 REVERT

Function Selectorの動きを解説すると
1. transactionから渡されたfunctionIdをコピー(EQ実行したときに消えるので)

054 DUP1

2. functionIdが一致しているかチェック


055 PUSH4 6057361d
060 EQ

3. functionIdが一致していたらその関数の処理(本体)があるprogram counter address へjumpする


061 PUSH2 005c
064 JUMPI

4. 1〜3のOPCODESがこのContractが持つpublic / externalな関数の数だけ続く

065 DUP1
066 PUSH4 99130cc4
071 EQ
072 PUSH2 0089
075 JUMPI

076 DUP1
077 PUSH4 9fa6dd35
082 EQ
083 PUSH2 00b4
086 JUMPI

5. どれにもヒットしなかったらエラーで終了する

087 JUMPDEST
088 PUSH1 00
090 DUP1
091 REVERT

Function Modifiersが与える影響

Solidityのコードとしていろいろな関数修飾子が定義されております。これらの修飾子は基本的に関数のIDには影響しません。ただし、幾つかの修飾子はFunction Selectorの生成に影響を与えます。具体的に影響を与える修飾子とその内容は以下の通りです。

  • public / external: Function Selectorが生成される
  • private / internal: Function Selectorが生成されない。

理由は明確で、public, external修飾子を与えた関数は外部呼び出しを可能にする必要があります。そのためfunctionIdによる呼び出し処理である、Function SelectorのOPCODESが生成されます。
逆に外部からの呼び出しを許可しない、private / internalではFunction SelectorのOPCODESが生成されません。

なお、 public externalの違いは以下の通りです。

  • public: 内部、外部の両方から呼び出し可能
  • external: 外部からのみ呼び出し可能。内部からは呼び出し不可。

となっていますが、実はOPCODE(=EVM)レベルで見ると両者には違いはありません。externalについてはcompilerがparseするときにエラーとしているだけで、最終的に吐き出されるOPCODEはpublicを指定したときと同じになります。なので例えば以下のようなコードは実行可能です。
ethfiddle.com

まとめ

今回の記事は今までの記事よりも細かくOPCODESの処理に触れることができた気がします。結局SolidityのOPCODE及びEVMについて説明するときにはABIの仕組みの説明になってしまうのは仕方ないのかな。
今回の内容でTransactionからどのようにしてContractが呼び出されているかを理解できたと思います。この辺の低レベル層の仕組みは理解しても新しいことができるようになるわけではないのですが、バグが発生したときの原因の特定やDappsのアーキテクチャ設計をする場合などに大きな手助けになると思います。