Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-7706: calldata 的独立 gas 类型

为 calldata 创建独立的 basefee 和 gaslimit

Authors Vitalik Buterin (@vbuterin)
Created 2024-05-13
Discussion Link https://ethereum-magicians.org/t/eip-7706-create-a-separate-basefee-and-gaslimit-for-calldata/19998
Requires EIP-1559, EIP-4844

摘要

为交易 calldata 添加一个新的 gas 类型。添加一个新的交易类型,该类型提供一个 max_basefeepriority_fee 的向量,为执行 gas、blob gas 和 calldata gas 提供值。修改 basefee 调整,以便为这三种 gas 类型使用相同的机制。

动机

反对提高以太坊 gas 限制、降低 calldata 成本或在 PeerDAS 等技术可用之前增加 EIP-4844 blob 数量的一个主要论点是,以太坊区块的理论最大大小已经太大,我们无法承受进一步增加它。然而,这里存在一个效率低下的问题:当前区块的平均大小(不包括 blob)约为 100 kB,而理论最大值为 30,000,000 / 16 = 1,875,000 字节(可以使用零字节创建更大的区块,但实际上,由于 snappy 压缩,零字节占比重的区块将被压缩到小于 187 万字节)。理想情况下,我们应该有一种方法来限制最大值,而不会使 calldata 平均而言 更加稀缺。

这个 EIP 正是这样做的,它采用了与 EIP-4844 中应用于 blob 数据的相同技术:我们为 calldata 引入一个独立的费用市场,具有独立的 basefee 和独立的每区块 gas 限制。区块的理论最大 calldata 大小将大大减少,而基本的经济分析表明,平均而言,calldata 将变得相当便宜。

该 EIP 还引入了一种新的交易类型,其中包括三种类型的 max-basefee 和 priority fee 作为向量,允许相同的代码路径处理所有三种类型的 gas。我们还对 basefee 调整进行了修改,目前执行 gas(在 EIP-1559 中引入)和 blob(在 EIP-4844 中引入)使用不同的机制,现在对所有三种类型的 gas 使用相同的方法。这简化了 basefee 调整规则,并确保新的 EIP-4844 basefee 调整算法的更强的数学特性涵盖了所有三种类型的 gas。

规范

本文档中使用的关键词“MUST”,“MUST NOT”,“REQUIRED”,“SHALL”,“SHALL NOT”,“SHOULD”,“SHOULD NOT”,“RECOMMENDED”,“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释 。

参数

  • FORK_BLKNUM = TBD
  • NEW_TX_TYPE = TBD
  • CALLDATA_GAS_PER_TOKEN = 4
  • TOKENS_PER_NONZERO_BYTE = 4
  • CALLDATA_GAS_LIMIT_RATIO = 4
  • LIMIT_TARGET_RATIOS = [2, 2, 4]
  • MIN_BASE_FEE_PER_GAS = 1 # 重命名自 EIP-4844 MIN_BASE_FEE_PER_BLOB_GAS
  • BASE_FEE_UPDATE_FRACTION = 8 # 大致匹配 EIP-4844 参数

新的交易类型

FORK_BLOCK_NUMBER 开始,引入一种新的 EIP-2718 交易,其 TransactionType = TX_TYPE(NEW_TX_TYPE)

此交易的 EIP-2718 TransactionPayload

[chain_id, nonce, gas_limit, to, value, data, access_list, blob_versioned_hashes, max_fees_per_gas, priority_fees_per_gas, y_parity, r, s]

我们要求 max_fees_per_gaspriority_fees_per_gas 是长度为 3 的向量,每个向量都包含从 02**64-1 的整数。

新交易的固有成本继承自 EIP-4844,只是删除了 calldata gas 成本(每个非零字节 16,每个零字节 4)。

区块处理和交易费用

我们添加函数 get_max_feesget_priority_fees,以计算先前交易类型的这些长度为 3 的向量:

def get_max_fees(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return tx.max_fees_per_gas
    elif tx.type == BLOB_TX_TYPE:
        return [tx.max_fee_per_gas, tx.max_fee_per_blob_gas, tx.max_fee_per_gas]
    elif is_eip_1559(tx.type):
        return [tx.max_fee_per_gas, 0, tx.max_fee_per_gas]
    else:
        return [tx.gasprice, 0, tx.gasprice]
def get_priority_fees(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return tx.priority_fees_per_gas
    elif tx.type == BLOB_TX_TYPE:
        return [tx.max_priority_fee_per_gas, 0, tx.max_priority_fee_per_gas]
    elif is_eip_1559(tx.type):
        return [tx.max_priority_fee_per_gas, 0, tx.max_priority_fee_per_gas]
    else:
        return [tx.gasprice, 0, tx.gasprice]

我们还添加了一些辅助函数:

def all_less_or_equal(v1: [int, int, int], v2: [int, int, int]) -> bool:
    return all(x <= y for x, y in zip(v1, v2))

def vector_add(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x+y for x, y in zip(v1, v2)]

def vector_subtract(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x-y for x, y in zip(v1, v2)]

def vector_subtract_clamp_at_zero(v1: [int, int, int], v2: [int, int, int]) -> [uint, uint, uint]:
    return [x-y if x >= y else 0 for x, y in zip(v1, v2)]

def vector_mul(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x*y for x, y in zip(v1, v2)]
# 与当前 calldata 定价相同的规则,但重新措辞(类似于 EIP-7623)
def get_calldata_gas(calldata: bytes) -> int:
    tokens = calldata.count(0) + (len(calldata) - calldata.count(0)) * TOKENS_PER_NONZERO_BYTE
    return tokens * CALLDATA_GAS_PER_TOKEN
def get_gaslimits(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return [tx.gaslimit, len(tx.blob_versioned_hashes) * GAS_PER_BLOB, get_calldata_gas(tx.data)]
    elif tx.type == BLOB_TX_TYPE:
        return [tx.gaslimit, len(tx.blob_versioned_hashes) * GAS_PER_BLOB, get_calldata_gas(tx.data)]
    elif is_eip_1559(tx.type):
        return [tx.gaslimit, 0, get_calldata_gas(tx.data)]
    else:
        return [tx.gaslimit, 0, get_calldata_gas(tx.data)]
def get_fees_per_gas(tx: Transaction, block_basefees: [int, int, int]) -> [int, int, int]:
    max_fees = get_max_fees(tx)
    priority_fees = get_priority_fees(tx)
    output = []
    # 费用充足性检查,类似于 EIP-1559 和 4844
    require(all_less_or_equal(block_basefees, max_fees))
    # 类似于 EIP-1559 和 4844 的逻辑
    return [
        min(basefee + priority_fee, max_fee)
        for block_basefee, max_fee, priority_fee in zip(block_basefees, max_fees, priority_fees)
    ]
get_block_gaslimits(block: Block) -> [int, int, int]:
    return [block.gaslimit, MAX_BLOB_GAS_PER_BLOCK, block.gaslimit // CALLDATA_GAS_LIMIT_RATIO]

在区块处理开始时

  • 我们初始化一个向量 gas_used_so_far[0, 0, 0]

在处理交易开始时

  • 计算 fees_per_gas = get_fees_per_gas(tx, get_block_basefees(block))tx_gaslimits = get_gaslimits(tx)
  • 检查 all_less_or_equal(vector_add(gas_used_so_far, tx_gaslimits), block.gas_limits)
  • tx.origin 帐户中扣除 sum(vector_mul(fees_per_gas, tx_gaslimits)) wei

请注意,get_block_basefees(block) 尚未定义,我们将在下面的部分中定义它。 block.gas_limits 字段也在下面的部分中定义。

在处理完交易之后

  • tx_gas_consumed 计算为三项向量,其中第一项是交易执行实际消耗的 gas 量,第二项和第三项与 get_gaslimits(tx) 中的值匹配
  • sum(vector_mul(fees_per_gas, vector_sub(tx_gaslimits, tx_gas_consumed))) 退还给 tx.origin 账户(实际上,目前只有第一项将为非零)
  • 设置 gas_used_so_far = vector_add(gas_used_so_far, tx_gas_consumed)

在处理完一个区块之后

  • 要求 block.gas_used = gas_used_so_far

block.gas_used 字段将在下面的部分中定义。

区块结构:

我们更新 BlockHeader 字段,以删除 blob_gas_usedgas_usedbase_fee_per_gasgas_limitexcess_blob_gas 字段,并添加新字段,所有字段均为 [int, int, int] 类型:gas_limitsgas_usedexcess_gas。 生成的 RLP 编码变为:

rlp([
    parent_hash,
    0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, # ommers 哈希
    coinbase,
    state_root,
    txs_root,
    receipts_root,
    logs_bloom,
    0, # 难度
    number,
    timestamp,
    extradata,
    prev_randao,
    0x0000000000000000, # nonce
    withdrawals_root,
    gas_limits,
    gas_used,
    excess_gas
])

我们定义:

get_block_gas_targets(parent: Header) -> [int, int, int]:
    return [limit // target_ratio for limit, target_ratio in zip(parent.gas_limits, LIMIT_TARGET_RATIOS)]

我们按如下方式计算所需的 excess_gas 值:

def calc_excess_gas(parent: Header) -> [int, int, int]:
    return vector_subtract_clamp_at_zero(vector_add(parent_excess, parent_used), get_block_gas_targets(parent))

我们按如下方式计算所需的 gas_limits

  • gas_limits[0] 必须遵循现有的调整公式
  • gas_limits[1] 必须等于 MAX_BLOB_GAS_PER_BLOCK
  • gas_limits[2] 必须等于 gas_limits[0] // CALLDATA_GAS_LIMIT_RATIO

现在,我们定义 get_block_basefees:

def get_block_basefees(parent: Header) -> [int, int, int]:
    return [
        fake_exponential(
            MIN_BASE_FEE_PER_GAS,
            excess_gas,
            target * BASE_FEE_UPDATE_FRACTION
        )
        for (excess_gas, target) in zip(parent.excess_gas, get_block_gas_targets(parent))
    ]

理由

将所有 gas 相关机制转换为向量

这使得用于处理 gas 的相同逻辑可以处理所有三种类型的 gas。 因此,可以说这是对协议 gas 处理逻辑的净简化,尽管 gas 类型的总数从 2 增加到 3

目标比率

执行 gas 和 blob 的目标比率设置为 2; calldata 的目标比率设置为 4。这大大减少了 calldata 实际达到限制的情况的数量,这减轻了 EIP 的经济影响,因为在“低于限制”条件下分析 EIP-1559 风格的费用市场比在“达到限制”条件下简单得多。此外,它降低了需要大量 calldata 的应用程序完全停止工作的风险。

当前的参数将每个区块的目标 calldata 设置为 187,500 字节,约为当前平均值的 2 倍。 使用基本的供需推理,这意味着由于此 EIP,calldata 可能会变得更加便宜。

向后兼容性

先前的交易类型将 calldata basefee 和 priority fee 设置为彼此相等。 calldata gas 成本有意设置为与今天相同,并且 gas 目标与当前使用情况相似,因此将两个费用设置为彼此相等是对最佳行为的合理近似。 实际上,新的交易类型会更好,因此我们希望用户随着时间的推移切换到它。 但是,旧式交易用户遭受的损失不会那么高,因为与 basefee 相比,priority fee 通常很小,并且用户支付的金额与 basefee 成正比。

安全考虑

由于此 EIP,最佳区块构建行为变得更加复杂,尤其是在区块已满一个或两个类型的 gas 的边界条件下。 我们认为这种影响不会太大,因为实际上超过 90% 的区块未满,并且天真的“贪婪算法”可以获得接近最佳的结果。 因此,专有区块构建算法的中心化风险可能远小于 eg. MEV 提取的其他现有风险。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), "EIP-7706: calldata 的独立 gas 类型 [DRAFT]," Ethereum Improvement Proposals, no. 7706, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7706.