はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、トランザクション料金の不安定性とオークションの非効率性に対処し、ブロックごとに基本料金が調整され、トランザクションごとの料金が自動設定されるようになる仕組みを提案しているEIP1559についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
ガス代とは
そもそもガス代についてあまり理解していない方向けに簡単に特徴だけ説明します。
概要
Ethereumネットワークのトランザクション手数料とブロックサイズの動的調整に関する仕組みが提案されています。
EIP2718
この提案では、EIP2718と呼ばれる新しいトランザクションタイプを導入しています。
EIP2718については以下の記事を参考にしてください。
0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])
トランザクションの形式は上記で、それぞれの値については以下を参考にしてください。
各パラメータ
パラメータ | 説明 |
---|---|
0x02 |
トランザクションタイプの識別子 |
rlp(...) |
データをエンコードする形式(Recursive Length Prefix) |
chain_id |
チェーンの識別子 |
nonce |
トランザクションのユニークカウンタ |
max_priority_fee_per_gas |
トランザクションを優先的に処理してもらうために支払う最大手数料 |
max_fee_per_gas |
ガス1単位あたりに支払う総額の最大値(優先手数料 + 基本手数料) |
gas_limit |
トランザクションで使用可能な最大ガス量 |
destination |
送信先アドレス |
amount |
送金額 |
data |
トランザクションに含めるデータ(通常、コントラクト呼び出しに使用) |
access_list |
事前にアクセスするストレージのリスト |
signature_y_parity , signature_r , signature_s
|
トランザクションの署名 |
基本手数料
親ブロック(1つ前のブロック)のガス使用量とガスターゲット(目標ガス使用量)に基づいて、基本手数料(Base Fee)が増減します。
ガス使用量がガスターゲットを上回ると基本手数料が増加し、下回ると減少します。
基本手数料(Base Fee)はburnされます。
優先手数料
トランザクション実行で支払う手数料の最大値である最大手数料(Max Fee)を設定します。
最大手数料(Max Fee)には、マイナー(バリデーター)に支払う報酬である優先手数料(Priority Fee)と基本手数料(Base Fee)が含まれています。
モチベーション
現在の問題点
現在の手数料システムには以下の問題点があります。
-
手数料の変動とコストの不一致
- 現在の手数料のオークション方式には無駄がある。
- トランザクション手数料が極端に変動し、ネットワークにかかるコストが一定しない。
- 例えば、手数料が10倍になっても、実際に処理するトランザクションのコスト(リソースやエネルギー)はほとんど変わらない。
-
待ち時間
- ブロックごとにガスリミットが低いため、トランザクションの処理が遅くなり待たされてしまう。
-
価格オークションの非効率性
- ユーザーが過剰な手数料を支払ってしまう可能性がある。
-
ブロック報酬のないブロックチェーンの不安定性
- 将来的にマイナー(バリデーター)への報酬がなくなった時、トランザクション手数料のみで補うには不安。
新たな提案
EIP2718では、ネットワークの混雑度に基づいて調整される基本手数料(Base Fee)の導入を提案しています。
-
基本手数料の調整
- 目標とするガス使用量を超えると基本手数料が増加し、下回ると減少します。
-
優先手数料と最大手数料
- マイナー(バリデーター)に優先的に処理してもらうために、報酬となる手数料を渡します。
- この時、支払う全体のコストの許容最大値である最大手数料も設定します。
-
基本手数料のburn
- 基本手数量はburnされてネットワークから消えます。
これによりETHのインフレを防ぎつつ、マイナーが故意に手数料を操作するインセンティブをなくしています。
それぞれの用語をまとめたものが以下になります。
また、ガスリミットについてもまとめました。
基本手数料の増減については以下になります。
Etherscanで実際にEIP1559のガス代の表示を確認したものが以下になります。
最初に「Base fee(基本手数料)」と「Priority Fee(優先手数料)」を足し合わせ、そこに「Gas Used(ガス使用量)」を掛け合わせています。
オレンジ枠では、ガスリミットに対してガス使用量がどれくらいかを表しています。
最後に掛け合わせた値のうち、「Base fee(基本手数料)」と「Priority Fee(優先手数料)」が赤枠に表示されています。
「Base fee(基本手数料)」はBurnされ、「Priority Fee(優先手数料)」はTipとしてマイナー(バリデーター)に支払われます。
仕様
以下はコード説明部分と照らし合わせながら確認してください。
-
GASPRICE
(0x3a
)というオペコードが追加されeffective_gas_price
を返します。 -
FORK_BLOCK_NUMBER
に達したら、新しいEIP2718のTransactionType
が導入されます。 - 新しいトランザクションコストは、EIP2718の以下を継承しています。
21000 + 16 * non-zero calldata bytes + 4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access list address count
各パラメーターの説明
パラメーター | 説明 |
---|---|
基本コスト | 21000ガス |
非ゼロのcalldataバイトごとのコスト | 16ガス |
ゼロのcalldataバイトごとのコスト | 4ガス |
アクセスリストのストレージキーごとのコスト | 1900ガス |
アクセスリストのアドレスごとのコスト | 2400ガス |
- EIP2718トランザクションのデータ構造は以下のようになっています。
TransactionPayloadrlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])
各パラメーターの説明
パラメーター | 説明 |
---|---|
chain_id |
チェーンの識別子 |
nonce |
トランザクションの一意のカウンタ |
max_priority_fee_per_gas |
マイナーに支払う優先手数料の最大値 |
max_fee_per_gas |
ガス1単位あたりの総額の最大値 |
gas_limit |
トランザクションで使用可能な最大ガス量 |
destination |
送信先アドレス |
amount |
送金額 |
data |
トランザクションに含めるデータ(通常はスマートコントラクト呼び出しに使用) |
access_list |
事前にアクセスするストレージのリスト |
signature_y_parity , signature_r , signature_s
|
トランザクションの署名 |
- トランザクションには以下のsecp256k1署名が使用されます。
keccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list]))
- トランザクションのレシートは以下のようになっています。
rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])
各パラメーターの説明
パラメーター | 説明 |
---|---|
status |
トランザクションの成功(1)または失敗(0)を示すステータス |
cumulative_transaction_gas_used |
トランザクションの累積ガス使用量 |
logs_bloom |
ログのブルームフィルタ(ログの効率的な検索を助けるデータ構造) |
logs |
トランザクションの実行中に生成されたログエントリ |
コード
では、実際のコードを見ていきましょう。
コード全体は以下に記載していますが、重要な部分のみ抜粋して解説していきます。
コード全体
from typing import Union, Dict, Sequence, List, Tuple, Literal
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
@dataclass
class TransactionLegacy:
signer_nonce: int = 0
gas_price: int = 0
gas_limit: int = 0
destination: int = 0
amount: int = 0
payload: bytes = bytes()
v: int = 0
r: int = 0
s: int = 0
@dataclass
class Transaction2930Payload:
chain_id: int = 0
signer_nonce: int = 0
gas_price: int = 0
gas_limit: int = 0
destination: int = 0
amount: int = 0
payload: bytes = bytes()
access_list: List[Tuple[int, List[int]]] = field(default_factory=list)
signature_y_parity: bool = False
signature_r: int = 0
signature_s: int = 0
@dataclass
class Transaction2930Envelope:
type: Literal[1] = 1
payload: Transaction2930Payload = Transaction2930Payload()
@dataclass
class Transaction1559Payload:
chain_id: int = 0
signer_nonce: int = 0
max_priority_fee_per_gas: int = 0
max_fee_per_gas: int = 0
gas_limit: int = 0
destination: int = 0
amount: int = 0
payload: bytes = bytes()
access_list: List[Tuple[int, List[int]]] = field(default_factory=list)
signature_y_parity: bool = False
signature_r: int = 0
signature_s: int = 0
@dataclass
class Transaction1559Envelope:
type: Literal[2] = 2
payload: Transaction1559Payload = Transaction1559Payload()
Transaction2718 = Union[Transaction1559Envelope, Transaction2930Envelope]
Transaction = Union[TransactionLegacy, Transaction2718]
@dataclass
class NormalizedTransaction:
signer_address: int = 0
signer_nonce: int = 0
max_priority_fee_per_gas: int = 0
max_fee_per_gas: int = 0
gas_limit: int = 0
destination: int = 0
amount: int = 0
payload: bytes = bytes()
access_list: List[Tuple[int, List[int]]] = field(default_factory=list)
@dataclass
class Block:
parent_hash: int = 0
uncle_hashes: Sequence[int] = field(default_factory=list)
author: int = 0
state_root: int = 0
transaction_root: int = 0
transaction_receipt_root: int = 0
logs_bloom: int = 0
difficulty: int = 0
number: int = 0
gas_limit: int = 0 # note the gas_limit is the gas_target * ELASTICITY_MULTIPLIER
gas_used: int = 0
timestamp: int = 0
extra_data: bytes = bytes()
proof_of_work: int = 0
nonce: int = 0
base_fee_per_gas: int = 0
@dataclass
class Account:
address: int = 0
nonce: int = 0
balance: int = 0
storage_root: int = 0
code_hash: int = 0
INITIAL_BASE_FEE = 1000000000
INITIAL_FORK_BLOCK_NUMBER = 10 # TBD
BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
ELASTICITY_MULTIPLIER = 2
class World(ABC):
def validate_block(self, block: Block) -> None:
parent_gas_target = self.parent(block).gas_limit // ELASTICITY_MULTIPLIER
parent_gas_limit = self.parent(block).gas_limit
# on the fork block, don't account for the ELASTICITY_MULTIPLIER to avoid
# unduly halving the gas target.
if INITIAL_FORK_BLOCK_NUMBER == block.number:
parent_gas_target = self.parent(block).gas_limit
parent_gas_limit = self.parent(block).gas_limit * ELASTICITY_MULTIPLIER
parent_base_fee_per_gas = self.parent(block).base_fee_per_gas
parent_gas_used = self.parent(block).gas_used
transactions = self.transactions(block)
# check if the block used too much gas
assert block.gas_used <= block.gas_limit, 'invalid block: too much gas used'
# check if the block changed the gas limit too much
assert block.gas_limit < parent_gas_limit + parent_gas_limit // 1024, 'invalid block: gas limit increased too much'
assert block.gas_limit > parent_gas_limit - parent_gas_limit // 1024, 'invalid block: gas limit decreased too much'
# check if the gas limit is at least the minimum gas limit
assert block.gas_limit >= 5000
# check if the base fee is correct
if INITIAL_FORK_BLOCK_NUMBER == block.number:
expected_base_fee_per_gas = INITIAL_BASE_FEE
elif parent_gas_used == parent_gas_target:
expected_base_fee_per_gas = parent_base_fee_per_gas
elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_gas_target
base_fee_per_gas_delta = max(parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR, 1)
expected_base_fee_per_gas = parent_base_fee_per_gas + base_fee_per_gas_delta
else:
gas_used_delta = parent_gas_target - parent_gas_used
base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
expected_base_fee_per_gas = parent_base_fee_per_gas - base_fee_per_gas_delta
assert expected_base_fee_per_gas == block.base_fee_per_gas, 'invalid block: base fee not correct'
# execute transactions and do gas accounting
cumulative_transaction_gas_used = 0
for unnormalized_transaction in transactions:
# Note: this validates transaction signature and chain ID which must happen before we normalize below since normalized transactions don't include signature or chain ID
signer_address = self.validate_and_recover_signer_address(unnormalized_transaction)
transaction = self.normalize_transaction(unnormalized_transaction, signer_address)
signer = self.account(signer_address)
signer.balance -= transaction.amount
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover attached value'
# the signer must be able to afford the transaction
assert signer.balance >= transaction.gas_limit * transaction.max_fee_per_gas
# ensure that the user was willing to at least pay the base fee
assert transaction.max_fee_per_gas >= block.base_fee_per_gas
# Prevent impossibly large numbers
assert transaction.max_fee_per_gas < 2**256
# Prevent impossibly large numbers
assert transaction.max_priority_fee_per_gas < 2**256
# The total must be the larger of the two
assert transaction.max_fee_per_gas >= transaction.max_priority_fee_per_gas
# priority fee is capped because the base fee is filled first
priority_fee_per_gas = min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)
# signer pays both the priority fee and the base fee
effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas
signer.balance -= transaction.gas_limit * effective_gas_price
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover gas'
gas_used = self.execute_transaction(transaction, effective_gas_price)
gas_refund = transaction.gas_limit - gas_used
cumulative_transaction_gas_used += gas_used
# signer gets refunded for unused gas
signer.balance += gas_refund * effective_gas_price
# miner only receives the priority fee; note that the base fee is not given to anyone (it is burned)
self.account(block.author).balance += gas_used * priority_fee_per_gas
# check if the block spent too much gas transactions
assert cumulative_transaction_gas_used == block.gas_used, 'invalid block: gas_used does not equal total gas used in all transactions'
# TODO: verify account balances match block's account balances (via state root comparison)
# TODO: validate the rest of the block
def normalize_transaction(self, transaction: Transaction, signer_address: int) -> NormalizedTransaction:
# legacy transactions
if isinstance(transaction, TransactionLegacy):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.signer_nonce,
gas_limit = transaction.gas_limit,
max_priority_fee_per_gas = transaction.gas_price,
max_fee_per_gas = transaction.gas_price,
destination = transaction.destination,
amount = transaction.amount,
payload = transaction.payload,
access_list = [],
)
# 2930 transactions
elif isinstance(transaction, Transaction2930Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.gas_price,
max_fee_per_gas = transaction.payload.gas_price,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
# 1559 transactions
elif isinstance(transaction, Transaction1559Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.max_priority_fee_per_gas,
max_fee_per_gas = transaction.payload.max_fee_per_gas,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
else:
raise Exception('invalid transaction: unexpected number of items')
@abstractmethod
def parent(self, block: Block) -> Block: pass
@abstractmethod
def block_hash(self, block: Block) -> int: pass
@abstractmethod
def transactions(self, block: Block) -> Sequence[Transaction]: pass
# effective_gas_price is the value returned by the GASPRICE (0x3a) opcode
@abstractmethod
def execute_transaction(self, transaction: NormalizedTransaction, effective_gas_price: int) -> int: pass
@abstractmethod
def validate_and_recover_signer_address(self, transaction: Transaction) -> int: pass
@abstractmethod
def account(self, address: int) -> Account: pass
INITIAL_BASE_FEE = 1000000000
INITIAL_FORK_BLOCK_NUMBER = 10 # TBD
BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
ELASTICITY_MULTIPLIER = 2
def validate_block(self, block: Block) -> None:
parent_gas_target = self.parent(block).gas_limit // ELASTICITY_MULTIPLIER
parent_gas_limit = self.parent(block).gas_limit
# on the fork block, don't account for the ELASTICITY_MULTIPLIER to avoid
# unduly halving the gas target.
if INITIAL_FORK_BLOCK_NUMBER == block.number:
parent_gas_target = self.parent(block).gas_limit
parent_gas_limit = self.parent(block).gas_limit * ELASTICITY_MULTIPLIER
parent_base_fee_per_gas = self.parent(block).base_fee_per_gas
parent_gas_used = self.parent(block).gas_used
transactions = self.transactions(block)
# check if the block used too much gas
assert block.gas_used <= block.gas_limit, 'invalid block: too much gas used'
# check if the block changed the gas limit too much
assert block.gas_limit < parent_gas_limit + parent_gas_limit // 1024, 'invalid block: gas limit increased too much'
assert block.gas_limit > parent_gas_limit - parent_gas_limit // 1024, 'invalid block: gas limit decreased too much'
# check if the gas limit is at least the minimum gas limit
assert block.gas_limit >= 5000
# check if the base fee is correct
if INITIAL_FORK_BLOCK_NUMBER == block.number:
expected_base_fee_per_gas = INITIAL_BASE_FEE
elif parent_gas_used == parent_gas_target:
expected_base_fee_per_gas = parent_base_fee_per_gas
elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_gas_target
base_fee_per_gas_delta = max(parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR, 1)
expected_base_fee_per_gas = parent_base_fee_per_gas + base_fee_per_gas_delta
else:
gas_used_delta = parent_gas_target - parent_gas_used
base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
expected_base_fee_per_gas = parent_base_fee_per_gas - base_fee_per_gas_delta
assert expected_base_fee_per_gas == block.base_fee_per_gas, 'invalid block: base fee not correct'
# execute transactions and do gas accounting
cumulative_transaction_gas_used = 0
for unnormalized_transaction in transactions:
# Note: this validates transaction signature and chain ID which must happen before we normalize below since normalized transactions don't include signature or chain ID
signer_address = self.validate_and_recover_signer_address(unnormalized_transaction)
transaction = self.normalize_transaction(unnormalized_transaction, signer_address)
signer = self.account(signer_address)
signer.balance -= transaction.amount
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover attached value'
# the signer must be able to afford the transaction
assert signer.balance >= transaction.gas_limit * transaction.max_fee_per_gas
# ensure that the user was willing to at least pay the base fee
assert transaction.max_fee_per_gas >= block.base_fee_per_gas
# Prevent impossibly large numbers
assert transaction.max_fee_per_gas < 2**256
# Prevent impossibly large numbers
assert transaction.max_priority_fee_per_gas < 2**256
# The total must be the larger of the two
assert transaction.max_fee_per_gas >= transaction.max_priority_fee_per_gas
# priority fee is capped because the base fee is filled first
priority_fee_per_gas = min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)
# signer pays both the priority fee and the base fee
effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas
signer.balance -= transaction.gas_limit * effective_gas_price
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover gas'
gas_used = self.execute_transaction(transaction, effective_gas_price)
gas_refund = transaction.gas_limit - gas_used
cumulative_transaction_gas_used += gas_used
# signer gets refunded for unused gas
signer.balance += gas_refund * effective_gas_price
# miner only receives the priority fee; note that the base fee is not given to anyone (it is burned)
self.account(block.author).balance += gas_used * priority_fee_per_gas
# check if the block spent too much gas transactions
assert cumulative_transaction_gas_used == block.gas_used, 'invalid block: gas_used does not equal total gas used in all transactions'
# TODO: verify account balances match block's account balances (via state root comparison)
# TODO: validate the rest of the block
上記のコード部分でガス代の調整をしています。
1つずつ上から確認していきます。
親ブロックデータのガスリミットとガスターゲット取得
ELASTICITY_MULTIPLIER = 2
parent_gas_target = self.parent(block).gas_limit // ELASTICITY_MULTIPLIER
parent_gas_limit = self.parent(block).gas_limit
基本手数料の増加/減少に関連する、親ブロック(1つ前のブロック)ガスターゲット(目標となるガス量)とブロックガスリミット(ブロックに含めることができるガスの最大値)を取得しています。
ELASTICITY_MULTIPLIER
には2
が設定されていることから、ガスターゲットはガスリミットの半分ということがわかります。
例えば、ブロックのガスリミットが3000万だとすると、ガスターゲットは1500万になります。
初期ブロックの設定
if INITIAL_FORK_BLOCK_NUMBER == block.number:
parent_gas_target = self.parent(block).gas_limit
parent_gas_limit = self.parent(block).gas_limit * ELASTICITY_MULTIPLIER
ここでは、EIP1559が適用された時の最初のブロックでのガガスターゲットとスリミットを設定しています。
先ほどと異なり、ガスターゲットに対してガスリミットが2倍になるように設定されています。
親ブロックの基本手数料とガス使用量を取得
parent_base_fee_per_gas = self.parent(block).base_fee_per_gas
parent_gas_used = self.parent(block).gas_used
transactions = self.transactions(block)
親ブロックの基本手数料とガス使用量、トランザクションデータを取得しています。
ガス使用量(gas_used
)とは、ETHの送金やコントラクトの処理実行時に消費されるガスのことで、今回取得しているのはブロック内の全てのトランザクションで使用されたガス使用量の合計値である、ブロックガス使用量です。
ガス使用量のチェック
# check if the block used too much gas
assert block.gas_used <= block.gas_limit, 'invalid block: too much gas used'
ブロックガス使用量が、新しいブロックのガスリミットを超えていたらエラーを返しています。
ガスリミットのチェック
# check if the block changed the gas limit too much
assert block.gas_limit < parent_gas_limit + parent_gas_limit // 1024, 'invalid block: gas limit increased too much'
assert block.gas_limit > parent_gas_limit - parent_gas_limit // 1024, 'invalid block: gas limit decreased too much'
ここでは、新しいブロックのガスリミットが親ブロックのガスリミットの1/1024
以上の増加、または減少しかしていないことをチェックしています。
これにより、ガスリミットの急激な変動を防いでいます。
ガスリミットの最低値チェック
# check if the gas limit is at least the minimum gas limit
assert block.gas_limit >= 5000
最低でも新しいブロックのガスリミットは5000
以上である必要があります。
基本手数料の予測値
INITIAL_BASE_FEE = 1000000000
BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
# check if the base fee is correct
if INITIAL_FORK_BLOCK_NUMBER == block.number:
expected_base_fee_per_gas = INITIAL_BASE_FEE
elif parent_gas_used == parent_gas_target:
expected_base_fee_per_gas = parent_base_fee_per_gas
elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_gas_target
base_fee_per_gas_delta = max(parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR, 1)
expected_base_fee_per_gas = parent_base_fee_per_gas + base_fee_per_gas_delta
else:
gas_used_delta = parent_gas_target - parent_gas_used
base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
expected_base_fee_per_gas = parent_base_fee_per_gas - base_fee_per_gas_delta
assert expected_base_fee_per_gas == block.base_fee_per_gas, 'invalid block: base fee not correct'
ここでは、基本手数料についてチェックをしています。
-
INITIAL_FORK_BLOCK_NUMBER == block.number
- EIP1559が導入された最初のブロックの場合、基本手数料は固定値である
INITIAL_BASE_FEE
(1000000000
)が設定されています。
- EIP1559が導入された最初のブロックの場合、基本手数料は固定値である
-
parent_gas_used == parent_gas_target
- 親ブロックのガス使用量がガスターゲットと同じである場合、基本手数料は親ブロックの時と同じになります。
-
parent_gas_used > parent_gas_target
- 親ブロックのガス使用量が親ブロックで設定されていたガスターゲットを超えている場合、以下の計算を行い予測される新しいブロックの基本手数料の増加量を計算しています。
- 親ブロックの基本手数料(
parent_base_fee_per_gas
)と親ブロックのガス使用量とガスターゲットの差(gas_used_delta
)を掛け合わせます。 - 掛け合わせた値に親ブロックのガスターゲット(
parent_gas_target
)で割り、さらにBASE_FEE_MAX_CHANGE_DENOMINATOR
(8
)という値で割った値と1
と比較して大きい方を取得します。 - 最後に計算した値を親ブロックの基本手数料に足し合わせて、新しいブロックの基本手数料を算出しています。
BASE_FEE_MAX_CHANGE_DENOMINATOR
で割っているのは、は基本手数料の変動を制限するです。
具体例
-
parent_base_fee_per_gas
100 gwei
-
gas_used_delta
2,000,000 gas
-
parent_gas_target
10,000,000 gas
-
BASE_FEE_MAX_CHANGE_DENOMINATOR
8
-
``parent_base_fee_per_gas * gas_used_delta`
100 gwei * 2,000,000 gas = 200,000,000 gwei
// parent_gas_target
200,000,000 gwei // 10,000,000 gas = 20 gwei
// BASE_FEE_MAX_CHANGE_DENOMINATOR
20 gwei // 8 = 2.5 gwei(整数除算では2 gwei)
- `max(..., 1)
max(2 gwei, 1 gwei) = 2 gwei
-
else
- 先ほどとは逆で、親ブロックのガス使用量が親ブロック設定されていたガスターゲットを下回っている場合に、新しいブロックの基本手数料の減少量を計算しています。
- 計算式は先ほどの計算と同じで最後に部分だけ異なり、計算した値と親ブロックの基本手数料の差分を取得しています。
-
assert expected_base_fee_per_gas == block.base_fee_per_gas
- 最後に先ほど算出した基本手数料の予測値と新しいブロックに設定されている基本手数料が同じかをチェックしています。
基本手数料については、以下の図でよりわかりやすくイメージできると思います。
トランザクションチェック
cumulative_transaction_gas_used = 0
for unnormalized_transaction in transactions:
ブロック内のトランザクションを1つずつチェックしています。
signer_address = self.validate_and_recover_signer_address(unnormalized_transaction)
トランザクションの署名とチェーンIDを検証し、署名者のアドレスを取得しています。
transaction = self.normalize_transaction(unnormalized_transaction, signer_address)
トランザクションを正規化し、署名やチェーンIDなどの不要な情報を除去しています。
詳しい処理
def normalize_transaction(self, transaction: Transaction, signer_address: int) -> NormalizedTransaction:
# legacy transactions
if isinstance(transaction, TransactionLegacy):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.signer_nonce,
gas_limit = transaction.gas_limit,
max_priority_fee_per_gas = transaction.gas_price,
max_fee_per_gas = transaction.gas_price,
destination = transaction.destination,
amount = transaction.amount,
payload = transaction.payload,
access_list = [],
)
# 2930 transactions
elif isinstance(transaction, Transaction2930Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.gas_price,
max_fee_per_gas = transaction.payload.gas_price,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
# 1559 transactions
elif isinstance(transaction, Transaction1559Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.max_priority_fee_per_gas,
max_fee_per_gas = transaction.payload.max_fee_per_gas,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
else:
raise Exception('invalid transaction: unexpected number of items')
上記の関数を実行します。
def normalize_transaction(self, transaction: Transaction, signer_address: int) -> NormalizedTransaction:
2つの引数と戻り値が定義されています。
引数
-
transaction
- トランザクションオブジェクト。
- どのタイプのトランザクションかによって処理が異なります。
-
signer_address
- トランザクションの署名者のアドレス。
戻り値
-
NormalizedTransaction
- 正規化されたトランザクションオブジェクト。
if isinstance(transaction, TransactionLegacy):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.signer_nonce,
gas_limit = transaction.gas_limit,
max_priority_fee_per_gas = transaction.gas_price,
max_fee_per_gas = transaction.gas_price,
destination = transaction.destination,
amount = transaction.amount,
payload = transaction.payload,
access_list = [],
)
トランザクションがレガシートランザクションタイプ(EIP1559導入以前のトランザクションタイプ)である場合、新しく導入されたガス価格設定メカニズムの処理が実行できるように、以下の点でトランザクションの中身を調整しています。
-
max_priority_fee_per_gas
とmax_fee_per_gas
は同じgas_price
を使用します。 - レガシートランザクションには
access_list
が存在しないため、空のリストを設定します。
elif isinstance(transaction, Transaction2930Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.gas_price,
max_fee_per_gas = transaction.payload.gas_price,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
先ほど同様、Transaction2930Envelope
タイプの場合のトランザクションを処理します。
-
max_priority_fee_per_gas
とmax_fee_per_gas
は同じgas_price
を使用します。 -
access_list
はEIP2930トランザクションのペイロードから取得されます。
elif isinstance(transaction, Transaction1559Envelope):
return NormalizedTransaction(
signer_address = signer_address,
signer_nonce = transaction.payload.signer_nonce,
gas_limit = transaction.payload.gas_limit,
max_priority_fee_per_gas = transaction.payload.max_priority_fee_per_gas,
max_fee_per_gas = transaction.payload.max_fee_per_gas,
destination = transaction.payload.destination,
amount = transaction.payload.amount,
payload = transaction.payload.payload,
access_list = transaction.payload.access_list,
)
最後に、EIP1559タイプの場合のトランザクションを処理します。
-
max_priority_fee_per_gas
とmax_fee_per_gas
はそれぞれ異なるフィールドから取得されます。 -
access_list
はEIP1559トランザクションのペイロードから取得されます。
else:
raise Exception('invalid transaction: unexpected number of items')
3つのいずれにも該当しないトランザクションの場合はエラーを返します。
signer = self.account(signer_address)
署名者のアカウント情報を取得しています。
signer.balance -= transaction.amount
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover attached value'
署名者の残高からトランザクションで使用される金額があれば引き落とし、残高のチェックをします。
assert signer.balance >= transaction.gas_limit * transaction.max_fee_per_gas
署名者の残高がトランザクションのガスリミットと最大手数料をカバーできるか確認しています。
assert transaction.max_fee_per_gas >= block.base_fee_per_gas
トランザクションの最大手数料がブロックの基本手数料以上であることを確認しています。
トランザクションの最大手数料は、署名者が許与できる手数料の最大値です。
そのため、基本手数料を下回っていた場合はブロックに含めることができません。
assert transaction.max_fee_per_gas < 2**256
assert transaction.max_priority_fee_per_gas < 2**256
assert transaction.max_fee_per_gas >= transaction.max_priority_fee_per_gas
トランザクションの手数料がめちゃくちゃ大きくないか確認しています。
また、優先手数料(マイナー、バリデーターに支払われる報酬)がトランザクションの最大手数料を超えていないかチェックしています。
priority_fee_per_gas = min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)
effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas
優先手数料を計算し、実際のガス価格(基本手数料 + 優先手数料)を計算しています。
signer.balance -= transaction.gas_limit * effective_gas_price
assert signer.balance >= 0, 'invalid transaction: signer does not have enough ETH to cover gas'
トランザクションのガスリミットとガスを掛け合わせた値を署名者の残高から引き、支払う余力があるかチェックしています。
gas_used = self.execute_transaction(transaction, effective_gas_price)
トランザクションを実行し、実際に使用されたガス量を取得しています。
gas_refund = transaction.gas_limit - gas_used
cumulative_transaction_gas_used += gas_used
signer.balance += gas_refund * effective_gas_price
使用されなかったガスを署名者に払い戻しています。
self.account(block.author).balance += gas_used * priority_fee_per_gas
使用されたガスのうち、優先手数料をマイナー(バリデーター)に支払っています。
# check if the block spent too much gas transactions
assert cumulative_transaction_gas_used == block.gas_used, 'invalid block: gas_used does not equal total gas used in all transactions'
ブロック内の全てのトランザクションで使用されたガスの合計が、新しいブロック内で設定されているガス使用量の値と一致するか確認しています。
互換性
レガシートランザクションの場合、引き続きブロックに含まれて処理されますが、新しいガス価格決定アルゴリズムに沿って処理されます。
ブロックハッシュの変更
ブロックハッシュを計算するためにkeccak256
に渡されるデータ構造が変更されるため、ブロック検証などにブロックハッシュを使用しているアプリケーションは、新しいデータ構造に対応する必要があります。
GASPRICEの変更
これまでGASPRICE
は、トランザクションごとに署名した人がガス単位あたり支払うETHと、マイナーが受け取るETHの両方を返していました。
しかし、EIP1559の導入により、ガス単位あたり支払うETHのみを返すように変更されました。
セキュリティ
ブロックサイズの増加/複雑性の増加
EIP-1559では最大ブロックサイズが増加しますが、マイナーがブロックを処理するのに時間がかかりすぎると、空のブロックをマイニングする可能性があります。
そのため、マイナーは最大サイズのブロックを処理できるように、ハードウェアマシーンを調整する必要があります。
トランザクションの順番
優先手数料での競争の余地がすくなくなり、トランザクションの順番がクライアントの内部実装に依存するようになります。
マイナーは高い手数料のトランザクションを優先的に処理し、同じ優先手数料を持つトランザクションを受け取った時間に基づいてソートすることが推奨されます。
マイナーが空のブロックをマイニングする可能性
基本手数料がめちゃくちゃ低くなるまで空のブロックをマイニングし、その後にマイニングを行う可能性があります。
マイナーが分散されている限り問題はありません。
ETHのBurnによる供給量の変動
基本手数料のBurnはETHの供給量が変動し、マイニング報酬で生成される量によってインフレ、デフレ傾向になるが、これは予測できずETHの供給量をコントロールできなくなります。
引用
Vitalik Buterin (@vbuterin), Eric Conner (@econoar), Rick Dudley (@AFDudley), Matthew Slipper (@mslipper), Ian Norden (@i-norden), Abdelhamid Bakhta (@abdelhamidbakhta), "EIP-1559: Fee market change for ETH 1.0 chain," Ethereum Improvement Proposals, no. 1559, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1559.
最後に
今回は「」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!