这篇文章介绍了Ethereum中的一种新交易定价机制,即EIP-2718。该机制通过设置基本费用并根据网络拥堵情况动态调整,从而提高交易的效率和稳定性。文章详细说明了新交易类型的结构以及如何计算费用,解决了交易费用波动和排队等问题。
一种交易定价机制,包括每个区块的固定网络费用,这些费用被销毁,并且能够动态扩展/收缩区块大小以处理瞬时拥堵。
我们引入了一种新的 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 的最高费用,旨在激励矿工将其交易纳入(即:优先费用)。 交易还指定它们愿意支付的总最高费用(即:max fee),这涵盖优先费用和区块的每单位 gas 网络费用(即:基础费用)。 发送者将始终支付其交易被纳入的区块的每单位 gas 基础费用,并且他们将支付交易中设定的每单位 gas 优先费用,只要这两项费用的总和不超过交易的最高费用。
以太坊历史上使用简单的拍卖机制定价交易费用,用户以出价(“gasprices”)发送交易,矿工选择出价最高的交易,而被包括的交易支付他们指定的出价。这导致了几大低效源:
本 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 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
元素表示对 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 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, '无效区块:使用的 gas 太多'
# 检查块是否过多改变了 gas 限制
assert block.gas_limit < parent_gas_limit + parent_gas_limit // 1024, '无效区块:gas 限制增加太多'
assert block.gas_limit > parent_gas_limit - parent_gas_limit // 1024, '无效区块:gas 限制减少太多'
# 检查 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, '无效区块:基础费用不正确'
# 执行交易并做 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, '无效交易:签署人没有足够的 ETH 来覆盖附加价值'
# 签署人必须能够承担交易
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, '无效交易:签署人没有足够的 ETH 来覆盖 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, '无效区块:gas_used 不等于所有交易总共使用的 gas'
# 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('无效交易:意外的项目数量')
@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 倍),这将要求他们在攻击在理论上可以克服叛逃者之前需要拥有更多的哈希功率。
通过销毁基础费用,我们无法再保证固定的 Ether 供应。这可能导致经济不稳定,因为 ETH 的长期供应将不再随时间而恒定。虽然这是一个有效的担忧,但要准确量化这一影响很困难。如果基础费用烧毁的数量超过矿业奖励生成的数量,ETH 将会出现贬值;如果矿业奖励生成的数量高于烧毁数量,则 ETH 将出现增值。由于我们无法控制用户对区块空间的需求,因此我们不能断言 ETH 最终会是通货膨胀还是通货紧缩,因此这一变更导致核心开发人员在以太长期数量上失去了一些控制权。
- 原文链接: github.com/ethereum/EIPs...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!