Verifying proofs of ONNX model execution in Solidity

ezkl allows developers to prove in zero-knowledge the correct execution of deep learning models. These proofs can be verified in the EVM. As such, smart contract developers can build applications which incorporate the ability to demonstrate knowledge of the output of a neural network model without revealing the secret inputs to said model.

Currently, ezkl's verify-evm uses the snark-verifier tool to generate EVM bytecode that, when given a proof (as well as the outputs of the model), verifies the proof.

This guide will explain how to generate a Solidity verifier that is composable with other contracts.

Proof generation

As per the instructions in the ezkl repository, generate a .params (SRS) file and a proof of model execution.

Generate the SRS:

cargo run --release --bin ezkl -- \
    --bits=16 -K=17 gen-srs \
    --pfsys=kzg --params-path=kzg.params

Generate a proof:

cargo run --release --bin ezkl -- \
    --bits=16 -K=17 prove \
    -D ./examples/onnx/examples/1l_relu/input.json \
    -M ./examples/onnx/examples/1l_relu/network.onnx \
    --proof-path 1l_relu.pf --vk-path 1l_relu.vk \
    --params-path=kzg.params

Solidity verifier contract generation

Run create-evm-verifier which will generate a .code file and a .sol file.

The .code file is used by the verify-evm subcommand, while the .sol file can be integrated with other Solidity development tools like Hardhat or Foundry, and composed with other contracts.

cargo run --release --bin ezkl -- \
    -K=17 --bits=16 create-evm-verifier \
    --pfsys=kzg --deployment-code-path 1l_relu.code \
    --params-path=kzg.params --vk-path 1l_relu.vk \
    --data examples/onnx/examples/1l_relu/input.json \
    --model examples/onnx/examples/1l_relu/network.onnx \
    --sol-code-path 1l_relu.sol

The .sol file now has a convenient verify() function with the interface:

function verify( uint256[NUM_PUBINPUTS] memory pubInputs, bytes memory proof ) public view returns (bool)

NUM_PUBINPUTS is the number of public inputs to the circuit. These include the publicly known outputs of the model. Note that there is currently a pending PR that makes this argument uint256[] memory pubInputs instead. To be sure, please check the contents of the Solidity code you use.

proof is a byte array.

For ease of development, you can print the pubInputs and proof as hexadecimal values using the print-proof-hex subcommand:

cargo run --release --bin ezkl \
    -- print-proof-hex \
    --pfsys=kzg \
    --proof-path 1l_relu.pf

The output will look like:

[0x0000000000000000000000000000000000000000000000000000000000000000, 0x000000000000000000000000000000000000000000000000000000000000013f, 0x000000000000000000000000000000000000000000000000000000000000004a] 0c00366f525876ab0315796a8eb0e3364b42f3627a32dc62892f7e95588ef27b0f675983934f6cc29281b3b5cfc0e318e0fcf00eafd621bfb4e7c9981e77d8a402e4b0e3c5a18ec767e9b1ae5b448aec8a6c93741ced063b44a54f467a5e516a15f3f687cd3c2209eb5a2d...

If you parse the .pf file in Rust, do note that the public inputs are stored as field elements in Montgomery form. The verify() function, importantly, requires the public inputs in non-Montgomery form. can be converted to ethers-rs U256 objects in non-Montgomery form as such:

let bytes = val.to_repr();
let u = U256::from_little_endian(bytes.as_slice());

Limitations

There is a limit on the number of public inputs that your model can have, due to the contract size limit in the EVM. The largest number of public inputs that we have tested and can confirm generates a valid verifier contract is 6. The next highest number of public inputs we have tested and can confirm does not generate a valid verifier contract is 9.

Select a repo