该文详细介绍了以太坊改进提案EIP-1559,该提案引入了一种新的交易费用机制。此机制包括一个根据网络拥堵程度动态调整并销毁的基础费用(Base Fee),以及一个支付给矿工的优先费用(Priority Fee),同时支持区块大小的动态扩展/收缩以应对瞬时拥堵。该提案旨在提高费用预测性、解决当前拍卖机制的低效问题,并通过销毁部分ETH来对抗通胀。
markdown
一种交易定价机制,其中包括固定每区块销毁的网络费用,并根据瞬时拥堵动态扩展/收缩区块大小。
我们引入了一种新的 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价格")的交易,矿工选择出价最高的交易,被打包的交易支付他们指定的出价。这导致了几个主要的效率低下来源:
本 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 \times \text{non-zero calldata bytes} + 4 \times \text{zero calldata bytes} + 1900 \times \text{access list storage key count} + 2400 \times \text{access list address count}$$
此交易的 EIP-2718 TransactionPayload 为 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])。
此交易的 signature_y_parity, signature_r, signature_s 元素表示对以下内容的 secp256k1 签名:
$$\text{keccak256}(0x02 \ || \ \text{rlp}([\text{chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list}]))$$
此交易的 EIP-2718 ReceiptPayload 为 rlp([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 # 注意 gas_limit 是 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 # 待定
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
# 在分叉区块上,不考虑 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)
# 检查区块是否使用了过多的 gas
assert block.gas_used <= block.gas_limit, 'invalid block: too much gas used'
# 检查区块是否过度改变了 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'
# 检查 gas 限制是否至少达到最小 gas 限制
assert block.gas_limit >= 5000
# 检查基础费用是否正确
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'
# 执行交易并进行 gas 核算
cumulative_transaction_gas_used = 0
for unnormalized_transaction in transactions:
# 注意:这将验证交易签名和链 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'
# 签名者必须能够支付交易
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
# 签名者获得未使用的 gas 退款
signer.balance += gas_refund * effective_gas_price
# 矿工只收取优先费;注意基础费不给任何人(它被销毁了)
self.account(block.author).balance += gas_used * priority_fee_per_gas
# 检查区块交易是否消耗了过多的 gas
assert cumulative_transaction_gas_used == block.gas_used, 'invalid block: gas_used does not equal total gas used in all transactions'
# TODO:验证账户余额是否与区块的账户余额匹配(通过状态根比较)
# TODO:验证区块的其余部分
def normalize_transaction(self, transaction: Transaction, signer_address: int) -> 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 = [],
)
# 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 交易
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 是 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_gas 和 priority_fee_per_gas 消耗。
传递给 keccak256 以计算区块哈希的数据结构正在改变,所有验证区块是否有效或使用区块哈希验证区块内容的应用都需要进行调整以支持新的数据结构(一个额外的项)。如果你只取区块头字节并对其进行哈希,你仍然应该正确获得哈希,但如果你从其组成元素构建区块头,你需要在末尾添加新的元素。
在此更改之前,GASPRICE 代表签名者每 gas 支付的 ETH 以及矿工每 gas 收到的 ETH。此更改后,GASPRICE 现在仅代表签名者每 gas 支付的 ETH 数量,矿工因交易收到的金额不再可以直接在 EVM 中访问。
这个 EIP 将增加最大区块大小,如果矿工无法足够快地处理区块,可能会导致问题,因为它会迫使他们挖空块。随着时间的推移,平均区块大小应该与没有此 EIP 的情况大致相同,所以这只是短期大小突增的问题。可能有一个或多个客户端在短期大小突增时处理不当并出错(例如内存不足或类似情况),客户端实现应确保其客户端能够适当地处理达到最大大小的单个区块。
由于大多数人不再在优先费上竞争,而是使用基准费用来获得打包,交易排序现在取决于各个客户端的内部实现细节,例如它们如何在内存中存储交易。建议将具有相同优先费用的交易按收到交易的时间排序,以保护网络免受垃圾邮件攻击,在这种攻击中,攻击者将大量交易投入待处理池,以确保至少一个交易落在有利位置。从自私挖矿的角度来看,矿工仍然应该优先选择 gas 溢价较高的交易,而不是 gas 溢价较低的交易。
矿工有可能一直挖空块,直到基础费用非常低,然后开始挖半满的区块并恢复根据优先费对交易进行排序。虽然这种攻击是可能的,但只要挖矿是去中心化的,它就不是一个特别稳定的均衡。在攻击持续的整个过程中(即使在基础费用达到 0 之后),任何偏离这种策略的叛逃者都将比参与攻击的矿工更有利可图。由于任何矿工都可以匿名脱离卡特尔,并且无法证明特定矿工叛逃,因此执行此攻击唯一可行的方法是控制 50% 或更多的算力。如果攻击者拥有恰好 50% 的算力,他们将无法从优先费中赚取任何以太币,而叛逃者将赚取双倍的以太币优先费。要让攻击者获利,他们需要拥有超过 50% 的算力,这意味着他们可以执行双花攻击或简单地忽略任何其他矿工,这是一种更有利可图的策略。
如果矿工试图执行此攻击,我们可以简单地增加弹性乘数(目前为 2 倍),这要求他们在理论上能够对抗叛逃者获利之前,拥有更多的可用算力。
通过销毁基础费用,我们无法再保证固定的以太币供应量。这可能导致经济不稳定,因为 ETH 的长期供应量将不再随时间保持不变。虽然这是一个合理的问题,但很难量化这将产生多大的影响。如果销毁的基础费用多于挖矿奖励产生的费用,则 ETH 将是通货紧缩的;如果挖矿奖励产生的费用多于销毁的费用,则 ETH 将是通货膨胀的。由于我们无法控制用户对区块空间的需求,我们目前无法确定 ETH 最终是通货膨胀还是通货紧缩,因此这一变化导致核心开发者对以太币的长期数量失去了一些控制。
通过 CC0 放弃版权及相关权利。
- 原文链接: github.com/nerolation/EI...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!