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:
- Compiled our contract with
nargo compile main
- Computed the witness with
nargo execute witness
- 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));