The UltraHonk zk-SNARK verifier is a Rust-based implementation of Noir's UltraHonk Solidity verifier, optimized to be used contextually with zkVerify.
Below, we present a basic use case of verifying an UltraHonk zk-SNARK proof using our implementation:
extern crate alloc;
use alloc::boxed::Box;
use ultrahonk_no_std::{ProofType, verify};
// Sample zero-knowledge proof, vk, and public inputs
let zk_proof_data = Box::from(include_bytes!("../tests/data/zk/proof").as_slice());
let vk: &[u8] = include_bytes!("../tests/data/zk/vk");
let pubs: Vec<[u8; 32]> = include_bytes!("../tests/data/zk/pubs")
.chunks_exact(32)
.map(|c| c.try_into().unwrap())
.collect();
// Use the `ProofType::ZK` variant to wrap around `zk_proof_data`.
let proof: ProofType = ProofType::ZK(zk_proof_data);
// Call the UltraHonk verifier.
assert!(verify::<()>(vk, &proof, &pubs).is_ok()); // successNote: Please note that this verifier currently only supports the following configuration:
- ZK vs Plain: Noir is able to generate "plain" proofs where there is no guarantee for witness privacy, as well as zero-knowledge (ZK) proofs for a given circuit. Our verifier is compatible with both types of proofs,
- Transcript generation: Keccak256 is used as the hash function,
- Recursive proofs: currently not supported.
When working with a plain proof, wrap its bytes in the
ProofType::Plainvariant, like so:let proof: ProofType = ProofType::Plain(Box::new(plain_proof_data));
Please, ensure that you are using the latest version of Noir and follow the official Noir documentation to generate a sample proof. At the end of the process you should have four binary files generated by bb, namely, the proof, vk, public_inputs, and vk_hash.
If you wish to generate a zero-knowledge proof, you will need to use -t evm to instruct bb of the fact. Alternatively, use -t evm-no-zk to specify that you would like bb to use the non zero-knowledge (plain) version of UltraHonk for proof generation. Please note that in both cases, Keccak256 is going to be used as the hash function. Finally, add the the write_vk subcommand to instruct bb to also generate the verification key for you.
Worked Example: Suppose that our Noir project name is
hello_worldand that we want to generate a ZK proof along with the verification key. Further, suppose that the bytecode file and witness file are found in thetargetdirectory. We would also like the generated artifacts frombbto be placed inside thetargetdirectory. Then, the appropriate command to issue is:
bb prove -t evm -b "./target/hello_world.json" -w "./target/hello_world.gz" -o ./target --write_vk
The binary files output by Noir are ready to use out of the box. However, for your ease of submission to zkVerify, below we provide a Bash script for converting them into hexadecimal files zkv_proof.hex, zkv_vk.hex, and zkv_pubs.hex.
Please run the following script, adjusting the path to the proof, vk, and public inputs files, accordingly:
#!/usr/bin/env bash
PROOF_TYPE="ZK" # Set to "Plain" if you are using the non-zk variant of UltraHonk
ARTIFACT_DIR_PATH="./target" # Adjust path depending on where the Noir-generated artifacts are
OUTPUT_DIR_PATH="./target" # Adjust path based on where you would like the zkv-ready artifacts to be placed
# You may ignore these:
PROOF_FILE_PATH="${ARTIFACT_DIR_PATH}/proof"
VK_FILE_PATH="${ARTIFACT_DIR_PATH}/vk"
PUBS_FILE_PATH="${ARTIFACT_DIR_PATH}/public_inputs"
ZKV_PROOF_HEX_FILE_PATH="${OUTPUT_DIR_PATH}/zkv_proof.hex"
ZKV_VK_HEX_FILE_PATH="${OUTPUT_DIR_PATH}/zkv_vk.hex"
ZKV_PUBS_HEX_FILE_PATH="${OUTPUT_DIR_PATH}/zkv_pubs.hex"
# Get bb version and format it (e.g., 3.0.3 -> V3_0)
BB_VERSION_FULL=$(bb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
# Extract major and minor, then join with underscore
BB_VERSION_KEY=$(echo "${BB_VERSION_FULL}" | awk -F. '{print "V" $1 "_" $2}')
# Convert proof to valid JSON
if [ -f "${PROOF_FILE_PATH}" ]; then
# Read bytes and remove newlines
PROOF_BYTES=$(xxd -p -c 1000000 "${PROOF_FILE_PATH}" | tr -d '\n')
printf '{\n "%s": {\n "%s": "0x%s"\n }\n}\n' \
"${BB_VERSION_KEY}" \
"${PROOF_TYPE}" \
"${PROOF_BYTES}" > "${ZKV_PROOF_HEX_FILE_PATH}"
echo "✅ Generated ${ZKV_PROOF_HEX_FILE_PATH} with version key ${BB_VERSION_KEY}"
else
echo "❌ Error: Proof file not found." >&2
fi
# Convert vk to hexadecimal format
{
if [ -f "${VK_FILE_PATH}" ]; then
printf "\"0x%s\"\n" "$(xxd -p -c 0 "${VK_FILE_PATH}")" > "${ZKV_VK_HEX_FILE_PATH}"
echo "✅ 'vk' hex file generated at ${ZKV_VK_HEX_FILE_PATH}."
else
echo "❌ Error: Verification key file '${VK_FILE_PATH}' not found. Skipping." >&2
fi
}
# Convert public inputs to hexadecimal format
{
if [ -f "${PUBS_FILE_PATH}" ]; then
xxd -p -c 32 "${PUBS_FILE_PATH}" | sed 's/.*/"0x&"/' | paste -sd, - | sed 's/.*/[&]/' > "${ZKV_PUBS_HEX_FILE_PATH}"
echo "✅ 'pubs' hex file generated at ${ZKV_PUBS_HEX_FILE_PATH}."
else
echo "❌ Error: Public inputs file '${PUBS_FILE_PATH}' not found. Skipping." >&2
fi
}And with that, you're all set!