Skip to main content

EVM Verification

WARNING: Halo2 Verifier Contracts MUST be compiled with solc version 0.8.19 or earlier. Failure to do so will result in an OpcodeNotFound when trying to deploy the contract. See related issue for explanation.

Zero Knowledge is highly applicable outside of blockchain. However, many usecases for snarks enable privacy preserving mechanics on the EVM, or simply rely on the decentralized execution environment as a neutral settlement layer for snark computations. For this reason, EVM verification is an integral part of an ACVM backend.

Methodology

An ACVM Backend must implement a trait SmartContract that includes the function eth_contract_from_vk.

fn eth_contract_from_vk(
&self,
mut common_reference_string: &[u8],
circuit: &Circuit,
verification_key: &[u8],
) -> Result<String, Self::Error> { ...

Intuitively, the common_reference_string and verification_key parameters are what is being encoded in the solidity verifier. However, there are differences between how Barretenberg handles verification keys and how Halo2 does it. Specifically, barretenberg serializes the number of public inputs into the verification key, while Halo2 does not need this information for regular proof verification. This becomes an issue when reaching EVM contract generation, since the snark-verifier API needs to know the number of public inputs in order to generate the contract. Thus, circuit is passed as well so that we can extract the number of public inputs that are used by the circuit.

Once we've marshalled all of this information into Halo2-friendly wrappers, we can simply call the []gen_evm_verifier](https://github.com/Ethan-000/halo2_backend/blob/0.1.3/crates/noir_halo2_backend_pse/src/acvm_interop/smart_contract.rs#L23) function written to handle creation of the contract. The snark-verifier SDK will construct a Yul contract that can be used to verify the snark, and passes it back as a string to the ACVM backend API. Nargo will subsequently save this in contract/plonk_vk.sol (you may need to change this to plonk_vk.yul).

Demo Repository

Though integration testing is performed, we also need to prove in full that the Halo2 Backend can be used on chain. This is done with the snark-verifier-tests repository.

This repository shows the basic use of the Halo2 verifier contracts generated by Nargo. Specifically, take the PublicIOVerifier.t.sol test:

// SPDX-License-Identifier: UNLICENSED
pragma solidity <0.9;

import "forge-std/Test.sol";
import {PublicIOVerifierWrapper} from "../contracts/PublicIOVerifierWrapper.sol";
import {YulDeployer} from "../contracts/YulDeployer.sol";

contract PublicIOVerifierTest is Test {
address verifierAddress;
PublicIOVerifierWrapper verifierWrapper;
YulDeployer yulDeployer;

function setUp() public {
yulDeployer = new YulDeployer();
verifierAddress = address(
yulDeployer.deployContract("../snark-verifiers/9_public_io")
);
verifierWrapper = new PublicIOVerifierWrapper(verifierAddress);
}

function test_verifier() external {
// Read in proof from calldata file
string memory proofStr = vm.readFile(
"snark-verifiers/data/9_public_io.calldata"
);

// Verify proof and assert success
verifierWrapper.validProof(7, vm.parseBytes(proofStr));
}
}

First, we need to put our Yul verifier onchain. This is done with the YulDeployer.sol helper contract which accesses the cast CLI using forge ffi to deploy the contract and return the address.

Once we have deployed the verifier, we need to supply it the proof in the correct format. To do that, we need to generate the calldata using the snark-verifier SDK. This repository includes a encode-calldata CLI tool for this purpose. Assuming that we have:

  1. Compiled our contract with nargo compile main
  2. Computed the witness with nargo execute witness
  3. Computed the proof with nargo prove p We will have the artifacts needed to construct the calldata needed to verify our proof. For instance, we can generate the calldata with ./target/release/calldata_encoder ./programs/10_public_io. These unit tests manually store this file, but a FFI call could similarly be made to programatically build and load the calldata into the contract call.

Finally, we can verify our proof. In the given example, we have a simple ZK proof that X+Y=Z, where X is 3 and Y is 4. Z is a public input that we are expected to supply in the EVM. Thus, we can verify the ZK proof using verifierWrapper.validProof(7, vm.parseBytes(proofStr));