Alert Source Discuss
Standards Track: Core

EIP-6110: 在链上提供验证者存款

将验证者存款作为添加到执行层区块的存款操作列表提供

Authors Mikhail Kalinin (@mkalinin), Danny Ryan (@djrtwo), Peter Davies (@petertdavies)
Created 2022-12-09
Requires EIP-7685

摘要

将验证者存款附加到执行层区块结构。这会将存款包含和验证的责任转移到执行层,并消除了共识层对存款(或 eth1data)投票的需求。

区块中提供的验证者存款列表是通过解析给定区块中包含的每个存款交易发出的存款合约日志事件来获得的。

动机

验证者存款是权益证明共识机制的核心组成部分。此 EIP 允许在共识层上使用协议内存款处理机制,并消除当前使用的提议者投票机制。这种提议的机制放宽了安全假设,并降低了客户端软件设计的复杂性,从而有助于存款流程的安全性。它还改善了验证者的 UX。

协议内存款处理的优点包括但不限于以下内容:

  • 通过取代提议者投票,显着提高了存款的安全性。使用提议的协议内机制,即使超过 2/3 的权益是对抗性的,也不能说服诚实的在线节点处理虚假存款。
  • 减少了在执行层上提交存款交易与其在共识层上处理之间的时间延迟。也就是说,使用协议内存款处理大约需要 13 分钟,而使用现有机制大约需要 12 小时。
  • 消除了信标区块提议对 JSON-RPC API 数据轮询的依赖性,该轮询受到 JSON-RPC API 实现之间不一致以及 API 调用处理对客户端软件内部状态(例如同步)依赖性导致的故障的影响。
  • 消除维护和分发存款合约快照的要求 (EIP-4881)。
  • 降低了共识层客户端软件在已被证明脆弱的组件上的设计和工程复杂性。

规范

执行层

常量

名称 备注
DEPOSIT_REQUEST_TYPE b'0' 存款操作的 EIP-7685 请求类型字节

配置

名称 备注
DEPOSIT_CONTRACT_ADDRESS 0x00000000219ab540356cbb839cbe05303d7705fa 主网
DEPOSIT_EVENT_SIGNATURE_HASH 0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5  

DEPOSIT_CONTRACT_ADDRESSDEPOSIT_EVENT_SIGNATURE_HASH 参数 必须 包含在客户端软件二进制分发中。

定义

  • FORK_BLOCK – 在此 EIP 激活后区块链中的第一个区块。

存款请求

表示新存款请求的结构包含以下字段:

  1. pubkey: Bytes48
  2. withdrawal_credentials: Bytes32
  3. amount: uint64
  4. signature: Bytes96
  5. index: uint64

存款是一种 EIP-7685 请求,具有以下编码:

request_type = DEPOSIT_REQUEST_TYPE
request_data = get_deposit_request_data(block.receipts)

区块有效性

FORK_BLOCK 开始,区块中累积的每个存款 必须 以它们在日志中出现的顺序出现在 EIP-7685 请求列表中。为了说明:

def parse_deposit_data(deposit_event_data) -> bytes[]:
  """
  Parses deposit data from DepositContract.DepositEvent data
  从 DepositContract.DepositEvent 数据解析存款数据
  """
  pass

def is_valid_deposit_event_data(deposit_event_data: bytes) -> bool:
    """
    Verifies the layout of the DepositEvent. Returns `False` if the layout is unsupported,
    `True` if the layout is of the expected format.
    验证 DepositEvent 的布局。如果不支持该布局,则返回 `False`,
    如果该布局是预期的格式,则返回 `True`。
    """
    if len(deposit_event_data) != 576:
        return False

    pubkey_offset = int.from_bytes(deposit_event_data[0:32], byteorder='big', signed=False)
    withdrawal_credentials_offset = int.from_bytes(deposit_event_data[32:64], byteorder='big', signed=False)
    amount_offset = int.from_bytes(deposit_event_data[64:96], byteorder='big', signed=False)
    signature_offset = int.from_bytes(deposit_event_data[96:128], byteorder='big', signed=False)
    index_offset = int.from_bytes(deposit_event_data[128:160], byteorder='big', signed=False)

    if (
        pubkey_offset != 160
        or withdrawal_credentials_offset != 256
        or amount_offset != 320
        or signature_offset != 384
        or index_offset != 512
    ):
        return False

    # These sizes are the sizes of the relevant data
    # 这些大小是相关数据的大小
    pubkey_size = int.from_bytes(deposit_event_data[pubkey_offset:pubkey_offset+32], byteorder='big', signed=False)
    withdrawal_credentials_size = int.from_bytes(deposit_event_data[withdrawal_credentials_offset:withdrawal_credentials_offset+32], byteorder='big', signed=False)
    amount_size = int.from_bytes(deposit_event_data[amount_offset:amount_offset+32], byteorder='big', signed=False)
    signature_size = int.from_bytes(deposit_event_data[signature_offset:signature_offset+32], byteorder='big', signed=False)
    index_size = int.from_bytes(deposit_event_data[index_offset:index_offset+32], byteorder='big', signed=False)

    return (
        pubkey_size == 48
        and withdrawal_credentials_size == 32
        and amount_size == 8
        and signature_size == 96
        and index_size == 8
    )

def event_data_to_deposit_request(deposit_event_data) -> bytes:
    deposit_data = parse_deposit_data(deposit_event_data)
    pubkey = Bytes48(deposit_data[0])
    withdrawal_credentials = Bytes32(deposit_data[1])
    amount = deposit_data[2]   # 8 bytes uint64 LE
    signature = Bytes96(deposit_data[3])
    index = deposit_data[4]    # 8 bytes uint64 LE

    return pubkey + withdrawal_credentials + amount + signature + index

def get_deposit_request_data(receipts)
    # Retrieve all deposits made in the block
    # 检索在区块中进行的所有存款
    deposit_requests = []
    for receipt in receipts:
        for log in receipt.logs:
            if log.address == DEPOSIT_CONTRACT_ADDRESS:
                if len(log.topics) > 0 and log.topics[0] == DEPOSIT_EVENT_SIGNATURE_HASH:
                    assert is_valid_deposit_event_data(log.data), 'invalid deposit log: unsupported data layout'
                    deposit_request = event_data_to_deposit_request(log.data)
                    deposit_requests.append(deposit_request)

    # Concatenate list of deposit request data
    # 连接存款请求数据列表
    return b''.join(deposit_requests)

共识层

共识层更改可以概括为以下列表:

  1. ExecutionRequests 扩展了一个新的 deposit_requests 字段,以容纳存款请求列表。
  2. BeaconState 附加了 deposit_requests_start_index,用于从之前的存款机制切换到新的存款机制。
  3. 作为过渡逻辑的一部分,添加了一个新的信标区块有效性条件,以约束 Eth1Data 轮询的使用。
  4. 添加了一个新的 process_deposit_request 函数到区块处理例程中,以处理 deposit_requests 处理。
  5. 验证者指南提供了一个在过渡完成后关闭 Eth1Data 轮询的逻辑。

详细的共识层规范可以在以下文档中找到:

验证者索引不变性

由于 Eth1Data 轮询的较大后续距离,在存款处理期间分配的新验证者的索引在区块树的不同分支中保持不变,即共识层客户端使用的现有机制 (pubkey, index) 缓存具有重新组织弹性。新的存款机制打破了这种不变性,共识层客户端将不得不处理验证者索引变得依赖于分支的事实,即具有相同 pubkey 的验证者在不同的区块树分支中可以具有不同的索引。

详细的 分析 表明 process_deposit 函数是 唯一 需要依赖于分支的 (pubkey, index) 缓存的地方。

Eth1Data 轮询弃用

一旦过渡期结束,共识层客户端将能够以一种非协调的方式删除 Eth1Data 轮询机制。当网络达到 state.eth1_deposit_index == state.deposit_requests_start_index 时,过渡期被认为是结束的。

理由

index 字段

存款 index 用于确定性地初始化 BeaconState 中的 deposit_requests_start_index,这可以防止在 Eth1Data 轮询弃用期间两次应用相同的存款。

不限制存款操作列表的大小

该列表是无界的,因为数据复杂性可忽略不计且不存在潜在的 DoS 攻击向量。有关更多详细信息,请参见 安全注意事项

通过 DEPOSIT_CONTRACT_ADDRESSDEPOSIT_EVENT_SIGNATURE_HASH 过滤事件

根据设计,存款智能合约可以在处理存款时发出不同类型的事件。例如,Sepolia 上的存款智能合约除了 DepositEvent 之外还发出 Transfer。因此,过滤掉不相关的事件非常重要。

向后兼容性

此 EIP 对区块结构和区块验证规则集引入了向后不兼容的更改。但是,这些更改都不会破坏与用户活动和体验相关的任何内容。

安全考虑

数据复杂性

在本文档最新更新时,提交的存款总数为 1,899,120,即 348MB 的存款数据。假设存款交易的频率保持不变,则此 EIP 引起的历史链数据复杂性可以估计为每年 84MB,与其他历史数据相比,这可以忽略不计。

在 2020 年 12 月启动信标链后,观察到的提交存款数量的最大峰值是在 2023 年 6 月 1 日。在 24 小时内提交了超过 12,000 笔存款交易,平均每块少于 2 笔存款,或 384 字节的数据。

考虑到以上因素,我们得出结论,此提案引入的数据复杂性可以忽略不计。

DoS 攻击向量

在最便宜的情况下(当所有存储槽都是热的时候,并且只需要修改单个叶子节点),存款合约中的代码运行需要 15,650 gas。批量存款中的某些存款成本更高,但是当分摊到大量存款时,这些成本很小,约为每个存款 ~1,000 gas。根据当前的 gas 定价规则,额外收取 6,900 gas 以进行传递 ETH 的 CALL,这是 gas 定价效率低下的一个案例,并且将来可能会降低。为了未来的健壮性,信标链需要能够在 30M gas 区块中承受 1,916 笔存款(每个存款 15,650 gas)。根据现行规则,30M gas 区块中的限制小于 1,271 笔存款。

执行层

以 1 ETH 作为最小存款金额,每字节存款数据的最低成本为 1 ETH/192 ~ 5,208,333 Gwei。这比事务的 calldata 的字节成本高几个数量级,因此将存款操作添加到区块不会增加执行层的 DoS 攻击面。

共识层

存款处理消耗最多的计算是签名验证。其复杂性受每个区块的最大存款数量的限制,目前在 30M gas 区块中约为 1,271 个。因此,大约是 ~1,271 个签名验证,大约需要 ~1.2 秒的处理时间(没有诸如批量签名验证之类的优化)。攻击者需要花费 1,000 ETH 才能将阻止处理速度降低一秒钟,从长远来看,这是一种不可持续且不可行的攻击。

乐观同步节点可能容易受到更严重的攻击。这样的节点无法验证有效负载中提供的存款列表,这使攻击者可以包含尽可能多的存款(限制允许)。当前,有 8,192 个存款(1.5MB 数据),大约需要 8 秒的处理时间。考虑到攻击者需要使用其加密经济上可行的签名来签署此区块(这需要构建一条备用链并将其提供给同步节点),因此不认为此攻击向量是可行的,因为它不会导致同步过程的显着减速。

乐观同步

乐观同步节点必须依赖于诚实多数假设。也就是说,如果对手足够强大以最终确定存款序列,则同步节点将不得不应用这些存款,而忽略存款请求相对于给定区块执行的有效性。因此,能够最终确定无效链的对手也可以说服诚实节点接受虚假存款。同样适用于当今的执行层世界状态的有效性,并且就此而言,新的存款处理设计在现有安全模型的范围内。

在线节点不会被诱骗到这种情况中,因为它们的执行层会根据区块执行来验证提供的存款。

弱主观性周期

此 EIP 取消了每个 epoch 中的存款数量的硬性限制,并使区块 gas 限制成为此数量的唯一限制。也就是说,每个 epoch 的存款限制从 MAX_DEPOSITS * SLOTS_PER_EPOCH = 512 变为 max_deposits_per_30m_gas_block * SLOTS_PER_EPOCH ~ 32,768 在 30M gas 区块(为简单起见,我们考虑 max_deposits_per_30m_gas_block = 1,024)。

此更改会影响每个 epoch 的补仓数量,这是弱主观性周期计算的输入之一。可以增加自己的验证者,以相对于那些泄露的验证者立即增加其拥有的股份比例。分析 没有表明弱主观性周期大小显着减少。此外,由于这种攻击需要消耗相当一部分股份作为初步措施之一,因此认为这种攻击是不可行的。

版权

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

Citation

Please cite this document as:

Mikhail Kalinin (@mkalinin), Danny Ryan (@djrtwo), Peter Davies (@petertdavies), "EIP-6110: 在链上提供验证者存款," Ethereum Improvement Proposals, no. 6110, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6110.