以太坊原生證明驗證機制

以太坊原生證明驗證機制

ethresear.ch·

本提案旨在透過引入標準的 L1 原語來簡化 L2 橋接與鏈上 ZK 證明驗證,並將 EIP-8025 通用化,讓任何專案都能直接繼承以太坊共識層的證明驗證基礎設施。

摘要

本提案的首要目標是透過引入一個標準的 L1 原語(primitive),讓任何專案都能採用它來取代自定義的鏈上驗證器堆疊,從而大幅降低 L2 橋接器以及更廣泛的任何驗證 ZK 證明的鏈上應用程式的風險並簡化其流程。這將透過兩項變更來實現:

  • 泛化 EIP-8025:使共識層的證明驗證基礎設施變得與程式無關(program-agnostic),不再僅限於 EVM 執行證明。

  • 一項新的 EIP:透過一種帶有證明的交易類型(proof-carrying transaction type)和三個操作碼(PROGRAMHASH、PUBVALUESHASH、PROOFCOUNT)將其暴露給智能合約。

結合這兩點,任何專案都能直接繼承 L1 的證明驗證基礎設施,而 zkVM 的修復將透過客戶端版本發佈,而非各個專案獨立的治理升級。

動機

現今,每個以太坊 Rollup 都維護著自定義的鏈上證明驗證基礎設施。ZK Rollup 部署 zkVM 驗證器合約、適配器合約、多重證明調度器以及程式白名單邏輯。Optimistic Rollup 則發佈自己的鏈上欺詐證明 VM(如 Arbitrum 的 WAVM、Optimism 的 Cannon MIPS 機器)以及相關的爭議邏輯。在上述兩種情況下,每個合約都必須針對其特定證明系統或 VM 的漏洞進行獨立的維護、補丁和升級,且每次升級都受限於自定義的多簽或 DAO。這不僅緩慢、危險,且在整個生態系統中造成了重複勞動。

EIP-8025 在以太坊共識層引入了 zkVM 證明驗證,但僅用於 L1 自身的目的:驗證執行負載以實現無狀態和次線性驗證。Rollup 仍然需要自己的鏈上驗證器合約。

然而,EIP-8025 為共識層(CL)帶來的基礎設施——證明引擎(ProofEngine)、證明傳播(proof gossip)和驗證邏輯——本質上並非 L1 專用。如果將其泛化為與程式無關,並透過新的交易類型暴露給智能合約,任何 Rollup(甚至是基於非 EVM 的)都可以將證明驗證委託給共識層。當 zkVM 實作需要修復時,以太坊客戶端團隊會發佈更新軟體,就像今天修復 geth 或 Nethermind 的漏洞一樣:透過客戶端發佈版本,而無需硬分叉。這與 原生 Rollup (native rollups) 的原理相同,但更具普適性:正如原生 Rollup 繼承 L1 的執行環境,原生證明驗證讓任何 Rollup 都能繼承 L1 的證明驗證基礎設施。

雖然本文圍繞 Rollup 展開提案,但同樣的原語也適用於任何在鏈上驗證 ZK 證明的合約:隱私系統、ZK 協處理器、身份驗證、ZK ML 等。

Rollup 今日如何驗證證明

每個 zkVM 供應商都會提供一個通用的 Solidity 驗證器合約(通常是基於 BN254 的 Groth16 或 Plonk 檢查)。程式識別碼和公共值(電路承諾的任何輸入和輸出)會隨證明一起傳遞。以 SP1 為例:

interface ISP1Verifier {
    function verifyProof(
        bytes32 programVKey,         // 程式雜湊 (program hash)
        bytes calldata publicValues, // 公共值 (輸入和/或輸出)
        bytes calldata proofBytes    // 證明數據
    ) external view;
}

關於術語的說明: SP1 將 programVKey 稱為「驗證密鑰 (verification key)」,但這會與 zkVM 自身的電路驗證密鑰混淆。本文將兩者分開:

  • 程式雜湊 (Program hash)(SP1 稱為 programVKey,Risc0 稱為 imageId):一個識別已編譯客體程式(guest program)的 bytes32。由於每個 zkVM 的編譯方式不同(例如 RV32IMA vs RV64IMA),它是針對每個(原始碼, zkVM)配對的。ERE 將其表示為每個後端的 zkVMVerifier::ProgramVk 關聯類型(封裝了 SP1VerifyingKey、Risc0 的 Digest 等)。

  • 驗證密鑰 (Verification key):zkVM 的電路 VK(多項式承諾、域參數)。硬編碼為鏈上驗證器中的常數,每個 zkVM 版本一個,由所有程式共享。

範例:Taiko(多重驗證器)

Taiko 展示了當 Rollup 使用多個證明系統時產生的複雜性。其驗證架構涉及跨三個層級的六個合約(兩個原始驗證器、兩個適配器、一個調度器、一個 SGX 驗證器),每個合約都透過自定義多簽獨立維護和升級。

1. 原始 zkVM 驗證器。 Taiko 同時部署了 SP1 Plonk 驗證器 (SP1Verifier.sol) 和 Risc0 Groth16 驗證器 (RiscZeroGroth16Verifier.sol)。這些是供應商提供的通用驗證器合約。

2. Taiko 專用適配器。 每個原始驗證器都被封裝在一個實作 Taiko IVerifier 介面的適配器合約中:

// TaikoSP1Verifier: SP1 的適配器
contract TaikoSP1Verifier is IVerifier {
    address public sp1RemoteVerifier;                    // 原始 SP1 驗證器
    mapping(bytes32 => bool) public isProgramTrusted;    // 白名單程式

    function 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(...)) 作為日誌摘要(journal digest)。

3. 多重驗證器調度器。 一個 ComposeVerifier 合約協調多個驗證器,並強制要求每個證明都必須經過足夠數量的驗證器驗證:

contract MainnetVerifier is ComposeVerifier {
    address public immutable sgxGethVerifier;    // SGX 驗證器 (必選)
    address public immutable risc0RethVerifier;  // Risc0 選項
    address public immutable sp1RethVerifier;    // SP1 選項

    function 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 {
        // 必須恰好有 2 個:sgxGethVerifier + (risc0 或 sp1)
    }
}

對 EIP-8025 的變更

EIP-8025 為 L1 區塊驗證引入了可選的執行證明。它為共識層帶來的基礎設施(證明引擎、傳播、驗證邏輯)之所以是 L1 專用的,僅是因為其類型定義:ExecutionProof.public_input 攜帶了 new_payload_request_root: Root,而 ProofType 是一個 uint8,列舉了一組固定的已接受 (客戶端, zkVM) 建置版本(參見 Lighthouse 實作):

ProofType客體程式zkVM 後端
0ethrexRisc0
1ethrexSP1
2ethrexZisk
3rethOpenVM
4rethRisc0
5rethSP1
6rethZisk

當客體程式的集合很小且預先已知時,這種方式可行,但無法容納任意的 Rollup 程式。

本 EIP 在此基礎上增加了一個通用的驗證原語,同時保持 EIP-8025 現有的介面(ExecutionProof, ProofType, verify_execution_proof, notify_new_payload, notify_forkchoice_updated, process_execution_proof, request_proofs, ProofAttributes)不變。這種泛化借鑒了 ERE,其 zkVMVerifier 特性(trait)與程式無關,而特定的客體程式則構建於其上。遵循 ERE 的設計,其中 CompilerzkVMVerifier 後端是獨立的特性,新的 Proof 容器將原本混淆的 ProofType 拆分為兩個軸:一個識別 zkVM 後端的 BackendType: uint8,以及一個識別客體程式的 program_hash: Bytes32(特定於 (客體程式, zkVM) 配對,參見術語說明)。引擎使用 backend_type 來選擇電路 VK;program_hash 是電路的公共輸入,在驗證期間與 public_values 一起檢查:

class ProofPublicInput(Container):
    program_hash: Bytes32
    public_values: ByteList[MAX_PUBLIC_VALUES_SIZE]

class Proof(Container):
    proof_data: ByteList[MAX_PROOF_SIZE]
    backend_type: BackendType
    public_input: ProofPublicInput

def verify_proof(self: ProofEngine, proof: Proof) -> bool: ...

EIP-8025 的 verify_execution_proof 可以重新實作為 verify_proof 的薄封裝以實現代碼共享,且在傳播層沒有可觀察到的變化:

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,
        ),
    ))

StatelessValidationResult 上的 serialize_stateless_output 位元組級佈局顯示在對原生 Rollup 的影響中,因為原生 Rollup 合約會在鏈上重建它。區塊有效性與證明驗證保持解耦;誠實證明者指南保持不變。透過邊車(sidecar)到達的證明(帶證明的交易,參見證明傳播)直接通過 verify_proof,無需 L1 封裝。

程式雜湊穩定性(開放問題)

原生證明驗證「修復透過客戶端發佈,鏈上無須變動」的特性取決於一個非平凡的要求:固定在鏈上的 program_hash 必須在 zkVM 補丁更新中保持穩定。如果任何補丁改變了雜湊值,固定了舊值的 Rollup 將會失效(bricked),除非它們進行升級,而這會使升級方案退回到鏈上治理的老路。

目前沒有任何 zkVM 能直接提供這種穩定性。兩個領先的候選方案都會對在正常的 SDK / 依賴項 / 工具鏈變動下會發生變化的產物進行指紋識別,而不僅僅是電路層的修復:

  • Risc0 的 imageId 是對 SystemState { pc: 0, merkle_root } 進行的 SHA-256 雜湊,其中 merkle_root 是初始記憶體映像的 Poseidon2 默克爾根,該映像包含用戶 ELF 和內核 ELF (binfmt/src/elf.rs#L435)。記憶體映像捕捉了精確的編譯位元組,因此依賴項更新、工具鏈更新或內核補丁都會改變 imageId,即使 STF 語義未變。

  • SP1 的 programVKey 是對 (preprocessed_commit, pc_start, ...) 進行的 Poseidon2 雜湊 (hypercube/src/verifier/hashable_key.rs#L107)。與 Risc0 的 imageId(純編譯位元組雜湊)不同,SP1 的 VK 是在 ELF 上運行電路設置(circuit setup)的副產品:preprocessed_commit 是 AIR 的預處理承諾,而 pc_start 來自鏈接器,因此電路變更、SDK 更新和工具鏈變更都會使其變動,即使客體原始碼在位元組層面是相同的。

直接使用其中任何一個作為鏈上 program_hash 都會使每次 zkVM 發佈成為 Rollup 可見的事件。

現實的路徑是增加一個間接層:鏈上 program_hash 是一個穩定的、由 Rollup 選擇的識別碼,作為證明的公共輸入;而 zkVM 內部的識別碼則是私有輸入,由客戶端維護並隨每次發佈自由變更。證明必須證明兩者之間存在關聯,從而確保穩定的 program_hash 確實承諾了所執行的內容。具體機制仍是一個開放的設計問題。

使用 NATIVE_PROGRAM 哨兵值的原生 Rollup 則完全避開了這個問題:哨兵值僅表示「無論 L1 目前接受什麼」,而接受的集合本身就是一個隨 zkVM 發佈而更新的客戶端產物。

新 EIP:帶證明的交易

交易格式

TransactionType: PROOF_TX_TYPE

TransactionPayloadBody:
[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[i].public_input.program_hash == proofs[i].program_hashsidecar[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 區塊證明如何覆蓋它們。

操作碼

新的操作碼讀取帶證明交易的欄位,對於非帶證明交易則返回零。

操作碼輸入輸出描述
PROGRAMHASHindexprogram_hash (bytes32)第 i 個證明的程式雜湊。索引方式同 BLOBHASH;若 index >= PROOFCOUNT() 則返回 bytes32(0)
PUBVALUESHASHpublic_values_hash (bytes32)程式公共輸出的雜湊值(所有證明共享)
PROOFCOUNTproof_count (uint8)交易證明列表的長度

自定義 Rollup 使用 PROOFCOUNT() 進行迭代,並根據自己的白名單檢查每個 PROGRAMHASH(i)

對於原生 Rollup,當第 i 個證明使用的程式是 L1 目前為其自身 EVM 執行證明所接受的程式時,PROGRAMHASH(i) 返回一個知名的哨兵值(例如 bytes32(1))。這樣合約只需檢查 PROGRAMHASH(i) == NATIVE_PROGRAM,而無需存儲特定的各 zkVM 雜湊,並自動遵循客戶端發佈中隨附的 L1 升級。

多重證明

proofs 列表讓每個 Rollup 可以選擇自己的安全/成本權衡:[(hash, SP1)] 是單一證明,[(hash_sp1, SP1), (hash_risc0, Risc0)] 則要求在 CL 接受交易之前,同一個陳述必須由兩個後端獨立證明。合約讀取 PROOFCOUNT() 並強制執行其自身的最低數量要求。

這取代了合約層級的多重證明編排(如 Taiko 的 ComposeVerifier 要求同時具備 SGX 和 ZK 驗證器),轉而使用協議層級的機制。由於 proofs 位於已簽名的交易主體中,因此無法被篡改。

證明傳播

證明必須透過內存池到達構建者,但不需要長期可用性。建議的方法是臨時邊車 (ephemeral sidecar):證明像 EIP-4844 的 blob 邊車一樣隨交易傳輸。內存池節點和構建者在轉發或包含交易之前,會對每個邊車條目運行 verify_proof(並檢查交易格式中的不變量)。構建者隨後在區塊包含之前剝離邊車,將其折疊進遞歸的 L1 區塊證明中,然後將其丟棄。驗證者只會看到交易主體(proofs 列表和 public_values_hash)以及 L1 區塊證明;他們永遠不需要原始證明位元組。因此,L1 區塊證明遞歸地覆蓋了區塊中的每個帶證明交易(後量子證明可能大到讓 L1 限制每個插槽只能有一個證明)。

大小。 EIP-8025 設定 MAX_PROOF_SIZE = 400 KiB 每個證明。規範沒有限制 len(proofs),但內存池客戶端的大小限制使得 2–3 個成為實際的上限。

對現有 Rollup 的影響

下表報告了各專案鏈上合約的 Solidity SLOC(非空行、非注釋源代碼行),分為「核心」Rollup 邏輯和原生證明驗證將取代的證明驗證堆疊。

專案證明系統核心 SLOC被取代 SLOC被取代百分比
ArbitrumOptimistic, WASM VM19,0348,18143.0%
BaseOptimistic, MIPS VM17,4268,90751.1%
ZKsync EraValidity, EraVM10,8232,37922.0%
LineaValidity, direct EVM8,1112,46030.3%
LighterValidity, 無 VM (自定義電路)5,4171,69931.4%
總計60,81123,62638.9%

這些數字是粗略估計。它們僅涵蓋鏈上 Solidity 代碼,不包括離線證明者、排序器以及每個 program_hash 背後的客體程式。治理介面(多簽、時間鎖、DAO 合約、代理管理員)、特定合作夥伴的橋接器和代理樣板代碼均不計入兩列。

Taiko 的六合約多重驗證器堆疊將簡化為單個收件箱(inbox)合約:

contract TaikoInbox {
    mapping(bytes32 => bool) public isTrustedProgram;  // 每個 zkVM 的白名單程式雜湊
    uint256 public minProofCount;                       // 多重證明閾值 (例如 2)

    function proveBatches(
        BatchMetadata[] calldata metas,
        Transition[] calldata trans
        // _proof 參數已移除:由共識層驗證
    ) external {
        // 驗證所有證明是否使用了受信任的程式。
        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");

        // 接受批次。
        ...
    }
}

單個 isTrustedProgram 白名單取代了 isProgramTrusted (SP1) 和 isImageTrusted (Risc0);minProofCount 取代了 areVerifiersSufficient

對原生 Rollup 的影響

來自原生 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,
        // ... 其餘欄位 ...
        getVersionedHashes(params.payloadBlobCount),
        l1Anchor, bytes32(0)
    );

    // SSZ 編碼 StatelessValidationResult 容器:
    //   new_payload_request_root (32 位元組) || successful_validation (1 位元組)
    //   || chain_id (8 位元組, 小端序)。
    // 必須與 execution-specs 中的 serialize_stateless_output() 匹配。
    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 升級(例如改變 verify_stateless_new_payload 的分叉)會自動傳播。使用自定義 VM 的 Rollup 則使用相同的模式,但具有不同的 programHash

        1 則貼文 - 1 位參與者

        [閱讀完整主題](https://ethresear.ch/t/native-proof-verification/24798)

ethresear.ch

相關文章

  1. 透過隱藏公鑰與零知識證明,僅需一筆交易即可將任何以太坊錢包升級至抗量子安全等級

    7 天前

  2. 如果後量子時代的以太坊根本不需要簽名會怎樣?

    大約 2 個月前

  3. 以太坊缺失的驗證原語

    18 天前

  4. 以 TEE 作為 BitVM 類橋接器的驗證者:瓦解規範標籤分發問題

    3 天前

  5. 以太坊錨定系統中的理性終局性停滯與預終局操作風險

    大約 2 個月前