抽象的
該提案的總體目標是透過引入一個標準的 L1 原語,大幅降低 L2 橋接的風險並簡化其操作,更廣泛地說,簡化任何驗證零知識證明的鏈上應用程式。任何專案都可以採用該標準原語來取代其客製化的鏈上驗證器堆疊。這透過兩項變更來實現:
- 將EIP-8025推廣,使共識層證明驗證基礎設施與程序無關,不再依賴 EVM 執行證明。
- 一個新的 EIP ,透過攜帶證明的交易類型和三個操作碼(
PROGRAMHASH、PUBVALUESHASH、PROOFCOUNT)將其暴露給智能合約。
他們共同讓任何專案都能直接繼承 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;}關於術語的說明。 SP1將programVKey稱為“驗證金鑰”,但這與 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取代了isProgramTrusted , sha256(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 現有的介面( ExecutionProof 、 ProofType 、 verify_execution_proof 、 notify_new_payload 、 notify_forkchoice_updated 、 process_execution_proof 、 request_proofs 、 ProofAttributes )保持不變。這種通用化與 ERE 類似, ERE的zkVMVerifier trait 與程式無關,特定的客戶機程式建構在其之上。遵循 ERE 的設計,其中Compiler和zkVMVerifier後端是獨立的 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_output對StatelessValidationResult的位元組級佈局,因為原生 Rollup 合約會在鏈上重建它。區塊有效性與證明驗證仍然分離; 誠實證明者指南保持不變。透過 Sidecar 到達的證明(攜帶證明的交易,請參閱「證明傳播」)直接經過verify_proof ,無需 L1 包裝器。
程序哈希穩定性(未解決的問題)
原生證明驗證的「修復透過客戶端版本發布,鏈上不做任何更改」特性依賴於一個重要的前提條件:鏈上固定的program_hash必須在 zkVM 補丁之間保持穩定。如果任何補丁更改了雜湊值,則固定舊值的 rollup 將會失效,除非它們進行升級,而升級過程又會回到鏈上治理層面。
目前還沒有任何 zkVM 能直接實現這一點。兩大主流候選方案都能辨識出在正常的 SDK/相依性/工具鏈變更(而不僅僅是電路層修復)下發生變化的工件指紋:
- Risc0
imageIdSHA-256 校驗值,其中SystemState { pc: 0, merkle_root }是初始記憶體鏡像的merkle_rootmerkle 根,該初始記憶體鏡像包含使用者 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_hashandsidecar[i].backend_type == proofs[i].backend_type. -
sha256(sidecar[i].public_input.public_values) == public_values_hash。
這些將 EVM 可見識別碼( proofs[i].program_hash 、 public_values_hash )綁定到傳遞給verify_proof底層Proof物件。有關證明如何到達建構器以及 L1 區塊證明如何覆蓋它們,請參閱證明傳播。
操作碼
新的操作碼讀取攜帶證明的交易的字段,對於不攜帶證明的交易返回零。
| 操作碼 | 輸入 | 輸出 | 描述 |
|---|---|---|---|
PROGRAMHASH | index | program_hash ( bytes32 ) | 第 i 個證明的程式哈希。索引方式與BLOBHASH相同;如果index >= PROOFCOUNT() ,則傳回bytes32(0) |
PUBVALUESHASH | 沒有任何 | public_values_hash ( bytes32 ) | 程式公開輸出的哈希值(所有證明共享) |
PROOFCOUNT | 沒有任何 | proof_count ( uint8 ) | 交易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 ,而是檢查PROGRAMHASH 、 PUBVALUESHASH和PROOFCOUNT :
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不同。



