EIP-8025 提案:可选执行证明 - 通过密码学证明,而不是重执行来验证状态

frisitano 发布于 2026-04-27 阅读 90

EIP-8025引入执行证明作为以太坊共识层的可选机制,允许节点通过生成和验证加密证明来替代完整状态重执行,实现子线性验证成本和无状态验证。该提案定义了两类节点:生成证明的证明节点(自利)和验证证明的验证节点(可选)。规范详细描述了共识层的证明引擎接口、Gossip协议、Req/Res协议,以及执行层的无状态输入/输出结构、证人构造和SSZ编码。此EIP不改变共识规则,旨在积累运营经验,为未来可能推广为强制机制做准备。

摘要

执行证明使共识层(CL)节点能够对执行负载进行无状态验证。利他的证明生成节点生成执行证明,并通过 CL p2p 网络广播它们。证明验证节点使用八卦传播的执行证明来断言执行负载的有效性。验证是无状态的,并且在 gas 限制下是次线性的,因此验证者的资源需求呈次线性扩展,这比重新执行的线性成本有所改进。该机制完全自愿加入,并且不改变任何共识规则,允许证明栈在生产环境中成熟;随后,当证明栈成熟后,可以通过单独的、未来的 EIP 提议使执行证明成为强制性的。本 EIP 不引入对证明生成节点的激励;因此,它们被认为是利他的,并且该机制是自愿加入的。

动机

今天,验证一个信标区块需要针对 EL 状态(账户和存储树数据的完整集合)重新执行其执行负载。重新执行的成本与 gas 限制呈线性关系。这将节点的资源需求与状态大小和链的吞吐量耦合在一起。

本 EIP 引入了一条具有以下属性的自愿加入路径:

  • 次线性验证成本。 验证者运行时间和证明大小均按照 O(log² n) 扩展,其中 n 是执行负载的客户程序执行步骤数。n 受负载的 gas 限制约束。
  • 无状态验证。 验证执行证明仅需要证明本身和对负载的公共承诺——不需要 EL 状态。
  • 更广泛的验证者参与。 次线性、无状态的验证降低了证明验证节点的资源门槛,即使在 gas 限制和状态增长的情况下,也能使拥有普通硬件和专业知识的参与者能够处理负载有效性检查。
  • 设计上可选。 将执行证明部署为可选,允许客户端和运营商积累操作经验,收集性能指标,并根据实时网络条件微调证明栈,而无需将未经测试的机制固化到协议中,也不会影响未选择加入的验证者。

规范

本文件中的关键词 "MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY" 和 "OPTIONAL" 应按照 RFC 2119RFC 8174 中的描述进行解释。

术语

本 EIP 使用以下证明系统术语:

  • 证明系统。 用于证明计算被正确执行的密码学系统。不同的证明系统可能有不同的证明格式、验证逻辑、证明成本和安全性假设。

  • 证明者。 一个运行执行验证计算并生成证明结果执行证明的节点。在本 EIP 中,证明者是自愿加入的、利他的证明生成验证者。

  • 验证者。 一个检查执行证明而不是重新执行负载验证的节点。

  • 客户。 其执行被证明的程序。这里,客户程序执行对执行负载的无状态验证。

  • 宿主。 准备客户输入、在证明系统内运行客户并打包结果证明的链下逻辑。

  • 私有输入。 由证明者提供给客户但未向验证者公开的数据。在本 EIP 中,序列化的 StatelessInput 是私有输入:它包含运行无状态负载验证所需的数据。

  • 执行见证。 私有输入中的执行数据,允许客户在没有验证者持有完整 EL 状态的情况下验证负载。

  • 公共输入。 由证明承诺并可供验证者查看的数据。在本 EIP 中,公共输入将证明绑定到特定的 NewPayloadRequest、链配置和验证结果。

  • 证明节点。 执行特定证明系统工作的外部组件,例如证明生成和证明验证。

  • 证明引擎。 共识客户端用于与证明节点通信的接口。ProofEngine 使共识层集成保持证明系统无关性。

  • 证明感知对等节点。 一个共识层对等节点,公告可选执行证明支持,并参与其支持证明类型的执行证明网络协议。

  • 服务器发送事件 (SSE)。 一种 HTML 标准,用于从服务器到客户端的单向、长连接 HTTP 事件流。在本 EIP 中,SSE 用于两个地方:信标节点向证明者推送 Block 事件,以及证明节点向证明者推送证明完成事件。

规范参考

本 EIP 的规范性规范与协议的其他部分一起在 consensus-specs 和 execution-specs 仓库中维护。以下各节总结了变更;权威来源是:

共识层

本 EIP 在共识层 p2p 网络上引入了一个新的 execution_proof 八卦主题,用于承载 SignedExecutionProof 消息,以及一个信标节点用于生成和验证执行证明的 ProofEngine 接口。证明生成和验证被委托给一个外部证明节点;ProofEngine 是客户端内调解与其通信的组件。一个节点可以选择启用以下两者之一或两者都启用:

  • 证明生成模式。 一个选择作为证明者的活动验证者——生成执行证明并在 execution_proof 八卦主题上广播。
  • 证明验证模式。 一个消费八卦传播的证明,并将已验证的证明视为对相应负载有效性的补充证明的节点。证明验证不需要本地 EL 状态。证明验证节点仍然自行重新执行负载;已验证的证明是一个额外的信号,而不是重新执行的替代。这种分离允许客户端在实时网络条件下部署和测量证明栈,而不使分叉选择依赖于它。后续的 EIP 可以提议将已验证的证明提升为承载角色,并在栈成熟后放弃重新执行。

下图总结了两种角色之间证明的流动方式:

通过 execution_proof 八卦主题,在证明生成节点和证明验证节点之间的执行证明流

在高层次上,证明生命周期如下:

  1. 证明者对新的信标区块做出反应,构造相应的 NewPayloadRequest,并调用 proof_engine.request_proofs(new_payload_request, ProofAttributes(proof_types)) 以在证明节点上为其配置的 proof_types 启动证明生成。
  2. 证明节点为每个请求的类型生成一个 ExecutionProof。证明者使用 DOMAIN_EXECUTION_PROOF 域对每个证明进行签名,将其包装在 SignedExecutionProof 中,并在 execution_proof 主题上广播。
  3. 证明验证对等节点接收八卦消息,运行八卦验证规则,然后对内部的 ExecutionProof 调用 proof_engine.verify_execution_proof 以确认负载有效性。

同步支持要求。 独立于上述的请求/生成/验证循环,每个证明感知节点必须保留自最终确定检查点起规范链区块的有效证明,以便同步或填补间隙的对等节点能够通过 ExecutionProofsByRangeExecutionProofsByRoot 回填它们(有关协议细节,请参见 Req/resp 域)。

证明类型和容器

共识层引入了一小组新类型和常量,用于标识证明、限制其大小以及路由八卦签名:

名称
ProofType uint8
MAX_PROOF_SIZE 409600 (= 400 KiB)
MAX_EXECUTION_PROOFS_PER_PAYLOAD uint64(4)
DOMAIN_EXECUTION_PROOF DomainType('0x0D000000')

MAX_PROOF_SIZE 设置为 400 KiB 作为网络带宽预算:在八卦目标为每个 (payload, proof_type) 一个证明,并且每个负载最多 MAX_EXECUTION_PROOFS_PER_PAYLOAD = 4 种证明类型的情况下,每个槽位最坏情况下的在途证明带宽为 4 × 400 KiB = 1.6 MiB——与现有的每槽位 blob 侧车流量相当。这限制了稳态流量。使用自然生成更大证明的系统的证明者,预期会在八卦之前应用证明压缩(递归、简洁包装)。

对等节点通过 ExecutionProofStatus 公告其支持的证明类型(请参见 Req/resp 域)。节点支持的 proof_types 集合是每个节点动态配置的;新的证明系统可以通过带外传播新的 ProofType 值并将其推广到同意的运营商来添加。由于本 EIP 是自愿加入且不改变任何共识规则,因此添加、弃用或交换证明系统需要硬分叉或协调的客户端发布——对等节点只需在 ExecutionProofStatus 握手期间发现哪些证明类型重叠,并相应地路由请求。

顶层的八卦消息是 SignedExecutionProof

class SignedExecutionProof(Container):
    message: ExecutionProof
    validator_index: ValidatorIndex
    signature: BLSSignature

其内部的 ExecutionProof 将序列化的证明 blob 与生成它所针对的证明系统和客户程序,以及标识其认证的负载的公共输入联系起来:

class ExecutionProof(Container):
    proof_data: ByteList[MAX_PROOF_SIZE]
    proof_type: ProofType
    public_input: PublicInput


class PublicInput(Container):
    new_payload_request_root: Root
    successful_validation: bool
    chain_config: ChainConfig

各字段的角色如下:

  • proof_data 是证明系统的序列化输出。它是唯一一个大小随证明者输出变化并由 MAX_PROOF_SIZE 约束的字段。
  • proof_type 选择验证者分派到哪个证明系统和客户程序。
  • public_input.new_payload_request_root 是本证明认证其执行的 Engine API NewPayloadRequest 的 SSZ hash_tree_root。它是证明与特定执行负载之间的链接;验证者使用它将证明绑定到他们正在验证的负载。
  • public_input.successful_validation 如果客户程序接受了负载为有效,则为 True。验证者必须检查此字段是否为 True,然后才能将证明视为正面的有效性证明。
  • public_input.chain_config 固定了客户程序运行时所针对的链 ID 和分叉配置,防止跨链重放和分叉不匹配。ChainConfig执行层部分中定义。
  • validator_indexsignature 标识证明者,并证明他们在 DOMAIN_EXECUTION_PROOF 下签署了证明,允许对等节点将无效证明归因于特定的活动验证者。

证明引擎接口

ProofEngine 接口封装了特定证明系统的逻辑,使共识层保持对使用的证明系统无感知。它充当信标节点的负载有效性预言机,其 API 形状模拟 Engine API,以便无缝集成到协议中。ProofEngine 作为客户端内组件实现,维护自己的证明状态,并将证明生成和验证的密码学工作委托给外部证明节点,类似于 ExecutionEngine 将负载执行委托给 EL 客户端的方式。该接口包含四个操作和一个 ProofAttributes 类型:

def verify_execution_proof(
    self: ProofEngine,
    execution_proof: ExecutionProof,
) -> bool: ...


def notify_new_payload(
    self: ProofEngine,
    new_payload_request: NewPayloadRequest,
) -> None: ...


def notify_forkchoice_updated(
    self: ProofEngine,
    head_block_hash: Hash32,
    safe_block_hash: Hash32,
    finalized_block_hash: Hash32,
) -> None: ...


@dataclass
class ProofAttributes:
    proof_types: Sequence[ProofType]


def request_proofs(
    self: ProofEngine,
    new_payload_request: NewPayloadRequest,
    proof_attributes: ProofAttributes,
) -> Root: ...

这些操作分解为三个关注点:

  • 验证 (verify_execution_proof) 在每次通过八卦验证的传入证明时调用。引擎执行特定证明系统的密码学验证,并将结果绑定到由 proof.public_input.new_payload_request_root 标识的特定负载。一旦某个负载的 k 个有效的 SignedExecutionProof 被验证,该负载被认为已通过证明验证。k 的值将在本 EIP 过渡到 Review 之前固定;在网络上收集真实世界数据期间,它有意保持开放。对等节点通过 ExecutionProofStatus 交换其当前的证明验证视图。
  • 生命周期通知 (notify_new_payload, notify_forkchoice_updated) 使引擎与信标节点的状态保持同步。notify_new_payload 让引擎将传入的证明与其认证的负载关联起来;notify_forkchoice_updated 让它跟踪规范链以驱动证明的保留和修剪。
  • 生成 (request_proofs) 由证明生成节点使用,用于触发针对给定负载和证明类型集的异步证明生成。返回值是 new_payload_request.hash_tree_root(),证明者将其用作请求 ID,以关联证明节点在准备好获取证明时发出的 SSE 事件。

信标链状态转换

EIP-8025 扩展了 process_block,将执行负载处理转发给执行引擎和证明引擎,并引入了一个新的 process_execution_proof 处理程序来处理传入的证明。

process_block 被修改为将 PROOF_ENGINE 传递给 process_execution_payload

def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)
    process_withdrawals(state, block.body.execution_payload)
    # [Modified in EIP8025]
    process_execution_payload(state, block.body, EXECUTION_ENGINE, PROOF_ENGINE)
    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body)
    process_sync_aggregate(state, block.body.sync_aggregate)

process_execution_payload 被修改为在执行引擎接受新负载后,另外通知证明引擎新负载。这个 Hook 让证明引擎得知每个新负载,以便在它们通过八卦到达时,将其与认证它们的证明关联起来:

def process_execution_payload(
    state: BeaconState,
    body: BeaconBlockBody,
    execution_engine: ExecutionEngine,
    proof_engine: ProofEngine,
) -> None:
    ...
    # 通过 ExecutionEngine 验证执行负载是有效的
    assert execution_engine.verify_and_notify_new_payload(
        NewPayloadRequest(...)
    )

    # [New in EIP8025]
    # 通知 ProofEngine 新的执行负载
    proof_engine.notify_new_payload(NewPayloadRequest(...))
    ...

一个新的 process_execution_proof 处理程序在处理一个 SignedExecutionProof 通过八卦验证后处理它。它重新检查证明者在当前状态中是否活跃,验证在 DOMAIN_EXECUTION_PROOF 下的 BLS 签名,并将密码学验证委托给证明引擎:

def process_execution_proof(
    state: BeaconState,
    signed_proof: SignedExecutionProof,
    proof_engine: ProofEngine,
) -> None:
    proof_message = signed_proof.message

    # 验证证明者是活跃的验证者
    validator = state.validators[signed_proof.validator_index]
    assert is_active_validator(validator, get_current_epoch(state))

    domain = get_domain(state, DOMAIN_EXECUTION_PROOF, compute_epoch_at_slot(state.slot))
    signing_root = compute_signing_root(proof_message, domain)
    assert bls.Verify(validator.pubkey, signing_root, signed_proof.signature)

    # 验证执行证明
    assert proof_engine.verify_execution_proof(proof_message)

process_execution_proof 在信标区块状态转换函数外部调用,并且不产生链上状态变化:它是一个运行时的 Hook,允许节点决定是否将八卦传播的证明视为对执行负载的有效证明。证明存储由 ProofEngine 管理;保留受 proof_serve_range 约束(请参见 Req/resp 域):客户端必须保留自最终确定检查点起规范链区块的证明,以便它们能够服务于 ExecutionProofsByRangeExecutionProofsByRoot 请求。

证明八卦

执行证明在一个新的全局主题 execution_proof 上八卦传播,该主题承载 SignedExecutionProof 消息。以下每条规则都是一个 libp2p gossipsub 验证规则,遵循其他信标节点八卦主题使用的相同 [IGNORE] / [REJECT] 约定。其中 proof = signed_execution_proof.message

  • [IGNORE] 该证明的 new_payload_request_root 已经通过八卦或非八卦源被看到(客户端可以排队等待证明,直到相应的负载到达)。
  • [IGNORE] 对于元组 (new_payload_request_root, proof_type),尚未收到有效的证明——即每个负载的每种类型只转发第一个有效证明。
  • [IGNORE] 这是为元组 (new_payload_request_root, proof_type, validator_index) 收到的第一个证明——即每个证明者每个 (payload, proof_type) 只有一次机会。
  • [REJECT] 位于 validator_index 的验证者是活跃验证者。
  • [REJECT] BLS 签名对于证明者的公钥在 DOMAIN_EXECUTION_PROOF 下是有效的。
  • [REJECT] proof.proof_data 非空且不大于 MAX_PROOF_SIZE
  • [REJECT] process_execution_proof 中的所有检查都通过。

这些规则与现有的 gossipsub 对等节点评分机制组合:中继触发 [REJECT] 消息的对等节点会被降分,与任何其他 CL 八卦主题上的不当行为评分机制相同。[IGNORE] 规则限制了稳态带宽——一旦一个节点对某个 (new_payload_request_root, proof_type) 有了一个有效证明,该元组的其他证明会被静默丢弃,无论它们由谁签名。

请求/响应域

三个请求/响应协议允许对等节点回填、定向和协调证明状态:

  • ExecutionProofsByRange — 请求 (start_slot, count, proof_types),响应 List[SignedExecutionProof]。镜像 BlobSidecarsByRangeDataColumnSidecarsByRange:基于范围、不基于根、按证明类型过滤。用于同步窗口后的大规模证明回填。
  • ExecutionProofsByRoot — 请求 List[ProofByRootIdentifier],其中每个标识符包含 (block_root, List[proof_type]),响应 List[SignedExecutionProof]。用于当节点知道其缺失证明的特定区块根时的定向检索。
  • ExecutionProofStatus — 请求和响应共享同一个容器:(block_root, slot, proof_types)。对等节点交换它们认为已通过证明验证的最新区块,以及其动态公告的受支持证明类型集。发起拨号的对等节点在第一次连接到任何证明感知对等节点时必须发送状态。这与信标链 Status v2 握手类似。

客户端必须为规范链上任何其槽位在 proof_serve_range(定义为 [finalized_checkpoint.slot, current_slot])内的区块服务范围和根请求。无法响应的对等节点以 3: ResourceUnavailable 响应,并可能被降分。这些协议的形状与现有的同步流程组合:向前同步区块的节点按范围拉取证明;修补特定间隙的节点按根拉取证明。

发现

发现过程通过一个新的以太坊节点记录 (ENR) 键 eproof 进行扩展,该键编码为 uint8。如果该字段存在且非零,则节点被视为证明感知,允许其他对等节点在发现过程中过滤证明感知的对等节点。这镜像了 Fulu 中通过 cgc ENR 字段公告数据列感知的现有模式,并且有意保持低成本:节点开始(或停止)公告证明支持不需要分叉或分叉版本提升。

证明者生命周期

证明者生命周期如下图所示:

证明者生命周期:信标节点、证明者和证明节点之间的 SSE 驱动流程

证明者的流程(如上文提到的证明者指南中所述)是完全事件驱动的:

  1. 启动时订阅两个 SSE 事件流
    • 来自信标节点的 Block 事件,表明已接收到新的有效信标区块。
    • 来自证明节点的证明完成事件。
  2. 收到 Block 事件时,通过 RPC 获取完整的 BeaconBlock,构造相应的 NewPayloadRequest,选择 ProofAttributes(证明者想要生成的 proof_types 集合),并调用 proof_engine.request_proofs(...)。返回的根用于跟踪请求。
  3. 收到与跟踪的根匹配的证明完成事件时,从证明节点获取完整的 ExecutionProof,在 DOMAIN_EXECUTION_PROOF 下签名,包装在 SignedExecutionProof 中,并在 execution_proof 八卦主题上广播。

签名辅助函数为:

def get_execution_proof_signature(
    state: BeaconState, proof: ExecutionProof, privkey: int
) -> BLSSignature:
    domain = get_domain(state, DOMAIN_EXECUTION_PROOF, compute_epoch_at_slot(state.slot))
    signing_root = compute_signing_root(proof, domain)
    return bls.Sign(privkey, signing_root)

证明者角色的范围限定在可选证明阶段。如果未来的 EIP 使执行证明成为强制性的,证明生成可能会合理地移到区块生产中,届时证明者角色和可选的八卦主题可以弃用,转向区块内证明承诺。

执行层

本 EIP 引入了一个无状态执行客户和证明者侧逻辑,用于构造该客户使用的输入。证明者针对私有的 StatelessInput 字节运行客户。证明将 StatelessValidationResult 作为公共输出暴露,允许验证者将证明绑定到正在被检查的负载请求。

一个证明验证节点仅在以下情况下接受执行层结果:

  • 证明针对预期的客户程序验证通过;
  • successful_validationtrue
  • new_payload_request_root 等于与正在验证的负载关联的 NewPayloadRequest 的 SSZ hash_tree_root;并且
  • chain_config 与验证者预期的链 ID 和分叉配置匹配。

在高层次上,证明生成遵循以下流程:

  1. 宿主(通常是一个有状态的执行层客户端)执行或构建区块,同时记录执行期间使用的账户、存储、代码和祖先区块头数据。
  2. 宿主构建一个 ExecutionWitness,将其与 Engine API NewPayloadRequest、链配置和交易公钥一起包装成一个 StatelessInput,并将该输入序列化为带有模式前缀的 SSZ 字节。
  3. 证明者在序列化的 StatelessInput 上运行客户程序。客户验证链配置和见证脚手架,针对见证支持的状态执行负载,并返回 StatelessValidationResult

无状态输入和输出

顶层的客户输入是私有证明者输入:

class StatelessInput:
    new_payload_request: NewPayloadRequest
    witness: ExecutionWitness
    chain_config: ChainConfig
    public_keys: Tuple[Bytes, ...]

客户输出是公共的:

class StatelessValidationResult:
    new_payload_request_root: Hash32
    successful_validation: bool
    chain_config: ChainConfig

注意此结构是证明类型和容器部分中描述的 PublicInput 的 EL 对应部分。

在此层次上:

  • new_payload_request 是由共识层提供的 Engine API 负载数据。
  • witness 是执行负载所需的账户、存储、代码和祖先区块头数据,无需本地 EL 状态。
  • chain_config 告诉客户要针对哪个链 ID 和分叉配置进行验证。
  • public_keys 包含每个交易签名者的一个 65 字节未压缩 secp256k1 公钥,顺序与负载中的交易相同。优化的客户可以使用这些密钥来避免公钥恢复,但交易签名和提供的公钥仍然必须被验证。

NewPayloadRequest 承诺负载、blob 版本哈希、parent_beacon_block_root 和类型化执行请求:

class NewPayloadRequest:
    execution_payload: ExecutionPayload
    versioned_hashes: Tuple[VersionedHash, ...]
    parent_beacon_block_root: Root
    execution_requests: ExecutionRequests


class ExecutionPayload:
    parent_hash: Hash32
    fee_recipient: Address
    state_root: Root
    receipts_root: Root
    logs_bloom: Bloom
    prev_randao: Bytes32
    block_number: Uint
    gas_limit: Uint
    gas_used: Uint
    timestamp: U256
    extra_data: Bytes
    base_fee_per_gas: Uint
    block_hash: Hash32
    transactions: Tuple[Bytes, ...]
    withdrawals: Tuple[Withdrawal, ...]
    blob_gas_used: U64
    excess_blob_gas: U64
    block_access_list: Bytes

执行请求表示为类型化容器,与共识层规范匹配:

class DepositRequest:
    pubkey: Bytes48
    withdrawal_credentials: Bytes32
    amount: U64
    signature: Bytes96
    index: U64


class WithdrawalRequest:
    source_address: Address
    validator_pubkey: Bytes48
    amount: U64


class ConsolidationRequest:
    source_address: Address
    source_pubkey: Bytes48
    target_pubkey: Bytes48


class ExecutionRequests:
    deposits: Tuple[DepositRequest, ...]
    withdrawals: Tuple[WithdrawalRequest, ...]
    consolidations: Tuple[ConsolidationRequest, ...]

其余顶层字段定义了客户使用的见证和链上下文:

class ExecutionWitness:
    state: Tuple[Bytes, ...]
    codes: Tuple[Bytes, ...]
    headers: Tuple[Bytes, ...]


class ChainConfig:
    chain_id: U64
    active_fork: ForkConfig


class ForkConfig:
    fork: ProtocolFork
    activation: ForkActivation
    blob_schedule: BlobSchedule | None


class ForkActivation:
    block_number: U64 | None
    timestamp: U64 | None


class BlobSchedule:
    target: U64
    max: U64
    base_fee_update_fraction: U64


class ProtocolFork(StrEnum):
    ...
    Amsterdam = "Amsterdam"

ExecutionWitness 字段的角色如下:

  • state: 执行和状态根重新计算期间所需的 RLP 编码的账户和存储树节点原像。
  • codes: 从预状态进行代码读取所需的字节码。正在执行的区块中创建的代码不包含在内,因为客户在重新执行期间会观察到它。
  • headers: RLP 编码的父区块头及祖先区块头,按区块号排序,以负载父区块头结束。这些区块头提供了父状态根和 BLOCKHASH 及系统合约逻辑使用的近期区块哈希。客户检查这些区块头是否构成一个连续的链。

客户验证

客户入口点解码带模式前缀的 SSZ 输入,运行无状态负载验证,并序列化结果。

def run_stateless_guest(input_bytes: Bytes) -> Bytes:
    stateless_input = deserialize_stateless_input(input_bytes)
    stateless_output = verify_stateless_new_payload(stateless_input)
    return serialize_stateless_output(stateless_output)

主验证路径首先计算公共负载请求承诺。然后验证分叉配置,检查区块头见证,创建一个 WitnessState,并将负载执行委托给执行引擎使用的相同 new_payload 路径:

def compute_new_payload_request_root(
    stateless_input: StatelessInput,
) -> Hash32:
    ssz_npr = _new_payload_request_to_ssz(stateless_input.new_payload_request)
    return Hash32(ssz_npr.hash_tree_root())


def validate_chain_config(
    chain_config: ChainConfig,
    new_payload_request: NewPayloadRequest,
) -> ForkConfig:
    active_fork = chain_config.active_fork
    execution_payload = new_payload_request.execution_payload

    if not _is_activation_active(active_fork.activation, execution_payload):
        raise InactiveForkConfigError(...)
    if active_fork.fork not in guest_supported_forks():
        raise UnsupportedForkConfigError(...)
    if active_fork.blob_schedule != _expected_amsterdam_blob_schedule():
        raise UnsupportedForkConfigError(...)

    return active_fork


def validate_headers(
    encoded_headers: Tuple[Bytes, ...],
) -> Tuple[List[Header | PreviousForkHeader], List[Hash32]]:
    assert len(encoded_headers) <= 256
    headers = [_decode_header(header) for header in encoded_headers]
    block_hashes = [keccak256(header) for header in encoded_headers]
    for i in range(1, len(headers)):
        if headers[i].parent_hash != block_hashes[i - 1]:
            raise Exception("Witness headers are not contiguous")
    return headers, block_hashes


def verify_stateless_new_payload(
    stateless_input: StatelessInput,
) -> StatelessValidationResult:
    new_payload_request_root = compute_new_payload_request_root(
        stateless_input
    )
    witness = stateless_input.witness

    try:
        validate_chain_config(
            stateless_input.chain_config,
            stateless_input.new_payload_request,
        )

        decoded_headers, block_hashes = validate_headers(witness.headers)
        parent_header = decoded_headers[-1]

        chain_context = ChainContext(
            chain_id=stateless_input.chain_config.chain_id,
            block_hashes=block_hashes,
            parent_header=parent_header,
        )

        pre_state = WitnessState(
            _node_db=build_node_db(witness.state),
            _state_root=parent_header.state_root,
            _code_db=build_code_db(witness.codes),
        )

        execute_new_payload_request(
            stateless_input.new_payload_request,
            pre_state,
            chain_context,
            transaction_public_keys=stateless_input.public_keys,
        )
        successful_validation = True
    except Exception:
        successful_validation = False

    return StatelessValidationResult(
        new_payload_request_root=new_payload_request_root,
        successful_validation=successful_validation,
        chain_config=stateless_input.chain_config,
    )

WitnessState 是本地预状态数据库的无状态替代品。它从见证中为账户、存储和代码读取提供服务,并根据执行差异计算后状态根:

class WitnessState:
    _node_db: Dict[Bytes, Bytes]
    _state_root: Root
    _code_db: Dict[Hash32, Bytes]

    def get_account_optional(self, address: Address) -> Optional[Account]: ...
    def get_storage(self, address: Address, key: Bytes32) -> U256: ...
    def get_code(self, code_hash: Hash32) -> Bytes: ...

    def compute_state_root_and_trie_changes(
        self,
        account_changes: Dict[Address, Optional[Account]],
        storage_changes: Dict[Address, Dict[Bytes32, U256]],
    ) -> Tuple[Root, List[InternalNode]]:
        ...

负载执行逻辑在执行区块之前执行正常的负载检查:

def execute_new_payload_request(
    new_payload_request: NewPayloadRequest,
    pre_state: PreState,
    chain_context: ChainContext,
    transaction_public_keys: Optional[Tuple[Bytes, ...]] = None,
) -> Tuple[BlockDiff, Block]:
    payload = new_payload_request.execution_payload

    if b"" in payload.transactions:
        raise InvalidBlock("Empty transaction in payload")
    if not is_valid_block_hash(
        payload,
        new_payload_request.parent_beacon_block_root,
        new_payload_request.execution_requests,
    ):
        raise InvalidBlock("Invalid block hash")
    if not is_valid_versioned_hashes(new_payload_request):
        raise InvalidBlock("Invalid versioned hashes")

    block = _payload_block(
        payload,
        new_payload_request.parent_beacon_block_root,
        new_payload_request.execution_requests,
    )
    block_diff = execute_block(
        block,
        pre_state,
        chain_context,
        transaction_public_keys=transaction_public_keys,
    )
    return block_diff, block

在交易处理期间,提供的公钥在派生发送者地址之前被验证:

def recover_sender_from_public_key(
    chain_id: U64,
    tx: Transaction,
    public_key: Bytes,
) -> Address:
    if public_key != recover_transaction_public_key(chain_id, tx):
        raise InvalidSignatureError
    return _sender_address_from_public_key(public_key)

优化的客户可以避免完整的公钥恢复,但它们仍然必须验证提供的密钥能验证交易签名,并且与恢复 ID 或 y 奇偶校验位一致。

宿主侧输入构建

在宿主侧,build_stateless_input 接收在区块执行或构建期间收集的工件,并将它们打包给客户:

def build_stateless_input(
    block: Block,
    *,
    execution_witness: ExecutionWitness,
    execution_requests: ExecutionRequests,
    block_access_list: BlockAccessList,
    chain_id: U64,
) -> StatelessInput:
    ...

    new_payload = NewPayloadRequest(
        execution_payload=payload,
        versioned_hashes=tuple(versioned_hashes),
        parent_beacon_block_root=header.parent_beacon_block_root,
        execution_requests=execution_requests,
    )

    return StatelessInput(
        new_payload_request=new_payload,
        witness=execution_witness,
        chain_config=build_chain_config(chain_id),
        public_keys=tuple(public_keys),
    )

宿主还提供与正在验证的负载相对应的分叉配置:

def build_chain_config(chain_id: U64) -> ChainConfig:
    return ChainConfig(
        chain_id=chain_id,
        active_fork=ForkConfig(
            fork=ProtocolFork.Amsterdam,
            activation=ForkActivation(
                block_number=None,
                timestamp=U64(0),
            ),
            blob_schedule=BlobSchedule(
                target=BLOB_SCHEDULE_TARGET,
                max=BLOB_SCHEDULE_MAX,
                base_fee_update_fraction=U64(BLOB_BASE_FEE_UPDATE_FRACTION),
            ),
        ),
    )

执行见证是根据区块级别的读/写跟踪器和预状态树数据构建的。读/写跟踪器是 EL 中已经使用的组件,用于跟踪执行期间触摸了哪些数据,以协助构建区块访问列表 (BAL)。从概念上讲,见证构建:

  1. 收集父区块头和 BLOCKHASH 触摸的任何更早祖先;
  2. 收集预状态字节码读取;
  3. 捕获所有读取和写入所需的账户和存储树节点;
  4. 捕获后状态根计算期间由于分支压缩可能需要的任何兄弟节点。
def build_execution_witness(
    block_state: BlockState,
    expected_post_state_root: Root,
    pre_state_accounts_data: Trie[Address, Optional[Account]],
    pre_state_storages_data: Dict[Address, Trie[Bytes32, U256]],
    blockchain_headers: Optional[List[Bytes]] = None,
) -> ExecutionWitness:
    ancestor_headers = get_witness_ancestors(
        blockchain_headers if blockchain_headers is not None else [],
        block_state.oldest_ancestor_offset,
    )
    codes = get_witness_codes(block_state.code_reads, block_state.pre_state)

    incr_storage_mpts = _build_pre_state_storage_mpts(pre_state_storages_data)
    incr_account_mpt = _build_pre_state_account_mpt(
        pre_state_accounts_data, incr_storage_mpts
    )

    all_storage_accesses = _collect_storage_accesses(block_state)
    _capture_pre_state_storage_nodes(incr_storage_mpts, all_storage_accesses)
    _apply_storage_writes(incr_storage_mpts, block_state.storage_writes)

    all_dirty_accounts = _get_all_dirty_accounts(block_state)
    _capture_pre_state_account_nodes(
        incr_account_mpt,
        block_state.account_reads,
        all_dirty_accounts,
    )
    _apply_account_writes(
        incr_account_mpt,
        incr_storage_mpts,
        block_state,
        all_dirty_accounts,
    )

    assert mpt_root(incr_account_mpt) == expected_post_state_root

    accessed_nodes = _collect_accessed_nodes(
        incr_account_mpt, incr_storage_mpts
    )

    return ExecutionWitness(
        state=tuple(sorted(accessed_nodes.values())),
        codes=tuple(codes),
        headers=tuple(ancestor_headers),
    )

SSZ 编码

宿主通过用 STATELESS_INPUT_SCHEMA_ID 作为前缀来序列化 StatelessInput。这允许未来的客户程序在分叉或编码规则演变时选择正确的输入模式。

STATELESS_INPUT_SCHEMA_ID = 0x0001
STATELESS_INPUT_SCHEMA_ID_SIZE = 2


def serialize_stateless_input(
    stateless_input: StatelessInput,
) -> Bytes:
    ssz_obj = stateless_input_to_ssz(stateless_input)
    return Bytes(
        STATELESS_INPUT_SCHEMA_ID_BYTES + bytes(ssz_obj.encode_bytes())
    )


def deserialize_stateless_input(data: Bytes) -> StatelessInput:
    if len(data) < STATELESS_INPUT_SCHEMA_ID_SIZE:
        raise ValueError("Stateless input is missing schema id")
    schema_id = int.from_bytes(
        data[:STATELESS_INPUT_SCHEMA_ID_SIZE],
        "big",
    )
    if schema_id != STATELESS_INPUT_SCHEMA_ID:
        raise ValueError(
            f"Unsupported stateless input schema id: 0x{schema_id:04x}"
        )
    ssz_obj = SszStatelessInput.decode_bytes(
        data[STATELESS_INPUT_SCHEMA_ID_SIZE:]
    )
    return ssz_to_stateless_input(ssz_obj)

SSZ 模式镜像了数据类,并对线表示应用了有界列表限制:

class SszExecutionWitness(Container):
    state: SszList[ByteList[MAX_BYTES_PER_WITNESS_NODE], MAX_WITNESS_NODES]
    codes: SszList[ByteList[MAX_BYTES_PER_CODE], MAX_WITNESS_CODES]
    headers: SszList[ByteList[MAX_BYTES_PER_HEADER], MAX_WITNESS_HEADERS]


class SszWithdrawal(Container):
    index: uint64
    validator_index: uint64
    address: ByteVector[20]
    amount: uint64


class SszExecutionPayload(Container):
    parent_hash: Bytes32
    fee_recipient: ByteVector[20]
    state_root: Bytes32
    receipts_root: Bytes32
    logs_bloom: ByteVector[256]
    prev_randao: Bytes32
    block_number: uint64
    gas_limit: uint64
    gas_used: uint64
    timestamp: uint64
    extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
    base_fee_per_gas: uint256
    block_hash: Bytes32
    transactions: SszList[
        ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD
    ]
    withdrawals: SszList[SszWithdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
    blob_gas_used: uint64
    excess_blob_gas: uint64
    block_access_list: ByteList[MAX_BLOCK_ACCESS_LIST_BYTES]


class SszDepositRequest(Container):
    pubkey: ByteVector[48]
    withdrawal_credentials: Bytes32
    amount: uint64
    signature: ByteVector[96]
    index: uint64


class SszWithdrawalRequest(Container):
    source_address: ByteVector[20]
    validator_pubkey: ByteVector[48]
    amount: uint64


class SszConsolidationRequest(Container):
    source_address: ByteVector[20]
    source_pubkey: ByteVector[48]
    target_pubkey: ByteVector[48]


class SszExecutionRequests(Container):
    deposits: SszList[SszDepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]
    withdrawals: SszList[
        SszWithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD
    ]
    consolidations: SszList[
        SszConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD
    ]


class SszNewPayloadRequest(Container):
    execution_payload: SszExecutionPayload
    versioned_hashes: SszList[Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK]
    parent_beacon_block_root: Bytes32
    execution_requests: SszExecutionRequests


class SszForkActivation(Container):
    block_number: SszList[uint64, MAX_OPTIONAL_FORK_ACTIVATION_VALUES]
    timestamp: SszList[uint64, MAX_OPTIONAL_FORK_ACTIVATION_VALUES]


class SszBlobSchedule(Container):
    target: uint64
    max: uint64
    base_fee_update_fraction: uint64


class SszForkConfig(Container):
    fork: uint64
    activation: SszForkActivation
    blob_schedule: SszList[SszBlobSchedule, MAX_BLOB_SCHEDULES_PER_FORK]


class SszChainConfig(Container):
    chain_id: uint64
    active_fork: SszForkConfig


class SszStatelessInput(Container):
    new_payload_request: SszNewPayloadRequest
    witness: SszExecutionWitness
    chain_config: SszChainConfig
    public_keys: SszList[ByteVector[PUBLIC_KEY_BYTES], MAX_PUBLIC_KEYS]


class SszStatelessValidationResult(Container):
    new_payload_request_root: Bytes32
    successful_validation: boolean
    chain_config: SszChainConfig

当前的 SSZ 最大大小仍有待调整。参考实现使用以下界限:

界限
MAX_TRANSACTIONS_PER_PAYLOAD 2**20
MAX_BYTES_PER_TRANSACTION 2**30
MAX_EXTRA_DATA_BYTES 32
MAX_WITHDRAWALS_PER_PAYLOAD 2**4
MAX_BLOCK_ACCESS_LIST_BYTES 2**24
MAX_BLOB_COMMITMENTS_PER_BLOCK 4096
MAX_DEPOSIT_REQUESTS_PER_PAYLOAD 2**13
MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD 2**4
MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD 2**1
MAX_WITNESS_NODES 2**20
MAX_WITNESS_CODES 2**16
MAX_WITNESS_HEADERS 256
MAX_BYTES_PER_WITNESS_NODE 2**20
MAX_BYTES_PER_CODE 2**24
MAX_BYTES_PER_HEADER 2**10
MAX_OPTIONAL_FORK_ACTIVATION_VALUES 1
MAX_BLOB_SCHEDULES_PER_FORK 1
MAX_PUBLIC_KEYS 2**20
PUBLIC_KEY_BYTES 65

理由

通往低资源验证之路

这一步骤将网络切实地推向低资源验证。无状态、次线性验证打破了节点硬件需求与 gas 限制或状态大小之间的耦合,降低了能够有意义地运行节点的人的门槛,并改善了去中心化。

积累操作经验

核心设计选择是将执行证明作为可选功能部署。无状态验证、见证格式以及证明系统本身正在成熟,但在使执行证明承担承载角色之前,我们需要操作经验。将执行证明视为非关键工件,允许栈在实时网络上成熟——证明大小、生成延迟、验证者吞吐量、八卦行为、证明者多样性——而不将任何这些东西置于分叉选择或证明的路径上。错误、故障或参数选择不当的影响仅限于选择加入的节点;它不能分叉链,也不会影响未订阅的验证者。

向后兼容性

本 EIP 完全自愿加入,不需要硬分叉。没有共识规则更改:未启用任一模式的验证者不会看到其行为、带宽或证明职责发生变化。选择加入的节点额外订阅证明八卦主题,通过 eproof ENR 字段公告自身,并可能运行一个证明者;这只会影响它们的本地资源配置。

测试用例

参考实现

安全考虑

八卦表面。 证明通过一个新的 gossipsub 主题承载,负载大小受 MAX_PROOF_SIZE 限制。针对无效证明的验证、反 DoS 速率限制和对等节点评分遵循与其他 CL 八卦主题相同的模式;行为不端的对等节点受到限制并可能被降分。

可靠性和共识影响。 本 EIP 不将证明验证接入分叉选择或任何其他共识规则:process_execution_proof 在信标区块状态转换函数外部运行。因此,伪造的证明不能分叉链、罚没验证者或以其他方式影响共识状态——其影响限于验证节点本地的负载有效性视图。

活性和关键路径延迟。 证明生成和验证不在证明热路径上。验证者不得在等待证明时延迟区块验证或证明生产;如果证明缺失或延迟,节点使用由执行引擎重新执行负载确定的分叉选择进行证明。

验证者-证明者去中心化不对称。 无状态、次线性验证降低了验证者的资源门槛,但证明生成本身仍然要求高:证明者必须持有执行期间使用的完整 EL 状态,并运行一个计算密集的证明栈,其成本随被证明计算的大小 nO(n log² n) 扩展。如果仅验证节点比例的增长速度快于能够生成证明的节点比例,那么维护完整 EL 状态并生成证明的参与者的集合可能会变得更集中,即使整体验证者参与度在扩大。

  • 原文链接: github.com/frisitano/EIP...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论