Motivation

Ethereum historically priced transaction fees using a simple auction mechanism:

  1. Users send transactions with bids (“gasprices”);
  2. Miners choose transactions with the highest bids;
  3. 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.
  • 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 the effective_gas_price.
  • As of FORK_BLOCK_NUMBER, a new EIP-2718 transaction is introduced with TransactionType 2.
    • The EIP-2718 TransactionPayload for this transaction is 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]).
      • The signature_y_parity, signature_r, signature_s elements of this transaction represent a secp256k1 signature over keccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list])).
    • The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).

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 of patriciaTrie(rlp(Index) => Transaction) where:
    • Index is the index in the block of this transaction
    • Transaction is either TransactionType || TransactionPayload or LegacyTransaction
      • || is the byte/byte-array concatenation operator.
      • LegacyTransaction is rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
      • TransactionType is a positive unsigned 8-bit number between 0 and 0x7f that represents the type of the transaction
      • TransactionPayload is an opaque byte array whose interpretation is dependent on the TransactionType 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 of patriciaTrie(rlp(Index) => Receipt) where:
    • Index is the index in the block of the transaction this receipt is for
    • Receipt is either TransactionType || ReceiptPayload or LegacyReceipt
      • TransactionType is a positive unsigned 8-bit number between 0 and 0x7f that represents the type of the transaction
        • The TransactionType of the receipt MUST match the TransactionType of the transaction with a matching Index.
      • ReceiptPayload is an opaque byte array whose interpretation is dependent on the TransactionType and defined in future EIPs
      • LegacyReceipt is rlp([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.

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