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

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

Solidity v0.6.5で追加されたImmutable keywordについて

今回は、Solidityのv0.6.5で追加されたImmutable keywordについて紹介します。
なお、v0.6.5は2020/04/06にリリースされております。執筆時点での最新バージョンはv0.6.8となっています。

Immutable keywordについての公式の説明はこちらのブログを参照ください。
solidity.ethereum.org

Immutable Keywordについて

v0.6.5で追加されたimmutable keywordは公式ブログに例示されているように、初期化フェーズに任意の値を不変値として定義することができます。v0.6.5以前にも似たkeywordとしてconstant keywordがありますが、こちらはその名の通り定数しか定義することができません。このキーワードを付与した定数はコンパイルフェーズにリテラルに変換されてコントラクトの本体コードに埋め込まれます。

つまり、immutable keywordの特徴はコントラクトの初期化フェーズに不変値を算出してコントラクトの本体コードの参照箇所を算出したリテラルに書き換えるという機能になります。

constantとimmutableの比較

constant immutable
処理されるフェーズ コンパイルフェーズ 初期化フェーズ
変数として定義可能なタイプ リテラルのみ 状態変数(msg.senderなど)や任意の演算結果
初期化フェーズでの参照 可能 不可能

constantはコンパイルフェーズに処理される(PUSHオペコードに置き換えられる)のに対し、immuableはコントラクトの初期化フェーズ、つまり、コントラクトをデプロイするTransactionが実行されたタイミングで処理されます。
そのため、immutable変数には状態変数(msg.senderなど)の値を格納することができますし、任意の演算結果を格納することができます。
その代わりに、immutable変数は初期化フェーズ時点ではまだ未設定なため参照することができません。
また、immutable変数は初期化フェーズで値を確定させなければなりません。immutable変数を最初に参照された時に値を設定するというような使い方はできません。これはコントラクトの本体コードはデプロイされた後は不変であるというルールに違反するためです。

immutableの動作概要

基本的な流れ

immutable keywordを付与した変数は以下の順番で最終的にリテラルとしてコントラクトの本体コードに埋め込まれます。

  1. コンパイルフェーズ時に、immutable 変数を参照している箇所をplaceholderとしてマークする。
  2. 続いて、コンパイルフェーズ時に、placeholder部分を不変値のリテラルに置き換える処理を初期化処理として埋め込む。
  3. コントラクト生成時に、初期化処理が実行され、最終的にstateに保存されるコントラクトの本体コードの中のplaceholderの部分がリテラルに置き換えられる。

デバッガーを使って確認してみる

では実際にimmutable変数をコントラクトの初期化フェーズ時に本体コードへのリテラルを埋め込む処理や、関数を呼び出した場合にリテラルとして変数の値が参照されていることをRemixデバッグ機能を使って確認してみます。
確認に用いたコードは、公式ブログに記載されていた、以下のサンプルコードを利用しました。
gist.github.com

コントラクトの初期化フェーズ時の動作

immutable変数がコントラクトの初期化フェーズに、本体コードの対応するplaceholder部分にリテラルとして埋め込まれていく動作を確認します。
コントラクトをデプロイした際に、コントラクトがどのようにstateに保存されるのかについては過去の記事で解説していますのでそちらを参照してください。
y-nakajo.hatenablog.com

コントラクトの本体コードの準備まで

コントラクトのデプロイTransactionも例に漏れずEVM上で実行されます。その実行結果としてreturnされたデータが最終的にコントラクトの本体コードとしてstateに保存されます。
それらの処理を実行するopcodeは下図のものになります。
f:id:y_nakajo:20200528100748p:plain

CODECOPY opcodeによって、最終的に保存されるコントラクトの本体コードがメモリ上にコピーされます。CODECOPYが実行された後のメモリの状態は次の通りです。下図のうち、赤い枠線で示している部分が、immutable変数によって置き換えられるplaceholderの箇所になります。今回はimmutable変数が3箇所で参照されているため、placeholderも3箇所存在しています。
f:id:y_nakajo:20200528101818p:plain

また、この時点でのstackの状態は下図のようになっており、これからplaceholder部分に埋め込まれる、42(2a)と420,000(0668a0)とmsg.senderのアドレス(ca35b7d915458ef540ade6068dfe2f44e8fa733c)がそれぞれstackに積まれていることがわかります。
f:id:y_nakajo:20200528102436p:plain

placeholderをリテラルに置き換える処理

owner変数をリテラルに置き換える処理を行なっているopcodeは次の部分になります。
f:id:y_nakajo:20200528103448p:plain
これはメモリ上のc4(196)の位置から32byteのデータを置き換えるという命令コードになります。このopcodeを実行するとメモリの該当部分が以下のようにmsg.senderのアドレスに置き換えられます。
f:id:y_nakajo:20200528103715p:plain

続いて、maxDonation変数をリテラルに置き換える処理は次の部分になります。
f:id:y_nakajo:20200528103815p:plain
このopcodeを実行することで、メモリ上の54(84)の位置から32byteのデータを0668a0(420,000)に置き換えます。MSTOR実行後の該当メモリの位置の値が以下のように置き換えられています。
f:id:y_nakajo:20200528104114p:plain

最後に、minDonation変数をリテラルに置き換える処理です。以下の通りowner変数やmaxDonation変数を置き換える時と同様のことが行われています。
f:id:y_nakajo:20200528104212p:plain
f:id:y_nakajo:20200528104228p:plain

immutable変数を参照するときの動作

初期化フェーズの動作を確認したので、続いて、関数を呼び出した時にimmutable変数を参照するときの動作を確認します。
ここでは、withdraw関数を呼び出したときのowner変数の参照のされ方のみを確認します。

withdraw関数の中でownerとmsg.senderを比較している箇所のopcodeは以下の通りです。下図から分かる通り、owner変数はPUSH32 000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733cとリテラルに置き換えられていることがわかります。
f:id:y_nakajo:20200528104919p:plain

まとめ

このように、immutable keywordの動作は初期化フェーズ時に任意の値をコントラクトの本体コード内にリテラルとして埋め込むというものになります。
これの利点は、サンプルコードにもある通り、msg.senderをownerとして保存したり、またはコンストラクタの引数としてわされた値を定数として保存したい場合にgas costを節約することができます。
今までは、定数を定義するためのconstant keywordしか存在していなかったため、上記のように初期化フェーズ時に決定される値を定数として定義することができませんでした。初期化フェーズ時に決定される値を保持するためには通常通り変数としてStorageに格納するしかないため、読み出す時にはSLOADが発生し、5000 gasを毎回支払う必要がありました。
これらをimmutable変数として定義することで、初期化フェーズ時にPUSH opcodeに置き換えられるため、参照時にはわずか3 gasのcostで値を参照することができるようになります。
また、リテラルとして埋め込むため、プログラマのミスで不変値を書き換えてしまうといった単純なミスも防ぐことができます。
このようにimmutableを用いることで、多くのケースでコントラクトの動作コストを削減することが可能になると思われます。