はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、Ethereumのトランザクションを拡張し、新しいタイプのトランザクションを柔軟に導入できる仕組みを提案しているEIP2718についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

EIP2718は、イーサリアムのトランザクションとレシートのフォーマットを拡張するための提案です。 この提案の目的は、将来の機能拡張に備えて、トランザクションとレシートのフォーマットを柔軟にすることです。

レシートとはトランザクションの結果です。

主なポイントは以下の通りです。

  • トランザクションは TransactionType || TransactionPayload という形式になります。

    • TransactionType はトランザクションのフォーマットを識別するための番号です。
    • TransactionPayload は実際のトランザクションの内容で、将来のEIPで定義されます。
  • レシートは TransactionType || ReceiptPayload という形式になります。

    • TransactionType はレシートのフォーマットを識別するための番号です。
    • ReceiptPayload は実際のレシートの内容で、これも将来のEIPで定義されます。

この拡張により、イーサリアムのプロトコルに新しい機能を追加する時に、既存のトランザクションやレシートのフォーマットを変更することなく、新しいフォーマットを導入できるようになります。 これにより、互換性を維持しつつ、イーサリアムのプロトコルを柔軟に拡張していくことが可能になります。

動機

EIP2711は、イーサリアムのトランザクションタイプを拡張するための提案です。 これまでのイーサリアムでは、新しいトランザクションタイプを導入する時に、既存のトランザクションとの後方互換性を維持するために、トランザクションのエンコードされたデータ(ペイロード)のみを使って新旧のトランザクションを区別する必要がありました。 この制約により、複数のトランザクションタイプに一致するようなトランザクションを作ることはできませんでした。 もし、あるトランザクションが複数のタイプに一致してしまうと、そのトランザクションがどのタイプに属するのか判断できなくなってしまうためです。 EIP155は、この制約の具体的な例です。 EIP155では、リプレイアタック保護のために、トランザクションに新しい値(チェーンID)を導入しました。 しかし、後方互換性を維持するために、この新しい値を既存のトランザクションのエンコードされたフィールドの1つ(v)にビットパックする必要がありました。

EIP155についてわかりやすく説明してください。

つまり、新しい値を追加するために、既存のフィールドの一部を再利用したのです。 このアプローチは機能しますが、新しいトランザクションタイプを追加するたびに、このようなビットパックを行う必要があり、設計が複雑になります。 また、将来的に追加できるトランザクションタイプの数にも制限ができてしまいます。

ビットパックとは、複数のデータ項目を1つのデータ構造に詰め込む技術です。 通常、それぞれのデータ項目は、使用可能なビット数よりも少ないビット数で表現されます。 これにより、データ構造のサイズを小さくすることができます。

例えば、もし1バイト(8ビット)のデータ構造があり、2つの値を格納する必要があるとします。 一方の値は2ビットで表現でき(0から3の範囲)、もう一方の値は3ビットで表現できる(0から7の範囲)とします。 この場合、2ビットと3ビットの値を1バイトにビットパックすることができます。 残りの3ビットは、将来の拡張用に予約しておくことができます。

イーサリアムのEIP155の場合、トランザクションのv値は、もともとはトランザクションの署名を復元するために使用されていました。 EIP155では、v値の一部のビットを再利用して、チェーンIDを格納しました。 これがビットパックの一例です。

ビットパックは、データを効率的に格納するための一般的な技術ですが、データ構造が複雑になり、可読性が下がるという欠点があります。 また、将来のデータ構造の拡張が難しくなる場合もあります。

EIP2711のエンベロープトランザクションタイプを使えば、この問題を解決できます。 新しいトランザクションタイプを追加する時、TransactionTypeフィールドを使ってトランザクションのタイプを明示的に指定できるため、ペイロードのエンコーディングに依存せずにトランザクションを区別できるようになります。 これにより、新しいトランザクションタイプの設計がシンプルになり、将来の拡張性も向上するのです。

現在議論されている複数の提案では、EOAアカウントがそのコンテキスト内で直接コードを実行できるようにするもの、msg.sender以外のアカウントがガス代を支払えるようにするもの、レイヤー1のマルチシグトランザクションに関連するものなど、新しいトランザクションタイプが定義されています。 これらはすべて、相互に互換性のある方法で定義する必要があり、EIP作成者やクライアントにとって複雑なルールに従わなければならないため負担になります。

EIP2711で提案されているエンベロープトランザクションタイプを導入することで、既存のトランザクションとの後方互換性を確保するだけでよくなります。 そして、TransactionType間で番号の競合がないことを確認するだけでよくなります。

具体的には以下のような利点があります。

  • 新しいトランザクションタイプを簡単に追加できる
  • 既存のトランザクションとの後方互換性を維持できる
  • トランザクションタイプ識別のための複雑なルールが不要になる
  • クライアントの実装が簡単になる。
  • 将来の拡張性が向上する。

仕様

EIP2711では、トランザクションとレシートのフォーマットに変更が加えられています。 これにより、新しいタイプのトランザクションを導入しやすくなります。

トランザクション

FORK_BLOCK_NUMBERから、ブロックヘッダのトランザクションルートは、patriciaTrie(rlp(Index) => Transaction)のルートハッシュでなければなりません。

パトリシアツリー(Patricia Trie)は、イーサリアムで使用されているデータ構造で、キーと値のペアを効率的に格納できます。 キーはRLP(Recursive Length Prefix)でエンコードされ、値はRLPでエンコードされたデータのハッシュ値です。

次に、EIP2711では、トランザクションのインデックス(ブロック内の位置)をキーとし、トランザクションデータ自体を値とするパトリシアツリーを構築します。

あるブロックに以下の3つのトランザクションがあるとします。

  1. LegacyTransaction: rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
  2. TransactionType: 0x01, TransactionPayload: 0xabcd...
  3. TransactionType: 0x02, TransactionPayload: 0xdef0...

これらのトランザクションは、以下のようにパトリシアツリーに格納されます。

  • キー: rlp(0), 値: LegacyTransaction のハッシュ
  • キー: rlp(1), 値: 0x01 || 0xabcd... のハッシュ
  • キー: rlp(2), 値: 0x02 || 0xdef0... のハッシュ

ここで、||は連結を表します。

このパトリシアツリーのルートハッシュは、ブロックヘッダーのトランザクションルートフィールドに格納されます。 これにより、ブロック内のすべてのトランザクションが一意に識別され、改ざんを防ぐことができます。

もし新しいトランザクションタイプが追加された場合、そのトランザクションは新しいキーと値のペアとしてパトリシアツリーに追加されます。 例えば、TransactionType 0x03のトランザクションが追加された場合、以下のペアが追加されます。

  • キー: rlp(3), 値: 0x03 || TransactionPayload のハッシュ

このように、EIP2711ではパトリシアツリーを使用して、異なるタイプのトランザクションを効率的かつ安全に格納しています。 ツリーのルートハッシュをブロックヘッダーに格納することで、トランザクションデータの整合性を保証しているのです。

Indexはブロック内のトランザクションのインデックスです。 Transactionは、TransactionType || TransactionPayloadまたはLegacyTransactionのどちらかです。 TransactionTypeは、0から0x7fまでの正の8ビット符号なし整数で、トランザクションのタイプを表します。 TransactionPayloadは、TransactionTypeに依存するOpaqueバイト配列で、その解釈は将来のEIPで定義されます。

Opaqueバイト配列(Opaque Byte Array)とは、その内部構造や意味が外部から直接的には見れないバイト列のことを指します。

EIP2711の文脈では、TransactionPayloadはそれ自体では特定の構造を持たない単なるバイト列として扱われます。 その解釈は、対応するTransactionTypeに依存します。 つまり、TransactionPayloadのフォーマットや内容は、TransactionTypeごとに異なる可能性があります。

例えば、TransactionTypeが0x01の場合、TransactionPayloadは特定のフォーマットを持つバイト列かもしれません。 一方、TransactionTypeが0x02の場合、TransactionPayloadは全く異なるフォーマットを持つかもしれません。 しかし、EIP2711自体はこれらのフォーマットを規定していません。

この設計の利点は、将来新しいトランザクションタイプを導入する時に、TransactionPayloadのフォーマットを自由に定義できることです。 新しいEIPで新しいTransactionTypeと、それに対応するTransactionPayloadのフォーマットを定義すれば、既存のシステムを大きく変更することなく、新機能を追加できます。

一方で、TransactionPayloadの解釈は、それを処理するアプリケーション(通常はイーサリアムクライアント)に委ねられます。 アプリケーションは、TransactionTypeに基づいてTransactionPayloadを適切に解釈・処理する必要があります。

このように、不透明なバイト配列は、システムの拡張性と柔軟性を高めるための重要な設計概念なのです。

LegacyTransactionは、rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])です。 将来のすべてのトランザクションタイプの署名は、署名されたデータの最初のバイトとしてTransactionTypeを含むことが推奨されます。 これにより、あるトランザクションタイプの署名が別のトランザクションタイプの署名として使用されることを心配する必要がなくなります。

レシート

FORK_BLOCK_NUMBERから、ブロックヘッダのレシートルートは、patriciaTrie(rlp(Index) => Receipt)のルートハッシュでなければなりません。 Indexは、このレシートが対応するトランザクションのブロック内のインデックスです。 Receiptは、TransactionType || ReceiptPayloadまたはLegacyReceiptのいずれかです。 TransactionTypeは、0から0x7fまでの正の8ビット符号なし整数で、トランザクションのタイプを表します。 ReceiptPayloadは、TransactionTypeに依存するOpaqueバイト配列で、その解釈は将来のEIPで定義されます。 LegacyReceiptは、rlp([status, cumulativeGasUsed, logsBloom, logs])です。 レシートのTransactionTypeは、Indexが一致するトランザクションのTransactionTypeと一致しなければなりません。

このように、EIP2711ではトランザクションとレシートの構造を拡張し、TransactionTypeフィールドを導入することで、新しいタイプのトランザクションを柔軟に追加できるようにしています。

補足

TransactionTypeの範囲が0x7fまでに制限されている理由

EIP2711では、TransactionTypeの範囲が0x7fまでに制限されています。 この理由は、当面は0x7fで十分であり、将来の拡張の選択肢を残していることが挙げられます。 また、この範囲はレガシートランザクションタイプ(0xc0以上で始まる)との衝突を防ぐことができます。

署名データの最初のバイトにTransactionTypeを含めることが"SHOULD"である理由

署名データの最初のバイトにTransactionTypeを含めることは必須にされていません。 ただ、署名の再利用問題を防ぐために、すべての新しいトランザクションはこの方式に従うことを強く推奨しています。 ただ、レガシー署名スキームと互換性のあるラップされたレガシートランザクションの場合、内部のレガシートランザクションの署名スキームを変更することができないため、最初のバイトにTransactionTypeを含めることが不可能です。 また、将来的には、従来の意味での署名(例:ECDSA署名)を持たないトランザクションが導入される可能性があります。 このトランザクションでは、別の有効性検証メカニズム(例:ZKプルーフ)を使用するかもしれないため、最初のバイトにTransactionTypeを含める方式が適用できない可能性があります。

TransactionType選択アルゴリズムが規定されていない理由

TransactionType選択アルゴリズムについては、この規格で定義することも検討されましたが、必要性の観点から規格の範囲外とされました。

ペイロードがOpaqueバイト配列である理由

トランザクションペイロードがOpaqueバイト配列とされている理由は、将来的に様々なエンコーディング方式をサポートするためです。 2バイト目以降をOpaqueバイト配列とすることで、SSZ、LEB128、固定幅フォーマットなどの新しいエンコーディング方式の採用が可能になります。 これはRLP(または他のエンコーディング)リストを使用するよりも柔軟性が高いです。

ORIGINとCALLERオペコードについて

ORIGINとCALLERオペコードについては、各トランザクションタイプごとにその動作を変更することも検討されました。 しかし、コントラクトがトランザクションタイプごとに挙動を変えないことが望ましいと判断されました。 また、既存のコントラクトとの後方互換性の懸念もありました。 今後は、すべてのトランザクションタイプが、最初のEVMフレームのCALLERを適切に表すアドレスを持ち、ORIGINはすべての場合で同じアドレスになると想定されます。 トランザクションタイプがコントラクトに追加情報を提供する必要がある場合は、新しいオペコードが必要になります。

互換性

EIP2711では、クライアントが最初のバイトを見ることで、レガシートランザクションと新しいタイプのトランザクションを区別できるようになっています。 これにより、既存のシステムとの下位互換性が確保されています。

具体的には、最初のバイトの値が以下の範囲に該当するかどうかで判断します。

  • [0, 0x7f]の範囲
    • 新しいトランザクションタイプ
  • [0xc0, 0xfe]の範囲
    • レガシートランザクションタイプ

この方式により、既存のクライアントは新しいタイプのトランザクションを無視することができ、新しいクライアントは両方のタイプのトランザクションを適切に処理できます。

また、0xffはRLPでエンコードされたトランザクションでは現実的な値ではないため、将来の拡張用のセンチネル値として予約されています。 これにより、将来の拡張性も確保されています。

このように、EIP2711では最初のバイトの値の範囲を使い分けることで、既存のシステムとの下位互換性と将来の拡張性を両立しています。

セキュリティ

EIP2711では、新しいトランザクションタイプを設計する時、署名対象のペイロードの最初のバイトにトランザクションタイプを含めることが強く推奨されています。

この推奨事項の背景には、署名の再利用問題があります。 もし異なるタイプのトランザクションが同じ署名を持つことができてしまうと、セキュリティ上の脆弱性が導入される可能性があります。

例えば、トランザクションタイプAとトランザクションタイプBがあり、両者の署名対象のペイロードが同じフォーマットだとします。 この場合、トランザクションタイプAに対して生成された署名が、トランザクションタイプBにも有効であるかもしれません。 これは、ユーザーがトランザクションタイプAに署名したつもりが、知らないうちにトランザクションタイプBにも署名してしまう可能性があることを意味します。

この問題を防ぐために、EIP2711では、署名対象のペイロードの最初のバイトにトランザクションタイプを含めることを強く推奨しています。 これにより、異なるタイプのトランザクション間で署名が再利用されるリスクを大幅に減らすことができます。

例えば、トランザクションタイプAの署名対象ペイロードが0x01 || data(0x01はトランザクションタイプA、dataは実際のペイロードデータ)、トランザクションタイプBの署名対象ペイロードが0x02 || data(0x02はトランザクションタイプB)とすれば、両者の署名は互いに無効になります。

ただし、この方式が常に可能とは限らないということも認識されています。 例えば、レガシー署名スキームと互換性のあるラップされたレガシートランザクションでは、最初のバイトにトランザクションタイプを含めることが難しい可能性があります。

そのため、EIP2711では必須ではなく推奨とされています。

引用

Micah Zoltu (@MicahZoltu), "EIP-2718: Typed Transaction Envelope," Ethereum Improvement Proposals, no. 2718, June 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2718.

最後に

今回は「Ethereumのトランザクションを拡張し、新しいタイプのトランザクションを柔軟に導入できる仕組みを提案しているEIP2718」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!