diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 93f4bb4eb45..1a9590e4d67 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -61,10 +61,10 @@ if (ENABLE_PIC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_subdirectory(barretenberg/world_state_napi) endif() -add_subdirectory(barretenberg/client_ivc) add_subdirectory(barretenberg/bb) add_subdirectory(barretenberg/boomerang_value_detection) add_subdirectory(barretenberg/circuit_checker) +add_subdirectory(barretenberg/client_ivc) add_subdirectory(barretenberg/commitment_schemes) add_subdirectory(barretenberg/commitment_schemes_recursion) add_subdirectory(barretenberg/common) @@ -87,6 +87,7 @@ add_subdirectory(barretenberg/relations) add_subdirectory(barretenberg/serialize) add_subdirectory(barretenberg/solidity_helpers) add_subdirectory(barretenberg/srs) +add_subdirectory(barretenberg/ultra_vanilla_client_ivc) add_subdirectory(barretenberg/stdlib) add_subdirectory(barretenberg/stdlib_circuit_builders) add_subdirectory(barretenberg/sumcheck) @@ -139,6 +140,7 @@ set(BARRETENBERG_TARGET_OBJECTS $ $ $ + $ $ $ $ diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/commitment_key.hpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/commitment_key.hpp index dca262c5edc..1d107e82296 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/commitment_key.hpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/commitment_key.hpp @@ -55,6 +55,7 @@ template class CommitmentKey { scalar_multiplication::pippenger_runtime_state pippenger_runtime_state; std::shared_ptr> crs_factory; std::shared_ptr> srs; + size_t dyadic_size; CommitmentKey() = delete; @@ -69,6 +70,7 @@ template class CommitmentKey { : pippenger_runtime_state(get_num_needed_srs_points(num_points)) , crs_factory(srs::get_crs_factory()) , srs(crs_factory->get_prover_crs(get_num_needed_srs_points(num_points))) + , dyadic_size(get_num_needed_srs_points(num_points)) {} // Note: This constructor is to be used only by Plonk; For Honk the srs lives in the CommitmentKey diff --git a/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp index 284c500ebef..bb7c633f120 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp @@ -44,6 +44,8 @@ template class RecursiveVerifierTest : public testing using RecursiveVerifier = UltraRecursiveVerifier_; using VerificationKey = typename RecursiveVerifier::VerificationKey; + using AggState = aggregation_state; + using VerifierOutput = bb::stdlib::recursion::honk::UltraRecursiveVerifierOutput; /** * @brief Create a non-trivial arbitrary inner circuit, the proof of which will be recursively verified * @@ -252,11 +254,9 @@ template class RecursiveVerifierTest : public testing OuterBuilder outer_circuit; RecursiveVerifier verifier{ &outer_circuit, verification_key }; - aggregation_state agg_obj = - init_default_aggregation_state(outer_circuit); - bb::stdlib::recursion::honk::UltraRecursiveVerifierOutput output = - verifier.verify_proof(inner_proof, agg_obj); - aggregation_state pairing_points = output.agg_obj; + AggState agg_obj = init_default_aggregation_state(outer_circuit); + VerifierOutput output = verifier.verify_proof(inner_proof, agg_obj); + AggState pairing_points = output.agg_obj; if constexpr (HasIPAAccumulator) { outer_circuit.add_ipa_claim(output.ipa_opening_claim.get_witness_indices()); outer_circuit.ipa_proof = convert_stdlib_proof_to_native(output.ipa_proof); diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.cpp index c07c75b34a3..a211564ae5d 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.cpp @@ -4,6 +4,14 @@ #include "barretenberg/ultra_honk/oink_prover.hpp" namespace bb { +template +UltraProver_::UltraProver_(const std::shared_ptr& proving_key, + const std::shared_ptr& commitment_key) + : proving_key(std::move(proving_key)) + , transcript(std::make_shared()) + , commitment_key(commitment_key) +{} + /** * @brief Create UltraProver_ from a decider proving key. * diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.hpp b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.hpp index 82608299ed5..896e5fd96f4 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.hpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_prover.hpp @@ -37,6 +37,8 @@ template class UltraProver_ { std::shared_ptr commitment_key; + UltraProver_(const std::shared_ptr&, const std::shared_ptr&); + explicit UltraProver_(const std::shared_ptr&, const std::shared_ptr& transcript = std::make_shared()); diff --git a/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/CMakeLists.txt new file mode 100644 index 00000000000..e8120167c08 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/CMakeLists.txt @@ -0,0 +1 @@ +barretenberg_module(ultra_vanilla_client_ivc stdlib_honk_verifier) \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/mock_circuit_producer.hpp b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/mock_circuit_producer.hpp new file mode 100644 index 00000000000..75f451c13bc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/mock_circuit_producer.hpp @@ -0,0 +1,90 @@ + +#include "barretenberg/common/op_count.hpp" +#include "barretenberg/goblin/mock_circuits.hpp" +#include "barretenberg/stdlib_circuit_builders/ultra_circuit_builder.hpp" +#include "barretenberg/ultra_honk/ultra_verifier.hpp" +#include "barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp" + +using namespace bb; + +namespace { + +/** + * @brief Manage the construction of mock app/kernel circuits for the private function execution setting + * @details Per the medium complexity benchmark spec, the first app circuit is size 2^19. Subsequent app and kernel + * circuits are size 2^17. Circuits produced are alternatingly app and kernel. Mock databus data is passed between the + * circuits in a manor conistent with the real architecture in order to facilitate testing of databus consistency + * checks. + */ +class PrivateFunctionExecutionMockCircuitProducer { + using ClientCircuit = UltraVanillaClientIVC::ClientCircuit; + using Flavor = MegaFlavor; + using VerificationKey = Flavor::VerificationKey; + + size_t circuit_counter = 0; + + bool large_first_app = true; // if true, first app is 2^19, else 2^17 + + public: + PrivateFunctionExecutionMockCircuitProducer(bool large_first_app = true) + : large_first_app(large_first_app) + {} + + /** + * @brief Create the next circuit (app/kernel) in a mocked private function execution stack + */ + ClientCircuit create_next_circuit(UltraVanillaClientIVC& ivc, bool force_is_kernel = false) + { + circuit_counter++; + + // Assume only every second circuit is a kernel, unless force_is_kernel == true + bool is_kernel = (circuit_counter % 2 == 0) || force_is_kernel; + + ClientCircuit circuit{ ivc.goblin.op_queue }; + if (is_kernel) { + GoblinMockCircuits::construct_mock_folding_kernel(circuit); // construct mock base logic + mock_databus.populate_kernel_databus(circuit); // populate databus inputs/outputs + ivc.complete_kernel_circuit_logic(circuit); // complete with recursive verifiers etc + } else { + bool use_large_circuit = large_first_app && (circuit_counter == 1); // first circuit is size 2^19 + GoblinMockCircuits::construct_mock_app_circuit(circuit, use_large_circuit); // construct mock app + mock_databus.populate_app_databus(circuit); // populate databus outputs + } + return circuit; + } + + /** + * @brief Tamper with databus data to facilitate failure testing + */ + void tamper_with_databus() { mock_databus.tamper_with_app_return_data(); } + + /** + * @brief Compute and return the verification keys for a mocked private function execution IVC + * @details For testing/benchmarking only. This method is robust at the cost of being extremely inefficient. It + * simply executes a full IVC for a given number of circuits and stores the verification keys along the way. (In + * practice these VKs will be known to a client prover in advance). + * + * @param num_circuits + * @param trace_structure Trace structuring must be known in advance because it effects the VKs + * @return set of num_circuits-many verification keys + */ + auto precompute_verification_keys(const size_t num_circuits, TraceSettings trace_settings) + { + UltraVanillaClientIVC ivc{ + trace_settings + }; // temporary IVC instance needed to produce the complete kernel circuits + + std::vector> vkeys; + + for (size_t idx = 0; idx < num_circuits; ++idx) { + ClientCircuit circuit = create_next_circuit(ivc); // create the next circuit + ivc.accumulate(circuit); // accumulate the circuit + vkeys.emplace_back(ivc.honk_vk); // save the VK for the circuit + } + circuit_counter = 0; // reset the internal circuit counter back to 0 + + return vkeys; + } +}; + +} // namespace \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.cpp b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.cpp new file mode 100644 index 00000000000..77b22b521e2 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.cpp @@ -0,0 +1,91 @@ +#include "barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp" +#include "barretenberg/ultra_honk/oink_prover.hpp" + +namespace bb { + +void UltraVanillaClientIVC::accumulate(Circuit& circuit, const Proof& proof, const std::shared_ptr& vk) +{ + RecursiveVerifier verifier{ &circuit, std::make_shared(&circuit, vk) }; + Accumulator agg_obj = stdlib::recursion::init_default_aggregation_state>(circuit); + accumulator = verifier.verify_proof(proof, agg_obj).agg_obj; +} + +HonkProof UltraVanillaClientIVC::prove(CircuitSource& source, const bool cache_vks) +{ + for (size_t step = 0; step < source.num_circuits(); step++) { + auto [circuit, vk] = source.next(); + if (step == 0) { + accumulator_indices = stdlib::recursion::init_default_agg_obj_indices(circuit); + } else { + accumulate(circuit, previous_proof, previous_vk); + accumulator_indices = accumulator.get_witness_indices(); + } + + circuit.add_pairing_point_accumulator(accumulator_indices); + accumulator_value = { accumulator.P0.get_value(), accumulator.P1.get_value() }; + + auto proving_key = std::make_shared(circuit); + + if (step < source.num_circuits() - 1) { + UltraProver prover{ proving_key, commitment_key }; + previous_proof = prover.construct_proof(); + } else { + // TODO(https://github.com/AztecProtocol/barretenberg/issues/1176) Use UltraZKProver when it exists + UltraProver prover{ proving_key, commitment_key }; + previous_proof = prover.construct_proof(); + } + + previous_vk = vk ? vk : std::make_shared(proving_key->proving_key); + if (cache_vks) { + vk_cache.push_back(previous_vk); + } + } + return previous_proof; +}; + +bool UltraVanillaClientIVC::verify(const Proof& proof, const std::shared_ptr& vk) +{ + + UltraVerifier verifer{ vk }; + bool verified = verifer.verify_proof(proof); + vinfo("proof verified: ", verified); + + using VerifierCommitmentKey = typename Flavor::VerifierCommitmentKey; + auto pcs_verification_key = std::make_shared(); + verified &= pcs_verification_key->pairing_check(accumulator_value[0], accumulator_value[1]); + vinfo("pairing verified: ", verified); + return verified; +} + +/** + * @brief Construct and verify a proof for the IVC + * @note Use of this method only makes sense when the prover and verifier are the same entity, e.g. in + * development/testing. + * + */ +bool UltraVanillaClientIVC::prove_and_verify(CircuitSource& source, const bool cache_vks) +{ + auto start = std::chrono::steady_clock::now(); + prove(source, cache_vks); + auto end = std::chrono::steady_clock::now(); + auto diff = std::chrono::duration_cast(end - start); + vinfo("time to call UltraVanillaClientIVC::prove: ", diff.count(), " ms."); + + start = end; + bool verified = verify(previous_proof, previous_vk); + end = std::chrono::steady_clock::now(); + + diff = std::chrono::duration_cast(end - start); + vinfo("time to verify UltraVanillaClientIVC proof: ", diff.count(), " ms."); + + return verified; +} + +std::vector> UltraVanillaClientIVC::compute_vks( + CircuitSource& source) +{ + prove_and_verify(source, /*cache_vks=*/true); + return vk_cache; +}; + +} // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp new file mode 100644 index 00000000000..b035ce672ce --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include "barretenberg/plonk_honk_shared/execution_trace/execution_trace_usage_tracker.hpp" +#include "barretenberg/plonk_honk_shared/types/aggregation_object_type.hpp" +#include "barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.hpp" +#include "barretenberg/stdlib/plonk_recursion/aggregation_state/aggregation_state.hpp" +#include "barretenberg/ultra_honk/ultra_prover.hpp" +#include "barretenberg/ultra_honk/ultra_verifier.hpp" +#include +namespace bb { + +/** + * @brief A function that produces a set of circuits and possibly their precomputed vks + * @details One has the option of not providing vks--just provide nullptr instead. + * This class is introduced as an experiment. We _could_ just use vectors of vks and shared_ptrs, but this limits + * applicability of the class because, in practice, we don't have sufficient memory to store all circuit builders at + * once. The idea is this class is applicable in both situations we care about testing via mocks (cf the test file for + * UltraVanillaClientIVC, which implements a source of mock circuits), and IVC of circuits written in Noir, where the + * source (not yet implemented) is ACIR and partial witnesses which are processed by our DSL code, expanding blackbox + * function calls. + * @todo Relocate this at the appropriate time, if it does become a standard interface. + */ +template + +class CircuitSource { + public: + struct Output { + Builder circuit; + std::shared_ptr vk; + }; + + virtual Output next(); + virtual size_t num_circuits() const; +}; + +/** + * @brief A class encapsulating multiple sequential steps of the IVC scheme that arises most naturally from recursive + * proof verification. + * + * @details "Vanilla" is in the colloquial sense of meaning "plain". "Client" refers to the fact that this is intended + * for executing proof construction in constrained environments. + */ +class UltraVanillaClientIVC { + + public: + using Flavor = UltraFlavor; + using FF = Flavor::FF; + using Circuit = UltraCircuitBuilder; + using PK = DeciderProvingKey_; + using VK = UltraFlavor::VerificationKey; + using Proof = HonkProof; + + using RecursiveFlavor = UltraRecursiveFlavor_; + using RecursiveVerifier = stdlib::recursion::honk::UltraRecursiveVerifier_; + using RecursiveVK = RecursiveFlavor::VerificationKey; + using Curve = stdlib::bn254; + using Accumulator = stdlib::recursion::aggregation_state; + + /** + * @brief Append a recursive verifier and update the accumulator. + */ + void accumulate(Circuit&, const Proof&, const std::shared_ptr&); + + public: + std::shared_ptr> commitment_key; + Proof previous_proof; + std::shared_ptr previous_vk; + Accumulator accumulator; + std::array accumulator_value; + PairingPointAccumulatorIndices accumulator_indices; + std::vector> vk_cache; + + UltraVanillaClientIVC(const size_t dyadic_size = 1 << 20) + : commitment_key(std::make_shared>(dyadic_size)) + {} + + /** + * @brief Iterate through all circuits and prove each, appending a recursive verifier of the previous proof after + * the first step. + * @param source A source of circuits, possibly accompanied by precomputed verification keys. + * @param cache_vks If true, case the verification key that is computed. + * @return HonkProof A proof of the final circuit which through recursive verification, demonstrates that all + * circuits were satisfied, or one of them was not satisfied, depending on whether it verifies or does not verify. + */ + HonkProof prove(CircuitSource& source, const bool cache_vks = false); + + /** + * @brief Verify an IVC proof. + * @details This verifies the final proof, including (natively) checking the pairing of the two points in the final + * accumulator. + * + * @param proof + * @param vk + * @return true All circuits provided have been satisfied. + * @return false Some circuit provided was not satisfied. + */ + bool verify(const Proof& proof, const std::shared_ptr& vk); + + /** + * @brief Prove and then verify the proof. This is used for testing. + */ + bool prove_and_verify(CircuitSource& source, const bool cache_vks = false); + + /** + * @brief Compute the verification key of each circuit provided by the source. + * @details This runs a full IVC prover. Our public interface provides a faster but more brittle method via dummy + * witnesses. This is a useful fallback that we might want for debugging. Currently it is used to test the prover + * flow that using precomputed verification keys. + */ + std::vector> compute_vks(CircuitSource& source); +}; +} // namespace bb \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.test.cpp new file mode 100644 index 00000000000..653b7d8f641 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.test.cpp @@ -0,0 +1,93 @@ +#include "barretenberg/ultra_vanilla_client_ivc/ultra_vanilla_client_ivc.hpp" +#include "barretenberg/goblin/goblin.hpp" +#include "barretenberg/goblin/mock_circuits.hpp" +#include "barretenberg/protogalaxy/folding_test_utils.hpp" +#include "barretenberg/stdlib_circuit_builders/mega_circuit_builder.hpp" +#include "barretenberg/stdlib_circuit_builders/ultra_circuit_builder.hpp" +#include + +using namespace bb; + +class UltraVanillaClientIVCTests : public ::testing::Test { + protected: + static void SetUpTestSuite() + { + srs::init_crs_factory("../srs_db/ignition"); + srs::init_grumpkin_crs_factory("../srs_db/grumpkin"); + } + + using Flavor = UltraVanillaClientIVC::Flavor; + using Builder = UltraCircuitBuilder; + using FF = typename Flavor::FF; + using VK = Flavor::VerificationKey; + using PK = DeciderProvingKey_; + + class MockCircuitSource : public CircuitSource { + std::vector _sizes; + std::vector> _vks; + uint32_t step{ 0 }; + + public: + MockCircuitSource(const std::vector& sizes) + : _sizes(sizes) + { + std::fill_n(std::back_inserter(_vks), sizes.size(), nullptr); + } + + MockCircuitSource(const MockCircuitSource& source_without_sizes, std::vector> vks) + : _sizes(source_without_sizes._sizes) + , _vks(vks) + {} + + Output next() override + { + Builder circuit; + MockCircuits::construct_arithmetic_circuit(circuit, _sizes[step]); + const auto& vk = _vks[step]; + ++step; + return { circuit, vk }; + } + + size_t num_circuits() const override + { + ASSERT(_sizes.size() == _vks.size()); + return _sizes.size(); + } + }; +}; + +TEST_F(UltraVanillaClientIVCTests, TwoCircuits) +{ + static constexpr size_t LOG_SIZE = 10; + UltraVanillaClientIVC ivc(1 << LOG_SIZE); + MockCircuitSource circuit_source{ { LOG_SIZE, LOG_SIZE } }; + EXPECT_TRUE(ivc.prove_and_verify(circuit_source)); +}; + +TEST_F(UltraVanillaClientIVCTests, ThreeCircuits) +{ + static constexpr size_t LOG_SIZE = 10; + UltraVanillaClientIVC ivc(1 << LOG_SIZE); + MockCircuitSource circuit_source{ { LOG_SIZE, LOG_SIZE, LOG_SIZE } }; + EXPECT_TRUE(ivc.prove_and_verify(circuit_source)); +}; + +/** + * @brief Prove and verify accumulation of an arbitrary set of circuits using precomputed verification keys + * @details The tests precomputed the keys via one pass of the ivc prover, then it usese then in a second pass. + */ +TEST_F(UltraVanillaClientIVCTests, PrecomputedVerificationKeys) +{ + + static constexpr size_t LOG_SIZE = 10; + + UltraVanillaClientIVC ivc_1(1 << LOG_SIZE); + MockCircuitSource circuit_source_no_vks{ { LOG_SIZE, LOG_SIZE } }; + auto vks = ivc_1.compute_vks(circuit_source_no_vks); + + UltraVanillaClientIVC ivc_2(1 << LOG_SIZE); // need to refactor accumulator_value use to reuse ivc_1 + MockCircuitSource circuit_source_with_vks{ circuit_source_no_vks, vks }; + EXPECT_TRUE(ivc_2.prove_and_verify(circuit_source_with_vks)); +}; + +// TODO(https://github.com/AztecProtocol/barretenberg/issues/1177) Implement failure tests \ No newline at end of file