Alert Source Discuss
Standards Track: Core

EIP-1559: ETH 1.0 链的费用市场变更

Authors Vitalik Buterin (@vbuterin), Eric Conner (@econoar), Rick Dudley (@AFDudley), Matthew Slipper (@mslipper), Ian Norden (@i-norden), Abdelhamid Bakhta (@abdelhamidbakhta)
Created 2019-04-13
Requires EIP-2718, EIP-2930

简单总结

一种交易定价机制,包括固定区块网络费用(会被销毁),并动态扩展/收缩区块大小以应对瞬时拥塞。

摘要

我们引入了一种新的 EIP-2718 交易类型,格式为 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])

协议中存在一个基础 gas 费用,该费用可以根据公式在每个区块中上下浮动,该公式是父区块中使用的 gas 和父区块的 gas 目标(区块 gas 限制除以弹性乘数)的函数。 该算法导致当区块高于 gas 目标时,每 gas 的基础费用增加,而当区块低于 gas 目标时,每 gas 的基础费用减少。 每 gas 的基础费用会被销毁。 交易指定他们愿意支付给矿工的最大每 gas 费用,以激励他们包括该交易(又名:优先级费用)。 交易还指定他们愿意支付的总的最大每 gas 费用(又名:最大费用),其中包括优先级费用和区块的网络Gas费每gas (又名:基础费用)。 发送者将始终支付其交易被包含的区块的每 gas 基础费用,并且他们将支付交易中设置的每 gas 优先级费用,只要两个费用的总金额不超过交易的最大每 gas 费用。

动机

以太坊历史上使用一种简单的拍卖机制来定价交易费用,用户发送带有出价(“gasprice”)的交易,矿工选择出价最高的交易,并且包含的交易支付他们指定的出价。这导致了几个大的效率低下来源:

  • 交易费用波动性与交易社会成本之间的不匹配:在成熟的公共区块链上包含交易的出价,这些区块链具有足够的使用量以至于区块已满,往往会非常不稳定。认为网络因接受一个更多交易到区块中而产生的成本,当每 gas 的成本为 10 nanoeth 时,实际上是 1 nanoeth 时的 10 倍,这很荒谬;在这两种情况下,都是 800 万 gas 和 802 万 gas 之间的差异。
  • 用户不必要的延迟:由于每个区块的硬性 gas 限制以及交易量的自然波动,交易通常会等待几个区块才能被包含,但这在社会上是无效的。没有人从缺乏“松弛”机制中获得显着收益,该机制允许一个区块更大,下一个区块更小,以满足区块之间需求的差异。
  • 第一价格拍卖的低效率:当前的方法是,交易发送者发布带有最大费用的出价交易,矿工选择最高支付的交易,并且每个人都支付他们出价的价格。这在机制设计文献中是众所周知的效率很低,因此需要复杂的费用估算算法。但是即使是这些算法通常最终也无法很好地工作,导致频繁的费用超额支付。
  • 没有区块奖励的区块链的不稳定性:从长远来看,目前没有发行的区块链(包括比特币和 Zcash)打算完全通过交易费用来奖励矿工。但是,已知存在许多问题可能导致大量不稳定,激励挖掘“姊妹区块”以窃取交易费用,从而打开更强大的自私挖掘攻击媒介等等。目前没有很好的缓解措施。

该 EIP 中的提案是从一个基础费用金额开始,该金额由协议根据网络的拥塞程度进行上下调整。当网络超过每个区块的 gas 使用目标时,基础费用略有增加;当容量低于目标时,基础费用略有减少。由于这些基础费用变更受到限制,因此区块之间基础费用的最大差异是可以预测的。这使得钱包能够以高度可靠的方式为用户自动设置 gas 费用。预计即使在高网络活动期间,大多数用户也不必手动调整 gas 费用。对于大多数用户来说,基础费用将由他们的钱包估算,并自动设置一个小的优先级费用,以补偿矿工承担的孤儿风险(例如 1 nanoeth)。用户还可以手动设置交易最大费用以限制其总成本。

此费用系统的一个重要方面是矿工只能保留优先级费用。基础费用始终被烧毁(即,它被协议销毁)。这确保了只有 ETH 才能用于支付以太坊上的交易费用,从而巩固了 ETH 在以太坊平台中的经济价值,并降低了与矿工可提取价值 (MEV) 相关的风险。此外,这种销毁抵消了以太坊的通货膨胀,同时仍然为矿工提供了区块奖励和优先级费用。最后,确保区块的矿工没有收到基础费用非常重要,因为它消除了矿工操纵费用以从用户那里提取更多费用的动机。

规范

区块有效性在下面的参考实现中定义。 GASPRICE (0x3a) 操作码 必须 返回如下参考实现中定义的 effective_gas_price

FORK_BLOCK_NUMBER 开始,引入了一种新的 EIP-2718 交易,其 TransactionType 为 2。

新交易的内在成本继承自 EIP-2930,具体为 21000 + 16 * non-zero calldata bytes + 4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access list address count

此交易的 EIP-2718 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])

此交易的 signature_y_parity, signature_r, signature_s 元素表示对 keccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list])) 的 secp256k1 签名。

此交易的 EIP-2718 ReceiptPayloadrlp([status, 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.
		# 在分叉区块上,不要考虑 ELASTICITY_MULTIPLIER,以避免不适当地将 gas 目标减半。
		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
		# 检查区块是否使用了过多的 gas
		assert block.gas_used <= block.gas_limit, 'invalid block: too much gas used'

		# check if the block changed the gas limit too much
		# 检查区块是否过度更改了 gas 限制
		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
		# 检查 gas 限制是否至少为最小 gas 限制
		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
		# 执行交易并进行 gas 记账
		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
			# 注意:这会验证交易签名和链 ID,这必须在我们下面规范化之前发生,因为规范化的交易不包括签名或链 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
			# 签名者获得未使用的 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
		# 检查区块是否在 gas 交易上花费过多
		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: 验证帐户余额与区块的帐户余额匹配(通过状态根比较)
		# TODO: validate the rest of the block
		# TODO:验证区块的其余部分

	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
		# 2930 交易
		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
		# 1559 交易
		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
	# effective_gas_price 是 GASPRICE (0x3a) 操作码返回的值
	@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

向后兼容性

传统的以太坊交易仍然可以工作并包含在区块中,但它们不会直接从新的定价系统中受益。这是因为从传统交易升级到新交易会导致传统交易的 gas_price 完全被 base_fee_per_gaspriority_fee_per_gas 消耗。

区块哈希更改

传递到 keccak256 以计算区块哈希的数据结构正在更改,并且所有正在验证区块有效的应用程序或使用区块哈希来验证区块内容的应用程序都需要进行调整以支持新的数据结构(一个附加项)。如果您只获取区块头字节并对其进行哈希处理,您仍然应该正确地获得哈希值,但是如果您从其组成元素构造区块头,则需要在最后添加新元素。

GASPRICE

在此更改之前,GASPRICE 代表签名者为交易支付的每 gas ETH 以及矿工收到的每 gas ETH。从此次更改开始,GASPRICE 现在仅代表签名者为每 gas 支付的 ETH 数量,而矿工为交易支付的数量不再可以直接在 EVM 中访问。

安全注意事项

增加最大区块大小/复杂性

此 EIP 将增加最大区块大小,如果矿工无法足够快地处理区块,这可能会导致问题,因为它会迫使他们挖掘空区块。随着时间的推移,平均区块大小应与没有此 EIP 的情况大致相同,因此这仅在短期大小突发时才是一个问题。一个或多个客户端可能会错误地处理短期大小突发并出错(例如内存不足或类似情况),并且客户端实现应确保其客户端可以适当地处理最大大小的单个区块。

交易排序

由于大多数人没有竞争优先级费用,而是使用基线费用来包含,因此交易排序现在取决于各个客户端的内部实现细节,例如它们如何将交易存储在内存中。建议具有相同优先级费用的交易按收到交易的时间排序,以保护网络免受垃圾邮件攻击,攻击者将一堆交易扔到待处理池中,以确保至少有一个交易落在有利的位置。纯粹从自私的挖掘角度来看,矿工仍然应该更喜欢 gas 溢价较高的交易,而不是 gas 溢价较低的交易。

矿工挖掘空区块

矿工有可能挖掘空区块,直到基础费用非常低,然后继续挖掘半满区块并恢复按优先级费用对交易进行排序。虽然这种攻击是可能的,但只要挖掘是分散的,它就不是一个特别稳定的平衡。只要攻击持续存在(即使在基础费用达到 0 之后),任何背离此策略的人都将比参与攻击的矿工更有利可图。由于任何矿工都可以匿名背离卡特尔,并且无法证明特定矿工背离了,因此执行此攻击的唯一可行方法是控制 50% 或更多的哈希算力。如果攻击者拥有恰好 50% 的哈希算力,他们将不会从优先级费用中获得任何以太币,而背离者将从优先级费用中获得双倍的以太币。对于攻击者来说,要获得利润,他们需要拥有超过 50% 哈希算力的某些数量,这意味着他们可以改为执行双重支出攻击或仅忽略任何其他矿工,这是一种更有利可图的策略。

如果矿工试图执行此攻击,我们可以简单地提高弹性乘数(目前为 2 倍),这要求他们具有更高的可用哈希算力,然后攻击才能在理论上对背离者有利可图。

ETH 燃烧排除了固定供应

通过燃烧基础费用,我们不再能保证固定的以太币供应。这可能会导致经济不稳定,因为 ETH 的长期供应量将不再随时间推移而保持不变。虽然这是一个有效的担忧,但很难量化这将产生多大的影响。如果燃烧的基础费用多于在挖掘奖励中产生的,那么 ETH 将是通货紧缩的;如果在挖掘奖励中产生的多于燃烧的,那么 ETH 将是通货膨胀的。由于我们无法控制用户对区块空间的需求,因此我们目前无法断言 ETH 最终会是通货膨胀还是通货紧缩,因此这种变化导致核心开发人员失去对以太币长期数量的控制权。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), Eric Conner (@econoar), Rick Dudley (@AFDudley), Matthew Slipper (@mslipper), Ian Norden (@i-norden), Abdelhamid Bakhta (@abdelhamidbakhta), "EIP-1559: ETH 1.0 链的费用市场变更," Ethereum Improvement Proposals, no. 1559, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1559.