Notes on EIP-1559
Motivation
Ethereum historically priced transaction fees using a simple auction mechanism:
- Users send transactions with bids (“gasprices”);
- Miners choose transactions with the highest bids;
- Transactions that get included pay the bid that they specify.
This fee modal leads to some problems:
- Bids to include transactions on mature public blockchains, that have enough usage so that blocks are full, tend to be extremely volatile.
- Because of the hard per-block gas limit coupled with natural volatility in transaction volume, transactions often wait for several blocks before getting included.
- There is no “slack” mechanism that allows one block to be bigger and the next block to be smaller to meet block-by-block differences in demand.
- The current approach, where transaction senders publish a transaction with a bid a maximum fee, miners choose the highest-paying transactions, and everyone pays what they bid. This is well-known in mechanism design literature to be highly inefficient, and so complex fee estimation algorithms are required. But even these algorithms often end up not working very well, leading to frequent fee overpayment.
- In the long run, blockchains where there is no issuance (including Bitcoin and Zcash) at present intend to switch to rewarding miners entirely through transaction fees. However, there are known issues with this that likely leads to a lot of instability, incentivizing mining “sister blocks” (sibling blocks) that steal transaction fees, opening up much stronger selfish mining attack vectors, and more.
- There is at present no good mitigation for this.
EIP-1559:
- Start with a base fee amount which is adjusted up and down by the protocol based on how congested the network is.
- The base fee changes are constrained, and the maximum difference in base fee from block to block is predictable.
- This allows wallets to auto-set the gas fees for users in a highly reliable fashion.
- The base fee changes are constrained, and the maximum difference in base fee from block to block is predictable.
- For most users the base fee will be estimated by their wallet and a small priority fee, which compensates miners taking on orphan risk (e.g. 1 nanoeth), will be automatically set.
- Users can also manually set the transaction max fee to bound their total costs.
- Miners only get to keep the priority fee. The base fee is always burned (i.e. it is destroyed by the protocol).
- This ensures that only ETH can ever be used to pay for transactions on Ethereum, cementing the economic value of ETH within the Ethereum platform and reducing risks associated with miner extractable value (MEV).
- This burn counterbalances Ethereum inflation while still giving the block reward and priority fee to miners.
- It removes miner incentive to manipulate the fee in order to extract more fees from users.
- It is expected that most users will not have to manually adjust gas fees, even in periods of high network activity.
Description
Key points:
- Transactions specify the maximum fee per gas they are willing to give to miners to incentivize them to include their transaction (aka: priority fee per gas).
- Transactions also specify the maximum fee per gas they are willing to pay total (aka: max fee), which covers both the priority fee and the block’s network fee per gas (aka: base fee per gas).
- The transaction will always pay the base fee per gas of the block it was included in, and they will pay the priority fee per gas set in the transaction, as long as the combined amount of the two fees doesn’t exceed the transaction’s maximum fee per gas.
- Base fee:
# // is integer division, round down
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
parent_base_fee_per_gas = self.parent(block).base_fee_per_gas
parent_gas_used = self.parent(block).gas_used
# check if the base fee is correct
if INITIAL_FORK_BLOCK_NUMBER == block.number:
# ...
elif parent_gas_used == parent_gas_target: # 父区块的实际使用的 gas VS 父区块 gas limit 的一半
expected_base_fee_per_gas = parent_base_fee_per_gas
elif parent_gas_used > parent_gas_target: # 父区块实际消耗了超过它的 gas limit 一半的 gas
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
# 和当前区块实际设置的 base_fee_per_gas 进行比较
assert expected_base_fee_per_gas == block.base_fee_per_gas, 'invalid block: base fee not correct'
Specification:
- The
GASPRICE
(0x3a
) opcode MUST return theeffective_gas_price
. - As of
FORK_BLOCK_NUMBER
, a new EIP-2718 transaction is introduced withTransactionType
2.- The EIP-2718
TransactionPayload
for this transaction isrlp([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])
.- The
signature_y_parity
,signature_r
,signature_s
elements of this transaction represent a secp256k1 signature overkeccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list]))
.
- The
- The EIP-2718
ReceiptPayload
for this transaction isrlp([status, cumulative_transaction_gas_used, logs_bloom, logs])
.
- The EIP-2718
Transaction payload evolution:
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
class Transaction2930Payload:
chain_id: int = 0 # new
signer_nonce: int = 0
gas_price: int = 0
gas_limit: int = 0
destination: int = 0
amount: int = 0
payload: bytes = bytes()
# new (access_list)
access_list: List[Tuple[int, List[int]]] = field(default_factory=list)
signature_y_parity: bool = False # changed
signature_r: int = 0
signature_s: int = 0
class Transaction1559Payload:
chain_id: int = 0
signer_nonce: int = 0
max_priority_fee_per_gas: int = 0 # new
max_fee_per_gas: int = 0 # changed
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
EIP-2718 introduces an envelope transaction type:
- As of
FORK_BLOCK_NUMBER
, the transaction root in the block header MUST be the root hash ofpatriciaTrie(rlp(Index) => Transaction)
where:Index
is the index in the block of this transactionTransaction
is eitherTransactionType || TransactionPayload
orLegacyTransaction
||
is the byte/byte-array concatenation operator.LegacyTransaction
isrlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
TransactionType
is a positive unsigned 8-bit number between0
and0x7f
that represents the type of the transactionTransactionPayload
is an opaque byte array whose interpretation is dependent on theTransactionType
and defined in future EIPs- All signatures for future transaction types SHOULD include the
TransactionType
as the first byte of the signed data.- This makes it so we do not have to worry about signatures for one transaction type being used as signatures for a different transaction type.
- As of
FORK_BLOCK_NUMBER
, the receipt root in the block header MUST be the root hash ofpatriciaTrie(rlp(Index) => Receipt)
where:Index
is the index in the block of the transaction this receipt is forReceipt
is eitherTransactionType || ReceiptPayload
orLegacyReceipt
TransactionType
is a positive unsigned 8-bit number between0
and0x7f
that represents the type of the transaction- The
TransactionType
of the receipt MUST match theTransactionType
of the transaction with a matchingIndex
.
- The
ReceiptPayload
is an opaque byte array whose interpretation is dependent on the TransactionType and defined in future EIPsLegacyReceipt
isrlp([status, cumulativeGasUsed, logsBloom, logs])
- Clients can differentiate between the legacy transactions and typed transactions by looking at the first byte.
- If it starts with a value in the range
[0, 0x7f]
then it is a new transaction type. - If it starts with a value in the range
[0xc0, 0xfe]
then it is a legacy transaction type. 0xff
is not realistic for an RLP encoded transaction, so it is reserved for future use as an extension sentinel value.
- If it starts with a value in the range
Transaction fee anatomy:
- Transaction Fee: 0.001328071246182166 Ether ($2.08)
- 0.001328071246182166 == 0.000000020861286893 * 63662
- Gas Price: 0.000000020861286893 Ether (20.861286893 Gwei)
- 0.000000020861286893 == (20.707704025 + 0.153582868) / 10^9
- Gas Limit & Usage by Txn: 500000 | 63662 (12.73%)
- 63662 gas units are used
- Gas Fees: Base: 20.707704025 Gwei | Max: 33.082076956 Gwei | Max Priority: 0.153582868 Gwei
- Base is base fee price
- Max is fee cap price
- Max Priority is miner tip price
- Burnt & Txn Savings Fees: 🔥 Burnt: 0.00131829385363955 Ether ($2.06) 💸 Txn Savings: 0.000777999936990706 Ether ($1.22)
- 0.00131829385363955 == 20.707704025 * 63662 / 10^9
- 0.000777999936990706 == (33.082076956 - 20.707704025 - 0.153582868) * 63662 / 10^9
1 Ether == 10^9 Gwei == 10^18 Wei
References