0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[EIP1559] Ethereumの新たなガス価格設定メカニズムを理解しよう!

Last updated at Posted at 2024-04-15

はじめに

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

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

今回は、トランザクション料金の不安定性とオークションの非効率性に対処し、ブロックごとに基本料金が調整され、トランザクションごとの料金が自動設定されるようになる仕組みを提案しているEIP1559についてまとめていきます!

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

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

ガス代とは

そもそもガス代についてあまり理解していない方向けに簡単に特徴だけ説明します。

44.png

概要

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のインフレを防ぎつつ、マイナーが故意に手数料を操作するインセンティブをなくしています。

それぞれの用語をまとめたものが以下になります。

45.png

47.png

また、ガスリミットについてもまとめました。

46.png

基本手数料の増減については以下になります。

48.png

49.png

Etherscanで実際にEIP1559のガス代の表示を確認したものが以下になります。

50.png

最初に「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_FEE1000000000)が設定されています。
  • 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_DENOMINATOR8)という値で割った値と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

    • 最後に先ほど算出した基本手数料の予測値と新しいブロックに設定されている基本手数料が同じかをチェックしています。

基本手数料については、以下の図でよりわかりやすくイメージできると思います。

49.png

トランザクションチェック

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_gasmax_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_gasmax_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_gasmax_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などからお気軽に質問してください!

Twitter @cardene777

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?