Skip to content

Commit a68369f

Browse files
authored
feat: UltraHonkZK contract (#11553)
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.
1 parent b168601 commit a68369f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3546
-511
lines changed

barretenberg/acir_tests/bootstrap.sh

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ function test_cmds {
9999
echo FLOW=sol_honk $run_test 1_mul
100100
echo FLOW=sol_honk $run_test slices
101101
echo FLOW=sol_honk $run_test verify_honk_proof
102+
echo FLOW=sol_honk_zk $run_test assert_statement
103+
echo FLOW=sol_honk_zk $run_test 1_mul
104+
echo FLOW=sol_honk_zk $run_test slices
105+
echo FLOW=sol_honk_zk $run_test verify_honk_proof
102106

103107
# barretenberg-acir-tests-bb.js:
104108
# Browser tests.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
set -eux
3+
4+
VFLAG=${VERBOSE:+-v}
5+
BFLAG="-b ./target/program.json"
6+
FLAGS="-c $CRS_PATH $VFLAG"
7+
8+
export PROOF="$PWD/sol_honk_zk_proof"
9+
export PROOF_AS_FIELDS="$PWD/sol_honk_zk_proof_fields.json"
10+
export VK="$PWD/sol_honk_zk_vk"
11+
12+
# Create a proof, write the solidity contract, write the proof as fields in order to extract the public inputs
13+
$BIN prove_ultra_keccak_honk_zk -o $PROOF $FLAGS $BFLAG
14+
$BIN write_vk_ultra_keccak_honk -o $VK $FLAGS $BFLAG
15+
$BIN verify_ultra_keccak_honk_zk -k $VK -p $PROOF $FLAGS
16+
$BIN proof_as_fields_honk $FLAGS -p $PROOF -o $PROOF_AS_FIELDS
17+
$BIN contract_ultra_honk_zk -k $VK $FLAGS -o ZKVerifier.sol
18+
19+
# Export the paths to the environment variables for the js test runner
20+
export VERIFIER_PATH="$PWD/ZKVerifier.sol"
21+
export TEST_PATH=$(realpath "../../sol-test/ZKHonkTest.sol")
22+
export TESTING_HONK="true"
23+
export HAS_ZK="true"
24+
25+
26+
# Use solcjs to compile the generated key contract with the template verifier and test contract
27+
# index.js will start an anvil, on a random port
28+
# Deploy the verifier then send a test transaction
29+
export TEST_NAME=$(basename $PWD)
30+
node ../../sol-test/src/index.js

barretenberg/acir_tests/sol-test/HonkTest.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ contract Test {
99
HonkVerifier verifier;
1010

1111
constructor() {
12-
verifier = new HonkVerifier();
12+
verifier = new HonkVerifier();
1313
}
1414

15-
function test(bytes calldata proof, bytes32[] calldata publicInputs) view public returns(bool) {
15+
function test(bytes calldata proof, bytes32[] calldata publicInputs) public view returns (bool) {
1616
return verifier.verify(proof, publicInputs);
1717
}
1818
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// THIS FILE WILL NOT COMPILE BY ITSELF
2+
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies
3+
4+
pragma solidity >=0.8.4;
5+
6+
import {HonkVerifier} from "./ZKVerifier.sol";
7+
8+
contract Test {
9+
HonkVerifier verifier;
10+
11+
constructor() {
12+
verifier = new HonkVerifier();
13+
}
14+
15+
function test(bytes calldata proof, bytes32[] calldata publicInputs) public view returns (bool) {
16+
return verifier.verify(proof, publicInputs);
17+
}
18+
}

barretenberg/acir_tests/sol-test/src/index.js

+27-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import solc from "solc";
77
// Size excluding number of public inputs
88
const NUMBER_OF_FIELDS_IN_PLONK_PROOF = 93;
99
const NUMBER_OF_FIELDS_IN_HONK_PROOF = 443;
10+
const NUMBER_OF_FIELDS_IN_HONK_ZK_PROOF = 494;
1011

1112
const WRONG_PUBLIC_INPUTS_LENGTH = "0xfa066593";
1213
const SUMCHECK_FAILED = "0x9fc3a218";
1314
const SHPLEMINI_FAILED = "0xa5d82e8a";
15+
const CONSISTENCY_FAILED = "0xa2a2ac83";
16+
const GEMINI_CHALLENGE_IN_SUBGROUP = "0x835eb8f7";
1417

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

58+
// If testing honk is set, then we compile the honk test suite
59+
const testingHonk = getEnvVarCanBeUndefined("TESTING_HONK");
60+
const hasZK = getEnvVarCanBeUndefined("HAS_ZK");
61+
62+
const verifierContract = hasZK ? "ZKVerifier.sol" : "Verifier.sol";
63+
console.log(verifierContract);
5564
export const compilationInput = {
5665
language: "Solidity",
5766
sources: {
5867
"Test.sol": {
5968
content: test,
6069
},
61-
"Verifier.sol": {
70+
[verifierContract]: {
6271
content: verifier,
6372
},
6473
},
@@ -76,10 +85,10 @@ export const compilationInput = {
7685
},
7786
};
7887

79-
// If testing honk is set, then we compile the honk test suite
80-
const testingHonk = getEnvVarCanBeUndefined("TESTING_HONK");
8188
const NUMBER_OF_FIELDS_IN_PROOF = testingHonk
82-
? NUMBER_OF_FIELDS_IN_HONK_PROOF
89+
? hasZK
90+
? NUMBER_OF_FIELDS_IN_HONK_ZK_PROOF
91+
: NUMBER_OF_FIELDS_IN_HONK_PROOF
8392
: NUMBER_OF_FIELDS_IN_PLONK_PROOF;
8493
if (!testingHonk) {
8594
const keyPath = getEnvVar("KEY_PATH");
@@ -98,9 +107,16 @@ if (!testingHonk) {
98107
}
99108

100109
var output = JSON.parse(solc.compile(JSON.stringify(compilationInput)));
101-
if (output.errors.some((e) => e.severity == "error")) {
102-
throw new Error(JSON.stringify(output.errors, null, 2));
103-
}
110+
111+
output.errors.forEach((e) => {
112+
// Stop execution if the contract exceeded the allowed bytecode size
113+
if (e.errorCode == "5574") throw new Error(JSON.stringify(e));
114+
// Throw if there are compilation errors
115+
if (e.severity == "error") {
116+
throw new Error(JSON.stringify(output.errors, null, 2));
117+
}
118+
});
119+
104120
const contract = output.contracts["Test.sol"]["Test"];
105121
const bytecode = contract.evm.bytecode.object;
106122
const abi = contract.abi;
@@ -250,6 +266,10 @@ try {
250266
throw new Error("Sumcheck round failed");
251267
case SHPLEMINI_FAILED:
252268
throw new Error("PCS round failed");
269+
case CONSISTENCY_FAILED:
270+
throw new Error("ZK contract: Subgroup IPA consistency check error");
271+
case GEMINI_CHALLENGE_IN_SUBGROUP:
272+
throw new Error("ZK contract: Gemini challenge error");
253273
default:
254274
throw e;
255275
}

barretenberg/cpp/src/barretenberg/bb/main.cpp

+54-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "barretenberg/dsl/acir_format/proof_surgeon.hpp"
1515
#include "barretenberg/dsl/acir_proofs/acir_composer.hpp"
1616
#include "barretenberg/dsl/acir_proofs/honk_contract.hpp"
17+
#include "barretenberg/dsl/acir_proofs/honk_zk_contract.hpp"
1718
#include "barretenberg/honk/proof_system/types/proof.hpp"
1819
#include "barretenberg/numeric/bitop/get_msb.hpp"
1920
#include "barretenberg/plonk/proof_system/proving_key/serialize.hpp"
@@ -570,7 +571,7 @@ void contract(const std::string& output_path, const std::string& vk_path)
570571
}
571572

572573
/**
573-
* @brief Writes a Honk Solidity verifier contract for an ACIR circuit to a file
574+
* @brief Writes a Honk Zero Knowledge Solidity verifier contract for an ACIR circuit to a file
574575
*
575576
* Communication:
576577
* - stdout: The Solidity verifier contract is written to stdout as a string
@@ -603,6 +604,40 @@ void contract_honk(const std::string& output_path, const std::string& vk_path)
603604
}
604605
}
605606

607+
/**
608+
* @brief Writes a zero-knowledge Honk Solidity verifier contract for an ACIR circuit to a file
609+
*
610+
* Communication:
611+
* - stdout: The Solidity verifier contract is written to stdout as a string
612+
* - Filesystem: The Solidity verifier contract is written to the path specified by outputPath
613+
*
614+
* Note: The fact that the contract was computed is for an ACIR circuit is not of importance
615+
* because this method uses the verification key to compute the Solidity verifier contract
616+
*
617+
* @param output_path Path to write the contract to
618+
* @param vk_path Path to the file containing the serialized verification key
619+
*/
620+
void contract_honk_zk(const std::string& output_path, const std::string& vk_path)
621+
{
622+
using VerificationKey = UltraKeccakZKFlavor::VerificationKey;
623+
using VerifierCommitmentKey = bb::VerifierCommitmentKey<curve::BN254>;
624+
625+
auto g2_data = get_bn254_g2_data(CRS_PATH);
626+
srs::init_crs_factory({}, g2_data);
627+
auto vk = std::make_shared<VerificationKey>(from_buffer<VerificationKey>(read_file(vk_path)));
628+
vk->pcs_verification_key = std::make_shared<VerifierCommitmentKey>();
629+
630+
std::string contract = get_honk_zk_solidity_verifier(vk);
631+
632+
if (output_path == "-") {
633+
writeStringToStdout(contract);
634+
vinfo("contract written to stdout");
635+
} else {
636+
write_file(output_path, { contract.begin(), contract.end() });
637+
vinfo("contract written to: ", output_path);
638+
}
639+
}
640+
606641
/**
607642
* @brief Converts a proof from a byte array into a list of field elements
608643
*
@@ -870,7 +905,7 @@ UltraProver_<Flavor> compute_valid_prover(const std::string& bytecodePath,
870905
using Prover = UltraProver_<Flavor>;
871906

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

887922
auto builder = acir_format::create_circuit<Builder>(program, metadata);
888923
auto prover = Prover{ builder };
889-
init_bn254_crs(prover.proving_key->proving_key.circuit_size);
924+
size_t required_crs_size = prover.proving_key->proving_key.circuit_size;
925+
if constexpr (Flavor::HasZK) {
926+
// Ensure there are enough points to commit to the libra polynomials required for zero-knowledge sumcheck
927+
if (required_crs_size < curve::BN254::SUBGROUP_SIZE * 2) {
928+
required_crs_size = curve::BN254::SUBGROUP_SIZE * 2;
929+
}
930+
}
931+
init_bn254_crs(required_crs_size);
890932

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

12491291
uint32_t honk_recursion = 0;
1250-
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor>) {
1292+
if constexpr (IsAnyOf<Flavor, UltraFlavor, UltraKeccakFlavor, UltraKeccakZKFlavor>) {
12511293
honk_recursion = 1;
12521294
} else if constexpr (IsAnyOf<Flavor, UltraRollupFlavor>) {
12531295
honk_recursion = 2;
@@ -1429,6 +1471,9 @@ int main(int argc, char* argv[])
14291471
} else if (command == "contract_ultra_honk") {
14301472
std::string output_path = get_option(args, "-o", "./target/contract.sol");
14311473
contract_honk(output_path, vk_path);
1474+
} else if (command == "contract_ultra_honk_zk") {
1475+
std::string output_path = get_option(args, "-o", "./target/contract.sol");
1476+
contract_honk_zk(output_path, vk_path);
14321477
} else if (command == "write_vk") {
14331478
std::string output_path = get_option(args, "-o", "./target/vk");
14341479
write_vk(bytecode_path, output_path, recursive);
@@ -1485,13 +1530,18 @@ int main(int argc, char* argv[])
14851530
} else if (command == "prove_ultra_keccak_honk") {
14861531
std::string output_path = get_option(args, "-o", "./proofs/proof");
14871532
prove_honk<UltraKeccakFlavor>(bytecode_path, witness_path, output_path, recursive);
1533+
} else if (command == "prove_ultra_keccak_honk_zk") {
1534+
std::string output_path = get_option(args, "-o", "./proofs/proof");
1535+
prove_honk<UltraKeccakZKFlavor>(bytecode_path, witness_path, output_path, recursive);
14881536
} else if (command == "prove_ultra_rollup_honk") {
14891537
std::string output_path = get_option(args, "-o", "./proofs/proof");
14901538
prove_honk<UltraRollupFlavor>(bytecode_path, witness_path, output_path, recursive);
14911539
} else if (command == "verify_ultra_honk") {
14921540
return verify_honk<UltraFlavor>(proof_path, vk_path) ? 0 : 1;
14931541
} else if (command == "verify_ultra_keccak_honk") {
14941542
return verify_honk<UltraKeccakFlavor>(proof_path, vk_path) ? 0 : 1;
1543+
} else if (command == "verify_ultra_keccak_honk_zk") {
1544+
return verify_honk<UltraKeccakZKFlavor>(proof_path, vk_path) ? 0 : 1;
14951545
} else if (command == "verify_ultra_rollup_honk") {
14961546
return verify_honk<UltraRollupFlavor>(proof_path, vk_path) ? 0 : 1;
14971547
} else if (command == "write_vk_ultra_honk") {

barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.hpp

+28-43
Original file line numberDiff line numberDiff line change
@@ -457,8 +457,6 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
457457
const FF& inner_product_eval_claim)
458458
{
459459

460-
const FF subgroup_generator_inverse = Curve::subgroup_generator_inverse;
461-
462460
// Compute the evaluation of the vanishing polynomia Z_H(X) at X = gemini_evaluation_challenge
463461
const FF vanishing_poly_eval = gemini_evaluation_challenge.pow(SUBGROUP_SIZE) - FF(1);
464462

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

480478
// Compute the evaluations of the challenge polynomial, Lagrange first, and Lagrange last for the fixed small
481479
// subgroup
482-
auto [challenge_poly, lagrange_first, lagrange_last] =
483-
compute_batched_barycentric_evaluations(challenge_polynomial_lagrange,
484-
gemini_evaluation_challenge,
485-
subgroup_generator_inverse,
486-
vanishing_poly_eval);
480+
auto [challenge_poly, lagrange_first, lagrange_last] = compute_batched_barycentric_evaluations(
481+
challenge_polynomial_lagrange, gemini_evaluation_challenge, vanishing_poly_eval);
487482

488483
const FF& concatenated_at_r = libra_evaluations[0];
489484
const FF& big_sum_shifted_eval = libra_evaluations[1];
@@ -493,7 +488,7 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
493488
// Compute the evaluation of
494489
// 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)
495490
FF diff = lagrange_first * big_sum_eval;
496-
diff += (gemini_evaluation_challenge - subgroup_generator_inverse) *
491+
diff += (gemini_evaluation_challenge - Curve::subgroup_generator_inverse) *
497492
(big_sum_shifted_eval - big_sum_eval - concatenated_at_r * challenge_poly);
498493
diff += lagrange_last * (big_sum_eval - inner_product_eval_claim) - vanishing_poly_eval * quotient_eval;
499494

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

528523
// Populate the vector with the powers of the challenges
529-
for (size_t idx_poly = 0; idx_poly < CONST_PROOF_SIZE_LOG_N; idx_poly++) {
530-
size_t current_idx = 1 + LIBRA_UNIVARIATES_LENGTH * idx_poly;
524+
size_t round_idx = 0;
525+
for (auto challenge : multivariate_challenge) {
526+
size_t current_idx = 1 + LIBRA_UNIVARIATES_LENGTH * round_idx; // Compute the current index into the vector
531527
challenge_polynomial_lagrange[current_idx] = FF(1);
532-
for (size_t idx = 1; idx < LIBRA_UNIVARIATES_LENGTH; idx++) {
533-
// Recursively compute the powers of the challenge
534-
challenge_polynomial_lagrange[current_idx + idx] =
535-
challenge_polynomial_lagrange[current_idx + idx - 1] * multivariate_challenge[idx_poly];
528+
for (size_t idx = current_idx + 1; idx < current_idx + LIBRA_UNIVARIATES_LENGTH; idx++) {
529+
// Recursively compute the powers of the challenge up to the length of libra univariates
530+
challenge_polynomial_lagrange[idx] = challenge_polynomial_lagrange[idx - 1] * challenge;
536531
}
532+
round_idx++;
537533
}
538534
return challenge_polynomial_lagrange;
539535
}
@@ -547,51 +543,40 @@ template <typename Curve> class SmallSubgroupIPAVerifier {
547543
* interpolation domain is given by \f$ (1, g, g^2, \ldots, g^{|H| -1 } )\f$
548544
*
549545
* @param coeffs Coefficients of the polynomial to be evaluated, in our case it is the challenge polynomial
550-
* @param z Evaluation point, we are using the Gemini evaluation challenge
546+
* @param r Evaluation point, we are using the Gemini evaluation challenge
551547
* @param inverse_root_of_unity Inverse of the generator of the subgroup H
552548
* @return std::array<FF, 3>
553549
*/
554550
static std::array<FF, 3> compute_batched_barycentric_evaluations(const std::vector<FF>& coeffs,
555551
const FF& r,
556-
const FF& inverse_root_of_unity,
557552
const FF& vanishing_poly_eval)
558553
{
559-
std::array<FF, SUBGROUP_SIZE> denominators;
560554
FF one = FF{ 1 };
561-
FF numerator = vanishing_poly_eval;
562-
563-
numerator *= one / FF(SUBGROUP_SIZE); // (r^n - 1) / n
564-
565-
denominators[0] = r - one;
566-
FF work_root = inverse_root_of_unity; // g^{-1}
567-
//
568-
// Compute the denominators of the Lagrange polynomials evaluated at r
569-
for (size_t i = 1; i < SUBGROUP_SIZE; ++i) {
570-
denominators[i] = work_root * r;
571-
denominators[i] -= one; // r * g^{-i} - 1
572-
work_root *= inverse_root_of_unity;
555+
556+
// Construct the denominators of the Lagrange polynomials evaluated at r
557+
std::array<FF, SUBGROUP_SIZE> denominators;
558+
FF running_power = one;
559+
for (size_t i = 0; i < SUBGROUP_SIZE; ++i) {
560+
denominators[i] = running_power * r - one; // r * g^{-i} - 1
561+
running_power *= Curve::subgroup_generator_inverse;
573562
}
574563

575564
// Invert/Batch invert denominators
576565
if constexpr (Curve::is_stdlib_type) {
577-
for (FF& denominator : denominators) {
578-
denominator = one / denominator;
579-
}
566+
std::transform(
567+
denominators.begin(), denominators.end(), denominators.begin(), [](FF& d) { return d.invert(); });
580568
} else {
581569
FF::batch_invert(&denominators[0], SUBGROUP_SIZE);
582570
}
583-
std::array<FF, 3> result;
584-
585-
// Accumulate the evaluation of the polynomials given by `coeffs` vector
586-
result[0] = FF{ 0 };
587-
for (const auto& [coeff, denominator] : zip_view(coeffs, denominators)) {
588-
result[0] += coeff * denominator; // + coeffs_i * 1/(r * g^{-i} - 1)
589-
}
590-
591-
result[0] = result[0] * numerator; // The evaluation of the polynomials given by its evaluations over H
592-
result[1] = denominators[0] * numerator; // Lagrange first evaluated at r
593-
result[2] = denominators[SUBGROUP_SIZE - 1] * numerator; // Lagrange last evaluated at r
594571

572+
// Construct the evaluation of the polynomial using its evaluations over H, Lagrange first evaluated at r,
573+
// Lagrange last evaluated at r
574+
FF numerator = vanishing_poly_eval * FF(SUBGROUP_SIZE).invert(); // (r^n - 1) / n
575+
std::array<FF, 3> result{ std::inner_product(coeffs.begin(), coeffs.end(), denominators.begin(), FF(0)),
576+
denominators[0],
577+
denominators[SUBGROUP_SIZE - 1] };
578+
std::transform(
579+
result.begin(), result.end(), result.begin(), [&](FF& denominator) { return denominator * numerator; });
595580
return result;
596581
}
597582
};

0 commit comments

Comments
 (0)