はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ブロック内のトランザクションを実行する前に検証する仕組みを提案しているEIP7886についてまとめていきます!

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

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

概要

EIP7886は、ブロックの検証をより効率的にする仕組みを提案しています。 具体的には、ブロック内のトランザクションを実行しなくても、そのブロックが正しいかどうかを簡単なチェックだけで確認できるようにします。 もし実行時に無効なトランザクションがあった場合でも、ブロック全体が無効になるのではなく、そのトランザクションだけをスキップする方式を取ります。

COINBASEとは?

ブロックのマイニングやバリデーションを行うバリデータ(またはマイナー)が報酬を受け取るための特別なトランザクションのことです。

ブロックが生成されると、バリデータ(またはマイナー)は新しいブロックを作成した報酬として、新規発行されたコインやトランザクション手数料を受け取ります。 この報酬を受け取るために、ブロック内にはCOINBASEトランザクションが含まれます。 このトランザクションは通常、ブロックの最初に配置され、新しいコインの発行や手数料の分配を行います。

検証の仕組み

従来のブロック検証では、ブロック内のすべてのトランザクションを実行し、最終的な状態を確認してから「このブロックは正しい」と判断する必要がありました。しかし、この方法では、検証に時間がかかりブロックの処理速度が制限されてしまいます。 この提案では、検証時に「前の状態」と「ブロックの基本的な構造」だけを確認すれば、ブロックの有効性を判断できるようになります。 これにより、トランザクションの実行を待つ必要がなくなり、検証がスムーズになります。

トランザクションのスキップ機能

ブロック内に無効なトランザクションが含まれていた場合、従来ならブロック全体が無効になっていました。 しかし、この提案では、そのトランザクションだけをスキップし、ブロック全体の有効性には影響を与えません。 これにより、一部のトランザクションの問題でブロックが無効になる事態を防ぎ、より柔軟な処理が可能になります。

コストの管理方法

無効なトランザクションをスキップする場合、問題になるのが「誰がそのコストを負担するのか」です。 この提案では、ブロックのCOINBASE(マイナーやバリデータ)が、トランザクションの基本的なコスト(ベースコスト、コールデータ、Blobのコスト)を事前に支払う仕組みを取ります。 そして、トランザクションが正常に実行された場合にのみ、実行者がそのコストを負担してCOINBASEが回収できるようになっています。 これにより、ネットワークの健全性を保ちつつ、ブロックの柔軟性を向上させます。

動機

EIP7886を導入することで、ブロックの検証がよりスピーディーになります。 現在は、ブロックを検証するためにすべてのトランザクションを実行する必要がありますが、EIP7886ではそのプロセスを短縮できます。 特に、ブロックのガスリミットを引き上げることが可能になり、より多くのトランザクションを処理できるようになる可能性があります。

また、検証が非同期で行えるようになることで、バリデータがすぐにブロックの正当性を証明できてネットワークの効率向上にもつながります。

仕様

ヘッダー構造の変更

EIP7886では、ブロックを実行せずに検証できるようにするため、ブロックヘッダーの構造を変更します。 具体的には、実行結果に依存するフィールドを親ブロックから引き継ぐようにします。

遅延されるフィールド

通常、ブロックヘッダーにはトランザクション実行後の結果が含まれますが、これがあると事前の検証ができません。 そこで、以下のフィールドを「親ブロック」の情報に置き換えます。

  • state_root
  • receipt_root
  • bloom
  • gas_used
  • requests_hash

この変更により、ヘッダーを事前に検証可能にしつつブロック全体の整合性を保ちます。

COINBASEの署名

COINBASEが事前にトランザクションの基本コストを支払うため、その承認を保証する仕組みが必要です。 そこで、COINBASEがヘッダー全体に署名する方式を採用します。

新しいヘッダーには、COINBASEが署名した以下のフィールドが追加されます。

  • y_parity(署名のY座標パリティ)
  • r(署名のr値)
  • s(署名のs値)

この署名により、COINBASEが本当にこのブロックを承認しているかを検証可能になります。

ヘッダーの最終構造

新しいブロックヘッダーは以下のようになります。

python
class Header:
    parent_hash: Hash32
    ommers_hash: Hash32
    coinbase: Address
    pre_state_root: Root # 遅延されたフィールド
    transactions_root: Root
    parent_receipt_root: Root # 遅延されたフィールド
    parent_bloom: Bloom # 遅延されたフィールド
    difficulty: Uint
    number: Uint
    gas_limit: Uint
    parent_gas_used: Uint
    timestamp: U256
    extra_data: Bytes
    prev_randao: Bytes32
    nonce: Bytes8
    base_fee_per_gas: Uint
    withdrawals_root: Root
    blob_gas_used: U64
    excess_blob_gas: U64
    parent_beacon_block_root: Root
    parent_requests_hash: Hash32 # 遅延されたフィールド
    # COINBASEの署名
    y_parity: U256
    r: U256
    s: U256

COINBASEの署名検証

ブロックが正しいかどうかを確認するにはCOINBASEが本当に署名したかを検証する必要があります。 そのために recover_header_signer という関数を使います。

python
def recover_header_signer(
    chain_id: U64,
    header: Header,
) -> Address:
    signing_hash = keccak256(
        b"0x06"
        + rlp.encode(
            (
                chain_id,
                header.parent_hash,
                header.ommers_hash,
                header.coinbase,
                header.pre_state_root,
                header.transactions_root,
                header.parent_receipt_root,
                header.parent_bloom,
                header.difficulty,
                header.number,
                header.gas_limit,
                header.parent_gas_used,
                header.timestamp,
                header.extra_data,
                header.prev_randao,
                header.nonce,
                header.base_fee_per_gas,
                header.withdrawals_root,
                header.blob_gas_used,
                header.excess_blob_gas,
                header.parent_beacon_block_root,
                header.parent_requests_hash,
            )
        )
    )

この関数は、ブロックヘッダーの情報を元にCOINBASEの署名者アドレスを復元します。

署名が一致しない場合の処理

復元した署名者アドレスがヘッダーのCOINBASEアドレスと一致しない場合、そのブロックは無効とされます。

python
header_signer = recover_header_signer(
    chain.chain_id,
    block.header,
)
if coinbase != header_signer:
    raise InvalidBlock

この仕組みにより、不正なブロックがネットワークに流れることを防ぎつつ、COINBASEが責任を持ってブロックを承認したことを保証できます。

スタティックなブロック検証とは

EIP7886では、ブロックの検証(validation)と実行(execution)を分離します。 ブロックの検証を先に行い、実行しなくてもブロックの正しさを確認できるようにすることで、検証を高速化してネットワークのスケーラビリティを向上させます。

Ethereumのexecution-specsでは、ブロックの静的検証(スタティックバリデーション)はvalidate_block関数で行われます。 ここでブロックが有効だと判断されると、すぐにアテステーション(承認)が可能になります。 一方、ブロックの実行はapply_body関数で行われ、実行後に最終的な状態が確定します。

検証のポイント

validate_block関数では以下の点をチェックします。

遅延されたヘッダーフィールドの検証

ブロックのヘッダーに含まれる一部のフィールドは、トランザクション実行後の結果に依存するため、親ブロックから継承した情報と一致しているかを確認します。

  • pre_state_root(前のステートルート)
  • parent_gas_used(親ブロックで使用されたガス量)
  • parent_receipt_root(親ブロックのレシートルート)
  • parent_bloom(親ブロックのログフィルター)
  • parent_requests_hash(親ブロックのリクエストハッシュ)

これらが前のブロックの実行結果と一致しない場合、ブロックは無効になります。

静的に検証可能なヘッダーフィールドのチェック

実行しなくても検証できるフィールドをチェックします。

  • transactions_rootとwithdrawals_rootが正しく構築されているか
  • blob_gas_usedが適切に計算されているか
  • COINBASEの署名が正しいか
各トランザクションの静的検証(check_transaction_static)

ブロック内の各トランザクションについて以下のチェックを行います。

  • 署名の検証
    • 送信者が正しいことを確認。
  • ガス価格のチェック
    • ガス価格が最低限必要なbase_fee_per_gasを下回っていないか。
  • COINBASEが支払うコストの計算
    • COINBASEが全てのトランザクションのインクルージョンコストを支払えるか。

もし、トランザクションの署名が無効だったり、ガス価格が不足していたりするとブロック全体が無効になります。

リソース制限のチェック
  • ガスの合計
    • ブロック内のすべてのトランザクションのガス使用量が、ブロックのgas_limitを超えていないか。
  • Blobガスの合計
    • ブロック内のBlobトランザクションのガス使用量が、最大許容値MAX_BLOB_GAS_PER_BLOCKを超えていないか。
COINBASEの支払い能力の確認

COINBASEは、全てのトランザクションのインクルージョンコストを事前に支払う必要があります。 そのため、COINBASEのアカウント残高がコストをカバーできるかを確認します。

python
coinbase_account = get_account(chain.state, coinbase)
if Uint(coinbase_account.balance) < inclusion_cost:
    raise InvalidBlock

もし残高が不足している場合、そのブロックは無効になります。

Trieの検証
  • transactions_rootがブロック内のトランザクションデータと一致しているか。
  • withdrawals_rootが正しく計算されているか。
  • blob_gas_usedの値が正しいか。

これらが適切でない場合、ブロックは無効とされます。

ブロックの実行(Block Execution)

ブロックの実行処理は、Ethereumのexecution-specsのapply_body関数で実装されています。 この提案では、COINBASEがブロック実行の開始時に全トランザクションのインクルージョンコストを先払いする仕組みを導入しています。

COINBASEがインクルージョンコストを事前に支払う仕組み

ブロックの開始時に、COINBASEが全てのトランザクションにかかる基本的なコストを支払います。 これは以下の要素を含みます。

  • 基本ガスコスト(21,000 gas)
  • コールデータのコスト(EIP7623の定義に基づく)
  • Blobガス(blob gas)のコスト(ブロックに含まれるBlobのデータサイズに基づく)

このコストは、ブロックの全てのトランザクションを対象に計算され、COINBASEの残高から引かれます。

python
def calculate_inclusion_gas_cost(tx: Transaction) -> Uint:
    tokens_in_calldata = zero_bytes + (len(tx.data) - zero_bytes) * 4
    calldata_floor_gas_cost = tokens_in_calldata * FLOOR_CALLDATA_COST
    return TX_BASE_COST + calldata_floor_gas_cost

apply_body関数内では、全トランザクションのインクルージョンコストを計算し、COINBASEの残高から差し引きます。

python
total_inclusion_gas = sum(calculate_inclusion_gas_cost(tx) for tx in decoded_transactions)
total_blob_gas_used = sum(calculate_total_blob_gas(tx) for tx in decoded_transactions)
inclusion_cost = (
    total_inclusion_gas * base_fee_per_gas
    + total_blob_gas_used * blob_gas_price
)
coinbase_account = get_account(state, coinbase)
coinbase_balance_after_inclusion_cost = (
    Uint(coinbase_account.balance) - inclusion_cost
)

# COINBASEの残高からインクルージョンコストを差し引く
set_account_balance(
    env.state,
    env.coinbase,
    U256(coinbase_balance_after_inclusion_cost),
)

この処理により、COINBASEはトランザクションの実行結果に関わらず、データ可用性(DA)と署名検証などの基本コストを負担することになります。

インクルージョンガスの管理

COINBASEが支払ったコストに応じて、gas_available(利用可能なガス量)も事前に調整されます。 ブロックの開始時に、ブロックのgas_limitからインクルージョンガスを差し引いた値をgas_availableとして設定します。

python
gas_available = block_gas_limit - total_inclusion_gas

その後、各トランザクションの実行時に以下のようにガスの管理が行われます。

  • トランザクションの実行前に、そのトランザクションのインクルージョンガスをgas_availableに加算。
  • トランザクションがスキップされた場合、その分のガスをgas_availableから引く。
  • トランザクションが実行された場合、実際に消費したガス量をgas_availableから引く。

この処理を実装したコードが以下の部分です。

python
for i, tx in enumerate(txs):
    ...
    inclusion_gas = calculate_inclusion_gas_cost(tx)
    gas_available += inclusion_gas

    (
        is_transaction_skipped,
        effective_gas_price,
        blob_versioned_hashes,
    ) = check_transaction(
        state,
        tx,
        sender_address,
        gas_available,
    )

    # トランザクションがスキップされた場合、その分のガスを引く
    if is_transaction_skipped:
        gas_available -= inclusion_gas
    else:
        gas_used, logs, error = process_transaction(env, tx)
        gas_available -= gas_used

スキップされたトランザクションとは

スキップされたトランザクション(Skipped Transaction)とは、ブロックに含まれているものの、実行されないトランザクション のことです。スキップされたトランザクションは、通常のトランザクションと異なり、ガスは消費せず、COINBASEが支払ったインクルージョンコストも返金されません。

なぜトランザクションがスキップされるのか?

トランザクションがスキップされる理由はいくつかあります。

  1. 送信者の残高不足
    • 送信者がトランザクションの最大ガス費用(max_gas_fee)と送金額(tx.value)をカバーできない場合。
  2. ブロックのガス不足
    • ブロックの利用可能なガス(gas_available)がトランザクションに必要なガス量(tx.gas)より少ない場合。
  3. 送信者のnonceが不一致
    • 送信者のnonceがトランザクションのnonceと一致しない場合。
  4. 送信者がEOA(Externally Owned Account)ではない
    • 送信者がEOA(外部所有アカウント)ではなく、スマートコントラクトアカウントである場合。

これらの条件に基づき、スキップすべきかどうかを判定します。

python
is_sender_eoa = (
    sender_account.code == bytearray() 
    or is_valid_delegation(sender_account.code)
)

is_transaction_skipped = (
    tx.gas > gas_available
    or Uint(sender_account.balance) < max_gas_fee + Uint(tx.value)
    or sender_account.nonce != tx.nonce
    or not is_sender_eoa
)

スキップのタイミング

スキップの判断は、実行時のチェックに依存する場合にのみ行われます。 つまり、署名検証などの静的なチェックはブロック検証時に完了しており、それに失敗した場合はブロック全体が無効になります。

スキップされるのは、ブロックとしてはすでに有効と認められたが実行してみたら処理できなかったトランザクションです。

トランザクションが実行された場合の処理

トランザクションがスキップされずに実行された場合、処理内容に大きな変更はありません。 ただし、COINBASEはインクルージョンコストの払い戻しを受けるようになります。

COINBASEの報酬の仕組み

通常、COINBASEはトランザクションの優先手数料(priority_fee)を受け取ります。 しかし、新しい仕組みではCOINBASEは以下の金額を受け取ります。

  • 優先手数料(priority_fee)
  • 先払いしたインクルージョンコストの払い戻し

この計算は以下のように行われます。

python
inclusion_cost_refund = (
    inclusion_gas * base_fee_per_gas
    + blob_gas_used * blob_gas_price
)

# COINBASEに優先手数料とインクルージョンコストの払い戻しを加算
coinbase_balance_after_transaction = (
    coinbase_account.balance 
    + priority_fee
    + inclusion_cost_refund
)

補足

遅延実行を可能にするための要件

ブロックの実行を遅らせつつ、そのブロックの正当性を事前に検証可能にするには2つの要素が必要です。

実行結果の遅延

通常、ブロックヘッダーにはトランザクションを実行した結果 が含まれますが、これがあると事前の検証ができません。 そこで、実行結果に関わるヘッダーフィールドを1つ前のブロックの結果に置き換えることで、トランザクションの実行を遅らせることができます。

具体的な変更点。

  • state_root → pre_state_root(実行前のステート)
  • receipt_root → parent_receipt_root(親ブロックのレシートルート)
  • requests_hash(EIP7685で定義されるリクエスト)も1ブロック遅らせる

これにより、現在のブロックではトランザクションを実行しなくても検証可能になります。 ただし、この変更だけではトランザクションの有効性を確認するために実行が必要になるため、完全に実行を遅らせることはできません。

COINBASEによるインクルージョンコストの事前支払い

ブロックの検証をトランザクションの実行なしで完了させるには、無効なトランザクションをスキップできるようにする必要があります。 しかし、現在のEthereumでは、トランザクションがブロックに含まれると、必ずそのデータ量に応じた手数料を支払わなければならない(データアベイラビリティの問題)ため、単純にスキップすることはできません。

そこで、COINBASEが全てのトランザクションのインクルージョンコストを事前に支払う仕組みを導入します。 これにより、トランザクションが実行時に無効と判定されても、プロトコルはすでにコストを回収済みなので問題なくスキップできるようになります。

この仕組みを導入することで、トランザクションの実行を遅らせながらもブロックの検証を迅速に行えるようになります。

COINBASEのヘッダー署名

COINBASEはブロックのヘッダーに署名することで、インクルージョンコストを負担することを正式に承認します。 この署名は、COINBASEがブロックの責任を受け入れることを示し、検証の時に重要な役割を果たします。

署名のポイント

  • COINBASEがヘッダーに署名することで、そのブロックのインクルージョンコストを負担する意思を明確にする。
  • 署名はブロック全体に対するものであるため、リプレイ攻撃(過去のブロックの署名を悪用する攻撃)を防ぐことができる。
  • 署名を検証することで、そのブロックのCOINBASEが確実に支払いを承認したことを確認できる。

この仕組みによって、COINBASEの責任が明確化されてトランザクションのスキップが可能になり、ネットワーク全体の効率が向上します。

互換性

この変更は互換性がないためハードフォークの実行が必要です。

セキュリティ

COINBASEの資金要件

ブロックを作成する時、COINBASEは全てのトランザクションのインクルージョンコストを事前に支払う必要があるため、十分な資金を保有しておく必要があります。

必要な資金量の計算方法

COINBASEがカバーすべき最大のコストは以下の2つの要素を合計したものになります。

  • ブロックの最大ガス使用量 × ベースガス料金(base_fee_per_gas)
  • ブロブガスの最大使用量 × ブロブガス料金(blob_gas_price)

例えば、「ベースガス料金が100 gwei(0.0000001 ETH)」 、「ブロックのガスリミットが36M gas(3600万ガス)」の場合、COINBASEは 3.6 ETH`(36M × 100 gwei)を持っていないと、最悪のケースではブロックの作成ができなくなります。

流動性(Liquidity)への影響

この要件は、特にベースガス料金が高いときにブロックプロポーザー(ブロックを提案するバリデータ)に流動性の制約を与える可能性があります。 例えば、ガス料金が急騰した場合、COINBASEが必要なETHを用意できないとブロックの提案が失敗する可能性があります。

実際のコストは?

ただし、ほとんどの状況では全てのガスリミットが埋まることはほとんどないので、COINBASEが実際に負担するコストはこれよりも少なくなります。

過去1年間のデータを見ると、平均的なブロックのインクルージョンコストは「約5.5M gas(550万ガス)」、「ベースガス料金が100 gweiのときに約0.55 ETH」程度でした。

つまり、最悪のケースを想定するとCOINBASEに3.6 ETHが必要だが、通常のケースではその10分の1程度で済むということになります。

引用

Francesco D`Amato (@fradamt), Toni Wahrstätter (@nerolation), "EIP-7886: Delayed execution [DRAFT]," Ethereum Improvement Proposals, no. 7886, February 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7886.

最後に

今回は「ブロック内のトランザクションを実行する前に検証する仕組みを提案しているEIP7886」についてまとめてきました! いかがだったでしょうか?

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

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