Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7557: 通过公平的成本节约实现区块级预热

通过访问列表对地址和插槽进行区块级预热

Authors Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn)
Created 2023-10-01
Discussion Link https://ethereum-magicians.org/t/eip-7557-block-level-warming/16642

摘要

一种在 accessList 中具有共享项目的多个交易之间,公平分配与访问地址和存储插槽相关的 gas 成本的机制。

动机

EIP-2929: Gas cost increases for state access opcodes 引入了一种新的 gas 成本模型,该模型区分了对帐户和存储插槽的“冷”访问和“热”访问。

然而,每个冷访问的成本都由每个交易单独承担,即使验证者只需要为整个区块获取一次状态对象。

当多个交易在同一区块中访问相同的状态对象时,为这些交易收取的费用并不能准确反映区块构建者和验证者在交易执行期间为区块链状态访问所执行的计算。

EIP-2930: Optional access lists 使交易可以预先指定并预先支付交易计划访问的帐户和存储插槽的费用, 然而,成本仍然由每个交易重复支付,而不是在区块级别支付一次。

随着 EIP-6800: Ethereum state using a unified verkle tree 纳入路线图,预计从以太坊状态读取的成本,特别是合约代码的成本将会增加。

受这种即将到来的变化特别影响的是涉及具有高代码大小的智能合约的交易。

区块中的每个此类交易都将被迫支付加载智能合约字节码的全部“零售”价格。

然而,验证者每个区块只需执行一次从以太坊状态的实际读取,并且随后对已引用值的读取效率会高得多。

如果将见证引入以太坊区块,则多个交易可以重复使用相同的见证。要求每个交易都支付费用,而不管区块的内容如何,都是不公平且效率低下的。

以太坊路线图上可能存在的另一项更改是帐户抽象。此更改将导致很大一部分交易直接由智能合约发起。有理由期望许多这些智能合约帐户依赖于相同的核心钱包实现。如果每个帐户抽象交易都被收取重复加载智能合约帐户代码的全部 gas 成本,则此类交易的价格将过高。

与 EOA 相比,这种情况尤其明显,EOA 的验证逻辑是免费加载和执行的。

在这种情况下,基本 gas 费用将从发送者那里收取并被不必要地烧掉,而区块提议者将享受不合理的过多的优先级 gas 费用收入。

本提案与 EIP-7863 等替代方案的主要区别在于,它在所有参与者(所有交易发送者和区块构建者)之间公平分配 gas 节省。

规范

EIP-2930: Optional access lists 已经引入了解决方案的第一部分。每个交易都可以指定一个 accessed_addressesaccessed_storage_keys 数组,以声明其在交易执行期间读取这些值的意图。

然后,交易的发送者会被预先收取访问此数据的费用,但与未声明的访问相比,可以获得少量折扣。

缺失的组件是一种聚合冷访问的 gas 成本并在参与交易之间重新分配由此产生的节省的机制。

概述

在交易执行期间,所有与存储相关的操作的成本不受影响,并且 EIP-2929 和 EIP-2930 中的所有规则继续适用。

区块头或交易负载没有变化,也不存在接收。

在每个区块的最后一个操作中,与存储访问相关的收集的 gas 成本将在区块中所有交易的发送者之间重新分配。 这会影响所有这些帐户的余额,并且只会影响下一个区块。

此报销的金额与最初支付的访问金额成正比。

参与者交易映射

在区块构建者最终确定区块内容后,它会迭代所有包含的交易,以读取每个受支持交易的 accessList 组件。

然后,区块构建者构造一个包含每个已访问地址和每个已访问插槽的数组,以及一个交易发送者地址数组,这些地址已启动对给定地址或插槽的至少一次访问, 以及为此类访问支付的 priorityFeePerGas

以下是一个 JSON 示例,表示了此类结构,并在下面的伪代码中使用:

[
  {
    "address": "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
    "accessors": [
      {
        "sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
        "priorityFeePerGas": "1000"
      },
      {
        "sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
        "priorityFeePerGas": "2000"
      },
      {
        "sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0",
        "priorityFeePerGas": "1000"
      },
      {
        "sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0",
        "priorityFeePerGas": "2000"
      },
      {
        "sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
        "priorityFeePerGas": "2000"
      },
      {
        "sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
        "priorityFeePerGas": "3000"
      }
    ],
    "slots": [
      {
        "id": "0x0000000000000000000000000000000000000000000000000000000000000003",
        "accessors": [
          {
            "sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
            "priorityFeePerGas": "1000"
          },
          {
            "sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
            "priorityFeePerGas": "2000"
          },
          {
            "sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
            "priorityFeePerGas": "2000"
          },
          {
            "sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
            "priorityFeePerGas": "3000"
          }
        ]
      },
      {
        "id": "0x0000000000000000000000000000000000000000000000000000000000000007",
        "accessors": [
          {
            "sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0",
            "priorityFeePerGas": "1000"
          },
          {
            "sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0",
            "priorityFeePerGas": "2000"
          }
        ]
      }
    ]
  }
]

计算已烧毁的基本费用的报销

考虑到访问地址或插槽所需的计算量相同,而与使用一个地址或插槽的交易数量无关,因此协议仅烧毁冷访问的 gas 成本一次是合理的。 由于同一区块中的所有交易支付完全相同的 baseFeePerGas,因此访问冷项目的单一成本在包含此类访问的所有交易中平均分配,并报销剩余的烧毁的基本费用。

设置冷状态访问的绝对最低成本

如果大量交易都访问相同的地址或插槽,则每次冷访问的成本可能会变得太低,这可能代表潜在的 DoS 攻击媒介。

为了防止这种情况,在访问列表中包含实体的 gas 成本不能低于 MIN_ACCESS_LIST_ENTRY_COSTMIN_ACCESS_LIST_ENTRY_COST 设置为 32 gas

此值等效于在 block_access_list 中包含两个字节条目标识符的 calldata 成本。

计算已收取的优先级费用的报销

每个交易都支付一个单独的 priorityFeePerGas 值,并且重新分配冷访问成本的这部分更加复杂。

我们提出了以下一种公平报销已支付的 priorityFee 的方法:

  1. 验证者仅对每次冷访问支付一次 priorityFee,但根据包含所述冷访问的交易中最高的 priorityFee 支付。
  2. 验证者作为 priorityFee 收取的所有剩余 Ether 将按其对交易的总收益边际贡献比例重新分配回包含相同冷访问的交易的所有发送者。
  3. 区块构建者只需支付一次访问费用,剩余价值将得到报销。 因此,total reimbursement 值是已收取值的总和与区块构建者奖励之间的差值。
  4. 交易的 total gains 由支付给区块构建者的 priorityFee 和支付给所有参与者的 total reimbursement 的总和定义。
  5. 从数学上讲,交易对 total gainsmarginal contribution 定义为 当包含区块中的所有交易时计算的 total gains 总和值, 以及当包含所有交易(除了此特定交易)时计算的 total gains 总和值之间的差值。 实际上,该值始终于 priorityFee * gasCost

    𝑀𝐶𝑖 = 𝑣(𝑆 ∪ {𝑖}) − 𝑣(𝑆)

单个交易访问未被其他交易共享的地址或插槽不会触发报销,因此边际贡献为零。

在区块历史记录中高效存储访问列表

accessList 参数的内容是以太坊历史记录的一部分,并且在实施此更改时必须考虑将此数据保留在区块链中的潜在成本。 目前,没有对 accessList 参数应用任何额外费用,这是因为在 accessList 中包含地址或存储插槽的成本是一个常量值,该值明显高于在动态大小的 calldata 字段中存储 accessList 的潜在成本。

通过区块级预热,进行了一项更改,使交易发送者可以构建包含大型 accessList 的交易,而包含这些交易的成本却很低,并且可以使用它来膨胀区块链的大小。

区块大小的这种潜在膨胀也给区块在 P2P 网络中的传播带来了挑战。

为了最大限度地减少永久存储访问列表的成本,我们建议对 execution_payload 结构进行以下更改:

  1. 添加一个新的 block_access_list 字段。

执行客户端创建一个组合的区块级“访问列表”,其中包含来自区块中所有交易的所有唯一条目。

  1. 所有单独的交易 accessList 字段都会将完整的条目替换为指向 block_access_list 的紧凑的 2 字节长引用,其顺序与它们最初出现的顺序相同。

通过这种方法,访问列表中的共享条目不会导致区块大小的充分膨胀。

无需对 RPC API 进行任何可观察的更改,因为客户端可以实时解包此“压缩”。

报销计算算法的伪代码实现

export function calculateBlockColdAccessReimbursement (
  baseFeePerGas: string,
  accessDetailsMap: AddressAccessDetails[]
): Map<string, Reimbursement> {
  const reimbursements = new Map<string, Reimbursement>()
  for (const accessDetail of accessDetailsMap) {
    calculateItemColdAccessReimbursement(accessDetail.accessors, baseFeePerGas, COLD_ACCOUNT_ACCESS_COST, reimbursements)
    for (const slot of accessDetail.slots) {
      calculateItemColdAccessReimbursement(slot.accessors, baseFeePerGas, COLD_SLOAD_COST, reimbursements)
    }
  }
  return reimbursements
}

function calculateItemColdAccessReimbursement (
  unsortedAccessors: AccessDetails[],
  baseFeePerGas: string,
  accessGasCost: string,
  reimbursements: Map<string, Reimbursement>
): void {
  const sortedAccessDetails = unsortedAccessors.sort((a, b) => { return parseInt(b.priorityFeePerGas) - parseInt(a.priorityFeePerGas) })
  const addressAccessN = sortedAccessDetails.length
  const reimbursementPercent = (addressAccessN - 1) / addressAccessN
  const reimbursementsFromCoinbase = calculatePriorityFeeReimbursements(sortedAccessDetails, accessGasCost)
  for (let i = 0; i < sortedAccessDetails.length; i++) {
    const accessor = sortedAccessDetails[i]
    const reimbursement = reimbursements.get(accessor.sender) ?? { reimbursementFromBurn: 0n, reimbursementFromCoinbase: 0n }
    const adjustedAccessGasCost = Math.max(MIN_ACCESS_LIST_ENTRY_COST, parseInt(accessGasCost) * reimbursementPercent)
    reimbursement.reimbursementFromBurn += BigInt(adjustedAccessGasCost * parseInt(baseFeePerGas))
    reimbursement.reimbursementFromCoinbase += BigInt(reimbursementsFromCoinbase[i])
    reimbursements.set(accessor.sender, reimbursement)
  }
}

export function calculatePriorityFeeReimbursements (sortedAccesses: AccessDetails[], accessGasCost: string) {
  // Validator charge is based on the highest paid priority fee per gas
  // 验证者费用基于每次 gas 支付的最高优先级费用
  const validatorFee = parseInt(sortedAccesses[0].priorityFeePerGas) * parseInt(accessGasCost)

  // Accumulate the sum of all "contributions", at least the top transaction contribution
  // 累积所有“贡献”的总和,至少是顶级交易贡献
  let totalContributions = validatorFee
  // Accumulate cost of gas paid to validator for accessing the same address/slot/chunk
  // 累积支付给验证者以访问相同地址/插槽/块的 gas 成本
  let totalSendersCharged = parseInt(sortedAccesses[0].priorityFeePerGas) * parseInt(accessGasCost)
  // Starting with `1` as element at `0` is explicitly shown here to be used as `validatorFee`
  // 从“1”开始,元素“0”在此处明确显示为用作“validatorFee”
  for (let i = 1; i < sortedAccesses.length; i++) {
    const charge = parseInt(sortedAccesses[i].priorityFeePerGas) * parseInt(accessGasCost)
    totalContributions += charge
    totalSendersCharged += charge
  }

  // Calculate the total amount of ether to be reimbursed for this access
  // 计算为此访问报销的 ether 总量
  const totalReimbursement = totalSendersCharged - validatorFee
  if (totalReimbursement == 0) {
    // possible if only single transaction or if all priority fees are 0
    // 如果只有单个交易或所有优先级费用都为 0,则可能
    return Array(sortedAccesses.length).fill(0)
  }

  // Calculate actual charges and reimbursements
  // 计算实际费用和报销
  const reimbursements = [Math.floor(totalReimbursement * topTransactionContribution / totalContributions)]
  for (let i = 1; i < sortedAccesses.length; i++) {
    const charge = parseInt(sortedAccesses[i].priorityFeePerGas) * parseInt(accessGasCost)
    const calldataCharge = parseInt(sortedAccesses[i].priorityFeePerGas) * MIN_ACCESS_LIST_ENTRY_COST
    const reimbursementToCalldata = charge - calldataCharge
    const reimbursementToContribution = Math.floor(totalReimbursement * charge / totalContributions)
    reimbursements.push(Math.min(reimbursementToCalldata, reimbursementToContribution))
  }
  return reimbursements
}

请注意,鉴于 EIP-1559: Fee market change for ETH 1.0 chain,需要两个累积值 reimbursementFromBurnreimbursementFromCoinbase,以便区分 来自减少的区块 gas 烧毁的 Ether 报销,以及来自减少的区块提议者优先级费用(每次 gas)的奖励。

未来 EIP-6800 gas 改革支持

一旦 EIP-6800 处于活动状态,预计访问冷地址的合约代码的成本将会发生变化。

总成本将由代码组成的 31 字节“块”的数量决定,而不是作为 COLD_ACCOUNT_ACCESS_COST(当前为 2600 gas)的常量值。 每个代码“块”的成本为 CODE_CHUNK_ACCESS_COST(当前为 200 gas)。

对于由 EIP-170 定义的最大合约大小 24576 bytes,访问此合约的成本将从 2600 飙升至 158600 gas。

此更改可能需要调整交易的 accessList 参数,以便交易能够指定将访问哪些代码块。

在这种情况下,更改也会反映在报销函数中,该函数通过添加以下代码进行更新, 以便重新分配在多个交易中访问同一代码块的共享成本:

const reimbursementsFromCoinbase = calculatePriorityFeeReimbursements(sortedCodeChunkAccessDetails, CHUNK_ACCESS_COST)
for (let i = 0; i < sortedAccessDetails.length; i++) {
  const reimbursement = reimbursements.get(accessor.sender)
  reimbursement.reimbursementFromBurn += CHUNK_ACCESS_COST * block.baseFeePerGas * reimbursementPercent
  reimbursement.reimbursementFromCoinbase += reimbursementsFromCoinbase[i]
}

成本重新分配系统操作

EIP-4895: Beacon chain push withdrawals as operations 通过引入 system-level withdrawal operation 的概念来树立先例。

我们建议引入另一个名为 cost redistribution 的系统级操作。 执行负载中的 redistributions 在应用任何用户级交易后进行处理。

区块构建者或验证者准备一个报销信息列表。

对于列表中的每个 redistribution,实现都会增加 reimbursementFromBurn + reimbursementFromCoinbase 量的指定地址的余额。

coinbase 的余额将减少所有 reimbursementFromCoinbase 值的总和。

原理

当前的冷存储 gas 成本是不公平的

动机 部分所述,用户在访问合约代码上花费的 gas 量并不能反映区块构建者或验证者访问此代码的实际成本。

合约代码或存储插槽越受欢迎,每个区块中的交易就应该分担更多的成本。 但是,当前系统会增加用户的成本,而不是分摊成本。

在交易后发布常规 gas 退款是不可能的

存在一个 EVM 指令列表,这些指令会同时触发 gas 收费和 gas 退款。 此类操作的一个显着示例是 EIP-1283: Net gas metering for SSTORE without dirty maps 中定义的 0x55 SSTORE 操作码。 直观地,似乎以相同的方式发布共享冷存储访问的 gas 报销是合理的。

但是,此方法会大大简化区块构建过程。 在这种情况下,在区块末尾包含或排除交易会触发在区块开头包含的交易中的可观察到的效果,这使得找到区块的有效交易集可能在计算上无法解决。

因此,我们建议在区块末尾执行报销,因为它不会更改区块中任何交易的行为。

加权优先级费用报销

对于计算参与者合作结果的公平重新分配的问题,常见的博弈论答案是使用 Shapley 值。

但是,我们认为,提议的 priorityFee 报销分配是足够公平的,同时更易于计算或表达。

向后兼容性

本提案不会对智能合约在其执行期间可以观察到的任何行为进行更改。 此更改的唯一效果是交易发送者的有效 gas 成本降低。

安全注意事项

每次 gas 收费均采用 COLD_ACCOUNT_ACCESS_COSTCOLD_SLOAD_COST 的全部成本完成,因此,此更改不会影响一个区块中存储读取的上限。

根据指定的算法计算报销所需的最大内存和计算量微不足道。

似乎此更改没有任何负面的安全影响。

版权

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

Citation

Please cite this document as:

Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), "EIP-7557: 通过公平的成本节约实现区块级预热 [DRAFT]," Ethereum Improvement Proposals, no. 7557, October 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7557.