Skip to content

Commit

Permalink
feat: UltraHonkZK contract (#11553)
Browse files Browse the repository at this point in the history
This PR introduces an UltraHonk ZK contract and unit tests together with
some refactorings/renamings in the pipeline for generating circuits for
Solidity unit tests.

Flows for testing a deployed contract will be added in a follow-up PR.
  • Loading branch information
maramihali authored Feb 3, 2025
1 parent b168601 commit a68369f
Show file tree
Hide file tree
Showing 51 changed files with 3,546 additions and 511 deletions.
4 changes: 4 additions & 0 deletions barretenberg/acir_tests/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ function test_cmds {
echo FLOW=sol_honk $run_test 1_mul
echo FLOW=sol_honk $run_test slices
echo FLOW=sol_honk $run_test verify_honk_proof
echo FLOW=sol_honk_zk $run_test assert_statement
echo FLOW=sol_honk_zk $run_test 1_mul
echo FLOW=sol_honk_zk $run_test slices
echo FLOW=sol_honk_zk $run_test verify_honk_proof

# barretenberg-acir-tests-bb.js:
# Browser tests.
Expand Down
30 changes: 30 additions & 0 deletions barretenberg/acir_tests/flows/sol_honk_zk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
set -eux

VFLAG=${VERBOSE:+-v}
BFLAG="-b ./target/program.json"
FLAGS="-c $CRS_PATH $VFLAG"

export PROOF="$PWD/sol_honk_zk_proof"
export PROOF_AS_FIELDS="$PWD/sol_honk_zk_proof_fields.json"
export VK="$PWD/sol_honk_zk_vk"

# Create a proof, write the solidity contract, write the proof as fields in order to extract the public inputs
$BIN prove_ultra_keccak_honk_zk -o $PROOF $FLAGS $BFLAG
$BIN write_vk_ultra_keccak_honk -o $VK $FLAGS $BFLAG
$BIN verify_ultra_keccak_honk_zk -k $VK -p $PROOF $FLAGS
$BIN proof_as_fields_honk $FLAGS -p $PROOF -o $PROOF_AS_FIELDS
$BIN contract_ultra_honk_zk -k $VK $FLAGS -o ZKVerifier.sol

# Export the paths to the environment variables for the js test runner
export VERIFIER_PATH="$PWD/ZKVerifier.sol"
export TEST_PATH=$(realpath "../../sol-test/ZKHonkTest.sol")
export TESTING_HONK="true"
export HAS_ZK="true"


# Use solcjs to compile the generated key contract with the template verifier and test contract
# index.js will start an anvil, on a random port
# Deploy the verifier then send a test transaction
export TEST_NAME=$(basename $PWD)
node ../../sol-test/src/index.js
4 changes: 2 additions & 2 deletions barretenberg/acir_tests/sol-test/HonkTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ contract Test {
HonkVerifier verifier;

constructor() {
verifier = new HonkVerifier();
verifier = new HonkVerifier();
}

function test(bytes calldata proof, bytes32[] calldata publicInputs) view public returns(bool) {
function test(bytes calldata proof, bytes32[] calldata publicInputs) public view returns (bool) {
return verifier.verify(proof, publicInputs);
}
}
18 changes: 18 additions & 0 deletions barretenberg/acir_tests/sol-test/ZKHonkTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// THIS FILE WILL NOT COMPILE BY ITSELF
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies

pragma solidity >=0.8.4;

import {HonkVerifier} from "./ZKVerifier.sol";

contract Test {
HonkVerifier verifier;

constructor() {
verifier = new HonkVerifier();
}

function test(bytes calldata proof, bytes32[] calldata publicInputs) public view returns (bool) {
return verifier.verify(proof, publicInputs);
}
}
34 changes: 27 additions & 7 deletions barretenberg/acir_tests/sol-test/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import solc from "solc";
// Size excluding number of public inputs
const NUMBER_OF_FIELDS_IN_PLONK_PROOF = 93;
const NUMBER_OF_FIELDS_IN_HONK_PROOF = 443;
const NUMBER_OF_FIELDS_IN_HONK_ZK_PROOF = 494;

const WRONG_PUBLIC_INPUTS_LENGTH = "0xfa066593";
const SUMCHECK_FAILED = "0x9fc3a218";
const SHPLEMINI_FAILED = "0xa5d82e8a";
const CONSISTENCY_FAILED = "0xa2a2ac83";
const GEMINI_CHALLENGE_IN_SUBGROUP = "0x835eb8f7";

// We use the solcjs compiler version in this test, although it is slower than foundry, to run the test end to end
// it simplifies of parallelising the test suite
Expand Down Expand Up @@ -52,13 +55,19 @@ const [test, verifier] = await Promise.all([
fsPromises.readFile(verifierPath, encoding),
]);

// If testing honk is set, then we compile the honk test suite
const testingHonk = getEnvVarCanBeUndefined("TESTING_HONK");
const hasZK = getEnvVarCanBeUndefined("HAS_ZK");

const verifierContract = hasZK ? "ZKVerifier.sol" : "Verifier.sol";
console.log(verifierContract);
export const compilationInput = {
language: "Solidity",
sources: {
"Test.sol": {
content: test,
},
"Verifier.sol": {
[verifierContract]: {
content: verifier,
},
},
Expand All @@ -76,10 +85,10 @@ export const compilationInput = {
},
};

// If testing honk is set, then we compile the honk test suite
const testingHonk = getEnvVarCanBeUndefined("TESTING_HONK");
const NUMBER_OF_FIELDS_IN_PROOF = testingHonk
? NUMBER_OF_FIELDS_IN_HONK_PROOF
? hasZK
? NUMBER_OF_FIELDS_IN_HONK_ZK_PROOF
: NUMBER_OF_FIELDS_IN_HONK_PROOF
: NUMBER_OF_FIELDS_IN_PLONK_PROOF;
if (!testingHonk) {
const keyPath = getEnvVar("KEY_PATH");
Expand All @@ -98,9 +107,16 @@ if (!testingHonk) {
}

var output = JSON.parse(solc.compile(JSON.stringify(compilationInput)));
if (output.errors.some((e) => e.severity == "error")) {
throw new Error(JSON.stringify(output.errors, null, 2));
}

output.errors.forEach((e) => {
// Stop execution if the contract exceeded the allowed bytecode size
if (e.errorCode == "5574") throw new Error(JSON.stringify(e));
// Throw if there are compilation errors
if (e.severity == "error") {
throw new Error(JSON.stringify(output.errors, null, 2));
}
});

const contract = output.contracts["Test.sol"]["Test"];
const bytecode = contract.evm.bytecode.object;
const abi = contract.abi;
Expand Down Expand Up @@ -250,6 +266,10 @@ try {
throw new Error("Sumcheck round failed");
case SHPLEMINI_FAILED:
throw new Error("PCS round failed");
case CONSISTENCY_FAILED:
throw new Error("ZK contract: Subgroup IPA consistency check error");
case GEMINI_CHALLENGE_IN_SUBGROUP:
throw new Error("ZK contract: Gemini challenge error");
default:
throw e;
}
Expand Down
58 changes: 54 additions & 4 deletions barretenberg/cpp/src/barretenberg/bb/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "barretenberg/dsl/acir_format/proof_surgeon.hpp"
#include "barretenberg/dsl/acir_proofs/acir_composer.hpp"
#include "barretenberg/dsl/acir_proofs/honk_contract.hpp"
#include "barretenberg/dsl/acir_proofs/honk_zk_contract.hpp"
#include "barretenberg/honk/proof_system/types/proof.hpp"
#include "barretenberg/numeric/bitop/get_msb.hpp"
#include "barretenberg/plonk/proof_system/proving_key/serialize.hpp"
Expand Down Expand Up @@ -570,7 +571,7 @@ void contract(const std::string& output_path, const std::string& vk_path)
}

/**
* @brief Writes a Honk Solidity verifier contract for an ACIR circuit to a file
* @brief Writes a Honk Zero Knowledge Solidity verifier contract for an ACIR circuit to a file
*
* Communication:
* - stdout: The Solidity verifier contract is written to stdout as a string
Expand Down Expand Up @@ -603,6 +604,40 @@ void contract_honk(const std::string& output_path, const std::string& vk_path)
}
}

/**
* @brief Writes a zero-knowledge Honk Solidity verifier contract for an ACIR circuit to a file
*
* Communication:
* - stdout: The Solidity verifier contract is written to stdout as a string
* - Filesystem: The Solidity verifier contract is written to the path specified by outputPath
*
* Note: The fact that the contract was computed is for an ACIR circuit is not of importance
* because this method uses the verification key to compute the Solidity verifier contract
*
* @param output_path Path to write the contract to
* @param vk_path Path to the file containing the serialized verification key
*/
void contract_honk_zk(const std::string& output_path, const std::string& vk_path)
{
using VerificationKey = UltraKeccakZKFlavor::VerificationKey;
using VerifierCommitmentKey = bb::VerifierCommitmentKey<curve::BN254>;

auto g2_data = get_bn254_g2_data(CRS_PATH);
srs::init_crs_factory({}, g2_data);
auto vk = std::make_shared<VerificationKey>(from_buffer<VerificationKey>(read_file(vk_path)));
vk->pcs_verification_key = std::make_shared<VerifierCommitmentKey>();

std::string contract = get_honk_zk_solidity_verifier(vk);

if (output_path == "-") {
writeStringToStdout(contract);
vinfo("contract written to stdout");
} else {
write_file(output_path, { contract.begin(), contract.end() });
vinfo("contract written to: ", output_path);
}
}

/**
* @brief Converts a proof from a byte array into a list of field elements
*
Expand Down Expand Up @@ -870,7 +905,7 @@ UltraProver_<Flavor> compute_valid_prover(const std::string& bytecodePath,
using Prover = UltraProver_<Flavor>;

uint32_t honk_recursion = 0;
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor>) {
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor, UltraKeccakZKFlavor>) {
honk_recursion = 1;
} else if constexpr (IsAnyOf<Flavor, UltraRollupFlavor>) {
honk_recursion = 2;
Expand All @@ -886,7 +921,14 @@ UltraProver_<Flavor> compute_valid_prover(const std::string& bytecodePath,

auto builder = acir_format::create_circuit<Builder>(program, metadata);
auto prover = Prover{ builder };
init_bn254_crs(prover.proving_key->proving_key.circuit_size);
size_t required_crs_size = prover.proving_key->proving_key.circuit_size;
if constexpr (Flavor::HasZK) {
// Ensure there are enough points to commit to the libra polynomials required for zero-knowledge sumcheck
if (required_crs_size < curve::BN254::SUBGROUP_SIZE * 2) {
required_crs_size = curve::BN254::SUBGROUP_SIZE * 2;
}
}
init_bn254_crs(required_crs_size);

// output the vk
typename Flavor::VerificationKey vk(prover.proving_key->proving_key);
Expand Down Expand Up @@ -1247,7 +1289,7 @@ void prove_honk_output_all(const std::string& bytecodePath,
using VerificationKey = Flavor::VerificationKey;

uint32_t honk_recursion = 0;
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor>) {
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor, UltraKeccakZKFlavor>) {
honk_recursion = 1;
} else if constexpr (IsAnyOf<Flavor, UltraRollupFlavor>) {
honk_recursion = 2;
Expand Down Expand Up @@ -1429,6 +1471,9 @@ int main(int argc, char* argv[])
} else if (command == "contract_ultra_honk") {
std::string output_path = get_option(args, "-o", "./target/contract.sol");
contract_honk(output_path, vk_path);
} else if (command == "contract_ultra_honk_zk") {
std::string output_path = get_option(args, "-o", "./target/contract.sol");
contract_honk_zk(output_path, vk_path);
} else if (command == "write_vk") {
std::string output_path = get_option(args, "-o", "./target/vk");
write_vk(bytecode_path, output_path, recursive);
Expand Down Expand Up @@ -1485,13 +1530,18 @@ int main(int argc, char* argv[])
} else if (command == "prove_ultra_keccak_honk") {
std::string output_path = get_option(args, "-o", "./proofs/proof");
prove_honk<UltraKeccakFlavor>(bytecode_path, witness_path, output_path, recursive);
} else if (command == "prove_ultra_keccak_honk_zk") {
std::string output_path = get_option(args, "-o", "./proofs/proof");
prove_honk<UltraKeccakZKFlavor>(bytecode_path, witness_path, output_path, recursive);
} else if (command == "prove_ultra_rollup_honk") {
std::string output_path = get_option(args, "-o", "./proofs/proof");
prove_honk<UltraRollupFlavor>(bytecode_path, witness_path, output_path, recursive);
} else if (command == "verify_ultra_honk") {
return verify_honk<UltraFlavor>(proof_path, vk_path) ? 0 : 1;
} else if (command == "verify_ultra_keccak_honk") {
return verify_honk<UltraKeccakFlavor>(proof_path, vk_path) ? 0 : 1;
} else if (command == "verify_ultra_keccak_honk_zk") {
return verify_honk<UltraKeccakZKFlavor>(proof_path, vk_path) ? 0 : 1;
} else if (command == "verify_ultra_rollup_honk") {
return verify_honk<UltraRollupFlavor>(proof_path, vk_path) ? 0 : 1;
} else if (command == "write_vk_ultra_honk") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,6 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
const FF& inner_product_eval_claim)
{

const FF subgroup_generator_inverse = Curve::subgroup_generator_inverse;

// Compute the evaluation of the vanishing polynomia Z_H(X) at X = gemini_evaluation_challenge
const FF vanishing_poly_eval = gemini_evaluation_challenge.pow(SUBGROUP_SIZE) - FF(1);

Expand All @@ -479,11 +477,8 @@ template <typename Curve> class SmallSubgroupIPAVerifier {

// Compute the evaluations of the challenge polynomial, Lagrange first, and Lagrange last for the fixed small
// subgroup
auto [challenge_poly, lagrange_first, lagrange_last] =
compute_batched_barycentric_evaluations(challenge_polynomial_lagrange,
gemini_evaluation_challenge,
subgroup_generator_inverse,
vanishing_poly_eval);
auto [challenge_poly, lagrange_first, lagrange_last] = compute_batched_barycentric_evaluations(
challenge_polynomial_lagrange, gemini_evaluation_challenge, vanishing_poly_eval);

const FF& concatenated_at_r = libra_evaluations[0];
const FF& big_sum_shifted_eval = libra_evaluations[1];
Expand All @@ -493,7 +488,7 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
// Compute the evaluation of
// L_1(X) * A(X) + (X - 1/g) (A(gX) - A(X) - F(X) G(X)) + L_{|H|}(X)(A(X) - s) - Z_H(X) * Q(X)
FF diff = lagrange_first * big_sum_eval;
diff += (gemini_evaluation_challenge - subgroup_generator_inverse) *
diff += (gemini_evaluation_challenge - Curve::subgroup_generator_inverse) *
(big_sum_shifted_eval - big_sum_eval - concatenated_at_r * challenge_poly);
diff += lagrange_last * (big_sum_eval - inner_product_eval_claim) - vanishing_poly_eval * quotient_eval;

Expand Down Expand Up @@ -526,14 +521,15 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
challenge_polynomial_lagrange[0] = FF{ 1 };

// Populate the vector with the powers of the challenges
for (size_t idx_poly = 0; idx_poly < CONST_PROOF_SIZE_LOG_N; idx_poly++) {
size_t current_idx = 1 + LIBRA_UNIVARIATES_LENGTH * idx_poly;
size_t round_idx = 0;
for (auto challenge : multivariate_challenge) {
size_t current_idx = 1 + LIBRA_UNIVARIATES_LENGTH * round_idx; // Compute the current index into the vector
challenge_polynomial_lagrange[current_idx] = FF(1);
for (size_t idx = 1; idx < LIBRA_UNIVARIATES_LENGTH; idx++) {
// Recursively compute the powers of the challenge
challenge_polynomial_lagrange[current_idx + idx] =
challenge_polynomial_lagrange[current_idx + idx - 1] * multivariate_challenge[idx_poly];
for (size_t idx = current_idx + 1; idx < current_idx + LIBRA_UNIVARIATES_LENGTH; idx++) {
// Recursively compute the powers of the challenge up to the length of libra univariates
challenge_polynomial_lagrange[idx] = challenge_polynomial_lagrange[idx - 1] * challenge;
}
round_idx++;
}
return challenge_polynomial_lagrange;
}
Expand All @@ -547,51 +543,40 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
* interpolation domain is given by \f$ (1, g, g^2, \ldots, g^{|H| -1 } )\f$
*
* @param coeffs Coefficients of the polynomial to be evaluated, in our case it is the challenge polynomial
* @param z Evaluation point, we are using the Gemini evaluation challenge
* @param r Evaluation point, we are using the Gemini evaluation challenge
* @param inverse_root_of_unity Inverse of the generator of the subgroup H
* @return std::array<FF, 3>
*/
static std::array<FF, 3> compute_batched_barycentric_evaluations(const std::vector<FF>& coeffs,
const FF& r,
const FF& inverse_root_of_unity,
const FF& vanishing_poly_eval)
{
std::array<FF, SUBGROUP_SIZE> denominators;
FF one = FF{ 1 };
FF numerator = vanishing_poly_eval;

numerator *= one / FF(SUBGROUP_SIZE); // (r^n - 1) / n

denominators[0] = r - one;
FF work_root = inverse_root_of_unity; // g^{-1}
//
// Compute the denominators of the Lagrange polynomials evaluated at r
for (size_t i = 1; i < SUBGROUP_SIZE; ++i) {
denominators[i] = work_root * r;
denominators[i] -= one; // r * g^{-i} - 1
work_root *= inverse_root_of_unity;

// Construct the denominators of the Lagrange polynomials evaluated at r
std::array<FF, SUBGROUP_SIZE> denominators;
FF running_power = one;
for (size_t i = 0; i < SUBGROUP_SIZE; ++i) {
denominators[i] = running_power * r - one; // r * g^{-i} - 1
running_power *= Curve::subgroup_generator_inverse;
}

// Invert/Batch invert denominators
if constexpr (Curve::is_stdlib_type) {
for (FF& denominator : denominators) {
denominator = one / denominator;
}
std::transform(
denominators.begin(), denominators.end(), denominators.begin(), [](FF& d) { return d.invert(); });
} else {
FF::batch_invert(&denominators[0], SUBGROUP_SIZE);
}
std::array<FF, 3> result;

// Accumulate the evaluation of the polynomials given by `coeffs` vector
result[0] = FF{ 0 };
for (const auto& [coeff, denominator] : zip_view(coeffs, denominators)) {
result[0] += coeff * denominator; // + coeffs_i * 1/(r * g^{-i} - 1)
}

result[0] = result[0] * numerator; // The evaluation of the polynomials given by its evaluations over H
result[1] = denominators[0] * numerator; // Lagrange first evaluated at r
result[2] = denominators[SUBGROUP_SIZE - 1] * numerator; // Lagrange last evaluated at r

// Construct the evaluation of the polynomial using its evaluations over H, Lagrange first evaluated at r,
// Lagrange last evaluated at r
FF numerator = vanishing_poly_eval * FF(SUBGROUP_SIZE).invert(); // (r^n - 1) / n
std::array<FF, 3> result{ std::inner_product(coeffs.begin(), coeffs.end(), denominators.begin(), FF(0)),
denominators[0],
denominators[SUBGROUP_SIZE - 1] };
std::transform(
result.begin(), result.end(), result.begin(), [&](FF& denominator) { return denominator * numerator; });
return result;
}
};
Expand Down
Loading

1 comment on commit a68369f

@AztecBot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'C++ Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.05.

Benchmark suite Current: a68369f Previous: dddab22 Ratio
wasmconstruct_proof_ultrahonk_power_of_2/20 14624.863808 ms/iter 13573.823908000002 ms/iter 1.08

This comment was automatically generated by workflow using github-action-benchmark.

CC: @ludamad @codygunton

Please sign in to comment.