はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、ブロック内のトランザクションを実行する前に検証する仕組みを提案しているEIP7886についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
EIP7886は、ブロックの検証をより効率的にする仕組みを提案しています。
具体的には、ブロック内のトランザクションを実行しなくても、そのブロックが正しいかどうかを簡単なチェックだけで確認できるようにします。
もし実行時に無効なトランザクションがあった場合でも、ブロック全体が無効になるのではなく、そのトランザクションだけをスキップする方式を取ります。
検証の仕組み
従来のブロック検証では、ブロック内のすべてのトランザクションを実行し、最終的な状態を確認してから「このブロックは正しい」と判断する必要がありました。しかし、この方法では、検証に時間がかかりブロックの処理速度が制限されてしまいます。
この提案では、検証時に「前の状態」と「ブロックの基本的な構造」だけを確認すれば、ブロックの有効性を判断できるようになります。
これにより、トランザクションの実行を待つ必要がなくなり、検証がスムーズになります。
トランザクションのスキップ機能
ブロック内に無効なトランザクションが含まれていた場合、従来ならブロック全体が無効になっていました。
しかし、この提案では、そのトランザクションだけをスキップし、ブロック全体の有効性には影響を与えません。
これにより、一部のトランザクションの問題でブロックが無効になる事態を防ぎ、より柔軟な処理が可能になります。
コストの管理方法
無効なトランザクションをスキップする場合、問題になるのが「誰がそのコストを負担するのか」です。
この提案では、ブロックの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
が本当にこのブロックを承認しているかを検証可能になります。
ヘッダーの最終構造
新しいブロックヘッダーは以下のようになります。
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
という関数を使います。
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
アドレスと一致しない場合、そのブロックは無効とされます。
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
を超えていないか。
- ブロック内のBlobトランザクションのガス使用量が、最大許容値
COINBASEの支払い能力の確認
COINBASE
は、全てのトランザクションのインクルージョンコストを事前に支払う必要があります。
そのため、COINBASE
のアカウント残高がコストをカバーできるかを確認します。
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
の残高から引かれます。
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
の残高から差し引きます。
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
として設定します。
gas_available = block_gas_limit - total_inclusion_gas
その後、各トランザクションの実行時に以下のようにガスの管理が行われます。
- トランザクションの実行前に、そのトランザクションのインクルージョンガスを
gas_available
に加算。 - トランザクションがスキップされた場合、その分のガスを
gas_available
から引く。 - トランザクションが実行された場合、実際に消費したガス量を
gas_available
から引く。
この処理を実装したコードが以下の部分です。
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が支払ったインクルージョンコストも返金されません。
なぜトランザクションがスキップされるのか?
トランザクションがスキップされる理由はいくつかあります。
-
送信者の残高不足
- 送信者がトランザクションの最大ガス費用(
max_gas_fee
)と送金額(tx.value
)をカバーできない場合。
- 送信者がトランザクションの最大ガス費用(
-
ブロックのガス不足
- ブロックの利用可能なガス(
gas_available
)がトランザクションに必要なガス量(tx.gas
)より少ない場合。
- ブロックの利用可能なガス(
-
送信者のnonceが不一致
- 送信者の
nonce
がトランザクションのnonce
と一致しない場合。
- 送信者の
-
送信者がEOA(Externally Owned Account)ではない
- 送信者がEOA(外部所有アカウント)ではなく、スマートコントラクトアカウントである場合。
これらの条件に基づき、スキップすべきかどうかを判定します。
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)
- 先払いしたインクルージョンコストの払い戻し
この計算は以下のように行われます。
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などからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!