EIP1559 - ETH 1.0 链的手续费市场改革

# 概要

一种交易定价机制,其中包括每个区块网络费用的固定部分,同时还有动态的可伸缩区块大小设计,以应付短暂的网络拥堵。

# 摘要

我们引入一种新的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上限 / Elasticity_MULTIPLIER) 进行计算。 当区块超过gas目标值,算法就会导致gas的基础费用增加。反之,基础费用就减少。 每个gas的基础费用被销毁。 交易指定了用户愿意向矿工支付的每个gas的最高费用,以鼓励矿工打包他们的交易 (又称:小费) 交易还规定了用户愿意支付的每个gas的最高费用 (又称:最高费用),它包含小费和区块的每个gas网络费用 (又称:基础费用)。 交易总是会支付打包区块的基础费用,也会支付小费,只要两者之和不超过最高费用。

# 动机

以往的以太坊定价机制是采用简单的拍卖机制,由用户出价 ("gasprices") ,矿工选择出价最高的交易,被打包进区块的交易支付其指定的出价。 这导致效率低下的几个主要原因:

  • 交易费用水平的波动和交易的社会成本之间不匹配: 成熟的公链比较繁忙,因此区块都是满的,交易的出价往往极不稳定。 荒谬的是,当gas为10 nanoeth时,网络多打包1个交易到区块的开销,是gas为1 nanoeth时的10倍。在这两种情况下,实际只是800万gas和802百万gas的区别。
  • 造成用户不必要的延迟等待: 因为每个区块的gas上限的限制和交易量的自然波动,这会导致交易经常需要等待几个区块才能被打包,社会效率低下。现在没有一个机制能够让区块的大小可以动态变化,以满足每个区块需求差异。
  • 第一次拍卖的效率低下: 当前处理方法中,用户按照最高费用出价,而矿工选择最高出价的交易,每个人出价他们的支付。 众所周知,这种机制设计非常低效,因此需要复杂的费用估算算法。 但即使有这些算法,也不太可能准确估算,导致经常多付费用。
  • 当区块奖励消失后,区块链会变得不稳定: 从长远来看,区块奖励会消失 (包括比特币和Zcash),目前打算切换到完全通过交易费奖励矿工。 然而,这方面存在一些已知问题,可能会导致许多不稳定,鼓励矿工开采"姐妹区块"以偷窃交易费用,开启更强的自私挖矿攻击向量,等等。 目前没有很好的缓解措施。

本EIP中建议引入基础费用的概念,它的数额由协议根据网络的拥堵程度进行调整。 当网络超过每个区块gas的目标使用量时,基础费用稍有增加;反之,略有下降。 因为这些基础费用的变化受到限制,所以区块和区块之间基础费用的最大差额是可以预测的。 这使钱包能够以非常可靠的方式自动为用户设置gas费用。 预计大多数用户不必手工调整gas费用,即使在网络活动频繁的时期也是如此。 对大多数用户来说,基础费用将由他们的钱包估算,另外1小笔用于补偿矿工的小费(例如1 nanoeth)将被自动设置。 用户也可以手工设置交易最高费用来约束其总成本。

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

# 规范

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

FORK_BLOCK_NUMBER,一个TransactionType 为2的新EIP-2718交易类型被引入。

新交易的内在成本从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代表了一个secp256k1 签名keccak256(0x02 || rlp([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 # 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248

# 向后兼容

旧的以太坊交易仍然可以工作,并被打包在区块中,但是它们不会直接从新的定价制度中受益。 这是因为升级后,原有交易的 gas_price完全被 base_fee_per_gas 和 priority_fee_per_gas消耗掉。

# 区块哈希更改

传递给keccak256用以计算区块哈希的数据结构正在变化,为了支持这种变化 (额外一项),所有正在验证区块是否有效,或者使用区块哈希用来校验区块内容的应用程序都需要调整。 如果你只是对区块头字节进行哈希计算,你仍然应该得到正确的哈希值。但是你如果从其组成元素中构建一个区块头,你将需要在新的区块结尾添加。

# 矿工费(GASPRICE)

在此次更改之前,GASPRICE 既代表了一个交易中用户用ETH支付的单位gas价格,也代表了矿工单位gas收到的ETH。 在此次更改中,GASPRICE只代表一个交易中用户用ETH支付的单位gas价格,矿工从交易中获取的ETH金额不再能够直接从EVM中获得。

# 测试用例

待完成

# 安全事项

# 增加最大块的大小/复杂度

本EIP将增加最大区块的大小,如果矿工无法以足够快的速度处理区块,可能会造成问题,因为这将迫使他们开采一个空块。 随着时间的推移,平均区块大小应保持与旧的方案大致相同,所以这种区块大小突变只是短期问题。 一个或多个客户端可能处理不好区块大小短暂突变 (例如内存溢出),客户端实现应该确保其客户端能够适当处理最大尺寸的单个区块。

# 交易排序

大多数矿工竞争打包权时,基于基准费用而不是小费,因此交易排序现在取决于个别客户端的内部实现细节,例如他们如何在内存中存储交易。 建议对相同小费的交易,按照收到交易的时间排序,以保护网络免遭滥发攻击。所谓滥发攻击,就是攻击者往待处理池里丢入一堆交易,以确保至少有一个交易处于有利位置。 存粹从自私挖矿的角度来看,矿工仍然应该更多地进行gas溢价高的交易,而不是那些gas溢价低的交易。

# 矿工开采空块

矿工可能开采空区块,直到基础费用非常低,然后继续开采一半满区块,然后恢复按照小费对交易进行排序。 虽然这种攻击是可能的,但是只要挖矿保持去中心化,这种攻击就很难持续。 只要攻击继续进行(即使在基础费用达到0后),这种策略的任何叛逃者都会比参与攻击的矿工更加有利可图。 因为任何矿工都可以匿名叛逃,而且无法证明具体是谁叛逃了,执行这种攻击的唯一可行方法是控制 50% 或更多的哈希算力。 如果攻击者恰好拥有 50% 的算力,他们将无法赚取小费,而叛逃者能赚取两倍的小费。 攻击者如果想获取利润,就需要拥有50%以上的算力,执行双花攻击,或者一种更加有利可图的策略是干脆忽略其他矿工。

如果矿工试图执行这种攻击,我们可以简单地增加ELASTICITY_MULTIPLIER参数(目前是2倍),这就要求他们拥有更多的哈希算力,才能使攻击理论上比叛逃者更加有利可图。

# ETH销毁不再保证固定供应

通过销毁基础费用,ETH将不再保证固定供应。 这可能导致经济不稳定,因为长期的ETH供应将不再稳定。 虽然这是一个合理的担忧,但是很难量化其影响。 如果销毁的基础费用比挖矿奖励的产出多,那么ETH将通货紧缩;反之,ETH将通货膨胀。 由于我们无法控制用户对区块空间的需求,我们目前无法断言ETH最终会出现通货膨胀还是通货紧缩,所以这种变化会导致核心开发人员失去对ETH长期数量的某种控制。

# 版权

通过 CC0 (opens new window) 放弃版权和相关权利。

▲ Powered by Vercel