原生证明验证

本文为机器翻译
展示原文

抽象的

该提案的总体目标是透过引入一个标准的 L1 原语,大幅降低 L2 桥接的风险并简化其操作,更广泛地说,简化任何验证零知识证明的链上应用程式。任何专案都可以采用该标准原语来取代其客制化的链上验证器堆叠。这透过两项变更来实现:

  1. EIP-8025推广,使共识层证明验证基础设施与程序无关,不再依赖 EVM 执行证明。
  2. 一个新的 EIP ,透过携带证明的交易类型和三个操作码( PROGRAMHASHPUBVALUESHASHPROOFCOUNT )将其暴露给智能合约。

他们共同让任何专案都能直接继承 L1 的证明验证基础设施,zkVM 修复程式透过客户端版本发布,而不是透过每个专案的治理升级发布。

动机

如今,每个以太坊Rollup都维护著客制化的链上证明验证基础设施。零知识 Rollup 部署了 zkVM 验证器合约、适配器合约、多重证明分发器和程式白名单逻辑。乐观 Rollup 则提供各自的链上防诈骗虚拟机(例如 Arbitrum 的 WAVM 和 Optimism 的 Cannon MIPS 机器)以及相关的争议解决逻辑。在这两种情况下,每个合约都需独立维护、修补和升级,以应对其特定证明系统或虚拟机器中的漏洞,每次升级都由定制的多重签名或 DAO 控制。这种方式速度慢、风险高,并且在整个生态系中重复劳动。

EIP-8025在以太坊共识层引入了 zkVM 证明验证,但仅限于 L1 层自身用途:验证执行载荷以实现无状态和亚线性验证。 Rollup 仍然需要它们自己的链上验证合约。

然而,EIP-8025 为 CL 引入的基础设施, ProofEngine 、证明讯息传递机制和验证逻辑,并非 L1 特有。如果将其通用化为与程序无关,并透过新的交易类型向智能合约开放,那么任何Rollup(即使是非 EVM Rollup)都可以将证明验证工作卸载到 CL。当 zkVM 实作需要修补时,以太坊用户端团队会以与修复 geth 或 Nethermind 漏洞相同的方式发布更新软体:透过用户端版本发布,而无需硬分叉。这与原生 Rollup 的原理相同,但更通用:正如原生 Rollup 继承了 L1 的执行环境一样,原生证明验证允许任何Rollup继承 L1 的证明验证基础设施。

虽然本文档围绕著 rollups 构建了该提案,但同样的原始机制适用于任何在链上验证 ZK 证明的合约:隐私系统、ZK 协处理器、身份、ZK ML 等。

如今总结如何验证证明

每个 zkVM 供应商都提供一个通用的 Solidity 验证器合约(通常是基于 BN254 的 Groth16 或 Plonk 检查)。程式标识和公共值(电路承诺的任何输入和输出)与证明一起传递。对于 SP1:

interface ISP1Verifier {function verifyProof(bytes32 programVKey, // program hashbytes calldata publicValues, // public values (inputs and/or outputs)bytes calldata proofBytes // the proof) external view;}

关于术语的说明。 SP1programVKey称为“验证金钥”,但这与 zkVM 本身的电路验证金钥相冲突。本文档将它们区分开来:

  • 程式杂凑(SP1 称为programVKey ,Risc0 称之为imageId ):一个bytes32用于标识已编译的客户机程式。由于每个 zkVM 的编译方式不同(例如 RV32IMA 与 RV64IMA),因此它是针对每个(source, zkVM)对的。 ERE将此表示为每个后端关联的zkVMVerifier::ProgramVk类型(封装SP1VerifyingKey 、Risc0 的DigestETC)。
  • 验证金钥:zkVM 的电路 VK(多项式承诺、域参数)。作为链上验证器的常数硬编码,每个 zkVM 版本一个,所有程式共用。

例如:Taiko(多重验证器)

Taiko 展示了当一个Rollup使用多个证明系统时所产生的复杂性。它的验证架构涉及三个层级的六个合约(两个原始验证器、两个适配器、一个调度器、一个 SGX 验证器),每个合约都透过自订多重签章独立维护和升级。

1. 原始 zkVM 验证器。 Taiko同时部署了 SP1 Plonk 验证器 ( SP1Verifier.sol ) 和 Risc0 Groth16 验证器 ( RiscZeroGroth16Verifier.sol )。这些是供应商提供的通用验证器合约。

2. Taiko 特有的适配器。每个原始验证器都封装在一个实现了 Taiko 的IVerifier介面的适配器合约中:

// TaikoSP1Verifier: adapter for SP1contract TaikoSP1Verifier is IVerifier {address public sp1RemoteVerifier; // raw SP1 verifiermapping(bytes32 => bool) public isProgramTrusted; // whitelisted programsfunction verifyProof(Context[] calldata _ctxs, bytes calldata _proof) external view {bytes32 aggregationProgram = bytes32(_proof[:32]);bytes32 blockProvingProgram = bytes32(_proof[32:64]);require(isProgramTrusted[aggregationProgram]);require(isProgramTrusted[blockProvingProgram]);bytes memory publicInputs = buildPublicInputs(_ctxs);ISP1Verifier(sp1RemoteVerifier).verifyProof(aggregationProgram, publicInputs, _proof[64:]);}}

并行的Risc0Verifier具有相同的形状,其中isImageTrusted取代了isProgramTrustedsha256(buildPublicInputs(...))作为日志摘要。

3. 多验证者调度器。 ComposeVerifier ComposeVerifier协调多个验证者,并确保每个证明都已由足够的验证者验证:

contract MainnetVerifier is ComposeVerifier {address public immutable sgxGethVerifier; // SGX verifier (required)address public immutable risc0RethVerifier; // Risc0 optionaddress public immutable sp1RethVerifier; // SP1 optionfunction verifyProof(Context[] calldata _ctxs, bytes calldata _proof) external {SubProof[] memory subProofs = abi.decode(_proof, (SubProof[]));for (uint256 i = 0; i < subProofs.length; ++i) {IVerifier(subProofs[i].verifier).verifyProof(_ctxs, subProofs[i].proof);}require(areVerifiersSufficient(verifiers));}function areVerifiersSufficient(address[] memory _verifiers) internal view override {// Must have exactly 2: sgxGethVerifier + (risc0 or sp1)}}

EIP-8025 的变更

EIP-8025 为 L1 区块验证引入了可选的执行证明。它为共识层带来的基础设施( ProofEngine 、gossip 协定、验证逻辑)之所以是 L1 特有的,仅仅是因为它的类型是: ExecutionProof.public_input包含new_payload_request_root: Root ,而ProofType是一个uint8类型,枚举了一组固定的实作已接受构建了一组固定的(client, zkVM) 配置:VM

ProofType嘉宾计划zkVM 后端
0艾瑟克斯风险0
1艾瑟克斯SP1
2艾瑟克斯齐斯克
3雷斯OpenVM
4雷斯风险0
5雷斯SP1
6雷斯齐斯克

当访客程序集较小且事先已知时,此方法有效,但无法容纳任意Rollup程序。

此 EIP 新增了一个通用的验证原语,而 EIP-8025 现有的介面( ExecutionProofProofTypeverify_execution_proofnotify_new_payloadnotify_forkchoice_updatedprocess_execution_proofrequest_proofsProofAttributes )保持不变。这种通用化与 ERE 类似, EREzkVMVerifier trait 与程式无关,特定的客户机程式建构在其之上。遵循 ERE 的设计,其中CompilerzkVMVerifier后端是独立的 trait,新的Proof容器将合并的ProofType拆分为两个轴: BackendType: uint8 ,仅标识 zkVM 后端;以及program_hash: Bytes32 ,用于标识客户机程式(特定于(guest program, zkVM)说明)。引擎使用backend_type来选择电路 VK; program_hash是电路的公共输入,在验证期间会与public_values一起进行检查:

class ProofPublicInput ( Container ):program_hash: Bytes32public_values: ByteList[MAX_PUBLIC_VALUES_SIZE] class Proof ( Container ):proof_data: ByteList[MAX_PROOF_SIZE]backend_type: BackendTypepublic_input: ProofPublicInput def verify_proof ( self: ProofEngine, proof: Proof ) -> bool : ...

EIP-8025 的verify_execution_proof可以重新实作为verify_proof一个轻量级封装,用于程式码共享,而 gossip 层不会发生任何可观察到的变化:

def verify_execution_proof ( self: ProofEngine, ep: ExecutionProof ) -> bool :backend_type, program_hash = self .resolve_proof_type(ep.proof_type)expected_public_values = serialize_stateless_output(StatelessValidationResult(new_payload_request_root=ep.public_input.new_payload_request_root,successful_validation= True ,chain_config= self .chain_config,)) return self .verify_proof(Proof(proof_data=ep.proof_data,backend_type=backend_type,public_input=ProofPublicInput(program_hash=program_hash,public_values=expected_public_values,),))

「对原生 Rollup 的影响」一节中展示了serialize_stateless_outputStatelessValidationResult的位元组级布局,因为原生 Rollup 合约会在链上重建它。区块有效性与证明验证仍然分离; 诚实证明者指南保持不变。透过 Sidecar 到达的证明(携带证明的交易,请参阅「证明传播」)直接经过verify_proof ,无需 L1 包装器。

程序哈希稳定性(未解决的问题)

原生证明验证的「修复透过客户端版本发布,链上不做任何更改」特性依赖于一个重要的前提条件:链上固定的program_hash必须在 zkVM 补丁之间保持稳定。如果任何补丁更改了杂凑值,则固定旧值的 rollup 将会失效,除非它们进行升级,而升级过程又会回到链上治理层面。

目前还没有任何 zkVM 能直接实现这一点。两大主流候选方案都能辨识出在正常的 SDK/相依性/工具链变更(而不仅仅是电路层修复)下发生变化的工件指纹:

  • Risc0 imageId SHA-256 校验值,其中SystemState { pc: 0, merkle_root }是初始记忆体镜像的merkle_root merkle 根,该初始记忆体镜像包含使用者 ELF 和核心 ELF( binfmt/src/elf.rs#L4355 )。记忆体镜像捕获的是精确的编译字节,因此依赖项更新、工具链更新或核心补丁都会改变imageId即使 STF 语义保持不变。
  • SP1 的programVKey是一个基于(preprocessed_commit, pc_start, ...)的 Poseidon2 哈希表( hypercube/src/verifier/hashable_key.rs#L107 )。与 Risc0 的imageId (编译位元组的纯杂凑值)不同,SP1 的 vk 是在 ELF 档案上执行电路设定时产生的副产品: preprocessed_commit是 AIR 的预处理提交, pc_start来自连结器,因此电路变更、SDK 更新和工具链变更都会影响它,即使使用者的虚拟机原始码完全相同。

直接使用其中任何一个作为链上program_hash ,都会使每次 zkVM 发布都成为 rollup 可见的事件。

更实际的方案是采用间接层:链上program_hash是一个稳定的、由 Rollup 选择的标识符,也是证明的公开输入;而 zkVM 内部标识符是一个私有输入,由客户端维护,并且可以随每次版本发布而更改。证明必须证明两者之间存在关联,从而确保稳定的program_hash能够真正反映实际执行的内容。具体的机制仍是一个开放的设计问题。

使用NATIVE_PROGRAM哨兵的原生汇总完全绕过了这一点:哨兵只是说“L1 目前接受的任何内容”,而接受的集合本身是一个客户端工件,会随著 zkVM 版本更新。

新EIP:携带证明的交易

交易格式

TransactionType: PROOF_TX_TYPETransactionPayloadBody:[chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit,to, value, data, access_list, max_fee_per_blob_gas,blob_versioned_hashes, proofs, public_values_hash,y_parity, r, s]

在哪里:

  • proofs :一个包含(program_hash, backend_type)对的列表。每个program_hash都是一个bytes32值,用于识别特定 zkVM 后端对应的客户机程式(请参阅术语说明)。每个backend_type都是一个uint8的值,并且在列表中必须是唯一的,因为来自同一后端的两个证明不会增加安全性。此列表的长度决定了proof_count
  • public_values_hash :程式公共输出的bytes32杂凑值(所有证明共享,因为所有后端都证明了相同的语句)。

CL 等级的Proof包含原始的public_values位元组;交易主体(以及PUBVALUESHASH操作码)仅公开其杂凑值。合约会重构预期的位元组并比较杂凑值。两个不变式将这两个视图关联起来(任何处理 sidecar 的节点都会在记忆体池传播时进行检查,建构者在组装The Block时也会再次检查):

  • sidecar[i].public_input.program_hash == proofs[i].program_hash and sidecar[i].backend_type == proofs[i].backend_type .
  • sha256(sidecar[i].public_input.public_values) == public_values_hash

这些将 EVM 可见识别码( proofs[i].program_hashpublic_values_hash )绑定到传递给verify_proof底层Proof物件。有关证明如何到达建构器以及 L1 区块证明如何覆盖它们,请参阅证明传播

操作码

新的操作码读取携带证明的交易的字段,对于不携带证明的交易返回零。

操作码输入输出描述
PROGRAMHASH index program_hashbytes32第 i 个证明的程式哈希。索引方式与BLOBHASH相同;如果index >= PROOFCOUNT() ,则传回bytes32(0)
PUBVALUESHASH没有任何public_values_hashbytes32程式公开输出的哈希值(所有证明共享)
PROOFCOUNT没有任何proof_countuint8交易proofs清单的长度

自订Rollup使用PROOFCOUNT()进行迭代,并根据其自身的白名单检查每个PROGRAMHASH(i)

对于原生 Rollup,当第 i 个证明使用的程式是 L1 目前接受的 EVM 执行证明程式时, PROGRAMHASH(i)会传回一个已知的哨兵值(例如bytes32(1) )。这样,合约无需储存每个 zkVM 的特定杂凑值即可检查PROGRAMHASH(i) == NATIVE_PROGRAM ,并自动追踪客户端版本中发布的 L1 升级。

多重证明

proofs清单允许每个Rollup自行选择安全性和成本之间的权衡: [(hash, SP1)]表示单一证明, [(hash_sp1, SP1), (hash_risc0, Risc0)]则要求同一语句由两者独立证明,然后 CL 才会接受交易。合约会读取PROOFCOUNT()并强制执行其自身的最低证明数量。

这使得合约等级的多重证明编排(例如 Taiko 的ComposeVerifier需要同时使用 SGX 和 ZK 验证器)被协议层级的机制所取代。由于proofs位于已签署的交易主体中,因此无法被窜改。

证明传播

证明必须透过记忆体池到达建构者,但无需长期可用。所提出的方法是一种临时边车:证明像EIP-4844 blob 边车一样与交易一起传输。记忆体池节点和建构者在转发或包含交易之前,会使用verify_proof对每个边车条目进行验证(并检查交易格式中的不变式)。然后,建构者在将交易包含到区块之前剥离边车,将其折叠到递归的 L1 区块证明中,然后丢弃边车。验证者只能看到交易主体( proofs清单和public_values_hash )以及 L1 区块证明;他们永远不需要原始证明位元组。因此,L1 区块证明递归地涵盖了The Block中每个携带证明的交易(后量子证明可能足够大,以至于 L1 区块证明每个时隙只能包含一个证明)。

大小。 EIP -8025 将每个证明的MAX_PROOF_SIZE = 400 KiB 。规格没有限制证明的数量len(proofs) ,但记忆体池客户端的大小限制使得 2-3 个证明成为实际的上限。

对现有汇总的影响

下表报告了每个专案链上合约的 Solidity SLOC(非空白、非注解原始码行),分为「核心」Rollup逻辑和原生证明验证将淘汰的证明验证堆叠。

专案证明系统核心SLOC已退役的SLOC退休人员比例
Arbitrum乐观的,WASM VM 19,034 8,181 43.0%
根据乐观的,MIPS VM 17,426 8,907 51.1%
ZKsync 时代有效性,EraVM 10,823 2,379 22.0%
线有效性,直接 EVM 8,111 2,460 30.3%
打火机有效性,无虚拟机器(客制化电路) 5,417 1,699 31.4%
全部的60,811 23,626 38.9%

这些数字是粗略估计。它们仅涵盖链上 Solidity 程式码,不包括链下证明器、序列器以及每个program_hash背后的访客程式。治理介面(多重签章、时间锁、DAO 合约、代理管理员)、合作伙伴特定的桥接器和代理样板代码均未包含在这两列中。

Taiko 的六个合约多验证器堆叠简化为一个收件匣合约:

contract TaikoInbox {mapping(bytes32 => bool) public isTrustedProgram; // whitelisted per-zkVM program hashesuint256 public minProofCount; // multi-proof threshold (eg 2)function proveBatches(BatchMetadata[] calldata metas,Transition[] calldata trans// _proof parameter removed: verified by the CL) external {// Verify all proofs used trusted programs.require(PROOFCOUNT() >= minProofCount, "insufficient proofs");for (uint256 i = 0; i < PROOFCOUNT(); i++) {require(isTrustedProgram[PROGRAMHASH(i)], "untrusted program");}bytes memory publicInputs = buildPublicInputs(metas, trans);require(PUBVALUESHASH() == sha256(publicInputs), "wrong public values");// Accept the batches....}}

一个isTrustedProgram白名单取代了isProgramTrusted (SP1) 和isImageTrusted (Risc0); minProofCount取代了areVerifiersSufficient

对原生汇总的影响

来自原生 Rollup 的 ZK 规范中的 NativeRollup 合约使用了相同的模式。它不是针对validation_result_root检查PROOFROOT ,而是检查PROGRAMHASHPUBVALUESHASHPROOFCOUNT

bytes32 constant NATIVE_PROGRAM = bytes32(uint256(1));uint256 public minProofCount;function advance(BlockParams calldata params) external {bytes32 l1Anchor = blockhash(block.number - 1);bytes32 npRoot = computeNewPayloadRequestRoot(blockHash, params.feeRecipient, params.stateRoot,// ... remaining fields ...getVersionedHashes(params.payloadBlobCount),l1Anchor, bytes32(0));// SSZ-encode the StatelessValidationResult container:// new_payload_request_root (32 bytes) || successful_validation (1 byte)// || chain_id (8 bytes, little-endian).// Must match serialize_stateless_output() in execution-specs.bytes memory expectedPublicValues = SSZ.encodeStatelessValidationResult(npRoot, true, chainId);bytes32 expectedPubValuesHash = sha256(expectedPublicValues);require(PROOFCOUNT() >= minProofCount, "insufficient proofs");for (uint256 i = 0; i < PROOFCOUNT(); i++) {require(PROGRAMHASH(i) == NATIVE_PROGRAM, "not a native program");}require(PUBVALUESHASH() == expectedPubValuesHash, "wrong public values");blockHash = params.blockHash;stateRoot = params.stateRoot;blockNumber = blockNumber + 1;stateRootHistory[blockNumber] = params.stateRoot;}

原生Rollup是指其programHash与 L1 本身所接受的杂凑值相符的 Rollup;L1 升级(例如,fork 更改verify_stateless_new_payload )会自动传播。使用自订虚拟机器的 Rollup 使用相同的模式,但programHash不同。


相关赛道:
来源
免责声明:以上内容仅为作者观点,不代表Followin的任何立场,不构成与Followin相关的任何投资建议。
喜欢
收藏
评论