はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、EVMが動的なサイズの返却データを扱うための新しいフレームワークを提案しているEIP211についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
この記事ではオペコードがいくつか出てくるので、各オペコードについて気になった方は以下の記事を参考にしてください。
概要
EVMでより柔軟にデータを扱うためのメカニズムについて提案しています。
現状、Ethereumのスマートコントラクト間で情報をやり取りする時には、データの長さに限りがあるため大量のデータをやり取りすることが難しいという問題があります。
この新しい提案により、スマートコントラクトは任意の長さのデータを返すことが可能になり、それを呼び出し元のコントラクトが必要に応じて部分的にまたは全体をメモリにコピーすることができます。
これは、ウェブサイトでのフォーム送信後にサーバーからのレスポンスデータが一時的にブラウザに保存されるのと似た仕組みです。
次に他のコントラクトを呼び出すとき、前回のデータは新しいデータで上書きされます。
これにより、データの取り扱いが柔軟になるだけでなく、ガスの消費も効率的になり、Ethereumネットワークの全体的なパフォーマンス向上に寄与する可能性があります。
また、100%後方互換性があるため、既存のスマートコントラクトに影響を与えることなく導入することができます。
動機
スマートコントラクトが事前に予想できない長さのデータを扱う必要がある場面では、複数の呼び出しを利用するなどの対策が考えられますが、これにはコストがかかります。
特に、あらゆる種類のコントラクトへのデータの転送を行う generic forwarding contract のような場合、呼び出されるコントラクトの詳細が不明であるため、返却されるデータのサイズを事前に知ることができません。
これは、コントラクトが他のコントラクトを呼び出した後に期待される出力の量を把握できないことを意味します。
この問題に対処する1つの方法は、コンパイラが返却データのサイズが不明な場合にゼロ長の領域を予約し、その後RETURNDATACOPY
とRETURNDATASIZE
のオペコードを使用してデータを取得することです。
これにより、関数やコントラクトは必要なデータを適切に取得し、処理することが可能になります。
さらに、この提案はEIP140(状態の意図的なrevert
時にデータを返すことを提案するEIP)の有用性を高めるものです。
失敗時に返却されるデータは、通常の返却データよりも大きくなる可能性があるため、CALL
オペコードが失敗を示した後でもデータを取得することができます。
これは、スマートコントラクトがエラー情報をより柔軟に扱えるようにすることで、開発者がより詳細なデバッグやエラー処理を行えるようになることを意味します。
EIP140については以下の記事を参考にしてください。
generic forwarding contract
generic forwarding contractは、Ethereumのスマートコントラクトの一種で、受け取ったコールデータ(関数呼び出しやその他の入力データ)をそのまま別のコントラクトに転送する機能を持ちます。
この種のコントラクトは「generic」と呼ばれる理由は、転送先のコントラクトや転送されるデータの内容に依存しない汎用的な動作をするからです。
つまり、特定の関数やプロトコルに限定されず、任意のデータやコールを別のアドレスへ転送できる柔軟性を持っています。
generic forwarding contractの主な用途としては以下が挙げられます。
-
メタトランザクション
- ユーザーが直接ガスを支払う代わりに、別のアカウントがトランザクションのガス費用を負担することを可能にします。
- これは、新規ユーザーがイーサリアムのガス費用を支払うことなくスマートコントラクトとやりとりできるようにするために使用されることがあります。
-
アップグレーダブルなコントラクト
- あるコントラクトのロジックを別の新しいコントラクトに転送することで、スマートコントラクトのコードをアップグレードすることが可能になります。
- これにより、スマートコントラクトの不変性の問題をある程度緩和できます。
-
権限委譲
- generic forwarding contractを介して特定の関数を実行することにより、特定の操作の実行権限を他のアカウントに委譲することができます。
generic forwarding contractの動作は基本的に次のようになります。
- コントラクトは外部からのコール(トランザクション)を受け取ります。
- コントラクトは受け取ったコールデータを分析せずに、あらかじめ設定された別のコントラクトアドレスへそのデータを転送します。
- 転送先のコントラクトは受け取ったコールデータに基づいて処理を実行し、結果をgeneric forwarding contractに返します。
- generic forwarding contractは、その結果を最初のコールを行った外部アカウントに返します。
このようなコントラクトは、Ethereumブロックチェーン上での柔軟なデータとコントラクトの相互作用を可能にし、さまざまな応用シナリオで有用です。
ゼロ長
「ゼロ長」の領域とは、そのサイズが0
である、つまり内容が空のデータ領域を指します。
関数やスマートコントラクトが呼び出された時に、返却されるデータのサイズが予め分からない場合に備えて、メモリ内に空のデータ領域を確保することなどに使用されます。
このような予約は、スマートコントラクトが他のコントラクトを呼び出し、その結果を受け取る時に特に重要です。
スマートコントラクトが実行されると、EVM(Ethereum Virtual Machine)は返却データを一時的に保持するための「返却データバッファ」を用意します。
このバッファの初期状態は「ゼロ長」、つまり空です。
呼び出し元のコントラクトが呼び出し先から返却データを受け取ると、そのデータはこの予約されたバッファに格納され、バッファのサイズは受け取ったデータのサイズに応じて変化します。
「ゼロ長の領域を予約する」というプロセスは、コンパイラやEVMが効率的にメモリを管理し、動的なデータの取り扱いを可能にするための1つの方法です。
これにより、スマートコントラクトは実行時にデータのサイズに関わらず柔軟に処理を行うことができます。
また、予約されたバッファが「ゼロ長」であることにより、不必要なメモリ消費を避けることができ、ガスのコストを抑えることが可能になります。
仕様
このテキストは、EthereumブロックチェーンのByzantiumフォーク(BYZANTIUM_FORK_BLKNUM
)以降に導入されたEVM(Ethereum Virtual Machine)の機能拡張について説明しています。
具体的には、2つの新しいオペコード(RETURNDATASIZE
とRETURNDATACOPY
)の追加と、コールフレームを生成する既存のオペコード(CALL
、CREATE
、DELEGATECALL
など)の修正について述べています。
コールフレーム
コールフレームは、プログラムやスクリプトが関数を呼び出す時に使用されるデータ構造です。
特に、Ethereum Virtual Machine (EVM) などのスタックベースの実行環境では、各関数呼び出しに対してコールフレーム(または実行フレーム)が生成されます。
このフレームは、関数の実行に必要な情報を保持し、関数の実行中に発生する様々な操作の文脈を提供します。
コールフレームには通常、以下のような情報が含まれます。
-
関数の引数
- 呼び出された関数に渡される引数の値。
-
ローカル変数
- 関数内で定義された変数の値。
-
リターンアドレス
- 関数の実行が完了した後に制御を戻すべきプログラムの位置。
-
前のコールフレームへのリンク
- スタックベースの実行環境では、各コールフレームは前のフレームへの参照(またはリンク)を保持することがあり、これにより実行スタックが形成されます。
EVMにおいては、コールフレームはスマートコントラクトが他のコントラクトを呼び出す(例:CALL
、DELEGATECALL
オペコードの使用)時にも使用されます。
このとき、新しいコールフレームが生成され、呼び出されたコントラクトの実行文脈が作られます。
このフレーム内では、呼び出されたコントラクトのコードが実行され、必要なデータ(例:返却データバッファ)が保持されます。
コールフレームの概念は、関数の呼び出しとリターンのメカニズムを管理し、プログラムの実行フローを構造化するために不可欠です。
EVMのようなスマートコントラクトの実行環境では、コールフレームを通じて、スマートコントラクト間の複雑な相互作用や状態の変更が効率的に行われます。
新しい内部バッファ、通称「返却データバッファ」は、EVMコールフレームごとに新しく空で作成されます。
コールライクオペコードが実行されると、このバッファはクリアされ(サイズが0
に設定され)、コールの実行後には、そのコールによる完全な返却データ(またはEIP140に記載されているような失敗データ)がこのバッファに格納され、サイズがそれに応じて変更されます。
CREATE
とCREATE2
のオペコードは例外で、成功した場合は空のバッファを、失敗した場合は失敗データを返します。
また、コールフレーム間で返却データバッファを共有することによる最適化が可能です。
これは、一度に非空の状態にあるバッファは最大でも1つだけであるためです。
Ethereum Virtual Machine (EVM) において、関数やスマートコントラクトが他のコントラクトを呼び出す時に使用される「コールフレーム」という実行環境の概念があります。
各コールフレームは、その関数やコントラクトの実行に関連するデータや状態を保持します。
この中には、「返却データバッファ」と呼ばれる特別なメモリ領域も含まれており、コントラクトが他のコントラクトからデータを受け取る際に使用されます。
「コールフレーム間で返却データバッファを共有する」というのは、複数の関数呼び出し(コールフレーム)が連続して行われる場合に、それぞれの呼び出しで新しい返却データバッファを作成するのではなく、同一のバッファを使い回すことを意味します。
これにより、メモリ使用量を節約し、システムの効率を向上させることができます。
この最適化が可能な理由は、「一度に非空の状態にあるバッファは最大でも1つだけである」ためです。
つまり、あるコールフレームが別のコントラクトを呼び出し、その結果として返却データを受け取ると、そのデータは返却データバッファに格納されます。
しかし、このコールフレームがさらに別のコントラクトを呼び出すと、新しいコールフレームが作成され、返却データバッファはクリアされて新しいデータを受け取る準備が整います。
このプロセスが連続している間は、常に1つのバッファだけがデータを保持している状態にあり、他のバッファは空(非空ではない)状態になります。
したがって、複数のコールフレームが同時にアクティブであっても、返却データを保持しているのは1つのバッファのみであり、このバッファをコールフレーム間で共有することで、メモリリソースの効率的な利用が可能になるのです。
RETURNDATASIZE
(0x3d
)オペコードは、返却データバッファのサイズをスタックにプッシュします。
これにかかるガスコストは2
で、CALLDATASIZE
オペコードと同じです。
RETURNDATACOPY
(0x3e
)オペコードは、CALLDATACOPY
と似たいて、コールデータからではなく返却データバッファからデータをコピーします。
バッファのサイズを超えてアクセスすると失敗になります。
具体的には、開始位置と長さの合計がRETURNDATASIZE
を超えるか、それによってオーバーフローする場合、現在のコールはガス不足の状態で停止します。
特に、バッファの末尾から0
バイトを読むと0
バイトが読まれますが、バッファを1バイト超えて0バイトを読もうとすると例外が発生します。
Ethereum Virtual Machine (EVM) におけるデータバッファの読み取り操作に関する特定の振る舞いについて説明しています。
ここで言う「バッファ」とは、スマートコントラクトが他のコントラクトから返されたデータを一時的に保持するメモリ領域を指します。
-
バッファの末尾から
0
バイトを読む- これは、バッファの最後の位置から何もデータを読み取らないことを意味します。
- つまり、要求された読み取りデータ量が
0
バイトであるため、実際にはバッファからデータを取得しません。 - この操作は正常に完了し、エラーは発生しません。
-
バッファを1バイト超えて
0
バイトを読もうとする- これは、バッファの存在しない部分、つまりバッファのサイズを超えた位置からデータを読み取ろうとすることを意味します。
- たとえ読み取りたいデータ量が
0
バイトであっても、アクセスしようとした位置がバッファの範囲外であるため、この操作は例外を引き起こします。
この振る舞いは、バッファの範囲内での操作は許可されるが、バッファの範囲を超えたアクセスはエラーとして扱われるという原則に基づいています。
たとえ実際にデータを読み取らない(0
バイト読み取り)場合でも、バッファの範囲外を指定するとエラーが発生します。
これにより、プログラムの安全性が保たれ、不正なメモリアクセスを防ぐことができます。
RETURNDATACOPY
のガスコストは、3
+ コピーする量を32
で割った値の上限に3
をかけたもので、これもCALLDATACOPY
と同じです。
3 + 3 * ceil(amount / 32)
これらの変更により、EVMは関数呼び出しの結果として得られるデータの長さが事前に不明であっても、そのデータを効率的に扱えるようになり、Ethereumスマートコントラクトの柔軟性と機能が大幅に向上しました。
補足
動的データを返すための他の解決策も検討されましたが、それらはすべてコールオペコードからガスを差し引く必要があり、実装と仕様の両方が複雑になります。
しかし、この提案はcalldata
の取り扱い方と非常に似ているため、概念にうまく適合します。
さらに、eWASMアーキテクチャは既に同じ方法で返却データを扱っています。
以前検討された他の解決策は、コールオペコードからガスを消費する必要があり、そのために実装と定義が複雑になるという問題がありました。
しかし、この新提案は、既存のcalldata
の扱いと類似しているため、現在のEVMの概念に容易に統合できます。
また、このアプローチはeWASMアーキテクチャでもすでに採用されており、返却データを同様の方法で処理しています。
EVMでは、現在のコールからのリターンまたは次のコールが行われるまで返却データを保持する必要がありますが、これは呼び出されたコントラクトのメモリの一部として既に支払われているコストを含んでいるため、追加の問題にはなりません。
実装によっては、返却データを特別なメモリ領域にコピーするか、次のコールまで呼び出されたコントラクトのメモリ全体を保持することを選択できます。
呼び出されたコントラクトのメモリを次のコールライクオペコードが実行されるまで保持することによるメモリ使用量の増加は、呼び出し元のフレーム内でのメモリ割り当てをコールの前に移動することで、ガスコストに変更がない限り、ピーク使用量に影響を与えません。
また、RETURNDATASIZE
とRETURNDATACOPY
のオペコードは、CALLDATASIZE
やCALLDATACOPY
と同じニブルブロックに割り当てられており、これによりEVM内での整合性が保たれています。
これらの変更により、EVMは動的なサイズのデータをより効率的に、そして複雑さを増すことなく扱えるようになります。
ニブルブロック
ニブルブロックとは、コンピュータ科学において4
ビットの単位を指します。
1
ニブルは半バイト(half-byte)とも呼ばれ、8
ビットの1
バイトを2等分したものです。
ニブルは2進数で4桁の数字を表すことができ、これは16進数で1桁(0からFまで)に相当します。
つまり、ニブルは0から
15`までの数値を保持することができます。
プログラミングやコンピュータアーキテクチャでは、ニブルはデータの表現や操作、特に低レベルのコンピューティングやメモリアドレスの指定に使用されます。
例えば、CPUの命令セットやマシンコードでは、特定のオペレーションやレジスタを指定するためにニブルが利用されることがあります。
オペコード(オペレーションコードの略)の文脈では、ニブルブロックはオペコードを分類するための方法として使用されることがあります。
オペコードは、CPUや仮想マシン(例えばEVM)が理解し実行できる機械語命令です。
オペコードをニブルブロックに分けることで、命令セットを整理し、関連するオペレーションをグループ化することが可能になります。
これにより、命令セットの設計やプログラムの解析が容易になります。
例えば、EVMの命令セットでは、CALLDATASIZE
やCALLDATACOPY
のようなオペコードが同じニブルブロック内に割り当てられている場合があります。
これは、これらのオペコードが類似した機能(この場合はコールデータの取り扱い)を持つため、論理的にグループ化されていることを示しています。
ニブルブロックの使用は、オペコードの識別と分類を効率的に行う手段として役立ちます。
互換性
この提案は2つの新しいオペコードを導入し、それ以外は完全な後方互換性を持ちます。
引用
Christian Reitwiessner chris@ethereum.org, "EIP-211: New opcodes: RETURNDATASIZE and RETURNDATACOPY," Ethereum Improvement Proposals, no. 211, February 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-211.
最後に
今回は「EVMが動的なサイズの返却データを扱うための新しいフレームワークを提案しているEIP211」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!