はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、Ethereumのストレージ書き込みにかかるガス代を、実際の動作に即した柔軟な仕組みに見直して無駄なガス消費を減らす仕組みを提案しているEIP2200についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
EIP2200は、Ethereumにおけるスマートコントラクトのストレージ操作(SSTORE オペコード)に対するガスコストの計算方法を見直す提案です。
具体的には、EIP1283とEIP1706を統合し、それらをより構造化された形で再定義することで、他のガス関連の改定(たとえばEIP1884)と互換性を持たせやすくすることを目的としています。
EIP1706については以下の記事を参考にしてください。
EIP1884については以下の記事を参考にしてください。
EIP1283では「ネットガスメータリング」と呼ばれる手法を導入しており、これはストレージへの変更が実質的に状態を変えない場合には、余分なガスを取らないようにする工夫です。
EIP1283については以下の記事を参考にしてください。
EIP2200では、この考え方を踏襲しつつ、以下の3つの情報に基づいてSSTOREのガスコストをより適切に測定します。
- ストレージスロットの元の値(トランザクション開始時の値)
- 現在の値(実行中に変更されたかもしれない値)
- 返金カウンター(書き換えによって将来ガスが返ってくる可能性)
これらの情報に基づいた新たなガスメータリング方式を採用することで、従来よりも柔軟かつ効率的なストレージ操作が可能になります。
動機
EIP2200の主な動機は、SSTORE に関するガスコストの測定方式を、より現実の実装や利用方法に合ったものへと改善することにあります。
従来の仕様では、実際には意味のないストレージ変更に対しても過剰なガスが課されており、これが特定のユースケースにおいて非効率さを生んでいました。
例えば、同じ関数呼び出しの中で同じストレージスロットに複数回書き込むケースや、エラーメッセージや一時的なステートのやり取りにストレージを使うような設計では、トランザクションが終了した時点でそのデータが消えるにも関わらず、従来は高いガスコストが発生していました。 EIP2200ではそうした一時的なストレージ操作に対して、より軽いガスコストを設定できるようにします。
また、EIP1283に起因する新たなリエントランシー攻撃の可能性も指摘されており、Solidityがデフォルトで2300ガスを与える送金処理との組み合わせで、攻撃を受けやすくなる可能性がありました。
EIP2200ではこのリスクにも配慮し、ガス残量が少ない状態ではSSTOREを実行できないようにするなどの対策を提案しています。
さらに、従来は定数で定められていたガス値を、より柔軟に変更可能なパラメータ形式に置き換えることで、将来的な仕様変更や最適化も容易になります。
このように、EIP2200は既存の仕様の問題点を解消し、Ethereum上でのスマートコントラクト開発をより安全かつ効率的にするための提案となっています。
仕様
ガス定数の定義と変更
EIP2200では、SSTORE(ストレージへの書き込み)にかかるガス計算を明確に定義するために、いくつかのガス関連の変数を定義しています。
SLOAD_GAS- ストレージ読み取り時のガス。
200→800に引き上げられました。
SSTORE_SET_GAS- 新たに値をセットする操作のガス。
20,000(変更なし)。
SSTORE_RESET_GAS- 0以外の値を別の非ゼロに変更する操作のガス。
5,000(変更なし)。
SSTORE_CLEARS_SCHEDULE- 0以外の値をゼロに変更することで、返金スケジュールに加算されるガス。
15,000(変更なし)。
これらの変数を用いて、EIP1283とEIP1706の統一仕様が記述されます。
SSTORE の新しいガス計算ロジック
EIP2200では、SSTORE によるストレージ書き込み時のガス消費とリファンド(ガスの返金)について、より正確かつ柔軟なロジックを定義しています。
ここでは、ストレージスロットに関する3つの値が重要です。
original value- トランザクション開始前の元の値。
current value- 現在の実行フレームで保持している最新の値。
new value- 今回の
SSTOREで書き込もうとしている値。
- 今回の
このロジックは、以下のように分岐して適用されます。
まず、gasleft(残ガス)がガススティペンド(stipend)以下の場合、つまり極端にガスが少ない状態では処理を中断し「ガス不足(out of gas)」例外を発生させます。
次に、current valueとnew valueが同じ場合、すなわち何も変更しない「no-op」の操作では、SLOAD_GAS だけが消費されます。
一方で、current valueとnew valueが異なる場合は、ストレージのステートに応じた複雑なガス処理が行われます。
ケース①:ストレージがまだ変更されていない場合(original = current)
このケースでは、初めてそのスロットに対して変更を加える場合です。
original valueが0のときは、値をセットする扱いとなり、SSTORE_SET_GAS(20,000)が課されます。- 0以外の場合は、
SSTORE_RESET_GAS(5,000)が課されます。 - また、
new valueが0ならば、SSTORE_CLEARS_SCHEDULE(15,000)が返金カウンターに加算されます。
ケース②:ストレージがすでに変更されている場合(original ≠ current)
この場合、すでに変更されたステートで再度SSTOREが呼ばれることになります。
- このときは、
SLOAD_GAS(800)のみが消費され、以下の2つの処理が追加で行われます。
-
original valueが0以外の場合current valueが0かつnew valueが0以外であれば、返金カウンターからSSTORE_CLEARS_SCHEDULEを差し引きます。new valueが0かつcurrent valueが0以外であれば、返金カウンターにSSTORE_CLEARS_SCHEDULEを加算します。
-
original valueとnew valueが同じ(リセットされる)場合original valueが0であれば、SSTORE_SET_GAS - SLOAD_GAS(19,200)を返金カウンターに加算します。- そうでなければ、
SSTORE_RESET_GAS - SLOAD_GAS(4,200)を加算します。
このように、ストレージの変更パターンに応じてガスコストと返金の挙動が緻密に設計されています。
実装上の注意点
EIP2200では、「返金カウンター」がコールフレーム単位で設けられる場合、一時的にマイナスの値になる可能性があります。 一方で、トランザクション単位で一括して扱う実装であれば、返金カウンターは常に非負になります。 よって、EVMの実装者は、このガスリファンドの集計単位に注意を払う必要があります。
補足
一時的ストレージの代替としての設計
EIP2200は、EIP1087やEIP1153で提案された「一時的ストレージ(Transient Storage)」の多くの用途を、より単純な仕組みで実現することを目的としています。 具体的には、「ダーティマップ(dirty map)」や追加のストレージ構造を新たに導入することなく、既存のストレージの枠組み内で効率的な書き込みを可能にします。
EIP1153については以下の記事を参考にしてください。
EIP1087では、ストレージの変更を追跡するためにダーティマップが必要でしたが、これには前提条件があります。 それは、トランザクション終了時にすべてのストレージ変更がステートトライ(Merkle Patricia Trie)へコミットされるというモデルです。 しかし、すべてのEthereumクライアントがこのように動作するわけではありません。
EIP1087については以下の記事を参考にしてください。
実装効率と柔軟性の両立
例えば、EIP658以降の効率的なクライアントでは、実行中のストレージ変更をメモリ上で一時的に保持するために、RLP(Recursive Length Prefix)エンコードを用いない軽量なトライ構造や不変データ構造(Immutable Data Structures)を使っています。 こうしたクライアントでは「変更されたストレージの一覧を逐一走査する」処理は、計算負荷やメモリ負荷が増大するため非効率です。
EIP658については以下の記事を参考にしてください。
EIP2200では、ストレージの元の値と現在の値さえ分かっていれば動作する仕組みとなっており、不要な処理の追加を避けることができます。 また、ランタイム時に余計な構造体やマップを保持する必要がないため、実行効率を損ないません。
ガスコストの安全性と整合性
EIP2200の大きな利点のひとつは、既存の SSTORE のガス計算よりもガスが多く消費されることはない点です。
そのため、どのようなケースにおいても、現在の実装よりも高コストになることはなく、互換性が保たれます。
さらに、EIP1087で挙げられた使用例に対しても、同等のガス節約効果を提供できます。
例えば、以下のような例が挙げられています(※SLOAD_GAS が200の場合)
* スロット0に1を代入し、再び0に戻す操作は、合計400ガス(20000 + 200 - 19800)。
* 空のストレージからスロット0を5回インクリメントすると、合計21000ガス(20000 + 5×200)。
* 残高を持つA→B→Cの転送は、合計10400ガス(5000×3 + 200 - 4800)。
セキュリティ対策と互換性の配慮
EIP1283には、Solidityのtransferやsendでデフォルト提供される2300ガスのスティペンドの範囲内でストレージを書き換えられてしまうというリエントランシー脆弱性の懸念がありました。
これに対しEIP2200では、残ガスがスティペンド以下の場合にSSTOREを許可しないルールを追加することで、従来のコントラクトが持っていた再入保護の性質を維持します。
他の代替案とその課題
この問題への対策としては他にも複数提案されましたが、いずれも課題があります。
- EIP1283の撤回
- 重要な機能を放棄することになり、テストネットやクライアントでの既存実装と整合性を欠く。
- ステート変更を禁止する新たな
callタイプの導入- 仕様が複雑化。
SSTOREのガスを2300以上に引き上げる- ネットガスメータリングの利点が失われる。
- スティペンド自体を減らす
transferの利便性が損なわれる。
- ガスリファンドを増やす案
- ユーザーに不要なガス提供と払い戻しが必要になる。
- コントラクトごとのEVMバージョン設定
- 仕様が大幅に複雑化する。
それに比べて、EIP2200は既存の設計との整合性を保ちながら、最小限の実装変更で柔軟性と安全性を両立する現実的な解決策となっています。
互換性
EIP2200の実装にはハードフォークが必要です。
ただし、導入によって既存のコントラクトが壊れることはありません。
理由は、SSTOREの実行に必要なガス量は元々最低でも5000ガス以上であり、EIP2200の新しいガスルールでも同じ条件が維持されるためです。
むしろ、多くのコントラクトではガスコストの削減が見込まれます。 特に、一時的にストレージを変更するようなユースケースでは、従来よりも少ないガスで操作が可能となり、効率の良い処理が実現できます。
ただし、ガス見積もり(Gas Estimation)を行う際には、この新しいガス要件を考慮する必要があります。 これはガスの返金や計算式の挙動が変更されるためです。
テストケース
以下に示すのは、実際のバイトコードとそれに対するガス消費・返金・ストレージスロットの値の変化を示した表です。
| バイトコード | 使用ガス | 返金ガス | 元の値 | 1回目 | 2回目 | 3回目 |
|---|---|---|---|---|---|---|
| 0x60006000556000600055 | 1612 | 0 | 0 | 0 | 0 | |
| 0x60006000556001600055 | 20812 | 0 | 0 | 0 | 1 | |
| 0x60016000556000600055 | 20812 | 19200 | 0 | 1 | 0 | |
| 0x60016000556002600055 | 20812 | 0 | 0 | 1 | 2 | |
| 0x60016000556001600055 | 20812 | 0 | 0 | 1 | 1 | |
| 0x60006000556000600055 | 5812 | 15000 | 1 | 0 | 0 | |
| 0x60006000556001600055 | 5812 | 4200 | 1 | 0 | 1 | |
| 0x60006000556002600055 | 5812 | 0 | 1 | 0 | 2 | |
| 0x60026000556000600055 | 5812 | 15000 | 1 | 2 | 0 | |
| 0x60026000556003600055 | 5812 | 0 | 1 | 2 | 3 | |
| 0x60026000556001600055 | 5812 | 4200 | 1 | 2 | 1 | |
| 0x60026000556002600055 | 5812 | 0 | 1 | 2 | 2 | |
| 0x60016000556000600055 | 5812 | 15000 | 1 | 1 | 0 | |
| 0x60016000556002600055 | 5812 | 0 | 1 | 1 | 2 | |
| 0x60016000556001600055 | 1612 | 0 | 1 | 1 | 1 | |
| 0x600160005560006000556001600055 | 40818 | 19200 | 0 | 1 | 0 | 1 |
| 0x600060005560016000556000600055 | 10818 | 19200 | 1 | 0 | 1 | 0 |
セキュリティに関する設計と証明
EIP2200では、SSTOREの操作に関する正確なガス計算とリファンドの理論的整合性が示されています。
特に、「ストレージスロットの元の値」がトランザクション開始時点で定義されているため、呼び出し元(call frame)によるガス計算への影響がないことが保証されています。
また、この設計では以下のようなケースごとにガスコストが理論的に証明されています。
元の値がゼロの場合の検証
- 最終的な値がゼロのまま → 書き込み不要 →
200 * Nガスのみ - 最終的に非ゼロになる → 初回に20000ガス、それ以外は200ガスずつ加算
元の値が0以外の場合の検証
- 値が変更されない →
200 * Nガスのみ - ゼロに変更 →
5000 - 15000 + 200 * (N - 1)(返金を考慮) - 別の非ゼロ値に変更 →
5000 + 200 * (N - 1)
これらのパターンについては帰納法によって正しさが検証されており、ステート遷移ごとの計算結果が常に期待通りのガス量となることが数式的に示されています。
引用
Wei Tang (@sorpaas), "EIP-2200: Structured Definitions for Net Gas Metering," Ethereum Improvement Proposals, no. 2200, July 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2200.
最後に
今回は「Ethereumのストレージ書き込みにかかるガス代を、実際の動作に即した柔軟な仕組みに見直して無駄なガス消費を減らす仕組みを提案しているEIP2200」についてまとめてきました! いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!
