From b848b0e1e1a670e770324aba35e3eaecd846e898 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:33:21 +0200 Subject: [PATCH 001/127] add sumcheck frontend --- .../include/icicle/backend/sumcheck_backend.h | 102 +++++++++++++ icicle/include/icicle/sumcheck/sumcheck.h | 138 ++++++++++++++++-- .../include/icicle/sumcheck/sumcheck_config.h | 35 +++++ .../include/icicle/sumcheck/sumcheck_proof.h | 47 ++++++ .../sumcheck/sumcheck_transcript_config.h | 6 +- 5 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 icicle/include/icicle/backend/sumcheck_backend.h create mode 100644 icicle/include/icicle/sumcheck/sumcheck_config.h create mode 100644 icicle/include/icicle/sumcheck/sumcheck_proof.h diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h new file mode 100644 index 0000000000..afd4f42fa1 --- /dev/null +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include "icicle/program/returning_value_program.h" +#include "icicle/sumcheck/sumcheck_config.h" +#include "icicle/sumcheck/sumcheck_proof.h" +#include "icicle/sumcheck/sumcheck_transcript_config.h" + +template +using CombineFunction = ReturningValueProgram; + +namespace icicle { + /** + * @brief Abstract base class for Sumcheck backend implementations. + * + * This backend handles the core logic for Sumcheck operations such as calculating the round polynomials + * per round and building the Sumcheck proof for the verifier, + * Derived classes will provide specific implementations for various devices (e.g., CPU, GPU). + */ + template + class SumcheckBackend + { + public: + /** + * @brief Constructor for the SumcheckBackend class. + * + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations + * @param transcript_config Configuration for encoding and hashing prover messages. + */ + SumcheckBackend(F& claimed_sum, SumcheckTranscriptConfig&& transcript_config = SumcheckTranscriptConfig()) + : m_claimed_sum(claimed_sum), m_transcript_config(transcript_config) + { + } + + virtual ~SumcheckBackend() = default; + + /** + * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. + * @param input_polynomials a vector of MLE polynomials to process + * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. + * @param config Configuration for the Sumcheck operation. + * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. + * @return Error code of type eIcicleError. + */ + virtual eIcicleError get_proof( + const std::vector< std::vector* >& input_polynomials, + const CombineFunction& combine_function, + SumCheckConfig& config, + SumCheckProof& sumcheck_proof /*out*/) const = 0; + + /** + * @brief Calculate alpha based on m_transcript_config and the round polynomial. + * @param round_polynomial a vector of MLE polynomials evaluated at x=0,1,2... + * @return alpha + */ + F get_alpha(std::vector< F >& round_polynomial) = 0; + + + const F& get_claimed_sum() const {return m_claimed_sum;} + + protected: + F m_claimed_sum; ///< Vector of hash functions for each layer. + SumcheckTranscriptConfig&& m_transcript_config; ///< Size of each leaf element in bytes. + }; + + + + /*************************** Backend Factory Registration ***************************/ + template + using SumcheckFactoryImpl = std::function&& transcript_config, + std::shared_ptr >& backend /*OUT*/)>; + + /** + * @brief Register a Sumcheck backend factory for a specific device type. + * + * @param deviceType String identifier for the device type. + * @param impl Factory function that creates tSumcheckBackend. + */ + template + void register_sumcheck_factory(const std::string& deviceType, SumcheckFactoryImpl impl); + + /** + * @brief Macro to register a Sumcheck backend factory. + * + * This macro registers a factory function for a specific backend by calling + * `register_sumcheck_factory` at runtime. + */ +#define REGISTER_SUMCHECK_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ + namespace { \ + static bool UNIQUE(_reg_sumcheck) = []() -> bool { \ + register_sumcheck_factory(DEVICE_TYPE, FUNC); \ + return true; \ + }(); \ + } + +} // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index e624270e04..a39fd31d16 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -1,13 +1,40 @@ #pragma once +#include "icicle/errors.h" +#include "icicle/program/returning_value_program.h" +#include "icicle/sumcheck/sumcheck_proof.h" +#include "icicle/sumcheck/sumcheck_config.h" #include "icicle/sumcheck/sumcheck_transcript_config.h" - +#include "icicle/backend/sumcheck_backend.h" namespace icicle { + template + class Sumcheck; + + // ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); + + // template class Sumcheck; + // /** + // * @brief Static factory method to create a Sumcheck instance. + // * + // * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + // * when evaluated over all possible Boolean input combinations + // * @param transcript_config Configuration for encoding and hashing prover messages. + // * @return A Sumcheck object initialized with the specified backend. + // */ + template + Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) { + std::shared_ptr> backend; + // ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); + Sumcheck sumcheck{backend}; + return sumcheck; + } /** - * @brief Sumcheck protocol implementation for a given field type. + * @brief Class for performing Sumcheck operations. * - * This class encapsulates the Sumcheck protocol, including its transcript configuration. + * This class provides a high-level interface for building and managing Sumcheck. The underlying + * logic for sumcheck operations, such as building and verifying, is delegated to the + * backend, which may be device-specific (e.g., CPU, GPU). * * @tparam F The field type used in the Sumcheck protocol. */ @@ -15,19 +42,110 @@ namespace icicle { class Sumcheck { public: + /** + * @brief Static factory method to create a Sumcheck instance. + * + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations + * @param transcript_config Configuration for encoding and hashing prover messages. + * @return A Sumcheck object initialized with the specified backend. + */ + static Sumcheck + create(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + { + return create_sumcheck(claimed_sum, std::move(transcript_config)); + } + /** - * @brief Constructs a Sumcheck instance with the given transcript configuration. - * @param transcript_config The configuration for the Sumcheck transcript. + * @brief Constructor for the Sumcheck class. + * @param backend Shared pointer to the backend responsible for Sumcheck operations. */ - explicit Sumcheck(SumcheckTranscriptConfig&& transcript_config) - : m_transcript_config(std::move(transcript_config)) - { + explicit Sumcheck(std::shared_ptr> backend) : m_backend{std::move(backend)} {} + + + /** + * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. + * @param input_polynomials a vector of MLE polynomials to process + * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. + * @param config Configuration for the Sumcheck operation. + * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. + * @return Error code of type eIcicleError. + */ + eIcicleError get_proof(const std::vector< std::vector< F >* >& input_polynomials, + const CombineFunction& combine_function, + SumCheckConfig& config, + SumCheckProof& sumcheck_proof /*out*/) const{ + return m_backend->get_proof(input_polynomials, combine_function, config, sumcheck_proof); + } + + /** + * @brief Verify an element against the Sumcheck round polynomial. + * @param sumcheck_proof The SumCheckProof object includes the round polynomials. + * @param valid output valid bit. True if the Proof is valid, false otherwise. + * @return Error code of type eIcicleError indicating success or failure. + */ + eIcicleError verify(SumCheckProof& sumcheck_proof, bool& valid /*out*/) { + const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); + // verify that the sum of round_polynomial-0 is the clamed_sum + const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); + F round_poly_0_sum = round_poly_0[0]; + for (int round_idx=1; round_idx < nof_rounds-1; round_idx++) { + round_poly_0_sum = round_poly_0_sum + round_poly_0[round_idx]; + } + const F& claimed_sum = m_backend->get_claimed_sum(); + if (round_poly_0_sum != claimed_sum) { + valid = false; + ICICLE_LOG_ERROR << "verification failed: sum of round polynomial 0 (" << round_poly_0_sum << ") != claimed_sum(" << claimed_sum << ")"; + return eIcicleError::SUCCESS; + } + + for (int round_idx=0; round_idx < nof_rounds-1; round_idx++) { + const std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); + F alpha = m_backend->get_alpha(round_poly); + F alpha_value = lagrange_interpolation(round_poly, alpha); + const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx+1); + F expected_alpha_value = next_round_poly[0] + next_round_poly[1]; + if (alpha_value != expected_alpha_value) { + valid = false; + ICICLE_LOG_ERROR << "verification failed on round: " << round_idx; + return eIcicleError::SUCCESS; + } + } + valid = true; + return eIcicleError::SUCCESS; } - // Add public methods for protocol operations, e.g., prove, verify. private: - SumcheckTranscriptConfig m_transcript_config; ///< Transcript configuration for the protocol. + std::shared_ptr> m_backend; ///< Shared pointer to the backend responsible for Sumcheck operations. + + + // Receive the polynomial in evaluation on x=0,1,2... + // retuרn the evaluation of the polynomial at x + F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) { + uint poly_degree = poly_evaluations.size(); + F result = F::zero(); + + // For each coefficient we want to compute + for (uint i = 0; i < poly_degree; ++i) { + // Compute the i-th coefficient + F numerator = poly_evaluations[i]; + F denumerator = F::one(); + + // Use Lagrange interpolation formula + const F i_field = F::from(i); + for (uint j = 0; j < poly_degree; ++j) { + if (j != i) { + const F j_field = F::from(j); + numerator = numerator * (x-j_field); + denumerator = denumerator * (i_field - j_field); + } + } + result = result + (numerator * F::inverse(denumerator)); + } + return result; + } }; + } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/sumcheck/sumcheck_config.h b/icicle/include/icicle/sumcheck/sumcheck_config.h new file mode 100644 index 0000000000..448ebc96e3 --- /dev/null +++ b/icicle/include/icicle/sumcheck/sumcheck_config.h @@ -0,0 +1,35 @@ +#pragma once + +#include "icicle/runtime.h" +#include "icicle/config_extension.h" + +namespace icicle { + /** + * @brief Configuration structure for Sumcheck operations. + * + * This structure holds the configuration options for sumcheck operations. + * It allows specifying whether the data (input MLE polynomials) + * reside on the device (e.g., GPU) or the host (e.g., CPU), and supports both synchronous and asynchronous + * execution modes, as well as backend-specific extensions. + */ + + struct SumCheckConfig { + icicleStreamHandle stream = nullptr; /**< Stream for asynchronous execution. Default is nullptr. */ + uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ + bool are_inputs_on_device = false; /**< True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. */ + bool are_outputs_on_device = false; /**< True if outputs reside on the device, false if on the host. Default is false. */ + bool is_async = false; /**< True to run the hash asynchronously, false to run synchronously. Default is false. */ + ConfigExtension* ext = nullptr; /**< Pointer to backend-specific configuration extensions. Default is nullptr. */ + }; + + /** + * @brief Generates a default configuration for Sumcheck operations. + * + * This function provides a default configuration for Sumcheck operations with synchronous execution + * and all data (leaves, tree results, and paths) residing on the host (CPU). + * + * @return A default SumCheckConfig with host-based execution and no backend-specific extensions. + */ + static SumCheckConfig default_sumcheck_config() { return SumCheckConfig(); } + +} // namespace icicle diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h new file mode 100644 index 0000000000..57d45e0166 --- /dev/null +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace icicle { + + /** + * @brief Represents a sumcheck proof. + * + * This class encapsulates the sumcheck proof, contains the evaluations of the round polynomials + * for each layer at the sumcheck proof. + * Evaluations are at x = 0, 1, 2 ... K + * Where K is the degree of the combiine function used at the sumcheck protocol. + * + * @tparam S Type of the field element (e.g., prime field or extension field elements). + */ + +template +class SumCheckProof { + public: + // Constructor + SumCheckProof(uint nof_round_polynomials, uint round_polynomial_degree) : + m_round_polynomilas(nof_round_polynomials, std::vector(round_polynomial_degree+1)) { + if (nof_round_polynomials == 0) { + ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials <<") in the proof must be >0"; + } + } + + // set the value of polynomial round_polynomial_idx at x = evaluation_idx + void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, S& value) { + m_round_polynomilas[round_polynomial_idx][evaluation_idx] = value; + } + + // return a reference to the round polynomial generated at round # round_polynomial_idx + const std::vector& get_round_polynomial(int round_polynomial_idx) const { + return m_round_polynomilas[round_polynomial_idx]; + } + + uint get_nof_round_polynomial() const {return m_round_polynomilas.size();} + uint get_round_polynomial_size() const {return m_round_polynomilas[0].size() + 1;} + + private: + std::vector > m_round_polynomilas; // logN vectors of round_poly_degree elements +}; + +} // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h index 6a2b7637d3..cacbff326e 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h @@ -1,7 +1,7 @@ #pragma once #include "icicle/hash/hash.h" - +#include "icicle/hash/keccak.h" /* This file defines the SumcheckTranscriptConfig class, which specifies how to encode and hash prover messages * in the Sumcheck protocol, ensuring deterministic randomness generation and correct message encoding. * @@ -46,10 +46,10 @@ namespace icicle { template class SumcheckTranscriptConfig { - public: public: // Default Constructor - SumcheckTranscriptConfig() : m_little_endian(true), m_seed_rng(0) {} + SumcheckTranscriptConfig() : m_little_endian(true), m_seed_rng(F::from(0)), m_hasher(std::move(create_keccak_256_hash())) {} + // Constructor with byte vector for labels SumcheckTranscriptConfig( From 92f3579e36deb675ad0f47b6bc6f24ab10f89d83 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:39:32 +0200 Subject: [PATCH 002/127] format --- .../include/icicle/backend/sumcheck_backend.h | 35 +++++----- icicle/include/icicle/sumcheck/sumcheck.h | 64 ++++++++++--------- .../include/icicle/sumcheck/sumcheck_config.h | 10 +-- .../include/icicle/sumcheck/sumcheck_proof.h | 30 +++++---- .../sumcheck/sumcheck_transcript_config.h | 6 +- 5 files changed, 76 insertions(+), 69 deletions(-) diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index afd4f42fa1..a9c8decd60 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -14,9 +14,9 @@ using CombineFunction = ReturningValueProgram; namespace icicle { /** * @brief Abstract base class for Sumcheck backend implementations. - * - * This backend handles the core logic for Sumcheck operations such as calculating the round polynomials - * per round and building the Sumcheck proof for the verifier, + * + * This backend handles the core logic for Sumcheck operations such as calculating the round polynomials + * per round and building the Sumcheck proof for the verifier, * Derived classes will provide specific implementations for various devices (e.g., CPU, GPU). */ template @@ -26,7 +26,7 @@ namespace icicle { /** * @brief Constructor for the SumcheckBackend class. * - * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) * when evaluated over all possible Boolean input combinations * @param transcript_config Configuration for encoding and hashing prover messages. */ @@ -37,7 +37,7 @@ namespace icicle { virtual ~SumcheckBackend() = default; - /** + /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. * @param input_polynomials a vector of MLE polynomials to process * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. @@ -46,35 +46,32 @@ namespace icicle { * @return Error code of type eIcicleError. */ virtual eIcicleError get_proof( - const std::vector< std::vector* >& input_polynomials, - const CombineFunction& combine_function, + const std::vector*>& input_polynomials, + const CombineFunction& combine_function, SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const = 0; - /** + /** * @brief Calculate alpha based on m_transcript_config and the round polynomial. * @param round_polynomial a vector of MLE polynomials evaluated at x=0,1,2... * @return alpha */ - F get_alpha(std::vector< F >& round_polynomial) = 0; + F get_alpha(std::vector& round_polynomial) = 0; - - const F& get_claimed_sum() const {return m_claimed_sum;} + const F& get_claimed_sum() const { return m_claimed_sum; } protected: - F m_claimed_sum; ///< Vector of hash functions for each layer. + F m_claimed_sum; ///< Vector of hash functions for each layer. SumcheckTranscriptConfig&& m_transcript_config; ///< Size of each leaf element in bytes. }; - - /*************************** Backend Factory Registration ***************************/ template using SumcheckFactoryImpl = std::function&& transcript_config, - std::shared_ptr >& backend /*OUT*/)>; + std::shared_ptr>& backend /*OUT*/)>; /** * @brief Register a Sumcheck backend factory for a specific device type. @@ -91,10 +88,10 @@ namespace icicle { * This macro registers a factory function for a specific backend by calling * `register_sumcheck_factory` at runtime. */ -#define REGISTER_SUMCHECK_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ +#define REGISTER_SUMCHECK_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ namespace { \ - static bool UNIQUE(_reg_sumcheck) = []() -> bool { \ - register_sumcheck_factory(DEVICE_TYPE, FUNC); \ + static bool UNIQUE(_reg_sumcheck) = []() -> bool { \ + register_sumcheck_factory(DEVICE_TYPE, FUNC); \ return true; \ }(); \ } diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index a39fd31d16..2c0ddfa9d3 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -16,13 +16,14 @@ namespace icicle { // /** // * @brief Static factory method to create a Sumcheck instance. // * - // * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + // * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) // * when evaluated over all possible Boolean input combinations // * @param transcript_config Configuration for encoding and hashing prover messages. // * @return A Sumcheck object initialized with the specified backend. // */ template - Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) { + Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + { std::shared_ptr> backend; // ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); Sumcheck sumcheck{backend}; @@ -34,7 +35,7 @@ namespace icicle { * * This class provides a high-level interface for building and managing Sumcheck. The underlying * logic for sumcheck operations, such as building and verifying, is delegated to the - * backend, which may be device-specific (e.g., CPU, GPU). + * backend, which may be device-specific (e.g., CPU, GPU). * * @tparam F The field type used in the Sumcheck protocol. */ @@ -42,16 +43,15 @@ namespace icicle { class Sumcheck { public: - /** - * @brief Static factory method to create a Sumcheck instance. - * - * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) - * when evaluated over all possible Boolean input combinations - * @param transcript_config Configuration for encoding and hashing prover messages. - * @return A Sumcheck object initialized with the specified backend. - */ - static Sumcheck - create(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + /** + * @brief Static factory method to create a Sumcheck instance. + * + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations + * @param transcript_config Configuration for encoding and hashing prover messages. + * @return A Sumcheck object initialized with the specified backend. + */ + static Sumcheck create(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) { return create_sumcheck(claimed_sum, std::move(transcript_config)); } @@ -62,8 +62,7 @@ namespace icicle { */ explicit Sumcheck(std::shared_ptr> backend) : m_backend{std::move(backend)} {} - - /** + /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. * @param input_polynomials a vector of MLE polynomials to process * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. @@ -71,10 +70,12 @@ namespace icicle { * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ - eIcicleError get_proof(const std::vector< std::vector< F >* >& input_polynomials, - const CombineFunction& combine_function, - SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) const{ + eIcicleError get_proof( + const std::vector*>& input_polynomials, + const CombineFunction& combine_function, + SumCheckConfig& config, + SumCheckProof& sumcheck_proof /*out*/) const + { return m_backend->get_proof(input_polynomials, combine_function, config, sumcheck_proof); } @@ -84,26 +85,28 @@ namespace icicle { * @param valid output valid bit. True if the Proof is valid, false otherwise. * @return Error code of type eIcicleError indicating success or failure. */ - eIcicleError verify(SumCheckProof& sumcheck_proof, bool& valid /*out*/) { + eIcicleError verify(SumCheckProof& sumcheck_proof, bool& valid /*out*/) + { const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); // verify that the sum of round_polynomial-0 is the clamed_sum const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); F round_poly_0_sum = round_poly_0[0]; - for (int round_idx=1; round_idx < nof_rounds-1; round_idx++) { + for (int round_idx = 1; round_idx < nof_rounds - 1; round_idx++) { round_poly_0_sum = round_poly_0_sum + round_poly_0[round_idx]; } const F& claimed_sum = m_backend->get_claimed_sum(); if (round_poly_0_sum != claimed_sum) { valid = false; - ICICLE_LOG_ERROR << "verification failed: sum of round polynomial 0 (" << round_poly_0_sum << ") != claimed_sum(" << claimed_sum << ")"; + ICICLE_LOG_ERROR << "verification failed: sum of round polynomial 0 (" << round_poly_0_sum + << ") != claimed_sum(" << claimed_sum << ")"; return eIcicleError::SUCCESS; } - for (int round_idx=0; round_idx < nof_rounds-1; round_idx++) { + for (int round_idx = 0; round_idx < nof_rounds - 1; round_idx++) { const std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); F alpha = m_backend->get_alpha(round_poly); F alpha_value = lagrange_interpolation(round_poly, alpha); - const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx+1); + const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx + 1); F expected_alpha_value = next_round_poly[0] + next_round_poly[1]; if (alpha_value != expected_alpha_value) { valid = false; @@ -115,14 +118,14 @@ namespace icicle { return eIcicleError::SUCCESS; } - private: - std::shared_ptr> m_backend; ///< Shared pointer to the backend responsible for Sumcheck operations. - + std::shared_ptr> + m_backend; ///< Shared pointer to the backend responsible for Sumcheck operations. // Receive the polynomial in evaluation on x=0,1,2... // retuרn the evaluation of the polynomial at x - F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) { + F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) + { uint poly_degree = poly_evaluations.size(); F result = F::zero(); @@ -131,13 +134,13 @@ namespace icicle { // Compute the i-th coefficient F numerator = poly_evaluations[i]; F denumerator = F::one(); - + // Use Lagrange interpolation formula const F i_field = F::from(i); for (uint j = 0; j < poly_degree; ++j) { if (j != i) { const F j_field = F::from(j); - numerator = numerator * (x-j_field); + numerator = numerator * (x - j_field); denumerator = denumerator * (i_field - j_field); } } @@ -147,5 +150,4 @@ namespace icicle { } }; - } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/sumcheck/sumcheck_config.h b/icicle/include/icicle/sumcheck/sumcheck_config.h index 448ebc96e3..2354101628 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_config.h @@ -7,17 +7,19 @@ namespace icicle { /** * @brief Configuration structure for Sumcheck operations. * - * This structure holds the configuration options for sumcheck operations. + * This structure holds the configuration options for sumcheck operations. * It allows specifying whether the data (input MLE polynomials) * reside on the device (e.g., GPU) or the host (e.g., CPU), and supports both synchronous and asynchronous * execution modes, as well as backend-specific extensions. */ - struct SumCheckConfig { + struct SumCheckConfig { icicleStreamHandle stream = nullptr; /**< Stream for asynchronous execution. Default is nullptr. */ uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ - bool are_inputs_on_device = false; /**< True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. */ - bool are_outputs_on_device = false; /**< True if outputs reside on the device, false if on the host. Default is false. */ + bool are_inputs_on_device = + false; /**< True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. */ + bool are_outputs_on_device = + false; /**< True if outputs reside on the device, false if on the host. Default is false. */ bool is_async = false; /**< True to run the hash asynchronously, false to run synchronously. Default is false. */ ConfigExtension* ext = nullptr; /**< Pointer to backend-specific configuration extensions. Default is nullptr. */ }; diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 57d45e0166..3ef6eaee0c 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -16,32 +16,36 @@ namespace icicle { * @tparam S Type of the field element (e.g., prime field or extension field elements). */ -template -class SumCheckProof { + template + class SumCheckProof + { public: // Constructor - SumCheckProof(uint nof_round_polynomials, uint round_polynomial_degree) : - m_round_polynomilas(nof_round_polynomials, std::vector(round_polynomial_degree+1)) { - if (nof_round_polynomials == 0) { - ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials <<") in the proof must be >0"; - } + SumCheckProof(uint nof_round_polynomials, uint round_polynomial_degree) + : m_round_polynomilas(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) + { + if (nof_round_polynomials == 0) { + ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials << ") in the proof must be >0"; } + } // set the value of polynomial round_polynomial_idx at x = evaluation_idx - void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, S& value) { + void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, S& value) + { m_round_polynomilas[round_polynomial_idx][evaluation_idx] = value; } // return a reference to the round polynomial generated at round # round_polynomial_idx - const std::vector& get_round_polynomial(int round_polynomial_idx) const { + const std::vector& get_round_polynomial(int round_polynomial_idx) const + { return m_round_polynomilas[round_polynomial_idx]; } - uint get_nof_round_polynomial() const {return m_round_polynomilas.size();} - uint get_round_polynomial_size() const {return m_round_polynomilas[0].size() + 1;} + uint get_nof_round_polynomial() const { return m_round_polynomilas.size(); } + uint get_round_polynomial_size() const { return m_round_polynomilas[0].size() + 1; } private: - std::vector > m_round_polynomilas; // logN vectors of round_poly_degree elements -}; + std::vector> m_round_polynomilas; // logN vectors of round_poly_degree elements + }; } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h index cacbff326e..5df1c3fa1b 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h @@ -48,8 +48,10 @@ namespace icicle { { public: // Default Constructor - SumcheckTranscriptConfig() : m_little_endian(true), m_seed_rng(F::from(0)), m_hasher(std::move(create_keccak_256_hash())) {} - + SumcheckTranscriptConfig() + : m_little_endian(true), m_seed_rng(F::from(0)), m_hasher(std::move(create_keccak_256_hash())) + { + } // Constructor with byte vector for labels SumcheckTranscriptConfig( From d6a9c59083192599f902f37611eb3db42506027a Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:52:12 +0200 Subject: [PATCH 003/127] strange compilation err --- icicle/include/icicle/program/program.h | 1 + 1 file changed, 1 insertion(+) diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index f8cf950d08..954e109e72 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "icicle/program/symbol.h" From e6672ff20534af9276eb2b1cafa4f4235eca8625 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:21:30 +0200 Subject: [PATCH 004/127] added test for kickoff --- .../include/icicle/backend/sumcheck_backend.h | 2 +- icicle/tests/test_field_api.cpp | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index a9c8decd60..d113faf582 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -56,7 +56,7 @@ namespace icicle { * @param round_polynomial a vector of MLE polynomials evaluated at x=0,1,2... * @return alpha */ - F get_alpha(std::vector& round_polynomial) = 0; + virtual F get_alpha(std::vector& round_polynomial) = 0; const F& get_claimed_sum() const { return m_claimed_sum; } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 3414811a7a..636eda83bc 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -15,6 +15,8 @@ #include "icicle/program/program.h" #include "icicle/program/returning_value_program.h" #include "../../icicle/backend/cpu/include/cpu_program_executor.h" +#include "icicle/sumcheck/sumcheck.h" + #include "test_base.h" using namespace field_config; @@ -1054,6 +1056,33 @@ TEST_F(FieldApiTestBase, CpuProgramExecutorReturningVal) ASSERT_EQ(0, memcmp(out_element_wise.get(), out_vec_ops.get(), total_size * sizeof(scalar_t))); } +#ifdef SUMCHECK +TEST_F(FieldApiTestBase, Sumcheck) +{ + int mle_poly_size = 1 << 13; + int nof_mle_poly = 4; + scalar_t claimed_sum = scalar_t::from(8); + + // create transcript_config + const SumcheckTranscriptConfig transcript_config; + + // create sumcheck + Sumcheck sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + + // generate inputs + std::vector< std::vector* > mle_polynomials(nof_mle_poly); + for (auto& mle_poly_ptr : mle_polynomials) { + mle_poly_ptr = std::make_shared>(mle_poly_size).get(); + scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); + } + CombineFunction combine_func(EQ_X_AB_MINUS_C); + SumCheckConfig config; + SumCheckProof sumcheck_proof(nof_mle_poly, 2); + + sumcheck.get_proof(mle_polynomials, combine_func, config, sumcheck_proof); +} +#endif + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From bb3b1d530d0ca1458985ad3dbb15f62b30b4c95f Mon Sep 17 00:00:00 2001 From: Yuval Shekel Date: Sun, 15 Dec 2024 11:01:05 +0200 Subject: [PATCH 005/127] small fixes to sumcheck frontend compilation --- icicle/cmake/target_editor.cmake | 2 +- .../include/icicle/backend/sumcheck_backend.h | 6 +- icicle/include/icicle/sumcheck/sumcheck.h | 31 ++--- icicle/src/sumcheck/sumcheck.cpp | 19 +++ icicle/src/sumcheck/sumcheck_c_api.cpp | 124 +++++++++--------- icicle/tests/CMakeLists.txt | 9 ++ icicle/tests/test_field_api.cpp | 8 +- 7 files changed, 109 insertions(+), 90 deletions(-) create mode 100644 icicle/src/sumcheck/sumcheck.cpp diff --git a/icicle/cmake/target_editor.cmake b/icicle/cmake/target_editor.cmake index 49192794c5..91e2f4e1c8 100644 --- a/icicle/cmake/target_editor.cmake +++ b/icicle/cmake/target_editor.cmake @@ -93,7 +93,7 @@ endfunction() function(handle_sumcheck TARGET FEATURE_LIST) if(SUMCHECK AND "SUMCHECK" IN_LIST FEATURE_LIST) target_compile_definitions(${TARGET} PUBLIC SUMCHECK=${SUMCHECK}) - target_sources(${TARGET} PRIVATE src/sumcheck/sumcheck_c_api.cpp) + target_sources(${TARGET} PRIVATE src/sumcheck/sumcheck.cpp src/sumcheck/sumcheck_c_api.cpp) set(SUMCHECK ON CACHE BOOL "Enable SUMCHECK feature" FORCE) else() set(SUMCHECK OFF CACHE BOOL "SUMCHECK not available for this field" FORCE) diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index d113faf582..9b696ed024 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -7,6 +7,8 @@ #include "icicle/sumcheck/sumcheck_config.h" #include "icicle/sumcheck/sumcheck_proof.h" #include "icicle/sumcheck/sumcheck_transcript_config.h" +#include "icicle/fields/field_config.h" +using namespace field_config; template using CombineFunction = ReturningValueProgram; @@ -61,7 +63,7 @@ namespace icicle { const F& get_claimed_sum() const { return m_claimed_sum; } protected: - F m_claimed_sum; ///< Vector of hash functions for each layer. + const F m_claimed_sum; ///< Vector of hash functions for each layer. SumcheckTranscriptConfig&& m_transcript_config; ///< Size of each leaf element in bytes. }; @@ -69,7 +71,7 @@ namespace icicle { template using SumcheckFactoryImpl = std::function&& transcript_config, std::shared_ptr>& backend /*OUT*/)>; diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 2c0ddfa9d3..6f3b2aa370 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -10,25 +10,16 @@ namespace icicle { template class Sumcheck; - // ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); - - // template class Sumcheck; - // /** - // * @brief Static factory method to create a Sumcheck instance. - // * - // * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) - // * when evaluated over all possible Boolean input combinations - // * @param transcript_config Configuration for encoding and hashing prover messages. - // * @return A Sumcheck object initialized with the specified backend. - // */ + /** + * @brief Static factory method to create a Sumcheck instance. + * + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations + * @param transcript_config Configuration for encoding and hashing prover messages. + * @return A Sumcheck object initialized with the specified backend. + */ template - Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) - { - std::shared_ptr> backend; - // ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); - Sumcheck sumcheck{backend}; - return sumcheck; - } + Sumcheck create_sumcheck(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config); /** * @brief Class for performing Sumcheck operations. @@ -51,7 +42,7 @@ namespace icicle { * @param transcript_config Configuration for encoding and hashing prover messages. * @return A Sumcheck object initialized with the specified backend. */ - static Sumcheck create(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + static Sumcheck create(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) { return create_sumcheck(claimed_sum, std::move(transcript_config)); } @@ -123,7 +114,7 @@ namespace icicle { m_backend; ///< Shared pointer to the backend responsible for Sumcheck operations. // Receive the polynomial in evaluation on x=0,1,2... - // retuרn the evaluation of the polynomial at x + // return the evaluation of the polynomial at x F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) { uint poly_degree = poly_evaluations.size(); diff --git a/icicle/src/sumcheck/sumcheck.cpp b/icicle/src/sumcheck/sumcheck.cpp new file mode 100644 index 0000000000..d2b3121c4c --- /dev/null +++ b/icicle/src/sumcheck/sumcheck.cpp @@ -0,0 +1,19 @@ +#include "icicle/errors.h" +#include "icicle/sumcheck/sumcheck.h" +#include "icicle/backend/sumcheck_backend.h" +#include "icicle/dispatcher.h" + +namespace icicle { + + ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); + + template <> + Sumcheck + create_sumcheck(const scalar_t& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + { + std::shared_ptr> backend; + ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config), backend)); + Sumcheck sumcheck{backend}; + return sumcheck; + } +} // namespace icicle \ No newline at end of file diff --git a/icicle/src/sumcheck/sumcheck_c_api.cpp b/icicle/src/sumcheck/sumcheck_c_api.cpp index c63af875bf..1d554f86b7 100644 --- a/icicle/src/sumcheck/sumcheck_c_api.cpp +++ b/icicle/src/sumcheck/sumcheck_c_api.cpp @@ -6,77 +6,75 @@ using namespace field_config; // TODO: Add methods for `prove`, `verify`, and the `proof` struct. -namespace icicle { - extern "C" { +extern "C" { - // Define the Sumcheck handle type - typedef Sumcheck SumcheckHandle; +// Define the Sumcheck handle type +typedef Sumcheck SumcheckHandle; - // Structure to represent the FFI transcript configuration - struct TranscriptConfigFFI { - Hash* hasher; - std::byte* domain_separator_label; - size_t domain_separator_label_len; - std::byte* round_poly_label; - size_t round_poly_label_len; - std::byte* round_challenge_label; - size_t round_challenge_label_len; - bool little_endian; - const scalar_t* seed_rng; - }; +// Structure to represent the FFI transcript configuration +struct TranscriptConfigFFI { + Hash* hasher; + std::byte* domain_separator_label; + size_t domain_separator_label_len; + std::byte* round_poly_label; + size_t round_poly_label_len; + std::byte* round_challenge_label; + size_t round_challenge_label_len; + bool little_endian; + const scalar_t* seed_rng; +}; - /** - * @brief Creates a new Sumcheck instance from the given FFI transcript configuration. - * @param ffi_transcript_config Pointer to the FFI transcript configuration structure. - * @return Pointer to the created Sumcheck instance. - */ - SumcheckHandle* CONCAT_EXPAND(FIELD, sumcheck_create)(const TranscriptConfigFFI* ffi_transcript_config) - { - if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { - ICICLE_LOG_ERROR << "Invalid FFI transcript configuration."; - return nullptr; - } - - ICICLE_LOG_DEBUG << "Constructing Sumcheck instance from FFI"; +/** + * @brief Creates a new Sumcheck instance from the given FFI transcript configuration. + * @param ffi_transcript_config Pointer to the FFI transcript configuration structure. + * @return Pointer to the created Sumcheck instance. + */ +SumcheckHandle* +CONCAT_EXPAND(FIELD, sumcheck_create)(const scalar_t* claimed_sum, const TranscriptConfigFFI* ffi_transcript_config) +{ + if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + ICICLE_LOG_ERROR << "Invalid FFI transcript configuration."; + return nullptr; + } - // Convert byte arrays to vectors - std::vector domain_separator_label( - ffi_transcript_config->domain_separator_label, - ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); - std::vector round_poly_label( - ffi_transcript_config->round_poly_label, - ffi_transcript_config->round_poly_label + ffi_transcript_config->round_poly_label_len); - std::vector round_challenge_label( - ffi_transcript_config->round_challenge_label, - ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); + ICICLE_LOG_DEBUG << "Constructing Sumcheck instance from FFI"; - // Construct the SumcheckTranscriptConfig - SumcheckTranscriptConfig config{*ffi_transcript_config->hasher, std::move(domain_separator_label), - std::move(round_poly_label), std::move(round_challenge_label), - *ffi_transcript_config->seed_rng, ffi_transcript_config->little_endian}; + // Convert byte arrays to vectors + std::vector domain_separator_label( + ffi_transcript_config->domain_separator_label, + ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); + std::vector round_poly_label( + ffi_transcript_config->round_poly_label, + ffi_transcript_config->round_poly_label + ffi_transcript_config->round_poly_label_len); + std::vector round_challenge_label( + ffi_transcript_config->round_challenge_label, + ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); - // Create and return the Sumcheck instance - return new Sumcheck(std::move(config)); - } + // Construct the SumcheckTranscriptConfig + SumcheckTranscriptConfig config{*ffi_transcript_config->hasher, std::move(domain_separator_label), + std::move(round_poly_label), std::move(round_challenge_label), + *ffi_transcript_config->seed_rng, ffi_transcript_config->little_endian}; - /** - * @brief Deletes the given Sumcheck instance. - * @param sumcheck_handle Pointer to the Sumcheck instance to be deleted. - * @return eIcicleError indicating the success or failure of the operation. - */ - eIcicleError CONCAT_EXPAND(FIELD, sumcheck_delete)(const SumcheckHandle* sumcheck_handle) - { - if (!sumcheck_handle) { - ICICLE_LOG_ERROR << "Cannot delete a null Sumcheck instance."; - return eIcicleError::INVALID_ARGUMENT; - } + // Create and return the Sumcheck instance + return new icicle::Sumcheck(icicle::create_sumcheck(*claimed_sum, std::move(config))); +} - ICICLE_LOG_DEBUG << "Destructing Sumcheck instance from FFI"; - delete sumcheck_handle; - - return eIcicleError::SUCCESS; +/** + * @brief Deletes the given Sumcheck instance. + * @param sumcheck_handle Pointer to the Sumcheck instance to be deleted. + * @return eIcicleError indicating the success or failure of the operation. + */ +eIcicleError CONCAT_EXPAND(FIELD, sumcheck_delete)(const SumcheckHandle* sumcheck_handle) +{ + if (!sumcheck_handle) { + ICICLE_LOG_ERROR << "Cannot delete a null Sumcheck instance."; + return eIcicleError::INVALID_ARGUMENT; } - } // extern "C" + ICICLE_LOG_DEBUG << "Destructing Sumcheck instance from FFI"; + delete sumcheck_handle; + + return eIcicleError::SUCCESS; +} -} // namespace icicle \ No newline at end of file +} // extern "C" diff --git a/icicle/tests/CMakeLists.txt b/icicle/tests/CMakeLists.txt index 066fe08039..33e9cb7ed8 100644 --- a/icicle/tests/CMakeLists.txt +++ b/icicle/tests/CMakeLists.txt @@ -41,6 +41,15 @@ if (FIELD) target_link_libraries(test_polynomial_api PRIVATE GTest::gtest_main icicle_field) gtest_discover_tests(test_polynomial_api) endif() + + if(SUMCHECK) + # Sumcheck relies on hash so this is mandatory + if(HASH) + target_link_libraries(test_field_api PRIVATE icicle_hash) + else() + message(FATAL_ERROR "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") + endif() + endif() endif() #curve API test diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 636eda83bc..2ef1ecbb16 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1064,13 +1064,13 @@ TEST_F(FieldApiTestBase, Sumcheck) scalar_t claimed_sum = scalar_t::from(8); // create transcript_config - const SumcheckTranscriptConfig transcript_config; + SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? // create sumcheck - Sumcheck sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + auto sumcheck = Sumcheck::create(claimed_sum, std::move(transcript_config)); // generate inputs - std::vector< std::vector* > mle_polynomials(nof_mle_poly); + std::vector*> mle_polynomials(nof_mle_poly); for (auto& mle_poly_ptr : mle_polynomials) { mle_poly_ptr = std::make_shared>(mle_poly_size).get(); scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); @@ -1081,7 +1081,7 @@ TEST_F(FieldApiTestBase, Sumcheck) sumcheck.get_proof(mle_polynomials, combine_func, config, sumcheck_proof); } -#endif +#endif // SUMCHECK int main(int argc, char** argv) { From 46b4d726ca32ce0b534570f6ab3b80af3beca279 Mon Sep 17 00:00:00 2001 From: Yuval Shekel Date: Sun, 15 Dec 2024 11:05:52 +0200 Subject: [PATCH 006/127] fix rust warppers --- wrappers/rust/icicle-core/src/sumcheck/mod.rs | 16 ++++++++++++---- wrappers/rust/icicle-core/src/sumcheck/tests.rs | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/wrappers/rust/icicle-core/src/sumcheck/mod.rs b/wrappers/rust/icicle-core/src/sumcheck/mod.rs index aa18d13146..a68ef67978 100644 --- a/wrappers/rust/icicle-core/src/sumcheck/mod.rs +++ b/wrappers/rust/icicle-core/src/sumcheck/mod.rs @@ -20,7 +20,10 @@ pub struct SumcheckTranscriptConfig<'a, F> { pub trait SumcheckConstructor { /// Creates a new Sumcheck prover instance. /// Optionally, consider returning `Box>` - fn new(transcript_config: &SumcheckTranscriptConfig) -> Result, eIcicleError>; + fn new( + claimed_sum: &F, + transcript_config: &SumcheckTranscriptConfig, + ) -> Result, eIcicleError>; } /// Trait for Sumcheck operations, including proving and verification. @@ -34,13 +37,14 @@ pub struct Sumcheck; impl Sumcheck { fn new<'a, F: FieldImpl>( + claimed_sum: &'a F, transcript_config: &'a SumcheckTranscriptConfig<'a, F>, ) -> Result + 'a, eIcicleError> where F: FieldImpl, F::Config: SumcheckConstructor, { - <::Config as SumcheckConstructor>::new(&transcript_config) + <::Config as SumcheckConstructor>::new(&claimed_sum, &transcript_config) } } @@ -156,7 +160,10 @@ macro_rules! impl_sumcheck { extern "C" { #[link_name = concat!($field_prefix, "_sumcheck_create")] - fn icicle_sumcheck_create(config: *const FFISumcheckTranscriptConfig<$field>) -> SumcheckHandle; + fn icicle_sumcheck_create( + claimed_sum: *const $field, + config: *const FFISumcheckTranscriptConfig<$field>, + ) -> SumcheckHandle; #[link_name = concat!($field_prefix, "_sumcheck_delete")] fn icicle_sumcheck_delete(handle: SumcheckHandle) -> eIcicleError; @@ -164,9 +171,10 @@ macro_rules! impl_sumcheck { impl SumcheckConstructor<$field> for $field_cfg { fn new( + claimed_sum: &$field, transcript_config: &SumcheckTranscriptConfig<$field>, ) -> Result, eIcicleError> { - let handle = unsafe { icicle_sumcheck_create(&transcript_config.into()) }; + let handle = unsafe { icicle_sumcheck_create(claimed_sum, &transcript_config.into()) }; if handle.is_null() { return Err(eIcicleError::UnknownError); } diff --git a/wrappers/rust/icicle-core/src/sumcheck/tests.rs b/wrappers/rust/icicle-core/src/sumcheck/tests.rs index dbbf97a63d..6743e3b4aa 100644 --- a/wrappers/rust/icicle-core/src/sumcheck/tests.rs +++ b/wrappers/rust/icicle-core/src/sumcheck/tests.rs @@ -65,7 +65,8 @@ where ); // Create a Sumcheck instance using the transcript configuration. - let sumcheck = Sumcheck::new::(&config).unwrap(); + let claimed_sum = F::from_u32(7); //dummy + let sumcheck = Sumcheck::new::(&claimed_sum, &config).unwrap(); // Generate dummy input data. let dummy_input: Vec = F::Config::generate_random(5); From 563694d0d08e0083bb32ee35279486acf390a023 Mon Sep 17 00:00:00 2001 From: Yuval Shekel Date: Sun, 15 Dec 2024 11:18:52 +0200 Subject: [PATCH 007/127] enable sumcheck for all fields and curves --- icicle/cmake/fields_and_curves.cmake | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/icicle/cmake/fields_and_curves.cmake b/icicle/cmake/fields_and_curves.cmake index 4729a1d806..ec3448dc8d 100644 --- a/icicle/cmake/fields_and_curves.cmake +++ b/icicle/cmake/fields_and_curves.cmake @@ -2,17 +2,17 @@ # Define available fields with an index and their supported features # Format: index:field:features set(ICICLE_FIELDS - 1001:babybear:NTT,EXT_FIELD,POSEIDON,POSEIDON2 - 1002:stark252:NTT,POSEIDON,POSEIDON2 - 1003:m31:EXT_FIELD,POSEIDON,POSEIDON2 + 1001:babybear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK + 1002:stark252:NTT,POSEIDON,POSEIDON2,SUMCHECK + 1003:m31:EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK ) # Define available curves with an index and their supported features # Format: index:curve:features set(ICICLE_CURVES 1:bn254:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK - 2:bls12_381:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2 - 3:bls12_377:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2 - 4:bw6_761:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2 - 5:grumpkin:MSM,POSEIDON,POSEIDON2 + 2:bls12_381:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK + 3:bls12_377:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK + 4:bw6_761:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK + 5:grumpkin:MSM,POSEIDON,POSEIDON2,SUMCHECK ) From ee5b5debc8845b8df44d6836e0c254fc293c2ed7 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:19:13 +0200 Subject: [PATCH 008/127] removed Sumcheck test from field api --- icicle/tests/test_field_api.cpp | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 2ef1ecbb16..a72a262c71 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1056,33 +1056,6 @@ TEST_F(FieldApiTestBase, CpuProgramExecutorReturningVal) ASSERT_EQ(0, memcmp(out_element_wise.get(), out_vec_ops.get(), total_size * sizeof(scalar_t))); } -#ifdef SUMCHECK -TEST_F(FieldApiTestBase, Sumcheck) -{ - int mle_poly_size = 1 << 13; - int nof_mle_poly = 4; - scalar_t claimed_sum = scalar_t::from(8); - - // create transcript_config - SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? - - // create sumcheck - auto sumcheck = Sumcheck::create(claimed_sum, std::move(transcript_config)); - - // generate inputs - std::vector*> mle_polynomials(nof_mle_poly); - for (auto& mle_poly_ptr : mle_polynomials) { - mle_poly_ptr = std::make_shared>(mle_poly_size).get(); - scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); - } - CombineFunction combine_func(EQ_X_AB_MINUS_C); - SumCheckConfig config; - SumCheckProof sumcheck_proof(nof_mle_poly, 2); - - sumcheck.get_proof(mle_polynomials, combine_func, config, sumcheck_proof); -} -#endif // SUMCHECK - int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From aa68bb55c06ac258692b45a7ec9af8bd0b06b968 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:03:32 +0200 Subject: [PATCH 009/127] dummy backend added --- icicle/backend/cpu/CMakeLists.txt | 9 ++++ icicle/backend/cpu/include/cpu_sumcheck.h | 42 +++++++++++++++++++ icicle/backend/cpu/src/field/sumcheck.cpp | 19 +++++++++ .../include/icicle/backend/sumcheck_backend.h | 4 +- icicle/include/icicle/program/program.h | 2 + icicle/src/sumcheck.cpp | 19 +++++++++ icicle/tests/test_field_api.cpp | 26 ++++++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 icicle/backend/cpu/include/cpu_sumcheck.h create mode 100644 icicle/backend/cpu/src/field/sumcheck.cpp create mode 100644 icicle/src/sumcheck.cpp diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index ab460bb5af..0837219ac4 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -18,6 +18,14 @@ if (FIELD) if (POSEIDON2) target_sources(icicle_field PRIVATE src/hash/cpu_poseidon2.cpp) endif() + if(SUMCHECK) + # Sumcheck relies on hash so this is mandatory + if(HASH) + target_sources(icicle_field PRIVATE src/field/sumcheck.cpp) + else() + message(FATAL_ERROR "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") + endif() + endif() target_include_directories(icicle_field PRIVATE include) endif() # FIELD @@ -45,3 +53,4 @@ if (HASH) target_include_directories(icicle_hash PUBLIC include) endif() + diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h new file mode 100644 index 0000000000..0b056e0470 --- /dev/null +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include "icicle/program/symbol.h" +#include "icicle/program/program.h" +#include "icicle/backend/sumcheck_backend.h" + +namespace icicle { + +template +class CpuSumcheckBackend : public SumcheckBackend +{ + public: + CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) : + SumcheckBackend(claimed_sum, std::move(transcript_config)) + {} + + virtual eIcicleError get_proof( + const std::vector< std::vector* >& input_polynomials, + const CombineFunction& combine_function, + SumCheckConfig& config, + SumCheckProof& sumcheck_proof /*out*/) override{ + + + } + + virtual F get_alpha(std::vector< F >& round_polynomial) override { + const std::vector& round_poly_label = this->m_transcript_config.m_round_poly_label(); + std::vector hash_input; + + // hash hash_input and return alpha + std::vector hash_result(this->m_transcript_config.hasher.output_size()); + this->m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), this->m_config, hash_result.data()); + this->m_prev_alpha = F::reduce(hash_result.data()); //TODO fix that + return this->m_prev_alpha; + } + + }; + +} + diff --git a/icicle/backend/cpu/src/field/sumcheck.cpp b/icicle/backend/cpu/src/field/sumcheck.cpp new file mode 100644 index 0000000000..9d750ebc40 --- /dev/null +++ b/icicle/backend/cpu/src/field/sumcheck.cpp @@ -0,0 +1,19 @@ +#include "icicle/backend/sumcheck_backend.h" +#include "cpu_sumcheck.h" + +namespace icicle { + + template + eIcicleError create_sumcheck_backend( + const Device& device, + const F& claimed_sum, + SumcheckTranscriptConfig&& transcript_config, + std::shared_ptr>& backend) + { + backend = std::make_shared(claimed_sum, std::move(transcript_config)); + return eIcicleError::SUCCESS; + } + + REGISTER_SUMCHECK_FACTORY_BACKEND("CPU", create_sumcheck_backend); + +} \ No newline at end of file diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 9b696ed024..09ab836cd3 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -32,7 +32,7 @@ namespace icicle { * when evaluated over all possible Boolean input combinations * @param transcript_config Configuration for encoding and hashing prover messages. */ - SumcheckBackend(F& claimed_sum, SumcheckTranscriptConfig&& transcript_config = SumcheckTranscriptConfig()) + SumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config = SumcheckTranscriptConfig()) : m_claimed_sum(claimed_sum), m_transcript_config(transcript_config) { } @@ -71,7 +71,7 @@ namespace icicle { template using SumcheckFactoryImpl = std::function&& transcript_config, std::shared_ptr>& backend /*OUT*/)>; diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 954e109e72..02aea24d59 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include "icicle/errors.h" #include "icicle/program/symbol.h" namespace icicle { diff --git a/icicle/src/sumcheck.cpp b/icicle/src/sumcheck.cpp new file mode 100644 index 0000000000..270b3d73ea --- /dev/null +++ b/icicle/src/sumcheck.cpp @@ -0,0 +1,19 @@ +#include "icicle/errors.h" +#include "icicle/sumcheck/sumcheck.h" +#include "icicle/backend/sumcheck_backend.h" +#include "icicle/dispatcher.h" + +namespace icicle { + + ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); + + template <> + Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + { + std::shared_ptr backend; + ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); + Sumcheck sumcheck{backend}; + return sumcheck; + } + +} // namespace icicle \ No newline at end of file diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a72a262c71..a9989514da 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1056,6 +1056,32 @@ TEST_F(FieldApiTestBase, CpuProgramExecutorReturningVal) ASSERT_EQ(0, memcmp(out_element_wise.get(), out_vec_ops.get(), total_size * sizeof(scalar_t))); } + +TEST_F(FieldApiTestBase, Sumcheck) +{ + int mle_poly_size = 1 << 13; + int nof_mle_poly = 4; + scalar_t claimed_sum = scalar_t::from(8); + + // create transcript_config + SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? + + // create sumcheck + auto sumcheck = Sumcheck::create(claimed_sum, std::move(transcript_config)); + + // generate inputs + std::vector*> mle_polynomials(nof_mle_poly); + for (auto& mle_poly_ptr : mle_polynomials) { + mle_poly_ptr = std::make_shared>(mle_poly_size).get(); + scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); + } + CombineFunction combine_func(EQ_X_AB_MINUS_C); + SumCheckConfig config; + SumCheckProof sumcheck_proof(nof_mle_poly, 2); + + sumcheck.get_proof(mle_polynomials, combine_func, config, sumcheck_proof); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From ffae6b403cfb078195e0d1966902c8bb8757c938 Mon Sep 17 00:00:00 2001 From: Yuval Shekel Date: Mon, 16 Dec 2024 16:03:03 +0200 Subject: [PATCH 010/127] compilation fix --- icicle/backend/cpu/CMakeLists.txt | 2 +- icicle/backend/cpu/include/cpu_sumcheck.h | 53 ++++++++++--------- icicle/backend/cpu/src/field/cpu_sumcheck.cpp | 21 ++++++++ icicle/backend/cpu/src/field/sumcheck.cpp | 19 ------- .../include/icicle/backend/sumcheck_backend.h | 13 +++-- .../sumcheck/sumcheck_transcript_config.h | 1 + icicle/src/sumcheck.cpp | 3 +- 7 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 icicle/backend/cpu/src/field/cpu_sumcheck.cpp delete mode 100644 icicle/backend/cpu/src/field/sumcheck.cpp diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index 0837219ac4..fbb0359050 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -21,7 +21,7 @@ if (FIELD) if(SUMCHECK) # Sumcheck relies on hash so this is mandatory if(HASH) - target_sources(icicle_field PRIVATE src/field/sumcheck.cpp) + target_sources(icicle_field PRIVATE src/field/cpu_sumcheck.cpp) else() message(FATAL_ERROR "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") endif() diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 0b056e0470..3065d96a82 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -8,35 +8,38 @@ namespace icicle { -template -class CpuSumcheckBackend : public SumcheckBackend -{ + template + class CpuSumcheckBackend : public SumcheckBackend + { public: - CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) : - SumcheckBackend(claimed_sum, std::move(transcript_config)) - {} - - virtual eIcicleError get_proof( - const std::vector< std::vector* >& input_polynomials, - const CombineFunction& combine_function, + CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + : SumcheckBackend(claimed_sum, std::move(transcript_config)) + { + } + + eIcicleError get_proof( + const std::vector*>& input_polynomials, + const CombineFunction& combine_function, SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) override{ - - - } + SumCheckProof& sumcheck_proof /*out*/) const override + { + return eIcicleError::API_NOT_IMPLEMENTED; + } - virtual F get_alpha(std::vector< F >& round_polynomial) override { - const std::vector& round_poly_label = this->m_transcript_config.m_round_poly_label(); - std::vector hash_input; + F get_alpha(std::vector& round_polynomial) override + { + // TODO miki fix + // const std::vector& round_poly_label = this->m_transcript_config.get_round_poly_label(); + // std::vector hash_input; // hash hash_input and return alpha - std::vector hash_result(this->m_transcript_config.hasher.output_size()); - this->m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), this->m_config, hash_result.data()); - this->m_prev_alpha = F::reduce(hash_result.data()); //TODO fix that - return this->m_prev_alpha; - } - + // std::vector hash_result(this->m_transcript_config.get_hasher().output_size()); + // this->m_transcript_config.get_hasher().hash( + // hash_input.data(), hash_input.size(), this->m_config, hash_result.data()); + // this->m_prev_alpha = F::reduce(hash_result.data()); // TODO fix that + // return this->m_prev_alpha; + return F::zero(); + } }; -} - +} // namespace icicle diff --git a/icicle/backend/cpu/src/field/cpu_sumcheck.cpp b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp new file mode 100644 index 0000000000..f89f8d6146 --- /dev/null +++ b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp @@ -0,0 +1,21 @@ +#include "icicle/backend/sumcheck_backend.h" +#include "cpu_sumcheck.h" + +using namespace field_config; + +namespace icicle { + + template + eIcicleError cpu_create_sumcheck_backend( + const Device& device, + const F& claimed_sum, + SumcheckTranscriptConfig&& transcript_config, + std::shared_ptr>& backend /*OUT*/) + { + backend = std::make_shared>(claimed_sum, std::move(transcript_config)); + return eIcicleError::SUCCESS; + } + + REGISTER_SUMCHECK_FACTORY_BACKEND("CPU", cpu_create_sumcheck_backend); + +} // namespace icicle \ No newline at end of file diff --git a/icicle/backend/cpu/src/field/sumcheck.cpp b/icicle/backend/cpu/src/field/sumcheck.cpp deleted file mode 100644 index 9d750ebc40..0000000000 --- a/icicle/backend/cpu/src/field/sumcheck.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "icicle/backend/sumcheck_backend.h" -#include "cpu_sumcheck.h" - -namespace icicle { - - template - eIcicleError create_sumcheck_backend( - const Device& device, - const F& claimed_sum, - SumcheckTranscriptConfig&& transcript_config, - std::shared_ptr>& backend) - { - backend = std::make_shared(claimed_sum, std::move(transcript_config)); - return eIcicleError::SUCCESS; - } - - REGISTER_SUMCHECK_FACTORY_BACKEND("CPU", create_sumcheck_backend); - -} \ No newline at end of file diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 09ab836cd3..a61967627c 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -32,8 +32,8 @@ namespace icicle { * when evaluated over all possible Boolean input combinations * @param transcript_config Configuration for encoding and hashing prover messages. */ - SumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config = SumcheckTranscriptConfig()) - : m_claimed_sum(claimed_sum), m_transcript_config(transcript_config) + SumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + : m_claimed_sum(claimed_sum), m_transcript_config(std::move(transcript_config)) { } @@ -63,15 +63,15 @@ namespace icicle { const F& get_claimed_sum() const { return m_claimed_sum; } protected: - const F m_claimed_sum; ///< Vector of hash functions for each layer. - SumcheckTranscriptConfig&& m_transcript_config; ///< Size of each leaf element in bytes. + const F m_claimed_sum; ///< Vector of hash functions for each layer. + const SumcheckTranscriptConfig m_transcript_config; ///< Size of each leaf element in bytes. }; /*************************** Backend Factory Registration ***************************/ template using SumcheckFactoryImpl = std::function&& transcript_config, std::shared_ptr>& backend /*OUT*/)>; @@ -81,8 +81,7 @@ namespace icicle { * @param deviceType String identifier for the device type. * @param impl Factory function that creates tSumcheckBackend. */ - template - void register_sumcheck_factory(const std::string& deviceType, SumcheckFactoryImpl impl); + void register_sumcheck_factory(const std::string& deviceType, SumcheckFactoryImpl impl); /** * @brief Macro to register a Sumcheck backend factory. diff --git a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h index 5df1c3fa1b..d1d56b3db9 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_transcript_config.h @@ -90,6 +90,7 @@ namespace icicle { } // Accessors + const Hash& get_hasher() const { return m_hasher; } const std::vector& get_domain_separator_label() const { return m_domain_separator_label; } const std::vector& get_round_poly_label() const { return m_round_poly_label; } const std::vector& get_round_challenge_label() const { return m_round_challenge_label; } diff --git a/icicle/src/sumcheck.cpp b/icicle/src/sumcheck.cpp index 270b3d73ea..a29d3c1ac8 100644 --- a/icicle/src/sumcheck.cpp +++ b/icicle/src/sumcheck.cpp @@ -8,7 +8,8 @@ namespace icicle { ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); template <> - Sumcheck create_sumcheck(F& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) + Sumcheck + create_sumcheck(scalar_t& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) { std::shared_ptr backend; ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); From f3331cc2b08cc13a4d6a6d98ff1271dc6e400141 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:51:33 +0400 Subject: [PATCH 011/127] test fix --- icicle/tests/test_field_api.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a9989514da..d4a3e85729 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1072,7 +1072,7 @@ TEST_F(FieldApiTestBase, Sumcheck) // generate inputs std::vector*> mle_polynomials(nof_mle_poly); for (auto& mle_poly_ptr : mle_polynomials) { - mle_poly_ptr = std::make_shared>(mle_poly_size).get(); + mle_poly_ptr = new std::vector(mle_poly_size); scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); } CombineFunction combine_func(EQ_X_AB_MINUS_C); From 6cdcee8528baa16a2daeadefb802fd511ed7b172 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Thu, 19 Dec 2024 07:55:38 +0200 Subject: [PATCH 012/127] format --- icicle/tests/test_field_api.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index d4a3e85729..c89cf1f142 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1056,7 +1056,6 @@ TEST_F(FieldApiTestBase, CpuProgramExecutorReturningVal) ASSERT_EQ(0, memcmp(out_element_wise.get(), out_vec_ops.get(), total_size * sizeof(scalar_t))); } - TEST_F(FieldApiTestBase, Sumcheck) { int mle_poly_size = 1 << 13; From 9e9ca5a01b92d814ba995ffcffdf0189c39866e4 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:18:34 +0400 Subject: [PATCH 013/127] changinf sumcheck without hash compilatio to warning --- icicle/backend/cpu/CMakeLists.txt | 2 +- icicle/tests/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index fbb0359050..e0c06318ce 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -23,7 +23,7 @@ if (FIELD) if(HASH) target_sources(icicle_field PRIVATE src/field/cpu_sumcheck.cpp) else() - message(FATAL_ERROR "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") + message(WARNING "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") endif() endif() target_include_directories(icicle_field PRIVATE include) diff --git a/icicle/tests/CMakeLists.txt b/icicle/tests/CMakeLists.txt index 33e9cb7ed8..f32b991eb8 100644 --- a/icicle/tests/CMakeLists.txt +++ b/icicle/tests/CMakeLists.txt @@ -47,7 +47,7 @@ if (FIELD) if(HASH) target_link_libraries(test_field_api PRIVATE icicle_hash) else() - message(FATAL_ERROR "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") + message(WARNING "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") endif() endif() endif() From 766dbe9b1d37e749c365db9f3ae6dfcebed7fb6d Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:07:05 +0200 Subject: [PATCH 014/127] removed sumcheck file duplication --- icicle/src/sumcheck.cpp | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 icicle/src/sumcheck.cpp diff --git a/icicle/src/sumcheck.cpp b/icicle/src/sumcheck.cpp deleted file mode 100644 index a29d3c1ac8..0000000000 --- a/icicle/src/sumcheck.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "icicle/errors.h" -#include "icicle/sumcheck/sumcheck.h" -#include "icicle/backend/sumcheck_backend.h" -#include "icicle/dispatcher.h" - -namespace icicle { - - ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); - - template <> - Sumcheck - create_sumcheck(scalar_t& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) - { - std::shared_ptr backend; - ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); - Sumcheck sumcheck{backend}; - return sumcheck; - } - -} // namespace icicle \ No newline at end of file From 588a84cdb5736aafe062797291a17927fcb8bf50 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:32:20 +0200 Subject: [PATCH 015/127] default hash on for cargo --- wrappers/rust/icicle-curves/icicle-bn254/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrappers/rust/icicle-curves/icicle-bn254/build.rs b/wrappers/rust/icicle-curves/icicle-bn254/build.rs index 1d1d98b2ef..7333a13464 100644 --- a/wrappers/rust/icicle-curves/icicle-bn254/build.rs +++ b/wrappers/rust/icicle-curves/icicle-bn254/build.rs @@ -25,7 +25,7 @@ fn main() { }; config .define("CURVE", "bn254") - .define("HASH", "OFF") + .define("HASH", "ON") .define("CMAKE_INSTALL_PREFIX", &icicle_install_dir); // build (or pull and build) cuda backend if feature enabled. From 1a83ec2e43cbc5abfbecf49066ab74c33fc6b7b4 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:10:54 +0200 Subject: [PATCH 016/127] cmake fix --- icicle/backend/cpu/CMakeLists.txt | 7 +------ icicle/tests/CMakeLists.txt | 9 ++------- wrappers/rust/icicle-curves/icicle-bn254/build.rs | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index e0c06318ce..1e1515fba6 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -18,13 +18,8 @@ if (FIELD) if (POSEIDON2) target_sources(icicle_field PRIVATE src/hash/cpu_poseidon2.cpp) endif() - if(SUMCHECK) - # Sumcheck relies on hash so this is mandatory - if(HASH) + if(SUMCHECK OR HASH) target_sources(icicle_field PRIVATE src/field/cpu_sumcheck.cpp) - else() - message(WARNING "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") - endif() endif() target_include_directories(icicle_field PRIVATE include) endif() # FIELD diff --git a/icicle/tests/CMakeLists.txt b/icicle/tests/CMakeLists.txt index f32b991eb8..9b79f83567 100644 --- a/icicle/tests/CMakeLists.txt +++ b/icicle/tests/CMakeLists.txt @@ -42,13 +42,8 @@ if (FIELD) gtest_discover_tests(test_polynomial_api) endif() - if(SUMCHECK) - # Sumcheck relies on hash so this is mandatory - if(HASH) - target_link_libraries(test_field_api PRIVATE icicle_hash) - else() - message(WARNING "SUMCHECK is enabled, but HASH is not enabled. Please enable HASH to proceed.") - endif() + if(SUMCHECK OR HASH) + target_link_libraries(test_field_api PRIVATE icicle_hash) endif() endif() diff --git a/wrappers/rust/icicle-curves/icicle-bn254/build.rs b/wrappers/rust/icicle-curves/icicle-bn254/build.rs index 7333a13464..1d1d98b2ef 100644 --- a/wrappers/rust/icicle-curves/icicle-bn254/build.rs +++ b/wrappers/rust/icicle-curves/icicle-bn254/build.rs @@ -25,7 +25,7 @@ fn main() { }; config .define("CURVE", "bn254") - .define("HASH", "ON") + .define("HASH", "OFF") .define("CMAKE_INSTALL_PREFIX", &icicle_install_dir); // build (or pull and build) cuda backend if feature enabled. From e7ed244b675fc9c679b19dfff8dbfeaba73b60b8 Mon Sep 17 00:00:00 2001 From: Miki <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 23 Dec 2024 08:34:06 +0200 Subject: [PATCH 017/127] removed commented code --- icicle/backend/cpu/include/cpu_sumcheck.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 3065d96a82..37c9619e7c 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -28,16 +28,6 @@ namespace icicle { F get_alpha(std::vector& round_polynomial) override { - // TODO miki fix - // const std::vector& round_poly_label = this->m_transcript_config.get_round_poly_label(); - // std::vector hash_input; - - // hash hash_input and return alpha - // std::vector hash_result(this->m_transcript_config.get_hasher().output_size()); - // this->m_transcript_config.get_hasher().hash( - // hash_input.data(), hash_input.size(), this->m_config, hash_result.data()); - // this->m_prev_alpha = F::reduce(hash_result.data()); // TODO fix that - // return this->m_prev_alpha; return F::zero(); } }; From 7248477f3c2a676585a7e42640bc24e12f136c63 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 23 Dec 2024 08:43:54 +0200 Subject: [PATCH 018/127] format --- icicle/backend/cpu/include/cpu_sumcheck.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 37c9619e7c..92ff38314f 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -26,10 +26,7 @@ namespace icicle { return eIcicleError::API_NOT_IMPLEMENTED; } - F get_alpha(std::vector& round_polynomial) override - { - return F::zero(); - } + F get_alpha(std::vector& round_polynomial) override { return F::zero(); } }; } // namespace icicle From 4a22355d4044a22bca57f246bfea2ec07606e915 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:49:17 +0200 Subject: [PATCH 019/127] review --- .../include/icicle/sumcheck/sumcheck_proof.h | 12 +++++------ icicle/src/sumcheck.cpp | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 icicle/src/sumcheck.cpp diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 3ef6eaee0c..bedd5bada4 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -22,7 +22,7 @@ namespace icicle { public: // Constructor SumCheckProof(uint nof_round_polynomials, uint round_polynomial_degree) - : m_round_polynomilas(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) + : m_round_polynomials(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) { if (nof_round_polynomials == 0) { ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials << ") in the proof must be >0"; @@ -32,20 +32,20 @@ namespace icicle { // set the value of polynomial round_polynomial_idx at x = evaluation_idx void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, S& value) { - m_round_polynomilas[round_polynomial_idx][evaluation_idx] = value; + m_round_polynomials[round_polynomial_idx][evaluation_idx] = value; } // return a reference to the round polynomial generated at round # round_polynomial_idx const std::vector& get_round_polynomial(int round_polynomial_idx) const { - return m_round_polynomilas[round_polynomial_idx]; + return m_round_polynomials[round_polynomial_idx]; } - uint get_nof_round_polynomial() const { return m_round_polynomilas.size(); } - uint get_round_polynomial_size() const { return m_round_polynomilas[0].size() + 1; } + uint get_nof_round_polynomial() const { return m_round_polynomials.size(); } + uint get_round_polynomial_size() const { return m_round_polynomials[0].size() + 1; } private: - std::vector> m_round_polynomilas; // logN vectors of round_poly_degree elements + std::vector> m_round_polynomials; // logN vectors of round_poly_degree elements }; } // namespace icicle \ No newline at end of file diff --git a/icicle/src/sumcheck.cpp b/icicle/src/sumcheck.cpp new file mode 100644 index 0000000000..cc3eaf7c57 --- /dev/null +++ b/icicle/src/sumcheck.cpp @@ -0,0 +1,20 @@ +// #include "icicle/errors.h" +// #include "icicle/sumcheck/sumcheck.h" +// #include "icicle/backend/sumcheck_backend.h" +// #include "icicle/dispatcher.h" + +// namespace icicle { + +// ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); + +// template <> +// Sumcheck +// create_sumcheck(scalar_t& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) +// { +// std::shared_ptr backend; +// ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); +// Sumcheck sumcheck{backend}; +// return sumcheck; +// } + +// } // namespace icicle \ No newline at end of file From a9b62564c9114897a4d10fd491841c617b2591d6 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:08:38 +0200 Subject: [PATCH 020/127] added comment to the MLE polynomial --- icicle/include/icicle/sumcheck/sumcheck.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 6f3b2aa370..636d5f41a1 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -55,7 +55,9 @@ namespace icicle { /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. - * @param input_polynomials a vector of MLE polynomials to process + * @param input_polynomials a vector of MLE polynomials to process. + * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + + * a_3 (1-X_1) X_2 X_3 + a_4 X_1 (1-X_2) (1-X_3) + a_5 X_1 (1-X_2) X_3+ a_6 X_1 X_2 (1-X_3) + a_7 X_1 X_2 X_3 * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. From b989711703dd8a33d6bc97034dbafc013ad7bd41 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Tue, 24 Dec 2024 13:55:46 +0200 Subject: [PATCH 021/127] review fixes --- icicle/backend/cpu/include/cpu_sumcheck.h | 4 ++-- .../include/icicle/backend/sumcheck_backend.h | 6 +++--- icicle/include/icicle/sumcheck/sumcheck.h | 20 ++++--------------- .../include/icicle/sumcheck/sumcheck_proof.h | 4 ++-- icicle/src/sumcheck.cpp | 20 ------------------- icicle/tests/test_field_api.cpp | 6 +++--- 6 files changed, 14 insertions(+), 46 deletions(-) delete mode 100644 icicle/src/sumcheck.cpp diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 92ff38314f..9749624308 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -18,9 +18,9 @@ namespace icicle { } eIcicleError get_proof( - const std::vector*>& input_polynomials, + const std::vector>>& mle_polynomials, const CombineFunction& combine_function, - SumCheckConfig& config, + const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const override { return eIcicleError::API_NOT_IMPLEMENTED; diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index a61967627c..051aa385df 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -41,16 +41,16 @@ namespace icicle { /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. - * @param input_polynomials a vector of MLE polynomials to process + * @param mle_polynomials a vector of MLE polynomials to process * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ virtual eIcicleError get_proof( - const std::vector*>& input_polynomials, + const std::vector>>& mle_polynomials, const CombineFunction& combine_function, - SumCheckConfig& config, + const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const = 0; /** diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 636d5f41a1..705da7d372 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -34,18 +34,6 @@ namespace icicle { class Sumcheck { public: - /** - * @brief Static factory method to create a Sumcheck instance. - * - * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) - * when evaluated over all possible Boolean input combinations - * @param transcript_config Configuration for encoding and hashing prover messages. - * @return A Sumcheck object initialized with the specified backend. - */ - static Sumcheck create(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - { - return create_sumcheck(claimed_sum, std::move(transcript_config)); - } /** * @brief Constructor for the Sumcheck class. @@ -55,7 +43,7 @@ namespace icicle { /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. - * @param input_polynomials a vector of MLE polynomials to process. + * @param mle_polynomials a vector of MLE polynomials to process. * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + * a_3 (1-X_1) X_2 X_3 + a_4 X_1 (1-X_2) (1-X_3) + a_5 X_1 (1-X_2) X_3+ a_6 X_1 X_2 (1-X_3) + a_7 X_1 X_2 X_3 * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. @@ -64,12 +52,12 @@ namespace icicle { * @return Error code of type eIcicleError. */ eIcicleError get_proof( - const std::vector*>& input_polynomials, + const std::vector>>& mle_polynomials, const CombineFunction& combine_function, - SumCheckConfig& config, + const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const { - return m_backend->get_proof(input_polynomials, combine_function, config, sumcheck_proof); + return m_backend->get_proof(mle_polynomials, combine_function, config, sumcheck_proof); } /** diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index bedd5bada4..56d8b6802f 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -21,7 +21,7 @@ namespace icicle { { public: // Constructor - SumCheckProof(uint nof_round_polynomials, uint round_polynomial_degree) + SumCheckProof(int nof_round_polynomials, int round_polynomial_degree) : m_round_polynomials(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) { if (nof_round_polynomials == 0) { @@ -30,7 +30,7 @@ namespace icicle { } // set the value of polynomial round_polynomial_idx at x = evaluation_idx - void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, S& value) + void set_round_polynomial_value(int round_polynomial_idx, int evaluation_idx, const S& value) { m_round_polynomials[round_polynomial_idx][evaluation_idx] = value; } diff --git a/icicle/src/sumcheck.cpp b/icicle/src/sumcheck.cpp deleted file mode 100644 index cc3eaf7c57..0000000000 --- a/icicle/src/sumcheck.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// #include "icicle/errors.h" -// #include "icicle/sumcheck/sumcheck.h" -// #include "icicle/backend/sumcheck_backend.h" -// #include "icicle/dispatcher.h" - -// namespace icicle { - -// ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); - -// template <> -// Sumcheck -// create_sumcheck(scalar_t& claimed_sum, const SumcheckTranscriptConfig&& transcript_config) -// { -// std::shared_ptr backend; -// ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config))); -// Sumcheck sumcheck{backend}; -// return sumcheck; -// } - -// } // namespace icicle \ No newline at end of file diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index c89cf1f142..68d7c3ea8d 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1066,12 +1066,12 @@ TEST_F(FieldApiTestBase, Sumcheck) SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? // create sumcheck - auto sumcheck = Sumcheck::create(claimed_sum, std::move(transcript_config)); + auto sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); // generate inputs - std::vector*> mle_polynomials(nof_mle_poly); + std::vector>> mle_polynomials(nof_mle_poly); for (auto& mle_poly_ptr : mle_polynomials) { - mle_poly_ptr = new std::vector(mle_poly_size); + mle_poly_ptr = std::make_shared>(mle_poly_size); scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); } CombineFunction combine_func(EQ_X_AB_MINUS_C); From 6a71b953aaca81bd6254c7266710ba816b32caa4 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Tue, 24 Dec 2024 14:00:27 +0200 Subject: [PATCH 022/127] format --- icicle/include/icicle/sumcheck/sumcheck.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 705da7d372..9450170b40 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -34,7 +34,6 @@ namespace icicle { class Sumcheck { public: - /** * @brief Constructor for the Sumcheck class. * @param backend Shared pointer to the backend responsible for Sumcheck operations. @@ -44,7 +43,7 @@ namespace icicle { /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. * @param mle_polynomials a vector of MLE polynomials to process. - * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + + * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + * a_3 (1-X_1) X_2 X_3 + a_4 X_1 (1-X_2) (1-X_3) + a_5 X_1 (1-X_2) X_3+ a_6 X_1 X_2 (1-X_3) + a_7 X_1 X_2 X_3 * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. From 44663c182a8f03f7805f8a40362a7d7ff98b087b Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:38:07 +0200 Subject: [PATCH 023/127] mle polynomials is a vector of pointers --- icicle/backend/cpu/include/cpu_sumcheck.h | 3 ++- icicle/include/icicle/backend/sumcheck_backend.h | 4 +++- icicle/include/icicle/sumcheck/sumcheck.h | 6 ++++-- icicle/tests/test_field_api.cpp | 12 ++++++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 9749624308..b1d9e0a822 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -18,7 +18,8 @@ namespace icicle { } eIcicleError get_proof( - const std::vector>>& mle_polynomials, + const std::vector& mle_polynomials, + const uint64_t mle_polynomial_size, const CombineFunction& combine_function, const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const override diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 051aa385df..0f0b48abd2 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -42,13 +42,15 @@ namespace icicle { /** * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. * @param mle_polynomials a vector of MLE polynomials to process + * @param mle_polynomial_size the size of each MLE polynomial * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ virtual eIcicleError get_proof( - const std::vector>>& mle_polynomials, + const std::vector& mle_polynomials, + const uint64_t mle_polynomial_size, const CombineFunction& combine_function, const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const = 0; diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 9450170b40..30ed662271 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -45,18 +45,20 @@ namespace icicle { * @param mle_polynomials a vector of MLE polynomials to process. * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + * a_3 (1-X_1) X_2 X_3 + a_4 X_1 (1-X_2) (1-X_3) + a_5 X_1 (1-X_2) X_3+ a_6 X_1 X_2 (1-X_3) + a_7 X_1 X_2 X_3 + * @param mle_polynomial_size the size of each MLE polynomial * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ eIcicleError get_proof( - const std::vector>>& mle_polynomials, + const std::vector& mle_polynomials, + const uint64_t mle_polynomial_size, const CombineFunction& combine_function, const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const { - return m_backend->get_proof(mle_polynomials, combine_function, config, sumcheck_proof); + return m_backend->get_proof(mle_polynomials, mle_polynomial_size, combine_function, config, sumcheck_proof); } /** diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 68d7c3ea8d..15eaa49efd 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1069,16 +1069,20 @@ TEST_F(FieldApiTestBase, Sumcheck) auto sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); // generate inputs - std::vector>> mle_polynomials(nof_mle_poly); + std::vector mle_polynomials(nof_mle_poly); for (auto& mle_poly_ptr : mle_polynomials) { - mle_poly_ptr = std::make_shared>(mle_poly_size); - scalar_t::rand_host_many(mle_poly_ptr->data(), mle_poly_size); + mle_poly_ptr = new scalar_t[mle_poly_size]; + scalar_t::rand_host_many(mle_poly_ptr, mle_poly_size); } CombineFunction combine_func(EQ_X_AB_MINUS_C); SumCheckConfig config; SumCheckProof sumcheck_proof(nof_mle_poly, 2); - sumcheck.get_proof(mle_polynomials, combine_func, config, sumcheck_proof); + sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); + + for (auto& mle_poly_ptr : mle_polynomials) { + delete[] mle_poly_ptr; + } } int main(int argc, char** argv) From 85de44dd1697184927b0dbacdd1a0fcbe8e63990 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Wed, 25 Dec 2024 14:41:08 +0200 Subject: [PATCH 024/127] format --- icicle/tests/test_field_api.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 15eaa49efd..c03090bb66 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1081,7 +1081,7 @@ TEST_F(FieldApiTestBase, Sumcheck) sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); for (auto& mle_poly_ptr : mle_polynomials) { - delete[] mle_poly_ptr; + delete[] mle_poly_ptr; } } From 01b7137388549119a8cfbb308b809688da582476 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:36:16 +0200 Subject: [PATCH 025/127] cpu backend inplementation start --- icicle/backend/cpu/include/cpu_sumcheck.h | 89 ++++++++++++- .../cpu/include/cpu_sumcheck_transcript.h | 125 ++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 icicle/backend/cpu/include/cpu_sumcheck_transcript.h diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index b1d9e0a822..6d4c2c47a0 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -4,10 +4,17 @@ #include #include "icicle/program/symbol.h" #include "icicle/program/program.h" +#include "cpu_sumcheck_transcript.h" +#include "cpu_program_executor.h" #include "icicle/backend/sumcheck_backend.h" namespace icicle { + + + + + template class CpuSumcheckBackend : public SumcheckBackend { @@ -24,10 +31,90 @@ namespace icicle { const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const override { - return eIcicleError::API_NOT_IMPLEMENTED; + // generate a program executor for the combine fumction + CpuProgramExecutor program_executor(program); + + // calc the number of rounds = log2(poly_size) + const int nof_rounds = std::log2(mle_polynomial_size); + + // run round 0 and update the proof + calc_round_polynomial(mle_polynomials); + for (int round_idx=1; round_idx < nof_rounds; ++round_idx) { + // calculate alpha for the next round + S alpha = fiat_shamir(); + + // run the next round and update the proof + calc_round_polynomial(m_folded_polynomials) + } + return eIcicleError::SUCCESS; } F get_alpha(std::vector& round_polynomial) override { return F::zero(); } + + + private: + void calc_round_polynomial(const std::vector& input_polynomials, bool fold, S& alpha) { + const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; + const uint nof_polynomials = input_polynomials.size(); + const uint round_poly_degree = m_combine_prog.m_poly_degree; + m_round_polynomial.resize(round_poly_degree+1); + TODO:: init m_round_polynomial to zero; + + // init m_program_executor input pointers + vector combine_func_inputs(m_combine_prog.m_nof_inputs); + for (int poly_idx=0; i + struct SumcheckTranscriptConfig { + Hash hasher; ///< Hash function used for randomness generation. + // TODO: Should labels be user-configurable or hardcoded? + const char* domain_separator_label; ///< Label for the domain separator in the transcript. + const char* round_poly_label; ///< Label for round polynomials in the transcript. + const char* round_challenge_label; ///< Label for round challenges in the transcript. + const bool little_endian = true; ///< Encoding endianness (default: little-endian). + S seed_rng; ///< Seed for initializing the RNG. + }; + + + +template +class CpuSumCheckTranscript { +public: + CpuSumCheckTranscript(const uint32_t num_vars, + const uint32_t poly_degree, + const S& claimed_sum, + SumcheckTranscriptConfig& transcript_config) : + m_num_vars(num_vars), + m_poly_degree(poly_degree), + m_claimed_sum(claimed_sum), + m_transcript_config(transcript_config) { + reset(); + } + + // add round polynomial to the transcript + S get_alpha(const vector & round_poly) { + const std::vector& round_poly_label = transcript_config.m_round_poly_label() + vector hash_input; + (poly_degree == 0) ? build_hash_input_round_0(hash_input, round_poly) : + build_hash_input_round_i(hash_input, round_poly); + + + // hash hash_input and return alpha + vector hash_result(transcript_config.hasher.output_size()); + m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); + m_prev_alpha = S::reduce(hash_result.data()); TODO fix that + return m_prev_alpha; + + } + + + + + // reset the transcript + voiud reset() { + m_hash_input.clear(); + m_entry_0.clear(); + m_round_idx = 0; + } + +private: + SumcheckTranscriptConfig& m_transcript_config; // configuration how to build the transcript + HashConfig m_config; // hash config - default + uint32_t m_round_idx; // + vector m_entry_0; // + const uint32_t m_num_vars; + const uint32_t m_poly_degree; + const S m_claimed_sum; + S m_prev_alpha; + + + // append to hash_input a stream of bytes received as chars + void append_data(vector& byte_vec, const std::vector& label) { + byte_vec.insert(byte_vec.end(), label.begin(), label.end()); + } + + // append an integer uint32_t to hash input + void append_u32(vector& byte_vec, const uint32_t data) { + const std::byte* data_bytes = reinterpret_cast(&data); + byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(uint32_t)); + } + + void append_field(vector& byte_vec, const S& field) { + const std::byte* data_bytes = reinterpret_cast(field.get_limbs()); + byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(S)); + } + + + void build_hash_input_round_0(vector& hash_input, const vector & round_poly) { + const std::vector& round_poly_label = transcript_config.m_round_poly_label() + // append entry_DS = [domain_separator_label || proof.num_vars || proof.degree || public (hardcoded?) || claimed_sum] + append_data(hash_input, m_transcript_config.get_domain_separator_label()); + append_u32(hash_input, m_num_vars); + append_u32(hash_input, m_poly_degree); + append_field(hash_input, m_claimed_sum); + + // append seed_rng + append_data(hash_input, m_transcript_config.get_seed_rng()); + + // append round_challenge_label + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + + // build entry_0 = [round_poly_label || r_0[x].len() || k=0 || r_0[x]] + append_data(m_entry_0, round_poly_label); + append_u32(m_entry_0, round_poly.size()); + append_u32(m_entry_0, m_round_idx++); + for (S& r_i :round_poly) { + append_field(r_i, S); + } + + // append entry_0 + append_data(hash_input, m_entry_0); + } + void build_hash_input_round_i(vector& hash_input, const vector & round_poly) { + // entry_i = [round_poly_label || r_i[x].len() || k=i || r_i[x]] + // alpha_i = Hash(entry_0 || alpha_(i-1) || round_challenge_label || entry_i).to_field() + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + + append_data(hash_input, round_poly_label); + append_u32(hash_input, round_poly.size()); + append_u32(hash_input, m_round_idx++); + for (S& r_i :round_poly) { + append_field(r_i, S); + } + } +}; From d61ca9ef8496fd059874d15517286d22053cbe7f Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:21:13 +0200 Subject: [PATCH 026/127] nackend implementation --- icicle/backend/cpu/include/cpu_sumcheck.h | 18 +++++++++--------- .../cpu/include/cpu_sumcheck_transcript.h | 4 ---- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 6d4c2c47a0..d4f95adb97 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -10,11 +10,6 @@ namespace icicle { - - - - - template class CpuSumcheckBackend : public SumcheckBackend { @@ -49,10 +44,18 @@ namespace icicle { return eIcicleError::SUCCESS; } - F get_alpha(std::vector& round_polynomial) override { return F::zero(); } + // caculate alpha for the next round based on the round_polynomial of the current round + F get_alpha(std::vector& round_polynomial) override { + return m_cpu_sumcheck_transcript.get_alpha(round_polynomial); + } private: + // memebers + CpuSumCheckTranscript m_cpu_sumcheck_transcript; + std::vector m_round_polynomial; + std::vector> m_folded_polynomials; + void calc_round_polynomial(const std::vector& input_polynomials, bool fold, S& alpha) { const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; const uint nof_polynomials = input_polynomials.size(); @@ -112,9 +115,6 @@ namespace icicle { } } } - - - }; } // namespace icicle diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 13bb2f8d95..7a8c22a20b 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -42,12 +42,8 @@ class CpuSumCheckTranscript { m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); m_prev_alpha = S::reduce(hash_result.data()); TODO fix that return m_prev_alpha; - } - - - // reset the transcript voiud reset() { m_hash_input.clear(); From d8abe7a2c9d36bdfbe3d458437c16a78c433168a Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:21:30 +0200 Subject: [PATCH 027/127] backend implementation --- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 7a8c22a20b..056b15791e 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -1,18 +1,15 @@ - - template struct SumcheckTranscriptConfig { Hash hasher; ///< Hash function used for randomness generation. - // TODO: Should labels be user-configurable or hardcoded? const char* domain_separator_label; ///< Label for the domain separator in the transcript. const char* round_poly_label; ///< Label for round polynomials in the transcript. const char* round_challenge_label; ///< Label for round challenges in the transcript. const bool little_endian = true; ///< Encoding endianness (default: little-endian). S seed_rng; ///< Seed for initializing the RNG. }; - + template @@ -36,7 +33,6 @@ class CpuSumCheckTranscript { (poly_degree == 0) ? build_hash_input_round_0(hash_input, round_poly) : build_hash_input_round_i(hash_input, round_poly); - // hash hash_input and return alpha vector hash_result(transcript_config.hasher.output_size()); m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); From 30fc2bb3f9ae03f904b4c5e19d785e590725a1e4 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 12 Jan 2025 09:51:27 +0200 Subject: [PATCH 028/127] compilation start --- icicle/backend/cpu/include/cpu_sumcheck.h | 199 ++++++++++++------ .../cpu/include/cpu_sumcheck_transcript.h | 90 ++++---- .../include/icicle/backend/sumcheck_backend.h | 9 + .../include/icicle/sumcheck/sumcheck_proof.h | 8 + 4 files changed, 195 insertions(+), 111 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index d4f95adb97..7552bdc37a 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -7,18 +7,17 @@ #include "cpu_sumcheck_transcript.h" #include "cpu_program_executor.h" #include "icicle/backend/sumcheck_backend.h" - +#include "cpu_sumcheck_transcript.h" namespace icicle { - template class CpuSumcheckBackend : public SumcheckBackend { public: CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - : SumcheckBackend(claimed_sum, std::move(transcript_config)) - { + : SumcheckBackend(claimed_sum, std::move(transcript_config)) { } + // Calculate a proof for the mle polynomials eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, @@ -26,20 +25,35 @@ namespace icicle { const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const override { + const int nof_mle_poly = mle_polynomials.size(); + std::vector folded_mle_polynomials(nof_mle_poly); + for (auto& mle_poly_ptr : folded_mle_polynomials) { + mle_poly_ptr = new F[mle_polynomial_size/2]; + } + + const uint32_t nof_rounds = std::log2(mle_polynomial_size); + reset(nof_rounds, combine_function.m_degree); + // reset the sumcheck proof to accumulate + sumcheck_proof.reset(); + // generate a program executor for the combine fumction - CpuProgramExecutor program_executor(program); + CpuProgramExecutor program_executor(combine_function); // calc the number of rounds = log2(poly_size) - const int nof_rounds = std::log2(mle_polynomial_size); - // run round 0 and update the proof - calc_round_polynomial(mle_polynomials); - for (int round_idx=1; round_idx < nof_rounds; ++round_idx) { - // calculate alpha for the next round - S alpha = fiat_shamir(); - + int cur_mle_polynomial_size = mle_polynomial_size; + + for (int round_idx=0; round_idx < nof_rounds; ++round_idx) { + const std::vector& in_mle_polynomials= round_idx == 0 ? mle_polynomials : folded_mle_polynomials; // run the next round and update the proof - calc_round_polynomial(m_folded_polynomials) + build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, sumcheck_proof.get_round_polynomial(round_idx)); + + if (round_idx +1 < nof_rounds) { + // calculate alpha for the next round + F alpha = get_alpha(sumcheck_proof.get_round_polynomial(round_idx-1)); + + fold_mle_polynomials(alpha, cur_mle_polynomial_size, in_mle_polynomials, folded_mle_polynomials); + } } return eIcicleError::SUCCESS; } @@ -49,72 +63,131 @@ namespace icicle { return m_cpu_sumcheck_transcript.get_alpha(round_polynomial); } + // Reset the sumcheck transcript with e + void reset(const uint32_t num_vars, const uint32_t poly_degree) override { + m_cpu_sumcheck_transcript.reset(num_vars, poly_degree); + } private: // memebers - CpuSumCheckTranscript m_cpu_sumcheck_transcript; - std::vector m_round_polynomial; - std::vector> m_folded_polynomials; - - void calc_round_polynomial(const std::vector& input_polynomials, bool fold, S& alpha) { - const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; - const uint nof_polynomials = input_polynomials.size(); - const uint round_poly_degree = m_combine_prog.m_poly_degree; - m_round_polynomial.resize(round_poly_degree+1); - TODO:: init m_round_polynomial to zero; - - // init m_program_executor input pointers - vector combine_func_inputs(m_combine_prog.m_nof_inputs); - for (int poly_idx=0; i m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) + + void build_round_polynomial(const std::vector& in_mle_polynomials, + const int mle_polynomial_size, + CpuProgramExecutor& program_executor, + std::vector& round_polynomial) { + + // init program_executor input pointers + const int nof_polynomials = in_mle_polynomials.size(); + std::vector combine_func_inputs(nof_polynomials); + for (int poly_idx=0; poly_idx& in_mle_polynomials, + std::vector& folded_mle_polynomials) { + + const int nof_polynomials = in_mle_polynomials.size(); + const F one_minus_alpha = F::one() - alpha; + mle_polynomial_size <<= 1; + + // run over all elements in all polynomials + for (int element_idx=0; element_idx& input_polynomials, bool fold, S& alpha) { + // const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; + // const uint nof_polynomials = input_polynomials.size(); + // const uint round_poly_degree = m_combine_prog.m_poly_degree; + // m_round_polynomial.resize(round_poly_degree+1); + // TODO:: init m_round_polynomial to zero; + + // // init m_program_executor input pointers + // vector combine_func_inputs(m_combine_prog.m_nof_inputs); + // for (int poly_idx=0; i - struct SumcheckTranscriptConfig { - Hash hasher; ///< Hash function used for randomness generation. - const char* domain_separator_label; ///< Label for the domain separator in the transcript. - const char* round_poly_label; ///< Label for round polynomials in the transcript. - const char* round_challenge_label; ///< Label for round challenges in the transcript. - const bool little_endian = true; ///< Encoding endianness (default: little-endian). - S seed_rng; ///< Seed for initializing the RNG. - }; - - +#pragma once +#include "icicle/sumcheck/sumcheck_transcript_config.h" template class CpuSumCheckTranscript { public: - CpuSumCheckTranscript(const uint32_t num_vars, - const uint32_t poly_degree, - const S& claimed_sum, - SumcheckTranscriptConfig& transcript_config) : - m_num_vars(num_vars), - m_poly_degree(poly_degree), + CpuSumCheckTranscript(const S& claimed_sum, + SumcheckTranscriptConfig& transcript_config) : m_claimed_sum(claimed_sum), m_transcript_config(transcript_config) { - reset(); + reset(0, 0); } // add round polynomial to the transcript - S get_alpha(const vector & round_poly) { - const std::vector& round_poly_label = transcript_config.m_round_poly_label() - vector hash_input; - (poly_degree == 0) ? build_hash_input_round_0(hash_input, round_poly) : + S get_alpha(const std::vector & round_poly) { + // Make sure reset was called (Internal assertion) + ICICLE_ASSERT(m_num_vars > 0) << "num_vars must reset with value > 0"; + ICICLE_ASSERT(m_poly_degree > 0) << "poly_degree must reset with value > 0"; + + if (m_num_vars) + const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); + std::vector hash_input; + (m_round_idx == 0) ? build_hash_input_round_0(hash_input, round_poly) : build_hash_input_round_i(hash_input, round_poly); // hash hash_input and return alpha - vector hash_result(transcript_config.hasher.output_size()); - m_transcript_config.hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); - m_prev_alpha = S::reduce(hash_result.data()); TODO fix that + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); + m_round_idx++; + m_prev_alpha = S::reduce(hash_result.data()); // TBD fix that return m_prev_alpha; } // reset the transcript - voiud reset() { - m_hash_input.clear(); + void reset(const uint32_t num_vars, const uint32_t poly_degree) { + m_num_vars = num_vars; + m_poly_degree = poly_degree; m_entry_0.clear(); m_round_idx = 0; } -private: - SumcheckTranscriptConfig& m_transcript_config; // configuration how to build the transcript - HashConfig m_config; // hash config - default - uint32_t m_round_idx; // - vector m_entry_0; // - const uint32_t m_num_vars; - const uint32_t m_poly_degree; - const S m_claimed_sum; - S m_prev_alpha; + private: + SumcheckTranscriptConfig& m_transcript_config; // configuration how to build the transcript + HashConfig m_config; // hash config - default + uint32_t m_round_idx; // + std::vector m_entry_0; // + const uint32_t m_num_vars; + const uint32_t m_poly_degree; + const S m_claimed_sum; + S m_prev_alpha; // append to hash_input a stream of bytes received as chars - void append_data(vector& byte_vec, const std::vector& label) { + void append_data(std::vector& byte_vec, const std::vector& label) { byte_vec.insert(byte_vec.end(), label.begin(), label.end()); } // append an integer uint32_t to hash input - void append_u32(vector& byte_vec, const uint32_t data) { - const std::byte* data_bytes = reinterpret_cast(&data); + void append_u32(std::vector& byte_vec, const uint32_t data) { + const std::byte* data_bytes = reinterpret_cast(&data); byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(uint32_t)); } - void append_field(vector& byte_vec, const S& field) { - const std::byte* data_bytes = reinterpret_cast(field.get_limbs()); + void append_field(std::vector& byte_vec, const S& field) { + const std::byte* data_bytes = reinterpret_cast(field.get_limbs()); byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(S)); } - void build_hash_input_round_0(vector& hash_input, const vector & round_poly) { - const std::vector& round_poly_label = transcript_config.m_round_poly_label() + void build_hash_input_round_0(std::vector& hash_input, const std::vector & round_poly) { + const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); // append entry_DS = [domain_separator_label || proof.num_vars || proof.degree || public (hardcoded?) || claimed_sum] append_data(hash_input, m_transcript_config.get_domain_separator_label()); append_u32(hash_input, m_num_vars); @@ -94,13 +87,14 @@ class CpuSumCheckTranscript { append_u32(m_entry_0, round_poly.size()); append_u32(m_entry_0, m_round_idx++); for (S& r_i :round_poly) { - append_field(r_i, S); + append_field(hash_input, r_i); } // append entry_0 append_data(hash_input, m_entry_0); } - void build_hash_input_round_i(vector& hash_input, const vector & round_poly) { + void build_hash_input_round_i(std::vector& hash_input, const std::vector & round_poly) { + const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); // entry_i = [round_poly_label || r_i[x].len() || k=i || r_i[x]] // alpha_i = Hash(entry_0 || alpha_(i-1) || round_challenge_label || entry_i).to_field() append_data(hash_input, m_entry_0); @@ -111,7 +105,7 @@ class CpuSumCheckTranscript { append_u32(hash_input, round_poly.size()); append_u32(hash_input, m_round_idx++); for (S& r_i :round_poly) { - append_field(r_i, S); + append_field(hash_input, r_i); } } }; diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 0f0b48abd2..fd43695579 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -55,6 +55,14 @@ namespace icicle { const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) const = 0; + /** + * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. + * @param num_vars + * @param poly_degree the degree of the combine function + */ + + virtual void reset(const uint32_t num_vars, const uint32_t poly_degree) = 0; + /** * @brief Calculate alpha based on m_transcript_config and the round polynomial. * @param round_polynomial a vector of MLE polynomials evaluated at x=0,1,2... @@ -62,6 +70,7 @@ namespace icicle { */ virtual F get_alpha(std::vector& round_polynomial) = 0; + const F& get_claimed_sum() const { return m_claimed_sum; } protected: diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 56d8b6802f..78d3fd6e6f 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -44,6 +44,14 @@ namespace icicle { uint get_nof_round_polynomial() const { return m_round_polynomials.size(); } uint get_round_polynomial_size() const { return m_round_polynomials[0].size() + 1; } + // Reset the proof to zeros + void reset() { + for (auto& round_poly : m_round_polynomials) { + for (auto& element : round_poly) { + element = S::zero(); + } + } + } private: std::vector> m_round_polynomials; // logN vectors of round_poly_degree elements }; From 1628b529d7a089c486297468ce04d0d323c8bbc2 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:40:15 +0200 Subject: [PATCH 029/127] compile, fail on verification test --- icicle/backend/cpu/include/cpu_sumcheck.h | 42 ++++++++++++------- .../cpu/include/cpu_sumcheck_transcript.h | 34 +++++++-------- .../include/icicle/backend/sumcheck_backend.h | 2 +- icicle/include/icicle/program/program.h | 1 - .../icicle/program/returning_value_program.h | 21 +++++++++- icicle/include/icicle/sumcheck/sumcheck.h | 13 +++--- .../include/icicle/sumcheck/sumcheck_proof.h | 17 +++++++- icicle/tests/test_field_api.cpp | 33 +++++++++++---- 8 files changed, 113 insertions(+), 50 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 7552bdc37a..5293c90535 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -14,7 +14,8 @@ namespace icicle { { public: CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - : SumcheckBackend(claimed_sum, std::move(transcript_config)) { + : SumcheckBackend(claimed_sum, std::move(transcript_config)), + m_cpu_sumcheck_transcript(claimed_sum, std::move(transcript_config)) { } // Calculate a proof for the mle polynomials @@ -23,7 +24,7 @@ namespace icicle { const uint64_t mle_polynomial_size, const CombineFunction& combine_function, const SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) const override + SumCheckProof& sumcheck_proof /*out*/) override { const int nof_mle_poly = mle_polynomials.size(); std::vector folded_mle_polynomials(nof_mle_poly); @@ -31,8 +32,20 @@ namespace icicle { mle_poly_ptr = new F[mle_polynomial_size/2]; } - const uint32_t nof_rounds = std::log2(mle_polynomial_size); - reset(nof_rounds, combine_function.m_degree); + // Check that the size of the the proof feet the size of the mle polynomials. + const uint32_t nof_rounds = sumcheck_proof.get_nof_round_polynomials(); + if (std::log2(mle_polynomial_size) != nof_rounds) { + ICICLE_LOG_ERROR << "Sumcheck proof size(" << nof_rounds << ") should be log of the mle polynomial size(" << mle_polynomial_size << ")"; + return eIcicleError::INVALID_ARGUMENT; + } + // check that the combine function has a legal polynomial degree + int poly_degree = combine_function.get_polynomial_degee(); + if (poly_degree < 0) { + ICICLE_LOG_ERROR << "Illegal polynomial degree (" << poly_degree << ") for provided combine function"; + return eIcicleError::INVALID_ARGUMENT; + } + reset(nof_rounds, uint32_t(poly_degree)); + // reset the sumcheck proof to accumulate sumcheck_proof.reset(); @@ -44,7 +57,7 @@ namespace icicle { int cur_mle_polynomial_size = mle_polynomial_size; for (int round_idx=0; round_idx < nof_rounds; ++round_idx) { - const std::vector& in_mle_polynomials= round_idx == 0 ? mle_polynomials : folded_mle_polynomials; + const std::vector& in_mle_polynomials = (round_idx == 0) ? mle_polynomials : folded_mle_polynomials; // run the next round and update the proof build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, sumcheck_proof.get_round_polynomial(round_idx)); @@ -81,27 +94,28 @@ namespace icicle { const int nof_polynomials = in_mle_polynomials.size(); std::vector combine_func_inputs(nof_polynomials); for (int poly_idx=0; poly_idx>= 1; // run over all elements in all polynomials for (int element_idx=0; element_idx class CpuSumCheckTranscript { public: CpuSumCheckTranscript(const S& claimed_sum, - SumcheckTranscriptConfig& transcript_config) : + SumcheckTranscriptConfig&& transcript_config) : m_claimed_sum(claimed_sum), - m_transcript_config(transcript_config) { + m_transcript_config( std::move(transcript_config)) { reset(0, 0); } @@ -17,7 +17,6 @@ class CpuSumCheckTranscript { ICICLE_ASSERT(m_num_vars > 0) << "num_vars must reset with value > 0"; ICICLE_ASSERT(m_poly_degree > 0) << "poly_degree must reset with value > 0"; - if (m_num_vars) const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); std::vector hash_input; (m_round_idx == 0) ? build_hash_input_round_0(hash_input, round_poly) : @@ -28,7 +27,8 @@ class CpuSumCheckTranscript { std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); m_round_idx++; - m_prev_alpha = S::reduce(hash_result.data()); // TBD fix that + S* hash_result_as_a_field = (S*)(hash_result.data()); + m_prev_alpha = (*hash_result_as_a_field) * S::one(); // TBD fix that to reduce return m_prev_alpha; } @@ -41,15 +41,15 @@ class CpuSumCheckTranscript { } private: - SumcheckTranscriptConfig& m_transcript_config; // configuration how to build the transcript - HashConfig m_config; // hash config - default - uint32_t m_round_idx; // - std::vector m_entry_0; // - const uint32_t m_num_vars; - const uint32_t m_poly_degree; - const S m_claimed_sum; - S m_prev_alpha; - + const SumcheckTranscriptConfig m_transcript_config; // configuration how to build the transcript + HashConfig m_config; // hash config - default + uint32_t m_round_idx; // + std::vector m_entry_0; // + uint32_t m_num_vars = 0; + uint32_t m_poly_degree = 0; + const S m_claimed_sum; + S m_prev_alpha; + // append to hash_input a stream of bytes received as chars void append_data(std::vector& byte_vec, const std::vector& label) { @@ -63,7 +63,7 @@ class CpuSumCheckTranscript { } void append_field(std::vector& byte_vec, const S& field) { - const std::byte* data_bytes = reinterpret_cast(field.get_limbs()); + const std::byte* data_bytes = reinterpret_cast(field.limbs_storage.limbs); byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(S)); } @@ -77,7 +77,7 @@ class CpuSumCheckTranscript { append_field(hash_input, m_claimed_sum); // append seed_rng - append_data(hash_input, m_transcript_config.get_seed_rng()); + append_field(hash_input, m_transcript_config.get_seed_rng()); // append round_challenge_label append_data(hash_input, m_transcript_config.get_round_challenge_label()); @@ -86,7 +86,7 @@ class CpuSumCheckTranscript { append_data(m_entry_0, round_poly_label); append_u32(m_entry_0, round_poly.size()); append_u32(m_entry_0, m_round_idx++); - for (S& r_i :round_poly) { + for (const S& r_i : round_poly) { append_field(hash_input, r_i); } @@ -104,7 +104,7 @@ class CpuSumCheckTranscript { append_data(hash_input, round_poly_label); append_u32(hash_input, round_poly.size()); append_u32(hash_input, m_round_idx++); - for (S& r_i :round_poly) { + for (const S& r_i :round_poly) { append_field(hash_input, r_i); } } diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index fd43695579..b1000b9764 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -53,7 +53,7 @@ namespace icicle { const uint64_t mle_polynomial_size, const CombineFunction& combine_function, const SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) const = 0; + SumCheckProof& sumcheck_proof /*out*/) = 0; /** * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 24d5fc7aa9..02aea24d59 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -6,7 +6,6 @@ #include #include "icicle/errors.h" #include "icicle/program/symbol.h" -#include "icicle/utils/log.h" namespace icicle { diff --git a/icicle/include/icicle/program/returning_value_program.h b/icicle/include/icicle/program/returning_value_program.h index 9eff6515cf..7cc01dc6ab 100644 --- a/icicle/include/icicle/program/returning_value_program.h +++ b/icicle/include/icicle/program/returning_value_program.h @@ -24,9 +24,28 @@ namespace icicle { this->set_as_inputs(program_parameters); program_parameters[nof_inputs] = program_func(program_parameters); // place the output after the all inputs this->generate_program(program_parameters); + m_poly_degree = program_parameters[nof_inputs].m_operation->m_poly_degree; } // Generate a program based on a PreDefinedPrograms - ReturningValueProgram(PreDefinedPrograms pre_def) : Program(pre_def) {} + ReturningValueProgram(PreDefinedPrograms pre_def) : Program(pre_def) { + switch (pre_def) { + case AB_MINUS_C: + m_poly_degree = 2; + break; + case EQ_X_AB_MINUS_C: + m_poly_degree = 3; + break; + default: + ICICLE_LOG_ERROR << "Illegal opcode: " << int(pre_def); + } + } + + int get_polynomial_degee() const { + return m_poly_degree; + } + private: + int m_poly_degree = 0; + }; } // namespace icicle diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 30ed662271..0a271a72cf 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -70,8 +70,11 @@ namespace icicle { eIcicleError verify(SumCheckProof& sumcheck_proof, bool& valid /*out*/) { const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); - // verify that the sum of round_polynomial-0 is the clamed_sum const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); + const uint32_t poly_degree = round_poly_0.size() - 1; + m_backend->reset(nof_rounds, poly_degree); + + // verify that the sum of round_polynomial-0 is the clamed_sum F round_poly_0_sum = round_poly_0[0]; for (int round_idx = 1; round_idx < nof_rounds - 1; round_idx++) { round_poly_0_sum = round_poly_0_sum + round_poly_0[round_idx]; @@ -85,9 +88,9 @@ namespace icicle { } for (int round_idx = 0; round_idx < nof_rounds - 1; round_idx++) { - const std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); - F alpha = m_backend->get_alpha(round_poly); - F alpha_value = lagrange_interpolation(round_poly, alpha); + std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); + const F alpha = m_backend->get_alpha(round_poly); + const F alpha_value = lagrange_interpolation(round_poly, alpha); const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx + 1); F expected_alpha_value = next_round_poly[0] + next_round_poly[1]; if (alpha_value != expected_alpha_value) { @@ -106,7 +109,7 @@ namespace icicle { // Receive the polynomial in evaluation on x=0,1,2... // return the evaluation of the polynomial at x - F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) + F lagrange_interpolation(const std::vector& poly_evaluations, const F& x) const { uint poly_degree = poly_evaluations.size(); F result = F::zero(); diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 78d3fd6e6f..d191a48dcc 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -36,12 +36,12 @@ namespace icicle { } // return a reference to the round polynomial generated at round # round_polynomial_idx - const std::vector& get_round_polynomial(int round_polynomial_idx) const + std::vector& get_round_polynomial(int round_polynomial_idx) { return m_round_polynomials[round_polynomial_idx]; } - uint get_nof_round_polynomial() const { return m_round_polynomials.size(); } + uint get_nof_round_polynomials() const { return m_round_polynomials.size(); } uint get_round_polynomial_size() const { return m_round_polynomials[0].size() + 1; } // Reset the proof to zeros @@ -54,6 +54,19 @@ namespace icicle { } private: std::vector> m_round_polynomials; // logN vectors of round_poly_degree elements + + public: + // for debug + void print_proof() + { + std::cout << "Sumcheck Proof :" << std::endl; + for (int round_poly_i = 0; round_poly_i < m_round_polynomials.size(); round_poly_i++) { + std::cout << " Round polynomial " << round_poly_i << ":" << std::endl;; + for (auto& element : m_round_polynomials[round_poly_i]) { + std::cout << " " << element << std::endl; + } + } + } }; } // namespace icicle \ No newline at end of file diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index ac498e05c7..a6086f6ef3 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1192,31 +1192,46 @@ TEST_F(FieldApiTestBase, ProgramExecutorVecOpDataOnDevice) TEST_F(FieldApiTestBase, Sumcheck) { - int mle_poly_size = 1 << 13; + int log_mle_poly_size = 3; + int mle_poly_size = 1 << log_mle_poly_size; int nof_mle_poly = 4; - scalar_t claimed_sum = scalar_t::from(8); + scalar_t claimed_sum = scalar_t::hex_str2scalar("0x00000000000000000000000000000000000000000000000000000000000616b0"); // create transcript_config SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? + // ===== Prover side ====== // create sumcheck - auto sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + auto prover_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); // generate inputs std::vector mle_polynomials(nof_mle_poly); - for (auto& mle_poly_ptr : mle_polynomials) { - mle_poly_ptr = new scalar_t[mle_poly_size]; - scalar_t::rand_host_many(mle_poly_ptr, mle_poly_size); + for (int poly_i = 0; poly_i < nof_mle_poly; poly_i++) { + mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; + for (int element_i = 0; element_i < mle_poly_size; element_i++) { + mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i*10 + element_i+1); + std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] << std::endl; + } } CombineFunction combine_func(EQ_X_AB_MINUS_C); SumCheckConfig config; - SumCheckProof sumcheck_proof(nof_mle_poly, 2); - - sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); + SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly-1); + prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); + sumcheck_proof.print_proof(); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; } + + // ===== Verifier side ====== + // create sumcheck + auto verifier_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + bool verification_pass; + verifier_sumcheck.verify(sumcheck_proof, verification_pass); + + ASSERT_EQ(true, verification_pass); + + } int main(int argc, char** argv) { From 3b1165ec68ae4e2a9cc26e41914ab8c92f6715cd Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 12 Jan 2025 18:21:54 +0200 Subject: [PATCH 030/127] verification failed on round 1 --- icicle/backend/cpu/include/cpu_sumcheck.h | 6 ++++-- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 5293c90535..9e36523fe4 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -58,12 +58,14 @@ namespace icicle { for (int round_idx=0; round_idx < nof_rounds; ++round_idx) { const std::vector& in_mle_polynomials = (round_idx == 0) ? mle_polynomials : folded_mle_polynomials; + std::vector& round_polynomial = sumcheck_proof.get_round_polynomial(round_idx); + // run the next round and update the proof - build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, sumcheck_proof.get_round_polynomial(round_idx)); + build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, round_polynomial); if (round_idx +1 < nof_rounds) { // calculate alpha for the next round - F alpha = get_alpha(sumcheck_proof.get_round_polynomial(round_idx-1)); + F alpha = get_alpha(round_polynomial); fold_mle_polynomials(alpha, cur_mle_polynomial_size, in_mle_polynomials, folded_mle_polynomials); } diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index fe74cb6b65..c9d09a63cd 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -17,6 +17,9 @@ class CpuSumCheckTranscript { ICICLE_ASSERT(m_num_vars > 0) << "num_vars must reset with value > 0"; ICICLE_ASSERT(m_poly_degree > 0) << "poly_degree must reset with value > 0"; + for (int element_i = 0; element_i transcript_config; // TODO Miki: define labels? @@ -1210,6 +1209,16 @@ TEST_F(FieldApiTestBase, Sumcheck) mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; for (int element_i = 0; element_i < mle_poly_size; element_i++) { mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i*10 + element_i+1); + // if (poly_i == 3) { + // mle_polynomials[poly_i][0] = scalar_t::from(2); + // mle_polynomials[poly_i][1] = scalar_t::from(10); + // mle_polynomials[poly_i][2] = scalar_t::from(1); + // mle_polynomials[poly_i][3] = scalar_t::from(6); + // mle_polynomials[poly_i][4] = scalar_t::from(9); + // mle_polynomials[poly_i][5] = scalar_t::from(3); + // mle_polynomials[poly_i][6] = scalar_t::from(8); + // mle_polynomials[poly_i][7] = scalar_t::from(7); + // } std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] << std::endl; } } From b2b5ff1971b3713856aa8dcece2a4aabc0098af7 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Sun, 12 Jan 2025 16:59:58 +0000 Subject: [PATCH 032/127] format --- icicle/backend/cpu/src/field/cpu_vec_ops.cpp | 2 +- icicle/backend/cpu/src/hash/cpu_poseidon2.cpp | 2 +- icicle/include/icicle/backend/sumcheck_backend.h | 3 +-- .../icicle/program/returning_value_program.h | 11 +++++------ icicle/include/icicle/sumcheck/sumcheck_proof.h | 14 +++++++------- icicle/tests/test_curve_api.cpp | 14 ++++++++++---- icicle/tests/test_field_api.cpp | 11 +++++------ 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp index f279c7b034..a9d53e3593 100644 --- a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp +++ b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp @@ -371,7 +371,7 @@ class VectorOpTask : public TaskBase public: T m_intermidiate_res; // pointer to the output. Can be a vector or scalar pointer uint64_t m_idx_in_batch; // index in the batch. Used in intermediate res tasks -}; // class VectorOpTask +}; // class VectorOpTask #define NOF_OPERATIONS_PER_TASK 512 #define CONFIG_NOF_THREADS_KEY "n_threads" diff --git a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp index 5f664860e7..df1c519cd6 100644 --- a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp +++ b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp @@ -147,7 +147,7 @@ namespace icicle { ICICLE_LOG_ERROR << "cpu_poseidon2_init_default_constants: T (width) must be one of [2, 3, 4, 8, 12, 16, 20, 24]\n"; return eIcicleError::INVALID_ARGUMENT; - } // switch (T) { + } // switch (T) { if (full_rounds == 0 && partial_rounds == 0) { // All arrays are empty in this case. continue; } diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index b1000b9764..ad2f999990 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -57,7 +57,7 @@ namespace icicle { /** * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. - * @param num_vars + * @param num_vars * @param poly_degree the degree of the combine function */ @@ -70,7 +70,6 @@ namespace icicle { */ virtual F get_alpha(std::vector& round_polynomial) = 0; - const F& get_claimed_sum() const { return m_claimed_sum; } protected: diff --git a/icicle/include/icicle/program/returning_value_program.h b/icicle/include/icicle/program/returning_value_program.h index 7cc01dc6ab..9b716d03f0 100644 --- a/icicle/include/icicle/program/returning_value_program.h +++ b/icicle/include/icicle/program/returning_value_program.h @@ -28,7 +28,8 @@ namespace icicle { } // Generate a program based on a PreDefinedPrograms - ReturningValueProgram(PreDefinedPrograms pre_def) : Program(pre_def) { + ReturningValueProgram(PreDefinedPrograms pre_def) : Program(pre_def) + { switch (pre_def) { case AB_MINUS_C: m_poly_degree = 2; @@ -40,12 +41,10 @@ namespace icicle { ICICLE_LOG_ERROR << "Illegal opcode: " << int(pre_def); } } - - int get_polynomial_degee() const { - return m_poly_degree; - } + + int get_polynomial_degee() const { return m_poly_degree; } + private: int m_poly_degree = 0; - }; } // namespace icicle diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index d191a48dcc..8f168baadf 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -36,32 +36,32 @@ namespace icicle { } // return a reference to the round polynomial generated at round # round_polynomial_idx - std::vector& get_round_polynomial(int round_polynomial_idx) - { - return m_round_polynomials[round_polynomial_idx]; - } + std::vector& get_round_polynomial(int round_polynomial_idx) { return m_round_polynomials[round_polynomial_idx]; } uint get_nof_round_polynomials() const { return m_round_polynomials.size(); } uint get_round_polynomial_size() const { return m_round_polynomials[0].size() + 1; } // Reset the proof to zeros - void reset() { + void reset() + { for (auto& round_poly : m_round_polynomials) { for (auto& element : round_poly) { element = S::zero(); } } } + private: std::vector> m_round_polynomials; // logN vectors of round_poly_degree elements - + public: // for debug void print_proof() { std::cout << "Sumcheck Proof :" << std::endl; for (int round_poly_i = 0; round_poly_i < m_round_polynomials.size(); round_poly_i++) { - std::cout << " Round polynomial " << round_poly_i << ":" << std::endl;; + std::cout << " Round polynomial " << round_poly_i << ":" << std::endl; + ; for (auto& element : m_round_polynomials[round_poly_i]) { std::cout << " " << element << std::endl; } diff --git a/icicle/tests/test_curve_api.cpp b/icicle/tests/test_curve_api.cpp index e5ce23d704..e4fac2b334 100644 --- a/icicle/tests/test_curve_api.cpp +++ b/icicle/tests/test_curve_api.cpp @@ -83,21 +83,27 @@ class CurveApiTest : public IcicleTestBase template void MSM_CPU_THREADS_test() { - const int logn = 8; - const int c = 3; + const int logn = 10; + const int c = 11; // Low c to have a large amount of tasks required in phase 2 // For example for bn254: #bms = ceil(254/3)=85 // #tasks in phase 2 = 2 * #bms = 170 > 64 = TASK_PER_THREAD // As such the default amount of tasks and 1 thread shouldn't be enough and the program should readjust the task // number per thread. const int batch = 3; - const int N = (1 << logn) - rand_uint_32b(0, 5 * logn); // make it not always power of two - const int precompute_factor = 1; // Precompute is 1 to increase number of BMs + const int N = 64; //(1 << logn) - rand_uint_32b(0, 5 * logn); // make it not always power of two + const int precompute_factor = 1; // Precompute is 1 to increase number of BMs const int total_nof_elemets = batch * N; auto scalars = std::make_unique(total_nof_elemets); auto bases = std::make_unique(N); scalar_t::rand_host_many(scalars.get(), total_nof_elemets); + scalars[0] = scalar_t::zero() - scalar_t::one(); + scalars[1] = scalar_t::zero() - scalar_t::one(); + scalars[2] = scalar_t::zero() - scalar_t::one(); + scalars[3] = scalar_t::zero() - scalar_t::one(); + scalars[4] = scalar_t::zero() - scalar_t::one(); + // scalars[0] = scalar_t::get_modulus()-scalar_t::one(); P::rand_host_many(bases.get(), N); auto result_multi_thread = std::make_unique(batch); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 5f4a839910..33a25b21cb 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1208,7 +1208,7 @@ TEST_F(FieldApiTestBase, Sumcheck) for (int poly_i = 0; poly_i < nof_mle_poly; poly_i++) { mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; for (int element_i = 0; element_i < mle_poly_size; element_i++) { - mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i*10 + element_i+1); + mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i * 10 + element_i + 1); // if (poly_i == 3) { // mle_polynomials[poly_i][0] = scalar_t::from(2); // mle_polynomials[poly_i][1] = scalar_t::from(10); @@ -1219,12 +1219,13 @@ TEST_F(FieldApiTestBase, Sumcheck) // mle_polynomials[poly_i][6] = scalar_t::from(8); // mle_polynomials[poly_i][7] = scalar_t::from(7); // } - std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] << std::endl; + std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] + << std::endl; } } CombineFunction combine_func(EQ_X_AB_MINUS_C); SumCheckConfig config; - SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly-1); + SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); sumcheck_proof.print_proof(); @@ -1237,10 +1238,8 @@ TEST_F(FieldApiTestBase, Sumcheck) auto verifier_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); bool verification_pass; verifier_sumcheck.verify(sumcheck_proof, verification_pass); - - ASSERT_EQ(true, verification_pass); - + ASSERT_EQ(true, verification_pass); } int main(int argc, char** argv) { From 3928fd985aa8f6e51136ecce057dc865805cfff7 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Sun, 12 Jan 2025 17:02:03 +0000 Subject: [PATCH 033/127] spell check --- icicle/backend/cpu/include/cpu_sumcheck.h | 210 +++++++++--------- .../cpu/include/cpu_sumcheck_transcript.h | 72 +++--- 2 files changed, 146 insertions(+), 136 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 51e7a1827c..33b1242594 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -15,7 +15,8 @@ namespace icicle { public: CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) : SumcheckBackend(claimed_sum, std::move(transcript_config)), - m_cpu_sumcheck_transcript(claimed_sum, std::move(transcript_config)) { + m_cpu_sumcheck_transcript(claimed_sum, std::move(transcript_config)) + { } // Calculate a proof for the mle polynomials @@ -29,13 +30,14 @@ namespace icicle { const int nof_mle_poly = mle_polynomials.size(); std::vector folded_mle_polynomials(nof_mle_poly); for (auto& mle_poly_ptr : folded_mle_polynomials) { - mle_poly_ptr = new F[mle_polynomial_size/2]; + mle_poly_ptr = new F[mle_polynomial_size / 2]; } // Check that the size of the the proof feet the size of the mle polynomials. - const uint32_t nof_rounds = sumcheck_proof.get_nof_round_polynomials(); + const uint32_t nof_rounds = sumcheck_proof.get_nof_round_polynomials(); if (std::log2(mle_polynomial_size) != nof_rounds) { - ICICLE_LOG_ERROR << "Sumcheck proof size(" << nof_rounds << ") should be log of the mle polynomial size(" << mle_polynomial_size << ")"; + ICICLE_LOG_ERROR << "Sumcheck proof size(" << nof_rounds << ") should be log of the mle polynomial size(" + << mle_polynomial_size << ")"; return eIcicleError::INVALID_ARGUMENT; } // check that the combine function has a legal polynomial degree @@ -49,21 +51,21 @@ namespace icicle { // reset the sumcheck proof to accumulate sumcheck_proof.reset(); - // generate a program executor for the combine fumction + // generate a program executor for the combine function CpuProgramExecutor program_executor(combine_function); // calc the number of rounds = log2(poly_size) - + int cur_mle_polynomial_size = mle_polynomial_size; - - for (int round_idx=0; round_idx < nof_rounds; ++round_idx) { + + for (int round_idx = 0; round_idx < nof_rounds; ++round_idx) { const std::vector& in_mle_polynomials = (round_idx == 0) ? mle_polynomials : folded_mle_polynomials; std::vector& round_polynomial = sumcheck_proof.get_round_polynomial(round_idx); // run the next round and update the proof build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, round_polynomial); - if (round_idx +1 < nof_rounds) { + if (round_idx + 1 < nof_rounds) { // calculate alpha for the next round F alpha = get_alpha(round_polynomial); @@ -73,139 +75,141 @@ namespace icicle { return eIcicleError::SUCCESS; } - // caculate alpha for the next round based on the round_polynomial of the current round - F get_alpha(std::vector& round_polynomial) override { + // calculate alpha for the next round based on the round_polynomial of the current round + F get_alpha(std::vector& round_polynomial) override + { return m_cpu_sumcheck_transcript.get_alpha(round_polynomial); } - // Reset the sumcheck transcript with e - void reset(const uint32_t num_vars, const uint32_t poly_degree) override { + // Reset the sumcheck transcript with e + void reset(const uint32_t num_vars, const uint32_t poly_degree) override + { m_cpu_sumcheck_transcript.reset(num_vars, poly_degree); } private: - // memebers - CpuSumCheckTranscript m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) - - void build_round_polynomial(const std::vector& in_mle_polynomials, - const int mle_polynomial_size, - CpuProgramExecutor& program_executor, - std::vector& round_polynomial) { - + // members + CpuSumCheckTranscript m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) + + void build_round_polynomial( + const std::vector& in_mle_polynomials, + const int mle_polynomial_size, + CpuProgramExecutor& program_executor, + std::vector& round_polynomial) + { // init program_executor input pointers const int nof_polynomials = in_mle_polynomials.size(); std::vector combine_func_inputs(nof_polynomials); - for (int poly_idx=0; poly_idx& in_mle_polynomials, - std::vector& folded_mle_polynomials) { - + void fold_mle_polynomials( + const F& alpha, + int& mle_polynomial_size, + const std::vector& in_mle_polynomials, + std::vector& folded_mle_polynomials) + { const int nof_polynomials = in_mle_polynomials.size(); const F one_minus_alpha = F::one() - alpha; mle_polynomial_size >>= 1; // run over all elements in all polynomials - for (int element_idx=0; element_idx& input_polynomials, bool fold, S& alpha) { - // const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; - // const uint nof_polynomials = input_polynomials.size(); - // const uint round_poly_degree = m_combine_prog.m_poly_degree; - // m_round_polynomial.resize(round_poly_degree+1); - // TODO:: init m_round_polynomial to zero; - - // // init m_program_executor input pointers - // vector combine_func_inputs(m_combine_prog.m_nof_inputs); - // for (int poly_idx=0; i& input_polynomials, bool fold, S& alpha) { + // const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; + // const uint nof_polynomials = input_polynomials.size(); + // const uint round_poly_degree = m_combine_prog.m_poly_degree; + // m_round_polynomial.resize(round_poly_degree+1); + // TODO:: init m_round_polynomial to zero; + + // // init m_program_executor input pointers + // vector combine_func_inputs(m_combine_prog.m_nof_inputs); + // for (int poly_idx=0; i -class CpuSumCheckTranscript { +class CpuSumCheckTranscript +{ public: - CpuSumCheckTranscript(const S& claimed_sum, - SumcheckTranscriptConfig&& transcript_config) : - m_claimed_sum(claimed_sum), - m_transcript_config( std::move(transcript_config)) { - reset(0, 0); - } + CpuSumCheckTranscript(const S& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + : m_claimed_sum(claimed_sum), m_transcript_config(std::move(transcript_config)) + { + reset(0, 0); + } // add round polynomial to the transcript - S get_alpha(const std::vector & round_poly) { + S get_alpha(const std::vector& round_poly) + { // Make sure reset was called (Internal assertion) ICICLE_ASSERT(m_num_vars > 0) << "num_vars must reset with value > 0"; ICICLE_ASSERT(m_poly_degree > 0) << "poly_degree must reset with value > 0"; - for (int element_i = 0; element_i Date: Mon, 13 Jan 2025 08:20:15 +0200 Subject: [PATCH 036/127] adjusting sumcheck test to all fields --- backend/cuda | 1 + icicle/run | 4 ++++ icicle/tests/test_field_api.cpp | 33 +++++++++++++++++---------------- 3 files changed, 22 insertions(+), 16 deletions(-) create mode 160000 backend/cuda create mode 100755 icicle/run diff --git a/backend/cuda b/backend/cuda new file mode 160000 index 0000000000..f373d8a91a --- /dev/null +++ b/backend/cuda @@ -0,0 +1 @@ +Subproject commit f373d8a91ace751b798774f7c250b12df065d484 diff --git a/icicle/run b/icicle/run new file mode 100755 index 0000000000..3384e588f8 --- /dev/null +++ b/icicle/run @@ -0,0 +1,4 @@ +rm -rf build +cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DMSM=OFF -DSUMCHECK=ON -DHASH=ON -DG2=OFF -DECNTT=OFF -DEXT_FIELD=OFF -DFIELD=babybear -S . -B build +cmake --build build -j +build/tests/test_field_api --gtest_filter="*Sumcheck*" diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 33a25b21cb..8fbe7c079d 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1195,13 +1195,9 @@ TEST_F(FieldApiTestBase, Sumcheck) int log_mle_poly_size = 3; int mle_poly_size = 1 << log_mle_poly_size; int nof_mle_poly = 4; - scalar_t claimed_sum = scalar_t::hex_str2scalar("0x000000000000000000000000000000000000000000000000000000000000348c"); - // create transcript_config - SumcheckTranscriptConfig transcript_config; // TODO Miki: define labels? - // ===== Prover side ====== - // create sumcheck - auto prover_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + // create transcript_config + SumcheckTranscriptConfig transcript_config; // default configuration // generate inputs std::vector mle_polynomials(nof_mle_poly); @@ -1209,20 +1205,25 @@ TEST_F(FieldApiTestBase, Sumcheck) mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; for (int element_i = 0; element_i < mle_poly_size; element_i++) { mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i * 10 + element_i + 1); - // if (poly_i == 3) { - // mle_polynomials[poly_i][0] = scalar_t::from(2); - // mle_polynomials[poly_i][1] = scalar_t::from(10); - // mle_polynomials[poly_i][2] = scalar_t::from(1); - // mle_polynomials[poly_i][3] = scalar_t::from(6); - // mle_polynomials[poly_i][4] = scalar_t::from(9); - // mle_polynomials[poly_i][5] = scalar_t::from(3); - // mle_polynomials[poly_i][6] = scalar_t::from(8); - // mle_polynomials[poly_i][7] = scalar_t::from(7); - // } std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] << std::endl; } } + + // calculate the claimed sum + scalar_t claimed_sum = scalar_t::zero(); + for (int element_i = 0; element_i < mle_poly_size; element_i++) { + const scalar_t a = mle_polynomials[0][element_i]; + const scalar_t b = mle_polynomials[1][element_i]; + const scalar_t c = mle_polynomials[2][element_i]; + const scalar_t eq = mle_polynomials[3][element_i]; + claimed_sum = claimed_sum + (a*b-c)*eq; + } + + // ===== Prover side ====== + // create sumcheck + auto prover_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + CombineFunction combine_func(EQ_X_AB_MINUS_C); SumCheckConfig config; SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); From d7673f681f88dbbb3d00a6a9d83f5d16b2232674 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 13 Jan 2025 08:22:24 +0200 Subject: [PATCH 037/127] format --- icicle/run | 4 ---- icicle/tests/test_field_api.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) delete mode 100755 icicle/run diff --git a/icicle/run b/icicle/run deleted file mode 100755 index 3384e588f8..0000000000 --- a/icicle/run +++ /dev/null @@ -1,4 +0,0 @@ -rm -rf build -cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DMSM=OFF -DSUMCHECK=ON -DHASH=ON -DG2=OFF -DECNTT=OFF -DEXT_FIELD=OFF -DFIELD=babybear -S . -B build -cmake --build build -j -build/tests/test_field_api --gtest_filter="*Sumcheck*" diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 8fbe7c079d..f455634a7f 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1213,11 +1213,11 @@ TEST_F(FieldApiTestBase, Sumcheck) // calculate the claimed sum scalar_t claimed_sum = scalar_t::zero(); for (int element_i = 0; element_i < mle_poly_size; element_i++) { - const scalar_t a = mle_polynomials[0][element_i]; - const scalar_t b = mle_polynomials[1][element_i]; - const scalar_t c = mle_polynomials[2][element_i]; + const scalar_t a = mle_polynomials[0][element_i]; + const scalar_t b = mle_polynomials[1][element_i]; + const scalar_t c = mle_polynomials[2][element_i]; const scalar_t eq = mle_polynomials[3][element_i]; - claimed_sum = claimed_sum + (a*b-c)*eq; + claimed_sum = claimed_sum + (a * b - c) * eq; } // ===== Prover side ====== From 3cb70fa380c49ebc33bfba6bf4fd487909d96c37 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:57:22 +0200 Subject: [PATCH 038/127] reduce alpha feet small and large fields --- icicle/backend/cpu/include/cpu_sumcheck.h | 28 +++++++++---------- .../cpu/include/cpu_sumcheck_transcript.h | 10 +++++-- icicle/cmake/fields_and_curves.cmake | 2 +- .../include/icicle/backend/sumcheck_backend.h | 2 +- icicle/include/icicle/sumcheck/sumcheck.h | 2 +- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 33b1242594..9f214d857a 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -27,10 +27,13 @@ namespace icicle { const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) override { + // Allocate memory for the intermidiate calcultion: the folded mle polynomials const int nof_mle_poly = mle_polynomials.size(); - std::vector folded_mle_polynomials(nof_mle_poly); - for (auto& mle_poly_ptr : folded_mle_polynomials) { - mle_poly_ptr = new F[mle_polynomial_size / 2]; + std::vector folded_mle_polynomials(nof_mle_poly); // folded mle_polynomials with the same format as inputs + std::vector folded_mle_polynomials_values(nof_mle_poly*mle_polynomial_size/2); // folded_mle_polynomials data itself + // init the folded_mle_polynomials pointers + for (int mle_polynomial_idx=0; mle_polynomial_idx < nof_mle_poly; mle_polynomial_idx++) { + folded_mle_polynomials[mle_polynomial_idx] = &(folded_mle_polynomials_values[mle_polynomial_idx*mle_polynomial_size/2]); } // Check that the size of the the proof feet the size of the mle polynomials. @@ -46,29 +49,26 @@ namespace icicle { ICICLE_LOG_ERROR << "Illegal polynomial degree (" << poly_degree << ") for provided combine function"; return eIcicleError::INVALID_ARGUMENT; } - reset(nof_rounds, uint32_t(poly_degree)); - // reset the sumcheck proof to accumulate - sumcheck_proof.reset(); + reset_transcript(nof_rounds, uint32_t(poly_degree)); // reset the transcript for the Fiat-Shamir + sumcheck_proof.reset(); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function CpuProgramExecutor program_executor(combine_function); - // calc the number of rounds = log2(poly_size) - + // run log2(poly_size) rounds int cur_mle_polynomial_size = mle_polynomial_size; - for (int round_idx = 0; round_idx < nof_rounds; ++round_idx) { + // For the first round work on the input mle_polynomials, else work on the folded const std::vector& in_mle_polynomials = (round_idx == 0) ? mle_polynomials : folded_mle_polynomials; std::vector& round_polynomial = sumcheck_proof.get_round_polynomial(round_idx); - // run the next round and update the proof + // build round polynomial and update the proof build_round_polynomial(in_mle_polynomials, cur_mle_polynomial_size, program_executor, round_polynomial); + // if its not the last round, calculate alpha and fold the mle polynomials if (round_idx + 1 < nof_rounds) { - // calculate alpha for the next round F alpha = get_alpha(round_polynomial); - fold_mle_polynomials(alpha, cur_mle_polynomial_size, in_mle_polynomials, folded_mle_polynomials); } } @@ -81,8 +81,8 @@ namespace icicle { return m_cpu_sumcheck_transcript.get_alpha(round_polynomial); } - // Reset the sumcheck transcript with e - void reset(const uint32_t num_vars, const uint32_t poly_degree) override + // Reset the sumcheck transcript before a new proof generation or verification + void reset_transcript(const uint32_t num_vars, const uint32_t poly_degree) override { m_cpu_sumcheck_transcript.reset(num_vars, poly_degree); } diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 4a31695ee4..ac5f9112c0 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -31,8 +31,7 @@ class CpuSumCheckTranscript std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); m_round_idx++; - S* hash_result_as_a_field = (S*)(hash_result.data()); - m_prev_alpha = (*hash_result_as_a_field) * S::one(); // TBD fix that to reduce + reduce_kash_result_to_field(m_prev_alpha, hash_result); std::cout << "alpha[" << m_round_idx - 1 << "] = " << m_prev_alpha << std::endl; return m_prev_alpha; } @@ -62,6 +61,13 @@ class CpuSumCheckTranscript byte_vec.insert(byte_vec.end(), label.begin(), label.end()); } + void reduce_kash_result_to_field(S& alpha, const std::vector& hash_result) { + alpha = S::zero(); + const int nof_bytes_to_copy = std::min(sizeof(alpha), hash_result.size()); + std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); + alpha = alpha * S::one(); + } + // append an integer uint32_t to hash input void append_u32(std::vector& byte_vec, const uint32_t data) { diff --git a/icicle/cmake/fields_and_curves.cmake b/icicle/cmake/fields_and_curves.cmake index 953dbd9011..e67f4fd6a7 100644 --- a/icicle/cmake/fields_and_curves.cmake +++ b/icicle/cmake/fields_and_curves.cmake @@ -5,7 +5,7 @@ set(ICICLE_FIELDS 1001:babybear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK 1002:stark252:NTT,POSEIDON,POSEIDON2,SUMCHECK 1003:m31:EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK - 1004:koalabear:NTT,EXT_FIELD,POSEIDON,POSEIDON2 + 1004:koalabear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK ) # Define available curves with an index and their supported features diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index ad2f999990..7997ee2ba2 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -61,7 +61,7 @@ namespace icicle { * @param poly_degree the degree of the combine function */ - virtual void reset(const uint32_t num_vars, const uint32_t poly_degree) = 0; + virtual void reset_transcript(const uint32_t num_vars, const uint32_t poly_degree) = 0; /** * @brief Calculate alpha based on m_transcript_config and the round polynomial. diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 0a271a72cf..bd06941b59 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -72,7 +72,7 @@ namespace icicle { const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); const uint32_t poly_degree = round_poly_0.size() - 1; - m_backend->reset(nof_rounds, poly_degree); + m_backend->reset_transcript(nof_rounds, poly_degree); // verify that the sum of round_polynomial-0 is the clamed_sum F round_poly_0_sum = round_poly_0[0]; From 07c1e85a728dac420d56d2084549e404cf5f19ff Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 13 Jan 2025 09:59:54 +0200 Subject: [PATCH 039/127] format --- icicle/backend/cpu/include/cpu_sumcheck.h | 14 ++++++++------ .../backend/cpu/include/cpu_sumcheck_transcript.h | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 9f214d857a..ad7db79075 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -27,13 +27,15 @@ namespace icicle { const SumCheckConfig& config, SumCheckProof& sumcheck_proof /*out*/) override { - // Allocate memory for the intermidiate calcultion: the folded mle polynomials + // Allocate memory for the intermediate calculation: the folded mle polynomials const int nof_mle_poly = mle_polynomials.size(); std::vector folded_mle_polynomials(nof_mle_poly); // folded mle_polynomials with the same format as inputs - std::vector folded_mle_polynomials_values(nof_mle_poly*mle_polynomial_size/2); // folded_mle_polynomials data itself - // init the folded_mle_polynomials pointers - for (int mle_polynomial_idx=0; mle_polynomial_idx < nof_mle_poly; mle_polynomial_idx++) { - folded_mle_polynomials[mle_polynomial_idx] = &(folded_mle_polynomials_values[mle_polynomial_idx*mle_polynomial_size/2]); + std::vector folded_mle_polynomials_values( + nof_mle_poly * mle_polynomial_size / 2); // folded_mle_polynomials data itself + // init the folded_mle_polynomials pointers + for (int mle_polynomial_idx = 0; mle_polynomial_idx < nof_mle_poly; mle_polynomial_idx++) { + folded_mle_polynomials[mle_polynomial_idx] = + &(folded_mle_polynomials_values[mle_polynomial_idx * mle_polynomial_size / 2]); } // Check that the size of the the proof feet the size of the mle polynomials. @@ -50,7 +52,7 @@ namespace icicle { return eIcicleError::INVALID_ARGUMENT; } - reset_transcript(nof_rounds, uint32_t(poly_degree)); // reset the transcript for the Fiat-Shamir + reset_transcript(nof_rounds, uint32_t(poly_degree)); // reset the transcript for the Fiat-Shamir sumcheck_proof.reset(); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index ac5f9112c0..73c0942dd0 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -61,7 +61,8 @@ class CpuSumCheckTranscript byte_vec.insert(byte_vec.end(), label.begin(), label.end()); } - void reduce_kash_result_to_field(S& alpha, const std::vector& hash_result) { + void reduce_kash_result_to_field(S& alpha, const std::vector& hash_result) + { alpha = S::zero(); const int nof_bytes_to_copy = std::min(sizeof(alpha), hash_result.size()); std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); From 9e60bb990d1d3ccdcd8989f670ddcff2d856e2a2 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:26:25 +0200 Subject: [PATCH 040/127] documentation --- icicle/backend/cpu/include/cpu_sumcheck.h | 79 +++---------------- .../cpu/include/cpu_sumcheck_transcript.h | 25 +++--- .../include/icicle/backend/sumcheck_backend.h | 7 +- icicle/include/icicle/program/program.h | 4 +- icicle/include/icicle/sumcheck/sumcheck.h | 4 +- 5 files changed, 31 insertions(+), 88 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index ad7db79075..962ea04bd4 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -46,13 +46,13 @@ namespace icicle { return eIcicleError::INVALID_ARGUMENT; } // check that the combine function has a legal polynomial degree - int poly_degree = combine_function.get_polynomial_degee(); - if (poly_degree < 0) { - ICICLE_LOG_ERROR << "Illegal polynomial degree (" << poly_degree << ") for provided combine function"; + int combine_function_poly_degree = combine_function.get_polynomial_degee(); + if (combine_function_poly_degree < 0) { + ICICLE_LOG_ERROR << "Illegal polynomial degree (" << combine_function_poly_degree << ") for provided combine function"; return eIcicleError::INVALID_ARGUMENT; } - reset_transcript(nof_rounds, uint32_t(poly_degree)); // reset the transcript for the Fiat-Shamir + reset_transcript(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the transcript for the Fiat-Shamir sumcheck_proof.reset(); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function @@ -84,9 +84,9 @@ namespace icicle { } // Reset the sumcheck transcript before a new proof generation or verification - void reset_transcript(const uint32_t num_vars, const uint32_t poly_degree) override + void reset_transcript(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) override { - m_cpu_sumcheck_transcript.reset(num_vars, poly_degree); + m_cpu_sumcheck_transcript.reset(mle_polynomial_size, combine_function_poly_degree); } private: @@ -132,15 +132,16 @@ namespace icicle { } } + // Fold the MLE polynomials based on alpha void fold_mle_polynomials( const F& alpha, int& mle_polynomial_size, - const std::vector& in_mle_polynomials, - std::vector& folded_mle_polynomials) + const std::vector& in_mle_polynomials, // input + std::vector& folded_mle_polynomials) // output { const int nof_polynomials = in_mle_polynomials.size(); const F one_minus_alpha = F::one() - alpha; - mle_polynomial_size >>= 1; + mle_polynomial_size >>= 1; // update the mle_polynomial size to /2 det to folding // run over all elements in all polynomials for (int element_idx = 0; element_idx < mle_polynomial_size; ++element_idx) { @@ -152,66 +153,6 @@ namespace icicle { } } } - - // void calc_round_polynomial(const std::vector& input_polynomials, bool fold, S& alpha) { - // const uint64_t polynomial_size = fold ? input_polynomials[0].size()/4 : input_polynomials[0].size()/2; - // const uint nof_polynomials = input_polynomials.size(); - // const uint round_poly_degree = m_combine_prog.m_poly_degree; - // m_round_polynomial.resize(round_poly_degree+1); - // TODO:: init m_round_polynomial to zero; - - // // init m_program_executor input pointers - // vector combine_func_inputs(m_combine_prog.m_nof_inputs); - // for (int poly_idx=0; i& round_poly) { // Make sure reset was called (Internal assertion) - ICICLE_ASSERT(m_num_vars > 0) << "num_vars must reset with value > 0"; - ICICLE_ASSERT(m_poly_degree > 0) << "poly_degree must reset with value > 0"; + ICICLE_ASSERT(m_mle_polynomial_size > 0) << "mle_polynomial_size must reset with value > 0"; + ICICLE_ASSERT(m_combine_function_poly_degree > 0) << "combine_function_poly_degree must reset with value > 0"; for (int element_i = 0; element_i < round_poly.size(); element_i++) { std::cout << "round_poly[" << m_round_idx << "][" << element_i << "] = " << round_poly[element_i] << std::endl; @@ -37,10 +37,10 @@ class CpuSumCheckTranscript } // reset the transcript - void reset(const uint32_t num_vars, const uint32_t poly_degree) + void reset(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) { - m_num_vars = num_vars; - m_poly_degree = poly_degree; + m_mle_polynomial_size = mle_polynomial_size; + m_combine_function_poly_degree = combine_function_poly_degree; m_entry_0.clear(); m_round_idx = 0; } @@ -50,8 +50,8 @@ class CpuSumCheckTranscript HashConfig m_config; // hash config - default uint32_t m_round_idx; // std::vector m_entry_0; // - uint32_t m_num_vars = 0; - uint32_t m_poly_degree = 0; + uint32_t m_mle_polynomial_size = 0; + uint32_t m_combine_function_poly_degree = 0; const S m_claimed_sum; S m_prev_alpha; @@ -61,6 +61,7 @@ class CpuSumCheckTranscript byte_vec.insert(byte_vec.end(), label.begin(), label.end()); } + // convert a vector of bytes to a field void reduce_kash_result_to_field(S& alpha, const std::vector& hash_result) { alpha = S::zero(); @@ -76,20 +77,22 @@ class CpuSumCheckTranscript byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(uint32_t)); } + // append a field to hash input void append_field(std::vector& byte_vec, const S& field) { const std::byte* data_bytes = reinterpret_cast(field.limbs_storage.limbs); byte_vec.insert(byte_vec.end(), data_bytes, data_bytes + sizeof(S)); } + // round 0 hash input void build_hash_input_round_0(std::vector& hash_input, const std::vector& round_poly) { const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); - // append entry_DS = [domain_separator_label || proof.num_vars || proof.degree || public (hardcoded?) || + // append entry_DS = [domain_separator_label || proof.mle_polynomial_size || proof.degree || public (hardcoded?) || // claimed_sum] append_data(hash_input, m_transcript_config.get_domain_separator_label()); - append_u32(hash_input, m_num_vars); - append_u32(hash_input, m_poly_degree); + append_u32(hash_input, m_mle_polynomial_size); + append_u32(hash_input, m_combine_function_poly_degree); append_field(hash_input, m_claimed_sum); // append seed_rng @@ -109,6 +112,8 @@ class CpuSumCheckTranscript // append entry_0 append_data(hash_input, m_entry_0); } + + // round !=0 hash input void build_hash_input_round_i(std::vector& hash_input, const std::vector& round_poly) { const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 7997ee2ba2..5051d0f635 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -57,11 +57,10 @@ namespace icicle { /** * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. - * @param num_vars - * @param poly_degree the degree of the combine function + * @param mle_polynomial_size the number of elements in an MLE polynomial + * @param combine_function_poly_degree the degree of the combine function */ - - virtual void reset_transcript(const uint32_t num_vars, const uint32_t poly_degree) = 0; + virtual void reset_transcript(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) = 0; /** * @brief Calculate alpha based on m_transcript_config and the round polynomial. diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 02aea24d59..5c7c1d67b6 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -1,11 +1,9 @@ #pragma once -#include -#include #include #include -#include "icicle/errors.h" #include "icicle/program/symbol.h" +#include "icicle/utils/log.h" namespace icicle { diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index bd06941b59..351a46bf1d 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -71,8 +71,8 @@ namespace icicle { { const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); - const uint32_t poly_degree = round_poly_0.size() - 1; - m_backend->reset_transcript(nof_rounds, poly_degree); + const uint32_t combine_function_poly_degree = round_poly_0.size() - 1; + m_backend->reset_transcript(nof_rounds, combine_function_poly_degree); // verify that the sum of round_polynomial-0 is the clamed_sum F round_poly_0_sum = round_poly_0[0]; From 53dd569d715de304edec2e30b2149e1a97ee6adb Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 13 Jan 2025 10:27:20 +0200 Subject: [PATCH 041/127] format --- icicle/backend/cpu/include/cpu_sumcheck.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 962ea04bd4..5d0055baca 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -48,7 +48,8 @@ namespace icicle { // check that the combine function has a legal polynomial degree int combine_function_poly_degree = combine_function.get_polynomial_degee(); if (combine_function_poly_degree < 0) { - ICICLE_LOG_ERROR << "Illegal polynomial degree (" << combine_function_poly_degree << ") for provided combine function"; + ICICLE_LOG_ERROR << "Illegal polynomial degree (" << combine_function_poly_degree + << ") for provided combine function"; return eIcicleError::INVALID_ARGUMENT; } @@ -136,12 +137,12 @@ namespace icicle { void fold_mle_polynomials( const F& alpha, int& mle_polynomial_size, - const std::vector& in_mle_polynomials, // input - std::vector& folded_mle_polynomials) // output + const std::vector& in_mle_polynomials, // input + std::vector& folded_mle_polynomials) // output { const int nof_polynomials = in_mle_polynomials.size(); const F one_minus_alpha = F::one() - alpha; - mle_polynomial_size >>= 1; // update the mle_polynomial size to /2 det to folding + mle_polynomial_size >>= 1; // update the mle_polynomial size to /2 det to folding // run over all elements in all polynomials for (int element_idx = 0; element_idx < mle_polynomial_size; ++element_idx) { From b7d5ddce2a9774756c94005388766707c0e41740 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:35:41 +0200 Subject: [PATCH 042/127] go comp issue --- icicle/include/icicle/program/program.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 5c7c1d67b6..48964cd7b9 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -167,7 +167,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(operation->m_variable_idx); InstructionType instruction; - std::memcpy(&instruction, int_arr, sizeof(InstructionType)); + memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } @@ -183,7 +183,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(dest_idx); InstructionType instruction; - std::memcpy(&instruction, int_arr, sizeof(InstructionType)); + memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } From d94ebecb370d58e5ef003f15e4052e10847d49bb Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:38:16 +0200 Subject: [PATCH 043/127] remove prints --- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 4 ---- icicle/tests/test_field_api.cpp | 3 --- 2 files changed, 7 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 9933ccc9ef..0085a43ef1 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -18,9 +18,6 @@ class CpuSumCheckTranscript ICICLE_ASSERT(m_mle_polynomial_size > 0) << "mle_polynomial_size must reset with value > 0"; ICICLE_ASSERT(m_combine_function_poly_degree > 0) << "combine_function_poly_degree must reset with value > 0"; - for (int element_i = 0; element_i < round_poly.size(); element_i++) { - std::cout << "round_poly[" << m_round_idx << "][" << element_i << "] = " << round_poly[element_i] << std::endl; - } const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); std::vector hash_input; (m_round_idx == 0) ? build_hash_input_round_0(hash_input, round_poly) @@ -32,7 +29,6 @@ class CpuSumCheckTranscript hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); m_round_idx++; reduce_kash_result_to_field(m_prev_alpha, hash_result); - std::cout << "alpha[" << m_round_idx - 1 << "] = " << m_prev_alpha << std::endl; return m_prev_alpha; } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index f455634a7f..48ce66f44b 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1205,8 +1205,6 @@ TEST_F(FieldApiTestBase, Sumcheck) mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; for (int element_i = 0; element_i < mle_poly_size; element_i++) { mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i * 10 + element_i + 1); - std::cout << "mle_polynomials[" << poly_i << "][" << element_i << "] = " << mle_polynomials[poly_i][element_i] - << std::endl; } } @@ -1229,7 +1227,6 @@ TEST_F(FieldApiTestBase, Sumcheck) SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); - sumcheck_proof.print_proof(); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; } From 7b06f73f0b4274b98529d1d2a366b63ac18d7469 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:43:02 +0200 Subject: [PATCH 044/127] return the std:: --- icicle/include/icicle/program/program.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 48964cd7b9..5c7c1d67b6 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -167,7 +167,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(operation->m_variable_idx); InstructionType instruction; - memcpy(&instruction, int_arr, sizeof(InstructionType)); + std::memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } @@ -183,7 +183,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(dest_idx); InstructionType instruction; - memcpy(&instruction, int_arr, sizeof(InstructionType)); + std::memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } From 2840f9130cc272e3e336ef9ce4eaa04ec5ddf246 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:27:47 +0200 Subject: [PATCH 045/127] name fix --- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 0085a43ef1..a3e901d0d1 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -28,7 +28,7 @@ class CpuSumCheckTranscript std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_config, hash_result.data()); m_round_idx++; - reduce_kash_result_to_field(m_prev_alpha, hash_result); + reduce_hash_result_to_field(m_prev_alpha, hash_result); return m_prev_alpha; } @@ -58,7 +58,7 @@ class CpuSumCheckTranscript } // convert a vector of bytes to a field - void reduce_kash_result_to_field(S& alpha, const std::vector& hash_result) + void reduce_hash_result_to_field(S& alpha, const std::vector& hash_result) { alpha = S::zero(); const int nof_bytes_to_copy = std::min(sizeof(alpha), hash_result.size()); From c22d38e6c6a43e46fc0ea5cd5662e7ecf13e0e2f Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 13 Jan 2025 11:28:06 +0200 Subject: [PATCH 046/127] include added for compilation --- icicle/include/icicle/program/program.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/icicle/include/icicle/program/program.h b/icicle/include/icicle/program/program.h index 5c7c1d67b6..ff64cb8547 100644 --- a/icicle/include/icicle/program/program.h +++ b/icicle/include/icicle/program/program.h @@ -2,6 +2,7 @@ #include #include +#include #include "icicle/program/symbol.h" #include "icicle/utils/log.h" @@ -167,7 +168,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(operation->m_variable_idx); InstructionType instruction; - std::memcpy(&instruction, int_arr, sizeof(InstructionType)); + memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } @@ -183,7 +184,7 @@ namespace icicle { // Set instruction::operand2 int_arr[INST_RESULT] = std::byte(dest_idx); InstructionType instruction; - std::memcpy(&instruction, int_arr, sizeof(InstructionType)); + memcpy(&instruction, int_arr, sizeof(InstructionType)); m_instructions.push_back(instruction); } From 48f30df39fa876e741e6a534192526a73bd163b8 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:17:44 +0200 Subject: [PATCH 047/127] review fixes --- icicle/backend/cpu/include/cpu_sumcheck.h | 6 +++--- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 4 ++-- icicle/include/icicle/backend/sumcheck_backend.h | 10 +++++----- icicle/include/icicle/sumcheck/sumcheck.h | 10 +++++----- icicle/include/icicle/sumcheck/sumcheck_config.h | 6 +++--- icicle/include/icicle/sumcheck/sumcheck_proof.h | 6 +++--- icicle/tests/test_field_api.cpp | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 5d0055baca..e694684476 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -24,8 +24,8 @@ namespace icicle { const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, const CombineFunction& combine_function, - const SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) override + const SumcheckConfig& config, + SumcheckProof& sumcheck_proof /*out*/) override { // Allocate memory for the intermediate calculation: the folded mle polynomials const int nof_mle_poly = mle_polynomials.size(); @@ -92,7 +92,7 @@ namespace icicle { private: // members - CpuSumCheckTranscript m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) + CpuSumcheckTranscript m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) void build_round_polynomial( const std::vector& in_mle_polynomials, diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index a3e901d0d1..f8bb12ec6d 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -2,10 +2,10 @@ #include "icicle/sumcheck/sumcheck_transcript_config.h" template -class CpuSumCheckTranscript +class CpuSumcheckTranscript { public: - CpuSumCheckTranscript(const S& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + CpuSumcheckTranscript(const S& claimed_sum, SumcheckTranscriptConfig&& transcript_config) : m_claimed_sum(claimed_sum), m_transcript_config(std::move(transcript_config)) { reset(0, 0); diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 5051d0f635..b4853b49a3 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -45,15 +45,15 @@ namespace icicle { * @param mle_polynomial_size the size of each MLE polynomial * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. - * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. + * @param sumcheck_proof Reference to the SumcheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ virtual eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, const CombineFunction& combine_function, - const SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) = 0; + const SumcheckConfig& config, + SumcheckProof& sumcheck_proof /*out*/) = 0; /** * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. @@ -72,8 +72,8 @@ namespace icicle { const F& get_claimed_sum() const { return m_claimed_sum; } protected: - const F m_claimed_sum; ///< Vector of hash functions for each layer. - const SumcheckTranscriptConfig m_transcript_config; ///< Size of each leaf element in bytes. + const F m_claimed_sum; // claimed sum fof the mle polinomials + const SumcheckTranscriptConfig m_transcript_config; // configuration how to build the transcript }; /*************************** Backend Factory Registration ***************************/ diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 351a46bf1d..0da642a51e 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -48,26 +48,26 @@ namespace icicle { * @param mle_polynomial_size the size of each MLE polynomial * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. * @param config Configuration for the Sumcheck operation. - * @param sumcheck_proof Reference to the SumCheckProof object where all round polynomials will be stored. + * @param sumcheck_proof Reference to the SumcheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, const CombineFunction& combine_function, - const SumCheckConfig& config, - SumCheckProof& sumcheck_proof /*out*/) const + const SumcheckConfig& config, + SumcheckProof& sumcheck_proof /*out*/) const { return m_backend->get_proof(mle_polynomials, mle_polynomial_size, combine_function, config, sumcheck_proof); } /** * @brief Verify an element against the Sumcheck round polynomial. - * @param sumcheck_proof The SumCheckProof object includes the round polynomials. + * @param sumcheck_proof The SumcheckProof object includes the round polynomials. * @param valid output valid bit. True if the Proof is valid, false otherwise. * @return Error code of type eIcicleError indicating success or failure. */ - eIcicleError verify(SumCheckProof& sumcheck_proof, bool& valid /*out*/) + eIcicleError verify(SumcheckProof& sumcheck_proof, bool& valid /*out*/) { const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); diff --git a/icicle/include/icicle/sumcheck/sumcheck_config.h b/icicle/include/icicle/sumcheck/sumcheck_config.h index 2354101628..9c84245578 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_config.h @@ -13,7 +13,7 @@ namespace icicle { * execution modes, as well as backend-specific extensions. */ - struct SumCheckConfig { + struct SumcheckConfig { icicleStreamHandle stream = nullptr; /**< Stream for asynchronous execution. Default is nullptr. */ uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ bool are_inputs_on_device = @@ -30,8 +30,8 @@ namespace icicle { * This function provides a default configuration for Sumcheck operations with synchronous execution * and all data (leaves, tree results, and paths) residing on the host (CPU). * - * @return A default SumCheckConfig with host-based execution and no backend-specific extensions. + * @return A default SumcheckConfig with host-based execution and no backend-specific extensions. */ - static SumCheckConfig default_sumcheck_config() { return SumCheckConfig(); } + static SumcheckConfig default_sumcheck_config() { return SumcheckConfig(); } } // namespace icicle diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 8f168baadf..dcdb876932 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -11,17 +11,17 @@ namespace icicle { * This class encapsulates the sumcheck proof, contains the evaluations of the round polynomials * for each layer at the sumcheck proof. * Evaluations are at x = 0, 1, 2 ... K - * Where K is the degree of the combiine function used at the sumcheck protocol. + * Where K is the degree of the combine function used at the sumcheck protocol. * * @tparam S Type of the field element (e.g., prime field or extension field elements). */ template - class SumCheckProof + class SumcheckProof { public: // Constructor - SumCheckProof(int nof_round_polynomials, int round_polynomial_degree) + SumcheckProof(int nof_round_polynomials, int round_polynomial_degree) : m_round_polynomials(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) { if (nof_round_polynomials == 0) { diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 48ce66f44b..a43fd3bae8 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1223,8 +1223,8 @@ TEST_F(FieldApiTestBase, Sumcheck) auto prover_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); CombineFunction combine_func(EQ_X_AB_MINUS_C); - SumCheckConfig config; - SumCheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); + SumcheckConfig config; + SumcheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); for (auto& mle_poly_ptr : mle_polynomials) { From 81d870f67cfbe5925f8d9c978ca6fb3cc2637155 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 13 Jan 2025 18:21:49 +0200 Subject: [PATCH 048/127] format --- icicle/include/icicle/backend/sumcheck_backend.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index b4853b49a3..017f623466 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -72,7 +72,7 @@ namespace icicle { const F& get_claimed_sum() const { return m_claimed_sum; } protected: - const F m_claimed_sum; // claimed sum fof the mle polinomials + const F m_claimed_sum; // claimed sum for the mle polynomials const SumcheckTranscriptConfig m_transcript_config; // configuration how to build the transcript }; From 5baee05ad1d42a304dea73bc63f9ad2f85e2df7a Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:11:57 +0200 Subject: [PATCH 049/127] removed OR HASH --- icicle/backend/cpu/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index e925ccc0e0..32711c58cc 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -45,7 +45,7 @@ if (FIELD) if (POSEIDON2) target_sources(icicle_field PRIVATE src/hash/cpu_poseidon2.cpp) endif() - if(SUMCHECK OR HASH) + if(SUMCHECK) target_sources(icicle_field PRIVATE src/field/cpu_sumcheck.cpp) endif() target_include_directories(icicle_field PRIVATE include) From ae57939012021b917c05023cde7f2ad225dccd52 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:08:46 +0200 Subject: [PATCH 050/127] review fixes --- icicle/backend/cpu/include/cpu_sumcheck.h | 10 +++------ .../cpu/include/cpu_sumcheck_transcript.h | 1 + icicle/include/icicle/sumcheck/sumcheck.h | 4 +++- .../include/icicle/sumcheck/sumcheck_proof.h | 21 +++++++------------ icicle/tests/test_field_api.cpp | 12 +++++------ 5 files changed, 19 insertions(+), 29 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index e694684476..b0ac4eb606 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -39,12 +39,8 @@ namespace icicle { } // Check that the size of the the proof feet the size of the mle polynomials. - const uint32_t nof_rounds = sumcheck_proof.get_nof_round_polynomials(); - if (std::log2(mle_polynomial_size) != nof_rounds) { - ICICLE_LOG_ERROR << "Sumcheck proof size(" << nof_rounds << ") should be log of the mle polynomial size(" - << mle_polynomial_size << ")"; - return eIcicleError::INVALID_ARGUMENT; - } + const uint32_t nof_rounds = std::log2(mle_polynomial_size); + // check that the combine function has a legal polynomial degree int combine_function_poly_degree = combine_function.get_polynomial_degee(); if (combine_function_poly_degree < 0) { @@ -54,7 +50,7 @@ namespace icicle { } reset_transcript(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the transcript for the Fiat-Shamir - sumcheck_proof.reset(); // reset the sumcheck proof to accumulate the round polynomials + sumcheck_proof.init(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function CpuProgramExecutor program_executor(combine_function); diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index f8bb12ec6d..89a330cc9f 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -20,6 +20,7 @@ class CpuSumcheckTranscript const std::vector& round_poly_label = m_transcript_config.get_round_poly_label(); std::vector hash_input; + hash_input.reserve(2048); (m_round_idx == 0) ? build_hash_input_round_0(hash_input, round_poly) : build_hash_input_round_i(hash_input, round_poly); diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 0da642a51e..00254d008e 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -63,12 +63,15 @@ namespace icicle { /** * @brief Verify an element against the Sumcheck round polynomial. + * First round polynomial is verified agains the claimed sum. + * Last round polynomial is not verified. * @param sumcheck_proof The SumcheckProof object includes the round polynomials. * @param valid output valid bit. True if the Proof is valid, false otherwise. * @return Error code of type eIcicleError indicating success or failure. */ eIcicleError verify(SumcheckProof& sumcheck_proof, bool& valid /*out*/) { + valid = false; const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); const uint32_t combine_function_poly_degree = round_poly_0.size() - 1; @@ -94,7 +97,6 @@ namespace icicle { const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx + 1); F expected_alpha_value = next_round_poly[0] + next_round_poly[1]; if (alpha_value != expected_alpha_value) { - valid = false; ICICLE_LOG_ERROR << "verification failed on round: " << round_idx; return eIcicleError::SUCCESS; } diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index dcdb876932..43bb8889cc 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -21,12 +21,15 @@ namespace icicle { { public: // Constructor - SumcheckProof(int nof_round_polynomials, int round_polynomial_degree) - : m_round_polynomials(nof_round_polynomials, std::vector(round_polynomial_degree + 1)) - { + SumcheckProof() { + } + + // Init the round polynomial values for the problem + void init(int nof_round_polynomials, int round_polynomial_degree) { if (nof_round_polynomials == 0) { - ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials << ") in the proof must be >0"; + ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials << ") in the proof must be > 0"; } + m_round_polynomials.resize(nof_round_polynomials, std::vector(round_polynomial_degree + 1, S::zero())); } // set the value of polynomial round_polynomial_idx at x = evaluation_idx @@ -41,16 +44,6 @@ namespace icicle { uint get_nof_round_polynomials() const { return m_round_polynomials.size(); } uint get_round_polynomial_size() const { return m_round_polynomials[0].size() + 1; } - // Reset the proof to zeros - void reset() - { - for (auto& round_poly : m_round_polynomials) { - for (auto& element : round_poly) { - element = S::zero(); - } - } - } - private: std::vector> m_round_polynomials; // logN vectors of round_poly_degree elements diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a43fd3bae8..4dc9506474 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1203,9 +1203,7 @@ TEST_F(FieldApiTestBase, Sumcheck) std::vector mle_polynomials(nof_mle_poly); for (int poly_i = 0; poly_i < nof_mle_poly; poly_i++) { mle_polynomials[poly_i] = new scalar_t[mle_poly_size]; - for (int element_i = 0; element_i < mle_poly_size; element_i++) { - mle_polynomials[poly_i][element_i] = scalar_t::from(poly_i * 10 + element_i + 1); - } + scalar_t::rand_host_many(mle_polynomials[poly_i], mle_poly_size); } // calculate the claimed sum @@ -1224,9 +1222,9 @@ TEST_F(FieldApiTestBase, Sumcheck) CombineFunction combine_func(EQ_X_AB_MINUS_C); SumcheckConfig config; - SumcheckProof sumcheck_proof(log_mle_poly_size, nof_mle_poly - 1); + SumcheckProof sumcheck_proof; - prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof); + ICICLE_CHECK(prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof)); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; } @@ -1234,8 +1232,8 @@ TEST_F(FieldApiTestBase, Sumcheck) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); - bool verification_pass; - verifier_sumcheck.verify(sumcheck_proof, verification_pass); + bool verification_pass = false; + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, verification_pass)); ASSERT_EQ(true, verification_pass); } From 7956576a16f20cb6ea5b59211676750f1b97d6b2 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Wed, 15 Jan 2025 09:10:23 +0200 Subject: [PATCH 051/127] format --- icicle/backend/cpu/include/cpu_sumcheck.h | 4 +++- icicle/include/icicle/sumcheck/sumcheck.h | 2 +- icicle/include/icicle/sumcheck/sumcheck_proof.h | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index b0ac4eb606..1b777b55d7 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -50,7 +50,9 @@ namespace icicle { } reset_transcript(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the transcript for the Fiat-Shamir - sumcheck_proof.init(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the sumcheck proof to accumulate the round polynomials + sumcheck_proof.init( + nof_rounds, + uint32_t(combine_function_poly_degree)); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function CpuProgramExecutor program_executor(combine_function); diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 00254d008e..69e75df91a 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -63,7 +63,7 @@ namespace icicle { /** * @brief Verify an element against the Sumcheck round polynomial. - * First round polynomial is verified agains the claimed sum. + * First round polynomial is verified agains the claimed sum. * Last round polynomial is not verified. * @param sumcheck_proof The SumcheckProof object includes the round polynomials. * @param valid output valid bit. True if the Proof is valid, false otherwise. diff --git a/icicle/include/icicle/sumcheck/sumcheck_proof.h b/icicle/include/icicle/sumcheck/sumcheck_proof.h index 43bb8889cc..1d51f0c2d9 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_proof.h +++ b/icicle/include/icicle/sumcheck/sumcheck_proof.h @@ -21,11 +21,11 @@ namespace icicle { { public: // Constructor - SumcheckProof() { - } + SumcheckProof() {} // Init the round polynomial values for the problem - void init(int nof_round_polynomials, int round_polynomial_degree) { + void init(int nof_round_polynomials, int round_polynomial_degree) + { if (nof_round_polynomials == 0) { ICICLE_LOG_ERROR << "Number of round polynomials(" << nof_round_polynomials << ") in the proof must be > 0"; } From 51d6e8d2994a50ea3b1b9f0392cbddc6f4cbaa5c Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:12:41 +0200 Subject: [PATCH 052/127] spell --- icicle/include/icicle/sumcheck/sumcheck.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 69e75df91a..87c0c30899 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -63,7 +63,7 @@ namespace icicle { /** * @brief Verify an element against the Sumcheck round polynomial. - * First round polynomial is verified agains the claimed sum. + * First round polynomial is verified against the claimed sum. * Last round polynomial is not verified. * @param sumcheck_proof The SumcheckProof object includes the round polynomials. * @param valid output valid bit. True if the Proof is valid, false otherwise. From 5bd4e56155371fafdfd262f7212baa0422b29cb2 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:04:02 +0200 Subject: [PATCH 053/127] added use_extension_field for Sumcheckconfig --- icicle/backend/cpu/include/cpu_sumcheck.h | 4 ++++ icicle/include/icicle/sumcheck/sumcheck_config.h | 1 + 2 files changed, 5 insertions(+) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 1b777b55d7..af14341e7e 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -27,6 +27,10 @@ namespace icicle { const SumcheckConfig& config, SumcheckProof& sumcheck_proof /*out*/) override { + if (config.use_extension_field) { + ICICLE_LOG_ERROR << "SumcheckConfig::use_extension_field field = true is currently unsupported"; + return eIcicleError::INVALID_ARGUMENT; + } // Allocate memory for the intermediate calculation: the folded mle polynomials const int nof_mle_poly = mle_polynomials.size(); std::vector folded_mle_polynomials(nof_mle_poly); // folded mle_polynomials with the same format as inputs diff --git a/icicle/include/icicle/sumcheck/sumcheck_config.h b/icicle/include/icicle/sumcheck/sumcheck_config.h index 9c84245578..7d7acf430b 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_config.h @@ -15,6 +15,7 @@ namespace icicle { struct SumcheckConfig { icicleStreamHandle stream = nullptr; /**< Stream for asynchronous execution. Default is nullptr. */ + bool use_extension_field = false; /**< If true, then use extension field for the fiat shamir result. Recommended for small fields for security*/ uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ bool are_inputs_on_device = false; /**< True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. */ From 6d208560abe08568447ea19f8996599d6e3285d6 Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Wed, 15 Jan 2025 13:05:10 +0200 Subject: [PATCH 054/127] format --- icicle/include/icicle/sumcheck/sumcheck_config.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck_config.h b/icicle/include/icicle/sumcheck/sumcheck_config.h index 7d7acf430b..3d34a8ae82 100644 --- a/icicle/include/icicle/sumcheck/sumcheck_config.h +++ b/icicle/include/icicle/sumcheck/sumcheck_config.h @@ -15,8 +15,9 @@ namespace icicle { struct SumcheckConfig { icicleStreamHandle stream = nullptr; /**< Stream for asynchronous execution. Default is nullptr. */ - bool use_extension_field = false; /**< If true, then use extension field for the fiat shamir result. Recommended for small fields for security*/ - uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ + bool use_extension_field = false; /**< If true, then use extension field for the fiat shamir result. Recommended for + small fields for security*/ + uint64_t batch = 1; /**< Number of input chunks to hash in batch. Default is 1. */ bool are_inputs_on_device = false; /**< True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. */ bool are_outputs_on_device = From 9ea4eeaf382f349f672d3fd309bc356edea0194d Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:34:42 +0200 Subject: [PATCH 055/127] enlarging the sumcheck test to 8k --- icicle/include/icicle/sumcheck/sumcheck.h | 6 ++---- icicle/tests/test_field_api.cpp | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 87c0c30899..ff7ac7c17f 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -78,10 +78,8 @@ namespace icicle { m_backend->reset_transcript(nof_rounds, combine_function_poly_degree); // verify that the sum of round_polynomial-0 is the clamed_sum - F round_poly_0_sum = round_poly_0[0]; - for (int round_idx = 1; round_idx < nof_rounds - 1; round_idx++) { - round_poly_0_sum = round_poly_0_sum + round_poly_0[round_idx]; - } + F round_poly_0_sum = round_poly_0[0] + round_poly_0[1]; + const F& claimed_sum = m_backend->get_claimed_sum(); if (round_poly_0_sum != claimed_sum) { valid = false; diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 4dc9506474..8da3152140 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1192,7 +1192,7 @@ TEST_F(FieldApiTestBase, ProgramExecutorVecOpDataOnDevice) TEST_F(FieldApiTestBase, Sumcheck) { - int log_mle_poly_size = 3; + int log_mle_poly_size = 13; int mle_poly_size = 1 << log_mle_poly_size; int nof_mle_poly = 4; From a9da45da46088c7dfc21848c962d8442818ed976 Mon Sep 17 00:00:00 2001 From: mickeyasa <100796045+mickeyasa@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:33:54 +0200 Subject: [PATCH 056/127] fiat shamir moved to frontend --- icicle/backend/cpu/include/cpu_sumcheck.h | 34 +++++----------- .../cpu/include/cpu_sumcheck_transcript.h | 24 ++++------- icicle/backend/cpu/src/field/cpu_sumcheck.cpp | 4 +- .../include/icicle/backend/sumcheck_backend.h | 40 ++++--------------- icicle/include/icicle/sumcheck/sumcheck.h | 32 +++++++++------ icicle/src/sumcheck/sumcheck.cpp | 4 +- icicle/src/sumcheck/sumcheck_c_api.cpp | 2 +- icicle/tests/test_field_api.cpp | 11 +++-- 8 files changed, 55 insertions(+), 96 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index af14341e7e..77bc8e7776 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -13,21 +13,19 @@ namespace icicle { class CpuSumcheckBackend : public SumcheckBackend { public: - CpuSumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - : SumcheckBackend(claimed_sum, std::move(transcript_config)), - m_cpu_sumcheck_transcript(claimed_sum, std::move(transcript_config)) - { - } + CpuSumcheckBackend() : SumcheckBackend() {} // Calculate a proof for the mle polynomials eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, + const F& claimed_sum, const CombineFunction& combine_function, - const SumcheckConfig& config, + const SumcheckTranscriptConfig&& transcript_config, + const SumcheckConfig& sumcheck_config, SumcheckProof& sumcheck_proof /*out*/) override { - if (config.use_extension_field) { + if (sumcheck_config.use_extension_field) { ICICLE_LOG_ERROR << "SumcheckConfig::use_extension_field field = true is currently unsupported"; return eIcicleError::INVALID_ARGUMENT; } @@ -53,10 +51,12 @@ namespace icicle { return eIcicleError::INVALID_ARGUMENT; } - reset_transcript(nof_rounds, uint32_t(combine_function_poly_degree)); // reset the transcript for the Fiat-Shamir + // create sumcheck_transcript for the Fiat-Shamir + const uint32_t combine_function_poly_degree_u = combine_function_poly_degree; + CpuSumcheckTranscript sumcheck_transcript(claimed_sum, nof_rounds, combine_function_poly_degree_u, std::move(transcript_config)); sumcheck_proof.init( nof_rounds, - uint32_t(combine_function_poly_degree)); // reset the sumcheck proof to accumulate the round polynomials + combine_function_poly_degree_u); // reset the sumcheck proof to accumulate the round polynomials // generate a program executor for the combine function CpuProgramExecutor program_executor(combine_function); @@ -73,29 +73,15 @@ namespace icicle { // if its not the last round, calculate alpha and fold the mle polynomials if (round_idx + 1 < nof_rounds) { - F alpha = get_alpha(round_polynomial); + F alpha = sumcheck_transcript.get_alpha(round_polynomial); fold_mle_polynomials(alpha, cur_mle_polynomial_size, in_mle_polynomials, folded_mle_polynomials); } } return eIcicleError::SUCCESS; } - // calculate alpha for the next round based on the round_polynomial of the current round - F get_alpha(std::vector& round_polynomial) override - { - return m_cpu_sumcheck_transcript.get_alpha(round_polynomial); - } - - // Reset the sumcheck transcript before a new proof generation or verification - void reset_transcript(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) override - { - m_cpu_sumcheck_transcript.reset(mle_polynomial_size, combine_function_poly_degree); - } private: - // members - CpuSumcheckTranscript m_cpu_sumcheck_transcript; // Generates alpha for the next round (Fial-Shamir) - void build_round_polynomial( const std::vector& in_mle_polynomials, const int mle_polynomial_size, diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index 89a330cc9f..b3e866a1df 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -5,10 +5,11 @@ template class CpuSumcheckTranscript { public: - CpuSumcheckTranscript(const S& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - : m_claimed_sum(claimed_sum), m_transcript_config(std::move(transcript_config)) + CpuSumcheckTranscript(const S& claimed_sum, const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree, const SumcheckTranscriptConfig&& transcript_config) + : m_claimed_sum(claimed_sum), m_mle_polynomial_size(mle_polynomial_size), m_combine_function_poly_degree(combine_function_poly_degree), m_transcript_config(std::move(transcript_config)) { - reset(0, 0); + m_entry_0.clear(); + m_round_idx = 0; } // add round polynomial to the transcript @@ -33,20 +34,11 @@ class CpuSumcheckTranscript return m_prev_alpha; } - // reset the transcript - void reset(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) - { - m_mle_polynomial_size = mle_polynomial_size; - m_combine_function_poly_degree = combine_function_poly_degree; - m_entry_0.clear(); - m_round_idx = 0; - } - private: - const SumcheckTranscriptConfig m_transcript_config; // configuration how to build the transcript - HashConfig m_config; // hash config - default - uint32_t m_round_idx; // - std::vector m_entry_0; // + const SumcheckTranscriptConfig&& m_transcript_config; // configuration how to build the transcript + HashConfig m_config; // hash config - default + uint32_t m_round_idx; // + std::vector m_entry_0; // uint32_t m_mle_polynomial_size = 0; uint32_t m_combine_function_poly_degree = 0; const S m_claimed_sum; diff --git a/icicle/backend/cpu/src/field/cpu_sumcheck.cpp b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp index f89f8d6146..e13630a4ef 100644 --- a/icicle/backend/cpu/src/field/cpu_sumcheck.cpp +++ b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp @@ -8,11 +8,9 @@ namespace icicle { template eIcicleError cpu_create_sumcheck_backend( const Device& device, - const F& claimed_sum, - SumcheckTranscriptConfig&& transcript_config, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(claimed_sum, std::move(transcript_config)); + backend = std::make_shared>(); return eIcicleError::SUCCESS; } diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 017f623466..630a8d4c1c 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -27,15 +27,8 @@ namespace icicle { public: /** * @brief Constructor for the SumcheckBackend class. - * - * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) - * when evaluated over all possible Boolean input combinations - * @param transcript_config Configuration for encoding and hashing prover messages. */ - SumcheckBackend(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config) - : m_claimed_sum(claimed_sum), m_transcript_config(std::move(transcript_config)) - { - } + SumcheckBackend() {} virtual ~SumcheckBackend() = default; @@ -43,45 +36,28 @@ namespace icicle { * @brief Calculate the sumcheck based on the inputs and retrieve the Sumcheck proof. * @param mle_polynomials a vector of MLE polynomials to process * @param mle_polynomial_size the size of each MLE polynomial + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. - * @param config Configuration for the Sumcheck operation. + * @param transcript_config Configuration for encoding and hashing prover messages. + * @param sumcheck_config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumcheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ virtual eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, + const F& claimed_sum, const CombineFunction& combine_function, - const SumcheckConfig& config, + const SumcheckTranscriptConfig&& transcript_config, + const SumcheckConfig& sumcheck_config, SumcheckProof& sumcheck_proof /*out*/) = 0; - - /** - * @brief Initialize the transcript for the upcoming calculation of the FIat shamir. - * @param mle_polynomial_size the number of elements in an MLE polynomial - * @param combine_function_poly_degree the degree of the combine function - */ - virtual void reset_transcript(const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree) = 0; - - /** - * @brief Calculate alpha based on m_transcript_config and the round polynomial. - * @param round_polynomial a vector of MLE polynomials evaluated at x=0,1,2... - * @return alpha - */ - virtual F get_alpha(std::vector& round_polynomial) = 0; - - const F& get_claimed_sum() const { return m_claimed_sum; } - - protected: - const F m_claimed_sum; // claimed sum for the mle polynomials - const SumcheckTranscriptConfig m_transcript_config; // configuration how to build the transcript }; /*************************** Backend Factory Registration ***************************/ template using SumcheckFactoryImpl = std::function&& transcript_config, std::shared_ptr>& backend /*OUT*/)>; /** diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index ff7ac7c17f..6846737f87 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -6,20 +6,18 @@ #include "icicle/sumcheck/sumcheck_config.h" #include "icicle/sumcheck/sumcheck_transcript_config.h" #include "icicle/backend/sumcheck_backend.h" +#include "cpu_sumcheck_transcript.h" + namespace icicle { template class Sumcheck; /** * @brief Static factory method to create a Sumcheck instance. - * - * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) - * when evaluated over all possible Boolean input combinations - * @param transcript_config Configuration for encoding and hashing prover messages. * @return A Sumcheck object initialized with the specified backend. */ template - Sumcheck create_sumcheck(const F& claimed_sum, SumcheckTranscriptConfig&& transcript_config); + Sumcheck create_sumcheck(); /** * @brief Class for performing Sumcheck operations. @@ -46,19 +44,24 @@ namespace icicle { * F(X_1,X_2,X_3) = a_0 (1-X_1) (1-X_2) (1-X_3) + a_1 (1-X_1)(1-X_2) X_3 + a_2 (1-X_1) X_2 (1-X_3) + * a_3 (1-X_1) X_2 X_3 + a_4 X_1 (1-X_2) (1-X_3) + a_5 X_1 (1-X_2) X_3+ a_6 X_1 X_2 (1-X_3) + a_7 X_1 X_2 X_3 * @param mle_polynomial_size the size of each MLE polynomial + * @param claimed_sum The total sum of the values of a multivariate polynomial f(x₁, x₂, ..., xₖ) + * when evaluated over all possible Boolean input combinations * @param combine_function a program that define how to fold all MLS polynomials into the round polynomial. - * @param config Configuration for the Sumcheck operation. + * @param transcript_config Configuration for encoding and hashing prover messages. + * @param sumcheck_config Configuration for the Sumcheck operation. * @param sumcheck_proof Reference to the SumcheckProof object where all round polynomials will be stored. * @return Error code of type eIcicleError. */ eIcicleError get_proof( const std::vector& mle_polynomials, const uint64_t mle_polynomial_size, + const F& claimed_sum, const CombineFunction& combine_function, - const SumcheckConfig& config, + const SumcheckTranscriptConfig&& transcript_config, + const SumcheckConfig& sumcheck_config, SumcheckProof& sumcheck_proof /*out*/) const { - return m_backend->get_proof(mle_polynomials, mle_polynomial_size, combine_function, config, sumcheck_proof); + return m_backend->get_proof(mle_polynomials, mle_polynomial_size, claimed_sum, combine_function, std::move(transcript_config), sumcheck_config, sumcheck_proof); } /** @@ -69,18 +72,20 @@ namespace icicle { * @param valid output valid bit. True if the Proof is valid, false otherwise. * @return Error code of type eIcicleError indicating success or failure. */ - eIcicleError verify(SumcheckProof& sumcheck_proof, bool& valid /*out*/) + eIcicleError verify( + SumcheckProof& sumcheck_proof, + const F& claimed_sum, + const SumcheckTranscriptConfig&& transcript_config, + bool& valid /*out*/) { valid = false; const int nof_rounds = sumcheck_proof.get_nof_round_polynomials(); const std::vector& round_poly_0 = sumcheck_proof.get_round_polynomial(0); const uint32_t combine_function_poly_degree = round_poly_0.size() - 1; - m_backend->reset_transcript(nof_rounds, combine_function_poly_degree); // verify that the sum of round_polynomial-0 is the clamed_sum F round_poly_0_sum = round_poly_0[0] + round_poly_0[1]; - const F& claimed_sum = m_backend->get_claimed_sum(); if (round_poly_0_sum != claimed_sum) { valid = false; ICICLE_LOG_ERROR << "verification failed: sum of round polynomial 0 (" << round_poly_0_sum @@ -88,9 +93,12 @@ namespace icicle { return eIcicleError::SUCCESS; } + // create sumcheck_transcript for the Fiat-Shamir + CpuSumcheckTranscript sumcheck_transcript(claimed_sum, nof_rounds, combine_function_poly_degree, std::move(transcript_config)); + for (int round_idx = 0; round_idx < nof_rounds - 1; round_idx++) { std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); - const F alpha = m_backend->get_alpha(round_poly); + const F alpha = sumcheck_transcript.get_alpha(round_poly); const F alpha_value = lagrange_interpolation(round_poly, alpha); const std::vector& next_round_poly = sumcheck_proof.get_round_polynomial(round_idx + 1); F expected_alpha_value = next_round_poly[0] + next_round_poly[1]; diff --git a/icicle/src/sumcheck/sumcheck.cpp b/icicle/src/sumcheck/sumcheck.cpp index d2b3121c4c..c94da370f0 100644 --- a/icicle/src/sumcheck/sumcheck.cpp +++ b/icicle/src/sumcheck/sumcheck.cpp @@ -9,10 +9,10 @@ namespace icicle { template <> Sumcheck - create_sumcheck(const scalar_t& claimed_sum, SumcheckTranscriptConfig&& transcript_config) + create_sumcheck() { std::shared_ptr> backend; - ICICLE_CHECK(SumcheckDispatcher::execute(claimed_sum, std::move(transcript_config), backend)); + ICICLE_CHECK(SumcheckDispatcher::execute(backend)); Sumcheck sumcheck{backend}; return sumcheck; } diff --git a/icicle/src/sumcheck/sumcheck_c_api.cpp b/icicle/src/sumcheck/sumcheck_c_api.cpp index 1d554f86b7..4ca2cc2f7d 100644 --- a/icicle/src/sumcheck/sumcheck_c_api.cpp +++ b/icicle/src/sumcheck/sumcheck_c_api.cpp @@ -56,7 +56,7 @@ CONCAT_EXPAND(FIELD, sumcheck_create)(const scalar_t* claimed_sum, const Transcr *ffi_transcript_config->seed_rng, ffi_transcript_config->little_endian}; // Create and return the Sumcheck instance - return new icicle::Sumcheck(icicle::create_sumcheck(*claimed_sum, std::move(config))); + return new icicle::Sumcheck(icicle::create_sumcheck()); } /** diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 8da3152140..3cc7cc7994 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1218,22 +1218,21 @@ TEST_F(FieldApiTestBase, Sumcheck) // ===== Prover side ====== // create sumcheck - auto prover_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + auto prover_sumcheck = create_sumcheck(); CombineFunction combine_func(EQ_X_AB_MINUS_C); - SumcheckConfig config; + SumcheckConfig sumcheck_config; SumcheckProof sumcheck_proof; - - ICICLE_CHECK(prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, combine_func, config, sumcheck_proof)); + ICICLE_CHECK(prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, claimed_sum, combine_func, std::move(transcript_config), sumcheck_config, sumcheck_proof)); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; } // ===== Verifier side ====== // create sumcheck - auto verifier_sumcheck = create_sumcheck(claimed_sum, std::move(transcript_config)); + auto verifier_sumcheck = create_sumcheck(); bool verification_pass = false; - ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, verification_pass)); + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); ASSERT_EQ(true, verification_pass); } From 17a659e970cb655c643084796c734d4ac5ed925e Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Sun, 19 Jan 2025 19:37:41 +0200 Subject: [PATCH 057/127] format --- icicle/backend/cpu/include/cpu_sumcheck.h | 4 ++-- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 11 ++++++++--- icicle/backend/cpu/src/field/cpu_sumcheck.cpp | 4 +--- icicle/include/icicle/backend/sumcheck_backend.h | 5 ++--- icicle/src/sumcheck/sumcheck.cpp | 3 +-- icicle/tests/test_field_api.cpp | 4 +++- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck.h b/icicle/backend/cpu/include/cpu_sumcheck.h index 77bc8e7776..0c9d6d6cb6 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck.h +++ b/icicle/backend/cpu/include/cpu_sumcheck.h @@ -53,7 +53,8 @@ namespace icicle { // create sumcheck_transcript for the Fiat-Shamir const uint32_t combine_function_poly_degree_u = combine_function_poly_degree; - CpuSumcheckTranscript sumcheck_transcript(claimed_sum, nof_rounds, combine_function_poly_degree_u, std::move(transcript_config)); + CpuSumcheckTranscript sumcheck_transcript( + claimed_sum, nof_rounds, combine_function_poly_degree_u, std::move(transcript_config)); sumcheck_proof.init( nof_rounds, combine_function_poly_degree_u); // reset the sumcheck proof to accumulate the round polynomials @@ -80,7 +81,6 @@ namespace icicle { return eIcicleError::SUCCESS; } - private: void build_round_polynomial( const std::vector& in_mle_polynomials, diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index b3e866a1df..e56e544ceb 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -5,8 +5,13 @@ template class CpuSumcheckTranscript { public: - CpuSumcheckTranscript(const S& claimed_sum, const uint32_t mle_polynomial_size, const uint32_t combine_function_poly_degree, const SumcheckTranscriptConfig&& transcript_config) - : m_claimed_sum(claimed_sum), m_mle_polynomial_size(mle_polynomial_size), m_combine_function_poly_degree(combine_function_poly_degree), m_transcript_config(std::move(transcript_config)) + CpuSumcheckTranscript( + const S& claimed_sum, + const uint32_t mle_polynomial_size, + const uint32_t combine_function_poly_degree, + const SumcheckTranscriptConfig&& transcript_config) + : m_claimed_sum(claimed_sum), m_mle_polynomial_size(mle_polynomial_size), + m_combine_function_poly_degree(combine_function_poly_degree), m_transcript_config(std::move(transcript_config)) { m_entry_0.clear(); m_round_idx = 0; @@ -37,7 +42,7 @@ class CpuSumcheckTranscript private: const SumcheckTranscriptConfig&& m_transcript_config; // configuration how to build the transcript HashConfig m_config; // hash config - default - uint32_t m_round_idx; // + uint32_t m_round_idx; // std::vector m_entry_0; // uint32_t m_mle_polynomial_size = 0; uint32_t m_combine_function_poly_degree = 0; diff --git a/icicle/backend/cpu/src/field/cpu_sumcheck.cpp b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp index e13630a4ef..d1019688b8 100644 --- a/icicle/backend/cpu/src/field/cpu_sumcheck.cpp +++ b/icicle/backend/cpu/src/field/cpu_sumcheck.cpp @@ -6,9 +6,7 @@ using namespace field_config; namespace icicle { template - eIcicleError cpu_create_sumcheck_backend( - const Device& device, - std::shared_ptr>& backend /*OUT*/) + eIcicleError cpu_create_sumcheck_backend(const Device& device, std::shared_ptr>& backend /*OUT*/) { backend = std::make_shared>(); return eIcicleError::SUCCESS; diff --git a/icicle/include/icicle/backend/sumcheck_backend.h b/icicle/include/icicle/backend/sumcheck_backend.h index 630a8d4c1c..3cd57e8823 100644 --- a/icicle/include/icicle/backend/sumcheck_backend.h +++ b/icicle/include/icicle/backend/sumcheck_backend.h @@ -56,9 +56,8 @@ namespace icicle { /*************************** Backend Factory Registration ***************************/ template - using SumcheckFactoryImpl = std::function>& backend /*OUT*/)>; + using SumcheckFactoryImpl = + std::function>& backend /*OUT*/)>; /** * @brief Register a Sumcheck backend factory for a specific device type. diff --git a/icicle/src/sumcheck/sumcheck.cpp b/icicle/src/sumcheck/sumcheck.cpp index c94da370f0..4d0edfdedf 100644 --- a/icicle/src/sumcheck/sumcheck.cpp +++ b/icicle/src/sumcheck/sumcheck.cpp @@ -8,8 +8,7 @@ namespace icicle { ICICLE_DISPATCHER_INST(SumcheckDispatcher, sumcheck_factory, SumcheckFactoryImpl); template <> - Sumcheck - create_sumcheck() + Sumcheck create_sumcheck() { std::shared_ptr> backend; ICICLE_CHECK(SumcheckDispatcher::execute(backend)); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 3cc7cc7994..35d6e8528a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1223,7 +1223,9 @@ TEST_F(FieldApiTestBase, Sumcheck) CombineFunction combine_func(EQ_X_AB_MINUS_C); SumcheckConfig sumcheck_config; SumcheckProof sumcheck_proof; - ICICLE_CHECK(prover_sumcheck.get_proof(mle_polynomials, mle_poly_size, claimed_sum, combine_func, std::move(transcript_config), sumcheck_config, sumcheck_proof)); + ICICLE_CHECK(prover_sumcheck.get_proof( + mle_polynomials, mle_poly_size, claimed_sum, combine_func, std::move(transcript_config), sumcheck_config, + sumcheck_proof)); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; } From 12469fb5e0d686f0881a42fcec0b80b51dae16db Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Sun, 19 Jan 2025 19:39:06 +0200 Subject: [PATCH 058/127] another format --- icicle/include/icicle/sumcheck/sumcheck.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/icicle/include/icicle/sumcheck/sumcheck.h b/icicle/include/icicle/sumcheck/sumcheck.h index 6846737f87..84d569e9ba 100644 --- a/icicle/include/icicle/sumcheck/sumcheck.h +++ b/icicle/include/icicle/sumcheck/sumcheck.h @@ -61,7 +61,9 @@ namespace icicle { const SumcheckConfig& sumcheck_config, SumcheckProof& sumcheck_proof /*out*/) const { - return m_backend->get_proof(mle_polynomials, mle_polynomial_size, claimed_sum, combine_function, std::move(transcript_config), sumcheck_config, sumcheck_proof); + return m_backend->get_proof( + mle_polynomials, mle_polynomial_size, claimed_sum, combine_function, std::move(transcript_config), + sumcheck_config, sumcheck_proof); } /** @@ -73,9 +75,9 @@ namespace icicle { * @return Error code of type eIcicleError indicating success or failure. */ eIcicleError verify( - SumcheckProof& sumcheck_proof, + SumcheckProof& sumcheck_proof, const F& claimed_sum, - const SumcheckTranscriptConfig&& transcript_config, + const SumcheckTranscriptConfig&& transcript_config, bool& valid /*out*/) { valid = false; @@ -94,7 +96,8 @@ namespace icicle { } // create sumcheck_transcript for the Fiat-Shamir - CpuSumcheckTranscript sumcheck_transcript(claimed_sum, nof_rounds, combine_function_poly_degree, std::move(transcript_config)); + CpuSumcheckTranscript sumcheck_transcript( + claimed_sum, nof_rounds, combine_function_poly_degree, std::move(transcript_config)); for (int round_idx = 0; round_idx < nof_rounds - 1; round_idx++) { std::vector& round_poly = sumcheck_proof.get_round_polynomial(round_idx); From bc5e63976f8d6a17b1929fad11c81eb04daa58d6 Mon Sep 17 00:00:00 2001 From: Leon Hibnik <107353745+LeonHibnik@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:56:16 +0200 Subject: [PATCH 059/127] Fix/release script (#721) --- .github/workflows/release.yml | 4 +++- scripts/release/Dockerfile.ubi8 | 1 + scripts/release/Dockerfile.ubi9 | 1 + scripts/release/Dockerfile.ubuntu20 | 1 + scripts/release/Dockerfile.ubuntu22 | 1 + scripts/release/build_release_and_tar.sh | 2 +- 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9f58318f3..9886b5635f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,9 @@ jobs: npm run docusaurus docs:version $LATEST_VERSION git add --all git commit -m "Bump docs version" - git push + - name: Push to github branch main + run: | + git push origin main --tags - name: Create draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/release/Dockerfile.ubi8 b/scripts/release/Dockerfile.ubi8 index b12519fa57..3026099939 100644 --- a/scripts/release/Dockerfile.ubi8 +++ b/scripts/release/Dockerfile.ubi8 @@ -10,6 +10,7 @@ RUN dnf update -y && dnf install -y \ ninja-build \ wget \ gnupg2 \ + git \ && dnf clean all # Add the RPM-based LLVM repository for Clang diff --git a/scripts/release/Dockerfile.ubi9 b/scripts/release/Dockerfile.ubi9 index ad8528b9b4..605df5d1b7 100644 --- a/scripts/release/Dockerfile.ubi9 +++ b/scripts/release/Dockerfile.ubi9 @@ -14,6 +14,7 @@ RUN dnf update -y && dnf install -y \ gcc-c++ \ make \ gnupg \ + git \ && dnf clean all # Add LLVM repository for Clang installation diff --git a/scripts/release/Dockerfile.ubuntu20 b/scripts/release/Dockerfile.ubuntu20 index 20d8024536..2f307275ca 100644 --- a/scripts/release/Dockerfile.ubuntu20 +++ b/scripts/release/Dockerfile.ubuntu20 @@ -22,6 +22,7 @@ RUN apt-get update && apt-get install -y \ clang \ lldb \ lld \ + git \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Verify installations diff --git a/scripts/release/Dockerfile.ubuntu22 b/scripts/release/Dockerfile.ubuntu22 index 0613edc0ad..5bdc103808 100644 --- a/scripts/release/Dockerfile.ubuntu22 +++ b/scripts/release/Dockerfile.ubuntu22 @@ -13,6 +13,7 @@ RUN apt-get update && apt-get install -y \ software-properties-common \ wget \ gnupg \ + git \ && wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - \ && add-apt-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy main" diff --git a/scripts/release/build_release_and_tar.sh b/scripts/release/build_release_and_tar.sh index 2ea4648744..ca058b2a9c 100755 --- a/scripts/release/build_release_and_tar.sh +++ b/scripts/release/build_release_and_tar.sh @@ -8,7 +8,7 @@ ICICLE_OS=${2:-unknown_os} # Default to "unknown_os" if not set ICICLE_CUDA_VERSION=${3:-cuda_unknown} # Default to "cuda_unknown" if not set # List of fields and curves -fields=("babybear" "stark252" "m31") +fields=("babybear" "stark252" "m31" "koalabear") curves=("bn254" "bls12_381" "bls12_377" "bw6_761" "grumpkin") cd / From 8447e1a7bfed03ff445f79d39583740a7190da20 Mon Sep 17 00:00:00 2001 From: yshekel Date: Mon, 13 Jan 2025 09:59:52 +0200 Subject: [PATCH 060/127] Fix bug in CPU vec ops regarding nof workers (#731) --- icicle/backend/cpu/src/field/cpu_vec_ops.cpp | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp index f279c7b034..496621c312 100644 --- a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp +++ b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp @@ -371,7 +371,7 @@ class VectorOpTask : public TaskBase public: T m_intermidiate_res; // pointer to the output. Can be a vector or scalar pointer uint64_t m_idx_in_batch; // index in the batch. Used in intermediate res tasks -}; // class VectorOpTask +}; #define NOF_OPERATIONS_PER_TASK 512 #define CONFIG_NOF_THREADS_KEY "n_threads" @@ -381,8 +381,9 @@ int get_nof_workers(const VecOpsConfig& config) { if (config.ext && config.ext->has(CONFIG_NOF_THREADS_KEY)) { return config.ext->get(CONFIG_NOF_THREADS_KEY); } - int hw_threads = std::thread::hardware_concurrency(); - return ((hw_threads > 1) ? hw_threads - 1 : 1); // reduce 1 for the main + const int hw_threads = std::thread::hardware_concurrency(); + // Note: no need to account for the main thread in vec-ops since it's doing little work + return std::max(1, hw_threads); } // Execute a full task from the type vector = vector (op) vector @@ -390,7 +391,7 @@ template eIcicleError cpu_2vectors_op(VecOperation op, const T* vec_a, const U* vec_b, uint64_t size, const VecOpsConfig& config, T* output) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); const uint64_t total_nof_operations = size * config.batch_size; for (uint64_t i = 0; i < total_nof_operations; i += NOF_OPERATIONS_PER_TASK) { VectorOpTask* task_p = task_manager.get_idle_or_completed_task(); @@ -406,7 +407,7 @@ template eIcicleError cpu_scalar_vector_op( VecOperation op, const T* scalar_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); const uint64_t total_nof_operations = size; const uint32_t stride = config.columns_batch ? config.batch_size : 1; for (uint32_t idx_in_batch = 0; idx_in_batch < config.batch_size; idx_in_batch++) { @@ -479,7 +480,7 @@ template eIcicleError cpu_convert_montgomery( const Device& device, const T* input, uint64_t size, bool is_to_montgomery, const VecOpsConfig& config, T* output) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); const uint64_t total_nof_operations = size * config.batch_size; for (uint64_t i = 0; i < total_nof_operations; i += NOF_OPERATIONS_PER_TASK) { VectorOpTask* task_p = task_manager.get_idle_or_completed_task(); @@ -499,7 +500,7 @@ REGISTER_CONVERT_MONTGOMERY_BACKEND("CPU", cpu_convert_montgomery); template eIcicleError cpu_vector_sum(const Device& device, const T* vec_a, uint64_t size, const VecOpsConfig& config, T* output) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); std::vector output_initialized = std::vector(config.batch_size, false); uint64_t vec_a_offset = 0; uint64_t idx_in_batch = 0; @@ -539,7 +540,7 @@ template eIcicleError cpu_vector_product(const Device& device, const T* vec_a, uint64_t size, const VecOpsConfig& config, T* output) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); std::vector output_initialized = std::vector(config.batch_size, false); uint64_t vec_a_offset = 0; uint64_t idx_in_batch = 0; @@ -610,7 +611,7 @@ template eIcicleError out_of_place_matrix_transpose( const Device& device, const T* mat_in, uint32_t nof_rows, uint32_t nof_cols, const VecOpsConfig& config, T* mat_out) { - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); uint32_t stride = config.columns_batch ? config.batch_size : 1; const uint64_t total_elements_one_mat = static_cast(nof_rows) * nof_cols; const uint32_t NOF_ROWS_PER_TASK = @@ -695,7 +696,7 @@ eIcicleError matrix_transpose_necklaces( std::vector start_indices_in_mat; // Collect start indices gen_necklace(1, 1, k, length, necklace, start_indices_in_mat); - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); for (uint64_t i = 0; i < start_indices_in_mat.size(); i += max_nof_operations) { uint64_t nof_operations = std::min((uint64_t)max_nof_operations, start_indices_in_mat.size() - i); for (uint64_t idx_in_batch = 0; idx_in_batch < config.batch_size; idx_in_batch++) { @@ -746,7 +747,7 @@ cpu_bit_reverse(const Device& device, const T* vec_in, uint64_t size, const VecO ICICLE_ASSERT((1ULL << logn) == size) << "Invalid argument - size is not a power of 2"; // Perform the bit reverse - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); for (uint64_t idx_in_batch = 0; idx_in_batch < config.batch_size; idx_in_batch++) { for (uint64_t i = 0; i < size; i += NOF_OPERATIONS_PER_TASK) { VectorOpTask* task_p = task_manager.get_idle_or_completed_task(); @@ -783,7 +784,7 @@ eIcicleError cpu_slice( ICICLE_ASSERT(vec_in != nullptr && vec_out != nullptr) << "Error: Invalid argument - input or output vector is null"; ICICLE_ASSERT(offset + (size_out - 1) * stride < size_in) << "Error: Invalid argument - slice out of bound"; - TasksManager> task_manager(get_nof_workers(config) - 1); + TasksManager> task_manager(get_nof_workers(config)); for (uint64_t idx_in_batch = 0; idx_in_batch < config.batch_size; idx_in_batch++) { for (uint64_t i = 0; i < size_out; i += NOF_OPERATIONS_PER_TASK) { VectorOpTask* task_p = task_manager.get_idle_or_completed_task(); From db785555da51e80c3d748befbfd1a9bc4e38e888 Mon Sep 17 00:00:00 2001 From: yshekel Date: Tue, 14 Jan 2025 13:10:44 +0200 Subject: [PATCH 061/127] Support android and vulkan (#735) - Update Cmake for android cross compilation - Update log.h for android logcat --- icicle/CMakeLists.txt | 42 +--------------- icicle/cmake/curve.cmake | 2 +- icicle/cmake/field.cmake | 2 +- icicle/cmake/setup.cmake | 82 +++++++++++++++++++++++++++++++ icicle/include/icicle/utils/log.h | 44 +++++++++++++++-- 5 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 icicle/cmake/setup.cmake diff --git a/icicle/CMakeLists.txt b/icicle/CMakeLists.txt index 246215676f..45d0e3db71 100644 --- a/icicle/CMakeLists.txt +++ b/icicle/CMakeLists.txt @@ -1,59 +1,23 @@ cmake_minimum_required(VERSION 3.18) +include(cmake/setup.cmake) + project(icicle) # Specify the C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) -# Select the C++ compiler -find_program(CLANG_COMPILER clang++) -find_program(CLANG_C_COMPILER clang) - -if(CLANG_COMPILER AND CLANG_C_COMPILER) - set(CMAKE_CXX_COMPILER ${CLANG_COMPILER} CACHE STRING "Clang++ compiler" FORCE) - set(CMAKE_C_COMPILER ${CLANG_C_COMPILER} CACHE STRING "Clang compiler" FORCE) - message(STATUS "Using Clang++ as the C++ compiler: ${CLANG_COMPILER}") - message(STATUS "Using Clang as the C compiler: ${CLANG_C_COMPILER}") -else() - message(WARNING "ICICLE CPU works best with clang++ and clang. Defaulting to ${CLANG_COMPILER}") -endif() - - include(cmake/field.cmake) include(cmake/curve.cmake) include(cmake/hash.cmake) -# Set the default build type to Release if not specified -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build: Debug, Release, RelWithDebInfo, MinSizeRel." FORCE) -endif() - -# Print the selected build type -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") - # Prevent build if both SANITIZE and CUDA_BACKEND are enabled if(SANITIZE AND CUDA_BACKEND) message(FATAL_ERROR "Address sanitizer and Cuda cannot be enabled at the same time.") endif() -# Find the ccache program -find_program(CCACHE_PROGRAM ccache) -# If ccache is found, use it as the compiler launcher -if(CCACHE_PROGRAM) - message(STATUS "ccache found: ${CCACHE_PROGRAM}") - - # Use ccache for C and C++ compilers - set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) - set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) - set(CMAKE_CUDA_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) -else() - message(STATUS "ccache not found") -endif() - -set(CMAKE_POSITION_INDEPENDENT_CODE ON) - # Build options option(BUILD_TESTS "Build unit test2s. Default=OFF" OFF) # Backends: typically CPU is built into the frontend, the rest are DSOs loaded at runtime from installation @@ -87,8 +51,6 @@ add_library(icicle_device SHARED src/runtime.cpp src/config_extension.cpp ) -target_link_libraries(icicle_device PUBLIC dl) - include_directories(include) # Define the install directory (default is /usr/local) diff --git a/icicle/cmake/curve.cmake b/icicle/cmake/curve.cmake index c82d1b90b0..53148314ce 100644 --- a/icicle/cmake/curve.cmake +++ b/icicle/cmake/curve.cmake @@ -58,7 +58,7 @@ function(setup_curve_target CURVE CURVE_INDEX FEATURES_STRING) # Add additional feature handling calls here set_target_properties(icicle_curve PROPERTIES OUTPUT_NAME "icicle_curve_${CURVE}") - target_link_libraries(icicle_curve PUBLIC icicle_device icicle_field pthread) + target_link_libraries(icicle_curve PUBLIC icicle_device icicle_field) # Ensure CURVE is defined in the cache for backends to see set(CURVE "${CURVE}" CACHE STRING "") diff --git a/icicle/cmake/field.cmake b/icicle/cmake/field.cmake index 953c0d6fca..b7864c869a 100644 --- a/icicle/cmake/field.cmake +++ b/icicle/cmake/field.cmake @@ -56,7 +56,7 @@ function(setup_field_target FIELD FIELD_INDEX FEATURES_STRING) # Add additional feature handling calls here set_target_properties(icicle_field PROPERTIES OUTPUT_NAME "icicle_field_${FIELD}") - target_link_libraries(icicle_field PUBLIC icicle_device pthread) + target_link_libraries(icicle_field PUBLIC icicle_device) # Ensure FIELD is defined in the cache for backends to see set(FIELD "${FIELD}" CACHE STRING "") diff --git a/icicle/cmake/setup.cmake b/icicle/cmake/setup.cmake new file mode 100644 index 0000000000..c60d5330e6 --- /dev/null +++ b/icicle/cmake/setup.cmake @@ -0,0 +1,82 @@ + +# Option to cross-compile for Android +option(BUILD_FOR_ANDROID "Cross-compile for Android" OFF) + +if (BUILD_FOR_ANDROID) + message(STATUS "Configuring for Android...") + + # Check for NDK in the environment variable + if (NOT DEFINED ENV{ANDROID_NDK} AND NOT DEFINED ANDROID_NDK) + message(FATAL_ERROR "ANDROID_NDK is not defined. Please set the environment variable or pass -DANDROID_NDK=") + endif() + + # Use the CMake option if specified; otherwise, use the environment variable + if (DEFINED ANDROID_NDK) + set(CMAKE_ANDROID_NDK ${ANDROID_NDK}) + elseif (DEFINED ENV{ANDROID_NDK}) + set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK}) + endif() + + # Debugging message for NDK path + message(STATUS "Using Android NDK: ${CMAKE_ANDROID_NDK}") + + # Set toolchain and other options + set(ANDROID_MIN_API 24) # Minimum API (24 is for android 7.0 and later) + set(CMAKE_SYSTEM_NAME Android CACHE STRING "Target system name for cross-compilation") + set(ANDROID_ABI arm64-v8a CACHE STRING "Default Android ABI") + set(ANDROID_PLATFORM "android-${ANDROID_MIN_API}" CACHE STRING "Android API level") + set(CMAKE_ANDROID_ARCH_ABI "${ANDROID_ABI}" CACHE STRING "Target ABI for Android") + set(CMAKE_ANDROID_STL_TYPE c++_shared CACHE STRING "Android STL type") + set(CMAKE_TOOLCHAIN_FILE "${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake" CACHE FILEPATH "Path to the Android toolchain file") + list(APPEND CMAKE_SYSTEM_LIBRARY_PATH "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/${ANDROID_MIN_API}") + + message(STATUS "Using ANDROID_MIN_API: ${ANDROID_MIN_API}") + message(STATUS "Using ANDROID_ABI: ${ANDROID_ABI}") + +endif() + +# Platform specific libraries and compiler +if (CMAKE_SYSTEM_NAME STREQUAL "Android") + find_library(LOG_LIB log REQUIRED) # Android log library + set(PLATFORM_LIBS ${LOG_LIB}) +else() + message(STATUS "Configuring for native platform...") + # Select the C++ compiler + find_program(CLANG_COMPILER clang++) + find_program(CLANG_C_COMPILER clang) + + if(CLANG_COMPILER AND CLANG_C_COMPILER) + set(CMAKE_CXX_COMPILER ${CLANG_COMPILER} CACHE STRING "Clang++ compiler" FORCE) + set(CMAKE_C_COMPILER ${CLANG_C_COMPILER} CACHE STRING "Clang compiler" FORCE) + else() + message(WARNING "ICICLE CPU works best with clang++ and clang. Defaulting to ${CLANG_COMPILER}") + endif() + + set(PLATFORM_LIBS pthread dl) +endif() + +link_libraries(${PLATFORM_LIBS}) + +# Find the ccache program +find_program(CCACHE_PROGRAM ccache) +# If ccache is found, use it as the compiler launcher +if(CCACHE_PROGRAM) + message(STATUS "ccache found: ${CCACHE_PROGRAM}") + + # Use ccache for C and C++ compilers + set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) + set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) + set(CMAKE_CUDA_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) +else() + message(STATUS "ccache not found") +endif() + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Set the default build type to Release if not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build: Debug, Release, RelWithDebInfo, MinSizeRel." FORCE) +endif() + +# Print the selected build type +message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") \ No newline at end of file diff --git a/icicle/include/icicle/utils/log.h b/icicle/include/icicle/utils/log.h index f8e0af490b..62ae869f66 100644 --- a/icicle/include/icicle/utils/log.h +++ b/icicle/include/icicle/utils/log.h @@ -3,6 +3,10 @@ #include #include +#ifdef __ANDROID__ + #include +#endif + #define ICICLE_LOG_VERBOSE Log(Log::Verbose) #define ICICLE_LOG_DEBUG Log(Log::Debug) #define ICICLE_LOG_INFO Log(Log::Info) @@ -21,7 +25,16 @@ class Log ~Log() { - if (level >= s_min_log_level) { std::cerr << oss.str() << std::endl; } + if (level >= s_min_log_level) { +#ifdef __ANDROID__ + // Use Android logcat + android_LogPriority androidPriority = logLevelToAndroidPriority(level); + __android_log_print(androidPriority, "ICICLE", "%s", oss.str().c_str()); +#else + // Use standard error stream for other platforms + std::cerr << oss.str() << std::endl; +#endif + } } template @@ -31,7 +44,7 @@ class Log return *this; } - // Static method to set the log level + // Static method to set the minimum log level static void set_min_log_level(eLogLevel level) { s_min_log_level = level; } private: @@ -43,7 +56,7 @@ class Log { switch (level) { case Verbose: - return "DEBUG"; + return "VERBOSE"; case Debug: return "DEBUG"; case Info: @@ -57,7 +70,28 @@ class Log } } - // logging message with level>=s_min_log_level +#ifdef __ANDROID__ + // Map custom log level to Android log priority + android_LogPriority logLevelToAndroidPriority(eLogLevel level) const + { + switch (level) { + case Verbose: + return ANDROID_LOG_VERBOSE; + case Debug: + return ANDROID_LOG_DEBUG; + case Info: + return ANDROID_LOG_INFO; + case Warning: + return ANDROID_LOG_WARN; + case Error: + return ANDROID_LOG_ERROR; + default: + return ANDROID_LOG_UNKNOWN; + } + } +#endif + + // Static member to hold the minimum log level #if defined(NDEBUG) static inline eLogLevel s_min_log_level = eLogLevel::Info; #else @@ -65,4 +99,4 @@ class Log #endif // Note: for verbose, need to explicitly call `set_min_log_level(eLogLevel::Verbose)` -}; +}; \ No newline at end of file From 28cab475872a423f9df57adaa6d41661cda03845 Mon Sep 17 00:00:00 2001 From: Miki <100796045+mickeyasa@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:19:50 +0200 Subject: [PATCH 062/127] Parallelize-vecop-program-execution (#736) --- icicle/backend/cpu/src/field/cpu_vec_ops.cpp | 39 ++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp index 496621c312..38d859e022 100644 --- a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp +++ b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp @@ -10,6 +10,7 @@ #include #include +#include "taskflow/taskflow.hpp" #include "icicle/program/program.h" #include "cpu_program_executor.h" @@ -852,20 +853,36 @@ eIcicleError cpu_execute_program( << " parameters"; return eIcicleError::INVALID_ARGUMENT; } + tf::Taskflow taskflow; // Accumulate tasks + tf::Executor executor; // execute all tasks accumulated on multiple threads const uint64_t total_nof_operations = size * config.batch_size; - CpuProgramExecutor prog_executor(program); - // init prog_executor to point to data vectors - for (int param_idx = 0; param_idx < program.m_nof_parameters; ++param_idx) { - prog_executor.m_variable_ptrs[param_idx] = data[param_idx]; - } - // run over all elements in the arrays and execute the program - for (uint64_t i = 0; i < total_nof_operations; i++) { - prog_executor.execute(); - for (int param_idx = 0; param_idx < program.m_nof_parameters; ++param_idx) { - (prog_executor.m_variable_ptrs[param_idx])++; - } + // Divide the problem to workers + const int nof_workers = get_nof_workers(config); + const uint64_t worker_task_size = (total_nof_operations + nof_workers - 1) / nof_workers; // round up + + for (uint64_t start_idx = 0; start_idx < total_nof_operations; start_idx += worker_task_size) { + taskflow.emplace([=]() { + CpuProgramExecutor prog_executor(program); + // init prog_executor to point to data vectors + for (int param_idx = 0; param_idx < program.m_nof_parameters; ++param_idx) { + prog_executor.m_variable_ptrs[param_idx] = &(data[param_idx][start_idx]); + } + + const uint64_t task_size = std::min(worker_task_size, total_nof_operations - start_idx); + // run over all task elements in the arrays and execute the program + for (uint64_t i = 0; i < task_size; i++) { + prog_executor.execute(); + // update the program pointers + for (int param_idx = 0; param_idx < program.m_nof_parameters; ++param_idx) { + (prog_executor.m_variable_ptrs[param_idx])++; + } + } + }); } + + executor.run(taskflow).wait(); + taskflow.clear(); return eIcicleError::SUCCESS; } From 31dbb47dc0d048a7900c62bbe040db133797f18f Mon Sep 17 00:00:00 2001 From: idanfr-ingo Date: Tue, 14 Jan 2025 13:55:38 +0200 Subject: [PATCH 063/127] Create docs for program & program execution (#722) --- docs/docs/icicle/primitives/program.md | 88 ++++++++++++++++++++++++++ docs/docs/icicle/primitives/vec_ops.md | 15 +++++ docs/sidebars.ts | 5 ++ 3 files changed, 108 insertions(+) create mode 100644 docs/docs/icicle/primitives/program.md diff --git a/docs/docs/icicle/primitives/program.md b/docs/docs/icicle/primitives/program.md new file mode 100644 index 0000000000..85e5441883 --- /dev/null +++ b/docs/docs/icicle/primitives/program.md @@ -0,0 +1,88 @@ +# Programs + +## Overview + +Program is a class that let users define expressions on vector elements, and have ICICLE compile it for the backends for a fused implementation. This solves memory bottlenecks and also let users customize algorithms such as sumcheck. Program can create only element-wise lambda functions. + + +## C++ API + +### Symbol + +Symbol is the basic (template) class that allow users to define their own program. The lambda function the user define will operate on symbols. + +### Defining lambda function + +To define a custom lambda function the user will use Symbol: +```cpp +void lambda_multi_result(std::vector>& vars) +{ + const Symbol& A = vars[0]; + const Symbol& B = vars[1]; + const Symbol& C = vars[2]; + const Symbol& EQ = vars[3]; + vars[4] = EQ * (A * B - C) + scalar_t::from(9); + vars[5] = A * B - C.inverse(); + vars[6] = vars[5]; + vars[3] = 2 * (var[0] + var[1]) // all variables can be both inputs and outputs +} +``` + +Each symbol element at the vector argument `var` represent an input or an output. The type of the symbol (`scalar_t` in this example) will be the type of the inputs and outputs. In this example we created a lambda function with four inputs and three outputs. + +In this example there are four input variables and three three outputs. Inside the function the user can define custom expressions on them. + +Program support few pre-defined programs. The user can use those pre-defined programs without creating a lambda function, as will be explained in the next section. + +### Creating program + +To execute the lambda function we just created we need to create a program from it. +To create program from lambda function we can use the following constructor: + +```cpp +Program(std::function>&)> program_func, const int nof_parameters) +``` + +`program_func` is the lambda function (in the example above `lambda_multi_result`) and `nof_parameters` is the total number of parameter (inputs + outputs) for the lambda (seven in the above example). + +#### Pre-defined programs + +As mentioned before, there are few pre-defined programs the user can use without the need to create a lambda function first. The enum `PreDefinedPrograms` contains the pre-defined program. Using pre-defined function will lead to better performance compared to creating the equivalent lambda function. +To create a pre-defined program a different constructor is bing used: + +```cpp +Program(PreDefinedPrograms pre_def) +``` + +`pre_def` is the pre-defined program (from `PreDefinedPrograms`). + +##### PreDefinedPrograms + +```cpp +enum PreDefinedPrograms { + AB_MINUS_C = 0, + EQ_X_AB_MINUS_C +}; +``` + +`AB_MINUS_C` - the pre-defined program `AB - C` for the input vectors `A`, `B` and `C` + +`EQ_X_AB_MINUS_C` - the pre-defined program `EQ(AB - C)` for the input vectors `A`, `B`, `C` and `EQ` + + +### Executing program + +To execute the program the `execute_program` function from the vector operation API should be used. This operation is supported by the CPU and CUDA backends. + + +```cpp +template +eIcicleError +execute_program(std::vector& data, const Program& program, uint64_t size, const VecOpsConfig& config); +``` + +The `data` vector is a vector of pointers to the inputs and output vectors, `program` is the program to execute, `size` is the length of the vectors and `config` is the configuration of the operation. + +For the configuration the field `is_a_on_device` determined whethere the data (*inputs and outputs*) is on device or not. After the execution `data` will reside in the same place as it did before (i.e. the field `is_result_on_device` is irrelevant.) + +> **_NOTE:_** Using program for executing lambdas is recommended only while using the CUDA backend. Program's primary use is to let users to customize algorithms (such as sumcheck). diff --git a/docs/docs/icicle/primitives/vec_ops.md b/docs/docs/icicle/primitives/vec_ops.md index 7f546dc16c..4725f7a490 100644 --- a/docs/docs/icicle/primitives/vec_ops.md +++ b/docs/docs/icicle/primitives/vec_ops.md @@ -78,6 +78,20 @@ template eIcicleError vector_div(const T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); ``` +#### `execute_program` + +Execute a user-defined lambda function with arbitrary number of input and output variables. + +```cpp +template +eIcicleError +execute_program(std::vector& data, const Program& program, uint64_t size, const VecOpsConfig& config); +``` + +`is_result_on_device` of VecOpsConfig is not used here. + +For more details see [program](./program.md). + #### `vector_accumulate` Adds vector b to a, inplace. @@ -208,6 +222,7 @@ template eIcicleError polynomial_division(const T* numerator, int64_t numerator_deg, const T* denumerator, int64_t denumerator_deg, const VecOpsConfig& config, T* q_out /*OUT*/, uint64_t q_size, T* r_out /*OUT*/, uint64_t r_size); ``` + ### Rust and Go bindings - [Golang](../golang-bindings/vec-ops.md) diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 32f264d709..dcc9b77c0b 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -66,6 +66,11 @@ const cppApi = [ label: "Vector operations", id: "icicle/primitives/vec_ops", }, + { + type: "doc", + label: "Program", + id: "icicle/primitives/program", + }, { type: "doc", label: "Polynomials", From 64767a6a9e383c2a08f80b77abede8a1a9cced4f Mon Sep 17 00:00:00 2001 From: Aviad Dahan <116077675+aviadingo@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:59:51 +0200 Subject: [PATCH 064/127] Feat: Blake3 (#733) --- .github/workflows/cpp-golang-rust.yml | 111 ++-- docs/docs/icicle/libraries.md | 1 + docs/docs/icicle/primitives/hash.md | 9 +- examples/c++/best-practice-ntt/example.cpp | 4 +- icicle/backend/cpu/CMakeLists.txt | 4 + icicle/backend/cpu/src/field/cpu_vec_ops.cpp | 2 +- icicle/backend/cpu/src/hash/blake3.c | 591 ++++++++++++++++++ icicle/backend/cpu/src/hash/blake3.h | 57 ++ icicle/backend/cpu/src/hash/blake3_dispatch.c | 62 ++ icicle/backend/cpu/src/hash/blake3_impl.h | 205 ++++++ icicle/backend/cpu/src/hash/blake3_portable.c | 176 ++++++ icicle/backend/cpu/src/hash/cpu_blake3.cpp | 53 ++ icicle/backend/cpu/src/hash/cpu_poseidon2.cpp | 2 +- icicle/cmake/hash.cmake | 1 + .../icicle/backend/hash/blake3_backend.h | 25 + .../include/icicle/fields/quartic_extension.h | 2 +- icicle/include/icicle/fields/storage.h | 12 +- icicle/include/icicle/hash/blake3.h | 20 + icicle/src/hash/blake3.cpp | 17 + icicle/src/hash/hash_c_api.cpp | 14 + icicle/tests/test_hash_api.cpp | 73 +++ wrappers/golang/hash/blake3.go | 19 + wrappers/golang/hash/include/blake3.h | 17 + wrappers/golang/hash/tests/hash_test.go | 73 +++ wrappers/rust/icicle-hash/src/blake3.rs | 18 + wrappers/rust/icicle-hash/src/lib.rs | 1 + wrappers/rust/icicle-hash/src/tests.rs | 67 ++ 27 files changed, 1569 insertions(+), 67 deletions(-) create mode 100644 icicle/backend/cpu/src/hash/blake3.c create mode 100644 icicle/backend/cpu/src/hash/blake3.h create mode 100644 icicle/backend/cpu/src/hash/blake3_dispatch.c create mode 100644 icicle/backend/cpu/src/hash/blake3_impl.h create mode 100644 icicle/backend/cpu/src/hash/blake3_portable.c create mode 100644 icicle/backend/cpu/src/hash/cpu_blake3.cpp create mode 100644 icicle/include/icicle/backend/hash/blake3_backend.h create mode 100644 icicle/include/icicle/hash/blake3.h create mode 100644 icicle/src/hash/blake3.cpp create mode 100644 wrappers/golang/hash/blake3.go create mode 100644 wrappers/golang/hash/include/blake3.h create mode 100644 wrappers/rust/icicle-hash/src/blake3.rs diff --git a/.github/workflows/cpp-golang-rust.yml b/.github/workflows/cpp-golang-rust.yml index 11c9242c7a..09eda977d8 100644 --- a/.github/workflows/cpp-golang-rust.yml +++ b/.github/workflows/cpp-golang-rust.yml @@ -1,4 +1,4 @@ -name: Spell Check, C++/CUDA/Go/RUST & Examples +name: C++/CUDA/Go/RUST on: pull_request: @@ -31,6 +31,8 @@ jobs: name: Check Code Format runs-on: ubuntu-22.04 needs: check-changed-files + container: + image: silkeh/clang:19-bookworm # Replace with a container having the desired clang-format version steps: - name: Checkout uses: actions/checkout@v4 @@ -41,7 +43,9 @@ jobs: go-version: '1.22.0' - name: Check clang-format if: needs.check-changed-files.outputs.cpp == 'true' - run: ./scripts/format_all.sh . --check --exclude "build|wrappers" + run: | + clang-format --version + ./scripts/format_all.sh . --check --exclude "build|wrappers" - name: Check gofmt if: needs.check-changed-files.outputs.golang == 'true' run: if [[ $(go list ./... | xargs go fmt) ]]; then echo "Please run go fmt"; exit 1; fi @@ -82,7 +86,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 - name: Checkout CUDA Backend - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' uses: actions/checkout@v4 with: repository: ingonyama-zk/icicle-cuda-backend @@ -90,7 +94,7 @@ jobs: ssh-key: ${{ secrets.CUDA_PULL_KEY }} ref: ${{ needs.extract-cuda-backend-branch.outputs.cuda-backend-branch }} - name: Get CUDA Backend Commit SHA - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' working-directory: ./icicle/backend/cuda id: extract-cuda-sha run: | @@ -98,7 +102,7 @@ jobs: echo "CUDA Backend Commit SHA: $CUDA_BACKEND_SHA" echo "cuda-backend-sha=$CUDA_BACKEND_SHA" >> $GITHUB_OUTPUT - name: Set CUDA backend flag - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' id: cuda-flag run: | CUDA_BACKEND_SHA=${{ steps.extract-cuda-sha.outputs.cuda-backend-sha }} @@ -136,33 +140,17 @@ jobs: echo "CMAKE_INSTALL_PREFIX=-DCMAKE_INSTALL_PREFIX=${INSTALL_PATH}" >> $GITHUB_OUTPUT echo "ICICLE_BACKEND_INSTALL_DIR=${INSTALL_PATH}/lib" >> $GITHUB_OUTPUT fi - - name: Setup go - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' - timeout-minutes: 15 - uses: actions/setup-go@v5 - with: - go-version: '1.22.0' - name: Build curve working-directory: ./icicle - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' run: | mkdir -p build && rm -rf build/* cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON -DCURVE=${{ matrix.curve.name }} ${{ matrix.curve.build_args }} ${{ steps.cuda-flag.outputs.CUDA_FLAG }} ${{ steps.cuda-flag.outputs.CMAKE_INSTALL_PREFIX }} -S . -B build cmake --build build --target install -j rm -rf ${{ steps.cuda-flag.outputs.INSTALL_PATH }}/lib/gh_commit_sha_${{ matrix.curve.name }}* touch ${{ steps.cuda-flag.outputs.COMMIT_FILE_PATH }} - - name: Run RUST Curve Tests - working-directory: ./wrappers/rust/icicle-curves - if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp == 'true' - run: | - CURVE=${{ matrix.curve.name }} - CURVE_DIR=icicle-${CURVE//_/-} - export ICICLE_BACKEND_INSTALL_DIR=${{ steps.cuda-flag.outputs.INSTALL_PATH }} - cd ./$CURVE_DIR - cargo test --release --verbose -- --skip phase - cargo test phase2 --release - cargo test phase3 --release + - name: Run C++ Curve Tests working-directory: ./icicle/build/tests if: needs.check-changed-files.outputs.cpp == 'true' @@ -183,6 +171,23 @@ jobs: cd - fi done + - name: Run RUST Curve Tests + working-directory: ./wrappers/rust/icicle-curves + if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp == 'true' + run: | + CURVE=${{ matrix.curve.name }} + CURVE_DIR=icicle-${CURVE//_/-} + export ICICLE_BACKEND_INSTALL_DIR=${{ steps.cuda-flag.outputs.INSTALL_PATH }} + cd ./$CURVE_DIR + cargo test --release --verbose -- --skip phase + cargo test phase2 --release + cargo test phase3 --release + - name: Setup go + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + timeout-minutes: 15 + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' - name: Run Golang curve Tests working-directory: ./wrappers/golang/curves if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' @@ -214,7 +219,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 - name: Checkout CUDA Backend - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' uses: actions/checkout@v4 with: repository: ingonyama-zk/icicle-cuda-backend @@ -222,7 +227,7 @@ jobs: ssh-key: ${{ secrets.CUDA_PULL_KEY }} ref: ${{ needs.extract-cuda-backend-branch.outputs.cuda-backend-branch }} - name: Get CUDA Backend Commit SHA - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' working-directory: ./icicle/backend/cuda id: extract-cuda-sha run: | @@ -230,7 +235,7 @@ jobs: echo "CUDA Backend Commit SHA: $CUDA_BACKEND_SHA" echo "cuda-backend-sha=$CUDA_BACKEND_SHA" >> $GITHUB_OUTPUT - name: Set CUDA backend flag - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' id: cuda-flag run: | CUDA_BACKEND_SHA=${{ steps.extract-cuda-sha.outputs.cuda-backend-sha }} @@ -268,12 +273,6 @@ jobs: echo "CMAKE_INSTALL_PREFIX=-DCMAKE_INSTALL_PREFIX=${INSTALL_PATH}" >> $GITHUB_OUTPUT echo "ICICLE_BACKEND_INSTALL_DIR=${INSTALL_PATH}/lib" >> $GITHUB_OUTPUT fi - - name: Setup go - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' - timeout-minutes: 15 - uses: actions/setup-go@v5 - with: - go-version: '1.22.0' - name: Build field working-directory: ./icicle @@ -284,17 +283,7 @@ jobs: cmake --build build --target install -j rm -rf ${{ steps.cuda-flag.outputs.INSTALL_PATH }}/lib/gh_commit_sha_${{ matrix.field.name }}* touch ${{ steps.cuda-flag.outputs.COMMIT_FILE_PATH }} - - name: Run RUST Field Tests - working-directory: ./wrappers/rust/icicle-fields - if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp == 'true' - run: | - FIELD=${{ matrix.field.name }} - FIELD_DIR=icicle-${FIELD//_/-} - export ICICLE_BACKEND_INSTALL_DIR=${{ steps.cuda-flag.outputs.INSTALL_PATH }} - cd ./$FIELD_DIR - cargo test --release --verbose -- --skip phase - cargo test phase2 --release - cargo test phase3 --release + - name: Run C++ field Tests working-directory: ./icicle/build/tests if: needs.check-changed-files.outputs.cpp == 'true' @@ -317,6 +306,24 @@ jobs: fi done + - name: Run RUST Field Tests + working-directory: ./wrappers/rust/icicle-fields + if: needs.check-changed-files.outputs.rust == 'true' || needs.check-changed-files.outputs.cpp == 'true' + run: | + FIELD=${{ matrix.field.name }} + FIELD_DIR=icicle-${FIELD//_/-} + export ICICLE_BACKEND_INSTALL_DIR=${{ steps.cuda-flag.outputs.INSTALL_PATH }} + cd ./$FIELD_DIR + cargo test --release --verbose -- --skip phase + cargo test phase2 --release + cargo test phase3 --release + + - name: Setup go + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + timeout-minutes: 15 + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' - name: Run Golang field Tests working-directory: ./wrappers/golang/fields if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' @@ -343,7 +350,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 - name: Checkout CUDA Backend - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' uses: actions/checkout@v4 with: repository: ingonyama-zk/icicle-cuda-backend @@ -351,7 +358,7 @@ jobs: ssh-key: ${{ secrets.CUDA_PULL_KEY }} ref: ${{ needs.extract-cuda-backend-branch.outputs.cuda-backend-branch }} - name: Get CUDA Backend Commit SHA - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' working-directory: ./icicle/backend/cuda id: extract-cuda-sha run: | @@ -359,7 +366,7 @@ jobs: echo "CUDA Backend Commit SHA: $CUDA_BACKEND_SHA" echo "cuda-backend-sha=$CUDA_BACKEND_SHA" >> $GITHUB_OUTPUT - name: Set CUDA backend flag - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' || needs.check-changed-files.outputs.rust == 'true' id: cuda-flag run: | CUDA_BACKEND_SHA=${{ steps.extract-cuda-sha.outputs.cuda-backend-sha }} @@ -396,12 +403,6 @@ jobs: echo "CMAKE_INSTALL_PREFIX=-DCMAKE_INSTALL_PREFIX=${INSTALL_PATH}" >> $GITHUB_OUTPUT echo "ICICLE_BACKEND_INSTALL_DIR=${INSTALL_PATH}/lib" >> $GITHUB_OUTPUT fi - - name: Setup go - if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' - timeout-minutes: 15 - uses: actions/setup-go@v5 - with: - go-version: '1.22.0' - name: Build working-directory: ./icicle if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' @@ -420,6 +421,12 @@ jobs: cargo test --release --verbose -- --skip phase cargo test phase2 --release cargo test phase3 --release + - name: Setup go + if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' + timeout-minutes: 15 + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' - name: Test GoLang Hashes working-directory: ./wrappers/golang/hash if: needs.check-changed-files.outputs.golang == 'true' || needs.check-changed-files.outputs.cpp == 'true' diff --git a/docs/docs/icicle/libraries.md b/docs/docs/icicle/libraries.md index 684a6b6177..6a71e4cb1e 100644 --- a/docs/docs/icicle/libraries.md +++ b/docs/docs/icicle/libraries.md @@ -53,6 +53,7 @@ Each library has a corresponding crate. See [programmers guide](./programmers_gu | [Keccak](primitives/hash#keccak-and-sha3) | supporting 256b and 512b digest | | [SHA3](primitives/hash#keccak-and-sha3) | supporting 256b and 512b digest | | [Blake2s](primitives/hash#blake2s) | digest is 256b | +| [Blake3](primitives/hash#blake3) | digest is 256b | | [Merkle-Tree](primitives/merkle) | works with any combination of hash functions | diff --git a/docs/docs/icicle/primitives/hash.md b/docs/docs/icicle/primitives/hash.md index 702487a5a0..a88bc996fe 100644 --- a/docs/docs/icicle/primitives/hash.md +++ b/docs/docs/icicle/primitives/hash.md @@ -23,8 +23,9 @@ ICICLE supports the following hash functions: 3. **SHA3-256** 4. **SHA3-512** 5. **Blake2s** -6. **Poseidon** -7. **Poseidon2** +6. **Blake3** +7. **Poseidon** +8. **Poseidon2** :::info Additional hash functions might be added in the future. Stay tuned! @@ -40,6 +41,10 @@ Keccak can take input messages of any length and produce a fixed-size hash. It u [Blake2s](https://www.rfc-editor.org/rfc/rfc7693.txt) is an optimized cryptographic hash function that provides high performance while ensuring strong security. Blake2s is ideal for hashing small data (such as field elements), especially when speed is crucial. It produces a 256-bit (32-byte) output and is often used in cryptographic protocols. +### Blake3 + +[Blake3](https://www.ietf.org/archive/id/draft-aumasson-blake3-00.html) is a high-performance cryptographic hash function designed for both small and large data. With a a tree-based design for efficient parallelism, it offers strong security, speed, and scalability for modern cryptographic applications. + ### Poseidon [Poseidon](https://eprint.iacr.org/2019/458) is a cryptographic hash function designed specifically for field elements. It is highly optimized for zero-knowledge proofs (ZKPs) and is commonly used in ZK-SNARK systems. Poseidon’s main strength lies in its arithmetization-friendly design, meaning it can be efficiently expressed as arithmetic constraints within a ZK-SNARK circuit. diff --git a/examples/c++/best-practice-ntt/example.cpp b/examples/c++/best-practice-ntt/example.cpp index 7db88aa357..7f4bc974d1 100644 --- a/examples/c++/best-practice-ntt/example.cpp +++ b/examples/c++/best-practice-ntt/example.cpp @@ -116,8 +116,8 @@ int main(int argc, char* argv[]) // Clean-up for (int i = 0; i < 2; i++) { ICICLE_CHECK(icicle_free(d_vec[i])); - delete[](h_inp[i]); - delete[](h_out[i]); + delete[] (h_inp[i]); + delete[] (h_out[i]); } ICICLE_CHECK(icicle_destroy_stream(stream_compute)); ICICLE_CHECK(icicle_destroy_stream(stream_d2h)); diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index 32711c58cc..aa31e0c517 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -70,6 +70,10 @@ if (HASH) target_sources(icicle_hash PRIVATE src/hash/cpu_keccak.cpp src/hash/cpu_blake2s.cpp + src/hash/cpu_blake3.cpp + src/hash/blake3.c + src/hash/blake3_dispatch.c + src/hash/blake3_portable.c src/hash/cpu_merkle_tree.cpp ) target_include_directories(icicle_hash PUBLIC include) diff --git a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp index 38d859e022..18fb5dd032 100644 --- a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp +++ b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp @@ -372,7 +372,7 @@ class VectorOpTask : public TaskBase public: T m_intermidiate_res; // pointer to the output. Can be a vector or scalar pointer uint64_t m_idx_in_batch; // index in the batch. Used in intermediate res tasks -}; +}; // class VectorOpTask #define NOF_OPERATIONS_PER_TASK 512 #define CONFIG_NOF_THREADS_KEY "n_threads" diff --git a/icicle/backend/cpu/src/hash/blake3.c b/icicle/backend/cpu/src/hash/blake3.c new file mode 100644 index 0000000000..617949f109 --- /dev/null +++ b/icicle/backend/cpu/src/hash/blake3.c @@ -0,0 +1,591 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#include +#include "blake3.h" +#include "blake3_impl.h" + +const char* blake3_version(void) { return BLAKE3_VERSION_STRING; } + +INLINE void chunk_state_init(blake3_chunk_state* self, const uint32_t key[8], uint8_t flags) +{ + memcpy(self->cv, key, BLAKE3_KEY_LEN); + self->chunk_counter = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + self->buf_len = 0; + self->blocks_compressed = 0; + self->flags = flags; +} + +INLINE void chunk_state_reset(blake3_chunk_state* self, const uint32_t key[8], uint64_t chunk_counter) +{ + memcpy(self->cv, key, BLAKE3_KEY_LEN); + self->chunk_counter = chunk_counter; + self->blocks_compressed = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + self->buf_len = 0; +} + +INLINE size_t chunk_state_len(const blake3_chunk_state* self) +{ + return (BLAKE3_BLOCK_LEN * (size_t)self->blocks_compressed) + ((size_t)self->buf_len); +} + +INLINE size_t chunk_state_fill_buf(blake3_chunk_state* self, const uint8_t* input, size_t input_len) +{ + size_t take = BLAKE3_BLOCK_LEN - ((size_t)self->buf_len); + if (take > input_len) { take = input_len; } + uint8_t* dest = self->buf + ((size_t)self->buf_len); + memcpy(dest, input, take); + self->buf_len += (uint8_t)take; + return take; +} + +INLINE uint8_t chunk_state_maybe_start_flag(const blake3_chunk_state* self) +{ + if (self->blocks_compressed == 0) { + return CHUNK_START; + } else { + return 0; + } +} + +typedef struct { + uint32_t input_cv[8]; + uint64_t counter; + uint8_t block[BLAKE3_BLOCK_LEN]; + uint8_t block_len; + uint8_t flags; +} output_t; + +INLINE output_t make_output( + const uint32_t input_cv[8], const uint8_t block[BLAKE3_BLOCK_LEN], uint8_t block_len, uint64_t counter, uint8_t flags) +{ + output_t ret; + memcpy(ret.input_cv, input_cv, 32); + memcpy(ret.block, block, BLAKE3_BLOCK_LEN); + ret.block_len = block_len; + ret.counter = counter; + ret.flags = flags; + return ret; +} + +// Chaining values within a given chunk (specifically the compress_in_place +// interface) are represented as words. This avoids unnecessary bytes<->words +// conversion overhead in the portable implementation. However, the hash_many +// interface handles both user input and parent node blocks, so it accepts +// bytes. For that reason, chaining values in the CV stack are represented as +// bytes. +INLINE void output_chaining_value(const output_t* self, uint8_t cv[32]) +{ + uint32_t cv_words[8]; + memcpy(cv_words, self->input_cv, 32); + blake3_compress_in_place(cv_words, self->block, self->block_len, self->counter, self->flags); + store_cv_words(cv, cv_words); +} + +INLINE void output_root_bytes(const output_t* self, uint64_t seek, uint8_t* out, size_t out_len) +{ + if (out_len == 0) { return; } + uint64_t output_block_counter = seek / 64; + size_t offset_within_block = seek % 64; + uint8_t wide_buf[64]; + if (offset_within_block) { + blake3_compress_xof( + self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf); + const size_t available_bytes = 64 - offset_within_block; + const size_t bytes = out_len > available_bytes ? available_bytes : out_len; + memcpy(out, wide_buf + offset_within_block, bytes); + out += bytes; + out_len -= bytes; + output_block_counter += 1; + } + if (out_len / 64) { + blake3_xof_many( + self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, out, out_len / 64); + } + output_block_counter += out_len / 64; + out += out_len & -64; + out_len -= out_len & -64; + if (out_len) { + blake3_compress_xof( + self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf); + memcpy(out, wide_buf, out_len); + } +} + +INLINE void chunk_state_update(blake3_chunk_state* self, const uint8_t* input, size_t input_len) +{ + if (self->buf_len > 0) { + size_t take = chunk_state_fill_buf(self, input, input_len); + input += take; + input_len -= take; + if (input_len > 0) { + blake3_compress_in_place( + self->cv, self->buf, BLAKE3_BLOCK_LEN, self->chunk_counter, self->flags | chunk_state_maybe_start_flag(self)); + self->blocks_compressed += 1; + self->buf_len = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + } + } + + while (input_len > BLAKE3_BLOCK_LEN) { + blake3_compress_in_place( + self->cv, input, BLAKE3_BLOCK_LEN, self->chunk_counter, self->flags | chunk_state_maybe_start_flag(self)); + self->blocks_compressed += 1; + input += BLAKE3_BLOCK_LEN; + input_len -= BLAKE3_BLOCK_LEN; + } + + chunk_state_fill_buf(self, input, input_len); +} + +INLINE output_t chunk_state_output(const blake3_chunk_state* self) +{ + uint8_t block_flags = self->flags | chunk_state_maybe_start_flag(self) | CHUNK_END; + return make_output(self->cv, self->buf, self->buf_len, self->chunk_counter, block_flags); +} + +INLINE output_t parent_output(const uint8_t block[BLAKE3_BLOCK_LEN], const uint32_t key[8], uint8_t flags) +{ + return make_output(key, block, BLAKE3_BLOCK_LEN, 0, flags | PARENT); +} + +// Given some input larger than one chunk, return the number of bytes that +// should go in the left subtree. This is the largest power-of-2 number of +// chunks that leaves at least 1 byte for the right subtree. +INLINE size_t left_len(size_t content_len) +{ + // Subtract 1 to reserve at least one byte for the right side. content_len + // should always be greater than BLAKE3_CHUNK_LEN. + size_t full_chunks = (content_len - 1) / BLAKE3_CHUNK_LEN; + return round_down_to_power_of_2(full_chunks) * BLAKE3_CHUNK_LEN; +} + +// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE chunks at the same time +// on a single thread. Write out the chunk chaining values and return the +// number of chunks hashed. These chunks are never the root and never empty; +// those cases use a different codepath. +INLINE size_t compress_chunks_parallel( + const uint8_t* input, size_t input_len, const uint32_t key[8], uint64_t chunk_counter, uint8_t flags, uint8_t* out) +{ + const uint8_t* chunks_array[MAX_SIMD_DEGREE]; + size_t input_position = 0; + size_t chunks_array_len = 0; + while (input_len - input_position >= BLAKE3_CHUNK_LEN) { + chunks_array[chunks_array_len] = &input[input_position]; + input_position += BLAKE3_CHUNK_LEN; + chunks_array_len += 1; + } + + blake3_hash_many( + chunks_array, chunks_array_len, BLAKE3_CHUNK_LEN / BLAKE3_BLOCK_LEN, key, chunk_counter, true, flags, CHUNK_START, + CHUNK_END, out); + + // Hash the remaining partial chunk, if there is one. Note that the empty + // chunk (meaning the empty message) is a different codepath. + if (input_len > input_position) { + uint64_t counter = chunk_counter + (uint64_t)chunks_array_len; + blake3_chunk_state chunk_state; + chunk_state_init(&chunk_state, key, flags); + chunk_state.chunk_counter = counter; + chunk_state_update(&chunk_state, &input[input_position], input_len - input_position); + output_t output = chunk_state_output(&chunk_state); + output_chaining_value(&output, &out[chunks_array_len * BLAKE3_OUT_LEN]); + return chunks_array_len + 1; + } else { + return chunks_array_len; + } +} + +// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE parents at the same time +// on a single thread. Write out the parent chaining values and return the +// number of parents hashed. (If there's an odd input chaining value left over, +// return it as an additional output.) These parents are never the root and +// never empty; those cases use a different codepath. +INLINE size_t compress_parents_parallel( + const uint8_t* child_chaining_values, size_t num_chaining_values, const uint32_t key[8], uint8_t flags, uint8_t* out) +{ + const uint8_t* parents_array[MAX_SIMD_DEGREE_OR_2]; + size_t parents_array_len = 0; + while (num_chaining_values - (2 * parents_array_len) >= 2) { + parents_array[parents_array_len] = &child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN]; + parents_array_len += 1; + } + + blake3_hash_many( + parents_array, parents_array_len, 1, key, + 0, // Parents always use counter 0. + false, flags | PARENT, + 0, // Parents have no start flags. + 0, // Parents have no end flags. + out); + + // If there's an odd child left over, it becomes an output. + if (num_chaining_values > 2 * parents_array_len) { + memcpy( + &out[parents_array_len * BLAKE3_OUT_LEN], &child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN], + BLAKE3_OUT_LEN); + return parents_array_len + 1; + } else { + return parents_array_len; + } +} + +// The wide helper function returns (writes out) an array of chaining values +// and returns the length of that array. The number of chaining values returned +// is the dynamically detected SIMD degree, at most MAX_SIMD_DEGREE. Or fewer, +// if the input is shorter than that many chunks. The reason for maintaining a +// wide array of chaining values going back up the tree, is to allow the +// implementation to hash as many parents in parallel as possible. +// +// As a special case when the SIMD degree is 1, this function will still return +// at least 2 outputs. This guarantees that this function doesn't perform the +// root compression. (If it did, it would use the wrong flags, and also we +// wouldn't be able to implement extendable output.) Note that this function is +// not used when the whole input is only 1 chunk long; that's a different +// codepath. +// +// Why not just have the caller split the input on the first update(), instead +// of implementing this special rule? Because we don't want to limit SIMD or +// multi-threading parallelism for that update(). +static size_t blake3_compress_subtree_wide( + const uint8_t* input, size_t input_len, const uint32_t key[8], uint64_t chunk_counter, uint8_t flags, uint8_t* out) +{ + // Note that the single chunk case does *not* bump the SIMD degree up to 2 + // when it is 1. If this implementation adds multi-threading in the future, + // this gives us the option of multi-threading even the 2-chunk case, which + // can help performance on smaller platforms. + if (input_len <= blake3_simd_degree() * BLAKE3_CHUNK_LEN) { + return compress_chunks_parallel(input, input_len, key, chunk_counter, flags, out); + } + + // With more than simd_degree chunks, we need to recurse. Start by dividing + // the input into left and right subtrees. (Note that this is only optimal + // as long as the SIMD degree is a power of 2. If we ever get a SIMD degree + // of 3 or something, we'll need a more complicated strategy.) + size_t left_input_len = left_len(input_len); + size_t right_input_len = input_len - left_input_len; + const uint8_t* right_input = &input[left_input_len]; + uint64_t right_chunk_counter = chunk_counter + (uint64_t)(left_input_len / BLAKE3_CHUNK_LEN); + + // Make space for the child outputs. Here we use MAX_SIMD_DEGREE_OR_2 to + // account for the special case of returning 2 outputs when the SIMD degree + // is 1. + uint8_t cv_array[2 * MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN]; + size_t degree = blake3_simd_degree(); + if (left_input_len > BLAKE3_CHUNK_LEN && degree == 1) { + // The special case: We always use a degree of at least two, to make + // sure there are two outputs. Except, as noted above, at the chunk + // level, where we allow degree=1. (Note that the 1-chunk-input case is + // a different codepath.) + degree = 2; + } + uint8_t* right_cvs = &cv_array[degree * BLAKE3_OUT_LEN]; + + // Recurse! If this implementation adds multi-threading support in the + // future, this is where it will go. + size_t left_n = blake3_compress_subtree_wide(input, left_input_len, key, chunk_counter, flags, cv_array); + size_t right_n = + blake3_compress_subtree_wide(right_input, right_input_len, key, right_chunk_counter, flags, right_cvs); + + // The special case again. If simd_degree=1, then we'll have left_n=1 and + // right_n=1. Rather than compressing them into a single output, return + // them directly, to make sure we always have at least two outputs. + if (left_n == 1) { + memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN); + return 2; + } + + // Otherwise, do one layer of parent node compression. + size_t num_chaining_values = left_n + right_n; + return compress_parents_parallel(cv_array, num_chaining_values, key, flags, out); +} + +// Hash a subtree with compress_subtree_wide(), and then condense the resulting +// list of chaining values down to a single parent node. Don't compress that +// last parent node, however. Instead, return its message bytes (the +// concatenated chaining values of its children). This is necessary when the +// first call to update() supplies a complete subtree, because the topmost +// parent node of that subtree could end up being the root. It's also necessary +// for extended output in the general case. +// +// As with compress_subtree_wide(), this function is not used on inputs of 1 +// chunk or less. That's a different codepath. +INLINE void compress_subtree_to_parent_node( + const uint8_t* input, + size_t input_len, + const uint32_t key[8], + uint64_t chunk_counter, + uint8_t flags, + uint8_t out[2 * BLAKE3_OUT_LEN]) +{ + uint8_t cv_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN]; + size_t num_cvs = blake3_compress_subtree_wide(input, input_len, key, chunk_counter, flags, cv_array); + assert(num_cvs <= MAX_SIMD_DEGREE_OR_2); + // The following loop never executes when MAX_SIMD_DEGREE_OR_2 is 2, because + // as we just asserted, num_cvs will always be <=2 in that case. But GCC + // (particularly GCC 8.5) can't tell that it never executes, and if NDEBUG is + // set then it emits incorrect warnings here. We tried a few different + // hacks to silence these, but in the end our hacks just produced different + // warnings (see https://github.com/BLAKE3-team/BLAKE3/pull/380). Out of + // desperation, we ifdef out this entire loop when we know it's not needed. +#if MAX_SIMD_DEGREE_OR_2 > 2 + // If MAX_SIMD_DEGREE_OR_2 is greater than 2 and there's enough input, + // compress_subtree_wide() returns more than 2 chaining values. Condense + // them into 2 by forming parent nodes repeatedly. + uint8_t out_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN / 2]; + while (num_cvs > 2) { + num_cvs = compress_parents_parallel(cv_array, num_cvs, key, flags, out_array); + memcpy(cv_array, out_array, num_cvs * BLAKE3_OUT_LEN); + } +#endif + memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN); +} + +INLINE void hasher_init_base(blake3_hasher* self, const uint32_t key[8], uint8_t flags) +{ + memcpy(self->key, key, BLAKE3_KEY_LEN); + chunk_state_init(&self->chunk, key, flags); + self->cv_stack_len = 0; +} + +void blake3_hasher_init(blake3_hasher* self) { hasher_init_base(self, IV, 0); } + +void blake3_hasher_init_keyed(blake3_hasher* self, const uint8_t key[BLAKE3_KEY_LEN]) +{ + uint32_t key_words[8]; + load_key_words(key, key_words); + hasher_init_base(self, key_words, KEYED_HASH); +} + +void blake3_hasher_init_derive_key_raw(blake3_hasher* self, const void* context, size_t context_len) +{ + blake3_hasher context_hasher; + hasher_init_base(&context_hasher, IV, DERIVE_KEY_CONTEXT); + blake3_hasher_update(&context_hasher, context, context_len); + uint8_t context_key[BLAKE3_KEY_LEN]; + blake3_hasher_finalize(&context_hasher, context_key, BLAKE3_KEY_LEN); + uint32_t context_key_words[8]; + load_key_words(context_key, context_key_words); + hasher_init_base(self, context_key_words, DERIVE_KEY_MATERIAL); +} + +void blake3_hasher_init_derive_key(blake3_hasher* self, const char* context) +{ + blake3_hasher_init_derive_key_raw(self, context, strlen(context)); +} + +// As described in hasher_push_cv() below, we do "lazy merging", delaying +// merges until right before the next CV is about to be added. This is +// different from the reference implementation. Another difference is that we +// aren't always merging 1 chunk at a time. Instead, each CV might represent +// any power-of-two number of chunks, as long as the smaller-above-larger stack +// order is maintained. Instead of the "count the trailing 0-bits" algorithm +// described in the spec, we use a "count the total number of 1-bits" variant +// that doesn't require us to retain the subtree size of the CV on top of the +// stack. The principle is the same: each CV that should remain in the stack is +// represented by a 1-bit in the total number of chunks (or bytes) so far. +INLINE void hasher_merge_cv_stack(blake3_hasher* self, uint64_t total_len) +{ + size_t post_merge_stack_len = (size_t)popcnt(total_len); + while (self->cv_stack_len > post_merge_stack_len) { + uint8_t* parent_node = &self->cv_stack[(self->cv_stack_len - 2) * BLAKE3_OUT_LEN]; + output_t output = parent_output(parent_node, self->key, self->chunk.flags); + output_chaining_value(&output, parent_node); + self->cv_stack_len -= 1; + } +} + +// In reference_impl.rs, we merge the new CV with existing CVs from the stack +// before pushing it. We can do that because we know more input is coming, so +// we know none of the merges are root. +// +// This setting is different. We want to feed as much input as possible to +// compress_subtree_wide(), without setting aside anything for the chunk_state. +// If the user gives us 64 KiB, we want to parallelize over all 64 KiB at once +// as a single subtree, if at all possible. +// +// This leads to two problems: +// 1) This 64 KiB input might be the only call that ever gets made to update. +// In this case, the root node of the 64 KiB subtree would be the root node +// of the whole tree, and it would need to be ROOT finalized. We can't +// compress it until we know. +// 2) This 64 KiB input might complete a larger tree, whose root node is +// similarly going to be the root of the whole tree. For example, maybe +// we have 196 KiB (that is, 128 + 64) hashed so far. We can't compress the +// node at the root of the 256 KiB subtree until we know how to finalize it. +// +// The second problem is solved with "lazy merging". That is, when we're about +// to add a CV to the stack, we don't merge it with anything first, as the +// reference impl does. Instead we do merges using the *previous* CV that was +// added, which is sitting on top of the stack, and we put the new CV +// (unmerged) on top of the stack afterwards. This guarantees that we never +// merge the root node until finalize(). +// +// Solving the first problem requires an additional tool, +// compress_subtree_to_parent_node(). That function always returns the top +// *two* chaining values of the subtree it's compressing. We then do lazy +// merging with each of them separately, so that the second CV will always +// remain unmerged. (That also helps us support extendable output when we're +// hashing an input all-at-once.) +INLINE void hasher_push_cv(blake3_hasher* self, uint8_t new_cv[BLAKE3_OUT_LEN], uint64_t chunk_counter) +{ + hasher_merge_cv_stack(self, chunk_counter); + memcpy(&self->cv_stack[self->cv_stack_len * BLAKE3_OUT_LEN], new_cv, BLAKE3_OUT_LEN); + self->cv_stack_len += 1; +} + +void blake3_hasher_update(blake3_hasher* self, const void* input, size_t input_len) +{ + // Explicitly checking for zero avoids causing UB by passing a null pointer + // to memcpy. This comes up in practice with things like: + // std::vector v; + // blake3_hasher_update(&hasher, v.data(), v.size()); + if (input_len == 0) { return; } + + const uint8_t* input_bytes = (const uint8_t*)input; + + // If we have some partial chunk bytes in the internal chunk_state, we need + // to finish that chunk first. + if (chunk_state_len(&self->chunk) > 0) { + size_t take = BLAKE3_CHUNK_LEN - chunk_state_len(&self->chunk); + if (take > input_len) { take = input_len; } + chunk_state_update(&self->chunk, input_bytes, take); + input_bytes += take; + input_len -= take; + // If we've filled the current chunk and there's more coming, finalize this + // chunk and proceed. In this case we know it's not the root. + if (input_len > 0) { + output_t output = chunk_state_output(&self->chunk); + uint8_t chunk_cv[32]; + output_chaining_value(&output, chunk_cv); + hasher_push_cv(self, chunk_cv, self->chunk.chunk_counter); + chunk_state_reset(&self->chunk, self->key, self->chunk.chunk_counter + 1); + } else { + return; + } + } + + // Now the chunk_state is clear, and we have more input. If there's more than + // a single chunk (so, definitely not the root chunk), hash the largest whole + // subtree we can, with the full benefits of SIMD (and maybe in the future, + // multi-threading) parallelism. Two restrictions: + // - The subtree has to be a power-of-2 number of chunks. Only subtrees along + // the right edge can be incomplete, and we don't know where the right edge + // is going to be until we get to finalize(). + // - The subtree must evenly divide the total number of chunks up until this + // point (if total is not 0). If the current incomplete subtree is only + // waiting for 1 more chunk, we can't hash a subtree of 4 chunks. We have + // to complete the current subtree first. + // Because we might need to break up the input to form powers of 2, or to + // evenly divide what we already have, this part runs in a loop. + while (input_len > BLAKE3_CHUNK_LEN) { + size_t subtree_len = round_down_to_power_of_2(input_len); + uint64_t count_so_far = self->chunk.chunk_counter * BLAKE3_CHUNK_LEN; + // Shrink the subtree_len until it evenly divides the count so far. We know + // that subtree_len itself is a power of 2, so we can use a bitmasking + // trick instead of an actual remainder operation. (Note that if the caller + // consistently passes power-of-2 inputs of the same size, as is hopefully + // typical, this loop condition will always fail, and subtree_len will + // always be the full length of the input.) + // + // An aside: We don't have to shrink subtree_len quite this much. For + // example, if count_so_far is 1, we could pass 2 chunks to + // compress_subtree_to_parent_node. Since we'll get 2 CVs back, we'll still + // get the right answer in the end, and we might get to use 2-way SIMD + // parallelism. The problem with this optimization, is that it gets us + // stuck always hashing 2 chunks. The total number of chunks will remain + // odd, and we'll never graduate to higher degrees of parallelism. See + // https://github.com/BLAKE3-team/BLAKE3/issues/69. + while ((((uint64_t)(subtree_len - 1)) & count_so_far) != 0) { + subtree_len /= 2; + } + // The shrunken subtree_len might now be 1 chunk long. If so, hash that one + // chunk by itself. Otherwise, compress the subtree into a pair of CVs. + uint64_t subtree_chunks = subtree_len / BLAKE3_CHUNK_LEN; + if (subtree_len <= BLAKE3_CHUNK_LEN) { + blake3_chunk_state chunk_state; + chunk_state_init(&chunk_state, self->key, self->chunk.flags); + chunk_state.chunk_counter = self->chunk.chunk_counter; + chunk_state_update(&chunk_state, input_bytes, subtree_len); + output_t output = chunk_state_output(&chunk_state); + uint8_t cv[BLAKE3_OUT_LEN]; + output_chaining_value(&output, cv); + hasher_push_cv(self, cv, chunk_state.chunk_counter); + } else { + // This is the high-performance happy path, though getting here depends + // on the caller giving us a long enough input. + uint8_t cv_pair[2 * BLAKE3_OUT_LEN]; + compress_subtree_to_parent_node( + input_bytes, subtree_len, self->key, self->chunk.chunk_counter, self->chunk.flags, cv_pair); + hasher_push_cv(self, cv_pair, self->chunk.chunk_counter); + hasher_push_cv(self, &cv_pair[BLAKE3_OUT_LEN], self->chunk.chunk_counter + (subtree_chunks / 2)); + } + self->chunk.chunk_counter += subtree_chunks; + input_bytes += subtree_len; + input_len -= subtree_len; + } + + // If there's any remaining input less than a full chunk, add it to the chunk + // state. In that case, also do a final merge loop to make sure the subtree + // stack doesn't contain any unmerged pairs. The remaining input means we + // know these merges are non-root. This merge loop isn't strictly necessary + // here, because hasher_push_chunk_cv already does its own merge loop, but it + // simplifies blake3_hasher_finalize below. + if (input_len > 0) { + chunk_state_update(&self->chunk, input_bytes, input_len); + hasher_merge_cv_stack(self, self->chunk.chunk_counter); + } +} + +void blake3_hasher_finalize(const blake3_hasher* self, uint8_t* out, size_t out_len) +{ + blake3_hasher_finalize_seek(self, 0, out, out_len); +} + +void blake3_hasher_finalize_seek(const blake3_hasher* self, uint64_t seek, uint8_t* out, size_t out_len) +{ + // Explicitly checking for zero avoids causing UB by passing a null pointer + // to memcpy. This comes up in practice with things like: + // std::vector v; + // blake3_hasher_finalize(&hasher, v.data(), v.size()); + if (out_len == 0) { return; } + + // If the subtree stack is empty, then the current chunk is the root. + if (self->cv_stack_len == 0) { + output_t output = chunk_state_output(&self->chunk); + output_root_bytes(&output, seek, out, out_len); + return; + } + // If there are any bytes in the chunk state, finalize that chunk and do a + // roll-up merge between that chunk hash and every subtree in the stack. In + // this case, the extra merge loop at the end of blake3_hasher_update + // guarantees that none of the subtrees in the stack need to be merged with + // each other first. Otherwise, if there are no bytes in the chunk state, + // then the top of the stack is a chunk hash, and we start the merge from + // that. + output_t output; + size_t cvs_remaining; + if (chunk_state_len(&self->chunk) > 0) { + cvs_remaining = self->cv_stack_len; + output = chunk_state_output(&self->chunk); + } else { + // There are always at least 2 CVs in the stack in this case. + cvs_remaining = self->cv_stack_len - 2; + output = parent_output(&self->cv_stack[cvs_remaining * 32], self->key, self->chunk.flags); + } + while (cvs_remaining > 0) { + cvs_remaining -= 1; + uint8_t parent_block[BLAKE3_BLOCK_LEN]; + memcpy(parent_block, &self->cv_stack[cvs_remaining * 32], 32); + output_chaining_value(&output, &parent_block[32]); + output = parent_output(parent_block, self->key, self->chunk.flags); + } + output_root_bytes(&output, seek, out, out_len); +} + +void blake3_hasher_reset(blake3_hasher* self) +{ + chunk_state_reset(&self->chunk, self->key, 0); + self->cv_stack_len = 0; +} diff --git a/icicle/backend/cpu/src/hash/blake3.h b/icicle/backend/cpu/src/hash/blake3.h new file mode 100644 index 0000000000..55b238f1ac --- /dev/null +++ b/icicle/backend/cpu/src/hash/blake3.h @@ -0,0 +1,57 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#ifndef BLAKE3_H +#define BLAKE3_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BLAKE3_VERSION_STRING "1.5.5" +#define BLAKE3_KEY_LEN 32 +#define BLAKE3_OUT_LEN 32 +#define BLAKE3_BLOCK_LEN 64 +#define BLAKE3_CHUNK_LEN 1024 +#define BLAKE3_MAX_DEPTH 54 + +// This struct is a private implementation detail. It has to be here because +// it's part of blake3_hasher below. +typedef struct { + uint32_t cv[8]; + uint64_t chunk_counter; + uint8_t buf[BLAKE3_BLOCK_LEN]; + uint8_t buf_len; + uint8_t blocks_compressed; + uint8_t flags; +} blake3_chunk_state; + +typedef struct { + uint32_t key[8]; + blake3_chunk_state chunk; + uint8_t cv_stack_len; + // The stack size is MAX_DEPTH + 1 because we do lazy merging. For example, + // with 7 chunks, we have 3 entries in the stack. Adding an 8th chunk + // requires a 4th entry, rather than merging everything down to 1, because we + // don't know whether more input is coming. This is different from how the + // reference implementation does things. + uint8_t cv_stack[(BLAKE3_MAX_DEPTH + 1) * BLAKE3_OUT_LEN]; +} blake3_hasher; + +const char* blake3_version(void); +void blake3_hasher_init(blake3_hasher* self); +void blake3_hasher_init_keyed(blake3_hasher* self, const uint8_t key[BLAKE3_KEY_LEN]); +void blake3_hasher_init_derive_key(blake3_hasher* self, const char* context); +void blake3_hasher_init_derive_key_raw(blake3_hasher* self, const void* context, size_t context_len); +void blake3_hasher_update(blake3_hasher* self, const void* input, size_t input_len); +void blake3_hasher_finalize(const blake3_hasher* self, uint8_t* out, size_t out_len); +void blake3_hasher_finalize_seek(const blake3_hasher* self, uint64_t seek, uint8_t* out, size_t out_len); +void blake3_hasher_reset(blake3_hasher* self); + +#ifdef __cplusplus +} +#endif + +#endif /* BLAKE3_H */ diff --git a/icicle/backend/cpu/src/hash/blake3_dispatch.c b/icicle/backend/cpu/src/hash/blake3_dispatch.c new file mode 100644 index 0000000000..296e769e31 --- /dev/null +++ b/icicle/backend/cpu/src/hash/blake3_dispatch.c @@ -0,0 +1,62 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#include +#include +#include + +#include "blake3_impl.h" + +void blake3_compress_in_place( + uint32_t cv[8], const uint8_t block[BLAKE3_BLOCK_LEN], uint8_t block_len, uint64_t counter, uint8_t flags) +{ + blake3_compress_in_place_portable(cv, block, block_len, counter, flags); +} + +void blake3_compress_xof( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64]) +{ + blake3_compress_xof_portable(cv, block, block_len, counter, flags, out); +} + +void blake3_xof_many( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64], + size_t outblocks) +{ + if (outblocks == 0) { + // The current assembly implementation always outputs at least 1 block. + return; + } + + for (size_t i = 0; i < outblocks; ++i) { + blake3_compress_xof(cv, block, block_len, counter + i, flags, out + 64 * i); + } +} + +void blake3_hash_many( + const uint8_t* const* inputs, + size_t num_inputs, + size_t blocks, + const uint32_t key[8], + uint64_t counter, + bool increment_counter, + uint8_t flags, + uint8_t flags_start, + uint8_t flags_end, + uint8_t* out) +{ + blake3_hash_many_portable( + inputs, num_inputs, blocks, key, counter, increment_counter, flags, flags_start, flags_end, out); +} + +// The dynamically detected SIMD degree of the current platform. +size_t blake3_simd_degree(void) { return 1; } diff --git a/icicle/backend/cpu/src/hash/blake3_impl.h b/icicle/backend/cpu/src/hash/blake3_impl.h new file mode 100644 index 0000000000..1bd2bc0046 --- /dev/null +++ b/icicle/backend/cpu/src/hash/blake3_impl.h @@ -0,0 +1,205 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#ifndef BLAKE3_IMPL_H +#define BLAKE3_IMPL_H + +#include +#include +#include +#include +#include + +#include "blake3.h" + +// internal flags +enum blake3_flags { + CHUNK_START = 1 << 0, + CHUNK_END = 1 << 1, + PARENT = 1 << 2, + ROOT = 1 << 3, + KEYED_HASH = 1 << 4, + DERIVE_KEY_CONTEXT = 1 << 5, + DERIVE_KEY_MATERIAL = 1 << 6, +}; + +// This C implementation tries to support recent versions of GCC, Clang, and +// MSVC. + +#define INLINE static inline __attribute__((always_inline)) +#define MAX_SIMD_DEGREE 1 + +// There are some places where we want a static size that's equal to the +// MAX_SIMD_DEGREE, but also at least 2. +#define MAX_SIMD_DEGREE_OR_2 (MAX_SIMD_DEGREE > 2 ? MAX_SIMD_DEGREE : 2) + +static const uint32_t IV[8] = {0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL, 0xA54FF53AUL, + 0x510E527FUL, 0x9B05688CUL, 0x1F83D9ABUL, 0x5BE0CD19UL}; + +static const uint8_t MSG_SCHEDULE[7][16] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, {2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8}, + {3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1}, {10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6}, + {12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4}, {9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7}, + {11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13}, +}; + +/* Find index of the highest set bit */ +/* x is assumed to be nonzero. */ +static unsigned int highest_one(uint64_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return 63 ^ (unsigned int)__builtin_clzll(x); +#else + unsigned int c = 0; + if (x & 0xffffffff00000000ULL) { + x >>= 32; + c += 32; + } + if (x & 0x00000000ffff0000ULL) { + x >>= 16; + c += 16; + } + if (x & 0x000000000000ff00ULL) { + x >>= 8; + c += 8; + } + if (x & 0x00000000000000f0ULL) { + x >>= 4; + c += 4; + } + if (x & 0x000000000000000cULL) { + x >>= 2; + c += 2; + } + if (x & 0x0000000000000002ULL) { c += 1; } + return c; +#endif +} + +// Count the number of 1 bits. +INLINE unsigned int popcnt(uint64_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return (unsigned int)__builtin_popcountll(x); +#else + unsigned int count = 0; + while (x != 0) { + count += 1; + x &= x - 1; + } + return count; +#endif +} + +// Largest power of two less than or equal to x. As a special case, returns 1 +// when x is 0. +INLINE uint64_t round_down_to_power_of_2(uint64_t x) { return 1ULL << highest_one(x | 1); } + +INLINE uint32_t counter_low(uint64_t counter) { return (uint32_t)counter; } + +INLINE uint32_t counter_high(uint64_t counter) { return (uint32_t)(counter >> 32); } + +INLINE uint32_t load32(const void* src) +{ + const uint8_t* p = (const uint8_t*)src; + return ((uint32_t)(p[0]) << 0) | ((uint32_t)(p[1]) << 8) | ((uint32_t)(p[2]) << 16) | ((uint32_t)(p[3]) << 24); +} + +INLINE void load_key_words(const uint8_t key[BLAKE3_KEY_LEN], uint32_t key_words[8]) +{ + key_words[0] = load32(&key[0 * 4]); + key_words[1] = load32(&key[1 * 4]); + key_words[2] = load32(&key[2 * 4]); + key_words[3] = load32(&key[3 * 4]); + key_words[4] = load32(&key[4 * 4]); + key_words[5] = load32(&key[5 * 4]); + key_words[6] = load32(&key[6 * 4]); + key_words[7] = load32(&key[7 * 4]); +} + +INLINE void load_block_words(const uint8_t block[BLAKE3_BLOCK_LEN], uint32_t block_words[16]) +{ + for (size_t i = 0; i < 16; i++) { + block_words[i] = load32(&block[i * 4]); + } +} + +INLINE void store32(void* dst, uint32_t w) +{ + uint8_t* p = (uint8_t*)dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); +} + +INLINE void store_cv_words(uint8_t bytes_out[32], uint32_t cv_words[8]) +{ + store32(&bytes_out[0 * 4], cv_words[0]); + store32(&bytes_out[1 * 4], cv_words[1]); + store32(&bytes_out[2 * 4], cv_words[2]); + store32(&bytes_out[3 * 4], cv_words[3]); + store32(&bytes_out[4 * 4], cv_words[4]); + store32(&bytes_out[5 * 4], cv_words[5]); + store32(&bytes_out[6 * 4], cv_words[6]); + store32(&bytes_out[7 * 4], cv_words[7]); +} + +void blake3_compress_in_place( + uint32_t cv[8], const uint8_t block[BLAKE3_BLOCK_LEN], uint8_t block_len, uint64_t counter, uint8_t flags); + +void blake3_compress_xof( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64]); + +void blake3_xof_many( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64], + size_t outblocks); + +void blake3_hash_many( + const uint8_t* const* inputs, + size_t num_inputs, + size_t blocks, + const uint32_t key[8], + uint64_t counter, + bool increment_counter, + uint8_t flags, + uint8_t flags_start, + uint8_t flags_end, + uint8_t* out); + +size_t blake3_simd_degree(void); + +// Declarations for implementation-specific functions. +void blake3_compress_in_place_portable( + uint32_t cv[8], const uint8_t block[BLAKE3_BLOCK_LEN], uint8_t block_len, uint64_t counter, uint8_t flags); + +void blake3_compress_xof_portable( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64]); + +void blake3_hash_many_portable( + const uint8_t* const* inputs, + size_t num_inputs, + size_t blocks, + const uint32_t key[8], + uint64_t counter, + bool increment_counter, + uint8_t flags, + uint8_t flags_start, + uint8_t flags_end, + uint8_t* out); + +#endif /* BLAKE3_IMPL_H */ diff --git a/icicle/backend/cpu/src/hash/blake3_portable.c b/icicle/backend/cpu/src/hash/blake3_portable.c new file mode 100644 index 0000000000..9ef1293418 --- /dev/null +++ b/icicle/backend/cpu/src/hash/blake3_portable.c @@ -0,0 +1,176 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#include "blake3_impl.h" +#include + +INLINE uint32_t rotr32(uint32_t w, uint32_t c) { return (w >> c) | (w << (32 - c)); } + +INLINE void g(uint32_t* state, size_t a, size_t b, size_t c, size_t d, uint32_t x, uint32_t y) +{ + state[a] = state[a] + state[b] + x; + state[d] = rotr32(state[d] ^ state[a], 16); + state[c] = state[c] + state[d]; + state[b] = rotr32(state[b] ^ state[c], 12); + state[a] = state[a] + state[b] + y; + state[d] = rotr32(state[d] ^ state[a], 8); + state[c] = state[c] + state[d]; + state[b] = rotr32(state[b] ^ state[c], 7); +} + +INLINE void round_fn(uint32_t state[16], const uint32_t* msg, size_t round) +{ + // Select the message schedule based on the round. + const uint8_t* schedule = MSG_SCHEDULE[round]; + + // Mix the columns. + g(state, 0, 4, 8, 12, msg[schedule[0]], msg[schedule[1]]); + g(state, 1, 5, 9, 13, msg[schedule[2]], msg[schedule[3]]); + g(state, 2, 6, 10, 14, msg[schedule[4]], msg[schedule[5]]); + g(state, 3, 7, 11, 15, msg[schedule[6]], msg[schedule[7]]); + + // Mix the rows. + g(state, 0, 5, 10, 15, msg[schedule[8]], msg[schedule[9]]); + g(state, 1, 6, 11, 12, msg[schedule[10]], msg[schedule[11]]); + g(state, 2, 7, 8, 13, msg[schedule[12]], msg[schedule[13]]); + g(state, 3, 4, 9, 14, msg[schedule[14]], msg[schedule[15]]); +} + +INLINE void compress_pre( + uint32_t state[16], + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags) +{ + uint32_t block_words[16]; + block_words[0] = load32(block + 4 * 0); + block_words[1] = load32(block + 4 * 1); + block_words[2] = load32(block + 4 * 2); + block_words[3] = load32(block + 4 * 3); + block_words[4] = load32(block + 4 * 4); + block_words[5] = load32(block + 4 * 5); + block_words[6] = load32(block + 4 * 6); + block_words[7] = load32(block + 4 * 7); + block_words[8] = load32(block + 4 * 8); + block_words[9] = load32(block + 4 * 9); + block_words[10] = load32(block + 4 * 10); + block_words[11] = load32(block + 4 * 11); + block_words[12] = load32(block + 4 * 12); + block_words[13] = load32(block + 4 * 13); + block_words[14] = load32(block + 4 * 14); + block_words[15] = load32(block + 4 * 15); + + state[0] = cv[0]; + state[1] = cv[1]; + state[2] = cv[2]; + state[3] = cv[3]; + state[4] = cv[4]; + state[5] = cv[5]; + state[6] = cv[6]; + state[7] = cv[7]; + state[8] = IV[0]; + state[9] = IV[1]; + state[10] = IV[2]; + state[11] = IV[3]; + state[12] = counter_low(counter); + state[13] = counter_high(counter); + state[14] = (uint32_t)block_len; + state[15] = (uint32_t)flags; + + round_fn(state, &block_words[0], 0); + round_fn(state, &block_words[0], 1); + round_fn(state, &block_words[0], 2); + round_fn(state, &block_words[0], 3); + round_fn(state, &block_words[0], 4); + round_fn(state, &block_words[0], 5); + round_fn(state, &block_words[0], 6); +} + +void blake3_compress_in_place_portable( + uint32_t cv[8], const uint8_t block[BLAKE3_BLOCK_LEN], uint8_t block_len, uint64_t counter, uint8_t flags) +{ + uint32_t state[16]; + compress_pre(state, cv, block, block_len, counter, flags); + cv[0] = state[0] ^ state[8]; + cv[1] = state[1] ^ state[9]; + cv[2] = state[2] ^ state[10]; + cv[3] = state[3] ^ state[11]; + cv[4] = state[4] ^ state[12]; + cv[5] = state[5] ^ state[13]; + cv[6] = state[6] ^ state[14]; + cv[7] = state[7] ^ state[15]; +} + +void blake3_compress_xof_portable( + const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, + uint64_t counter, + uint8_t flags, + uint8_t out[64]) +{ + uint32_t state[16]; + compress_pre(state, cv, block, block_len, counter, flags); + + store32(&out[0 * 4], state[0] ^ state[8]); + store32(&out[1 * 4], state[1] ^ state[9]); + store32(&out[2 * 4], state[2] ^ state[10]); + store32(&out[3 * 4], state[3] ^ state[11]); + store32(&out[4 * 4], state[4] ^ state[12]); + store32(&out[5 * 4], state[5] ^ state[13]); + store32(&out[6 * 4], state[6] ^ state[14]); + store32(&out[7 * 4], state[7] ^ state[15]); + store32(&out[8 * 4], state[8] ^ cv[0]); + store32(&out[9 * 4], state[9] ^ cv[1]); + store32(&out[10 * 4], state[10] ^ cv[2]); + store32(&out[11 * 4], state[11] ^ cv[3]); + store32(&out[12 * 4], state[12] ^ cv[4]); + store32(&out[13 * 4], state[13] ^ cv[5]); + store32(&out[14 * 4], state[14] ^ cv[6]); + store32(&out[15 * 4], state[15] ^ cv[7]); +} + +INLINE void hash_one_portable( + const uint8_t* input, + size_t blocks, + const uint32_t key[8], + uint64_t counter, + uint8_t flags, + uint8_t flags_start, + uint8_t flags_end, + uint8_t out[BLAKE3_OUT_LEN]) +{ + uint32_t cv[8]; + memcpy(cv, key, BLAKE3_KEY_LEN); + uint8_t block_flags = flags | flags_start; + while (blocks > 0) { + if (blocks == 1) { block_flags |= flags_end; } + blake3_compress_in_place_portable(cv, input, BLAKE3_BLOCK_LEN, counter, block_flags); + input = &input[BLAKE3_BLOCK_LEN]; + blocks -= 1; + block_flags = flags; + } + store_cv_words(out, cv); +} + +void blake3_hash_many_portable( + const uint8_t* const* inputs, + size_t num_inputs, + size_t blocks, + const uint32_t key[8], + uint64_t counter, + bool increment_counter, + uint8_t flags, + uint8_t flags_start, + uint8_t flags_end, + uint8_t* out) +{ + while (num_inputs > 0) { + hash_one_portable(inputs[0], blocks, key, counter, flags, flags_start, flags_end, out); + if (increment_counter) { counter += 1; } + inputs += 1; + num_inputs -= 1; + out = &out[BLAKE3_OUT_LEN]; + } +} diff --git a/icicle/backend/cpu/src/hash/cpu_blake3.cpp b/icicle/backend/cpu/src/hash/cpu_blake3.cpp new file mode 100644 index 0000000000..9afe6674b1 --- /dev/null +++ b/icicle/backend/cpu/src/hash/cpu_blake3.cpp @@ -0,0 +1,53 @@ +/*BLAKE3 Hash function based on the original design by the BLAKE3 team https://github.com/BLAKE3-team/BLAKE3 */ + +#include "blake3.h" +#include "icicle/backend/hash/blake3_backend.h" +#include "icicle/utils/modifiers.h" +#include +#include +#include + +namespace icicle { + + class Blake3BackendCPU : public HashBackend + { + public: + Blake3BackendCPU(uint64_t input_chunk_size) : HashBackend("Blake3-CPU", BLAKE3_OUTBYTES, input_chunk_size) {} + + eIcicleError hash(const std::byte* input, uint64_t size, const HashConfig& config, std::byte* output) const override + { + const auto digest_size_in_bytes = output_size(); + const auto single_input_size = get_single_chunk_size(size); + + // Initialize the hasher + blake3_hasher hasher; + + for (unsigned batch_idx = 0; batch_idx < config.batch; ++batch_idx) { + const std::byte* batch_input = input + batch_idx * single_input_size; + uint64_t batch_size = single_input_size; + + blake3_hasher_init(&hasher); + blake3_hasher_update(&hasher, reinterpret_cast(batch_input), batch_size); + + uint8_t* batch_output = reinterpret_cast(output + batch_idx * digest_size_in_bytes); + blake3_hasher_finalize(&hasher, batch_output, digest_size_in_bytes); + } + + return eIcicleError::SUCCESS; + } + + private: + static constexpr unsigned int BLAKE3_OUTBYTES = 32; // BLAKE3 default output size in bytes + }; + + /************************ Blake3 registration ************************/ + eIcicleError + create_blake3_hash_backend(const Device& device, uint64_t input_chunk_size, std::shared_ptr& backend) + { + backend = std::make_shared(input_chunk_size); + return eIcicleError::SUCCESS; + } + + REGISTER_BLAKE3_FACTORY_BACKEND("CPU", create_blake3_hash_backend); + +} // namespace icicle \ No newline at end of file diff --git a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp index 5f664860e7..df1c519cd6 100644 --- a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp +++ b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp @@ -147,7 +147,7 @@ namespace icicle { ICICLE_LOG_ERROR << "cpu_poseidon2_init_default_constants: T (width) must be one of [2, 3, 4, 8, 12, 16, 20, 24]\n"; return eIcicleError::INVALID_ARGUMENT; - } // switch (T) { + } // switch (T) { if (full_rounds == 0 && partial_rounds == 0) { // All arrays are empty in this case. continue; } diff --git a/icicle/cmake/hash.cmake b/icicle/cmake/hash.cmake index 6d43c3e034..e8fc0d97c0 100644 --- a/icicle/cmake/hash.cmake +++ b/icicle/cmake/hash.cmake @@ -5,6 +5,7 @@ function(setup_hash_target) target_sources(icicle_hash PRIVATE src/hash/keccak.cpp src/hash/blake2s.cpp + src/hash/blake3.cpp src/hash/merkle_tree.cpp src/hash/hash_c_api.cpp src/hash/merkle_c_api.cpp diff --git a/icicle/include/icicle/backend/hash/blake3_backend.h b/icicle/include/icicle/backend/hash/blake3_backend.h new file mode 100644 index 0000000000..f9b5825946 --- /dev/null +++ b/icicle/include/icicle/backend/hash/blake3_backend.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "icicle/utils/utils.h" +#include "icicle/device.h" +#include "icicle/hash/blake3.h" + +namespace icicle { + + /*************************** Backend registration ***************************/ + using Blake3FactoryImpl = std::function& backend /*OUT*/)>; + + // Blake3 256 + void register_blake3_factory(const std::string& deviceType, Blake3FactoryImpl impl); + +#define REGISTER_BLAKE3_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ + namespace { \ + static bool UNIQUE(_reg_blake3) = []() -> bool { \ + register_blake3_factory(DEVICE_TYPE, FUNC); \ + return true; \ + }(); \ + } + +} // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 784298cac9..6478e915c6 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -241,7 +241,7 @@ class QuarticExtensionField FF::reduce( (CONFIG::nonresidue_is_negative ? (FF::mul_wide(xs.real, x0) + FF::template mul_unsigned(FF::mul_wide(xs.im2, x2))) - : (FF::mul_wide(xs.real, x0)) - FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), + : (FF::mul_wide(xs.real, x0))-FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), FF::reduce( (CONFIG::nonresidue_is_negative ? FWide::neg(FF::template mul_unsigned(FF::mul_wide(xs.im3, x2))) diff --git a/icicle/include/icicle/fields/storage.h b/icicle/include/icicle/fields/storage.h index 76245db166..097db881fd 100644 --- a/icicle/include/icicle/fields/storage.h +++ b/icicle/include/icicle/fields/storage.h @@ -16,8 +16,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<1> -{ + storage<1> { static constexpr unsigned LC = 1; uint32_t limbs[1]; }; @@ -28,8 +27,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<3> -{ + storage<3> { static constexpr unsigned LC = 3; uint32_t limbs[3]; }; @@ -40,8 +38,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage -{ + storage { static_assert(LIMBS_COUNT % 2 == 0, "odd number of limbs is not supported\n"); static constexpr unsigned LC = LIMBS_COUNT; union { // works only with even LIMBS_COUNT @@ -55,7 +52,6 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage_array -{ + storage_array { storage storages[OMEGAS_COUNT]; }; \ No newline at end of file diff --git a/icicle/include/icicle/hash/blake3.h b/icicle/include/icicle/hash/blake3.h new file mode 100644 index 0000000000..089122572d --- /dev/null +++ b/icicle/include/icicle/hash/blake3.h @@ -0,0 +1,20 @@ +#pragma once + +#include "icicle/hash/hash.h" + +namespace icicle { + + /** + * @brief Creates a Blake3 hash object. + * + * This function constructs a Hash object configured for Blake3, with the + * appropriate backend selected based on the current device. + * + * @param input_chunk_size size of input in bytes for the Blake3 hash. + * @return Hash object encapsulating the Blake3 backend. + */ + Hash create_blake3_hash(uint64_t input_chunk_size = 0); + struct Blake3 { + inline static Hash create(uint64_t input_chunk_size = 0) { return create_blake3_hash(input_chunk_size); } + }; +} // namespace icicle \ No newline at end of file diff --git a/icicle/src/hash/blake3.cpp b/icicle/src/hash/blake3.cpp new file mode 100644 index 0000000000..3279fb0155 --- /dev/null +++ b/icicle/src/hash/blake3.cpp @@ -0,0 +1,17 @@ +#include "icicle/errors.h" +#include "icicle/backend/hash/blake3_backend.h" +#include "icicle/dispatcher.h" + +namespace icicle { + + // Blake3 + ICICLE_DISPATCHER_INST(Blake3Dispatcher, blake3_factory, Blake3FactoryImpl); + + Hash create_blake3_hash(uint64_t input_chunk_size) + { + std::shared_ptr backend; + ICICLE_CHECK(Blake3Dispatcher::execute(input_chunk_size, backend)); + Hash blake3{backend}; + return blake3; + } +} // namespace icicle \ No newline at end of file diff --git a/icicle/src/hash/hash_c_api.cpp b/icicle/src/hash/hash_c_api.cpp index 460b83d17c..820af1545f 100644 --- a/icicle/src/hash/hash_c_api.cpp +++ b/icicle/src/hash/hash_c_api.cpp @@ -3,6 +3,7 @@ #include "icicle/errors.h" #include "icicle/hash/keccak.h" #include "icicle/hash/blake2s.h" +#include "icicle/hash/blake3.h" extern "C" { // Define a type for the HasherHandle (which is a pointer to Hash) @@ -123,4 +124,17 @@ HasherHandle icicle_create_blake2s(uint64_t input_chunk_size) { return new icicle::Hash(icicle::create_blake2s_hash(input_chunk_size)); } + +/** + * @brief Creates a Blake3 hash object. + * + * This function constructs a Hash object configured for Blake2s. + * + * @param input_chunk_size Size of the input in bytes for the Blake2s hash. + * @return HasherHandle A handle to the created Blake2s Hash object. + */ +HasherHandle icicle_create_blake3(uint64_t input_chunk_size) +{ + return new icicle::Hash(icicle::create_blake3_hash(input_chunk_size)); +} } \ No newline at end of file diff --git a/icicle/tests/test_hash_api.cpp b/icicle/tests/test_hash_api.cpp index c04aaecebf..860e42e358 100644 --- a/icicle/tests/test_hash_api.cpp +++ b/icicle/tests/test_hash_api.cpp @@ -7,6 +7,7 @@ #include "icicle/hash/hash.h" #include "icicle/hash/keccak.h" #include "icicle/hash/blake2s.h" +#include "icicle/hash/blake3.h" #include "icicle/merkle/merkle_tree.h" #include "icicle/fields/field.h" @@ -93,6 +94,30 @@ TEST_F(HashApiTest, Blake2s) } } +TEST_F(HashApiTest, Blake3) +{ + // TODO: Add CUDA test, same as blake2s + auto config = default_hash_config(); + + const std::string input = + "Hello world I am blake3. This is a semi-long C++ test with a lot of characters. " + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const std::string expected_output = "4b71f2c5cb7c26da2ba67cc742228e55b66c8b64b2b250e7ccce6f7f6d17c9ae"; + + const uint64_t output_size = 32; + auto output = std::make_unique(output_size); + for (const auto& device : s_registered_devices) { + ICICLE_LOG_DEBUG << "Blake2s test on device=" << device; + ICICLE_CHECK(icicle_set_device("CPU")); + + auto blake3 = Blake3::create(); + ICICLE_CHECK(blake3.hash(input.data(), input.size() / config.batch, config, output.get())); + // Convert the output do a hex string and compare to expected output string + std::string output_as_str = voidPtrToHexString(output.get(), output_size); + ASSERT_EQ(output_as_str, expected_output); + } +} + TEST_F(HashApiTest, Keccak256Batch) { auto config = default_hash_config(); @@ -214,6 +239,54 @@ TEST_F(HashApiTest, Blake2sLarge) ICICLE_CHECK(icicle_free(d_output)); } +TEST_F(HashApiTest, Blake3Large) +{ + auto config = default_hash_config(); + config.batch = 1 << 8; + const unsigned chunk_size = 1 << 11; // 2KB chunks + const unsigned total_size = chunk_size * config.batch; + auto input = std::make_unique(total_size); + randomize((uint64_t*)input.get(), total_size / sizeof(uint64_t)); + + const uint64_t output_size = 32; + auto output_main = std::make_unique(output_size * config.batch); + auto output_main_case_2 = std::make_unique(output_size * config.batch); + auto output_ref = std::make_unique(output_size * config.batch); + + ICICLE_CHECK(icicle_set_device(IcicleTestBase::reference_device())); + auto blake3CPU = Blake3::create(); + START_TIMER(cpu_timer); + ICICLE_CHECK(blake3CPU.hash(input.get(), chunk_size, config, output_ref.get())); + END_TIMER(cpu_timer, "CPU blake3 large time", true); + + ICICLE_CHECK(icicle_set_device(IcicleTestBase::main_device())); + auto blake3MainDev = Blake3::create(); + + // test with host memory + START_TIMER(mainDev_timer); + config.are_inputs_on_device = false; + config.are_outputs_on_device = false; + ICICLE_CHECK(blake3MainDev.hash(input.get(), chunk_size, config, output_main.get())); + END_TIMER(mainDev_timer, "MainDev blake3 large time (on host memory)", true); + ASSERT_EQ(0, memcmp(output_main.get(), output_ref.get(), output_size * config.batch)); + + // test with device memory + std::byte *d_input = nullptr, *d_output = nullptr; + ICICLE_CHECK(icicle_malloc((void**)&d_input, total_size)); + ICICLE_CHECK(icicle_malloc((void**)&d_output, output_size * config.batch)); + ICICLE_CHECK(icicle_copy(d_input, input.get(), total_size)); + config.are_inputs_on_device = true; + config.are_outputs_on_device = true; + START_TIMER(mainDev_timer_device_mem); + ICICLE_CHECK(blake3MainDev.hash(d_input, chunk_size, config, d_output)); + END_TIMER(mainDev_timer_device_mem, "MainDev blake3 large time (on device memory)", true); + ICICLE_CHECK(icicle_copy(output_main_case_2.get(), d_output, output_size * config.batch)); + ASSERT_EQ(0, memcmp(output_main_case_2.get(), output_ref.get(), output_size * config.batch)); + + ICICLE_CHECK(icicle_free(d_input)); + ICICLE_CHECK(icicle_free(d_output)); +} + TEST_F(HashApiTest, sha3) { auto config = default_hash_config(); diff --git a/wrappers/golang/hash/blake3.go b/wrappers/golang/hash/blake3.go new file mode 100644 index 0000000000..bd985c2f3b --- /dev/null +++ b/wrappers/golang/hash/blake3.go @@ -0,0 +1,19 @@ +package hash + +// #cgo CFLAGS: -I./include/ +// #include "blake3.h" +import "C" +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func NewBlake3Hasher(inputChunkSize uint64) (Hasher, runtime.EIcicleError) { + h := C.icicle_create_blake3((C.ulong)(inputChunkSize)) + if h == nil { + return Hasher{handle: nil}, runtime.UnknownError + } + + return Hasher{ + handle: h, + }, runtime.Success +} diff --git a/wrappers/golang/hash/include/blake3.h b/wrappers/golang/hash/include/blake3.h new file mode 100644 index 0000000000..f855f15b3c --- /dev/null +++ b/wrappers/golang/hash/include/blake3.h @@ -0,0 +1,17 @@ +#include +#include "hash.h" + +#ifndef _BLAKE3_HASH + #define _BLAKE3_HASH + + #ifdef __cplusplus +extern "C" { + #endif + +Hash* icicle_create_blake3(uint64_t default_input_chunk_size); + + #ifdef __cplusplus +} + #endif + +#endif diff --git a/wrappers/golang/hash/tests/hash_test.go b/wrappers/golang/hash/tests/hash_test.go index dbd32786b7..f424d7394d 100644 --- a/wrappers/golang/hash/tests/hash_test.go +++ b/wrappers/golang/hash/tests/hash_test.go @@ -99,6 +99,77 @@ func testBlake2s(s *suite.Suite) { s.NotEqual(outputEmpty, outputMain) } +func testBlake3_cpu_gpu(s *suite.Suite) { + singleHashInputSize := 567 + batch := 11 + const outputBytes = 32 // 32 bytes is output size of Blake3 + + input := make([]byte, singleHashInputSize*batch) + _, err := rand.Read(input) + if err != nil { + fmt.Println("error:", err) + return + } + + Blake3Hasher, error := hash.NewBlake3Hasher(0 /*default chunk size*/) + if error != runtime.Success { + fmt.Println("error:", error) + return + } + + outputRef := make([]byte, outputBytes*batch) + Blake3Hasher.Hash( + core.HostSliceFromElements(input), + core.HostSliceFromElements(outputRef), + core.GetDefaultHashConfig(), + ) + + runtime.SetDevice(&devices[1]) + Blake3Hasher, error = hash.NewBlake3Hasher(0 /*default chunk size*/) + if error != runtime.Success { + fmt.Println("error:", error) + return + } + + outputMain := make([]byte, outputBytes*batch) + Blake3Hasher.Hash( + core.HostSliceFromElements(input), + core.HostSliceFromElements(outputMain), + core.GetDefaultHashConfig(), + ) + + outputEmpty := make([]byte, outputBytes*batch) + s.Equal(outputRef, outputMain) + s.NotEqual(outputEmpty, outputMain) +} + +func testBlake3(s *suite.Suite) { + const outputBytes = 32 // 32 bytes is output size of Blake3 + + // Known input string and expected hash + inputString := "Hello world I am blake3. This is a semi-long Go test with a lot of characters. 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + expectedHash := "a2b794acb5a604bbd2c4c0380e935697e0b934ea6f194b9f5246fbb212ebe549" + + input := []byte(inputString) + + Blake3Hasher, error := hash.NewBlake3Hasher(0 /*default chunk size*/) + if error != runtime.Success { + fmt.Println("error:", error) + return + } + + outputRef := make([]byte, outputBytes) + Blake3Hasher.Hash( + core.HostSliceFromElements(input), + core.HostSliceFromElements(outputRef), + core.GetDefaultHashConfig(), + ) + + outputRefHex := fmt.Sprintf("%x", outputRef) + + s.Equal(expectedHash, outputRefHex, "Hash mismatch: got %s, expected %s", outputRefHex, expectedHash) +} + func testSha3(s *suite.Suite) { singleHashInputSize := 1153 batch := 1 @@ -150,6 +221,8 @@ type HashTestSuite struct { func (s *HashTestSuite) TestHash() { s.Run("TestKeccakBatch", testWrapper(&s.Suite, testKeccakBatch)) s.Run("TestBlake2s", testWrapper(&s.Suite, testBlake2s)) + s.Run("TestBlake3_CPU_GPU", testWrapper(&s.Suite, testBlake3_cpu_gpu)) + s.Run("TestBlake3", testWrapper(&s.Suite, testBlake3)) s.Run("TestSha3", testWrapper(&s.Suite, testSha3)) } diff --git a/wrappers/rust/icicle-hash/src/blake3.rs b/wrappers/rust/icicle-hash/src/blake3.rs new file mode 100644 index 0000000000..923be6de22 --- /dev/null +++ b/wrappers/rust/icicle-hash/src/blake3.rs @@ -0,0 +1,18 @@ +use icicle_core::hash::{Hasher, HasherHandle}; +use icicle_runtime::errors::eIcicleError; + +extern "C" { + fn icicle_create_blake3(default_input_chunk_size: u64) -> HasherHandle; +} + +pub struct Blake3; + +impl Blake3 { + pub fn new(default_input_chunk_size: u64) -> Result { + let handle: HasherHandle = unsafe { icicle_create_blake3(default_input_chunk_size) }; + if handle.is_null() { + return Err(eIcicleError::UnknownError); + } + Ok(Hasher::from_handle(handle)) + } +} diff --git a/wrappers/rust/icicle-hash/src/lib.rs b/wrappers/rust/icicle-hash/src/lib.rs index 67bf4dd183..df84faa31c 100644 --- a/wrappers/rust/icicle-hash/src/lib.rs +++ b/wrappers/rust/icicle-hash/src/lib.rs @@ -1,4 +1,5 @@ pub mod blake2s; +pub mod blake3; pub mod keccak; pub mod sha3; diff --git a/wrappers/rust/icicle-hash/src/tests.rs b/wrappers/rust/icicle-hash/src/tests.rs index 3618185b08..78f223d81b 100644 --- a/wrappers/rust/icicle-hash/src/tests.rs +++ b/wrappers/rust/icicle-hash/src/tests.rs @@ -3,6 +3,7 @@ mod tests { use crate::{ blake2s::Blake2s, + blake3::Blake3, keccak::{Keccak256, Keccak512}, sha3::Sha3_256, }; @@ -88,6 +89,72 @@ mod tests { assert_eq!(output_ref, output_main); } + #[test] + fn blake3_hashing_cpu_gpu() { + initialize(); + let single_hash_input_size = 567; + let batch = 11; + + let mut input = vec![0 as u8; single_hash_input_size * batch]; + rand::thread_rng().fill(&mut input[..]); + let mut output_ref = vec![0 as u8; 32 * batch]; // 32B (=256b) is the output size of blake3 + let mut output_main = vec![0 as u8; 32 * batch]; + + test_utilities::test_set_ref_device(); + let blake3_hasher = Blake3::new(0 /*default chunk size */).unwrap(); + blake3_hasher + .hash( + HostSlice::from_slice(&input), + &HashConfig::default(), + HostSlice::from_mut_slice(&mut output_ref), + ) + .unwrap(); + + test_utilities::test_set_main_device(); + let blake3_hasher = Blake3::new(0 /*default chunk size */).unwrap(); + blake3_hasher + .hash( + HostSlice::from_slice(&input), + &HashConfig::default(), + HostSlice::from_mut_slice(&mut output_main), + ) + .unwrap(); + assert_eq!(output_ref, output_main); + } + + #[test] + fn blake3_hashing() { + // Known input string and expected hash + let input_string = "Hello world I am blake3. This is a semi-long Rust test with a lot of characters. 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let expected_hash = "ee4941ff90437a4fd7489ffa6d559e644a68b2547e95a690949b902da128b273"; + + let input = input_string.as_bytes(); + let mut output_ref = vec![0u8; 32]; // 32B (=256b) is the output size of blake3 + + test_utilities::test_set_ref_device(); + let blake3_hasher = Blake3::new(0 /*default chunk size */).unwrap(); + blake3_hasher + .hash( + HostSlice::from_slice(&input), + &HashConfig::default(), + HostSlice::from_mut_slice(&mut output_ref), + ) + .unwrap(); + + // Convert output_ref to hex for comparison + let output_ref_hex: String = output_ref + .iter() + .map(|b| format!("{:02x}", b)) + .collect(); + assert_eq!( + output_ref_hex, expected_hash, + "Hash mismatch: got {}, expected {}", + output_ref_hex, expected_hash + ); + + println!("Test passed: Computed hash matches expected hash."); + } + #[test] fn sha3_hashing() { initialize(); From be8cd4ccddf993d2eb377a3e3830c47389c6d61b Mon Sep 17 00:00:00 2001 From: release-bot Date: Tue, 14 Jan 2025 15:00:58 +0000 Subject: [PATCH 065/127] Bump rust crates' version icicle-babybear@3.4.0 icicle-bls12-377@3.4.0 icicle-bls12-381@3.4.0 icicle-bn254@3.4.0 icicle-bw6-761@3.4.0 icicle-core@3.4.0 icicle-grumpkin@3.4.0 icicle-hash@3.4.0 icicle-koalabear@3.4.0 icicle-m31@3.4.0 icicle-runtime@3.4.0 icicle-stark252@3.4.0 Generated by cargo-workspaces --- wrappers/rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrappers/rust/Cargo.toml b/wrappers/rust/Cargo.toml index a1c705e56a..9495b8ca73 100644 --- a/wrappers/rust/Cargo.toml +++ b/wrappers/rust/Cargo.toml @@ -17,7 +17,7 @@ members = [ exclude = [] [workspace.package] -version = "3.3.0" +version = "3.4.0" edition = "2021" authors = [ "Ingonyama" ] homepage = "https://www.ingonyama.com" From 7f98ad6dcb375e2c621b8b47fecc0f39aa747667 Mon Sep 17 00:00:00 2001 From: release-bot Date: Tue, 14 Jan 2025 15:02:16 +0000 Subject: [PATCH 066/127] Bump docs version --- .../version-3.4.0/contributor-guide.md | 23 ++ docs/versioned_docs/version-3.4.0/grants.md | 23 ++ .../version-3.4.0/icicle/arch_overview.md | 27 ++ .../version-3.4.0/icicle/benchmarks.md | 3 + .../version-3.4.0/icicle/build_from_source.md | 179 +++++++++ .../icicle/build_your_own_backend.md | 3 + .../icicle/colab-instructions.md | 162 ++++++++ .../icicle/faq_and_troubleshooting.md | 9 + .../version-3.4.0/icicle/getting_started.md | 170 ++++++++ .../version-3.4.0/icicle/golang-bindings.md | 126 ++++++ .../icicle/golang-bindings/ecntt.md | 100 +++++ .../icicle/golang-bindings/hash.md | 111 ++++++ .../icicle/golang-bindings/merkle.md | 238 +++++++++++ .../golang-bindings/msm-pre-computation.md | 110 +++++ .../icicle/golang-bindings/msm.md | 205 ++++++++++ .../icicle/golang-bindings/multi-gpu.md | 161 ++++++++ .../icicle/golang-bindings/ntt.md | 140 +++++++ .../icicle/golang-bindings/vec-ops.md | 197 +++++++++ .../version-3.4.0/icicle/image.png | Bin 0 -> 35743 bytes .../icicle/install_cuda_backend.md | 30 ++ .../version-3.4.0/icicle/integrations.md | 97 +++++ .../version-3.4.0/icicle/libraries.md | 70 ++++ .../version-3.4.0/icicle/migrate_from_v2.md | 93 +++++ .../version-3.4.0/icicle/multi-device.md | 79 ++++ .../version-3.4.0/icicle/overview.md | 84 ++++ .../version-3.4.0/icicle/polynomials/ffi.uml | 27 ++ .../icicle/polynomials/hw_backends.uml | 86 ++++ .../icicle/polynomials/overview.md | 376 ++++++++++++++++++ .../primitives/Icicle_Release_README.md | 89 +++++ .../version-3.4.0/icicle/primitives/hash.md | 187 +++++++++ .../icicle/primitives/image-1.png | Bin 0 -> 225713 bytes .../icicle/primitives/image-2.png | Bin 0 -> 220588 bytes .../icicle/primitives/image-3.png | Bin 0 -> 329359 bytes .../version-3.4.0/icicle/primitives/image.png | Bin 0 -> 115380 bytes .../version-3.4.0/icicle/primitives/merkle.md | 269 +++++++++++++ .../primitives/merkle_diagrams/diagram1.gv | 115 ++++++ .../primitives/merkle_diagrams/diagram1.png | Bin 0 -> 80489 bytes .../merkle_diagrams/diagram1_path.gv | 117 ++++++ .../merkle_diagrams/diagram1_path.png | Bin 0 -> 73224 bytes .../merkle_diagrams/diagram1_path_full.gv | 113 ++++++ .../merkle_diagrams/diagram1_path_full.png | Bin 0 -> 76821 bytes .../primitives/merkle_diagrams/diagram2.gv | 89 +++++ .../primitives/merkle_diagrams/diagram2.png | Bin 0 -> 63262 bytes .../version-3.4.0/icicle/primitives/msm.md | 203 ++++++++++ .../version-3.4.0/icicle/primitives/ntt.md | 322 +++++++++++++++ .../icicle/primitives/overview.md | 12 + .../icicle/primitives/poseidon.md | 218 ++++++++++ .../icicle/primitives/poseidon2.md | 180 +++++++++ .../icicle/primitives/program.md | 88 ++++ .../icicle/primitives/vec_ops.md | 229 +++++++++++ .../icicle/programmers_guide/cpp.md | 315 +++++++++++++++ .../icicle/programmers_guide/general.md | 113 ++++++ .../icicle/programmers_guide/go.md | 309 ++++++++++++++ .../icicle/programmers_guide/rust.md | 262 ++++++++++++ .../version-3.4.0/icicle/rust-bindings.md | 34 ++ .../icicle/rust-bindings/ecntt.md | 25 ++ .../icicle/rust-bindings/hash.md | 110 +++++ .../icicle/rust-bindings/merkle.md | 277 +++++++++++++ .../version-3.4.0/icicle/rust-bindings/msm.md | 117 ++++++ .../icicle/rust-bindings/multi-gpu.md | 204 ++++++++++ .../version-3.4.0/icicle/rust-bindings/ntt.md | 106 +++++ .../icicle/rust-bindings/polynomials.md | 284 +++++++++++++ .../icicle/rust-bindings/vec-ops.md | 109 +++++ .../version-3.4.0/introduction.md | 42 ++ .../version-3.4.0-sidebars.json | 295 ++++++++++++++ docs/versions.json | 1 + 66 files changed, 7763 insertions(+) create mode 100644 docs/versioned_docs/version-3.4.0/contributor-guide.md create mode 100644 docs/versioned_docs/version-3.4.0/grants.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/arch_overview.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/benchmarks.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/build_from_source.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/build_your_own_backend.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/colab-instructions.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/faq_and_troubleshooting.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/getting_started.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ecntt.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/hash.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/merkle.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm-pre-computation.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/multi-gpu.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ntt.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/golang-bindings/vec-ops.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/image.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/install_cuda_backend.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/integrations.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/libraries.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/migrate_from_v2.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/multi-device.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/overview.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/polynomials/ffi.uml create mode 100644 docs/versioned_docs/version-3.4.0/icicle/polynomials/hw_backends.uml create mode 100644 docs/versioned_docs/version-3.4.0/icicle/polynomials/overview.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/Icicle_Release_README.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/hash.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/image-1.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/image-2.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/image-3.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/image.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.gv create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.gv create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path_full.gv create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path_full.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram2.gv create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram2.png create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/msm.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/ntt.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/overview.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/program.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/vec_ops.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/programmers_guide/cpp.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/programmers_guide/general.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/programmers_guide/go.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/programmers_guide/rust.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ecntt.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/hash.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/merkle.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/msm.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/multi-gpu.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ntt.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/polynomials.md create mode 100644 docs/versioned_docs/version-3.4.0/icicle/rust-bindings/vec-ops.md create mode 100644 docs/versioned_docs/version-3.4.0/introduction.md create mode 100644 docs/versioned_sidebars/version-3.4.0-sidebars.json diff --git a/docs/versioned_docs/version-3.4.0/contributor-guide.md b/docs/versioned_docs/version-3.4.0/contributor-guide.md new file mode 100644 index 0000000000..ffd82cd9c7 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/contributor-guide.md @@ -0,0 +1,23 @@ +# Contributor's Guide + +We welcome all contributions with open arms. At Ingonyama we take a village approach, believing it takes many hands and minds to build a ecosystem. + +## Contributing to ICICLE + +- Make suggestions or report bugs via [GitHub issues](https://github.com/ingonyama-zk/icicle/issues) +- Contribute to the ICICLE by opening a [pull request](https://github.com/ingonyama-zk/icicle/pulls). +- Contribute to our [documentation](https://github.com/ingonyama-zk/icicle/tree/main/docs) and [examples](https://github.com/ingonyama-zk/icicle/tree/main/examples). +- Ask questions on Discord + +### Opening a pull request + +When opening a [pull request](https://github.com/ingonyama-zk/icicle/pulls) please keep the following in mind. + +- `Clear Purpose` - The pull request should solve a single issue and be clean of any unrelated changes. +- `Clear description` - If the pull request is for a new feature describe what you built, why you added it and how its best that we test it. For bug fixes please describe the issue and the solution. +- `Consistent style` - Rust and Golang code should be linted by the official linters (golang fmt and rust fmt) and maintain a proper style. For CUDA and C++ code we use [`clang-format`](https://github.com/ingonyama-zk/icicle/blob/main/.clang-format), [here](https://github.com/ingonyama-zk/icicle/blob/605c25f9d22135c54ac49683b710fe2ce06e2300/.github/workflows/main-format.yml#L46) you can see how we run it. +- `Minimal Tests` - please add test which cover basic usage of your changes . + +## Questions? + +Find us on [Discord](https://discord.gg/6vYrE7waPj). diff --git a/docs/versioned_docs/version-3.4.0/grants.md b/docs/versioned_docs/version-3.4.0/grants.md new file mode 100644 index 0000000000..1647d5400a --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/grants.md @@ -0,0 +1,23 @@ +# Ingonyama Grant programs + +Ingonyama understands the importance of supporting and fostering a vibrant community of researchers and builders to advance ZK. To encourage progress, we are not only developing in the open but also sharing resources with researchers and builders through various programs. + +## ICICLE ZK-GPU Ecosystem Grant + +Ingonyama invites researchers and practitioners to collaborate in advancing ZK acceleration. We are allocating $100,000 for grants to support this initiative. + +### Bounties & Grants + +Eligibility for grants includes: + +1. **Students**: Utilize ICICLE in your research. +2. **Performance Improvement**: Enhance the performance of accelerated primitives in ICICLE. +3. **Protocol Porting**: Migrate existing ZK protocols to ICICLE. +4. **New Primitives**: Contribute new primitives to ICICLE. +5. **Benchmarking**: Compare ZK benchmarks against ICICLE. + +## Contact + +For questions or submissions: [grants@ingonyama.com](mailto:grants@ingonyama.com) + +**Read the full article [here](https://www.ingonyama.com/blog/icicle-for-researchers-grants-challenges)** diff --git a/docs/versioned_docs/version-3.4.0/icicle/arch_overview.md b/docs/versioned_docs/version-3.4.0/icicle/arch_overview.md new file mode 100644 index 0000000000..a45f96a9b9 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/arch_overview.md @@ -0,0 +1,27 @@ +# Architecture Overview + + +ICICLE v3 is designed with flexibility and extensibility in mind, offering a robust framework that supports multiple compute backends and accommodates various cryptographic needs. This section provides an overview of ICICLE's architecture, highlighting its open and closed components, multi-device support, and extensibility. + +### Frontend and CPU Backend + +- **Frontend (FE):** The ICICLE frontend is open-source and designed to provide a unified API across different programming languages, including C++, Rust, and Go. This frontend abstracts the complexity of working with different backends, allowing developers to write backend-agnostic code that can be deployed across various platforms. +- **CPU Backend:** ICICLE includes an open-source CPU backend that allows for development and testing on standard hardware. This backend is ideal for prototyping and for environments where specialized hardware is not available. + +## CUDA Backend + +- **CUDA Backend:** ICICLE also includes a high-performance CUDA backend that is closed-source. This backend is optimized for NVIDIA GPUs and provides significant acceleration for cryptographic operations. +- **Installation and Licensing:** The CUDA backend needs to be downloaded and installed. Refer to the [installation guide](./install_cuda_backend.md) for detailed instructions. + +## Multi-Device Support + +- **Scalability:** ICICLE supports multi-device configurations, enabling the distribution of workloads across multiple GPUs or other hardware accelerators. This feature allows for scaling ZK proofs and other cryptographic operations across larger data centers or high-performance computing environments. + + +## Build Your Own Backend + +ICICLE is designed to be extensible, allowing developers to integrate new backends or customize existing ones to suit their specific needs. The architecture supports: + +- **Custom Backends:** Developers can create their own backends to leverage different hardware or optimize for specific use cases. The process of building and integrating a custom backend is documented in the [Build Your Own Backend](./build_your_own_backend.md) section. +- **Pluggable Components:** ICICLE's architecture allows for easy integration of additional cryptographic primitives or enhancements, ensuring that the framework can evolve with the latest advancements in cryptography and hardware acceleration. + diff --git a/docs/versioned_docs/version-3.4.0/icicle/benchmarks.md b/docs/versioned_docs/version-3.4.0/icicle/benchmarks.md new file mode 100644 index 0000000000..4a9ee6a08f --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/benchmarks.md @@ -0,0 +1,3 @@ +# Benchmarks + +TODO \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/build_from_source.md b/docs/versioned_docs/version-3.4.0/icicle/build_from_source.md new file mode 100644 index 0000000000..15eb01ffda --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/build_from_source.md @@ -0,0 +1,179 @@ + +# Build ICICLE from source + +This guide will help you get started with building, testing, and installing ICICLE, whether you're using C++, Rust, or Go. It also covers installation of the CUDA backend and important build options. + +## Building and Testing ICICLE frontend + +### C++: Build, Test, and Install (Frontend) + +ICICLE can be built and tested in C++ using CMake. The build process is straightforward, but there are several flags you can use to customize the build for your needs. + +#### Build Commands + +1. **Clone the ICICLE repository:** + ```bash + git clone https://github.com/ingonyama-zk/icicle.git + cd icicle + ``` + +2. **Configure the build:** + ```bash + mkdir -p build && rm -rf build/* + cmake -S icicle -B build -DFIELD=babybear + ``` + +:::info +To specify the field, use the flag -DFIELD=field, where field can be one of the following: babybear, stark252, m31, koalabear. + +To specify a curve, use the flag -DCURVE=curve, where curve can be one of the following: bn254, bls12_377, bls12_381, bw6_761, grumpkin. +::: + +:::tip +If you have access to cuda backend repo, it can be built along ICICLE frontend by adding the following to the cmake command +- `-DCUDA_BACKEND=local` # if you have it locally +- `-DCUDA_BACKEND=` # to pull CUDA backend, given you have access +::: + +3. **Build the project:** + ```bash + cmake --build build -j + ``` + This is building the [libicicle_device](./libraries.md#icicle-device) and the [libicicle_field_babybear](./libraries.md#icicle-core) frontend lib that correspond to the field or curve. + +4. **Link:** +Link you application (or library) to ICICLE: +```cmake +target_link_libraries(yourApp PRIVATE icicle_field_babybear icicle_device) +``` + +5. **Installation (optional):** +To install the libs, specify the install prefix in the [cmake command](./build_from_source.md#build-commands) +`-DCMAKE_INSTALL_PREFIX=/install/dir/`. Default install path on linux is `/usr/local` if not specified. For other systems it may differ. The cmake command will print it to the log +``` +-- CMAKE_INSTALL_PREFIX=/install/dir/for/cmake/install +``` +Then after building, use cmake to install the libraries: +``` +cmake -S icicle -B build -DFIELD=babybear -DCMAKE_INSTALL_PREFIX=/path/to/install/dir/ +cmake --build build -j # build +cmake --install build # install icicle to /path/to/install/dir/ +``` + +6. **Run tests (optional):** +Add `-DBUILD_TESTS=ON` to the [cmake command](./build_from_source.md#build-commands) and build. +Execute all tests +```bash +cmake -S icicle -B build -DFIELD=babybear -DBUILD_TESTS=ON +cmake --build build -j +cd build/tests +ctest +``` +or choose the test-suite +```bash +./build/tests/test_field_api # or another test suite +# can specify tests using regex. For example for tests with ntt in the name: +./build/tests/test_field_api --gtest_filter="*ntt*" +``` +:::note +Most tests assume a cuda backend exists and will fail otherwise if cannot find a CUDA device. +::: + +#### Build Flags + +You can customize your ICICLE build with the following flags: + +- `-DCPU_BACKEND=ON/OFF`: Enable or disable built-in CPU backend. `default=ON`. +- `-DCMAKE_INSTALL_PREFIX=/install/dir`: Specify install directory. `default=/usr/local`. +- `-DBUILD_TESTS=ON/OFF`: Enable or disable tests. `default=OFF`. +- `-DBUILD_BENCHMARKS=ON/OFF`: Enable or disable benchmarks. `default=OFF`. + +#### Features + +By default, all [features](./libraries.md#supported-curves-and-operations) are enabled. +This is since installed backends may implement and register all APIs. Missing APIs in the frontend would cause linkage to fail due to missing symbols. Therefore by default we include them in the frontend part too. + +To disable features, add the following to the cmake command. +- ntt: `-DNTT=OFF` +- msm: `-DMSM=OFF` +- g2 msm: `-DG2=OFF` +- ecntt: `-DECNTT=OFF` +- extension field: `-DEXT_FIELD=OFF` + +:::tip +Disabling features is useful when developing with a backend that is slow to compile (e.g. CUDA backend); +::: + +### Rust: Build, Test, and Install + +To build and test ICICLE in Rust, follow these steps: + +1. **Navigate to the Rust bindings directory:** +```bash +cd wrappers/rust # or go to a specific field/curve 'cd wrappers/rust/icicle-fields/icicle-babybear' +``` + +2. **Build the Rust project:** +```bash +cargo build --release +``` +By default, all [supported features are enabled](#features). +Cargo features are used to disable features, rather than enable them, for the reason explained [here](#features): +- `no_g2` to disable G2 MSM +- `no_ecntt` to disable ECNTT + +They can be disabled as follows: +```bash +cargo build --release --features=no_ecntt,no_g2 +``` + +:::note +If you have access to cuda backend repo, it can be built along ICICLE frontend by using the following cargo features: +- `cuda_backend` : if the cuda backend resides in `icicle/backend/cuda` +- `pull_cuda_backend` : to pull main branch and build it +::: + + +3. **Run tests:** +```bash +cargo test # optional: --features=no_ecntt,no_g2,cuda_backend +``` +:::note +Most tests assume a CUDA backend is installed and fail otherwise. +::: + +4. **Install the library:** + +By default, the libraries are installed to the `target//deps/icicle` dir. If you want them installed elsewhere, define the env variable: +```bash +export ICICLE_INSTALL_DIR=/path/to/install/dir +``` + +#### Use as cargo dependency +In cargo.toml, specify the ICICLE libs to use: + +```bash +[dependencies] +icicle-runtime = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +icicle-core = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +icicle-babybear = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +# add other ICICLE crates here if need additional fields/curves +``` + +Can specify `branch = ` or `tag = ` or `rev = `. + +To disable features: +```bash +icicle-bls12-377 = { git = "https://github.com/ingonyama-zk/icicle.git", features = ["no_g2"] } +``` + +As explained above, the libs will be built and installed to `target//deps/icicle` so you can easily link to them. Alternatively you can set `ICICLE_INSTALL_DIR` env variable for a custom install directory. + +:::warning +Make sure to install icicle libs when installing a library/application that depends on icicle such that it is located at runtime. +::: + +### Go: Build, Test, and Install (TODO) + +--- +**To install CUDA backend and license click [here](./install_cuda_backend.md#installation)** diff --git a/docs/versioned_docs/version-3.4.0/icicle/build_your_own_backend.md b/docs/versioned_docs/version-3.4.0/icicle/build_your_own_backend.md new file mode 100644 index 0000000000..5cb2fc52a4 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/build_your_own_backend.md @@ -0,0 +1,3 @@ +# Build Your Own Backend + +TODO \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/colab-instructions.md b/docs/versioned_docs/version-3.4.0/icicle/colab-instructions.md new file mode 100644 index 0000000000..5ed7443624 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/colab-instructions.md @@ -0,0 +1,162 @@ +# Run ICICLE on Google Colab + +Google Colab lets you use a GPU free of charge, it's an Nvidia T4 GPU with 16 GB of memory, capable of running latest CUDA (tested on Cuda 12.2) +As Colab is able to interact with shell commands, a user can also install a framework and load git repositories into Colab space. + +## Prepare Colab environment + +First thing to do in a notebook is to set the runtime type to a T4 GPU. + +- in the upper corner click on the dropdown menu and select "change runtime type" + +![Change runtime](/img/colab_change_runtime.png) + +- In the window select "T4 GPU" and press Save + +![T4 GPU](/img/t4_gpu.png) + +Installing Rust is rather simple, just execute the following command: + +```sh +!apt install rustc cargo +``` + +To test the installation of Rust: + +```sh +!rustc --version +!cargo --version +``` + +A successful installation will result in a rustc and cargo version print, a faulty installation will look like this: + +```sh +/bin/bash: line 1: rustc: command not found +/bin/bash: line 1: cargo: command not found +``` + +Now we will check the environment: + +```sh +!nvcc --version +!gcc --version +!cmake --version +!nvidia-smi +``` + +A correct environment should print the result with no bash errors for `nvidia-smi` command and result in a **Teslt T4 GPU** type: + +```sh +nvcc: NVIDIA (R) Cuda compiler driver +Copyright (c) 2005-2023 NVIDIA Corporation +Built on Tue_Aug_15_22:02:13_PDT_2023 +Cuda compilation tools, release 12.2, V12.2.140 +Build cuda_12.2.r12.2/compiler.33191640_0 +gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 +Copyright (C) 2021 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +cmake version 3.27.9 + +CMake suite maintained and supported by Kitware (kitware.com/cmake). +Wed Jan 17 13:10:18 2024 ++---------------------------------------------------------------------------------------+ +| NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 | +|-----------------------------------------+----------------------+----------------------+ +| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | +| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | +| | | MIG M. | +|=========================================+======================+======================| +| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 | +| N/A 39C P8 9W / 70W | 0MiB / 15360MiB | 0% Default | +| | | N/A | ++-----------------------------------------+----------------------+----------------------+ + ++---------------------------------------------------------------------------------------+ +| Processes: | +| GPU GI CI PID Type Process name GPU Memory | +| ID ID Usage | +|=======================================================================================| +| No running processes found | ++---------------------------------------------------------------------------------------+ +``` + +## Cloning ICICLE and running test + +Now we are ready to clone ICICE repository, + +```sh +!git clone https://github.com/ingonyama-zk/icicle.git +``` + +We can browse the repository and run tests to check the runtime environment: + +```sh +!ls -la +%cd /content/icicle +``` + +## Download CUDA backend + +First let's create a backend directory + +```sh +%cd /content +!rm -rf cuda_backend/ +!mkdir cuda_backend +%cd cuda_backend +``` + +Download and extract a backend from [ICICLE released](https://github.com/ingonyama-zk/icicle/releases) backends +In this example we are using [ICICLE Cuda backend v3.1.0](https://github.com/ingonyama-zk/icicle/releases/download/v3.1.0/icicle_3_1_0-ubuntu22-cuda122.tar.gz) + +```sh +!curl -O -L https://github.com/ingonyama-zk/icicle/releases/download/v3.1.0/icicle_3_1_0-ubuntu22-cuda122.tar.gz +!tar -xvf icicle_3_1_0-ubuntu22-cuda122.tar.gz +``` + +## Setting CUDA backend installation directory +Point colab to the extracted cuda backend using an [environment variable](https://github.com/ingonyama-zk/icicle/blob/f638e9d3056d2a5d6271a67ba4f63973a2ba2c1a/docs/docs/icicle/getting_started.md#backend-loading) + +```sh +import os +os.envvar["ICICLE_BACKEND_INSTALL_DIR"] = "/content/cuda_backend/icicle" +``` + +## Fun with ICICLE + +Let's run a test! +Navigate to icicle/wrappers/rust/icicle-curves/icicle-bn254 and run cargo test: + +```sh +%cd /content/icicle/wrappers/rust/icicle-curves/icicle-bn254/ +!cargo test --release -- ntt +``` + +:::note + +Compiling the first time may take a while + +::: + +Test run should end like this: + +```sh +running 9 tests +[WARNING] Defaulting to Ingonyama icicle-cuda-license-server at `5053@license.icicle.ingonyama.com`. For more information about icicle-cuda-license, please contact support@ingonyama.com. +[INFO] ICICLE backend loaded from $ICICLE_BACKEND_INSTALL_DIR=/content/cuda_backend/icicle +test ecntt::tests::test_ecntt::test_ecntt_batch ... ok +test ntt::tests::test_ntt ... ok +test ntt::tests::test_ntt_arbitrary_coset ... ok +test ntt::tests::test_ntt_batch ... ok +test ntt::tests::test_ntt_coset_from_subgroup ... ok +test ntt::tests::test_ntt_coset_interpolation_nm ... ok +test ecntt::tests::test_ecntt::test_ecntt ... ok +test ntt::tests::test_ntt_device_async ... ok +test ntt::tests::test_ntt_release_domain ... ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 36 filtered out; finished in 42.71s +``` + +Viola, ICICLE in Colab! diff --git a/docs/versioned_docs/version-3.4.0/icicle/faq_and_troubleshooting.md b/docs/versioned_docs/version-3.4.0/icicle/faq_and_troubleshooting.md new file mode 100644 index 0000000000..79d07f5e63 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/faq_and_troubleshooting.md @@ -0,0 +1,9 @@ +# FAQ and troubleshooting + +## Frequently asked questions + +TODO + +## Troubleshooting + +TODO \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/getting_started.md b/docs/versioned_docs/version-3.4.0/icicle/getting_started.md new file mode 100644 index 0000000000..a081110925 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/getting_started.md @@ -0,0 +1,170 @@ +# Getting started Guide + +## Overview + +This guide will walk you through the entire process of building, testing, and installing ICICLE using your preferred programming language: C++, Rust, or Go. Whether you're deploying on a CPU or leveraging CUDA for accelerated performance, this guide provides comprehensive instructions to get you started. It also outlines the typical workflow for a user, including key installation steps: + + +1. **Install ICICLE or build it from source**: This is explained in this guide. For building from source, refer to the [Build from Source page](./build_from_source.md). +2. **Follow the [Programmer’s Guide](./programmers_guide/general.md)**: Learn how to use ICICLE APIs. +3. **Start using ICICLE APIs on your CPU**: Your application will now use ICICLE on the CPU. +4. **Accelerate your application on a GPU**: [install the CUDA backend](./install_cuda_backend.md), load it, and select it in your application ([C++](./programmers_guide/cpp.md#loading-a-backend),[Rust](./programmers_guide/rust.md#loading-a-backend), [Go](./programmers_guide/go.md#loading-a-backend)). +5. **Run on the GPU**: Once the GPU backend is selected, all subsequent API calls will execute on the GPU. +6. **Optimize for multi-GPU environments**: Refer to the [Multi-GPU](./multi-device.md) Guide to fully utilize your system’s capabilities. +7. **Review memory management**: Revisit the [Memory Management section](./programmers_guide/general.md#device-abstraction) to allocate memory on the device efficiently and try to keep data on the GPU as long as possible. + + +The rest of this page details the content of a release, how to install it, and how to use it. ICICLE binaries are released for multiple Linux distributions, including Ubuntu 20.04, Ubuntu 22.04, RHEL 8, and RHEL 9. + +:::note +Future releases will also include support for macOS and other systems. +::: + +## Content of a Release + +Each ICICLE release includes a tar file named `icicle30-.tar.gz`, where `icicle30` indicates version 3.0. This tar file contains ICICLE frontend build artifacts and headers for a specific distribution. The tar file structure includes: + +- **`./icicle/include/`**: This directory contains all the necessary header files for using the ICICLE library from C++. +- **`./icicle/lib/`**: + - **Icicle Libraries**: All the core ICICLE libraries are located in this directory. Applications linking to ICICLE will use these libraries. + - **Backends**: The `./icicle/lib/backend/` directory houses backend libraries, including the CUDA backend (not included in this tar). + +- **CUDA backend** comes as separate tar `icicle30--cuda122.tar.gz` + - per distribution, for ICICLE-frontend v3.0 and CUDA 12.2. + +## Installing and using ICICLE + +- [Full C++ example](https://github.com/ingonyama-zk/icicle/tree/main/examples/c++/install-and-use-icicle) +- [Full Rust example](https://github.com/ingonyama-zk/icicle/tree/main/examples/rust/install-and-use-icicle) +- [Full Go example](https://github.com/ingonyama-zk/icicle/tree/main/examples/golang/install-and-use-icicle) + +1. **Extract and install the Tar Files**: + - [Download](https://github.com/ingonyama-zk/icicle/releases) the appropriate tar files for your distribution (Ubuntu 20.04, Ubuntu 22.04, or UBI 8,9 for RHEL compatible binaries). + - **Frontend libs and headers** should be installed in default search paths (such as `/usr/lib` and `usr/local/include`) for the compiler and linker to find. + - **Backend libs** should be installed in `/opt` + - Extract it to your desired location: + ```bash + # install the frontend part (Can skip for Rust) + tar xzvf icicle30-ubuntu22.tar.gz + cp -r ./icicle/lib/* /usr/lib/ + cp -r ./icicle/include/icicle/ /usr/local/include/ # copy C++ headers + # extract CUDA backend (OPTIONAL) + tar xzvf icicle30-ubuntu22-cuda122.tar.gz -C /opt + ``` + + :::note + Installing the frontend is optional for Rust. Rust does not use it. + ::: + + :::tip + You may install to any directory, but you need to ensure it can be found by the linker at compile and runtime. + You can install anywhere and use a symlink to ensure it can be easily found as if it were in the default directory. + ::: + +2. **Linking Your Application**: + + Applications need to link to the ICICLE device library and to every field and/or curve library. The backend libraries are dynamically loaded at runtime, so there is no need to link to them. + + **C++** + - When compiling your C++ application, link against the ICICLE libraries: + ```bash + g++ -o myapp myapp.cpp -licicle_device -licicle_field_bn254 -licicle_curve_bn254 + # if not installed in standard dirs, for example /custom/path/, need to specify it + g++ -o myapp myapp.cpp -I/custom/path/icicle/include -L/custom/path/icicle/lib -licicle_device -licicle_field_bn254 -licicle_curve_bn254 -Wl,-rpath,/custom/path/icicle/lib/ + ``` + + - Or via cmake + ```bash + # Add the executable + add_executable(example example.cpp) + # Link the libraries + target_link_libraries(example icicle_device icicle_field_bn254 icicle_curve_bn254) + + # OPTIONAL (if not installed in default location) + + # The following is setting compile and runtime paths for headers and libs assuming + # - headers in /custom/path/icicle/include + # - libs in/custom/path/icicle/lib + + # Include directories + target_include_directories(example PUBLIC /custom/path/icicle/include) + # Library directories + target_link_directories(example PUBLIC /custom/path/icicle/lib/) + # Set the RPATH so linker finds icicle libs at runtime + set_target_properties(example PROPERTIES + BUILD_RPATH /custom/path/icicle/lib/ + INSTALL_RPATH /custom/path/icicle/lib/) + ``` + + :::tip + If you face linkage issues, try `ldd myapp` to see the runtime dependencies. If ICICLE libs are not found, you need to add the install directory to the search path of the linker. In a development environment, you can do that using the environment variable export `LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/custom/path/icicle/lib` or similar (for non-Linux). For deployment, make sure it can be found and avoid using LD_LIBRARY_PATH. + + Alternatively, you can embed the search path in the app as an rpath by adding `-Wl,-rpath,/custom/path/icicle/lib/`. This is demonstrated above. + ::: + + **Rust** + - When building the ICICLE crates, ICICLE frontend libs are built from source, along with the Rust bindings. They are installed to `target//deps/icicle`, and Cargo will link them correctly. Note that you still need to install the CUDA backend if you have a CUDA GPU. + - Simply use `cargo build` or `cargo run` and it should link to ICICLE libs. + + **Go** - TODO + +:::warning +When deploying an application (whether in C++, Rust, or Go), you must make sure to either deploy the ICICLE libs (that you download or build from source) along with the application binaries (as tar, Docker image, package manager installer, or otherwise) or make sure to install ICICLE (and the backend) on the target machine. Otherwise, the target machine will have linkage issues. +::: + +## Backend Loading + +The ICICLE library dynamically loads backend libraries at runtime. By default, it searches for backends in the following order: + +1. **Environment Variable**: If the `ICICLE_BACKEND_INSTALL_DIR` environment variable is defined, ICICLE will prioritize this location. +2. **Default Directory**: If the environment variable is not set, Icicle will search in the default directory `/opt/icicle/lib/backend`. + +:::warning +If building ICICLE frontend from source, make sure to load a backend that is compatible with the frontend version. CUDA backend libs are forward compatible with newer frontends (e.g., CUDA-backend-3.0 works with ICICLE-3.2). The opposite is not guaranteed. +::: + +If you install in a custom dir, make sure to set `ICICLE_BACKEND_INSTALL_DIR`: +```bash +ICICLE_BACKEND_INSTALL_DIR=path/to/icicle/lib/backend/ myapp # for an executable myapp +ICICLE_BACKEND_INSTALL_DIR=path/to/icicle/lib/backend/ cargo run # when using cargo +``` + +Then to load backend from ICICLE_BACKEND_INSTALL_DIR or `/opt/icicle/lib/backend` in your application: + +**C++** +```cpp +extern "C" eIcicleError icicle_load_backend_from_env_or_default(); +``` +**Rust** +```rust +pub fn load_backend_from_env_or_default() -> Result<(), eIcicleError>; +``` +**Go** +```go +func LoadBackendFromEnvOrDefault() EIcicleError +``` + +### Custom Backend Loading + +If you need to load a backend from a custom location at any point during runtime, you can call the following function: + +**C++** +```cpp +extern "C" eIcicleError icicle_load_backend(const char* path, bool is_recursive); +``` +- **`path`**: The directory where the backend libraries are located. +- **`is_recursive`**: If `true`, the function will search for backend libraries recursively within the specified path. + +**Rust** +```rust + pub fn load_backend(path: &str) -> Result<(), eIcicleError>; // OR + pub fn load_backend_non_recursive(path: &str) -> Result<(), eIcicleError>; +``` +- **`path`**: The directory where the backend libraries are located. + +**Go** +```go +func LoadBackend(path string, isRecursive bool) EIcicleError +``` +- **`path`**: The directory where the backend libraries are located. +- **`isRecursive`**: If `true`, the function will search for backend libraries recursively within the specified path. diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings.md new file mode 100644 index 0000000000..87f95a4392 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings.md @@ -0,0 +1,126 @@ +# Golang bindings + +Golang bindings allow you to use ICICLE as a golang library. +The source code for all Golang packages can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang). + +The Golang bindings are comprised of multiple packages. + +[`core`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/core) which defines all shared methods and structures, such as configuration structures, or memory slices. + +[`runtime`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/runtime) which defines abstractions for ICICLE methods for allocating memory, initializing and managing streams, and `Device` which enables users to define and keep track of devices. + +Each supported curve and field has its own package which you can find in the respective directories [here](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang). If your project uses BN254 you only need to import that single package named [`bn254`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/curves/bn254). + +## Using ICICLE Golang bindings in your project + +To add ICICLE to your `go.mod` file. + +```bash +go get github.com/ingonyama-zk/icicle/v3 +``` + +If you want to specify a specific branch + +```bash +go get github.com/ingonyama-zk/icicle/v3@ +``` + +For a specific commit + +```bash +go get github.com/ingonyama-zk/icicle/v3@ +``` + +### Building from source + +To build the shared libraries you can run [this](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/golang/build.sh) script inside the downloaded go dependency: + +```sh +./build.sh [-curve=] [-field=] [-cuda_version=] [-skip_msm] [-skip_ntt] [-skip_g2] [-skip_ecntt] [-skip_fieldext] + +curve - The name of the curve to build or "all" to build all supported curves +field - The name of the field to build or "all" to build all supported fields +-skip_msm - Optional - build with MSM disabled +-skip_ntt - Optional - build with NTT disabled +-skip_g2 - Optional - build with G2 disabled +-skip_ecntt - Optional - build with ECNTT disabled +-skip_fieldext - Optional - build without field extension +-help - Optional - Displays usage information +``` + +:::note + +If more than one curve or more than one field is supplied, the last one supplied will be built + +::: + +To build ICICLE libraries for all supported curves without certain features, you can use their -skip_\ flags. For example, for disabling G2 and ECNTT: + +```bash +./build.sh -curve=all -skip_g2 -skip_ecntt +``` + +By default, all features are enabled. To build for a specific field or curve, you can pass the `-field=` or `-curve=` flags: + +``` bash +./build.sh -curve=bn254 +``` + +Now you can import ICICLE into your project + +```go +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) +... +``` + +### Building with precompiled libs + +Download the frontend release binaries from our [github release page](https://github.com/ingonyama-zk/icicle/releases), for example: icicle30-ubuntu22.tar.gz for ICICLE v3 on ubuntu 22.04 + +Extract the libs and move them to the downloaded go dependency in your GOMODCACHE + +```sh +# extract frontend part +tar xzvf icicle30-ubuntu22.tar.gz +cp -r ./icicle/lib/* $(go env GOMODCACHE)/github.com/ingonyama-zk/icicle/v3@/build/lib/ +``` + +## Running tests + +To run all tests, for all curves: + +```bash +go test ./... -count=1 +``` + +If you wish to run test for a specific curve or field: + +```bash +go test -count=1 +``` + +## How do Golang bindings work? + +The golang packages are binded to the libraries produced from compiling ICICLE using cgo. + +1. These libraries (named `libicicle_curve_.a` and `libicicle_field_.a`) can be imported in your Go project to leverage the accelerated functionalities provided by ICICLE. + +2. In your Go project, you can use `cgo` to link these libraries. Here's a basic example on how you can use `cgo` to link these libraries: + +```go +/* +#cgo LDFLAGS: -L/path/to/shared/libs -licicle_device -lstdc++ -lm -Wl,-rpath=/path/to/shared/libs +#include "icicle.h" // make sure you use the correct header file(s) +*/ +import "C" + +func main() { + // Now you can call the C functions from the ICICLE libraries. + // Note that C function calls are prefixed with 'C.' in Go code. +} +``` + +Replace `/path/to/shared/libs` with the actual path where the shared libraries are located on your system. diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ecntt.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ecntt.md new file mode 100644 index 0000000000..4fd1acf8e3 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ecntt.md @@ -0,0 +1,100 @@ +# ECNTT + +## ECNTT Method + +The `ECNtt[T any]()` function performs the Elliptic Curve Number Theoretic Transform (EC-NTT) on the input points slice, using the provided dir (direction), cfg (configuration), and stores the results in the results slice. + +```go +func ECNtt[T any](points core.HostOrDeviceSlice, dir core.NTTDir, cfg *core.NTTConfig[T], results core.HostOrDeviceSlice) runtime.EIcicleError +``` + +### Parameters + +- **`points`**: A slice of elliptic curve points (in projective coordinates) that will be transformed. The slice can be stored on the host or the device, as indicated by the `core.HostOrDeviceSlice` type. +- **`dir`**: The direction of the EC-NTT transform, either `core.KForward` or `core.KInverse`. +- **`cfg`**: A pointer to an `NTTConfig` object, containing configuration options for the NTT operation. +- **`results`**: A slice that will store the transformed elliptic curve points (in projective coordinates). The slice can be stored on the host or the device, as indicated by the `core.HostOrDeviceSlice` type. + +### Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the EC-NTT operation was successful, or an error if something went wrong. + +## NTT Configuration (NTTConfig) + +The `NTTConfig` structure holds configuration parameters for the NTT operation, allowing customization of its behavior to optimize performance based on the specifics of your protocol. + +```go +type NTTConfig[T any] struct { + StreamHandle runtime.Stream + CosetGen T + BatchSize int32 + ColumnsBatch bool + Ordering Ordering + areInputsOnDevice bool + areOutputsOnDevice bool + IsAsync bool + Ext config_extension.ConfigExtensionHandler +} +``` + +### Fields + +- **`StreamHandle`**: Specifies the stream (queue) to use for async execution. +- **`CosetGen`**: Coset generator. Used to perform coset (i)NTTs. +- **`BatchSize`**: The number of NTTs to compute in one operation, defaulting to 1. +- **`ColumnsBatch`**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. +- **`Ordering`**: Ordering of inputs and outputs (`KNN`, `KNR`, `KRN`, `KRR`), affecting how data is arranged. +- **`areInputsOnDevice`**: Indicates if input scalars are located on the device. +- **`areOutputsOnDevice`**: Indicates if results are stored on the device. +- **`IsAsync`**: Controls whether the NTT operation runs asynchronously. +- **`Ext`**: Extended configuration for backend. + +### Default Configuration + +Use `GetDefaultNTTConfig` to obtain a default configuration, customizable as needed. + +```go +func GetDefaultNTTConfig[T any](cosetGen T) NTTConfig[T] +``` + +## ECNTT Example + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/ecntt" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/ntt" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func Main() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + // Set Cuda device to perform + device := runtime.CreateDevice("CUDA", 0) + runtime.SetDevice(&device) + // Obtain the default NTT configuration with a predefined coset generator. + cfg := ntt.GetDefaultNttConfig() + + // Define the size of the input scalars. + size := 1 << 18 + + // Generate Points for the ECNTT operation. + points := bn254.GenerateProjectivePoints(size) + + // Set the direction of the NTT (forward or inverse). + dir := core.KForward + + // Allocate memory for the results of the NTT operation. + results := make(core.HostSlice[bn254.Projective], size) + + // Perform the NTT operation. + err := ecntt.ECNtt(points, dir, &cfg, results) + if err != runtime.Success { + panic("ECNTT operation failed") + } +} +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/hash.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/hash.md new file mode 100644 index 0000000000..4305820a72 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/hash.md @@ -0,0 +1,111 @@ +# ICICLE Hashing in Golang + +:::note + +For a general overview of ICICLE's hashing logic and supported algorithms, check out the [ICICLE Hashing Overview](../primitives/hash.md). + +::: + +:::caution Warning + +Using the Hash package requires `go` version 1.22 + +::: + +## Overview + +The ICICLE library provides Golang bindings for hashing using a variety of cryptographic hash functions. These hash functions are optimized for both general-purpose data and cryptographic operations such as multi-scalar multiplication, commitment generation, and Merkle tree construction. + +This guide will show you how to use the ICICLE hashing API in Golang with examples for common hash algorithms, such as Keccak-256, Keccak-512, SHA3-256, SHA3-512, Blake2s, Poseidon. + +## Importing Hash Functions + +To use the hashing functions in Golang, you only need to import the hash package from the ICICLE Golang bindings. For example: + +```go +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/hash" +) +``` + +## API Usage + +### 1. Creating a Hasher Instance + +Each hash algorithm can be instantiated by calling its respective constructor. The `NewHasher` function takes an optional default input size, which can be set to 0 unless required for a specific use case. + +Example for Keccak-256: + +```go +keccakHasher := hash.NewKeccak256Hasher(0 /* default input size */) +``` + +### 2. Hashing a Simple String + +Once you have created a hasher instance, you can hash any input data, such as strings or byte arrays, and store the result in an output buffer. +Here’s how to hash a simple string using Keccak-256: + +```go +import ( + "encoding/hex" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/hash" +) + +inputStrAsBytes := []bytes("I like ICICLE! It's so fast and simple") +keccakHasher, error := hash.NewKeccak256Hasher(0 /*default chunk size*/) +if error != runtime.Success { + fmt.Println("error:", error) + return +} + +outputRef := make([]byte, 32) +keccakHasher.Hash( + core.HostSliceFromElements(inputStrAsBytes), + core.HostSliceFromElements(outputRef), + core.GetDefaultHashConfig(), +) + +// convert the output to a hex string for easy readability +outputAsHexStr = hex.EncodeToString(outputRef) +fmt.Println!("Hash(`", input_str, "`) =", &outputAsHexStr) +``` + +Using other hash algorithms is similar and only requires replacing the Hasher constructor with the relevant hashing algorithm. + +### 3. Poseidon Example (field elements) and batch hashing + +The Poseidon hash is designed for cryptographic field elements and curves, making it ideal for use cases such as zero-knowledge proofs (ZKPs). Poseidon hash using babybear field: + +:::note + +Since poseidon is designed for use with field elements and curves, it is located within the field or curve packages and not in the Hash package though it does rely on using the Hash package. + +::: + +```go +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + babybear "github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear/poseidon" +) + +batch := 1 << 4 +t := 3 // Currently support arity of 3, 5, 9, 12 +// (t - 1) is due to domainTag being non nil +// if domainTag is nil, then the input size should be `batch * t` +// See more in our tests: https://github.com/ingonyama-zk/icicle/blob/docs/v3/golang/poseidon/wrappers/golang/curves/bn254/tests/poseidon_test.go#L23-L27 +inputsSize = batch * (t - 1) +inputs := babybear.GenerateScalars(inputsSize) +domainTag := babybear.GenerateScalars(1)[0] + +outputsRef := make([]babybear.ScalarField, batch) +poseidonHasherRef, _ := poseidon.NewHasher(uint64(t), &domainTag) +poseidonHasherRef.Hash( + core.HostSliceFromElements(inputs), + core.HostSliceFromElements(outputsRef), + core.GetDefaultHashConfig(), +) + +poseidonHasherRef.Delete() +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/merkle.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/merkle.md new file mode 100644 index 0000000000..c5bff7c1dc --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/merkle.md @@ -0,0 +1,238 @@ +# Merkle Tree API Documentation (Golang) + +This is the Golang version of the **Merkle Tree API Documentation** ([C++ documentation](../primitives/merkle.md)). It mirrors the structure and functionality of the C++ version, providing equivalent APIs in Golang. +For more detailed explanations, refer to the [C++ documentation](../primitives/merkle.md). + +To see a complete implementation, visit the [Hash and Merkle example](https://github.com/ingonyama-zk/icicle/tree/main/examples/rust/hash-and-merkle) for a full example. + +:::caution Warning + +Using the Hash package requires `go` version 1.22 + +::: + +## Tree Structure and Configuration in Golang + +### Defining a Merkle Tree + +```go +/// * `layerHashers` - A vector of hash objects representing the hashers of each layer. +/// * `leafElementSize` - Size of each leaf element. +/// * `outputStoreMinLayer` - Minimum layer at which the output is stored. +/// +/// # Returns a new `MerkleTree` instance or EIcicleError. +func CreateMerkleTree( + layerHashers []hash.Hasher, + leafElementSize, + outputStoreMinLayer uint64, +) (MerkleTree, runtime.EIcicleError) +``` + +The `outputStoreMinLayer` parameter defines the lowest layer that will be stored in memory. Layers below this value will not be stored, saving memory at the cost of additional computation when proofs are generated. + +### Building the Tree + +The Merkle tree can be constructed from input data of any type, allowing flexibility in its usage. The size of the input must align with the tree structure defined by the hash layers and leaf size. If the input size does not match the expected size, padding may be applied. + +Refer to the [Padding Section](#padding) for more details on how mismatched input sizes are handled. + +```go +/// * `mt` - The merkle tree object to build +/// * `leaves` - A slice of leaves (input data). +/// * `config` - Configuration for the Merkle tree. +/// +/// # Returns a result indicating success or failure. +func BuildMerkleTree[T any]( + mt *MerkleTree, + leaves core.HostOrDeviceSlice, + cfg core.MerkleTreeConfig, +) runtime.EIcicleError +``` + +## Tree Examples in Golang + +### Example A: Binary Tree + +A binary tree with **5 layers**, using **Keccak-256**: + +![Merkle Tree Diagram](../primitives/merkle_diagrams/diagram1.png) + +```go +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/hash" + merkletree "github.com/ingonyama-zk/icicle/v3/wrappers/golang/merkle-tree" +) + +leafSize := 1024 +maxInputSize := leafSize * 16 +input := make([]byte, maxInputSize) + +hasher, _ := hash.NewKeccak256Hasher(uint64(leafSize)) +compress, _ := hash.NewKeccak256Hasher(2 * hasher.OutputSize()) +layerHashers := []hash.Hasher{hasher, compress, compress, compress, compress} + +mt, _ := merkletree.CreateMerkleTree(layerHashers, uint64(leafSize), 0 /* min layer to store */) + +merkletree.BuildMerkleTree[byte](&mt, core.HostSliceFromElements(input), core.GetDefaultMerkleTreeConfig()) +``` + +### Example B: Tree with Arity 4 + +![Merkle Tree Diagram](../primitives/merkle_diagrams/diagram2.png) + +This example uses **Blake2s** in upper layers: + +```go +// define layer hashers +// we want one hash layer to hash every 1KB to 32B then compress every 128B so only 2 more layers +hasher, _ := hash.NewKeccak256Hasher(uint64(leafSize)) +compress, _ := hash.NewBlake2sHasher(2 * hasher.OutputSize()) +layerHashers := []hash.Hasher{hasher, compress, compress,} + +mt, _ := merkletree.CreateMerkleTree(layerHashers, uint64(leafSize), 0 /* min layer to store */) + +merkletree.BuildMerkleTree[byte](&mt, core.HostSliceFromElements(input), core.GetDefaultMerkleTreeConfig()) +``` + +## Padding + +When the input for **layer 0** is smaller than expected, ICICLE can apply **padding** to align the data. + +**Padding Schemes:** + +1. **Zero padding:** Adds zeroes to the remaining space. +2. **Repeat last leaf:** The final leaf element is repeated to fill the remaining space. + +```go +// type PaddingPolicy = int + +// const ( +// NoPadding PaddingPolicy = iota // No padding, assume input is correctly sized. +// ZeroPadding // Pad the input with zeroes to fit the expected input size. +// LastValuePadding // Pad the input by repeating the last value. +// ) + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" +) + +config := core.GetDefaultMerkleTreeConfig(); +config.PaddingPolicy = core.ZeroPadding; +merkletree.BuildMerkleTree[byte](&mt, core.HostSliceFromElements(input), core.GetDefaultMerkleTreeConfig()) +``` + +## Root as Commitment + +Retrieve the Merkle-root and serialize. + +```go +/// Retrieve the root of the Merkle tree. +/// +/// # Returns +/// A reference to the root hash. +func GetMerkleTreeRoot[T any](mt *MerkleTree) ([]T, runtime.EIcicleError) + +commitment := merkletree.GetMerkleTreeRoot[byte](&mt) +fmt.Println!("Commitment:", commitment) +``` + +:::warning +The commitment can be serialized to the proof. This is not handled by ICICLE. +::: + +## Generating Merkle Proofs + +Merkle proofs are used to **prove the integrity of opened leaves** in a Merkle tree. A proof ensures that a specific leaf belongs to the committed data by enabling the verifier to reconstruct the **root hash (commitment)**. + +A Merkle proof contains: + +- **Leaf**: The data being verified. +- **Index** (leaf_idx): The position of the leaf in the original dataset. +- **Path**: A sequence of sibling hashes (tree nodes) needed to recompute the path from the leaf to the root. + +![Merkle Pruned Phat Diagram](../primitives/merkle_diagrams/diagram1_path.png) + +```go +/// * `leaves` - A slice of leaves (input data). +/// * `leaf_idx` - Index of the leaf to generate a proof for. +/// * `pruned_path` - Whether the proof should be pruned. +/// * `config` - Configuration for the Merkle tree. +/// +/// # Returns a `MerkleProof` object or eIcicleError +func GetMerkleTreeProof[T any]( + mt *MerkleTree, + leaves core.HostOrDeviceSlice, + leafIndex uint64, + prunedPath bool, + cfg core.MerkleTreeConfig, +) (MerkleProof, runtime.EIcicleError) +``` + +### Example: Generating a Proof + +Generating a proof for leaf idx 5: + +```go +mp, _ := merkletree.GetMerkleTreeProof[byte]( + &mt, + core.HostSliceFromElements(input), + 5, /* leafIndex */ + true, /* prunedPath */ + core.GetDefaultMerkleTreeConfig(), +) +``` + +:::warning +The Merkle-path can be serialized to the proof along with the leaf. This is not handled by ICICLE. +::: + +## Verifying Merkle Proofs + +```go +/// * `proof` - The Merkle proof to verify. +/// +/// # Returns a result indicating whether the proof is valid. +func (mt *MerkleTree) Verify(mp *MerkleProof) (bool, runtime.EIcicleError) +``` + +### Example: Verifying a Proof + +```go +isVerified, err := mt.Verify(&mp) +assert.True(isVerified) +``` + +## Pruned vs. Full Merkle-paths + +A **Merkle path** is a collection of **sibling hashes** that allows the verifier to **reconstruct the root hash** from a specific leaf. +This enables anyone with the **path and root** to verify that the **leaf** belongs to the committed dataset. +There are two types of paths that can be computed: + +- [**Pruned Path:**](#generating-merkle-proofs) Contains only necessary sibling hashes. +- **Full Path:** Contains all sibling nodes and intermediate hashes. + +![Merkle Full Path Diagram](../primitives//merkle_diagrams/diagram1_path_full.png) + +To compute a full path, specify `pruned=false`: + +```go +mp, _ := merkletree.GetMerkleTreeProof[byte]( + &mt, + core.HostSliceFromElements(input), + 5, /* leafIndex */ + false, /*non-pruned is a full path --> note the pruned flag here*/ + core.GetDefaultMerkleTreeConfig(), +) +``` + +## Handling Partial Tree Storage + +In cases where the **Merkle tree is large**, only the **top layers** may be stored to conserve memory. +When opening leaves, the **first layers** (closest to the leaves) are **recomputed dynamically**. + +For example to avoid storing first layer we can define a tree as follows: + +```go +mt, err := merkletree.CreateMerkleTree(layerHashers, leafSize, 1 /*min layer to store*/); +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm-pre-computation.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm-pre-computation.md new file mode 100644 index 0000000000..f13998d3e0 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm-pre-computation.md @@ -0,0 +1,110 @@ +# MSM Pre computation + +To understand the theory behind MSM pre computation technique refer to Niall Emmart's [talk](https://youtu.be/KAWlySN7Hm8?feature=shared&t=1734). + +## Core package + +### MSM PrecomputeBases + +`PrecomputeBases` and `G2PrecomputeBases` exists for all supported curves. + +#### Description + +This function extends each provided base point $(P)$ with its multiples $(2^lP, 2^{2l}P, ..., 2^{(precompute_factor - 1) \cdot l}P)$, where $(l)$ is a level of precomputation determined by the `precompute_factor`. The extended set of points facilitates faster MSM computations by allowing the MSM algorithm to leverage precomputed multiples of base points, reducing the number of point additions required during the computation. + +The precomputation process is crucial for optimizing MSM operations, especially when dealing with large sets of points and scalars. By precomputing and storing multiples of the base points, the MSM function can more efficiently compute the scalar-point multiplications. + +#### `PrecomputeBases` + +Precomputes points for MSM by extending each base point with its multiples. + +```go +func PrecomputeBases(bases core.HostOrDeviceSlice, cfg *core.MSMConfig, outputBases core.DeviceSlice) runtime.EIcicleError +``` + +##### Parameters + +- **`bases`**: A slice of the original affine points to be extended with their multiples. +- **`cfg`**: The MSM configuration parameters. +- **`outputBases`**: The device slice allocated for storing the extended points. + +##### Example + +```go +package main + +import ( + "log" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + // Set Cuda device to perform + device := runtime.CreateDevice("CUDA", 0) + runtime.SetDevice(&device) + + cfg := core.GetDefaultMSMConfig() + points := bn254.GenerateAffinePoints(1024) + cfg.PrecomputeFactor = 8 + var precomputeOut core.DeviceSlice + precomputeOut.Malloc(points[0].Size(), points.Len()*int(cfg.PrecomputeFactor)) + + err := msm.PrecomputeBases(points, &cfg, precomputeOut) + if err != runtime.Success { + log.Fatalf("PrecomputeBases failed: %v", err) + } +} +``` + +#### `G2PrecomputeBases` + +This method is the same as `PrecomputePoints` but for G2 points. Extends each G2 curve base point with its multiples for optimized MSM computations. + +```go +func G2PrecomputeBases(bases core.HostOrDeviceSlice, cfg *core.MSMConfig, outputBases core.DeviceSlice) runtime.EIcicleError +``` + +##### Parameters + +- **`bases`**: A slice of the original affine points to be extended with their multiples. +- **`cfg`**: The MSM configuration parameters. +- **`outputBases`**: The device slice allocated for storing the extended points. + +##### Example + +```go +package main + +import ( + "log" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/g2" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + // Set Cuda device to perform + device := runtime.CreateDevice("CUDA", 0) + runtime.SetDevice(&device) + + cfg := core.GetDefaultMSMConfig() + points := g2.G2GenerateAffinePoints(1024) + cfg.PrecomputeFactor = 8 + var precomputeOut core.DeviceSlice + precomputeOut.Malloc(points[0].Size(), points.Len()*int(cfg.PrecomputeFactor)) + + err := g2.G2PrecomputeBases(points, &cfg, precomputeOut) + if err != runtime.Success { + log.Fatalf("PrecomputeBases failed: %v", err) + } +} +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm.md new file mode 100644 index 0000000000..7209eca47e --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/msm.md @@ -0,0 +1,205 @@ +# MSM + +## MSM Example + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + // Set Cuda device to perform + device := runtime.CreateDevice("CUDA", 0) + runtime.SetDevice(&device) + + // Obtain the default MSM configuration. + cfg := core.GetDefaultMSMConfig() + + // Define the size of the problem, here 2^18. + size := 1 << 18 + + // Generate scalars and points for the MSM operation. + scalars := bn254.GenerateScalars(size) + points := bn254.GenerateAffinePoints(size) + + // Create a CUDA stream for asynchronous operations. + stream, _ := runtime.CreateStream() + var p bn254.Projective + + // Allocate memory on the device for the result of the MSM operation. + var out core.DeviceSlice + _, e := out.MallocAsync(p.Size(), 1, stream) + + if e != runtime.Success { + panic(e) + } + + // Set the CUDA stream in the MSM configuration. + cfg.StreamHandle = stream + cfg.IsAsync = true + + // Perform the MSM operation. + e = msm.Msm(scalars, points, &cfg, out) + + if e != runtime.Success { + panic(e) + } + + // Allocate host memory for the results and copy the results from the device. + outHost := make(core.HostSlice[bn254.Projective], 1) + runtime.SynchronizeStream(stream) + runtime.DestroyStream(stream) + outHost.CopyFromDevice(&out) + + // Free the device memory allocated for the results. + out.Free() +} +``` + +## MSM Method + +```go +func Msm(scalars core.HostOrDeviceSlice, points core.HostOrDeviceSlice, cfg *core.MSMConfig, results core.HostOrDeviceSlice) runtime.EIcicleError +``` + +### Parameters + +- **`scalars`**: A slice containing the scalars for multiplication. It can reside either in host memory or device memory. +- **`points`**: A slice containing the points to be multiplied with scalars. Like scalars, these can also be in host or device memory. +- **`cfg`**: A pointer to an `MSMConfig` object, which contains various configuration options for the MSM operation. +- **`results`**: A slice where the results of the MSM operation will be stored. This slice can be in host or device memory. + +### Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. + +## MSMConfig + +The `MSMConfig` structure holds configuration parameters for the MSM operation, allowing customization of its behavior to optimize performance based on the specifics of the operation or the underlying hardware. + +```go +type MSMConfig struct { + StreamHandle runtime.Stream + PrecomputeFactor int32 + C int32 + Bitsize int32 + BatchSize int32 + ArePointsSharedInBatch bool + areScalarsOnDevice bool + AreScalarsMontgomeryForm bool + areBasesOnDevice bool + AreBasesMontgomeryForm bool + areResultsOnDevice bool + IsAsync bool + Ext config_extension.ConfigExtensionHandler +} +``` + +### Fields + +- **`StreamHandle`**: Specifies the stream (queue) to use for async execution. +- **`PrecomputeFactor`**: Controls the number of extra points to pre-compute. +- **`C`**: Window bitsize, a key parameter in the "bucket method" for MSM. +- **`Bitsize`**: Number of bits of the largest scalar. +- **`BatchSize`**: Number of results to compute in one batch. +- **`ArePointsSharedInBatch`**: Bases are shared for batch. Set to true if all MSMs use the same bases. Otherwise, the number of bases and number of scalars are expected to be equal. +- **`areScalarsOnDevice`**: Indicates if scalars are located on the device. +- **`AreScalarsMontgomeryForm`**: True if scalars are in Montgomery form. +- **`areBasesOnDevice`**: Indicates if bases are located on the device. +- **`AreBasesMontgomeryForm`**: True if point coordinates are in Montgomery form. +- **`areResultsOnDevice`**: Indicates if results are stored on the device. +- **`IsAsync`**: If true, runs MSM asynchronously. +- **`Ext`**: Extended configuration for backend. + +### Default Configuration + +Use `GetDefaultMSMConfig` to obtain a default configuration, which can then be customized as needed. + +```go +func GetDefaultMSMConfig() MSMConfig +``` + +## Batched msm + +For batch msm, simply allocate the results array with size corresponding to batch size and set the `ArePointsSharedInBatch` flag in config struct. + +```go +... + +// Obtain the default MSM configuration. +cfg := GetDefaultMSMConfig() + +cfg.Ctx.IsBigTriangle = true + +... +``` + +## How do I toggle between MSM modes? + +Toggling between MSM modes occurs automatically based on the number of results you are expecting from the `MSM` function. + +The number of results is interpreted from the size of `var out core.DeviceSlice`. Thus its important when allocating memory for `var out core.DeviceSlice` to make sure that you are allocating ` X `. + +```go +... + +batchSize := 3 +var p G2Projective +var out core.DeviceSlice +out.Malloc(p.Size(), batchSize) + +... +``` + +## Parameters for optimal performance + +Please refer to the [primitive description](../primitives/msm#choosing-optimal-parameters) + +## Support for G2 group + +To activate G2 support first you must make sure you are building the static libraries with G2 feature enabled as described in the [Golang building instructions](../golang-bindings.md#using-icicle-golang-bindings-in-your-project). + +Now you may import `g2` package of the specified curve. + +```go +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/g2" +) +``` + +This package include `G2Projective` and `G2Affine` points as well as a `G2Msm` method. + +```go +package main + +import ( + "log" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + cfg := core.GetDefaultMSMConfig() + points := bn254.GenerateAffinePoints(1024) + var precomputeFactor int32 = 8 + var precomputeOut core.DeviceSlice + precomputeOut.Malloc(points[0].Size(), points.Len()*int(precomputeFactor)) + + err := msm.PrecomputeBases(points, &cfg, precomputeOut) + if err != runtime.Success { + log.Fatalf("PrecomputeBases failed: %v", err) + } +} +``` + +`G2Msm` works the same way as normal MSM, the difference is that it uses G2 Points. diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/multi-gpu.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/multi-gpu.md new file mode 100644 index 0000000000..7f0a662570 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/multi-gpu.md @@ -0,0 +1,161 @@ +# Multi GPU APIs + +To learn more about the theory of Multi GPU programming refer to [this part](../multi-device.md) of documentation. + +Here we will cover the core multi GPU apis and an [example](#a-multi-gpu-example) + +## A Multi GPU example + +In this example we will display how you can + +1. Fetch the number of devices installed on a machine +2. For every GPU launch a thread and set an active device per thread. +3. Execute a MSM on each GPU + +```go +package main + +import ( + "fmt" + "sync" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + bn254 "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + + device := runtime.CreateDevice("CUDA", 0) + err := runtime.SetDevice(&device) + numDevices, _ := runtime.GetDeviceCount() + fmt.Println("There are ", numDevices, " devices available") + + if err != runtime.Success { + panic(err) + } + wg := sync.WaitGroup{} + + for i := 0; i < numDevices; i++ { + internalDevice := runtime.Device{DeviceType: device.DeviceType, Id: int32(i)} + wg.Add(1) + runtime.RunOnDevice(&internalDevice, func(args ...any) { + defer wg.Done() + currentDevice, err := runtime.GetActiveDevice() + if err != runtime.Success { + panic("Failed to get current device") + } + + fmt.Println("Running on ", currentDevice.GetDeviceType(), " ", currentDevice.Id, " device") + + cfg := msm.GetDefaultMSMConfig() + cfg.IsAsync = true + size := 1 << 10 + scalars := bn254.GenerateScalars(size) + points := bn254.GenerateAffinePoints(size) + + stream, _ := runtime.CreateStream() + var p bn254.Projective + var out core.DeviceSlice + _, err = out.MallocAsync(p.Size(), 1, stream) + if err != runtime.Success { + panic("Allocating bytes on device for Projective results failed") + } + cfg.StreamHandle = stream + + err = msm.Msm(scalars, points, &cfg, out) + if err != runtime.Success { + panic("Msm failed") + } + outHost := make(core.HostSlice[bn254.Projective], 1) + outHost.CopyFromDeviceAsync(&out, stream) + out.FreeAsync(stream) + + runtime.SynchronizeStream(stream) + runtime.DestroyStream(stream) + // Check with gnark-crypto + }) + } + wg.Wait() +} +``` + +This example demonstrates a basic pattern for distributing tasks across multiple GPUs. The `RunOnDevice` function ensures that each goroutine is executed on its designated GPU and a corresponding thread. + +## Device Management API + +To streamline device management we offer as part of `runtime` package methods for dealing with devices. + +### `RunOnDevice` + +Runs a given function on a specific GPU device, ensuring that all CUDA calls within the function are executed on the selected device. + +In Go, most concurrency can be done via Goroutines. However, there is no guarantee that a goroutine stays on a specific host thread. + +`RunOnDevice` was designed to solve this caveat and ensure that the goroutine will stay on a specific host thread. + +`RunOnDevice` locks a goroutine into a specific host thread, sets a current GPU device, runs a provided function, and unlocks the goroutine from the host thread after the provided function finishes. + +While the goroutine is locked to the host thread, the Go runtime will not assign other goroutines to that host thread. + +**Parameters:** + +- **`device *Device`**: A pointer to the `Device` instance to be used to run code. +- **`funcToRun func(args ...any)`**: The function to be executed on the specified device. +- **`args ...any`**: Arguments to be passed to `funcToRun`. + +**Behavior:** + +- The function `funcToRun` is executed in a new goroutine that is locked to a specific OS thread to ensure that all CUDA calls within the function target the specified device. + +:::note +Any goroutines launched within `funcToRun` are not automatically bound to the same GPU device. If necessary, `RunOnDevice` should be called again within such goroutines with the same `deviceId`. +::: + +**Example:** + +```go +device := runtime.CreateDevice("CUDA", 0) +RunOnDevice(&device, func(args ...any) { + fmt.Println("This runs on GPU 0") + // CUDA-related operations here will target GPU 0 +}, nil) +``` + +### `SetDevice` + +Sets the active device for the current host thread. All subsequent calls made from this thread will target the specified device. + +:::warning +This function should not be used directly in conjunction with goroutines. If you want to run multi-gpu scenarios with goroutines you should use [RunOnDevice](#runondevice) +::: + +**Parameters:** + +- **`device *Device`**: A pointer to the `Device` instance to be used to run code. + +**Returns:** + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. + +### `GetDeviceCount` + +Retrieves the number of devices available on the host. + +**Returns:** + +- **`(int, EIcicleError)`**: The number of devices and an error code indicating the success or failure of the operation. + +### `GetActiveDevice` + +Gets the device of the currently active device for the calling host thread. + +**Returns:** + +- **`(*Device, EIcicleError)`**: The device pointer and an error code indicating the success or failure of the operation. + + +This documentation should provide a clear understanding of how to effectively manage multiple GPUs in Go applications using CUDA and other backends, with a particular emphasis on the `RunOnDevice` function for executing tasks on specific GPUs. diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ntt.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ntt.md new file mode 100644 index 0000000000..9a947603e5 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/ntt.md @@ -0,0 +1,140 @@ +# NTT + +## NTT Example + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/ntt" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" +) + +func init() { + // Load backend using env path + runtime.LoadBackendFromEnvOrDefault() + // Set Cuda device to perform + device := runtime.CreateDevice("CUDA", 0) + runtime.SetDevice(&device) + + cfg := core.GetDefaultNTTInitDomainConfig() + initDomain(18, cfg) +} + +func initDomain(largestTestSize int, cfg core.NTTInitDomainConfig) runtime.EIcicleError { + rouMont, _ := fft.Generator(uint64(1 << largestTestSize)) + rou := rouMont.Bits() + rouIcicle := bn254.ScalarField{} + limbs := core.ConvertUint64ArrToUint32Arr(rou[:]) + + rouIcicle.FromLimbs(limbs) + e := ntt.InitDomain(rouIcicle, cfg) + return e +} + +func main() { + // Obtain the default NTT configuration with a predefined coset generator. + cfg := ntt.GetDefaultNttConfig() + + // Define the size of the input scalars. + size := 1 << 18 + + // Generate scalars for the NTT operation. + scalars := bn254.GenerateScalars(size) + + // Set the direction of the NTT (forward or inverse). + dir := core.KForward + + // Allocate memory for the results of the NTT operation. + results := make(core.HostSlice[bn254.ScalarField], size) + + // Perform the NTT operation. + err := ntt.Ntt(scalars, dir, &cfg, results) + if err != runtime.Success { + panic("NTT operation failed") + } + + ntt.ReleaseDomain() +} +``` + +## NTT Method + +```go +func Ntt[T any](scalars core.HostOrDeviceSlice, dir core.NTTDir, cfg *core.NTTConfig[T], results core.HostOrDeviceSlice) runtime.EIcicleError +``` + +### Parameters + +- **`scalars`**: A slice containing the input scalars for the transform. It can reside either in host memory or device memory. +- **`dir`**: The direction of the NTT operation (`KForward` or `KInverse`). +- **`cfg`**: A pointer to an `NTTConfig` object, containing configuration options for the NTT operation. +- **`results`**: A slice where the results of the NTT operation will be stored. This slice can be in host or device memory. + +### Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. + +## NTT Configuration (NTTConfig) + +The `NTTConfig` structure holds configuration parameters for the NTT operation, allowing customization of its behavior to optimize performance based on the specifics of your protocol. + +```go +type NTTConfig[T any] struct { + StreamHandle runtime.Stream + CosetGen T + BatchSize int32 + ColumnsBatch bool + Ordering Ordering + areInputsOnDevice bool + areOutputsOnDevice bool + IsAsync bool + Ext config_extension.ConfigExtensionHandler +} +``` + +### Fields + +- **`StreamHandle`**: Specifies the stream (queue) to use for async execution. +- **`CosetGen`**: Coset generator. Used to perform coset (i)NTTs. +- **`BatchSize`**: The number of NTTs to compute in one operation, defaulting to 1. +- **`ColumnsBatch`**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. +- **`Ordering`**: Ordering of inputs and outputs (`KNN`, `KNR`, `KRN`, `KRR`), affecting how data is arranged. +- **`areInputsOnDevice`**: Indicates if input scalars are located on the device. +- **`areOutputsOnDevice`**: Indicates if results are stored on the device. +- **`IsAsync`**: Controls whether the NTT operation runs asynchronously. +- **`Ext`**: Extended configuration for backend. + +### Default Configuration + +Use `GetDefaultNTTConfig` to obtain a default configuration, customizable as needed. + +```go +func GetDefaultNTTConfig[T any](cosetGen T) NTTConfig[T] +``` + +### Initializing the NTT Domain + +Before performing NTT operations, it's necessary to initialize the NTT domain; it only needs to be called once per GPU since the twiddles are cached. + +```go +func InitDomain(primitiveRoot bn254.ScalarField, cfg core.NTTInitDomainConfig) runtime.EIcicleError +``` + +This function initializes the domain with a given primitive root, optionally using fast twiddle factors to optimize the computation. + +### Releasing the domain + +The `ReleaseDomain` function is responsible for releasing the resources associated with a specific domain in the CUDA device context. + +```go +func ReleaseDomain() runtime.EIcicleError +``` + +### Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. diff --git a/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/vec-ops.md b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/vec-ops.md new file mode 100644 index 0000000000..e219ec26d1 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/golang-bindings/vec-ops.md @@ -0,0 +1,197 @@ +# Vector Operations + +## Overview + +Icicle exposes a number of vector operations which a user can use: + +* The VecOps API provides efficient vector operations such as addition, subtraction, and multiplication, supporting both single and batched operations. +* MatrixTranspose API allows a user to perform a transpose on a vector representation of a matrix, with support for batched transpositions. + +## VecOps API Documentation + +### Example + +#### Vector addition + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/vecOps" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + testSize := 1 << 12 + a := bn254.GenerateScalars(testSize) + b := bn254.GenerateScalars(testSize) + out := make(core.HostSlice[bn254.ScalarField], testSize) + cfg := core.DefaultVecOpsConfig() + + // Perform vector multiplication + err := vecOps.VecOp(a, b, out, cfg, core.Add) + if err != runtime.Success { + panic("Vector addition failed") + } +} +``` + +#### Vector Subtraction + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/vecOps" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + testSize := 1 << 12 + a := bn254.GenerateScalars(testSize) + b := bn254.GenerateScalars(testSize) + out := make(core.HostSlice[bn254.ScalarField], testSize) + cfg := core.DefaultVecOpsConfig() + + // Perform vector multiplication + err := vecOps.VecOp(a, b, out, cfg, core.Sub) + if err != runtime.Success { + panic("Vector subtraction failed") + } +} +``` + +#### Vector Multiplication + +```go +package main + +import ( + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/vecOps" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" +) + +func main() { + testSize := 1 << 12 + a := bn254.GenerateScalars(testSize) + b := bn254.GenerateScalars(testSize) + out := make(core.HostSlice[bn254.ScalarField], testSize) + cfg := core.DefaultVecOpsConfig() + + // Perform vector multiplication + err := vecOps.VecOp(a, b, out, cfg, core.Mul) + if err != runtime.Success { + panic("Vector multiplication failed") + } +} +``` + +### VecOps Method + +```go +func VecOp(a, b, out core.HostOrDeviceSlice, config core.VecOpsConfig, op core.VecOps) (ret runtime.EIcicleError) +``` + +#### Parameters + +- **`a`**: The first input vector. +- **`b`**: The second input vector. +- **`out`**: The output vector where the result of the operation will be stored. +- **`config`**: A `VecOpsConfig` object containing various configuration options for the vector operations. +- **`op`**: The operation to perform, specified as one of the constants (`Sub`, `Add`, `Mul`) from the `VecOps` type. + +#### Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. + +### VecOpsConfig + +The `VecOpsConfig` structure holds configuration parameters for the vector operations, allowing customization of its behavior. + +```go +type VecOpsConfig struct { + StreamHandle runtime.Stream + isAOnDevice bool + isBOnDevice bool + isResultOnDevice bool + IsAsync bool + batch_size int + columns_batch bool + Ext config_extension.ConfigExtensionHandler +} +``` + +#### Fields + +- **`StreamHandle`**: Specifies the stream (queue) to use for async execution. +- **`isAOnDevice`**: Indicates if vector `a` is located on the device. +- **`isBOnDevice`**: Indicates if vector `b` is located on the device. +- **`isResultOnDevice`**: Specifies where the result vector should be stored (device or host memory). +- **`IsAsync`**: Controls whether the vector operation runs asynchronously. +- **`batch_size`**: Number of vectors (or operations) to process in a batch. Each vector operation will be performed independently on each batch element. +- **`columns_batch`**: true if the batched vectors are stored as columns in a 2D array (i.e., the vectors are strided in memory as columns of a matrix). If false, the batched vectors are stored contiguously in memory (e.g., as rows or in a flat array). +- **`Ext`**: Extended configuration for backend. + +#### Default Configuration + +Use `DefaultVecOpsConfig` to obtain a default configuration, customizable as needed. + +```go +func DefaultVecOpsConfig() VecOpsConfig +``` + +## MatrixTranspose API Documentation + +This section describes the functionality of the `TransposeMatrix` function used for matrix transposition. + +The function takes a matrix represented as a 1D slice and transposes it, storing the result in another 1D slice. + +If VecOpsConfig specifies a batch_size greater than one, the transposition is performed on multiple matrices simultaneously, producing corresponding transposed matrices. The storage arrangement of batched matrices is determined by the columns_batch field in the VecOpsConfig. + +### Function + +```go +func TransposeMatrix(in, out core.HostOrDeviceSlice, columnSize, rowSize int, config core.VecOpsConfig) runtime.EIcicleError +``` + +## Parameters + +- **`in`**: The input matrix is a `core.HostOrDeviceSlice`, stored as a 1D slice. +- **`out`**: The output matrix is a `core.HostOrDeviceSlice`, which will be the transpose of the input matrix, stored as a 1D slice. +- **`columnSize`**: The number of columns in the input matrix. +- **`rowSize`**: The number of rows in the input matrix. +- **`config`**: A `VecOpsConfig` object containing various configuration options for the vector operations. + +## Return Value + +- **`EIcicleError`**: A `runtime.EIcicleError` value, which will be `runtime.Success` if the operation was successful, or an error if something went wrong. + +## Example Usage + +```go +var input = make(core.HostSlice[ScalarField], 20) +var output = make(core.HostSlice[ScalarField], 20) + +// Populate the input matrix +// ... + +// Get device context +cfg, _ := runtime.GetDefaultDeviceContext() + +// Transpose the matrix +err := TransposeMatrix(input, output, 5, 4, cfg) +if err != runtime.Success { + // Handle the error +} + +// Use the transposed matrix +// ... +``` + +In this example, the `TransposeMatrix` function is used to transpose a 5x4 matrix stored in a 1D slice. The input and output slices are stored on the host (CPU), and the operation is executed synchronously. diff --git a/docs/versioned_docs/version-3.4.0/icicle/image.png b/docs/versioned_docs/version-3.4.0/icicle/image.png new file mode 100644 index 0000000000000000000000000000000000000000..9e6aecaaf38977e47e7983116107b0d821bdfe11 GIT binary patch literal 35743 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 + Untitled design (29) +

+ +## What is ICICLE? + +[![GitHub Release](https://img.shields.io/github/v/release/ingonyama-zk/icicle)](https://github.com/ingonyama-zk/icicle/releases) + +[ICICLE](https://github.com/ingonyama-zk/icicle) is a cryptography library designed to accelerate zero-knowledge proofs (ZKPs) using multiple compute backends, including GPUs, CPUs, and potentially other platforms. ICICLE's key strength lies in its ability to implement blazing-fast cryptographic primitives, enabling developers to significantly reduce proving times with minimal effort. + +## Key Features + +- **Acceleration of “zk” Math:** ICICLE provides optimized implementations for cryptographic primitives crucial to zero-knowledge proofs, such as elliptic curve operations, MSM, NTT, Poseidon hash, and more. +- **Set of Libraries:** ICICLE includes a comprehensive set of libraries supporting various fields, curves, and other cryptographic needs. +- **Cross-Language Support:** Available bindings for C++, Rust, Go, and potentially Python make ICICLE accessible across different development environments. +- **Backend Agnosticism:** Develop on CPU and deploy on various backends, including GPUs, specialized hardware, and other emerging platforms, depending on your project's needs. +- **Extensibility:** Designed for easy integration and extension, allowing you to build and deploy custom backends and cryptographic primitives. + +## Evolution from v2 to v3 + +Originally, ICICLE was focused solely on GPU acceleration. With the release of v3, ICICLE now supports multiple backends, making it more versatile and adaptable to different hardware environments. Whether you're leveraging the power of GPUs or exploring other compute platforms, ICICLE v3 is designed to fit your needs. + +## Who Uses ICICLE? + +ICICLE has been successfully integrated and used by leading ZK companies such as [Celer Network](https://github.com/celer-network), [Gnark](https://github.com/Consensys/gnark), and others to enhance their ZK proving pipelines. + +## Don't Have Access to a GPU? + +We understand that not all developers have access to GPUs, but this shouldn't limit your ability to develop with ICICLE. Here are some ways to gain access to GPUs. + +### Grants + +At Ingonyama, we are committed to accelerating progress in ZK and cryptography. If you're an engineer, developer, or academic researcher, we invite you to check out [our grant program](https://www.ingonyama.com/blog/icicle-for-researchers-grants-challenges). We can provide access to GPUs and even fund your research. + +### Google Colab + +Google Colab is a great platform to start experimenting with ICICLE instantly. It offers free access to NVIDIA T4 GPUs, which are more than sufficient for experimenting and prototyping with ICICLE. + +For a detailed guide on setting up Google Colab with ICICLE, refer to [this article](./colab-instructions.md). + +### Vast.ai + +[Vast.ai](https://vast.ai/) offers a global GPU marketplace where you can rent different types of GPUs by the hour at competitive prices. Whether you need on-demand or interruptible rentals, Vast.ai provides flexibility for various use cases. Learn more about their rental options [here](https://vast.ai/faq#rental-types). + +## What Can You Do with ICICLE? + +[ICICLE](https://github.com/ingonyama-zk/icicle) can be used similarly to any other cryptography library. Through various integrations, ICICLE has proven effective in multiple use cases: + +### Boost Your ZK Prover Performance + +If you're a circuit developer facing bottlenecks, integrating ICICLE into your prover may solve performance issues. ICICLE is integrated into popular ZK provers like [Gnark](https://github.com/Consensys/gnark) and [Halo2](https://github.com/zkonduit/halo2), enabling immediate GPU acceleration without additional code changes. + +### Integrating into Existing ZK Provers + +ICICLE allows for selective acceleration of specific parts of your ZK prover, helping to address specific bottlenecks without requiring a complete rewrite of your prover. + +### Developing Your Own ZK Provers + +For those building ZK provers from the ground up, ICICLE is an ideal tool for creating optimized and scalable provers. The ability to scale across multiple machines within a data center is a key advantage when using ICICLE with GPUs. + +### Developing Proof of Concepts + +ICICLE is also well-suited for prototyping and developing small-scale projects. With bindings for Golang and Rust, you can easily create a library implementing specific cryptographic primitives, such as a KZG commitment library, using ICICLE. + +--- + +## Get Started with ICICLE + +Explore the full capabilities of ICICLE by diving into the [Architecture](./arch_overview.md), [Getting Started Guide](./getting_started.md) and the [Programmer's Guide](./programmers_guide/general.md) to learn how to integrate, deploy, and extend ICICLE across different backends. + +If you have any questions or need support, feel free to reach out on [Discord], [GitHub] or [via support email][SupportEmail]. We're here to help you accelerate your ZK development with ICICLE. + + +[Discord]: https://discord.gg/6vYrE7waPj +[Github]: https://github.com/ingonyama-zk +[SupportEmail]: mailto:support@ingonyama.com + diff --git a/docs/versioned_docs/version-3.4.0/icicle/polynomials/ffi.uml b/docs/versioned_docs/version-3.4.0/icicle/polynomials/ffi.uml new file mode 100644 index 0000000000..5d0a1e3c81 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/polynomials/ffi.uml @@ -0,0 +1,27 @@ +@startuml +skinparam componentStyle uml2 + +' Define Components +component "C++ Template\nComponent" as CppTemplate { + [Parameterizable Interface] +} +component "C API Wrapper\nComponent" as CApiWrapper { + [C API Interface] +} +component "Rust Code\nComponent" as RustCode { + [Macro Interface\n(Template Instantiation)] +} + +' Define Artifact +artifact "Static Library\n«artifact»" as StaticLib + +' Connections +CppTemplate -down-> CApiWrapper : Instantiates +CApiWrapper .down.> StaticLib : Compiles into +RustCode -left-> StaticLib : Links against\nand calls via FFI + +' Notes +note right of CppTemplate : Generic C++\ntemplate implementation +note right of CApiWrapper : Exposes C API for FFI\nto Rust/Go +note right of RustCode : Uses macros to\ninstantiate templates +@enduml diff --git a/docs/versioned_docs/version-3.4.0/icicle/polynomials/hw_backends.uml b/docs/versioned_docs/version-3.4.0/icicle/polynomials/hw_backends.uml new file mode 100644 index 0000000000..bb96db092e --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/polynomials/hw_backends.uml @@ -0,0 +1,86 @@ +@startuml + +' Define Interface for Polynomial Backend Operations +interface IPolynomialBackend { + +add() + +subtract() + +multiply() + +divide() + +evaluate() +} + +' Define Interface for Polynomial Context (State Management) +interface IPolynomialContext { + +initFromCoeffs() + +initFromEvals() + +getCoeffs() + +getEvals() +} + +' PolynomialAPI now uses two strategies: Backend and Context +class PolynomialAPI { + -backendStrategy: IPolynomialBackend + -contextStrategy: IPolynomialContext + -setBackendStrategy(IPolynomialBackend) + -setContextStrategy(IPolynomialContext) + +add() + +subtract() + +multiply() + +divide() + +evaluate() +} + +' Backend Implementations +class GPUPolynomialBackend implements IPolynomialBackend { + #gpuResources: Resource + +add() + +subtract() + +multiply() + +divide() + +evaluate() +} + +class ZPUPolynomialBackend implements IPolynomialBackend { + #zpuResources: Resource + +add() + +subtract() + +multiply() + +divide() + +evaluate() +} + +class TracerPolynomialBackend implements IPolynomialBackend { + #traceData: Data + +add() + +subtract() + +multiply() + +divide() + +evaluate() +} + +' Context Implementations (Placeholder for actual implementation) +class GPUContext implements IPolynomialContext { + +initFromCoeffs() + +initFromEvals() + +getCoeffs() + +getEvals() +} + +class ZPUContext implements IPolynomialContext { + +initFromCoeffs() + +initFromEvals() + +getCoeffs() + +getEvals() +} + +class TracerContext implements IPolynomialContext { + +initFromCoeffs() + +initFromEvals() + +getCoeffs() + +getEvals() +} + +' Relationships +PolynomialAPI o-- IPolynomialBackend : uses +PolynomialAPI o-- IPolynomialContext : uses +@enduml diff --git a/docs/versioned_docs/version-3.4.0/icicle/polynomials/overview.md b/docs/versioned_docs/version-3.4.0/icicle/polynomials/overview.md new file mode 100644 index 0000000000..881eeef884 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/polynomials/overview.md @@ -0,0 +1,376 @@ +# Polynomial API Overview + +:::note +Read our paper on the Polynomials API in ICICLE v2 by clicking [here](https://eprint.iacr.org/2024/973). +::: + +## Introduction + +The Polynomial API offers a robust framework for polynomial operations within a computational environment. It's designed for flexibility and efficiency, supporting a broad range of operations like arithmetic, evaluation, and manipulation, all while abstracting from the computation and storage specifics. This enables adaptability to various backend technologies, employing modern C++ practices. + +## Key Features + +### Backend Agnostic Architecture + +Our API is structured to be independent of any specific computational backend. While a CUDA backend is currently implemented, the architecture facilitates easy integration of additional backends. This capability allows users to perform polynomial operations without the need to tailor their code to specific hardware, enhancing code portability and scalability. + +### Templating in the Polynomial API + +The Polynomial API is designed with a templated structure to accommodate different data types for coefficients, the domain, and images. This flexibility allows the API to be adapted for various computational needs and types of data. + +```cpp +template +class Polynomial { + // Polynomial class definition +} +``` + +In this template: + +- **`Coeff`**: Represents the type of the coefficients of the polynomial. +- **`Domain`**: Specifies the type for the input values over which the polynomial is evaluated. By default, it is the same as the type of the coefficients but can be specified separately to accommodate different computational contexts. +- **`Image`**: Defines the type of the output values of the polynomial. This is typically the same as the coefficients. + +#### Default instantiation + +```cpp +extern template class Polynomial; +``` + +#### Extended use cases + +The templated nature of the Polynomial API also supports more complex scenarios. For example, coefficients and images could be points on an elliptic curve (EC points), which are useful in cryptographic applications and advanced algebraic structures. This approach allows the API to be extended easily to support new algebraic constructions without modifying the core implementation. + +### Supported Operations + +The Polynomial class encapsulates a polynomial, providing a variety of operations: + +- **Construction**: Create polynomials from coefficients or evaluations on roots-of-unity domains. +- **Arithmetic Operations**: Perform addition, subtraction, multiplication, and division. +- **Evaluation**: Directly evaluate polynomials at specific points or across a domain. +- **Manipulation**: Features like slicing polynomials, adding or subtracting monomials inplace, and computing polynomial degrees. +- **Memory Access**: Access internal states or obtain device-memory views of polynomials. + +## Usage + +This section outlines how to use the Polynomial API in C++. Bindings for Rust and Go are detailed under the Bindings sections. + +:::note +Make sure to set an ICICLE device prior to using the polynomial API. +::: + +### Construction + +Polynomials can be constructed from coefficients, from evaluations on roots-of-unity domains, or by cloning existing polynomials. + +```cpp +// Construction +static Polynomial from_coefficients(const Coeff* coefficients, uint64_t nof_coefficients); +static Polynomial from_rou_evaluations(const Image* evaluations, uint64_t nof_evaluations); +// Clone the polynomial +Polynomial clone() const; +``` + +Example: + +```cpp +auto p_from_coeffs = Polynomial_t::from_coefficients(coeff /* :scalar_t* */, nof_coeffs); +auto p_from_rou_evals = Polynomial_t::from_rou_evaluations(rou_evals /* :scalar_t* */, nof_evals); +auto p_cloned = p.clone(); // p_cloned and p do not share memory +``` + +:::note +The coefficients or evaluations may be allocated either on host or device memory. In both cases the memory is copied to the backend device. +::: + +### Arithmetic + +Constructed polynomials can be used for various arithmetic operations: + +```cpp +// Addition +Polynomial operator+(const Polynomial& rhs) const; +Polynomial& operator+=(const Polynomial& rhs); // inplace addition + +// Subtraction +Polynomial operator-(const Polynomial& rhs) const; + +// Multiplication +Polynomial operator*(const Polynomial& rhs) const; +Polynomial operator*(const Domain& scalar) const; // scalar multiplication + +// Division A(x) = B(x)Q(x) + R(x) +std::pair divide(const Polynomial& rhs) const; // returns (Q(x), R(x)) +Polynomial operator/(const Polynomial& rhs) const; // returns quotient Q(x) +Polynomial operator%(const Polynomial& rhs) const; // returns remainder R(x) +Polynomial divide_by_vanishing_polynomial(uint64_t degree) const; // sdivision by the vanishing polynomial V(x)=X^N-1 +``` + +#### Example + +Given polynomials A(x),B(x),C(x) and V(x) the vanishing polynomial. + +$$ +H(x)=\frac{A(x) \cdot B(x) - C(x)}{V(x)} \space where \space V(x) = X^{N}-1 +$$ + +```cpp +auto H = (A*B-C).divide_by_vanishing_polynomial(N); +``` + +### Evaluation + +Evaluate polynomials at arbitrary domain points, across a domain or on a roots-of-unity domain. + +```cpp +Image operator()(const Domain& x) const; // evaluate f(x) +void evaluate(const Domain* x, Image* evals /*OUT*/) const; +void evaluate_on_domain(Domain* domain, uint64_t size, Image* evals /*OUT*/) const; // caller allocates memory +void evaluate_on_rou_domain(uint64_t domain_log_size, Image* evals /*OUT*/) const; // caller allocate memory +``` + +Example: + +```cpp +Coeff x = rand(); +Image f_x = f(x); // evaluate f at x + +// evaluate f(x) on a domain +uint64_t domain_size = ...; +auto domain = /*build domain*/; // host or device memory +auto evaluations = std::make_unique(domain_size); // can be device memory too +f.evaluate_on_domain(domain, domain_size, evaluations); + +// evaluate f(x) on roots of unity domain +uint64_t domain_log_size = ...; +auto evaluations_rou_domain = std::make_unique(1 << domain_log_size); // can be device memory too +f.evaluate_on_rou_domain(domain_log_size, evaluations_rou_domain); +``` + +### Manipulations + +Beyond arithmetic, the API supports efficient polynomial manipulations: + +#### Monomials + +```cpp +// Monomial operations +Polynomial& add_monomial_inplace(Coeff monomial_coeff, uint64_t monomial = 0); +Polynomial& sub_monomial_inplace(Coeff monomial_coeff, uint64_t monomial = 0); +``` + +The ability to add or subtract monomials directly and in-place is an efficient way to manipualte polynomials. + +Example: + +```cpp +f.add_monomial_in_place(scalar_t::from(5)); // f(x) += 5 +f.sub_monomial_in_place(scalar_t::from(3), 8); // f(x) -= 3x^8 +``` + +#### Computing the degree of a Polynomial + +```cpp +// Degree computation +int64_t degree(); +``` + +The degree of a polynomial is a fundamental characteristic that describes the highest power of the variable in the polynomial expression with a non-zero coefficient. +The `degree()` function in the API returns the degree of the polynomial, corresponding to the highest exponent with a non-zero coefficient. + +- For the polynomial $f(x) = x^5 + 2x^3 + 4$, the degree is 5 because the highest power of $x$ with a non-zero coefficient is 5. +- For a scalar value such as a constant term (e.g., $f(x) = 7$, the degree is considered 0, as it corresponds to $x^0$. +- The degree of the zero polynomial, $f(x) = 0$, where there are no non-zero coefficients, is defined as -1. This special case often represents an "empty" or undefined state in many mathematical contexts. + +Example: + +```cpp +auto f = /*some expression*/; +auto degree_of_f = f.degree(); +``` + +#### Slicing + +```cpp +// Slicing and selecting even or odd components. +Polynomial slice(uint64_t offset, uint64_t stride, uint64_t size = 0 /*0 means take all elements*/); +Polynomial even(); +Polynomial odd(); +``` + +The Polynomial API provides methods for slicing polynomials and selecting specific components, such as even or odd indexed terms. Slicing allows extracting specific sections of a polynomial based on an offset, stride, and size. + +The following examples demonstrate folding a polynomial's even and odd parts and arbitrary slicing; + +```cpp +// folding a polynomials even and odd parts with randomness +auto x = rand(); +auto even = f.even(); +auto odd = f.odd(); +auto fold_poly = even + odd * x; + +// arbitrary slicing (first quarter) +auto first_quarter = f.slice(0 /*offset*/, 1 /*stride*/, f.degree()/4 /*size*/); +``` + +### Memory access (copy/view) + +Access to the polynomial's internal state can be vital for operations like commitment schemes or when more efficient custom operations are necessary. This can be done either by copying or viewing the polynomial + +#### Copying + +Copies the polynomial coefficients to either host or device allocated memory. + +:::note +Copying to host memory is backend agnostic while copying to device memory requires the memory to be allocated on the corresponding backend. +::: + +```cpp +Coeff get_coeff(uint64_t idx) const; // copy single coefficient to host +uint64_t copy_coeffs(Coeff* coeffs, uint64_t start_idx, uint64_t end_idx) const; +``` + +Example: + +```cpp +auto coeffs_device = /*allocate CUDA or host memory*/ +f.copy_coeffs(coeffs_device, 0/*start*/, f.degree()); + +MSMConfig cfg = msm::defaultMSMConfig(); +cfg.are_points_on_device = true; // assuming copy to device memory +auto rv = msm::MSM(coeffs_device, points, msm_size, cfg, results); +``` + +#### Views + +The Polynomial API supports efficient data handling through the use of memory views. These views provide direct access to the polynomial's internal state without the need to copy data. This feature is particularly useful for operations that require direct access to device memory, enhancing both performance and memory efficiency. + +##### What is a Memory View? + +A memory view is essentially a pointer to data stored in device memory. By providing a direct access pathway to the data, it eliminates the need for data duplication, thus conserving both time and system resources. This is especially beneficial in high-performance computing environments where data size and operation speed are critical factors. + +##### Applications of Memory Views + +Memory views are extremely versatile and can be employed in various computational contexts such as: + +- **Commitments**: Views can be used to commit polynomial states in cryptographic schemes, such as Multi-Scalar Multiplications (MSM). +- **External Computations**: They allow external functions or algorithms to utilize the polynomial's data directly, facilitating operations outside the core polynomial API. This is useful for custom operations that are not covered by the API. + +##### Obtaining and Using Views + +To create and use views within the Polynomial API, functions are provided to obtain pointers to both coefficients and evaluation data. Here’s how they are generally structured: + +```cpp +// Obtain a view of the polynomial's coefficients +std::tuple, uint64_t /*size*/, uint64_t /*device_id*/> get_coefficients_view(); +``` + +Example usage: + +```cpp +auto [coeffs_view, size, device_id] = polynomial.get_coefficients_view(); + +// Use coeffs_view in a computational routine that requires direct access to polynomial coefficients +// Example: Passing the view to a GPU-accelerated function +gpu_accelerated_function(coeffs_view.get(),...); +``` + +##### Integrity-Pointer: Managing Memory Views + +Within the Polynomial API, memory views are managed through a specialized tool called the Integrity-Pointer. This pointer type is designed to safeguard operations by monitoring the validity of the memory it points to. It can detect if the memory has been modified or released, thereby preventing unsafe access to stale or non-existent data. +The Integrity-Pointer not only acts as a regular pointer but also provides additional functionality to ensure the integrity of the data it references. Here are its key features: + +```cpp +// Checks whether the pointer is still considered valid +bool isValid() const; + +// Retrieves the raw pointer or nullptr if pointer is invalid +const T* get() const; + +// Dereferences the pointer. Throws exception if the pointer is invalid. +const T& operator*() const; + +//Provides access to the member of the pointed-to object. Throws exception if the pointer is invalid. +const T* operator->() const; +``` + +Consider the Following case: + +```cpp +auto [coeff_view, size, device] = f.get_coefficients_view(); + +// Use the coefficients view to perform external operations +commit_to_polynomial(coeff_view.get(), size); + +// Modification of the original polynomial +f += g; // Any operation that modifies 'f' potentially invalidates 'coeff_view' + +// Check if the view is still valid before using it further +if (coeff_view.isValid()) { + perform_additional_computation(coeff_view.get(), size); +} else { + handle_invalid_data(); +} +``` + + + +## Multi-GPU Support with CUDA Backend + +The Polynomial API includes comprehensive support for multi-GPU environments, a crucial feature for leveraging the full computational power of systems equipped with multiple NVIDIA GPUs. This capability is part of the API's CUDA backend, which is designed to efficiently manage polynomial computations across different GPUs. + +### Setting the CUDA Device + +Like other components of the icicle framework, the Polynomial API allows explicit setting of the current CUDA device: + +```cpp +icicle_set_device(devA); +``` + +This function sets the active CUDA device. All subsequent operations that allocate or deal with polynomial data will be performed on this device. + +### Allocation Consistency + +Polynomials are always allocated on the current CUDA device at the time of their creation. It is crucial to ensure that the device context is correctly set before initiating any operation that involves memory allocation: + +```cpp +// Set the device before creating polynomials +icicle_set_device(devA); +Polynomial p1 = Polynomial::from_coefficients(coeffs, size); + +icicle_set_device(devB); +Polynomial p2 = Polynomial::from_coefficients(coeffs, size); +``` + +### Matching Devices for Operations + +When performing operations that result in the creation of new polynomials (such as addition or multiplication), it is imperative that both operands are on the same CUDA device. If the operands reside on different devices, an exception is thrown: + +```cpp +// Ensure both operands are on the same device +icicle_set_device(devA); +auto p3 = p1 + p2; // Throws an exception if p1 and p2 are not on the same device +``` + +### Device-Agnostic Operations + +Operations that do not involve the creation of new polynomials, such as computing the degree of a polynomial or performing in-place modifications, can be executed regardless of the current device setting: + +```cpp +// 'degree' and in-place operations do not require device matching +int deg = p1.degree(); +p1 += p2; // Valid if p1 and p2 are on the same device, throws otherwise +``` + +### Error Handling + +The API is designed to throw exceptions if operations are attempted across polynomials that are not located on the same GPU. This ensures that all polynomial operations are performed consistently and without data integrity issues due to device mismatches. + +### Best Practices + +To maximize the performance and avoid runtime errors in a multi-GPU setup, always ensure that: + +- The CUDA device is set correctly before polynomial allocation. +- Operations involving new polynomial creation are performed with operands on the same device. + +By adhering to these guidelines, developers can effectively harness the power of multiple GPUs to handle large-scale polynomial computations efficiently. diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/Icicle_Release_README.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/Icicle_Release_README.md new file mode 100644 index 0000000000..510a4de7c6 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/Icicle_Release_README.md @@ -0,0 +1,89 @@ + +# Icicle Release README + +## Overview + +Icicle is a powerful C++ library designed to provide flexible and efficient computation through its modular backend architecture. This README explains how to build and release Icicle for multiple Linux distributions, including Ubuntu 20.04, Ubuntu 22.04, and CentOS 7. It also describes the content of a release and how to use the generated tar files. + +## Content of a Release + +Each Icicle release includes a tar file containing the build artifacts for a specific distribution. The tar file includes the following structure: + +- **`./icicle/include/`**: This directory contains all the necessary header files for using the Icicle library from C++. + +- **`./icicle/lib/`**: + - **Icicle Libraries**: All the core Icicle libraries are located in this directory. Applications linking to Icicle will use these libraries. + - **Backends**: The `./icicle/lib/backend/` directory houses backend libraries, including the CUDA backend. While the CUDA backend is included, it will only be used on machines with a GPU. On machines without a GPU, the CUDA backend is not utilized. + +### Considerations + +Currently, the CUDA backend is included in every installation tar file, even on machines without a GPU. This ensures consistency across installations but results in additional files being installed that may not be used. + +## Build Docker Image + +To build the Docker images for each distribution and CUDA version, use the following commands: + +```bash +# Ubuntu 22.04, CUDA 12.2 +docker build -t icicle-release-ubuntu22-cuda122 -f Dockerfile.ubuntu22 . + +# Ubuntu 20.04, CUDA 12.2 +docker build -t icicle-release-ubuntu20-cuda122 -f Dockerfile.ubuntu20 . + +# CentOS 7, CUDA 12.2 +docker build -t icicle-release-centos7-cuda122 -f Dockerfile.centos7 . +``` + +### Docker Environment Explanation + +The Docker images you build represent the target environment for the release. Each Docker image is tailored to a specific distribution and CUDA version. You first build the Docker image, which sets up the environment, and then use this Docker image to build the release tar file. This ensures that the build process is consistent and reproducible across different environments. + +## Build Libraries Inside the Docker + +To build the Icicle libraries inside a Docker container and output the tar file to the `release_output` directory: + +```bash +mkdir -p release_output +docker run --rm --gpus all -v ./icicle:/icicle -v ./release_output:/output -v ./scripts:/scripts icicle-release-ubuntu22-cuda122 bash /scripts/release/build_release_and_tar.sh +``` + +This command executes the `build_release_and_tar.sh` script inside the Docker container, which provides the build environment. It maps the source code and output directory to the container, ensuring the generated tar file is available on the host system. + +You can replace `icicle-release-ubuntu22-cuda122` with another Docker image tag to build in the corresponding environment (e.g., Ubuntu 20.04 or CentOS 7). + +## Installing and Using the Release + +1. **Extract the Tar File**: + - Download the appropriate tar file for your distribution (Ubuntu 20.04, Ubuntu 22.04, or CentOS 7). + - Extract it to your desired location: + ```bash + tar -xzvf icicle--cuda122.tar.gz -C /path/to/install/location + ``` + +2. **Linking Your Application**: + - When compiling your C++ application, link against the Icicle libraries found in `./icicle/lib/`: + ```bash + g++ -o myapp myapp.cpp -L/path/to/icicle/lib -licicle_device -licicle_field_or_curve + ``` + - Note: You only need to link to the Icicle device and field or curve libraries. The backend libraries are dynamically loaded at runtime. + +## Backend Loading + +The Icicle library dynamically loads backend libraries at runtime. By default, it searches for backends in the following order: + +1. **Environment Variable**: If the `ICICLE_BACKEND_INSTALL_DIR` environment variable is defined, Icicle will prioritize this location. +2. **Default Directory**: If the environment variable is not set, Icicle will search in the default directory `/opt/icicle/lib/backend`. + +### Custom Backend Loading + +If you need to load a backend from a custom location at any point during runtime, you can call the following function: + +```cpp +extern "C" eIcicleError icicle_load_backend(const char* path, bool is_recursive); +``` + +- **`path`**: The directory where the backend libraries are located. +- **`is_recursive`**: If `true`, the function will search for backend libraries recursively within the specified path. + +--- + diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/hash.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/hash.md new file mode 100644 index 0000000000..a88bc996fe --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/hash.md @@ -0,0 +1,187 @@ +# ICICLE Hashing Logic + +## Overview + +ICICLE’s hashing system is designed to be flexible, efficient, and optimized for both general-purpose and cryptographic operations. Hash functions are essential in operations such as generating commitments, constructing Merkle trees, executing the Sumcheck protocol, and more. + +ICICLE provides an easy-to-use interface for hashing on both CPU and GPU, with transparent backend selection. You can choose between several hash algorithms such as Keccak-256, Keccak-512, SHA3-256, SHA3-512, Blake2s, Poseidon, Poseidon2 and more, which are optimized for processing both general data and cryptographic field elements or elliptic curve points. + +## Hashing Logic + +Hashing in ICICLE involves creating a hasher instance for the desired algorithm, configuring the hash function if needed, and then processing the data. Data can be provided as strings, arrays, or field elements, and the output is collected in a buffer that automatically adapts to the size of the hashed data. + +## Batch Hashing + +For scenarios where large datasets need to be hashed efficiently, ICICLE supports batch hashing. The batch size is automatically derived from the output size, making it adaptable and optimized for parallel computation on the GPU (when using the CUDA backend). This is useful for Merkle-trees and more. + +## Supported Hash Algorithms + +ICICLE supports the following hash functions: + +1. **Keccak-256** +2. **Keccak-512** +3. **SHA3-256** +4. **SHA3-512** +5. **Blake2s** +6. **Blake3** +7. **Poseidon** +8. **Poseidon2** + +:::info +Additional hash functions might be added in the future. Stay tuned! +::: + +### Keccak and SHA3 + +[Keccak](https://keccak.team/files/Keccak-implementation-3.2.pdf) is a cryptographic hash function designed by Guido Bertoni, Joan Daemen, Michaël Peeters, and Gilles Van Assche. It was selected as the winner of the NIST hash function competition, becoming the basis for the [SHA-3 standard](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf). + +Keccak can take input messages of any length and produce a fixed-size hash. It uses the sponge construction, which absorbs the input data and squeezes out the final hash value. The permutation function, operating on a state array, applies iterative rounds of operations to derive the hash. + +### Blake2s + +[Blake2s](https://www.rfc-editor.org/rfc/rfc7693.txt) is an optimized cryptographic hash function that provides high performance while ensuring strong security. Blake2s is ideal for hashing small data (such as field elements), especially when speed is crucial. It produces a 256-bit (32-byte) output and is often used in cryptographic protocols. + +### Blake3 + +[Blake3](https://www.ietf.org/archive/id/draft-aumasson-blake3-00.html) is a high-performance cryptographic hash function designed for both small and large data. With a a tree-based design for efficient parallelism, it offers strong security, speed, and scalability for modern cryptographic applications. + +### Poseidon + +[Poseidon](https://eprint.iacr.org/2019/458) is a cryptographic hash function designed specifically for field elements. It is highly optimized for zero-knowledge proofs (ZKPs) and is commonly used in ZK-SNARK systems. Poseidon’s main strength lies in its arithmetization-friendly design, meaning it can be efficiently expressed as arithmetic constraints within a ZK-SNARK circuit. + +Traditional hash functions, such as SHA-2, are difficult to represent within ZK circuits because they involve complex bitwise operations that don’t translate efficiently into arithmetic operations. Poseidon, however, is specifically designed to minimize the number of constraints required in these circuits, making it significantly more efficient for use in ZK-SNARKs and other cryptographic protocols that require hashing over field elements. + +Currently the Poseidon implementation is the [Optimized Poseidon](https://hackmd.io/@jake/poseidon-spec#Optimized-Poseidon). Optimized Poseidon significantly decreases the calculation time of the hash. + +The optional `domain_tag` pointer parameter enables domain separation, allowing isolation of hash outputs across different contexts or applications. + +### Poseidon2 + +[Poseidon2](https://eprint.iacr.org/2023/323.pdf) is a cryptographic hash function designed specifically for field elements. +It is an improved version of the original [Poseidon](https://eprint.iacr.org/2019/458) hash, offering better performance on modern hardware. Poseidon2 is optimized for use with elliptic curve cryptography and finite fields, making it ideal for decentralized systems like blockchain. Its main advantage is balancing strong security with efficient computation, which is crucial for applications that require fast, reliable hashing. + +The optional `domain_tag` pointer parameter enables domain separation, allowing isolation of hash outputs across different contexts or applications. + +:::info + +The supported values of state size ***t*** as defined in [eprint 2023/323](https://eprint.iacr.org/2023/323.pdf) are 2, 3, 4, 8, 12, 16, 20 and 24. Note that ***t*** sizes 8, 12, 16, 20 and 24 are supported only for small fields (babybear and m31). + +::: + +:::info + +The S box power alpha, number of full rounds and partial rounds, rounds constants, MDS matrix, and partial matrix for each field and ***t*** can be found in this [folder](https://github.com/ingonyama-zk/icicle/tree/9b1506cda9eab30fc6a8d0a338e2cfab877402f7/icicle/include/icicle/hash/poseidon2_constants/constants). + +::: + +In the current version the padding is not supported and should be performed by the user. + +## Using Hash API + +### 1. Creating a Hasher Object + +First, you need to create a hasher object for the specific hash function you want to use: + +```cpp +#include "icicle/hash/keccak.h" +#include "icicle/hash/blake2s.h" +#include "icicle/hash/poseidon.h" +#include "icicle/hash/poseidon2.h" + +// Create hasher instances for different algorithms +auto keccak256 = Keccak256::create(); +auto keccak512 = Keccak512::create(); +auto sha3_256 = Sha3_256::create(); +auto sha3_512 = Sha3_512::create(); +auto blake2s = Blake2s::create(); +// Poseidon requires specifying the field-type and t parameter (supported 3,5,9,12) as defined by the Poseidon paper. +auto poseidon = Poseidon::create(t); +// Optionally, Poseidon can accept a domain-tag, which is a field element used to separate applications or contexts. +// The domain tag acts as the first input to the hash function, with the remaining t-1 inputs following it. +scalar_t domain_tag = scalar_t::zero(); // Example using zero; this can be set to any valid field element. +auto poseidon_with_domain_tag = Poseidon::create(t, &domain_tag); +// Poseidon2 requires specifying the field-type and t parameter (supported 2, 3, 4, 8, 12, 16, 20, 24) as defined by +// the Poseidon2 paper. For large fields (field width >= 252) the supported values of t are 2, 3, 4. +auto poseidon2 = Poseidon2::create(t); +// Optionally, Poseidon2 can accept a domain-tag, which is a field element used to separate applications or contexts. +// The domain tag acts as the first input to the hash function, with the remaining t-1 inputs following it. +scalar_t domain_tag = scalar_t::zero(); // Example using zero; this can be set to any valid field element. +auto poseidon2_with_domain_tag = Poseidon2::create(t, &domain_tag); +// This version of the hasher with a domain tag expects t-1 inputs per hasher. +``` + +### 2. Hashing Data + +Once you have a hasher object, you can hash any input data by passing the input, its size, a configuration, and an output buffer: + +```cpp +/** + * @brief Perform a hash operation. + * + * This function delegates the hash operation to the backend. + * + * @param input Pointer to the input data as bytes. + * @param size The number of bytes to hash. If 0, the default chunk size is used. + * @param config Configuration options for the hash operation. + * @param output Pointer to the output data as bytes. + * @return An error code of type eIcicleError indicating success or failure. + */ +eIcicleError hash(const std::byte* input, uint64_t size, const HashConfig& config, std::byte* output) const; + +/** + * @brief Perform a hash operation using typed data. + * + * Converts input and output types to `std::byte` pointers and delegates the call to the backend. + * + * @tparam PREIMAGE The type of the input data. + * @tparam IMAGE The type of the output data. + * @param input Pointer to the input data. + * @param size The number of elements of type `PREIMAGE` to a single hasher. + * @param config Configuration options for the hash operation. + * @param output Pointer to the output data. + * @return An error code of type eIcicleError indicating success or failure. + */ +template +eIcicleError hash(const PREIMAGE* input, uint64_t size, const HashConfig& config, IMAGE* output) const; +``` + +Example Usage: + +```cpp +// Using the Blake2s hasher +const std::string input = "Hello, I am Blake2s!"; +const uint64_t output_size = 32; // Blake2s outputs 32 bytes +auto output = std::make_unique(output_size); +auto config = default_hash_config(); + +eIcicleErr err = blake2s.hash(input.data(), input.size(), config, output.get()); + +// Alternatively, use another hasher (e.g., Keccak256, SHA3-512) +``` + +### 3. Batch Hashing + +To perform batch hashing, set the `config.batch` field to indicate the number of batches. This allows for multiple inputs to be hashed in parallel: + +```cpp +auto config = default_hash_config(); +config.batch = 2; + +const std::string input = "0123456789abcdef"; // This is a batch of "01234567" and "89abcdef" +auto output = std::make_unique(32 * config.batch); // Allocate output for 2 batches + +eIcicleErr err = keccak256.hash(input.data(), input.size() / config.batch, config, output.get()); +``` + +### 4. Poseidon sponge function + +Currently the poseidon sponge mode (sponge function description could be found in Sec 2.1 of [eprint 2019/458](https://eprint.iacr.org/2019/458.pdf)) isn't implemented. + +### 5. Poseidon2 sponge function + +Currently the poseidon2 is implemented in compression mode, the sponge mode discussed in [eprint 2023/323](https://eprint.iacr.org/2023/323.pdf) is not implemented. + +### Supported Bindings + +- [Rust](../rust-bindings/hash) +- [Go](../golang-bindings/hash) diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/image-1.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/image-1.png new file mode 100644 index 0000000000000000000000000000000000000000..cc0ba59164d4bab76df84418f26f58d58ddd6484 GIT binary patch literal 225713 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�A`q+h{H5sTWW_ z<9Q3jiZ%7LWn0Y5=BLy+@K;g2?mqAAsk7+fsUjsJn|;tRnwKD8V+ol?pGu_r`0!jU z3dE$0`nP2OIpdwCD2G0ZjXq%q7BQ1gxCk{Hn_N=qR|H5`p>JA$0ZnhoY~K7HF#7`| zgix>=_aLm85YB9t&O}ZTnujMi-si8N-Q)jZnCA*iAdjt^WZryV*%imGzU$~|J+p?E zHN*H9lJ5~DJ8~3l)ky_7taAnrDPddN7uy+NxrkEa*DC)94~ArmEi6-3$} zeMmXR!to|p*|2>(_}ihN8ivXfzA$&n2M5OoYnoNboFDn!tyfl`BIO!iHl%OR#I5Ea ze*$SlE74j@$BUWusNWEBA~8rKald#)5EAYH3Cn<)eN_L@*0&iP-J>#lC(;`Er_j@|b!23zu7eN2ETJ#87%nZK^|0HMOQM zuR6KuIJ;TZR;^q4LJ(@5g|gof#44ZD#zxU&o<}pU#=p1oNJG*v}v8eeix2n*xjGbN9{pJ0zS zBb%{xHVx(Df>+L=`rlrg@&6gpd-F1NN)@*>LA3zuR{H5Y~AKQp;9 zb|<}~7zG{QIn%wsI#3|PT>`z=We8>%lKR43oyG6?jCf$s!})Dg`$N;qj{mqbyywpq z`lR5@E=~_c*}Y>|n+1;g*oKG2IL-O(m23nB^OJ<_21)WYXpd2xchy9 z)$OUX(K@C9J<<}1_v5!Gzbeddmt_?!W(gj@T_MMjCs3miWKbq(($Hr<$8b-(pXnMU z-Hjl5q-Gg@Ie4}Y)xW@;iH#?0K7ddkT*MP8jS@(kH5fuX90sFD(!@bEgJVA#{$^Qv zsbuC6%W!=#>KSDn&u&VcP%$o}E83-UV{gk(M>^JtPG?==)cucxognjJJWTn*ftlki z2iimjmH>dMruEn??;u}(2j6}pYjPqJ*n9Rlo#6S8+TYUWLnuk;`KrcGF~As`7YEOo zyiGq!MNGk4t~~EO{|$NTvooAQCh!16sBGz7Fdo104tk?GGwqn=taZx+=f7*H$Otwq z5j?F3_v}gz9G)AW`(6GTefSK3fz&&MYh*k4>>1`+{^hWR zBlYeFwx@Dq&Rz`yO7*Ke#y*=d z2x#ir+2)|wO6(oON@I%~{A@^Oq}PqNo!WV#g(<}s7O-jG7Lzpw9T!SKBRc9TcX+

{9exQoT#Bo5yBqw>Gz9l>lN7nhel$EPN3 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%0A6rw>)$i4%J1}m*V*HnaUKfeTU`!Z!;1SS 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/image-2.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/image-2.png new file mode 100644 index 0000000000000000000000000000000000000000..e59a16925ec93f337ebabe9b75aab80a3ca938ba GIT binary patch literal 220588 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�Oy*WMmrGm*6l4m*QU-`w5!7DgxG|KM%)IMQx$ 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

+(&5N0r%bJ&ii`av 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

  • ho~ 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
  • TTg5 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

    leA;ztGLEVF@)vJpH{z^H?M5GV`d;x&8n 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

    BKSUZ|mJbS*)V3P$cQDexVQVEfKXzPbYAsp_6 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%0Dt6}cTO-{`#6jGrGor7SG3KV-OcCWTK>-iUagv* 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%0Do4Ue#4C?Dt-s{WwqNy++9o&-RDWG z^yUYeGdoWBg_z#q=%P)T{y|=f{O5`;+zAWGD=S#G(q7xbK-2+Rnmd_>waPoF7|D{r zF~pYg^=rtlbU=cK+CDxe*`E_BxMNrL{k(?%W^e_?n($cP4UZp%EDls{fp(DLxAQo(WPYVw9Jh{ptU-pai{jx$xGJ=?Xls z)_N@9;JImlhk;qmw>M0?|Sk7`W{gIovj{Jhxq|&Lo;ANrS30{bvSZ6QIcLs z)OUUuh}QWY6HRSxZPW0SI*fxCI>PV172DLDsQ77C~xL_~YV>k-O<1w5F<4%~rp5SCTTexyGy26|{ypJ^* zNl$NB3k4HoqWxLS>!N{hcnpjF4 zi1_rWbkYp3;XSn^%S6A`?AIlXa+&&RxOr*A(%YfUa%z$jaJ&TqQvBn=D^RG&K7SWB z`6*Y0PgjEEQh-fLF+9yvN@Na&t(ODK*Q0!r0UcqzA0Z{0GB|71Gpwx5o8zLhY z7o|~cr|tJ0IF~1LQ7eZ%B}ZWfUq|T@J9TqGpq^AZn(5?UomTJSfOhDrOJPc&rFp0w zGj!V9oRJN-SPEd|j-z< zvA8bEery3J8{-Uq=8bW1AZ@)%N&B&)yPK-!;~o<78dfQsaHXz>QQV9JH6OLgQz}Yu zedR_{-SXx}A3ro)#{aoXMI32|UzNIbi=jRf$tzpqhzi>+IbGX}s2tF5T>R~YL6+K0 z*DOZ1ZsC&#z64-yF%K7B@9pN9xvp+F%cNQm42x8(bSRUr{W%z{_C3zKy zOmDI_u!iEz>u9&&J{cwoYDG|(rg*T~Q)_RkwJ5!CyBcVzyODXWupx6E=18R+s=$f4~ie_ zJhVhXZr3>pq9J|1OBg@*YfUeaQyP>I+ZV)>A0~`64;=x^no7P+7QyH(D&kGeDW)zi zqN4=Ve#**{K+^om>Qcvi52vuYo_kC{Kwv|v4M4dVpYO9&Ic~g%)>?)F^T6@^Fr&~$ z>8g9bu(}_v~YX*3BscC|YdLrs9zdf|5e3+E2|Ylu3+`|?Y1re?cBdp>}K*Zo*k zz<_D-_6hUJH@yVj+AsbKV_oxWWsmty--t1+Mi z=sv#~a8#iR9>)H)_uJYy*<2Zk)`Bq{eRbz@N+q^D60pG;G&?OW2l8@6awLL5!)+u( zHEQ#Rtu8yyH32rxsEoj-e*T=vTwB))jKL!*@@m{+HmwY%s8;nzA30RXIk=0?C@X z#YP-#SHb*uBSKR4zd!b35RyWI5RSVX(yT>n$1_8H-jxB)?MFY7;~m#`K*ubRY4m2i zwo{A6X`kSlm`s0Grvx3|xlhI$*|Jy!xrJv6`{uZbn-G>{pA2SJ_?<2c!r1|n@pv&U4GHSWN1(aT5O(>rqf~Y?UYyXR^sQ1~-y9^9^D=lsw8-NgmhkB@hlMxNX9ep$}vE_?N z(WX0`PqA18HS+*!*4HP(2asPa(Li@xn&gTwg9Xu~1hyYpAl1a!%uMFCFlOg;=LNOT zaZWEK1A{0&0l}&-@m&_X_RLe}`YpjO@1NMhB5>%L@X9C!20>$&!*@0zsQWS8zP=&v z@MvT<2`I|*6rNjH&eyUh90ddfNIN)uWiM340sLKDAaQ%>Xql2(T5zbWYMUZ?xExuo z$&XFGSZb5a8=ZKdFp35ss+0t`(*|ma$R(ul2RP?P@SJc{4g1=|@$uLpJ}0@^)z99& zf_?q85WNC`9lCg9mRBeMH45n52msyS6%TSUWD4Sk&e!ycA**+KfEJ4;OJCmvILu>! z$&s$VqX8#FbWF_0=0^5?2EP2<2v)Pcfq~cNofDZ47*Iat2nYFLMCkH}KJsg&0cd7h zV^LfVhh~QQCFLXRw)5pS(=?eq^nF!4{_Jd{A&D(Uk^diwahK7;7a3X9NOE^KJZQY= z3QrpF%B8avz$-*vRbq{5S?kJZ-%QNRC~7ys>OeOqmNwbug9mNMM}iOj`k z4Eiqqa!IVSA6xibnin@*QtY)1NSM2OUP9ETXJ(2>Xq8nH7kkwQ5C8s8{f87eUj>Xl zUJiU^;+cxirmaeQcmcxDc(0Y{uw&9SjF)wR`kh81_1)f`_xKgnaM+)jr&smx@PgIv z^Xl3-THrD4T)~^2TCVaF50uG&(Ix_92AfezxPU?JfouL(jkqrgP$bR!Su7_1=+|L* zz^{AsL@E7~zb)MU0$^13JA-;+AGqOvm#dj(_x>lmw~`NSj{m+jW+3CS^92dM%0Kfz zrK16SjTs@{gU!{yZmszpkao!IWiI#6d|@7v056qpZ~Ra?=)afk|1J98Kb-$r^os$c zbHukH{mZUvRPAa1;NkI5ZAV8Zqytp%#K?`yn<*xJOTRo8WAfRI$3av4jS&EeQY|bu zMv+u=*o#gW^;%HSx>AzUvKrHDCqZ|sb!A9)tFqw=Fms8Ef3KO*%B`=@?CojMc`P9= z8JdxiF)=wIRT|WG$26sbi}Ux_{$DRd88C=)c?+sPlK&Eq(nwN))06;%$ab|KD0!Y_ zBPxD7o+a6<;g5l;e~8IhcMxq>wQkX4Az- zl^}NK6}pYGtgK(GhMnwyyGVL@8U1@e;2Ig*hv(=cvYNEx(x<1vrx zZW!27EjO~Cf-Nr}Tw$0)!OE(&r>?8Q=d4rZj}_5R=RSude`QBnK-8- z;%QV&#wRejJ} zgS&P-6fZK9dN>vz*dZ(K_*5xn%n3QcXrv##ikufMQ&fbc|{5-7O#J z7U~!-=Qi4z#Bm`D0QD-jqZvP_Jr9h46i)+C(jJnxDl9kYJIjPY9yU#l-Wag{mXN@m zrBIAZ9vR1})fV4q2FA{SlbLrvj5tku0j|Ati2@)5&|P!RaaEe9R8sQXv$y*fpZ3d+ z*-QubG^bwMiZ&GMTm{PCuCBrNUse~`p8;Mwv);FA$WM4SCTXdtRB;*VCCjQVq5bb5 zdblBFW3&{8<(s@JchD}I>~ivvd<7ED+4|x9N!AR*mDF7^u^+r}^7%`F&P%tR1l+de zYa5tZa5Yr4dHs{-pVo!$wdkd43{Wv2gD73%AJ(}lk1=jE(zc>TdjoQD57DLn#5Tv_ z>?!-4imJar1-w3EM&>*bVgWk~%ev^TgKP2dWM~pyPs%_iG5W^|Gy=C40tp>nzO%ZS z8F0WEwYaY%k- zl-;Oc^@MZ(Xzc=7|9}jY)Lf1zr0|VFJ9*2<@kY^ld&k1KMHP?kr|bATFhWv3FzGoz z&j2Bf)&4EfT$|iwE5O|WUSD$<60u4d4e21ETc+TpQ@0WNv>Cw8$wBC8rCtGNg3?nPggH?*NO%m0=ad{u%+iZkE3(7WYut?Fly&Q>%f)pnaG8gVsZsGfEr1(}gZvktv z6j=$+|HXd1{8ac>IlNqo;3ws~nsXL`df^&c%cpS83$AH*o!(cKYSzzaRhRsO1ixwM zyUK&k90nLIO$HN!q2btc?c^%R#Fex)vVpTaBGfqSM@>rtCN|@3Qxz03)s!6C+S)0* zOCBdelCJ0bMb$pxYHn9JkXDk2{x`gOgX@-0sggNc3Jz0N>O%r8)WKf!1tlsu63At- z@p^9BBTZ1Vg~q(~Ull*xVx+HcPg@S}bS6u&`@}x)$kV+Mqjn7ayrx8mCV1*Dph%^G zY^rt9xpXF|dz%HrsH(gsaNMiFOF|jV>k;}b9@m)z*6Oh!4k+Br-vys;#d~XcA8G2o zr*=oRhU83`X(Q+e`c7)q^0FcVAe5ovI<*1~v!?VNu&1SX5NJeR|Z$oBGDY zBNd>N_o;p~s$kD+r{1Q+Rtudh)+#ey(6{^RV{PEUtC^aC`e&+W&zu~)0%m!-)hcI; zk2i^i`MN&NK*Ps4Xc`&ccIp`}-HOD9gdjtvdg^9bR@;3SRxP#=vQ)U(fY=$2Cg=j* z*+#oYHyimL4cK(GB#f?nKyfEK(^dUu11pzbIW(J@Xsw|o5Ev*iTa^aoqv94P%aZ0R z%r33Xop8xxi;y-a@Wo0s$p1fZ>OcKgJ2I@!bNNsr0ku^l_}vNbwbZQ^1A=!LzTa^W zLS&=Z8Q+TL04L=PbW&Fv-CquwhE*DB6%jl$y;Q%l?P_VVNTSd?IMv3wi^=qyBcBAR z_Hf>0QlAcskTk-REg784U^M=zf#u(|o*sJ_Kv6X^1iqABa@n!|no z`dZ4k`ktx4DMWP3Y$my27Kh7V52@km$7{RfXn@oKd(+fv$dlW2R|y{T zobmDTPJp+C1^X^ToP672XzhXNqU)Ag!|i$PJ?&%^?QWLS`736|sG;{u(GlUK+Xs=S zk5EFM`0BkhNQ~x`0+fTP__ngKvA6m{rIEXgtld{M)%6(I@)+22Eja7iY{NpojLm6l zCz;$wkw~U3esf*+)ql{coA|bGiO*sb%761aOWta??1t=iVkC9<)v1DSes(albKscu zG#ycm`*wv_8b6VVMLLEGIGwZK=~A0r+-t!6mbZh)33#Tu=7x zd_g;0tZI8;(d4M2nJPRS9FZ&dovdt#@mSW)oXx!kYt0&)dTG7%%PgZ)OI2Gjd;7w9 z&yfo%AbH|HnMP!|(e}&(>gz22k{Ud`I`|O}^-pVpj|G?C zJFpBYG=pPOz)b4TXS5!kb9ZifYVxTG2TrG>%Wo37EDKt@y=`8kARr*1yQc>f1v-30 zMsd1*IXhJ|d1m!A#AXfyLoLuO1-J3K`>>y5$xF=R*3aaIV<{>+MGP})wANBotxqe< z#t{^FY03O+0gelwgoKsTGNd)c*NuV<6LWKP{QC>E5OySxcDjN>?nEMZJxYk5;UN>X zpX5#~fMf0g*OAO*`4QtX>Chx|wjLGO5D*Yp!OCId>P5Ek-_e`b%%t?Bvbx57h{BS0 z)M$)MjV*vzFYt*5#(Voh2~MgHquk16-@eT+*7I!Qzq>3eJ7sV9*CH_hdCozleY_Zg zZ#bc+tv*$4b*Qha@;Xee^d6vooYkeJwRwg7{HbnkVLd4*x6r&{HtqT2b37k}o5lIS z=oaJzEf1hfb ziXv?wYq30}k)VcyTEhaFl=uf<7#ybX*f+dU|LvXEP8%8SKuufN=&cc2qE>nG<=<95>t1f(qfSXGA%)vc^*+Bn;ZW$ecc!22>gufDJ00Du-O{!@TqO}a zx&mDhJ$Hz!z=+UeuahkZj^W{)!)x;?cBU`%B79p{d^8nJHBbX&0Ak zdGdqjDZDjS&SdUoup75C7%p;YJdOi<<-^Gkm`ukeCZ4 z|Gd9HVQr(KlVDS=>%1@&oxt4%Vc`MTWs*x;Y-Q119b#d}`!mR(j+-3GD|^sBI7M7s zvhU_^?K2?`ph&{W$$k16-@fi(4=L>N;F~o!cuAW2a?P8o0L;(^ey^l zoqaVgR+l!4^l!uS*LWqvWMYCyZ(8z=NmbDUcxOd&3Q;@I;I6i6D9HuKuZu`9T3rt# zYK%#e&RF&@Dz!_1p;+(zZiU;D&8!0sDP(O$G7hhh8i}$hw9?Jr9q+2) zcBYVe{^DJ9c{#!rbh^z9$RWwxw)L`#!x@!C@ca4-pu>dv_p|rAmh6^%v-zovlPtpO zkh^|}eGV|dJ(RGv+D$L+xi3wkQh9ggc%FGjf{spQ43?i1g0e`2y;vnf-=5qG~H#+`&orBN^TF2cdJDBh46B} zw3F7io4g}aM;7w1@@skO1b4gaRzyp0HtW!2;(pi>wYGXmj*0`UGh#ppeuq%aBV(Kt=~eJI^Cgm)p*uG7`-5LAMh(WFbgja$Eu{i}dFKw{J$t<$ zU*vdm7QFOUO)apcf8#TTLtVH9}_s4hq8dagMv1F+gFi1Ww4$0-UOln6HSZZsHiC?Jf{>repIM_zw{L#Cx3{nLSR4~h$3xI`>mW{m0Sfj} zGdLsT=i8Q+R#vftgT@a?zS(qt=YNZK{o5~oOFj+X{oRw-lk!N+{2OL11Cdoy(g!U6e;57li_vtchX%_3+@hDC_P#># z6BfewjTRog*+W=S!+v&!6M@enAN?5xkXv|#K}P1;Qr6Zg4*;DDr2>LpkpRtF)@G@G zCyeR3ktE>xqwn!D8+JL+)0L64s+nMf??R1Mn_k#aovwILomCLHZwjCKhOzw#2B?Ef z#{0=+ZN>d?g+^u6=?xpT7*zAZFqAE>}}azSr*$CFPOV51%rP zO}jgt8wUu^N5@R-54alY-mT>xLgjD#@bLc}qW{Zz^tWHwcEd6ea38JQ4cVX1_tA5T zSckU9Vk3mejoIkyrce_kld|PTH|%LCrL`t>cD_6X;o$0Qs{@()_RZC&O6vs`&!|8S zihuUa|LJGKWH|LE%i?6ZTmz!*l)iDBZYzM5THy)EG#M5A$!_kfReCSUTOGtuX*PM^ z1rqbPpR0lgUOnEE6`&bCgrNn>^X{b)eL}*$oPvr9FgZcK4U#IZiHuIa10vZQ9lw{) zWpuj(tr;BvT@-uB*C`{;#MDyRcf>DaXoezhubum$XL~8LFDbJC&A#jI))W60yY11H zXhG+S=9A%~O3P_e_nM;?0BT>|+oLtBlJ7ePaml=UmokQ11otsJNABlg&cRNCa!^*&eIyg`X$PSLkGpF;kn3yr{LhliN>wK8&E9Zj>meC5F(Ix-gPf)U zDi3;0%=A$yCo`C3aB?X4Z{C)@Z}o^J=B|IQZN<5k)ZF~Ze*Y39i{EY1N@vAwDg7gYDrt{pbo z4Sh>8EF8^r<;vQ%E?wuvJa`TU{n~!3<$zuH-=jgtyYwRk#ow(PKw+T-Y)VT}fDO?% z6ANFl^X%|LTmH_qjv%kH!}G`@IJaW=mBVvu>ozW%MT2U&G!})@_jWN#PVrh!LtAGw zHMQP1YY{!Sw>P&NcQ!%{6ciZ;OgW^Fb*Rv{m7c0-WKB(~_wN+1*`NN>>l+@zE*dIy z*5Fet)`3bOqBWiB$2oHdS|Oa8{s+FCwEBOKFI#}sQzqx8SO_h)ur;J_Q(CWj+ESBh zKYD^+fyKc@SM*WAKM!n{Vns{9EFZ*3(oPoL%k+cI?Gzq6IJWa1jO3WG*Lx@SNTa~_ zSKi0{pUu`&hwfv&%e-aoZYD>l%9c~)bHY&+0azRsnklTQ^EKOC1d~n;&#dof-odyZ z{zqsO$xfXBjj6u3@l4iMNtCNIKLwk{eSf}eJ^p06lj^V5Pccf~1@D0AyhRC;DkT+_ zse0>uHX~1-^8DF#ZlSII?5DI3!^SBXSlMg~Wuqh8o14(@RcpZ)hNK2<$mxOax99mBlME$|} z9R$aqGQRm1s47jiQoi}EHp`-x`OzVTF!#1!b{HMA$SNx4Pfn@nA2-sEt@y%wz4rAL zk+0)Sp3@@)vKmQJuS=yo&NGd>h`XIFc}Hw$p0Mh;xaSJUmB7fVaN;CSNQV}Ya)O%1 zJi06AUHrF#qV@f|dl>_l_vFHI=B2(HWa#-@M9G9|VRBz%ZT2>6$$|!Z`$)^0TU&d!i3o`; z*4^LJkdq6mrU$Hn?ZU&)=*aN*p%T?=9P8IXKHXfx`XAI*2=d?#y^fm3Jy;@*0ALiu zFyd4Yi0Nl@JRM;Fa(rB@R$p3N+=vA6^i zsAx9&5lRK1!Cd+#`p;`Ry^p;_!b`a2>~-wHL#8o>NyBs3P4C?tOy*c%3tO^_Z>jT} z?lcO${u5%!d`3^-Z!)q7kyj~dzO0-ph!$Uyl{+*(;xnVe-d3i|=J6ql3@4554-I*ghK-({Bd6XsJ0yIHZRh=}xeE9;<^fd5?;NV;+^Gh$6m_cs_pBAYq!q#I@S#< z)&x z;8@y7Uh+`HtVs_l|BcY#bY(#x;MUB=7}W5XHM37VvZl_^(zmm1-*8G&d|+ZHN?R=szo`iannwBB!fWYKY-wJTs@iTHqdWFEsA=NxX! zw#jcbhcFcf-E9LJZBUZi`f#PJ@%0RNZ|0#Pxmg!emgGTbjv9#{8Pf5{Krj|r@NOAA z&iu8plJE1~UNm>*rVc=*p3ME|t5?x-I53jdkn*GM@IF}O7HFWF?js!CZN7kp6Jtav zWjcjlLZ|Phx97%sKWs;`B&T|N&Sck!p?!U<7Vv4$fqPd{JIqWZJAkyHXI`TP8VKjw zxVjfSs6~5$a0=%uM&w0(t$$r$2OAU-A?@)@>?|Gey=)Az=LK5jNyGJNPT^nZUiCgQ zp8W<_o2OzWaroTtvokh78lX$IKAIsQKb_wR*w()W2sG3sC00Vo;UZW$Io-#-ltNLg zkS60w-TgA%Uln}?9>JzrWdIy^e87qu^69r)*n9z-DR)+`tdo;p%A*g{WOmi?y*-_xEaJ5j zkV&ai>?IXB`CDMXFeiQ8PgT8!L}^ZNIG9QusIQuHq`%wPrvZICKdTB434tg+-^O0W+Tm&%RCi0IZx@C6Z^< zc8byYsjuzrE3+#y?xHV_dr*BvI>r_C^Wa1B9q;$eynn`5SY1obTrRrB%QhY+3&s7a zug!Wl$%NB(*?l1n=@EI{Frblg`Fx~1xn}L@uzqj!K?~`@a7o#_CCF@uCML`Rj-oH- z!izZD<{&xujI=U{aE9P_nwn=~h9ckZj!p?}AR7A9q^~#tB9<`$99*gQv#W%6m{@%6 z+jWk6e6Vu(geH$cNa=hqxd#rp%nz`f3Je5NF#=4vm*|9}P8o`>^0v4*t;qR=7)c`M-I>1b$$ z7SU@9EDjOe&CM+}KZ9~oQc`whWigk06whN9C`^WH01g(svDG4kStK8w{Pn848*cH9 z+U6sAg->8S727A_r@NvbdwhT6IY$8S((f<9@{b0Q2OxYD??FxlFwcDy8u$w~_mKh^ z)Gl4(w2$u9hl|+`0X}`&o!6iAN0;7C2|%6vMwwWT@womfYfmMBPeWH#KyrU{qsvhM zpPZCs>=EO-zvT_k%m)XFqk>ABKf2N1Zvns~ZSdVeyJesq5!Yu)ttXdN7o zL~Q(C@ni)ZzXJ@ZKYyG#j{6oLe-4qTdXysrESn&;LFFa1K_@aIB7#BRBZfiO#lNc? z1yFBMHH(SRdQMF>@TBDXWE&i(jn~8z&DI-9N`=RhOaS_|LAP~3=!u5o>aAB&IR$t_ zG_2ud-{zaER@aQj{b)Y-CPZQ7OcAN~5;GSGtAPRm7w_2YY-|D^`;-UU3Kal{X`%Ih zEN}m7E2})zvmT6I(VV%AA2JIXl|!0D?LbK^u?o@J@7Q?nU|0A!vQlAPg6JvS4<3C#u)HOoS~RhNLpFjH%lp} z6ai;ZLH=?zfFW6SgVf05|91q?!_6rNXIe#Vy_Wz42f%#PKqnj<53hbtM&M6^Q;Q_v z*$_~cmL}NoQ$2b9s{6Xz$odc$8=t`+Ga7W0KyW-i;2kyFxT{x%Ph-CUEAthJ%C+~6 zC(H_l60E#VgH*?#(CGml_D(*ken?q@#~>74+W+&82o{bSsB)_vcjnXYX9YI-6LXzE zUV^SxlN;^m8_z+$ex3Zrt}C;{0Gja!5xH3;-IG2zq^btsbcXMU8kX{xK^P2$;*6&k z!|X2rAHa4Wasr@W%6w^*^Nz38AJ`p9$;l{Bs(zqSOHw~ti9Ga0+%kmoNND%@WPWWy zfA-)3Hfw6r^WzLn7dEwAva9VTlnS}`NL@YD_#tJ5rR>L#W-x!9IzupVugyPY*Op3F zev+3Fv756)R!>$Nq^bI)T{3HpNu9c>pAHcm6peohD=3D>k{2t8$I9pM|^!{L@Xy~-Ff;x#kF-3ekB$vfiRIJtwp-6v~T&SZA*l`)mZo^ipLU~hb_GP0w&J8^->CGhBzke0qRg2vqd&P z_h>AoT=#`mi4EQu?mJ|8D@h+N=ssvH_3%RykIYq0+k-nZVSu2M)sl3mK!J`+N&yfH zpa^**DVNxkcCY_ri=~zL^i4s+%}6l-6q1WEq3NA+P&iAYTb!itL9viMoNZlBl#r_hX6r5k}1AkB;*)yWXsntN$Y;BWCa$`rOqd3>gTRKoKyrP zX138n%XO$^)$(ZK*{gk$GRn@*jbUBWv5v*^!RWF(Pa`|WvL>9XPn zdzwp57oK&y#M$dLeyfv*kU&> zE&W#DE;2j%gPjUw!DSCw@N`64_aCe&`QHTbM$)GK=+poE-T&|m{&&6qF~t23uL;Ag z+Zh9?uOJgTfY5O*c?7lg=cV0M0osENx<`O1vI+FGL0MJj74!WTLEY_#$2S>Zvd0Jb z1LpMs>e=Mw(A+s{V1_5)v!nw7dj=CdUH|m8Jid^cWEkm^A=Tr6mhjSlFbXPoADV^N z;bg(m*XLALzb>OqwoKi|V*yt3Zvv$3-0(P&XECRIUgeMl*+m|Jv!ts2P1;5Lo5$U! zy8beG52})qlKAFu)PH#E{!^d(-_lPm0kx*e~c*2c)WO91`?6*7!7YjuhUCG7W4ZC*)>Up zvE*XTA|U-7*X7#95MD+?1c1@E0E+8%0W^@m4$ZEPy+b9PrJUAl()OtFDQwsYk2HG{)Q$N?}yS4ZRzZfXH06^53nCM8h?k5KoVc-{z@nOqKT@4{fC293Tg z$Y+Wjx?b9M=-WpO0SQl%_c8ayE|6Jq&fea_tV6H##|QY(+Gx+@99f+^F%#MC<6z-o z2NF+c*M*YP;wt-%){-_psms1q3^3U=?ajby3UhN#G&kPr8}+5kX)e^{Ja8S_=5$y_ zNaS>Cz5LKrabq^XwrW}1TRnDSG8`ZQ)j=LO*?{OW4@3L%q{(=;g8Mkd+b4LsZ_K%|<5?M`4KZMP?eUH*I*XRpnIiY^-m2aZUO??eC zSMBW@GsGx39IN#Tfl(}AKqV+n)=Y@kUKlM!QGoVc$^*IFt}kQ~6%Sx&vO+gDP5O2w zz+tS9=fCN7NDlP2ogX;?-IfZdw#8myPWh#%jc)v$>lHlg*S55_bIc~a2a?Wt*2*!0 zhxJ_vD<`OOlIv#!O3LN<_6h5n^!{kS1*>Q-DK<7m2i z!;WvD<91{Hs1!dow_C?g)?;A0S3@y2Zs^<pL=<+^A>|A&)546PpYhgE) zGtk(Xd7ienQee!Fl!DLVQOwY6(ZF(0ph5;Hl6;7KjFa||`0fs`k4wto02O-Mx?)ez zLPPcRsqUxgx^?%Z>-+W1WF}Ue=>T^S@3ANV}EsIOwyj$>V z?l-DsDpuf1R7+p{y|W0Ue#O_E_C7a9cX(Z3uoQC>R0>W`vqtyzxnnSylP-7ntsJWhN@IF<0V28IQ!#yx&4FGraM9>Y}ElrcWKz2lAKdrIl5iCdA=A_);F9* z#0NpKyZ(B8dgps50=1)@Lu<3t*-B6n!CJaMN$o);T%k}p-a2fh+HJHnT_hiAyVWp8 z4WE_}7booQ%=DmKN`2?3mLo3N{iS<#E_Gl#Hz!eGH9z$m%s z9}vnTbF>SV{UEtqiLj2F3edMEUleOU`O(v|7v?Sz1m3$6%X1=fTvN#-x>B2sA`@y{ zbGC=fCx7A50^4v$YJ+> zwI^>f{<~^=P}8Ylk2)~nc2-dQI)66 zzr=g8f?(~lT-0=(K$*gsB%`e;9BJsJQ=;NiRJt$%UafnkA_iPX6yP%Z<7y6)bbjWc zlG3j!(+v1*i)1>5Zo9BcDH?nk*i+qrhhN&AtlemT5A0T5x7+kz6sfanfon_{VR8W5 zPp7alr(vOedRl{URvyvLeFiX$UcWqw2t99v68=fGv^@kv3MZfNj)s6E6T-YUCA*=b zMRu{t!BQv4q^RnYF@dy4v=_>&ACeX&6dOYkBRL%XO@#7{7pFHJfYy+Z5TfeyVPDe? z#AgQ>x-A^-Q$p*SyodF;H%j{o^x!!7ZLe*pD0s&TT8%r5Se=Vm`{8m@r?s@zK2|po zH8nQ&Z)1^hDIy^shgRk#Zf|pESpt;ADCcIvvU3r1cGD4V|FtssC-_l%9inD# z_7DKb5cUo8yoT#8)h`d1Pfc(6XdP0)op=>BumarYoO z+$;;3ua~deZAT0Q>|Wm}ZD@Wyj9WUZmfASNeBi1)DUU~|rW)T5HbaxKlK@n1<~Cck z=gveGXwqtJ@a{ReVf}w4Tj7;yY%HhSy4OrNJ38hlk)jJJh7(ecuwkr3xtSFW(Uou} zZ{dw&{geCsuNBj8)~5sC5I>#1hKGCJ1Zclo!OuNJ=p^Y4GnZ#6gp^IND+ zQ(=uvupZS5Z;2z)6q#q+8}6Gj^t=pGM|2Qjm=$OK@kAa z;rn9AGY$vlmutQ*W3nqey|6)prJD+;w2K8AaG|>ZOOW!=Tv9;c-ltYLI;rrS97Ull zkL(|F>c88P%W^QdXYm)D5RkK)T;PCfP+)+p&gqkulFC9InSr?oBwJSz=EC{%l?{15 z`N7mCO+P=soXX61=lcr>>~sAx)6eBoUvVSeTn4pq>Qxqfj-h2+2Uu;kyK9;sK3a^1 z(7Cj$AvM&?`WMwIC~GJt(Vj0*=SC#b9w`AM`Wce7#4fU}=U0aVkpZfS+(wOp)rDn+ zx2N}Lp@h+<@6PmXZo7Nu*b4l?zp8Y-&`8*IhSy1)ciB!?qh1!WE)5;v1qQkT>F8N{ z2H;_uwBx%=HGAYgwzcmlC(zWxkP#aRidN5N4%T6phG@!DU~%^{8A+2_9aIL{y4 zNT8KT9O&4~-BnD+CmX+J#H_&;@Nfmf-^^9!!fEa18pM9^TWR3N0(k;8Ta_mxNu!eZ zf8C`2q#lC?_~aC+p(qg0pHt!trwTF34Tb4C=DqNhMcU_YRBCnlaX~%GtQZZC8o%WQ zZPm1zt-zc`_Ih75)T?iwZtcMBViaGj=Mv-<*a!yp?b%If=#c$LAsHbhWEy%VK3i|r zGT(ezgt<+Yedus>d7&Q%&2{q}vbcfv@U}pdFO)y&@*IX#zPv@wgo!71Ub~#zYl9pg z1CY_KFWQ~g9jo`E@=TT4`}kW(Sn#Yo27qn59kt)-LOmf?x}M2H!)-Dj$7j`?XCH;4 zef1~}ENU9OJ9t2%*ZPHreCsnI(|T2`G{-Z5jFbF@67yw~OyRAW!a`olq|KYg0-uL= z!WE=SD7vzLHRNn~jeDV|Ti~Pi??8bW-s-fm@MT321iFp&G>oDetr0HvNof)D@8504 zc8_K4Pd&ii`_qL`*%E~_rKgf;f_8PS^Z|55*)S5SbVYPghc`#EnFo1!OkQ_h*ZXK^ zUKUa)?xO+r(1K0pZKK~(bL6G>3k_LTQel8SkTfZ8r(+&!jclo2!F0Z(S9yt*Svab$ z4sOtkPm2<3Sy}Kp=UJGKhUjgpzmPD#wu}R6wqmB@76vwHg*NTzXyx_w0+g9Hn=McS zyuY4ydojuTjPPQ7R&t4OGqTv`%zat?^+18=;JjX-eyS#+-KAPPeQH$~TzcQu3fh_8 z_zb82Q^Rde0e9yqjTlE>2I3EDwu^*y-0x?a9bl7avislDz_i&bo(8Ssi?-X=0ZTE_ z&UIh-JB7olPt&HZTM&)L{lj0ECD)|U3BO!g3L6@vy^cZ-&7aaiIvBz|`yL0#TBO@u zy>A!Us|MKPA#+@MxlshQd@vT%L4zqMw-c#*%_U_>4%_nO{zYe?ja2Nj9czoJ+8xdN zo)G(qT|R1QK0iu;05Zu$bTAIM;hg67E$MMlCd+4}ICiJy81fNdFn}AB|7dy#=*_6MPiAGY;6Hkf{B_DM zOFS?G#(o$TK8m|MB*vpv54TlJoAoDM1|J_hM0D;I3AB)cKfu(1hV%#$;9FbuJ@oTG ztUM5t;i=LVzBMR#;EqPFRG3#YJ8rObcmgsPRVrflqj#Yq=a+kd~iA^r{ZV9 z1MK(TS8=0wu>7bGCfrdyxo#EOC7y;0#ToGkuo5(HHTOqHJUD^*W2*4?bQniJx`nt! z+q`u_86M47M+2Pk1!?yM0pN(po}_VWcsZ1}sgz3o6I8GG^*lUgIluCp{w%ABPjHO4 zYDL*kg$Ysjmh!lLmJO(|a<-iE8DqrY701n4w%6(=P@9nZUJpI4Ibw{~i87Mk3lvVbi%uC>~B)XldIGMMK`RA2C zrS$CpTqR{l@HLT;i`?$6CG;~)(WNSkq223f{v$W@V*2r z{sL0CoCW8?a5r0piOH6&QWwD`G-e4y3s@i6c0B>8X4#EYO%pl=m;10Z`i?P2*Rw3P zMm^EY()qZ=>B{H9o}Ns$tDoJyXbaZ(;5o3@0ju)mu58z$>LvK6m;weL06nSK#%2RP z*|llLU|K>UlSvR_3O=84VrZgQm{*Zc`Ws4HqxR(}R|0Tb+tcDqa`YTa6uhp|DYwIas2eeFufkL3p2^e_N-I&Gk4-WEa<&FkQh z$Ej~Ma2Qo!9-YK0`Q7cjPFYKLu>cL{R@UHG==s$QC>vP$P<{J0SP7~xSGJL0 zz(ka`PF8Jf2VjuO&0TenvX9McQf|0{yK>=Rr_^xtYTXqy&viXN0wOWPq%Koy*P&YF zJqSsP({US~42&;0#tUz!Clxo2xZ|7FM*HZgA-gs^b*@`uvC8^t9t}2qmk9VUV&Gv` zKN{^CUEIO}V`aeFC- zlg`mTTYcX5?dTIV>q~8b5K!EBAnytdlww-i$Dg5xT@8|-+S2+~qj1l84c!yLDvb2@ zDAJowzC#RAn95Vpa*^S@hnx=xa@0^ty;aThO4}DJlP$u1vSKcSzj%&!y1Qk2%Y5&B zJ`B_P)%bc1J{sK_FPdDMyuRsulybV_ymnz(nRWB7JEZL#s=X@{MkryS_`%K@k&yr3 zK(zBdr6IA!osr9)vYp4Ha~SwD=%sPc!q>)QhW`W=sfy((O}cFQr#3{Hp8gBcfcotp z{cduYcyCZXW4Wz;;mdRVoH$Dh{Y{fD#7cGwwG}|1%`d|{_k@q4Jj7c?Jobulx_Sb7 z_?C9qgeO=AFzp3YUcVmo(j@cgH9jDT&*`lD2Lwb>cL~ z2QgQFoWZ0U6A+pklJ*hDJTLW1+FP4PtvEHGR`88-+DMPKMn!xt<=pJ_{%dJ=%F@Iu z-LLq~n&z}_TlUntF#)L|b+6*${l0T+vIB-(I-4|9%ey$_;Yp(1lj`gue5KdB-9$;V zfzO0Qf-o+G2XNKVSxhjHX|1=bhTy?}a1)b(K1wSAMIp(`(_<>`vFz0S{jwASlms5l z9M9COnZ8HH{;yl?2TMe_3_vB9v?Wq;cWC9@ygN@@gal~Bep`Gr`MqieE{vDz-b*trh4E; zXVw`b#18OonCDn65g2H6PtaG#R|{akFFF@h?=`x_H20QYe zp>ndWeb~4iafJSi;%E@}cF4eZ57zM6=9ioLe4z|-E`sJGkJ_HSAz}7Wz$9s-+n>j9)@9Pz%Z8_9nV;s^*=f6+MWOQ_cGq})qg8xYk$_MNVF5_2fDaDd$Gus$O|&hiA|*pO*oH? zKHMS7%mxo>6=uK2sJU={8_ZJ@v*nZa-c!o5l@tU4H!@Pg>H1|8WcDk9OGnK5Qrw?J@Fk z``mak@^N6hrqg9B(Ovad^ct$17m)5|m9s}H%5 zQH%Ay`ftv~LfG36aAsAs;t|UXh?~2F%ryqSz6iitv~ufcZ#ev#I9r(_rHJ<-!V%XL z>|bQVJ?fPkfVIBT*|Vs%ubmJ}RrIETiVKP%R!vH0BEo0J?0=JB?4ADVr4}xRtUfl? zY1$EQb+vWQI-h&p)1~uU3ZrG1Ul$Wt9^JG#X12Oi0un$6pk>QSgsqnhr`Qs>X$Rx z**|!;@cG?XUsf#c_ETx0HvDc;XeZ+RGU_N^AR#pFyGua_u-?89gUuggKK@lf@>SpO zGW;f@O6Uy-3ZX)jj&vF&*d{$lE3mGqwo)BygCB*EVgvZvlFIrwi;;d7B1$o{R@gM3+>?^vKz_1!eh!bS(U5i92?f}H@mN5q=4AMra3ol-jAHp;zr~OcQHNR2(U0y6R`F|ALFp86 zBABJ*;~}(H8TQwBtFU6_Cyuo_m>0(RQQB)0=TkQY-w_IxS|EQ$=iQ2!`1M8(TqNly zakKl%X#P}3OkI?e5)zl8FMQ8k3*voX(FLub2rS+04Fk(%7dAnw^Vp!JIQ3buu!tnX zI!H%EL=x#&7Cgp1s#xHM?2~-&^tI3S^$rF(mHPWfj^tv&^_SJ~v=_n^VF3ghO6U|2 z?Z8dK$h|l{Nz^tXI7~gGH*Ifd31r4TA)E!4%DEoo%MtOJ;d?HjaSPi}HVSVEQEt9L zdao08Vh}~fbL_=`&i+NHrb2Ha+udL^#o1C#>SjmI)^6#dW;Rbfu*_Umzeu&@Lscwx zFwLudiSgJ8+cOKqYU?H4-xLanSa>Y~8H{?0{e-fYu(d(yP77VR(U~5v1_y){u!ono zSK%0ivFL(0-Jo^8(joeo8{2Kg)!e<(vao3P2DT?C*jDll7jl=EGkuDYB`rcB$5j+7 z%HJx}^lhceYs~|v+d1Agdx-YsiFbCd@$OYe$0Wj)llKc~vTLwS#_hC$nZ$YGzmJsnjd^cpg8^mamenu1^5dX^fYuyC+RS07jY zZF#fFzfLRumT+K>h9?x-rijI_NGVhYe%FP6f=Er;bdU4)?N>NwE@oRN2MlWZ(c9ZH zBK7?9)%77!gKIp(2!!>i<=Py+E<-ZyYZ$7uCFk$u?bt_G$BK(ha*m#)F-=bl3 zyY7^)6*W0@+K>9U)V(JKzj?C5E2n(<;9m{2*Kb)fNlapd4q2W;8t zqh;f(|BGCuiS7!uR8WR@>1{nS$XM+qn;_D*!fc6x4UQ2yrYiB}6+xm^eV546q6)kJ z%=!z1>ADLs(-fyCiS;kEE~B`=FRJJT=q}BdO)9CUAhY0)ukM$YrBf;Jam&HI+6P}s z-J?JIh2f98jX;~pd8bg2CE(_<&AMq=;dOcu?>HQyobQWCmGo**J|J~TFnz-N7{H&M zo^P|57jUEQ_L!eC*y(L(`SJUQUT+Y`A-_Fp$^P-? z;Qq~EfO&)fwoDcb99x^`5p%tZ&PizcR}bwVG0ka1nX0udp}g!1t4l(s+q;TNLDTbz zmpTZDgfs>XX-n>JXCyNOy!KO@2%mFNN2>gDJgeZAep*Pa`j zfk%=fEPz$Fa9hYtt2&fXBGg%{Ht>xwb=1Abler9Mdh>Syn&@PyK6WipfLIV$eUx3u7|H$|Y_5&v-n8haJyS4F5yUNT8o|1y#NKPfIv02Zw2$*?{J8y`*& zO7ct76nOYrHJ4bUgpe|gR`>>2Eg4c;YeAUSS0<5hZdX2Up;9Oq120pq6iAU6%yq#Q z&o>K(TZ9Bn-*78HNZ#K!b|-yY(84nH5w+V6N=kR?5jqZI8v=qpP-AsU=6 zZ`|>7Ol%UjWlxr53>Fl9fp|P9LczfQ-nl#BId@a51t*{DbDr9}7g+)nN)pEnsp$AT zG6hQ?I%o!|GJ3krM!FLj6OiFhfUmXb%l8Lzra>5)q9)J08>DJJr7u&1` z(dd@2K^M3irQrMh)Om95=DclQ*Ga_?npZLW=Hg^Cbe)cxT64mDD0cP-+onpbn1STo zG@XWt{djzYR|?LGG0-#ufTyWjKw z{(VM2(o1Sp%{i;eG0_a)Wu&3HT4;j{jr$Hst2|%4@hwEIwECV z{M3jfDpnlGGigy_(1EiNyrzr6HScS6j1SYoK<5#MwQ8*Z{nbu---3H=<&+&uP#k>5 zb4{z{Lcum_M}XzJJVjm+sxc!)zL?k~dFV(zi9>>;`i)^XRQeS@`|L;|o*cdx6)&I_ z!yyGPzTum=)yFB#rxjzYhKT3W5DEcnp$EjcPh6l<3^uc#>ije}UJ888 z5IK$Lqg3C2W}5q^6#IUbY`fVJW9$TQYX0WKqu`VCNCpPl(3;T9-Lh!RRktXh@G12Pu z;0aEO{**6UPF_EMY-(xCUs%j6#pSxq@76OF?dMH}Sbnp+OVmys>Egrsq6QU4Ws52a zcSl9RrvtOd#Q%?=_`hw5qNqY&hfb!Ls;(vkyXz4-jgZ>czgt!zy5Dro?>lNeBR(Cm z6o!@1J&4vig-7}T6fDVjgVawkO$3JPKx`~5Y_cU;vY zxVrFXJw3RN`!1SmtWkXWIGTaNH%2=5rfs|F81_vi_Puc{!r`$N(1pF!%`R_aV5F_j z*T&eXW*ZyD%|zs17CY{?zl#c%S7m%D$E1NJ7^lS}b8I65+ag4+LU;cjE(3?8@pZ}Q zVsG)4kalx2B>&V;M6gfUIz?S^jV6!eyTxHhO(EZ{Po2)7BKbz(Hy7XPJb@AH#a#!t zo#g0)-!*Oi>Y!=2uD){L(eIpWP6zO5#eN%5{PaaKW6|>W93UXPh2d)iCnkiZO{DMI z>6Rv+jLtb9`x5E7j&|ui+wOsOw>O@~Ij3h&N#&2Z$RPzf3Jxx5J)ZAv0rs`^XMRWR zj}Z)>*p&qe^0ajS$mw;#!m7JibWGt5fcKZ7$g{PwV1oISo}HRqL6wt|$6_c<6@k@s zaYRDin7cVQJ&jhXI-q-W*SadePslXls|8c#n6KzZokEF(7(XDE!tso&wM$_cWR!_b z7!G&;%lJk53QjfU*S)>j(TPRVI=9y%I_bQ!-ZfoDAdxz+T2@9;#1U`nDHC=TKAt~) zz8cK{+$5Gm)66#6G=`b1W^R8!mFg$upSP!JHC@_j=NHV^e3}}SRc-bywn5ej>a+}g z0z*=7zp8j72WEvZq_;W&+FEHv79Zn3?3-02aLU;T+x?8mzn(YmQX{!l;7Kn?YNn@_ z;?%aay$}jv)A6vSo6}xe{7M{6W}g=M?{s zmi1IY2$ye0kyuSv81by0g9B07ygacNjZB*Ds=C1G#cIlQXkU?r>&d{jC#0Ncx@{h11*w z1&_BMhUB?xh%%+7r-p`YKlGbJvimo{&R!;}(B&3Ada@0RtDa6YyMM_=0H+!=~{*C*$8K#$Z(=qIp6GWJ%zbLLp zXs6gnUzfG?X5^A}<~)^3X>4a=bwmwH7JVM07@zch>9ePOTaAcq_y$$x=^VWO_5(_e zUH>}W*&)D-E0xdRp-==|O6=#Hd&>VU!7*K~a%<_IfIZXqp!<%561 zD^cv7LZM)pus;l$nZK-PuO|p%Q?)%ug*SQ=flszbFy2)bmr{cji7)>9Cv9pPF|#Bs z+QmB%P{yUJu1J}y#Mev2k7h)vEsz|9;1%#oZsPoehP@-o9IZhV`-(uSodF=|%joKY3}hg~=Nm7z!75ADr#;@#0?1aCc{159 zzj*OBo@3QJRS|NLS*$yJ)5*$5$Q91{_33fJ68^J>_DVp6!6uwa=o?Y*(qG?g_S=m6 zm1DoCFa0_4e;eTjgMn`+WOIm@4g&N#flDg_?V;G3>HBX1s3Ms;X@4{$TL8ka&8OB4 z4!_xLB9Ca9m>gn`xd`8%-?{SlH7{)=mu+B;f1sMx^yIvB($nchsM^PHw<-T4V8^f) zrm2QXQzk{7CaOqms(qdef=HYoOzX1%|LsF$QH!#Vm9r7>;ARp1sG|uJt#tx$mwqFC*|Ep(d32{K9B?;eX2LzC{zg6v6)P~-CGJ0v0!tRx z)c$d!(Y%Y$kr<5mljWwtY`mcYhPSEvBVjq@S>|xIg9!{l<;fb{h+tOcZQK?7{5FJT zL8gnyUqGiye>mSlhNAb#t{7zR{+26^PK6?O4ZQS<+VB*-Fp-XyD_)*1BazDbl-TEY04;4GhWxhky^Z{z+IeMK1d6_U5@ zFMR!1rw^P%JMr(l{*MDyGQwdYEk=EVtT6Nou}$*W(P!&dWx6!iL*2FSaG~eTdxN+> z=bpzxaJV8LRDum(b2p{{3Lh@md0L3;EF@pB^Yoh7YZCsEwOpovoI?0zJQ}=N2X$f4 zr0J7?H2_%*k1GO?JvT@QFaAdq_uOsg*lTQRVs1TG$Jqm_=lmsJ0-G|n0!RY+X;8|T zV%qxq7Ru+T;wCntVU$Zz(6vfTg+O8(`f+Vnm8?&3orgbx>nQo|9WBBMr!>kLsPH;U z84H_#XvCO_ac=dezFgAQuRC`0E6TP2pG5o0Ki{H*{b5HoK1vX5#E)$Eu!wMKk*IM~ zzUKl^Q$>AeJA1;WOGq(tU$)-kr1Qdd{Y$TkyP8A^JTg3wS&k*L)1O^=EGo3E#llfjFy z2LUf)Ei1f>7br6L9pp^Ylvvf+MvnLJ{>xwJx1*&39!18u`|&d80gbO!NB-Vvj2;e!iO&!Qd* zzw`M4`%d$^1bz)zs2NM>j+zmob9y-PEZqK}c%Jh7XXd&dp{YyTcdDowENs?&AN(Mv z;FDgp4u`H)=Ty@+Mj~phi6Zgmp4LgSA^LH_WD zh3{<*>cNPs6c(nQ1n$p$Sa_(JC-k8FO7{_`;WjA8^Bsh6iS2_JI+T**6zU@T2su4lxvr~c-C zYCa;ZEh_x23{c_vVTg?8^e%e-EvrOd!uvH12o2LdF(c25DA3dKV{Pw#D~19sPf$NT zg@9AkASDCBm!7d*-~PD=(9>>_h0b4I6``T~ZruFRgF`>}S7o+>YwnQ(q*~W+jW7lYJI-st(U}-8B3x*B)hlAVLTK*W3 zKthsn<47Ycr3?Jgixf&CgP=ls`r(Z%3>`PwA^|!8pG`#R>mdVGxlg3*5Ca7N0Ical zmFTnmiFQAmk&Y`7uxEAsW4hOPzSB!DNc!@A9Kd(?q90k@8iET1NE=Z(!)D_%M!XXa zE^K|W1?I!RSw3agALhzrzK*^JU_C3pq5HsiLW1F1de+}Oy(UHqMRl=V3ScNCQQVEQ zX%yqFM^!iTE4Cfb#DWMFg066Vab?O_=X}_N7wK*pU)xw43Qy$>GdOY9SfeF1Hx-7C zBgpxYrO0%mcTjb9dX>avL6%c2(jdgOmYC~0VEzhznEBt_yZ6cd*kj7P6 zUo5D+Q02)o48sWnH}e589(TI~6}2{|Jm3OFFH5e17Jk@Mi9p94$l0h31#3M4lS2h* zK}wfnA=0k$4~FwUxZb|G|9G88xFH!VnE3qd5QYgbC*V8D}k-oU~%(|#6sbRV~SV1 zIxTPohEdbZy{5l;#HnE2;vWIUV47MV-Y+tq&( zPK6*YB!X`}8A$}0|KH5$zn<*`0iK!ve^UR4$?~K!HEP}bkA>vq0fj|38Il5Qad}q6 zK3nOzmKHGt_saY4IWx?J=|Ig!3LKq;ro%(GwcA9}-|1~7CH65QRR|Oiy4?j)@OJSS zcI(6aDU-r*e<1&=*NhT}+;D2Zb-9g#pu4^@u=5)v{zG`bC?g;1U%+yZ7b7@}Kr?^q z+OsG;TnGHIws0_lLV;$7lTrXWlb|Qz^mOd(Gp)<5@FM5!{*>K)jt#`lrckI# zMdg>U22Hk*;68%9x&{XQ&-axS+z;xBonGq)s~Gs$DByAj5#WN>i#{SOWB)BM35}vu zrP_rFgluSaj{xk2MV-JePr%qKk+0N}BwTRloq-0+RVv?h^-jl6+a;obENp=X`Ebp| zyxq{;B#(>az3o;ou#uu4kb{z~Uguk1oBWk8gxP%dFzcDAp1u%o-K42IEhLQgjVz(n ze9!DZ%y4FSLd|vo>lnh$INQ9c8z^#Te|0jfE&T06Z4+PCk`rSw7AQ+}^H8oaR*frk z1eYmEoFnDhJ3CCAsT8NLJ+#XHH4W^M8v6kSAF+}H9}13$ia=v}0FK~~E4Xy*Ka0OC z_VEN+YZ9{>qjz@7aXc%+y(>sUdn^tmY)6Gtq6bD!FWMK`1Nfebl*1i zs8?0!BeIM`^T)mF3KA^x!?e?0r>w<8>vBvs+ey+WVWV8#f_p6mzoW$|1hak%Ovm9d zYr`coupoT0i4B-3U^5WG`zaJkAu$pXAI~&2 zYy?i>aqhY-Z>9ru$|!9ij|g)$M{jh`NOz)8u^hkQx?8a;pX zs9+^gfM2sN7@AbPY5Lmt7qIi(T{-#w$Z1mY7n3FFVkeH3P+>fdLP%iMqrZZgBFGHL zTvaR9UWubaq{MqYIq)Ig*@r;S$Pf~}U0?ysJo!?tVnWTG`8{LcHculwy z);iKHxP&&Akjw@}0YTGjmW%mDUKy}>nZD${f+P7{A6X#yO*X%*)bXm_<8&{Rg1m_e zP1A?|T+5I-y6slR?DWK&oZ~4@y@z2f&uyCs19J(v#*9_inE%&E_^lC~a#U)oNa2+(Nn@^7|Xgu@^P*EF`*lj-cJj5%HiE3u@M#*V}4xy|kQ7Uin z9%THeRR*g}N7wpRG}s5_`^azIb*G?Y*K*q$v29D~nO%;enV)=Z!A#^UCOu#9CXU9Y z_!Tbu9@r3IMI0$#{x<1^f~>XeiK||2d}UR-em1HE>cA;?AyfJ#!J!u}nOm1u)iwa2 z+`gol?zgf^43yDlJJy|X__3hi_u_v&&&L+d!1>cQBd7bZh@4WG=2Vx=Pt_*jjL-L` z<}w5+)KFcy4VGlQtK-$n`u&a<38$L&(k3!`8IEjEDa?TSf&Oll>Y!y5>@FMabrwI< zI16BU;SSKU*_9mKvgltc$KlQ$#=zn{I;_Q;WM+f9c`Xyz0@|bl5HW3>UoW&|hb~njeY4?Wa&P$A zjuoM;7LHnVp*?@~S9%V4>m(^Ft7>b*@vTN3b@vOX#={-IWw$05u22auVU1tsI67I! z14^#yWW$|s@S^Kn214S@q?c;~ivnNJL@yO55$V=S`Y74Rbu}9-emrfPai$dJroq-Y zk+OM?8~O-6QwKExS(&&MF!8xPKCi5;LZhxUx(uXz1#jUB&{~%-$z;AidRs>aw679h zK%4d6^LaAy*O#$g|5JGi3e`~1IoQMMjXOLyP4vY6ZU3wMsNN*jM!Av0YK z;i+JaP7bk(lh%QXuCtH%1+I~>z;2iz|Is$E-I0}EIP8$bDuWDC4Z(`9FPZw^3txW?E z0k!wloip@pp?(sJABolX2I|aiPI_UVIiaidBZZ7tTDy4xUB2n>TF{L0(G_xF?4R*P zOGEK1py4~4cfb~r09{K6EB!2!gy{M-o9PDSDEGogD0svU{Dan} z1~Hujq1`Q=pzuE**^VyCw$WFlRBCM3Y;8q6xL`3=i*~L4=kqY==#d_cmECS-keiF7 zGk)AfbFZ(3Yst)5%Ziwr)FLK$4>^+LC4Bb;%?jfs;_fKYy5wWDd0q#rV)M)uwQ}l8 z7b;-$m$>&{AOAItlVcTQ*4OU`9BGF%Rv!A}1!_(PA38klNhPwa6uD6mcqP?P@(2h) zKGc{vl@3j1NRehExp53_CpD7+ex%vJLSU+MHn{-E;&h*r4d~Zi&LZtn(h7Xs`<-`6 z?xn8dn>gTkAl|YY%Dd1M?_+OIRd$lH# zS9*hGpj)(CE8^pjLg4tz?k`19^71A>q%;5c>bXOu(QZ;8=@hxV@APkSt+}p0pt8YA zVWFWV0V|jL2+_rtf4Z>@!AxTOobc~IKE_ftG(>rLcwqB7OD8mGETb8Ax!7Ph1{TLZ zy1TlDcM0xenN51+@^JjOm)u7Q!>LhnS8=k{-b?C;d0Un2I6$y3T8zNB-h2HC}Sk zj;4fwE(*0*>MqHe^K7fJOw7;kVT?A>sv0SnMs36w2`W##D%ks5nGMScPC#~fx(gynrfIG= z=@pEGDt1VhBww{d#E213Z^?%XP0P)smp5i$Db_*1FGBMrO$8vjI_GU6%e2-;L0HZQ zj$u>S7QdL{j<_#-iwR%;{v&<{*iIhN{m~;$wur#wsLvM?ZNi2m$@x_-LSjqFo`Ze` zKd?rHDE9b?>SBOhO-jtkI zmQo|JFr#2Ig?rw|hp=-(O=1hs9L!~sNkU?=Y>H$mwAp;dj{mhJOPyFsC|n+ogMva~Crr~S<8(vLR z_?N=KMWp)lfHgP({^q7pshz(bn^Wtf5NfU-;q%gF#XL)i@ z)ylnr6vmcY!d6y5Z4g{xr3l3Wl{-S}{23N3(ZV7-i^Am%e`kziSPFQ|n&j>v<~g#H z;!O2;!VdUY+KeMQLskzzR1_U3kvSax<+)BJYkPi3@&0BvA6)gQluh!KW zZY)p1QS*PtFq4*5HJw)91DJux3J0uEAOS5Bxjo^&DuJPZ2$q3$*vQek^wW2h!b1BU zFQX`(?7AJ;V?k2@2AU@kr<4ruEKVfW+%)MH%qiPv_*EQiJd&+vD#5$nDRU7wG3BFU zeXGdNXwwCRD%8{=af!k)lqvYfU`<{_Hw6}T*lcf9*qV3rYH}duvqE^7BPD#yzU^2L zr}pMh=KH?{q-;t>Rizj~elNoFur-6szYveER}0V$w3(s#h3)PFL^>=`KN~(L`W(Vq zjImg72ds(AVkKJ>#j>L99jR{D-wq3#ZAOWKDVo0b#BIX$MO$NFKe6kB{x2wJ*DpM3nHe;f+lb3_Z$>EyWcze z`{+Xc4>r3)iT=LVssAM;YGXmb$5G~8aoE`Yv8!mqkdq>lmYqS{rSj+lFPgi9up78% zkBT$jfM#sG4UA?l}$l-K2fzpb+!D? zrCRRCSFyFkVNTwJegV_|lnuVvEPDFJj~;%tlrI?!Dp`)Jj&5dQv@SERdy?l8 z+bX8qMA}!`WOB?nny5jV<+hE+Bu*jucA~<&18N9=fjSGy^;4%row-BIZY@rCv&{Se#CrrG+D28+fkeHZ1`zEh zST#?>a+dNpxoXxK0(YFN;feHr@VhsD>zm?8(e5Yet|}ca#Ik#?#SkJZ9m^B~wx|L$_lYbaQcWy_XQ<>`YC4HiPb7rC&BnQacaX6%VtGLp`_V(ZbA*rc3P zSrobg9f6iE16M2u1ZAb<=x$#ycZM#uD*{PX$1=A{jznE_(XVctVr5LNk(_*2Y-G%Sm7bU}guJH|o7P57|;CPzX1+3Mx}btIzC1z7CU%-$RcSK*vKRov0#V!V+>Z7%R4sTi)FugmHL0_dSSa` z#AO)}E2A@#);8E(=()E(bxeS|;Zty&J<4n{(;$U;uA!`k>Q!)j|Ettq(O7kUE86Yu zD1x`}La5L>eB`XFGiBpR={L^btODY}Jm1G#L+n7wDm^1jE@Ypjzv^05GDg13_Q&ns z{kwjwrxF`l3Z|%tx@%AUoKITkH0)lQxzT*GH1jz7bxgi z=8$G>%in;-YjQ%uTkm^URn^sI3-sq)vf0_$b}p}N#VL!+tQZ~Mr^uYA+f$1qAq{Xl-KhBLsUU%}$%RhDL@IIPeq}=HRjVTG_F$$*&&| z^OoeWTm$WKu>mdUqPo5M8}{b<#oJt+*sufN(XFn@{itB)ln8>4FWY5r=+p7M4Ms$? zH7lLUNbj%e;4!FKG9o7u8nn48~NYlFpr#H z`%FpiHY&*DS*$j^oRHCo8R|DBK49jY-+6DpbU*kD46y)PwxdZQ6fi&>O zCzw~B=ph7Dc(Ie*?`FWxeeyEVIunEAMKf|mVqUC*cNg}9N@IM+NH*8?x^|2ue#{&8 zA!$D^Bth?^^^+-#I6$1(=15Q-_JBj4r*Qkg?|pq?U61OhrzA#!PHa_2ygtX(dXLZ(kI7&1N$lG(Ls|Vx%SyR>+a`sgXE3%PekNbz zCB;CxuWm=_b4%r-dVaqDwOnFf^!=nr?CLoOrw1Y95NpAh)}brlL+)H*f$-9@yh#7d zv%0~WMRQ8i&Z{vu^AMH|=kqY{pgmRVDGP6#yfn1*UtZZ7Zg?3UD=v78^~!4g(3lQG zi*g3VSZrmVCkMgxu?IN>HqiA<-}J1Mb!?f6Dt<>-Ap-n|F{D7yo2m^_b;_05?_QnQ zez|*T8)6R-+E==@^@bA?-iCX+d+XHla}Kh12x3*;B5sur)tEB@G>~2_$8S%OT+4Qf zv%mXAKUfdS;JH!&5!M3X@c;CTGV9kF~>BCEBwruI2gM%lq7J06xuiTQQ zP#y432yx;K={f0+MuCxi8=aCt7vkO_BV>Tvw6evo7$lIQ?f=wmcH~U9tlL)Xv_5Co z0e9ioelKvwbCS3yrMGkUd*WXVQIO2ZSGiC%ET zKp8K10qpC zZZHHD&tDmLNYyYZbj&S^vsy`L0Zz%!L7Z=nZutXb1X#)`G4=xkcpfXjc4~G*9c-QI zmm4Ww9UOVtrA5^V`9;&b=iA5vp;ngnzSN)PkV;00OiPo4;RcXU6BsoRM^1Pxx(%z9$7=6{+#E`9lTi zKb48;#uZDbaqe#u!n?+sA;g)FrG;^d5XW3BJL6XwfEw{5zCXFi6?%gbZ6SLo7IKXfUWhL#O#TWIQA>_G%H5u+07l2A(Kb!B(=hcJ;>QDu+$ddhRp!#a6 zl}rL94*ITI_(-ys#r<@8OnocaP3{HgDY2Ih(N%NcO*!5;dim+}!>~-F8a*!FiQvbR zaP^ib6qWk`B4*}fu6JSoy$nc?*Vv>ab^f%LITDkK^10r^dGNAa+XW65+017Om6%t2 zvkL>i$$%#^PikH}}6om$;;a><=m6>aDl2^BDi z1hR&nCktx2v7jFem6!hgcL|#Pu?34KaX#l@v#Oz{x>9L4Mr!da|4BcY`lFE?D$&n+1|r$d7QLB@8^@Ns-`+b?B9A`hdxrMK=E`kp%W7H zybZ`zik$4M_kwCEfs^yavU5k02-~nVKG$nD-W~K`HRoI$$MD5E(Od?-QI87~fH26W?k~wEWGo0}V>^r8&*vxvm?EEjo*i}>!dmcf4n^(H_^>(K5c?6jAx?RmApKTkqxJ=s z#~Fvd!O7b7B`0)ASyG!sRi)$a`VNqFo3HM;=UOZNC7L=hZ1af3e2B)U-Ti7~;BQl; zcf(nvRICIhKN8|tSazEURsT>7{hnGE4Jh6wtp@c$Vlm_t=>`AB4=X2@$^-Ll@nU`! zFJpp)g4Cg~da%=ba$N!6G^sEm!k{pvQ$|q;O=+$p*l+vAn7`!66+|^|ZK}D50Z<$I z)4mtUq~Y*`i~mLWy&okQB2|ia|Eg>lcqioW+mfA-Vf_S#(^OOP;f7kdqWp^WUJm&F zFiwxpk+C@^H_n#XhOJl^r_eJC6P^JjzU!D8Ox+@bc&wJ_4=870P)E}GMsbB|Q|Utz zhG;lD^@CNHHH{8$j+Lks_Iki3ayZ zwTob-CH_!<^wjI@tn6xWqEV~tG_z}e7lbn_=?WQVbp1@BmPzF2b|ofFy~XC0tX2tj z6^d9?#a1^nnEzYG^QA^%{5-aD_t-&NqUazNZpWq`s z)}w5EgM{>nW#=A;$=ji>%s+!INnC!mowBjnJIq%lsF-*=$wUN-phM2<0C;#A+GTW4 z)~b3{cCakxKV;(06}Kwtd&p7J$=nf2NRuiP8*$oU!e{JqNa@oEcY=-DO!#vU%P}Z2LYcaP zqQt>#n8-Wlj<%%N*`m5~n0}ac=xOA64u3hc-pe6JXW74zAt-+FE-N8}s=uFv1CL>* z$m1>w11}hsk*PMc)&v4*${E>6W^s5 zVTzzk_*%Z-b=gC=JH_*1^}y?}MnXh1RXOz9Whg8CaBw|{M#qqv60z~>eYS#wJK2lg z^ZpE7qV+8+}~U&9wD{LupFhaanFDQoKY?^-aGU=s29(3h=$s)DBx*TcG-dd zAzqLOL&Mx*`Zy;;JClvbqkER@eipvAHHwYj+0w&=hIT{@4$Uvk8%pGENqm{DIdC~2 zy}$Fa`?H|`&llBYcR)=Ei`~g-Mi%lO;WElJe7t>)VLL>>Pq2nD&s|Y5YaYgyqhSYVHB*4}N4sC#J3yRe}!hhS}RV zDx@Jl7rplyY79uL#kLI@ z(h9J9Iu>w`B#*2BdEAF47Z^h`U&AjR_oNX?@|wk8p#MIOCV#`Po@OarLv~vdY4_D$ z_fJm7WgTS5i=t6NyHm@5<~U@oJXm+X)Rw{@U^=>g2fz=&vfE;>`QI zCQ^UYdJ?y6M}#_-TyRg_18jukDI{knr)7#02wpI~I$KHN!XVxv+DuP|xeRH!8hYuZ zAc=%z;q_me7TbT%yr?%3_LtV4|C&jyc^qL(T>2}L<872xT@njBXMcSeclY*(I(Aw1 zIA@F^Yjo7sLrAC#Ws?M%TGI6D5vw8x2Bzm32ksYOb!X3}7pQVPWb?mrdMTB88$Wt7 zm*XJMb~VT%=uvP0A|+3kWX2lf=y^VPap)InOG-3wn-vTafp1T5(G=XzMfCD!uoq7t z!cR{9D?OZbv^IvBB}b(yCRsHRZscl<-|%$eAZJxk$|M5m>OyEZ+>f3 z%z3IL6@KJ3-1eZFoDxsq6E~IWZFqF6^tp-TVhpIl1_yrRxaCpv`(7oAxZ*Z8`e*~= zb$rno5*I1N_CDV{ib>DdJY}<1031AC9 z9r$E6HM{4rt)i4@mCjS?4*j;77yX`U&B!j;^XH1O7j6Gobup8*KnP!2V5_n*+1(?Uwz~Z5Hdm zG>j7D=?b31GEpOvsb^X|kjTet#@=3ma4iF;C~1@_xj;o4r25+8kQUTkT|EOtRS&u`e6)hRDa2Na_ zEHQ2|OZo_#m?CMwmC3-Sa4ejxq!}X$xQ{Wb|D{MAoi5>38KhssHLu0{esXi;B}1)6 zbWIb+(G9qdu4KJusBY#*^Jw<#D!sZI(Y?*{9V}&h4_QF{D@jcJ;yX9F3Xb>YeatcF zjsV&03Gb4=Gg*ukvPoohq&Fy1sFNq2^AEG}YYJt;r)=8yk~+LOdJlP{1m1QFTYOA5 z&IlTdthlM%WyDL^=VDshtDK~auh@Q^vmkZVOn38GiScqXO|S2_i}7 zzPQitsD~YvwIQR4=QLJo6~ozc_)rEX9BFpHvMT?gn<|HrRt5XlnH{5 z06d|uw#L#@=kL{Ga420sA%Vz(B7%8)ycZkb{E*7!hV$K3&|qk$&?y&df?eMPU0pY2 zP7y(y$=kM}EafHi)EHP`MC8V&6nsDt0yj$P6PW)cMs|yxzNeKl=MSSJGn#T`p&n-% zkI-({ilX@^LJv&?ChV@1R6IC~EuCr7=XG)qIMh7YXb|?x?APFC+psTkJ{WrQ&R>UL>e{Ao^ljarKzKMft5oq7kwt%oboH5G55xR zQegT2mo}TR-1si;q>>dmaCwBFd12}$Dk@g|6TLvn`LK^v)gPmEe*w}t?Y$2rbm?ny zeRqQ&7m@*Jv@7At`B%3|sxfPy+M3-MS=z2n;-&^H;2#eo_-wYL!9j2c6f6jETuJjJuyT9K} z%t3T;<}B7Y4@*V5+(hi!v<0=VRizR6V@d*@@dMWzJiEV@kx5Ie7Rdv5?vq7=ieqSP zu@)5^fuA4B8(fbgLkpb2v2_)$lIqrX=F4mG(_XJIlreRfeSt?l5A8ImR0Idzp< z9ADtT`}9vLxUxo&xbzkTwSxZTQZpWNgf}uc)b->aX?Ex>Db;iL@=_ZwEdt9^z9(%7 zmj2~PeY)2(?()H%S(Uy60N$!H?u~i^NRg0lhbu%1umUS5_-1QMSWd4Ib%vZ`kCns- zeYMCHiy0|A@Dsn`?o|1QH@V^+)Gzm61ipzDZkC<_xO_gN2(pjsW=FrUd2r( zuYU1&>bhYl{V+FfXF(tq?p!Y_WS7G$!k183vA{GNW)($B6Sa5sX+iv{4cPPm5gfwm10~@9E6z$UJ`~!1u_6&%TC-%|jaJsv`j7NYH zw7h?X;zh!F?PMh_68TkirlM>B@))jgBXoI0nQR5ZAn3htn&se$oaDo{g`}H0b5x;b zX_IYZ2;{t3vTqe`y*Mpa3o*A2(Op{7ubnfjSPc8s1#Svp(~A}E_FUiKN$7NdgBh8S zdz&z;ZI|+u%a6G@l-2G5UpX`ZZd4AT$e9jQ9j&Hih85r{s(xM_W{tMKQ^b9pb^G|E z&VTa=!-jTJ-qgSPaR9RL?Z7wG+5@>#UtJc$j^cQ6Z$s_8sA;lxYL4uMIdO>i*~?`D z#D*YYjnARu@y2p~rJX1%UNe8wx4k_Oavd-E9X@J@%{uZ}smhRb%gKHZwv0Y`!h$#0QuO9C3nHh zd2c|-(bxEC)5?wIg>P_8_viM$h)oS{L;hM!66a(Qv zj+6+9k%22}V?Mh&XKVR{Z%0uBHVP4dv0+uNyNzNo7$l=FLGO(&rt{v3!awDqsveBs zo6~Je_|)PI#&s@HbT@WdBw-a#?#@Xv`ug z3Ow{Q>9xJn&j9d-e0LG>D^)L;ZAuF;)*ze`pf^loyB0xIrMDb&^!_MBDA^^$j4iPs z7vBK3ls7yJqJO?_7aiRMOP|SAs4S_~pZ3cBic)gBw;au|UU&X_Xsn`r$*P=Rd0>I2 zWuTa_6+o2cseWb6TWg)xy|({f?R{lfTie!dDOS8K#f!AXTCBKBX>o_*THGm`28tIb zw75&KBE^D3@#4i@0|bg|fCK`Bz|Hy2+2?%UIs4vc-{1G8k^O|QJK5PQmiJ91MbyOy zG;vuuev&}kEt*6#q{DUMge)((_(}}*Bzc|>r|WVcT0_@ZI30R;?@C7ET;K=ehttMA z2EDZm-opcHWb-{5j042wGK~PFGH7yw)_4mhHb~78q%!=sc4KCD#R1NIba!QD<-9cw z@T9#zM8A*{(uL{me7HU~U@uouxt4r}AIpg9{;iWJ;=YB4FGpH)SbZfTo zd&=O#{QT-_Ym?+&!}7?8&L@MD<71pt>!;nKb)~0u7RD5#I;MW6qmjP1%|$9I06Ka+_f7Q)ca!P1bYjmLoQuNGlS*7t8PGo?Fh?XT%uJ{e57+^9N9+ zm(xH65h=?wKCE^1OydsygugHWfjuQ_TW)SJxCGfjkRvHP@^FM^dcx)Skg$DYF+Il&-30ZsE4+1EP`yaRG#UhJzl1@ zKDxd*M-MW+IP>F-BHxkg`J4L+z)0`#(n^1J3bQlB)6)L*&R~>2ESmbaY~USpQd`P= zB&}6Saw#ZzXLnA%_#R1|3IJwV^$4HSp-36a_qABn3EfK}Ke0QNTn=7lGvE%JcHa`x zxo@MDYDR|>ooS7aEuZ3lKil^%eJJ>L^$s@Bds3LzAQe?Ax$%97V$-ffU(U#%p}frO zGwq{N1^MBH*D~1*hzjScZ>(YDLlJfJ_?7!pOw~QHm16iq5MWTeyjo z9n3fEI5Gl+>r4aSG&EHf3<0l{8w^qaVd`iw5=6AZ;EqCvc zUI2Ha89pn@%+a@@>PYuvvhUjJ8bgz(P)YX<5li%9mr^GRp%WNT`-$-@Vm}-bC>R0$ z=HG-TWsiCJ@6v(jL}gwf>zeH9LTTRVRL0j@Y*RIpO zG8>Lyo{=-L+yLPSzp2O4D$cQAUSsi&9w5(8s6s6p)v`$t`j}8+D(>v?Xu1I2;}lJ( zkHf0}$`UFFhkPbIIr$PgtK^NK)HH2LnPr zS$K(GG(Wk@?r%5On3d(sG6We?P{7bptn*a8Uz6_f5!1W+hNN0Zr>XKr<;Y8>TwLL@ zpM{0j=}Zghbr&IJz>L7PIg$?)S~dHYBv#dSB#R{ z8~;@ECC4o;L0zo^iYz#I+zqu>-Yk8?A;D(dR9YooIue&$;UBRVhKyYFIlQ8KeUGFc ziK8b2*&1sxcV-BO2e$2N~!3ZQocpl>xC-o=9+;iV{sw zEBIVhWONp0EnSrWoR-&(4vrC1mG4YMOp#`Y&3Mi~NTC)LM^Jy~gi=1kVJ`^hE&G1O z>uP6Z=Ukhh|CMKjcI4P*_skl>mnY>TbwgZtLKaGNvPcuYwbv3QEm85FG9olIu_<4z zMv^Q{Y=!zLXqevaopAX3{O60#4}aLwvH%W3-P#n0r&0L&x7xj`x~#%e<@&UWdD0Il zIDsc@Pp03)3w~^XpI(0%0PR;jDTio2k;*mQ#r;kANKG>h&wog<`EH->xBXO|%EaRP zp7Q)%9RW5B&Bjosm|D+U0jeK z)Wk|>BTmxQ2LDw>TQ7$l?GdC(-yqlK>7F->u5KITAV|)`$|n3MD&%{vBO4?O6xcaB z*yH-d{(79kpqtwH&K*HRr8lqMqA;x3U05G0jD0bbQ(wmaPAwJg!|!m#eXB-UZi@}n ziqo@|?a^225`1=7DzkKKLiv$;8edc8b$gLHIRAxLVnBeW5u@b(^IQ>_wZM<`Y%c>YHK~uaHLWmv@|BHgWtM6@nFBP)d z*eWnF-K^dzHan#z8IK<)?|T?dd^XFwxi|&>_~1eHwJX?8^p`SrIl@pin}Sbk9&yIu z>9o20njxZzC35%Z!v*%q*IZ8oyoK!dS*}0BcVwe(Bt-oH{kmE%I3!ba2lpcTHf& zga;o#n<)o45xB=f;I^?ntO1kA{UoWC>Ja7SpN*e#VG`G`+9l?PXn*sSmD`u#A@}35 z_&I{6z}MWH09nUS2`>!Mq$BSR1QRz;?2RChBCEqL-@2MGsl;A@6}1Z6Fw$CG-b7S( z2HkYO+xQ+SEAh%@5dwLwPh31+=*fdQM}dERx$XGHD-16b)`TL^lGasR!QwU-#cx_z zP~M4Jn|zIJcDu!?!h`dv=`_0Dl(cG6^&A@?pbK$k2}q$O=Xl$a7$1FuRq<}NTs+y1 zgM^ihwU=ORsD98%grJ+e*?V87zSvz$IgDT|aF9IYR;aXGoKdoJDpXsq|vsN(98REq=Xa;UgcEWSz!kUs&b#lE1@sl8SC$KqYAh& z;RWdC^7Z8M1+_B@GuF)-?lW3Q|H6Xd05|7!fQCMwQ~a-rSBfU5K2JLulhW^#eSaPD zy_-o(KkB%#2Lfxo*9dLDBdf{z26-?2%NM$@)Vx^X#5CAWftRD-#}yy--0q>w#>e#G znbCVXQ0$rj9Tht}{muF3AIbl_qLmR9x9{qNyD*#o_%i@_uUty6t5FHGi-L&^Odl-Ccl#xkN%l9grdt+Q(+7( z-1|nerzdjX!A1VQP2{=a+qj2}Bwd9eM9#?xp1MURqVq zM2?xQmm4!fx9@L!!W*D5tP;Ef@Cs)T?AJBipX7zwY%SGfKXQaJS@Ar=r!GSR_?2S z-~10HO&=?C#*3+bt@fqmi+y7-X|g29bG_MYlF(1=up3EbQ=^oMFjNTyXJd_5!$fvs z7~D)=z9CI7c6<31gl2X&I0>XYd?ram#f_GyS%S8o`-5IeUPW(O-Xq(k5105#P2`^6 z5z8{SEp;jS;VL5PMIt!Mv1*LiJ7@P2=+*y%+Begd^_^zRk!=|HM!oHJC1a$tG_m0X z1*|28ee#5 zcpR7m;IFOCT(C)03xf{XEu;e$#i8k-L;!oG#uwh!U8aW{h?*(M+ygzDszUqqNzCUv zDCi;loJI+~Y1lp+zy&S@w10--HHo|;r@aI3`VzaD(db!BF4#3-=nqD=G9;RCu&>eV z7VQ(U&6LwzgN`;fH%&v$NmB(3Rvb?9l#{KY3uP?$Ar4-aMPu8{P7pnsvz+Y%U~Ye- z&Nc(9Ohw}EUSaEDi0$g-Ynz+vg$Kqtk;rz7Hs4M?F}+H&wHC5~bZ<#hvI?%eitCHy zOo@Lax)M3Xc_^4K4!Oq4q*x>1UbgX1m>|d)KZ!Z?+sQ<)b8?hztQ0VcpTCBqr0RMj zvn5fP#k(9sLQ`*yKD@aRg0Cke2?zI`WjP+KdeA;GEwp5MAwDJX+=rPN{nb9I@C$JV zdWMc>Q?*cknHG3!wczF-mfYC0gm{1RUR^NofYZ34s!Axi@@(|hJP3h<);kf@tYyBw>U@lsM03$=n4s%}1S8`H&UG zD<}sb-IdvP%Z371%q<_dIH%-`o*t|^73Gq2XxC%9Dy$*cTsp7_Ja*M9p_z9xvKUv6 zelc((oUfh9uzxhPb=H}SFU)Tz!=YRc_4L;*(Nh64o}{-KCYeZHAZkXNU3*Oc6U`*X zP2FLlacdZ6e-?NOI=T z7CMf@UKTk}iK6T9pBZFxIX5xSaD6PQ_af6%h2x2K=Hq8hYGn?+$wC?6)U3B0vVe!r zy)#8>KR0I53fEHOBbHF^OYA7IhW+#g@kd&y75G8%FT~_ zcH?G$e|Y{m#nN)Qq*GXW+ovq4Iw0(;5El-G?j+kO=x5}M>{&0%h4zM?Z#*tE>#h3? zsVV))hA;ImVYoCRd0dJh&t7bw<=|Nm_>=_CYp@mK1u1~<+BJkXZS^LWX3h>|70jjN zt1doOta~(emYds7l`@uT8#>SqynXI4d&5kZRRk4YygM!olVqsA7=eJSo8Qyzshiu* ze&b4{@X0`}Z@aF33SjKZh`N$cESY+#KjD}GmbX-sJ6rG?oWk%Al(-q)e?Qp>4RFm7 z0`w8>n;PS$y<8(gO>&E??>zjFTU_WsBcQ$dN#`>2emC@KG@YR5mzzy}#+L3;nb5kv zQH!-!Mndo`Pa3O;pqC+<`@zZ@o% z8D%D;0R51K6!;O&Q+tcoAI3k8CZ^0zrwjgWk-{}!sAGSy^0lM!x0h$q7Ei1FLfcS)l;iaAEo$Cx zWTh2o)pNRIXV9WFId`kdK_1w2CRO4v`dkuB+e%ropY{PpG&}0>K%OWi)i0_f`)8!w z7MxzC)U2%t0Sk-~-^ZJxA5D4={jpB`&lFyl4-~1-R-`u<^p6tLYuWnit_kRUm3j%2 z4_a$6+p$n?OQR}k+YC|9QnXf?v@=LwPkqXsR`6g$6o!Fv_HgZ#BQ+oA<>l)gQoWVk z6@)Vs>O$%oxy$`qQBnPlVgk@e5u52Wqam$$Nnl1;IQA?S)}?NAxme-CxI4>zTuM9J z>R)g;mLoj`nyxcc0sJT+whtW4oaL_VohT~wOTTGCYlkTh{F?kJ5v=nbrzmGYeaV+36R4>N!85!2P?A|}> zY~V)XDl4%1sW zP!A{DZ+al+5gb)fo(|a2XQF=Iclua$UWSjhUx`kgTFzwY0kf75y*#QYVimEfM_X5r zU02S&jT7}wJP@&o>c1+ zGIzi!d0O4SI?2>k>~z&ugBoqHO#%6rbd^qX*%LzR7!x(l;iuvldTeOPRgb@X65%_T z$D&|o%%R~5C*Z?Gm&H#6O@!#ynv|hou+{vyK@yobk8!TgwQ~rDkNB->eKf0n5_(iWe-S2g{3Q%t*lB53YqxzIn z+p@Oan0T2{jcfSiyJ2mNn;}OURO7Px)Y$N^+oytW#$zgemg1=Eq!=U>!L*Xn$P}ZTXW;DucO{pegwOi^ zBp7H{qANMmUT1g`qW)ahY2bK>A$~0t&+IT05a~cpH#PTV3TBA}H*vhQGEySuFBEdI z%XFT#%{#lTPdKVkI?;en|=NL zN{v(D&G20rcjqbNAkwYy5&*O-;u)^5fb#j1&u%`&EE~wP zez*TdP86B3nyB%`R_*f-kO8tbtI5_&@{F%(Go8Sr&n8e%gR@asc;)VPFvD10QKXM# zcE))I;qkF8sy;*uhc!RzdsYRdNb!5Q&O~l@T|9fVF% z+ETgk)eNHB(^fizniUBe`jy2l^WjU+i3%PiBl!BV_CD{7#a*^}>+Kj-KnAJ3K8f^K zRP4wUUB_I^MNf9%sH!i6W9_N_4chc_NSaM-0dtZzmD4E|H$>_M%bdV+kAyAQFF3~n zgfiXGdx&ze-rKHBXUahPB&4Wp112Nw&`O8_b2Zuz{P0XbmQvxKDMuhEC?rWNy5L($ z-PC$ziCn}ApZ)xl7IOQvqn$;6`8F8+RTS*|*w}l=sbzmW>IS47(V*hf@o?UHj zLI4aYdP~_NCJDQR<#eka8?yLS%#Jm{3CTW8u8JK#eVypT$hUE%m7gQX=5WUt$sayg z6>t74=jH^QNI%zN)(uHpwA0R3#d@pR(FqBr>Ayqo-c_bZ!N39wO4sL%8O_h&oQ?1%V`U!Y0Wd$+edqXHhWUgyY@r!MD@f!LBzZ{kj8hkaSN-VHR>VH5ru8ivBu+;q3 zSdb;fr1wg)=Je>Va<1?%A#EDJo}))=;Br@P6ZsZ*hI&&?o`J-?sr)mH%~zPPT;yIQ z;wfdhUjcvPv@o;jr)Ih}&uu9zh6{JtRo+Y%srh9Vc6`AjY419``8{WDH=jb=3Wxe5 zzj;i^>8rF&3kVxy(>CYKE0pS%y@PkPAPpK_>p9IH&-1t#dj1-xE68`q>r!eq2Z`sf zH!k~jn4q>HAZL`+yDkG1csotqGlUJ1)wa`BCc70aluzd9FzIJ!s($ahHU8v#uOy?az1AQ3HTw zD}G4~d_*ANHZCtOE%K@kLgnT}Y_5uu@i`zwKVT#qT27UI0)ilpO?%-?ZhweOs z#fc$-OP^GS`+w<62QX9R_(U&vtQ-3zQypE5o2vM3iP4<*Faeg_ED%3Z3(U7`4^KyE z>rznQfKkw4yMDxgTgRA3{}(@ty=elESwrkYM)AW%?*t$`;Q8^>GbtpV!*sJJ_{=fP z=awj<6wrJfY?zdHIJWMwDGh?B+k9b$f~@SzTWNpJY?f+z-=B)idXq*ENu=w z9kqYX268fQ=I&U(=&16P*`8wisL>*Fzo2k^Wg4C(-SSObZ3vrZ$-jYKLr4Sc%K@QH z&wb>7S?KTQGxVDs>uJEyo53k~O?ye&q<%r2on6YuE2EvJn;UrJuX?hWYGLWK5{MJy z)x2JADzC;kaO|w9$cfOUNJ;`E%4~450iq00cbKE>O`lB*3aKC3QV!kfUpS%-Ph%o? zdC&#SP#Y=|&Ou)_X>jUyN?}r;9_t@8d0O1A#95UO1)x@Rt|$|VqWw*c0DC<3Y5;=8 z-~EM+9gdkrGvI+N?q^$Gb{T1;= zhA};Q)Zj7IFgdE|@@?^1V%%L{`MUiS#`ce|)x}V>eoGoOUS$Y6>hA`#nHK69*Sjix zOb{9_Z6?_FL!zrInj~)5J5$u4)=o`+6`l^ zF18DpFLo~9o5XtdNX0uFP5ju6tUTOaB-oimPl5@H{7;$zR9|d73H`kCFjFGQ2=H zQTTpKHrdz`hDMwvr`4m;#2boVRMVJ$xTO@zoHc9HI+uvfoD1Vjg~s>w`v9nB_?(#J z3JT^*ch{jY0;1rLe#?B&LiHfog5+clbFCM;YVi!LtIFoIX_f(^VJ1#z;!_ach+6Qq z8Paiv2&ZAsH)cN|#$R+F(XMfMO);@wlZePl6Lk3MC4iOS%P{ZM6$Y5UQ+7PoQ2Q0? zH?vp2d|;~8TIa>$8lh$`r) zM);O>esPobDFjeOd4E7P5Cc%jWwTxpWWt&l;7?JgwCTPT>nQWi$Y%6)@f?l# zAeGtN{z2zMBh{8hsjO1u=HLR`(U_E9+>Q!IFJiGI8Kyq?Yo1vpmHWNCXPd^YLjpp2 z)4AoV)TKYfh3p61KJ0R8YzY=Kmc}c0l@3fCWLp5Uy9vV`H)~^@PDxvcD2{((_K+DC z;ruRPO?XZpri|S>*i|U|89#i7j=R|= zFxTE}g&Q{Ys5sAssie!|8yexGXDvH(=7CS=%o*(oUU7Uq+dP|=A@$e3d9M_ETbO#Y zfTy_AZ8VC!#UkI3xY^h63H{XH!)`Lya_J<}uVn=06r*Da&(4iJ!$Jw_(&)glgF?z%pQIMdxJq^Nl zhi0BljGng*FHEN${?;#KGzjy5#U`U-9Gx!Z?@_1rNp)&JMW>H|yyIRvrArTr&U|}F zsKZt14@F<5^)5j~4Lb0xwd=x8%MrbJFZVzjU1_g=p(5@^W3h5mZK1w80SON4)el7^_-ItdmysJ$!2cUg8cg^qoJ57N1kV6 zlqNA`xcl<(u`$=}lQCfK`a}Yt$sSJcV}!#XiH90dD;bV1+nAcWWv!HcuXQx0Ep_e* zU0JR<-ga|#dbm;pj9pJrbK0Qy%+lx1Is)~Q_BTUUq_b*A+IunvlOpdt+$Z$df><<% zjc-AoEvJottSIokbPF7w*+%))E@j4&#;ffI02)gv6l3D0@}zeYsjQcb$(p9%V)si- zfTppdjQhMN#>gGE%9MpIrH{oDD|Ado@yJE&&?Q&j)*&~8`k#2Z_SwxJIA-f9Lfze@ zpMJ5^Kyd#ijrHBJOcy)XZM4+o5Ndrz-h6#m1AaRi5((S^4?w=Q;x&Ht-O!#mbDTVN z)C@X;ldZ2*QFI2oY)ByyyT8X2mfbkS4r_cpMcM3SLvi}9QR?ZJhkn7mlDFcFBZ_m{ zcC#O1K(#SH{i+UDU=4A=EZH#YsE>Ace+OjV6#9|w+!!*#n`TyXo1}5FuZW;AF_i`{ zyVvbpL;PGx_eRVl8?W!WsYa)mn)aZ_nrt7x$lVWiRppS#5$-fu(WAw@u#+l)gJ;E4 z9XyuU_%#6fV8vCkD-e4gG~6F3-r$^aXgs-WmEYe@Cy+O{Gof)~n04gSnyILGPwx{D zjt+0jN$dud1q_J;uX?UKjzrW_Ml$zO3@cey_6K4rGB;CU=uqCQv!3hh2-qXo$rm7+ zck~DJV~%5~&E1vyvKmFDF)pFNm*qD;rw|^P z(5au~xc3IquhY-voCB)BLkbwtQ7hx8_cLGRs-Yi6`@Q&ZI406hB_ z6QGvZQ9q_(--F9kzEW4w8U=VW9bahWOgac#U*0-jfnSoP8%Y(j;F+p&sOqP&`4c^> zFszl$0+V^6jV=$~-rZ~Z-g(8-Ccw8>Bsl+j!HzLyp|-e%0vb;_4m9^ab698)oC5Wh ztS-4WUti35#?BM^iSu^3CB}^@egZBfqli@nk^`?cE@J2*y-jRu{KkrUzl1!CK1eq{ z;otsxeS@+D?hOQfZUC<`FYCLk$s~e3XG) zw zFO=2G{~Hu2H~m!NaIxvNyi`EtHQ&AhvblUa+w*4kI#aEYX)53zI`h?Xw*S!+!)m?{ zhwdS)lxY^9dx7^pri-zR8&&$aO%^zm9Y4K_$+0yen<&`42$>t$wgzQDk*n2M`2fwaWlQTE1Qm zacqqwk6elmwft;>wX-sQJTCHEfq1@TyRQ-(pH1dB+)bsWGhZDk1he-vLGb7A&u&PX z3;sITErunk!WpvbP}yzS9y@kAL#0ZUC(WZL>(V6VoO2%nj)-Q<4d+}Q$X7m757GuF zN7&e3HhYUNZ0!k(^FUvZWHyo-LY73OBQROl@BBk+-%!XMj>PtrYn{?+($+1}FAjby zxa@6h>zY(gr`JqDe7Rs?ma>q|@ZI3o5MT9hnc5p6$7xl{bjC!C@jKe)!4?6z{mLpe z(0SfT2x$(JwhkqzgG8Qli48T6lv$tbL5D~3vEQli8fD-hVpBXvX`H`YU&CWF*fj12 za;En9kK4YdYm|CbSN5g+>sbTeiz$;bLVk>!hR8E8%5V9K*#cCjmtfhv!)|rsV$}E+ zL$ilLFj5>F3cw|yC2G+{25sx&GBrFr^#2S#|_Rn4yGVxW`$dg;|%5~hCVL70eo54XfPe;1PQYg zdHytX!x(;xC^C-D2W}}k(9Fg{xNZsJ@T4ukVR*57CU=o>rhsAFh8<*3vmEu!bZw_8 zcj!nM{a_zh70~M@WbkCx`_bk{+MREx(wu#SE|JaIZ*vb^O$r;}Qx}yEcwI}*qbb~Q zpWwvwcI8=~oSd@#EX5%zg?2NQI?-$S3bBmLUK=@%k+bHK3gBahU5qA{!?7gqHezi9u6>f~?OBt^T_d^pDu{;7Iv=CD*$M!>)T zn<6ot2o%Z`aCxqeem)+R-0U!iL(Ud6)hVm8Sn=8s9iEUdHPTl!vRgE8;T^V^Q6l7N zE?u8@z>B3eq}F>CzvST}AZ~1CAb)@L7 zekeaFfN>|B?kM4{C#S&pT6O;sZ%+Cf2iwuthuRxRDAnek&rjH!a?_UV=~CVPrQ&ki zZRihPQ@&v+Ni}mtt{QJ3&kp$|LD3LnMbNCC5Gg=?2MC$=^3EmO;S@5pJ%G^B8hHG0 zaf+L4D=H)+IX9^HZEC$5)X}0U(Pl9`YIgbrvfHa8I4vyR5C2Zo$E#&*t>`q~mP$7%SF%5*@D&BqlfEZ0v6Z6`6 zfsIdE-*S2tm_4Gi`l*3cr_9H+r5q0>ar)UIZ`e8K(;vXl^X$Ih`={9y{VmS#aMgQ^ zxl^c4h_Zfe{v!VYk5PD&^Epi%Nd>$`-#!M4|6+I&^pEEXO>#em4SMVz>#H3bz~dy{ zo3ZWApsQ=h}iff%X;~^>Ytk-%pQnAgK#Ce~)vg2H}{L8}6GSr9=h*GeQ zC0Z%5Jq8H0eRBuv?>_IR1UM-E&jR?D_n6+lC6u`7X;at?sGa(!0_^Z$e3nH%om;4` zKEmN7%tG&6_g!YZ{WvSPe<}9_J0VdbOBd~5;$~>siwIDFm(FQr&IxVm@#GKG85WUIWyyQ;Wng!efpwNzicZF zYbSATC*%}BxH`%h8QIxW1?ET+M#?CoR3MJ?t+Xr#-d7CYRLPq@xXZ)BISEzy6J@%|Ix<3S-T@-4ald}@+IM!~%hIj`mM zKYIJ$OaI4=yZ>1y=x3w|mf_<+Zv3+`fA63F`BS;_PB8iZFZ|zo_Wz-7Vd5R#G?Y-a z7+>WmaDl{lSU{x~Fo`sV&0iX~XI>N@EYxy1tII$5pVQ{grV>Vsf8biWy63B=^Yxm} zYFDEPl_(0GWet@|HMKBa$M{5FM4}V8ss3#eNcQO;i^6}1!oU7_Uq`^~V?SqVJBFG7 zba3c?=PW)GH=o=SOjb^QG1PewTH^rsO_y>?;50ccgYhZ&ZS4Tek_nZRv|D^6X}mf& zwJUt1rb`lE)H}{4k7Q29`Y@a`{spNBe#jE1$tBv`D3i>gn_R|C$6b+8U;Bz&EU}?L zvr;{SSOooW_v5uv`w;zM`T{=LFR-b&&E}xZ=_M6o^J8zv;^yvug);mf+7~1;$j(bIc9F5VjC`=*X#fc z`R7vn&ya~ftA|B}$$c}!rb+p2nXPF__S8c;dE4ez@hux+3rkq`vd7>6_5t6r#tsK# zfY6J%Q)?U?3^^hh?jJ_`e~rYSTqRiq<8;(`@6Y#-WQ%y0WA@MKphf(a9s9-a^~@=1 zUfVYRJAnGPnZ&Mkw>_9Vnog(@mAmXwZqPQpoHN>=!z@@j=hZ)y^!%B_S!VNk;X9@h zSve(jF!yF+LR8%kieaCBnvwp6#D8+7S%dQtn=K*XZt}G_z(&dE{FU)-&}X6ce&jvvn6`A0@CzWC^Z zt(D(|$|~z{SGU?zJY7t6 zFxn@t!&jKLNq-n%9Bzjlcbfiey8OpIP3yY^6&Z|dXM06WbgCGeX+$lVjmknz-2c-75j^>VJ|eg#nOX!2(spnb{n2qv#?lffLj=nqbNASk_wy` zYJ6IC%d9qUP&p*&4J@9u5~!uuPk<*e#3IP2jr)nI<^d+N6~^?i7lsUf>17BNF)zc+ zqDySJ-0ttkQf--^q;?O7rhK_dJHS`~0xH%Uk$D+rYwZHv+e)nqCZu?g2A02zTGekj zcA((EH=aDsl&%DB4kim%cr!2b)|HWlu%5p7b^RZM{3o*g2pmL`_3PLC{hH*jTy%xn zy+uMisPR{q)=Q8lQ^o?Kee}?(>Wzk6OiF`@CZnvKh1!m{rEd)zTt<>|iZpzXukjmP zIOdt+Ha7N-uC~WAH%a;bq?SK{E|%ArMwhFQkp!giIwc5{Gv3QG$Z41NIaoSMX)yAL zDwzIm)h}#U6rt*A_u>gQzn%JsZo+xLcSA7wE}ihNje&JfHJ*-}HEzXW6( zI!y0iE6aR=6Fg78@R;6Y9!OV>Z3%v#GidhOVIe4jkdkEluwFgWE>fitciB1^r=*kB z>-wCebWh|}@SgzAf3}oGhw0?o7&I;vpKR;JvYmMw$2Lf?2Tuuc2yEElNFyCsJmaug z>B_$1 zc6*lWzZ1u4+OD}m1Fx~RvqNzk=ku#weqnW%EwJi$1)IwE6+Il0UHel}{5PZ)p~Zdp z?GORupc<&Xy@_~RX7rl2P%yQ^FukEL^59^J>+Kt34W$eryA__vmriAzW)hW}Q8O(_ zn#t3&c_yqzQZMQ+_1`7cXIhc9av#V!7ayQg)voSsheozU zyXFwI>K(aKi6LaU?{6#caN6x)Fg9vg0`IRhMkd$#Ue6;DExsrtF=bwi|NYW?lz5?C zQl-IlBncC&q^!{SoaL`fAcZ`P295i1yk3R0d6V7ZtzN`mXq3zo0_I1PzJ^?XrG|S< sn!j=HWwD<`e7^fv^rj%0cLdkcHRT9X$?5WocbHEtVPKG0&!Pp4*&oF literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/image.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/image.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa4826e9ff96742d17674f5e48a72edfd2cbc81 GIT binary patch literal 115380 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle.md new file mode 100644 index 0000000000..d3f3682b39 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle.md @@ -0,0 +1,269 @@ + +# Merkle Tree API Documentation + +## What is a Merkle Tree? + +A **Merkle tree** is a cryptographic data structure that allows for **efficient verification of data integrity**. It consists of: +- **Leaf nodes**, each containing a piece of data. +- **Internal nodes**, which store the **hashes of their child nodes**, make up the layers leading to the **root node** which is the cryptographic commitment. + + +## Tree Structure and Configuration + +### Structure Definition + +With ICICLE, you have the **flexibility** to build various tree topologies based on your needs. A tree is defined by: + +1. **Hasher per layer** ([Link to Hasher API](./hash.md)) with a **default input size**. +2. **Size of a leaf element** (in bytes): This defines the **granularity** of the data used for opening proofs. + +The **root node** is assumed to be a single node. The **height of the tree** is determined by the **number of layers**. +Each layer's **arity** is calculated as: + +$$ +{arity}_i = \frac{layers[i].inputSize}{layers[i-1].outputSize} +$$ + +For **layer 0**: + +$$ +{arity}_0 = \frac{layers[0].inputSize}{leafSize} +$$ + +:::note +Each layer has a shrinking-factor defined by $\frac{layer.outputSize}{layer.inputSize}$. +This factor is used to compute the input size, assuming a single root node. +::: + +When dealing with very large Merkle trees, storing the entire tree can be memory-intensive. To manage this, ICICLE allows users to store only the upper layers of the tree while omitting the lower layers, which can be recomputed later as needed. This approach conserves memory but requires recomputing the omitted layers when generating Merkle proofs. + + + +### Defining a Merkle Tree + +```cpp +// icicle/merkle/merkle_tree.h +static MerkleTree create( + const std::vector& layer_hashers, + uint64_t leaf_element_size, + uint64_t output_store_min_layer = 0); +``` + +The `output_store_min_layer` parameter defines the lowest layer that will be stored in memory. Layers below this value will not be stored, saving memory at the cost of additional computation when proofs are generated. + + + +### Building the Tree + +The Merkle tree can be constructed from input data of any type, allowing flexibility in its usage. The size of the input must align with the tree structure defined by the hash layers and leaf size. If the input size does not match the expected size, padding may be applied. + +Refer to the [Padding Section](#padding) for more details on how mismatched input sizes are handled. + +```cpp +// icicle/merkle/merkle_tree.h +inline eIcicleError build( + const std::byte* leaves, + uint64_t leaves_size, + const MerkleTreeConfig& config); + +template +inline eIcicleError build( + const T* leaves, + uint64_t nof_leaves, + const MerkleTreeConfig& config); +``` + + + +## Tree Examples + +### Example A: Binary Tree + +A binary tree with **5 layers**, using **Keccak-256**: + +![Merkle Tree Diagram](./merkle_diagrams/diagram1.png) + +```cpp +const uint64_t leaf_size = 1024; +// Allocate a dummy input. It can be any type as long as the total size matches. +const uint32_t max_input_size = leaf_size * 16; +auto input = std::make_unique(max_input_size / sizeof(uint64_t)); + +// Define hashers +auto hash = Keccak256::create(leaf_size); // hash 1KB -> 32B +auto compress = Keccak256::create(2 * hasher.output_size()); // hash every 64B to 32B + +// Construct the tree using the layer hashers and leaf-size +std::vector hashers = {hasher, compress, compress, compress, compress}; +auto merkle_tree = MerkleTree::create(hashers, leaf_size); + +// compute the tree +merkle_tree.build(input.get(), max_input_size / sizeof(uint64_t), default_merkle_tree_config()); +``` + + + +### Example B: Tree with Arity 4 + +This example uses **Blake2s** in the upper layer: + +![Merkle Tree Diagram](./merkle_diagrams/diagram2.png) + +```cpp +#include "icicle/merkle/merkle_tree.h" + +const uint64_t leaf_size = 1024; +const uint32_t max_input_size = leaf_size * 16; +auto input = std::make_unique(max_input_size / sizeof(uint64_t)); + +// note here we use Blake2S for the upper layer +auto hash = Keccak256::create(leaf_size); +auto compress = Blake2s::create(4 * hash.output_size()); + +std::vector hashers = {hash, compress, compress}; +auto merkle_tree = MerkleTree::create(hashers, leaf_size); + +merkle_tree.build(input.get(), max_input_size / sizeof(uint64_t), default_merkle_tree_config()); +``` + +:::note +Any combination of hashers is valid including **Poseidon** that computes on field elements. +::: + + + +## Padding + +When the input for **layer 0** is smaller than expected, ICICLE can apply **padding** to align the data. + +**Padding Schemes:** +1. **Zero padding:** Adds zeroes to the remaining space. +2. **Repeat last leaf:** The final leaf element is repeated to fill the remaining space. + +```cpp +auto config = default_merkle_tree_config(); +config.padding_policy = PaddingPolicy::ZeroPadding; +merkle_tree.build(input.get(), max_input_size / sizeof(uint64_t), config); +``` + + + +## Root as Commitment + +Retrieve the Merkle-root and serialize. + +```cpp +/** + * @brief Returns a pair containing the pointer to the root (ON HOST) data and its size. + * @return A pair of (root data pointer, root size). + */ +inline std::pair get_merkle_root() const; + +auto [commitment, size] = merkle_tree.get_merkle_root(); +serialize_commitment_application_code(...); +``` + +:::warning +The commitment can be serialized to the proof. This is not handled by ICICLE. +::: + + + +## Generating Merkle Proofs + +Merkle proofs are used to **prove the integrity of opened leaves** in a Merkle tree. A proof ensures that a specific leaf belongs to the committed data by enabling the verifier to reconstruct the **root hash (commitment)**. + +A Merkle proof contains: + +- **Leaf**: The data being verified. +- **Index** (leaf_idx): The position of the leaf in the original dataset. +- **Path**: A sequence of sibling hashes (tree nodes) needed to recompute the path from the leaf to the root. + +![Merkle Pruned Phat Diagram](./merkle_diagrams/diagram1_path.png) + + +```cpp +// icicle/merkle/merkle_proof.h +class MerkleProof { + // Represents the Merkle proof with leaf, root, and path data. +}; +``` + +### Example: Generating a Proof + +Generating a proof for leaf idx 3: + +```cpp +MerkleProof proof{}; +auto err = merkle_tree.get_merkle_proof( + input.get(), + max_input_size / sizeof(uint64_t), + 3 /*leaf-idx*/, true, + default_merkle_tree_config(), proof); + +auto [_leaf, _leaf_size, _leaf_idx] = proof.get_leaf(); +auto [_path, _path_size] = proof.get_path(); +``` + +:::warning +The Merkle-path can be serialized to the proof along with the leaf. This is not handled by ICICLE. +::: + + + +## Verifying Merkle Proofs + +```cpp +/** + * @brief Verify an element against the Merkle path using layer hashers. + * @param merkle_proof The MerkleProof object includes the leaf, path, and the root. + * @param valid output valid bit. True if the proof is valid, false otherwise. + */ +eIcicleError verify(const MerkleProof& merkle_proof, bool& valid) const +``` + +### Example: Verifying a Proof + +```cpp +bool valid = false; +auto err = merkle_tree.verify(proof, valid); +``` + + + +## Pruned vs. Full Merkle-paths + +A **Merkle path** is a collection of **sibling hashes** that allows the verifier to **reconstruct the root hash** from a specific leaf. +This enables anyone with the **path and root** to verify that the **leaf** belongs to the committed dataset. +There are two types of paths that can be computed: + +- [**Pruned Path:**](#generating-merkle-proofs) Contains only necessary sibling hashes. +- **Full Path:** Contains all sibling nodes and intermediate hashes. + + +![Merkle Full Path Diagram](./merkle_diagrams/diagram1_path_full.png) + +To compute a full path, specify `pruned=false`: + +```cpp +MerkleProof proof{}; +auto err = merkle_tree.get_merkle_proof( + input.get(), + max_input_size / sizeof(uint64_t), + 3 /*leaf-idx*/, false /*=pruned*/, // --> note the pruned flag here + default_merkle_tree_config(), proof); +``` + + + +## Handling Partial Tree Storage + +In cases where the **Merkle tree is large**, only the **top layers** may be stored to conserve memory. +When opening leaves, the **first layers** (closest to the leaves) are **recomputed dynamically**. + +For example to avoid storing first layer we can define a tree as follows: + +```cpp +const int min_layer_to_store = 1; +auto merkle_tree = MerkleTree::create(hashers, leaf_size, min_layer_to_store); +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.gv b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.gv new file mode 100644 index 0000000000..c8ebf342f1 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.gv @@ -0,0 +1,115 @@ +digraph MerkleTree { + rankdir = BT; + // Change to bottom-to-top for reversed flow + node [shape = circle; style = filled; color = lightblue; fontname = "Helvetica"; fontsize = 10;]; + + // Root node + Root [label = "Root\n (Commitment) | Keccak256";]; + + // Internal nodes + L1_0 [label = "Keccak256";]; + L1_1 [label = "";]; + + L2_0 [label = "Keccak256";]; + L2_1 [label = "";]; + L2_2 [label = "";]; + L2_3 [label = "";]; + + L3_0 [label = "Keccak256";]; + L3_1 [label = "";]; + L3_2 [label = "";]; + L3_3 [label = "";]; + L3_4 [label = "";]; + L3_5 [label = "";]; + L3_6 [label = "";]; + L3_7 [label = "";]; + + L4_0 [label = "Keccak256";]; + L4_1 [label = "";]; + L4_2 [label = "";]; + L4_3 [label = "";]; + L4_4 [label = "";]; + L4_5 [label = "";]; + L4_6 [label = "";]; + L4_7 [label = "";]; + L4_8 [label = "";]; + L4_9 [label = "";]; + L4_10 [label = "";]; + L4_11 [label = "";]; + L4_12 [label = "";]; + L4_13 [label = "";]; + L4_14 [label = "";]; + L4_15 [label = "";]; + + node [style = filled; fillcolor = lightgreen; shape = rect;]; + // Leaf nodes + Leaf_0 [label = "Leaf-0";]; + Leaf_1 [label = "Leaf-1";]; + Leaf_2 [label = "Leaf-2";]; + Leaf_3 [label = "Leaf-3";]; + Leaf_4 [label = "Leaf-4";]; + Leaf_5 [label = "Leaf-5";]; + Leaf_6 [label = "Leaf-6";]; + Leaf_7 [label = "Leaf-7";]; + Leaf_8 [label = "Leaf-8";]; + Leaf_9 [label = "Leaf-9";]; + Leaf_10 [label = "Leaf-10";]; + Leaf_11 [label = "Leaf-11";]; + Leaf_12 [label = "Leaf-12";]; + Leaf_13 [label = "Leaf-13";]; + Leaf_14 [label = "Leaf-14";]; + Leaf_15 [label = "Leaf-15";]; + + // Connections: Reverse direction from leaves to root + L4_0 -> L3_0; + L4_1 -> L3_0; + L4_2 -> L3_1; + L4_3 -> L3_1; + L4_4 -> L3_2; + L4_5 -> L3_2; + L4_6 -> L3_3; + L4_7 -> L3_3; + L4_8 -> L3_4; + L4_9 -> L3_4; + L4_10 -> L3_5; + L4_11 -> L3_5; + L4_12 -> L3_6; + L4_13 -> L3_6; + L4_14 -> L3_7; + L4_15 -> L3_7; + + L3_0 -> L2_0; + L3_1 -> L2_0; + L3_2 -> L2_1; + L3_3 -> L2_1; + L3_4 -> L2_2; + L3_5 -> L2_2; + L3_6 -> L2_3; + L3_7 -> L2_3; + + L2_0 -> L1_0; + L2_1 -> L1_0; + L2_2 -> L1_1; + L2_3 -> L1_1; + + L1_0 -> Root; + L1_1 -> Root; + + // Leaves connected to layer 4 + Leaf_0 -> L4_0; + Leaf_1 -> L4_1; + Leaf_2 -> L4_2; + Leaf_3 -> L4_3; + Leaf_4 -> L4_4; + Leaf_5 -> L4_5; + Leaf_6 -> L4_6; + Leaf_7 -> L4_7; + Leaf_8 -> L4_8; + Leaf_9 -> L4_9; + Leaf_10 -> L4_10; + Leaf_11 -> L4_11; + Leaf_12 -> L4_12; + Leaf_13 -> L4_13; + Leaf_14 -> L4_14; + Leaf_15 -> L4_15; +} \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1.png new file mode 100644 index 0000000000000000000000000000000000000000..c2260c5af409e3c34bca78731708b7092ffa8d36 GIT binary patch literal 80489 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.gv b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.gv new file mode 100644 index 0000000000..cc6b941fb5 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.gv @@ -0,0 +1,117 @@ +digraph MerkleTree { + rankdir = BT; + // Bottom-to-top direction for root construction + node [shape = circle; style = filled; color = lightblue; fontname = "Helvetica"; fontsize = 10;]; + + // Root node + Root [label = "Root\n (Commitment)";]; + + // Internal nodes + L1_0 [label = "";]; + L1_1 [label = "path.3";fillcolor = yellow;]; + + L2_0 [label = "";]; + L2_1 [label = "path.2";fillcolor = yellow;]; + L2_2 [label = "";]; + L2_3 [label = "";]; + + L3_0 [label = "path.1";fillcolor = yellow;]; + L3_1 [label = "";]; + L3_2 [label = "";]; + L3_3 [label = "";]; + L3_4 [label = "";]; + L3_5 [label = "";]; + L3_6 [label = "";]; + L3_7 [label = "";]; + + L4_0 [label = "";]; + L4_1 [label = "";]; + L4_2 [label = "path.0";fillcolor = yellow;]; + L4_3 [label = "";]; + L4_4 [label = "";]; + L4_5 [label = "";]; + L4_6 [label = "";]; + L4_7 [label = "";]; + L4_8 [label = "";]; + L4_9 [label = "";]; + L4_10 [label = "";]; + L4_11 [label = "";]; + L4_12 [label = "";]; + L4_13 [label = "";]; + L4_14 [label = "";]; + L4_15 [label = "";]; + + // Leaf nodes (rectangular, green) + node [style = filled; fillcolor = lightgreen; shape = rect;]; + Leaf_0 [label = "Leaf-0";]; + Leaf_1 [label = "Leaf-1";]; + Leaf_2 [label = "Leaf-2";]; + Leaf_3 [label = "Leaf-3";fillcolor = yellow;]; + // Highlighted leaf in path + Leaf_4 [label = "Leaf-4";]; + Leaf_5 [label = "Leaf-5";]; + Leaf_6 [label = "Leaf-6";]; + Leaf_7 [label = "Leaf-7";]; + Leaf_8 [label = "Leaf-8";]; + Leaf_9 [label = "Leaf-9";]; + Leaf_10 [label = "Leaf-10";]; + Leaf_11 [label = "Leaf-11";]; + Leaf_12 [label = "Leaf-12";]; + Leaf_13 [label = "Leaf-13";]; + Leaf_14 [label = "Leaf-14";]; + Leaf_15 [label = "Leaf-15";]; + + // Reverse connections from leaves to root + // Connections: Reverse direction from leaves to root + L4_0 -> L3_0; + L4_1 -> L3_0; + L4_2 -> L3_1; + L4_3 -> L3_1; + L4_4 -> L3_2; + L4_5 -> L3_2; + L4_6 -> L3_3; + L4_7 -> L3_3; + L4_8 -> L3_4; + L4_9 -> L3_4; + L4_10 -> L3_5; + L4_11 -> L3_5; + L4_12 -> L3_6; + L4_13 -> L3_6; + L4_14 -> L3_7; + L4_15 -> L3_7; + + L3_0 -> L2_0; + L3_1 -> L2_0; + L3_2 -> L2_1; + L3_3 -> L2_1; + L3_4 -> L2_2; + L3_5 -> L2_2; + L3_6 -> L2_3; + L3_7 -> L2_3; + + L2_0 -> L1_0; + L2_1 -> L1_0; + L2_2 -> L1_1; + L2_3 -> L1_1; + + L1_0 -> Root; + L1_1 -> Root; + + // Leaves connected to layer 4 + Leaf_0 -> L4_0; + Leaf_1 -> L4_1; + Leaf_2 -> L4_2; + Leaf_3 -> L4_3; + Leaf_4 -> L4_4; + Leaf_5 -> L4_5; + Leaf_6 -> L4_6; + Leaf_7 -> L4_7; + Leaf_8 -> L4_8; + Leaf_9 -> L4_9; + Leaf_10 -> L4_10; + Leaf_11 -> L4_11; + Leaf_12 -> L4_12; + Leaf_13 -> L4_13; + Leaf_14 -> L4_14; + Leaf_15 -> L4_15; +} \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path.png new file mode 100644 index 0000000000000000000000000000000000000000..178d5366ef88c1917e5a7145156457ee51cfdff7 GIT binary patch literal 73224 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 NBMoP2nPfsx}WlS0-4%k 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 L3_0; + L4_1 -> L3_0; + L4_2 -> L3_1; + L4_3 -> L3_1; + L4_4 -> L3_2; + L4_5 -> L3_2; + L4_6 -> L3_3; + L4_7 -> L3_3; + L4_8 -> L3_4; + L4_9 -> L3_4; + L4_10 -> L3_5; + L4_11 -> L3_5; + L4_12 -> L3_6; + L4_13 -> L3_6; + L4_14 -> L3_7; + L4_15 -> L3_7; + + L3_0 -> L2_0; + L3_1 -> L2_0; + L3_2 -> L2_1; + L3_3 -> L2_1; + L3_4 -> L2_2; + L3_5 -> L2_2; + L3_6 -> L2_3; + L3_7 -> L2_3; + + L2_0 -> L1_0; + L2_1 -> L1_0; + L2_2 -> L1_1; + L2_3 -> L1_1; + + L1_0 -> Root; + L1_1 -> Root; + + // Leaves connected to layer 4 + Leaf_0 -> L4_0; + Leaf_1 -> L4_1; + Leaf_2 -> L4_2; + Leaf_3 -> L4_3; + Leaf_4 -> L4_4; + Leaf_5 -> L4_5; + Leaf_6 -> L4_6; + Leaf_7 -> L4_7; + Leaf_8 -> L4_8; + Leaf_9 -> L4_9; + Leaf_10 -> L4_10; + Leaf_11 -> L4_11; + Leaf_12 -> L4_12; + Leaf_13 -> L4_13; + Leaf_14 -> L4_14; + Leaf_15 -> L4_15; +} \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path_full.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram1_path_full.png new file mode 100644 index 0000000000000000000000000000000000000000..ec963f5e5392f40333c0750dda3df586d1861ce7 GIT binary patch literal 76821 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

    GYAE?dbM-ju$sA&fDw}AP|9=t)Jx#% zY6muET{V-LpUF3a+$^Ack0JPuYF1Hy>@&T zaY(+de=yL*JGlYH2W@4H(hTZ_!@V&aN2A)dIpyUlVbAUU2r+4viiWAEYz+YpGSiXZ?b9YJEL|_pZbs@AOv6-kZi!Qi_%BS$ z9oa?}YK%3_nO$BU3~WR3;lgORXTAh;I+;QMZu$U;#cws%6K0ZDkPG&1_9tn5O zjt~|UCh%Xf=Nc(ejeWph7%pHsQ|r9`JFSy$&UxOY?(v-)5g{ST&3?BnmwtxU9#2{t z+nK%v8~m5(5cQvL4X_-|*97C=yX9x<1z%|Y;=ENcmUzs4wmhQcS}YpYrfPqB?((!r zKM#YdIw8w@yA@k7UtQl@)u#zjK5%i2_+7WtmL**9WCp zMt-MdHL<)OS{oiOF*<6w&+-8^ygF)lZkMgejJ%>ot>xuqni)Ouu!Y6N3hSvCV1~{7 zU~P@%q1?-~d)*5i)dp>>7#SVaM`?K9KqagH@@8lKUI%gQ$*iY*-HhDMY(s_Ho?(p+ z&0HyDns+#AOkrY>ha1nAjeF7E*T-)xxnak37$A5#AOG1)vis{vHf+PGorlz+80MkD z`Zn9#@auV{6mQm3{jlwtRWe=2K288eLATyDCwhKdsj5=;6KsKH&zlqET7DoePeInv-))_<^%kNov3zH~)ecj?{b3!@pg>{&*rpHpK z_;cs=;gAGJSM$h5TH0{R7#;R8?f?VY{$-UNyl)Xpcl(ZCj*N`dUhelqCMB^*#&X%G zCc3W>YDQ(yDQEswZ#s{B{f#IiTF+xGU3(u)j%M(I_zz?$^#vLw!Xg1!sumWxc#Im! z00*QntGPz+_34^2V3?J*bA?MwOCgDMy~MvJuT#;VoVANN&ahHyh5ihowenebcUW!P-B&lRr1mVu_ z+)=I?dl3!T2&C?gpt(=pS<01}j8;!DZ0`hj%-vQ3Zymnt1 zo0>v=+}+z-&O1m*nxFmKzjZnH_%_|4d^0NsKo3-5WarSl3so5V&6c|(iFll{!@|Qk zVQ{Ys+qv0o2{5V<^A0gk3cdxvlsbaRivS63Y}f6K0`hy73MeS^Q%P;3Y-XWNSiVNI zvVV)?5Q?`}1UHe_)c2aK#!akG5yD-QC@#WMx$c68S6a=7muOP^&WNz_&Wr z0MVj)0WKmUBi9l=w|}RKeK`am&^(!gHG%cTnQKn!Y8CW}E$80-&YpgYJI|IN%vkc< zBELmaX!;>nF!apW#>jUzqr(nj+rJa0Nh8Yg{8X)fZbH?f@%P9g=bPXH?J9a28u5&b z43Co;*O4@F?7C+kKTxeHhr6YGb!uuM8b@R8?{q(wNi=NYqx?Pu@7a+ z4h{GmbIiC*Sp!S=2Q0c31W@AZAAnTxM>~Gl0>5_WTSpqbJ^K=P)d43~@G2$H79N|c zn^%A$iZNN-P;h3RZGgega>n)=5{vNI=_*8ZFksZO#5qm0eFBj{pT4c>3pZZOLx3Fz9}+q-n;I{FML`Dj)gN2%picer}p zPn4*~c^UN4mB2risy)|PDxQ7Oh^TGm8f5SB@g$EQJ)(X2QvK@UqzYL-C&EJ7&~R{U zEYUY2PN_1JzQM8qfjs-gjtSB(ypeawP(=}@`R#LqfaYiu&2(#IYlw`r12>697(p7I zB&0S&%h0#U^%WIULZ69S%(r~>fS;`%0rxyf3PB$8I#MI9>-qRJIyg9#d!E?zCGu;7 z%_hu*w!EKv?p!^X1xhkls-xM|Z>0Q}LK>FgRV>T|{t=t@Ip&7e?n!v7<~R>eP4{P9 zzw;gb67c|XYI#p|`lSNROBk%aQCdi1XmM}NZRm6eJ^Y@=Ak&aptsc%QLOrr$=OyJ6w9Teu0x?4Wp zcLq4wfILq;w<7?jG4f7CSx{$|U_O*-zByh}JMT|?xDsbe&&@rtErFZWO|BBdk}Rb! z6bb)R66^~q_C7sf?pRxmJ@X-*=CmDaDq)>DZmwJ6VVxNu0-b(zvbx#B(xf6M7j)^< zw9*%swYh19v=Is(l)gHw*DLA>BFzIn>Tga~;-KRIKTSZZfop#DqjE;jVVa81vuUgx-K-}a)g}d7#W9) z4O+{6t~?;z0-mkpRZ?*z@?X%At226Twzjr*zT1k;X}=&k>bdZ4DG zlMnecmipcIdfrE>(AhZDjCbLhjpoM)TpcPyW=_QCR-EW_Zmt<+?8*ohDaz_I1gQ3h z`$Vbn7XV}xJ-tHXo~RtplWoAkEmATvU-jIVA4Bfls558;T;owULlLcj!0fgecs)=^ z2CBvViElu_FM#q47#tjoh>jKy#g6X)vIY`YOuDry4W1{X+cR}2a=C-5PL!EwMp4Tr z<|n}qvuc;4w4r0+G#_l+)({%sn5rsLE6~aShhZ~QyS5lCi0X@Zih;Dcekn1rkN%j% znV%lgtOK_=1h$V(Lu_*$-|2u+C`ME*Mjjib#WtPU%R6ep`)UB={^CUmanY1fq zCMG8C-M9wmj8sg(?j1(!uWI|n@;A-b8m^9~h`8*Mu}FE|B9He2H`1u$2g}WeoMxQI za`c;gv}znOCdO2<^ia@AL*m+7$FJr6uoFH?>4!Yf>FoU z-z+|R_n`@-BqWM?@o=0$u#*KmeJ+qg>)^cfU{}s4^O8ZD*Pk1=u}BmU@T*AR=3VE! z)x)V}!wd)jl2X;Qv|ZwtgF;GtLfHr;sFU;0w8_WON2*=26U-)5$r?S?fn*`eVa6%F9kXTR}IU zy8(|iLX+f`YS8L${xjqWk)Th#%uqQm9@wZ=8xjwg!yVsNKuM32M2DJ4do79f=KI2uup3<-II>;W4`7GQmE{cSjzRZbG{9xgXm2G*&#blt$} z@b7P}(zl-=n}cLRoy%4!(k)pbI~#uaYD8sph0|<+_;@72bv7qMIyzS}oEqGW-K{Yl z$8SKIErGVG0_~R|YHmZ~AsdxD!W0F-x2fBDAsf%7IKY1rujg}S?9N99fX^ijfH@3p z75)0Hzy8EFzy1q+kGC38#uKEFqH?wbvc0>j>o?GShyA=eWDu)=T6dCA{f-N$DhhTIAW9oPRf z`+i85A98(&~KI83u%2eOzX zTnY)iuA{&@>Gt2<`U?(u7=pZU4<0x)S$*%LNEGCsBG-z`jC05A@^lY!Vq~71YyeH3 zKvPx#gvRN3AB_NxNdRe(9K8k)^VxcLq%Ib^gUlC*7N@2@ov~}hjOB68g{)sT_PHe1 z?Pxb4k?5G193a)#pvj@Y+vn0PCes|rNIGcIGhmZwc0P@l`_hnD-#OU;+DIAP0dz08 z!JTPqLs@yi3?NP32tT)kGe8|L#u!^?8VR50J3Oc?B>AFzLLS;A#O3T&vb8r?j+mGw3Eo@6pZK|fWHPD`le!*ip1j=}g zM)2&DP4)aO*+f1dSHK3jZX*L$9Q6OI5uxOY5@1BIEQ)LG9Ubeou!}ezm(A?E52?*S zyca9rUAwTbuo(ohcfK4*9;4`G(khn#t~z>ZOHEIYOiaM{z9E4>po}aihp^LxXavzq z8gOuvNcJ>E?X?4n72|9hHOKT}4QlxsDWe72fU|@mszLUl0iDXx_EFb}Pda{zK+nvge1)o^eqT^UF!0=i-&C4g)Yq;MFU=Aa=#kt^5$ zk^*Q(^VxD8H4bP0jO*+EXo^OEVDx?0UT;* zvw(H7rK6d(>Rgz>8F{#NlDe9;e||h!=UN1DHh2jooI8=&9cTOpdk&MYKJbGiOCYeN zBqd9D@qBOPIly&2kO zWN2s#3=HP+=iTk+R^zPS$OZK>&5`$#{K|0x>Q72enJhN&13Kps#;qej3%QnKgs zMIGkoOu2+yhQey$V1e^&5Zp2V*oV7f0b3dWP{IPXsro*X%J!EEXla4{iZGr?_`3v6 z4-q_IUZ9&GY!q{P9GXFj2cQ{}LoaM+tz6T?KSvya3;N?l7}ZV%`bq*Bqte5Onu1g= zRJJj*Tl5R=CFyg%3vXKcWuWodHBQQ@la`M9br)F``y&5mSKf`6)s#J@hrJvFe_fj{ z68@z|ByFndW*yOf)N~IFjDhS!xltz;6oP>4MD7VjXgwS_W1VDVzXcBNwakpG}o{A@>+GD1O9JQRyj_F{bj$$0AIk z-Lce`uz0*k;s=8>Rrp=U@Riz^S>xBKHj)@)wTkhKtyq`>UFVL#E*JY3XL(aLPQf%lPG6!K~3ZO+AL`y99K0Bp%tEjb@T13O^*%wBK zdun8R()cr6%`Ep8bFtp8g}s)vNJ^xPUc7wy<~!i=M7M>zGd>qv&MzA7p}9;nde_-B z9H~9V!=r!s^5p%j^CRmGLEu~iA!791#R1KWJW45B;yn=$$|Ms?JLs5>^()_&Fh;vX zV~vf8Q+Jz#q?cN4ELBJ15ZmUsIuY6?bZa1_T?e5(SJA~g(J5%r3gVD)`CfH8Fsu7d~@5c~RL5T=syx|jhO zNc7xHfj|8w+69m||4$kZVD;jwK0a#gq^XdcIG)s2D*)#cxp}5F6UU$J=Gr(D>hnja z9a9VTSFFA6!El;pCP->43%CLrf$}&?%E}c$V}nFlAzAn}-v-F~_;-I?Umwi)veFxq z1~@rszvg!(1pMI=oq!n-rwg@K)MN|0-zl$O=o>&FpNNu3Vse4(>5r>&&~~D_W}9Ifq>~Q58Iju~<z5j0kCN z>_#K12I}b>0h{pS$B#WO_c~Q|bqn5IzirVvKbWnFcsT9=?qs;gwi@c}KC)0&{OA&? zlIvL}KFqhqU$+>~(RwG_GQ5*g^Zae6{9#X$9!>O6j%uD-4u}d}Ie~OJg1mO1I4T1IPk@Z^Rbi%x(S?e z)=G2by!|)Xn5xvaU?MvMEF;gVmb1memFJdl(THlfzu;NIe&i&<|3-qfN?V+p32jgV zVnoUdqFYGywJx4BR)l#|e1v&R9#?-!BCK^cPrU5*^WBx>$!$(!b~1D{8rAiqo|(*s z!i`3*+))IzGhnneB)^G&p(vqs60(8lJZ@CA@M!t4UPXCpPsW&vK8B zLY2U`3xviEuE0s%A%#S$!fGQuV%2y%fGX@^cRbJH38zldAoEGUKj*d1}0)gZM@=HG198f>Bl2NYXVwQ^b zmT1w96S4}~3)rNRlZsCV2l{h6)H;wAgZ19g)CUz3KJr~`+M7X2EX=ZT)LnU`GN#we z{-8)2S;im!1hUdF|wX*7sT_ZeJ?eX#?7oKi0 zF_eK4?=bHuY!n7g*A?Odrwtg(3Gf~T$=JIEk~M9B-}t-zr6ut zLar29mJ*U6%{ivyHVNSCH>^t=r|0nbcr8;I>f?qMS_~gEL{5+F1OK2BGnNW-gb#z^gMruCx#dLz1UuCGDseb9$3_`;e8ae%rTWT%60M>(VFSpx9&#mkrKI#0YW z?YEM8y3?cequw`{T})a}rLXj5&?H5?4vOG$3c(_)FXod>lm*oj=1mGT9Q3EoSFpYI z$)YQ`_*fx+Q~>gHZbBx2!#-)i_RK_l8;N2 zOBmPQ-L|0NRxb#1*@>F0ik@{_tWT2;ThO_^jH$4K7U&aN3l}_)*>Y^Wupg2g9Fk9R zb&`benrs8fMcgH_ z-C;g^UnZM&rZwqlLu?vUJ+>-f^ePmVTdBBK$z@!C~VJ<-x9v)UK#GGCv& z+E_Qkp6CSKJL@a+vIIG?VhLRYricR%;%cv(q2l3{JiqhooIfS{HQaK3PVtq2wgKPs9OG&Pn41>KXGlbyw_ zam6~so_zTJrIIq`*?KQODj7npO z9T4kQwjq)b+&i}OT*#H+qgHYw_%Xg*t7mhk$GLgeVrVR9IGMYs$4nCAL51>R+{>hMuBHoJ>n_LTNF^5+>yiWSsVeD<69nvdn~$%%Wg3CFh@rZWdhxTP zk)Gm4g|j&Wbw(##bW`NXs+l8CtNWErm0Edy6wFq_E%xscwJ?Q)Jvni2 z7~|gwDZVLu+zY=lk_qWfT<)Sw8grhHlwlw6TEX7wopuyFd1!U7K<6%p$&VtRo56g8 zwX1J9iT2LEjgpFrQr`GrafL|LM-XMoW6L3`qSl)N0|U!%$4#J%ii)Oh6l=Zzc3+<& zRkia|Fm|DsSyas3$jCP(H)vQ8`_X^Ko{8TYdm1laq?gqc6v%ZUx4dG+wDH@0FI-$)bB@Y^ zIBq?dOkR+(Ht}>Q2_fMn{OZJS!&cz@4oJeC?urnwJv}XhUk$(*d&J@cz9*2z@5)GU zd24%JMza*V<3t!rh|5K?n?`<9ozm;1j@8p; zccB7*|Ch+_NmG(f{bL5XqKUOR!Esit;$&5Mzt{43p_RM;y}R37&iACG{`eDXJ-))o zMSvX+l6@6QuAm9|0u~W}eBLPXw*4wJMG{t>f6;V+<#jRDywct5hx71Ze8*+#)rB8} zeeBqRipmYwXT+~}ec0IE)O|I6PljzZhebzcx~n8LpsR%s##Q&r$(U= zVJ(Z=>nlYXx0q*OR$WOM4KC;H`;6?35}KN(O%3u%98K~-Mjhdw%<9wET^%y7jNW^b z(I5WG>bsuJCy5iB?xQBA95`5}gy-*AQs|D9_Y81VUVf|Hyy)y`8Bwn?pa>tXy|sWn z9*C*VAud{bdjvs&x=j{~73jq{si1pl9W!y%w(G_-u(mt2%r=V()Lf%>*Im9Oo!Zq+ zD1!BVSciq6V2#gtAO=t^OC{!Sz;=zqwz_|lHj`)8?iropJb}q=T--H^T*NEXDe4XP z;^X3mzA!E$pnR>tJ!%&|p(wL>|AX>`9Y397sEIgunl%@Jdbsi5!_kDq((YRYZdboe z#M# zJvQRt^#%9tBxl=rMy;xaP3Pn zGT4&3)ol5vwl>k0mX^SvAZi|-*y3W=2M-<;Pi?hzbUYv=loS=c0qTn5TP~o&c^4a- zhK}wNNT|hJIYG?wkp0>_p&y$#5Ma+_68U04NQq`u`DYcOwy{=${!YkOh{AVlWBCgU z4I|!td4V&^gO$ET7&Zx2Z*5|cN$=nBU26M_2WOqL!30t9Exv8rH7r`9H~x+u3Gz?B z0p-nn?J6v^x|tvY0s?>Zc=*NI!P)M`l|IiX3+zmq%;$6-s}H(`Cn*w^E?_}R`P+>b z+fhXgdMtWH_|ZKh`vY4p;B2&r&eZZEv$JX8t1x%~$ZMjrW{dL$Y}lmcuF)$eiil6S zFWlC^B=%lRgKpU{cUuRMmYV8zwjAvaG9qZZZhX}<+bmaCeA>_ESTR7zH1vB9pny^o zg#uGhD3Eq>ITpJr>@xvK$O(q4QB37 zm?||~)!B661<8uk^705!Jn&vyeKr)9ZI5X(ms9A)Ch8X)%tXmNEtPabcKJ)7@4DcR z=^Dqgi#ddxsA$_wMB@3CPvc3$PKCMxFxRzfDAbqHv#@04|4NK$VhRiaH%*{^j^S6g zUHb*JM@3dwS09kMYB*r98Ep9$MgON4K=Crqkgick)U*AiScN4)5(z9YrOTf3b$p%V zv!{ckIq=&cM0-CtsB{aP%xl!Y5nKew5_+xj&ZWpF30&?+H>Kz2=QpH6j1#DYJkjwA zOMDQazBM*xGwyzL=_8n@R*;&X{}psBVVlaV68pbvH=GB(YD{uj2k$MfqcQq7SFF)J zcRMd#9*0$|Dz7Qn=wI837L->~;qnVN>ER>dOz1v!&QC4 z47%_~hwyCgEM&P)Efp!M=^7ebBd<^#1*|Ywys2 zeSp+pBI0Te!GA>{Ku-M9rftn>A2gJOMntrM+uyRo()6@blMOfio!I&h(V zR#TPVE-$^q1& zRJE=psjZYXd01E`eKxxN}d?C9G+; zV8nvz<;!S5GRHe}10W0Y6O;-F1-!UGjjwslY4F#tUuyEw5ZXRaLq4MR9cW?z#nAS_ z3~6_eJ12NOE6|vP zo;B13YWFfuPR~KrZ~@fej6vT}K|i?RrwMwYNGs6QFgK@@%L)f2$b~&O0zn@>tN=fd z@+NF$WnR-@j^23x&0o4?<1J|tNs+NaaFd;JchS^l^;Xwn@M41qL$*usLhaW_VNY9o zgD9UqZ~&`O(bSZ5aM-7sSp_{xe2B{fM!ov)0|FOcL5Bwk)@_iXc}D$u%nq{mRHVJH zS;!^uh#43>fk>GgcF6{keMX>=H0^ydLrFyyL%M@;A;(#K_Lr`7L2o6uoZP5vIF58J zK37-dEO|k*9xhWjAoYxSvjrdi7&o*``O22KZ|EFkv5i4>33!W4nV1%kODEv>D4*B^ zH|#j2c*xrd2%HlKjl%q1NtR$p ze_no2_3%^RPxowiRqzV-sd~P~yMfh3P-twQw{tqjJ_UGMsz;vPUCz~7wAqu!6aK>5 zb^5YJ-AW;}W0jGzt2Vl)R^kMS`wumTj__~t>|IZ7wbF@4-@WN(o%wsiXrqK4Ys5n$aJ)H$tSL)DH>cYtL(%wsrGk{<9 z!j z@$~^FK7!_9M^Iqlc=H3TY_TFwnHif`UPW1PgPJG*{bu#!x1Con6_5R_&$-YUz91UFEU%DC`g?&p{Wd7gtbi*1w9}9*7YP=Wh_!mbNzk9syqH${ zTee_II$=t-pbj^h#dpBAd7}zv_kxWPZ&Qu@#r3CKYKE<;fp^OuklWC3vtww#jQjBj zckLdt3L-8CuAc~Mv-_YVu@9;#cA%3ns_gnvEr4p96KeALxswqr60Q(##}zwJ!U~Cs zGI6O7Sv|QpYb8hg#0DLleL!11CEc7@O5=3GkvEP!5J)8L<9-5$vjGlUBFe z7yY{1-;{vyDqC{MVm^KPBop`I8~7>!s2mPtCHYvmh=R#%UWG#nXe-_Q9t>EtMu8f& z%=aIlM)|X^kCRs`dS%Z|LQ*oNwl)?V;O^m&d>knd-#or^J6Wa!i6nwP@#L3(Zu5b@ z6BF_E!S0?OO|UPh(PW|tCI~vMz-62Cet)H+5`iS#+NT8*5EDv^2a(L5ffEO~FahnB zOtw~8vT?6LbJE0K?MKjuZ#(U=QGf>2zQg6IeM@VrtQ-|sr8N+9kfA|-JBXAw^M0iZ zTGQMB=p3>Gs0Zy)qT=ElAZZPu@O2Q0IYhgmg`!9muGO=1QU=<`K(C9ai^~gW4nkfR zM$qG&nx1|mpf%;S&uvgJHSU4W*4FaC_kv;H<0ZgWTktnvs;ZG{1%Kd^YRJ=Z&ZP)O>vL-Q7|N zq)(m>euVngra;HZ*QwOh)a?9xN_O_abd_t(Wt5*ke?A4Zj9$=kA}l9|#bMh27PwwY zYU)^0c@R`YnDqq(2L5axZ}5Bx$x~qOHq%_d<7w15P=U^*KcF6>{H!S)0ybVx42vxv z8Fo?q{%6Tg0G-eBHA+&A+vUo@K@@0h+<5TtAut-NnOaaZc>t=nr)%;sDGv`GAWjTH z<JFsv<9w*u|(*YCU=qPDu z;-KxOmkquJuI^{kRHHX9v|SSucIR~!H1L+JrY0e1kWm491)W})0!cg4RTo|Yqec1y zpsz?q0s`vRa4 zQ{K3O1-QIX1{snkS5`)WCZe$D=uXh|=mr`|&*rgV67KHY?d|O{@m#oZeI_n4+`%B% zm2hC{Z?L=Yg~jC!5%80=rrIT=y{iZ2BU&`k;o%~$UZI2f5FhA{QBhF|0po$fvPQXC zsK8eM(X(riHHO+0l$SpO0DJ<+*b7BXrmmOaTTS1kp9ef+e5%i~t z0x1ZTn8i}ScL3)ENBW09c1{jJ3#GD}+UUC+@NNUxU4kp=m$@BNzzrlo*j-nav;+;d zUq9Z*SJKn#2dz3exw#8q_B0T1Ev>FP{`rAk_6*qRcVXzP*IocNk9lcJ20BQ4lLV7M z2~9~yrxz5XHHzQ7W3I+)0b; zIpm>KA}T{kKJ|GBik?|r}j z|2y8}J@(%5JomlUwXWg3&huJ3CDY^Zy+|#Eu4%tJ1WW>a76;EK@`!`iHyiIp+^BD8 zNJ>c=yzEQRnG%l0<(B@6Y?zcIHKDvy@PA-1U8(+bVz?z0?ha?|ow}yxa?ngvgd-n* zd4vN)mRL*cIe+=ZM!9y|^G{*hE`x7GFZ}%GU1_6Qf#&$@_h-txmZT;>fd;BA%QW4!^Qk2YRiCq84ks6r)jtk#6EMLCd#Pdtc zq6-38Myh5ob301UZ^7C7KGGifd#pU+`*&CPjarz-%ZLr|#+@Vur(H$Qbi<}il7TVOp5$}*#G?~L7DqDlH245?Cu75waZKJfb4QP~v><+XdP6&Aj3Lz6q zOA4Ia-#sFcgjMy|S`@bXZ*C8s9%+v!Ki0Qzv1WM-)>N;n((wn0glXsVBR_!{HuK7r z^^J`+GAxVX6xI$}$p1JpBpRdWoO~I513H|`=Vwfd#zME4>jmF@IR5tS+c-5})nyDU zcY$queP5Ihh5=4kBiSYjrAyxdZn(b5L)^1u0?U9C#4%5`YYYAD|rqM+^rktXNhE{)aK8VLd zB(8$|=M`9Px`u{4BsfA32xTW(7;S}nq!=rR4+&xBkvjGa`CeT^!$~OMc1}Fw1ZVs3 z!Gjek`hX4>-~6mTVY>tVAW@{@#+HX^X(7l1!d1Ozufh`H_7;E8^wy&NJ@8UBZ{YI5 zhZz^xvV;5N(3uy2P>loM)PzI?R)Gv>GyV_Ji1zujCH&>{??Z2m^Q@Q6?d}Z1fa{&LV{GWvnYth zp96T!=uS-b)qnri6^{e)n@(ShW`I^;=nTmb?rIQ>kTkXR0IA20dYcy z$%iJJ1_TA+c#M5`9F6>elnjqD;x}*FRMk5XOMYbc;+vnXcOUK8<>R9o7ZT71HnvLL@=E$eX1Jpsq+?B8z%|7~^ZR5aG|RD!n+Nv@gni6PZ# z+S%LN$5n|S$$*g73Q5LjS80(obGJ?Y|2*4a#+55cQGY)t=PE!d!p$Q95w`T-+!9e> zanBhSLDyI5fLP(xX*X}a03!?Eo(y=>npsU*`TgS22dHyQON(J8&t_YG|DpD55fsQG zB*4WhM1tEEOOD`fKeE%4>!90Hysf7dDclG$i^o)>!c6CDpXYcnNG`+`6nLv;UPb^? z0~B}&X!&0cdloTta%#%B+($)hZ-hf!TXuG~!s6LEw0L$x`!~*og|U!I1Mfig;{7AX z%|h)y7$=`o6 z%**e9mfe(p>VVH&ugE#j_w|n*151&dz=y!c|F!EO!~b`B>M>rqLTBH;lMspELq3u< zwp@&(CA9y~>6@E*?famt>=pn_EUF_h1pHZoBzMDx4R`SuYJY|fz=wsFmF+~~@*Ii< zMVHV1#P*Y~kOH&0X2$F3^^J3goB6g)6nK@6JnNO~)~!P=m6(*&_;)ubr)XEN`XO&X zad7S0HRs-Ng-K+SS824St(hW>^oyG&qz?B(lO`N)5|DlV%*y_ov*%gqG;qyB3HGmJ zq}=EUeLEYeHm{iA{evF|o<&IM8ga2GzsQbhE0Z&vywc^d2^Tjv4T=!>2G7ylT5#Mp zm7X7a0B;M`06Raw9f`8|V;4|dj3-_J8JpK+y~O@lnM-7YUcTJ_&qyfM)q)s`>k?)6 z(Y0!72`Ec}%`L3}`n1EjhYsm=GK3ZDB^Lb*?T2#>_t6|E^8T$ak?>1Zs~{_1v(w`Z z*^Fi_;tiPek?RACBB0@y7cn2;Vn^Q>MZ!C+*Ske+wAn^16n|XN#$v>O(neH%Uv%aa zv>PO^K~zM-y;onK-7LGEIOAq!@{InmK<{D62XLTJ6N$^p(vY0p{);7RgJk`$*LtP_ zyvzc$*ELG6R9-WWC4P_u5Qo!U-u=N3If5~sktY9T;&ElA}Lf%hB~Fa6lQzm>K<7sLuZZ9G&4E(ml6yy$cwAoyu9e zBj?--v8>YEFY>J-D6_NSyod^-DuFTpvAQS314fS=_@TL>Y}~l<;jo7ES9nax2yP)A9DnG|*S*u|qz5ohMOYs6enQZGS)x+W(9&A0d|xFa z&YHxkvGu6`BlcVG$l+Fc{U5zn&v~K*{;bzwUuPs0e#3io{TYE;0jC~YZ)t9~g+6$VE%?Vl=pM$Q2v1)0Ij>s!rgMjExr0V_ zTGeD)d%Vb|<_O72y`Bgh>Gb-pFHqLWqN97_U?kt}=|eFqScR={^moqmMU!KQdVDKV zRh;z{?`ssY>)InQyJpo0VU-ss4~)t@WFdmF!|8#Z3(GC5e21!ie(`nP0hZQ((j9GM z*1G>kng(rCB}Z+^>5k9-ec|Hi%N5kRAa^cl^V-*AstnPbTOtkv8`Hcz>prSHJIy)$a zrtC`yw(3pKcWLVChICH6yVfx9@YUCv5!RmR6~|A@hhA#5WaG3E`;gWeMrrPc2Or8> zy2K;#`=3w^tml3Y@?stAILFaH=$*G;*pOqioYrAch98w^+xbLfOw7>o| zyMB}4hc&02oBF1U=3b6WylLrB-*4j8;23DZCTO!xYSXQjLFYq>RXMhYjFP)zu3ygs z>W}hLYtNo#D7IOL?Ecu_2ojr?j%Y94(b-oy@-yM*&#ycygW25=B~@#Ou9i^!@Nt)p zX7Wmux5=fmgWB9HPMINn#{d7u4eSP>riA=7ZI6kzMCu8qU1VF`{M7{dl_?;T<`~1m?N~AXzOi z>9B)h1SM%wqsW0|$-|4D@`%~{XAfiKS{{2#t(s}e<6{6iW0uQqLR*vm;HmZs@a&bL z_1(Ge`sRBmGvb{}n1E@amWpU>^PE@vKD%sG$+-4T^r~M==lA{U7@uRQu+No|kqx<| zV%gHdOWSuJ$L9RGjLI726$hM;ygM)$pVKm`=t%L4Ilr3c(AeYax!%pWMU^Wn5(Xff z{W}+c@)wFWDskP2!6=nZMcdH7M+O8K-0|u$5GEF%Tz}w4-jMdHm~iP|Wiub!HV5va z<7*MSDXJrDR3wm(AQL&f_QN6}Gb-MYoX z*KLaHDNrno456%FABe2ocKPmuCquRCHs;0NR@YE^-gJSN+f7*Of%ITh_;$Z*DMdp<=5^Ok#(2!}`Ydq= zRDr@<127-Ig3}rRk(j0AdoMBZGQ5l2q~<&7;sImvU0d<{%aKuqdo)%yM*8vJ%gT=G zYZ@2s;MXY02d@disSXHn2l^$fJICoD_Ff7S4Z)C*EHq<0P=Dm{kso=vqx93l=0OUR zqDH@mRVrBuoz4WQJ*E2d{DUoD_Eg<(`u8U7Fprqi%>y@u9-2P(R`Kxe{{21Tz)26? z4>x@0&%6}|1|!9r?>J`1O8ch07=X3tJjxS8#0vir$}t$IVmYxtn?CO|1r4$0%s;=4 zTXJ)ANqJ8IFU-^^n*Fs(SVZu}A3+t6pi&SUy@eXb~(8Ti~&0Mx3+Gr7t~O@Qd63_<_m# zPn>_Nnp!m3jB_oQ6FvqM^Y>BJAaijaTppuQ693J_*FB-EOd$q2chjYtT0d^v4{i^i zd&{D_K}|DB^)B}iSM%Gq#yKaJK#6`2zlwUx1&Y^bK!{)+prt;wPAI$2SuPwKX=3%; za{1CF0xGJy5eD%>ni@(3@GL35A{V~ha~aEiujbJWLeDLW`8#@D471D{@uu4Q_xC{} zXn~IVW|QKps1JdqB_AJrHGRt5P0HyXgJ7!3!Q1Z}9wgn)x5KQcafYQ{M*7y^{LNe@ zC9Y8-u}sl{W+I>FZD2FWHU_8#ZQButT$Cd<4fTyqTT0t z&mNhkA2*6i+P!V|4jokW70-6qvTobvkkbrlo?9#fu_*KNorZzye_E+q7IeDc7YmD6 z?Lskv_Ltd%m!d8@v7g z`8}nX9N-Sqwsv#cA|ZGW8E1Zyt77_nnySviUA@z;8vC2NMh<9=9$Rg2{*}&QcG|1N zvPZ!uS7qsS*RANdkiigHJ~hBK^y)u#clW~lPm=xgefi=->PghrK-QTmuAeNt?JT^| zu#~>zY+2hVAEmh-l|gfWP>P*+v-{<1Dd!s_EJH?%DSD4t>`lz_lc=uBX==n+dGj*! zc$QU<^6}R6{>(4ZZQsMrw48~F31zS9{I8QAFN{c$h6b9y`vKo=vw3ZEex~W_FSaHT zzsZ|g_FWfLHc}pvd^F%lOY2qsQidzvJe#umQho*(+`gNbRX@6c(iJfr2{%$VvnKVt z{>Tsa7v8O>IO*JPXN-M6y+fmKzrqLSWhBs%CeQ3((&!eo{RXas8N0w-GnJU$l}Nqy zfEfOl$(d9aW9@Jsw1(F+U*#yI^{%=wA=XSY@KuK==$rd-$G$>l_sLD=aSA;T6AeY0 zL;Mr{{pr_qm);Ojeaw|{FaOF%5k}{nNBXCqY9t3BaW`$5VBih>B(`yKS$GVr}jQovWQzD}J(61}!%_#_>IL8G21vo9i_;97;6FroXeWRk8&zhX{(cFIg?Q-pLMU? zR{T=W#S^?T34tz~S~l7~E;_yK9=rQC37ukQyPNk>D5_I4`77BG&DN5xXZ~O1lo@Ix zt5tsb{oH!N`1qrWAdyF^d-fag7ur7vc8xulsa6|N`N;9qe0rv%DFdm3wZ zUMdhpgf?TEyXQE=oK0=9!$-dafg7)Wa4n|2;@N8IN$t809m0o&7eXXY^gi!tmyz>MtYAmHsX$mT!h(2ulvQFaw%e@!ZbyIUC*O zCG@;JzYNt!SN@(;&Q|!9zmkdm#1Ds@V_Q^4I{lAFews{;Ti?vS`|V>1}|Vd+2Yv!z0Y#lGv_?_DX>0wJ;&cl$%n z7WRLRUYO-&S;MF{Cu(K0J5pH2S~utM^6_`4Wvm!kgjwwD#(n1xjQ>c_V%BTzb{E|F z^TSZ@pMHhP?tOZJATrFQOU{3npoZ9cb7sbM9SUj}DS2oR;vaP|OYPzh`pC}f?N$cF zIHDVPXXTo@mfyLeeZF^chROfO-rzJ9%2INU*U2fo9nG|T;Q6HTx#3~XHA)zxQL#d# zV$4@$Zsve5GppK~4?0gg&p8Xd+AFfWTyNi%qh^=vmV#MnA}*j!|8El5&5LBUThREk9wJk8A}=C^O zsrjJVa+*`^2`AP$vTxb@3KtU zgjXxO-}tvUic*?lX~lLN?Sq* zbKR@J(r=faTm(Nf0KyC5iy3aOcl|XK>4Ti5Rp;1-oaUA=esS`ldvoF`m9FFE>a`Td z*FR>Y)y>!D{Z(Sz|Bn*G{9h%8nMb+z{|Yfif`pl~-rCwT>%IEcEpki!*YEC6*N;mt zczXPi?bv-@B~e?OsdaXn^V-q(=VyN_fAdrL=$A*jG+2XTA#D`H#i{7`bd|@Wi`Z4wAP9`P@4;-48ICKl&s`4IAHI$!PCfgys-j7;v z42zR_{8LZmw$uL{I^^7V;55k~Z>7$RX>2G;&e$|U5v{e6m66%)eV=Mi>VSYM$#AAF{@w6e|R{FfWyJxz41ek-C`Ft1Kuq9~Bhu7wHAh)FS93QI+ zmr2swI|4hsgY)(lj9sBJvXxAq>QVnS^V;IYwH|)Dqvk?h85hSKTi+hUftpdE$x9lF zxjx*tisf^4>q}lHxm*kRxl3;x2YxfDLd+n>#@HHeK{=~B@>7JO(*K%epU?NaZws%K z$G@JbPCZ=N$|);bY1gskUAbDZfP|>%FKqJ^D}8J_+rzJ);?L~=hm_;S3YgbWzlx%Qrf{jWCR>D z+sFOoC|~B;eG3K@hnuf>hx$X5cE$gr@ebZuyztv4$Fmt3=U6&^44v!$iE$z2PEI*- zjUqXR$8=h`u4u1d{xW;Auxk9%4c~<=f5S^biZg=yoT%ba9!7fYVCHSgfmD-3?f4k& z_yDc!?$Hf5*1dB&B4(~ZhbI0|-skG-9j({Sb^eSDUzHWd=+>Gg$wZ$4ZnEuv#hZ;y z7NVVXD{_uYJqvo@7=iG2@6>+!>ue@dc%$Y-VDF-o+y2DV4Ro#ertjQoxtO6^WH$z;-TNBojNR_{hWl%^&CuLO~ei0sf zx6xSxBkioT-~waqh?Wam*qMO%Cx@c6uBW+2wD`V-o@L_K$sJA3a;f^jfY^r&4Sb2| z0;=C_>)l~T$HeZ=Ay#0ySKNTVqT<=$)!DU8GAB+%^5vgI{!MTw-s}8=4YU%I4^K6a zG9;G$SXh>kTx(x`S9|Z_KcqU|k$^(%WyhPiSFCi^Q76gzWVR@XGRLzkWy-``+&klS zty2~P52oA9>nS%ji^(qyT@qwpix=ctVgKOGw)IO(HSg;sC#BAl^KDX>(`Uq@yBYW+37%E}Z|*72m@pE|`&i%2>jOEHaYw7e?J**JgrygFl< zk=?$+Z}Yco`S!S**xs&^YCe+k_d~S{x?WsOj@47UEkc(y9CyNSs~x}p^Tq&xsSY4DX(udz-eZPy1@nR=e=gN~C9$66!gE{mLaOn<>t}T^()M%J z=MqcH6t>KRDIo zm34#c#;u%l`rR2oF0F~A29q7>+YL>?(XAZ0?p2R&)}U^cVUQU598EF($E)}@d#Z~~ zt)k>IzBqxsUd7VIQWSHNA}YuzZoCs6F1F)P!fB|0ax*6%YP@5~vlJuM;Mdeez+B^f zrb#@FQER|%P>?~R)4?SA&KtX}!P@VKZ?I{W`={vrSSa50TE^p3y;?5DQ2+4anke>4zi1|90EQQ9kt?nut^3 z*^}<9RT&o=HaEwF?+=wwkxx8a=Dy7@tb_4sa}h5D2AF!V6?l}-bh`Njh%^MPS7L%| zqJOzsl=3yZegs}7i2v#@#p)>EQ|`CiKe2Xn9(+Ut6q41jUrsrK>h%|gHq>Ne+3Yh-*|by62cEW%tdo!k z0zP#jzjOS;6BbZRA%>7ahaVFZEDs%Imo=t6XVEALW{i)KjNDvx4fvYi^y~uy3BTmy6`%!@&TvC_Dxc79IMi@e8xr3zma#?vbYn623qMM(k>0 z#6HADs`JLOqh|G~515_4O|7cRIAT+4w{7o8?c}R_)_vdiC!hCfH~S*Wk+x_*mXE!B zR5KK7CK!&Evc8a4_!hXXX6uHH52C-m=AlIxboTU{b3Yva_2<|7$EG%X3>G{q+ik@0 z%UZWoix;2SMw0&+l4``8*tvwUJ?MBXLF;s>N zpjQ<(%zwZkHq%i=U1N7tyU2;(!N;){ z`eCH?08Gfe8n47&+JAcY-5^52=by!`(={{^?;gJKB4(qDzp)g(mui^dw~8D-2A_n; zXH=Q2*Q1r~dC@I(yfhy9(8}>^icdQgVn&O#S*jVcy6tLY=J&N6LjdoTRC@s z^jaqtlXp|c>sZ_w_p*lmiP}lMnN4?j+pQm%jt5+&#%AwLzJ2=Qx^!}uHQ@&t)@yO* z`byqW5sBsziuf`W__T)cTZvl2mU+F{*4@C}yI}_=X z%5orFOnz>P^$*K2djg;(ec|-$a2BjS+*^F#d{ffJ=bVn^zedh}@aPRkG3@YY%6E{D zksb|jG~<&)-;&%4Q@Fed6#u2+Mm|{X*WZ34tZ*~02~AQg&H%vv!zXIT}$=dC0#zbf3E&2zBhpV=L|iD+D>_*4boZcB8K^J>(!LmnYg7B z5H+RKZtiR;e$w)|%wx{SYrf}oCQr`s&CmSbQ&;BG_}H~{4fsB_wKX=kwY121jGw@6 z1a^-tG{1I~7wrE1@uvrXhU3$UU~+K}ne3)XC!_HXLhm>rFK`J&h|}=csXks%issuq z*xf~QK4ZVMM|rNWvc9hTvQPBkfY;{0+KgYknIEGYm4n*Y7?Y<8Gq%e$YwW(d2rFiQ z_r4X){NdqYZEbD33VXg}jpgxHH^f9mFM+5-Sln2UkS?b)4Rw@e&4A-lXpctm3wl2P zD&jT1B+$Yc!|#e3dY{+Su%(gsLyN+aCw3QnnB*AD>%t@!a8rvb?z5!g8XHXr2|4Dji zx80hLX!KbUYCAgW_e@24uOD}^0I%ni*Du?`W5E#-&u#K23bVb#!SpIR&A=|3&^Ai- zPk~n?FGTa8_}h zk7j$9X5x45-K|t@V@*xnK@Pi3+B6QvOW&=cbRK-{77=3?za}2N0*VjK8 z+i_>csvlH>+n0N5PO|ZS4qDx0aqwKEJtezhc8`We;YLZxYS8q8(D!NId?+;|LqlC% z{YvB$09IjqvP<1dg*L@7L(lmfL$5n}dO2ug=&ac`Co|FYfTnaXRp3mQ1BkO^tRRT$ zyRKWO7;JKQSHJ{D-g7XkoPk?|i$jN|eE9WCRR0%!FT5xLc}AN%O57yTu5gflzv?Kb zlu7aU&2TR|#%|KNLhs5J;HCwy>Rn|l%sQo?b25Qzr_wp-t#t4yvZ;lH|rT(w=HL$ zjeSPi_&VvbLuyxS^JQ+`#KeT6+wir0ug;D4>~bA^^C|89kxks-bIT%gx^XLrS{7ep zxy#B*JWOj~%HT?+ADS0~)3aJCV`!PM3kY0jRM_D=KXZzOPlgUHWJ31Gn?g@rZXQRqI6-KxK0(= zL`~qERMyiaH7!zdNLvo{@&NRcOwW&)QV!9}PX%8aMJ_;o3C#)c!7kK<3S;L$3jU-9 zXkp(gYqvVZ<7NT3CeLymt$&l=z{Y(wAjQv-%G?pG-esp z#a>8ENfQ%KdGoR-@_OYZ>gtv&2ewfe>6kDM>e|vOv^aMg#ai0k&)Qn6_sne(J3k7_9d^#|%J2U-fFjbm=r-uw=8Ru#W zRqk9E-bkobm!o&o=Y?Sixd=GM&rU|F1forDp8tcLM#+ggkZgeYTAKFun}|O^z-1=7 zN3&1Fmtq&F!%K~8RH~6?eX>wb|F4$3Z2^aRg+h)^IjKEY#>}F z%#3>u0dE9YrrR(40oPgl-6O|Pv{XW#j%aA8T4NHJ$B)iaE}1tTMv05ATen=&FvGci z#>zO*F#XAJbmk4)?E>I%e!`s;d%V2L_Px^4_;^1rkAp^|K~nMbv-=aXNRaZiJf+Oc ziI>1|t?;W?*~n``PWy0BTf;ytuD$&1dUnq>5bR?T_NOu51!ZW1f)QSJVGaR;2@za_ zHNO%+0D`sryXc^`w;3F($ed!P+wjHz))yL)?9nTf;3?ZR;)Gw#pNGQCe#XzC^B&_^9dXyF&ITr1Ih~d3?bMVIP!%^b&3=D~7Wo2#I?;Yep@Vg7& z(vg3P6>-+o-H9@}8kF$B8q3GS?&+-|$Sis=Gn4yjn!Cz{@9K~@f!PoyZCY{yd^B*9 zse?uu2CNSKoEx#|>;L#bh5 z&p=on&O6D;-qdPk{Cpt6cXf>J@)+GC`A2HTQVllUke^zzR#EZS%|@@nW2;KLR^xPn z^O-xOsd3b}#MLhI;+uU@{_FN#_b|Z4kvA32M1USxHa|N8C0ZpsYAD$EwuM<2rT@_g zKd?@Tj=k#^m`b9uvg~RLbK5|$&a&+@x0K<_ffIuM;jnsDItH&EjRi0KrA_|e290DH zqEO3q{#13-x4rG<8J2tZ?!B=u1RaXdz;e8QyBFQML9IKg6Xg#l*o!#@vGR@rH{~6f zb)xPbGW~O?wY7Yuz4D!7U*}rdz#RzyQBqVv!CRvF5qc;*cMxv#uj;tI%by1~2<+ zY^+D)7*b>@us^$EzrBN~CIl%Sxo^Uii*o2Hp7A6!o<~RHN3&MC!AV!z;+rFQP|gb7vZ+_fiqKRBvWl#JV#XTZ*gt7Af8TpN zUhZ@t>Ps-rSxNhxH<7Jr6{alr-Q*o*pbM(Lgtb&ypH>Q5*^1vkzV3mfe*DcIR+oz= zlz^*!BS%Ci9R1i|qQM6%A`Ba8feFeOAz2Sn;5bO)uN$ zuE5PCLYz|!)|9Dx7MR?kN=gD)tMpE@f#?IOmFPkfZ3EQ&FRTYBlrfhdqsGr_YD|mb zSQU>BBjG1HJoik&%whJ2uEI0Kx3tYRJ97vZTz-w1^C_!ci~%Ox1EaE z_gYiq<4H&sWcXO{Txl~&O}2NNY_p0RPsp6We@^WEcvV~{cBki7oDwQD87N+s;O-S3 zyg=5aMlT90rC}>ndBF%DM9hTY9M8ewkybLi9dqqk4+2ZpV?KNW7&oas1{!^-J~Ll< zcg~Gh%fvV0Fdy~qy8S-XbR>KkTqL3%`+=Pk^qYA?-%lbmEG=vRK;?!d|ODUR0j>kQMmjg5`TK|1aI_7asUuwF$)2veAMVfeUz zKPDJ#2g^0*>=)g8JE+tTnJ`?!5;ybsv^+UEnNV?Zj%6mEWN)&DN13F zoIg@*AZc?d_2Fjk;)9zY!x2`aehn-ySNrj;D-=wI(1@0`1FsWmF=IOWSfPEz8ff$+Q?8UFqBxNz_w|r-?@a(-O=LpnHT$)spEq) zi_DMZCd9^4F~5QybenHsni_qD#DgKcVwp@`WRKCJqN3hBK6fhXl>{ZB?cSah$ZA=! z7!V7n$Uw2*M5B5MW097Ek#Y~E0aO_b(3%B-JZ*OV_=8cAEHHg9_dQa&nEmkE>8+TEuU=D_WE zufqIznX`S`^z`)VNlg{@g(XkoK#^Ud6Gcadn(!IY3z8vW)(pAZ!Krb#M&C*>vaf(| z#sg_VVv-5T74c883TN&hplj?#s0R1E7vlTH41rJ=cAV9KzfO3DGbw0oWz_@O=w7J5 zFr9!F8@oX%ipE(tXnl(EsuXec6N{E2b58;*SJn z_6Rvxw{Mg56k{tsAyefR3a%T_o-DlnhJ)4++h_xU921TY`oGa+ltN@DYfB8iHQXF- zEwKA1CeCn^IkR0?snlzq>m?F2pkE-_PO<5AYyA4JnVH8@`;Hvpp8NAByZ9@REox+B zE#xKRpa?HI@S;+~KR>xL7BpP9Avax0r<$KR`b8xIW5jzgM_^zTFG_}eR%i2%CTh8V z`>=yWcqgCQpYNQcScKlGi6%fI0@S_s(pF5QXjk<5C4=cEE;T><2L?)&h1orGKqiDj zMi?b=`S9r|@yCTU)}AyiRl4n`+gM;Md%n*Az)z>?ZYs_&MsZu7ji$} zNKlp%$py)(6uoB)_GS-u0?;6C8aHCR8hM0)OdOY{fgA*6XTB`|fXqq;Z-s?}wmT8Q zFj0)%Gk^cLxC1SW$XdzM+`1J8DH#Q-ssVu+4Gje-)|gTMKf@%Q(WdYTDhifQ9Nd);I9Y#&LbbX6?R`F~(oPL`xCGnWQ zzkd$~{S(1-0J+D}1H(QJ8XIp^;-MY2+?G%mKtRbX(^5+WZIl7GxO81WcNOvyRCP>_VY+ zBXFL56r>k>4jtlvug9@!Dm{M&-ix~P9bS6^1B=CQvyJPucpf%3KO%^Mwl}cRhEphr zs!cOm2&R@D1xDloH_gYw;5yX29CNNX$O_?;Thx&-qZX;b_6B3u3H}XTBqtJsy59Q_ zA12}Nv~_hkgzoDRSC3Q_oLR%gM<@0)>4f|U{N&w;!=w_z^lFHdL*cf#;kNAkpiLx^ zV$rp}x*ZDFx*IdjBH*kiAh#MvhHHTcnwZpL42OX+1Ek_;(x+0n1`hpMs1})DtE787 zcb=2IYbWYYq;D3ucg6!uKVmVVyj$QOlRZ#vwZ6qv|In-q?CBrX^A*36g4#w zRD z+D5hMAKb*KiMbyWO?yQ|w{7FZjRf*7_J#OsjgaU&cOsyC<-inpHlh2wNJ;|~0C<+s z;{KB9OvE%z;Eq&`1T%5&Iq?Vko8BN z*R%>!MOLIWFvBW%2{K%UO4i>90Om6!<-t(9hDLnmfYP-ZZYkHInt_d4e=KU9G?aTf)HzQpl*P*2|b;l^Y=mh#mDDZeEb8zlW+}3 z#M_4lNX`zvD?3i^N%*$!%D4@4FOKWI_7NJ)5_eA!pTPARs4DbMV9eZLCRa`0&0W&%PCc9*NcAqAl% z7L}IP!=h2LbD*jshXj=fhfvKIgH7D{!7HTBDl0Q_T|=QnBH|#!I)+y|iS;>~3340> z;zaQ}F`S-Fk(1+IAFV@npvp#-ebGgrdPW0FK?cNej6135?lxciE-Z9Jt?)$_$9Dxd z0{UTVc5uL30p{f-iEolg+Cov6d;H*r@XL6kxWh$Zg#>atw+?uwNRP}pH{{^@St^KJx@VZKNkW&di1&APtSM>&&Al$61qIs| zOs~OMCkApl0fH1a^3SlyPhm#9n$H@z!y2?rFlvSY z5_Bwz{ZZQoqc-Ss&sZRy{@Wy+Q7`b>D_u@| zG5JB>p<}HQhknw@Z3$)MhJ_}SIsi~0G~tD_M2^Ki6Ncs?9Rf7zk!tQ`r$|Yy`u%f^ zlzy6fMJZ>~5o{Nqn^I(lh6r$rD>x&)h!i!r(W=K;5gCmAc8C7xcHj}ns?@=UZ^|}T z!>zGG@FygTnqi5q_Xh}m5;fn!^wz{)#-{uGcrnU|f*X8E6e*I8+h12#_jm2jN~)=; zA@yOyZE=;DqHzXOK3FmA1D_=pnvsWz91nFg&hI9AynZI(o7*;17!9v z-z?(OoGNF>p22R!pCcOQAdZtNW3VGv3{|fryjlYsrdcm;sQ|VA83r2rqcC2~{KzsT z6YLSQNHKrKe{jQYl|SF#VoX3a-1Fixt!(p^Aja+j11Iqx6BrUokjjCzo zu=CyIcOffdZ2AP!UD8_o58q#ZYh(KEtXzSa_1nUD1lQzkRvP9ecp@|&BABQPghlJn zp{PM^)uEqL66MV-bi!)B)H*lZRPB$HaR~x!-r$C+q3H`{-<$WL`Vt17GN_1y5i-4? zKys$8kSzdIIt1yR17MFmL zLnp9mWFZDAs2ApB@1dh3g>;Yj_lqQ(C5@8DUwDY%(6%)#21~h$R&Vn((xq;zw82f< zVZ%$IF+3gCOuI^^sNcIUpZKHC=PJRLtjzSJgiau4C@rC=V~@V|>D5VJ3=NE&cIiw% z*|6O$pp~6YEnG;7rINaGVlUO~{oqfG4CTcwmd_!%rxF+g2;{+}gtzUUZ|6iQ?G5$y zmAFtm1TGw?gr@77NK%0TY9xYrHXQ8~ecV3~F`ZC%J|}LhGeSfy0`<&i*aiWVF@o3v zL3da(dY^^2Mq%S6&UM_1riQE2P0_gODwbH!DSOFyy!}@BQ14*N{Wq!m*b^60`{nuV zuNfBA2E6#FcwCiBLieVq25Z6Um6{GYY6ejwW3ORgGFEkLNJLJwXPFiQk2Uq>Nc-eC z6+{)N1w_Km%^d*nj~ek=pKK83F^eiI3nFVx?1k)W&4}+;lJxJahIlg&=;#TYO)?1t zKGxqC;}{O67XMb<$a=K@;^R&6LVH8Q6c5{6HZ%=6zGooOT6_JRoM*VJ7L(?YpPyYj z&*2H#d<2*rtE$;VIC}CPj6z$3(SCu@@S{PpOjJ}fYlnvsM0mFzTPn3ru8UA&GwQ{Rn7?qtqgTJ z{;Ry{;St^s93^#`;YM_fZ|>{~iY;IJxOExh8wX88J<+G-2kGOJzKw|x_fLj{JVj6b zrest;ty}T$v;D?Jda|fQX;^!(y)7<$sG-fw1X?ep0P^KEhzyrASXM zq2rHGO|Aljo5<1;Q=*d#>-<*nBY!5RV`Y<=yUgWjwa^IHJshLO+a?!!vg@lWL*H6F zdhc@HRlBjBb&aAQyiJv5E*Y;t%JA5>QqSHw(ZtzYWh(@`8Co)ouuyU_WN;bP}|M@)i3%V{oYjhPnE^n znn`bp%s(U1m>}H*9ASTQUE!4oB`rR2)5^(BFN=+g!_Vfg+DfPGN6*AzHK5k>;m2rm z^zuS+(XHx1vFa*`e_D!rAGPse{xfva(t~CjLN_;%Y0eYie^|b|xM26@mx98tO7kQh z*8^1TdZUG|$ms;_&e6QEvJiWdjP1Ra_xW4pSJ0`ad5$X87f7Hg6 zrhbi7|7zSs8(wGR%eCv>Tg7YLf(cT`!zPmUenR)T2C>mXCTGWcBN&uz>n8u-BN}@t zlOFOxVVvvMk2Xry1V`y!GdOFDdRGYZCE>lBQ~t2n>Yr9t-afoLKUkLuJ%VlVM?`S$(a&M4S@ZhY`LJi8(L+g`j0;>bGtP-}= zbTXe>*4AG<$-b%gU8HIJp1>xDn#UpbbB9crq8nFV(OV{Tbo62Y(0*PF45~s3ON4vh z5pO<*{$o1x`^Izv-J1RvUIDZ7yJcSq?tAmZTFrkjk{a>$L~Tf|RZHWEf$e0 zg`aX7-fvLMqMK`YFe4sqCsxRm|dYg5h<3U5UNL8vYArM#=Iyd&QFi zMA?o#-PdJX<~DC|tk)$&F=8qxg$HSx`z^^`X4ZubX4JVmt)JBOJh@+Nme-gaxu-uN z_Uwl9T(CA*n@FzWyv7T$hKk(J%%k;^0Zo>fZuecUL@bQ>N@lvx9kRKsyR_<$+Xv3B zJ` z;bkM{I(!_<%iprktYaBGf0+X}}z-^r!HjAJ#@UdIfae zJjcFzX6=OnuQi+MHz`Fl#Kgb1cpI19q`GNMQQ-2Bh+a2EU#eG7qv*-@#~fUS@m-VR zlRw4jz9wC1dbd|3bd4{CBVc;(<_I;~))>R%xqlpF9r8ZdN~UIOuG0Z|!}TZTmVX6aNjy!$wuco^@7Mu zw*Rv<6;_o$@@sPT@Jx2ZEK#KEyLLph$~;K@ThdrAM*Cd7c{7Bh-K6&dW?sqpvDN8& zZ2m}`-|^Ds>24#T)uS(@)V&5N&w?_zgdXZ^{_>98_T@lQRpQ=ld;A6!%`Qy^)t&F~ z@unC% zHRR~K4<2O09Z$Fm*O^W9YxKe8+#_m_t1YCDAEBD-?!T%ZpiJoo7Sfc8Ye6LKH@&(Z zZ&A>pQ9Hqmi1~CELykldy|s$O1JSoR8?oo*Odra^|Do=fq{x!7-WZ>NY94N*AP^ZF2dL_}D^) zW%jV5;i%xxB3T5*nTRA!?mC6XW0y_ca@ERNzSxZUTnX8(}yW)UXOMNj^mu`Dt__|g=IQk;1t(g;bge4}ual;#JKw?)h)V#HT#cav#5)HrvA#xdkW~L$A ztk*Z`bA?X@O$C)Du$(YA;}>W3xzq1AmGrW9-zK0RJ0jHBgR5r6cxn^0<6bn%ui271 zPW{uEt$F>@$(ooCN^N^!!P>}wilgi$Ur2F$-mS_lRP$B5KG3LIqHg1W;l}Bjnd+H6 zPd~Y)GHHc29&YzugDYUJ&zduH z{+{pp&0NUdGkK&3kf}VH}{M}8ZD@CDSO41&% zzSy58F4I(y`C+gHf(H{zY%AF9!=L)HPv!~fd*4Ytq|X@7`hsCW-;$sY)lZd_cTT#` z%;V;Re`e?GV__+yBs+9|>sg5cHcbKBF_JD>9InY4(eI)D_PAr^JiBDmA3tgY=~V1< zCmmaQvxk>#q|IACxSmlaUr>vFd%2#XQ~ptw#X0(4gg3AmIuwiQ`m**uB1KkIZ?|V! zx3$JUT&&E=dtXp7mvomXO%mOKrvEu3&PBu*XN%sK3J@NrheOP7)CfN+R`Nn{bcx?A z$$e>)=X(R!3-PR{!&U8Bf4^?3ZP76jsz4QD4w2$ljoHfy0oVVyR`;(Io%x_TmwfLa ze`%<7djRog>~B&y5M@-SSoYUsz{VPu9u79N4WWvK!idh)p6B$QfV*$sMT=2gWvN6W zX3@Rr{y)seB2i z2+9QG4zbDgWYAT(Q11P7Z=6XW`8h%|*DWiapy5Z#!WvYxc=_-ReBF*{Zvl&RkaBM} z+1;d%%gSkLU*lY8IWCi7PoH^s>Au(&)6qZHd;}Jz^DF*(G^t0(WBVe)*TWFM8-KQ= z4mY%2Oqw|--u>LkH6|u_qCvyfW^wP)#LF*fcD1bcDA|=*xAwg3m(EX4>onS9SjF?O zzKzFeS!*jVP^<`?OjpxQAm$62w|PKV?disr+EQPJd!Nxz`D& z{=DR(#d3O7zrLMfG;X?FZ~V`@$VI+XDljqc-(|nKfvFlJKbWE2r;u zJ$R{}1ix)GH%R!+HrXYge)BP(!laE#gL}s6l8j8_W7)>l9)fS3T@Vp1A2}_rH?QAV zO<~V=BS|;zw^V)+sYNmI?fvKiGcopiwwOfoqqXWO>LD ztp={S(v21_vy(xpAO3vLE&(lz-zSuQyT@+m?T4$v3|A%ZRDu*GjTEbCF0mp{Z_y(! z>a_I*qDT_U`q3{Jr9?3CJS1xYbDs|l{@!d*O09qcM@iZJc5&Thgxq^^UuzptOQigw zYMy;q(NO1|>Wq(b34QxMq*4T~M0i~ax^SW87YS_K0r!-?lR-A3^4sqBmGRp3AGmm5 zpDa;(Z-D1In4o5I>s{l(Zj0!h7oWs3RoCO{-ng=&1?JGGorLF;22_O zjGmr$(hp6N?@AVmF1<#EK5G^OPq0(&cBS6X*GzJr_N(5%Jbw|Kjr=yiB;?I@B$`!l$IJyUPdvI*<>yQ>M7Tx4^AzE!7EL&3ozBX#HHOdl~^h%Vs}&a*oW4D-`z0 zB=f|!m!fgu=x*a5C(1PV1dWJ)PaubS@WTa99mswDI{zOF&3p5>^D17s^m-DLgp@Y; z{<5CPBI&IZTPEQ7dQ16wOL*wHs4JYvE?y?iIYETox%YBg_3B-(!n8}BZh`Unrs+3R zNxE73XJ@+yh9#`_Hs-P-IlbkLuervlySBW#vvLs3i@v^khHCdVBNIu58_np=(p6Io zO%v>*Ua5(O*HOM3E5@Rjrhwvk_Vu@ayz)Jb(h#kto8LNL><^0v2&*OGovGf7qDiN3 zvvTR(I^L7qqb%kPG%bFM7-)itNRV|uAqx#nE6kVj+c`$h`D2enp-SP}aR2KEHrMi3 zuN>oD-gRhs7gl|6jkSW(edv`;z4p6RVJW(fcSKgN>aLgKc96??@W3>xS+o$|MNnTo za-P0|iUzvBN^Y{&jo(#i7AGQBsNETm3?N8siEL^53AEum3^VV7{V zxn49hH0+*`(C~Yr>oIO)^AwF$-y$v6?A!r{R==zTczW(N)4^jaL=d^T^@Rk%u>0Zb zn?~ozvx&X49gqEa%9R5usWcR2Ma6xgBj1+!sVs4c2=;@ijM2Z$SWV(&!II9cO`oIe z!L8bZml?CFYYtr0+c(=D@*{k;JsJ+hIq%H#iAF+1jdnR__EKH`bSkniD1Y+Ic>T60 zq>V6wBBcmnCjHM}ZsXizdF@`WMeY#x<-;Z^I0;H{SoT>#R#(kwnNxnTL8F4$ zR7#tSUAx_wyPHoK_Q7{WfXMi;OGx5=j`F5)PZOsB5B(-%8sF5rLjTj=%ojeq5@9}DrF%N_yf<^()ZWydz=X%CtC*(&Zn+0Th4im7 z&QpXX2e#h(!?!0oef)FQcfaGWTaAH$7voDXwwo>2po|`c> zKBaz-U(Rs>>=A5ghGDU0VJ}9H>YRg6=aFHZfF5ebKVJ8dLl_P!4$OTq4}={}Z7_Ix{|vwD{Th<#(<&be?iuM6B3DGcEKI*N&yB!LD)*>yT>X1m7r05;`Wo zbzrooR`t-I4kF9P#k;j+%Z&_L~e|8-|~G4G-!^(a^rl|H#JQS-+l` zGF@=f#K|r8VV$1to4?{!_uhQSj^1O8>>PxMs+FwH@wwJ>{Q2Q3rMC_oQD5rYeR7JC zo+ne+i$p4LAsS*7C~T?530=3@!41Z^X^s=AXrQUBo3#L+(q!nv* zp3O0a7YTb(Vn-)Js9E0hN($P*=Y-ncZyrhmtxVHGWw!^3q?woWFw>fKHye!p*!LTI zvQ9$BXhOsl^;|Dzdc8Czz4|45-5Yi%)ZKfd4nDHInWgpg=`ZwsCZp4lp$!K<7-`;9 zKVRL?f|B?!!{TO=@+o7DS(%&8CmHFdN_BGCFD)7j)Bm|eaBH+~(v7M$-13&T>l7u0 z&k8ZJCc8K1C?{t)AN6>6+pn?0xE_uS^`O~XfB}QeDg~SORSDWiqikH{4=4gBO_PG@ z(^+k;wbf@2_qcI!{OL=oBFM5`weK#sOHA4rj7bXaD{djy(alnXZSr$GYNz3ru>bZ+ zs@4Ru(&+e)SBq##f;uwdx9HWvy-K~3!I1stNyIC9yK@i~pB$xP&?gqetv7sIE*4Ju za}{G8*BaAJy3Icb^2#Q6-#BX~{VVEg+sOw}Q22<3e6z1WXq1U3BPhv_4M)u zWBnxS)N)Zh<5P@Dk0YjDd6aaVJ@OfQoehrF^VdyuaQC-V-C0G8C3M<7^G%<%+;tA* zrZmXTH1%?lBme7ZdFBT``tyEX&E+SG!#8UmBvO1prQg-;VdP!bgRMAfTzH~%YhMKw zwfDkS@~??_>%D^+d{{xdw>wfWtdn>|R(>nXFHlHx&1zG;FFBZAO(LKwM$;=SgUe7a zE)G^5!XrgiBYcRIXc)72ykVc5?%jC>NfR3%DcV^h-I0AH4*prmC)(gUasjXgzeK>( zjs0riyQMfD^29T7U*9#NYP|lllh=RIaj-E4N4ibLCU)ivQEqBe)cWk14?MU!QLpmK z<;(OijLQm^;ct>d=kyZQ$7{V}l8osNnw+kk%-;veU2;?C*Y3osXIlK+xp!B(h|oVI zbnfA$K7qVnqQ$;0?_LMfMwM?g*{4KFrihY_I4szW{ zUykkyFteiW7b;moXWgmZH|yh_0}(7D`LEOu62%9oQg8jF%nz@&$5v3%Y2P*cG`Tq5 z%`;#5W*I$vtVHU}ZxTJ4VEU(N#$} zsLu#+DKLp6%lzOpCw)a!VeZ669O`FeV#Tin7Z!DnQSLED8?^_l>Ipk!0(+wTH4P~NT7=NlG;CIn9{$C(aF5t2Qws0uNRgJo6LD}(ooOxcwFc!z2 zM|zI@q7AMeR&pdwN>fN*vpR!ZB8;SWbh5p;_(xWGdjpJ}AQ&J_rC3#54FQOE4h&ej zRbSB-B3@{?4x=lF6U0eUeX$g-r^Vkdib5qzkC9&^H0R+%tF9T&BNsV(^UU_$X8HM8 zN8pgU)FN@d5FDe|`%<@0p0$5rf5nX`n?x?h>t&+xse$YRV-?rElol0|Dbp2+#~d9q zXJd(~kj3JK8r})$=K`ZYQDn<&fE@|}_9YWWc>uNb;eIdTE#^SaXFphlrZOQtCT zi~+>T{tau6Zd@}=<3I!T;MVwEVS8LIIfVCI%;x9H#HujlV;bJBcyaF^XZjM|^2$Uh zYp*8@YO{rxNMvW3motb_&Cw;kfbo^>)wvlu>=p^=Oci~ec-2*dnw%iB62LQf`Q-&5 zIY*;Twk!{RWM<7^p~(j5k{>Z6=o3{sMZhP5L9QkinP@%!&sa3H@bw%(r(wD`1iE`U zH8nNk27NH41E&O`7ewoWo>`UQKN-BgNVY=E02VJHF63PR9YO>r{fN&#U>g}eb19?Fs!k+rExIr%D(y@_3Vg18B!I0Bh=|Q@*Ow z@DR4P{41OXf5ia)jSFNUBc47LLSu5Qv283S(Rvto%Qp0uF#lNw(|VZhw^wh0oCum@ z3e34*61|?`##(ECUeL0%DQv~yLT_c`=d?})w$qbCbwgYFS6MpP2%X%_r1)AW2nL5t2 zxH`H8wK4IYAD>Oa<_AQNm9vM`aqpQepLIFD$^yY90}u5PGX?XG|&pe4uXdckv9d<)U?0l&$`A=b=qiV!BlAl7qMyLcJ0&rjf;7n z_0tS4s;jcTY1XvqJ8sJCY+)NF0$>GyjC|z)PoNAEGmKBL_!}KB0>{{xS!17RSc?OZ z0yE$pzF9)+v~aUL@0X1%9uPRanHs&{OK^wsV9E_Z7l6+Vot&HuNQc=9zJ&&Nh&~v5 zt{%$WC#m2Q^D2z0W;7vudkgEk}PP zF3iN0TK6Tt#PEr5uu?Z?_zoo?_j?2B|3aU=>Ee-r9hvvQUklK%u&e)pGJ`yaJZkg6e+HC}GQ6f+XxN0Dn6YQdBNGH4 zw!WphmlTu}5fRbsRD47OFfe?6-sdJrFTj1VSei-tU5+>9x%A>usWEEQ0B{?%*7cIi zR#!@a5h2?kb@;4NCx*&^_3Qn!f`gWnwOSGOI7)2_7frK9L-sXYhD;R-%6Ka*{<`lM zI+lml##Upz+s}WANUy6`N^pl*QCQs$&%9KoMqKHGWzcwXioDEoS2DcHZE$`ycfOUu z`_xI7Jtdu|XdYb)reHjQnOe+k5KI^h5rZI%W$8@NTAwb=e6+HNC4z00w5dj->ExH5 zb9t?A!N=0p)`lDUdDQp&4K0{X{|kbp^CGI_WjX$)FhnIQhz$gZFcteqd4@IhW=iN= zy>f|$k%#l1sY3d_3+phWRkB%WsJCrN$d5lFo6TfAcIonE2k2YNE>YPzeTC`19bj%~ ze9aeN7EEhG1U&e(I#5|BHl?sRHWmYBnFu5L?l!+I?Oy+OZ$WI9iMjg0bM{`OP2Z#& zzwJBmOqTYLGc$(L>0QV^tb`j4jk&a{OkjGP zQeE*rp4#O*;QB(mFHJ-K;C-Mp#GqqLD#lVL zK+4=I7)4&mV(q+cE#|b)SD+>pMIlOGLQ2|Hc#^C&#^!oXu)1Dw_~^SsUoMtDn&5M$ z7Yey}v^L{W*TS~{mjBh>bieaQ>-J})0N9fF`1rth8yCO>NLC~m zzatTwzwN*Mmlu~TL6;EC?*litr8p~CO+j(-54nQ|hK8y$CG?H8y7MsihQ>WB>&cu# z8-Cl>2Od9iE6OJd%%?I*J*;rXc2W`CKt}hfs9FMS^+(ufdlz!50uuKJOwu$G<2=6FfyWZrM-?jxWf`mpv!h;j`z(94L3WH{_ zJEhKI6INgno7-!yTSrSP>_@4=aFzRnDw00Ddx_)Fy~Fl=yRp&HL>PoEgX133GPSS6 zGkR?j6j9sZK9pbW%RZ{g`v}k^mCl3HfWTPUu4EIp?ET7{1*ORo<4sd`ur#Mi2QWc^ zhxp9F$It*`F)L^k7f~)i1bQpLRgj8%1H0@%V2XYTkamRC=1rJO+l@*+?K<+TCg0W9 zFjs^MVKe~N!S@| z@R)sY_R0DJ3@Srxx%s_vb#=8IW`HiMSuOh3A`ouI`PA;7iETT8B^m_b;MTu2KWiHS zD3hbSNjloY&3L-=i&(*Ox4M-RXk-k4-7QQ;%4k3ub46-=*Rdy1Kgp0M7i^Tycu5|(eu{YO<4^7mHNJt>Mu3!it1Ox@M4N4bQ z{9%?xvUc$ZqK>eva^ta)WGkureG;H4=IK-VXNL%13tzeJ=n#31xK>?2a3uf}nHU>; zye)ln`Vf+n2J~tHpC9mCJB0Rlh_ri`#VOh~K^0iQv8d85-aD$i$#&}2Zp{gMzJH07#684IODE2>~FA9VSJM*Ztw=U0oO0JF#DF1NN-?+^hd(2wEaev&U% zg9CyQ*ua-J6_T*g9TPu&9bpKx?28N1g$Fri5?>cVMXue3TYp&>hc>D^T=KM_pr!+H z5iY0>4(|nsjDYV=Vea$$e$_HUe#FCa3GYk@?@*N-;0GK+k0|3k(a1r-^ zwA(XGY3=;o%8?klxgPc_NcIFrL@kbM*+%egcpy)btZ!q8X>LLMl z=1GlKREHhJ3}~3Wxmgo2Uu3rnI1LUFnDxL`lmb*A$g>3bskbqA0lx!GIrfl-5gq64 zD=jCiN5^g}BJeeEW}tlq%OpSM&4jS&8e!yU{xvE--U62b3O85Ya9x#VH&oewJG2u{ zzO{Gz*-YMGE*2J_AEp8ghdPgdx(}K^-P_v>{-7mv(vctq%4>cL#_Ha`Mma!et{p|< zP=ko(jm#T!Aed&B6Y}2_=H)d4PYQ3Zge@2|1c-M8g;^q{Lw~(pq#B3QC``O1f>JCP z7Le2W-an}1^CKL{%*atf`lDUhcaa@TtoP^iKHdZhxQ50!f1%H3?QTT?_AFyF$A&eQ z?DEUw{6}?kbu(6(h~28Vz+EFO3ZT=8k1&~J$_^5=@xGr7JbpV`8x>k)~X%JOaS8`Cc;Ro3yzc~*90uJ3O1WRsphw;moL1Pd?0FejW z&38!$;rm{>WoyD52-qV~sWio<2z3KwA_lD)3MrW0#n4+QGQO-h=&K6cI*<{;P#6NV z?)`qpEZEnLUTy>6f|1S|pq=~!)>{!EY+vofG4mvhx{lwea)^n_ds z_>vzKPO<6F<1b0GgPDTUh4L~qyAISW)jwa2JPw?R*6ZQi#X|Zj z!fM6{qzE2Sc9%gaxibRrGW_0cDx?Vs?*h8&Az-ZZl1TNFh&Pt4F}c59 zxz}y%LOczTc(aS#3<@XQgH}_6yEK`vX76Oo52AVw(t+E|M2HK7en!L#K-d?|dlM4OxiK~zcoKww zBtFJKW(7J22$_u_pCaPotw?2N!2GO$s;a8}QAiI30LO;##E?az8A*hN1+}Uk*UU`c z07CB5r%}9-`#=Q}i3-BE=LJ1yvo!^6jab-5u!tL)Cd{B?xDPHb^Tr7$0K*}KGr#~T zkE`~5A0iC5w*|(10l|Cxa-`a%6n2~14frX7`LG}}39>qKxXtPTLl3C#umM<934pA` z%c^T~FhGk&(8~h1e+PFlaB;=L&rhO~4d8N!{Hw-a1JXg}=7!67xw=MvIe`zntpLub zKTUqz9~A)+QXtJR7fxd+?QTXz49@6h6R^F60ie$w#M_!LBWf|&cZOayFQ#g+oeE>6tN;e#nlvR3sj#NkGF$>yKSHNpy32Cx$61sSP!s) zPu87zAm1P21{(z!sw=lt06q&ssm-t~5;moz;nW;LTh4t zfpG63*cugt@%bAvmF0y1Wl$=biF1BRxrlW=8qFIVgeH{{#226@!EFwdTPr|I27c`3 z+d;?$WPmc|WxZijF6h=nR@bD4U!jA-YCzf{gy#eaL=89sCAGg4`)26#zX8>yHb{^I zR5WP3xI0Q4X*i=f!qcg8H1O|EjkRct!h3x#Cmc?ZJ$B3`0J*bl}UIeqssf^C>4fTcqFMgZxUtJ zgcp8;igJjU7Ybbucuqeh-cb8awwn<;03bROQ`0c8$EIr{NGTfTE4zz*^-SR;wYCM) z0CN&O3h?(pd%Y3y&IE8f<&NK&;{j2e@^?R7D=q@01gZ4`fsjsuX8+|l908dB82&H-cbMUtd34mk5K{v_K>(yPaQ=WY_Ly zb&6C<%Tyx_czXgZ$25SOnN?^jO`$f1w6u{BQZ(;u9aLwLH3B}YS{*v)Lmvx1?GivN z7dFcz7ECEW;ENC=8W04C1#k=6IuP#h!Y&Vij4_}L>wEi!K`Qx^Cy4JZj=qNVYv+~` zDkEdJL!m~5Z3gxZNJ0b==oq#bC@94d`)u{%i%#u?kAi^66ghmj3HA?`{q^NrydCK1 z?Phq-!Y{u)CG{p8@pK@59UWM+60BJa)_f1}cn9G6L*i99Z(_&+j7w0uU50cehBsn< zKODX(hkA9Gb`=1edK_XLf#wM#TDY*$lU9%p4Tma0UrvQ3AWPkIC_;JmFtb-@wI3wV&PeVP7QH9DR7F`x?UEKr1 z!W0uhyAt^IQY-`$dY>TFIXy&p>W6}GXnT~r{>IstlXhu&vT6R~Girz~B?FkVU&3b& ziQOhv24M@J6Oe~1?Nb$evS}6(X~0!Q2Of6$ag%)`v?LI$wSQm$#VR_6O9UwQ!L_dZ z0+w+FU@bF`HYC zpM3@vfBgK7Sr*U*<$}KLVpBVvXeaDSt2p>iBZSG;46cN54aETO8~^6>TI^|wa+M5g;W#>LQlhYHgF)swzchH zR{WLGA4LuZs0nr94sV*1>CHAJ39eIHm}dzqpMd*hi?YRrD}K8Yn;1w+_2~lD0@UeCqod7 zfkmPDnnHi~ac)|~#{`|Lj6<80E^!b$PeWX#V`1?S?B(ND@Uqiz4qMTmM3p?SHtFEo z!W|((K*kj1y+3~ZkPh1Ohr@LiEH#RffnCc4*4hPYZGyF;L=N#bAO%}M>o>p;l!2Zh zG<-m9;&8UENgJFk6wN}>7_L!z2sq6yxGM2LIN=D)8$h82b}UgEB2RZXQWOw(qfnKR ziAg8n`gw?IWkJ$?ZAULli=N^I)gkANOfP@pcLEgT9 zAoiM!vl7grQxB}|5`uER^7aLD^ES|TJ91uZu{*rem-1d`a;?VvRvLuNw89};O` zMUsFMfoTLIg6LLkyt-szX#*s5%E1tDOhV=dCW2u%WB3hl!m%R4GgROK9(XV;BKAXs zez1i4g@^+wF);*P=bQ@2Zdwpg1`yA{!JCI-SV#+OC)@*f#2hpUae6b$SsMZLT+}&% zl65FpSwucDlUD#CKB^Kd3I~AZ-!I3e zpE!||X#MkUNg06z0^cBGz_vpxXX-YWTcZ}8iVy^)rcfZH&`K9 zRH!OV7qQuPKp2DKKoIjBl}`)h-OZS1gS+q;5$OS#9EcKFR8AHGO^hQXQ;7c%PIh-J zj*s2u!`VmKA}L2MQ8DWruk<+ilm)1PB!Q`oy@GF-SIJjg(R)4^;#{LOk$&28LUi}T z`}e~US{>5iEr^Q1hku19Wxs?4i2+Ga(!o<6aB>!2WB_YjJlG%9zhp)>x{QEJh}{;w z33L~K(aAs#1Z+$YhxHwXPIw#OR6}>hOk&$YA^V?(GZfJ|i+tTt`0?j)oU&A?nTX(B zaK?ksRrmp$Yv4$@$BHogh+hjBMh*usXRybe7!6n`hO_A%fm^bJ1@=K~y;9YHqX9-I zH5mZxg851pfKcSDtq%)OIq(NCH`KtgjjgO=fL=ilmKt9MOZ^F;*p`FIHldIsK^lyV z-ff(bx7#9?Q3CD=lzdMBgOzr|^h>}MBKlB71oR(wgoC(2G{2yr8A2AsJ&YvK!JzkX z#KgoPIx&c}V|d$^05KT~nbE37H8b4o?A4=>P9v>MaMRJSN=vw~1@;7{biUmTZ79CA zfIhD`;X^gUffM|ys=PqE2A2XkzrUXL2r>j{VWJWx`~Eu*DrR9nG=QN1jK59-BRxbY z$Ogfg6AoZ@qJYhl_yiDBMA-sG*){}LP0;GU3~%ue!cP&PtBU;ey%pHcAiOyQ83~+D zNqAD2noq%Gocwqr3O^uc7dU)aA71l{N9|Y&gaywH(_?_HwQq*B6%o%JUxEi zM5F@_kJa4sRJZ0ZI@Kr$TmU$u4u4V=ZbK!c3EGIt5Pmoc=$@l^pLIcXH5Lp%NTJ(- z87*$cpXFm)9l5&sYYa8Zm-|hPrfL>Sm|#!OS(KiSL)8mt=x6Ivvg;>F1B29u3^hJz z31qit@q;(~-SroX!QvMt$ffd1OHue=2yYY!-cnr=So@&PRk{n7%@&JW2j?ss!>!H2)Oh^0b$ z^@z6#5qPM?IKfd^ft!_;nW>KU^ez4@9oy=yHE35kK;nwBAdvh8oB#OOU+wFDO2i$b z5`)eTs;WXYMJRxoN)U=iK2TOb`;F34;D>@e@kaG$sMo%mKrA>=5g_e?01wrDV0;Jp zV|b$K1wtX1H`O6XND(3*2B5KR(6K}(?>>0M*jJ?Bf9pPIv{$-S5NJN|!=`0`<5Qm%awfW%wtfod0UU?FQ<6tk~ zeKk&4F!w8ri4?guPQ6#;&dX~X|H!#0Uh)?HXV3C{Y=-E9v3;TLr7&vAbJsR1Q4wu; z*b0W2REYLyE9M|w)`GGwZ~>*_{CY@uY2MQ; z8A>;3@}jKF_&+;Xm9D>r30MV7n3b2e4-vm13SUpXxinsmXTHKGf70In;%qXLlqtf9 zArpA+l=753C2l}kztTf2ak*p4qw|7?zXQM6_(XlyAv(qIh%85ccE#J)^w(B`RKO-e zB}>P_A-i$OxDI(B=xmCC=ZS>t&io=fGrMWs>sMu-({iK4&56Nd9GvtHMWOHPBm0ag zFv}eb9-UM0NhZ%^EHNMOjqw?%*vqYqzsJ6z>K4q7&8Wte+xIS4g6Gufy*N(Ucg@K0Xkb%wx zTNN8#lIE_1^X&D!w%M=t(zY*zvv>AvThSURJEf_hpPXF}yKASyt@h9-yiFwMo-Ctn zb~6LpTeYtg2jCN94PageMJ~YGg4j=h|-+I>8UyQPOh;{-gbp#rx^E1W7=J+vV^7;PZM^%r2UrS-jlOAXpPJf zzbt}(vR>`-fi$HSM?SmpW}YQE%xvLGlr*2~`a%SICj+BJJne?s7L_;=JsKog@K3zoP9>HX zw%#UiAJ(k@sSwcC5ZpILFuYbWay|Kw7MIAjU#qA3xT?fgdEt-OnN?=K zO;*`PI%p3s(R01x9x-}2(^_9Gls6Y!Cm{bV(oE+TnIqK)`{)m-Vdu ztssVi;XbazO%T^GTO!f{H0gsHc2Mz+L%BY9XEN5^jDEzY0~eJHBw@03R|>&^p(Zax z2@rvrq?Qw`A-0<<58S>EMF^e!uowq(ioU`>0VNB6B| z+(CgoE}KuYP_+%(r&AeWdGBm^_Hbj*l>3Wpdp~{I&vVJm7l%R_Y)iEWt?dkMs=UkR zn!DOPu1!0#C;F<~`6yS^Db)r*xDKkNPlHTwvDm>!p`yXBngvN9CjwhAjQlV=ap&GK%uWGF|g{MprvNV=A z*MyXR6W!6{ijGxMCApVnxMn%eT}eFX$93mxhfSbgHgWB#&?}y=SGw-y46v}@J;rp_ zub#)`VOVy%rddZ2EYf1XUI6J0bC#`|Nufo_g!%G3GPoHIZ=jC|J(RbQpiQlm$shJW zf3qAKvXI&#no>}VQtHbgZ3W6V5S@s5Aa)k2LV7HVgoiT@K}Vq}h{hR!(*w~CfeA<- zvUX~D-h9$K#_EpqrxSvrUv>Kqq&K;gT~PmcpjvrbgS)t-@wQ`|$9&*OI%6$~a{{Y| z1+BgIuzMnFI`vhj+yJs`eaG3K_^Mo-rLTXl7Ja7Hbj{eC7QFgOk%bt@dWQogAuF`Y zPz@*@4t_M0$?brWaG&3(Ck66BCd7i!QiYgd+Cqu>*bqUYy>0KZGSIR8T~%RVWrh6b zqry3&S1jh!#?NR-?h&F+F%gJ!zS65$yj0XJZe6ZenDWSIyd5eyHZBoV_nen2AsO^^ z9c@xKl(enlqj>YrS#(~oNtI)ap30aWnYpB3!W3xDLq-kXS`IY6=#f}BBB$PiLJUM* zl_c=~u8_hyyN=g!0H>C_y!GO-lSW4EK$2)|^<(17Zw4}|-G)o2vnj4plLdE8X~umD zv&!Y7P1v7M)F>e)&2=zEi1NZ;^C;Pxr}rJ@uwJV{PWSw+b#oN2u-SSiVjC!Fu8H61m&qCj(wQz) z&gU}zE?68oXA?o~FqduDEPi#tZSOh9Gjf|JNzr2m4-TF2r`r*Jn`wQzdcNE{8doa0 z(O~(Z;Rfnt0<%~!oK+G;(F{(Zdv{{1kWY_mi15RK5Y@Jq+KzW`fY@Y++YKU}Jii;0 z7R=SkRHw-)PlScj=MW{bG8M5N$kzODcXG@j7t|D>;+r|YLh=LR(Gyp%ilDKy{7=tU zb_N+Xu&YCaIpM=G^>SUlom7VX$0oI1N!~9SR$t<{iy;xhP%&d-i1qIB>o@Ej#e}EE z>D}3e@;|cxlNW7S zN`9rq5B2h7>pICMb4SFrCufe|qP*>+|4}EOr!>F)bMwiyU(1Y=F$8RO`^vk7OW0=o zZ(V7TgS7ddA@7(c-nFw`#FnREJIyUD9DunV71&j`zcT41d4FzbQHFT}JKH^+AnU68 z@eS;5CPw6?k|!LrHlKJ?&?6R7KZ7A-WEL66-rlqc$iz_DA7Ss684YbebY;?SkfxoW zvJ&3Abg*~yMd--e=LbqmIX@DX*-No;j&_yYyM(ld-!IEFeE$BnDMpd|f$WQFM` zMRL8nS^WJM8zagS=UJI(?={^tsUCAfygdiu1H;f#kwxMAi3nd(!JZ9d{^HFBCJXwb z^`eIO1GqC@e-&{N*qqqZF{#~-dMeC17Z(|&w9DzKtkG%;_7}#VSQFMV=X^S$S)EmXCJbB zHUF|=dWnZ~loy(3b`Nuf^nceM;J9!(NCsrI7g58GM{z4;wf8@?$+TNxRkZh00-sd0*}I(Ymt5Y0&@MK!Wny9 z7DbjJU_*#)LDgb+nUlFgIn{qgJP$qJ@f;2Lv(d<}9fTO(T!TAFB<~XK_)B+u?+>K* z9v3CHQoiLQNmqZ|;tunIlg-05;l7!QbDFK>{fbhpAu)#YE^)!NDC6?7fQDUGb+LmjApc8{hrZCv$g_K_#hx)_gox<(bjc<8B@XzUS$0T6~kuX0G~> zop@>+sdwrLBrfJNb(n?UgJ8J0C4aF)jV|k-GdVDubZO6T;==vUwG13QOm`j0y`Dfb z6PBeb{tfIK)1WgJ zP%5*2CFmJDpB6X#eB9ozQ!gF_sM|%Yv@brvz^h}n%8OVpZHU^8ZTqIs$Y!^j*xC%y zn)TboJIeC($qpApuUdSWY>vNYUVUuEL`XXjT0;X7>|zWKqHpP*wIp(L#2E7^hY#`n z_vHsta(NC}YLXCom+d=Eo&Pybu=Rs#WR2a^PK#2dEZ6s}k|`oykw3N6x*#|2Y;C#` zUB4WdAB&qP;yYHEVz=P!cq@qPhYD{)(AOVCDp2#UGb+lS79*^ z&*o3_?0B`lnJt6JvaTV!m7DAbUoO|KvS3mT@AEg;@gxhW9`^QN2oHtvL zct*JNJxZCZ$72}=K8s>LGDhVo4K*oOJZ84d*;bk1+{WG@k6el^^vHg$v6Yd=f_afW zsck@_s2kyoZ>p}tuSd+Cvi-1HS5cm_WWwwslBfR0noGDKvFkfIf0kpl?*@%K<+tyA zv+IVCgGXw-+dR;00|vAqmU<+)M)$D&lq}DB`~7y&v!i57y3HHO1DXAjmm9?FWQ+(3#^>Xz%WUWA@#zpTHH#eg zxUiVoQCJeva)A2LR7zJoc~$5WPp@!pyYMHT?#;i*;|HQ}B2JnA@=p2KgT53S$%C4G zAI{V#7m4I~R+r_r9tyaB`M)3ZAwzwNo!JN^JGI|V;=NO;#io)fl_tK!hlJ1|t;j3K zZx1XIqw$ynYa{=_l-{NEyEA-6d75*1Ec@j7*scdMKms}MVjCSc`v9VUrHuzQDY=#( z>})?AtzOwHh`kZhwl~nGyxwzAY{<%zOee>P$P~k!|6=B1y4IS=m8tboDP=o2i4wxz zG7~u>5bV(C=kU1`*Xb;6bif5Yu_iLtri`1rTFI06kV)^3rEtLg;3FK1^PF!m+pm5E z4_>?RhDp|tQpI6{ayhYWfnitD#gLL^U9vA{i)Q!aKZl!R)ISJbH^wI!>W0c?&upk zk=dAbXP0@#N_H!JNUL3qwp7^ICGB&;YKCS|6*B8!(2iWBp7*TeKjbZ+tx22Hc$rNl z=HJduK33MSYO~uaE<=lqikp{?);xNfG{E$~XJ%oa^#{LBDq8dY@0YKcg@%)`3?JUH zb9{m~b@9t=a#c^xo6QDn?)&TXGFfWVmKe5&mvov|9$P)tk)RrqseFE_{|t9&e#C7z zR+E3<4(yHM=SxK@>Dd^H&V|X(jeOPr@A;J0A9_k(wLFWuk@sBcqK-xgI)%`ZHE@7; zu$faoJx>&#nh?#alhWT|u5ga6`b9yWU(>7%f7{Nr#`t{M!-*Nq-(@miXmHV4nlSy# z7uh+xv@z*#@a7w@%?(Ljof9F&=hxWl@IJTEt$w*4F7a*7{EJVuV{|yNb=bVX<5p!{ zZbbZxO^72dM{YV^on z1?X|!ns@bLChPdrXrc6vErF+se;eU;8%lEIbK;%%&0 z0CSdumS5AwJi4Lg>-ld=#u`G1F+44UIsNCw0`{B#%Og3se;59&sS)E_F5$CK8YQZ| zR($xMhklaRWL=H@NjtWfv*8Q}6-HHu9$j|HJOj!jM1EqbxH%7u9hQb$t)zQ@;>Esn;ax z=Qzp?BIvx1UCtb=wRzlJf9AigLjH-}KRzTEZ!TAz`6<;o3zL_}=66N)pKHFhK1B2_ z=~A_Fz^y^DyF~p8tv_=&oIiYO_+hgiRl&`CeQ{!s+_I{BcTd^kAOQKH}-b#uP7!{Tz2FUe|xflH}|;fyElCY5-`|Z>yayz zG7lly&Ikz8S{gbFw$4EY3%+#n?P{Uqx5KQfXW3Tw)5=9!xBCqe8zvAZFHWe<$wmBk z!z3qTx0^g7uN9wCVpCgPlS&3gO5CygRY?c;qLp@|)RN z5+Gl3uOo7q00n2v*RPn)H3xW@#C^<-3;(@RYi?g^H#ST zCh9n%vbkPlASP z<#)T&fi7jd_PgJ&HA&u5E3!Qp@2DG}YH6fX!_@lTu!Dh?9@TG6VdD`_E!P@qq4Dv1 z^%hr%!xSS=SG|Lhcj8d=q2DSHnX19|KG=?>28R1W3%9Y z&v5!(ib=-r(c!NM-_m5M%PfQMGuyx2KY7mxbB>%A+;vaT(TVuuJB-P?G>ZR4u+*LTdtdz2tgq7 zI?+HencNnLWh?QMQLF1JWttzwwMpobw-LT#HXaQBU$ISC*cVK-Pd%>kTw})Te;jA- z67dt8r?bjcjX%_%9-mwaITm|kC;5wgxGCsrI(Hlai!$Dz=1*_=W_UNae=m?g=m#z43Qg3U6Akc~Oo% zyly6+HH_3!tHNvLs1YGXIx_3?jSC~^4-jwtKE*H5wQJws6c+O6Xh^F2hHBm`uPd3> z^7dbjEVYhs&~Jey=@6wzJlr4wINTPWGdnZ_`cUsJ}GG12$etuKYft zUi^|v`~=;H6NJwv!oTcG7A7_{av3=<-1vGotmPAtm|W zL?3%rW1AN2?ELE?RlIfJqKul1GHHxqhPPW~3VlI&ge*h}<9=U+%=+opzBg1^g#e7p z?+^oXTlnuCq9~emtcgCxLF-Ol9^cnhXVO!@pz}q~RWLTeS8s2Z|4&X3#?@2$PqJW2 zXg+;nEaz{gzy|Rin|G#KAS0}PXh*BxA zkV7c*lv&E<&63Mc@b}mmLJMx6FRN9Ig=m|pvP$rI_SDC9VVY}na|-mKN_&%Bc6 zTUBe19?ctSeD4nNh$V!eCNp*P<}`2mj*EJzeT9HBy(qDx%3wdrHX7x4;r~g`&&vCV z{Yz5H!7Uk+(BI|dagcJJgN^7OIli&&pOa>7G;htv_ErZ9qP=^RM=|pZOak6~l@H+! z1oYK~otdeIzc*jkSXT&7z2v=b*;fi0sPQ*$V4uC6(HIQ{EE zD`LIp*SWthEG-!d-3%%YB%9w?SO1%b7H4R&=LltC9V(trR6pQvaygdiM*$o>=c+nW)U0O@?evEt7Dp{zWNEnhZAbEhh5};sy+5mJK5@ngPY3H76$Gu$6N8l6K8KL+tEX0;omaR=F@!cQQhgmT%FqzBp5uv1-WKfz$rfyQ*Dxv~22>3Fp#hCbGjb zwDkTdy{<|1Zb`8HREL`hfWMOUJ##I+@q{z~HC#y=YIW@?2Od;=R z?}2}$bUE6fh5U(T=!t%(K;OD^#IvUkU%7FYlZ>_g)!9@^*iKp1cq3ap!7`~k!UpHP zA1ZG=d_PWA#wkOV#9k=eDa_8;d0yeC*w*F5LP^OUwp;p*E3Xa%3Ja#YM+3K$fNUE`xAUVgK-l7td)U&+|%doI^DhFa?3e-*mW74zmgp%8aT1-|K&?%+2F@Q8 z@9y77)0$*LSxuYfCa`y%7Ut>Hu zZ{+3N643KwQq~o2qj;a?Aj9`6z|PfmD&apZB6Q5_eHzHiZ{)D-6XZbLZOXd0>JKZ* zE+GB?=wOleTN?S4pi|@ysZD{>Y$BYRV_4DKtOmRaCh?5V-)R=1s90%Mbb?xc)K~1> zUVY<1TP;}yxthH?BC8Gk2boh`J&(HIvqoLo^Op8zZ6Qn9lu6;KOGhTlw%zXZM|oM1JKbC#VVRa!DT79fmLd<5t_> zU+?zc+S4%>QJB&5asoCyLJk<#J!Ct4NttYQ@wgwwZI+LlcK&nkGTi?@Z@kLY9ejTm z&_uR+c=a#+2a%_}A#ZXd*sLG95@4AN2gK(|C$I54dlFYP6H zwhUsq#X-gFxg~A+vy4oq$qvhmJmlG}vJLvY$EomTl9kB;7Uz$m zHT-{cBqNY+<>5l|1|EAJ{+UlK6X8{+r!tqe z%xNe|CY^ZNo6sQHGY>sKU_9BqFK!F+Y52)<*ksN;bo$TeLwiry_c)NM;zQH@b0e4c z;KjqdiK3GU`eRk4v2OHZG>`GA3&(F=y?+-BD9PRMl{V#HPDNj`O&jgMm7%?7fRPW5 zjyDN0_RBHNe7?qdV_G!tD9PR(rG`+>K=%5pV*<0cgKwmEujD>-A9L^+i=YK=g>y1| z@oplrdNdCtv|fH77?}iLKInJC98SRN7={lIJeDG-jAsHzMx85B@%)EU_}R+WIUN_) zO-*8#gjPRH1@7Cw9wy~e8{XZ45+ECSc9*K*${i%ff0rNjQS=?%MAl{1EW~4rh0Ds? zqaeMQ#QKu>fQE=FttBrWxJ)6!74Z7fLVtF6;JdB>a0FpQYwf-<9YW~$6uUD1Kz)M zV>8Q7?+ReYhtk87PN zGfV~Og*(W$##>XQ`D3Mk3lFLCJ77j@p`Ida+dGl+U!f{bVtP7kq(@u6ee$%iue8L+fKDIYD4(!5|VtcD|nG=zK_SvybPV8AjTdpR<<53_S$-bqrPd}Jgkg?Rru26>PEdYcG+=}#b) zrGu34%A=lz6Th2>bW^cWgRiW*E-;f|Z|_tEH;6_51qj z_?wyNqtAONY*sMWp4u`dW>Fcde1j>=dCUCExwTJF$VKT;|01M@ej&o)E#ysL(WDYD zaQIFF*8`}98B&`4cQ4SbY%s55h*bck0r1i9+$Sw3SRbsz#~cIsXs?3ty^oz0M)m6P6bsqd~5JM$FTEmAgi~gj&Q(qw~&ZT-l*BNAzxOH|lrr zN-)({%s5J3$h(bfQ;`m|@hCuLKe_1;=V+D;f9AIhXzlQOh9l;W!=9ds?>j+H-WkII_}@RH0Y#M$ z`%Xko=j;I9lunEiz;<}^pUiMDh`dMErU7F3vhglV9#Cdf7|J|KhuHm5t%xG$e5!p`bXZK})J<{gAK{9Le8Llr=4u+tDY}dyO**r+h=5 zLzgW^T{nr7HkmFv_4tPTN2_%k9OiYAZfCS^xN`8Ry{Ut*zB(+ty*sc#xsApAZ8VTV zpXHlYVZsNQagedAw4~F+0E6~BLx*JS{A+7#B;6W&RK91AuC6YrM%!4{s{h~G2e~C9 z`EzbkOpug^h)qBR>05x(f5v`* zu1z@HyDg>QqACwJsx6E%UjzqN_uIjd@Vf1r&#Cv2P+HH@llnlJe;@P1D5Jfg{NYtw z+l4H2iNVE9r~`OVJ=MO7SR4R4@EK-U7XdVYL;&T2nn=+D2s{aR^(pIF17J*UUfOx& z#`*dgAOyxBL2mqISNZYKyMB` ziF10CO!4Un=X@a=$mh|Zje;5Xgp7>SadUdl7nipYd;DT;3O9Zy2yJ=Ujg{&+)Bd(S z^6>|UUK@Gtlh=8#+t+7=PTtj}FYPO_Yt}2>we8QZuqz(P&#~?IZL-h=aBoi9ks0oB zS&CPQhgjrVTVb23`6 zB*d_B0f`GB*ylO5AWvrZ?(mK;mo1h}9NY4&u}jF8OVL_yy&eqM1y@xUrQkWE zA>D41#OU;->{p($g^z!s+ODIEK3ry?SrX+Q)5t$C`)WGpLVSUBuI9qBl=6*F%=$>| z(SVCh{co!-QhsX;Nx)U}Lcddf3#JHg;$VzxkEQ7RXCc|Y8yK7{Z|aEY^`Pgq&@C*) zz9eELz{HI!?DX8qNB}9|Vd{kntdwt>s=uM9Q;5bKrYqP;v0p~}=?!W;%k#^|Uo%B2 zD>e3t2P?^a|Gtj37BU5Fn<^ySFRYbAuKhhIE-MoBO>*4t1;fyt`w!+d9Tl&iU49`k zet2Ok^ZEtF0KHFm?N4}pmrn)Ha-aMvx*Rs^x^Pe=uydG6`;Lcibh1Ek!^C|=Xpxo~ z-T#4XQbdUb@Uk zF^4J6L|L9d+h`#p7C;cd&C9zm$dA!hPC`@igpES4wbQb`y^(u&b{N?BzW-SvT$qj0i zs&`s{?4Op9kXW5JM~s4%zT&ig7-;^xAwV+XMKrFvEs$qcZDeZ;L*6v<4Q&Mq!vhNA z+`0qB>vd0`_9}Q2&@kZ+d`q^BTJ8{fg}UHJN2XW64B-jYX35Wb{NZz;6?6gwb)I2S z{i=i16wLZztkO_L@ulK3b?BTH(h8~y2&Qhpzy%Qm)zs7~Ab7B4I3Yvz@8O6sbrKQOOKB4rX+@bny2|p z)X8JM#Xy}FqcCY(Q1$C|c6RpU#&%_N^LsNPhThk-JgjU5Z)Rp@E=jTcmV_t_i49C* z32r{dii*CZl_p4X&aQM?P*9Ne&;cE7?G(V$%BDCKJeJCb@HjIiMHZ68O_y7~ zeVZZFI2L3<`x%(>M1kJM06uu#Fkw`9TRtI@{CqJ))FaMWx~8UaaAo^gE|Ca2MA9;| zX}XJ$pDn#^%FoBw2W91)0nT_Iii&4vW?mAITYXBf=bZ~O{EBt&L2t~MoIP{sKv;Nq z8brJY_w7Bn?`b}|(po4Ra=D`!({tV;ZD5XO64U5Ut9xXEn)xcPN1D1AmNq~b<^&ue z_kh@so1b4_j8PAM<>h^@CB01wuF$ryNaHJ?46zFe4-dZwidX>>tdKS{FtDEPkRlDB zhV+_FKZjpHK!6dX@$=Pe-mbl%iUx0`ZE2Z4I5b2OkC9O}AnK!;A~L{CA! z0WeC%(iaM0>yC3kU6EN+V+K|SUX(%P!MoY>l1y886L6ntj>K(hzERJt>S|M1G8k1j zW$^*trn^>I6`I%$7_QGureq{Eel!a0t^X6o(;4W|vOj+8S^CyxY-}6@g{U*r)6?@C z03D`dYMKXtk>?kH0rZda?l+VK$c)`%)=jD3|07Lgio349;OK0Cm&P5~yPSbcC7MZn z@Ej5cBU>%hh)m@VT23oLdD7Q)w~vrLPxsxIIt(=YC<2Pd6K6750nY2Yzc$5p>1 zP>nqXVFP*DRqECqoeRhhi`ft)-wx+ndka{u3iTPf+8`csA5N5289izZOowC&la%0A z+;LG>?!(JcOhNO2s_XzZnwFzSr8SYgj0KvUsnCLlburDPKl$e{l1}+8R+yz8mVcfe z85wB#3xLNRYyzfmOJMr;~#+xST-TA8?v)SPK_$KVw)Mg_bJ8R9ttgd!Rc)(Z{4J65)PCRa8$A5{?W>wy zN4BW=RU>y?odq#fRk5LNsc}H0!Q@fG9GK^l+c`Lx4fGo&4Ity8BC8+~yJ72z%)go{ z3~1n~$;mR}-BFuA4P^;_^=oCNoSLWZ8~U+f#Uv&thlOKBNp$(K33y&bTh7wbQk%Nh zwD<4bD|~o&)*iT35>9smB9!EJMFI{f%I=aoRC&rIsEhqL(4WxaLAl2KX*ro#DJv_x z-}tA&lO5m4G*x0{MHKTfU(gygs{Kk)k>=ipKua9Lioq5DIM(9sUCs*^Nd5{!sHT<{ z9aU8db*`u-mOrWlLYKX-g)(4B)J;66mjio5R*t|t=rEBV0aI*2`RKlNMk zH=Yvkp212lENkRRospPC<6~qiNnBBL^Yh|Sibg;LFgtiH1Dr-)ULHyPjEsz&CIUpc z>HM=uIw!hMrC6|CN}D6W^8@f<>KGVguCKdz0Xq~dUvU_Gaq z3SG63y{RgfPC4y1Rz-q}r5;(sZdv1ueMudcC03S1A;6NVN*(!~B@YPT41xY=aeaWN zpei{cf{p~heW@@-30y*VO7W60z}SSYfvCdDT4x3Xc_sVBwez+E??10za(DwHcEblc zD72C%OER+ah-qc3`g8_oY|DiaQdmp-3a(Vzp0$snEZo0nz?(t;!{I;We7 zw9)&?L6@xEPu=tTr*Yo|xgx z8D}8X=$8E~K$1J{9A@R=ev|pP$kGR=Z|HuJmD(SdavqHx&TSFpxNzaQWzGKIVzJ_V zxIM>M@aLAo)++FiI$$el5lra^?#AjPzyglKb|0TP|51-*|X5P z7vWoNa=YYjRgyF_=j*c_L`bhnhRI*_Q!yPFmZJx;@)HG;YS6b+pDw zQjtcZeE04s59uud1pI^HJ^e;t;w=kX_$EtlUET2HPR_{hUrvJQGnRaR$V7TD1=;ib znwGvl_n3fHwYIhjt?NP3?Z5|>k0 zsO2a3%(BJ@1Q#{5f-P?K1gim}j2NaPH^oc73;0^rtdR81h?AxZ6!%NG!7)YX;Qb!- z^nGjVx2(}ybR<`2*G?KHRTUMOqAlDxpG^Hf+Uau*CW7qTTp<|qKdr^ z`ckV^Ag-J*LZ)w`%iY}9z{i}Jo$B+2X9PnCPvxYj6X}f(Ap~NWFUxb{)kM ze8~9c0LiD7izBA}B<0d)2_C!46n|9^`_E09oRfpi52~LKs6KJ`pskXl?5Rld!0otN z2%3W<`11P|>n*NybErKz2cBQ;k8knVyT<&4WTbL}SSFHMwffD!&Wk#ap&F zUp)8kd%!J#|I8{Z>|E9CpddX(6N%%Z_RD<#zGL-L7t=r8B01e7K9eh3*QpM^KL|<& zre+A^mfKbC*c;t6?l0d>iAhq#Z>*qEt>1ruGOd1eadS#PlL1Exn2HzJ2_UmtO zFdFm_K z(N1NngdF`^VTFbtp&HW%$PIrDs{5mD^`2UqdHOBCV{TJjwzm(ukE@H!lqgELL!`_N zMAmo7+3I-RcH3>fy=a86pwH$9cekwg^($5$tO%9jZYlHcB{#-p%9@Z%@zEKb5`C6t zLw%MBsnJ9hey>r7f(ZE7oygSv#oNwpvHrU8-yVI%U#)0J*l|@SY9kBNeLEX$fdi*9 zS-P?@i_(ed!nF9HaAf?~&!->Oxcz@$k2wha^LPib|BoNv z9JAHF(=3=hY!=nkpUYFwoh_+98)t=`U9plFl=wKmW-3v(aZ?35WP82NJS2Kul&eZS zwrj)XjP|m9?!BJ%;-TX0fRWcFa@qu<-3N!H)lrWuuU2V~)C9!DsAgvd1uHcj^MpaT zBXmtMWZiBk=U3|#mls-8-~>`1t)T0r*}IUFSIC=o$ALn7E8!K#YNxqO7kPnaVxN+Y z3KpyOGZ1~pwtcvOkf^flvZy+HYWRx|v0?BIVX2s^_alZY4Zk3gK5o9PmFH4bB^yv2 zz~yD1;W+Nutm=4+qaG=WSLINDgNej_QGWw;i>AM%KAzugmyJ7xsGGf`kmM!0wE66h zsG7UgCtap~r|ncqh#wo5M=OV&`!(WGjC#%Fq*(&C#-);u-yrwt1@decjl6VI88z%? zujq|+qTOv4VqE-Iq!mq;-%O>3m#77_hJ75tjf<|W@@nMVz2TRQ%GDz#dhU*GF=rx6 zM6NT>jiExHQ=52~GDq5?FmSJ=UAaO~U}l?YKt&MeF{#TFX&N%YxVG?k&&$quJUjcm zMk;$U)_mPVElyH5iQnh%_qLC$LmuA{CkE*-dTPT|qo9pbSN;y-k=wCx>7S|b$7?^# zG;p4B{$;WGacRnjo6+dq2Z1u5k!AEkQvd3Bff=yxTB@ge7jNj3TJ`Q5(obKVuZt9_ z?5YxSEL{WG<55~zu3hz}QLNhaujt;moR5V59N`B$mvr&@edSn5ee>Gpnv+-){)+}Ry*gd(R1bbWS-Y({g zz5RGXMY@+`G>hNTot-pmY9wb?!XRFcbctKjsva9Ecj}eSh^lm_8@e8~ig$ovxUP@bBsDOCratC5lLuUoG+$ zYwntG$kC}slF>Cv^rN1)N*jm{z05+mHv(DAkE4|=c7}!qi|0^!v(di!otse;y#_;> zlIrEML*|3|&>bUpM!WE)O;Kj+K2>Bl#E!6{#^6P;?)8~>n!e|Hf4Jo#11hxq$z80c zf4KwxXDt3Q{bFPOrXc-doau7GsBYexf8V?Z4<_TGV0~6iwwbHv>@govU)r4;m)6K} z)GixC?_R*AzGBAXj!dn!&qtS8;_lx6;&$?* z!esEJ$>dyzW+t^6EO5X2f=w#`6+ zcTp+70$C2K#8}GvI4u-6D%7x2o_f1OSdxw2Qv8rjRO-svz+~rET~v#bl=l-8&J@+F z!<6Le$*MCZ%dvDWq#hbEJb^@y2y=VnTwlQ_@GBp=OYXEVZbR!|f0Qw)$nXR@p`F9L zJ}DOMxE!PMmD=-d7X{Ba)}T8RS7Av@?$4&Y`+A$Jl(TS2e~srH^XN5&BKt?DxzD{ zLPAdT2L=t16}2@OERLvGB&)j9x>J%aBE6YMF1$O`Ej*uYcz+O^iuGP7okJyK+BVlu zyA#)^g{LzdSL@u&b!uzKM%)JDqSssR5k9Y=I^r@$vI=&`ZK}<(HLG)sWE+kxtYA6qXg4#YE}^4XRT1G`85bmG_6VH-j^45WszR}P;|{a z-)xtvN6licVPio$J8+eT=;enUIg;wnU$T4rY-# z?sAZbA}#o0KW3SvpizQSxqP|AJifi_r%%znpUm~yoBcoYJD;2+v!3`6KxhuZWZram zs=nKoMQG|Z7jOt`v#{8m{fJEL5+?G{J*a%5VoYF@bna^eZkg%f(deyRVeV^;jL-cb z+8n0+HYJo#<%f*Ny-(Sj^v8H~v`sgt)bG3WbG}`8<^L%_KmdE2&+}E*7N6q})|1Ot zaP7Bwm%e*>&mIahWx1<5K`{dA6P9nZKZ z@!K4drWbK}xi*@;L1Pni4@W7OCim!IYb1tfVe4g=lWs0;+Arl4Ko)d5H+_)3_oqty z(Zy{C*rnNeVOOAS40WsU_;A)KI$U;sl{)y>5%2WORHd_>$10Dt(n8M$9P1gx7&Zzo z5H0K@?#)s~-oN7TM7{Et5NU+tz4Tryq``Kn3Tx`q{b@V}VS2eri+;%FL|56v8)^X` z9jzWs5d+V5MPi0`ox!$7y+pUBlUZh~RzlE$a;fCZVImLSva4VBHqe}@=FQo@t4j=$ za!zHcCTf{JY^R;mA8!Bjd))A#kz}!#u4I@ySKzQHb4jzoq$!F>o=Wo)%i*zVWgYX} z*JxYAJn$g8F!1^)r(*8Wtd0cqmX)xMsC65vUBDr=?QR2tu^~DJsF&~1P@a|SX_Iw4 zm@|$7N7}^O)cf@He$djSIn&;z`Gky!mvmD!xYf(l_8+*3C)?r@1J{r7A{kSJaW->O zVB_q7ze;%)zkuC)a)lT7#rG0cxY=o^T83WaG}2eES8Yg0Z^H8RcRZAZFeFRSRK=Bj zoY~gzGCeWOZ?j*&Z%Be(q_3r5L^}tJaKssBI>N*sWx$ev&2ZJP&RIhn@WbT^j({0Q zOa@gyI+Gr+KI-sbrYwo@MZYGIkWH{db$4|n-bUeprp}QkqE-s>1kI5@k(-_$a z8#Ujp!Rc>L~<@tpDMplrcxUtVdpwLvg= zd2hjaQ6pr@-D^S5e)z?jfk704EKQD;bfEMpvK1QEv1hr z9HS`Eueb6I)5qIQ#2W7s>5p;sZ0LDw_&vVnn&4HchfLo$b;E`?pf&j*SmWRMbrAO@ z;qnRO^BuCkMu;C~A8?@+Ya_;$uWjzAr3^+7=@h12%ULu@^Wo?1S!%3hQ!Yhst(%!! z5rn&iccW)bv6$*{+XZY9W{63?msxLP^F`f!?rT(F063tAardzpDo!4LRg~9!MiX)6GR^hI#!Ith?ncH^wN#t z(4A3yB(5d6ub_0%VE9}1j`>QAaMNOBUO#aun)M#G%}eF$Nq4W#`FP2v;tVbF4rWC>#L%9k?U;iD~Bh%G);5Y$I5dMQ)tQW7d95%rMCm2IKr9Y1c5W{)Ay zs8?t^E%1dJFguMn-ZHkcAAVnOwV1irWt+ic-JqE9L1+3@4D7KxC5}H~DjVNxYgTCJ z{6k`jcFI}pMTrbXcYEV!WZ*3siLEb&qA zZ6v>d%Q3^c>!-5Od#BG<$dr(U)z1F!d!?n2TMOQIN?BpKl-tT9g2oZYXIt7fgA&Be zzz4fexfXEJ1!VD=WCS#e%%zHtjtCBa-D(evba66e`{C?TkG#m@u*R2E4{pt=E8l>C zszj<%K1Kbk#g7vbF>$a~zt=BKKbrEIx3T(;D5}U1!uiMAI*z6Bqs*#^1*(w!t-1mNbnY>Scb<$+3!}eNex`qaE zSi?J{N_nv}*1JN;qr~a+!Ny*sx3{axtudTvCci7t+_p)@Bxpc?vFb`%e>k@whxRsV zv|zUXdcR?RoJH>s@%Xsq_NL~pUMjP^Itse^-feSTRWJYambhd?(bf-hON{M{gzg*I z%wqe#%uzU%9hxOf7{Geq6*R-Tr9EBts(QGPg)5DqXXV_+tltpTNJ7WP-{u3RB z5;G4F4v2oVOk=tfN)b2>E#O+-;LiApT`Ygk|M2TtKW(tyt}}kGkMG5zNy;D^h^(;W z+d7v%UfK2Cz^|K_y2U;6e^jY{U_NI~SM1}`yLBo5;s%|ytKlzW#pI&Iqqxr4+Zk&< zpUas4uuNLv)~2cM+G68}Cx|aDq5b@bTvw|%cQVnVrTgH>1it2aWCmzwB6UgENW$+pZu#$Jx2O0xkRen?>g@^h$M zW9#`ZT`?hN*HLz2Pl682v9Lx0qtsRR4kQa?c=utM8o85;>U@{&s&6y!;uhDg*F%pT zwVoKAHXpygwsep!;h#1gFl#e7e0ee Root; + L1_1 -> Root; + L1_2 -> Root; + L1_3 -> Root; + + L2_0 -> L1_0; + L2_1 -> L1_0; + L2_2 -> L1_0; + L2_3 -> L1_0; + L2_4 -> L1_1; + L2_5 -> L1_1; + L2_6 -> L1_1; + L2_7 -> L1_1; + L2_8 -> L1_2; + L2_9 -> L1_2; + L2_10 -> L1_2; + L2_11 -> L1_2; + L2_12 -> L1_3; + L2_13 -> L1_3; + L2_14 -> L1_3; + L2_15 -> L1_3; + + // Connections: Leaves to internal nodes + Leaf_0 -> L2_0; + Leaf_1 -> L2_1; + Leaf_2 -> L2_2; + Leaf_3 -> L2_3; + Leaf_4 -> L2_4; + Leaf_5 -> L2_5; + Leaf_6 -> L2_6; + Leaf_7 -> L2_7; + Leaf_8 -> L2_8; + Leaf_9 -> L2_9; + Leaf_10 -> L2_10; + Leaf_11 -> L2_11; + Leaf_12 -> L2_12; + Leaf_13 -> L2_13; + Leaf_14 -> L2_14; + Leaf_15 -> L2_15; +} \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram2.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/merkle_diagrams/diagram2.png new file mode 100644 index 0000000000000000000000000000000000000000..2fd2d95d654aa80eb4fc517adefa14daf3556b52 GIT binary patch literal 63262 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 +eIcicleError msm(const S* scalars, const A* bases, int msm_size, const MSMConfig& config, P* results); +``` + +:::note +The API is template and can work with all ICICLE curves (if corresponding lib is linked), including G2 groups. +::: + +### Batched MSM + +The MSM supports batch mode - running multiple MSMs in parallel. It's always better to use the batch mode instead of running single msms in serial as long as there is enough memory available. We support running a batch of MSMs that share the same points as well as a batch of MSMs that use different points. + +Config fields `are_points_shared_in_batch` and `batch_size` are used to configure msm for batch mode. + +### G2 MSM + +for G2 MSM, use the [same msm api](#msm-function) with the G2 types. + +:::note +Supported curves have types for both G1 and G2. +::: + +### Precompution + +#### What It Does: + +- The function computes a set of additional points derived from the original base points. These precomputed points are stored and later reused during the MSM computation. +- Purpose: By precomputing and storing these points, the MSM operation can reduce the number of operations needed at runtime, which can significantly speed up the calculation. + +#### When to Use: + +- Memory vs. Speed Trade-off: Precomputation increases the memory footprint because additional points are stored, but it reduces the computational effort during MSM, making the process faster. +- Best for Repeated Use: It’s especially beneficial when the same set of base points is used multiple times in different MSM operations. + +```cpp +template +eIcicleError msm_precompute_bases(const A* input_bases, int bases_size, const MSMConfig& config, A* output_bases); +``` + +:::note +User is allocating the `output_bases` (on host or device memory) and later use it as bases when calling msm. +::: + +## Rust and Go bindings + +The Rust and Go bindings provide equivalent functionality for their respective environments. Refer to their documentation for details on usage. + +- [Golang](../golang-bindings/msm.md) +- [Rust](../rust-bindings/msm.md) + +## CUDA backend MSM +This section describes the CUDA msm implementation and how to customize it (optional). + +### Algorithm description + +We follow the bucket method algorithm. The GPU implementation consists of four phases: + +1. Preparation phase - The scalars are split into smaller scalars of `c` bits each. These are the bucket indices. The points are grouped according to their corresponding bucket index and the buckets are sorted by size. +2. Accumulation phase - Each bucket accumulates all of its points using a single thread. More than one thread is assigned to large buckets, in proportion to their size. A bucket is considered large if its size is above the large bucket threshold that is determined by the `large_bucket_factor` parameter. The large bucket threshold is the expected average bucket size times the `large_bucket_factor` parameter. +3. Buckets Reduction phase - bucket results are multiplied by their corresponding bucket number and each bucket module is reduced to a small number of final results. By default, this is done by an iterative algorithm which is highly parallel. Setting `is_big_triangle` to `true` will switch this phase to the running sum algorithm described in the above YouTube talk which is much less parallel. +4. Final accumulation phase - The final results from the last phase are accumulated using the double-and-add algorithm. + +## Configuring CUDA msm +Use `ConfigExtension` object to pass backend specific configuration. +CUDA specific msm configuration: + +```cpp +ConfigExtension ext; +ext.set("large_bucket_factor", 15); +// use the config-extension in the msm config for the backend to see. +msm_config.ext = &ext; +// call msm +msm(..., config,...); // msm backend is reading the config-extension +``` + +### Choosing optimal parameters + +`is_big_triangle` should be `false` in almost all cases. It might provide better results only for very small MSMs (smaller than 2^8) with a large batch (larger than 100) but this should be tested per scenario. +Large buckets exist in two cases: +1. When the scalar distribution isn't uniform. +2. When `c` does not divide the scalar bit-size. + +`large_bucket_factor` that is equal to 10 yields good results for most cases, but it's best to fine tune this parameter per `c` and per scalar distribution. +The two most important parameters for performance are `c` and the `precompute_factor`. They affect the number of EC additions as well as the memory size. When the points are not known in advance we cannot use precomputation. In this case the best `c` value is usually around $log_2(msmSize) - 4$. However, in most protocols the points are known in advanced and precomputation can be used unless limited by memory. Usually it's best to use maximum precomputation (such that we end up with only a single bucket module) combined we a `c` value around $log_2(msmSize) - 1$. + +## Memory usage estimation + +The main memory requirements of the MSM are the following: + +- Scalars - `sizeof(scalar_t) * msm_size * batch_size` +- Scalar indices - `~6 * sizeof(unsigned) * nof_bucket_modules * msm_size * batch_size` +- Points - `sizeof(affine_t) * msm_size * precomp_factor * batch_size` +- Buckets - `sizeof(projective_t) * nof_bucket_modules * 2^c * batch_size` + +where `nof_bucket_modules = ceil(ceil(bitsize / c) / precompute_factor)` + +During the MSM computation first the memory for scalars and scalar indices is allocated, then the indices are freed and points and buckets are allocated. This is why a good estimation for the required memory is the following formula: + +$max(scalars + scalarIndices, scalars + points + buckets)$ + +This gives a good approximation within 10% of the actual required memory for most cases. + +## Example parameters + +Here is a useful table showing optimal parameters for different MSMs. They are optimal for BLS12-377 curve when running on NVIDIA GeForce RTX 3090 Ti. This is the configuration used: + +Here are the parameters and the results for the different cases: + +| MSM size | Batch size | Precompute factor | c | Memory estimation (GB) | Actual memory (GB) | Single MSM time (ms) | +| -------- | ---------- | ----------------- | --- | ---------------------- | ------------------ | -------------------- | +| 10 | 1 | 1 | 9 | 0.00227 | 0.00277 | 9.2 | +| 10 | 1 | 23 | 11 | 0.00259 | 0.00272 | 1.76 | +| 10 | 1000 | 1 | 7 | 0.94 | 1.09 | 0.051 | +| 10 | 1000 | 23 | 11 | 2.59 | 2.74 | 0.025 | +| 15 | 1 | 1 | 11 | 0.011 | 0.019 | 9.9 | +| 15 | 1 | 16 | 16 | 0.061 | 0.065 | 2.4 | +| 15 | 100 | 1 | 11 | 1.91 | 1.92 | 0.84 | +| 15 | 100 | 19 | 14 | 6.32 | 6.61 | 0.56 | +| 18 | 1 | 1 | 14 | 0.128 | 0.128 | 14.4 | +| 18 | 1 | 15 | 17 | 0.40 | 0.42 | 5.9 | +| 22 | 1 | 1 | 17 | 1.64 | 1.65 | 68 | +| 22 | 1 | 13 | 21 | 5.67 | 5.94 | 54 | +| 24 | 1 | 1 | 18 | 6.58 | 6.61 | 232 | +| 24 | 1 | 7 | 21 | 12.4 | 13.4 | 199 | + +The optimal values can vary per GPU and per curve. It is best to try a few combinations until you get the best results for your specific case. diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/ntt.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/ntt.md new file mode 100644 index 0000000000..967cc0e6aa --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/ntt.md @@ -0,0 +1,322 @@ +# NTT - Number Theoretic Transform + +## Overview + +The Number Theoretic Transform (NTT) is a variant of the Fourier Transform used over finite fields, particularly those of integers modulo a prime number. NTT operates in a discrete domain and is used primarily in applications requiring modular arithmetic, such as cryptography and polynomial multiplication. + +NTT is defined similarly to the Discrete Fourier Transform (DFT), but instead of using complex roots of unity, it uses roots of unity within a finite field. The definition hinges on the properties of the finite field, specifically the existence of a primitive root of unity of order $N$ (where $N$ is typically a power of 2), and the modulo operation is performed with respect to a specific prime number that supports these roots. + +Formally, given a sequence of integers $a_0, a_1, ..., a_{N-1}$, the NTT of this sequence is another sequence of integers $A_0, A_1, ..., A_{N-1}$, computed as follows: + +$$ +A_k = \sum_{n=0}^{N-1} a_n \cdot \omega^{nk} \mod p +$$ + +where: + +- $N$ is the size of the input sequence and is a power of 2, +- $p$ is a prime number such that $p = kN + 1$ for some integer $k$, ensuring that $p$ supports the existence of $N$th roots of unity, +- $\omega$ is a primitive $N$th root of unity modulo $p$, meaning $\omega^N \equiv 1 \mod p$ and no smaller positive power of $\omega$ is congruent to 1 modulo $p$, +- $k$ ranges from 0 to $N-1$, and it indexes the output sequence. + +NTT is particularly useful because it enables efficient polynomial multiplication under modulo arithmetic, crucial for algorithms in cryptographic protocols and other areas requiring fast modular arithmetic operations. + +There exists also INTT which is the inverse operation of NTT. INTT can take as input an output sequence of integers from an NTT and reconstruct the original sequence. + +## C++ API + +### Ordering + +The `Ordering` enum defines how inputs and outputs are arranged for the NTT operation, offering flexibility in handling data according to different algorithmic needs or compatibility requirements. It primarily affects the sequencing of data points for the transform, which can influence both performance and the compatibility with certain algorithmic approaches. The available ordering options are: + +- **`kNN` (Natural-Natural):** Both inputs and outputs are in their natural order. This is the simplest form of ordering, where data is processed in the sequence it is given, without any rearrangement. + +- **`kNR` (Natural-Reversed):** Inputs are in natural order, while outputs are in bit-reversed order. This ordering is typically used in algorithms that benefit from having the output in a bit-reversed pattern. + +- **`kRN` (Reversed-Natural):** Inputs are in bit-reversed order, and outputs are in natural order. This is often used with the Cooley-Tukey FFT algorithm. + +- **`kRR` (Reversed-Reversed):** Both inputs and outputs are in bit-reversed order. + +- **`kNM` (Natural-Mixed):** Inputs are provided in their natural order, while outputs are arranged in a digit-reversed (mixed) order. This ordering is good for mixed radix NTT operations, where the mixed or digit-reversed ordering of outputs is a generalization of the bit-reversal pattern seen in simpler, radix-2 cases. + +- **`kMN` (Mixed-Natural):** Inputs are in a digit-reversed (mixed) order, while outputs are restored to their natural order. This ordering would primarily be used for mixed radix NTT + +Choosing an algorithm is heavily dependent on your use case. For example Cooley-Tukey will often use `kRN` and Gentleman-Sande often uses `kNR`. + +```cpp +enum class Ordering { + kNN, /**< Inputs and outputs are in natural-order. */ + kNR, /**< Inputs are in natural-order and outputs are in bit-reversed-order. */ + kRN, /**< Inputs are in bit-reversed-order and outputs are in natural-order. */ + kRR, /**< Inputs and outputs are in bit-reversed-order. */ + kNM, /**< Inputs are in natural-order and outputs are in digit-reversed-order. */ + kMN /**< Inputs are in digit-reversed-order and outputs are in natural-order. */ +}; +``` + +### `NTTConfig` Struct + +The `NTTConfig` struct configures the NTT operation. It allows customization of parameters like the batch size, column batch computation, order of inputs and outputs etc. + +```cpp + template + struct NTTConfig { + icicleStreamHandle stream; + S coset_gen; + int batch_size; + bool columns_batch; + Ordering ordering; + bool are_inputs_on_device; + bool are_outputs_on_device; + bool is_async; + ConfigExtension* ext = nullptr; + }; +``` + +#### Default configuration + +You can obtain a default `NTTConfig` using: +```cpp +template +static NTTConfig default_ntt_config() +{ + NTTConfig config = { + nullptr, // stream + S::one(), // coset_gen + 1, // batch_size + false, // columns_batch + Ordering::kNN, // ordering + false, // are_inputs_on_device + false, // are_outputs_on_device + false, // is_async + }; + return config; +} +``` + +### NTT domain +Before computing an NTT, it is mandatory to initialize the roots of unity domain for computing the NTT. + +:::note +NTT domain is constructed for a given size $2^N$ and can be used for any NTT of size smaller or equal to $2^N$. For example a domain of size 32 can be used to compute NTTs of size 2,4,8,16,32. +::: + +```cpp +template +eIcicleError ntt_init_domain(const S& primitive_root, const NTTInitDomainConfig& config); +``` + +:::note +Domain is constructed per device. When using multiple devices (e.g. GPUs), need to call it per device prior to calling ntt. +::: + +To retrieve a root of unity from the domain: +```cpp +template S get_root_of_unity(uint64_t max_size); +``` + +Finally, release the domain to free up device memory when not required: +```cpp +template eIcicleError ntt_release_domain(); +``` + +where + +```cpp +struct NTTInitDomainConfig { + icicleStreamHandle stream; /**< Stream for asynchronous execution. */ + bool is_async; /**< True if operation is asynchronous. Default value is false. */ + ConfigExtension* ext = nullptr; /**< Backend-specific extensions. */ +}; + +static NTTInitDomainConfig default_ntt_init_domain_config() +{ + NTTInitDomainConfig config = { + nullptr, // stream + false // is_async + }; + return config; +} +``` + +### `ntt` Function + +the `ntt` function computes the NTT operation: + +```cpp +template +eIcicleError ntt(const E* input, int size, NTTDir dir, const NTTConfig& config, E* output); + +// Where NTTDir specific whether it is a forward or inverse transform +enum class NTTDir { + kForward, /**< Perform forward NTT. */ + kInverse /**< Perform inverse NTT (iNTT). */ +}; +``` +### EC-NTT +[The ntt api](#ntt-function) works for ECNTT too, given correct types, for supported curves. + +### Batch NTT + +Batch NTT allows you to compute many NTTs with a single API call. Batch NTT can significantly reduce read/write times as well as computation overhead by executing multiple NTT operations in parallel. Batch mode may also offer better utilization of computational resources (memory and compute). + +To compute a batch, set the `batch_size` and `columns_batch` fields of the config struct. + +### Rust and Go bindings + +- [Golang](../golang-bindings/ntt.md) +- [Rust](../rust-bindings/ntt.md) + +### Example + +The following example demonstartes how to use ntt and how pass custom configurations to the CUDA backend. Details are discussed below. + +```cpp +#include "icicle/backend/ntt_config.h" + +// allocate and init input/output +int batch_size = /*...*/; +int log_ntt_size = /*...*/; +int ntt_size = 1 << log_ntt_size; +auto input = std::make_unique(batch_size * ntt_size); +auto output = std::make_unique(batch_size * ntt_size); +initialize_input(ntt_size, batch_size, input.get()); + +// Initialize NTT domain with fast twiddles (CUDA backend) +scalar_t basic_root = scalar_t::omega(log_ntt_size); +auto ntt_init_domain_cfg = default_ntt_init_domain_config(); +ConfigExtension backend_cfg_ext; +backend_cfg_ext.set(CudaBackendConfig::CUDA_NTT_FAST_TWIDDLES_MODE, true); +ntt_init_domain_cfg.ext = &backend_cfg_ext; +ntt_init_domain(basic_root, ntt_init_domain_cfg); + +// ntt configuration +NTTConfig config = default_ntt_config(); +ConfigExtension ntt_cfg_ext; +config.batch_size = batch_size; + +// Compute NTT with explicit selection of Mixed-Radix algorithm. +ntt_cfg_ext.set(CudaBackendConfig::CUDA_NTT_ALGORITHM, CudaBackendConfig::NttAlgorithm::MixedRadix); +config.ext = &ntt_cfg_ext; +ntt(input.get(), ntt_size, NTTDir::kForward, config, output.get()); +``` + +### CUDA backend NTT +This section describes the CUDA ntt implementation and how to use it. + +Our CUDA NTT implementation supports two algorithms `radix-2` and `mixed-radix`. + +### Radix 2 + +At its core, the Radix-2 NTT algorithm divides the problem into smaller sub-problems, leveraging the properties of "divide and conquer" to reduce the overall computational complexity. The algorithm operates on sequences whose lengths are powers of two. + +1. **Input Preparation:** + The input is a sequence of integers $a_0, a_1, \ldots, a_{N-1}, \text{ where } N$ is a power of two. + +2. **Recursive Decomposition:** + The algorithm recursively divides the input sequence into smaller sequences. At each step, it separates the sequence into even-indexed and odd-indexed elements, forming two subsequences that are then processed independently. + +3. **Butterfly Operations:** + The core computational element of the Radix-2 NTT is the "butterfly" operation, which combines pairs of elements from the sequences obtained in the decomposition step. + + Each butterfly operation involves multiplication by a "twiddle factor," which is a root of unity in the finite field, and addition or subtraction of the results, all performed modulo the prime modulus. + + $$ + X_k = (A_k + B_k \cdot W^k) \mod p + $$ + + $X_k$ - The output of the butterfly operation for the $k$-th element + + $A_k$ - an element from the even-indexed subset + + $B_k$ - an element from the odd-indexed subset + + $p$ - prime modulus + + $k$ - The index of the current operation within the butterfly or the transform stage + + The twiddle factors are precomputed to save runtime and improve performance. + +4. **Bit-Reversal Permutation:** + A final step involves rearranging the output sequence into the correct order. Due to the halving process in the decomposition steps, the elements of the transformed sequence are initially in a bit-reversed order. A bit-reversal permutation is applied to obtain the final sequence in natural order. + +### Mixed Radix + +The Mixed Radix NTT algorithm extends the concepts of the Radix-2 algorithm by allowing the decomposition of the input sequence based on various factors of its length. Specifically ICICLEs implementation splits the input into blocks of sizes 16, 32, or 64 compared to radix2 which is always splitting such that we end with NTT of size 2. This approach offers enhanced flexibility and efficiency, especially for input sizes that are composite numbers, by leveraging the "divide and conquer" strategy across multiple radices. + +The NTT blocks in Mixed Radix are implemented more efficiently based on winograd NTT but also optimized memory and register usage is better compared to Radix-2. + +Mixed Radix can reduce the number of stages required to compute for large inputs. + +1. **Input Preparation:** + The input to the Mixed Radix NTT is a sequence of integers $a_0, a_1, \ldots, a_{N-1}$, where $N$ is not strictly required to be a power of two. Instead, $N$ can be any composite number, ideally factorized into primes or powers of primes. + +2. **Factorization and Decomposition:** + Unlike the Radix-2 algorithm, which strictly divides the computational problem into halves, the Mixed Radix NTT algorithm implements a flexible decomposition approach which isn't limited to prime factorization. + + For example, an NTT of size 256 can be decomposed into two stages of $16 \times \text{NTT}_{16}$, leveraging a composite factorization strategy rather than decomposing into eight stages of $\text{NTT}_{2}$. This exemplifies the use of composite factors (in this case, $256 = 16 \times 16$) to apply smaller NTT transforms, optimizing computational efficiency by adapting the decomposition strategy to the specific structure of $N$. + +3. **Butterfly Operations with Multiple Radices:** + The Mixed Radix algorithm utilizes butterfly operations for various radix sizes. Each sub-transform involves specific butterfly operations characterized by multiplication with twiddle factors appropriate for the radix in question. + + The generalized butterfly operation for a radix-$r$ element can be expressed as: + + $$ + X_{k,r} = \sum_{j=0}^{r-1} (A_{j,k} \cdot W^{jk}) \mod p + $$ + + where: + + $X_{k,r}$ - is the output of the $radix-r$ butterfly operation for the $k-th$ set of inputs + + $A_{j,k}$ - represents the $j-th$ input element for the $k-th$ operation + + $W$ - is the twiddle factor + + $p$ - is the prime modulus + +4. **Recombination and Reordering:** + After applying the appropriate butterfly operations across all decomposition levels, the Mixed Radix algorithm recombines the results into a single output sequence. Due to the varied sizes of the sub-transforms, a more complex reordering process may be required compared to Radix-2. This involves digit-reversal permutations to ensure that the final output sequence is correctly ordered. + +### Which algorithm should I choose ? + +Both work only on inputs of power of 2 (e.g., 256, 512, 1024). + +Radix 2 is faster for small NTTs. A small NTT would be around logN = 16 and batch size 1. Radix 2 won't necessarily perform better for smaller `logn` with larger batches. + +Mixed radix on the other hand works better for larger NTTs with larger input sizes. + +Performance really depends on logn size, batch size, ordering, inverse, coset, coeff-field and which GPU you are using. + +For this reason we implemented our [heuristic auto-selection](https://github.com/ingonyama-zk/icicle/blob/main/icicle/src/ntt/ntt.cu#L573) which should choose the most efficient algorithm in most cases. + +We still recommend you benchmark for your specific use case if you think a different configuration would yield better results. + +To Explicitly choose the algorithm: + +```cpp +#include "icicle/backend/ntt_config.h" + +NTTConfig config = default_ntt_config(); +ConfigExtension ntt_cfg_ext; +ntt_cfg_ext.set(CudaBackendConfig::CUDA_NTT_ALGORITHM, CudaBackendConfig::NttAlgorithm::MixedRadix); +config.ext = &ntt_cfg_ext; +ntt(input.get(), ntt_size, NTTDir::kForward, config, output.get()); +``` + + +### Fast twiddles + +When using the Mixed-radix algorithm, it is recommended to initialize the domain in "fast-twiddles" mode. This is essentially allocating the domain using extra memory but enables faster ntt. +To do so simply, pass this flag to the CUDA backend. + +```cpp +#include "icicle/backend/ntt_config.h" + +scalar_t basic_root = scalar_t::omega(log_ntt_size); +auto ntt_init_domain_cfg = default_ntt_init_domain_config(); +ConfigExtension backend_cfg_ext; +backend_cfg_ext.set(CudaBackendConfig::CUDA_NTT_FAST_TWIDDLES_MODE, true); +ntt_init_domain_cfg.ext = &backend_cfg_ext; +ntt_init_domain(basic_root, ntt_init_domain_cfg); +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/overview.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/overview.md new file mode 100644 index 0000000000..6c28028617 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/overview.md @@ -0,0 +1,12 @@ +# ICICLE Compute APIs + +This section of the documentation is dedicated to the main APIs provided by ICICLE. We will cover the usage and internal details of our core primitives, such as Multi-Scalar Multiplication (MSM), Number Theoretic Transform (NTT), and various hashing algorithms. Each primitive has its own dedicated page with examples and explanations for C++, Rust, and Go. + +## Supported primitives + +- [MSM](./msm.md) +- [NTT](./ntt.md) +- [Vector Operations](./vec_ops.md) +- [Polynomials](../polynomials/overview.md) +- [Hash](./hash.md) +- [Merkle-tree commitment](./merkle.md) diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon.md new file mode 100644 index 0000000000..37ea7455a9 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon.md @@ -0,0 +1,218 @@ +# Poseidon + +TODO update for V3 + +[Poseidon](https://eprint.iacr.org/2019/458.pdf) is a popular hash in the ZK ecosystem primarily because its optimized to work over large prime fields, a common setting for ZK proofs, thereby minimizing the number of multiplicative operations required. + +Poseidon has also been specifically designed to be efficient when implemented within ZK circuits, Poseidon uses far less constraints compared to other hash functions like Keccak or SHA-256 in the context of ZK circuits. + +Poseidon has been used in many popular ZK protocols such as Filecoin and [Plonk](https://drive.google.com/file/d/1bZZvKMQHaZGA4L9eZhupQLyGINkkFG_b/view?usp=drive_open). + +Our implementation of Poseidon is implemented in accordance with the optimized [Filecoin version](https://spec.filecoin.io/algorithms/crypto/poseidon/). + +Lets understand how Poseidon works. + +## Initialization + +Poseidon starts with the initialization of its internal state, which is composed of the input elements and some pre-generated constants. An initial round constant is added to each element of the internal state. Adding the round constants ensures the state is properly mixed from the beginning. + +This is done to prevent collisions and to prevent certain cryptographic attacks by ensuring that the internal state is sufficiently mixed and unpredictable. + +![Poseidon initialization of internal state added with pre-generated round constants](https://github.com/ingonyama-zk/icicle/assets/122266060/52257f5d-6097-47c4-8f17-7b6449b9d162) + +## Applying full and partial rounds + +To generate a secure hash output, the algorithm goes through a series of "full rounds" and "partial rounds" as well as transformations between these sets of rounds in the following order: + +```First full rounds -> apply S-box and Round constants -> partial rounds -> Last full rounds -> Apply S-box``` + +### Full rounds + +![Full round iterations consisting of S box operations, adding round constants, and a Full MDS matrix multiplication](https://github.com/ingonyama-zk/icicle/assets/122266060/e4ce0e98-b90b-4261-b83e-3cd8cce069cb) + +**Uniform Application of S-box:** In full rounds, the S-box (a non-linear transformation) is applied uniformly to every element of the hash function's internal state. This ensures a high degree of mixing and diffusion, contributing to the hash function's security. The functions S-box involves raising each element of the state to a certain power denoted by `α` a member of the finite field defined by the prime `p`; `α` can be different depending on the implementation and user configuration. + +**Linear Transformation:** After applying the S-box, a linear transformation is performed on the state. This involves multiplying the state by a MDS (Maximum Distance Separable) Matrix. which further diffuses the transformations applied by the S-box across the entire state. + +**Addition of Round Constants:** Each element of the state is then modified by adding a unique round constant. These constants are different for each round and are precomputed as part of the hash function's initialization. The addition of round constants ensures that even minor changes to the input produce significant differences in the output. + +### Partial Rounds + +![Partial round iterations consisting of selective S box operation, adding a round constant and performing an MDS multiplication with a sparse matrix](https://github.com/ingonyama-zk/icicle/assets/122266060/e8c198b4-7aa4-4b4d-9ec4-604e39e07692) + +**Selective Application of S-Box:** Partial rounds apply the S-box transformation to only one element of the internal state per round, rather than to all elements. This selective application significantly reduces the computational complexity of the hash function without compromising its security. The choice of which element to apply the S-box to can follow a specific pattern or be fixed, depending on the design of the hash function. + +**Linear Transformation and Round Constants:** A linear transformation is performed and round constants are added. The linear transformation in partial rounds can be designed to be less computationally intensive (this is done by using a sparse matrix) than in full rounds, further optimizing the function's efficiency. + +The user of Poseidon can often choose how many partial or full rounds he wishes to apply; more full rounds will increase security but degrade performance. The choice and balance is highly dependent on the use case. + +## Using Poseidon + +ICICLE Poseidon is implemented for GPU and parallelization is performed for each element of the state rather than for each state. +What that means is we calculate multiple hash-sums over multiple pre-images in parallel, rather than going block by block over the input vector. + +So for Poseidon of arity 2 and input of size 1024 * 2, we would expect 1024 elements of output. Which means each block would be of size 2 and that would result in 1024 Poseidon hashes being performed. + +### Supported Bindings + +[`Go`](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/golang/curves/bn254/poseidon/poseidon.go) +[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon) + +### Constants + +Poseidon is extremely customizable and using different constants will produce different hashes, security levels and performance results. + +We support pre-calculated and optimized constants for each of the [supported curves](../libraries#supported-curves-and-operations).The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon/constants) and are labeled clearly per curve `_poseidon.h`. + +If you wish to generate your own constants you can use our python script which can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon/constants/generate_parameters.py). + +Prerequisites: + +- Install python 3 +- `pip install poseidon-hash` +- `pip install galois==0.3.7` +- `pip install numpy` + +You will then need to modify the following values before running the script. + +```python +# Modify these +arity = 11 # we support arity 2, 4, 8 and 11. +p = 0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 # bls12-381 +# p = 0x12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001 # bls12-377 +# p = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 # bn254 +# p = 0x1ae3a4617c510eac63b05c06ca1493b1a22d9f300f5138f1ef3622fba094800170b5d44300000008508c00000000001 # bw6-761 +prime_bit_len = 255 +field_bytes = 32 + +... + +# primitive_element = None +primitive_element = 7 # bls12-381 +# primitive_element = 22 # bls12-377 +# primitive_element = 5 # bn254 +# primitive_element = 15 # bw6-761 +``` + +### Rust API + +This is the most basic way to use the Poseidon API. + +```rust +let test_size = 1 << 10; +let arity = 2u32; +let ctx = get_default_device_context(); +let poseidon = Poseidon::load(arity, &ctx).unwrap(); +let config = HashConfig::default(); + +let inputs = vec![F::one(); test_size * arity as usize]; +let outputs = vec![F::zero(); test_size]; +let mut input_slice = HostOrDeviceSlice::on_host(inputs); +let mut output_slice = HostOrDeviceSlice::on_host(outputs); + +poseidon.hash_many::( + &mut input_slice, + &mut output_slice, + test_size as u32, + arity as u32, + 1, // Output length + &config, +) +.unwrap(); +``` + +The `HashConfig` can be modified, by default the inputs and outputs are set to be on `Host` for example. + +```rust +impl<'a> Default for HashConfig<'a> { + fn default() -> Self { + let ctx = get_default_device_context(); + Self { + ctx, + are_inputs_on_device: false, + are_outputs_on_device: false, + is_async: false, + } + } +} +``` + +In the example above `Poseidon::load(arity, &ctx).unwrap();` is used which will load the correct constants based on arity and curve. Its possible to [generate](#constants) your own constants and load them. + +```rust +let ctx = get_default_device_context(); +let custom_poseidon = Poseidon::new( + arity, // The arity of poseidon hash. The width will be equal to arity + 1 + alpha, // The S-box power + full_rounds_half, + partial_rounds, + round_constants, + mds_matrix, + non_sparse_matrix, + sparse_matrices, + domain_tag, + ctx, +) +.unwrap(); +``` + +## The Tree Builder + +The tree builder allows you to build Merkle trees using Poseidon. + +You can define both the tree's `height` and its `arity`. The tree `height` determines the number of layers in the tree, including the root and the leaf layer. The `arity` determines how many children each internal node can have. + +```rust +use icicle_bn254::tree::Bn254TreeBuilder; +use icicle_bn254::poseidon::Poseidon; + +let mut config = TreeBuilderConfig::default(); +let arity = 2; +config.arity = arity as u32; +let input_block_len = arity; +let leaves = vec![F::one(); (1 << height) * arity]; +let mut digests = vec![F::zero(); merkle_tree_digests_len((height + 1) as u32, arity as u32, 1)]; + +let leaves_slice = HostSlice::from_slice(&leaves); +let digests_slice = HostSlice::from_mut_slice(&mut digests); + +let ctx = device_context::DeviceContext::default(); +let hash = Poseidon::load(2, &ctx).unwrap(); + +let mut config = TreeBuilderConfig::default(); +config.keep_rows = 5; +Bn254TreeBuilder::build_merkle_tree( + leaves_slice, + digests_slice, + height, + input_block_len, + &hash, + &hash, + &config, +) +.unwrap(); +``` + +Similar to Poseidon, you can also configure the Tree Builder `TreeBuilderConfig::default()` + +- `keep_rows`: The number of rows which will be written to output, 0 will write all rows. +- `are_inputs_on_device`: Have the inputs been loaded to device memory ? +- `is_async`: Should the TreeBuilder run asynchronously? `False` will block the current CPU thread. `True` will require you call `cudaStreamSynchronize` or `cudaDeviceSynchronize` to retrieve the result. + +### Benchmarks + +We ran the Poseidon tree builder on: + +**CPU**: 12th Gen Intel(R) Core(TM) i9-12900K/ + +**GPU**: RTX 3090 Ti + +**Tree height**: 30 (2^29 elements) + +The benchmarks include copying data from and to the device. + +| Rows to keep parameter | Run time, Icicle | Supranational PC2 +| ----------- | ----------- | ----------- +| 10 | 9.4 seconds | 13.6 seconds +| 20 | 9.5 seconds | 13.6 seconds +| 29 | 13.7 seconds | 13.6 seconds diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md new file mode 100644 index 0000000000..09806fb7d4 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md @@ -0,0 +1,180 @@ +# Poseidon2 + +[Poseidon2](https://eprint.iacr.org/2023/323) is a recently released optimized version of Poseidon. The two versions differ in two crucial points. First, Poseidon is a sponge hash function, while Poseidon2 can be either a sponge or a compression function depending on the use case. Secondly, Poseidon2 is instantiated by new and more efficient linear layers with respect to Poseidon. These changes decrease the number of multiplications in the linear layer by up to 90% and the number of constraints in Plonk circuits by up to 70%. This makes Poseidon2 currently the fastest arithmetization-oriented hash function without lookups. Since the compression mode is efficient it is ideal for use in Merkle trees as well. + +An overview of the Poseidon2 hash is provided in the diagram below + +![alt text](/img/Poseidon2.png) + +## Description + +### Round constants + +* In the first full round and last full rounds Round constants are of the structure $[c_0,c_1,\ldots , c_{t-1}]$, where $c_i\in \mathbb{F}$ +* In the partial rounds the round constants is only added to first element $[\tilde{c}_0,0,0,\ldots, 0_{t-1}]$, where $\tilde{c_0}\in \mathbb{F}$ + +Poseidon2 is also extremely customizable and using different constants will produce different hashes, security levels and performance results. + +We support pre-calculated constants for each of the [supported curves](../libraries#supported-curves-and-operations). The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon2/constants) and are labeled clearly per curve `_poseidon2.h`. + +You can also use your own set of constants as shown [here](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-fields/icicle-babybear/src/poseidon2/mod.rs#L290) + +### S box + +Allowed values of $\alpha$ for a given prime is the smallest integer such that $gcd(\alpha,p-1)=1$ + +For ICICLE supported curves/fields + +* Mersene $\alpha = 5$ +* Babybear $\alpha=7$ +* Bls12-377 $\alpha =11$ +* Bls12-381 $\alpha=5$ +* BN254 $\alpha = 5$ +* Grumpkin $\alpha = 5$ +* Stark252 $\alpha=3$ +* Koalabear $\alpha=3$ + +### MDS matrix structure + +There are only two matrices: There is one type of matrix for full round and another for partial round. There are two cases available one for state size $t'=4\cdot t$ and another for $t=2,3$. + +#### $t=4\cdot t'$ where $t'$ is an integer + +**Full Matrix** $M_{full}$ (Referred in paper as $M_{\mathcal{E}}$). These are hard coded (same for all primes $p>2^{30}$) for any fixed state size $t=4\cdot t'$ where $t'$ is an integer. + +$$ +M_{4} = \begin{pmatrix} +5 & 7 & 1 & 3 \\ +4& 6 & 1 & 1 \\ +1 & 3 & 5 & 7\\ +1 & 1 & 4 & 6\\ +\end{pmatrix} +$$ + +As per the [paper](https://eprint.iacr.org/2023/323.pdf) this structure is always maintained and is always MDS for any prime $p>2^{30}$. + +eg for $t=8$ the matrix looks like +$$ +M_{full}^{8\times 8} = \begin{pmatrix} +2\cdot M_4 & M_4 \\ +M_4 & 2\cdot M_4 \\ +\end{pmatrix} +$$ + +**Partial Matrix** $M_{partial}$(referred in paper as $M_{\mathcal{I}}$) - There is only ONE partial matrix for all the partial rounds and has non zero diagonal entries along the diagonal and $1$ everywhere else. + +$$ +M_{Partial}^{t\times t} = \begin{pmatrix} +\mu_0 &1 & \ldots & 1 \\ +1 &\mu_1 & \ldots & 1 \\ +\vdots & \vdots & \ddots & \vdots \\ + 1 & 1 &\ldots & \mu_{t-1}\\ +\end{pmatrix} +$$ + +where $\mu_i \in \mathbb{F}$. In general this matrix is different for each prime since one has to find values that satisfy some inequalities in a field. However unlike Poseidon there is only one $M_{partial}$ for all partial rounds. + +### $t=2,3$ + +These are special state sizes. In all ICICLE supported curves/fields the matrices for $t=3$ are + +$$ +M_{full} = \begin{pmatrix} +2 & 1 & 1 \\ +1 & 2 & 1 \\ +1 & 1 & 2 \\ +\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} +2 & 1 & 1 \\ +1 & 2 & 1 \\ +1 & 1 & 3 \\ +\end{pmatrix} +$$ + +and the matrices for $t=2$ are + +$$ +M_{full} = \begin{pmatrix} +2 & 1 \\ +1 & 2 \\ +\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} +2 & 1 \\ +1 & 3 \\ +\end{pmatrix} +$$ + +## Supported Bindings + +[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon2) + +## Rust API + +This is the most basic way to use the Poseidon2 API. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code + +```rust +let test_size = 4; +let poseidon = Poseidon2::new::(test_size,None).unwrap(); +let config = HashConfig::default(); +let inputs = vec![F::one(); test_size]; +let input_slice = HostSlice::from_slice(&inputs); +//digest is a single element +let out_init:F = F::zero(); +let mut binding = [out_init]; +let out_init_slice = HostSlice::from_mut_slice(&mut binding); + +poseidon.hash(input_slice, &config, out_init_slice).unwrap(); +println!("computed digest: {:?} ",out_init_slice.as_slice().to_vec()[0]); +``` + +## Merkle Tree Builder + +You can use Poseidon2 in a Merkle tree builder. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code. + +```rust +pub fn compute_binary_tree( + mut test_vec: Vec, + leaf_size: u64, + hasher: Hasher, + compress: Hasher, + mut tree_config: MerkleTreeConfig, +) -> MerkleTree +{ + let tree_height: usize = test_vec.len().ilog2() as usize; + //just to be safe + tree_config.padding_policy = PaddingPolicy::ZeroPadding; + let layer_hashes: Vec<&Hasher> = std::iter::once(&hasher) + .chain(std::iter::repeat(&compress).take(tree_height)) + .collect(); + let vec_slice: &mut HostSlice = HostSlice::from_mut_slice(&mut test_vec[..]); + let merkle_tree: MerkleTree = MerkleTree::new(&layer_hashes, leaf_size, 0).unwrap(); + + let _ = merkle_tree + .build(vec_slice,&tree_config); + merkle_tree +} + +//poseidon2 supports t=2,3,4,8,12,16,20,24. In this example we build a binary tree with Poseidon2 t=2. +let poseidon_state_size = 2; +let leaf_size:u64 = 4;// each leaf is a 32 bit element 32/8 = 4 bytes + +let mut test_vec = vec![F::from_u32(random::()); 1024* (poseidon_state_size as usize)]; +println!("Generated random vector of size {:?}", 1024* (poseidon_state_size as usize)); +//to use later for merkle proof +let mut binding = test_vec.clone(); +let test_vec_slice = HostSlice::from_mut_slice(&mut binding); +//define hash and compression functions (You can use different hashes here) +//note:"None" does not work with generics, use F= Fm31, Fbabybear etc +let hasher :Hasher = Poseidon2::new::(poseidon_state_size.try_into().unwrap(),None).unwrap(); +let compress: Hasher = Poseidon2::new::((hasher.output_size()*2).try_into().unwrap(),None).unwrap(); +//tree config +let tree_config = MerkleTreeConfig::default(); +let merk_tree = compute_binary_tree(test_vec.clone(), leaf_size, hasher, compress,tree_config.clone()); +println!("computed Merkle root {:?}", merk_tree.get_root::().unwrap()); + +let random_test_index = rand::thread_rng().gen_range(0..1024*(poseidon_state_size as usize)); +print!("Generating proof for element {:?} at random test index {:?} ",test_vec[random_test_index], random_test_index); +let merkle_proof = merk_tree.get_proof::(test_vec_slice, random_test_index.try_into().unwrap(), false, &tree_config).unwrap(); + +//actually should construct verifier tree :) +assert!(merk_tree.verify(&merkle_proof).unwrap()); +println!("\n Merkle proof verified successfully!"); +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/program.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/program.md new file mode 100644 index 0000000000..85e5441883 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/program.md @@ -0,0 +1,88 @@ +# Programs + +## Overview + +Program is a class that let users define expressions on vector elements, and have ICICLE compile it for the backends for a fused implementation. This solves memory bottlenecks and also let users customize algorithms such as sumcheck. Program can create only element-wise lambda functions. + + +## C++ API + +### Symbol + +Symbol is the basic (template) class that allow users to define their own program. The lambda function the user define will operate on symbols. + +### Defining lambda function + +To define a custom lambda function the user will use Symbol: +```cpp +void lambda_multi_result(std::vector>& vars) +{ + const Symbol& A = vars[0]; + const Symbol& B = vars[1]; + const Symbol& C = vars[2]; + const Symbol& EQ = vars[3]; + vars[4] = EQ * (A * B - C) + scalar_t::from(9); + vars[5] = A * B - C.inverse(); + vars[6] = vars[5]; + vars[3] = 2 * (var[0] + var[1]) // all variables can be both inputs and outputs +} +``` + +Each symbol element at the vector argument `var` represent an input or an output. The type of the symbol (`scalar_t` in this example) will be the type of the inputs and outputs. In this example we created a lambda function with four inputs and three outputs. + +In this example there are four input variables and three three outputs. Inside the function the user can define custom expressions on them. + +Program support few pre-defined programs. The user can use those pre-defined programs without creating a lambda function, as will be explained in the next section. + +### Creating program + +To execute the lambda function we just created we need to create a program from it. +To create program from lambda function we can use the following constructor: + +```cpp +Program(std::function>&)> program_func, const int nof_parameters) +``` + +`program_func` is the lambda function (in the example above `lambda_multi_result`) and `nof_parameters` is the total number of parameter (inputs + outputs) for the lambda (seven in the above example). + +#### Pre-defined programs + +As mentioned before, there are few pre-defined programs the user can use without the need to create a lambda function first. The enum `PreDefinedPrograms` contains the pre-defined program. Using pre-defined function will lead to better performance compared to creating the equivalent lambda function. +To create a pre-defined program a different constructor is bing used: + +```cpp +Program(PreDefinedPrograms pre_def) +``` + +`pre_def` is the pre-defined program (from `PreDefinedPrograms`). + +##### PreDefinedPrograms + +```cpp +enum PreDefinedPrograms { + AB_MINUS_C = 0, + EQ_X_AB_MINUS_C +}; +``` + +`AB_MINUS_C` - the pre-defined program `AB - C` for the input vectors `A`, `B` and `C` + +`EQ_X_AB_MINUS_C` - the pre-defined program `EQ(AB - C)` for the input vectors `A`, `B`, `C` and `EQ` + + +### Executing program + +To execute the program the `execute_program` function from the vector operation API should be used. This operation is supported by the CPU and CUDA backends. + + +```cpp +template +eIcicleError +execute_program(std::vector& data, const Program& program, uint64_t size, const VecOpsConfig& config); +``` + +The `data` vector is a vector of pointers to the inputs and output vectors, `program` is the program to execute, `size` is the length of the vectors and `config` is the configuration of the operation. + +For the configuration the field `is_a_on_device` determined whethere the data (*inputs and outputs*) is on device or not. After the execution `data` will reside in the same place as it did before (i.e. the field `is_result_on_device` is irrelevant.) + +> **_NOTE:_** Using program for executing lambdas is recommended only while using the CUDA backend. Program's primary use is to let users to customize algorithms (such as sumcheck). diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/vec_ops.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/vec_ops.md new file mode 100644 index 0000000000..4725f7a490 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/primitives/vec_ops.md @@ -0,0 +1,229 @@ + +# Vector Operations API + +## Overview + +The Vector Operations API in Icicle provides a set of functions for performing element-wise and scalar-vector operations on vectors, matrix operations, and miscellaneous operations like bit-reversal and slicing. These operations can be performed on the host or device, with support for asynchronous execution. + +### VecOpsConfig + +The `VecOpsConfig` struct is a configuration object used to specify parameters for vector operations. + +#### Fields + +- **`stream: icicleStreamHandle`**: Specifies the CUDA stream for asynchronous execution. If `nullptr`, the default stream is used. +- **`is_a_on_device: bool`**: Indicates whether the first input vector (`a`) is already on the device. If `false`, the vector will be copied from the host to the device. +- **`is_b_on_device: bool`**: Indicates whether the second input vector (`b`) is already on the device. If `false`, the vector will be copied from the host to the device. This field is optional. +- **`is_result_on_device: bool`**: Indicates whether the result should be stored on the device. If `false`, the result will be transferred back to the host. +- **`is_async: bool`**: Specifies whether the vector operation should be performed asynchronously. When `true`, the operation will not block the CPU, allowing other operations to proceed concurrently. Asynchronous execution requires careful synchronization to ensure data integrity. +- **`batch_size: int`**: Number of vectors (or operations) to process in a batch. Each vector operation will be performed independently on each batch element. +- **`columns_batch: bool`**: True if the batched vectors are stored as columns in a 2D array (i.e., the vectors are strided in memory as columns of a matrix). If false, the batched vectors are stored contiguously in memory (e.g., as rows or in a flat array). +- **`ext: ConfigExtension*`**: Backend-specific extensions. + +#### Default Configuration + +```cpp +static VecOpsConfig default_vec_ops_config() { + VecOpsConfig config = { + nullptr, // stream + false, // is_a_on_device + false, // is_b_on_device + false, // is_result_on_device + false, // is_async + 1, // batch_size + false, // columns_batch + nullptr // ext + }; + return config; +} +``` + +### Element-wise Operations + +These functions perform element-wise operations on two input vectors a and b. If VecOpsConfig specifies a batch_size greater than one, the operations are performed on multiple pairs of vectors simultaneously, producing corresponding output vectors. + +#### `vector_add` + +Adds two vectors element-wise. + +```cpp +template +eIcicleError vector_add(const T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `vector_sub` + +Subtracts vector `b` from vector `a` element-wise. + +```cpp +template +eIcicleError vector_sub(const T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `vector_mul` + +Multiplies two vectors element-wise. + +```cpp +template +eIcicleError vector_mul(const T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `vector_div` + +Divides vector `a` by vector `b` element-wise. + +```cpp +template +eIcicleError vector_div(const T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `execute_program` + +Execute a user-defined lambda function with arbitrary number of input and output variables. + +```cpp +template +eIcicleError +execute_program(std::vector& data, const Program& program, uint64_t size, const VecOpsConfig& config); +``` + +`is_result_on_device` of VecOpsConfig is not used here. + +For more details see [program](./program.md). + +#### `vector_accumulate` + +Adds vector b to a, inplace. + +```cpp +template +eIcicleError vector_accumulate(T* vec_a, const T* vec_b, uint64_t size, const VecOpsConfig& config); +``` + +#### `convert_montogomery` + +Convert a vector of field elements to/from montgomery form. +```cpp +template +eIcicleError convert_montgomery(const T* input, uint64_t size, bool is_into, const VecOpsConfig& config, T* output); +``` + +### Reduction operations + +These functions perform reduction operations on vectors. If VecOpsConfig specifies a batch_size greater than one, the operations are performed on multiple vectors simultaneously, producing corresponding output values. The storage arrangement of batched vectors is determined by the columns_batch field in the VecOpsConfig. + +#### `vector_sum` + +Computes the sum of all elements in each vector in a batch. + +```cpp +template +eIcicleError vector_sum(const T* vec_a, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `vector_product` + +Computes the product of all elements in each vector in a batch. + +```cpp +template +eIcicleError vector_product(const T* vec_a, uint64_t size, const VecOpsConfig& config, T* output); +``` + +### Scalar-Vector Operations + +These functions apply a scalar operation to each element of a vector. If VecOpsConfig specifies a batch_size greater than one, the operations are performed on multiple vector-scalar pairs simultaneously, producing corresponding output vectors. + +#### `scalar_add_vec / scalar_sub_vec` + +Adds a scalar to each element of a vector. + +```cpp +template +eIcicleError scalar_add_vec(const T* scalar_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `scalar_sub_vec` + +Subtract each element of a vector from a scalar `scalar-vec`. + +```cpp +template +eIcicleError scalar_sub_vec(const T* scalar_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +#### `scalar_mul_vec` + +Multiplies each element of a vector by a scalar. + +```cpp +template +eIcicleError scalar_mul_vec(const T* scalar_a, const T* vec_b, uint64_t size, const VecOpsConfig& config, T* output); +``` + +### Matrix Operations + +These functions perform operations on matrices. If VecOpsConfig specifies a batch_size greater than one, the operations are performed on multiple matrices simultaneously, producing corresponding output matrices. + +#### `matrix_transpose` + +Transposes a matrix. + +```cpp +template +eIcicleError matrix_transpose(const T* mat_in, uint32_t nof_rows, uint32_t nof_cols, const VecOpsConfig& config, T* mat_out); +``` + +### Miscellaneous Operations + +#### `bit_reverse` + +Reorders the vector elements based on a bit-reversal pattern. If VecOpsConfig specifies a batch_size greater than one, the operation is performed on multiple vectors simultaneously. + +```cpp +template +eIcicleError bit_reverse(const T* vec_in, uint64_t size, const VecOpsConfig& config, T* vec_out); +``` + +#### `slice` + +Extracts a slice from a vector. If VecOpsConfig specifies a batch_size greater than one, the operation is performed on multiple vectors simultaneously, producing corresponding output vectors. + +```cpp +template +eIcicleError slice(const T* vec_in, uint64_t offset, uint64_t stride, uint64_t size_in, uint64_t size_out, const VecOpsConfig& config, T* vec_out); +``` + +#### `highest_non_zero_idx` + +Finds the highest non-zero index in a vector. If VecOpsConfig specifies a batch_size greater than one, the operation is performed on multiple vectors simultaneously. + +```cpp +template +eIcicleError highest_non_zero_idx(const T* vec_in, uint64_t size, const VecOpsConfig& config, int64_t* out_idx); +``` + +#### `polynomial_eval` + +Evaluates a polynomial at given domain points. If VecOpsConfig specifies a batch_size greater than one, the operation is performed on multiple vectors simultaneously. + +```cpp +template +eIcicleError polynomial_eval(const T* coeffs, uint64_t coeffs_size, const T* domain, uint64_t domain_size, const VecOpsConfig& config, T* evals /*OUT*/); +``` + +#### `polynomial_division` + +Divides two polynomials. If VecOpsConfig specifies a batch_size greater than one, the operation is performed on multiple vectors simultaneously. + +```cpp +template +eIcicleError polynomial_division(const T* numerator, int64_t numerator_deg, const T* denumerator, int64_t denumerator_deg, const VecOpsConfig& config, T* q_out /*OUT*/, uint64_t q_size, T* r_out /*OUT*/, uint64_t r_size); +``` + + +### Rust and Go bindings + +- [Golang](../golang-bindings/vec-ops.md) +- [Rust](../rust-bindings/vec-ops.md) diff --git a/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/cpp.md b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/cpp.md new file mode 100644 index 0000000000..cff576571b --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/cpp.md @@ -0,0 +1,315 @@ +# Icicle C++ Usage Guide + +## Overview + +This guide covers the usage of ICICLE's C++ API, including device management, memory operations, data transfer, synchronization, and compute APIs. + +## Device Management + +:::note +See all ICICLE runtime APIs in [runtime.h](https://github.com/ingonyama-zk/icicle/blob/main/icicle/include/icicle/runtime.h) +::: + +### Loading a Backend + +The backend can be loaded from a specific path or from an environment variable. This is essential for setting up the computing environment. + +```cpp +#include "icicle/runtime.h" +eIcicleError result = icicle_load_backend_from_env_or_default(); +// or load from custom install dir +eIcicleError result = icicle_load_backend("/path/to/backend/installdir", true); +``` + +### Setting and Getting Active Device + +You can set the active device for the current thread and retrieve it when needed: + +```cpp +icicle::Device device = {"CUDA", 0}; // or other +eIcicleError result = icicle_set_device(device); +// or query current (thread) device +eIcicleError result = icicle_get_active_device(device); +``` + +### Setting and Getting the Default Device + +You can set the default device for all threads: + +```cpp +icicle::Device device = {"CUDA", 0}; // or other +eIcicleError result = icicle_set_default_device(device); +``` + +:::caution + +Setting a default device should be done **once** from the main thread of the application. If another device or backend is needed for a specific thread [icicle_set_device](#setting-and-getting-active-device) should be used instead. + +::: + +### Querying Device Information + +Retrieve the number of available devices and check if a pointer is allocated on the host or on the active device: + +```cpp +int device_count; +eIcicleError result = icicle_get_device_count(device_count); + +bool is_host_memory; +eIcicleError result = icicle_is_host_memory(ptr); + +bool is_device_memory; +eIcicleError result = icicle_is_active_device_memory(ptr); +``` + +## Memory Management + +### Allocating and Freeing Memory + +Memory can be allocated and freed on the active device: + +```cpp +void* ptr; +eIcicleError result = icicle_malloc(&ptr, 1024); // Allocate 1024 bytes +eIcicleError result = icicle_free(ptr); // Free the allocated memory +``` + +### Asynchronous Memory Operations + +You can perform memory allocation and deallocation asynchronously using streams: + +```cpp +icicleStreamHandle stream; +eIcicleError err = icicle_create_stream(&stream); + +void* ptr; +err = icicle_malloc_async(&ptr, 1024, stream); +err = icicle_free_async(ptr, stream); +``` + +### Querying Available Memory + +Retrieve the total and available memory on the active device: + +```cpp +size_t total_memory, available_memory; +eIcicleError err = icicle_get_available_memory(total_memory, available_memory); +``` + +### Setting Memory Values + +Set memory to a specific value on the active device, synchronously or asynchronously: + +```cpp +eIcicleError err = icicle_memset(ptr, 0, 1024); // Set 1024 bytes to 0 +eIcicleError err = icicle_memset_async(ptr, 0, 1024, stream); +``` + +## Data Transfer + +### Copying Data + +Data can be copied between host and device, or between devices. The location of the memory is inferred from the pointers: + +```cpp +eIcicleError result = icicle_copy(dst, src, size); +eIcicleError result = icicle_copy_async(dst, src, size, stream); +``` + +### Explicit Data Transfers + +To avoid device-inference overhead, use explicit copy functions: + +```cpp +eIcicleError result = icicle_copy_to_host(host_dst, device_src, size); +eIcicleError result = icicle_copy_to_host_async(host_dst, device_src, size, stream); + +eIcicleError result = icicle_copy_to_device(device_dst, host_src, size); +eIcicleError result = icicle_copy_to_device_async(device_dst, host_src, size, stream); +``` + +## Stream Management + +### Creating and Destroying Streams + +Streams are used to manage asynchronous operations: + +```cpp +icicleStreamHandle stream; +eIcicleError result = icicle_create_stream(&stream); +eIcicleError result = icicle_destroy_stream(stream); +``` + +## Synchronization + +### Synchronizing Streams and Devices + +Ensure all previous operations on a stream or device are completed before proceeding: + +```cpp +eIcicleError result = icicle_stream_synchronize(stream); +eIcicleError result = icicle_device_synchronize(); +``` + +## Device Properties + +### Checking Device Availability + +Check if a device is available and retrieve a list of registered devices: + +```cpp +icicle::Device dev; +eIcicleError result = icicle_is_device_available(dev); +``` + +### Querying Device Properties + +Retrieve properties of the active device: + +```cpp +DeviceProperties properties; +eIcicleError result = icicle_get_device_properties(properties); + +/******************/ +// where DeviceProperties is +struct DeviceProperties { + bool using_host_memory; // Indicates if the device uses host memory + int num_memory_regions; // Number of memory regions available on the device + bool supports_pinned_memory; // Indicates if the device supports pinned memory + // Add more properties as needed +}; +``` + + +## Compute APIs + +### Multi-Scalar Multiplication (MSM) Example + +Icicle provides high-performance compute APIs such as the Multi-Scalar Multiplication (MSM) for cryptographic operations. Here's a simple example of how to use the MSM API. + +```cpp +#include +#include "icicle/runtime.h" +#include "icicle/api/bn254.h" + +using namespace bn254; + +int main() +{ + // Load installed backends + icicle_load_backend_from_env_or_default(); + + // trying to choose CUDA if available, or fallback to CPU otherwise (default device) + const bool is_cuda_device_available = (eIcicleError::SUCCESS == icicle_is_device_available("CUDA")); + if (is_cuda_device_available) { + Device device = {"CUDA", 0}; // GPU-0 + ICICLE_CHECK(icicle_set_device(device)); // ICICLE_CHECK asserts that the api call returns eIcicleError::SUCCESS + } // else we stay on CPU backend + + // Setup inputs + int msm_size = 1024; + auto scalars = std::make_unique(msm_size); + auto points = std::make_unique(msm_size); + projective_t result; + + // Generate random inputs + scalar_t::rand_host_many(scalars.get(), msm_size); + projective_t::rand_host_many(points.get(), msm_size); + + // (optional) copy scalars to device memory explicitly + scalar_t* scalars_d = nullptr; + auto err = icicle_malloc((void**)&scalars_d, sizeof(scalar_t) * msm_size); + // Note: need to test err and make sure no errors occurred + err = icicle_copy(scalars_d, scalars.get(), sizeof(scalar_t) * msm_size); + + // MSM configuration + MSMConfig config = default_msm_config(); + // tell icicle that the scalars are on device. Note that EC points and result are on host memory in this example. + config.are_scalars_on_device = true; + + // Execute the MSM kernel (on the current device) + eIcicleError result_code = msm(scalars_d, points.get(), msm_size, config, &result); + // OR call bn254_msm(scalars_d, points.get(), msm_size, config, &result); + + // Free the device memory + icicle_free(scalars_d); + + // Check for errors + if (result_code == eIcicleError::SUCCESS) { + std::cout << "MSM result: " << projective_t::to_affine(result) << std::endl; + } else { + std::cerr << "MSM computation failed with error: " << get_error_string(result_code) << std::endl; + } + + return 0; +} +``` + +### Polynomial Operations Example + +Here's another example demonstrating polynomial operations using Icicle: + +```cpp +#include +#include "icicle/runtime.h" +#include "icicle/polynomials/polynomials.h" +#include "icicle/api/bn254.h" + +using namespace bn254; + +// define bn254Poly to be a polynomial over the scalar field of bn254 +using bn254Poly = Polynomial; + +static bn254Poly randomize_polynomial(uint32_t size) +{ + auto coeff = std::make_unique(size); + for (int i = 0; i < size; i++) + coeff[i] = scalar_t::rand_host(); + return bn254Poly::from_rou_evaluations(coeff.get(), size); +} + +int main() +{ + // Load backend and set device + icicle_load_backend_from_env_or_default(); + + // trying to choose CUDA if available, or fallback to CPU otherwise (default device) + const bool is_cuda_device_available = (eIcicleError::SUCCESS == icicle_is_device_available("CUDA")); + if (is_cuda_device_available) { + Device device = {"CUDA", 0}; // GPU-0 + ICICLE_CHECK(icicle_set_device(device)); // ICICLE_CHECK asserts that the API call returns eIcicleError::SUCCESS + } // else we stay on CPU backend + + int poly_size = 1024; + + // build domain for ntt is required for some polynomial ops that rely on ntt + ntt_init_domain(scalar_t::omega(12), default_ntt_init_domain_config()); + + // randomize polynomials f(x),g(x) over the scalar field of bn254 + bn254Poly f = randomize_polynomial(poly_size); + bn254Poly g = randomize_polynomial(poly_size); + + // Perform polynomial multiplication + auto result = f * g; // Executes on the current device + + ICICLE_LOG_INFO << "Done"; + + return 0; +} +``` + +In this example, the polynomial multiplication is used to perform polynomial multiplication on CUDA or CPU, showcasing the flexibility and power of Icicle's compute APIs. + +## Error Handling + +### Checking for Errors + +Icicle APIs return an `eIcicleError` enumeration value. Always check the returned value to ensure that operations were successful. + +```cpp +if (result != eIcicleError::SUCCESS) { + // Handle error +} +``` + +This guide provides an overview of the essential APIs available in Icicle for C++. The provided examples should help you get started with integrating Icicle into your high-performance computing projects. diff --git a/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/general.md b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/general.md new file mode 100644 index 0000000000..0405cf35cf --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/general.md @@ -0,0 +1,113 @@ + +# Icicle Programmer's Guide + +## Compute APIs + +ICICLE offers a variety of compute APIs, including Number Theoretic Transforms (NTT), Multi Scalar Multiplication (MSM), vector operations, Elliptic Curve NTT (ECNTT), polynomials, and more. These APIs follow a consistent structure, making it straightforward to apply the same usage patterns across different operations. + +[Check out all details about compute APIs here](../primitives/overview.md). + +### Common Structure of Compute APIs + +Each compute API in Icicle typically involves the following components: + +- **Inputs and Outputs**: The data to be processed and the resulting output are passed to the API functions. These can reside either on the host (CPU) or on a device (GPU). + +- **Parameters**: Parameters such as the size of data to be processed are provided to control the computation. + +- **Configuration Struct**: A configuration struct is used to specify additional options for the computation. This struct has default values but can be customized as needed. + +The configuration struct allows users to modify settings such as: + +- Specifying whether inputs and outputs are on the host or device. +- Adjusting the data layout for specific optimizations. +- Setting batching parameters (batch_size and columns_batch) to perform operations on multiple data sets simultaneously. +- Passing custom options to the backend implementation through an extension mechanism, such as setting the number of CPU cores to use. + +### Example (C++) + +```cpp +#include "icicle/vec_ops.h" + +// Create config struct for vector add +VecOpsConfig config = default_vec_ops_config(); +// optionally modify the config struct here +config.batch_size = 4; // Process 4 vector operations in a batch +config.columns_batch = true; // Batched vectors are stored as columns + +// Call the API +eIcicleError err = vector_add(vec_a, vec_b, size, config, vec_res); +``` + +Where `VecOpsConfig` is defined in `icicle/vec_ops.h`: + +```cpp +struct VecOpsConfig { + icicleStreamHandle stream; /**< Stream for asynchronous execution. */ + bool is_a_on_device; /**< True if `a` is on the device, false if it is not. Default value: false. */ + bool is_b_on_device; /**< True if `b` is on the device, false if it is not. Default value: false. OPTIONAL. */ + bool is_result_on_device; /**< If true, the output is preserved on the device, otherwise on the host. Default value: false. */ + bool is_async; /**< Whether to run the vector operations asynchronously. */ + int batch_size; /**< Number of vector operations to process in a batch. Default value: 1. */ + bool columns_batch; /**< True if batched vectors are stored as columns; false if stored contiguously. Default value: false. */ + ConfigExtension* ext = nullptr; /**< Backend-specific extension. */ +}; +``` + +This pattern is consistent across most Icicle APIs, in C++/Rust/Go, providing flexibility while maintaining a familiar structure. For NTT, MSM, and other operations, include the corresponding header and call the template APIs. + +### Config struct extension + +In special cases, where an application wants to specify backend specific options, this is achieved with a config-extension struct. +For example the CPU backend has an option regarding how many threads to use for a vector addition looks as follows: +```cpp +#include "icicle/vec_ops.h" + +// Create config struct for vector add +VecOpsConfig config = default_vec_ops_config(); +ConfigExtension ext; +config.ext = &ext; +ext.set("n_threads", 8); // tell the CPU backend to use 8 threads +// Call the API +eIcicleError err = vector_add(vec_a, vec_b, size, config, vec_res); +``` + +:::note +This is not device-agnostic behavior, meaning such code is aware of the backend. +Having said that, it is not an error to pass options to a backend that is not aware of them. +::: + +## Device Abstraction + +ICICLE provides a device abstraction layer that allows you to interact with different compute devices such as CPUs and GPUs seamlessly. The device abstraction ensures that your code can work across multiple hardware platforms without modification. + +### Device Management + +- **Loading Backends**: Backends are loaded dynamically based on the environment configuration or a specified path. +- **Setting Active Device**: The active device for a thread can be set, allowing for targeted computation on a specific device. +- **Setting Default Device**: The default device for any thread without an active device can be set, removing the need to specify an alternative device on each thread. This is especially useful when running on a backend that is not the built-in CPU backend which is the default device to start. + +## Streams + +Streams in ICICLE allow for asynchronous execution and memory operations, enabling parallelism and non-blocking execution. Streams are associated with specific devices, and you can create, destroy, and synchronize streams to manage your workflow. + +:::note +For compute APIs, streams go into the `config.stream` field along with the `is_async=true` config flag. +::: + +### Memory Management + +Icicle provides functions for allocating, freeing, and managing memory across devices. Memory operations can be performed synchronously or asynchronously, depending on the use case. + +### Data Transfer + +Data transfer between the host and devices, or between different devices, is handled through a set of APIs that ensure efficient and error-checked operations. Asynchronous operations are supported to maximize performance. + +### Synchronization + +Synchronization ensures that all previous operations on a stream or device are completed. This is crucial when coordinating between multiple operations that depend on one another. + +## Additional Information + +- **Error Handling**: Icicle uses a specific error enumeration (`eIcicleError`) to handle and return error states across its APIs. +- **Device Properties**: You can query various properties of devices to tailor operations according to the hardware capabilities. diff --git a/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/go.md b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/go.md new file mode 100644 index 0000000000..03f7932c1b --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/go.md @@ -0,0 +1,309 @@ +# ICICLE Golang Usage Guide + +## Overview + +This guide covers the usage of ICICLE's Golang API, including device management, memory operations, data transfer, synchronization, and compute APIs. + +## Device Management + +:::note +See all ICICLE runtime APIs in [runtime.go](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/golang/runtime/runtime.go) +::: + +### Loading a Backend + +The backend can be loaded from a specific path or from an environment variable. This is essential for setting up the computing environment. + +```go +import "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + +result := runtime.LoadBackendFromEnvOrDefault() +// or load from custom install dir +result := runtime.LoadBackend("/path/to/backend/installdir", true) +``` + +### Setting and Getting Active Device + +You can set the active device for the current thread and retrieve it when needed: + +```go +device := runtime.CreateDevice("CUDA", 0) // or other +result := runtime.SetDevice(device) +// or query current (thread) device +activeDevice := runtime.GetActiveDevice() +``` + +### Setting and Getting the Default Device + +You can set the default device for all threads: + +```go +device := runtime.CreateDevice("CUDA", 0) // or other +defaultDevice := runtime.SetDefaultDevice(device); +``` + +:::caution + +Setting a default device should be done **once** from the main thread of the application. If another device or backend is needed for a specific thread [runtime.SetDevice](#setting-and-getting-active-device) should be used instead. + +::: + +### Querying Device Information + +Retrieve the number of available devices and check if a pointer is allocated on the host or on the active device: + +```go +numDevices := runtime.GetDeviceCount() + +var ptr unsafe.Pointer +isHostMemory = runtime.IsHostMemory(ptr) +isDeviceMemory = runtime.IsActiveDeviceMemory(ptr) +``` + +## Memory Management + +### Allocating and Freeing Memory + +Memory can be allocated and freed on the active device: + +```go +ptr, err := runtime.Malloc(1024) // Allocate 1024 bytes +err := runtime.Free(ptr) // Free the allocated memory +``` + +### Asynchronous Memory Operations + +You can perform memory allocation and deallocation asynchronously using streams: + +```go +stream, err := runtime.CreateStream() + +ptr, err := runtime.MallocAsync(1024, stream) +err = runtime.FreeAsync(ptr, stream) +``` + +### Querying Available Memory + +Retrieve the total and available memory on the active device: + +```go +size_t total_memory, available_memory; +availableMemory, err := runtime.GetAvailableMemory() +freeMemory := availableMemory.Free +totalMemory := availableMemory.Total +``` + +### Setting Memory Values + +Set memory to a specific value on the active device, synchronously or asynchronously: + +```go +err := runtime.Memset(ptr, 0, 1024) // Set 1024 bytes to 0 +err := runtime.MemsetAsync(ptr, 0, 1024, stream) +``` + +## Data Transfer + +### Explicit Data Transfers + +To avoid device-inference overhead, use explicit copy functions: + +```go +result := runtime.CopyToHost(host_dst, device_src, size) +result := runtime.CopyToHostAsync(host_dst, device_src, size, stream) +result := runtime.CopyToDevice(device_dst, host_src, size) +result := runtime.CopyToDeviceAsync(device_dst, host_src, size, stream) +``` + +## Stream Management + +### Creating and Destroying Streams + +Streams are used to manage asynchronous operations: + +```go +stream, err := runtime.CreateStream() +err = runtime.DestroyStream(stream) +``` + +## Synchronization + +### Synchronizing Streams and Devices + +Ensure all previous operations on a stream or device are completed before proceeding: + +```go +err := runtime.StreamSynchronize(stream) +err := runtime.DeviceSynchronize() +``` + +## Device Properties + +### Checking Device Availability + +Check if a device is available and retrieve a list of registered devices: + +```go +dev := runtime.CreateDevice("CPU", 0) +isCPUAvail := runtime.IsDeviceAvailable(dev) +``` + +### Querying Device Properties + +Retrieve properties of the active device: + +```go +properties, err := runtime.GetDeviceProperties(properties); + +/******************/ +// where DeviceProperties is +type DeviceProperties struct { + UsingHostMemory bool // Indicates if the device uses host memory + NumMemoryRegions int32 // Number of memory regions available on the device + SupportsPinnedMemory bool // Indicates if the device supports pinned memory +} +``` + +## Compute APIs + +### Multi-Scalar Multiplication (MSM) Example + +Icicle provides high-performance compute APIs such as the Multi-Scalar Multiplication (MSM) for cryptographic operations. Here's a simple example of how to use the MSM API. + +```go +package main + +import ( + "fmt" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" + bn254Msm "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" +) + +func main() { + + // Load installed backends + runtime.LoadBackendFromEnvOrDefault() + + // trying to choose CUDA if available, or fallback to CPU otherwise (default device) + deviceCuda := runtime.CreateDevice("CUDA", 0) // GPU-0 + if runtime.IsDeviceAvailable(&deviceCuda) { + runtime.SetDevice(&deviceCuda) + } // else we stay on CPU backend + + // Setup inputs + const size = 1 << 18 + + // Generate random inputs + scalars := bn254.GenerateScalars(size) + points := bn254.GenerateAffinePoints(size) + + // (optional) copy scalars to device memory explicitly + var scalarsDevice core.DeviceSlice + scalars.CopyToDevice(&scalarsDevice, true) + + // MSM configuration + cfgBn254 := core.GetDefaultMSMConfig() + + // allocate memory for the result + result := make(core.HostSlice[bn254.Projective], 1) + + // execute bn254 MSM on device + err := bn254Msm.Msm(scalarsDevice, points, &cfgBn254, result) + + // Check for errors + if err != runtime.Success { + errorString := fmt.Sprint( + "bn254 Msm failed: ", err) + panic(errorString) + } + + // free explicitly allocated device memory + scalarsDevice.Free() +} +``` + +### Polynomial Operations Example + +Here's another example demonstrating polynomial operations using Icicle: + +```go +package main + +import ( + "fmt" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear/ntt" + "github.com/ingonyama-zk/icicle/v3/wrappers/golang/fields/babybear/polynomial" +) + +func initBabybearDomain() runtime.EIcicleError { + cfgInitDomain := core.GetDefaultNTTInitDomainConfig() + rouIcicle := babybear.ScalarField{} + rouIcicle.FromUint32(1461624142) + return ntt.InitDomain(rouIcicle, cfgInitDomain) +} + +func init() { + // Load installed backends + runtime.LoadBackendFromEnvOrDefault() + + // trying to choose CUDA if available, or fallback to CPU otherwise (default device) + deviceCuda := runtime.CreateDevice("CUDA", 0) // GPU-0 + if runtime.IsDeviceAvailable(&deviceCuda) { + runtime.SetDevice(&deviceCuda) + } // else we stay on CPU backend + + // build domain for ntt is required for some polynomial ops that rely on ntt + err := initBabybearDomain() + if err != runtime.Success { + errorString := fmt.Sprint( + "Babybear Domain initialization failed: ", err) + panic(errorString) + } +} + +func main() { + + // Setup inputs + const polySize = 1 << 10 + + // randomize two polynomials over babybear field + var fBabybear polynomial.DensePolynomial + defer fBabybear.Delete() + var gBabybear polynomial.DensePolynomial + defer gBabybear.Delete() + fBabybear.CreateFromCoeffecitients(babybear.GenerateScalars(polySize)) + gBabybear.CreateFromCoeffecitients(babybear.GenerateScalars(polySize / 2)) + + // Perform polynomial multiplication + rBabybear := fBabybear.Multiply(&gBabybear) // Executes on the current device + defer rBabybear.Delete() + rDegree := rBabybear.Degree() + + fmt.Println("f Degree: ", fBabybear.Degree()) + fmt.Println("g Degree: ", gBabybear.Degree()) + fmt.Println("r Degree: ", rDegree) +} +``` + +In this example, the polynomial multiplication is used to perform polynomial multiplication on CUDA or CPU, showcasing the flexibility and power of Icicle's compute APIs. + +## Error Handling + +### Checking for Errors + +Icicle APIs return an `EIcicleError` enumeration value. Always check the returned value to ensure that operations were successful. + +```go +if result != runtime.SUCCESS { + // Handle error +} +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/rust.md b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/rust.md new file mode 100644 index 0000000000..55188cfe2b --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/programmers_guide/rust.md @@ -0,0 +1,262 @@ + +# Icicle Rust Usage Guide + +## Overview + +This guide covers the usage of ICICLE’s Rust API, including device management, memory operations, data transfer, synchronization, and compute APIs. + +## Build the Rust Application and Execute + +To successfully build and execute the Rust application using ICICLE, you need to define the ICICLE dependencies in your Cargo.toml file: + +```bash +[dependencies] +icicle-runtime = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +icicle-core = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +icicle-babybear = { git = "https://github.com/ingonyama-zk/icicle.git", branch="main" } +# add other ICICLE crates here as needed +``` + +Once the dependencies are defined, you can build and run your application using the following command: +```bash +cargo run --release +``` + +This will compile your Rust application with optimizations and execute it. + +:::note +The icicle-runtime crate is used to load backends, select a device, and interact with the device in an abstract way when managing memory, streams, and other resources, as explained in this guide. +::: + +## Device Management + +### Loading a Backend + +The backend can be loaded from a specific path or from an environment variable. This is essential for setting up the computing environment. + +```rust +use icicle_runtime::runtime; + +runtime::load_backend_from_env_or_default().unwrap(); +// or load from custom install dir +runtime::load_backend("/path/to/backend/installdir").unwrap(); +``` + +### Setting and Getting Active Device + +You can set the active device for the current thread and retrieve it when needed: + +```rust +use icicle_runtime::Device; + +let device = Device::new("CUDA", 0); // or other +icicle_runtime::set_device(&device).unwrap(); + +let active_device = icicle_runtime::get_active_device().unwrap(); +``` + +### Setting and Getting the Default Device + +You can set the default device for all threads: + +```caution +let device = Device::new("CUDA", 0); // or other +let default_device = icicle_runtime::set_default_device(device); +``` + +:::note + +Setting a default device should be done **once** from the main thread of the application. If another device or backend is needed for a specific thread [icicle_runtime::set_device](#setting-and-getting-active-device) should be used instead. + +::: + +### Querying Device Information + +Retrieve the number of available devices and check if a pointer is allocated on the host or on the active device: + +```rust +let device_count = icicle_runtime::get_device_count().unwrap(); +``` + +## Memory Management + +### Allocating and Freeing Memory + +Memory can be allocated on the active device using the `DeviceVec` API. This memory allocation is flexible, as it supports allocation on any device, including the CPU if the CPU backend is used. + +```rust +use icicle_runtime::memory::DeviceVec; + +// Allocate 1024 elements on the device +let mut device_memory: DeviceVec = DeviceVec::::device_malloc(1024).unwrap(); +``` + +The memory is released when the `DeviceVec` object is dropped. + +### Asynchronous Memory Operations + +Asynchronous memory operations can be performed using streams. This allows for non-blocking execution, with memory allocation and deallocation occurring asynchronously. +```rust +use icicle_runtime::stream::IcicleStream; +use icicle_runtime::memory::DeviceVec; + +let mut stream = IcicleStream::create().unwrap(); // mutability is for the destroy() method + +// Allocate 1024 elements asynchronously on the device +let mut device_memory: DeviceVec = DeviceVec::::device_malloc_async(1024, &stream).unwrap(); + +// dispatch additional copy, compute etc. ops to the stream + +// Synchronize the stream to ensure all operations are complete +stream.synchronize().unwrap(); +stream.destroy().unwrap(); // +``` + +:::note +Streams need be explicitly destroyed before being dropped. +::: + +### Querying Available Memory + +You can retrieve the total and available memory on the active device using the `get_available_memory` function. + +```rust +use icicle_runtime::memory::get_available_memory; + +// Retrieve total and available memory on the active device +let (total_memory, available_memory) = get_available_memory().unwrap(); + +println!("Total memory: {}", total_memory); +println!("Available memory: {}", available_memory); +``` + +This function returns a tuple containing the total memory and the currently available memory on the device. It is essential for managing and optimizing resource usage in your applications. + +## Data Transfer + +### Copying Data + +Data can be copied between the host and device, or between devices. The location of the memory is handled by the `HostOrDeviceSlice` and `DeviceSlice` traits: + +```rust +use icicle_runtime::memory::{DeviceVec, HostSlice}; + +// Copy data from host to device +let input = vec![1, 2, 3, 4]; +let mut d_mem = DeviceVec::::device_malloc(input.len()).unwrap(); +d_mem.copy_from_host(HostSlice::from_slice(&input)).unwrap(); +// OR +d_mem.copy_from_host_async(HostSlice::from_slice(&input, &stream)).unwrap(); + +// Copy data back from device to host +let mut output = vec![0; input.len()]; +d_mem.copy_to_host(HostSlice::from_mut_slice(&mut output)).unwrap(); +// OR +d_mem.copy_to_host_async(HostSlice::from_mut_slice(&mut output, &stream)).unwrap(); +``` +## Stream Management + +### Creating and Destroying Streams + +Streams in Icicle are used to manage asynchronous operations, ensuring that computations can run in parallel without blocking the CPU thread: + +```rust +use icicle_runtime::stream::IcicleStream; + +// Create a stream +let mut stream = IcicleStream::create().unwrap(); + +// Destroy the stream +stream.destroy().unwrap(); +``` + +## Synchronization + +### Synchronizing Streams and Devices + +Synchronization ensures that all previous operations on a stream or device are completed before moving on to the next task. This is crucial when coordinating between multiple dependent operations: + +```rust +use icicle_runtime::stream::IcicleStream; + +// Synchronize the stream +stream.synchronize().unwrap(); + +// Synchronize the device +icicle_runtime::device_synchronize().unwrap(); +``` +These functions ensure that your operations are properly ordered and completed before the program proceeds, which is critical in parallel computing environments. + +## Device Properties + +### Checking Device Availability + +Check if a specific device is available and retrieve a list of registered devices: +```rust +use icicle_runtime::Device; + +let cuda_device = Device::new("CUDA", 0); +if icicle_runtime::is_device_available(&cuda_device) { + println!("CUDA device is available."); +} else { + println!("CUDA device is not available."); +} + +let registered_devices = icicle_runtime::get_registered_devices().unwrap(); +println!("Registered devices: {:?}", registered_devices); +``` + +### Querying Device Properties + +Retrieve properties of the active device to understand its capabilities and configurations: + +```rust +use icicle_runtime::Device; + +let cuda_device = Device::new("CUDA", 0); +if icicle_runtime::is_device_available(&cuda_device) { + icicle_runtime::set_device(&cuda_device); + let device_props = icicle_runtime::get_device_properties().unwrap(); + println!("Device using host memory: {}", device_props.using_host_memory); +} +``` + +These functions allow you to query device capabilities and ensure that your application is running on the appropriate hardware. + +## Compute APIs + +### Multi-Scalar Multiplication (MSM) Example + +Icicle provides high-performance compute APIs such as Multi-Scalar Multiplication (MSM) for cryptographic operations. Here's a simple example of how to use the MSM API in Rust. + +```rust +// Using bls12-377 curve +use icicle_bls12_377::curve::{CurveCfg, G1Projective, ScalarCfg}; +use icicle_core::{curve::Curve, msm, msm::MSMConfig, traits::GenerateRandom}; +use icicle_runtime::{device::Device, memory::HostSlice}; + +fn main() { + // Load backend and set device + let _ = icicle_runtime::runtime::load_backend_from_env_or_default(); + let cuda_device = Device::new("CUDA", 0); + if icicle_runtime::is_device_available(&cuda_device) { + icicle_runtime::set_device(&cuda_device).unwrap(); + } + + let size = 1024; + + // Randomize inputs + let points = CurveCfg::generate_random_affine_points(size); + let scalars = ScalarCfg::generate_random(size); + + let mut msm_results = vec![G1Projective::zero(); 1]; + msm::msm( + HostSlice::from_slice(&scalars), + HostSlice::from_slice(&points), + &MSMConfig::default(), + HostSlice::from_mut_slice(&mut msm_results[..]), + ) + .unwrap(); + println!("MSM result = {:?}", msm_results); +} +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings.md new file mode 100644 index 0000000000..ac5915b7a1 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings.md @@ -0,0 +1,34 @@ +# Rust bindings + +Rust bindings allow you to use ICICLE as a rust library. + +`icicle-core` defines all interfaces, macros and common methods. + +`icicle-runtime` contains runtime APIs for memory management, stream management and more. + +`icicle-curves` / `icicle-fields` implement all interfaces and macros from icicle-core for each curve. For example icicle-bn254 implements curve bn254. Each curve has its own build script which will build the ICICLE libraries for that curve as part of the rust-toolchain build. + +## Using ICICLE Rust bindings in your project + +Simply add the following to your `Cargo.toml`. + +```toml +# GPU Icicle integration +icicle-runtime = { git = "https://github.com/ingonyama-zk/icicle.git" } +icicle-core = { git = "https://github.com/ingonyama-zk/icicle.git" } +icicle-bn254 = { git = "https://github.com/ingonyama-zk/icicle.git" } +``` + +`icicle-bn254` being the curve you wish to use and `icicle-core` and `icicle-runtime` contain ICICLE utilities and CUDA wrappers. + +If you wish to point to a specific ICICLE branch add `branch = ""` or `tag = ""` to the ICICLE dependency. For a specific commit add `rev = ""`. + +When you build your project ICICLE will be built as part of the build command. + +## How do the rust bindings work? + +The rust bindings are rust crates that wrap the ICICLE Core libraries (C++). Each crate can wrap one or more ICICLE core libraries. They are built too when building the crate. + +:::note +Since ICICLE V3, core libraries are shared-libraries. This means that they must be installed in a directory that can be found by the linker. In addition, installing an application that depends on ICICLE must make sure to install ICICLE or have it installed on the target machine. +::: diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ecntt.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ecntt.md new file mode 100644 index 0000000000..426324a93a --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ecntt.md @@ -0,0 +1,25 @@ +# ECNTT + +## ECNTT Method + +The `ecntt` function computes the Elliptic Curve Number Theoretic Transform (EC-NTT) or its inverse on a batch of points of a curve. + +```rust +pub fn ecntt( + input: &(impl HostOrDeviceSlice> + ?Sized), + dir: NTTDir, + cfg: &NTTConfig, + output: &mut (impl HostOrDeviceSlice> + ?Sized), +) -> Result<(), eIcicleError> +``` + +## Parameters + +- **`input`**: The input data as a slice of `Projective`. This represents points on a specific elliptic curve `C`. +- **`dir`**: The direction of the NTT. It can be `NTTDir::kForward` for forward NTT or `NTTDir::kInverse` for inverse NTT. +- **`cfg`**: The NTT configuration object of type `NTTConfig`. This object specifies parameters for the NTT computation, such as the batch size and algorithm to use. +- **`output`**: The output buffer to write the results into. This should be a slice of `Projective` with the same size as the input. + +## Return Value + +- **`Result<(), eIcicleError>`**: This function returns an `eIcicleError` which is a wrapper type that indicates success or failure of the NTT computation. On success, it contains `Ok(())`. diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/hash.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/hash.md new file mode 100644 index 0000000000..b57c76fe8a --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/hash.md @@ -0,0 +1,110 @@ +# ICICLE Hashing in Rust + +:::note +For a general overview of ICICLE's hashing logic and supported algorithms, check out the [ICICLE Hashing Overview](../primitives/hash.md). +::: + +## Overview + +The ICICLE library provides Rust bindings for hashing using a variety of cryptographic hash functions. These hash functions are optimized for both general-purpose data and cryptographic operations such as multi-scalar multiplication, commitment generation, and Merkle tree construction. + +This guide will show you how to use the ICICLE hashing API in Rust with examples for common hash algorithms, such as Keccak-256, Keccak-512, SHA3-256, SHA3-512, Blake2s, and Poseidon. + +## Importing Hash Functions + +To use the hashing functions in Rust, you will need to import the specific hash algorithm module from the ICICLE Rust bindings. For example: + +```rust +use icicle_hash::keccak::Keccak256; +use icicle_hash::keccak::Keccak512; +use icicle_hash::sha3::Sha3_256; +use icicle_hash::sha3::Sha3_512; +use icicle_hash::blake2s::Blake2s; +use icicle_core::poseidon::Poseidon; +``` + +## API Usage + +### 1. Creating a Hasher Instance + +Each hash algorithm can be instantiated by calling its respective constructor. The new function takes an optional default input size, which can be set to 0 unless required for a specific use case. + +Example for Keccak-256: + +```rust +let keccak_hasher = Keccak256::new(0 /* default input size */).unwrap(); +``` + +### 2. Hashing a Simple String + +Once you have created a hasher instance, you can hash any input data, such as strings or byte arrays, and store the result in an output buffer. +Here’s how to hash a simple string using Keccak-256: + +```rust +use icicle_hash::keccak::Keccak256; +use icicle_runtime::memory::HostSlice; +use icicle_core::hash::HashConfig; +use hex; + +let input_str = "I like ICICLE! it's so fast and easy"; +let mut output = vec![0u8; 32]; // 32-byte output buffer + +let keccak_hasher = Keccak256::new(0 /* default input size */).unwrap(); +keccak_hasher + .hash( + HostSlice::from_slice(input_str.as_bytes()), // Input data + &HashConfig::default(), // Default configuration + HostSlice::from_mut_slice(&mut output), // Output buffer + ) + .unwrap(); + +// convert the output to a hex string for easy readability +let output_as_hex_str = hex::encode(output); +println!("Hash(`{}`) = {:?}", input_str, &output_as_hex_str); + +``` + +### 3. Poseidon Example (field elements) and batch hashing + +The Poseidon hash is designed for cryptographic field elements and curves, making it ideal for use cases such as zero-knowledge proofs (ZKPs). +Poseidon hash using babybear field: + +```rust +use icicle_babybear::field::{ScalarCfg, ScalarField}; +use icicle_core::hash::HashConfig; +use icicle_core::poseidon::{Poseidon, PoseidonHasher}; +use icicle_core::traits::FieldImpl; +use icicle_runtime::memory::HostSlice; + +let batch = 1 << 10; // Number of hashes to compute in a single batch +let t = 3; // Poseidon parameter that specifies the arity (number of inputs) for each hash function +let mut outputs = vec![ScalarField::zero(); batch]; // Output array sized for the batch count + +// Case (1): Hashing without a domain tag +// Generates 'batch * t' random input elements as each hash needs 't' inputs +let inputs = ScalarCfg::generate_random(batch * t); +let poseidon_hasher = Poseidon::new::(t as u32, None /*=domain-tag*/).unwrap(); // Instantiate Poseidon without domain tag + +poseidon_hasher + .hash( + HostSlice::from_slice(&inputs), // Input slice for the hash function + &HashConfig::default(), // Default hashing configuration + HostSlice::from_mut_slice(&mut outputs), // Output slice to store hash results + ) + .unwrap(); + +// Case (2): Hashing with a domain tag +// Generates 'batch * (t - 1)' inputs, as domain tag counts as one input in each hash +let inputs = ScalarCfg::generate_random(batch * (t - 1)); +let domain_tag = ScalarField::zero(); // Example domain tag (can be any valid field element) +let poseidon_hasher_with_domain_tag = Poseidon::new::(t as u32, Some(&domain_tag) /*=domain-tag*/).unwrap(); + +poseidon_hasher_with_domain_tag + .hash( + HostSlice::from_slice(&inputs), // Input slice with 't - 1' elements per hash + &HashConfig::default(), // Default hashing configuration + HostSlice::from_mut_slice(&mut outputs), // Output slice to store hash results + ) + .unwrap(); +``` + diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/merkle.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/merkle.md new file mode 100644 index 0000000000..ea241feefb --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/merkle.md @@ -0,0 +1,277 @@ + +# Merkle Tree API Documentation (Rust) + +This is the Rust version of the **Merkle Tree API Documentation** ([C++ documentation](../primitives/merkle.md)). It mirrors the structure and functionality of the C++ version, providing equivalent APIs in Rust. +For more detailed explanations, refer to the [C++ documentation](../primitives/merkle.md). + +To see a complete implementation, visit the [Hash and Merkle example](https://github.com/ingonyama-zk/icicle/tree/main/examples/rust/hash-and-merkle) for a full example. + +## Tree Structure and Configuration in Rust + +### Defining a Merkle Tree + +```rust +struct MerkleTree{ + /// * `layer_hashers` - A vector of hash objects representing the hashers of each layer. + /// * `leaf_element_size` - Size of each leaf element. + /// * `output_store_min_layer` - Minimum layer at which the output is stored. + /// + /// # Returns a new `MerkleTree` instance or eIcicleError. + pub fn new( + layer_hashers: &[&Hasher], + leaf_element_size: u64, + output_store_min_layer: u64, + ) -> Result; +} +``` + +The `output_store_min_layer` parameter defines the lowest layer that will be stored in memory. Layers below this value will not be stored, saving memory at the cost of additional computation when proofs are generated. + + + +### Building the Tree + +The Merkle tree can be constructed from input data of any type, allowing flexibility in its usage. The size of the input must align with the tree structure defined by the hash layers and leaf size. If the input size does not match the expected size, padding may be applied. + +Refer to the [Padding Section](#padding) for more details on how mismatched input sizes are handled. + +```rust +struct MerkleTree{ + /// * `leaves` - A slice of leaves (input data). + /// * `config` - Configuration for the Merkle tree. + /// + /// # Returns a result indicating success or failure. + pub fn build( + &self, + leaves: &(impl HostOrDeviceSlice + ?Sized), + cfg: &MerkleTreeConfig, + ) -> Result<(), eIcicleError>; +} +``` + + + +## Tree Examples in Rust + +### Example A: Binary Tree + +A binary tree with **5 layers**, using **Keccak-256**: + +![Merkle Tree Diagram](../primitives/merkle_diagrams/diagram1.png) + +```rust +use icicle_core::{ + hash::{HashConfig, Hasher}, + merkle::{MerkleTree, MerkleTreeConfig}, +}; +use icicle_hash::keccak::Keccak256; +use icicle_runtime::memory::HostSlice; + +let leaf_size = 1024_u64; +let max_input_size = leaf_size as usize * 16; +let input: Vec = vec![0; max_input_size]; + +// define layer hashers +// we want one hash layer to hash every 1KB to 32B then compress every 64B so 4 more binary layers +let hash = Keccak256::new(leaf_size).unwrap(); +let compress = Keccak256::new(2 * hash.output_size()).unwrap(); +let _layer_hashers = vec![&hash, &compress, &compress, &compress, &compress]; +// or like that +let layer_hashers: Vec<&Hasher> = std::iter::once(&hash) + .chain(std::iter::repeat(&compress).take(4)) + .collect(); + +let merkle_tree = MerkleTree::new(&layer_hashers, leaf_size, 0 /*min layer to store */).unwrap(); + +// compute the tree +merkle_tree + .build(HostSlice::from_slice(&input), &MerkleTreeConfig::default()) + .unwrap(); +``` + + + +### Example B: Tree with Arity 4 + +![Merkle Tree Diagram](../primitives/merkle_diagrams/diagram2.png) + +This example uses **Blake2s** in upper layers: + +```rust +use icicle_hash::blake2s::Blake2s; + +// define layer hashers +// we want one hash layer to hash every 1KB to 32B then compress every 128B so only 2 more layers +let hash = Keccak256::new(leaf_size).unwrap(); +let compress = Blake2s::new(4 * hash.output_size()).unwrap(); +let layer_hashers = vec![&hash, &compress, &compress]; + +let merkle_tree = MerkleTree::new(&layer_hashers, leaf_size, 0 /*min layer to store */).unwrap(); + +merkle_tree + .build(HostSlice::from_slice(&input), &MerkleTreeConfig::default()) + .unwrap(); +``` + + + +## Padding + +When the input for **layer 0** is smaller than expected, ICICLE can apply **padding** to align the data. + +**Padding Schemes:** +1. **Zero padding:** Adds zeroes to the remaining space. +2. **Repeat last leaf:** The final leaf element is repeated to fill the remaining space. + +```rust +// pub enum PaddingPolicy { +// None, // No padding, assume input is correctly sized. +// ZeroPadding, // Pad the input with zeroes to fit the expected input size. +// LastValue, // Pad the input by repeating the last value. +// } + +use icicle_core::merkle::PaddingPolicy; +let mut config = MerkleTreeConfig::default(); +config.padding_policy = PaddingPolicy::ZeroPadding; +merkle_tree + .build(HostSlice::from_slice(&input), &config) + .unwrap(); +``` + + + +## Root as Commitment + +Retrieve the Merkle-root and serialize. + +```rust +struct MerkleTree{ + /// Retrieve the root of the Merkle tree. + /// + /// # Returns + /// A reference to the root hash. + pub fn get_root(&self) -> Result<&[T], eIcicleError>; +} + +let commitment: &[u8] = merkle_tree + .get_root() + .unwrap(); +println!("Commitment: {:?}", commitment);**** +``` + +:::warning +The commitment can be serialized to the proof. This is not handled by ICICLE. +::: + + + +## Generating Merkle Proofs + +Merkle proofs are used to **prove the integrity of opened leaves** in a Merkle tree. A proof ensures that a specific leaf belongs to the committed data by enabling the verifier to reconstruct the **root hash (commitment)**. + +A Merkle proof contains: + +- **Leaf**: The data being verified. +- **Index** (leaf_idx): The position of the leaf in the original dataset. +- **Path**: A sequence of sibling hashes (tree nodes) needed to recompute the path from the leaf to the root. + +![Merkle Pruned Phat Diagram](../primitives/merkle_diagrams/diagram1_path.png) + +```rust +struct MerkleTree{ + /// * `leaves` - A slice of leaves (input data). + /// * `leaf_idx` - Index of the leaf to generate a proof for. + /// * `pruned_path` - Whether the proof should be pruned. + /// * `config` - Configuration for the Merkle tree. + /// + /// # Returns a `MerkleProof` object or eIcicleError + pub fn get_proof( + &self, + leaves: &(impl HostOrDeviceSlice + ?Sized), + leaf_idx: u64, + pruned_path: bool, + config: &MerkleTreeConfig, + ) -> Result; +} +``` + +### Example: Generating a Proof + +Generating a proof for leaf idx 5: + +```rust +let merkle_proof = merkle_tree + .get_proof( + HostSlice::from_slice(&input), + 5, /*=leaf-idx*/ + true, /*pruned*/ + &MerkleTreeConfig::default(), + ) + .unwrap(); +``` + +:::warning +The Merkle-path can be serialized to the proof along with the leaf. This is not handled by ICICLE. +::: + + + +## Verifying Merkle Proofs + +```rust +struct MerkleTree{ + /// * `proof` - The Merkle proof to verify. + /// + /// # Returns a result indicating whether the proof is valid. + pub fn verify(&self, proof: &MerkleProof) -> Result; +} +``` + +### Example: Verifying a Proof + +```rust +let valid = merkle_tree + .verify(&merkle_proof) + .unwrap(); +assert!(valid); +``` + + + +## Pruned vs. Full Merkle-paths + +A **Merkle path** is a collection of **sibling hashes** that allows the verifier to **reconstruct the root hash** from a specific leaf. +This enables anyone with the **path and root** to verify that the **leaf** belongs to the committed dataset. +There are two types of paths that can be computed: + +- [**Pruned Path:**](#generating-merkle-proofs) Contains only necessary sibling hashes. +- **Full Path:** Contains all sibling nodes and intermediate hashes. + +![Merkle Full Path Diagram](../primitives//merkle_diagrams/diagram1_path_full.png) + +To compute a full path, specify `pruned=false`: + +```rust +let merkle_proof = merkle_tree + .get_proof( + HostSlice::from_slice(&input), + 5, /*=leaf-idx*/ + false, /*non-pruned is a full path --> note the pruned flag here*/ + &MerkleTreeConfig::default(), + ) + .unwrap(); +``` + + + +## Handling Partial Tree Storage + +In cases where the **Merkle tree is large**, only the **top layers** may be stored to conserve memory. +When opening leaves, the **first layers** (closest to the leaves) are **recomputed dynamically**. + +For example to avoid storing first layer we can define a tree as follows: + + +```rust +let mut merkle_tree = MerkleTree::new(&layer_hashers, leaf_size, 1 /*min layer to store*/); +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/msm.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/msm.md new file mode 100644 index 0000000000..e4f86444f2 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/msm.md @@ -0,0 +1,117 @@ +# MSM + +## MSM API Overview + +```rust +pub fn msm>( + scalars: &(impl HostOrDeviceSlice + ?Sized), + bases: &(impl HostOrDeviceSlice> + ?Sized), + cfg: &MSMConfig, + results: &mut (impl HostOrDeviceSlice> + ?Sized), +) -> Result<(), eIcicleError>; +``` + +### Parameters + +- **`scalars`**: A buffer containing the scalar values to be multiplied with corresponding points. +- **`points`**: A buffer containing the points to be multiplied by the scalars. +- **`cfg`**: MSM configuration specifying additional parameters for the operation. +- **`results`**: A buffer where the results of the MSM operations will be stored. + +### MSM Config + +```rust +pub struct MSMConfig { + pub stream_handle: IcicleStreamHandle, + pub precompute_factor: i32, + pub c: i32, + pub bitsize: i32, + batch_size: i32, + are_points_shared_in_batch: bool, + are_scalars_on_device: bool, + pub are_scalars_montgomery_form: bool, + are_points_on_device: bool, + pub are_points_montgomery_form: bool, + are_results_on_device: bool, + pub is_async: bool, + pub ext: ConfigExtension, +} +``` + +- **`stream_handle: IcicleStreamHandle`**: Specifies a stream for asynchronous execution. +- **`precompute_factor: i32`**: Determines the number of extra points to pre-compute for each point, affecting memory footprint and performance. +- **`c: i32`**: The "window bitsize," a parameter controlling the computational complexity and memory footprint of the MSM operation. +- **`bitsize: i32`**: The number of bits of the largest scalar, typically equal to the bit size of the scalar field. +- **`batch_size: i32`**: The number of MSMs to compute in a single batch, for leveraging parallelism. +- **`are_scalars_montgomery_form`**: Set to `true` if scalars are in montgomery form. +- **`are_points_montgomery_form`**: Set to `true` if points are in montgomery form. +- **`are_scalars_on_device: bool`**, **`are_points_on_device: bool`**, **`are_results_on_device: bool`**: Indicate whether the corresponding buffers are on the device memory. +- **`is_async: bool`**: Whether to perform the MSM operation asynchronously. +- **`ext: ConfigExtension`**: extended configuration for backend. + +### Usage + +The `msm` function is designed to compute the sum of multiple scalar-point multiplications efficiently. It supports both single MSM operations and batched operations for increased performance. The configuration allows for detailed control over the execution environment and performance characteristics of the MSM operation. + +When performing MSM operations, it's crucial to match the size of the `scalars` and `points` arrays correctly and ensure that the `results` buffer is appropriately sized to hold the output. The `MSMConfig` should be set up to reflect the specifics of the operation, including whether the operation should be asynchronous and any device-specific settings. + +## Example + +```rust +// Using bls12-377 curve +use icicle_bls12_377::curve::{CurveCfg, G1Projective, ScalarCfg}; +use icicle_core::{curve::Curve, msm, msm::MSMConfig, traits::GenerateRandom}; +use icicle_runtime::{device::Device, memory::HostSlice}; + +fn main() { + // Load backend and set device ... + + // Randomize inputs + let size = 1024; + let points = CurveCfg::generate_random_affine_points(size); + let scalars = ScalarCfg::generate_random(size); + + let mut msm_results = vec![G1Projective::zero(); 1]; + msm::msm( + HostSlice::from_slice(&scalars), + HostSlice::from_slice(&points), + &MSMConfig::default(), + HostSlice::from_mut_slice(&mut msm_results[..]), + ) + .unwrap(); + println!("MSM result = {:?}", msm_results); +} + +``` + +## Batched msm + +For batch msm, simply allocate the results array with size corresponding to batch size and set the `are_points_shared_in_batch` flag in config struct. + +## Precomputationg + +Precomputes bases for the multi-scalar multiplication (MSM) by extending each base point with its multiples, facilitating more efficient MSM calculations. + +```rust +/// Returns `Ok(())` if no errors occurred or a `eIcicleError` otherwise. +pub fn precompute_bases>( + points: &(impl HostOrDeviceSlice> + ?Sized), + config: &MSMConfig, + output_bases: &mut DeviceSlice>, +) -> Result<(), eIcicleError>; +``` + +### Parameters + +- **`points`**: The original set of affine points (\(P_1, P_2, ..., P_n\)) to be used in the MSM. For batch MSM operations, this should include all unique points concatenated together. +- **`msm_size`**: The size of a single msm in order to determine optimal parameters. +- **`cfg`**: The MSM configuration parameters. +- **`output_bases`**: The output buffer for the extended bases. Its size must be `points.len() * precompute_factor`. This buffer should be allocated on the device for GPU computations. + +#### Returns + +`Ok(())` if the operation is successful, or an `eIcicleError` error otherwise. + +## Parameters for optimal performance + +Please refer to the [primitive description](../primitives/msm#choosing-optimal-parameters) diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/multi-gpu.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/multi-gpu.md new file mode 100644 index 0000000000..527e780eef --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/multi-gpu.md @@ -0,0 +1,204 @@ +# Multi GPU APIs + +TODO update for V3 + +To learn more about the theory of Multi GPU programming refer to [this part](../multi-device.md) of documentation. + +Here we will cover the core multi GPU apis and a [example](#a-multi-gpu-example) + + +## A Multi GPU example + +In this example we will display how you can + +1. Fetch the number of devices installed on a machine +2. For every GPU launch a thread and set an active device per thread. +3. Execute a MSM on each GPU + + + +```rust + +... + +let device_count = get_device_count().unwrap(); + +(0..device_count) + .into_par_iter() + .for_each(move |device_id| { + set_device(device_id).unwrap(); + + // you can allocate points and scalars_d here + + let mut cfg = MSMConfig::default_for_device(device_id); + cfg.ctx.stream = &stream; + cfg.is_async = true; + cfg.are_scalars_montgomery_form = true; + msm(&scalars_d, &HostOrDeviceSlice::on_host(points), &cfg, &mut msm_results).unwrap(); + + // collect and process results + }) + +... +``` + + +We use `get_device_count` to fetch the number of connected devices, device IDs will be `0, 1, 2, ..., device_count - 1` + +[`into_par_iter`](https://docs.rs/rayon/latest/rayon/iter/trait.IntoParallelIterator.html#tymethod.into_par_iter) is a parallel iterator, you should expect it to launch a thread for every iteration. + +We then call `set_device(device_id).unwrap();` it should set the context of that thread to the selected `device_id`. + +Any data you now allocate from the context of this thread will be linked to the `device_id`. We create our `MSMConfig` with the selected device ID `let mut cfg = MSMConfig::default_for_device(device_id);`, behind the scene this will create for us a `DeviceContext` configured for that specific GPU. + +We finally call our `msm` method. + + +## Device management API + +To streamline device management we offer as part of `icicle-cuda-runtime` package methods for dealing with devices. + +#### [`set_device`](https://github.com/ingonyama-zk/icicle/blob/e6035698b5e54632f2c44e600391352ccc11cad4/wrappers/rust/icicle-cuda-runtime/src/device.rs#L6) + +Sets the current CUDA device by its ID, when calling `set_device` it will set the current thread to a CUDA device. + +**Parameters:** + +- **`device_id: usize`**: The ID of the device to set as the current device. Device IDs start from 0. + +**Returns:** + +- **`CudaResult<()>`**: An empty result indicating success if the device is set successfully. In case of failure, returns a `CudaError`. + +**Errors:** + +- Returns a `CudaError` if the specified device ID is invalid or if a CUDA-related error occurs during the operation. + +**Example:** + +```rust +let device_id = 0; // Device ID to set +match set_device(device_id) { + Ok(()) => println!("Device set successfully."), + Err(e) => eprintln!("Failed to set device: {:?}", e), +} +``` + +#### [`get_device_count`](https://github.com/ingonyama-zk/icicle/blob/e6035698b5e54632f2c44e600391352ccc11cad4/wrappers/rust/icicle-cuda-runtime/src/device.rs#L10) + +Retrieves the number of CUDA devices available on the machine. + +**Returns:** + +- **`CudaResult`**: The number of available CUDA devices. On success, contains the count of CUDA devices. On failure, returns a `CudaError`. + +**Errors:** + +- Returns a `CudaError` if a CUDA-related error occurs during the retrieval of the device count. + +**Example:** + +```rust +match get_device_count() { + Ok(count) => println!("Number of devices available: {}", count), + Err(e) => eprintln!("Failed to get device count: {:?}", e), +} +``` + +#### [`get_device`](https://github.com/ingonyama-zk/icicle/blob/e6035698b5e54632f2c44e600391352ccc11cad4/wrappers/rust/icicle-cuda-runtime/src/device.rs#L15) + +Retrieves the ID of the current CUDA device. + +**Returns:** + +- **`CudaResult`**: The ID of the current CUDA device. On success, contains the device ID. On failure, returns a `CudaError`. + +**Errors:** + +- Returns a `CudaError` if a CUDA-related error occurs during the retrieval of the current device ID. + +**Example:** + +```rust +match get_device() { + Ok(device_id) => println!("Current device ID: {}", device_id), + Err(e) => eprintln!("Failed to get current device: {:?}", e), +} +``` + +## Device context API + +The `DeviceContext` is embedded into `NTTConfig`, `MSMConfig` and `PoseidonConfig`, meaning you can simply pass a `device_id` to your existing config and the same computation will be triggered on a different device. + +#### [`DeviceContext`](https://github.com/ingonyama-zk/icicle/blob/e6035698b5e54632f2c44e600391352ccc11cad4/wrappers/rust/icicle-cuda-runtime/src/device_context.rs#L11) + +Represents the configuration a CUDA device, encapsulating the device's stream, ID, and memory pool. The default device is always `0`. + +```rust +pub struct DeviceContext<'a> { + pub stream: &'a CudaStream, + pub device_id: usize, + pub mempool: CudaMemPool, +} +``` + +##### Fields + +- **`stream: &'a CudaStream`** + + A reference to a `CudaStream`. This stream is used for executing CUDA operations. By default, it points to a null stream CUDA's default execution stream. + +- **`device_id: usize`** + + The index of the GPU currently in use. The default value is `0`, indicating the first GPU in the system. + + In some cases assuming `CUDA_VISIBLE_DEVICES` was configured, for example as `CUDA_VISIBLE_DEVICES=2,3,7` in the system with 8 GPUs - the `device_id=0` will correspond to GPU with id 2. So the mapping may not always be a direct reflection of the number of GPUs installed on a system. + +- **`mempool: CudaMemPool`** + + Represents the memory pool used for CUDA memory allocations. The default is set to a null pointer, which signifies the use of the default CUDA memory pool. + +##### Implementation Notes + +- The `DeviceContext` structure is cloneable and can be debugged, facilitating easier logging and duplication of contexts when needed. + + +#### [`DeviceContext::default_for_device(device_id: usize) -> DeviceContext<'static>`](https://github.com/ingonyama-zk/icicle/blob/e6035698b5e54632f2c44e600391352ccc11cad4/wrappers/rust/icicle-cuda-runtime/src/device_context.rs#L30) + +Provides a default `DeviceContext` with system-wide defaults, ideal for straightforward setups. + +#### Returns + +A `DeviceContext` instance configured with: +- The default stream (`null_mut()`). +- The default device ID (`0`). +- The default memory pool (`null_mut()`). + +#### Parameters + +- **`device_id: usize`**: The ID of the device for which to create the context. + +#### Returns + +A `DeviceContext` instance with the provided `device_id` and default settings for the stream and memory pool. + + +#### [`check_device(device_id: i32)`](https://github.com/vhnatyk/icicle/blob/eef6876b037a6b0797464e7cdcf9c1ecfcf41808/wrappers/rust/icicle-cuda-runtime/src/device_context.rs#L42) + +Validates that the specified `device_id` matches the ID of the currently active device, ensuring operations are targeted correctly. + +#### Parameters + +- **`device_id: i32`**: The device ID to verify against the currently active device. + +#### Behavior + +- **`Panics`** if the `device_id` does not match the active device's ID, preventing cross-device operation errors. + +#### Example + +```rust +let device_id: i32 = 0; // Example device ID +check_device(device_id); +// Ensures that the current context is correctly set for the specified device ID. +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ntt.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ntt.md new file mode 100644 index 0000000000..06758972ea --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/ntt.md @@ -0,0 +1,106 @@ +# NTT + +## NTT API overview + +```rust +pub fn ntt( + input: &(impl HostOrDeviceSlice + ?Sized), + dir: NTTDir, + cfg: &NTTConfig, + output: &mut (impl HostOrDeviceSlice + ?Sized), +) -> Result<(), eIcicleError>; + +pub fn ntt_inplace( + inout: &mut (impl HostOrDeviceSlice + ?Sized), + dir: NTTDir, + cfg: &NTTConfig, +) -> Result<(), eIcicleError> +``` + +- **`input`** - buffer to read the inputs of the NTT from. +- **`dir`** - whether to compute forward or inverse NTT. +- **`cfg`** - config used to specify extra arguments of the NTT. +- **`output`** - buffer to write the NTT outputs into. Must be of the same size as input. + +The `input` and `output` buffers can be on device or on host. Being on host means that they will be transferred to device during runtime. + +### NTT Config + +```rust +pub struct NTTConfig { + pub stream_handle: IcicleStreamHandle, + pub coset_gen: S, + pub batch_size: i32, + pub columns_batch: bool, + pub ordering: Ordering, + pub are_inputs_on_device: bool, + pub are_outputs_on_device: bool, + pub is_async: bool, + pub ext: ConfigExtension, +} +``` + +The `NTTConfig` struct is a configuration object used to specify parameters for an NTT instance. + +#### Fields + +- **`stream_handle: IcicleStreamHandle`**: Specifies the stream (queue) to use for async execution + +- **`coset_gen: S`**: Defines the coset generator used for coset (i)NTTs. By default, this is set to `S::one()`, indicating that no coset is being used. + +- **`batch_size: i32`**: Determines the number of NTTs to compute in a single batch. The default value is 1, meaning that operations are performed on individual inputs without batching. Batch processing can significantly improve performance by leveraging parallelism in GPU computations. + +- **`columns_batch`**: If true the function will compute the NTTs over the columns of the input matrix and not over the rows. Defaults to `false`. + +- **`ordering: Ordering`**: Controls the ordering of inputs and outputs for the NTT operation. This field can be used to specify decimation strategies (in time or in frequency) and the type of butterfly algorithm (Cooley-Tukey or Gentleman-Sande). The ordering is crucial for compatibility with various algorithmic approaches and can impact the efficiency of the NTT. + +- **`are_inputs_on_device: bool`**: Indicates whether the input data has been preloaded on the device memory. If `false` inputs will be copied from host to device. + +- **`are_outputs_on_device: bool`**: Indicates whether the output data is preloaded in device memory. If `false` outputs will be copied from host to device. If the inputs and outputs are the same pointer NTT will be computed in place. + +- **`is_async: bool`**: Specifies whether the NTT operation should be performed asynchronously. When set to `true`, the NTT function will not block the CPU, allowing other operations to proceed concurrently. Asynchronous execution requires careful synchronization to ensure data integrity and correctness. +- **`ext: ConfigExtension`**: extended configuration for backend. + +#### Example + +```rust +// Setting Bn254 points and scalars +println!("Generating random inputs on host for bn254..."); +let scalars = Bn254ScalarCfg::generate_random(size); +let mut ntt_results = DeviceVec::::device_malloc(size).unwrap(); + +// constructing NTT domain +initialize_domain( + ntt::get_root_of_unity::( + size.try_into() + .unwrap(), + ), + &ntt::NTTInitDomainConfig::default(), +) +.unwrap(); + +// Using default config +let cfg = ntt::NTTConfig::::default(); + +// Computing NTT +ntt::ntt( + HostSlice::from_slice(&scalars), + ntt::NTTDir::kForward, + &cfg, + &mut ntt_results[..], +) +.unwrap(); +``` + +### NTT Domain + +Before performing NTT operations, it is mandatory to construct the domain as [explained here](../primitives/ntt.md#ntt-domain). +In rust, we have the following functions to construct, destruct the domain and retrieve a root of unity from it: + +```rust +pub trait NTTDomain { + pub fn initialize_domain(primitive_root: F, config: &NTTInitDomainConfig) -> Result<(), eIcicleError>; + pub fn release_domain() -> Result<(), eIcicleError>; + pub fn get_root_of_unity(max_size: u64) -> F; +} +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/polynomials.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/polynomials.md new file mode 100644 index 0000000000..edb99c9171 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/polynomials.md @@ -0,0 +1,284 @@ +# Rust FFI Bindings for Univariate Polynomial + +:::note +Please refer to the Polynomials overview page for a deep overview. This section is a brief description of the Rust FFI bindings. +::: + +This documentation is designed to provide developers with a clear understanding of how to utilize the Rust bindings for polynomial operations efficiently and effectively, leveraging the robust capabilities of both Rust and C++ in their applications. + +## Introduction + +The Rust FFI bindings for the Univariate Polynomial serve as a "shallow wrapper" around the underlying C++ implementation. These bindings provide a straightforward Rust interface that directly calls functions from a C++ library, effectively bridging Rust and C++ operations. The Rust layer handles simple interface translations without delving into complex logic or data structures, which are managed on the C++ side. This design ensures efficient data handling, memory management, and execution of polynomial operations directly via C++. +Currently, these bindings are tailored specifically for polynomials where the coefficients, domain, and images are represented as scalar fields. + +## Initialization Requirements + +Before utilizing any functions from the polynomial API, it is mandatory to initialize the appropriate polynomial backend (e.g., CUDA). Additionally, the NTT (Number Theoretic Transform) domain must also be initialized, as the CUDA backend relies on this for certain operations. Failing to properly initialize these components can result in errors. + +:::note +**Field-Specific Initialization Requirement** + +The ICICLE library is structured such that each field or curve has its dedicated library implementation. As a result, initialization must be performed individually for each field or curve to ensure the correct setup and functionality of the library. +::: + +## Core Trait: `UnivariatePolynomial` + +The `UnivariatePolynomial` trait encapsulates the essential functionalities required for managing univariate polynomials in the Rust ecosystem. This trait standardizes the operations that can be performed on polynomials, regardless of the underlying implementation details. It allows for a unified approach to polynomial manipulation, providing a suite of methods that are fundamental to polynomial arithmetic. + +### Trait Definition + +```rust +pub trait UnivariatePolynomial +where + Self::Field: FieldImpl, + Self::FieldConfig: FieldConfig, +{ + type Field: FieldImpl; + type FieldConfig: FieldConfig; + + // Methods to create polynomials from coefficients or roots-of-unity evaluations. + fn from_coeffs + ?Sized>(coeffs: &S, size: usize) -> Self; + fn from_rou_evals + ?Sized>(evals: &S, size: usize) -> Self; + + // Method to divide this polynomial by another, returning quotient and remainder. + fn divide(&self, denominator: &Self) -> (Self, Self) where Self: Sized; + + // Method to divide this polynomial by the vanishing polynomial 'X^N-1'. + fn div_by_vanishing(&self, degree: u64) -> Self; + + // Methods to add or subtract a monomial in-place. + fn add_monomial_inplace(&mut self, monomial_coeff: &Self::Field, monomial: u64); + fn sub_monomial_inplace(&mut self, monomial_coeff: &Self::Field, monomial: u64); + + // Method to slice the polynomial, creating a sub-polynomial. + fn slice(&self, offset: u64, stride: u64, size: u64) -> Self; + + // Methods to return new polynomials containing only the even or odd terms. + fn even(&self) -> Self; + fn odd(&self) -> Self; + + // Method to evaluate the polynomial at a given domain point. + fn eval(&self, x: &Self::Field) -> Self::Field; + + // Method to evaluate the polynomial over a domain and store the results. + fn eval_on_domain + ?Sized, E: HostOrDeviceSlice + ?Sized>( + &self, + domain: &D, + evals: &mut E, + ); + + // Method to evaluate the polynomial over the roots-of-unity domain for power-of-two sized domain + fn eval_on_rou_domain + ?Sized>(&self, domain_log_size: u64, evals: &mut E); + + // Method to retrieve a coefficient at a specific index. + fn get_coeff(&self, idx: u64) -> Self::Field; + + // Method to copy coefficients into a provided slice. + fn copy_coeffs + ?Sized>(&self, start_idx: u64, coeffs: &mut S); + + // Method to get the degree of the polynomial. + fn degree(&self) -> i64; +} +``` + +## `DensePolynomial` Struct + +The DensePolynomial struct represents a dense univariate polynomial in Rust, leveraging a handle to manage its underlying memory within the CUDA device context. This struct acts as a high-level abstraction over complex C++ memory management practices, facilitating the integration of high-performance polynomial operations through Rust's Foreign Function Interface (FFI) bindings. + +```rust +pub struct DensePolynomial { + handle: PolynomialHandle, +} +``` + +### Traits implementation and methods + +#### `Drop` + +Ensures proper resource management by releasing the CUDA memory when a DensePolynomial instance goes out of scope. This prevents memory leaks and ensures that resources are cleaned up correctly, adhering to Rust's RAII (Resource Acquisition Is Initialization) principles. + +#### `Clone` + +Provides a way to create a new instance of a DensePolynomial with its own unique handle, thus duplicating the polynomial data in the CUDA context. Cloning is essential since the DensePolynomial manages external resources, which cannot be safely shared across instances without explicit duplication. + +#### Operator Overloading: `Add`, `Sub`, `Mul`, `Rem`, `Div` + +These traits are implemented for references to DensePolynomial (i.e., &DensePolynomial), enabling natural mathematical operations such as addition (+), subtraction (-), multiplication (*), division (/), and remainder (%). This syntactic convenience allows users to compose complex polynomial expressions in a way that is both readable and expressive. + +#### Key Methods + +In addition to the traits, the following methods are implemented: + +```rust +impl DensePolynomial { + // Returns a mutable slice of the polynomial coefficients on the device + pub fn coeffs_mut_slice(&mut self) -> &mut DeviceSlice {...} +} +``` + +## Flexible Memory Handling With `HostOrDeviceSlice` + +The DensePolynomial API is designed to accommodate a wide range of computational environments by supporting both host and device memory through the `HostOrDeviceSlice` trait. This approach ensures that polynomial operations can be seamlessly executed regardless of where the data resides, making the API highly adaptable and efficient for various hardware configurations. + +### Overview of `HostOrDeviceSlice` + +The HostOrDeviceSlice is a Rust trait that abstracts over slices of memory that can either be on the host (CPU) or the device (GPU), as managed by CUDA. This abstraction is crucial for high-performance computing scenarios where data might need to be moved between different memory spaces depending on the operations being performed and the specific hardware capabilities available. + +### Usage in API Functions + +Functions within the DensePolynomial API that deal with polynomial coefficients or evaluations use the HostOrDeviceSlice trait to accept inputs. This design allows the functions to be agnostic of the actual memory location of the data, whether it's in standard system RAM accessible by the CPU or in GPU memory accessible by CUDA cores. + +```rust +// Assume `coeffs` could either be in host memory or CUDA device memory +let coeffs: DeviceSlice = DeviceVec::::device_malloc(coeffs_len).unwrap(); +let p_from_coeffs = PolynomialBabyBear::from_coeffs(&coeffs, coeffs.len()); + +// Similarly for evaluations from roots of unity +let evals: HostSlice = HostSlice::from_slice(&host_memory_evals); +let p_from_evals = PolynomialBabyBear::from_rou_evals(&evals, evals.len()); + +// Same applies for any API that accepts HostOrDeviceSlice +``` + +## Usage + +This section outlines practical examples demonstrating how to utilize the `DensePolynomial` Rust API. The API is flexible, supporting multiple scalar fields. Below are examples showing how to use polynomials defined over different fields and perform a variety of operations. + +### Initialization and Basic Operations + +First, choose the appropriate field implementation for your polynomial operations, initializing the CUDA backend if necessary + +```rust +use icicle_babybear::polynomials::DensePolynomial as PolynomialBabyBear; + +let f = PolynomialBabyBear::from_coeffs(...); + +// now use f by calling the implemented traits + +// For operations over another field, such as BN254 +use icicle_bn254::polynomials::DensePolynomial as PolynomialBn254; +// Use PolynomialBn254 similarly +``` + +### Creation + +Polynomials can be created from coefficients or evaluations: + +```rust +let coeffs = ...; +let p_from_coeffs = PolynomialBabyBear::from_coeffs(HostSlice::from_slice(&coeffs), size); + +let evals = ...; +let p_from_evals = PolynomialBabyBear::from_rou_evals(HostSlice::from_slice(&evals), size); + +``` + +### Arithmetic Operations + +Utilize overloaded operators for intuitive mathematical expressions: + +```rust +let add = &f + &g; // Addition +let sub = &f - &g; // Subtraction +let mul = &f * &g; // Multiplication +let mul_scalar = &f * &scalar; // Scalar multiplication +``` + +### Division and Remainder + +Compute quotient and remainder or perform division by a vanishing polynomial: + +```rust +let (q, r) = f.divide(&g); // Compute both quotient and remainder +let q = &f / &g; // Quotient +let r = &f % &g; // Remainder + +let h = f.div_by_vanishing(N); // Division by V(x) = X^N - 1 + +``` + +### Monomial Operations + +Add or subtract monomials in-place for efficient polynomial manipulation: + +```rust +f.add_monomial_inplace(&three, 1 /*monmoial*/); // Adds 3*x to f +f.sub_monomial_inplace(&one, 0 /*monmoial*/); // Subtracts 1 from f +``` + +### Slicing + +Extract specific components: + +```rust +let even = f.even(); // Polynomial of even-indexed terms +let odd = f.odd(); // Polynomial of odd-indexed terms +let arbitrary_slice = f.slice(offset, stride, size); +``` + +### Evaluate + +Evaluate the polynoomial: + +```rust +let x = rand(); // Random field element +let f_x = f.eval(&x); // Evaluate f at x + +// Evaluate on a predefined domain +let domain = [one, two, three]; +let mut host_evals = vec![ScalarField::zero(); domain.len()]; +f.eval_on_domain(HostSlice::from_slice(&domain), HostSlice::from_mut_slice(&mut host_evals)); + +// Evaluate on roots-of-unity-domain +let domain_log_size = 4; +let mut device_evals = DeviceVec::::device_malloc(1 << domain_log_size).unwrap(); +f.eval_on_rou_domain(domain_log_size, &mut device_evals[..]); +``` + +### Read coefficients + +Read or copy polynomial coefficients for further processing: + +```rust +let x_squared_coeff = f.get_coeff(2); // Coefficient of x^2 + +// Copy coefficients to a device-specific memory space +let mut device_mem = DeviceVec::::device_malloc(coeffs.len()).unwrap(); +f.copy_coeffs(0, &mut device_mem[..]); +``` + +### Polynomial Degree + +Determine the highest power of the variable with a non-zero coefficient: + +```rust +let deg = f.degree(); // Degree of the polynomial +``` + +### Memory Management: Views (rust slices) + +Rust enforces correct usage of views at compile time, eliminating the need for runtime checks: + +```rust +let mut f = Poly::from_coeffs(HostSlice::from_slice(&coeffs), size); + +// Obtain a mutable slice of coefficients as a DeviceSlice +let coeffs_slice_dev = f.coeffs_mut_slice(); + +// Operations on f are restricted here due to mutable borrow of coeffs_slice_dev + +// Compute evaluations or perform other operations directly using the slice +// example: evaluate f on a coset of roots-of-unity. Computing from GPU to HOST/GPU +let mut config: NTTConfig<'_, F> = NTTConfig::default(); +config.coset_gen = /*some coset gen*/; +let mut coset_evals = vec![F::zero(); coeffs_slice_dev.len()]; +ntt( + coeffs_slice_dev, + NTTDir::kForward, + &config, + HostSlice::from_mut_slice(&mut coset_evals), +) +.unwrap(); + +// now can f can be borrowed once again +``` diff --git a/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/vec-ops.md b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/vec-ops.md new file mode 100644 index 0000000000..c42caafb50 --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/icicle/rust-bindings/vec-ops.md @@ -0,0 +1,109 @@ +# Vector Operations API + +Our vector operations API includes fundamental methods for addition, subtraction, and multiplication of vectors, with support for both host and device memory, as well as batched operations. + +## Vector Operations Configuration + +The `VecOpsConfig` struct encapsulates the settings for vector operations, including device context, operation modes, and batching parameters. + +### `VecOpsConfig` + +Defines configuration parameters for vector operations. + +```rust +pub struct VecOpsConfig { + pub stream_handle: IcicleStreamHandle, + pub is_a_on_device: bool, + pub is_b_on_device: bool, + pub is_result_on_device: bool, + pub is_async: bool, + pub batch_size: usize, + pub columns_batch: bool, + pub ext: ConfigExtension, +} +``` + +#### Fields + +- **`stream_handle: IcicleStreamHandle`**: Specifies the stream (queue) to use for async execution +- **`is_a_on_device: bool`**: Indicates whether the input data a has been preloaded on the device memory. If `false` inputs will be copied from host to device. +- **`is_b_on_device: bool`**: Indicates whether the input b data has been preloaded on the device memory. If `false` inputs will be copied from host to device. +- **`is_result_on_device: bool`**: Indicates whether the output data is preloaded in device memory. If `false` outputs will be copied from host to device. +- **`is_async: bool`**: Specifies whether the NTT operation should be performed asynchronously. +- **`batch_size: usize`**: Number of vector operations to process in a single batch. Each operation will be performed independently on each batch element. +- **`columns_batch: bool`**: true if the batched vectors are stored as columns in a 2D array (i.e., the vectors are strided in memory as columns of a matrix). If false, the batched vectors are stored contiguously in memory (e.g., as rows or in a flat array). + +- **`ext: ConfigExtension`**: extended configuration for backend. + +### Default Configuration + +`VecOpsConfig` can be initialized with default settings tailored for a specific device: + +```rust +let cfg = VecOpsConfig::default(); +``` + +## Vector Operations + +Vector operations are implemented through the `VecOps` trait, providing methods for addition, subtraction, and multiplication of vectors. These methods support both single and batched operations based on the batch_size and columns_batch configurations. + +### Methods + +All operations are element-wise operations, and the results placed into the `result` param. These operations are not in place, except for accumulate. + +- **`add`**: Computes the element-wise sum of two vectors. +- **`accumulate`**: Sum input b to a inplace. +- **`sub`**: Computes the element-wise difference between two vectors. +- **`mul`**: Performs element-wise multiplication of two vectors. +- **`transpose`**: Performs matrix transpose. +- **`bit_reverse/bit_reverse_inplace`**: Reverse order of elements based on bit-reverse. + + + +```rust +pub fn add_scalars( + a: &(impl HostOrDeviceSlice + ?Sized), + b: &(impl HostOrDeviceSlice + ?Sized), + result: &mut (impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; + +pub fn accumulate_scalars( + a: &mut (impl HostOrDeviceSlice + ?Sized), + b: &(impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; + +pub fn sub_scalars( + a: &(impl HostOrDeviceSlice + ?Sized), + b: &(impl HostOrDeviceSlice + ?Sized), + result: &mut (impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; + +pub fn mul_scalars( + a: &(impl HostOrDeviceSlice + ?Sized), + b: &(impl HostOrDeviceSlice + ?Sized), + result: &mut (impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; + +pub fn transpose_matrix( + input: &(impl HostOrDeviceSlice + ?Sized), + nof_rows: u32, + nof_cols: u32, + output: &mut (impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; + +pub fn bit_reverse( + input: &(impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, + output: &mut (impl HostOrDeviceSlice + ?Sized), +) -> Result<(), eIcicleError>; + +pub fn bit_reverse_inplace( + input: &mut (impl HostOrDeviceSlice + ?Sized), + cfg: &VecOpsConfig, +) -> Result<(), eIcicleError>; +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-3.4.0/introduction.md b/docs/versioned_docs/version-3.4.0/introduction.md new file mode 100644 index 0000000000..b173cf5f9e --- /dev/null +++ b/docs/versioned_docs/version-3.4.0/introduction.md @@ -0,0 +1,42 @@ +--- +slug: / +displayed_sidebar: GettingStartedSidebar +title: '' +--- + +# Welcome to Ingonyama's Developer Documentation + +Ingonyama is a next-generation semiconductor company focusing on Zero-Knowledge Proof hardware acceleration. We build accelerators for advanced cryptography, unlocking real-time applications. Our focus is on democratizing access to compute-intensive cryptography and making it accessible for developers to build upon. + +Our flagship product is **ICICLE** + +#### **ICICLE v3** +[ICICLE v3](https://github.com/ingonyama-zk/icicle) is a versatile cryptography library designed to support multiple compute backends, including CUDA, CPU, and potentially others like Metal, WebGPU, Vulkan, and ZPU. Originally focused on GPU acceleration, ICICLE has evolved to offer backend-agnostic cryptographic acceleration, allowing you to build ZK provers or other cryptographic applications with ease, leveraging the best available hardware for your needs. + +- **Multiple Backend Support:** Develop on CPU and deploy on various backends including CUDA and potentially Metal, WebGPU, Vulkan, ZPU, or even remote machines. +- **Cross-Language Compatibility:** Use ICICLE across multiple programming languages such as C++, Rust, Go, and possibly Python. +- **Optimized for ZKPs:** Accelerate cryptographic operations like elliptic curve operations, MSM, NTT, Poseidon hash, and more. + +**Learn more about ICICLE and its multi-backend support [here][ICICLE-OVERVIEW].** + +--- + +## Our Approach to Hardware Acceleration + +We believe that GPUs are as critical for ZK as they are for AI. + +- **Parallelism:** Approximately 97% of ZK protocol runtime is naturally parallel, making GPUs an ideal match. +- **Developer-Friendly:** GPUs offer simplicity in scaling and usage compared to other hardware platforms. +- **Cost-Effective:** GPUs provide a competitive balance of power, performance, and cost, often being 3x cheaper than FPGAs. + +For a more in-depth understanding on this topic we suggest you read [our article on the subject](https://www.ingonyama.com/blog/revisiting-paradigm-hardware-acceleration-for-zero-knowledge-proofs). + + +## Get in Touch + +If you have any questions, ideas, or are thinking of building something in this space, join the discussion on [Discord]. You can explore our code on [github](https://github.com/ingonyama-zk) or read some of [our research papers](https://github.com/ingonyama-zk/papers). + +Follow us on [Twitter](https://x.com/Ingo_zk) and [YouTube](https://www.youtube.com/@ingo_ZK) and join us IRL at our [next event](https://www.ingonyama.com/events) + +[ICICLE-OVERVIEW]: ./icicle/overview.md +[Discord]: https://discord.gg/6vYrE7waPj diff --git a/docs/versioned_sidebars/version-3.4.0-sidebars.json b/docs/versioned_sidebars/version-3.4.0-sidebars.json new file mode 100644 index 0000000000..71e548e363 --- /dev/null +++ b/docs/versioned_sidebars/version-3.4.0-sidebars.json @@ -0,0 +1,295 @@ +{ + "GettingStartedSidebar": [ + { + "type": "doc", + "label": "Introduction", + "id": "introduction" + }, + { + "type": "category", + "label": "ICICLE", + "link": { + "type": "doc", + "id": "icicle/overview" + }, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "Getting started", + "link": { + "type": "doc", + "id": "icicle/getting_started" + }, + "collapsed": false, + "items": [ + { + "type": "doc", + "label": "Build ICICLE from source", + "id": "icicle/build_from_source" + }, + { + "type": "category", + "label": "Programmers guide", + "link": { + "type": "doc", + "id": "icicle/programmers_guide/general" + }, + "collapsed": false, + "items": [ + { + "type": "doc", + "label": "C++", + "id": "icicle/programmers_guide/cpp" + }, + { + "type": "doc", + "label": "Rust", + "id": "icicle/programmers_guide/rust" + }, + { + "type": "doc", + "label": "Go", + "id": "icicle/programmers_guide/go" + } + ] + } + ] + }, + { + "type": "category", + "label": "Architecture overview", + "link": { + "type": "doc", + "id": "icicle/arch_overview" + }, + "collapsed": false, + "items": [ + { + "type": "doc", + "label": "CUDA Backend", + "id": "icicle/install_cuda_backend" + }, + { + "type": "doc", + "label": "Multi-Device Support", + "id": "icicle/multi-device" + } + ] + }, + { + "type": "doc", + "label": "ICICLE libraries", + "id": "icicle/libraries" + }, + { + "type": "category", + "label": "Compute API", + "link": { + "type": "doc", + "id": "icicle/primitives/overview" + }, + "collapsed": true, + "items": [ + { + "type": "doc", + "label": "MSM", + "id": "icicle/primitives/msm" + }, + { + "type": "doc", + "label": "NTT / ECNTT", + "id": "icicle/primitives/ntt" + }, + { + "type": "doc", + "label": "Vector operations", + "id": "icicle/primitives/vec_ops" + }, + { + "type": "doc", + "label": "Program", + "id": "icicle/primitives/program" + }, + { + "type": "doc", + "label": "Polynomials", + "id": "icicle/polynomials/overview" + }, + { + "type": "doc", + "label": "Hash", + "id": "icicle/primitives/hash" + }, + { + "type": "doc", + "label": "Poseidon2", + "id": "icicle/primitives/poseidon2" + }, + { + "type": "doc", + "label": "Merkle-Tree", + "id": "icicle/primitives/merkle" + }, + { + "type": "category", + "label": "Golang bindings", + "link": { + "type": "doc", + "id": "icicle/golang-bindings" + }, + "collapsed": true, + "items": [ + { + "type": "category", + "label": "MSM", + "link": { + "type": "doc", + "id": "icicle/golang-bindings/msm" + }, + "collapsed": true, + "items": [ + { + "type": "doc", + "label": "MSM pre computation", + "id": "icicle/golang-bindings/msm-pre-computation" + } + ] + }, + { + "type": "doc", + "label": "NTT", + "id": "icicle/golang-bindings/ntt" + }, + { + "type": "doc", + "label": "EC-NTT", + "id": "icicle/golang-bindings/ecntt" + }, + { + "type": "doc", + "label": "Vector operations", + "id": "icicle/golang-bindings/vec-ops" + }, + { + "type": "doc", + "label": "Multi GPU Support", + "id": "icicle/golang-bindings/multi-gpu" + }, + { + "type": "doc", + "label": "Hash", + "id": "icicle/golang-bindings/hash" + }, + { + "type": "doc", + "label": "Merkle-Tree", + "id": "icicle/golang-bindings/merkle" + } + ] + }, + { + "type": "category", + "label": "Rust bindings", + "link": { + "type": "doc", + "id": "icicle/rust-bindings" + }, + "collapsed": true, + "items": [ + { + "type": "doc", + "label": "MSM", + "id": "icicle/rust-bindings/msm" + }, + { + "type": "doc", + "label": "NTT", + "id": "icicle/rust-bindings/ntt" + }, + { + "type": "doc", + "label": "ECNTT", + "id": "icicle/rust-bindings/ecntt" + }, + { + "type": "doc", + "label": "Vector operations", + "id": "icicle/rust-bindings/vec-ops" + }, + { + "type": "doc", + "label": "Polynomials", + "id": "icicle/rust-bindings/polynomials" + }, + { + "type": "doc", + "label": "Hash", + "id": "icicle/rust-bindings/hash" + }, + { + "type": "doc", + "label": "Merkle-Tree", + "id": "icicle/rust-bindings/merkle" + } + ] + } + ] + }, + { + "type": "doc", + "label": "Migrate from ICICLE v2", + "id": "icicle/migrate_from_v2" + }, + { + "type": "doc", + "label": "Google Colab Instructions", + "id": "icicle/colab-instructions" + }, + { + "type": "doc", + "label": "ICICLE Provers", + "id": "icicle/integrations" + } + ] + }, + { + "type": "doc", + "label": "Ingonyama Grant program", + "id": "grants" + }, + { + "type": "doc", + "label": "Contributor guide", + "id": "contributor-guide" + }, + { + "type": "category", + "label": "Additional Resources", + "collapsed": false, + "collapsible": false, + "items": [ + { + "type": "link", + "label": "YouTube", + "href": "https://www.youtube.com/@ingo_ZK" + }, + { + "type": "link", + "label": "Ingonyama Blog", + "href": "https://www.ingonyama.com/blog" + }, + { + "type": "link", + "label": "Ingopedia", + "href": "https://www.ingonyama.com/ingopedia" + }, + { + "href": "https://github.com/ingonyama-zk", + "type": "link", + "label": "GitHub" + } + ] + } + ] +} diff --git a/docs/versions.json b/docs/versions.json index 77b69690f7..b351c7b0db 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,4 +1,5 @@ [ + "3.4.0", "3.3.0", "3.2.0", "3.1.0", From 8a026044660ef847e6a4312a1a5a5ef4b4f38739 Mon Sep 17 00:00:00 2001 From: ShaniBabayoff Date: Tue, 14 Jan 2025 17:27:19 +0200 Subject: [PATCH 067/127] Update sidebars.ts (#729) delete poseidon2 sidebar --- docs/sidebars.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/sidebars.ts b/docs/sidebars.ts index dcc9b77c0b..a47a911b0a 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -81,11 +81,6 @@ const cppApi = [ label: "Hash", id: "icicle/primitives/hash", }, - { - type: "doc", - label: "Poseidon2", - id: "icicle/primitives/poseidon2", - }, { type: "doc", label: "Merkle-Tree", From 752dc930154067de1919c3839431d08511e708d3 Mon Sep 17 00:00:00 2001 From: ShaniBabayoff Date: Tue, 14 Jan 2025 19:16:55 +0200 Subject: [PATCH 068/127] Update documentation for v3.4 (#738) Co-authored-by: Leon Hibnik --- docs/docs/icicle/primitives/poseidon2.md | 180 ------------------ docs/docusaurus.config.ts | 9 - .../icicle/primitives/poseidon2.md | 180 ------------------ .../version-3.4.0-sidebars.json | 5 - 4 files changed, 374 deletions(-) delete mode 100644 docs/docs/icicle/primitives/poseidon2.md delete mode 100644 docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md diff --git a/docs/docs/icicle/primitives/poseidon2.md b/docs/docs/icicle/primitives/poseidon2.md deleted file mode 100644 index 09806fb7d4..0000000000 --- a/docs/docs/icicle/primitives/poseidon2.md +++ /dev/null @@ -1,180 +0,0 @@ -# Poseidon2 - -[Poseidon2](https://eprint.iacr.org/2023/323) is a recently released optimized version of Poseidon. The two versions differ in two crucial points. First, Poseidon is a sponge hash function, while Poseidon2 can be either a sponge or a compression function depending on the use case. Secondly, Poseidon2 is instantiated by new and more efficient linear layers with respect to Poseidon. These changes decrease the number of multiplications in the linear layer by up to 90% and the number of constraints in Plonk circuits by up to 70%. This makes Poseidon2 currently the fastest arithmetization-oriented hash function without lookups. Since the compression mode is efficient it is ideal for use in Merkle trees as well. - -An overview of the Poseidon2 hash is provided in the diagram below - -![alt text](/img/Poseidon2.png) - -## Description - -### Round constants - -* In the first full round and last full rounds Round constants are of the structure $[c_0,c_1,\ldots , c_{t-1}]$, where $c_i\in \mathbb{F}$ -* In the partial rounds the round constants is only added to first element $[\tilde{c}_0,0,0,\ldots, 0_{t-1}]$, where $\tilde{c_0}\in \mathbb{F}$ - -Poseidon2 is also extremely customizable and using different constants will produce different hashes, security levels and performance results. - -We support pre-calculated constants for each of the [supported curves](../libraries#supported-curves-and-operations). The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon2/constants) and are labeled clearly per curve `_poseidon2.h`. - -You can also use your own set of constants as shown [here](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-fields/icicle-babybear/src/poseidon2/mod.rs#L290) - -### S box - -Allowed values of $\alpha$ for a given prime is the smallest integer such that $gcd(\alpha,p-1)=1$ - -For ICICLE supported curves/fields - -* Mersene $\alpha = 5$ -* Babybear $\alpha=7$ -* Bls12-377 $\alpha =11$ -* Bls12-381 $\alpha=5$ -* BN254 $\alpha = 5$ -* Grumpkin $\alpha = 5$ -* Stark252 $\alpha=3$ -* Koalabear $\alpha=3$ - -### MDS matrix structure - -There are only two matrices: There is one type of matrix for full round and another for partial round. There are two cases available one for state size $t'=4\cdot t$ and another for $t=2,3$. - -#### $t=4\cdot t'$ where $t'$ is an integer - -**Full Matrix** $M_{full}$ (Referred in paper as $M_{\mathcal{E}}$). These are hard coded (same for all primes $p>2^{30}$) for any fixed state size $t=4\cdot t'$ where $t'$ is an integer. - -$$ -M_{4} = \begin{pmatrix} -5 & 7 & 1 & 3 \\ -4& 6 & 1 & 1 \\ -1 & 3 & 5 & 7\\ -1 & 1 & 4 & 6\\ -\end{pmatrix} -$$ - -As per the [paper](https://eprint.iacr.org/2023/323.pdf) this structure is always maintained and is always MDS for any prime $p>2^{30}$. - -eg for $t=8$ the matrix looks like -$$ -M_{full}^{8\times 8} = \begin{pmatrix} -2\cdot M_4 & M_4 \\ -M_4 & 2\cdot M_4 \\ -\end{pmatrix} -$$ - -**Partial Matrix** $M_{partial}$(referred in paper as $M_{\mathcal{I}}$) - There is only ONE partial matrix for all the partial rounds and has non zero diagonal entries along the diagonal and $1$ everywhere else. - -$$ -M_{Partial}^{t\times t} = \begin{pmatrix} -\mu_0 &1 & \ldots & 1 \\ -1 &\mu_1 & \ldots & 1 \\ -\vdots & \vdots & \ddots & \vdots \\ - 1 & 1 &\ldots & \mu_{t-1}\\ -\end{pmatrix} -$$ - -where $\mu_i \in \mathbb{F}$. In general this matrix is different for each prime since one has to find values that satisfy some inequalities in a field. However unlike Poseidon there is only one $M_{partial}$ for all partial rounds. - -### $t=2,3$ - -These are special state sizes. In all ICICLE supported curves/fields the matrices for $t=3$ are - -$$ -M_{full} = \begin{pmatrix} -2 & 1 & 1 \\ -1 & 2 & 1 \\ -1 & 1 & 2 \\ -\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} -2 & 1 & 1 \\ -1 & 2 & 1 \\ -1 & 1 & 3 \\ -\end{pmatrix} -$$ - -and the matrices for $t=2$ are - -$$ -M_{full} = \begin{pmatrix} -2 & 1 \\ -1 & 2 \\ -\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} -2 & 1 \\ -1 & 3 \\ -\end{pmatrix} -$$ - -## Supported Bindings - -[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon2) - -## Rust API - -This is the most basic way to use the Poseidon2 API. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code - -```rust -let test_size = 4; -let poseidon = Poseidon2::new::(test_size,None).unwrap(); -let config = HashConfig::default(); -let inputs = vec![F::one(); test_size]; -let input_slice = HostSlice::from_slice(&inputs); -//digest is a single element -let out_init:F = F::zero(); -let mut binding = [out_init]; -let out_init_slice = HostSlice::from_mut_slice(&mut binding); - -poseidon.hash(input_slice, &config, out_init_slice).unwrap(); -println!("computed digest: {:?} ",out_init_slice.as_slice().to_vec()[0]); -``` - -## Merkle Tree Builder - -You can use Poseidon2 in a Merkle tree builder. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code. - -```rust -pub fn compute_binary_tree( - mut test_vec: Vec, - leaf_size: u64, - hasher: Hasher, - compress: Hasher, - mut tree_config: MerkleTreeConfig, -) -> MerkleTree -{ - let tree_height: usize = test_vec.len().ilog2() as usize; - //just to be safe - tree_config.padding_policy = PaddingPolicy::ZeroPadding; - let layer_hashes: Vec<&Hasher> = std::iter::once(&hasher) - .chain(std::iter::repeat(&compress).take(tree_height)) - .collect(); - let vec_slice: &mut HostSlice = HostSlice::from_mut_slice(&mut test_vec[..]); - let merkle_tree: MerkleTree = MerkleTree::new(&layer_hashes, leaf_size, 0).unwrap(); - - let _ = merkle_tree - .build(vec_slice,&tree_config); - merkle_tree -} - -//poseidon2 supports t=2,3,4,8,12,16,20,24. In this example we build a binary tree with Poseidon2 t=2. -let poseidon_state_size = 2; -let leaf_size:u64 = 4;// each leaf is a 32 bit element 32/8 = 4 bytes - -let mut test_vec = vec![F::from_u32(random::()); 1024* (poseidon_state_size as usize)]; -println!("Generated random vector of size {:?}", 1024* (poseidon_state_size as usize)); -//to use later for merkle proof -let mut binding = test_vec.clone(); -let test_vec_slice = HostSlice::from_mut_slice(&mut binding); -//define hash and compression functions (You can use different hashes here) -//note:"None" does not work with generics, use F= Fm31, Fbabybear etc -let hasher :Hasher = Poseidon2::new::(poseidon_state_size.try_into().unwrap(),None).unwrap(); -let compress: Hasher = Poseidon2::new::((hasher.output_size()*2).try_into().unwrap(),None).unwrap(); -//tree config -let tree_config = MerkleTreeConfig::default(); -let merk_tree = compute_binary_tree(test_vec.clone(), leaf_size, hasher, compress,tree_config.clone()); -println!("computed Merkle root {:?}", merk_tree.get_root::().unwrap()); - -let random_test_index = rand::thread_rng().gen_range(0..1024*(poseidon_state_size as usize)); -print!("Generating proof for element {:?} at random test index {:?} ",test_vec[random_test_index], random_test_index); -let merkle_proof = merk_tree.get_proof::(test_vec_slice, random_test_index.try_into().unwrap(), false, &tree_config).unwrap(); - -//actually should construct verifier tree :) -assert!(merk_tree.verify(&merkle_proof).unwrap()); -println!("\n Merkle proof verified successfully!"); -``` diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index b4bb5ce484..10162483ef 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -171,15 +171,6 @@ const config: Config = { darkTheme: darkCodeTheme, additionalLanguages: ['rust', 'go'], }, - image: 'img/logo.png', - announcementBar: { - id: 'announcement', // Any value that will identify this message. - content: - '❄️🎉 New Release! ICICLE v3.3! 🎉❄️', - backgroundColor: '#64f5ef', // Light blue background color. - textColor: '#000000', // Black text color. - isCloseable: true, // Defaults to `true`. - }, } satisfies Preset.ThemeConfig, }; diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md b/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md deleted file mode 100644 index 09806fb7d4..0000000000 --- a/docs/versioned_docs/version-3.4.0/icicle/primitives/poseidon2.md +++ /dev/null @@ -1,180 +0,0 @@ -# Poseidon2 - -[Poseidon2](https://eprint.iacr.org/2023/323) is a recently released optimized version of Poseidon. The two versions differ in two crucial points. First, Poseidon is a sponge hash function, while Poseidon2 can be either a sponge or a compression function depending on the use case. Secondly, Poseidon2 is instantiated by new and more efficient linear layers with respect to Poseidon. These changes decrease the number of multiplications in the linear layer by up to 90% and the number of constraints in Plonk circuits by up to 70%. This makes Poseidon2 currently the fastest arithmetization-oriented hash function without lookups. Since the compression mode is efficient it is ideal for use in Merkle trees as well. - -An overview of the Poseidon2 hash is provided in the diagram below - -![alt text](/img/Poseidon2.png) - -## Description - -### Round constants - -* In the first full round and last full rounds Round constants are of the structure $[c_0,c_1,\ldots , c_{t-1}]$, where $c_i\in \mathbb{F}$ -* In the partial rounds the round constants is only added to first element $[\tilde{c}_0,0,0,\ldots, 0_{t-1}]$, where $\tilde{c_0}\in \mathbb{F}$ - -Poseidon2 is also extremely customizable and using different constants will produce different hashes, security levels and performance results. - -We support pre-calculated constants for each of the [supported curves](../libraries#supported-curves-and-operations). The constants can be found [here](https://github.com/ingonyama-zk/icicle/tree/main/icicle/include/poseidon2/constants) and are labeled clearly per curve `_poseidon2.h`. - -You can also use your own set of constants as shown [here](https://github.com/ingonyama-zk/icicle/blob/main/wrappers/rust/icicle-fields/icicle-babybear/src/poseidon2/mod.rs#L290) - -### S box - -Allowed values of $\alpha$ for a given prime is the smallest integer such that $gcd(\alpha,p-1)=1$ - -For ICICLE supported curves/fields - -* Mersene $\alpha = 5$ -* Babybear $\alpha=7$ -* Bls12-377 $\alpha =11$ -* Bls12-381 $\alpha=5$ -* BN254 $\alpha = 5$ -* Grumpkin $\alpha = 5$ -* Stark252 $\alpha=3$ -* Koalabear $\alpha=3$ - -### MDS matrix structure - -There are only two matrices: There is one type of matrix for full round and another for partial round. There are two cases available one for state size $t'=4\cdot t$ and another for $t=2,3$. - -#### $t=4\cdot t'$ where $t'$ is an integer - -**Full Matrix** $M_{full}$ (Referred in paper as $M_{\mathcal{E}}$). These are hard coded (same for all primes $p>2^{30}$) for any fixed state size $t=4\cdot t'$ where $t'$ is an integer. - -$$ -M_{4} = \begin{pmatrix} -5 & 7 & 1 & 3 \\ -4& 6 & 1 & 1 \\ -1 & 3 & 5 & 7\\ -1 & 1 & 4 & 6\\ -\end{pmatrix} -$$ - -As per the [paper](https://eprint.iacr.org/2023/323.pdf) this structure is always maintained and is always MDS for any prime $p>2^{30}$. - -eg for $t=8$ the matrix looks like -$$ -M_{full}^{8\times 8} = \begin{pmatrix} -2\cdot M_4 & M_4 \\ -M_4 & 2\cdot M_4 \\ -\end{pmatrix} -$$ - -**Partial Matrix** $M_{partial}$(referred in paper as $M_{\mathcal{I}}$) - There is only ONE partial matrix for all the partial rounds and has non zero diagonal entries along the diagonal and $1$ everywhere else. - -$$ -M_{Partial}^{t\times t} = \begin{pmatrix} -\mu_0 &1 & \ldots & 1 \\ -1 &\mu_1 & \ldots & 1 \\ -\vdots & \vdots & \ddots & \vdots \\ - 1 & 1 &\ldots & \mu_{t-1}\\ -\end{pmatrix} -$$ - -where $\mu_i \in \mathbb{F}$. In general this matrix is different for each prime since one has to find values that satisfy some inequalities in a field. However unlike Poseidon there is only one $M_{partial}$ for all partial rounds. - -### $t=2,3$ - -These are special state sizes. In all ICICLE supported curves/fields the matrices for $t=3$ are - -$$ -M_{full} = \begin{pmatrix} -2 & 1 & 1 \\ -1 & 2 & 1 \\ -1 & 1 & 2 \\ -\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} -2 & 1 & 1 \\ -1 & 2 & 1 \\ -1 & 1 & 3 \\ -\end{pmatrix} -$$ - -and the matrices for $t=2$ are - -$$ -M_{full} = \begin{pmatrix} -2 & 1 \\ -1 & 2 \\ -\end{pmatrix} \ , \ M_{Partial} = \begin{pmatrix} -2 & 1 \\ -1 & 3 \\ -\end{pmatrix} -$$ - -## Supported Bindings - -[`Rust`](https://github.com/ingonyama-zk/icicle/tree/main/wrappers/rust/icicle-core/src/poseidon2) - -## Rust API - -This is the most basic way to use the Poseidon2 API. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code - -```rust -let test_size = 4; -let poseidon = Poseidon2::new::(test_size,None).unwrap(); -let config = HashConfig::default(); -let inputs = vec![F::one(); test_size]; -let input_slice = HostSlice::from_slice(&inputs); -//digest is a single element -let out_init:F = F::zero(); -let mut binding = [out_init]; -let out_init_slice = HostSlice::from_mut_slice(&mut binding); - -poseidon.hash(input_slice, &config, out_init_slice).unwrap(); -println!("computed digest: {:?} ",out_init_slice.as_slice().to_vec()[0]); -``` - -## Merkle Tree Builder - -You can use Poseidon2 in a Merkle tree builder. See the [examples/poseidon2](https://github.com/ingonyama-zk/icicle/tree/b12d83e6bcb8ee598409de78015bd118458a55d0/examples/rust/poseidon2) folder for the relevant code. - -```rust -pub fn compute_binary_tree( - mut test_vec: Vec, - leaf_size: u64, - hasher: Hasher, - compress: Hasher, - mut tree_config: MerkleTreeConfig, -) -> MerkleTree -{ - let tree_height: usize = test_vec.len().ilog2() as usize; - //just to be safe - tree_config.padding_policy = PaddingPolicy::ZeroPadding; - let layer_hashes: Vec<&Hasher> = std::iter::once(&hasher) - .chain(std::iter::repeat(&compress).take(tree_height)) - .collect(); - let vec_slice: &mut HostSlice = HostSlice::from_mut_slice(&mut test_vec[..]); - let merkle_tree: MerkleTree = MerkleTree::new(&layer_hashes, leaf_size, 0).unwrap(); - - let _ = merkle_tree - .build(vec_slice,&tree_config); - merkle_tree -} - -//poseidon2 supports t=2,3,4,8,12,16,20,24. In this example we build a binary tree with Poseidon2 t=2. -let poseidon_state_size = 2; -let leaf_size:u64 = 4;// each leaf is a 32 bit element 32/8 = 4 bytes - -let mut test_vec = vec![F::from_u32(random::()); 1024* (poseidon_state_size as usize)]; -println!("Generated random vector of size {:?}", 1024* (poseidon_state_size as usize)); -//to use later for merkle proof -let mut binding = test_vec.clone(); -let test_vec_slice = HostSlice::from_mut_slice(&mut binding); -//define hash and compression functions (You can use different hashes here) -//note:"None" does not work with generics, use F= Fm31, Fbabybear etc -let hasher :Hasher = Poseidon2::new::(poseidon_state_size.try_into().unwrap(),None).unwrap(); -let compress: Hasher = Poseidon2::new::((hasher.output_size()*2).try_into().unwrap(),None).unwrap(); -//tree config -let tree_config = MerkleTreeConfig::default(); -let merk_tree = compute_binary_tree(test_vec.clone(), leaf_size, hasher, compress,tree_config.clone()); -println!("computed Merkle root {:?}", merk_tree.get_root::().unwrap()); - -let random_test_index = rand::thread_rng().gen_range(0..1024*(poseidon_state_size as usize)); -print!("Generating proof for element {:?} at random test index {:?} ",test_vec[random_test_index], random_test_index); -let merkle_proof = merk_tree.get_proof::(test_vec_slice, random_test_index.try_into().unwrap(), false, &tree_config).unwrap(); - -//actually should construct verifier tree :) -assert!(merk_tree.verify(&merkle_proof).unwrap()); -println!("\n Merkle proof verified successfully!"); -``` diff --git a/docs/versioned_sidebars/version-3.4.0-sidebars.json b/docs/versioned_sidebars/version-3.4.0-sidebars.json index 71e548e363..6975e6f036 100644 --- a/docs/versioned_sidebars/version-3.4.0-sidebars.json +++ b/docs/versioned_sidebars/version-3.4.0-sidebars.json @@ -121,11 +121,6 @@ "label": "Hash", "id": "icicle/primitives/hash" }, - { - "type": "doc", - "label": "Poseidon2", - "id": "icicle/primitives/poseidon2" - }, { "type": "doc", "label": "Merkle-Tree", From 87677d0f57e244c9f4913f5d2d3dde398016d539 Mon Sep 17 00:00:00 2001 From: yshekel Date: Wed, 15 Jan 2025 17:52:37 +0200 Subject: [PATCH 069/127] Deprecated icicle/api headers and updated examples/docs (#740) --- docs/docs/icicle/programmers_guide/cpp.md | 53 +++++- examples/c++/best-practice-ntt/example.cpp | 3 +- .../c++/install-and-use-icicle/example.cpp | 4 +- examples/c++/msm/README.md | 4 +- examples/c++/msm/example.cpp | 7 +- examples/c++/ntt/README.md | 4 +- examples/c++/ntt/example.cpp | 3 +- examples/c++/pedersen-commitment/example.cpp | 4 +- examples/c++/polynomial-api/example.cpp | 4 +- .../c++/polynomial-multiplication/example.cpp | 12 +- examples/c++/risc0/example.cpp | 3 +- .../cpu/src/curve/cpu_mont_conversion.cpp | 2 +- icicle/backend/cpu/src/curve/cpu_msm.cpp | 2 +- icicle/backend/cpu/src/field/cpu_vec_ops.cpp | 2 +- icicle/backend/cpu/src/hash/cpu_poseidon2.cpp | 2 +- icicle/cmake/target_editor.cmake | 2 +- icicle/include/icicle/api/babybear.h | 25 +++ icicle/include/icicle/api/bls12_377.h | 34 ++++ icicle/include/icicle/api/bls12_381.h | 35 ++++ icicle/include/icicle/api/bn254.h | 35 ++++ icicle/include/icicle/api/bw6_761.h | 34 ++++ icicle/include/icicle/api/grumpkin.h | 32 +++- icicle/include/icicle/api/koalabear.h | 25 +++ icicle/include/icicle/api/stark252.h | 25 +++ .../api/templates/curves/curve.template | 13 -- .../api/templates/curves/curve_g2.template | 13 -- .../api/templates/curves/ecntt.template | 2 - .../icicle/api/templates/curves/msm.template | 8 - .../api/templates/curves/msm_g2.template | 8 - .../api/templates/fields/field.template | 4 - .../api/templates/fields/field_ext.template | 4 - .../icicle/api/templates/fields/ntt.template | 7 - .../api/templates/fields/ntt_ext.template | 2 - .../api/templates/fields/vec_ops.template | 18 -- .../api/templates/fields/vec_ops_ext.template | 18 -- icicle/include/icicle/backend/msm_backend.h | 2 +- .../icicle/curves/montgomery_conversion.h | 2 +- icicle/src/curves/ffi_extern.cpp | 2 +- icicle/src/curves/montgomery_conversion.cpp | 4 +- icicle/src/msm.cpp | 4 +- icicle/tests/test_curve_api.cpp | 4 +- icicle/tests/test_polynomial_api.cpp | 4 +- scripts/format_all.sh | 12 ++ scripts/gen_c_api.py | 160 ------------------ scripts/release/build_all.sh | 7 + 45 files changed, 359 insertions(+), 295 deletions(-) delete mode 100644 icicle/include/icicle/api/templates/curves/curve.template delete mode 100644 icicle/include/icicle/api/templates/curves/curve_g2.template delete mode 100644 icicle/include/icicle/api/templates/curves/ecntt.template delete mode 100644 icicle/include/icicle/api/templates/curves/msm.template delete mode 100644 icicle/include/icicle/api/templates/curves/msm_g2.template delete mode 100644 icicle/include/icicle/api/templates/fields/field.template delete mode 100644 icicle/include/icicle/api/templates/fields/field_ext.template delete mode 100644 icicle/include/icicle/api/templates/fields/ntt.template delete mode 100644 icicle/include/icicle/api/templates/fields/ntt_ext.template delete mode 100644 icicle/include/icicle/api/templates/fields/vec_ops.template delete mode 100644 icicle/include/icicle/api/templates/fields/vec_ops_ext.template delete mode 100755 scripts/gen_c_api.py diff --git a/docs/docs/icicle/programmers_guide/cpp.md b/docs/docs/icicle/programmers_guide/cpp.md index cff576571b..aef36fe551 100644 --- a/docs/docs/icicle/programmers_guide/cpp.md +++ b/docs/docs/icicle/programmers_guide/cpp.md @@ -183,15 +183,59 @@ struct DeviceProperties { ## Compute APIs -### Multi-Scalar Multiplication (MSM) Example +### Including Curves and Fields + +To use a specific elliptic curve (e.g., BN254) or its associated fields, include the relevant header files. For example: + +```cpp +#include "icicle/msm.h" +#include "icicle/curves/params/bn254.h" +``` + +The bn254 namespace includes key types like: + +- **scalar_t**: Scalar field elements. +- **projective_t**: Points on the elliptic curve in projective coordinates. +- **affine_t**: Points on the elliptic curve in affine coordinates. + +### Namespace Usage + +There are two ways to access types and functionality for a specific field or curve: + +1. **Bring the Namespace into Scope** +This approach simplifies the code but may cause conflicts if multiple curves are used: + +```cpp +using namespace bn254; + +scalar_t s; // Scalar field element +projective_t p; // Point in projective coordinates +affine_t a; // Point in affine coordinates +``` + +2. **Use Fully Qualified Names** +This is recommended if you are working with multiple curves or libraries to avoid namespace conflicts: + +```cpp +bn254::scalar_t s; // Scalar field element +bn254::projective_t p; // Point in projective coordinates +bn254::affine_t a; // Point in affine coordinates +``` + +### Leveraging Template APIs + +ICICLE’s APIs are designed to work seamlessly with templated types, enabling flexibility and type safety. For example: + +#### Multi-Scalar Multiplication (MSM) Example Icicle provides high-performance compute APIs such as the Multi-Scalar Multiplication (MSM) for cryptographic operations. Here's a simple example of how to use the MSM API. ```cpp #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/curves/params/bn254.h" +#include "icicle/msm.h" using namespace bn254; int main() @@ -245,15 +289,16 @@ int main() } ``` -### Polynomial Operations Example +#### Polynomial Operations Example Here's another example demonstrating polynomial operations using Icicle: ```cpp #include #include "icicle/runtime.h" +#include "icicle/curves/params/bn254.h" +#include "icicle/ntt.h" #include "icicle/polynomials/polynomials.h" -#include "icicle/api/bn254.h" using namespace bn254; diff --git a/examples/c++/best-practice-ntt/example.cpp b/examples/c++/best-practice-ntt/example.cpp index 7f4bc974d1..e6321c9ad2 100644 --- a/examples/c++/best-practice-ntt/example.cpp +++ b/examples/c++/best-practice-ntt/example.cpp @@ -4,7 +4,8 @@ #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/ntt.h" +#include "icicle/curves/params/bn254.h" using namespace bn254; #include "examples_utils.h" diff --git a/examples/c++/install-and-use-icicle/example.cpp b/examples/c++/install-and-use-icicle/example.cpp index f86eceb97e..10a7ccf5ad 100644 --- a/examples/c++/install-and-use-icicle/example.cpp +++ b/examples/c++/install-and-use-icicle/example.cpp @@ -1,7 +1,9 @@ #include #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" + +#include "icicle/curves/params/bn254.h" +#include "icicle/ntt.h" using namespace bn254; // This makes scalar_t a bn254 scalar instead of bn254::scalar_t diff --git a/examples/c++/msm/README.md b/examples/c++/msm/README.md index af365f836e..1a5d621f03 100644 --- a/examples/c++/msm/README.md +++ b/examples/c++/msm/README.md @@ -11,7 +11,9 @@ 3. Call msm api ```c++ -#include "icicle/api/bn254.h" +#include "icicle/msm.h" +#include "icicle/curves/params/bn254.h" +using namespace bn254; ... MSMConfig config = default_msm_config(); ... diff --git a/examples/c++/msm/example.cpp b/examples/c++/msm/example.cpp index b3b3acc6ff..9c11ad1d51 100644 --- a/examples/c++/msm/example.cpp +++ b/examples/c++/msm/example.cpp @@ -3,7 +3,8 @@ #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/msm.h" +#include "icicle/curves/params/bn254.h" using namespace bn254; #include "examples_utils.h" @@ -38,7 +39,7 @@ int main(int argc, char* argv[]) std::cout << "\nRunning MSM kernel with on-host inputs" << std::endl; // Execute the MSM kernel START_TIMER(MSM_host_mem); - ICICLE_CHECK(bn254_msm(scalars.get(), points.get(), msm_size, &config, &result)); + ICICLE_CHECK(msm(scalars.get(), points.get(), msm_size, config, &result)); END_TIMER(MSM_host_mem, "MSM from host-memory took"); std::cout << projective_t::to_affine(result) << std::endl; @@ -91,7 +92,7 @@ int main(int argc, char* argv[]) config.are_points_on_device = false; g2_projective_t g2_result; START_TIMER(MSM_g2); - ICICLE_CHECK(bn254_g2_msm(scalars.get(), g2_points.get(), msm_size, &config, &g2_result)); + ICICLE_CHECK(msm(scalars.get(), g2_points.get(), msm_size, config, &g2_result)); END_TIMER(MSM_g2, "MSM G2 from host-memory took"); std::cout << g2_projective_t::to_affine(g2_result) << std::endl; diff --git a/examples/c++/ntt/README.md b/examples/c++/ntt/README.md index c95e50dcf9..a7a8ed416e 100644 --- a/examples/c++/ntt/README.md +++ b/examples/c++/ntt/README.md @@ -11,7 +11,9 @@ 3. Call ntt api ```c++ -#include "icicle/api/bn254.h" +#include "icicle/ntt.h" +#include "icicle/curves/params/bn254.h" +using namespace bn254; ... auto ntt_init_domain_cfg = default_ntt_init_domain_config(); ... diff --git a/examples/c++/ntt/example.cpp b/examples/c++/ntt/example.cpp index 2b21946358..76f51398a7 100644 --- a/examples/c++/ntt/example.cpp +++ b/examples/c++/ntt/example.cpp @@ -2,7 +2,8 @@ #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/ntt.h" +#include "icicle/curves/params/bn254.h" using namespace bn254; #include "examples_utils.h" diff --git a/examples/c++/pedersen-commitment/example.cpp b/examples/c++/pedersen-commitment/example.cpp index e8afabdb77..e09efa5f40 100644 --- a/examples/c++/pedersen-commitment/example.cpp +++ b/examples/c++/pedersen-commitment/example.cpp @@ -3,7 +3,7 @@ #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/msm.h" #include "icicle/curves/params/bn254.h" using namespace bn254; @@ -150,7 +150,7 @@ int main(int argc, char** argv) std::cout << "Executing MSM" << std::endl; auto config = default_msm_config(); START_TIMER(msm); - bn254_msm(scalars, points, N + 1, &config, &result); + ICICLE_CHECK(msm(scalars, points, N + 1, config, &result)); END_TIMER(msm, "Time to execute MSM"); std::cout << "Computed commitment: " << result << std::endl; diff --git a/examples/c++/polynomial-api/example.cpp b/examples/c++/polynomial-api/example.cpp index 5b1d166d4e..4046c68ad0 100644 --- a/examples/c++/polynomial-api/example.cpp +++ b/examples/c++/polynomial-api/example.cpp @@ -1,7 +1,9 @@ #include #include -#include "icicle/api/bn254.h" +#include "icicle/ntt.h" +#include "icicle/msm.h" +#include "icicle/curves/params/bn254.h" #include "icicle/polynomials/polynomials.h" #include "examples_utils.h" diff --git a/examples/c++/polynomial-multiplication/example.cpp b/examples/c++/polynomial-multiplication/example.cpp index 1fdfeb5014..bbc50bfa0d 100644 --- a/examples/c++/polynomial-multiplication/example.cpp +++ b/examples/c++/polynomial-multiplication/example.cpp @@ -3,7 +3,9 @@ #include #include "icicle/runtime.h" -#include "icicle/api/bn254.h" +#include "icicle/ntt.h" +#include "icicle/vec_ops.h" +#include "icicle/curves/params/bn254.h" using namespace bn254; #include "examples_utils.h" @@ -38,7 +40,7 @@ int main(int argc, char** argv) // init domain scalar_t basic_root = scalar_t::omega(NTT_LOG_SIZE); auto config = default_ntt_init_domain_config(); - bn254_ntt_init_domain(&basic_root, &config); + ICICLE_CHECK(ntt_init_domain(basic_root, config)); // (1) cpu allocation auto polyA = std::make_unique(NTT_SIZE); @@ -65,8 +67,8 @@ int main(int argc, char** argv) ntt_config.are_inputs_on_device = false; ntt_config.are_outputs_on_device = true; ntt_config.ordering = Ordering::kNM; - ICICLE_CHECK(bn254_ntt(polyA.get(), NTT_SIZE, NTTDir::kForward, &ntt_config, d_polyA)); - ICICLE_CHECK(bn254_ntt(polyB.get(), NTT_SIZE, NTTDir::kForward, &ntt_config, d_polyB)); + ICICLE_CHECK(ntt(polyA.get(), NTT_SIZE, NTTDir::kForward, ntt_config, d_polyA)); + ICICLE_CHECK(ntt(polyB.get(), NTT_SIZE, NTTDir::kForward, ntt_config, d_polyB)); // (4) multiply A,B VecOpsConfig config = default_vec_ops_config(); @@ -94,7 +96,7 @@ int main(int argc, char** argv) benchmark(false); // warmup benchmark(true); - ICICLE_CHECK(bn254_ntt_release_domain()); + ntt_release_domain(); return 0; } \ No newline at end of file diff --git a/examples/c++/risc0/example.cpp b/examples/c++/risc0/example.cpp index fe5c48e4b2..2ef38cdebf 100644 --- a/examples/c++/risc0/example.cpp +++ b/examples/c++/risc0/example.cpp @@ -6,7 +6,8 @@ #include "examples_utils.h" #include "icicle/polynomials/polynomials.h" -#include "icicle/api/babybear.h" +#include "icicle/ntt.h" +#include "icicle/fields/stark_fields/babybear.h" using namespace babybear; diff --git a/icicle/backend/cpu/src/curve/cpu_mont_conversion.cpp b/icicle/backend/cpu/src/curve/cpu_mont_conversion.cpp index 5c94ebabb7..6dee7abf75 100644 --- a/icicle/backend/cpu/src/curve/cpu_mont_conversion.cpp +++ b/icicle/backend/cpu/src/curve/cpu_mont_conversion.cpp @@ -22,7 +22,7 @@ cpu_convert_mont(const Device& device, const T* input, size_t n, bool is_into, c REGISTER_AFFINE_CONVERT_MONTGOMERY_BACKEND("CPU", cpu_convert_mont); REGISTER_PROJECTIVE_CONVERT_MONTGOMERY_BACKEND("CPU", cpu_convert_mont); -#ifdef G2 +#ifdef G2_ENABLED REGISTER_AFFINE_G2_CONVERT_MONTGOMERY_BACKEND("CPU", cpu_convert_mont); REGISTER_PROJECTIVE_G2_CONVERT_MONTGOMERY_BACKEND("CPU", cpu_convert_mont); #endif // G2 \ No newline at end of file diff --git a/icicle/backend/cpu/src/curve/cpu_msm.cpp b/icicle/backend/cpu/src/curve/cpu_msm.cpp index ccb924c385..7f28d49505 100644 --- a/icicle/backend/cpu/src/curve/cpu_msm.cpp +++ b/icicle/backend/cpu/src/curve/cpu_msm.cpp @@ -7,7 +7,7 @@ REGISTER_MSM_PRE_COMPUTE_BASES_BACKEND("CPU", (cpu_msm_precompute_bases)); REGISTER_MSM_BACKEND("CPU", (cpu_msm)); -#ifdef G2 +#ifdef G2_ENABLED REGISTER_MSM_G2_PRE_COMPUTE_BASES_BACKEND("CPU", (cpu_msm_precompute_bases)); REGISTER_MSM_G2_BACKEND("CPU", (cpu_msm)); #endif \ No newline at end of file diff --git a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp index 18fb5dd032..38d859e022 100644 --- a/icicle/backend/cpu/src/field/cpu_vec_ops.cpp +++ b/icicle/backend/cpu/src/field/cpu_vec_ops.cpp @@ -372,7 +372,7 @@ class VectorOpTask : public TaskBase public: T m_intermidiate_res; // pointer to the output. Can be a vector or scalar pointer uint64_t m_idx_in_batch; // index in the batch. Used in intermediate res tasks -}; // class VectorOpTask +}; #define NOF_OPERATIONS_PER_TASK 512 #define CONFIG_NOF_THREADS_KEY "n_threads" diff --git a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp index df1c519cd6..7f74552769 100644 --- a/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp +++ b/icicle/backend/cpu/src/hash/cpu_poseidon2.cpp @@ -147,7 +147,7 @@ namespace icicle { ICICLE_LOG_ERROR << "cpu_poseidon2_init_default_constants: T (width) must be one of [2, 3, 4, 8, 12, 16, 20, 24]\n"; return eIcicleError::INVALID_ARGUMENT; - } // switch (T) { + } if (full_rounds == 0 && partial_rounds == 0) { // All arrays are empty in this case. continue; } diff --git a/icicle/cmake/target_editor.cmake b/icicle/cmake/target_editor.cmake index 91e2f4e1c8..64d330aab9 100644 --- a/icicle/cmake/target_editor.cmake +++ b/icicle/cmake/target_editor.cmake @@ -53,7 +53,7 @@ endfunction() function(handle_g2 TARGET FEATURE_LIST) if(G2 AND "G2" IN_LIST FEATURE_LIST) - target_compile_definitions(${TARGET} PUBLIC G2=${G2}) + target_compile_definitions(${TARGET} PUBLIC G2_ENABLED=${G2}) set(G2 "G2" CACHE BOOL "Enable G2 feature" FORCE) else() set(G2 OFF CACHE BOOL "G2 not available for this curve" FORCE) diff --git a/icicle/include/icicle/api/babybear.h b/icicle/include/icicle/api/babybear.h index b0a8108cb3..a4ce15c64f 100644 --- a/icicle/include/icicle/api/babybear.h +++ b/icicle/include/icicle/api/babybear.h @@ -1,3 +1,28 @@ +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/fields/stark_fields/babybear.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace babybear; * +// * scalar_t a; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * babybear::scalar_t a; * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/bls12_377.h b/icicle/include/icicle/api/bls12_377.h index 972bd59e2d..3ff44901a5 100644 --- a/icicle/include/icicle/api/bls12_377.h +++ b/icicle/include/icicle/api/bls12_377.h @@ -1,3 +1,37 @@ +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/curves/params/bls12_377.h" * +// * #include "icicle/msm.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace bls12_377; * +// * scalar_t a; * +// * projective_t p; * +// * affine_t q; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * bls12_377::scalar_t a; * +// * bls12_377::projective_t p; * +// * bls12_377::affine_t q; * +// * * +// * Note: The bls12_377 namespace also includes other types such as G2 types, * +// * which are not explicitly mentioned here but are accessible through * +// * the same pattern of usage. * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/bls12_381.h b/icicle/include/icicle/api/bls12_381.h index 03e3bdd366..57322a7f8e 100644 --- a/icicle/include/icicle/api/bls12_381.h +++ b/icicle/include/icicle/api/bls12_381.h @@ -1,3 +1,38 @@ + +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/curves/params/bls12_381.h" * +// * #include "icicle/msm.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace bls12_381; * +// * scalar_t a; * +// * projective_t p; * +// * affine_t q; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * bls12_381::scalar_t a; * +// * bls12_381::projective_t p; * +// * bls12_381::affine_t q; * +// * * +// * Note: The bls12_381 namespace also includes other types such as G2 types, * +// * which are not explicitly mentioned here but are accessible through * +// * the same pattern of usage. * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/bn254.h b/icicle/include/icicle/api/bn254.h index e9d26c299b..5629515653 100644 --- a/icicle/include/icicle/api/bn254.h +++ b/icicle/include/icicle/api/bn254.h @@ -1,3 +1,38 @@ + +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/curves/params/bn254.h" * +// * #include "icicle/msm.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace bn254; * +// * scalar_t a; * +// * projective_t p; * +// * affine_t q; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * bn254::scalar_t a; * +// * bn254::projective_t p; * +// * bn254::affine_t q; * +// * * +// * Note: The bn254 namespace also includes other types such as G2 types, * +// * which are not explicitly mentioned here but are accessible through * +// * the same pattern of usage. * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/bw6_761.h b/icicle/include/icicle/api/bw6_761.h index 6d0226c57b..e9fedc9d1b 100644 --- a/icicle/include/icicle/api/bw6_761.h +++ b/icicle/include/icicle/api/bw6_761.h @@ -1,3 +1,37 @@ +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/curves/params/bw6_761.h" * +// * #include "icicle/msm.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace bw6_761; * +// * scalar_t a; * +// * projective_t p; * +// * affine_t q; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * bw6_761::scalar_t a; * +// * bw6_761::projective_t p; * +// * bw6_761::affine_t q; * +// * * +// * Note: The bw6_761 namespace also includes other types such as G2 types, * +// * which are not explicitly mentioned here but are accessible through * +// * the same pattern of usage. * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/grumpkin.h b/icicle/include/icicle/api/grumpkin.h index 235b728438..289a855789 100644 --- a/icicle/include/icicle/api/grumpkin.h +++ b/icicle/include/icicle/api/grumpkin.h @@ -1,4 +1,34 @@ -// WARNING: This file is auto-generated by a script. + +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/curves/params/grumpkin.h" * +// * #include "icicle/msm.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace grumpkin; * +// * scalar_t a; * +// * projective_t p; * +// * affine_t q; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * grumpkin::scalar_t a; * +// * grumpkin::projective_t p; * +// * grumpkin::affine_t q; * +// * * +// ***************************************************************************** + +// W2ARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. // Path to the code generation script: scripts/gen_c_api.py diff --git a/icicle/include/icicle/api/koalabear.h b/icicle/include/icicle/api/koalabear.h index 01c128b4a7..f87687e872 100644 --- a/icicle/include/icicle/api/koalabear.h +++ b/icicle/include/icicle/api/koalabear.h @@ -1,3 +1,28 @@ +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/fields/stark_fields/koalabear.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace koalabear; * +// * scalar_t a; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * koalabear::scalar_t a; * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/stark252.h b/icicle/include/icicle/api/stark252.h index cd4d190ab1..d2526d3b82 100644 --- a/icicle/include/icicle/api/stark252.h +++ b/icicle/include/icicle/api/stark252.h @@ -1,3 +1,28 @@ +// ***************************************************************************** +// * DEPRECATED HEADER FILE * +// ***************************************************************************** +// * This header file is deprecated and should not be used in new code. * +// * It is maintained only for backward compatibility and may be removed in * +// * a future release. * +// * * +// * Deprecation Date: January 15, 2025 * +// * * +// * Please migrate to using template headers with the corresponding types. * +// * For example: * +// * * +// * #include "icicle/fields/stark_fields/stark252.h" * +// * #include "icicle/ntt.h" * +// * #include "icicle/vec_ops.h" * +// * * +// * Option 1: Bring the namespace into scope (simplifies usage): * +// * using namespace stark252; * +// * scalar_t a; * +// * * +// * Option 2: Use fully qualified names (avoids namespace conflicts): * +// * stark252::scalar_t a; * +// * * +// ***************************************************************************** + // WARNING: This file is auto-generated by a script. // Any changes made to this file may be overwritten. // Please modify the code generation script instead. diff --git a/icicle/include/icicle/api/templates/curves/curve.template b/icicle/include/icicle/api/templates/curves/curve.template deleted file mode 100644 index e85ad21830..0000000000 --- a/icicle/include/icicle/api/templates/curves/curve.template +++ /dev/null @@ -1,13 +0,0 @@ -extern "C" bool ${CURVE}_eq(${CURVE}::projective_t* point1, ${CURVE}::projective_t* point2); - -extern "C" void ${CURVE}_to_affine(${CURVE}::projective_t* point, ${CURVE}::affine_t* point_out); - -extern "C" void ${CURVE}_generate_projective_points(${CURVE}::projective_t* points, int size); - -extern "C" void ${CURVE}_generate_affine_points(${CURVE}::affine_t* points, int size); - -extern "C" eIcicleError ${CURVE}_affine_convert_montgomery( - const ${CURVE}::affine_t* input, size_t n, bool is_into, const VecOpsConfig* config, ${CURVE}::affine_t* output); - -extern "C" eIcicleError ${CURVE}_projective_convert_montgomery( - const ${CURVE}::projective_t* input, size_t n, bool is_into, const VecOpsConfig* config, ${CURVE}::projective_t* output); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/curves/curve_g2.template b/icicle/include/icicle/api/templates/curves/curve_g2.template deleted file mode 100644 index d5a56db8e2..0000000000 --- a/icicle/include/icicle/api/templates/curves/curve_g2.template +++ /dev/null @@ -1,13 +0,0 @@ -extern "C" bool ${CURVE}_g2_eq(${CURVE}::g2_projective_t* point1, ${CURVE}::g2_projective_t* point2); - -extern "C" void ${CURVE}_g2_to_affine(${CURVE}::g2_projective_t* point, ${CURVE}::g2_affine_t* point_out); - -extern "C" void ${CURVE}_g2_generate_projective_points(${CURVE}::g2_projective_t* points, int size); - -extern "C" void ${CURVE}_g2_generate_affine_points(${CURVE}::g2_affine_t* points, int size); - -extern "C" eIcicleError ${CURVE}_g2_affine_convert_montgomery( - const ${CURVE}::g2_affine_t* input, size_t n, bool is_into, const VecOpsConfig* config, ${CURVE}::g2_affine_t* output); - -extern "C" eIcicleError ${CURVE}_g2_projective_convert_montgomery( - const ${CURVE}::g2_projective_t* input, size_t n, bool is_into, const VecOpsConfig* config, ${CURVE}::g2_projective_t* output); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/curves/ecntt.template b/icicle/include/icicle/api/templates/curves/ecntt.template deleted file mode 100644 index d2306fbf29..0000000000 --- a/icicle/include/icicle/api/templates/curves/ecntt.template +++ /dev/null @@ -1,2 +0,0 @@ -extern "C" eIcicleError ${CURVE}_ecntt( - const ${CURVE}::projective_t* input, int size, NTTDir dir, const NTTConfig<${CURVE}::scalar_t>* config, ${CURVE}::projective_t* output); diff --git a/icicle/include/icicle/api/templates/curves/msm.template b/icicle/include/icicle/api/templates/curves/msm.template deleted file mode 100644 index c0922782e6..0000000000 --- a/icicle/include/icicle/api/templates/curves/msm.template +++ /dev/null @@ -1,8 +0,0 @@ -extern "C" eIcicleError ${CURVE}_precompute_msm_bases( - const ${CURVE}::affine_t* bases, - int nof_bases, - const MSMConfig* config, - ${CURVE}::affine_t* output_bases); - -extern "C" eIcicleError ${CURVE}_msm( - const ${CURVE}::scalar_t* scalars, const ${CURVE}::affine_t* points, int msm_size, const MSMConfig* config, ${CURVE}::projective_t* out); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/curves/msm_g2.template b/icicle/include/icicle/api/templates/curves/msm_g2.template deleted file mode 100644 index 0880aace04..0000000000 --- a/icicle/include/icicle/api/templates/curves/msm_g2.template +++ /dev/null @@ -1,8 +0,0 @@ -extern "C" eIcicleError ${CURVE}_g2_precompute_msm_bases( - const ${CURVE}::g2_affine_t* bases, - int nof_bases, - const MSMConfig* config, - ${CURVE}::g2_affine_t* output_bases); - -extern "C" eIcicleError ${CURVE}_g2_msm( - const ${CURVE}::scalar_t* scalars, const ${CURVE}::g2_affine_t* points, int msm_size, const MSMConfig* config, ${CURVE}::g2_projective_t* out); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/fields/field.template b/icicle/include/icicle/api/templates/fields/field.template deleted file mode 100644 index 1e17151d54..0000000000 --- a/icicle/include/icicle/api/templates/fields/field.template +++ /dev/null @@ -1,4 +0,0 @@ -extern "C" void ${FIELD}_generate_scalars(${FIELD}::scalar_t* scalars, int size); - -extern "C" void ${FIELD}_scalar_convert_montgomery( - const ${FIELD}::scalar_t* input, uint64_t size, bool is_into, const VecOpsConfig* config, ${FIELD}::scalar_t* output); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/fields/field_ext.template b/icicle/include/icicle/api/templates/fields/field_ext.template deleted file mode 100644 index 7e9e7dc11c..0000000000 --- a/icicle/include/icicle/api/templates/fields/field_ext.template +++ /dev/null @@ -1,4 +0,0 @@ -extern "C" void ${FIELD}_extension_generate_scalars(${FIELD}::extension_t* scalars, int size); - -extern "C" eIcicleError ${FIELD}_extension_scalar_convert_montgomery( - const ${FIELD}::extension_t* input, uint64_t size, bool is_into, const VecOpsConfig* config, ${FIELD}::extension_t* output); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/fields/ntt.template b/icicle/include/icicle/api/templates/fields/ntt.template deleted file mode 100644 index d188e2c72a..0000000000 --- a/icicle/include/icicle/api/templates/fields/ntt.template +++ /dev/null @@ -1,7 +0,0 @@ -extern "C" eIcicleError ${FIELD}_ntt_init_domain( - ${FIELD}::scalar_t* primitive_root, const NTTInitDomainConfig* config); - -extern "C" eIcicleError ${FIELD}_ntt( - const ${FIELD}::scalar_t* input, int size, NTTDir dir, const NTTConfig<${FIELD}::scalar_t>* config, ${FIELD}::scalar_t* output); - -extern "C" eIcicleError ${FIELD}_ntt_release_domain(); \ No newline at end of file diff --git a/icicle/include/icicle/api/templates/fields/ntt_ext.template b/icicle/include/icicle/api/templates/fields/ntt_ext.template deleted file mode 100644 index 1b2819c6ec..0000000000 --- a/icicle/include/icicle/api/templates/fields/ntt_ext.template +++ /dev/null @@ -1,2 +0,0 @@ -extern "C" eIcicleError ${FIELD}_extension_ntt( - const ${FIELD}::extension_t* input, int size, NTTDir dir, const NTTConfig<${FIELD}::scalar_t>* config, ${FIELD}::extension_t* output); diff --git a/icicle/include/icicle/api/templates/fields/vec_ops.template b/icicle/include/icicle/api/templates/fields/vec_ops.template deleted file mode 100644 index 5ebbed7aa8..0000000000 --- a/icicle/include/icicle/api/templates/fields/vec_ops.template +++ /dev/null @@ -1,18 +0,0 @@ -extern "C" eIcicleError ${FIELD}_vector_mul( - const ${FIELD}::scalar_t* vec_a, const ${FIELD}::scalar_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::scalar_t* result); - -extern "C" eIcicleError ${FIELD}_vector_add( - const ${FIELD}::scalar_t* vec_a, const ${FIELD}::scalar_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::scalar_t* result); - -extern "C" eIcicleError ${FIELD}_vector_sub( - const ${FIELD}::scalar_t* vec_a, const ${FIELD}::scalar_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::scalar_t* result); - -extern "C" eIcicleError ${FIELD}_matrix_transpose( - const ${FIELD}::scalar_t* input, - uint32_t nof_rows, - uint32_t nof_cols, - const VecOpsConfig* config, - ${FIELD}::scalar_t* output); - -extern "C" eIcicleError ${FIELD}_bit_reverse( - const ${FIELD}::scalar_t* input, uint64_t n, const VecOpsConfig* config, ${FIELD}::scalar_t* output); diff --git a/icicle/include/icicle/api/templates/fields/vec_ops_ext.template b/icicle/include/icicle/api/templates/fields/vec_ops_ext.template deleted file mode 100644 index d798617933..0000000000 --- a/icicle/include/icicle/api/templates/fields/vec_ops_ext.template +++ /dev/null @@ -1,18 +0,0 @@ -extern "C" eIcicleError ${FIELD}_extension_vector_mul( - const ${FIELD}::extension_t* vec_a, const ${FIELD}::extension_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::extension_t* result); - -extern "C" eIcicleError ${FIELD}_extension_vector_add( - const ${FIELD}::extension_t* vec_a, const ${FIELD}::extension_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::extension_t* result); - -extern "C" eIcicleError ${FIELD}_extension_vector_sub( - const ${FIELD}::extension_t* vec_a, const ${FIELD}::extension_t* vec_b, uint64_t n, const VecOpsConfig* config, ${FIELD}::extension_t* result); - -extern "C" eIcicleError ${FIELD}_extension_matrix_transpose( - const ${FIELD}::extension_t* input, - uint32_t nof_rows, - uint32_t nof_cols, - const VecOpsConfig* config, - ${FIELD}::extension_t* output); - -extern "C" eIcicleError ${FIELD}_extension_bit_reverse( - const ${FIELD}::extension_t* input, uint64_t n, const VecOpsConfig* config, ${FIELD}::extension_t* output); diff --git a/icicle/include/icicle/backend/msm_backend.h b/icicle/include/icicle/backend/msm_backend.h index 501e7e7781..ff9b45d341 100644 --- a/icicle/include/icicle/backend/msm_backend.h +++ b/icicle/include/icicle/backend/msm_backend.h @@ -43,7 +43,7 @@ namespace icicle { }(); \ } -#ifdef G2 +#ifdef G2_ENABLED using MsmG2Impl = std::function(); } TEST_F(CurveApiTest, MontConversionProjective) { mont_conversion_test(); } - #ifdef G2 + #ifdef G2_ENABLED TEST_F(CurveApiTest, msmG2) { MSM_test(); } TEST_F(CurveApiTest, MontConversionG2Affine) { mont_conversion_test(); } TEST_F(CurveApiTest, MontConversionG2Projective) { mont_conversion_test(); } @@ -311,7 +311,7 @@ class CurveSanity : public ::testing::Test { }; -#ifdef G2 +#ifdef G2_ENABLED typedef testing::Types CTImplementations; #else typedef testing::Types CTImplementations; diff --git a/icicle/tests/test_polynomial_api.cpp b/icicle/tests/test_polynomial_api.cpp index 124eee26ef..3a4fba0e02 100644 --- a/icicle/tests/test_polynomial_api.cpp +++ b/icicle/tests/test_polynomial_api.cpp @@ -579,7 +579,7 @@ TEST_F(PolynomialTest, slicing) using curve_config::affine_t; using curve_config::projective_t; - #ifdef G2 + #ifdef G2_ENABLED using curve_config::g2_affine_t; using curve_config::g2_projective_t; #endif // G2 @@ -1045,7 +1045,7 @@ TEST_F(PolynomialTest, DummyGroth16) } } - #ifdef G2 + #ifdef G2_ENABLED TEST_F(PolynomialTest, Groth16) { for (auto device : s_registered_devices) { diff --git a/scripts/format_all.sh b/scripts/format_all.sh index dbd104526c..227886dc0a 100755 --- a/scripts/format_all.sh +++ b/scripts/format_all.sh @@ -1,5 +1,17 @@ #!/bin/bash +# Note: Different clang-format versions may produce slightly different results. +# To ensure consistency, you can run clang-format inside a Docker container: +# +# docker run --rm -v $(pwd):/icicle -w /icicle silkeh/clang:19-bookworm bash ./scripts/format_all.sh . +# +# Explanation: +# - `--rm`: Automatically remove the container after it exits. +# - `-v $(pwd):/icicle`: Mounts the current directory into the container at `/icicle`. +# - `-w /icicle`: Sets the working directory inside the container to `/icicle`. +# - `silkeh/clang:19-bookworm`: Specifies the Docker image with clang-format version 19. +# - `bash ./scripts/format_all.sh .`: Executes the formatting script for all files in the current directory. + # Exit immediately if a command exits with a non-zero status set -e diff --git a/scripts/gen_c_api.py b/scripts/gen_c_api.py deleted file mode 100755 index afee7c13ef..0000000000 --- a/scripts/gen_c_api.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -""" -This script generates extern declarators for every curve/field -""" - -from itertools import chain -from pathlib import Path -from string import Template - -API_PATH = Path(__file__).resolve().parent.parent.joinpath("icicle").joinpath("include").joinpath("icicle").joinpath("api") -TEMPLATES_PATH = API_PATH.joinpath("templates") - -""" -Defines a set of curves to generate API for. -A set corresponding to each curve contains headers that shouldn't be included. -""" -CURVES_CONFIG = { - "bn254": [ - "field_ext.template", - "vec_ops_ext.template", - "ntt_ext.template", - ], - "bls12_381": [ - # "poseidon2.template", - "field_ext.template", - "vec_ops_ext.template", - "ntt_ext.template", - ], - "bls12_377": [ - # "poseidon2.template", - "field_ext.template", - "vec_ops_ext.template", - "ntt_ext.template", - ], - "bw6_761": [ - # "poseidon2.template", - "field_ext.template", - "vec_ops_ext.template", - "ntt_ext.template", - ], - "grumpkin": { - # "poseidon2.template", - "curve_g2.template", - "msm_g2.template", - "ecntt.template", - "ntt.template", - "vec_ops_ext.template", - "field_ext.template", - "ntt_ext.template", - }, -} - -""" -Defines a set of fields to generate API for. -A set corresponding to each field contains headers that shouldn't be included. -""" -FIELDS_CONFIG = { - "babybear": { - # "poseidon.template", - }, - "stark252": { - # "poseidon.template", - # "poseidon2.template", - "field_ext.template", - "vec_ops_ext.template", - "ntt_ext.template", - }, - "koalabear": { - } - # "m31": { - # "ntt_ext.template", - # "ntt.template", - # "poseidon.template", - # "poseidon2.template", - # } -} - -COMMON_INCLUDES = [] - -WARN_TEXT = """\ -// WARNING: This file is auto-generated by a script. -// Any changes made to this file may be overwritten. -// Please modify the code generation script instead. -// Path to the code generation script: scripts/gen_c_api.py - -""" - -INCLUDE_ONCE = """\ -#pragma once - -""" - -CURVE_HEADERS = list(TEMPLATES_PATH.joinpath("curves").iterdir()) -FIELD_HEADERS = list(TEMPLATES_PATH.joinpath("fields").iterdir()) - -if __name__ == "__main__": - - # Generate API for ingo_curve - for curve, skip in CURVES_CONFIG.items(): - curve_api = API_PATH.joinpath(f"{curve}.h") - - headers = [header for header in chain(CURVE_HEADERS, FIELD_HEADERS) if header.name not in skip] - - # Collect includes - includes = COMMON_INCLUDES.copy() - includes.append(f'#include "icicle/curves/params/{curve}.h"') - if any(header.name.startswith("ntt") for header in headers): - includes.append('#include "icicle/ntt.h"') - if any(header.name.startswith("msm") for header in headers): - includes.append('#include "icicle/msm.h"') - if any(header.name.startswith("vec_ops") for header in headers): - includes.append('#include "icicle/vec_ops.h"') - if any(header.name.startswith("poseidon.h") for header in headers): - includes.append('#include "poseidon/poseidon.h"') - if any(header.name.startswith("poseidon2.h") for header in headers): - includes.append('#include "poseidon2/poseidon2.h"') - - contents = WARN_TEXT + INCLUDE_ONCE.format(curve.upper()) + "\n".join(includes) + "\n\n" - for header in headers: - with open(header) as f: - template = Template(f.read()) - contents += template.safe_substitute({ - "CURVE": curve, - "FIELD": curve, - }) - contents += "\n\n" - - with open(curve_api, "w") as f: - f.write(contents) - - - # Generate API for ingo_field - for field, skip in FIELDS_CONFIG.items(): - field_api = API_PATH.joinpath(f"{field}.h") - - headers = [header for header in FIELD_HEADERS if header.name not in skip] - - # Collect includes - includes = COMMON_INCLUDES.copy() - includes.append(f'#include "icicle/fields/stark_fields/{field}.h"') - if any(header.name.startswith("ntt") for header in headers): - includes.append('#include "icicle/ntt.h"') - if any(header.name.startswith("vec_ops") for header in headers): - includes.append('#include "icicle/vec_ops.h"') - if any(header.name.startswith("poseidon.h") for header in headers): - includes.append('#include "icicle/poseidon/poseidon.h"') - if any(header.name.startswith("poseidon2.h") for header in headers): - includes.append('#include "icicle/poseidon2/poseidon2.h"') - - contents = WARN_TEXT + INCLUDE_ONCE.format(field.upper()) + "\n".join(includes) + "\n\n" - for header in headers: - with open(header) as f: - template = Template(f.read()) - contents += template.safe_substitute({ - "FIELD": field, - }) - contents += "\n\n" - - with open(field_api, "w") as f: - f.write(contents) \ No newline at end of file diff --git a/scripts/release/build_all.sh b/scripts/release/build_all.sh index 973e458691..f08279697d 100755 --- a/scripts/release/build_all.sh +++ b/scripts/release/build_all.sh @@ -35,6 +35,13 @@ docker pull ghcr.io/ingonyama-zk/icicle-release-ubuntu20-cuda122:latest docker pull ghcr.io/ingonyama-zk/icicle-release-ubi8-cuda122:latest docker pull ghcr.io/ingonyama-zk/icicle-release-ubi9-cuda122:latest +# Alternatively, build the images locally +# echo "Building Docker images..." +# docker build -t icicle-release-ubuntu22-cuda122 -f ./scripts/release/Dockerfile.ubuntu22 . +# docker build -t icicle-release-ubuntu20-cuda122 -f ./scripts/release/Dockerfile.ubuntu20 . +# docker build -t icicle-release-ubi8-cuda122 -f ./scripts/release/Dockerfile.ubi8 . +# docker build -t icicle-release-ubi9-cuda122 -f ./scripts/release/Dockerfile.ubi9 . + # Compile and tar release in each # Inform the user of what is being done From 31a5efb055fa7ba58f906dd5badcefc070f2a41e Mon Sep 17 00:00:00 2001 From: Miki Asa Date: Mon, 20 Jan 2025 08:26:55 +0200 Subject: [PATCH 070/127] avoid warning --- icicle/backend/cpu/include/cpu_sumcheck_transcript.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h index e56e544ceb..f634be62c3 100644 --- a/icicle/backend/cpu/include/cpu_sumcheck_transcript.h +++ b/icicle/backend/cpu/include/cpu_sumcheck_transcript.h @@ -1,5 +1,6 @@ #pragma once #include "icicle/sumcheck/sumcheck_transcript_config.h" +#include template class CpuSumcheckTranscript @@ -60,7 +61,7 @@ class CpuSumcheckTranscript { alpha = S::zero(); const int nof_bytes_to_copy = std::min(sizeof(alpha), hash_result.size()); - std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); + memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); alpha = alpha * S::one(); } From 430afc1970b55374a258d6ccc978f3d8034222d0 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Feb 2025 12:40:21 +0200 Subject: [PATCH 071/127] fri_cpu initial implementation --- icicle/CMakeLists.txt | 1 + icicle/backend/cpu/CMakeLists.txt | 3 + icicle/backend/cpu/include/cpu_fri_backend.h | 211 ++++++++++++++ icicle/backend/cpu/include/cpu_fri_rounds.h | 107 +++++++ .../backend/cpu/include/cpu_fri_transcript.h | 269 ++++++++++++++++++ icicle/backend/cpu/src/field/cpu_fri.cpp | 25 ++ icicle/cmake/field.cmake | 1 + icicle/cmake/fields_and_curves.cmake | 14 +- icicle/cmake/target_editor.cmake | 10 + icicle/include/icicle/backend/fri_backend.h | 116 ++++++++ icicle/include/icicle/fri/fri.h | 111 ++++++++ icicle/include/icicle/fri/fri_config.h | 37 +++ icicle/include/icicle/fri/fri_proof.h | 98 +++++++ .../icicle/fri/fri_transcript_config.h | 134 +++++++++ icicle/include/icicle/merkle/merkle_proof.h | 19 ++ icicle/include/icicle/utils/rand_gen.h | 29 ++ icicle/src/fri/fri.cpp | 58 ++++ icicle/src/fri/fri_c_api.cpp | 211 ++++++++++++++ icicle/tests/CMakeLists.txt | 2 +- 19 files changed, 1448 insertions(+), 8 deletions(-) create mode 100644 icicle/backend/cpu/include/cpu_fri_backend.h create mode 100644 icicle/backend/cpu/include/cpu_fri_rounds.h create mode 100644 icicle/backend/cpu/include/cpu_fri_transcript.h create mode 100644 icicle/backend/cpu/src/field/cpu_fri.cpp create mode 100644 icicle/include/icicle/backend/fri_backend.h create mode 100644 icicle/include/icicle/fri/fri.h create mode 100644 icicle/include/icicle/fri/fri_config.h create mode 100644 icicle/include/icicle/fri/fri_proof.h create mode 100644 icicle/include/icicle/fri/fri_transcript_config.h create mode 100644 icicle/src/fri/fri.cpp create mode 100644 icicle/src/fri/fri_c_api.cpp diff --git a/icicle/CMakeLists.txt b/icicle/CMakeLists.txt index 45d0e3db71..1d76d731e0 100644 --- a/icicle/CMakeLists.txt +++ b/icicle/CMakeLists.txt @@ -37,6 +37,7 @@ option(HASH "Build hashes and tree builders" ON) option(POSEIDON "Build poseidon hash" ON) option(POSEIDON2 "Build poseidon2 hash" ON) option(SUMCHECK "Build sumcheck" ON) +option(FRI "Build fri" ON) option(SANITIZE "Enable memory address sanitizer" OFF) # address sanitizer diff --git a/icicle/backend/cpu/CMakeLists.txt b/icicle/backend/cpu/CMakeLists.txt index aa31e0c517..7fad6c689a 100644 --- a/icicle/backend/cpu/CMakeLists.txt +++ b/icicle/backend/cpu/CMakeLists.txt @@ -48,6 +48,9 @@ if (FIELD) if(SUMCHECK) target_sources(icicle_field PRIVATE src/field/cpu_sumcheck.cpp) endif() + if(FRI) + target_sources(icicle_field PRIVATE src/field/cpu_fri.cpp) + endif() target_include_directories(icicle_field PRIVATE include) endif() # FIELD diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h new file mode 100644 index 0000000000..b7319cd9da --- /dev/null +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -0,0 +1,211 @@ +#pragma once + +#include +#include +#include +#include +#include "icicle/errors.h" +#include "cpu_fri_transcript.h" +#include "icicle/backend/fri_backend.h" +#include "cpu_fri_transcript.h" +#include "cpu_fri_rounds.h" +#include "cpu_ntt_domain.h" +#include "icicle/utils/log.h" + +namespace icicle { + template + class CpuFriBackend : public FriBackend + { + public: + /** + * @brief Constructor for the case where you only have a Merkle hash function & store layer data in a fixed tree. + * + * @param input_size The size of the input polynomial - number of evaluations. + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree Stopping degree threshold for the final polynomial. + * @param hash_for_merkle_tree The hash function used for Merkle tree commitments. + * @param output_store_min_layer Optional parameter (default=0). Controls partial storage of layers. + */ + CpuFriBackend(const size_t input_size, const size_t folding_factor, const size_t stopping_degree, const Hash& hash_for_merkle_tree, const uint64_t output_store_min_layer = 0) + : FriBackend(folding_factor, stopping_degree), + m_log_input_size(static_cast(std::log2(static_cast(input_size)))), + m_input_size(input_size), + m_fri_rounds(m_log_input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer) + { + } + + /** + * @brief Constructor that accepts an existing array of Merkle trees. + * + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree Stopping degree threshold for the final polynomial. + * @param merkle_trees A moved vector of MerkleTree pointers. + */ + CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees) + : FriBackend(folding_factor, stopping_degree), + m_log_input_size(merkle_trees.size()), + m_input_size(pow(2, m_log_input_size)), + m_fri_rounds(std::move(merkle_trees)) + { + } + + eIcicleError get_fri_proof( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + FriProof& fri_proof /*out*/) override + { + if (fri_config.use_extension_field) { + ICICLE_LOG_ERROR << "FriConfig::use_extension_field = true is currently unsupported"; + return eIcicleError::API_NOT_IMPLEMENTED; + } + ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; + + CpuFriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); + + // Determine the number of folding rounds + size_t df = this->m_stopping_degree; + size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + size_t nof_fri_rounds = (m_log_input_size > log_df_plus_1) ? (m_log_input_size - log_df_plus_1) : 0; + + //commit fold phase + ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, nof_fri_rounds, fri_proof)); + + //proof of work + if (fri_config.pow_bits != 0){ + ICICLE_CHECK(proof_of_work(transcript, fri_config.pow_bits, fri_proof)); + } + + //query phase + ICICLE_CHECK(query_phase(transcript, fri_config, nof_fri_rounds, fri_proof)); + + return eIcicleError::SUCCESS; + } + + private: + FriRounds m_fri_rounds; // Holds intermediate rounds + const size_t m_log_input_size; // Log size of the input polynomial + const size_t m_input_size; // Size of the input polynomial + + /** + * @brief Perform the commit-fold phase of the FRI protocol. + * + * @param input_data The initial polynomial evaluations. + * @param fri_proof The proof object to update. + * @param transcript The transcript to generate challenges. + * @return eIcicleError Error code indicating success or failure. + */ + eIcicleError commit_fold_phase(const F* input_data, CpuFriTranscript& transcript, const FriConfig& fri_config, size_t nof_fri_rounds, FriProof& fri_proof){ + ICICLE_ASSERT(this->m_folding_factor==2) << "Folding factor must be 2"; + + const F* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); + uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); + + // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with capacity 2^(m_log_input_size - round_idx). + F* round_evals = m_fri_rounds.get_round_evals(0); + // Resize the persistent vector so it holds m_input_size elements. + round_evals->resize(m_input_size); + // Copy input_data into the persistent storage. + std::copy(input_data, input_data + m_input_size, round_evals->begin()); + + size_t current_size = m_input_size; + size_t current_log_size = m_log_input_size; + + for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx){ + // if (current_size == (df + 1)) { FIXME SHANIE - do I need this? + // fri_proof.finalpoly->assign(round_evals->begin(), round_evals->end()); + // break; + // } + + // Merkle tree for the current round_idx + MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); + current_round_tree->build(reinterpret_cast(round_evals->data()), sizeof(F), MerkleTreeConfig()); + auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); + ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; + + // FIXME SHANIE - do I need to add the root to the proof here? + + // Add root to transcript and get alpha + std::vector merkle_commit(root_ptr, root_ptr + root_size); //FIXME SHANIE - is this the right way to convert? + F alpha = transcript.get_alpha(merkle_commit); + + // Fold the evaluations + size_t half = current_size>>1; + std::vector peven(half); + std::vector podd(half); + + for (size_t i = 0; i < half; ++i){ + peven[i] = (round_evals[i] + round_evals[i + half]) * F::inv_log_size(1); + uint64_t tw_idx = domain_max_size - ((domain_max_size>>current_log_size) * i); + podd[i] = ((round_evals[i] - round_evals[i + half]) * F::inv_log_size(1)) * twiddles[tw_idx]; + } + + if (round_idx == nof_fri_rounds - 1){ + round_evals = fri_proof.get_final_poly(); + } else { + round_evals = m_fri_rounds.get_round_evals(round_idx + 1); + } + + for (size_t i = 0; i < half; ++i){ + (*round_evals)[i] = peven[i] + (alpha * podd[i]); + } + + current_size>>=1; + current_log_size--; + } + + return eIcicleError::SUCCESS; + } + + eIcicleError proof_of_work(CpuFriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ + for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) + { + if(transcript.hash_and_get_nof_leading_zero_bits(nonce, pow_bits) == pow_bits){ + transcript.set_pow_nonce(nonce); + fri_proof.set_pow_nonce(nonce); + return eIcicleError::SUCCESS; + } + } + ICICLE_LOG_ERROR << "Failed to find a proof-of-work nonce"; + return eIcicleError::UNKNOWN_ERROR; + } + + + /** + * @brief Perform the query phase of the FRI protocol. + * + * @param transcript The transcript object. + * @param fri_config The FRI configuration object. + * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. + * @return eIcicleError + */ + eIcicleError query_phase(CpuFriTranscript& transcript, const FriConfig& fri_config, size_t nof_fri_rounds, FriProof& fri_proof) + { + ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; + size_t seed = transcript.get_seed_for_query_phase(); + seed_rand_generator(seed); + std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, fri_proof.get_final_poly()->size(), m_input_size); + + for (size_t q = 0; q < query_indices.size(); q++){ + size_t query = query_indices[q]; + for (size_t round_idx = 0; round_idx < nof_fri_rounds; round_idx++){ + size_t round_size = (1ULL << (m_log_input_size - round_idx)); + size_t query_idx = query % round_size; + size_t query_idx_sym = (query + (round_size >> 1)) % round_size; + std::vector* round_evals = m_fri_rounds.get_round_evals(round_idx); + const std::byte* leaves = reinterpret_cast(round_evals->data()); + uint64_t leaves_size = sizeof(F); + + MerkleProof& proof_ref = fri_proof.get_query_proof(query_idx, round_idx); + eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), query_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); + if (err != eIcicleError::SUCCESS) return err; + MerkleProof& proof_ref_sym = fri_proof.get_query_proof(query_idx_sym, round_idx); + eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), query_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); + if (err_sym != eIcicleError::SUCCESS) return err_sym; + } + } + return eIcicleError::SUCCESS; + } + }; + +} // namespace icicle diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h new file mode 100644 index 0000000000..f5a4bfd9fc --- /dev/null +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include "icicle/merkle/merkle_tree.h" +#include "icicle/errors.h" +#include "icicle/hash/hash.h" +#include "icicle/hash/keccak.h" +#include "icicle/utils/log.h" + + +namespace icicle { + +template +class FriRounds +{ +public: + /** + * @brief Constructor that stores parameters for building Merkle trees. + * + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree The polynomial degree threshold to stop folding. + * @param hash_for_merkle_tree Hash function used for each Merkle tree layer. + * @param output_store_min_layer Minimum layer index to store fully in the output (default=0). + */ + FriRounds(size_t log_input_size, + size_t folding_factor, + size_t stopping_degree, + const Hash& hash_for_merkle_tree, + uint64_t output_store_min_layer = 0) + { + ICICLE_ASSERT(folding_factor == 2) << "Only folding factor of 2 is supported"; + size_t df = stopping_degree; + size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + + m_round_evals.resize(fold_rounds); + m_merkle_trees.reserve(fold_rounds); + std::vector hashes_for_merkle_tree_vec(fold_rounds, hash_for_merkle_tree); + for (size_t i = 0; i < fold_rounds; i++) { + m_merkle_trees.push_back(std::make_unique(hashes_for_merkle_tree_vec, sizeof(F), output_store_min_layer)); + hashes_for_merkle_tree_vec.pop_back(); + m_round_evals[i] = std::make_unique>(); + m_round_evals[i]->reserve(1ULL << (log_input_size - i)); + } + } + + /** + * @brief Constructor that accepts an already-existing array of Merkle trees. + * Ownership is transferred from the caller. + * + * @param merkle_trees A moved vector of `unique_ptr`. + */ + FriRounds(std::vector>&& merkle_trees) + : m_merkle_trees(std::move(merkle_trees)) + { + size_t fold_rounds = m_merkle_trees.size(); + m_round_evals.resize(fold_rounds); + for (size_t i = 0; i < fold_rounds; i++) { + m_round_evals[i] = std::make_unique>(); + m_round_evals[i]->reserve(1ULL << (fold_rounds - i)); + } + } + + /** + * @brief Get the Merkle tree for a specific fri round. + * + * @param round_idx The index of the fri round. + * @return A pointer to the Merkle tree backend for the specified fri round. + */ + MerkleTree* get_merkle_tree(size_t round_idx) + { + ICICLE_ASSERT(round_idx < m_merkle_trees.size()) << "round index out of bounds"; + return m_merkle_trees[round_idx].get(); + } + + F* get_round_evals(size_t round_idx) + { + ICICLE_ASSERT(round_idx < m_round_evals.size()) << "round index out of bounds"; + return m_round_evals[round_idx].get(); + } + + /** + * @brief Retrieve the Merkle root for a specific fri round. + * + * @param round_idx The index of the round. + * @return A pair containing a pointer to the Merkle root bytes and its size. + */ + std::pair get_merkle_root_for_round(size_t round_idx) const + { + if (round_idx >= m_merkle_trees.size()) { + return {nullptr, 0}; + } + return m_merkle_trees[round_idx]->get_merkle_root(); + } + +private: + // Persistent polynomial evaluations for each round (heap allocated). + // For round i, the expected length is 2^(m_initial_log_size - i). + std::vector> m_round_evals; + + // Holds unique ownership of each MerkleTree for each round. m_merkle_trees[i] is the tree for round i. + std::vector> m_merkle_trees; +}; + +} // namespace icicle diff --git a/icicle/backend/cpu/include/cpu_fri_transcript.h b/icicle/backend/cpu/include/cpu_fri_transcript.h new file mode 100644 index 0000000000..8e0566c7b2 --- /dev/null +++ b/icicle/backend/cpu/include/cpu_fri_transcript.h @@ -0,0 +1,269 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "icicle/fri/fri_transcript_config.h" +#include "icicle/errors.h" +#include "icicle/hash/hash.h" + +namespace icicle { + +template +class CpuFriTranscript +{ +public: + CpuFriTranscript(FriTranscriptConfig&& transcript_config, const size_t log_input_size) + : m_transcript_config(std::move(transcript_config)) + , m_log_input_size(log_input_size) + , m_prev_alpha(F::zero()) + , m_first_round(true) + , m_pow_nonce(0) + { + m_entry_0.clear(); + m_first_round = true; + } + + /** + * @brief Add a Merkle commit to the transcript and generate a new alpha challenge. + * + * @param merkle_commit The raw bytes of the Merkle commit. + * @return A field element alpha derived via Fiat-Shamir. + */ + F get_alpha(const std::vector& merkle_commit) + { + ICICLE_ASSERT(m_transcript_config.get_domain_separator_label().size() > 0) << "Domain separator label must be set"; + // Prepare a buffer for hashing + m_entry_0.reserve(1024); // pre-allocate some space + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + + // Build the round's hash input + if (m_first_round) { + build_entry_0(); + build_hash_input_round_0(hash_input); + m_first_round = false; + } else { + build_hash_input_round_i(hash_input); + } + append_data(hash_input, merkle_commit); + + // Hash the input and return alpha + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + reduce_hash_result_to_field(m_prev_alpha, hash_result); + return m_prev_alpha; + } + + size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce, const size_t pow_bits) + { + // Prepare a buffer for hashing + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + + // Build the hash input + build_hash_input_pow(hash_input); + + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + + return count_leading_zero_bits(hash_result); + } + + /** + * @brief Add a proof-of-work nonce to the transcript, to be included in subsequent rounds. + * @param pow_nonce The proof-of-work nonce. + */ + void set_pow_nonce(uint32_t pow_nonce) + { + m_pow_nonce = pow_nonce; + } + + size_t get_seed_for_query_phase() + { + // Prepare a buffer for hashing + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + + // Build the hash input + build_hash_input_query_phase(hash_input); + + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + uint64_t seed = bytes_to_uint_64(hash_result); + return seed; + } + +private: + const FriTranscriptConfig m_transcript_config; // Transcript configuration (labels, seeds, etc.) + const size_t m_log_input_size; // Logarithm of the initial input size + const HashConfig m_hash_config; // hash config - default + bool m_first_round; // Indicates if this is the first round + std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds + F m_prev_alpha; // The previous alpha generated + uint64_t m_pow_nonce; // Proof-of-work nonce - optional + + /** + * @brief Append a vector of bytes to another vector of bytes. + * @param dest (OUT) Destination byte vector. + * @param src Source byte vector. + */ + void append_data(std::vector& dest, const std::vector& src) + { + dest.insert(dest.end(), src.begin(), src.end()); + } + + /** + * @brief Append an unsigned 64-bit integer to the byte vector (little-endian). + * @param dest (OUT) Destination byte vector. + * @param value The 64-bit value to append. + */ + void append_u32(std::vector& dest, uint32_t value) + { + const std::byte* data_bytes = reinterpret_cast(&value); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint32_t)); + } + + /** + * @brief Append a field element to the byte vector. + * @param dest (OUT) Destination byte vector. + * @param field The field element to append. + */ + void append_field(std::vector& dest, const F& field) + { + const std::byte* data_bytes = reinterpret_cast(field.limbs_storage.limbs); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); + } + + /** + * @brief Convert a hash output into a field element by copying a minimal number of bytes. + * @param alpha (OUT) The resulting field element. + * @param hash_result A buffer of bytes (from the hash function). + */ + void reduce_hash_result_to_field(F& alpha, const std::vector& hash_result) + { + alpha = F::zero(); + const int nof_bytes_to_copy = std::min(sizeof(alpha), static_cast(hash_result.size())); + std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); + alpha = alpha * F::one(); + } + + + /** + * @brief Build the hash input for round 0 (commit phase 0). + * + * DS =[domain_seperator||log_2(initial_domain_size).LE32()] + * entry_0 =[DS||public.LE32()] + * + */ + void build_entry_0() + { + append_data(m_entry_0, m_transcript_config.get_domain_separator_label()); + append_u32(m_entry_0, m_log_input_size); + append_data(m_entry_0, m_transcript_config.get_public_state()); + } + + + /** + * @brief Build the hash input for round 0 (commit phase 0). + * + * alpha_0 = hash(entry_0||rng||round_challenge_label[u8]||commit_label[u8]|| root_0.LE32()).to_ext_field() + * root is added outside this function + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_round_0(std::vector& hash_input) + { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_transcript_config.get_seed_rng()); + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + append_data(hash_input, m_transcript_config.get_commit_phase_label()); + } + + /** + * @brief Build the hash input for the subsequent rounds (commit phase i). + * + * alpha_n = hash(entry0||alpha_n-1||round_challenge_label[u8]||commit_label[u8]|| root_n.LE32()).to_ext_field() + * root is added outside this function + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_round_i(std::vector& hash_input) + { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + append_data(hash_input, m_transcript_config.get_commit_phase_label()); + } + + /** + * @brief Build the hash input for the proof-of-work nonce. + * hash_input = entry_0||alpha_{n-1}||"nonce"||nonce + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_pow(std::vector& hash_input, uint32_t temp_pow_nonce) + { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); + append_data(hash_input, m_transcript_config.get_nonce_label()); + append_u32(hash_input, temp_pow_nonce); + } + + /** + * @brief Build the hash input for the query phase. + * hash_input = entry_0||alpha_{n-1}||"query"||seed + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_query_phase(std::vector& hash_input) + { + if (m_pow_nonce ==0){ + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); + } else { + append_data(hash_input, m_entry_0); + append_data(hash_input, m_transcript_config.get_nonce_label()); + append_u32(hash_input, m_pow_nonce); + } + } + + static size_t count_leading_zero_bits(const std::vector& data) + { + size_t zero_bits = 0; + for (size_t i = 0; i < data.size(); i++) { + uint8_t byte_val = static_cast(data[i]); + if (byte_val == 0) { + zero_bits += 8; + } else { + for (int bit = 7; bit >= 0; bit--) { + if ((byte_val & (1 << bit)) == 0) { + zero_bits++; + } else { + return zero_bits; + } + } + break; + } + } + return zero_bits; + } + + + uint64_t bytes_to_uint_64(const std::vector& data) + { + uint64_t result = 0; + for (size_t i = 0; i < sizeof(uint64_t); i++) { + result |= static_cast(data[i]) << (i * 8); + } + return result; + } + +}; + +} // namespace icicle diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp new file mode 100644 index 0000000000..0f10222acd --- /dev/null +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -0,0 +1,25 @@ +#include "icicle/backend/fri_backend.h" +#include "cpu_fri_backend.h" + +using namespace field_config; + +namespace icicle { + + template + eIcicleError cpu_create_fri_backend(const Device& device, const size_t input_size, const size_t folding_factor, const size_t stopping_degree, const Hash& hash_for_merkle_tree, const uint64_t output_store_min_layer, std::shared_ptr>& backend /*OUT*/) + { + backend = std::make_shared>(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + return eIcicleError::SUCCESS; + } + + // template + // eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees, std::shared_ptr>& backend /*OUT*/) + // { + // backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); + // return eIcicleError::SUCCESS; + // } + + // REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); + REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); + +} // namespace icicle \ No newline at end of file diff --git a/icicle/cmake/field.cmake b/icicle/cmake/field.cmake index b7864c869a..02a51fdd99 100644 --- a/icicle/cmake/field.cmake +++ b/icicle/cmake/field.cmake @@ -53,6 +53,7 @@ function(setup_field_target FIELD FIELD_INDEX FEATURES_STRING) handle_poseidon(icicle_field "${FEATURES_LIST}") handle_poseidon2(icicle_field "${FEATURES_LIST}") handle_sumcheck(icicle_field "${FEATURES_LIST}") + handle_fri(icicle_field "${FEATURES_LIST}") # Add additional feature handling calls here set_target_properties(icicle_field PROPERTIES OUTPUT_NAME "icicle_field_${FIELD}") diff --git a/icicle/cmake/fields_and_curves.cmake b/icicle/cmake/fields_and_curves.cmake index e67f4fd6a7..aa20be9337 100644 --- a/icicle/cmake/fields_and_curves.cmake +++ b/icicle/cmake/fields_and_curves.cmake @@ -2,18 +2,18 @@ # Define available fields with an index and their supported features # Format: index:field:features set(ICICLE_FIELDS - 1001:babybear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK - 1002:stark252:NTT,POSEIDON,POSEIDON2,SUMCHECK + 1001:babybear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK,FRI + 1002:stark252:NTT,POSEIDON,POSEIDON2,SUMCHECK,FRI 1003:m31:EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK - 1004:koalabear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK + 1004:koalabear:NTT,EXT_FIELD,POSEIDON,POSEIDON2,SUMCHECK,FRI ) # Define available curves with an index and their supported features # Format: index:curve:features set(ICICLE_CURVES - 1:bn254:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK - 2:bls12_381:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK - 3:bls12_377:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK - 4:bw6_761:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK + 1:bn254:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK,FRI + 2:bls12_381:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK,FRI + 3:bls12_377:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK,FRI + 4:bw6_761:NTT,MSM,G2,ECNTT,POSEIDON,POSEIDON2,SUMCHECK,FRI 5:grumpkin:MSM,POSEIDON,POSEIDON2,SUMCHECK ) diff --git a/icicle/cmake/target_editor.cmake b/icicle/cmake/target_editor.cmake index 64d330aab9..2b823debc4 100644 --- a/icicle/cmake/target_editor.cmake +++ b/icicle/cmake/target_editor.cmake @@ -99,3 +99,13 @@ function(handle_sumcheck TARGET FEATURE_LIST) set(SUMCHECK OFF CACHE BOOL "SUMCHECK not available for this field" FORCE) endif() endfunction() + +function(handle_fri TARGET FEATURE_LIST) + if(FRI AND "FRI" IN_LIST FEATURE_LIST) + target_compile_definitions(${TARGET} PUBLIC FRI=${FRI}) + target_sources(${TARGET} PRIVATE src/fri/fri.cpp src/fri/fri_c_api.cpp) + set(FRI ON CACHE BOOL "Enable FRI feature" FORCE) + else() + set(FRI OFF CACHE BOOL "FRI not available for this field" FORCE) + endif() +endfunction() diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h new file mode 100644 index 0000000000..532f81fff8 --- /dev/null +++ b/icicle/include/icicle/backend/fri_backend.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include + +#include "icicle/errors.h" +#include "icicle/runtime.h" +#include "icicle/fri/fri_config.h" +#include "icicle/fri/fri_proof.h" +#include "icicle/fri/fri_transcript_config.h" +#include "icicle/fields/field_config.h" +#include "icicle/backend/merkle/merkle_tree_backend.h" + +using namespace field_config; + +namespace icicle { + +/** + * @brief Abstract base class for FRI backend implementations. + * @tparam F Field type used in the FRI protocol. + */ +template +class FriBackend +{ +public: + /** + * @brief Constructor for the case where you only have a Merkle hash function & store layer data in a fixed tree. + * + * @param input_size The size of the input polynomial - number of evaluations. + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree Stopping degree threshold for the final polynomial. + * @param hash_for_merkle_tree The hash function used for Merkle tree commitments. + * @param output_store_min_layer Optional parameter (default=0). Controls partial storage of layers. + */ + FriBackend(const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& hash_for_merkle_tree, + uint64_t output_store_min_layer = 0) + : m_folding_factor(folding_factor) + , m_stopping_degree(stopping_degree) + {} + + /** + * @brief Constructor that accepts an existing array of Merkle trees. + * + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree Stopping degree threshold for the final polynomial. + * @param merkle_trees A moved vector of MerkleTree pointers. + */ + FriBackend(const size_t folding_factor, + const size_t stopping_degree, + std::vector&& merkle_trees) + : m_folding_factor(folding_factor) + , m_stopping_degree(stopping_degree) + {} + + virtual ~FriBackend() = default; + + /** + * @brief Generate the FRI proof from given inputs. + * + * @param fri_config Configuration for FRI operations (e.g., proof-of-work bits, queries). + * @param fri_transcript_config Configuration for encoding/hashing FRI messages (Fiat-Shamir). + * @param input_data Evaluations of the polynomial (or other relevant data). + * @param fri_proof (OUT) A FriProof object to store the proof's Merkle layers, final poly, etc. + * @return eIcicleError Error code indicating success or failure. + */ + virtual eIcicleError get_fri_proof( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + FriProof& fri_proof + ) = 0; + +protected: + const size_t m_folding_factor; + const size_t m_stopping_degree; +}; + +/*************************** Backend Factory Registration ***************************/ + +//FIXME SHANIE - need two FriFactoryImpl + +/** + * @brief A function signature for creating a FriBackend instance for a specific device. + */ +template +using FriFactoryImpl = + std::function>& backend /*OUT*/)>; + +/** + * @brief Register a FRI backend factory for a specific device type. + * + * @param deviceType String identifier for the device type. + * @param impl A factory function that creates a FriBackend. + */ +void register_fri_factory(const std::string& deviceType, FriFactoryImpl impl); + +/** + * @brief Macro to register a FRI backend factory. + * + * This macro registers a factory function for a specific backend by calling + * `register_fri_factory` at runtime. + * + */ +#define REGISTER_FRI_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ + namespace { \ + static bool UNIQUE(_reg_fri) = []() -> bool { \ + register_fri_factory(DEVICE_TYPE, FUNC); \ + return true; \ + }(); \ + } + +} // namespace icicle diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h new file mode 100644 index 0000000000..1bcacd1d26 --- /dev/null +++ b/icicle/include/icicle/fri/fri.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include "icicle/errors.h" +#include "icicle/backend/fri_backend.h" +#include "icicle/fri/fri_config.h" +#include "icicle/fri/fri_proof.h" +#include "icicle/fri/fri_transcript_config.h" +#include "icicle/hash/hash.h" +#include "icicle/merkle/merkle_tree.h" + +namespace icicle { + +/** + * @brief Forward declaration for the FRI class template. + */ +template +class Fri; + +/** + * @brief Constructor for the case where only binary Merkle trees are used + * with a constant hash function. + * + * @param input_size The size of the input polynomial - number of evaluations. + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree The minimal polynomial degree at which to stop folding. + * @param hash_for_merkle_tree The hash function used for the Merkle commitments. + * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. + * @return A `Fri` object built around the chosen backend. + */ +template +Fri create_fri( + size_t input_size, + size_t folding_factor, + size_t stopping_degree, + Hash& hash_for_merkle_tree, + uint64_t output_store_min_layer = 0); + +/** + * @brief Constructor for the case where Merkle trees are already given. + * + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree The minimal polynomial degree at which to stop folding. + * @param merkle_trees A moved vector of `MerkleTree` objects. + * @return A `Fri` object built around the chosen backend. + */ +template +Fri create_fri( + size_t folding_factor, + size_t stopping_degree, + std::vector&& merkle_trees); + +/** + * @brief Class for performing FRI operations. + * + * This class provides a high-level interface for constructing and managing a FRI proof. + * + * @tparam F The field type used in the FRI protocol. + */ +template +class Fri +{ +public: + /** + * @brief Constructor for the Fri class. + * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. + */ + explicit Fri(std::shared_ptr> backend) + : m_backend(std::move(backend)) + {} + + /** + * @brief Generate a FRI proof from the given polynomial evaluations (or input data). + * @param fri_config Configuration for FRI operations (e.g., proof-of-work, queries). + * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). + * @param input_data Evaluations or other relevant data for constructing the proof. + * @param fri_proof Reference to a FriProof object (output). + * @return An eIcicleError indicating success or failure. + */ + eIcicleError get_fri_proof( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const std::vector& input_data, + FriProof& fri_proof /* OUT */) const + { + return m_backend->get_fri_proof(fri_config, fri_transcript_config, input_data, fri_proof); + } + + /** + * @brief Verify a FRI proof. + * @param fri_config Configuration for FRI operations. + * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). + * @param fri_proof The proof object to verify. + * @param verification_pass (OUT) Set to true if verification succeeds, false otherwise. + * @return An eIcicleError indicating success or failure. + */ + eIcicleError verify( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const FriProof& fri_proof, + bool& verification_pass /* OUT */) const + { + return eIcicleError::API_NOT_IMPLEMENTED; + } + +private: + std::shared_ptr> m_backend; // Shared pointer to the backend for FRI operations. +}; + +} // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h new file mode 100644 index 0000000000..5b6335d8e0 --- /dev/null +++ b/icicle/include/icicle/fri/fri_config.h @@ -0,0 +1,37 @@ +#pragma once + +#include "icicle/runtime.h" +#include "icicle/config_extension.h" + +namespace icicle { + + /** + * @brief Configuration structure for FRI operations. + * + * This structure holds the configuration options for FRI operations. + * It provides control over proof-of-work requirements, query iterations, + * execution modes (synchronous/asynchronous), and device/host data placement. + * It also supports backend-specific extensions for customization. + */ + struct FriConfig { + icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. + bool use_extension_field = false; // If true, then use extension field for the fiat shamir result. Recommended for small fields for security. TODO SHANIE - this was not part of the design plan, do we need to add this like in sumcheck? + size_t pow_bits = 0; // Number of leading zeros required for proof-of-work. Default is 0. + size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. + bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. + bool are_outputs_on_device = false; // True if outputs reside on the device, false if on the host. Default is false. + bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. + ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. Default is nullptr. + }; + + /** + * @brief Generates a default configuration for FRI operations. + * + * This function provides a default configuration for FRI operations with synchronous execution + * and all data (inputs, outputs, etc.) residing on the host (CPU). + * + * @return A default FriConfig with host-based execution and no backend-specific extensions. + */ + static FriConfig default_fri_config() { return FriConfig(); } + +} // namespace icicle diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h new file mode 100644 index 0000000000..e9f2add3ce --- /dev/null +++ b/icicle/include/icicle/fri/fri_proof.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include "icicle/backend/merkle/merkle_tree_backend.h" +#include "icicle/merkle/merkle_tree.h" + + +namespace icicle { + + /** + * @brief Represents a FRI proof. + * + * @tparam F Type of the field element (e.g., prime field or extension field elements). + */ + + template + class FriProof + { + public: + // Constructor + FriProof() : m_pow_nonce(0){} + + /** + * @brief Initialize the Merkle proofs and final polynomial storage. + * + * @param nof_queries Number of queries in the proof. + * @param final_poly_degree Degree of the final polynomial. + * @param nof_fri_rounds Number of FRI rounds (rounds). + */ + void init(int nof_queries, int final_poly_degree, int nof_fri_rounds) + { + ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) + << "Number of queries and FRI rounds must be > 0"; + + // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns + m_query_proofs.resize(nof_queries, std::vector(nof_fri_rounds)); + + // Initialize the final polynomial + m_final_poly.resize(final_poly_degree + 1, F::zero()); + } + + /** + * @brief Get a reference to a specific Merkle proof. + * + * @param query_idx Index of the query. + * @param round_idx Index of the round (FRI round). + * @return Reference to the Merkle proof at the specified position. + */ + MerkleProof& get_query_proof(int query_idx, int round_idx) + { + if (query_idx < 0 || query_idx >= m_query_proofs.size()) { + throw std::out_of_range("Invalid query index"); + } + if (round_idx < 0 || round_idx >= m_query_proofs[query_idx].size()) { + throw std::out_of_range("Invalid round index"); + } + return m_query_proofs[query_idx][round_idx]; + } + + void set_pow_nonce(uint64_t pow_nonce) + { + m_pow_nonce = pow_nonce; + } + + uint64_t get_pow_nonce() const + { + return m_pow_nonce; + } + + //get pointer to the final polynomial + std::vector* get_final_poly() const + { + return m_final_poly.get(); + } + + private: + std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf + std::unique_ptr> m_final_poly; // Final polynomial (constant in canonical FRI) + uint64_t m_pow_nonce; // Proof-of-work nonce + + public: + // for debug + void print_proof() + { + std::cout << "FRI Proof:" << std::endl; + for (int query_idx = 0; query_idx < m_query_proofs.size(); query_idx++) { + std::cout << " Query " << query_idx << ":" << std::endl; + for (int round_idx = 0; round_idx < m_query_proofs[query_idx].size(); round_idx++) { + std::cout << " round " << round_idx << ":" << std::endl; + m_query_proofs[query_idx][round_idx].print_proof(); + } + } + } + }; + +} // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/fri/fri_transcript_config.h b/icicle/include/icicle/fri/fri_transcript_config.h new file mode 100644 index 0000000000..3a7168b042 --- /dev/null +++ b/icicle/include/icicle/fri/fri_transcript_config.h @@ -0,0 +1,134 @@ +#pragma once + +#include "icicle/hash/hash.h" +#include "icicle/hash/keccak.h" +#include // for std::strlen +#include +#include + +namespace icicle { + +/** + * @brief Configuration for encoding and hashing messages in the FRI protocol. + * + * @tparam F Type of the field element (e.g., prime field or extension field elements). + */ +template +class FriTranscriptConfig +{ +public: + // Default Constructor + FriTranscriptConfig() + : m_hasher(create_keccak_256_hash()), + m_domain_separator_label({}), + m_commit_phase_label({}), + m_nonce_label(cstr_to_bytes("nonce")), + m_public({}), + m_seed_rng(F::zero()) + { + } + + // Constructor with std::byte vectors for labels + FriTranscriptConfig( + Hash hasher, + std::vector&& domain_separator_label, + std::vector&& round_challenge_label, + std::vector&& commit_phase_label, + std::vector&& nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), + m_domain_separator_label(std::move(domain_separator_label)), + m_round_challenge_label(std::move(round_challenge_label)), + m_commit_phase_label(std::move(commit_phase_label)), + m_nonce_label(std::move(nonce_label)), + m_public(std::move(public_state)), + m_seed_rng(seed_rng) + { + } + + // Constructor with const char* arguments + FriTranscriptConfig( + Hash hasher, + const char* domain_separator_label, + const char* round_challenge_label, + const char* commit_phase_label, + const char* nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), + m_domain_separator_label(cstr_to_bytes(domain_separator_label)), + m_round_challenge_label(cstr_to_bytes(round_challenge_label)), + m_commit_phase_label(cstr_to_bytes(commit_phase_label)), + m_nonce_label(cstr_to_bytes(nonce_label)), + m_public(std::move(public_state)), + m_seed_rng(seed_rng) + { + } + + // Move Constructor + FriTranscriptConfig(FriTranscriptConfig&& other) noexcept + : m_hasher(std::move(other.m_hasher)), + m_domain_separator_label(std::move(other.m_domain_separator_label)), + m_round_challenge_label(std::move(other.m_round_challenge_label)), + m_commit_phase_label(std::move(other.m_commit_phase_label)), + m_nonce_label(std::move(other.m_nonce_label)), + m_public(std::move(other.m_public)), + m_seed_rng(other.m_seed_rng) + { + } + + const Hash& get_hasher() const { return m_hasher; } + + const std::vector& get_domain_separator_label() const { + return m_domain_separator_label; + } + + const std::vector& get_round_challenge_label() const { + return m_round_challenge_label; + } + + const std::vector& get_commit_phase_label() const { + return m_commit_phase_label; + } + + const std::vector& get_nonce_label() const { + return m_nonce_label; + } + + const std::vector& get_public_state() const { + return m_public; + } + + const F& get_seed_rng() const { return m_seed_rng; } + +private: + + // Hash function used for randomness generation. + Hash m_hasher; + + // Common transcript labels + std::vector m_domain_separator_label; + std::vector m_round_challenge_label; + + // FRI-specific labels + std::vector m_commit_phase_label; + std::vector m_nonce_label; + std::vector m_public; + + // Seed for initializing the RNG. + F m_seed_rng; + + + static inline std::vector cstr_to_bytes(const char* str) + { + if (str == nullptr) return {}; + const size_t length = std::strlen(str); + return { + reinterpret_cast(str), + reinterpret_cast(str) + length + }; + } +}; + +} // namespace icicle diff --git a/icicle/include/icicle/merkle/merkle_proof.h b/icicle/include/icicle/merkle/merkle_proof.h index 67d8811713..d89be1bea1 100644 --- a/icicle/include/icicle/merkle/merkle_proof.h +++ b/icicle/include/icicle/merkle/merkle_proof.h @@ -163,6 +163,25 @@ namespace icicle { std::vector m_leaf; std::vector m_root; std::vector m_path; + +//TODO SHANIE - remove from here + public: + // For debugging and testing purposes + // FIXME SHANIE: how to get correct element_size and nof_elements? (for print_bytes) + eIcicleError print_proof() const + { + std::cout << "Merkle Proof:" << std::endl; + std::cout << " Leaf index: " << m_leaf_index << std::endl; + std::cout << " Pruned path: " << (m_pruned ? "Yes" : "No") << std::endl; + std::cout << " Leaf data:" << std::endl; + print_bytes(m_leaf.data(), m_leaf.size(), 1); + std::cout << " Root data:" << std::endl; + print_bytes(m_root.data(), m_root.size(), 1); + std::cout << " Path data:" << std::endl; + print_bytes(m_path.data(), m_path.size(), 1); + return eIcicleError::SUCCESS; + } +//TODO SHANIE - remove until here }; } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/utils/rand_gen.h b/icicle/include/icicle/utils/rand_gen.h index 2a41511952..60c53c61bb 100644 --- a/icicle/include/icicle/utils/rand_gen.h +++ b/icicle/include/icicle/utils/rand_gen.h @@ -1,4 +1,5 @@ #pragma once +#include #include inline std::mt19937 rand_generator = std::mt19937{std::random_device{}()}; @@ -14,4 +15,32 @@ static uint32_t rand_uint_32b(uint32_t min = 0, uint32_t max = UINT32_MAX) { std::uniform_int_distribution dist(min, max); return dist(rand_generator); +} + +/** + * @brief Generate random unsigned integer in range (inclusive) + * @param min Lower limit. + * @param max Upper limit. + * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. + */ +static size_t rand_size_t(size_t min = 0, size_t max = SIZE_MAX) +{ + std::uniform_int_distribution dist(min, max); + return dist(rand_generator); +} + + +/** + * @brief Generate random unsigned integer in range (inclusive) + * @param min Lower limit. + * @param max Upper limit. + * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. + */ +static std::vector rand_size_t_vector(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX) +{ + std::vector vec(nof_queries); + for (size_t i = 0; i < nof_queries; i++) { + vec[i] = rand_size_t(min, max); + } + return vec; } \ No newline at end of file diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp new file mode 100644 index 0000000000..6476f54f99 --- /dev/null +++ b/icicle/src/fri/fri.cpp @@ -0,0 +1,58 @@ +#include "icicle/errors.h" +#include "icicle/fri/fri.h" +#include "icicle/backend/fri_backend.h" +#include "icicle/dispatcher.h" +#include + +namespace icicle { + + ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryImpl); + + + /** + * @brief Specialization of create_fri for the case of + * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + */ + template <> + Fri create_fri( + size_t input_size, + size_t folding_factor, + size_t stopping_degree, + const Hash& hash_for_merkle_tree, + uint64_t output_store_min_layer) + { + std::shared_ptr> backend; + ICICLE_CHECK(FriDispatcher::execute( + input_size, + folding_factor, + stopping_degree, + hash_for_merkle_tree, + output_store_min_layer, + backend)); + + Fri fri{backend}; + return fri; + } + + /** + * @brief Specialization of create_fri for the case of + * (folding_factor, stopping_degree, vector&&). + */ + template <> + Fri create_fri( + size_t folding_factor, + size_t stopping_degree, + std::vector&& merkle_trees) + { + std::shared_ptr> backend; + ICICLE_CHECK(FriDispatcher::execute( + folding_factor, + stopping_degree, + std::move(merkle_trees), + backend)); + + Fri fri{backend}; + return fri; + } + +} // namespace icicle diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp new file mode 100644 index 0000000000..a40c6e67e0 --- /dev/null +++ b/icicle/src/fri/fri_c_api.cpp @@ -0,0 +1,211 @@ +#include "icicle/fields/field_config.h" +#include "icicle/utils/utils.h" +#include "icicle/fri/fri.h" +#include "icicle/fri/fri_transcript_config.h" +#include + + +using namespace field_config; + +// TODO: Add methods for `prove`, `verify`, and the `proof` struct. + +extern "C" { + +// Define the FRI handle type +typedef icicle::Fri FriHandle; + +// Structure to represent the FFI transcript configuration. +struct FriTranscriptConfigFFI { + Hash* hasher; + std::byte* domain_separator_label; + size_t domain_separator_label_len; + std::byte* round_challenge_label; + size_t round_challenge_label_len; + std::byte* commit_label; + size_t commit_label_len; + std::byte* nonce_label; + size_t nonce_label_len; + std::byte* public_state; + size_t public_state_len; + const scalar_t* seed_rng; +}; + +/** + * @brief Structure representing creation parameters for the "hash-based" constructor + * `create_fri(folding_factor, stopping_degree, Hash&, output_store_min_layer)`. + */ +struct FriCreateHashFFI { + size_t input_size; + size_t folding_factor; + size_t stopping_degree; + Hash* hash_for_merkle_tree; + uint64_t output_store_min_layer; +}; + +/** + * @brief Structure representing creation parameters for the "existing Merkle trees" constructor + * `create_fri(folding_factor, stopping_degree, vector&&)`. + */ +struct FriCreateWithTreesFFI { + size_t folding_factor; + size_t stopping_degree; + MerkleTree* merkle_trees; // An array of MerkleTree* (pointers). + size_t merkle_trees_count; // Number of items in merkle_trees. +}; + +/** + * @brief Creates a new FRI instance from the given FFI transcript configuration + * and creation parameters (folding_factor, stopping_degree, hash, etc.). + * @param create_config Pointer to the creation parameters (FriCreateConfigFFI). + * @param transcript_config Pointer to the FFI transcript configuration structure. + * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. + */ +FriHandle* +CONCAT_EXPAND(FIELD, fri_create)( + const FriCreateHashFFI* create_config, + const FriTranscriptConfigFFI* ffi_transcript_config +) +{ + if (!create_config || !create_config->hash_for_merkle_tree) { + ICICLE_LOG_ERROR << "Invalid FRI creation config."; + return nullptr; + } + if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; + return nullptr; + } + + + ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (hash-based)"; + + // Convert byte arrays to vectors + //TODO SHANIE - check if this is the correct way + std::vector domain_separator_label( + ffi_transcript_config->domain_separator_label, + ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); + std::vector round_challenge_label( + ffi_transcript_config->round_challenge_label, + ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); + std::vector commit_label( + ffi_transcript_config->commit_label, + ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + std::vector nonce_label( + ffi_transcript_config->nonce_label, + ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + std::vector public_state( + ffi_transcript_config->public_state, + ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); + + + // Construct a FriTranscriptConfig + FriTranscriptConfig config{ + *(ffi_transcript_config->hasher), + std::move(domain_separator_label), + std::move(round_challenge_label), + std::move(commit_label), + std::move(nonce_label), + std::move(public_state), + *(ffi_transcript_config->seed_rng) + }; + + // Create and return the Fri instance + return new icicle::Fri(icicle::create_fri( + create_config->folding_factor, + create_config->stopping_degree, + *(create_config->hash_for_merkle_tree), + create_config->output_store_min_layer + )); +} + +// fri_create_with_trees - Using vector&& constructor + +/** + * @brief Creates a new FRI instance using the vector&& constructor. + * @param create_config Pointer to a FriCreateWithTreesFFI struct with the necessary parameters. + * @param transcript_config Pointer to the FFI transcript configuration structure. + * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. + */ +FriHandle* +CONCAT_EXPAND(FIELD, fri_create_with_trees)( + const FriCreateWithTreesFFI* create_config, + const FriTranscriptConfigFFI* ffi_transcript_config +) +{ + if (!create_config || !create_config->merkle_trees) { + ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; + return nullptr; + } + if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; + return nullptr; + } + + + ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (with existing trees)"; + + // Convert the raw array of MerkleTree* into a std::vector + std::vector merkle_trees_vec; + merkle_trees_vec.reserve(create_config->merkle_trees_count); + for (size_t i = 0; i < create_config->merkle_trees_count; ++i) { + merkle_trees_vec.push_back(create_config->merkle_trees[i]); + } + + + // Convert byte arrays to vectors + //TODO SHANIE - check if this is the correct way + std::vector domain_separator_label( + ffi_transcript_config->domain_separator_label, + ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); + std::vector round_challenge_label( + ffi_transcript_config->round_challenge_label, + ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); + std::vector commit_label( + ffi_transcript_config->commit_label, + ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + std::vector nonce_label( + ffi_transcript_config->nonce_label, + ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + std::vector public_state( + ffi_transcript_config->public_state, + ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); + + + // Construct a FriTranscriptConfig + FriTranscriptConfig config{ + *(ffi_transcript_config->hasher), + std::move(domain_separator_label), + std::move(round_challenge_label), + std::move(commit_label), + std::move(nonce_label), + std::move(public_state), + *(ffi_transcript_config->seed_rng) + }; + + // Create and return the Fri instance + return new icicle::Fri(icicle::create_fri( + create_config->folding_factor, + create_config->stopping_degree, + std::move(merkle_trees_vec) + )); +} + + +/** + * @brief Deletes the given Fri instance. + * @param fri_handle Pointer to the Fri instance to be deleted. + * @return eIcicleError indicating the success or failure of the operation. + */ +eIcicleError CONCAT_EXPAND(FIELD, fri_delete)(const FriHandle* fri_handle) +{ + if (!fri_handle) { + ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; + return eIcicleError::INVALID_ARGUMENT; + } + + ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; + delete fri_handle; + + return eIcicleError::SUCCESS; +} + +} // extern "C" diff --git a/icicle/tests/CMakeLists.txt b/icicle/tests/CMakeLists.txt index 9b79f83567..50e2f427b3 100644 --- a/icicle/tests/CMakeLists.txt +++ b/icicle/tests/CMakeLists.txt @@ -42,7 +42,7 @@ if (FIELD) gtest_discover_tests(test_polynomial_api) endif() - if(SUMCHECK OR HASH) + if(SUMCHECK OR HASH OR FRI) target_link_libraries(test_field_api PRIVATE icicle_hash) endif() endif() From b478aa61f545dab7ff1eba6bd1fe35a7341d21b7 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Feb 2025 16:59:57 +0200 Subject: [PATCH 072/127] When creating FRI without a given MerkleTree vector, generate it in the frontend --- icicle/backend/cpu/include/cpu_fri_backend.h | 42 +++++-------- icicle/backend/cpu/include/cpu_fri_rounds.h | 47 +++----------- .../backend/cpu/include/cpu_fri_transcript.h | 15 +++-- icicle/backend/cpu/src/field/cpu_fri.cpp | 13 +--- icicle/cmake/target_editor.cmake | 1 + icicle/include/icicle/backend/fri_backend.h | 26 +------- icicle/include/icicle/fri/fri_proof.h | 10 +-- icicle/src/fri/fri.cpp | 61 +++++++++++++------ icicle/src/fri/fri_c_api.cpp | 1 + 9 files changed, 83 insertions(+), 133 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index b7319cd9da..9a8b222415 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -17,23 +17,6 @@ namespace icicle { class CpuFriBackend : public FriBackend { public: - /** - * @brief Constructor for the case where you only have a Merkle hash function & store layer data in a fixed tree. - * - * @param input_size The size of the input polynomial - number of evaluations. - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree Stopping degree threshold for the final polynomial. - * @param hash_for_merkle_tree The hash function used for Merkle tree commitments. - * @param output_store_min_layer Optional parameter (default=0). Controls partial storage of layers. - */ - CpuFriBackend(const size_t input_size, const size_t folding_factor, const size_t stopping_degree, const Hash& hash_for_merkle_tree, const uint64_t output_store_min_layer = 0) - : FriBackend(folding_factor, stopping_degree), - m_log_input_size(static_cast(std::log2(static_cast(input_size)))), - m_input_size(input_size), - m_fri_rounds(m_log_input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer) - { - } - /** * @brief Constructor that accepts an existing array of Merkle trees. * @@ -68,6 +51,9 @@ namespace icicle { size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; size_t nof_fri_rounds = (m_log_input_size > log_df_plus_1) ? (m_log_input_size - log_df_plus_1) : 0; + // Initialize the proof + fri_proof.init(fri_config.nof_queries, nof_fri_rounds); + //commit fold phase ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, nof_fri_rounds, fri_proof)); @@ -103,10 +89,7 @@ namespace icicle { // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with capacity 2^(m_log_input_size - round_idx). F* round_evals = m_fri_rounds.get_round_evals(0); - // Resize the persistent vector so it holds m_input_size elements. - round_evals->resize(m_input_size); - // Copy input_data into the persistent storage. - std::copy(input_data, input_data + m_input_size, round_evals->begin()); + std::copy(input_data, input_data + m_input_size, round_evals); size_t current_size = m_input_size; size_t current_log_size = m_log_input_size; @@ -119,14 +102,17 @@ namespace icicle { // Merkle tree for the current round_idx MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); - current_round_tree->build(reinterpret_cast(round_evals->data()), sizeof(F), MerkleTreeConfig()); + current_round_tree->build(reinterpret_cast(round_evals), sizeof(F), MerkleTreeConfig()); auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; // FIXME SHANIE - do I need to add the root to the proof here? // Add root to transcript and get alpha - std::vector merkle_commit(root_ptr, root_ptr + root_size); //FIXME SHANIE - is this the right way to convert? + // std::vector merkle_commit(root_ptr, root_ptr + root_size); //FIXME SHANIE - what is the right way to convert? + std::vector merkle_commit(root_size); + std::memcpy(merkle_commit.data(), root_ptr, root_size); + F alpha = transcript.get_alpha(merkle_commit); // Fold the evaluations @@ -147,7 +133,7 @@ namespace icicle { } for (size_t i = 0; i < half; ++i){ - (*round_evals)[i] = peven[i] + (alpha * podd[i]); + round_evals[i] = peven[i] + (alpha * podd[i]); } current_size>>=1; @@ -160,7 +146,7 @@ namespace icicle { eIcicleError proof_of_work(CpuFriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) { - if(transcript.hash_and_get_nof_leading_zero_bits(nonce, pow_bits) == pow_bits){ + if(transcript.hash_and_get_nof_leading_zero_bits(nonce) == pow_bits){ transcript.set_pow_nonce(nonce); fri_proof.set_pow_nonce(nonce); return eIcicleError::SUCCESS; @@ -184,7 +170,7 @@ namespace icicle { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); - std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, fri_proof.get_final_poly()->size(), m_input_size); + std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); for (size_t q = 0; q < query_indices.size(); q++){ size_t query = query_indices[q]; @@ -192,8 +178,8 @@ namespace icicle { size_t round_size = (1ULL << (m_log_input_size - round_idx)); size_t query_idx = query % round_size; size_t query_idx_sym = (query + (round_size >> 1)) % round_size; - std::vector* round_evals = m_fri_rounds.get_round_evals(round_idx); - const std::byte* leaves = reinterpret_cast(round_evals->data()); + F* round_evals = m_fri_rounds.get_round_evals(round_idx); + const std::byte* leaves = reinterpret_cast(round_evals); uint64_t leaves_size = sizeof(F); MerkleProof& proof_ref = fri_proof.get_query_proof(query_idx, round_idx); diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index f5a4bfd9fc..fb8480e5a4 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -16,50 +16,19 @@ template class FriRounds { public: - /** - * @brief Constructor that stores parameters for building Merkle trees. - * - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree The polynomial degree threshold to stop folding. - * @param hash_for_merkle_tree Hash function used for each Merkle tree layer. - * @param output_store_min_layer Minimum layer index to store fully in the output (default=0). - */ - FriRounds(size_t log_input_size, - size_t folding_factor, - size_t stopping_degree, - const Hash& hash_for_merkle_tree, - uint64_t output_store_min_layer = 0) - { - ICICLE_ASSERT(folding_factor == 2) << "Only folding factor of 2 is supported"; - size_t df = stopping_degree; - size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; - size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; - - m_round_evals.resize(fold_rounds); - m_merkle_trees.reserve(fold_rounds); - std::vector hashes_for_merkle_tree_vec(fold_rounds, hash_for_merkle_tree); - for (size_t i = 0; i < fold_rounds; i++) { - m_merkle_trees.push_back(std::make_unique(hashes_for_merkle_tree_vec, sizeof(F), output_store_min_layer)); - hashes_for_merkle_tree_vec.pop_back(); - m_round_evals[i] = std::make_unique>(); - m_round_evals[i]->reserve(1ULL << (log_input_size - i)); - } - } - /** * @brief Constructor that accepts an already-existing array of Merkle trees. * Ownership is transferred from the caller. * * @param merkle_trees A moved vector of `unique_ptr`. */ - FriRounds(std::vector>&& merkle_trees) + FriRounds(std::vector&& merkle_trees) : m_merkle_trees(std::move(merkle_trees)) { - size_t fold_rounds = m_merkle_trees.size(); + size_t fold_rounds = m_merkle_trees.size(); //FIXME - consider stopping degree? m_round_evals.resize(fold_rounds); for (size_t i = 0; i < fold_rounds; i++) { - m_round_evals[i] = std::make_unique>(); - m_round_evals[i]->reserve(1ULL << (fold_rounds - i)); + m_round_evals[i] = std::make_unique(1ULL << (fold_rounds - i)); } } @@ -72,7 +41,7 @@ class FriRounds MerkleTree* get_merkle_tree(size_t round_idx) { ICICLE_ASSERT(round_idx < m_merkle_trees.size()) << "round index out of bounds"; - return m_merkle_trees[round_idx].get(); + return &m_merkle_trees[round_idx]; } F* get_round_evals(size_t round_idx) @@ -92,16 +61,16 @@ class FriRounds if (round_idx >= m_merkle_trees.size()) { return {nullptr, 0}; } - return m_merkle_trees[round_idx]->get_merkle_root(); + return m_merkle_trees[round_idx].get_merkle_root(); } private: // Persistent polynomial evaluations for each round (heap allocated). // For round i, the expected length is 2^(m_initial_log_size - i). - std::vector> m_round_evals; + std::vector> m_round_evals; - // Holds unique ownership of each MerkleTree for each round. m_merkle_trees[i] is the tree for round i. - std::vector> m_merkle_trees; + // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. + std::vector m_merkle_trees; }; } // namespace icicle diff --git a/icicle/backend/cpu/include/cpu_fri_transcript.h b/icicle/backend/cpu/include/cpu_fri_transcript.h index 8e0566c7b2..3ea6cb78ea 100644 --- a/icicle/backend/cpu/include/cpu_fri_transcript.h +++ b/icicle/backend/cpu/include/cpu_fri_transcript.h @@ -58,14 +58,14 @@ class CpuFriTranscript return m_prev_alpha; } - size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce, const size_t pow_bits) + size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce) { // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space // Build the hash input - build_hash_input_pow(hash_input); + build_hash_input_pow(hash_input, nonce); const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); @@ -129,6 +129,12 @@ class CpuFriTranscript dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint32_t)); } + void append_u64(std::vector& dest, uint64_t value) + { + const std::byte* data_bytes = reinterpret_cast(&value); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint64_t)); + } + /** * @brief Append a field element to the byte vector. * @param dest (OUT) Destination byte vector. @@ -168,7 +174,6 @@ class CpuFriTranscript append_data(m_entry_0, m_transcript_config.get_public_state()); } - /** * @brief Build the hash input for round 0 (commit phase 0). * @@ -207,12 +212,12 @@ class CpuFriTranscript * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ - void build_hash_input_pow(std::vector& hash_input, uint32_t temp_pow_nonce) + void build_hash_input_pow(std::vector& hash_input, uint64_t temp_pow_nonce) { append_data(hash_input, m_entry_0); append_field(hash_input, m_prev_alpha); append_data(hash_input, m_transcript_config.get_nonce_label()); - append_u32(hash_input, temp_pow_nonce); + append_u64(hash_input, temp_pow_nonce); } /** diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index 0f10222acd..387c21d369 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -6,20 +6,11 @@ using namespace field_config; namespace icicle { template - eIcicleError cpu_create_fri_backend(const Device& device, const size_t input_size, const size_t folding_factor, const size_t stopping_degree, const Hash& hash_for_merkle_tree, const uint64_t output_store_min_layer, std::shared_ptr>& backend /*OUT*/) + eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + backend = std::make_shared>(folding_factor, stopping_degree, std::move(merkle_trees)); return eIcicleError::SUCCESS; } - - // template - // eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees, std::shared_ptr>& backend /*OUT*/) - // { - // backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); - // return eIcicleError::SUCCESS; - // } - - // REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); } // namespace icicle \ No newline at end of file diff --git a/icicle/cmake/target_editor.cmake b/icicle/cmake/target_editor.cmake index 2b823debc4..f251513292 100644 --- a/icicle/cmake/target_editor.cmake +++ b/icicle/cmake/target_editor.cmake @@ -104,6 +104,7 @@ function(handle_fri TARGET FEATURE_LIST) if(FRI AND "FRI" IN_LIST FEATURE_LIST) target_compile_definitions(${TARGET} PUBLIC FRI=${FRI}) target_sources(${TARGET} PRIVATE src/fri/fri.cpp src/fri/fri_c_api.cpp) + target_link_libraries(${TARGET} PRIVATE icicle_hash) set(FRI ON CACHE BOOL "Enable FRI feature" FORCE) else() set(FRI OFF CACHE BOOL "FRI not available for this field" FORCE) diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index 532f81fff8..8954c8b036 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -24,34 +24,14 @@ template class FriBackend { public: - /** - * @brief Constructor for the case where you only have a Merkle hash function & store layer data in a fixed tree. - * - * @param input_size The size of the input polynomial - number of evaluations. - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree Stopping degree threshold for the final polynomial. - * @param hash_for_merkle_tree The hash function used for Merkle tree commitments. - * @param output_store_min_layer Optional parameter (default=0). Controls partial storage of layers. - */ - FriBackend(const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, - const Hash& hash_for_merkle_tree, - uint64_t output_store_min_layer = 0) - : m_folding_factor(folding_factor) - , m_stopping_degree(stopping_degree) - {} - /** * @brief Constructor that accepts an existing array of Merkle trees. * * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree Stopping degree threshold for the final polynomial. - * @param merkle_trees A moved vector of MerkleTree pointers. */ FriBackend(const size_t folding_factor, - const size_t stopping_degree, - std::vector&& merkle_trees) + const size_t stopping_degree) : m_folding_factor(folding_factor) , m_stopping_degree(stopping_degree) {} @@ -81,14 +61,12 @@ class FriBackend /*************************** Backend Factory Registration ***************************/ -//FIXME SHANIE - need two FriFactoryImpl - /** * @brief A function signature for creating a FriBackend instance for a specific device. */ template using FriFactoryImpl = - std::function>& backend /*OUT*/)>; + std::function&& merkle_trees, std::shared_ptr>& backend /*OUT*/)>; /** * @brief Register a FRI backend factory for a specific device type. diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index e9f2add3ce..acd6271980 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -26,19 +26,15 @@ namespace icicle { * @brief Initialize the Merkle proofs and final polynomial storage. * * @param nof_queries Number of queries in the proof. - * @param final_poly_degree Degree of the final polynomial. * @param nof_fri_rounds Number of FRI rounds (rounds). */ - void init(int nof_queries, int final_poly_degree, int nof_fri_rounds) + void init(int nof_queries, int nof_fri_rounds) { ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) << "Number of queries and FRI rounds must be > 0"; // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns m_query_proofs.resize(nof_queries, std::vector(nof_fri_rounds)); - - // Initialize the final polynomial - m_final_poly.resize(final_poly_degree + 1, F::zero()); } /** @@ -70,14 +66,14 @@ namespace icicle { } //get pointer to the final polynomial - std::vector* get_final_poly() const + F* get_final_poly() const { return m_final_poly.get(); } private: std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf - std::unique_ptr> m_final_poly; // Final polynomial (constant in canonical FRI) + std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) uint64_t m_pow_nonce; // Proof-of-work nonce public: diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 6476f54f99..73af961953 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -1,37 +1,65 @@ #include "icicle/errors.h" #include "icicle/fri/fri.h" #include "icicle/backend/fri_backend.h" +#include "icicle/merkle/merkle_tree.h" #include "icicle/dispatcher.h" +#include "icicle/hash/hash.h" #include namespace icicle { ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryImpl); + /** + * @brief Create a FRI instance. + * @return A `Fri` object built around the chosen backend. + */ + template + Fri create_fri_with_merkle_trees( + size_t folding_factor, + size_t stopping_degree, + std::vector&& merkle_trees) + { + std::shared_ptr> backend; + ICICLE_CHECK(FriDispatcher::execute( + folding_factor, + stopping_degree, + std::move(merkle_trees), + backend)); + + Fri fri{backend}; + return fri; + } /** * @brief Specialization of create_fri for the case of * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). */ template <> - Fri create_fri( + Fri create_fri( size_t input_size, size_t folding_factor, size_t stopping_degree, - const Hash& hash_for_merkle_tree, + Hash& hash_for_merkle_tree, uint64_t output_store_min_layer) { - std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute( - input_size, + ICICLE_ASSERT(folding_factor == 2) << "Only folding factor of 2 is supported"; + size_t log_input_size = static_cast(std::log2(static_cast(input_size))); + size_t df = stopping_degree; + size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + + std::vector merkle_trees; + merkle_trees.reserve(fold_rounds); + std::vector hashes_for_merkle_tree_vec(fold_rounds, hash_for_merkle_tree); + for (size_t i = 0; i < fold_rounds; i++) { + merkle_trees.emplace_back(MerkleTree::create(hashes_for_merkle_tree_vec, sizeof(scalar_t), output_store_min_layer)); + hashes_for_merkle_tree_vec.pop_back(); + } + return create_fri_with_merkle_trees( folding_factor, stopping_degree, - hash_for_merkle_tree, - output_store_min_layer, - backend)); - - Fri fri{backend}; - return fri; + std::move(merkle_trees)); } /** @@ -39,20 +67,15 @@ namespace icicle { * (folding_factor, stopping_degree, vector&&). */ template <> - Fri create_fri( + Fri create_fri( size_t folding_factor, size_t stopping_degree, std::vector&& merkle_trees) { - std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute( + return create_fri_with_merkle_trees( folding_factor, stopping_degree, - std::move(merkle_trees), - backend)); - - Fri fri{backend}; - return fri; + std::move(merkle_trees)); } } // namespace icicle diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index a40c6e67e0..e9e1677c04 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -110,6 +110,7 @@ CONCAT_EXPAND(FIELD, fri_create)( // Create and return the Fri instance return new icicle::Fri(icicle::create_fri( + create_config->input_size, create_config->folding_factor, create_config->stopping_degree, *(create_config->hash_for_merkle_tree), From 9ee1b8d46659433a0cfd5feb24ac2bad450dba6a Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Feb 2025 18:36:22 +0200 Subject: [PATCH 073/127] basic test added, api fix --- icicle/include/icicle/fri/fri.h | 2 +- icicle/include/icicle/fri/fri_proof.h | 2 +- icicle/tests/test_field_api.cpp | 53 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 1bcacd1d26..56e702f32b 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -81,7 +81,7 @@ class Fri eIcicleError get_fri_proof( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - const std::vector& input_data, + const F* input_data, FriProof& fri_proof /* OUT */) const { return m_backend->get_fri_proof(fri_config, fri_transcript_config, input_data, fri_proof); diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index acd6271980..b63400fcfb 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -31,7 +31,7 @@ namespace icicle { void init(int nof_queries, int nof_fri_rounds) { ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) - << "Number of queries and FRI rounds must be > 0"; + << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns m_query_proofs.resize(nof_queries, std::vector(nof_fri_rounds)); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 35d6e8528a..e4c9a0242a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -16,6 +16,13 @@ #include "icicle/program/returning_value_program.h" #include "../../icicle/backend/cpu/include/cpu_program_executor.h" #include "icicle/sumcheck/sumcheck.h" +#include "icicle/hash/hash.h" +#include "icicle/merkle/merkle_tree.h" + +#include "icicle/fri/fri.h" +#include "icicle/fri/fri_config.h" +#include "icicle/fri/fri_proof.h" +#include "icicle/fri/fri_transcript_config.h" #include "test_base.h" @@ -1238,6 +1245,52 @@ TEST_F(FieldApiTestBase, Sumcheck) ASSERT_EQ(true, verification_pass); } + +TYPED_TEST(FieldApiTest, Fri) +{ + // Randomize configuration + const int log_input_size = rand_uint_32b(5, 13); + const size_t input_size = 1 << log_input_size; + const int folding_factor = 2; + const int stopping_degree = rand_uint_32b(1, 8); + const uint64_t output_store_min_layer = 0; + + ICICLE_LOG_DEBUG << "log_input_size = " << log_input_size; + ICICLE_LOG_DEBUG << "input_size = " << input_size; + ICICLE_LOG_DEBUG << "folding_factor = " << folding_factor; + ICICLE_LOG_DEBUG << "stopping_degree = " << stopping_degree; + + // Initialize domain + auto init_domain_config = default_ntt_init_domain_config(); + init_domain_config.is_async = false; + ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); + + // Generate input polynomial evaluations + auto scalars = std::make_unique(input_size); + scalar_t::rand_host_many(scalars.get(), input_size); + + // ===== Prover side ====== + Hash hash_for_merkle_tree = create_keccak_256_hash(); + Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + + FriTranscriptConfig transcript_config; + FriConfig fri_config; + fri_config.nof_queries = 2; + FriProof fri_proof; + + ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + + // ===== Verifier side ====== + Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + bool verification_pass = false; + ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, verification_pass)); + + ASSERT_EQ(true, verification_pass); + + // Release domain + ICICLE_CHECK(ntt_release_domain()); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From 7e3dbd5a6274750166badffd9abc15354a4387bc Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Feb 2025 15:49:48 +0200 Subject: [PATCH 074/127] merkle_trees passed by value (only holds std::shared_ptr) --- icicle/backend/cpu/include/cpu_fri_backend.h | 49 +++++++++---------- icicle/backend/cpu/include/cpu_fri_rounds.h | 15 +++--- icicle/backend/cpu/src/field/cpu_fri.cpp | 4 +- icicle/include/icicle/backend/fri_backend.h | 2 +- icicle/include/icicle/fri/fri.h | 4 +- icicle/include/icicle/fri/fri_proof.h | 8 +-- .../icicle/fri/fri_transcript_config.h | 6 +-- icicle/src/fri/fri.cpp | 11 +++-- icicle/src/fri/fri_c_api.cpp | 2 +- icicle/tests/test_field_api.cpp | 15 ++++-- 10 files changed, 62 insertions(+), 54 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 9a8b222415..db1b8f47ad 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,11 +24,12 @@ namespace icicle { * @param stopping_degree Stopping degree threshold for the final polynomial. * @param merkle_trees A moved vector of MerkleTree pointers. */ - CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees) + CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) : FriBackend(folding_factor, stopping_degree), - m_log_input_size(merkle_trees.size()), + m_nof_fri_rounds(merkle_trees.size()), + m_log_input_size(m_nof_fri_rounds + std::log2(static_cast(stopping_degree))), m_input_size(pow(2, m_log_input_size)), - m_fri_rounds(std::move(merkle_trees)) + m_fri_rounds(merkle_trees, m_log_input_size) { } @@ -46,16 +47,11 @@ namespace icicle { CpuFriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); - // Determine the number of folding rounds - size_t df = this->m_stopping_degree; - size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; - size_t nof_fri_rounds = (m_log_input_size > log_df_plus_1) ? (m_log_input_size - log_df_plus_1) : 0; - // Initialize the proof - fri_proof.init(fri_config.nof_queries, nof_fri_rounds); + fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree); //commit fold phase - ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, nof_fri_rounds, fri_proof)); + ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); //proof of work if (fri_config.pow_bits != 0){ @@ -63,15 +59,16 @@ namespace icicle { } //query phase - ICICLE_CHECK(query_phase(transcript, fri_config, nof_fri_rounds, fri_proof)); + ICICLE_CHECK(query_phase(transcript, fri_config, fri_proof)); return eIcicleError::SUCCESS; } private: - FriRounds m_fri_rounds; // Holds intermediate rounds + const size_t m_nof_fri_rounds; // Number of FRI rounds const size_t m_log_input_size; // Log size of the input polynomial const size_t m_input_size; // Size of the input polynomial + FriRounds m_fri_rounds; // Holds intermediate rounds /** * @brief Perform the commit-fold phase of the FRI protocol. @@ -81,7 +78,7 @@ namespace icicle { * @param transcript The transcript to generate challenges. * @return eIcicleError Error code indicating success or failure. */ - eIcicleError commit_fold_phase(const F* input_data, CpuFriTranscript& transcript, const FriConfig& fri_config, size_t nof_fri_rounds, FriProof& fri_proof){ + eIcicleError commit_fold_phase(const F* input_data, CpuFriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ ICICLE_ASSERT(this->m_folding_factor==2) << "Folding factor must be 2"; const F* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); @@ -94,7 +91,7 @@ namespace icicle { size_t current_size = m_input_size; size_t current_log_size = m_log_input_size; - for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx){ + for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; ++round_idx){ // if (current_size == (df + 1)) { FIXME SHANIE - do I need this? // fri_proof.finalpoly->assign(round_evals->begin(), round_evals->end()); // break; @@ -102,7 +99,7 @@ namespace icicle { // Merkle tree for the current round_idx MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); - current_round_tree->build(reinterpret_cast(round_evals), sizeof(F), MerkleTreeConfig()); + current_round_tree->build(reinterpret_cast(round_evals), sizeof(F)*current_size, MerkleTreeConfig()); auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; @@ -126,7 +123,7 @@ namespace icicle { podd[i] = ((round_evals[i] - round_evals[i + half]) * F::inv_log_size(1)) * twiddles[tw_idx]; } - if (round_idx == nof_fri_rounds - 1){ + if (round_idx == m_nof_fri_rounds - 1){ round_evals = fri_proof.get_final_poly(); } else { round_evals = m_fri_rounds.get_round_evals(round_idx + 1); @@ -165,28 +162,28 @@ namespace icicle { * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. * @return eIcicleError */ - eIcicleError query_phase(CpuFriTranscript& transcript, const FriConfig& fri_config, size_t nof_fri_rounds, FriProof& fri_proof) + eIcicleError query_phase(CpuFriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); - for (size_t q = 0; q < query_indices.size(); q++){ - size_t query = query_indices[q]; - for (size_t round_idx = 0; round_idx < nof_fri_rounds; round_idx++){ + for (size_t query_idx = 0; query_idx < query_indices.size(); query_idx++){ + size_t query = query_indices[query_idx]; + for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++){ size_t round_size = (1ULL << (m_log_input_size - round_idx)); - size_t query_idx = query % round_size; - size_t query_idx_sym = (query + (round_size >> 1)) % round_size; + size_t leaf_idx = query % round_size; + size_t leaf_idx_sym = (query + (round_size >> 1)) % round_size; F* round_evals = m_fri_rounds.get_round_evals(round_idx); const std::byte* leaves = reinterpret_cast(round_evals); uint64_t leaves_size = sizeof(F); - MerkleProof& proof_ref = fri_proof.get_query_proof(query_idx, round_idx); - eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), query_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); + MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); + eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); if (err != eIcicleError::SUCCESS) return err; - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(query_idx_sym, round_idx); - eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), query_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); + MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2*query_idx+1, round_idx); + eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); if (err_sym != eIcicleError::SUCCESS) return err_sym; } } diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index fb8480e5a4..e9a62961fe 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -8,6 +8,7 @@ #include "icicle/hash/hash.h" #include "icicle/hash/keccak.h" #include "icicle/utils/log.h" +#include "icicle/fri/fri.h" namespace icicle { @@ -22,13 +23,13 @@ class FriRounds * * @param merkle_trees A moved vector of `unique_ptr`. */ - FriRounds(std::vector&& merkle_trees) - : m_merkle_trees(std::move(merkle_trees)) + FriRounds(std::vector merkle_trees, const size_t log_input_size) + : m_merkle_trees(merkle_trees) { size_t fold_rounds = m_merkle_trees.size(); //FIXME - consider stopping degree? - m_round_evals.resize(fold_rounds); + m_rounds_evals.resize(fold_rounds); for (size_t i = 0; i < fold_rounds; i++) { - m_round_evals[i] = std::make_unique(1ULL << (fold_rounds - i)); + m_rounds_evals[i] = std::make_unique(1ULL << (log_input_size - i)); } } @@ -46,8 +47,8 @@ class FriRounds F* get_round_evals(size_t round_idx) { - ICICLE_ASSERT(round_idx < m_round_evals.size()) << "round index out of bounds"; - return m_round_evals[round_idx].get(); + ICICLE_ASSERT(round_idx < m_rounds_evals.size()) << "round index out of bounds"; + return m_rounds_evals[round_idx].get(); } /** @@ -67,7 +68,7 @@ class FriRounds private: // Persistent polynomial evaluations for each round (heap allocated). // For round i, the expected length is 2^(m_initial_log_size - i). - std::vector> m_round_evals; + std::vector> m_rounds_evals; // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. std::vector m_merkle_trees; diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index 387c21d369..480ea7ab97 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -6,9 +6,9 @@ using namespace field_config; namespace icicle { template - eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector&& merkle_trees, std::shared_ptr>& backend /*OUT*/) + eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(folding_factor, stopping_degree, std::move(merkle_trees)); + backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); return eIcicleError::SUCCESS; } REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index 8954c8b036..a9ed6f979e 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -66,7 +66,7 @@ class FriBackend */ template using FriFactoryImpl = - std::function&& merkle_trees, std::shared_ptr>& backend /*OUT*/)>; + std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; /** * @brief Register a FRI backend factory for a specific device type. diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 56e702f32b..d4ef70276a 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -42,14 +42,14 @@ Fri create_fri( * * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param merkle_trees A moved vector of `MerkleTree` objects. + * @param merkle_trees A reference vector of `MerkleTree` objects. * @return A `Fri` object built around the chosen backend. */ template Fri create_fri( size_t folding_factor, size_t stopping_degree, - std::vector&& merkle_trees); + std::vector merkle_trees); /** * @brief Class for performing FRI operations. diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index b63400fcfb..11bfd140b2 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -5,6 +5,7 @@ #include #include "icicle/backend/merkle/merkle_tree_backend.h" #include "icicle/merkle/merkle_tree.h" +#include "icicle/utils/log.h" namespace icicle { @@ -28,13 +29,14 @@ namespace icicle { * @param nof_queries Number of queries in the proof. * @param nof_fri_rounds Number of FRI rounds (rounds). */ - void init(int nof_queries, int nof_fri_rounds) + void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t m_stopping_degree) { ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns - m_query_proofs.resize(nof_queries, std::vector(nof_fri_rounds)); + m_query_proofs.resize(2*nof_queries, std::vector(nof_fri_rounds)); //for each query, we have 2 proofs (for the leaf and its symmetric) + m_final_poly = std::make_unique(m_stopping_degree + 1); } /** @@ -44,7 +46,7 @@ namespace icicle { * @param round_idx Index of the round (FRI round). * @return Reference to the Merkle proof at the specified position. */ - MerkleProof& get_query_proof(int query_idx, int round_idx) + MerkleProof& get_query_proof(const size_t query_idx, const size_t round_idx) { if (query_idx < 0 || query_idx >= m_query_proofs.size()) { throw std::out_of_range("Invalid query index"); diff --git a/icicle/include/icicle/fri/fri_transcript_config.h b/icicle/include/icicle/fri/fri_transcript_config.h index 3a7168b042..ea4a86b62a 100644 --- a/icicle/include/icicle/fri/fri_transcript_config.h +++ b/icicle/include/icicle/fri/fri_transcript_config.h @@ -20,10 +20,10 @@ class FriTranscriptConfig // Default Constructor FriTranscriptConfig() : m_hasher(create_keccak_256_hash()), - m_domain_separator_label({}), - m_commit_phase_label({}), + m_domain_separator_label(cstr_to_bytes("ds")), + m_commit_phase_label(cstr_to_bytes("commit")), m_nonce_label(cstr_to_bytes("nonce")), - m_public({}), + m_public(cstr_to_bytes("public")), m_seed_rng(F::zero()) { } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 73af961953..9f7dda3152 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -4,6 +4,7 @@ #include "icicle/merkle/merkle_tree.h" #include "icicle/dispatcher.h" #include "icicle/hash/hash.h" +#include "icicle/utils/log.h" #include namespace icicle { @@ -18,13 +19,13 @@ namespace icicle { Fri create_fri_with_merkle_trees( size_t folding_factor, size_t stopping_degree, - std::vector&& merkle_trees) + std::vector merkle_trees) { std::shared_ptr> backend; ICICLE_CHECK(FriDispatcher::execute( folding_factor, stopping_degree, - std::move(merkle_trees), + merkle_trees, backend)); Fri fri{backend}; @@ -59,7 +60,7 @@ namespace icicle { return create_fri_with_merkle_trees( folding_factor, stopping_degree, - std::move(merkle_trees)); + merkle_trees); } /** @@ -70,12 +71,12 @@ namespace icicle { Fri create_fri( size_t folding_factor, size_t stopping_degree, - std::vector&& merkle_trees) + std::vector merkle_trees) { return create_fri_with_merkle_trees( folding_factor, stopping_degree, - std::move(merkle_trees)); + merkle_trees); } } // namespace icicle diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index e9e1677c04..be40a4f42b 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -186,7 +186,7 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( return new icicle::Fri(icicle::create_fri( create_config->folding_factor, create_config->stopping_degree, - std::move(merkle_trees_vec) + merkle_trees_vec )); } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index e4c9a0242a..0ea2829bb4 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -20,6 +20,7 @@ #include "icicle/merkle/merkle_tree.h" #include "icicle/fri/fri.h" +#include "icicle/backend/fri_backend.h" #include "icicle/fri/fri_config.h" #include "icicle/fri/fri_proof.h" #include "icicle/fri/fri_transcript_config.h" @@ -1249,10 +1250,13 @@ TEST_F(FieldApiTestBase, Sumcheck) TYPED_TEST(FieldApiTest, Fri) { // Randomize configuration - const int log_input_size = rand_uint_32b(5, 13); + // const int log_input_size = rand_uint_32b(5, 13); + const int log_input_size = 5; const size_t input_size = 1 << log_input_size; const int folding_factor = 2; - const int stopping_degree = rand_uint_32b(1, 8); + // const int log_stopping_degree = rand_uint_32b(0, 3); + const int log_stopping_degree = 3; + const size_t stopping_degree = 1 << log_stopping_degree; const uint64_t output_store_min_layer = 0; ICICLE_LOG_DEBUG << "log_input_size = " << log_input_size; @@ -1267,10 +1271,13 @@ TYPED_TEST(FieldApiTest, Fri) // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); - scalar_t::rand_host_many(scalars.get(), input_size); + // scalar_t::rand_host_many(scalars.get(), input_size); + for (size_t i = 0; i < input_size; i++) { + scalars[i] = scalar_t::from(i); + } // ===== Prover side ====== - Hash hash_for_merkle_tree = create_keccak_256_hash(); + Hash hash_for_merkle_tree = create_keccak_256_hash((2*32)); // arity = 2 Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); FriTranscriptConfig transcript_config; From a334b0ed269618ab2d1fd0618272240f4059f1db Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Thu, 13 Feb 2025 16:06:51 +0200 Subject: [PATCH 075/127] fri_transcript moved to frontend --- icicle/backend/cpu/include/cpu_fri_backend.h | 11 +++++------ .../icicle/fri/fri_transcript.h} | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) rename icicle/{backend/cpu/include/cpu_fri_transcript.h => include/icicle/fri/fri_transcript.h} (98%) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index db1b8f47ad..19279b678a 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -5,9 +5,8 @@ #include #include #include "icicle/errors.h" -#include "cpu_fri_transcript.h" +#include "icicle/fri/fri_transcript.h" #include "icicle/backend/fri_backend.h" -#include "cpu_fri_transcript.h" #include "cpu_fri_rounds.h" #include "cpu_ntt_domain.h" #include "icicle/utils/log.h" @@ -45,7 +44,7 @@ namespace icicle { } ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; - CpuFriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); + FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); // Initialize the proof fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree); @@ -78,7 +77,7 @@ namespace icicle { * @param transcript The transcript to generate challenges. * @return eIcicleError Error code indicating success or failure. */ - eIcicleError commit_fold_phase(const F* input_data, CpuFriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ + eIcicleError commit_fold_phase(const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ ICICLE_ASSERT(this->m_folding_factor==2) << "Folding factor must be 2"; const F* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); @@ -140,7 +139,7 @@ namespace icicle { return eIcicleError::SUCCESS; } - eIcicleError proof_of_work(CpuFriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ + eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) { if(transcript.hash_and_get_nof_leading_zero_bits(nonce) == pow_bits){ @@ -162,7 +161,7 @@ namespace icicle { * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. * @return eIcicleError */ - eIcicleError query_phase(CpuFriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) + eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); diff --git a/icicle/backend/cpu/include/cpu_fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h similarity index 98% rename from icicle/backend/cpu/include/cpu_fri_transcript.h rename to icicle/include/icicle/fri/fri_transcript.h index 3ea6cb78ea..f9aef855fb 100644 --- a/icicle/backend/cpu/include/cpu_fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -12,10 +12,10 @@ namespace icicle { template -class CpuFriTranscript +class FriTranscript { public: - CpuFriTranscript(FriTranscriptConfig&& transcript_config, const size_t log_input_size) + FriTranscript(FriTranscriptConfig&& transcript_config, const size_t log_input_size) : m_transcript_config(std::move(transcript_config)) , m_log_input_size(log_input_size) , m_prev_alpha(F::zero()) From 1b1fc4ede8ab12f6a1022d7588b56f74cfd4129c Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 16 Feb 2025 23:50:04 +0200 Subject: [PATCH 076/127] verifier added --- icicle/backend/cpu/include/cpu_fri_backend.h | 6 +- icicle/include/icicle/backend/fri_backend.h | 6 +- icicle/include/icicle/fri/fri.h | 135 ++++++++++++++++- icicle/include/icicle/fri/fri_domain.h | 143 +++++++++++++++++++ icicle/include/icicle/fri/fri_proof.h | 48 ++++++- icicle/include/icicle/fri/fri_transcript.h | 41 +++--- icicle/tests/test_field_api.cpp | 14 +- 7 files changed, 358 insertions(+), 35 deletions(-) create mode 100644 icicle/include/icicle/fri/fri_domain.h diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 19279b678a..67c9d1785f 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,7 +24,7 @@ namespace icicle { * @param merkle_trees A moved vector of MerkleTree pointers. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree), + : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), m_log_input_size(m_nof_fri_rounds + std::log2(static_cast(stopping_degree))), m_input_size(pow(2, m_log_input_size)), @@ -47,7 +47,7 @@ namespace icicle { FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); // Initialize the proof - fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree); + fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree+1); //commit fold phase ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); @@ -168,7 +168,7 @@ namespace icicle { seed_rand_generator(seed); std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); - for (size_t query_idx = 0; query_idx < query_indices.size(); query_idx++){ + for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ size_t query = query_indices[query_idx]; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++){ size_t round_size = (1ULL << (m_log_input_size - round_idx)); diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index a9ed6f979e..5dbbbe56a3 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -31,9 +31,11 @@ class FriBackend * @param stopping_degree Stopping degree threshold for the final polynomial. */ FriBackend(const size_t folding_factor, - const size_t stopping_degree) + const size_t stopping_degree, + std::vector merkle_trees) : m_folding_factor(folding_factor) , m_stopping_degree(stopping_degree) + , m_merkle_trees(merkle_trees) {} virtual ~FriBackend() = default; @@ -54,6 +56,8 @@ class FriBackend FriProof& fri_proof ) = 0; + std::vector m_merkle_trees; + protected: const size_t m_folding_factor; const size_t m_stopping_degree; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index d4ef70276a..9d1409804e 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include "icicle/errors.h" #include "icicle/backend/fri_backend.h" @@ -9,6 +11,9 @@ #include "icicle/fri/fri_transcript_config.h" #include "icicle/hash/hash.h" #include "icicle/merkle/merkle_tree.h" +#include "icicle/fri/fri_transcript.h" +// #include "fri_domain.h" // FIXME SHANIE + namespace icicle { @@ -98,10 +103,136 @@ class Fri eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - const FriProof& fri_proof, + FriProof& fri_proof, // FIXME - const? bool& verification_pass /* OUT */) const { - return eIcicleError::API_NOT_IMPLEMENTED; + verification_pass = false; + ICICLE_ASSERT(fri_config.nof_queries > 0) << "No queries specified in FriConfig."; + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + const size_t input_size = 1 << (log_input_size); + std::vector alpha_values(nof_fri_rounds); + + + // set up the transcript + FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), log_input_size); + for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { + auto [root_ptr, root_size] = fri_proof.get_root(round_idx); + ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; + std::vector merkle_commit(root_size); + std::memcpy(merkle_commit.data(), root_ptr, root_size); + alpha_values[round_idx] = transcript.get_alpha(merkle_commit); + } + + // proof-of-work + if (fri_config.pow_bits != 0) { + bool valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); + if (!valid) return eIcicleError::SUCCESS; // return with verification_pass = false + transcript.set_pow_nonce(fri_proof.get_pow_nonce()); + } + + // get query indices + size_t seed = transcript.get_seed_for_query_phase(); + seed_rand_generator(seed); + ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; + std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); + + // const F* twiddles_inv = FriDomain::s_fri_domain.get_twiddles_inv(); + // uint64_t domain_max_size = FriDomain::s_fri_domain.get_max_size(); + + uint64_t domain_max_size = 0; + uint64_t max_log_size = 0; + F primitive_root = F::omega(log_input_size); + bool found_logn = false; + F omega = primitive_root; + const unsigned omegas_count = F::get_omegas_count(); + for (int i = 0; i < omegas_count; i++) { + omega = F::sqr(omega); + if (!found_logn) { + ++max_log_size; + found_logn = omega == F::one(); + if (found_logn) break; + } + } + domain_max_size = (int)pow(2, max_log_size); + if (domain_max_size == input_size) { + ICICLE_LOG_DEBUG << "FIXME SHANIE domain_max_size matches input_size, calculation can be removed"; + } + if (omega != F::one()) { // FIXME - redundant? + ICICLE_LOG_ERROR << "Primitive root provided to the InitDomain function is not a root-of-unity"; + return eIcicleError::INVALID_ARGUMENT; + } + + + auto twiddles = std::make_unique(domain_max_size+1); //FIXME - input_size or log_input_size? + twiddles[0] = F::one(); + for (int i = 1; i <= input_size; i++) { + twiddles[i] = twiddles[i - 1] * primitive_root; + } + + for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ + size_t query = query_indices[query_idx]; + size_t current_log_size = log_input_size; + for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { + ICICLE_LOG_DEBUG << "Query " << query << ", Round " << round_idx; + + MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; + MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); + bool valid = false; + eIcicleError err = current_round_tree.verify(proof_ref, valid); + if (err != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return err; + } + if (!valid){ + ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false + } + + MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2*query_idx+1, round_idx); + valid = false; + eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, valid); + if (err_sym != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return err_sym; + } + if (!valid){ + ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false + } + + // collinearity check + const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); //TODO shanie - need also to verify is leaf_index==query? + const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); + F leaf_data_f = F::from(leaf_data, leaf_size); + F leaf_data_sym_f = F::from(leaf_data_sym, leaf_size_sym); + F alpha = alpha_values[round_idx]; + uint64_t tw_idx = domain_max_size - ((domain_max_size>>current_log_size)*leaf_index); + F l_even = (leaf_data_f + leaf_data_sym_f) * F::inv_log_size(1); + F l_odd = ((leaf_data_f - leaf_data_sym_f) * F::inv_log_size(1)) * twiddles[tw_idx]; + F folded = l_even + (alpha * l_odd); + + if (round_idx == nof_fri_rounds - 1) { + const F* final_poly = fri_proof.get_final_poly(); + if (final_poly[query%final_poly_size] != folded) { + ICICLE_LOG_ERROR << "Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false; + } + } else { + MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2*query_idx, round_idx+1); + const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); + F leaf_data_folded_f = F::from(leaf_data_folded, leaf_size_folded); + if (leaf_data_folded_f != folded) { + ICICLE_LOG_ERROR << "Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false + } + } + current_log_size--; + } + } + verification_pass = true; + return eIcicleError::SUCCESS; } private: diff --git a/icicle/include/icicle/fri/fri_domain.h b/icicle/include/icicle/fri/fri_domain.h new file mode 100644 index 0000000000..7c29aacdb1 --- /dev/null +++ b/icicle/include/icicle/fri/fri_domain.h @@ -0,0 +1,143 @@ +#pragma once +#include "icicle/errors.h" +#include "icicle/utils/log.h" +#include "icicle/config_extension.h" +#include "icicle/runtime.h" +#include "icicle/fields/field.h" +#include "icicle/utils/utils.h" + + + +#include +#include +#include +#include +#include + +namespace icicle { + /** + * @struct FriInitDomainConfig + * @brief Configuration for initializing the Fri domain. + */ + struct FriInitDomainConfig { + icicleStreamHandle stream; /**< Stream for asynchronous execution. */ + bool is_async; /**< True if operation is asynchronous. Default value is false. */ + ConfigExtension* ext = nullptr; /**< Backend-specific extensions. */ + }; + + /** + * @brief Returns the default value of FriInitDomainConfig. + * + * @return Default value of FriInitDomainConfig. + */ + static FriInitDomainConfig default_fri_init_domain_config() + { + FriInitDomainConfig config = { + nullptr, // stream + false // is_async + }; + return config; + } + + + template + class FriDomain + { + int max_size = 0; + int max_log_size = 0; + std::unique_ptr twiddles_inv; + + public: + static eIcicleError fri_init_domain(const S& primitive_root, const FriInitDomainConfig& config); + static eIcicleError fri_release_domain(); + const inline int get_max_size() const { return max_size; } + + const inline S* get_twiddles_inv() const { return twiddles_inv.get(); } + static inline FriDomain s_fri_domain; + }; + + + /** + * @brief Initializes the Fri domain. + * + * This static function sets up the Fri domain by computing + * and storing the necessary twiddle factors. + * + * @param primitive_root The primitive root of unity used for Fri computations. + * @param config Configuration parameters for initializing the Fri domain, such as + * the maximum log size and other domain-specific settings. + * + * @return eIcicleError Returns `SUCCESS` if the domain is successfully initialized, + * or an appropriate error code if initialization fails. + */ + template + eIcicleError FriDomain::fri_init_domain(const S& primitive_root, const FriInitDomainConfig& config) + { + // (1) check if need to refresh domain. This need to be checked before locking the mutex to avoid unnecessary + // locking + if (s_fri_domain.twiddles_inv != nullptr) { return eIcicleError::SUCCESS; } + + // Lock the mutex to ensure thread safety during initialization + std::lock_guard lock(s_fri_domain.domain_mutex); + + // Check if domain is already initialized by another thread + if (s_fri_domain.twiddles_inv == nullptr) { + // (2) build the domain + + bool found_logn = false; + S omega = primitive_root; + const unsigned omegas_count = S::get_omegas_count(); + for (int i = 0; i < omegas_count; i++) { + omega = S::sqr(omega); + if (!found_logn) { + ++s_fri_domain.max_log_size; + found_logn = omega == S::one(); + if (found_logn) break; + } + } + + s_fri_domain.max_size = (int)pow(2, s_fri_domain.max_log_size); + if (omega != S::one()) { + ICICLE_LOG_ERROR << "Primitive root provided to the InitDomain function is not a root-of-unity"; + return eIcicleError::INVALID_ARGUMENT; + } + + // calculate twiddles_inv + // Note: radix-2 IFri needs ONE in last element (in addition to first element), therefore have n+1 elements + + // Using temp_twiddles_inv to store twiddles_inv before assigning to twiddles_inv using unique_ptr. + // This is to ensure that twiddles_inv are nullptr during calculation, + // otherwise the init domain function might return on another thread before twiddles_inv are calculated. + auto temp_twiddles_inv = std::make_unique(s_fri_domain.max_size); + S primitive_root_inv = S::inv(primitive_root); + + temp_twiddles_inv[0] = S::one(); + for (int i = 1; i < s_fri_domain.max_size; i++) { + temp_twiddles_inv[i] = temp_twiddles_inv[i - 1] * primitive_root_inv; + } + s_fri_domain.twiddles_inv = std::move(temp_twiddles_inv); // Assign twiddles_inv using unique_ptr + } + return eIcicleError::SUCCESS; + } + + + /** + * @brief Releases the resources associated with the NTT domain for the specified device. + * + * @param device The device whose NTT domain resources are to be released. + * + * @return eIcicleError Returns `SUCCESS` if the domain resources are successfully released, + * or an appropriate error code if the release process fails. + */ + template + eIcicleError FriDomain::fri_release_domain() + { + std::lock_guard lock(s_fri_domain.domain_mutex); + s_fri_domain.twiddles_inv.reset(); // Set twiddles to nullptr + s_fri_domain.max_size = 0; + s_fri_domain.max_log_size = 0; + return eIcicleError::SUCCESS; + } + + +} \ No newline at end of file diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 7e0ad7ed38..abcba65208 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -29,14 +30,15 @@ namespace icicle { * @param nof_queries Number of queries in the proof. * @param nof_fri_rounds Number of FRI rounds (rounds). */ - void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t stopping_degree) + void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns m_query_proofs.resize(2*nof_queries, std::vector(nof_fri_rounds)); //for each query, we have 2 proofs (for the leaf and its symmetric) - m_final_poly = std::make_unique(stopping_degree + 1); + m_final_poly_size = final_poly_size; + m_final_poly = std::make_unique(final_poly_size); } /** @@ -57,6 +59,45 @@ namespace icicle { return m_query_proofs[query_idx][round_idx]; } + /** + * @brief Returns a pair containing the pointer to the root data and its size. + * @return A pair of (root data pointer, root size). + */ + std::pair get_root(const size_t round_idx) const + { + return m_query_proofs[0][round_idx].get_root(); + } + + // /** + // * @brief Returns a tuple containing the pointer to the leaf data, its size and index. + // * @return A tuple of (leaf data pointer, leaf size, leaf_index). + // */ + // std::tuple get_leaf(const size_t query_idx, const size_t round_idx) const + // { + // return m_query_proofs[query_idx][round_idx].get_leaf(); + // } + + + /** + * @brief Get the number of FRI rounds in the proof. + * + * @return Number of FRI rounds. + */ + size_t get_nof_fri_rounds() const + { + return m_query_proofs[0].size(); + } + + /** + * @brief Get the final poly size. + * + * @return final_poly_size. + */ + size_t get_final_poly_size() const + { + return m_final_poly_size; + } + void set_pow_nonce(uint64_t pow_nonce) { m_pow_nonce = pow_nonce; @@ -74,8 +115,9 @@ namespace icicle { } private: - std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf + std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf. for each query, we have 2 proofs (for the leaf in [2*query] and its symmetric in [2*query+1]) std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) + size_t m_final_poly_size; // Size of the final polynomial uint64_t m_pow_nonce; // Proof-of-work nonce public: diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index f9aef855fb..f8aff17e0a 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -15,14 +15,15 @@ template class FriTranscript { public: - FriTranscript(FriTranscriptConfig&& transcript_config, const size_t log_input_size) + FriTranscript(FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) : m_transcript_config(std::move(transcript_config)) - , m_log_input_size(log_input_size) , m_prev_alpha(F::zero()) , m_first_round(true) , m_pow_nonce(0) { m_entry_0.clear(); + m_entry_0.reserve(1024); // pre-allocate some space + build_entry_0(log_input_size); m_first_round = true; } @@ -35,14 +36,11 @@ class FriTranscript F get_alpha(const std::vector& merkle_commit) { ICICLE_ASSERT(m_transcript_config.get_domain_separator_label().size() > 0) << "Domain separator label must be set"; - // Prepare a buffer for hashing - m_entry_0.reserve(1024); // pre-allocate some space std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space // Build the round's hash input if (m_first_round) { - build_entry_0(); build_hash_input_round_0(hash_input); m_first_round = false; } else { @@ -54,7 +52,8 @@ class FriTranscript const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); - reduce_hash_result_to_field(m_prev_alpha, hash_result); + m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); + // reduce_hash_result_to_field(m_prev_alpha, hash_result); //FIXME SHANIE - remove this return m_prev_alpha; } @@ -101,7 +100,6 @@ class FriTranscript private: const FriTranscriptConfig m_transcript_config; // Transcript configuration (labels, seeds, etc.) - const size_t m_log_input_size; // Logarithm of the initial input size const HashConfig m_hash_config; // hash config - default bool m_first_round; // Indicates if this is the first round std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds @@ -146,18 +144,19 @@ class FriTranscript dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); } - /** - * @brief Convert a hash output into a field element by copying a minimal number of bytes. - * @param alpha (OUT) The resulting field element. - * @param hash_result A buffer of bytes (from the hash function). - */ - void reduce_hash_result_to_field(F& alpha, const std::vector& hash_result) - { - alpha = F::zero(); - const int nof_bytes_to_copy = std::min(sizeof(alpha), static_cast(hash_result.size())); - std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); - alpha = alpha * F::one(); - } + //FIXME SHANIE - remove this + // /** + // * @brief Convert a hash output into a field element by copying a minimal number of bytes. + // * @param alpha (OUT) The resulting field element. + // * @param hash_result A buffer of bytes (from the hash function). + // */ + // void reduce_hash_result_to_field(F& alpha, const std::vector& hash_result) + // { + // alpha = F::zero(); + // const int nof_bytes_to_copy = std::min(sizeof(alpha), static_cast(hash_result.size())); + // std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); + // alpha = alpha * F::one(); + // } /** @@ -167,10 +166,10 @@ class FriTranscript * entry_0 =[DS||public.LE32()] * */ - void build_entry_0() + void build_entry_0(uint32_t log_input_size) { append_data(m_entry_0, m_transcript_config.get_domain_separator_label()); - append_u32(m_entry_0, m_log_input_size); + append_u32(m_entry_0, log_input_size); append_data(m_entry_0, m_transcript_config.get_public_state()); } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 3231c87d8e..fb5911ac34 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -25,6 +25,7 @@ #include "icicle/fri/fri_transcript_config.h" #include "test_base.h" +#include "icicle/fri/fri_domain.h" // FIXME SHANIE using namespace field_config; using namespace icicle; @@ -1714,10 +1715,12 @@ TYPED_TEST(FieldApiTest, Fri) ICICLE_LOG_DEBUG << "stopping_degree = " << stopping_degree; // Initialize domain - auto init_domain_config = default_ntt_init_domain_config(); - init_domain_config.is_async = false; + NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); + // FriInitDomainConfig init_fri_domain_config = default_fri_init_domain_config(); + // ICICLE_CHECK(fri_init_domain(scalar_t::omega(log_input_size), init_fri_domain_config)); + // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); // scalar_t::rand_host_many(scalars.get(), input_size); @@ -1729,17 +1732,18 @@ TYPED_TEST(FieldApiTest, Fri) Hash hash_for_merkle_tree = create_keccak_256_hash((2*32)); // arity = 2 Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); - FriTranscriptConfig transcript_config; + FriTranscriptConfig prover_transcript_config; + FriTranscriptConfig verifier_transcript_config; //FIXME - do verfier and prover need different configs? (moved...) FriConfig fri_config; fri_config.nof_queries = 2; FriProof fri_proof; - ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(prover_transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); bool verification_pass = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, verification_pass)); + ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(verifier_transcript_config), fri_proof, verification_pass)); ASSERT_EQ(true, verification_pass); From c4122f1e69d6d4c84ffe668d496faef8b920cb6f Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Mon, 24 Feb 2025 18:19:47 +0200 Subject: [PATCH 077/127] Change create_fri API to accept separate hash functions for Merkle tree leaves and node compression. Fix MerkleTree build to correctly handle const T* leaves by using get_leaf_element_size() instead of sizeof(T). --- icicle/backend/cpu/include/cpu_fri_backend.h | 25 +--- icicle/backend/cpu/include/cpu_fri_rounds.h | 5 +- icicle/include/icicle/fri/fri.h | 73 ++++------ icicle/include/icicle/fri/fri_domain.h | 143 ------------------- icicle/include/icicle/merkle/merkle_tree.h | 2 +- icicle/src/fri/fri.cpp | 25 ++-- icicle/src/fri/fri_c_api.cpp | 8 +- icicle/tests/test_field_api.cpp | 41 +++--- 8 files changed, 76 insertions(+), 246 deletions(-) delete mode 100644 icicle/include/icicle/fri/fri_domain.h diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 67c9d1785f..e150c724e3 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -21,12 +21,12 @@ namespace icicle { * * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree Stopping degree threshold for the final polynomial. - * @param merkle_trees A moved vector of MerkleTree pointers. + * @param merkle_trees A vector of MerkleTrees. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), - m_log_input_size(m_nof_fri_rounds + std::log2(static_cast(stopping_degree))), + m_log_input_size(merkle_trees.size() + std::log2(static_cast(stopping_degree+1))), m_input_size(pow(2, m_log_input_size)), m_fri_rounds(merkle_trees, m_log_input_size) { @@ -78,7 +78,7 @@ namespace icicle { * @return eIcicleError Error code indicating success or failure. */ eIcicleError commit_fold_phase(const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ - ICICLE_ASSERT(this->m_folding_factor==2) << "Folding factor must be 2"; + ICICLE_ASSERT(this->m_folding_factor==2) << "Currently only folding factor of 2 is supported"; //TODO SHANIE - remove when supporting other folding factors const F* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); @@ -91,21 +91,13 @@ namespace icicle { size_t current_log_size = m_log_input_size; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; ++round_idx){ - // if (current_size == (df + 1)) { FIXME SHANIE - do I need this? - // fri_proof.finalpoly->assign(round_evals->begin(), round_evals->end()); - // break; - // } - // Merkle tree for the current round_idx MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); - current_round_tree->build(reinterpret_cast(round_evals), sizeof(F)*current_size, MerkleTreeConfig()); + current_round_tree->build(round_evals, current_size, MerkleTreeConfig()); auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; - // FIXME SHANIE - do I need to add the root to the proof here? - // Add root to transcript and get alpha - // std::vector merkle_commit(root_ptr, root_ptr + root_size); //FIXME SHANIE - what is the right way to convert? std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); @@ -161,8 +153,7 @@ namespace icicle { * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. * @return eIcicleError */ - eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) - { + eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); @@ -175,14 +166,12 @@ namespace icicle { size_t leaf_idx = query % round_size; size_t leaf_idx_sym = (query + (round_size >> 1)) % round_size; F* round_evals = m_fri_rounds.get_round_evals(round_idx); - const std::byte* leaves = reinterpret_cast(round_evals); - uint64_t leaves_size = sizeof(F); MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); - eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); + eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(round_evals, round_size, leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); if (err != eIcicleError::SUCCESS) return err; MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2*query_idx+1, round_idx); - eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(leaves, sizeof(F), leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); + eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(round_evals, round_size, leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); if (err_sym != eIcicleError::SUCCESS) return err_sym; } } diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index e9a62961fe..68ad0f3028 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -21,12 +21,13 @@ class FriRounds * @brief Constructor that accepts an already-existing array of Merkle trees. * Ownership is transferred from the caller. * - * @param merkle_trees A moved vector of `unique_ptr`. + * @param merkle_trees A vector of MerkleTrees. + * @param log_input_size The log of the input size. */ FriRounds(std::vector merkle_trees, const size_t log_input_size) : m_merkle_trees(merkle_trees) { - size_t fold_rounds = m_merkle_trees.size(); //FIXME - consider stopping degree? + size_t fold_rounds = m_merkle_trees.size(); m_rounds_evals.resize(fold_rounds); for (size_t i = 0; i < fold_rounds; i++) { m_rounds_evals[i] = std::make_unique(1ULL << (log_input_size - i)); diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 9d1409804e..bc61fae7b1 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -12,8 +12,7 @@ #include "icicle/hash/hash.h" #include "icicle/merkle/merkle_tree.h" #include "icicle/fri/fri_transcript.h" -// #include "fri_domain.h" // FIXME SHANIE - +#include "icicle/utils/log.h" namespace icicle { @@ -30,17 +29,19 @@ class Fri; * @param input_size The size of the input polynomial - number of evaluations. * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param hash_for_merkle_tree The hash function used for the Merkle commitments. + * @param merkle_tree_leaves_hash The hash function used for leaves of the Merkle tree. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. * @return A `Fri` object built around the chosen backend. */ template Fri create_fri( - size_t input_size, - size_t folding_factor, - size_t stopping_degree, - Hash& hash_for_merkle_tree, - uint64_t output_store_min_layer = 0); + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer = 0); /** * @brief Constructor for the case where Merkle trees are already given. @@ -52,8 +53,8 @@ Fri create_fri( */ template Fri create_fri( - size_t folding_factor, - size_t stopping_degree, + const size_t folding_factor, + const size_t stopping_degree, std::vector merkle_trees); /** @@ -138,55 +139,28 @@ class Fri ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); - // const F* twiddles_inv = FriDomain::s_fri_domain.get_twiddles_inv(); - // uint64_t domain_max_size = FriDomain::s_fri_domain.get_max_size(); - uint64_t domain_max_size = 0; uint64_t max_log_size = 0; - F primitive_root = F::omega(log_input_size); - bool found_logn = false; - F omega = primitive_root; - const unsigned omegas_count = F::get_omegas_count(); - for (int i = 0; i < omegas_count; i++) { - omega = F::sqr(omega); - if (!found_logn) { - ++max_log_size; - found_logn = omega == F::one(); - if (found_logn) break; - } - } - domain_max_size = (int)pow(2, max_log_size); - if (domain_max_size == input_size) { - ICICLE_LOG_DEBUG << "FIXME SHANIE domain_max_size matches input_size, calculation can be removed"; - } - if (omega != F::one()) { // FIXME - redundant? - ICICLE_LOG_ERROR << "Primitive root provided to the InitDomain function is not a root-of-unity"; - return eIcicleError::INVALID_ARGUMENT; - } - - - auto twiddles = std::make_unique(domain_max_size+1); //FIXME - input_size or log_input_size? - twiddles[0] = F::one(); - for (int i = 1; i <= input_size; i++) { - twiddles[i] = twiddles[i - 1] * primitive_root; - } + F primitive_root_inv = F::omega_inv(log_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ size_t query = query_indices[query_idx]; size_t current_log_size = log_input_size; for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - ICICLE_LOG_DEBUG << "Query " << query << ", Round " << round_idx; - + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; + MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); bool valid = false; eIcicleError err = current_round_tree.verify(proof_ref, valid); if (err != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; return err; } if (!valid){ - ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; return eIcicleError::SUCCESS; // return with verification_pass = false } @@ -205,18 +179,19 @@ class Fri // collinearity check const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); //TODO shanie - need also to verify is leaf_index==query? const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); + ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; + ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) << "Leaf index symmetry from proof doesn't match query expected index"; F leaf_data_f = F::from(leaf_data, leaf_size); F leaf_data_sym_f = F::from(leaf_data_sym, leaf_size_sym); - F alpha = alpha_values[round_idx]; - uint64_t tw_idx = domain_max_size - ((domain_max_size>>current_log_size)*leaf_index); F l_even = (leaf_data_f + leaf_data_sym_f) * F::inv_log_size(1); - F l_odd = ((leaf_data_f - leaf_data_sym_f) * F::inv_log_size(1)) * twiddles[tw_idx]; + F l_odd = ((leaf_data_f - leaf_data_sym_f) * F::inv_log_size(1)) * F::pow(primitive_root_inv, leaf_index*(input_size>>current_log_size)); + F alpha = alpha_values[round_idx]; F folded = l_even + (alpha * l_odd); if (round_idx == nof_fri_rounds - 1) { const F* final_poly = fri_proof.get_final_poly(); if (final_poly[query%final_poly_size] != folded) { - ICICLE_LOG_ERROR << "Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + ICICLE_LOG_ERROR << "[VERIFIER] (last round) Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; return eIcicleError::SUCCESS; // return with verification_pass = false; } } else { @@ -224,7 +199,7 @@ class Fri const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); F leaf_data_folded_f = F::from(leaf_data_folded, leaf_size_folded); if (leaf_data_folded_f != folded) { - ICICLE_LOG_ERROR << "Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; + ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" << leaf_data_folded_f; return eIcicleError::SUCCESS; // return with verification_pass = false } } diff --git a/icicle/include/icicle/fri/fri_domain.h b/icicle/include/icicle/fri/fri_domain.h deleted file mode 100644 index 7c29aacdb1..0000000000 --- a/icicle/include/icicle/fri/fri_domain.h +++ /dev/null @@ -1,143 +0,0 @@ -#pragma once -#include "icicle/errors.h" -#include "icicle/utils/log.h" -#include "icicle/config_extension.h" -#include "icicle/runtime.h" -#include "icicle/fields/field.h" -#include "icicle/utils/utils.h" - - - -#include -#include -#include -#include -#include - -namespace icicle { - /** - * @struct FriInitDomainConfig - * @brief Configuration for initializing the Fri domain. - */ - struct FriInitDomainConfig { - icicleStreamHandle stream; /**< Stream for asynchronous execution. */ - bool is_async; /**< True if operation is asynchronous. Default value is false. */ - ConfigExtension* ext = nullptr; /**< Backend-specific extensions. */ - }; - - /** - * @brief Returns the default value of FriInitDomainConfig. - * - * @return Default value of FriInitDomainConfig. - */ - static FriInitDomainConfig default_fri_init_domain_config() - { - FriInitDomainConfig config = { - nullptr, // stream - false // is_async - }; - return config; - } - - - template - class FriDomain - { - int max_size = 0; - int max_log_size = 0; - std::unique_ptr twiddles_inv; - - public: - static eIcicleError fri_init_domain(const S& primitive_root, const FriInitDomainConfig& config); - static eIcicleError fri_release_domain(); - const inline int get_max_size() const { return max_size; } - - const inline S* get_twiddles_inv() const { return twiddles_inv.get(); } - static inline FriDomain s_fri_domain; - }; - - - /** - * @brief Initializes the Fri domain. - * - * This static function sets up the Fri domain by computing - * and storing the necessary twiddle factors. - * - * @param primitive_root The primitive root of unity used for Fri computations. - * @param config Configuration parameters for initializing the Fri domain, such as - * the maximum log size and other domain-specific settings. - * - * @return eIcicleError Returns `SUCCESS` if the domain is successfully initialized, - * or an appropriate error code if initialization fails. - */ - template - eIcicleError FriDomain::fri_init_domain(const S& primitive_root, const FriInitDomainConfig& config) - { - // (1) check if need to refresh domain. This need to be checked before locking the mutex to avoid unnecessary - // locking - if (s_fri_domain.twiddles_inv != nullptr) { return eIcicleError::SUCCESS; } - - // Lock the mutex to ensure thread safety during initialization - std::lock_guard lock(s_fri_domain.domain_mutex); - - // Check if domain is already initialized by another thread - if (s_fri_domain.twiddles_inv == nullptr) { - // (2) build the domain - - bool found_logn = false; - S omega = primitive_root; - const unsigned omegas_count = S::get_omegas_count(); - for (int i = 0; i < omegas_count; i++) { - omega = S::sqr(omega); - if (!found_logn) { - ++s_fri_domain.max_log_size; - found_logn = omega == S::one(); - if (found_logn) break; - } - } - - s_fri_domain.max_size = (int)pow(2, s_fri_domain.max_log_size); - if (omega != S::one()) { - ICICLE_LOG_ERROR << "Primitive root provided to the InitDomain function is not a root-of-unity"; - return eIcicleError::INVALID_ARGUMENT; - } - - // calculate twiddles_inv - // Note: radix-2 IFri needs ONE in last element (in addition to first element), therefore have n+1 elements - - // Using temp_twiddles_inv to store twiddles_inv before assigning to twiddles_inv using unique_ptr. - // This is to ensure that twiddles_inv are nullptr during calculation, - // otherwise the init domain function might return on another thread before twiddles_inv are calculated. - auto temp_twiddles_inv = std::make_unique(s_fri_domain.max_size); - S primitive_root_inv = S::inv(primitive_root); - - temp_twiddles_inv[0] = S::one(); - for (int i = 1; i < s_fri_domain.max_size; i++) { - temp_twiddles_inv[i] = temp_twiddles_inv[i - 1] * primitive_root_inv; - } - s_fri_domain.twiddles_inv = std::move(temp_twiddles_inv); // Assign twiddles_inv using unique_ptr - } - return eIcicleError::SUCCESS; - } - - - /** - * @brief Releases the resources associated with the NTT domain for the specified device. - * - * @param device The device whose NTT domain resources are to be released. - * - * @return eIcicleError Returns `SUCCESS` if the domain resources are successfully released, - * or an appropriate error code if the release process fails. - */ - template - eIcicleError FriDomain::fri_release_domain() - { - std::lock_guard lock(s_fri_domain.domain_mutex); - s_fri_domain.twiddles_inv.reset(); // Set twiddles to nullptr - s_fri_domain.max_size = 0; - s_fri_domain.max_log_size = 0; - return eIcicleError::SUCCESS; - } - - -} \ No newline at end of file diff --git a/icicle/include/icicle/merkle/merkle_tree.h b/icicle/include/icicle/merkle/merkle_tree.h index 3ec2f29c1e..5fae016205 100644 --- a/icicle/include/icicle/merkle/merkle_tree.h +++ b/icicle/include/icicle/merkle/merkle_tree.h @@ -75,7 +75,7 @@ namespace icicle { inline eIcicleError build(const T* leaves, uint64_t nof_leaves /* number of leaf elements */, const MerkleTreeConfig& config) { - return build(reinterpret_cast(leaves), nof_leaves * sizeof(T), config); + return build(reinterpret_cast(leaves), nof_leaves * m_backend->get_leaf_element_size(), config); } /** diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 9f7dda3152..e1bc026abe 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -6,6 +6,7 @@ #include "icicle/hash/hash.h" #include "icicle/utils/log.h" #include +#include namespace icicle { @@ -17,8 +18,8 @@ namespace icicle { */ template Fri create_fri_with_merkle_trees( - size_t folding_factor, - size_t stopping_degree, + const size_t folding_factor, + const size_t stopping_degree, std::vector merkle_trees) { std::shared_ptr> backend; @@ -38,11 +39,12 @@ namespace icicle { */ template <> Fri create_fri( - size_t input_size, - size_t folding_factor, - size_t stopping_degree, - Hash& hash_for_merkle_tree, - uint64_t output_store_min_layer) + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) { ICICLE_ASSERT(folding_factor == 2) << "Only folding factor of 2 is supported"; size_t log_input_size = static_cast(std::log2(static_cast(input_size))); @@ -52,10 +54,13 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); - std::vector hashes_for_merkle_tree_vec(fold_rounds, hash_for_merkle_tree); + size_t first_merkle_tree_height = log_input_size+1; //FIXME SHANIE - assuming merkle_tree_arity = 2 + std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); + layer_hashes[0] = merkle_tree_leaves_hash; + uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); for (size_t i = 0; i < fold_rounds; i++) { - merkle_trees.emplace_back(MerkleTree::create(hashes_for_merkle_tree_vec, sizeof(scalar_t), output_store_min_layer)); - hashes_for_merkle_tree_vec.pop_back(); + merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); + layer_hashes.pop_back(); } return create_fri_with_merkle_trees( folding_factor, diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index be40a4f42b..ab177197b1 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -38,7 +38,8 @@ struct FriCreateHashFFI { size_t input_size; size_t folding_factor; size_t stopping_degree; - Hash* hash_for_merkle_tree; + Hash* merkle_tree_leaves_hash; + Hash* merkle_tree_compress_hash; uint64_t output_store_min_layer; }; @@ -66,7 +67,7 @@ CONCAT_EXPAND(FIELD, fri_create)( const FriTranscriptConfigFFI* ffi_transcript_config ) { - if (!create_config || !create_config->hash_for_merkle_tree) { + if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { ICICLE_LOG_ERROR << "Invalid FRI creation config."; return nullptr; } @@ -113,7 +114,8 @@ CONCAT_EXPAND(FIELD, fri_create)( create_config->input_size, create_config->folding_factor, create_config->stopping_degree, - *(create_config->hash_for_merkle_tree), + *(create_config->merkle_tree_leaves_hash), + *(create_config->merkle_tree_compress_hash), create_config->output_store_min_layer )); } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index fb5911ac34..c6bc2e2f9a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -25,7 +26,6 @@ #include "icicle/fri/fri_transcript_config.h" #include "test_base.h" -#include "icicle/fri/fri_domain.h" // FIXME SHANIE using namespace field_config; using namespace icicle; @@ -1700,48 +1700,49 @@ TEST_F(FieldApiTestBase, SumcheckSingleInputProgram) TYPED_TEST(FieldApiTest, Fri) { // Randomize configuration - // const int log_input_size = rand_uint_32b(5, 13); - const int log_input_size = 5; + const int log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; - const int folding_factor = 2; - // const int log_stopping_degree = rand_uint_32b(0, 3); - const int log_stopping_degree = 3; - const size_t stopping_degree = 1 << log_stopping_degree; + const int folding_factor = 2; // TODO SHANIE - add support for other folding factors + const int log_stopping_size = rand_uint_32b(0, log_input_size-2); + const size_t stopping_size = 1 << log_stopping_size; + const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; + const size_t pow_bits = rand_uint_32b(0, 3); + const size_t nof_queries = rand_uint_32b(2, 4); ICICLE_LOG_DEBUG << "log_input_size = " << log_input_size; ICICLE_LOG_DEBUG << "input_size = " << input_size; ICICLE_LOG_DEBUG << "folding_factor = " << folding_factor; ICICLE_LOG_DEBUG << "stopping_degree = " << stopping_degree; - // Initialize domain + // Initialize ntt domain NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); - // FriInitDomainConfig init_fri_domain_config = default_fri_init_domain_config(); - // ICICLE_CHECK(fri_init_domain(scalar_t::omega(log_input_size), init_fri_domain_config)); - // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); - // scalar_t::rand_host_many(scalars.get(), input_size); - for (size_t i = 0; i < input_size; i++) { - scalars[i] = scalar_t::from(i); - } + scalar_t::rand_host_many(scalars.get(), input_size); // ===== Prover side ====== - Hash hash_for_merkle_tree = create_keccak_256_hash((2*32)); // arity = 2 - Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + uint64_t merkle_tree_arity = 2; // TODO SHANIE - add support for other arities + + // Define hashers for merkle tree + Hash hash = Keccak256::create(sizeof(scalar_t)); // hash element -> 32B + Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B + + Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); FriTranscriptConfig prover_transcript_config; - FriTranscriptConfig verifier_transcript_config; //FIXME - do verfier and prover need different configs? (moved...) + FriTranscriptConfig verifier_transcript_config; //FIXME SHANIE - verfier and prover should have the same config FriConfig fri_config; - fri_config.nof_queries = 2; + fri_config.nof_queries = nof_queries; + fri_config.pow_bits = pow_bits; FriProof fri_proof; ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(prover_transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== - Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer); + Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool verification_pass = false; ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(verifier_transcript_config), fri_proof, verification_pass)); From c06a038c857bef2e1a519fa6da1e7a5daa564450 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 25 Feb 2025 14:58:20 +0200 Subject: [PATCH 078/127] FIXMEs and TODOs fixed and removed --- icicle/include/icicle/fri/fri.h | 4 ++-- icicle/include/icicle/fri/fri_transcript.h | 16 ---------------- icicle/include/icicle/merkle/merkle_proof.h | 18 ------------------ icicle/src/fri/fri.cpp | 14 ++++++++------ icicle/src/fri/fri_c_api.cpp | 1 - 5 files changed, 10 insertions(+), 43 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index bc61fae7b1..ae0280bb4c 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -104,7 +104,7 @@ class Fri eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, // FIXME - const? + FriProof& fri_proof, bool& verification_pass /* OUT */) const { verification_pass = false; @@ -177,7 +177,7 @@ class Fri } // collinearity check - const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); //TODO shanie - need also to verify is leaf_index==query? + const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) << "Leaf index symmetry from proof doesn't match query expected index"; diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index f8aff17e0a..389915f47b 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -53,7 +53,6 @@ class FriTranscript std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); - // reduce_hash_result_to_field(m_prev_alpha, hash_result); //FIXME SHANIE - remove this return m_prev_alpha; } @@ -144,21 +143,6 @@ class FriTranscript dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); } - //FIXME SHANIE - remove this - // /** - // * @brief Convert a hash output into a field element by copying a minimal number of bytes. - // * @param alpha (OUT) The resulting field element. - // * @param hash_result A buffer of bytes (from the hash function). - // */ - // void reduce_hash_result_to_field(F& alpha, const std::vector& hash_result) - // { - // alpha = F::zero(); - // const int nof_bytes_to_copy = std::min(sizeof(alpha), static_cast(hash_result.size())); - // std::memcpy(&alpha, hash_result.data(), nof_bytes_to_copy); - // alpha = alpha * F::one(); - // } - - /** * @brief Build the hash input for round 0 (commit phase 0). * diff --git a/icicle/include/icicle/merkle/merkle_proof.h b/icicle/include/icicle/merkle/merkle_proof.h index d89be1bea1..dc4291d0d6 100644 --- a/icicle/include/icicle/merkle/merkle_proof.h +++ b/icicle/include/icicle/merkle/merkle_proof.h @@ -164,24 +164,6 @@ namespace icicle { std::vector m_root; std::vector m_path; -//TODO SHANIE - remove from here - public: - // For debugging and testing purposes - // FIXME SHANIE: how to get correct element_size and nof_elements? (for print_bytes) - eIcicleError print_proof() const - { - std::cout << "Merkle Proof:" << std::endl; - std::cout << " Leaf index: " << m_leaf_index << std::endl; - std::cout << " Pruned path: " << (m_pruned ? "Yes" : "No") << std::endl; - std::cout << " Leaf data:" << std::endl; - print_bytes(m_leaf.data(), m_leaf.size(), 1); - std::cout << " Root data:" << std::endl; - print_bytes(m_root.data(), m_root.size(), 1); - std::cout << " Path data:" << std::endl; - print_bytes(m_path.data(), m_path.size(), 1); - return eIcicleError::SUCCESS; - } -//TODO SHANIE - remove until here }; } // namespace icicle \ No newline at end of file diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index e1bc026abe..177df4c8ff 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -46,15 +46,17 @@ namespace icicle { const Hash& merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - ICICLE_ASSERT(folding_factor == 2) << "Only folding factor of 2 is supported"; - size_t log_input_size = static_cast(std::log2(static_cast(input_size))); - size_t df = stopping_degree; - size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; - size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; + const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); + const size_t df = stopping_degree; + const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; std::vector merkle_trees; merkle_trees.reserve(fold_rounds); - size_t first_merkle_tree_height = log_input_size+1; //FIXME SHANIE - assuming merkle_tree_arity = 2 + size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size()/merkle_tree_compress_hash.output_size(); + ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; + size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index ab177197b1..214ffbb06a 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -7,7 +7,6 @@ using namespace field_config; -// TODO: Add methods for `prove`, `verify`, and the `proof` struct. extern "C" { From 2efd9ae04cd560d8eb00107c54652cb2d42cd97e Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 11:59:13 +0200 Subject: [PATCH 079/127] FRI: ext_field support added. F::from() function added for ext_field --- icicle/backend/cpu/include/cpu_fri_backend.h | 19 +-- icicle/backend/cpu/src/field/cpu_fri.cpp | 15 +- icicle/include/icicle/backend/fri_backend.h | 41 ++++- .../include/icicle/fields/complex_extension.h | 9 + .../include/icicle/fields/quartic_extension.h | 12 ++ icicle/include/icicle/fri/fri.h | 30 ++-- icicle/include/icicle/fri/fri_config.h | 1 - icicle/include/icicle/fri/fri_transcript.h | 2 +- icicle/src/fri/fri.cpp | 159 ++++++++++++++++-- icicle/src/fri/fri_c_api.cpp | 156 ++++++++++++++++- icicle/tests/test_field_api.cpp | 29 ++-- icicle/tests/test_mod_arithmetic_api.h | 6 + 12 files changed, 409 insertions(+), 70 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index e150c724e3..89b92fc387 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -12,8 +12,8 @@ #include "icicle/utils/log.h" namespace icicle { - template - class CpuFriBackend : public FriBackend + template + class CpuFriBackend : public FriBackend { public: /** @@ -24,7 +24,7 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree, merkle_trees), + : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), m_log_input_size(merkle_trees.size() + std::log2(static_cast(stopping_degree+1))), m_input_size(pow(2, m_log_input_size)), @@ -38,10 +38,6 @@ namespace icicle { const F* input_data, FriProof& fri_proof /*out*/) override { - if (fri_config.use_extension_field) { - ICICLE_LOG_ERROR << "FriConfig::use_extension_field = true is currently unsupported"; - return eIcicleError::API_NOT_IMPLEMENTED; - } ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); @@ -80,8 +76,8 @@ namespace icicle { eIcicleError commit_fold_phase(const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ ICICLE_ASSERT(this->m_folding_factor==2) << "Currently only folding factor of 2 is supported"; //TODO SHANIE - remove when supporting other folding factors - const F* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); - uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); + const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); + uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with capacity 2^(m_log_input_size - round_idx). F* round_evals = m_fri_rounds.get_round_evals(0); @@ -109,9 +105,9 @@ namespace icicle { std::vector podd(half); for (size_t i = 0; i < half; ++i){ - peven[i] = (round_evals[i] + round_evals[i + half]) * F::inv_log_size(1); + peven[i] = (round_evals[i] + round_evals[i + half]) * S::inv_log_size(1); uint64_t tw_idx = domain_max_size - ((domain_max_size>>current_log_size) * i); - podd[i] = ((round_evals[i] - round_evals[i + half]) * F::inv_log_size(1)) * twiddles[tw_idx]; + podd[i] = ((round_evals[i] - round_evals[i + half]) * S::inv_log_size(1)) * twiddles[tw_idx]; } if (round_idx == m_nof_fri_rounds - 1){ @@ -127,7 +123,6 @@ namespace icicle { current_size>>=1; current_log_size--; } - return eIcicleError::SUCCESS; } diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index 480ea7ab97..b80eead5a9 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -2,15 +2,22 @@ #include "cpu_fri_backend.h" using namespace field_config; +using namespace icicle; + namespace icicle { - template - eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) + template + eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); + backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); return eIcicleError::SUCCESS; } - REGISTER_FRI_FACTORY_BACKEND("CPU", cpu_create_fri_backend); + +REGISTER_FRI_FACTORY_BACKEND("CPU", (cpu_create_fri_backend)); +#ifdef EXT_FIELD + REGISTER_FRI_EXT_FACTORY_BACKEND("CPU", (cpu_create_fri_backend)); +#endif // EXT_FIELD + } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index 5dbbbe56a3..3b2ae168f6 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -20,7 +20,7 @@ namespace icicle { * @brief Abstract base class for FRI backend implementations. * @tparam F Field type used in the FRI protocol. */ -template +template class FriBackend { public: @@ -68,9 +68,9 @@ class FriBackend /** * @brief A function signature for creating a FriBackend instance for a specific device. */ -template +template using FriFactoryImpl = - std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; + std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; /** * @brief Register a FRI backend factory for a specific device type. @@ -78,7 +78,7 @@ using FriFactoryImpl = * @param deviceType String identifier for the device type. * @param impl A factory function that creates a FriBackend. */ -void register_fri_factory(const std::string& deviceType, FriFactoryImpl impl); +void register_fri_factory(const std::string& deviceType, FriFactoryImpl impl); /** * @brief Macro to register a FRI backend factory. @@ -95,4 +95,37 @@ void register_fri_factory(const std::string& deviceType, FriFactoryImpl + using FriExtFactoryImpl = + std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; + + /** + * @brief Register a FRI backend factory for a specific device type. + * + * @param deviceType String identifier for the device type. + * @param impl A factory function that creates a FriBackend. + */ + void register_extension_fri_factory(const std::string& deviceType, FriExtFactoryImpl impl); + + /** + * @brief Macro to register a FRI backend factory. + * + * This macro registers a factory function for a specific backend by calling + * `register_fri_factory` at runtime. + * + */ + #define REGISTER_FRI_EXT_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ + namespace { \ + static bool UNIQUE(_reg_fri_ext_field) = []() -> bool { \ + register_extension_fri_factory(DEVICE_TYPE, static_cast>(FUNC)); \ + return true; \ + }(); \ + } + +#endif // EXT_FIELD + } // namespace icicle diff --git a/icicle/include/icicle/fields/complex_extension.h b/icicle/include/icicle/fields/complex_extension.h index c65ad37ec4..a50a997b9b 100644 --- a/icicle/include/icicle/fields/complex_extension.h +++ b/icicle/include/icicle/fields/complex_extension.h @@ -305,6 +305,15 @@ class ComplexExtensionField } return res; } + + /* Receives an array of bytes and its size and returns extension field element. */ + static constexpr HOST_DEVICE_INLINE ComplexExtensionField from(const std::byte* in, unsigned nof_bytes) + { + ICICLE_ASSERT(nof_bytes >= 2 * sizeof(FF)) << "Input size is too small"; + return ComplexExtensionField{FF::from(in, sizeof(FF)), + FF::from(in + sizeof(FF), sizeof(FF))}; + + } }; #ifdef __CUDACC__ diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 6478e915c6..de4c168c42 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -1,5 +1,6 @@ #pragma once +#include "icicle/errors.h" #include "icicle/fields/field.h" #include "icicle/utils/modifiers.h" @@ -262,6 +263,17 @@ class QuarticExtensionField } return res; } + + /* Receives an array of bytes and its size and returns extension field element. */ + static constexpr HOST_DEVICE_INLINE QuarticExtensionField from(const std::byte* in, unsigned nof_bytes) + { + ICICLE_ASSERT(nof_bytes >= 4 * sizeof(FF)) << "Input size is too small"; + return QuarticExtensionField{FF::from(in, sizeof(FF)), + FF::from(in + sizeof(FF), sizeof(FF)), + FF::from(in + 2 * sizeof(FF), sizeof(FF)), + FF::from(in + 3 * sizeof(FF), sizeof(FF))}; + + } }; #if __CUDACC__ template diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index ae0280bb4c..46110e41f7 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -19,7 +19,7 @@ namespace icicle { /** * @brief Forward declaration for the FRI class template. */ -template +template class Fri; /** @@ -34,8 +34,8 @@ class Fri; * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. * @return A `Fri` object built around the chosen backend. */ -template -Fri create_fri( +template +Fri create_fri( const size_t input_size, const size_t folding_factor, const size_t stopping_degree, @@ -51,8 +51,8 @@ Fri create_fri( * @param merkle_trees A reference vector of `MerkleTree` objects. * @return A `Fri` object built around the chosen backend. */ -template -Fri create_fri( +template +Fri create_fri( const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees); @@ -64,15 +64,15 @@ Fri create_fri( * * @tparam F The field type used in the FRI protocol. */ -template +template class Fri { public: /** * @brief Constructor for the Fri class. - * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. + * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. */ - explicit Fri(std::shared_ptr> backend) + explicit Fri(std::shared_ptr> backend) : m_backend(std::move(backend)) {} @@ -141,7 +141,7 @@ class Fri uint64_t domain_max_size = 0; uint64_t max_log_size = 0; - F primitive_root_inv = F::omega_inv(log_input_size); + S primitive_root_inv = S::omega_inv(log_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ size_t query = query_indices[query_idx]; @@ -181,10 +181,10 @@ class Fri const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) << "Leaf index symmetry from proof doesn't match query expected index"; - F leaf_data_f = F::from(leaf_data, leaf_size); - F leaf_data_sym_f = F::from(leaf_data_sym, leaf_size_sym); - F l_even = (leaf_data_f + leaf_data_sym_f) * F::inv_log_size(1); - F l_odd = ((leaf_data_f - leaf_data_sym_f) * F::inv_log_size(1)) * F::pow(primitive_root_inv, leaf_index*(input_size>>current_log_size)); + const F& leaf_data_f = *reinterpret_cast(leaf_data); + const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); + F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); + F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * S::pow(primitive_root_inv, leaf_index*(input_size>>current_log_size)); F alpha = alpha_values[round_idx]; F folded = l_even + (alpha * l_odd); @@ -197,7 +197,7 @@ class Fri } else { MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2*query_idx, round_idx+1); const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); - F leaf_data_folded_f = F::from(leaf_data_folded, leaf_size_folded); + const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); if (leaf_data_folded_f != folded) { ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" << leaf_data_folded_f; return eIcicleError::SUCCESS; // return with verification_pass = false @@ -211,7 +211,7 @@ class Fri } private: - std::shared_ptr> m_backend; // Shared pointer to the backend for FRI operations. + std::shared_ptr> m_backend; // Shared pointer to the backend for FRI operations. }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index 5b6335d8e0..c7dba1a03a 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -15,7 +15,6 @@ namespace icicle { */ struct FriConfig { icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. - bool use_extension_field = false; // If true, then use extension field for the fiat shamir result. Recommended for small fields for security. TODO SHANIE - this was not part of the design plan, do we need to add this like in sumcheck? size_t pow_bits = 0; // Number of leading zeros required for proof-of-work. Default is 0. size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 389915f47b..5ea39c7ae7 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -139,7 +139,7 @@ class FriTranscript */ void append_field(std::vector& dest, const F& field) { - const std::byte* data_bytes = reinterpret_cast(field.limbs_storage.limbs); + const std::byte* data_bytes = reinterpret_cast(&field); dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 177df4c8ff..b23675a0bc 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -1,6 +1,7 @@ #include "icicle/errors.h" #include "icicle/fri/fri.h" #include "icicle/backend/fri_backend.h" +#include "icicle/fields/stark_fields/babybear.h" #include "icicle/merkle/merkle_tree.h" #include "icicle/dispatcher.h" #include "icicle/hash/hash.h" @@ -10,26 +11,26 @@ namespace icicle { - ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryImpl); - + using FriFactoryScalar = FriFactoryImpl; + ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryScalar); /** * @brief Create a FRI instance. * @return A `Fri` object built around the chosen backend. */ - template - Fri create_fri_with_merkle_trees( + template + Fri create_fri_with_merkle_trees( const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) { - std::shared_ptr> backend; + std::shared_ptr> backend; ICICLE_CHECK(FriDispatcher::execute( folding_factor, stopping_degree, merkle_trees, backend)); - Fri fri{backend}; + Fri fri{backend}; return fri; } @@ -37,8 +38,8 @@ namespace icicle { * @brief Specialization of create_fri for the case of * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). */ - template <> - Fri create_fri( + template + Fri create_fri_template( const size_t input_size, const size_t folding_factor, const size_t stopping_degree, @@ -64,7 +65,7 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees( + return create_fri_with_merkle_trees( folding_factor, stopping_degree, merkle_trees); @@ -74,16 +75,148 @@ namespace icicle { * @brief Specialization of create_fri for the case of * (folding_factor, stopping_degree, vector&&). */ - template <> - Fri create_fri( + template + Fri create_fri_template( size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_with_merkle_trees( + return create_fri_with_merkle_trees( + folding_factor, + stopping_degree, + merkle_trees); + } + + #ifdef EXT_FIELD + using FriExtFactoryScalar = FriFactoryImpl; + ICICLE_DISPATCHER_INST(FriExtFieldDispatcher, extension_fri_factory, FriExtFactoryScalar); + /** + * @brief Create a FRI instance. + * @return A `Fri` object built around the chosen backend. + */ + template + Fri create_fri_with_merkle_trees_ext( + const size_t folding_factor, + const size_t stopping_degree, + std::vector merkle_trees) + { + std::shared_ptr> backend; + ICICLE_CHECK(FriExtFieldDispatcher::execute( + folding_factor, + stopping_degree, + merkle_trees, + backend)); + + Fri fri{backend}; + return fri; + } + /** + * @brief Specialization of create_fri for the case of + * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + */ + template + Fri create_fri_template_ext( + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) + { + ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; + const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); + const size_t df = stopping_degree; + const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + + std::vector merkle_trees; + merkle_trees.reserve(fold_rounds); + size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size()/merkle_tree_compress_hash.output_size(); + ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; + size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; + std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); + layer_hashes[0] = merkle_tree_leaves_hash; + uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); + for (size_t i = 0; i < fold_rounds; i++) { + merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); + layer_hashes.pop_back(); + } + return create_fri_with_merkle_trees_ext( + folding_factor, + stopping_degree, + merkle_trees); + } + + /** + * @brief Specialization of create_fri for the case of + * (folding_factor, stopping_degree, vector&&). + */ + template + Fri create_fri_template_ext( + size_t folding_factor, + size_t stopping_degree, + std::vector merkle_trees) + { + return create_fri_with_merkle_trees_ext( + folding_factor, + stopping_degree, + merkle_trees); + } + #endif // EXT_FIELD + + + template <> + Fri create_fri( + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) + { + return create_fri_template( + input_size, folding_factor, stopping_degree, - merkle_trees); + merkle_tree_leaves_hash, + merkle_tree_compress_hash, + output_store_min_layer); } + template <> + Fri create_fri( + size_t folding_factor, + size_t stopping_degree, + std::vector merkle_trees){ + return create_fri_template(folding_factor, stopping_degree, merkle_trees); + } + + #ifdef EXT_FIELD + template <> + Fri create_fri( + size_t folding_factor, + size_t stopping_degree, + std::vector merkle_trees){ + return create_fri_template_ext(folding_factor, stopping_degree, merkle_trees); + } + + template <> + Fri create_fri( + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) + { + return create_fri_template_ext( + input_size, + folding_factor, + stopping_degree, + merkle_tree_leaves_hash, + merkle_tree_compress_hash, + output_store_min_layer); + } +#endif + } // namespace icicle diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index 214ffbb06a..64ecf8396c 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -1,4 +1,5 @@ #include "icicle/fields/field_config.h" +#include "icicle/utils/log.h" #include "icicle/utils/utils.h" #include "icicle/fri/fri.h" #include "icicle/fri/fri_transcript_config.h" @@ -11,7 +12,11 @@ using namespace field_config; extern "C" { // Define the FRI handle type -typedef icicle::Fri FriHandle; +typedef icicle::Fri FriHandle; + +#ifdef EXT_FIELD +typedef icicle::Fri FriHandleExt; +#endif // Structure to represent the FFI transcript configuration. struct FriTranscriptConfigFFI { @@ -109,7 +114,7 @@ CONCAT_EXPAND(FIELD, fri_create)( }; // Create and return the Fri instance - return new icicle::Fri(icicle::create_fri( + return new icicle::Fri(icicle::create_fri( create_config->input_size, create_config->folding_factor, create_config->stopping_degree, @@ -184,7 +189,7 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( }; // Create and return the Fri instance - return new icicle::Fri(icicle::create_fri( + return new icicle::Fri(icicle::create_fri( create_config->folding_factor, create_config->stopping_degree, merkle_trees_vec @@ -209,5 +214,150 @@ eIcicleError CONCAT_EXPAND(FIELD, fri_delete)(const FriHandle* fri_handle) return eIcicleError::SUCCESS; } + + + +#ifdef EXT_FIELD // EXT_FIELD +FriHandleExt* +CONCAT_EXPAND(FIELD, fri_create_ext)( + const FriCreateHashFFI* create_config, + const FriTranscriptConfigFFI* ffi_transcript_config +) +{ + if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { + ICICLE_LOG_ERROR << "Invalid FRI creation config."; + return nullptr; + } + if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; + return nullptr; + } + + ICICLE_LOG_DEBUG << "Constructing FRI EXT_FIELD instance from FFI (hash-based)"; + + // Convert byte arrays to vectors + std::vector domain_separator_label( + ffi_transcript_config->domain_separator_label, + ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); + std::vector round_challenge_label( + ffi_transcript_config->round_challenge_label, + ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); + std::vector commit_label( + ffi_transcript_config->commit_label, + ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + std::vector nonce_label( + ffi_transcript_config->nonce_label, + ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + std::vector public_state( + ffi_transcript_config->public_state, + ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); + + // Construct a FriTranscriptConfig + FriTranscriptConfig config{ + *(ffi_transcript_config->hasher), + std::move(domain_separator_label), + std::move(round_challenge_label), + std::move(commit_label), + std::move(nonce_label), + std::move(public_state), + *(ffi_transcript_config->seed_rng) + }; + + // Create and return the Fri instance for the extension field + return new icicle::Fri(icicle::create_fri( + create_config->input_size, + create_config->folding_factor, + create_config->stopping_degree, + *(create_config->merkle_tree_leaves_hash), + *(create_config->merkle_tree_compress_hash), + create_config->output_store_min_layer + )); +} + + +FriHandleExt* +CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( + const FriCreateWithTreesFFI* create_config, + const FriTranscriptConfigFFI* ffi_transcript_config +) +{ + if (!create_config || !create_config->merkle_trees) { + ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; + return nullptr; + } + if (!ffi_transcript_config || !ffi_transcript_config + || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; + return nullptr; + } + + ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (with existing trees)"; + + // Convert the raw array of MerkleTree* into a std::vector + std::vector merkle_trees_vec; + merkle_trees_vec.reserve(create_config->merkle_trees_count); + for (size_t i = 0; i < create_config->merkle_trees_count; ++i) { + merkle_trees_vec.push_back(create_config->merkle_trees[i]); + } + + + // Convert byte arrays to vectors + //TODO SHANIE - check if this is the correct way + std::vector domain_separator_label( + ffi_transcript_config->domain_separator_label, + ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); + std::vector round_challenge_label( + ffi_transcript_config->round_challenge_label, + ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); + std::vector commit_label( + ffi_transcript_config->commit_label, + ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + std::vector nonce_label( + ffi_transcript_config->nonce_label, + ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + std::vector public_state( + ffi_transcript_config->public_state, + ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); + + + // Construct a FriTranscriptConfig + FriTranscriptConfig config{ + *(ffi_transcript_config->hasher), + std::move(domain_separator_label), + std::move(round_challenge_label), + std::move(commit_label), + std::move(nonce_label), + std::move(public_state), + *(ffi_transcript_config->seed_rng) + }; + + // Create and return the Fri instance + return new icicle::Fri(icicle::create_fri( + create_config->folding_factor, + create_config->stopping_degree, + merkle_trees_vec + )); +} + +/** + * @brief Deletes the given Fri instance. + * @param fri_handle_ext Pointer to the Fri instance to be deleted. + * @return eIcicleError indicating the success or failure of the operation. + */ + eIcicleError CONCAT_EXPAND(FIELD, fri_delete_ext)(const FriHandleExt* fri_handle_ext) + { + if (!fri_handle_ext) { + ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; + return eIcicleError::INVALID_ARGUMENT; + } + + ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; + delete fri_handle_ext; + + return eIcicleError::SUCCESS; + } + + +#endif // EXT_FIELD } // extern "C" diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 147def7738..0c964b9b12 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -576,9 +576,9 @@ TEST_F(FieldTestBase, SumcheckSingleInputProgram) #endif // SUMCHECK -#ifdef FRI +// #ifdef FRI -TYPED_TEST(FieldApiTest, Fri) +TYPED_TEST(FieldTest, Fri) { // Randomize configuration const int log_input_size = rand_uint_32b(3, 13); @@ -591,39 +591,34 @@ TYPED_TEST(FieldApiTest, Fri) const size_t pow_bits = rand_uint_32b(0, 3); const size_t nof_queries = rand_uint_32b(2, 4); - ICICLE_LOG_DEBUG << "log_input_size = " << log_input_size; - ICICLE_LOG_DEBUG << "input_size = " << input_size; - ICICLE_LOG_DEBUG << "folding_factor = " << folding_factor; - ICICLE_LOG_DEBUG << "stopping_degree = " << stopping_degree; - // Initialize ntt domain NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); // Generate input polynomial evaluations - auto scalars = std::make_unique(input_size); - scalar_t::rand_host_many(scalars.get(), input_size); + auto scalars = std::make_unique(input_size); + TypeParam::rand_host_many(scalars.get(), input_size); // ===== Prover side ====== - uint64_t merkle_tree_arity = 2; // TODO SHANIE - add support for other arities + uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities // Define hashers for merkle tree - Hash hash = Keccak256::create(sizeof(scalar_t)); // hash element -> 32B + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - FriTranscriptConfig prover_transcript_config; - FriTranscriptConfig verifier_transcript_config; //FIXME SHANIE - verfier and prover should have the same config + FriTranscriptConfig prover_transcript_config; + FriTranscriptConfig verifier_transcript_config; //FIXME SHANIE - verfier and prover should have the same config FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; - FriProof fri_proof; + FriProof fri_proof; ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(prover_transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== - Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool verification_pass = false; ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(verifier_transcript_config), fri_proof, verification_pass)); @@ -632,7 +627,7 @@ TYPED_TEST(FieldApiTest, Fri) // Release domain ICICLE_CHECK(ntt_release_domain()); } -endif // FRI +// #endif // FRI // TODO Hadar: this is a workaround for 'storage<18 - scalar_t::TLC>' failing due to 17 limbs not supported. diff --git a/icicle/tests/test_mod_arithmetic_api.h b/icicle/tests/test_mod_arithmetic_api.h index 57ccb49a6e..db26b002ce 100644 --- a/icicle/tests/test_mod_arithmetic_api.h +++ b/icicle/tests/test_mod_arithmetic_api.h @@ -17,6 +17,12 @@ #include "../backend/cpu/include/cpu_program_executor.h" #include "icicle/sumcheck/sumcheck.h" +#include "icicle/fri/fri.h" +#include "icicle/fri/fri_config.h" +#include "icicle/fri/fri_proof.h" +#include "icicle/fri/fri_transcript_config.h" + + #include "test_base.h" using namespace field_config; From 40392afa2212c0b10e50b341dc0455e9deac726b Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:01:54 +0200 Subject: [PATCH 080/127] format --- icicle/backend/cpu/include/cpu_fri_backend.h | 101 +++-- icicle/backend/cpu/include/cpu_fri_rounds.h | 104 +++-- icicle/backend/cpu/src/field/cpu_fri.cpp | 13 +- icicle/include/icicle/backend/fri_backend.h | 132 +++--- .../include/icicle/fields/complex_extension.h | 4 +- .../include/icicle/fields/quartic_extension.h | 10 +- icicle/include/icicle/fri/fri.h | 318 ++++++------- icicle/include/icicle/fri/fri_config.h | 15 +- icicle/include/icicle/fri/fri_proof.h | 59 +-- icicle/include/icicle/fri/fri_transcript.h | 422 +++++++++--------- .../icicle/fri/fri_transcript_config.h | 125 ++---- icicle/include/icicle/merkle/merkle_proof.h | 1 - icicle/include/icicle/utils/rand_gen.h | 1 - icicle/tests/test_field_api.cpp | 18 +- icicle/tests/test_mod_arithmetic_api.h | 1 - 15 files changed, 643 insertions(+), 681 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 89b92fc387..90d5680523 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,11 +24,9 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree, merkle_trees), - m_nof_fri_rounds(merkle_trees.size()), - m_log_input_size(merkle_trees.size() + std::log2(static_cast(stopping_degree+1))), - m_input_size(pow(2, m_log_input_size)), - m_fri_rounds(merkle_trees, m_log_input_size) + : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), + m_log_input_size(merkle_trees.size() + std::log2(static_cast(stopping_degree + 1))), + m_input_size(pow(2, m_log_input_size)), m_fri_rounds(merkle_trees, m_log_input_size) { } @@ -40,30 +38,29 @@ namespace icicle { { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; - FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), m_log_input_size); + FriTranscript transcript( + std::move(const_cast&>(fri_transcript_config)), m_log_input_size); // Initialize the proof - fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree+1); + fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); - //commit fold phase + // commit fold phase ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); - //proof of work - if (fri_config.pow_bits != 0){ - ICICLE_CHECK(proof_of_work(transcript, fri_config.pow_bits, fri_proof)); - } + // proof of work + if (fri_config.pow_bits != 0) { ICICLE_CHECK(proof_of_work(transcript, fri_config.pow_bits, fri_proof)); } - //query phase + // query phase ICICLE_CHECK(query_phase(transcript, fri_config, fri_proof)); return eIcicleError::SUCCESS; } private: - const size_t m_nof_fri_rounds; // Number of FRI rounds - const size_t m_log_input_size; // Log size of the input polynomial - const size_t m_input_size; // Size of the input polynomial - FriRounds m_fri_rounds; // Holds intermediate rounds + const size_t m_nof_fri_rounds; // Number of FRI rounds + const size_t m_log_input_size; // Log size of the input polynomial + const size_t m_input_size; // Size of the input polynomial + FriRounds m_fri_rounds; // Holds intermediate rounds /** * @brief Perform the commit-fold phase of the FRI protocol. @@ -73,20 +70,25 @@ namespace icicle { * @param transcript The transcript to generate challenges. * @return eIcicleError Error code indicating success or failure. */ - eIcicleError commit_fold_phase(const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ - ICICLE_ASSERT(this->m_folding_factor==2) << "Currently only folding factor of 2 is supported"; //TODO SHANIE - remove when supporting other folding factors + eIcicleError commit_fold_phase( + const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) + { + ICICLE_ASSERT(this->m_folding_factor == 2) + << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding + // factors const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); - // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with capacity 2^(m_log_input_size - round_idx). + // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with + // capacity 2^(m_log_input_size - round_idx). F* round_evals = m_fri_rounds.get_round_evals(0); std::copy(input_data, input_data + m_input_size, round_evals); size_t current_size = m_input_size; size_t current_log_size = m_log_input_size; - for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; ++round_idx){ + for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; ++round_idx) { // Merkle tree for the current round_idx MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); current_round_tree->build(round_evals, current_size, MerkleTreeConfig()); @@ -100,36 +102,36 @@ namespace icicle { F alpha = transcript.get_alpha(merkle_commit); // Fold the evaluations - size_t half = current_size>>1; + size_t half = current_size >> 1; std::vector peven(half); std::vector podd(half); - for (size_t i = 0; i < half; ++i){ + for (size_t i = 0; i < half; ++i) { peven[i] = (round_evals[i] + round_evals[i + half]) * S::inv_log_size(1); - uint64_t tw_idx = domain_max_size - ((domain_max_size>>current_log_size) * i); + uint64_t tw_idx = domain_max_size - ((domain_max_size >> current_log_size) * i); podd[i] = ((round_evals[i] - round_evals[i + half]) * S::inv_log_size(1)) * twiddles[tw_idx]; } - if (round_idx == m_nof_fri_rounds - 1){ + if (round_idx == m_nof_fri_rounds - 1) { round_evals = fri_proof.get_final_poly(); } else { round_evals = m_fri_rounds.get_round_evals(round_idx + 1); } - for (size_t i = 0; i < half; ++i){ + for (size_t i = 0; i < half; ++i) { round_evals[i] = peven[i] + (alpha * podd[i]); } - - current_size>>=1; + + current_size >>= 1; current_log_size--; } return eIcicleError::SUCCESS; } - eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ - for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) - { - if(transcript.hash_and_get_nof_leading_zero_bits(nonce) == pow_bits){ + eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof) + { + for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) { + if (transcript.hash_and_get_nof_leading_zero_bits(nonce) == pow_bits) { transcript.set_pow_nonce(nonce); fri_proof.set_pow_nonce(nonce); return eIcicleError::SUCCESS; @@ -139,34 +141,37 @@ namespace icicle { return eIcicleError::UNKNOWN_ERROR; } - /** - * @brief Perform the query phase of the FRI protocol. - * - * @param transcript The transcript object. - * @param fri_config The FRI configuration object. - * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. - * @return eIcicleError - */ - eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof){ + * @brief Perform the query phase of the FRI protocol. + * + * @param transcript The transcript object. + * @param fri_config The FRI configuration object. + * @param fri_proof (OUT) The proof object where we store the resulting Merkle proofs. + * @return eIcicleError + */ + eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) + { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); - std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); + std::vector query_indices = + rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); - for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ + for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { size_t query = query_indices[query_idx]; - for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++){ + for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++) { size_t round_size = (1ULL << (m_log_input_size - round_idx)); size_t leaf_idx = query % round_size; size_t leaf_idx_sym = (query + (round_size >> 1)) % round_size; F* round_evals = m_fri_rounds.get_round_evals(round_idx); - MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); - eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(round_evals, round_size, leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); + MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); + eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof( + round_evals, round_size, leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); if (err != eIcicleError::SUCCESS) return err; - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2*query_idx+1, round_idx); - eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof(round_evals, round_size, leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); + MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); + eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof( + round_evals, round_size, leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); if (err_sym != eIcicleError::SUCCESS) return err_sym; } } diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index 68ad0f3028..4c2106c3fc 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -10,69 +10,65 @@ #include "icicle/utils/log.h" #include "icicle/fri/fri.h" - namespace icicle { -template -class FriRounds -{ -public: - /** - * @brief Constructor that accepts an already-existing array of Merkle trees. - * Ownership is transferred from the caller. - * - * @param merkle_trees A vector of MerkleTrees. - * @param log_input_size The log of the input size. - */ - FriRounds(std::vector merkle_trees, const size_t log_input_size) - : m_merkle_trees(merkle_trees) + template + class FriRounds { - size_t fold_rounds = m_merkle_trees.size(); - m_rounds_evals.resize(fold_rounds); - for (size_t i = 0; i < fold_rounds; i++) { - m_rounds_evals[i] = std::make_unique(1ULL << (log_input_size - i)); + public: + /** + * @brief Constructor that accepts an already-existing array of Merkle trees. + * Ownership is transferred from the caller. + * + * @param merkle_trees A vector of MerkleTrees. + * @param log_input_size The log of the input size. + */ + FriRounds(std::vector merkle_trees, const size_t log_input_size) : m_merkle_trees(merkle_trees) + { + size_t fold_rounds = m_merkle_trees.size(); + m_rounds_evals.resize(fold_rounds); + for (size_t i = 0; i < fold_rounds; i++) { + m_rounds_evals[i] = std::make_unique(1ULL << (log_input_size - i)); + } } - } - /** - * @brief Get the Merkle tree for a specific fri round. - * - * @param round_idx The index of the fri round. - * @return A pointer to the Merkle tree backend for the specified fri round. - */ - MerkleTree* get_merkle_tree(size_t round_idx) - { - ICICLE_ASSERT(round_idx < m_merkle_trees.size()) << "round index out of bounds"; - return &m_merkle_trees[round_idx]; - } + /** + * @brief Get the Merkle tree for a specific fri round. + * + * @param round_idx The index of the fri round. + * @return A pointer to the Merkle tree backend for the specified fri round. + */ + MerkleTree* get_merkle_tree(size_t round_idx) + { + ICICLE_ASSERT(round_idx < m_merkle_trees.size()) << "round index out of bounds"; + return &m_merkle_trees[round_idx]; + } - F* get_round_evals(size_t round_idx) - { - ICICLE_ASSERT(round_idx < m_rounds_evals.size()) << "round index out of bounds"; - return m_rounds_evals[round_idx].get(); - } + F* get_round_evals(size_t round_idx) + { + ICICLE_ASSERT(round_idx < m_rounds_evals.size()) << "round index out of bounds"; + return m_rounds_evals[round_idx].get(); + } - /** - * @brief Retrieve the Merkle root for a specific fri round. - * - * @param round_idx The index of the round. - * @return A pair containing a pointer to the Merkle root bytes and its size. - */ - std::pair get_merkle_root_for_round(size_t round_idx) const - { - if (round_idx >= m_merkle_trees.size()) { - return {nullptr, 0}; + /** + * @brief Retrieve the Merkle root for a specific fri round. + * + * @param round_idx The index of the round. + * @return A pair containing a pointer to the Merkle root bytes and its size. + */ + std::pair get_merkle_root_for_round(size_t round_idx) const + { + if (round_idx >= m_merkle_trees.size()) { return {nullptr, 0}; } + return m_merkle_trees[round_idx].get_merkle_root(); } - return m_merkle_trees[round_idx].get_merkle_root(); - } -private: - // Persistent polynomial evaluations for each round (heap allocated). - // For round i, the expected length is 2^(m_initial_log_size - i). - std::vector> m_rounds_evals; + private: + // Persistent polynomial evaluations for each round (heap allocated). + // For round i, the expected length is 2^(m_initial_log_size - i). + std::vector> m_rounds_evals; - // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. - std::vector m_merkle_trees; -}; + // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. + std::vector m_merkle_trees; + }; } // namespace icicle diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index b80eead5a9..4217988097 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -4,20 +4,23 @@ using namespace field_config; using namespace icicle; - namespace icicle { template - eIcicleError cpu_create_fri_backend(const Device& device, const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) + eIcicleError cpu_create_fri_backend( + const Device& device, + const size_t folding_factor, + const size_t stopping_degree, + std::vector merkle_trees, + std::shared_ptr>& backend /*OUT*/) { backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); return eIcicleError::SUCCESS; } - -REGISTER_FRI_FACTORY_BACKEND("CPU", (cpu_create_fri_backend)); + + REGISTER_FRI_FACTORY_BACKEND("CPU", (cpu_create_fri_backend)); #ifdef EXT_FIELD REGISTER_FRI_EXT_FACTORY_BACKEND("CPU", (cpu_create_fri_backend)); #endif // EXT_FIELD - } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index 3b2ae168f6..6b4c30677e 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -16,33 +16,30 @@ using namespace field_config; namespace icicle { -/** - * @brief Abstract base class for FRI backend implementations. - * @tparam F Field type used in the FRI protocol. - */ -template -class FriBackend -{ -public: + /** + * @brief Abstract base class for FRI backend implementations. + * @tparam F Field type used in the FRI protocol. + */ + template + class FriBackend + { + public: /** * @brief Constructor that accepts an existing array of Merkle trees. * * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree Stopping degree threshold for the final polynomial. */ - FriBackend(const size_t folding_factor, - const size_t stopping_degree, - std::vector merkle_trees) - : m_folding_factor(folding_factor) - , m_stopping_degree(stopping_degree) - , m_merkle_trees(merkle_trees) - {} + FriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) + : m_folding_factor(folding_factor), m_stopping_degree(stopping_degree), m_merkle_trees(merkle_trees) + { + } virtual ~FriBackend() = default; /** * @brief Generate the FRI proof from given inputs. - * + * * @param fri_config Configuration for FRI operations (e.g., proof-of-work bits, queries). * @param fri_transcript_config Configuration for encoding/hashing FRI messages (Fiat-Shamir). * @param input_data Evaluations of the polynomial (or other relevant data). @@ -53,32 +50,35 @@ class FriBackend const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, - FriProof& fri_proof - ) = 0; + FriProof& fri_proof) = 0; std::vector m_merkle_trees; -protected: + protected: const size_t m_folding_factor; const size_t m_stopping_degree; -}; + }; -/*************************** Backend Factory Registration ***************************/ + /*************************** Backend Factory Registration ***************************/ -/** - * @brief A function signature for creating a FriBackend instance for a specific device. - */ -template -using FriFactoryImpl = - std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; + /** + * @brief A function signature for creating a FriBackend instance for a specific device. + */ + template + using FriFactoryImpl = std::function merkle_trees, + std::shared_ptr>& backend /*OUT*/)>; -/** - * @brief Register a FRI backend factory for a specific device type. - * - * @param deviceType String identifier for the device type. - * @param impl A factory function that creates a FriBackend. - */ -void register_fri_factory(const std::string& deviceType, FriFactoryImpl impl); + /** + * @brief Register a FRI backend factory for a specific device type. + * + * @param deviceType String identifier for the device type. + * @param impl A factory function that creates a FriBackend. + */ + void register_fri_factory(const std::string& deviceType, FriFactoryImpl impl); /** * @brief Macro to register a FRI backend factory. @@ -96,36 +96,40 @@ void register_fri_factory(const std::string& deviceType, FriFactoryImpl - using FriExtFactoryImpl = - std::function merkle_trees, std::shared_ptr>& backend /*OUT*/)>; - - /** - * @brief Register a FRI backend factory for a specific device type. - * - * @param deviceType String identifier for the device type. - * @param impl A factory function that creates a FriBackend. - */ - void register_extension_fri_factory(const std::string& deviceType, FriExtFactoryImpl impl); - - /** - * @brief Macro to register a FRI backend factory. - * - * This macro registers a factory function for a specific backend by calling - * `register_fri_factory` at runtime. - * - */ - #define REGISTER_FRI_EXT_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ - namespace { \ - static bool UNIQUE(_reg_fri_ext_field) = []() -> bool { \ - register_extension_fri_factory(DEVICE_TYPE, static_cast>(FUNC)); \ - return true; \ - }(); \ - } - + /** + * @brief A function signature for creating a FriBackend instance for a specific device, with extension field. + */ + template + using FriExtFactoryImpl = std::function merkle_trees, + std::shared_ptr>& backend /*OUT*/)>; + + /** + * @brief Register a FRI backend factory for a specific device type. + * + * @param deviceType String identifier for the device type. + * @param impl A factory function that creates a FriBackend. + */ + void register_extension_fri_factory(const std::string& deviceType, FriExtFactoryImpl impl); + + /** + * @brief Macro to register a FRI backend factory. + * + * This macro registers a factory function for a specific backend by calling + * `register_fri_factory` at runtime. + * + */ + #define REGISTER_FRI_EXT_FACTORY_BACKEND(DEVICE_TYPE, FUNC) \ + namespace { \ + static bool UNIQUE(_reg_fri_ext_field) = []() -> bool { \ + register_extension_fri_factory(DEVICE_TYPE, static_cast>(FUNC)); \ + return true; \ + }(); \ + } + #endif // EXT_FIELD } // namespace icicle diff --git a/icicle/include/icicle/fields/complex_extension.h b/icicle/include/icicle/fields/complex_extension.h index a50a997b9b..cfd69081f6 100644 --- a/icicle/include/icicle/fields/complex_extension.h +++ b/icicle/include/icicle/fields/complex_extension.h @@ -310,9 +310,7 @@ class ComplexExtensionField static constexpr HOST_DEVICE_INLINE ComplexExtensionField from(const std::byte* in, unsigned nof_bytes) { ICICLE_ASSERT(nof_bytes >= 2 * sizeof(FF)) << "Input size is too small"; - return ComplexExtensionField{FF::from(in, sizeof(FF)), - FF::from(in + sizeof(FF), sizeof(FF))}; - + return ComplexExtensionField{FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF))}; } }; diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index de4c168c42..8efcf54b35 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -242,7 +242,7 @@ class QuarticExtensionField FF::reduce( (CONFIG::nonresidue_is_negative ? (FF::mul_wide(xs.real, x0) + FF::template mul_unsigned(FF::mul_wide(xs.im2, x2))) - : (FF::mul_wide(xs.real, x0))-FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), + : (FF::mul_wide(xs.real, x0)) - FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), FF::reduce( (CONFIG::nonresidue_is_negative ? FWide::neg(FF::template mul_unsigned(FF::mul_wide(xs.im3, x2))) @@ -268,11 +268,9 @@ class QuarticExtensionField static constexpr HOST_DEVICE_INLINE QuarticExtensionField from(const std::byte* in, unsigned nof_bytes) { ICICLE_ASSERT(nof_bytes >= 4 * sizeof(FF)) << "Input size is too small"; - return QuarticExtensionField{FF::from(in, sizeof(FF)), - FF::from(in + sizeof(FF), sizeof(FF)), - FF::from(in + 2 * sizeof(FF), sizeof(FF)), - FF::from(in + 3 * sizeof(FF), sizeof(FF))}; - + return QuarticExtensionField{ + FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF)), FF::from(in + 2 * sizeof(FF), sizeof(FF)), + FF::from(in + 3 * sizeof(FF), sizeof(FF))}; } }; #if __CUDACC__ diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 46110e41f7..3809b6593c 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -16,26 +16,26 @@ namespace icicle { -/** - * @brief Forward declaration for the FRI class template. - */ -template -class Fri; - -/** - * @brief Constructor for the case where only binary Merkle trees are used - * with a constant hash function. - * - * @param input_size The size of the input polynomial - number of evaluations. - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param merkle_tree_leaves_hash The hash function used for leaves of the Merkle tree. - * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. - * @return A `Fri` object built around the chosen backend. - */ -template -Fri create_fri( + /** + * @brief Forward declaration for the FRI class template. + */ + template + class Fri; + + /** + * @brief Constructor for the case where only binary Merkle trees are used + * with a constant hash function. + * + * @param input_size The size of the input polynomial - number of evaluations. + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree The minimal polynomial degree at which to stop folding. + * @param merkle_tree_leaves_hash The hash function used for leaves of the Merkle tree. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. + * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. + * @return A `Fri` object built around the chosen backend. + */ + template + Fri create_fri( const size_t input_size, const size_t folding_factor, const size_t stopping_degree, @@ -43,38 +43,33 @@ Fri create_fri( const Hash& merkle_tree_compress_hash, const uint64_t output_store_min_layer = 0); -/** - * @brief Constructor for the case where Merkle trees are already given. - * - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param merkle_trees A reference vector of `MerkleTree` objects. - * @return A `Fri` object built around the chosen backend. - */ -template -Fri create_fri( - const size_t folding_factor, - const size_t stopping_degree, - std::vector merkle_trees); - -/** - * @brief Class for performing FRI operations. - * - * This class provides a high-level interface for constructing and managing a FRI proof. - * - * @tparam F The field type used in the FRI protocol. - */ -template -class Fri -{ -public: + /** + * @brief Constructor for the case where Merkle trees are already given. + * + * @param folding_factor The factor by which the codeword is folded each round. + * @param stopping_degree The minimal polynomial degree at which to stop folding. + * @param merkle_trees A reference vector of `MerkleTree` objects. + * @return A `Fri` object built around the chosen backend. + */ + template + Fri create_fri(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees); + + /** + * @brief Class for performing FRI operations. + * + * This class provides a high-level interface for constructing and managing a FRI proof. + * + * @tparam F The field type used in the FRI protocol. + */ + template + class Fri + { + public: /** * @brief Constructor for the Fri class. * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. */ - explicit Fri(std::shared_ptr> backend) - : m_backend(std::move(backend)) - {} + explicit Fri(std::shared_ptr> backend) : m_backend(std::move(backend)) {} /** * @brief Generate a FRI proof from the given polynomial evaluations (or input data). @@ -85,12 +80,12 @@ class Fri * @return An eIcicleError indicating success or failure. */ eIcicleError get_fri_proof( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - const F* input_data, - FriProof& fri_proof /* OUT */) const + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + FriProof& fri_proof /* OUT */) const { - return m_backend->get_fri_proof(fri_config, fri_transcript_config, input_data, fri_proof); + return m_backend->get_fri_proof(fri_config, fri_transcript_config, input_data, fri_proof); } /** @@ -102,116 +97,125 @@ class Fri * @return An eIcicleError indicating success or failure. */ eIcicleError verify( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, - bool& verification_pass /* OUT */) const + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + bool& verification_pass /* OUT */) const { - verification_pass = false; - ICICLE_ASSERT(fri_config.nof_queries > 0) << "No queries specified in FriConfig."; - const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); - const size_t final_poly_size = fri_proof.get_final_poly_size(); - const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - const size_t input_size = 1 << (log_input_size); - std::vector alpha_values(nof_fri_rounds); - - - // set up the transcript - FriTranscript transcript(std::move(const_cast&>(fri_transcript_config)), log_input_size); + verification_pass = false; + ICICLE_ASSERT(fri_config.nof_queries > 0) << "No queries specified in FriConfig."; + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + const size_t input_size = 1 << (log_input_size); + std::vector alpha_values(nof_fri_rounds); + + // set up the transcript + FriTranscript transcript( + std::move(const_cast&>(fri_transcript_config)), log_input_size); + for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { + auto [root_ptr, root_size] = fri_proof.get_root(round_idx); + ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; + std::vector merkle_commit(root_size); + std::memcpy(merkle_commit.data(), root_ptr, root_size); + alpha_values[round_idx] = transcript.get_alpha(merkle_commit); + } + + // proof-of-work + if (fri_config.pow_bits != 0) { + bool valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); + if (!valid) return eIcicleError::SUCCESS; // return with verification_pass = false + transcript.set_pow_nonce(fri_proof.get_pow_nonce()); + } + + // get query indices + size_t seed = transcript.get_seed_for_query_phase(); + seed_rand_generator(seed); + ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; + std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); + + uint64_t domain_max_size = 0; + uint64_t max_log_size = 0; + S primitive_root_inv = S::omega_inv(log_input_size); + + for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { + size_t query = query_indices[query_idx]; + size_t current_log_size = log_input_size; for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - auto [root_ptr, root_size] = fri_proof.get_root(round_idx); - ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; - std::vector merkle_commit(root_size); - std::memcpy(merkle_commit.data(), root_ptr, root_size); - alpha_values[round_idx] = transcript.get_alpha(merkle_commit); - } - - // proof-of-work - if (fri_config.pow_bits != 0) { - bool valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); - if (!valid) return eIcicleError::SUCCESS; // return with verification_pass = false - transcript.set_pow_nonce(fri_proof.get_pow_nonce()); - } - - // get query indices - size_t seed = transcript.get_seed_for_query_phase(); - seed_rand_generator(seed); - ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; - std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); - - uint64_t domain_max_size = 0; - uint64_t max_log_size = 0; - S primitive_root_inv = S::omega_inv(log_input_size); - - for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++){ - size_t query = query_indices[query_idx]; - size_t current_log_size = log_input_size; - for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - size_t round_size = (1ULL << (log_input_size - round_idx)); - size_t elem_idx = query % round_size; - size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; - - MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; - MerkleProof& proof_ref = fri_proof.get_query_proof(2*query_idx, round_idx); - bool valid = false; - eIcicleError err = current_round_tree.verify(proof_ref, valid); - if (err != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return err; - } - if (!valid){ - ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false - } - - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2*query_idx+1, round_idx); - valid = false; - eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, valid); - if (err_sym != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return err_sym; - } - if (!valid){ - ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false - } - - // collinearity check - const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); - const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); - ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; - ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) << "Leaf index symmetry from proof doesn't match query expected index"; - const F& leaf_data_f = *reinterpret_cast(leaf_data); - const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); - F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); - F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * S::pow(primitive_root_inv, leaf_index*(input_size>>current_log_size)); - F alpha = alpha_values[round_idx]; - F folded = l_even + (alpha * l_odd); - - if (round_idx == nof_fri_rounds - 1) { - const F* final_poly = fri_proof.get_final_poly(); - if (final_poly[query%final_poly_size] != folded) { - ICICLE_LOG_ERROR << "[VERIFIER] (last round) Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false; - } - } else { - MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2*query_idx, round_idx+1); - const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); - const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); - if (leaf_data_folded_f != folded) { - ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" << leaf_data_folded_f; - return eIcicleError::SUCCESS; // return with verification_pass = false - } - } - current_log_size--; + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; + + MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; + MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); + bool valid = false; + eIcicleError err = current_round_tree.verify(proof_ref, valid); + if (err != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification returned err for query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return err; + } + if (!valid) { + ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification failed for leaf query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false + } + + MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); + valid = false; + eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, valid); + if (err_sym != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return err_sym; + } + if (!valid) { + ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false + } + + // collinearity check + const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); + const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); + ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; + ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) + << "Leaf index symmetry from proof doesn't match query expected index"; + const F& leaf_data_f = *reinterpret_cast(leaf_data); + const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); + F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); + F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * + S::pow(primitive_root_inv, leaf_index * (input_size >> current_log_size)); + F alpha = alpha_values[round_idx]; + F folded = l_even + (alpha * l_odd); + + if (round_idx == nof_fri_rounds - 1) { + const F* final_poly = fri_proof.get_final_poly(); + if (final_poly[query % final_poly_size] != folded) { + ICICLE_LOG_ERROR << "[VERIFIER] (last round) Collinearity check failed for query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return eIcicleError::SUCCESS; // return with verification_pass = false; + } + } else { + MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2 * query_idx, round_idx + 1); + const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); + const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); + if (leaf_data_folded_f != folded) { + ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx + << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded + << "\nfolded_from_proof = \t" << leaf_data_folded_f; + return eIcicleError::SUCCESS; // return with verification_pass = false } + } + current_log_size--; } - verification_pass = true; - return eIcicleError::SUCCESS; + } + verification_pass = true; + return eIcicleError::SUCCESS; } -private: + private: std::shared_ptr> m_backend; // Shared pointer to the backend for FRI operations. -}; + }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index c7dba1a03a..acc99483fe 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -14,13 +14,14 @@ namespace icicle { * It also supports backend-specific extensions for customization. */ struct FriConfig { - icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. - size_t pow_bits = 0; // Number of leading zeros required for proof-of-work. Default is 0. - size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. - bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. - bool are_outputs_on_device = false; // True if outputs reside on the device, false if on the host. Default is false. - bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. - ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. Default is nullptr. + icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. + size_t pow_bits = 0; // Number of leading zeros required for proof-of-work. Default is 0. + size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. + bool are_inputs_on_device = + false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. + bool are_outputs_on_device = false; // True if outputs reside on the device, false if on the host. Default is false. + bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. + ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. Default is nullptr. }; /** diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index abcba65208..62a098f286 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -8,7 +8,6 @@ #include "icicle/merkle/merkle_tree.h" #include "icicle/utils/log.h" - namespace icicle { /** @@ -22,9 +21,9 @@ namespace icicle { { public: // Constructor - FriProof() : m_pow_nonce(0){} + FriProof() : m_pow_nonce(0) {} - /** + /** * @brief Initialize the Merkle proofs and final polynomial storage. * * @param nof_queries Number of queries in the proof. @@ -33,10 +32,13 @@ namespace icicle { void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) - << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; - + << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries + << ", nof_fri_rounds = " << nof_fri_rounds; + // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns - m_query_proofs.resize(2*nof_queries, std::vector(nof_fri_rounds)); //for each query, we have 2 proofs (for the leaf and its symmetric) + m_query_proofs.resize( + 2 * nof_queries, + std::vector(nof_fri_rounds)); // for each query, we have 2 proofs (for the leaf and its symmetric) m_final_poly_size = final_poly_size; m_final_poly = std::make_unique(final_poly_size); } @@ -50,9 +52,7 @@ namespace icicle { */ MerkleProof& get_query_proof(const size_t query_idx, const size_t round_idx) { - if (query_idx < 0 || query_idx >= m_query_proofs.size()) { - throw std::out_of_range("Invalid query index"); - } + if (query_idx < 0 || query_idx >= m_query_proofs.size()) { throw std::out_of_range("Invalid query index"); } if (round_idx < 0 || round_idx >= m_query_proofs[query_idx].size()) { throw std::out_of_range("Invalid round index"); } @@ -72,53 +72,40 @@ namespace icicle { // * @brief Returns a tuple containing the pointer to the leaf data, its size and index. // * @return A tuple of (leaf data pointer, leaf size, leaf_index). // */ - // std::tuple get_leaf(const size_t query_idx, const size_t round_idx) const + // std::tuple get_leaf(const size_t query_idx, const size_t round_idx) + // const // { // return m_query_proofs[query_idx][round_idx].get_leaf(); // } - /** * @brief Get the number of FRI rounds in the proof. * * @return Number of FRI rounds. */ - size_t get_nof_fri_rounds() const - { - return m_query_proofs[0].size(); - } + size_t get_nof_fri_rounds() const { return m_query_proofs[0].size(); } /** * @brief Get the final poly size. * * @return final_poly_size. */ - size_t get_final_poly_size() const - { - return m_final_poly_size; - } + size_t get_final_poly_size() const { return m_final_poly_size; } - void set_pow_nonce(uint64_t pow_nonce) - { - m_pow_nonce = pow_nonce; - } + void set_pow_nonce(uint64_t pow_nonce) { m_pow_nonce = pow_nonce; } - uint64_t get_pow_nonce() const - { - return m_pow_nonce; - } + uint64_t get_pow_nonce() const { return m_pow_nonce; } - //get pointer to the final polynomial - F* get_final_poly() const - { - return m_final_poly.get(); - } + // get pointer to the final polynomial + F* get_final_poly() const { return m_final_poly.get(); } private: - std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf. for each query, we have 2 proofs (for the leaf in [2*query] and its symmetric in [2*query+1]) - std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) - size_t m_final_poly_size; // Size of the final polynomial - uint64_t m_pow_nonce; // Proof-of-work nonce + std::vector> + m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf. for each query, we have 2 + // proofs (for the leaf in [2*query] and its symmetric in [2*query+1]) + std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) + size_t m_final_poly_size; // Size of the final polynomial + uint64_t m_pow_nonce; // Proof-of-work nonce public: // for debug diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 5ea39c7ae7..89369c093a 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -11,247 +11,241 @@ namespace icicle { -template -class FriTranscript -{ -public: - FriTranscript(FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) - : m_transcript_config(std::move(transcript_config)) - , m_prev_alpha(F::zero()) - , m_first_round(true) - , m_pow_nonce(0) - { - m_entry_0.clear(); - m_entry_0.reserve(1024); // pre-allocate some space - build_entry_0(log_input_size); - m_first_round = true; - } - - /** - * @brief Add a Merkle commit to the transcript and generate a new alpha challenge. - * - * @param merkle_commit The raw bytes of the Merkle commit. - * @return A field element alpha derived via Fiat-Shamir. - */ - F get_alpha(const std::vector& merkle_commit) - { - ICICLE_ASSERT(m_transcript_config.get_domain_separator_label().size() > 0) << "Domain separator label must be set"; - std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space - - // Build the round's hash input - if (m_first_round) { - build_hash_input_round_0(hash_input); - m_first_round = false; - } else { - build_hash_input_round_i(hash_input); + template + class FriTranscript + { + public: + FriTranscript(FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) + : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), m_first_round(true), + m_pow_nonce(0) + { + m_entry_0.clear(); + m_entry_0.reserve(1024); // pre-allocate some space + build_entry_0(log_input_size); + m_first_round = true; } - append_data(hash_input, merkle_commit); - - // Hash the input and return alpha - const Hash& hasher = m_transcript_config.get_hasher(); - std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); - m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); - return m_prev_alpha; - } - - size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce) - { - // Prepare a buffer for hashing - std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space - // Build the hash input - build_hash_input_pow(hash_input, nonce); + /** + * @brief Add a Merkle commit to the transcript and generate a new alpha challenge. + * + * @param merkle_commit The raw bytes of the Merkle commit. + * @return A field element alpha derived via Fiat-Shamir. + */ + F get_alpha(const std::vector& merkle_commit) + { + ICICLE_ASSERT(m_transcript_config.get_domain_separator_label().size() > 0) + << "Domain separator label must be set"; + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + + // Build the round's hash input + if (m_first_round) { + build_hash_input_round_0(hash_input); + m_first_round = false; + } else { + build_hash_input_round_i(hash_input); + } + append_data(hash_input, merkle_commit); + + // Hash the input and return alpha + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); + return m_prev_alpha; + } - const Hash& hasher = m_transcript_config.get_hasher(); - std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce) + { + // Prepare a buffer for hashing + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space - return count_leading_zero_bits(hash_result); - } + // Build the hash input + build_hash_input_pow(hash_input, nonce); - /** - * @brief Add a proof-of-work nonce to the transcript, to be included in subsequent rounds. - * @param pow_nonce The proof-of-work nonce. - */ - void set_pow_nonce(uint32_t pow_nonce) - { - m_pow_nonce = pow_nonce; - } + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); - size_t get_seed_for_query_phase() - { - // Prepare a buffer for hashing - std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space - - // Build the hash input - build_hash_input_query_phase(hash_input); - - const Hash& hasher = m_transcript_config.get_hasher(); - std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); - uint64_t seed = bytes_to_uint_64(hash_result); - return seed; - } + return count_leading_zero_bits(hash_result); + } -private: - const FriTranscriptConfig m_transcript_config; // Transcript configuration (labels, seeds, etc.) - const HashConfig m_hash_config; // hash config - default - bool m_first_round; // Indicates if this is the first round - std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds - F m_prev_alpha; // The previous alpha generated - uint64_t m_pow_nonce; // Proof-of-work nonce - optional + /** + * @brief Add a proof-of-work nonce to the transcript, to be included in subsequent rounds. + * @param pow_nonce The proof-of-work nonce. + */ + void set_pow_nonce(uint32_t pow_nonce) { m_pow_nonce = pow_nonce; } + + size_t get_seed_for_query_phase() + { + // Prepare a buffer for hashing + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + + // Build the hash input + build_hash_input_query_phase(hash_input); + + const Hash& hasher = m_transcript_config.get_hasher(); + std::vector hash_result(hasher.output_size()); + hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + uint64_t seed = bytes_to_uint_64(hash_result); + return seed; + } - /** - * @brief Append a vector of bytes to another vector of bytes. - * @param dest (OUT) Destination byte vector. - * @param src Source byte vector. - */ - void append_data(std::vector& dest, const std::vector& src) - { - dest.insert(dest.end(), src.begin(), src.end()); - } + private: + const FriTranscriptConfig m_transcript_config; // Transcript configuration (labels, seeds, etc.) + const HashConfig m_hash_config; // hash config - default + bool m_first_round; // Indicates if this is the first round + std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds + F m_prev_alpha; // The previous alpha generated + uint64_t m_pow_nonce; // Proof-of-work nonce - optional + + /** + * @brief Append a vector of bytes to another vector of bytes. + * @param dest (OUT) Destination byte vector. + * @param src Source byte vector. + */ + void append_data(std::vector& dest, const std::vector& src) + { + dest.insert(dest.end(), src.begin(), src.end()); + } - /** - * @brief Append an unsigned 64-bit integer to the byte vector (little-endian). - * @param dest (OUT) Destination byte vector. - * @param value The 64-bit value to append. - */ - void append_u32(std::vector& dest, uint32_t value) - { - const std::byte* data_bytes = reinterpret_cast(&value); - dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint32_t)); - } + /** + * @brief Append an unsigned 64-bit integer to the byte vector (little-endian). + * @param dest (OUT) Destination byte vector. + * @param value The 64-bit value to append. + */ + void append_u32(std::vector& dest, uint32_t value) + { + const std::byte* data_bytes = reinterpret_cast(&value); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint32_t)); + } - void append_u64(std::vector& dest, uint64_t value) - { - const std::byte* data_bytes = reinterpret_cast(&value); - dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint64_t)); - } + void append_u64(std::vector& dest, uint64_t value) + { + const std::byte* data_bytes = reinterpret_cast(&value); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint64_t)); + } - /** - * @brief Append a field element to the byte vector. - * @param dest (OUT) Destination byte vector. - * @param field The field element to append. - */ - void append_field(std::vector& dest, const F& field) - { - const std::byte* data_bytes = reinterpret_cast(&field); - dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); - } + /** + * @brief Append a field element to the byte vector. + * @param dest (OUT) Destination byte vector. + * @param field The field element to append. + */ + void append_field(std::vector& dest, const F& field) + { + const std::byte* data_bytes = reinterpret_cast(&field); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(F)); + } - /** - * @brief Build the hash input for round 0 (commit phase 0). - * - * DS =[domain_seperator||log_2(initial_domain_size).LE32()] - * entry_0 =[DS||public.LE32()] - * - */ - void build_entry_0(uint32_t log_input_size) - { - append_data(m_entry_0, m_transcript_config.get_domain_separator_label()); - append_u32(m_entry_0, log_input_size); - append_data(m_entry_0, m_transcript_config.get_public_state()); - } + /** + * @brief Build the hash input for round 0 (commit phase 0). + * + * DS =[domain_seperator||log_2(initial_domain_size).LE32()] + * entry_0 =[DS||public.LE32()] + * + */ + void build_entry_0(uint32_t log_input_size) + { + append_data(m_entry_0, m_transcript_config.get_domain_separator_label()); + append_u32(m_entry_0, log_input_size); + append_data(m_entry_0, m_transcript_config.get_public_state()); + } - /** - * @brief Build the hash input for round 0 (commit phase 0). - * - * alpha_0 = hash(entry_0||rng||round_challenge_label[u8]||commit_label[u8]|| root_0.LE32()).to_ext_field() - * root is added outside this function - * - * @param hash_input (OUT) The byte vector that accumulates data to be hashed. - */ - void build_hash_input_round_0(std::vector& hash_input) - { - append_data(hash_input, m_entry_0); - append_field(hash_input, m_transcript_config.get_seed_rng()); - append_data(hash_input, m_transcript_config.get_round_challenge_label()); - append_data(hash_input, m_transcript_config.get_commit_phase_label()); - } + /** + * @brief Build the hash input for round 0 (commit phase 0). + * + * alpha_0 = hash(entry_0||rng||round_challenge_label[u8]||commit_label[u8]|| root_0.LE32()).to_ext_field() + * root is added outside this function + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_round_0(std::vector& hash_input) + { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_transcript_config.get_seed_rng()); + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + append_data(hash_input, m_transcript_config.get_commit_phase_label()); + } - /** - * @brief Build the hash input for the subsequent rounds (commit phase i). - * - * alpha_n = hash(entry0||alpha_n-1||round_challenge_label[u8]||commit_label[u8]|| root_n.LE32()).to_ext_field() - * root is added outside this function - * - * @param hash_input (OUT) The byte vector that accumulates data to be hashed. - */ - void build_hash_input_round_i(std::vector& hash_input) - { - append_data(hash_input, m_entry_0); - append_field(hash_input, m_prev_alpha); - append_data(hash_input, m_transcript_config.get_round_challenge_label()); - append_data(hash_input, m_transcript_config.get_commit_phase_label()); - } + /** + * @brief Build the hash input for the subsequent rounds (commit phase i). + * + * alpha_n = hash(entry0||alpha_n-1||round_challenge_label[u8]||commit_label[u8]|| root_n.LE32()).to_ext_field() + * root is added outside this function + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_round_i(std::vector& hash_input) + { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); + append_data(hash_input, m_transcript_config.get_round_challenge_label()); + append_data(hash_input, m_transcript_config.get_commit_phase_label()); + } - /** - * @brief Build the hash input for the proof-of-work nonce. - * hash_input = entry_0||alpha_{n-1}||"nonce"||nonce - * - * @param hash_input (OUT) The byte vector that accumulates data to be hashed. - */ + /** + * @brief Build the hash input for the proof-of-work nonce. + * hash_input = entry_0||alpha_{n-1}||"nonce"||nonce + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ void build_hash_input_pow(std::vector& hash_input, uint64_t temp_pow_nonce) - { - append_data(hash_input, m_entry_0); - append_field(hash_input, m_prev_alpha); - append_data(hash_input, m_transcript_config.get_nonce_label()); - append_u64(hash_input, temp_pow_nonce); - } - - /** - * @brief Build the hash input for the query phase. - * hash_input = entry_0||alpha_{n-1}||"query"||seed - * - * @param hash_input (OUT) The byte vector that accumulates data to be hashed. - */ - void build_hash_input_query_phase(std::vector& hash_input) - { - if (m_pow_nonce ==0){ + { append_data(hash_input, m_entry_0); append_field(hash_input, m_prev_alpha); - } else { - append_data(hash_input, m_entry_0); append_data(hash_input, m_transcript_config.get_nonce_label()); - append_u32(hash_input, m_pow_nonce); + append_u64(hash_input, temp_pow_nonce); } - } - static size_t count_leading_zero_bits(const std::vector& data) - { - size_t zero_bits = 0; - for (size_t i = 0; i < data.size(); i++) { - uint8_t byte_val = static_cast(data[i]); - if (byte_val == 0) { - zero_bits += 8; + /** + * @brief Build the hash input for the query phase. + * hash_input = entry_0||alpha_{n-1}||"query"||seed + * + * @param hash_input (OUT) The byte vector that accumulates data to be hashed. + */ + void build_hash_input_query_phase(std::vector& hash_input) + { + if (m_pow_nonce == 0) { + append_data(hash_input, m_entry_0); + append_field(hash_input, m_prev_alpha); } else { - for (int bit = 7; bit >= 0; bit--) { - if ((byte_val & (1 << bit)) == 0) { - zero_bits++; - } else { - return zero_bits; + append_data(hash_input, m_entry_0); + append_data(hash_input, m_transcript_config.get_nonce_label()); + append_u32(hash_input, m_pow_nonce); + } + } + + static size_t count_leading_zero_bits(const std::vector& data) + { + size_t zero_bits = 0; + for (size_t i = 0; i < data.size(); i++) { + uint8_t byte_val = static_cast(data[i]); + if (byte_val == 0) { + zero_bits += 8; + } else { + for (int bit = 7; bit >= 0; bit--) { + if ((byte_val & (1 << bit)) == 0) { + zero_bits++; + } else { + return zero_bits; + } } + break; } - break; } + return zero_bits; } - return zero_bits; - } - - uint64_t bytes_to_uint_64(const std::vector& data) - { - uint64_t result = 0; - for (size_t i = 0; i < sizeof(uint64_t); i++) { - result |= static_cast(data[i]) << (i * 8); + uint64_t bytes_to_uint_64(const std::vector& data) + { + uint64_t result = 0; + for (size_t i = 0; i < sizeof(uint64_t); i++) { + result |= static_cast(data[i]) << (i * 8); + } + return result; } - return result; - } - -}; + }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_transcript_config.h b/icicle/include/icicle/fri/fri_transcript_config.h index ea4a86b62a..d79582e621 100644 --- a/icicle/include/icicle/fri/fri_transcript_config.h +++ b/icicle/include/icicle/fri/fri_transcript_config.h @@ -2,108 +2,85 @@ #include "icicle/hash/hash.h" #include "icicle/hash/keccak.h" -#include // for std::strlen +#include // for std::strlen #include #include namespace icicle { -/** - * @brief Configuration for encoding and hashing messages in the FRI protocol. - * - * @tparam F Type of the field element (e.g., prime field or extension field elements). - */ -template -class FriTranscriptConfig -{ -public: + /** + * @brief Configuration for encoding and hashing messages in the FRI protocol. + * + * @tparam F Type of the field element (e.g., prime field or extension field elements). + */ + template + class FriTranscriptConfig + { + public: // Default Constructor FriTranscriptConfig() - : m_hasher(create_keccak_256_hash()), - m_domain_separator_label(cstr_to_bytes("ds")), - m_commit_phase_label(cstr_to_bytes("commit")), - m_nonce_label(cstr_to_bytes("nonce")), - m_public(cstr_to_bytes("public")), - m_seed_rng(F::zero()) + : m_hasher(create_keccak_256_hash()), m_domain_separator_label(cstr_to_bytes("ds")), + m_commit_phase_label(cstr_to_bytes("commit")), m_nonce_label(cstr_to_bytes("nonce")), + m_public(cstr_to_bytes("public")), m_seed_rng(F::zero()) { } // Constructor with std::byte vectors for labels FriTranscriptConfig( - Hash hasher, - std::vector&& domain_separator_label, - std::vector&& round_challenge_label, - std::vector&& commit_phase_label, - std::vector&& nonce_label, - std::vector&& public_state, - F seed_rng) - : m_hasher(std::move(hasher)), - m_domain_separator_label(std::move(domain_separator_label)), - m_round_challenge_label(std::move(round_challenge_label)), - m_commit_phase_label(std::move(commit_phase_label)), - m_nonce_label(std::move(nonce_label)), - m_public(std::move(public_state)), - m_seed_rng(seed_rng) + Hash hasher, + std::vector&& domain_separator_label, + std::vector&& round_challenge_label, + std::vector&& commit_phase_label, + std::vector&& nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), m_domain_separator_label(std::move(domain_separator_label)), + m_round_challenge_label(std::move(round_challenge_label)), + m_commit_phase_label(std::move(commit_phase_label)), m_nonce_label(std::move(nonce_label)), + m_public(std::move(public_state)), m_seed_rng(seed_rng) { } // Constructor with const char* arguments FriTranscriptConfig( - Hash hasher, - const char* domain_separator_label, - const char* round_challenge_label, - const char* commit_phase_label, - const char* nonce_label, - std::vector&& public_state, - F seed_rng) - : m_hasher(std::move(hasher)), - m_domain_separator_label(cstr_to_bytes(domain_separator_label)), - m_round_challenge_label(cstr_to_bytes(round_challenge_label)), - m_commit_phase_label(cstr_to_bytes(commit_phase_label)), - m_nonce_label(cstr_to_bytes(nonce_label)), - m_public(std::move(public_state)), - m_seed_rng(seed_rng) + Hash hasher, + const char* domain_separator_label, + const char* round_challenge_label, + const char* commit_phase_label, + const char* nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), m_domain_separator_label(cstr_to_bytes(domain_separator_label)), + m_round_challenge_label(cstr_to_bytes(round_challenge_label)), + m_commit_phase_label(cstr_to_bytes(commit_phase_label)), m_nonce_label(cstr_to_bytes(nonce_label)), + m_public(std::move(public_state)), m_seed_rng(seed_rng) { } // Move Constructor FriTranscriptConfig(FriTranscriptConfig&& other) noexcept - : m_hasher(std::move(other.m_hasher)), - m_domain_separator_label(std::move(other.m_domain_separator_label)), - m_round_challenge_label(std::move(other.m_round_challenge_label)), - m_commit_phase_label(std::move(other.m_commit_phase_label)), - m_nonce_label(std::move(other.m_nonce_label)), - m_public(std::move(other.m_public)), - m_seed_rng(other.m_seed_rng) + : m_hasher(std::move(other.m_hasher)), m_domain_separator_label(std::move(other.m_domain_separator_label)), + m_round_challenge_label(std::move(other.m_round_challenge_label)), + m_commit_phase_label(std::move(other.m_commit_phase_label)), m_nonce_label(std::move(other.m_nonce_label)), + m_public(std::move(other.m_public)), m_seed_rng(other.m_seed_rng) { } const Hash& get_hasher() const { return m_hasher; } - const std::vector& get_domain_separator_label() const { - return m_domain_separator_label; - } + const std::vector& get_domain_separator_label() const { return m_domain_separator_label; } - const std::vector& get_round_challenge_label() const { - return m_round_challenge_label; - } + const std::vector& get_round_challenge_label() const { return m_round_challenge_label; } - const std::vector& get_commit_phase_label() const { - return m_commit_phase_label; - } + const std::vector& get_commit_phase_label() const { return m_commit_phase_label; } - const std::vector& get_nonce_label() const { - return m_nonce_label; - } + const std::vector& get_nonce_label() const { return m_nonce_label; } - const std::vector& get_public_state() const { - return m_public; - } + const std::vector& get_public_state() const { return m_public; } const F& get_seed_rng() const { return m_seed_rng; } -private: - + private: // Hash function used for randomness generation. Hash m_hasher; @@ -119,16 +96,12 @@ class FriTranscriptConfig // Seed for initializing the RNG. F m_seed_rng; - static inline std::vector cstr_to_bytes(const char* str) { - if (str == nullptr) return {}; - const size_t length = std::strlen(str); - return { - reinterpret_cast(str), - reinterpret_cast(str) + length - }; + if (str == nullptr) return {}; + const size_t length = std::strlen(str); + return {reinterpret_cast(str), reinterpret_cast(str) + length}; } -}; + }; } // namespace icicle diff --git a/icicle/include/icicle/merkle/merkle_proof.h b/icicle/include/icicle/merkle/merkle_proof.h index dc4291d0d6..67d8811713 100644 --- a/icicle/include/icicle/merkle/merkle_proof.h +++ b/icicle/include/icicle/merkle/merkle_proof.h @@ -163,7 +163,6 @@ namespace icicle { std::vector m_leaf; std::vector m_root; std::vector m_path; - }; } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/utils/rand_gen.h b/icicle/include/icicle/utils/rand_gen.h index 60c53c61bb..b7122f8fa8 100644 --- a/icicle/include/icicle/utils/rand_gen.h +++ b/icicle/include/icicle/utils/rand_gen.h @@ -29,7 +29,6 @@ static size_t rand_size_t(size_t min = 0, size_t max = SIZE_MAX) return dist(rand_generator); } - /** * @brief Generate random unsigned integer in range (inclusive) * @param min Lower limit. diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 0c964b9b12..2453c20e95 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -584,7 +584,7 @@ TYPED_TEST(FieldTest, Fri) const int log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; const int folding_factor = 2; // TODO SHANIE - add support for other folding factors - const int log_stopping_size = rand_uint_32b(0, log_input_size-2); + const int log_stopping_size = rand_uint_32b(0, log_input_size - 2); const size_t stopping_size = 1 << log_stopping_size; const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; @@ -601,15 +601,17 @@ TYPED_TEST(FieldTest, Fri) // ===== Prover side ====== uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - - // Define hashers for merkle tree - Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B + + // Define hashers for merkle tree + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + Fri prover_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); FriTranscriptConfig prover_transcript_config; - FriTranscriptConfig verifier_transcript_config; //FIXME SHANIE - verfier and prover should have the same config + FriTranscriptConfig + verifier_transcript_config; // FIXME SHANIE - verfier and prover should have the same config FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; @@ -618,7 +620,8 @@ TYPED_TEST(FieldTest, Fri) ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(prover_transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== - Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + Fri verifier_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool verification_pass = false; ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(verifier_transcript_config), fri_proof, verification_pass)); @@ -629,7 +632,6 @@ TYPED_TEST(FieldTest, Fri) } // #endif // FRI - // TODO Hadar: this is a workaround for 'storage<18 - scalar_t::TLC>' failing due to 17 limbs not supported. // It means we skip fields such as babybear! // TODO: this test make problem for curves too as they have extension fields too. Need to clean it up TODO Hadar diff --git a/icicle/tests/test_mod_arithmetic_api.h b/icicle/tests/test_mod_arithmetic_api.h index db26b002ce..5a9f98419f 100644 --- a/icicle/tests/test_mod_arithmetic_api.h +++ b/icicle/tests/test_mod_arithmetic_api.h @@ -22,7 +22,6 @@ #include "icicle/fri/fri_proof.h" #include "icicle/fri/fri_transcript_config.h" - #include "test_base.h" using namespace field_config; From d11f03a46c3e8dfacfd05a1dfe4136c21ed9725a Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:27:01 +0200 Subject: [PATCH 081/127] Fixed fri_transcript_config: using the same instance for both prover and verifier --- icicle/backend/cpu/include/cpu_fri_backend.h | 4 ++-- icicle/include/icicle/backend/fri_backend.h | 2 +- icicle/include/icicle/fri/fri.h | 6 +++--- icicle/include/icicle/fri/fri_transcript.h | 4 ++-- icicle/tests/test_field_api.cpp | 8 +++----- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 90d5680523..22c381c59c 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -32,14 +32,14 @@ namespace icicle { eIcicleError get_fri_proof( const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, + const FriTranscriptConfig&& fri_transcript_config, const F* input_data, FriProof& fri_proof /*out*/) override { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; FriTranscript transcript( - std::move(const_cast&>(fri_transcript_config)), m_log_input_size); + std::move(fri_transcript_config), m_log_input_size); // Initialize the proof fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index 6b4c30677e..fbe181b2fa 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -48,7 +48,7 @@ namespace icicle { */ virtual eIcicleError get_fri_proof( const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, + const FriTranscriptConfig&& fri_transcript_config, const F* input_data, FriProof& fri_proof) = 0; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 3809b6593c..88c5f60c3f 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -81,11 +81,11 @@ namespace icicle { */ eIcicleError get_fri_proof( const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, + const FriTranscriptConfig&& fri_transcript_config, const F* input_data, FriProof& fri_proof /* OUT */) const { - return m_backend->get_fri_proof(fri_config, fri_transcript_config, input_data, fri_proof); + return m_backend->get_fri_proof(fri_config, std::move(fri_transcript_config), input_data, fri_proof); } /** @@ -98,7 +98,7 @@ namespace icicle { */ eIcicleError verify( const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, + const FriTranscriptConfig&& fri_transcript_config, FriProof& fri_proof, bool& verification_pass /* OUT */) const { diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 89369c093a..c020c90eeb 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -15,7 +15,7 @@ namespace icicle { class FriTranscript { public: - FriTranscript(FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) + FriTranscript(const FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), m_first_round(true), m_pow_nonce(0) { @@ -94,7 +94,7 @@ namespace icicle { } private: - const FriTranscriptConfig m_transcript_config; // Transcript configuration (labels, seeds, etc.) + const FriTranscriptConfig&& m_transcript_config; // Transcript configuration (labels, seeds, etc.) const HashConfig m_hash_config; // hash config - default bool m_first_round; // Indicates if this is the first round std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 2453c20e95..5a2ac61ca0 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -609,21 +609,19 @@ TYPED_TEST(FieldTest, Fri) Fri prover_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - FriTranscriptConfig prover_transcript_config; - FriTranscriptConfig - verifier_transcript_config; // FIXME SHANIE - verfier and prover should have the same config + FriTranscriptConfig transcript_config; FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; FriProof fri_proof; - ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(prover_transcript_config), scalars.get(), fri_proof)); + ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== Fri verifier_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool verification_pass = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(verifier_transcript_config), fri_proof, verification_pass)); + ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, verification_pass)); ASSERT_EQ(true, verification_pass); From e503dd8ebf7c1d251d0318722ceb079d386ef34f Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:30:13 +0200 Subject: [PATCH 082/127] format --- icicle/backend/cpu/include/cpu_fri_backend.h | 3 +-- icicle/include/icicle/fri/fri_transcript.h | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 22c381c59c..6af148a640 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -38,8 +38,7 @@ namespace icicle { { ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; - FriTranscript transcript( - std::move(fri_transcript_config), m_log_input_size); + FriTranscript transcript(std::move(fri_transcript_config), m_log_input_size); // Initialize the proof fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index c020c90eeb..3124c4fbdd 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -15,7 +15,7 @@ namespace icicle { class FriTranscript { public: - FriTranscript(const FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) + FriTranscript(const FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), m_first_round(true), m_pow_nonce(0) { @@ -95,8 +95,8 @@ namespace icicle { private: const FriTranscriptConfig&& m_transcript_config; // Transcript configuration (labels, seeds, etc.) - const HashConfig m_hash_config; // hash config - default - bool m_first_round; // Indicates if this is the first round + const HashConfig m_hash_config; // hash config - default + bool m_first_round; // Indicates if this is the first round std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds F m_prev_alpha; // The previous alpha generated uint64_t m_pow_nonce; // Proof-of-work nonce - optional From b4b2d1dfce86a779af56b0b5e872f952f23abdc1 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:34:10 +0200 Subject: [PATCH 083/127] format --- icicle/include/icicle/math/storage.h | 12 +- icicle/src/fri/fri.cpp | 245 +++++++++++---------------- icicle/src/fri/fri_c_api.cpp | 157 ++++++----------- 3 files changed, 162 insertions(+), 252 deletions(-) diff --git a/icicle/include/icicle/math/storage.h b/icicle/include/icicle/math/storage.h index 097db881fd..76245db166 100644 --- a/icicle/include/icicle/math/storage.h +++ b/icicle/include/icicle/math/storage.h @@ -16,7 +16,8 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<1> { + storage<1> +{ static constexpr unsigned LC = 1; uint32_t limbs[1]; }; @@ -27,7 +28,8 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<3> { + storage<3> +{ static constexpr unsigned LC = 3; uint32_t limbs[3]; }; @@ -38,7 +40,8 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage { + storage +{ static_assert(LIMBS_COUNT % 2 == 0, "odd number of limbs is not supported\n"); static constexpr unsigned LC = LIMBS_COUNT; union { // works only with even LIMBS_COUNT @@ -52,6 +55,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage_array { + storage_array +{ storage storages[OMEGAS_COUNT]; }; \ No newline at end of file diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index b23675a0bc..f103fabb54 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -19,25 +19,19 @@ namespace icicle { */ template Fri create_fri_with_merkle_trees( - const size_t folding_factor, - const size_t stopping_degree, - std::vector merkle_trees) + const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) { - std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute( - folding_factor, - stopping_degree, - merkle_trees, - backend)); + std::shared_ptr> backend; + ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); Fri fri{backend}; return fri; } /** - * @brief Specialization of create_fri for the case of - * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). - */ + * @brief Specialization of create_fri for the case of + * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + */ template Fri create_fri_template( const size_t input_size, @@ -55,7 +49,8 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); - size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size()/merkle_tree_compress_hash.output_size(); + size_t compress_hash_arity = + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); @@ -65,106 +60,82 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees( - folding_factor, - stopping_degree, - merkle_trees); + return create_fri_with_merkle_trees(folding_factor, stopping_degree, merkle_trees); } /** - * @brief Specialization of create_fri for the case of - * (folding_factor, stopping_degree, vector&&). - */ + * @brief Specialization of create_fri for the case of + * (folding_factor, stopping_degree, vector&&). + */ template - Fri create_fri_template( - size_t folding_factor, - size_t stopping_degree, - std::vector merkle_trees) + Fri create_fri_template(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_with_merkle_trees( - folding_factor, - stopping_degree, - merkle_trees); - } + return create_fri_with_merkle_trees(folding_factor, stopping_degree, merkle_trees); + } - #ifdef EXT_FIELD - using FriExtFactoryScalar = FriFactoryImpl; - ICICLE_DISPATCHER_INST(FriExtFieldDispatcher, extension_fri_factory, FriExtFactoryScalar); - /** - * @brief Create a FRI instance. - * @return A `Fri` object built around the chosen backend. - */ - template - Fri create_fri_with_merkle_trees_ext( - const size_t folding_factor, - const size_t stopping_degree, - std::vector merkle_trees) - { - std::shared_ptr> backend; - ICICLE_CHECK(FriExtFieldDispatcher::execute( - folding_factor, - stopping_degree, - merkle_trees, - backend)); - - Fri fri{backend}; - return fri; - } - /** - * @brief Specialization of create_fri for the case of - * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). - */ - template - Fri create_fri_template_ext( - const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, - const uint64_t output_store_min_layer) - { - ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; - const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); - const size_t df = stopping_degree; - const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; - const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; - - std::vector merkle_trees; - merkle_trees.reserve(fold_rounds); - size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size()/merkle_tree_compress_hash.output_size(); - ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; - size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; - std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); - layer_hashes[0] = merkle_tree_leaves_hash; - uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); - for (size_t i = 0; i < fold_rounds; i++) { - merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); - layer_hashes.pop_back(); - } - return create_fri_with_merkle_trees_ext( - folding_factor, - stopping_degree, - merkle_trees); - } - - /** - * @brief Specialization of create_fri for the case of - * (folding_factor, stopping_degree, vector&&). - */ - template - Fri create_fri_template_ext( - size_t folding_factor, - size_t stopping_degree, - std::vector merkle_trees) - { - return create_fri_with_merkle_trees_ext( - folding_factor, - stopping_degree, - merkle_trees); +#ifdef EXT_FIELD + using FriExtFactoryScalar = FriFactoryImpl; + ICICLE_DISPATCHER_INST(FriExtFieldDispatcher, extension_fri_factory, FriExtFactoryScalar); + /** + * @brief Create a FRI instance. + * @return A `Fri` object built around the chosen backend. + */ + template + Fri create_fri_with_merkle_trees_ext( + const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) + { + std::shared_ptr> backend; + ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); + + Fri fri{backend}; + return fri; + } + /** + * @brief Specialization of create_fri for the case of + * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + */ + template + Fri create_fri_template_ext( + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) + { + ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; + const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); + const size_t df = stopping_degree; + const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + + std::vector merkle_trees; + merkle_trees.reserve(fold_rounds); + size_t compress_hash_arity = + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; + size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; + std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); + layer_hashes[0] = merkle_tree_leaves_hash; + uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); + for (size_t i = 0; i < fold_rounds; i++) { + merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); + layer_hashes.pop_back(); } - #endif // EXT_FIELD + return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, merkle_trees); + } + + /** + * @brief Specialization of create_fri for the case of + * (folding_factor, stopping_degree, vector&&). + */ + template + Fri create_fri_template_ext(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) + { + return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, merkle_trees); + } +#endif // EXT_FIELD - template <> Fri create_fri( const size_t input_size, @@ -175,48 +146,38 @@ namespace icicle { const uint64_t output_store_min_layer) { return create_fri_template( - input_size, - folding_factor, - stopping_degree, - merkle_tree_leaves_hash, - merkle_tree_compress_hash, + input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); } template <> - Fri create_fri( - size_t folding_factor, - size_t stopping_degree, - std::vector merkle_trees){ - return create_fri_template(folding_factor, stopping_degree, merkle_trees); - } + Fri + create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) + { + return create_fri_template(folding_factor, stopping_degree, merkle_trees); + } - #ifdef EXT_FIELD - template <> - Fri create_fri( - size_t folding_factor, - size_t stopping_degree, - std::vector merkle_trees){ - return create_fri_template_ext(folding_factor, stopping_degree, merkle_trees); - } +#ifdef EXT_FIELD + template <> + Fri + create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) + { + return create_fri_template_ext(folding_factor, stopping_degree, merkle_trees); + } - template <> - Fri create_fri( - const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, - const uint64_t output_store_min_layer) - { - return create_fri_template_ext( - input_size, - folding_factor, - stopping_degree, - merkle_tree_leaves_hash, - merkle_tree_compress_hash, - output_store_min_layer); - } -#endif + template <> + Fri create_fri( + const size_t input_size, + const size_t folding_factor, + const size_t stopping_degree, + const Hash& merkle_tree_leaves_hash, + const Hash& merkle_tree_compress_hash, + const uint64_t output_store_min_layer) + { + return create_fri_template_ext( + input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, + output_store_min_layer); + } +#endif } // namespace icicle diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index 64ecf8396c..21b49caea5 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -5,10 +5,8 @@ #include "icicle/fri/fri_transcript_config.h" #include - using namespace field_config; - extern "C" { // Define the FRI handle type @@ -54,7 +52,7 @@ struct FriCreateHashFFI { struct FriCreateWithTreesFFI { size_t folding_factor; size_t stopping_degree; - MerkleTree* merkle_trees; // An array of MerkleTree* (pointers). + MerkleTree* merkle_trees; // An array of MerkleTree* (pointers). size_t merkle_trees_count; // Number of items in merkle_trees. }; @@ -65,11 +63,8 @@ struct FriCreateWithTreesFFI { * @param transcript_config Pointer to the FFI transcript configuration structure. * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. */ -FriHandle* -CONCAT_EXPAND(FIELD, fri_create)( - const FriCreateHashFFI* create_config, - const FriTranscriptConfigFFI* ffi_transcript_config -) +FriHandle* CONCAT_EXPAND(FIELD, fri_create)( + const FriCreateHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { ICICLE_LOG_ERROR << "Invalid FRI creation config."; @@ -80,11 +75,10 @@ CONCAT_EXPAND(FIELD, fri_create)( return nullptr; } - ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (hash-based)"; // Convert byte arrays to vectors - //TODO SHANIE - check if this is the correct way + // TODO SHANIE - check if this is the correct way std::vector domain_separator_label( ffi_transcript_config->domain_separator_label, ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); @@ -92,15 +86,11 @@ CONCAT_EXPAND(FIELD, fri_create)( ffi_transcript_config->round_challenge_label, ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); std::vector commit_label( - ffi_transcript_config->commit_label, - ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); std::vector nonce_label( - ffi_transcript_config->nonce_label, - ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); std::vector public_state( - ffi_transcript_config->public_state, - ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - + ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); // Construct a FriTranscriptConfig FriTranscriptConfig config{ @@ -110,18 +100,13 @@ CONCAT_EXPAND(FIELD, fri_create)( std::move(commit_label), std::move(nonce_label), std::move(public_state), - *(ffi_transcript_config->seed_rng) - }; + *(ffi_transcript_config->seed_rng)}; // Create and return the Fri instance return new icicle::Fri(icicle::create_fri( - create_config->input_size, - create_config->folding_factor, - create_config->stopping_degree, - *(create_config->merkle_tree_leaves_hash), - *(create_config->merkle_tree_compress_hash), - create_config->output_store_min_layer - )); + create_config->input_size, create_config->folding_factor, create_config->stopping_degree, + *(create_config->merkle_tree_leaves_hash), *(create_config->merkle_tree_compress_hash), + create_config->output_store_min_layer)); } // fri_create_with_trees - Using vector&& constructor @@ -132,11 +117,8 @@ CONCAT_EXPAND(FIELD, fri_create)( * @param transcript_config Pointer to the FFI transcript configuration structure. * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. */ -FriHandle* -CONCAT_EXPAND(FIELD, fri_create_with_trees)( - const FriCreateWithTreesFFI* create_config, - const FriTranscriptConfigFFI* ffi_transcript_config -) +FriHandle* CONCAT_EXPAND(FIELD, fri_create_with_trees)( + const FriCreateWithTreesFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_trees) { ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; @@ -147,7 +129,6 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( return nullptr; } - ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (with existing trees)"; // Convert the raw array of MerkleTree* into a std::vector @@ -157,9 +138,8 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( merkle_trees_vec.push_back(create_config->merkle_trees[i]); } - // Convert byte arrays to vectors - //TODO SHANIE - check if this is the correct way + // TODO SHANIE - check if this is the correct way std::vector domain_separator_label( ffi_transcript_config->domain_separator_label, ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); @@ -167,15 +147,11 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( ffi_transcript_config->round_challenge_label, ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); std::vector commit_label( - ffi_transcript_config->commit_label, - ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); std::vector nonce_label( - ffi_transcript_config->nonce_label, - ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); std::vector public_state( - ffi_transcript_config->public_state, - ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - + ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); // Construct a FriTranscriptConfig FriTranscriptConfig config{ @@ -185,18 +161,13 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees)( std::move(commit_label), std::move(nonce_label), std::move(public_state), - *(ffi_transcript_config->seed_rng) - }; + *(ffi_transcript_config->seed_rng)}; // Create and return the Fri instance return new icicle::Fri(icicle::create_fri( - create_config->folding_factor, - create_config->stopping_degree, - merkle_trees_vec - )); + create_config->folding_factor, create_config->stopping_degree, merkle_trees_vec)); } - /** * @brief Deletes the given Fri instance. * @param fri_handle Pointer to the Fri instance to be deleted. @@ -214,15 +185,10 @@ eIcicleError CONCAT_EXPAND(FIELD, fri_delete)(const FriHandle* fri_handle) return eIcicleError::SUCCESS; } - - #ifdef EXT_FIELD // EXT_FIELD -FriHandleExt* -CONCAT_EXPAND(FIELD, fri_create_ext)( - const FriCreateHashFFI* create_config, - const FriTranscriptConfigFFI* ffi_transcript_config -) +FriHandleExt* CONCAT_EXPAND(FIELD, fri_create_ext)( + const FriCreateHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { ICICLE_LOG_ERROR << "Invalid FRI creation config."; @@ -243,14 +209,11 @@ CONCAT_EXPAND(FIELD, fri_create_ext)( ffi_transcript_config->round_challenge_label, ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); std::vector commit_label( - ffi_transcript_config->commit_label, - ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); std::vector nonce_label( - ffi_transcript_config->nonce_label, - ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); std::vector public_state( - ffi_transcript_config->public_state, - ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); + ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); // Construct a FriTranscriptConfig FriTranscriptConfig config{ @@ -260,33 +223,25 @@ CONCAT_EXPAND(FIELD, fri_create_ext)( std::move(commit_label), std::move(nonce_label), std::move(public_state), - *(ffi_transcript_config->seed_rng) - }; + *(ffi_transcript_config->seed_rng)}; // Create and return the Fri instance for the extension field return new icicle::Fri(icicle::create_fri( - create_config->input_size, - create_config->folding_factor, - create_config->stopping_degree, - *(create_config->merkle_tree_leaves_hash), - *(create_config->merkle_tree_compress_hash), - create_config->output_store_min_layer - )); + create_config->input_size, create_config->folding_factor, create_config->stopping_degree, + *(create_config->merkle_tree_leaves_hash), *(create_config->merkle_tree_compress_hash), + create_config->output_store_min_layer)); } - -FriHandleExt* -CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( - const FriCreateWithTreesFFI* create_config, - const FriTranscriptConfigFFI* ffi_transcript_config -) +FriHandleExt* CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( + const FriCreateWithTreesFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_trees) { ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; return nullptr; } - if (!ffi_transcript_config || !ffi_transcript_config - || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { + if ( + !ffi_transcript_config || !ffi_transcript_config || !ffi_transcript_config->hasher || + !ffi_transcript_config->seed_rng) { ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; return nullptr; } @@ -300,9 +255,8 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( merkle_trees_vec.push_back(create_config->merkle_trees[i]); } - // Convert byte arrays to vectors - //TODO SHANIE - check if this is the correct way + // TODO SHANIE - check if this is the correct way std::vector domain_separator_label( ffi_transcript_config->domain_separator_label, ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); @@ -310,15 +264,11 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( ffi_transcript_config->round_challenge_label, ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); std::vector commit_label( - ffi_transcript_config->commit_label, - ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); + ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); std::vector nonce_label( - ffi_transcript_config->nonce_label, - ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); + ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); std::vector public_state( - ffi_transcript_config->public_state, - ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - + ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); // Construct a FriTranscriptConfig FriTranscriptConfig config{ @@ -328,15 +278,11 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( std::move(commit_label), std::move(nonce_label), std::move(public_state), - *(ffi_transcript_config->seed_rng) - }; + *(ffi_transcript_config->seed_rng)}; // Create and return the Fri instance return new icicle::Fri(icicle::create_fri( - create_config->folding_factor, - create_config->stopping_degree, - merkle_trees_vec - )); + create_config->folding_factor, create_config->stopping_degree, merkle_trees_vec)); } /** @@ -344,19 +290,18 @@ CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( * @param fri_handle_ext Pointer to the Fri instance to be deleted. * @return eIcicleError indicating the success or failure of the operation. */ - eIcicleError CONCAT_EXPAND(FIELD, fri_delete_ext)(const FriHandleExt* fri_handle_ext) - { - if (!fri_handle_ext) { - ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; - return eIcicleError::INVALID_ARGUMENT; - } - - ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; - delete fri_handle_ext; - - return eIcicleError::SUCCESS; - } - +eIcicleError CONCAT_EXPAND(FIELD, fri_delete_ext)(const FriHandleExt* fri_handle_ext) +{ + if (!fri_handle_ext) { + ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; + return eIcicleError::INVALID_ARGUMENT; + } + + ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; + delete fri_handle_ext; + + return eIcicleError::SUCCESS; +} #endif // EXT_FIELD From 59b0e72e0c1c21de0ce80a4b5f7f794b60227623 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:36:20 +0200 Subject: [PATCH 084/127] format --- icicle/include/icicle/math/storage.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/icicle/include/icicle/math/storage.h b/icicle/include/icicle/math/storage.h index 76245db166..097db881fd 100644 --- a/icicle/include/icicle/math/storage.h +++ b/icicle/include/icicle/math/storage.h @@ -16,8 +16,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<1> -{ + storage<1> { static constexpr unsigned LC = 1; uint32_t limbs[1]; }; @@ -28,8 +27,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(1)) #endif - storage<3> -{ + storage<3> { static constexpr unsigned LC = 3; uint32_t limbs[3]; }; @@ -40,8 +38,7 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage -{ + storage { static_assert(LIMBS_COUNT % 2 == 0, "odd number of limbs is not supported\n"); static constexpr unsigned LC = LIMBS_COUNT; union { // works only with even LIMBS_COUNT @@ -55,7 +52,6 @@ struct #ifdef __CUDA_ARCH__ __align__(LIMBS_ALIGNMENT(LIMBS_COUNT)) #endif - storage_array -{ + storage_array { storage storages[OMEGAS_COUNT]; }; \ No newline at end of file From dc3f2bf0e517148ea64e32b342161195fa3abf49 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:40:06 +0200 Subject: [PATCH 085/127] format --- icicle/include/icicle/fields/quartic_extension.h | 1 + 1 file changed, 1 insertion(+) diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 8efcf54b35..3cb1dd3297 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -273,6 +273,7 @@ class QuarticExtensionField FF::from(in + 3 * sizeof(FF), sizeof(FF))}; } }; + #if __CUDACC__ template struct SharedMemory> { From 0b4dafe8c3201c7f6aedca27bad9d6807e1e037e Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 12:44:29 +0200 Subject: [PATCH 086/127] format --- icicle/include/icicle/fields/quartic_extension.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 3cb1dd3297..405a5fb468 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -264,7 +264,7 @@ class QuarticExtensionField return res; } - /* Receives an array of bytes and its size and returns extension field element. */ + // Receives an array of bytes and its size and returns extension field element. static constexpr HOST_DEVICE_INLINE QuarticExtensionField from(const std::byte* in, unsigned nof_bytes) { ICICLE_ASSERT(nof_bytes >= 4 * sizeof(FF)) << "Input size is too small"; From cdbe020b02bf773f92450f9d123b49551f3c7118 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 14:52:15 +0200 Subject: [PATCH 087/127] format --- icicle/include/icicle/fields/quartic_extension.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 405a5fb468..42ec10dff9 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -242,7 +242,7 @@ class QuarticExtensionField FF::reduce( (CONFIG::nonresidue_is_negative ? (FF::mul_wide(xs.real, x0) + FF::template mul_unsigned(FF::mul_wide(xs.im2, x2))) - : (FF::mul_wide(xs.real, x0)) - FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), + : (FF::mul_wide(xs.real, x0))-FF::template mul_unsigned(FF::mul_wide(xs.im2, x2)))), FF::reduce( (CONFIG::nonresidue_is_negative ? FWide::neg(FF::template mul_unsigned(FF::mul_wide(xs.im3, x2))) From 0e8670a94dfab7c2bbbdd0d0ce10a84ee9fae2f9 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 14:58:27 +0200 Subject: [PATCH 088/127] debug function removed --- icicle/include/icicle/fri/fri_proof.h | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 62a098f286..31b3b15318 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -106,20 +106,6 @@ namespace icicle { std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) size_t m_final_poly_size; // Size of the final polynomial uint64_t m_pow_nonce; // Proof-of-work nonce - - public: - // for debug - void print_proof() - { - std::cout << "FRI Proof:" << std::endl; - for (int query_idx = 0; query_idx < m_query_proofs.size(); query_idx++) { - std::cout << " Query " << query_idx << ":" << std::endl; - for (int round_idx = 0; round_idx < m_query_proofs[query_idx].size(); round_idx++) { - std::cout << " round " << round_idx << ":" << std::endl; - m_query_proofs[query_idx][round_idx].print_proof(); - } - } - } }; } // namespace icicle \ No newline at end of file From 13ef0f0e6e998403c2bba17e92837f4271fd8720 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 17:20:06 +0200 Subject: [PATCH 089/127] remove code in comment, fri test under ifdef --- icicle/include/icicle/fri/fri_proof.h | 10 ---- icicle/tests/test_field_api.cpp | 72 +++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 31b3b15318..3166617a7c 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -68,16 +68,6 @@ namespace icicle { return m_query_proofs[0][round_idx].get_root(); } - // /** - // * @brief Returns a tuple containing the pointer to the leaf data, its size and index. - // * @return A tuple of (leaf data pointer, leaf size, leaf_index). - // */ - // std::tuple get_leaf(const size_t query_idx, const size_t round_idx) - // const - // { - // return m_query_proofs[query_idx][round_idx].get_leaf(); - // } - /** * @brief Get the number of FRI rounds in the proof. * diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 5a2ac61ca0..3b6c0627d6 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -576,14 +576,14 @@ TEST_F(FieldTestBase, SumcheckSingleInputProgram) #endif // SUMCHECK -// #ifdef FRI +#ifdef FRI -TYPED_TEST(FieldTest, Fri) +TYPED_TEST(FieldTest, FriHashAPi) { // Randomize configuration const int log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; - const int folding_factor = 2; // TODO SHANIE - add support for other folding factors + const int folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors const int log_stopping_size = rand_uint_32b(0, log_input_size - 2); const size_t stopping_size = 1 << log_stopping_size; const size_t stopping_degree = stopping_size - 1; @@ -628,7 +628,71 @@ TYPED_TEST(FieldTest, Fri) // Release domain ICICLE_CHECK(ntt_release_domain()); } -// #endif // FRI + +TYPED_TEST(FieldTest, FriMerkleTreeAPi) +{ + // Randomize configuration + const int log_input_size = rand_uint_32b(3, 13); + const size_t input_size = 1 << log_input_size; + const int folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors + const int log_stopping_size = rand_uint_32b(0, log_input_size - 2); + const size_t stopping_size = 1 << log_stopping_size; + const size_t stopping_degree = stopping_size - 1; + const uint64_t output_store_min_layer = 0; + const size_t pow_bits = rand_uint_32b(0, 3); + const size_t nof_queries = rand_uint_32b(2, 4); + + // Initialize ntt domain + NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); + ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); + + // Generate input polynomial evaluations + auto scalars = std::make_unique(input_size); + TypeParam::rand_host_many(scalars.get(), input_size); + + const size_t df = stopping_degree; + const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; + const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; + + // Define hashers and merkle trees + uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B + Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B + + std::vector merkle_trees; + merkle_trees.reserve(fold_rounds); + size_t compress_hash_arity = compress.default_input_chunk_size() / compress.output_size(); + size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; + std::vector layer_hashes(first_merkle_tree_height, compress); + layer_hashes[0] = hash; + uint64_t leaf_element_size = hash.default_input_chunk_size(); + for (size_t i = 0; i < fold_rounds; i++) { + merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); + layer_hashes.pop_back(); + } + + // ===== Prover side ====== + Fri prover_fri = create_fri(folding_factor, stopping_degree, merkle_trees); + + FriTranscriptConfig transcript_config; + FriConfig fri_config; + fri_config.nof_queries = nof_queries; + fri_config.pow_bits = pow_bits; + FriProof fri_proof; + + ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + + // ===== Verifier side ====== + Fri verifier_fri = create_fri(folding_factor, stopping_degree, merkle_trees); + bool verification_pass = false; + ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, verification_pass)); + + ASSERT_EQ(true, verification_pass); + + // Release domain + ICICLE_CHECK(ntt_release_domain()); +} +#endif // FRI // TODO Hadar: this is a workaround for 'storage<18 - scalar_t::TLC>' failing due to 17 limbs not supported. // It means we skip fields such as babybear! From 00a5ef3fface46db2594429779bd9dbc7db283c4 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 2 Mar 2025 17:24:39 +0200 Subject: [PATCH 090/127] format --- icicle/tests/test_field_api.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 3b6c0627d6..a4635aa833 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -653,12 +653,12 @@ TYPED_TEST(FieldTest, FriMerkleTreeAPi) const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; - + // Define hashers and merkle trees - uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B + uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - + std::vector merkle_trees; merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = compress.default_input_chunk_size() / compress.output_size(); @@ -670,11 +670,11 @@ TYPED_TEST(FieldTest, FriMerkleTreeAPi) merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - + // ===== Prover side ====== Fri prover_fri = create_fri(folding_factor, stopping_degree, merkle_trees); - FriTranscriptConfig transcript_config; + FriTranscriptConfig transcript_config; FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; @@ -684,14 +684,14 @@ TYPED_TEST(FieldTest, FriMerkleTreeAPi) // ===== Verifier side ====== Fri verifier_fri = create_fri(folding_factor, stopping_degree, merkle_trees); - bool verification_pass = false; + bool verification_pass = false; ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, verification_pass)); ASSERT_EQ(true, verification_pass); // Release domain ICICLE_CHECK(ntt_release_domain()); -} +} #endif // FRI // TODO Hadar: this is a workaround for 'storage<18 - scalar_t::TLC>' failing due to 17 limbs not supported. From 518318d27b062c41a2e6da1563e2a160fe5ed1f9 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Mon, 3 Mar 2025 11:53:37 +0200 Subject: [PATCH 091/127] Remove unnecessary files --- backend/cuda | 1 - icicle/backend/metal | 1 - icicle/buil/_deps/taskflow-src | 1 - 3 files changed, 3 deletions(-) delete mode 160000 backend/cuda delete mode 160000 icicle/backend/metal delete mode 160000 icicle/buil/_deps/taskflow-src diff --git a/backend/cuda b/backend/cuda deleted file mode 160000 index f373d8a91a..0000000000 --- a/backend/cuda +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f373d8a91ace751b798774f7c250b12df065d484 diff --git a/icicle/backend/metal b/icicle/backend/metal deleted file mode 160000 index be4f208bc3..0000000000 --- a/icicle/backend/metal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit be4f208bc342b1d4cdf7661a0de58d94fdba9327 diff --git a/icicle/buil/_deps/taskflow-src b/icicle/buil/_deps/taskflow-src deleted file mode 160000 index d8c49c64b4..0000000000 --- a/icicle/buil/_deps/taskflow-src +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d8c49c64b4ee5015a3f1c0a42748fa7a2bf5529e From a9bce886123a602f878e8ab6ad326d7d663b8f4d Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Mon, 3 Mar 2025 14:47:32 +0200 Subject: [PATCH 092/127] asserts replaced with errors --- icicle/backend/cpu/include/cpu_fri_backend.h | 20 +++++----- icicle/include/icicle/fri/fri.h | 37 ++++++++++------- icicle/tests/test_field_api.cpp | 42 ++++++++++---------- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 6af148a640..9fc6e31463 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -21,7 +21,7 @@ namespace icicle { * * @param folding_factor The factor by which the codeword is folded each round. * @param stopping_degree Stopping degree threshold for the final polynomial. - * @param merkle_trees A vector of MerkleTrees. + * @param merkle_trees A vector of MerkleTrees, tree per FRI round. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), @@ -36,7 +36,9 @@ namespace icicle { const F* input_data, FriProof& fri_proof /*out*/) override { - ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; + if(__builtin_expect(fri_config.nof_queries <= 0, 0)){ + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + } FriTranscript transcript(std::move(fri_transcript_config), m_log_input_size); @@ -72,10 +74,10 @@ namespace icicle { eIcicleError commit_fold_phase( const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - ICICLE_ASSERT(this->m_folding_factor == 2) - << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding - // factors - + if (this->m_folding_factor != 2){ + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding + // factors + } const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); @@ -92,8 +94,9 @@ namespace icicle { MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); current_round_tree->build(round_evals, current_size, MerkleTreeConfig()); auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); - ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; - + if (root_ptr == nullptr || root_size <= 0){ + ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; + } // Add root to transcript and get alpha std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); @@ -150,7 +153,6 @@ namespace icicle { */ eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); std::vector query_indices = diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 88c5f60c3f..4c56912f72 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -93,17 +93,20 @@ namespace icicle { * @param fri_config Configuration for FRI operations. * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). * @param fri_proof The proof object to verify. - * @param verification_pass (OUT) Set to true if verification succeeds, false otherwise. + * @param valid (OUT) Set to true if verification succeeds, false otherwise. * @return An eIcicleError indicating success or failure. */ eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig&& fri_transcript_config, FriProof& fri_proof, - bool& verification_pass /* OUT */) const + bool& valid /* OUT */) const { - verification_pass = false; - ICICLE_ASSERT(fri_config.nof_queries > 0) << "No queries specified in FriConfig."; + valid = false; + if(__builtin_expect(fri_config.nof_queries <= 0, 0)){ + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + } + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); @@ -115,7 +118,9 @@ namespace icicle { std::move(const_cast&>(fri_transcript_config)), log_input_size); for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { auto [root_ptr, root_size] = fri_proof.get_root(round_idx); - ICICLE_ASSERT(root_ptr != nullptr && root_size > 0) << "Failed to retrieve Merkle root for round " << round_idx; + if (root_ptr == nullptr || root_size <= 0){ + ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; + } std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); alpha_values[round_idx] = transcript.get_alpha(merkle_commit); @@ -124,14 +129,13 @@ namespace icicle { // proof-of-work if (fri_config.pow_bits != 0) { bool valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); - if (!valid) return eIcicleError::SUCCESS; // return with verification_pass = false + if (!valid) return eIcicleError::SUCCESS; // return with valid = false transcript.set_pow_nonce(fri_proof.get_pow_nonce()); } // get query indices size_t seed = transcript.get_seed_for_query_phase(); seed_rand_generator(seed); - ICICLE_ASSERT(fri_config.nof_queries > 0) << "Number of queries must be > 0"; std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); uint64_t domain_max_size = 0; @@ -158,7 +162,7 @@ namespace icicle { if (!valid) { ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false + return eIcicleError::SUCCESS; // return with valid = false } MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); @@ -172,15 +176,18 @@ namespace icicle { if (!valid) { ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false + return eIcicleError::SUCCESS; // return with valid = false } // collinearity check const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); - ICICLE_ASSERT(elem_idx == leaf_index) << "Leaf index from proof doesn't match query expected index"; - ICICLE_ASSERT(elem_idx_sym == leaf_index_sym) - << "Leaf index symmetry from proof doesn't match query expected index"; + if(__builtin_expect(elem_idx != leaf_index, 0)){ + ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; + } + if(__builtin_expect(elem_idx_sym != leaf_index_sym, 0)){ + ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; + } const F& leaf_data_f = *reinterpret_cast(leaf_data); const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); @@ -194,7 +201,7 @@ namespace icicle { if (final_poly[query % final_poly_size] != folded) { ICICLE_LOG_ERROR << "[VERIFIER] (last round) Collinearity check failed for query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with verification_pass = false; + return eIcicleError::SUCCESS; // return with valid = false; } } else { MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2 * query_idx, round_idx + 1); @@ -204,13 +211,13 @@ namespace icicle { ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" << leaf_data_folded_f; - return eIcicleError::SUCCESS; // return with verification_pass = false + return eIcicleError::SUCCESS; // return with valid = false } } current_log_size--; } } - verification_pass = true; + valid = true; return eIcicleError::SUCCESS; } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a4635aa833..5f8001c392 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -186,11 +186,11 @@ TEST_F(FieldTestBase, Sumcheck) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(); - bool verification_pass = false; + bool valid = false; ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); + verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); }; for (const auto& device : s_registered_devices) @@ -259,10 +259,10 @@ TEST_F(FieldTestBase, SumcheckDataOnDevice) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(); - bool verification_pass = false; - ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); + bool valid = false; + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); for (auto& mle_poly_ptr : mle_polynomials) { delete[] mle_poly_ptr; @@ -329,11 +329,11 @@ TEST_F(FieldTestBase, SumcheckUserDefinedCombine) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(); - bool verification_pass = false; + bool valid = false; ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); + verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); }; for (const auto& device : s_registered_devices) { run(device, mle_polynomials, mle_poly_size, claimed_sum, "Sumcheck"); @@ -494,11 +494,11 @@ TEST_F(FieldTestBase, SumcheckIdentity) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(); - bool verification_pass = false; + bool valid = false; ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); + verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); }; for (const auto& device : s_registered_devices) @@ -559,11 +559,11 @@ TEST_F(FieldTestBase, SumcheckSingleInputProgram) // ===== Verifier side ====== // create sumcheck auto verifier_sumcheck = create_sumcheck(); - bool verification_pass = false; + bool valid = false; ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), verification_pass)); + verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); }; for (const auto& device : s_registered_devices) @@ -620,10 +620,10 @@ TYPED_TEST(FieldTest, FriHashAPi) // ===== Verifier side ====== Fri verifier_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - bool verification_pass = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, verification_pass)); + bool valid = false; + ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); // Release domain ICICLE_CHECK(ntt_release_domain()); @@ -684,10 +684,10 @@ TYPED_TEST(FieldTest, FriMerkleTreeAPi) // ===== Verifier side ====== Fri verifier_fri = create_fri(folding_factor, stopping_degree, merkle_trees); - bool verification_pass = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, verification_pass)); + bool valid = false; + ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, valid)); - ASSERT_EQ(true, verification_pass); + ASSERT_EQ(true, valid); // Release domain ICICLE_CHECK(ntt_release_domain()); From 77f26466641c64363ae2fd33a1451c5f34734676 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Mon, 3 Mar 2025 15:25:13 +0200 Subject: [PATCH 093/127] merkle_trees is now moved into CpuFriBackend when calling create_fri. It is then passed by reference to fri_rounds --- icicle/backend/cpu/include/cpu_fri_backend.h | 6 +++--- icicle/backend/cpu/include/cpu_fri_rounds.h | 4 ++-- icicle/backend/cpu/src/field/cpu_fri.cpp | 2 +- icicle/src/fri/fri.cpp | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 9fc6e31463..e3b24731c8 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,9 +24,9 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees, tree per FRI round. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(merkle_trees.size()), - m_log_input_size(merkle_trees.size() + std::log2(static_cast(stopping_degree + 1))), - m_input_size(pow(2, m_log_input_size)), m_fri_rounds(merkle_trees, m_log_input_size) + : FriBackend(folding_factor, stopping_degree, std::move(merkle_trees)), m_nof_fri_rounds(this->m_merkle_trees.size()), + m_log_input_size(this->m_merkle_trees.size() + std::log2(static_cast(stopping_degree + 1))), + m_input_size(pow(2, m_log_input_size)), m_fri_rounds(this->m_merkle_trees, m_log_input_size) { } diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index 4c2106c3fc..5ef2f83188 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -23,7 +23,7 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees. * @param log_input_size The log of the input size. */ - FriRounds(std::vector merkle_trees, const size_t log_input_size) : m_merkle_trees(merkle_trees) + FriRounds(std::vector& merkle_trees, const size_t log_input_size) : m_merkle_trees(merkle_trees) { size_t fold_rounds = m_merkle_trees.size(); m_rounds_evals.resize(fold_rounds); @@ -68,7 +68,7 @@ namespace icicle { std::vector> m_rounds_evals; // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. - std::vector m_merkle_trees; + std::vector& m_merkle_trees; }; } // namespace icicle diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index 4217988097..c7b70cd6f1 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -14,7 +14,7 @@ namespace icicle { std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); + backend = std::make_shared>(folding_factor, stopping_degree, std::move(merkle_trees)); return eIcicleError::SUCCESS; } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index f103fabb54..1d46ae7627 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -22,7 +22,7 @@ namespace icicle { const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) { std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); + ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); Fri fri{backend}; return fri; @@ -60,7 +60,7 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees(folding_factor, stopping_degree, merkle_trees); + return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); } /** @@ -70,7 +70,7 @@ namespace icicle { template Fri create_fri_template(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_with_merkle_trees(folding_factor, stopping_degree, merkle_trees); + return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); } #ifdef EXT_FIELD @@ -85,7 +85,7 @@ namespace icicle { const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) { std::shared_ptr> backend; - ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); + ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); Fri fri{backend}; return fri; @@ -122,7 +122,7 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, merkle_trees); + return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, std::move(merkle_trees)); } /** @@ -132,7 +132,7 @@ namespace icicle { template Fri create_fri_template_ext(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, merkle_trees); + return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, std::move(merkle_trees)); } #endif // EXT_FIELD @@ -154,7 +154,7 @@ namespace icicle { Fri create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_template(folding_factor, stopping_degree, merkle_trees); + return create_fri_template(folding_factor, stopping_degree, std::move(merkle_trees)); } #ifdef EXT_FIELD @@ -162,7 +162,7 @@ namespace icicle { Fri create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) { - return create_fri_template_ext(folding_factor, stopping_degree, merkle_trees); + return create_fri_template_ext(folding_factor, stopping_degree, std::move(merkle_trees)); } template <> From 907abbfe09f11ef3927b7fc4183863084e3f2fb4 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 10:52:21 +0200 Subject: [PATCH 094/127] Replaced assertions with error logs, applied additional code review fixes --- icicle/backend/cpu/include/cpu_fri_backend.h | 43 ++++++++++++------- icicle/backend/cpu/include/cpu_fri_rounds.h | 22 ++++------ icicle/include/icicle/backend/fri_backend.h | 2 +- .../include/icicle/fields/complex_extension.h | 4 +- .../include/icicle/fields/quartic_extension.h | 4 +- icicle/include/icicle/fri/fri.h | 12 +++--- icicle/include/icicle/fri/fri_config.h | 1 - icicle/include/icicle/fri/fri_proof.h | 9 ++-- icicle/include/icicle/fri/fri_transcript.h | 2 - icicle/src/fri/fri.cpp | 40 ++++++++--------- icicle/tests/test_field_api.cpp | 4 +- 11 files changed, 76 insertions(+), 67 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index e3b24731c8..4872102064 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -30,7 +30,7 @@ namespace icicle { { } - eIcicleError get_fri_proof( + eIcicleError get_proof( const FriConfig& fri_config, const FriTranscriptConfig&& fri_transcript_config, const F* input_data, @@ -46,22 +46,31 @@ namespace icicle { fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); // commit fold phase - ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); - + // ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); + eIcicleError err = commit_fold_phase(input_data, transcript, fri_config, fri_proof); + if(err != eIcicleError::SUCCESS){ + return err; + } + // proof of work - if (fri_config.pow_bits != 0) { ICICLE_CHECK(proof_of_work(transcript, fri_config.pow_bits, fri_proof)); } - + if (fri_config.pow_bits != 0) { + err = proof_of_work(transcript, fri_config.pow_bits, fri_proof); + if(err != eIcicleError::SUCCESS){ + return err; + } + } + // query phase - ICICLE_CHECK(query_phase(transcript, fri_config, fri_proof)); - - return eIcicleError::SUCCESS; + err = query_phase(transcript, fri_config, fri_proof); + + return err; } private: - const size_t m_nof_fri_rounds; // Number of FRI rounds - const size_t m_log_input_size; // Log size of the input polynomial - const size_t m_input_size; // Size of the input polynomial - FriRounds m_fri_rounds; // Holds intermediate rounds + const size_t m_nof_fri_rounds; + const size_t m_log_input_size; + const size_t m_input_size; + FriRounds m_fri_rounds; // Holds intermediate rounds /** * @brief Perform the commit-fold phase of the FRI protocol. @@ -77,12 +86,16 @@ namespace icicle { if (this->m_folding_factor != 2){ ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding // factors - } + } const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); + if (m_input_size > domain_max_size){ + ICICLE_LOG_ERROR << "Size is too large for domain. size = " << m_input_size << ", domain_max_size = " << domain_max_size; + } - // Get persistent storage for round from FriRounds. m_fri_rounds already allocated a vector for each round with - // capacity 2^(m_log_input_size - round_idx). + // Retrieve pre-allocated memory for the round from m_fri_rounds. + // The instance of FriRounds has already allocated a vector for each round with + // a capacity of 2^(m_log_input_size - round_idx). F* round_evals = m_fri_rounds.get_round_evals(0); std::copy(input_data, input_data + m_input_size, round_evals); diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index 5ef2f83188..fee2d475b3 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -1,15 +1,3 @@ -#pragma once - -#include -#include -#include -#include "icicle/merkle/merkle_tree.h" -#include "icicle/errors.h" -#include "icicle/hash/hash.h" -#include "icicle/hash/keccak.h" -#include "icicle/utils/log.h" -#include "icicle/fri/fri.h" - namespace icicle { template @@ -40,13 +28,19 @@ namespace icicle { */ MerkleTree* get_merkle_tree(size_t round_idx) { - ICICLE_ASSERT(round_idx < m_merkle_trees.size()) << "round index out of bounds"; + if(round_idx >= m_merkle_trees.size()){ + ICICLE_LOG_ERROR << "round index out of bounds"; + return nullptr; + } return &m_merkle_trees[round_idx]; } F* get_round_evals(size_t round_idx) { - ICICLE_ASSERT(round_idx < m_rounds_evals.size()) << "round index out of bounds"; + if(round_idx >= m_merkle_trees.size()){ + ICICLE_LOG_ERROR << "round index out of bounds"; + return nullptr; + } return m_rounds_evals[round_idx].get(); } diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index fbe181b2fa..ad8f92a8da 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -46,7 +46,7 @@ namespace icicle { * @param fri_proof (OUT) A FriProof object to store the proof's Merkle layers, final poly, etc. * @return eIcicleError Error code indicating success or failure. */ - virtual eIcicleError get_fri_proof( + virtual eIcicleError get_proof( const FriConfig& fri_config, const FriTranscriptConfig&& fri_transcript_config, const F* input_data, diff --git a/icicle/include/icicle/fields/complex_extension.h b/icicle/include/icicle/fields/complex_extension.h index cfd69081f6..42813fad40 100644 --- a/icicle/include/icicle/fields/complex_extension.h +++ b/icicle/include/icicle/fields/complex_extension.h @@ -309,7 +309,9 @@ class ComplexExtensionField /* Receives an array of bytes and its size and returns extension field element. */ static constexpr HOST_DEVICE_INLINE ComplexExtensionField from(const std::byte* in, unsigned nof_bytes) { - ICICLE_ASSERT(nof_bytes >= 2 * sizeof(FF)) << "Input size is too small"; + if(nof_bytes < 2 * sizeof(FF)){ + ICICLE_LOG_ERROR << "Input size is too small"; + } return ComplexExtensionField{FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF))}; } }; diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index 42ec10dff9..bc19a02afa 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -267,7 +267,9 @@ class QuarticExtensionField // Receives an array of bytes and its size and returns extension field element. static constexpr HOST_DEVICE_INLINE QuarticExtensionField from(const std::byte* in, unsigned nof_bytes) { - ICICLE_ASSERT(nof_bytes >= 4 * sizeof(FF)) << "Input size is too small"; + if(nof_bytes < 4 * sizeof(FF)){ + ICICLE_LOG_ERROR << "Input size is too small"; + } return QuarticExtensionField{ FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF)), FF::from(in + 2 * sizeof(FF), sizeof(FF)), FF::from(in + 3 * sizeof(FF), sizeof(FF))}; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 4c56912f72..d169eca695 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -39,8 +39,8 @@ namespace icicle { const size_t input_size, const size_t folding_factor, const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer = 0); /** @@ -79,13 +79,13 @@ namespace icicle { * @param fri_proof Reference to a FriProof object (output). * @return An eIcicleError indicating success or failure. */ - eIcicleError get_fri_proof( + eIcicleError get_proof( const FriConfig& fri_config, const FriTranscriptConfig&& fri_transcript_config, const F* input_data, FriProof& fri_proof /* OUT */) const { - return m_backend->get_fri_proof(fri_config, std::move(fri_transcript_config), input_data, fri_proof); + return m_backend->get_proof(fri_config, std::move(fri_transcript_config), input_data, fri_proof); } /** @@ -117,7 +117,7 @@ namespace icicle { FriTranscript transcript( std::move(const_cast&>(fri_transcript_config)), log_input_size); for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - auto [root_ptr, root_size] = fri_proof.get_root(round_idx); + auto [root_ptr, root_size] = fri_proof.get_merkle_tree_root(round_idx); if (root_ptr == nullptr || root_size <= 0){ ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; } @@ -222,7 +222,7 @@ namespace icicle { } private: - std::shared_ptr> m_backend; // Shared pointer to the backend for FRI operations. + std::shared_ptr> m_backend; }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index acc99483fe..e457cb52de 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -19,7 +19,6 @@ namespace icicle { size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. - bool are_outputs_on_device = false; // True if outputs reside on the device, false if on the host. Default is false. bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. Default is nullptr. }; diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 3166617a7c..310399947f 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -31,9 +31,10 @@ namespace icicle { */ void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { - ICICLE_ASSERT(nof_queries > 0 && nof_fri_rounds > 0) - << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries + if(nof_queries <= 0 || nof_fri_rounds <= 0){ + ICICLE_LOG_ERROR << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; + } // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns m_query_proofs.resize( @@ -60,10 +61,10 @@ namespace icicle { } /** - * @brief Returns a pair containing the pointer to the root data and its size. + * @brief Returns a pair containing the pointer to the merkle tree root data and its size. * @return A pair of (root data pointer, root size). */ - std::pair get_root(const size_t round_idx) const + std::pair get_merkle_tree_root(const size_t round_idx) const { return m_query_proofs[0][round_idx].get_root(); } diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 3124c4fbdd..1008f5f475 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -33,8 +33,6 @@ namespace icicle { */ F get_alpha(const std::vector& merkle_commit) { - ICICLE_ASSERT(m_transcript_config.get_domain_separator_label().size() > 0) - << "Domain separator label must be set"; std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 1d46ae7627..a085710d44 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -1,13 +1,5 @@ -#include "icicle/errors.h" #include "icicle/fri/fri.h" -#include "icicle/backend/fri_backend.h" -#include "icicle/fields/stark_fields/babybear.h" -#include "icicle/merkle/merkle_tree.h" #include "icicle/dispatcher.h" -#include "icicle/hash/hash.h" -#include "icicle/utils/log.h" -#include -#include namespace icicle { @@ -37,11 +29,13 @@ namespace icicle { const size_t input_size, const size_t folding_factor, const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; + if(folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; + } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; @@ -51,7 +45,9 @@ namespace icicle { merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; + if(compress_hash_arity != 2){ + ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; + } size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -99,11 +95,13 @@ namespace icicle { const size_t input_size, const size_t folding_factor, const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - ICICLE_ASSERT(folding_factor == 2) << " Currently only folding factor of 2 is supported"; + if(folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; + } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; @@ -113,7 +111,9 @@ namespace icicle { merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - ICICLE_ASSERT(compress_hash_arity == 2) << " Currently only compress hash arity of 2 is supported"; + if(compress_hash_arity != 2){ + ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; + } size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -141,8 +141,8 @@ namespace icicle { const size_t input_size, const size_t folding_factor, const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { return create_fri_template( @@ -170,8 +170,8 @@ namespace icicle { const size_t input_size, const size_t folding_factor, const size_t stopping_degree, - const Hash& merkle_tree_leaves_hash, - const Hash& merkle_tree_compress_hash, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { return create_fri_template_ext( diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 5f8001c392..2d0ac0fbdb 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -615,7 +615,7 @@ TYPED_TEST(FieldTest, FriHashAPi) fri_config.pow_bits = pow_bits; FriProof fri_proof; - ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + ICICLE_CHECK(prover_fri.get_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== Fri verifier_fri = create_fri( @@ -680,7 +680,7 @@ TYPED_TEST(FieldTest, FriMerkleTreeAPi) fri_config.pow_bits = pow_bits; FriProof fri_proof; - ICICLE_CHECK(prover_fri.get_fri_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + ICICLE_CHECK(prover_fri.get_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); // ===== Verifier side ====== Fri verifier_fri = create_fri(folding_factor, stopping_degree, merkle_trees); From 627947b0dd4cd609c2bf9ee4959fc7d872fa9096 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 12:22:47 +0200 Subject: [PATCH 095/127] rand_query_indicies function update --- icicle/backend/cpu/include/cpu_fri_backend.h | 6 +----- icicle/include/icicle/fri/fri.h | 8 +------- icicle/include/icicle/fri/fri_proof.h | 13 ++++++------- icicle/include/icicle/fri/fri_transcript.h | 19 ++++++++++++++++--- icicle/include/icicle/utils/rand_gen.h | 15 --------------- icicle/src/fri/fri_c_api.cpp | 6 +++--- 6 files changed, 27 insertions(+), 40 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 4872102064..d211f70011 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -166,11 +166,7 @@ namespace icicle { */ eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - size_t seed = transcript.get_seed_for_query_phase(); - seed_rand_generator(seed); - std::vector query_indices = - rand_size_t_vector(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); - + std::vector query_indices = transcript.rand_query_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { size_t query = query_indices[query_idx]; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++) { diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index d169eca695..69cca64ec7 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -133,13 +133,7 @@ namespace icicle { transcript.set_pow_nonce(fri_proof.get_pow_nonce()); } - // get query indices - size_t seed = transcript.get_seed_for_query_phase(); - seed_rand_generator(seed); - std::vector query_indices = rand_size_t_vector(fri_config.nof_queries, final_poly_size, input_size); - - uint64_t domain_max_size = 0; - uint64_t max_log_size = 0; + std::vector query_indices = transcript.rand_query_indicies(fri_config.nof_queries, final_poly_size, input_size); S primitive_root_inv = S::omega_inv(log_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 310399947f..8b336702eb 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -40,8 +40,8 @@ namespace icicle { m_query_proofs.resize( 2 * nof_queries, std::vector(nof_fri_rounds)); // for each query, we have 2 proofs (for the leaf and its symmetric) - m_final_poly_size = final_poly_size; - m_final_poly = std::make_unique(final_poly_size); + m_final_poly.resize(final_poly_size); + } /** @@ -75,27 +75,26 @@ namespace icicle { * @return Number of FRI rounds. */ size_t get_nof_fri_rounds() const { return m_query_proofs[0].size(); } - /** * @brief Get the final poly size. * * @return final_poly_size. */ - size_t get_final_poly_size() const { return m_final_poly_size; } + size_t get_final_poly_size() const { return m_final_poly.size(); } void set_pow_nonce(uint64_t pow_nonce) { m_pow_nonce = pow_nonce; } uint64_t get_pow_nonce() const { return m_pow_nonce; } // get pointer to the final polynomial - F* get_final_poly() const { return m_final_poly.get(); } + F* get_final_poly() { return m_final_poly.data(); } + const F* get_final_poly() const { return m_final_poly.data(); } private: std::vector> m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf. for each query, we have 2 // proofs (for the leaf in [2*query] and its symmetric in [2*query+1]) - std::unique_ptr m_final_poly; // Final polynomial (constant in canonical FRI) - size_t m_final_poly_size; // Size of the final polynomial + std::vector m_final_poly; // Final polynomial (constant in canonical FRI) uint64_t m_pow_nonce; // Proof-of-work nonce }; diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 1008f5f475..54829223a0 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -75,8 +75,16 @@ namespace icicle { */ void set_pow_nonce(uint32_t pow_nonce) { m_pow_nonce = pow_nonce; } - size_t get_seed_for_query_phase() - { + + /** + * @brief Generates random query indices for the query phase. + * The seed is derived from the current transcript state. + * @param nof_queries Number of query indices to generate. + * @param min Lower limit. + * @param max Upper limit. + * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. + */ + std::vector rand_query_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX){ // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space @@ -88,7 +96,12 @@ namespace icicle { std::vector hash_result(hasher.output_size()); hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); uint64_t seed = bytes_to_uint_64(hash_result); - return seed; + seed_rand_generator(seed); + std::vector vec(nof_queries); + for (size_t i = 0; i < nof_queries; i++) { + vec[i] = rand_size_t(min, max); + } + return vec; } private: diff --git a/icicle/include/icicle/utils/rand_gen.h b/icicle/include/icicle/utils/rand_gen.h index b7122f8fa8..a71bae15de 100644 --- a/icicle/include/icicle/utils/rand_gen.h +++ b/icicle/include/icicle/utils/rand_gen.h @@ -27,19 +27,4 @@ static size_t rand_size_t(size_t min = 0, size_t max = SIZE_MAX) { std::uniform_int_distribution dist(min, max); return dist(rand_generator); -} - -/** - * @brief Generate random unsigned integer in range (inclusive) - * @param min Lower limit. - * @param max Upper limit. - * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. - */ -static std::vector rand_size_t_vector(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX) -{ - std::vector vec(nof_queries); - for (size_t i = 0; i < nof_queries; i++) { - vec[i] = rand_size_t(min, max); - } - return vec; } \ No newline at end of file diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp index 21b49caea5..0be5460848 100644 --- a/icicle/src/fri/fri_c_api.cpp +++ b/icicle/src/fri/fri_c_api.cpp @@ -36,7 +36,7 @@ struct FriTranscriptConfigFFI { * @brief Structure representing creation parameters for the "hash-based" constructor * `create_fri(folding_factor, stopping_degree, Hash&, output_store_min_layer)`. */ -struct FriCreateHashFFI { +struct FriCreateWithHashFFI { size_t input_size; size_t folding_factor; size_t stopping_degree; @@ -64,7 +64,7 @@ struct FriCreateWithTreesFFI { * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. */ FriHandle* CONCAT_EXPAND(FIELD, fri_create)( - const FriCreateHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) + const FriCreateWithHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { ICICLE_LOG_ERROR << "Invalid FRI creation config."; @@ -188,7 +188,7 @@ eIcicleError CONCAT_EXPAND(FIELD, fri_delete)(const FriHandle* fri_handle) #ifdef EXT_FIELD // EXT_FIELD FriHandleExt* CONCAT_EXPAND(FIELD, fri_create_ext)( - const FriCreateHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) + const FriCreateWithHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) { if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { ICICLE_LOG_ERROR << "Invalid FRI creation config."; From 42fa63494d9e84afb557a61d82e28d2b0390d21d Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 12:32:32 +0200 Subject: [PATCH 096/127] Removed fri_c_api.cpp. It will be added back in the next PR --- icicle/cmake/target_editor.cmake | 2 +- icicle/src/fri/fri_c_api.cpp | 308 ------------------------------- 2 files changed, 1 insertion(+), 309 deletions(-) delete mode 100644 icicle/src/fri/fri_c_api.cpp diff --git a/icicle/cmake/target_editor.cmake b/icicle/cmake/target_editor.cmake index 4555e7b4bf..4e2cc08b54 100644 --- a/icicle/cmake/target_editor.cmake +++ b/icicle/cmake/target_editor.cmake @@ -111,7 +111,7 @@ endfunction() function(handle_fri TARGET FEATURE_LIST) if(FRI AND "FRI" IN_LIST FEATURE_LIST) target_compile_definitions(${TARGET} PUBLIC FRI=${FRI}) - target_sources(${TARGET} PRIVATE src/fri/fri.cpp src/fri/fri_c_api.cpp) + target_sources(${TARGET} PRIVATE src/fri/fri.cpp) target_link_libraries(${TARGET} PRIVATE icicle_hash) set(FRI ON CACHE BOOL "Enable FRI feature" FORCE) else() diff --git a/icicle/src/fri/fri_c_api.cpp b/icicle/src/fri/fri_c_api.cpp deleted file mode 100644 index 0be5460848..0000000000 --- a/icicle/src/fri/fri_c_api.cpp +++ /dev/null @@ -1,308 +0,0 @@ -#include "icicle/fields/field_config.h" -#include "icicle/utils/log.h" -#include "icicle/utils/utils.h" -#include "icicle/fri/fri.h" -#include "icicle/fri/fri_transcript_config.h" -#include - -using namespace field_config; - -extern "C" { - -// Define the FRI handle type -typedef icicle::Fri FriHandle; - -#ifdef EXT_FIELD -typedef icicle::Fri FriHandleExt; -#endif - -// Structure to represent the FFI transcript configuration. -struct FriTranscriptConfigFFI { - Hash* hasher; - std::byte* domain_separator_label; - size_t domain_separator_label_len; - std::byte* round_challenge_label; - size_t round_challenge_label_len; - std::byte* commit_label; - size_t commit_label_len; - std::byte* nonce_label; - size_t nonce_label_len; - std::byte* public_state; - size_t public_state_len; - const scalar_t* seed_rng; -}; - -/** - * @brief Structure representing creation parameters for the "hash-based" constructor - * `create_fri(folding_factor, stopping_degree, Hash&, output_store_min_layer)`. - */ -struct FriCreateWithHashFFI { - size_t input_size; - size_t folding_factor; - size_t stopping_degree; - Hash* merkle_tree_leaves_hash; - Hash* merkle_tree_compress_hash; - uint64_t output_store_min_layer; -}; - -/** - * @brief Structure representing creation parameters for the "existing Merkle trees" constructor - * `create_fri(folding_factor, stopping_degree, vector&&)`. - */ -struct FriCreateWithTreesFFI { - size_t folding_factor; - size_t stopping_degree; - MerkleTree* merkle_trees; // An array of MerkleTree* (pointers). - size_t merkle_trees_count; // Number of items in merkle_trees. -}; - -/** - * @brief Creates a new FRI instance from the given FFI transcript configuration - * and creation parameters (folding_factor, stopping_degree, hash, etc.). - * @param create_config Pointer to the creation parameters (FriCreateConfigFFI). - * @param transcript_config Pointer to the FFI transcript configuration structure. - * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. - */ -FriHandle* CONCAT_EXPAND(FIELD, fri_create)( - const FriCreateWithHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) -{ - if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { - ICICLE_LOG_ERROR << "Invalid FRI creation config."; - return nullptr; - } - if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { - ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; - return nullptr; - } - - ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (hash-based)"; - - // Convert byte arrays to vectors - // TODO SHANIE - check if this is the correct way - std::vector domain_separator_label( - ffi_transcript_config->domain_separator_label, - ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); - std::vector round_challenge_label( - ffi_transcript_config->round_challenge_label, - ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); - std::vector commit_label( - ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); - std::vector nonce_label( - ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); - std::vector public_state( - ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - - // Construct a FriTranscriptConfig - FriTranscriptConfig config{ - *(ffi_transcript_config->hasher), - std::move(domain_separator_label), - std::move(round_challenge_label), - std::move(commit_label), - std::move(nonce_label), - std::move(public_state), - *(ffi_transcript_config->seed_rng)}; - - // Create and return the Fri instance - return new icicle::Fri(icicle::create_fri( - create_config->input_size, create_config->folding_factor, create_config->stopping_degree, - *(create_config->merkle_tree_leaves_hash), *(create_config->merkle_tree_compress_hash), - create_config->output_store_min_layer)); -} - -// fri_create_with_trees - Using vector&& constructor - -/** - * @brief Creates a new FRI instance using the vector&& constructor. - * @param create_config Pointer to a FriCreateWithTreesFFI struct with the necessary parameters. - * @param transcript_config Pointer to the FFI transcript configuration structure. - * @return Pointer to the created FRI instance (FriHandle*), or nullptr on error. - */ -FriHandle* CONCAT_EXPAND(FIELD, fri_create_with_trees)( - const FriCreateWithTreesFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) -{ - if (!create_config || !create_config->merkle_trees) { - ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; - return nullptr; - } - if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { - ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; - return nullptr; - } - - ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (with existing trees)"; - - // Convert the raw array of MerkleTree* into a std::vector - std::vector merkle_trees_vec; - merkle_trees_vec.reserve(create_config->merkle_trees_count); - for (size_t i = 0; i < create_config->merkle_trees_count; ++i) { - merkle_trees_vec.push_back(create_config->merkle_trees[i]); - } - - // Convert byte arrays to vectors - // TODO SHANIE - check if this is the correct way - std::vector domain_separator_label( - ffi_transcript_config->domain_separator_label, - ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); - std::vector round_challenge_label( - ffi_transcript_config->round_challenge_label, - ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); - std::vector commit_label( - ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); - std::vector nonce_label( - ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); - std::vector public_state( - ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - - // Construct a FriTranscriptConfig - FriTranscriptConfig config{ - *(ffi_transcript_config->hasher), - std::move(domain_separator_label), - std::move(round_challenge_label), - std::move(commit_label), - std::move(nonce_label), - std::move(public_state), - *(ffi_transcript_config->seed_rng)}; - - // Create and return the Fri instance - return new icicle::Fri(icicle::create_fri( - create_config->folding_factor, create_config->stopping_degree, merkle_trees_vec)); -} - -/** - * @brief Deletes the given Fri instance. - * @param fri_handle Pointer to the Fri instance to be deleted. - * @return eIcicleError indicating the success or failure of the operation. - */ -eIcicleError CONCAT_EXPAND(FIELD, fri_delete)(const FriHandle* fri_handle) -{ - if (!fri_handle) { - ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; - return eIcicleError::INVALID_ARGUMENT; - } - - ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; - delete fri_handle; - - return eIcicleError::SUCCESS; -} - -#ifdef EXT_FIELD // EXT_FIELD -FriHandleExt* CONCAT_EXPAND(FIELD, fri_create_ext)( - const FriCreateWithHashFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) -{ - if (!create_config || !create_config->merkle_tree_leaves_hash || !create_config->merkle_tree_compress_hash) { - ICICLE_LOG_ERROR << "Invalid FRI creation config."; - return nullptr; - } - if (!ffi_transcript_config || !ffi_transcript_config->hasher || !ffi_transcript_config->seed_rng) { - ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; - return nullptr; - } - - ICICLE_LOG_DEBUG << "Constructing FRI EXT_FIELD instance from FFI (hash-based)"; - - // Convert byte arrays to vectors - std::vector domain_separator_label( - ffi_transcript_config->domain_separator_label, - ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); - std::vector round_challenge_label( - ffi_transcript_config->round_challenge_label, - ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); - std::vector commit_label( - ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); - std::vector nonce_label( - ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); - std::vector public_state( - ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - - // Construct a FriTranscriptConfig - FriTranscriptConfig config{ - *(ffi_transcript_config->hasher), - std::move(domain_separator_label), - std::move(round_challenge_label), - std::move(commit_label), - std::move(nonce_label), - std::move(public_state), - *(ffi_transcript_config->seed_rng)}; - - // Create and return the Fri instance for the extension field - return new icicle::Fri(icicle::create_fri( - create_config->input_size, create_config->folding_factor, create_config->stopping_degree, - *(create_config->merkle_tree_leaves_hash), *(create_config->merkle_tree_compress_hash), - create_config->output_store_min_layer)); -} - -FriHandleExt* CONCAT_EXPAND(FIELD, fri_create_with_trees_ext)( - const FriCreateWithTreesFFI* create_config, const FriTranscriptConfigFFI* ffi_transcript_config) -{ - if (!create_config || !create_config->merkle_trees) { - ICICLE_LOG_ERROR << "Invalid FRI creation config with trees."; - return nullptr; - } - if ( - !ffi_transcript_config || !ffi_transcript_config || !ffi_transcript_config->hasher || - !ffi_transcript_config->seed_rng) { - ICICLE_LOG_ERROR << "Invalid FFI transcript configuration for FRI."; - return nullptr; - } - - ICICLE_LOG_DEBUG << "Constructing FRI instance from FFI (with existing trees)"; - - // Convert the raw array of MerkleTree* into a std::vector - std::vector merkle_trees_vec; - merkle_trees_vec.reserve(create_config->merkle_trees_count); - for (size_t i = 0; i < create_config->merkle_trees_count; ++i) { - merkle_trees_vec.push_back(create_config->merkle_trees[i]); - } - - // Convert byte arrays to vectors - // TODO SHANIE - check if this is the correct way - std::vector domain_separator_label( - ffi_transcript_config->domain_separator_label, - ffi_transcript_config->domain_separator_label + ffi_transcript_config->domain_separator_label_len); - std::vector round_challenge_label( - ffi_transcript_config->round_challenge_label, - ffi_transcript_config->round_challenge_label + ffi_transcript_config->round_challenge_label_len); - std::vector commit_label( - ffi_transcript_config->commit_label, ffi_transcript_config->commit_label + ffi_transcript_config->commit_label_len); - std::vector nonce_label( - ffi_transcript_config->nonce_label, ffi_transcript_config->nonce_label + ffi_transcript_config->nonce_label_len); - std::vector public_state( - ffi_transcript_config->public_state, ffi_transcript_config->public_state + ffi_transcript_config->public_state_len); - - // Construct a FriTranscriptConfig - FriTranscriptConfig config{ - *(ffi_transcript_config->hasher), - std::move(domain_separator_label), - std::move(round_challenge_label), - std::move(commit_label), - std::move(nonce_label), - std::move(public_state), - *(ffi_transcript_config->seed_rng)}; - - // Create and return the Fri instance - return new icicle::Fri(icicle::create_fri( - create_config->folding_factor, create_config->stopping_degree, merkle_trees_vec)); -} - -/** - * @brief Deletes the given Fri instance. - * @param fri_handle_ext Pointer to the Fri instance to be deleted. - * @return eIcicleError indicating the success or failure of the operation. - */ -eIcicleError CONCAT_EXPAND(FIELD, fri_delete_ext)(const FriHandleExt* fri_handle_ext) -{ - if (!fri_handle_ext) { - ICICLE_LOG_ERROR << "Cannot delete a null Fri instance."; - return eIcicleError::INVALID_ARGUMENT; - } - - ICICLE_LOG_DEBUG << "Destructing Fri instance from FFI"; - delete fri_handle_ext; - - return eIcicleError::SUCCESS; -} - -#endif // EXT_FIELD - -} // extern "C" From 1569b2ac1137bc5f09df8619308818179561add0 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 14:32:47 +0200 Subject: [PATCH 097/127] m_first_round removed --- icicle/backend/cpu/include/cpu_fri_backend.h | 3 +- icicle/include/icicle/fri/fri.h | 4 +- icicle/include/icicle/fri/fri_proof.h | 2 +- icicle/include/icicle/fri/fri_transcript.h | 54 +++++++++----------- icicle/tests/test_field_api.cpp | 6 +++ icicle/tests/test_mod_arithmetic_api.h | 6 --- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index d211f70011..8832bf9935 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -46,7 +46,6 @@ namespace icicle { fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); // commit fold phase - // ICICLE_CHECK(commit_fold_phase(input_data, transcript, fri_config, fri_proof)); eIcicleError err = commit_fold_phase(input_data, transcript, fri_config, fri_proof); if(err != eIcicleError::SUCCESS){ return err; @@ -114,7 +113,7 @@ namespace icicle { std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); - F alpha = transcript.get_alpha(merkle_commit); + F alpha = transcript.get_alpha(merkle_commit, round_idx == 0); // Fold the evaluations size_t half = current_size >> 1; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 69cca64ec7..7b9f0480db 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -98,7 +98,7 @@ namespace icicle { */ eIcicleError verify( const FriConfig& fri_config, - const FriTranscriptConfig&& fri_transcript_config, + const FriTranscriptConfig fri_transcript_config, FriProof& fri_proof, bool& valid /* OUT */) const { @@ -123,7 +123,7 @@ namespace icicle { } std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); - alpha_values[round_idx] = transcript.get_alpha(merkle_commit); + alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx==0); } // proof-of-work diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 8b336702eb..1007926849 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -45,7 +45,7 @@ namespace icicle { } /** - * @brief Get a reference to a specific Merkle proof. + * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. * * @param query_idx Index of the query. * @param round_idx Index of the round (FRI round). diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 54829223a0..7ce2e164b2 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -16,13 +16,12 @@ namespace icicle { { public: FriTranscript(const FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) - : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), m_first_round(true), + : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), m_pow_nonce(0) { m_entry_0.clear(); m_entry_0.reserve(1024); // pre-allocate some space build_entry_0(log_input_size); - m_first_round = true; } /** @@ -31,24 +30,23 @@ namespace icicle { * @param merkle_commit The raw bytes of the Merkle commit. * @return A field element alpha derived via Fiat-Shamir. */ - F get_alpha(const std::vector& merkle_commit) + F get_alpha(const std::vector& merkle_commit, bool is_first_round) { std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space // Build the round's hash input - if (m_first_round) { - build_hash_input_round_0(hash_input); - m_first_round = false; + if (is_first_round) { + build_hash_input_round_0(hash_input, merkle_commit); } else { - build_hash_input_round_i(hash_input); + build_hash_input_round_i(hash_input, merkle_commit); } - append_data(hash_input, merkle_commit); // Hash the input and return alpha const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + const HashConfig hash_config; + hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); return m_prev_alpha; } @@ -64,7 +62,8 @@ namespace icicle { const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + const HashConfig hash_config; + hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); return count_leading_zero_bits(hash_result); } @@ -94,7 +93,8 @@ namespace icicle { const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); - hasher.hash(hash_input.data(), hash_input.size(), m_hash_config, hash_result.data()); + const HashConfig hash_config; + hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); uint64_t seed = bytes_to_uint_64(hash_result); seed_rand_generator(seed); std::vector vec(nof_queries); @@ -105,11 +105,9 @@ namespace icicle { } private: - const FriTranscriptConfig&& m_transcript_config; // Transcript configuration (labels, seeds, etc.) - const HashConfig m_hash_config; // hash config - default - bool m_first_round; // Indicates if this is the first round + const FriTranscriptConfig&& m_transcript_config; std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds - F m_prev_alpha; // The previous alpha generated + F m_prev_alpha; uint64_t m_pow_nonce; // Proof-of-work nonce - optional /** @@ -123,20 +121,16 @@ namespace icicle { } /** - * @brief Append an unsigned 64-bit integer to the byte vector (little-endian). + * @brief Append an integral value to the byte vector (little-endian). + * @tparam T Type of the value. * @param dest (OUT) Destination byte vector. * @param value The 64-bit value to append. */ - void append_u32(std::vector& dest, uint32_t value) + template + void append_value(std::vector& dest, T value) { const std::byte* data_bytes = reinterpret_cast(&value); - dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint32_t)); - } - - void append_u64(std::vector& dest, uint64_t value) - { - const std::byte* data_bytes = reinterpret_cast(&value); - dest.insert(dest.end(), data_bytes, data_bytes + sizeof(uint64_t)); + dest.insert(dest.end(), data_bytes, data_bytes + sizeof(T)); } /** @@ -160,7 +154,7 @@ namespace icicle { void build_entry_0(uint32_t log_input_size) { append_data(m_entry_0, m_transcript_config.get_domain_separator_label()); - append_u32(m_entry_0, log_input_size); + append_value(m_entry_0, log_input_size); append_data(m_entry_0, m_transcript_config.get_public_state()); } @@ -172,12 +166,13 @@ namespace icicle { * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ - void build_hash_input_round_0(std::vector& hash_input) + void build_hash_input_round_0(std::vector& hash_input, const std::vector& merkle_commit) { append_data(hash_input, m_entry_0); append_field(hash_input, m_transcript_config.get_seed_rng()); append_data(hash_input, m_transcript_config.get_round_challenge_label()); append_data(hash_input, m_transcript_config.get_commit_phase_label()); + append_data(hash_input, merkle_commit); } /** @@ -188,12 +183,13 @@ namespace icicle { * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ - void build_hash_input_round_i(std::vector& hash_input) + void build_hash_input_round_i(std::vector& hash_input, const std::vector& merkle_commit) { append_data(hash_input, m_entry_0); append_field(hash_input, m_prev_alpha); append_data(hash_input, m_transcript_config.get_round_challenge_label()); append_data(hash_input, m_transcript_config.get_commit_phase_label()); + append_data(hash_input, merkle_commit); } /** @@ -207,7 +203,7 @@ namespace icicle { append_data(hash_input, m_entry_0); append_field(hash_input, m_prev_alpha); append_data(hash_input, m_transcript_config.get_nonce_label()); - append_u64(hash_input, temp_pow_nonce); + append_value(hash_input, temp_pow_nonce); } /** @@ -224,7 +220,7 @@ namespace icicle { } else { append_data(hash_input, m_entry_0); append_data(hash_input, m_transcript_config.get_nonce_label()); - append_u32(hash_input, m_pow_nonce); + append_value(hash_input, m_pow_nonce); } } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 2d0ac0fbdb..23fe93f6c2 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1,5 +1,11 @@ #include "test_mod_arithmetic_api.h" +#include "icicle/sumcheck/sumcheck.h" +#include "icicle/fri/fri.h" +#include "icicle/fri/fri_config.h" +#include "icicle/fri/fri_proof.h" +#include "icicle/fri/fri_transcript_config.h" + // Derive all ModArith tests and add ring specific tests here template diff --git a/icicle/tests/test_mod_arithmetic_api.h b/icicle/tests/test_mod_arithmetic_api.h index 5a9f98419f..a91e462d7c 100644 --- a/icicle/tests/test_mod_arithmetic_api.h +++ b/icicle/tests/test_mod_arithmetic_api.h @@ -15,12 +15,6 @@ #include "icicle/program/program.h" #include "icicle/program/returning_value_program.h" #include "../backend/cpu/include/cpu_program_executor.h" -#include "icicle/sumcheck/sumcheck.h" - -#include "icicle/fri/fri.h" -#include "icicle/fri/fri_config.h" -#include "icicle/fri/fri_proof.h" -#include "icicle/fri/fri_transcript_config.h" #include "test_base.h" From 02a39f5b06221c854afab2f9bd00ce7722ed0c1f Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 18:13:22 +0200 Subject: [PATCH 098/127] refactored verify method with smaller calls --- icicle/backend/cpu/include/cpu_fri_backend.h | 4 +- icicle/include/icicle/fri/fri.h | 257 +++++++++++++------ icicle/include/icicle/fri/fri_transcript.h | 2 +- 3 files changed, 181 insertions(+), 82 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 8832bf9935..15fde4ac4c 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -165,9 +165,9 @@ namespace icicle { */ eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - std::vector query_indices = transcript.rand_query_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); + std::vector queries_indicies = transcript.rand_queries_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { - size_t query = query_indices[query_idx]; + size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++) { size_t round_size = (1ULL << (m_log_input_size - round_idx)); size_t leaf_idx = query % round_size; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 7b9f0480db..78ade6de94 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -102,7 +102,6 @@ namespace icicle { FriProof& fri_proof, bool& valid /* OUT */) const { - valid = false; if(__builtin_expect(fri_config.nof_queries <= 0, 0)){ ICICLE_LOG_ERROR << "Number of queries must be > 0"; } @@ -110,12 +109,39 @@ namespace icicle { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - const size_t input_size = 1 << (log_input_size); + + FriTranscript transcript(std::move(fri_transcript_config), log_input_size); std::vector alpha_values(nof_fri_rounds); + update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); + + // Validate proof-of-work + if (fri_config.pow_bits != 0) { + bool pow_valid = false; + check_pow_nonce_and_set_to_transcript(fri_proof, transcript, fri_config, pow_valid); + if (!pow_valid) return eIcicleError::SUCCESS; // return with valid = false + } + + //verify queries + bool queries_valid = false; + std::vector queries_indicies = transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size); + eIcicleError err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); + if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false + + valid = true; + return err; + } + + private: + std::shared_ptr> m_backend; - // set up the transcript - FriTranscript transcript( - std::move(const_cast&>(fri_transcript_config)), log_input_size); + /** + * @brief Updates the transcript with Merkle roots and generates alpha values for each round. + * @param fri_proof The proof object containing Merkle roots. + * @param transcript The transcript storing challenges. + * @param nof_fri_rounds Number of FRI rounds. + * @param alpha_values (OUT) Vector to store computed alpha values. + */ + void update_transcript_and_generate_alphas_from_proof(FriProof& fri_proof, FriTranscript& transcript, const size_t nof_fri_rounds, std::vector& alpha_values) const{ for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { auto [root_ptr, root_size] = fri_proof.get_merkle_tree_root(round_idx); if (root_ptr == nullptr || root_size <= 0){ @@ -125,98 +151,171 @@ namespace icicle { std::memcpy(merkle_commit.data(), root_ptr, root_size); alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx==0); } + } - // proof-of-work - if (fri_config.pow_bits != 0) { - bool valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); - if (!valid) return eIcicleError::SUCCESS; // return with valid = false + /** + * @brief Validates the proof-of-work nonce from the fri_proof and sets it in the transcript. + * @param fri_proof The proof containing the PoW nonce. + * @param transcript The transcript where the nonce is recorded. + * @param fri_config Configuration specifying required PoW bits. + * @param pow_valid (OUT) Set to true if PoW verification succeeds. + */ + void check_pow_nonce_and_set_to_transcript(FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const{ + pow_valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); + if (pow_valid) { transcript.set_pow_nonce(fri_proof.get_pow_nonce()); } + } - std::vector query_indices = transcript.rand_query_indicies(fri_config.nof_queries, final_poly_size, input_size); - S primitive_root_inv = S::omega_inv(log_input_size); + /** + * @brief Checks if the leaf index from the proof matches the expected index computed based on the transcript random generation. + * @param leaf_index Index extracted from the proof. + * @param leaf_index_sym Symmetric index extracted from the proof. + * @param query The query based on the transcript random generation. + * @param round_idx Current FRI round index. + * @param log_input_size Log of the initial input size. + * @return True if indices are consistent, false otherwise. + */ + bool leaf_index_consistency_check(const uint64_t leaf_index, const uint64_t leaf_index_sym, const size_t query, const size_t round_idx, const uint32_t log_input_size) const { + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; + if(__builtin_expect(elem_idx != leaf_index, 0)){ + ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; + return false; + } + if(__builtin_expect(elem_idx_sym != leaf_index_sym, 0)){ + ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; + return false; + } + return true; + } - for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { - size_t query = query_indices[query_idx]; - size_t current_log_size = log_input_size; - for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - size_t round_size = (1ULL << (log_input_size - round_idx)); - size_t elem_idx = query % round_size; - size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; + /** + * @brief Validates collinearity in the folding process for a specific round. + * This ensures that the folded value computed from the queried elements + * matches the expected value in the proof. + * @param fri_proof The proof object containing leaf data. + * @param leaf_data Pointer to the leaf data. + * @param leaf_data_sym Pointer to the symmetric leaf data. + * @param query_idx Index of the query being verified. + * @param query The query based on the transcript random generation. + * @param round_idx Current FRI round index. + * @param alpha_values Vector of alpha values for each round. + * @param log_input_size Log of the initial input size. + * @param primitive_root_inv Inverse primitive root used in calculations. + * @return True if the collinearity check passes, false otherwise. + */ + bool collinearity_check(FriProof& fri_proof, const std::byte* leaf_data, const std::byte* leaf_data_sym, const size_t query_idx, const size_t query, const size_t round_idx, std::vector& alpha_values, const uint32_t log_input_size, const S primitive_root_inv) const { + const F& leaf_data_f = *reinterpret_cast(leaf_data); + const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); + F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * + S::pow(primitive_root_inv, elem_idx * (1<& fri_proof, const size_t nof_queries, std::vector& queries_indicies, std::vector& alpha_values, bool& queries_valid) const{ + const uint32_t log_input_size = fri_proof.get_nof_fri_rounds() + static_cast(std::log2(fri_proof.get_final_poly_size())); + S primitive_root_inv = S::omega_inv(log_input_size); + for (size_t query_idx = 0; query_idx < nof_queries; query_idx++) { + size_t query = queries_indicies[query_idx]; + for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); - bool valid = false; - eIcicleError err = current_round_tree.verify(proof_ref, valid); - if (err != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification returned err for query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return err; - } - if (!valid) { - ICICLE_LOG_ERROR << "[VERIFIER] Merkle path verification failed for leaf query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with valid = false - } - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); - valid = false; - eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, valid); - if (err_sym != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "Merkle path verification returned err for query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return err_sym; - } - if (!valid) { - ICICLE_LOG_ERROR << "Merkle path verification failed for leaf query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with valid = false - } - - // collinearity check const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); - if(__builtin_expect(elem_idx != leaf_index, 0)){ - ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; - } - if(__builtin_expect(elem_idx_sym != leaf_index_sym, 0)){ - ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; + + if(!verify_merkle_proofs_for_query(current_round_tree, proof_ref, proof_ref_sym)){ + return eIcicleError::SUCCESS; // return with queries_valid = false + } + + if(!leaf_index_consistency_check(leaf_index, leaf_index_sym, query, round_idx, log_input_size)){ + return eIcicleError::SUCCESS; // return with queries_valid = false } - const F& leaf_data_f = *reinterpret_cast(leaf_data); - const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); - F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); - F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * - S::pow(primitive_root_inv, leaf_index * (input_size >> current_log_size)); - F alpha = alpha_values[round_idx]; - F folded = l_even + (alpha * l_odd); - - if (round_idx == nof_fri_rounds - 1) { - const F* final_poly = fri_proof.get_final_poly(); - if (final_poly[query % final_poly_size] != folded) { - ICICLE_LOG_ERROR << "[VERIFIER] (last round) Collinearity check failed for query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return eIcicleError::SUCCESS; // return with valid = false; - } - } else { - MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2 * query_idx, round_idx + 1); - const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); - const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); - if (leaf_data_folded_f != folded) { - ICICLE_LOG_ERROR << "[VERIFIER] Collinearity check failed. query=" << query << ", query_idx=" << query_idx - << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded - << "\nfolded_from_proof = \t" << leaf_data_folded_f; - return eIcicleError::SUCCESS; // return with valid = false - } + + if (!collinearity_check(fri_proof, leaf_data, leaf_data_sym, query_idx, query, round_idx, alpha_values, log_input_size, primitive_root_inv)){ + return eIcicleError::SUCCESS; // return with queries_valid = false } - current_log_size--; + } } - valid = true; + queries_valid = true; return eIcicleError::SUCCESS; } - private: - std::shared_ptr> m_backend; }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 7ce2e164b2..ffd28560c0 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -83,7 +83,7 @@ namespace icicle { * @param max Upper limit. * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. */ - std::vector rand_query_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX){ + std::vector rand_queries_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX){ // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space From 7205dbe17d8837bcd61a59e21589b90e33a5e2d2 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 22:24:17 +0200 Subject: [PATCH 099/127] Pass fri_transcript_config by reference, and test for non-default config --- icicle/backend/cpu/include/cpu_fri_backend.h | 4 ++-- icicle/include/icicle/backend/fri_backend.h | 2 +- icicle/include/icicle/fri/fri.h | 8 ++++---- icicle/include/icicle/fri/fri_transcript.h | 6 +++--- icicle/tests/test_field_api.cpp | 15 ++++++++++++--- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 15fde4ac4c..32cdfae358 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -32,7 +32,7 @@ namespace icicle { eIcicleError get_proof( const FriConfig& fri_config, - const FriTranscriptConfig&& fri_transcript_config, + const FriTranscriptConfig& fri_transcript_config, const F* input_data, FriProof& fri_proof /*out*/) override { @@ -40,7 +40,7 @@ namespace icicle { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } - FriTranscript transcript(std::move(fri_transcript_config), m_log_input_size); + FriTranscript transcript(fri_transcript_config, m_log_input_size); // Initialize the proof fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index ad8f92a8da..eac44c76b8 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -48,7 +48,7 @@ namespace icicle { */ virtual eIcicleError get_proof( const FriConfig& fri_config, - const FriTranscriptConfig&& fri_transcript_config, + const FriTranscriptConfig& fri_transcript_config, const F* input_data, FriProof& fri_proof) = 0; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 78ade6de94..09b4fce1bc 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -81,11 +81,11 @@ namespace icicle { */ eIcicleError get_proof( const FriConfig& fri_config, - const FriTranscriptConfig&& fri_transcript_config, + const FriTranscriptConfig& fri_transcript_config, const F* input_data, FriProof& fri_proof /* OUT */) const { - return m_backend->get_proof(fri_config, std::move(fri_transcript_config), input_data, fri_proof); + return m_backend->get_proof(fri_config, fri_transcript_config, input_data, fri_proof); } /** @@ -98,7 +98,7 @@ namespace icicle { */ eIcicleError verify( const FriConfig& fri_config, - const FriTranscriptConfig fri_transcript_config, + const FriTranscriptConfig& fri_transcript_config, FriProof& fri_proof, bool& valid /* OUT */) const { @@ -110,7 +110,7 @@ namespace icicle { const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - FriTranscript transcript(std::move(fri_transcript_config), log_input_size); + FriTranscript transcript(fri_transcript_config, log_input_size); std::vector alpha_values(nof_fri_rounds); update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index ffd28560c0..a170e0fa2f 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -15,8 +15,8 @@ namespace icicle { class FriTranscript { public: - FriTranscript(const FriTranscriptConfig&& transcript_config, const uint32_t log_input_size) - : m_transcript_config(std::move(transcript_config)), m_prev_alpha(F::zero()), + FriTranscript(const FriTranscriptConfig& transcript_config, const uint32_t log_input_size) + : m_transcript_config(transcript_config), m_prev_alpha(F::zero()), m_pow_nonce(0) { m_entry_0.clear(); @@ -105,7 +105,7 @@ namespace icicle { } private: - const FriTranscriptConfig&& m_transcript_config; + const FriTranscriptConfig& m_transcript_config; std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds F m_prev_alpha; uint64_t m_pow_nonce; // Proof-of-work nonce - optional diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 23fe93f6c2..5cc9f98cf9 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -615,19 +615,28 @@ TYPED_TEST(FieldTest, FriHashAPi) Fri prover_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - FriTranscriptConfig transcript_config; + // set transcript config + const char* domain_separator_label = "domain_separator_label"; + const char* round_challenge_label = "round_challenge_label"; + const char* commit_phase_label = "commit_phase_label"; + const char* nonce_label = "nonce_label"; + std::vector&& public_state = {}; + TypeParam seed_rng = TypeParam::one(); + + FriTranscriptConfig transcript_config(hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), seed_rng); + FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; FriProof fri_proof; - ICICLE_CHECK(prover_fri.get_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); + ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); // ===== Verifier side ====== Fri verifier_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool valid = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, valid)); + ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, valid)); ASSERT_EQ(true, valid); From f5cefa10b377e2b4bcad0ae58062765476c22c9a Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 23:09:48 +0200 Subject: [PATCH 100/127] FriProof init: Added description and error handling for invalid arguments. FriTranscriptConfig: Set default values to empty. --- icicle/backend/cpu/include/cpu_fri_backend.h | 7 +++++-- icicle/include/icicle/fri/fri_proof.h | 12 ++++++++++-- icicle/include/icicle/fri/fri_transcript_config.h | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 32cdfae358..0b2edc3aa6 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -43,10 +43,13 @@ namespace icicle { FriTranscript transcript(fri_transcript_config, m_log_input_size); // Initialize the proof - fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); + eIcicleError err = fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); + if(err != eIcicleError::SUCCESS){ + return err; + } // commit fold phase - eIcicleError err = commit_fold_phase(input_data, transcript, fri_config, fri_proof); + err = commit_fold_phase(input_data, transcript, fri_config, fri_proof); if(err != eIcicleError::SUCCESS){ return err; } diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 1007926849..08c656b375 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -24,16 +24,23 @@ namespace icicle { FriProof() : m_pow_nonce(0) {} /** - * @brief Initialize the Merkle proofs and final polynomial storage. + * @brief Initialize the Merkle proofs and final polynomial storage for the FRI proof. + * + * A FriProof instance should first be created and then initialized using this method + * before use. The initialization is done with the required parameters when setting up + * the prover and generating the proof. * * @param nof_queries Number of queries in the proof. * @param nof_fri_rounds Number of FRI rounds (rounds). + * @param final_poly_size Size of the final polynomial. + * @return An eIcicleError indicating success or failure. */ - void init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) + eIcicleError init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { if(nof_queries <= 0 || nof_fri_rounds <= 0){ ICICLE_LOG_ERROR << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries << ", nof_fri_rounds = " << nof_fri_rounds; + return eIcicleError::INVALID_ARGUMENT; } // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns @@ -42,6 +49,7 @@ namespace icicle { std::vector(nof_fri_rounds)); // for each query, we have 2 proofs (for the leaf and its symmetric) m_final_poly.resize(final_poly_size); + return eIcicleError::SUCCESS; } /** diff --git a/icicle/include/icicle/fri/fri_transcript_config.h b/icicle/include/icicle/fri/fri_transcript_config.h index d79582e621..aedb6e2a3f 100644 --- a/icicle/include/icicle/fri/fri_transcript_config.h +++ b/icicle/include/icicle/fri/fri_transcript_config.h @@ -19,9 +19,9 @@ namespace icicle { public: // Default Constructor FriTranscriptConfig() - : m_hasher(create_keccak_256_hash()), m_domain_separator_label(cstr_to_bytes("ds")), - m_commit_phase_label(cstr_to_bytes("commit")), m_nonce_label(cstr_to_bytes("nonce")), - m_public(cstr_to_bytes("public")), m_seed_rng(F::zero()) + : m_hasher(create_keccak_256_hash()), m_domain_separator_label({}), + m_commit_phase_label({}), m_nonce_label({}), + m_public({}), m_seed_rng(F::zero()) { } From 755b0f51fa43619947ebf7dfdc0d28c421dc94c4 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 4 Mar 2025 23:13:10 +0200 Subject: [PATCH 101/127] format --- icicle/backend/cpu/include/cpu_fri_backend.h | 44 ++++---- icicle/backend/cpu/include/cpu_fri_rounds.h | 4 +- .../include/icicle/fields/complex_extension.h | 4 +- .../include/icicle/fields/quartic_extension.h | 4 +- icicle/include/icicle/fri/fri.h | 100 ++++++++++++------ icicle/include/icicle/fri/fri_proof.h | 10 +- icicle/include/icicle/fri/fri_transcript.h | 9 +- .../icicle/fri/fri_transcript_config.h | 3 +- icicle/src/fri/fri.cpp | 16 +-- icicle/tests/test_field_api.cpp | 27 +++-- 10 files changed, 115 insertions(+), 106 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 0b2edc3aa6..f8ef5952d7 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,7 +24,8 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees, tree per FRI round. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree, std::move(merkle_trees)), m_nof_fri_rounds(this->m_merkle_trees.size()), + : FriBackend(folding_factor, stopping_degree, std::move(merkle_trees)), + m_nof_fri_rounds(this->m_merkle_trees.size()), m_log_input_size(this->m_merkle_trees.size() + std::log2(static_cast(stopping_degree + 1))), m_input_size(pow(2, m_log_input_size)), m_fri_rounds(this->m_merkle_trees, m_log_input_size) { @@ -36,35 +37,27 @@ namespace icicle { const F* input_data, FriProof& fri_proof /*out*/) override { - if(__builtin_expect(fri_config.nof_queries <= 0, 0)){ - ICICLE_LOG_ERROR << "Number of queries must be > 0"; - } + if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } FriTranscript transcript(fri_transcript_config, m_log_input_size); // Initialize the proof eIcicleError err = fri_proof.init(fri_config.nof_queries, m_nof_fri_rounds, this->m_stopping_degree + 1); - if(err != eIcicleError::SUCCESS){ - return err; - } + if (err != eIcicleError::SUCCESS) { return err; } // commit fold phase err = commit_fold_phase(input_data, transcript, fri_config, fri_proof); - if(err != eIcicleError::SUCCESS){ - return err; - } - + if (err != eIcicleError::SUCCESS) { return err; } + // proof of work if (fri_config.pow_bits != 0) { err = proof_of_work(transcript, fri_config.pow_bits, fri_proof); - if(err != eIcicleError::SUCCESS){ - return err; - } + if (err != eIcicleError::SUCCESS) { return err; } } - + // query phase err = query_phase(transcript, fri_config, fri_proof); - + return err; } @@ -85,18 +78,20 @@ namespace icicle { eIcicleError commit_fold_phase( const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - if (this->m_folding_factor != 2){ - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding + if (this->m_folding_factor != 2) { + ICICLE_LOG_ERROR + << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding // factors } const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); - if (m_input_size > domain_max_size){ - ICICLE_LOG_ERROR << "Size is too large for domain. size = " << m_input_size << ", domain_max_size = " << domain_max_size; + if (m_input_size > domain_max_size) { + ICICLE_LOG_ERROR << "Size is too large for domain. size = " << m_input_size + << ", domain_max_size = " << domain_max_size; } - // Retrieve pre-allocated memory for the round from m_fri_rounds. - // The instance of FriRounds has already allocated a vector for each round with + // Retrieve pre-allocated memory for the round from m_fri_rounds. + // The instance of FriRounds has already allocated a vector for each round with // a capacity of 2^(m_log_input_size - round_idx). F* round_evals = m_fri_rounds.get_round_evals(0); std::copy(input_data, input_data + m_input_size, round_evals); @@ -109,7 +104,7 @@ namespace icicle { MerkleTree* current_round_tree = m_fri_rounds.get_merkle_tree(round_idx); current_round_tree->build(round_evals, current_size, MerkleTreeConfig()); auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); - if (root_ptr == nullptr || root_size <= 0){ + if (root_ptr == nullptr || root_size <= 0) { ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; } // Add root to transcript and get alpha @@ -168,7 +163,8 @@ namespace icicle { */ eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - std::vector queries_indicies = transcript.rand_queries_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); + std::vector queries_indicies = + transcript.rand_queries_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++) { diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index fee2d475b3..4df4a7fd46 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -28,7 +28,7 @@ namespace icicle { */ MerkleTree* get_merkle_tree(size_t round_idx) { - if(round_idx >= m_merkle_trees.size()){ + if (round_idx >= m_merkle_trees.size()) { ICICLE_LOG_ERROR << "round index out of bounds"; return nullptr; } @@ -37,7 +37,7 @@ namespace icicle { F* get_round_evals(size_t round_idx) { - if(round_idx >= m_merkle_trees.size()){ + if (round_idx >= m_merkle_trees.size()) { ICICLE_LOG_ERROR << "round index out of bounds"; return nullptr; } diff --git a/icicle/include/icicle/fields/complex_extension.h b/icicle/include/icicle/fields/complex_extension.h index 42813fad40..a945339272 100644 --- a/icicle/include/icicle/fields/complex_extension.h +++ b/icicle/include/icicle/fields/complex_extension.h @@ -309,9 +309,7 @@ class ComplexExtensionField /* Receives an array of bytes and its size and returns extension field element. */ static constexpr HOST_DEVICE_INLINE ComplexExtensionField from(const std::byte* in, unsigned nof_bytes) { - if(nof_bytes < 2 * sizeof(FF)){ - ICICLE_LOG_ERROR << "Input size is too small"; - } + if (nof_bytes < 2 * sizeof(FF)) { ICICLE_LOG_ERROR << "Input size is too small"; } return ComplexExtensionField{FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF))}; } }; diff --git a/icicle/include/icicle/fields/quartic_extension.h b/icicle/include/icicle/fields/quartic_extension.h index bc19a02afa..60aba79bbc 100644 --- a/icicle/include/icicle/fields/quartic_extension.h +++ b/icicle/include/icicle/fields/quartic_extension.h @@ -267,9 +267,7 @@ class QuarticExtensionField // Receives an array of bytes and its size and returns extension field element. static constexpr HOST_DEVICE_INLINE QuarticExtensionField from(const std::byte* in, unsigned nof_bytes) { - if(nof_bytes < 4 * sizeof(FF)){ - ICICLE_LOG_ERROR << "Input size is too small"; - } + if (nof_bytes < 4 * sizeof(FF)) { ICICLE_LOG_ERROR << "Input size is too small"; } return QuarticExtensionField{ FF::from(in, sizeof(FF)), FF::from(in + sizeof(FF), sizeof(FF)), FF::from(in + 2 * sizeof(FF), sizeof(FF)), FF::from(in + 3 * sizeof(FF), sizeof(FF))}; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 09b4fce1bc..bcdfb38d56 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -102,9 +102,7 @@ namespace icicle { FriProof& fri_proof, bool& valid /* OUT */) const { - if(__builtin_expect(fri_config.nof_queries <= 0, 0)){ - ICICLE_LOG_ERROR << "Number of queries must be > 0"; - } + if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); @@ -120,11 +118,13 @@ namespace icicle { check_pow_nonce_and_set_to_transcript(fri_proof, transcript, fri_config, pow_valid); if (!pow_valid) return eIcicleError::SUCCESS; // return with valid = false } - - //verify queries + + // verify queries bool queries_valid = false; - std::vector queries_indicies = transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size); - eIcicleError err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); + std::vector queries_indicies = + transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size); + eIcicleError err = + verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false valid = true; @@ -141,15 +141,20 @@ namespace icicle { * @param nof_fri_rounds Number of FRI rounds. * @param alpha_values (OUT) Vector to store computed alpha values. */ - void update_transcript_and_generate_alphas_from_proof(FriProof& fri_proof, FriTranscript& transcript, const size_t nof_fri_rounds, std::vector& alpha_values) const{ + void update_transcript_and_generate_alphas_from_proof( + FriProof& fri_proof, + FriTranscript& transcript, + const size_t nof_fri_rounds, + std::vector& alpha_values) const + { for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { auto [root_ptr, root_size] = fri_proof.get_merkle_tree_root(round_idx); - if (root_ptr == nullptr || root_size <= 0){ + if (root_ptr == nullptr || root_size <= 0) { ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; } std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); - alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx==0); + alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0); } } @@ -160,15 +165,16 @@ namespace icicle { * @param fri_config Configuration specifying required PoW bits. * @param pow_valid (OUT) Set to true if PoW verification succeeds. */ - void check_pow_nonce_and_set_to_transcript(FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const{ + void check_pow_nonce_and_set_to_transcript( + FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const + { pow_valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); - if (pow_valid) { - transcript.set_pow_nonce(fri_proof.get_pow_nonce()); - } + if (pow_valid) { transcript.set_pow_nonce(fri_proof.get_pow_nonce()); } } /** - * @brief Checks if the leaf index from the proof matches the expected index computed based on the transcript random generation. + * @brief Checks if the leaf index from the proof matches the expected index computed based on the transcript random + * generation. * @param leaf_index Index extracted from the proof. * @param leaf_index_sym Symmetric index extracted from the proof. * @param query The query based on the transcript random generation. @@ -176,15 +182,21 @@ namespace icicle { * @param log_input_size Log of the initial input size. * @return True if indices are consistent, false otherwise. */ - bool leaf_index_consistency_check(const uint64_t leaf_index, const uint64_t leaf_index_sym, const size_t query, const size_t round_idx, const uint32_t log_input_size) const { + bool leaf_index_consistency_check( + const uint64_t leaf_index, + const uint64_t leaf_index_sym, + const size_t query, + const size_t round_idx, + const uint32_t log_input_size) const + { size_t round_size = (1ULL << (log_input_size - round_idx)); size_t elem_idx = query % round_size; size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; - if(__builtin_expect(elem_idx != leaf_index, 0)){ + if (__builtin_expect(elem_idx != leaf_index, 0)) { ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; return false; } - if(__builtin_expect(elem_idx_sym != leaf_index_sym, 0)){ + if (__builtin_expect(elem_idx_sym != leaf_index_sym, 0)) { ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; return false; } @@ -193,7 +205,7 @@ namespace icicle { /** * @brief Validates collinearity in the folding process for a specific round. - * This ensures that the folded value computed from the queried elements + * This ensures that the folded value computed from the queried elements * matches the expected value in the proof. * @param fri_proof The proof object containing leaf data. * @param leaf_data Pointer to the leaf data. @@ -206,14 +218,24 @@ namespace icicle { * @param primitive_root_inv Inverse primitive root used in calculations. * @return True if the collinearity check passes, false otherwise. */ - bool collinearity_check(FriProof& fri_proof, const std::byte* leaf_data, const std::byte* leaf_data_sym, const size_t query_idx, const size_t query, const size_t round_idx, std::vector& alpha_values, const uint32_t log_input_size, const S primitive_root_inv) const { + bool collinearity_check( + FriProof& fri_proof, + const std::byte* leaf_data, + const std::byte* leaf_data_sym, + const size_t query_idx, + const size_t query, + const size_t round_idx, + std::vector& alpha_values, + const uint32_t log_input_size, + const S primitive_root_inv) const + { const F& leaf_data_f = *reinterpret_cast(leaf_data); const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); size_t round_size = (1ULL << (log_input_size - round_idx)); size_t elem_idx = query % round_size; F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * - S::pow(primitive_root_inv, elem_idx * (1<& fri_proof, const size_t nof_queries, std::vector& queries_indicies, std::vector& alpha_values, bool& queries_valid) const{ - const uint32_t log_input_size = fri_proof.get_nof_fri_rounds() + static_cast(std::log2(fri_proof.get_final_poly_size())); + eIcicleError verify_queries( + FriProof& fri_proof, + const size_t nof_queries, + std::vector& queries_indicies, + std::vector& alpha_values, + bool& queries_valid) const + { + const uint32_t log_input_size = + fri_proof.get_nof_fri_rounds() + static_cast(std::log2(fri_proof.get_final_poly_size())); S primitive_root_inv = S::omega_inv(log_input_size); for (size_t query_idx = 0; query_idx < nof_queries; query_idx++) { size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { - MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); - if(!verify_merkle_proofs_for_query(current_round_tree, proof_ref, proof_ref_sym)){ + if (!verify_merkle_proofs_for_query(current_round_tree, proof_ref, proof_ref_sym)) { return eIcicleError::SUCCESS; // return with queries_valid = false - } + } - if(!leaf_index_consistency_check(leaf_index, leaf_index_sym, query, round_idx, log_input_size)){ + if (!leaf_index_consistency_check(leaf_index, leaf_index_sym, query, round_idx, log_input_size)) { return eIcicleError::SUCCESS; // return with queries_valid = false } - - if (!collinearity_check(fri_proof, leaf_data, leaf_data_sym, query_idx, query, round_idx, alpha_values, log_input_size, primitive_root_inv)){ + + if (!collinearity_check( + fri_proof, leaf_data, leaf_data_sym, query_idx, query, round_idx, alpha_values, log_input_size, + primitive_root_inv)) { return eIcicleError::SUCCESS; // return with queries_valid = false } - } } queries_valid = true; return eIcicleError::SUCCESS; } - }; } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 08c656b375..5dfa0b6d5c 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -26,8 +26,8 @@ namespace icicle { /** * @brief Initialize the Merkle proofs and final polynomial storage for the FRI proof. * - * A FriProof instance should first be created and then initialized using this method - * before use. The initialization is done with the required parameters when setting up + * A FriProof instance should first be created and then initialized using this method + * before use. The initialization is done with the required parameters when setting up * the prover and generating the proof. * * @param nof_queries Number of queries in the proof. @@ -37,9 +37,9 @@ namespace icicle { */ eIcicleError init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { - if(nof_queries <= 0 || nof_fri_rounds <= 0){ + if (nof_queries <= 0 || nof_fri_rounds <= 0) { ICICLE_LOG_ERROR << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries - << ", nof_fri_rounds = " << nof_fri_rounds; + << ", nof_fri_rounds = " << nof_fri_rounds; return eIcicleError::INVALID_ARGUMENT; } @@ -103,7 +103,7 @@ namespace icicle { m_query_proofs; // Matrix of Merkle proofs [query][round] - contains path, root, leaf. for each query, we have 2 // proofs (for the leaf in [2*query] and its symmetric in [2*query+1]) std::vector m_final_poly; // Final polynomial (constant in canonical FRI) - uint64_t m_pow_nonce; // Proof-of-work nonce + uint64_t m_pow_nonce; // Proof-of-work nonce }; } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index a170e0fa2f..6d3296d1ef 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -16,8 +16,7 @@ namespace icicle { { public: FriTranscript(const FriTranscriptConfig& transcript_config, const uint32_t log_input_size) - : m_transcript_config(transcript_config), m_prev_alpha(F::zero()), - m_pow_nonce(0) + : m_transcript_config(transcript_config), m_prev_alpha(F::zero()), m_pow_nonce(0) { m_entry_0.clear(); m_entry_0.reserve(1024); // pre-allocate some space @@ -74,7 +73,6 @@ namespace icicle { */ void set_pow_nonce(uint32_t pow_nonce) { m_pow_nonce = pow_nonce; } - /** * @brief Generates random query indices for the query phase. * The seed is derived from the current transcript state. @@ -83,7 +81,8 @@ namespace icicle { * @param max Upper limit. * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. */ - std::vector rand_queries_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX){ + std::vector rand_queries_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX) + { // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space @@ -108,7 +107,7 @@ namespace icicle { const FriTranscriptConfig& m_transcript_config; std::vector m_entry_0; // Hash input set in the first round and used in all subsequent rounds F m_prev_alpha; - uint64_t m_pow_nonce; // Proof-of-work nonce - optional + uint64_t m_pow_nonce; // Proof-of-work nonce - optional /** * @brief Append a vector of bytes to another vector of bytes. diff --git a/icicle/include/icicle/fri/fri_transcript_config.h b/icicle/include/icicle/fri/fri_transcript_config.h index aedb6e2a3f..2b769d8d77 100644 --- a/icicle/include/icicle/fri/fri_transcript_config.h +++ b/icicle/include/icicle/fri/fri_transcript_config.h @@ -19,8 +19,7 @@ namespace icicle { public: // Default Constructor FriTranscriptConfig() - : m_hasher(create_keccak_256_hash()), m_domain_separator_label({}), - m_commit_phase_label({}), m_nonce_label({}), + : m_hasher(create_keccak_256_hash()), m_domain_separator_label({}), m_commit_phase_label({}), m_nonce_label({}), m_public({}), m_seed_rng(F::zero()) { } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index a085710d44..fbfe0bb87f 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -33,9 +33,7 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - if(folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; - } + if (folding_factor != 2) { ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; @@ -45,9 +43,7 @@ namespace icicle { merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - if(compress_hash_arity != 2){ - ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; - } + if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -99,9 +95,7 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - if(folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; - } + if (folding_factor != 2) { ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; @@ -111,9 +105,7 @@ namespace icicle { merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - if(compress_hash_arity != 2){ - ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; - } + if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 5cc9f98cf9..c7f7ae511a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -6,7 +6,6 @@ #include "icicle/fri/fri_proof.h" #include "icicle/fri/fri_transcript_config.h" - // Derive all ModArith tests and add ring specific tests here template class FieldTest : public ModArithTest @@ -193,8 +192,7 @@ TEST_F(FieldTestBase, Sumcheck) // create sumcheck auto verifier_sumcheck = create_sumcheck(); bool valid = false; - ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); ASSERT_EQ(true, valid); }; @@ -336,8 +334,7 @@ TEST_F(FieldTestBase, SumcheckUserDefinedCombine) // create sumcheck auto verifier_sumcheck = create_sumcheck(); bool valid = false; - ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); ASSERT_EQ(true, valid); }; @@ -501,8 +498,7 @@ TEST_F(FieldTestBase, SumcheckIdentity) // create sumcheck auto verifier_sumcheck = create_sumcheck(); bool valid = false; - ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); ASSERT_EQ(true, valid); }; @@ -566,8 +562,7 @@ TEST_F(FieldTestBase, SumcheckSingleInputProgram) // create sumcheck auto verifier_sumcheck = create_sumcheck(); bool valid = false; - ICICLE_CHECK( - verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); + ICICLE_CHECK(verifier_sumcheck.verify(sumcheck_proof, claimed_sum, std::move(transcript_config), valid)); ASSERT_EQ(true, valid); }; @@ -616,14 +611,16 @@ TYPED_TEST(FieldTest, FriHashAPi) input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); // set transcript config - const char* domain_separator_label = "domain_separator_label"; - const char* round_challenge_label = "round_challenge_label"; - const char* commit_phase_label = "commit_phase_label"; - const char* nonce_label = "nonce_label"; - std::vector&& public_state = {}; + const char* domain_separator_label = "domain_separator_label"; + const char* round_challenge_label = "round_challenge_label"; + const char* commit_phase_label = "commit_phase_label"; + const char* nonce_label = "nonce_label"; + std::vector&& public_state = {}; TypeParam seed_rng = TypeParam::one(); - FriTranscriptConfig transcript_config(hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), seed_rng); + FriTranscriptConfig transcript_config( + hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), + seed_rng); FriConfig fri_config; fri_config.nof_queries = nof_queries; From 9b2a900fa8556e8275b44675ac88d430eb0b0c63 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 5 Mar 2025 00:16:55 +0200 Subject: [PATCH 102/127] revert MerkleTree build method to use sizeof(T) --- icicle/include/icicle/merkle/merkle_tree.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/include/icicle/merkle/merkle_tree.h b/icicle/include/icicle/merkle/merkle_tree.h index 5fae016205..3ec2f29c1e 100644 --- a/icicle/include/icicle/merkle/merkle_tree.h +++ b/icicle/include/icicle/merkle/merkle_tree.h @@ -75,7 +75,7 @@ namespace icicle { inline eIcicleError build(const T* leaves, uint64_t nof_leaves /* number of leaf elements */, const MerkleTreeConfig& config) { - return build(reinterpret_cast(leaves), nof_leaves * m_backend->get_leaf_element_size(), config); + return build(reinterpret_cast(leaves), nof_leaves * sizeof(T), config); } /** From 366f1b8573923e66f8617b4cbf9b9cc5cc895583 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 5 Mar 2025 14:05:00 +0200 Subject: [PATCH 103/127] Removed for this PR: create_fri frontend for the case where merkle_trees are given from the user. --- icicle/include/icicle/fri/fri.h | 11 ---- icicle/include/icicle/fri/fri_transcript.h | 1 - icicle/src/fri/fri.cpp | 33 ------------ icicle/tests/test_field_api.cpp | 63 ---------------------- 4 files changed, 108 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index bcdfb38d56..c60646e447 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -43,17 +43,6 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer = 0); - /** - * @brief Constructor for the case where Merkle trees are already given. - * - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param merkle_trees A reference vector of `MerkleTree` objects. - * @return A `Fri` object built around the chosen backend. - */ - template - Fri create_fri(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees); - /** * @brief Class for performing FRI operations. * diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 6d3296d1ef..6eec9b2b68 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -207,7 +207,6 @@ namespace icicle { /** * @brief Build the hash input for the query phase. - * hash_input = entry_0||alpha_{n-1}||"query"||seed * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index fbfe0bb87f..c22e437429 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -55,16 +55,6 @@ namespace icicle { return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); } - /** - * @brief Specialization of create_fri for the case of - * (folding_factor, stopping_degree, vector&&). - */ - template - Fri create_fri_template(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) - { - return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); - } - #ifdef EXT_FIELD using FriExtFactoryScalar = FriFactoryImpl; ICICLE_DISPATCHER_INST(FriExtFieldDispatcher, extension_fri_factory, FriExtFactoryScalar); @@ -117,15 +107,6 @@ namespace icicle { return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, std::move(merkle_trees)); } - /** - * @brief Specialization of create_fri for the case of - * (folding_factor, stopping_degree, vector&&). - */ - template - Fri create_fri_template_ext(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) - { - return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, std::move(merkle_trees)); - } #endif // EXT_FIELD template <> @@ -142,21 +123,7 @@ namespace icicle { output_store_min_layer); } - template <> - Fri - create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) - { - return create_fri_template(folding_factor, stopping_degree, std::move(merkle_trees)); - } - #ifdef EXT_FIELD - template <> - Fri - create_fri(size_t folding_factor, size_t stopping_degree, std::vector merkle_trees) - { - return create_fri_template_ext(folding_factor, stopping_degree, std::move(merkle_trees)); - } - template <> Fri create_fri( const size_t input_size, diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index c7f7ae511a..d9a67829ee 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -641,69 +641,6 @@ TYPED_TEST(FieldTest, FriHashAPi) ICICLE_CHECK(ntt_release_domain()); } -TYPED_TEST(FieldTest, FriMerkleTreeAPi) -{ - // Randomize configuration - const int log_input_size = rand_uint_32b(3, 13); - const size_t input_size = 1 << log_input_size; - const int folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors - const int log_stopping_size = rand_uint_32b(0, log_input_size - 2); - const size_t stopping_size = 1 << log_stopping_size; - const size_t stopping_degree = stopping_size - 1; - const uint64_t output_store_min_layer = 0; - const size_t pow_bits = rand_uint_32b(0, 3); - const size_t nof_queries = rand_uint_32b(2, 4); - - // Initialize ntt domain - NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); - ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); - - // Generate input polynomial evaluations - auto scalars = std::make_unique(input_size); - TypeParam::rand_host_many(scalars.get(), input_size); - - const size_t df = stopping_degree; - const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; - const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; - - // Define hashers and merkle trees - uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B - Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - - std::vector merkle_trees; - merkle_trees.reserve(fold_rounds); - size_t compress_hash_arity = compress.default_input_chunk_size() / compress.output_size(); - size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; - std::vector layer_hashes(first_merkle_tree_height, compress); - layer_hashes[0] = hash; - uint64_t leaf_element_size = hash.default_input_chunk_size(); - for (size_t i = 0; i < fold_rounds; i++) { - merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); - layer_hashes.pop_back(); - } - - // ===== Prover side ====== - Fri prover_fri = create_fri(folding_factor, stopping_degree, merkle_trees); - - FriTranscriptConfig transcript_config; - FriConfig fri_config; - fri_config.nof_queries = nof_queries; - fri_config.pow_bits = pow_bits; - FriProof fri_proof; - - ICICLE_CHECK(prover_fri.get_proof(fri_config, std::move(transcript_config), scalars.get(), fri_proof)); - - // ===== Verifier side ====== - Fri verifier_fri = create_fri(folding_factor, stopping_degree, merkle_trees); - bool valid = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, std::move(transcript_config), fri_proof, valid)); - - ASSERT_EQ(true, valid); - - // Release domain - ICICLE_CHECK(ntt_release_domain()); -} #endif // FRI // TODO Hadar: this is a workaround for 'storage<18 - scalar_t::TLC>' failing due to 17 limbs not supported. From f092a76b0c5dd08041a5a175e182a145e2942ce4 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 5 Mar 2025 18:50:05 +0200 Subject: [PATCH 104/127] added test for cases where fri should fail. The basic test is now with non-random parameters --- icicle/backend/cpu/include/cpu_fri_backend.h | 25 ++- icicle/include/icicle/fri/fri.h | 16 +- icicle/include/icicle/fri/fri_transcript.h | 8 +- icicle/src/fri/fri.cpp | 2 - icicle/tests/test_field_api.cpp | 198 ++++++++++++++----- 5 files changed, 184 insertions(+), 65 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index f8ef5952d7..3f0644c4f9 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -37,7 +37,15 @@ namespace icicle { const F* input_data, FriProof& fri_proof /*out*/) override { - if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } + if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + return eIcicleError::INVALID_ARGUMENT; + } + if (__builtin_expect(this->m_folding_factor != 2, 0)) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + FriTranscript transcript(fri_transcript_config, m_log_input_size); @@ -78,16 +86,12 @@ namespace icicle { eIcicleError commit_fold_phase( const F* input_data, FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { - if (this->m_folding_factor != 2) { - ICICLE_LOG_ERROR - << "Currently only folding factor of 2 is supported"; // TODO SHANIE - remove when supporting other folding - // factors - } const S* twiddles = ntt_cpu::CpuNttDomain::s_ntt_domain.get_twiddles(); uint64_t domain_max_size = ntt_cpu::CpuNttDomain::s_ntt_domain.get_max_size(); if (m_input_size > domain_max_size) { ICICLE_LOG_ERROR << "Size is too large for domain. size = " << m_input_size << ", domain_max_size = " << domain_max_size; + return eIcicleError::INVALID_ARGUMENT; } // Retrieve pre-allocated memory for the round from m_fri_rounds. @@ -106,12 +110,15 @@ namespace icicle { auto [root_ptr, root_size] = current_round_tree->get_merkle_root(); if (root_ptr == nullptr || root_size <= 0) { ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; + return eIcicleError::UNKNOWN_ERROR; } // Add root to transcript and get alpha std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); - F alpha = transcript.get_alpha(merkle_commit, round_idx == 0); + eIcicleError err; + F alpha = transcript.get_alpha(merkle_commit, round_idx == 0, err); + if (err != eIcicleError::SUCCESS){ return err; } // Fold the evaluations size_t half = current_size >> 1; @@ -163,8 +170,10 @@ namespace icicle { */ eIcicleError query_phase(FriTranscript& transcript, const FriConfig& fri_config, FriProof& fri_proof) { + eIcicleError err; std::vector queries_indicies = - transcript.rand_queries_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size); + transcript.rand_queries_indicies(fri_config.nof_queries, (this->m_stopping_degree + 1), m_input_size, err); + if (err != eIcicleError::SUCCESS) { return err; } for (size_t query_idx = 0; query_idx < fri_config.nof_queries; query_idx++) { size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < m_nof_fri_rounds; round_idx++) { diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index c60646e447..27416f66f6 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -99,7 +99,8 @@ namespace icicle { FriTranscript transcript(fri_transcript_config, log_input_size); std::vector alpha_values(nof_fri_rounds); - update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); + eIcicleError err = update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); + if (err != eIcicleError::SUCCESS){ return err; } // Validate proof-of-work if (fri_config.pow_bits != 0) { @@ -111,9 +112,9 @@ namespace icicle { // verify queries bool queries_valid = false; std::vector queries_indicies = - transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size); - eIcicleError err = - verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); + transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size, err); + if (err != eIcicleError::SUCCESS) { return err; } + err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false valid = true; @@ -130,7 +131,7 @@ namespace icicle { * @param nof_fri_rounds Number of FRI rounds. * @param alpha_values (OUT) Vector to store computed alpha values. */ - void update_transcript_and_generate_alphas_from_proof( + eIcicleError update_transcript_and_generate_alphas_from_proof( FriProof& fri_proof, FriTranscript& transcript, const size_t nof_fri_rounds, @@ -143,8 +144,11 @@ namespace icicle { } std::vector merkle_commit(root_size); std::memcpy(merkle_commit.data(), root_ptr, root_size); - alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0); + eIcicleError err; + alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0, err); + if (err != eIcicleError::SUCCESS){ return err; } } + return eIcicleError::SUCCESS; } /** diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 6eec9b2b68..0697c0fbf3 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -29,7 +29,7 @@ namespace icicle { * @param merkle_commit The raw bytes of the Merkle commit. * @return A field element alpha derived via Fiat-Shamir. */ - F get_alpha(const std::vector& merkle_commit, bool is_first_round) + F get_alpha(const std::vector& merkle_commit, bool is_first_round, eIcicleError& err) { std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space @@ -45,7 +45,7 @@ namespace icicle { const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); const HashConfig hash_config; - hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); + err = hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); m_prev_alpha = F::from(hash_result.data(), hasher.output_size()); return m_prev_alpha; } @@ -81,7 +81,7 @@ namespace icicle { * @param max Upper limit. * @return Random (uniform distribution) unsigned integer s.t. min <= integer <= max. */ - std::vector rand_queries_indicies(size_t nof_queries, size_t min = 0, size_t max = SIZE_MAX) + std::vector rand_queries_indicies(size_t nof_queries, size_t min, size_t max, eIcicleError& err) { // Prepare a buffer for hashing std::vector hash_input; @@ -93,7 +93,7 @@ namespace icicle { const Hash& hasher = m_transcript_config.get_hasher(); std::vector hash_result(hasher.output_size()); const HashConfig hash_config; - hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); + err = hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); uint64_t seed = bytes_to_uint_64(hash_result); seed_rand_generator(seed); std::vector vec(nof_queries); diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index c22e437429..a007ff9261 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -33,7 +33,6 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - if (folding_factor != 2) { ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; @@ -85,7 +84,6 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - if (folding_factor != 2) { ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; } const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index d9a67829ee..f41d3e017a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -579,66 +579,174 @@ TEST_F(FieldTestBase, SumcheckSingleInputProgram) #ifdef FRI -TYPED_TEST(FieldTest, FriHashAPi) +TYPED_TEST(FieldTest, Fri) +{ + size_t log_stopping_size; + size_t pow_bits; + size_t nof_queries; + for (size_t params_options = 0; params_options<=1; params_options++){ + if (params_options){ + log_stopping_size = 0; + pow_bits = 16; + nof_queries = 100; + } else { + log_stopping_size = 8; + pow_bits = 0; + nof_queries = 50; + } + for (size_t log_input_size = 16; log_input_size <=24; log_input_size+=4){ + const size_t input_size = 1 << log_input_size; + const size_t folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors + const size_t stopping_size = 1 << log_stopping_size; + const size_t stopping_degree = stopping_size - 1; + const uint64_t output_store_min_layer = 0; + + // Generate input polynomial evaluations + auto scalars = std::make_unique(input_size); + TypeParam::rand_host_many(scalars.get(), input_size); + + auto run = [log_input_size, input_size, folding_factor, stopping_degree, output_store_min_layer, nof_queries, pow_bits, &scalars]( + const std::string& dev_type + ) { + Device dev = {dev_type, 0}; + icicle_set_device(dev); + + // Initialize ntt domain + NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); + ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); + + // ===== Prover side ====== + uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities + + // Define hashers for merkle tree + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B + Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B + + Fri prover_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + + // set transcript config + const char* domain_separator_label = "domain_separator_label"; + const char* round_challenge_label = "round_challenge_label"; + const char* commit_phase_label = "commit_phase_label"; + const char* nonce_label = "nonce_label"; + std::vector&& public_state = {}; + TypeParam seed_rng = TypeParam::one(); + + FriTranscriptConfig transcript_config( + hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), + seed_rng); + + FriConfig fri_config; + fri_config.nof_queries = nof_queries; + fri_config.pow_bits = pow_bits; + FriProof fri_proof; + + // ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree << ". pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; + // std::ostringstream oss; + // oss << dev_type << " FRI proof"; + // START_TIMER(FRIPROOF_sync) + ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); + // END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); + + // ===== Verifier side ====== + Fri verifier_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + bool valid = false; + ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, valid)); + + ASSERT_EQ(true, valid); + + // Release domain + ICICLE_CHECK(ntt_release_domain()); + }; + + run(IcicleTestBase::reference_device()); + // run(IcicleTestBase::main_device()); + } + } +} + + +TYPED_TEST(FieldTest, FriShouldFailCases) { // Randomize configuration - const int log_input_size = rand_uint_32b(3, 13); + const size_t log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; - const int folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors - const int log_stopping_size = rand_uint_32b(0, log_input_size - 2); + const size_t log_stopping_size = rand_uint_32b(0, log_input_size - 2); const size_t stopping_size = 1 << log_stopping_size; const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; const size_t pow_bits = rand_uint_32b(0, 3); const size_t nof_queries = rand_uint_32b(2, 4); - // Initialize ntt domain - NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); - ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); - // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); TypeParam::rand_host_many(scalars.get(), input_size); + + auto run = [log_input_size, input_size, stopping_degree, output_store_min_layer, pow_bits, &scalars]( + const std::string& dev_type, const size_t nof_queries, const size_t folding_factor, const size_t log_domain_size + ) { + Device dev = {dev_type, 0}; + icicle_set_device(dev); + + // Initialize ntt domain + NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); + ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_domain_size), init_domain_config)); + + // ===== Prover side ====== + uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities + + // Define hashers for merkle tree + Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B + Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B + + Fri prover_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + + // set transcript config + const char* domain_separator_label = "domain_separator_label"; + const char* round_challenge_label = "round_challenge_label"; + const char* commit_phase_label = "commit_phase_label"; + const char* nonce_label = "nonce_label"; + std::vector&& public_state = {}; + TypeParam seed_rng = TypeParam::one(); + + FriTranscriptConfig transcript_config( + hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), + seed_rng); + + FriConfig fri_config; + fri_config.nof_queries = nof_queries; + fri_config.pow_bits = pow_bits; + FriProof fri_proof; - // ===== Prover side ====== - uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - - // Define hashers for merkle tree - Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B - Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - - Fri prover_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - - // set transcript config - const char* domain_separator_label = "domain_separator_label"; - const char* round_challenge_label = "round_challenge_label"; - const char* commit_phase_label = "commit_phase_label"; - const char* nonce_label = "nonce_label"; - std::vector&& public_state = {}; - TypeParam seed_rng = TypeParam::one(); - - FriTranscriptConfig transcript_config( - hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), - seed_rng); - - FriConfig fri_config; - fri_config.nof_queries = nof_queries; - fri_config.pow_bits = pow_bits; - FriProof fri_proof; - - ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); - - // ===== Verifier side ====== - Fri verifier_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - bool valid = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, valid)); + std::ostringstream oss; + oss << dev_type << " FRI proof"; + START_TIMER(FRIPROOF_sync) + eIcicleError error = prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); + END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); + + if (error == eIcicleError::SUCCESS){ + // ===== Verifier side ====== + Fri verifier_fri = create_fri( + input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + bool valid = false; + error = verifier_fri.verify(fri_config, transcript_config, fri_proof, valid); + + ASSERT_EQ(true, valid); + + } + ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); - ASSERT_EQ(true, valid); + // Release domain + ICICLE_CHECK(ntt_release_domain()); + }; - // Release domain - ICICLE_CHECK(ntt_release_domain()); + run(IcicleTestBase::reference_device(), 0/*nof_queries*/, 2/*folding_factor*/, log_input_size/*log_domain_size*/); + run(IcicleTestBase::reference_device(), 10/*nof_queries*/, 16/*folding_factor*/, log_input_size/*log_domain_size*/); + run(IcicleTestBase::reference_device(), 10/*nof_queries*/, 2/*folding_factor*/, log_input_size-1/*log_domain_size*/); + // run(IcicleTestBase::main_device()); } #endif // FRI From 881f0cb10adbcc5341ba723aed4385136a6fa2ef Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 5 Mar 2025 19:29:15 +0200 Subject: [PATCH 105/127] format --- icicle/backend/cpu/include/cpu_fri_backend.h | 8 +-- icicle/include/icicle/fri/fri.h | 9 ++-- icicle/tests/test_field_api.cpp | 57 ++++++++++---------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 3f0644c4f9..5f1ad7daa3 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -40,13 +40,13 @@ namespace icicle { if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; return eIcicleError::INVALID_ARGUMENT; - } + } if (__builtin_expect(this->m_folding_factor != 2, 0)) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when supporting other folding factors + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors return eIcicleError::INVALID_ARGUMENT; } - FriTranscript transcript(fri_transcript_config, m_log_input_size); // Initialize the proof @@ -118,7 +118,7 @@ namespace icicle { eIcicleError err; F alpha = transcript.get_alpha(merkle_commit, round_idx == 0, err); - if (err != eIcicleError::SUCCESS){ return err; } + if (err != eIcicleError::SUCCESS) { return err; } // Fold the evaluations size_t half = current_size >> 1; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 27416f66f6..a412d869d6 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -99,8 +99,9 @@ namespace icicle { FriTranscript transcript(fri_transcript_config, log_input_size); std::vector alpha_values(nof_fri_rounds); - eIcicleError err = update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); - if (err != eIcicleError::SUCCESS){ return err; } + eIcicleError err = + update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); + if (err != eIcicleError::SUCCESS) { return err; } // Validate proof-of-work if (fri_config.pow_bits != 0) { @@ -112,7 +113,7 @@ namespace icicle { // verify queries bool queries_valid = false; std::vector queries_indicies = - transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size, err); + transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size, err); if (err != eIcicleError::SUCCESS) { return err; } err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false @@ -146,7 +147,7 @@ namespace icicle { std::memcpy(merkle_commit.data(), root_ptr, root_size); eIcicleError err; alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0, err); - if (err != eIcicleError::SUCCESS){ return err; } + if (err != eIcicleError::SUCCESS) { return err; } } return eIcicleError::SUCCESS; } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index f41d3e017a..374d41c422 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -584,8 +584,8 @@ TYPED_TEST(FieldTest, Fri) size_t log_stopping_size; size_t pow_bits; size_t nof_queries; - for (size_t params_options = 0; params_options<=1; params_options++){ - if (params_options){ + for (size_t params_options = 0; params_options <= 1; params_options++) { + if (params_options) { log_stopping_size = 0; pow_bits = 16; nof_queries = 100; @@ -594,7 +594,7 @@ TYPED_TEST(FieldTest, Fri) pow_bits = 0; nof_queries = 50; } - for (size_t log_input_size = 16; log_input_size <=24; log_input_size+=4){ + for (size_t log_input_size = 16; log_input_size <= 24; log_input_size += 4) { const size_t input_size = 1 << log_input_size; const size_t folding_factor = 2; // TODO SHANIE (future) - add support for other folding factors const size_t stopping_size = 1 << log_stopping_size; @@ -604,20 +604,19 @@ TYPED_TEST(FieldTest, Fri) // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); TypeParam::rand_host_many(scalars.get(), input_size); - - auto run = [log_input_size, input_size, folding_factor, stopping_degree, output_store_min_layer, nof_queries, pow_bits, &scalars]( - const std::string& dev_type - ) { + + auto run = [log_input_size, input_size, folding_factor, stopping_degree, output_store_min_layer, nof_queries, + pow_bits, &scalars](const std::string& dev_type) { Device dev = {dev_type, 0}; icicle_set_device(dev); - + // Initialize ntt domain NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config)); - + // ===== Prover side ====== uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - + // Define hashers for merkle tree Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B @@ -642,7 +641,8 @@ TYPED_TEST(FieldTest, Fri) fri_config.pow_bits = pow_bits; FriProof fri_proof; - // ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree << ". pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; + // ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree << ". + // pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; // std::ostringstream oss; // oss << dev_type << " FRI proof"; // START_TIMER(FRIPROOF_sync) @@ -667,7 +667,6 @@ TYPED_TEST(FieldTest, Fri) } } - TYPED_TEST(FieldTest, FriShouldFailCases) { // Randomize configuration @@ -683,20 +682,20 @@ TYPED_TEST(FieldTest, FriShouldFailCases) // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); TypeParam::rand_host_many(scalars.get(), input_size); - + auto run = [log_input_size, input_size, stopping_degree, output_store_min_layer, pow_bits, &scalars]( - const std::string& dev_type, const size_t nof_queries, const size_t folding_factor, const size_t log_domain_size - ) { + const std::string& dev_type, const size_t nof_queries, const size_t folding_factor, + const size_t log_domain_size) { Device dev = {dev_type, 0}; icicle_set_device(dev); - + // Initialize ntt domain NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_domain_size), init_domain_config)); - + // ===== Prover side ====== uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - + // Define hashers for merkle tree Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B @@ -721,21 +720,20 @@ TYPED_TEST(FieldTest, FriShouldFailCases) fri_config.pow_bits = pow_bits; FriProof fri_proof; - std::ostringstream oss; - oss << dev_type << " FRI proof"; - START_TIMER(FRIPROOF_sync) + // std::ostringstream oss; + // oss << dev_type << " FRI proof"; + // START_TIMER(FRIPROOF_sync) eIcicleError error = prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); - END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); + // END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); - if (error == eIcicleError::SUCCESS){ + if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== Fri verifier_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool valid = false; error = verifier_fri.verify(fri_config, transcript_config, fri_proof, valid); - + ASSERT_EQ(true, valid); - } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); @@ -743,9 +741,12 @@ TYPED_TEST(FieldTest, FriShouldFailCases) ICICLE_CHECK(ntt_release_domain()); }; - run(IcicleTestBase::reference_device(), 0/*nof_queries*/, 2/*folding_factor*/, log_input_size/*log_domain_size*/); - run(IcicleTestBase::reference_device(), 10/*nof_queries*/, 16/*folding_factor*/, log_input_size/*log_domain_size*/); - run(IcicleTestBase::reference_device(), 10/*nof_queries*/, 2/*folding_factor*/, log_input_size-1/*log_domain_size*/); + run(IcicleTestBase::reference_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); + run( + IcicleTestBase::reference_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); + run( + IcicleTestBase::reference_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, + log_input_size - 1 /*log_domain_size*/); // run(IcicleTestBase::main_device()); } From 20585e6b59a59cb5bc4101d752a9ace5a42a0918 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 5 Mar 2025 19:48:07 +0200 Subject: [PATCH 106/127] minor --- icicle/tests/test_field_api.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 374d41c422..449e49bd2f 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -606,7 +606,7 @@ TYPED_TEST(FieldTest, Fri) TypeParam::rand_host_many(scalars.get(), input_size); auto run = [log_input_size, input_size, folding_factor, stopping_degree, output_store_min_layer, nof_queries, - pow_bits, &scalars](const std::string& dev_type) { + pow_bits, &scalars](const std::string& dev_type, bool measure) { Device dev = {dev_type, 0}; icicle_set_device(dev); @@ -641,13 +641,15 @@ TYPED_TEST(FieldTest, Fri) fri_config.pow_bits = pow_bits; FriProof fri_proof; - // ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree << ". - // pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; - // std::ostringstream oss; - // oss << dev_type << " FRI proof"; - // START_TIMER(FRIPROOF_sync) + std::ostringstream oss; + if (measure) { + ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree + << ".pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; + oss << dev_type << " FRI proof"; + } + START_TIMER(FRIPROOF_sync) ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); - // END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); + END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); // ===== Verifier side ====== Fri verifier_fri = create_fri( @@ -661,8 +663,8 @@ TYPED_TEST(FieldTest, Fri) ICICLE_CHECK(ntt_release_domain()); }; - run(IcicleTestBase::reference_device()); - // run(IcicleTestBase::main_device()); + run(IcicleTestBase::reference_device(), false); + // run(IcicleTestBase::main_device(), , VERBOSE); } } } @@ -720,11 +722,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) fri_config.pow_bits = pow_bits; FriProof fri_proof; - // std::ostringstream oss; - // oss << dev_type << " FRI proof"; - // START_TIMER(FRIPROOF_sync) eIcicleError error = prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); - // END_TIMER(FRIPROOF_sync, oss.str().c_str(), true); if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== From 48da21baeae0e5e0cb465296442d6ce69bca73b3 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Thu, 6 Mar 2025 19:01:30 +0200 Subject: [PATCH 107/127] minor --- icicle/tests/test_field_api.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 449e49bd2f..ebed8107c9 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -644,7 +644,7 @@ TYPED_TEST(FieldTest, Fri) std::ostringstream oss; if (measure) { ICICLE_LOG_INFO << "log_input_size: " << log_input_size << ". stopping_degree: " << stopping_degree - << ".pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; + << ". pow_bits: " << pow_bits << ". nof_queries:" << nof_queries; oss << dev_type << " FRI proof"; } START_TIMER(FRIPROOF_sync) From bd238aab77c106c9cdb4e8b04f11cb0bdded9d30 Mon Sep 17 00:00:00 2001 From: Jeremy Felder Date: Sun, 9 Mar 2025 15:14:32 +0200 Subject: [PATCH 108/127] FRI frontend and cpu changes (#803) --- icicle/backend/cpu/include/cpu_fri_backend.h | 23 ++++--- .../backend/merkle/merkle_tree_backend.h | 4 ++ icicle/include/icicle/fri/fri.h | 5 +- icicle/include/icicle/fri/fri_transcript.h | 62 +++++++++---------- icicle/include/icicle/hash/pow.h | 6 +- icicle/include/icicle/merkle/merkle_tree.h | 2 + icicle/tests/test_field_api.cpp | 23 +++++-- 7 files changed, 73 insertions(+), 52 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 5f1ad7daa3..4454b05616 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -147,17 +147,20 @@ namespace icicle { return eIcicleError::SUCCESS; } - eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof) - { - for (uint64_t nonce = 0; nonce < UINT64_MAX; nonce++) { - if (transcript.hash_and_get_nof_leading_zero_bits(nonce) == pow_bits) { - transcript.set_pow_nonce(nonce); - fri_proof.set_pow_nonce(nonce); - return eIcicleError::SUCCESS; - } + eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ + uint64_t nonce = 0; + bool found = false; + eIcicleError pow_err = transcript.solve_pow(nonce, pow_bits, found); + if (pow_err != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Failed to find a proof-of-work nonce"; + return pow_err; } - ICICLE_LOG_ERROR << "Failed to find a proof-of-work nonce"; - return eIcicleError::UNKNOWN_ERROR; + + ICICLE_ASSERT(found); + + transcript.set_pow_nonce(nonce); + fri_proof.set_pow_nonce(nonce); + return eIcicleError::SUCCESS; } /** diff --git a/icicle/include/icicle/backend/merkle/merkle_tree_backend.h b/icicle/include/icicle/backend/merkle/merkle_tree_backend.h index e6002e3d74..e8061b5160 100644 --- a/icicle/include/icicle/backend/merkle/merkle_tree_backend.h +++ b/icicle/include/icicle/backend/merkle/merkle_tree_backend.h @@ -57,6 +57,10 @@ namespace icicle { */ virtual std::pair get_merkle_root() const = 0; + virtual std::pair get_merkle_root(bool on_device) const { + return this->get_merkle_root(); + }; + /** * @brief Retrieve the Merkle path for a specific element. * @param leaves Pointer to the leaves of the tree. diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index a412d869d6..d3382130d5 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -162,8 +162,9 @@ namespace icicle { void check_pow_nonce_and_set_to_transcript( FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const { - pow_valid = (transcript.hash_and_get_nof_leading_zero_bits(fri_proof.get_pow_nonce()) == fri_config.pow_bits); - if (pow_valid) { transcript.set_pow_nonce(fri_proof.get_pow_nonce()); } + uint64_t proof_pow_nonce = fri_proof.get_pow_nonce(); + pow_valid = transcript.verify_pow(proof_pow_nonce, fri_config.pow_bits); + transcript.set_pow_nonce(proof_pow_nonce); } /** diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 0697c0fbf3..8cb40a01c8 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -8,6 +8,7 @@ #include "icicle/fri/fri_transcript_config.h" #include "icicle/errors.h" #include "icicle/hash/hash.h" +#include "icicle/hash/pow.h" namespace icicle { @@ -50,21 +51,35 @@ namespace icicle { return m_prev_alpha; } - size_t hash_and_get_nof_leading_zero_bits(uint64_t nonce) - { + bool verify_pow(uint64_t nonce, uint8_t pow_bits) { // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space + build_hash_input_pow(hash_input); + const Hash& hasher = m_transcript_config.get_hasher(); + const PowConfig cfg; + uint64_t mined_hash; + bool is_correct; + proof_of_work_verify(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, nonce, is_correct, mined_hash); + ICICLE_LOG_DEBUG << "Verified mined pow hash: " << mined_hash; + ICICLE_LOG_DEBUG << "Verified mined pow hash: 0x" << std::hex << mined_hash; + + return is_correct; + } - // Build the hash input - build_hash_input_pow(hash_input, nonce); - + eIcicleError solve_pow(uint64_t& nonce, size_t pow_bits, bool& found) { + // Prepare a buffer for hashing + std::vector hash_input; + hash_input.reserve(1024); // pre-allocate some space + build_hash_input_pow(hash_input); const Hash& hasher = m_transcript_config.get_hasher(); - std::vector hash_result(hasher.output_size()); - const HashConfig hash_config; - hasher.hash(hash_input.data(), hash_input.size(), hash_config, hash_result.data()); + const PowConfig cfg; + uint64_t mined_hash; + eIcicleError pow_error = proof_of_work(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, found, nonce, mined_hash); - return count_leading_zero_bits(hash_result); + ICICLE_LOG_DEBUG << "Mined pow hash: " << mined_hash; + ICICLE_LOG_DEBUG << "Mined pow hash: 0x" << std::hex << mined_hash; + return pow_error; } /** @@ -192,17 +207,17 @@ namespace icicle { } /** - * @brief Build the hash input for the proof-of-work nonce. - * hash_input = entry_0||alpha_{n-1}||"nonce"||nonce + * @brief Build the hash input prefix. The nonce is added later + * hash_input_prefix = entry_0||alpha_{n-1}||"nonce" + * hash_input = hash_input_prefix||nonce * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ - void build_hash_input_pow(std::vector& hash_input, uint64_t temp_pow_nonce) + void build_hash_input_pow(std::vector& hash_input) { append_data(hash_input, m_entry_0); append_field(hash_input, m_prev_alpha); append_data(hash_input, m_transcript_config.get_nonce_label()); - append_value(hash_input, temp_pow_nonce); } /** @@ -222,27 +237,6 @@ namespace icicle { } } - static size_t count_leading_zero_bits(const std::vector& data) - { - size_t zero_bits = 0; - for (size_t i = 0; i < data.size(); i++) { - uint8_t byte_val = static_cast(data[i]); - if (byte_val == 0) { - zero_bits += 8; - } else { - for (int bit = 7; bit >= 0; bit--) { - if ((byte_val & (1 << bit)) == 0) { - zero_bits++; - } else { - return zero_bits; - } - } - break; - } - } - return zero_bits; - } - uint64_t bytes_to_uint_64(const std::vector& data) { uint64_t result = 0; diff --git a/icicle/include/icicle/hash/pow.h b/icicle/include/icicle/hash/pow.h index a0e5a622d2..b47adc8cce 100644 --- a/icicle/include/icicle/hash/pow.h +++ b/icicle/include/icicle/hash/pow.h @@ -31,6 +31,7 @@ namespace icicle { */ static PowConfig default_pow_config() { return PowConfig(); } + extern "C" { /** * @brief Solves the proof-of-work (PoW) challenge using the given hashing algorithm. * @@ -48,7 +49,7 @@ namespace icicle { * * @return eIcicleError Error code indicating success or failure of the operation. */ - eIcicleError pow_solver( + eIcicleError proof_of_work( const Hash& hasher, const std::byte* challenge, uint32_t challenge_size, @@ -75,7 +76,7 @@ namespace icicle { * * @return eIcicleError Error code indicating success or failure of the verification process. */ - eIcicleError pow_verify( + eIcicleError proof_of_work_verify( const Hash& hasher, const std::byte* challenge, uint32_t challenge_size, @@ -84,4 +85,5 @@ namespace icicle { uint64_t nonce, bool& is_correct, uint64_t& mined_hash); + } } // namespace icicle \ No newline at end of file diff --git a/icicle/include/icicle/merkle/merkle_tree.h b/icicle/include/icicle/merkle/merkle_tree.h index 3ec2f29c1e..91c32419a0 100644 --- a/icicle/include/icicle/merkle/merkle_tree.h +++ b/icicle/include/icicle/merkle/merkle_tree.h @@ -84,6 +84,8 @@ namespace icicle { */ inline std::pair get_merkle_root() const { return m_backend->get_merkle_root(); } + inline std::pair get_merkle_root(bool on_device) const { return m_backend->get_merkle_root(on_device); } + template inline std::pair get_merkle_root() const { diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index cc699126b3..aceab8db05 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -1,4 +1,3 @@ - #include "test_mod_arithmetic_api.h" #include "icicle/sumcheck/sumcheck.h" #include "icicle/fri/fri.h" @@ -687,13 +686,19 @@ TYPED_TEST(FieldTest, Fri) }; run(IcicleTestBase::reference_device(), false); - // run(IcicleTestBase::main_device(), , VERBOSE); + run(IcicleTestBase::main_device(), false); } } } TYPED_TEST(FieldTest, FriShouldFailCases) { + // Non-random configuration + // const int log_input_size = 10; + // const int log_stopping_size = 4; + // const size_t pow_bits = 0; + // const size_t nof_queries = 4; + // Randomize configuration const size_t log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; @@ -728,7 +733,6 @@ TYPED_TEST(FieldTest, FriShouldFailCases) Fri prover_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - // set transcript config const char* domain_separator_label = "domain_separator_label"; const char* round_challenge_label = "round_challenge_label"; const char* commit_phase_label = "commit_phase_label"; @@ -762,13 +766,24 @@ TYPED_TEST(FieldTest, FriShouldFailCases) ICICLE_CHECK(ntt_release_domain()); }; + // Reference Device run(IcicleTestBase::reference_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); run( IcicleTestBase::reference_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); run( IcicleTestBase::reference_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); - // run(IcicleTestBase::main_device()); + + // Main Device + // Test invalid nof_queries + run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); + // Test invalid folding_factor + run( + IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); + // Test invalid input size + run( + IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, + log_input_size - 1 /*log_domain_size*/); } #endif // FRI From 6319dee1c6250e9a2836e42921be11d28a2466d6 Mon Sep 17 00:00:00 2001 From: Jeremy Felder Date: Sun, 9 Mar 2025 13:26:00 +0000 Subject: [PATCH 109/127] Formatting --- icicle/backend/cpu/include/cpu_fri_backend.h | 3 ++- .../icicle/backend/merkle/merkle_tree_backend.h | 3 ++- icicle/include/icicle/fri/fri_transcript.h | 9 ++++++--- icicle/include/icicle/merkle/merkle_tree.h | 5 ++++- icicle/tests/test_field_api.cpp | 14 ++++++-------- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 4454b05616..9a0f89cf41 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -147,7 +147,8 @@ namespace icicle { return eIcicleError::SUCCESS; } - eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof){ + eIcicleError proof_of_work(FriTranscript& transcript, const size_t pow_bits, FriProof& fri_proof) + { uint64_t nonce = 0; bool found = false; eIcicleError pow_err = transcript.solve_pow(nonce, pow_bits, found); diff --git a/icicle/include/icicle/backend/merkle/merkle_tree_backend.h b/icicle/include/icicle/backend/merkle/merkle_tree_backend.h index e8061b5160..59eaac1022 100644 --- a/icicle/include/icicle/backend/merkle/merkle_tree_backend.h +++ b/icicle/include/icicle/backend/merkle/merkle_tree_backend.h @@ -57,7 +57,8 @@ namespace icicle { */ virtual std::pair get_merkle_root() const = 0; - virtual std::pair get_merkle_root(bool on_device) const { + virtual std::pair get_merkle_root(bool on_device) const + { return this->get_merkle_root(); }; diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 8cb40a01c8..376cffd9c0 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -51,7 +51,8 @@ namespace icicle { return m_prev_alpha; } - bool verify_pow(uint64_t nonce, uint8_t pow_bits) { + bool verify_pow(uint64_t nonce, uint8_t pow_bits) + { // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space @@ -67,7 +68,8 @@ namespace icicle { return is_correct; } - eIcicleError solve_pow(uint64_t& nonce, size_t pow_bits, bool& found) { + eIcicleError solve_pow(uint64_t& nonce, size_t pow_bits, bool& found) + { // Prepare a buffer for hashing std::vector hash_input; hash_input.reserve(1024); // pre-allocate some space @@ -75,7 +77,8 @@ namespace icicle { const Hash& hasher = m_transcript_config.get_hasher(); const PowConfig cfg; uint64_t mined_hash; - eIcicleError pow_error = proof_of_work(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, found, nonce, mined_hash); + eIcicleError pow_error = + proof_of_work(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, found, nonce, mined_hash); ICICLE_LOG_DEBUG << "Mined pow hash: " << mined_hash; ICICLE_LOG_DEBUG << "Mined pow hash: 0x" << std::hex << mined_hash; diff --git a/icicle/include/icicle/merkle/merkle_tree.h b/icicle/include/icicle/merkle/merkle_tree.h index 91c32419a0..b2e892fd97 100644 --- a/icicle/include/icicle/merkle/merkle_tree.h +++ b/icicle/include/icicle/merkle/merkle_tree.h @@ -84,7 +84,10 @@ namespace icicle { */ inline std::pair get_merkle_root() const { return m_backend->get_merkle_root(); } - inline std::pair get_merkle_root(bool on_device) const { return m_backend->get_merkle_root(on_device); } + inline std::pair get_merkle_root(bool on_device) const + { + return m_backend->get_merkle_root(on_device); + } template inline std::pair get_merkle_root() const diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index aceab8db05..a83317f453 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -264,7 +264,8 @@ TEST_F(FieldTestBase, SumcheckDataOnDevice) data_main[idx] = tmp; } std::ostringstream oss; - oss << IcicleTestBase::main_device() << " " << "Sumcheck"; + oss << IcicleTestBase::main_device() << " " + << "Sumcheck"; SumcheckProof sumcheck_proof; @@ -698,7 +699,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) // const int log_stopping_size = 4; // const size_t pow_bits = 0; // const size_t nof_queries = 4; - + // Randomize configuration const size_t log_input_size = rand_uint_32b(3, 13); const size_t input_size = 1 << log_input_size; @@ -773,17 +774,14 @@ TYPED_TEST(FieldTest, FriShouldFailCases) run( IcicleTestBase::reference_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); - + // Main Device // Test invalid nof_queries run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); // Test invalid folding_factor - run( - IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); + run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); // Test invalid input size - run( - IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, - log_input_size - 1 /*log_domain_size*/); + run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); } #endif // FRI From 01893ed14061b7f15d5fc6b0e8cecafd8cdaf941 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Sun, 9 Mar 2025 16:22:53 +0200 Subject: [PATCH 110/127] added documentation, removed debug prints --- .../version-3.5.0/icicle/primitives/fri.md | 229 ++++++++++++++++++ icicle/include/icicle/backend/fri_backend.h | 2 +- icicle/include/icicle/fri/fri.h | 2 +- icicle/include/icicle/fri/fri_transcript.h | 4 - icicle/tests/test_field_api.cpp | 12 +- 5 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md diff --git a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md new file mode 100644 index 0000000000..61d63723f0 --- /dev/null +++ b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md @@ -0,0 +1,229 @@ +# Sumcheck API Documentation + +## Overview +The Fast Reed-Solomon Interactive Oracle Proof of Proximity (FRI) protocol is used to efficiently verify that a given polynomial has a bounded degree. + + The Prover asserts that they know a low-degree polynomial F(x) of degree d, and they provide oracle access to a Reed-Solomon (RS) codeword representing evaluations of this polynomial over a domain L: + +$$ +RS(F(x), L, n) = \{F(1), F(\alpha), F(\alpha^2), ..., F(\alpha^{n-1})\} +$$ + +where α is a primitive root of unity, and $n = 2^l$ (for $l ∈ Z$) is the domain size. + +## How it works +The proof construction consists of three phases: the Commit and Fold Phase, the Proof of Work Phase (optional), and the Query Phase. +Using a Fiat-Shamir (FS) scheme, the proof is generated in a non-interactive manner, enabling the Prover to generate the entire proof and send it to the Verifier for validation. +The polynomial size must be a power of 2 and is passed to the protocol in evaluation form. + +### Prover + +#### Commit and Fold Phase +* The prover commits to the polynomial evaluations by constructing a Merkle tree. +* A folding step is performed iteratively to reduce the polynomial degree. +* In each step, the polynomial is rewritten using random coefficients derived from Fiat-Shamir hashing, and a new Merkle tree is built for the reduced polynomial. +* This process continues recursively until the polynomial reaches a minimal length. +* Currently, only a folding factor of 2 is supported. + +#### Proof of Work Phase (Optional) +* If enabled, the prover is required to find a nonce such that, when hashed with the final Merkle tree root, the result meets a certain number of leading zero bits. + +#### Query Phase +* Using the Fiat-Shamir transform, the prover determines the random query indices based on the previously committed Merkle roots. +* For each sampled index, the prover provides the corresponding Merkle proof, showing that the value is part of the committed Merkle tree. +* The prover returns all required data as the FriProof, which is then verified by the verifier. + +### Verifier +* The verifier checks the Merkle proofs to ensure the sampled values were indeed committed to in the commit phase. +* The verifier reconstructs the Fiat-Shamir challenges from the prover's commitments and verifies that the prover followed the protocol honestly. +* The folding relation is checked for each sampled query. +* If all checks pass, the proof is accepted as valid. + + +## C++ API +A Fri object can be created using the following function: +```cpp +// icicle/fri/fri.h +Fri fri_instance = create_fri( + input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer) +``` + +* `merkle_tree_leaves_hash`, `merkle_tree_compress_hash` and `output_store_min_layer` refer to the hashes used in the Merkle Trees built in each round of the folding. For further information about ICICLE's Merkle Trees, see [Merkle-Tree documentation](./merkle.md) and [Hash documentation](./merkle.md). +* Currently, only a folding factor of 2 is supported. +* Currently, only arity 2 is supported for `merkle_tree_compress_hash`. + +There are two key configuration structs related to the Fri protocol. + +### FriConfig +The `FriConfig` struct is used to specify parameters for the FRI protocol. It contains the following fields: +- **`stream: icicleStreamHandle`**: The CUDA stream for asynchronous execution. If `nullptr`, the default stream is used. +- **`pow_bits: size_t`**: Number of leading zeros required for proof-of-work. If set, the optional proof-of-work phase is executed. +- **`nof_queries: size_t`**: Number of queries computed for each folded layer of FRI. +- **`are_inputs_on_device: bool`**: If true, the input polynomials are stored on the device (e.g., GPU); otherwise, they remain on the host (e.g., CPU). +- **`is_async: bool`**: If true, it runs the hash asynchronously. +- **`ext: ConfigExtension*`**: Backend-specific extensions. + +The default values are: +```cpp +// icicle/fri/fri_config.h +struct FriConfig { + icicleStreamHandle stream = nullptr; + size_t pow_bits = 0; + size_t nof_queries = 1; + bool are_inputs_on_device =false; + bool is_async = false; + ConfigExtension* ext = nullptr; +}; +``` + +### FriTranscriptConfig +The `FriTranscriptConfig` class is used to specify parameters for the Fiat-Shamir scheme used by the FRI protocol. It contains the following fields: +- **`hasher: Hash`**: The hash function used to generate randomness for Fiat-Shamir. +- **`domain_separator_label: std::vector`** +- **`round_challenge_label: std::vector`** +- **`commit_phase_label: std::vector`** +- **`nonce_label: std::vector`** +- **`public_state: std::vector`** +- **`seed_rng: TypeParam`**: The seed for initializing the RNG. + +Note that the encoding is little endian. + +There are three constructors for `FriTranscriptConfig`: + +* **Default constructor**: +```cpp +// icicle/fri/fri_transcript_config.h +FriTranscriptConfig() + : m_hasher(create_keccak_256_hash()), m_domain_separator_label({}), m_commit_phase_label({}), m_nonce_label({}), + m_public({}), m_seed_rng(F::zero()) +``` + +* **Constructor with byte vector for labels**: +```cpp +FriTranscriptConfig( + Hash hasher, + std::vector&& domain_separator_label, + std::vector&& round_challenge_label, + std::vector&& commit_phase_label, + std::vector&& nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), m_domain_separator_label(std::move(domain_separator_label)), + m_round_challenge_label(std::move(round_challenge_label)), + m_commit_phase_label(std::move(commit_phase_label)), m_nonce_label(std::move(nonce_label)), + m_public(std::move(public_state)), m_seed_rng(seed_rng) +``` + +* **Constructor with `const char*` arguments for labels**: +```cpp + FriTranscriptConfig( + Hash hasher, + const char* domain_separator_label, + const char* round_challenge_label, + const char* commit_phase_label, + const char* nonce_label, + std::vector&& public_state, + F seed_rng) + : m_hasher(std::move(hasher)), m_domain_separator_label(cstr_to_bytes(domain_separator_label)), + m_round_challenge_label(cstr_to_bytes(round_challenge_label)), + m_commit_phase_label(cstr_to_bytes(commit_phase_label)), m_nonce_label(cstr_to_bytes(nonce_label)), + m_public(std::move(public_state)), m_seed_rng(seed_rng) +``` + +### Generating FRI Proofs +To generate a proof, first, an empty proof needs to be created. The FRI proof is represented by the `FriProof` class: + +```cpp +// icicle/fri/fri_proof.h +template +class FriProof +``` + +The class has a default constructor `FriProof()` that takes no arguments. + +The proof can be generated using the get_proof method from the `Fri` object: +```cpp +// icicle/fri/fri.h +eIcicleError get_proof( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + FriProof& fri_proof /* OUT */) const +``` + +- **`input_data: const F*`**: Evaluations of The input polynomial. +- **`fri_proof: FriProof&`**: The output `FriProof` object containing the generated proof. + +Note: An NTT domain is used for proof generation, so before generating a proof, an NTT domain of at least the input_data size must be initialized. For more information see [NTT documentation](./ntt.md). + +```cpp +NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); +ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config) +``` + +#### Example: Generating a Proof + +```cpp +// Initialize ntt domain +NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); +ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config); + +// Define hashers for merkle tree +uint64_t merkle_tree_arity = 2; +Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B +Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B + +// Create fri +Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); + +// set transcript config +const char* domain_separator_label = "domain_separator_label"; +const char* round_challenge_label = "round_challenge_label"; +const char* commit_phase_label = "commit_phase_label"; +const char* nonce_label = "nonce_label"; +std::vector&& public_state = {}; +TypeParam seed_rng = TypeParam::one(); + +FriTranscriptConfig transcript_config( + hash, domain_separator_label, round_challenge_label, commit_phase_label, nonce_label, std::move(public_state), + seed_rng); + +// set fri config +FriConfig fri_config; +fri_config.nof_queries = 100; +fri_config.pow_bits = 16; + +FriProof fri_proof; + +// get fri proof +prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); + +// Release ntt domain +ntt_release_domain(); +``` + +### Verifying Fri Proofs + +To verify the proof, the verifier should use the verify method of the `Fri` object: + +```cpp +// icicle/fri/fri.h +eIcicleError verify( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + bool& valid /* OUT */) const +``` + +> **_NOTE:_** `FriConfig` and `FriTranscriptConfig` used for generating the proof must be identical to the one used for verification. + +#### Example: Verifying a Proof +```cpp +Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); +bool valid = false; +verifier_fri.verify(fri_config, transcript_config, fri_proof, valid); + +ASSERT_EQ(true, valid); // Ensure proof verification succeeds +``` + +After calling `verifier_fri.verify`, the variable `valid` will be set to `true` if the proof is valid, and `false` otherwise. \ No newline at end of file diff --git a/icicle/include/icicle/backend/fri_backend.h b/icicle/include/icicle/backend/fri_backend.h index eac44c76b8..1cce3b1417 100644 --- a/icicle/include/icicle/backend/fri_backend.h +++ b/icicle/include/icicle/backend/fri_backend.h @@ -42,7 +42,7 @@ namespace icicle { * * @param fri_config Configuration for FRI operations (e.g., proof-of-work bits, queries). * @param fri_transcript_config Configuration for encoding/hashing FRI messages (Fiat-Shamir). - * @param input_data Evaluations of the polynomial (or other relevant data). + * @param input_data Evaluations of the input polynomial. * @param fri_proof (OUT) A FriProof object to store the proof's Merkle layers, final poly, etc. * @return eIcicleError Error code indicating success or failure. */ diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index d3382130d5..362a5207e7 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -64,7 +64,7 @@ namespace icicle { * @brief Generate a FRI proof from the given polynomial evaluations (or input data). * @param fri_config Configuration for FRI operations (e.g., proof-of-work, queries). * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). - * @param input_data Evaluations or other relevant data for constructing the proof. + * @param input_data Evaluations of the input polynomial. * @param fri_proof Reference to a FriProof object (output). * @return An eIcicleError indicating success or failure. */ diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 376cffd9c0..a095aa90eb 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -62,8 +62,6 @@ namespace icicle { uint64_t mined_hash; bool is_correct; proof_of_work_verify(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, nonce, is_correct, mined_hash); - ICICLE_LOG_DEBUG << "Verified mined pow hash: " << mined_hash; - ICICLE_LOG_DEBUG << "Verified mined pow hash: 0x" << std::hex << mined_hash; return is_correct; } @@ -80,8 +78,6 @@ namespace icicle { eIcicleError pow_error = proof_of_work(hasher, hash_input.data(), hash_input.size(), pow_bits, cfg, found, nonce, mined_hash); - ICICLE_LOG_DEBUG << "Mined pow hash: " << mined_hash; - ICICLE_LOG_DEBUG << "Mined pow hash: 0x" << std::hex << mined_hash; return pow_error; } diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a83317f453..9e0ff09318 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -674,6 +674,9 @@ TYPED_TEST(FieldTest, Fri) ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); + // Release domain + ICICLE_CHECK(ntt_release_domain()); + // ===== Verifier side ====== Fri verifier_fri = create_fri( input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); @@ -681,9 +684,6 @@ TYPED_TEST(FieldTest, Fri) ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, valid)); ASSERT_EQ(true, valid); - - // Release domain - ICICLE_CHECK(ntt_release_domain()); }; run(IcicleTestBase::reference_device(), false); @@ -752,6 +752,9 @@ TYPED_TEST(FieldTest, FriShouldFailCases) eIcicleError error = prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); + // Release domain + ICICLE_CHECK(ntt_release_domain()); + if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== Fri verifier_fri = create_fri( @@ -762,9 +765,6 @@ TYPED_TEST(FieldTest, FriShouldFailCases) ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); - - // Release domain - ICICLE_CHECK(ntt_release_domain()); }; // Reference Device From 5f49f360146b4d4bcb4c6b5868a80f177ff2946b Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Mar 2025 14:18:20 +0200 Subject: [PATCH 111/127] api update - creat_fri() removed, now just calling get_proof or verify --- icicle/backend/cpu/include/cpu_fri_backend.h | 10 -- icicle/include/icicle/fri/fri.h | 110 ++++++++++++++--- icicle/include/icicle/fri/fri_config.h | 6 +- icicle/src/fri/fri.cpp | 118 ++++++++++++++++--- icicle/tests/test_field_api.cpp | 30 +++-- 5 files changed, 212 insertions(+), 62 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 9a0f89cf41..44b02ee8c6 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -37,16 +37,6 @@ namespace icicle { const F* input_data, FriProof& fri_proof /*out*/) override { - if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0"; - return eIcicleError::INVALID_ARGUMENT; - } - if (__builtin_expect(this->m_folding_factor != 2, 0)) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } - FriTranscript transcript(fri_transcript_config, m_log_input_size); // Initialize the proof diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 362a5207e7..beadafeda2 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -22,26 +22,104 @@ namespace icicle { template class Fri; - /** - * @brief Constructor for the case where only binary Merkle trees are used - * with a constant hash function. - * - * @param input_size The size of the input polynomial - number of evaluations. - * @param folding_factor The factor by which the codeword is folded each round. - * @param stopping_degree The minimal polynomial degree at which to stop folding. - * @param merkle_tree_leaves_hash The hash function used for leaves of the Merkle tree. - * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. - * @return A `Fri` object built around the chosen backend. - */ +/** + * @brief Generates a FRI proof using a binary Merkle tree structure. + * + * This function constructs a FRI proof by applying the Fast Reed-Solomon + * Interactive Oracle Proof of Proximity (FRI) protocol. The proof is built + * using a Merkle tree with a predefined hash function. + * + * @param fri_config Configuration parameters for the FRI protocol. + * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. + * @param input_data Pointer to the polynomial evaluations. + * @param input_size The number of evaluations in the input polynomial. + * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. + * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. + * @param fri_proof (OUT) The generated FRI proof. + * @return `eIcicleError` indicating success or failure of the proof generation. + */ + + template - Fri create_fri( + eIcicleError get_fri_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer = 0); + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */); + +/** + * @brief Verifies a given FRI proof using a binary Merkle tree structure. + * + * This function checks the validity of a FRI proof by reconstructing the + * Merkle tree and ensuring consistency with the committed data. The verification + * process leverages the Fiat-Shamir heuristic to maintain non-interactivity. + * + * @param fri_config Configuration parameters for the FRI protocol. + * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. + * @param fri_proof The FRI proof to be verified. + * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. + * @param output_store_min_layer The layer at which to store partial results. + * @param valid (OUT) Boolean flag indicating whether the proof is valid. + * @return `eIcicleError` indicating success or failure of the verification process. + */ + + template + eIcicleError verify_fri_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */); + + struct FRI { + template + inline static eIcicleError get_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + const size_t input_size, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */){ + return get_fri_proof_mt( + fri_config, + fri_transcript_config, + input_data, + input_size, + merkle_tree_leaves_hash, + merkle_tree_compress_hash, + output_store_min_layer, + fri_proof /* OUT */); + } + + template + inline static eIcicleError verify_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */){ + return verify_fri_mt( + fri_config, + fri_transcript_config, + fri_proof, + merkle_tree_leaves_hash, + merkle_tree_compress_hash, + output_store_min_layer, + valid /* OUT */); + } + }; /** * @brief Class for performing FRI operations. diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index e457cb52de..c556ba9d7d 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -15,8 +15,10 @@ namespace icicle { */ struct FriConfig { icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. - size_t pow_bits = 0; // Number of leading zeros required for proof-of-work. Default is 0. - size_t nof_queries = 1; // Number of queries, computed for each folded layer of FRI. Default is 1. + size_t folding_factor = 2; // The factor by which the codeword is folded in each round. + size_t stopping_degree = 0; // The minimal polynomial degree at which folding stops. + size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. Default is 0. + size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. Default is 50. bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index a007ff9261..c791324dc7 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -1,5 +1,7 @@ #include "icicle/fri/fri.h" #include "icicle/dispatcher.h" +#include "icicle/errors.h" + namespace icicle { @@ -26,14 +28,13 @@ namespace icicle { */ template Fri create_fri_template( - const size_t input_size, + const size_t log_input_size, const size_t folding_factor, const size_t stopping_degree, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; @@ -43,7 +44,7 @@ namespace icicle { size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } - size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; + size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); @@ -77,14 +78,13 @@ namespace icicle { */ template Fri create_fri_template_ext( - const size_t input_size, + const size_t log_input_size, const size_t folding_factor, const size_t stopping_degree, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer) { - const size_t log_input_size = static_cast(std::log2(static_cast(input_size))); const size_t df = stopping_degree; const size_t log_df_plus_1 = (df > 0) ? static_cast(std::log2(static_cast(df + 1))) : 0; const size_t fold_rounds = (log_input_size > log_df_plus_1) ? (log_input_size - log_df_plus_1) : 0; @@ -94,7 +94,7 @@ namespace icicle { size_t compress_hash_arity = merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } - size_t first_merkle_tree_height = std::ceil(std::log2(input_size) / std::log2(compress_hash_arity)) + 1; + size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; uint64_t leaf_element_size = merkle_tree_leaves_hash.default_input_chunk_size(); @@ -108,32 +108,114 @@ namespace icicle { #endif // EXT_FIELD template <> - Fri create_fri( + eIcicleError get_fri_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const scalar_t* input_data, const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer) + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */) { - return create_fri_template( - input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, + if (fri_config.nof_queries <= 0) { + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + return eIcicleError::INVALID_ARGUMENT; + } + if (fri_config.folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + const size_t log_input_size = std::log2(input_size); + Fri prover_fri = create_fri_template( + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); + return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); + } + + template <> + eIcicleError verify_fri_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */) + { + if (fri_config.nof_queries <= 0) { + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + return eIcicleError::INVALID_ARGUMENT; + } + if (fri_config.folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + Fri verifier_fri = create_fri_template( + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, + output_store_min_layer); + return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } #ifdef EXT_FIELD template <> - Fri create_fri( + eIcicleError get_fri_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const extension_t* input_data, const size_t input_size, - const size_t folding_factor, - const size_t stopping_degree, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer) + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */) + { + if (fri_config.nof_queries <= 0) { + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + return eIcicleError::INVALID_ARGUMENT; + } + if (fri_config.folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + const size_t log_input_size = std::log2(input_size); + Fri prover_fri = create_fri_template_ext( + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, + output_store_min_layer); + return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); + } + + template <> + eIcicleError verify_fri_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */) { - return create_fri_template_ext( - input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, + if (fri_config.nof_queries <= 0) { + ICICLE_LOG_ERROR << "Number of queries must be > 0"; + return eIcicleError::INVALID_ARGUMENT; + } + if (fri_config.folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + Fri verifier_fri = create_fri_template_ext( + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); + return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } #endif diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 9e0ff09318..eec30a8124 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -644,9 +644,6 @@ TYPED_TEST(FieldTest, Fri) Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - Fri prover_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - // set transcript config const char* domain_separator_label = "domain_separator_label"; const char* round_challenge_label = "round_challenge_label"; @@ -662,6 +659,8 @@ TYPED_TEST(FieldTest, Fri) FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; + fri_config.folding_factor = folding_factor; + fri_config.stopping_degree = stopping_degree; FriProof fri_proof; std::ostringstream oss; @@ -671,18 +670,19 @@ TYPED_TEST(FieldTest, Fri) oss << dev_type << " FRI proof"; } START_TIMER(FRIPROOF_sync) - ICICLE_CHECK(prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof)); + eIcicleError err = FRI::get_proof_mt( + fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); + ICICLE_CHECK(err); END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); // Release domain ICICLE_CHECK(ntt_release_domain()); // ===== Verifier side ====== - Fri verifier_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool valid = false; - ICICLE_CHECK(verifier_fri.verify(fri_config, transcript_config, fri_proof, valid)); - + err = FRI::verify_mt( + fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); + ICICLE_CHECK(err); ASSERT_EQ(true, valid); }; @@ -731,9 +731,6 @@ TYPED_TEST(FieldTest, FriShouldFailCases) Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B - Fri prover_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - const char* domain_separator_label = "domain_separator_label"; const char* round_challenge_label = "round_challenge_label"; const char* commit_phase_label = "commit_phase_label"; @@ -748,20 +745,21 @@ TYPED_TEST(FieldTest, FriShouldFailCases) FriConfig fri_config; fri_config.nof_queries = nof_queries; fri_config.pow_bits = pow_bits; + fri_config.folding_factor = folding_factor; + fri_config.stopping_degree = stopping_degree; FriProof fri_proof; - eIcicleError error = prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); + eIcicleError error = get_fri_proof_mt( + fri_config, transcript_config, scalars.get(),input_size, hash, compress, output_store_min_layer, fri_proof); // Release domain ICICLE_CHECK(ntt_release_domain()); if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== - Fri verifier_fri = create_fri( - input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool valid = false; - error = verifier_fri.verify(fri_config, transcript_config, fri_proof, valid); - + error = verify_fri_mt( + fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); From 14db3889f5a311d67ce855627015304c86d0ff63 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Mar 2025 14:18:44 +0200 Subject: [PATCH 112/127] format --- icicle/include/icicle/fri/fri.h | 142 ++++++++++++------------- icicle/include/icicle/fri/fri_config.h | 8 +- icicle/src/fri/fri.cpp | 29 +++-- icicle/tests/test_field_api.cpp | 2 +- 4 files changed, 85 insertions(+), 96 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index beadafeda2..3f8d8efd99 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -22,24 +22,23 @@ namespace icicle { template class Fri; -/** - * @brief Generates a FRI proof using a binary Merkle tree structure. - * - * This function constructs a FRI proof by applying the Fast Reed-Solomon - * Interactive Oracle Proof of Proximity (FRI) protocol. The proof is built - * using a Merkle tree with a predefined hash function. - * - * @param fri_config Configuration parameters for the FRI protocol. - * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. - * @param input_data Pointer to the polynomial evaluations. - * @param input_size The number of evaluations in the input polynomial. - * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. - * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. - * @param fri_proof (OUT) The generated FRI proof. - * @return `eIcicleError` indicating success or failure of the proof generation. - */ - + /** + * @brief Generates a FRI proof using a binary Merkle tree structure. + * + * This function constructs a FRI proof by applying the Fast Reed-Solomon + * Interactive Oracle Proof of Proximity (FRI) protocol. The proof is built + * using a Merkle tree with a predefined hash function. + * + * @param fri_config Configuration parameters for the FRI protocol. + * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. + * @param input_data Pointer to the polynomial evaluations. + * @param input_size The number of evaluations in the input polynomial. + * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. + * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. + * @param fri_proof (OUT) The generated FRI proof. + * @return `eIcicleError` indicating success or failure of the proof generation. + */ template eIcicleError get_fri_proof_mt( @@ -51,23 +50,23 @@ namespace icicle { Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */); - -/** - * @brief Verifies a given FRI proof using a binary Merkle tree structure. - * - * This function checks the validity of a FRI proof by reconstructing the - * Merkle tree and ensuring consistency with the committed data. The verification - * process leverages the Fiat-Shamir heuristic to maintain non-interactivity. - * - * @param fri_config Configuration parameters for the FRI protocol. - * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. - * @param fri_proof The FRI proof to be verified. - * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. - * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer The layer at which to store partial results. - * @param valid (OUT) Boolean flag indicating whether the proof is valid. - * @return `eIcicleError` indicating success or failure of the verification process. - */ + + /** + * @brief Verifies a given FRI proof using a binary Merkle tree structure. + * + * This function checks the validity of a FRI proof by reconstructing the + * Merkle tree and ensuring consistency with the committed data. The verification + * process leverages the Fiat-Shamir heuristic to maintain non-interactivity. + * + * @param fri_config Configuration parameters for the FRI protocol. + * @param fri_transcript_config Configuration for the Fiat-Shamir transcript used in FRI. + * @param fri_proof The FRI proof to be verified. + * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. + * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. + * @param output_store_min_layer The layer at which to store partial results. + * @param valid (OUT) Boolean flag indicating whether the proof is valid. + * @return `eIcicleError` indicating success or failure of the verification process. + */ template eIcicleError verify_fri_mt( @@ -79,47 +78,38 @@ namespace icicle { const uint64_t output_store_min_layer, bool& valid /* OUT */); - struct FRI { - template - inline static eIcicleError get_proof_mt( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - const F* input_data, - const size_t input_size, - Hash merkle_tree_leaves_hash, - Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, - FriProof& fri_proof /* OUT */){ - return get_fri_proof_mt( - fri_config, - fri_transcript_config, - input_data, - input_size, - merkle_tree_leaves_hash, - merkle_tree_compress_hash, - output_store_min_layer, - fri_proof /* OUT */); - } - - template - inline static eIcicleError verify_mt( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, - Hash merkle_tree_leaves_hash, - Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, - bool& valid /* OUT */){ - return verify_fri_mt( - fri_config, - fri_transcript_config, - fri_proof, - merkle_tree_leaves_hash, - merkle_tree_compress_hash, - output_store_min_layer, - valid /* OUT */); - } - }; + struct FRI { + template + inline static eIcicleError get_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + const size_t input_size, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */) + { + return get_fri_proof_mt( + fri_config, fri_transcript_config, input_data, input_size, merkle_tree_leaves_hash, merkle_tree_compress_hash, + output_store_min_layer, fri_proof /* OUT */); + } + + template + inline static eIcicleError verify_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */) + { + return verify_fri_mt( + fri_config, fri_transcript_config, fri_proof, merkle_tree_leaves_hash, merkle_tree_compress_hash, + output_store_min_layer, valid /* OUT */); + } + }; /** * @brief Class for performing FRI operations. diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index c556ba9d7d..f8358923f3 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -15,10 +15,10 @@ namespace icicle { */ struct FriConfig { icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. - size_t folding_factor = 2; // The factor by which the codeword is folded in each round. - size_t stopping_degree = 0; // The minimal polynomial degree at which folding stops. - size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. Default is 0. - size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. Default is 50. + size_t folding_factor = 2; // The factor by which the codeword is folded in each round. + size_t stopping_degree = 0; // The minimal polynomial degree at which folding stops. + size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. Default is 0. + size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. Default is 50. bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index c791324dc7..c85706fd82 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -2,7 +2,6 @@ #include "icicle/dispatcher.h" #include "icicle/errors.h" - namespace icicle { using FriFactoryScalar = FriFactoryImpl; @@ -116,7 +115,7 @@ namespace icicle { Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, - FriProof& fri_proof /* OUT */) + FriProof& fri_proof /* OUT */) { if (fri_config.nof_queries <= 0) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; @@ -129,11 +128,11 @@ namespace icicle { } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri_template( - log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, - output_store_min_layer); + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, + merkle_tree_compress_hash, output_store_min_layer); return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); } - + template <> eIcicleError verify_fri_mt( const FriConfig& fri_config, @@ -150,15 +149,15 @@ namespace icicle { } if (fri_config.folding_factor != 2) { ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors + // supporting other folding factors return eIcicleError::INVALID_ARGUMENT; } const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); Fri verifier_fri = create_fri_template( - log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, - output_store_min_layer); + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, + merkle_tree_compress_hash, output_store_min_layer); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } @@ -171,7 +170,7 @@ namespace icicle { const size_t input_size, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, + const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { if (fri_config.nof_queries <= 0) { @@ -185,11 +184,11 @@ namespace icicle { } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri_template_ext( - log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, - output_store_min_layer); + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, + merkle_tree_compress_hash, output_store_min_layer); return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); } - + template <> eIcicleError verify_fri_mt( const FriConfig& fri_config, @@ -197,7 +196,7 @@ namespace icicle { FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, + const uint64_t output_store_min_layer, bool& valid /* OUT */) { if (fri_config.nof_queries <= 0) { @@ -213,8 +212,8 @@ namespace icicle { const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); Fri verifier_fri = create_fri_template_ext( - log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, - output_store_min_layer); + log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, + merkle_tree_compress_hash, output_store_min_layer); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } #endif diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index eec30a8124..a1b8a273d3 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -750,7 +750,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) FriProof fri_proof; eIcicleError error = get_fri_proof_mt( - fri_config, transcript_config, scalars.get(),input_size, hash, compress, output_store_min_layer, fri_proof); + fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); // Release domain ICICLE_CHECK(ntt_release_domain()); From 2f16963981e068fd2705dd0598fadc100c99be10 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Mar 2025 16:46:26 +0200 Subject: [PATCH 113/127] Changed FriShouldFailCases test to be deterministic. Removed redundant code from fri.cpp --- icicle/src/fri/fri.cpp | 61 ++++++++++++--------------------- icicle/tests/test_field_api.cpp | 15 +++----- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index c85706fd82..921aebc47c 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -6,27 +6,13 @@ namespace icicle { using FriFactoryScalar = FriFactoryImpl; ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryScalar); - /** - * @brief Create a FRI instance. - * @return A `Fri` object built around the chosen backend. - */ - template - Fri create_fri_with_merkle_trees( - const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - { - std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); - - Fri fri{backend}; - return fri; - } /** - * @brief Specialization of create_fri for the case of - * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + * @brief Create a FRI instance. + * @return A `Fri` object built around the chosen backend. */ template - Fri create_fri_template( + Fri create_fri( const size_t log_input_size, const size_t folding_factor, const size_t stopping_degree, @@ -51,32 +37,25 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); + // return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); + std::shared_ptr> backend; + ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); + + Fri fri{backend}; + return fri; + } #ifdef EXT_FIELD using FriExtFactoryScalar = FriFactoryImpl; ICICLE_DISPATCHER_INST(FriExtFieldDispatcher, extension_fri_factory, FriExtFactoryScalar); - /** - * @brief Create a FRI instance. - * @return A `Fri` object built around the chosen backend. - */ - template - Fri create_fri_with_merkle_trees_ext( - const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - { - std::shared_ptr> backend; - ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); - Fri fri{backend}; - return fri; - } /** - * @brief Specialization of create_fri for the case of - * (input_size, folding_factor, stopping_degree, hash_for_merkle_tree, output_store_min_layer). + * @brief Create a FRI instance. + * @return A `Fri` object built around the chosen backend. */ template - Fri create_fri_template_ext( + Fri create_fri_ext( const size_t log_input_size, const size_t folding_factor, const size_t stopping_degree, @@ -101,7 +80,11 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - return create_fri_with_merkle_trees_ext(folding_factor, stopping_degree, std::move(merkle_trees)); + std::shared_ptr> backend; + ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); + + Fri fri{backend}; + return fri; } #endif // EXT_FIELD @@ -127,7 +110,7 @@ namespace icicle { return eIcicleError::INVALID_ARGUMENT; } const size_t log_input_size = std::log2(input_size); - Fri prover_fri = create_fri_template( + Fri prover_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); @@ -155,7 +138,7 @@ namespace icicle { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - Fri verifier_fri = create_fri_template( + Fri verifier_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); @@ -183,7 +166,7 @@ namespace icicle { return eIcicleError::INVALID_ARGUMENT; } const size_t log_input_size = std::log2(input_size); - Fri prover_fri = create_fri_template_ext( + Fri prover_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); return prover_fri.get_proof(fri_config, fri_transcript_config, input_data, fri_proof); @@ -211,7 +194,7 @@ namespace icicle { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - Fri verifier_fri = create_fri_template_ext( + Fri verifier_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a1b8a273d3..19026217d5 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -694,21 +694,14 @@ TYPED_TEST(FieldTest, Fri) TYPED_TEST(FieldTest, FriShouldFailCases) { - // Non-random configuration - // const int log_input_size = 10; - // const int log_stopping_size = 4; - // const size_t pow_bits = 0; - // const size_t nof_queries = 4; - - // Randomize configuration - const size_t log_input_size = rand_uint_32b(3, 13); + const int log_input_size = 10; + const int log_stopping_size = 4; + const size_t pow_bits = 0; + const size_t nof_queries = 4; const size_t input_size = 1 << log_input_size; - const size_t log_stopping_size = rand_uint_32b(0, log_input_size - 2); const size_t stopping_size = 1 << log_stopping_size; const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; - const size_t pow_bits = rand_uint_32b(0, 3); - const size_t nof_queries = rand_uint_32b(2, 4); // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); From 2d792dcbbcc2a35b33292bd90b9aeee01d8f6eee Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Tue, 11 Mar 2025 16:48:27 +0200 Subject: [PATCH 114/127] format --- icicle/src/fri/fri.cpp | 1 - icicle/tests/test_field_api.cpp | 17 +++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 921aebc47c..24fb6ea38f 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -43,7 +43,6 @@ namespace icicle { Fri fri{backend}; return fri; - } #ifdef EXT_FIELD diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 19026217d5..cec0e52f4a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -687,7 +687,7 @@ TYPED_TEST(FieldTest, Fri) }; run(IcicleTestBase::reference_device(), false); - run(IcicleTestBase::main_device(), false); + // run(IcicleTestBase::main_device(), false); } } } @@ -766,13 +766,14 @@ TYPED_TEST(FieldTest, FriShouldFailCases) IcicleTestBase::reference_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); - // Main Device - // Test invalid nof_queries - run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); - // Test invalid folding_factor - run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); - // Test invalid input size - run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); + // // Main Device + // // Test invalid nof_queries + // run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); + // // Test invalid folding_factor + // run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); + // // Test invalid input size + // run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 + // /*log_domain_size*/); } #endif // FRI From fafb40d1c2ef9c405c489a81525e9a35399e0ce5 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 11:20:50 +0200 Subject: [PATCH 115/127] Uncommented CUDA tests --- icicle/include/icicle/fri/fri_proof.h | 6 ----- icicle/include/icicle/fri/fri_transcript.h | 12 ++++++---- icicle/src/fri/fri.cpp | 28 +++++++++++----------- icicle/tests/test_field_api.cpp | 19 ++++++++------- 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 5dfa0b6d5c..7c92ea0439 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -37,12 +37,6 @@ namespace icicle { */ eIcicleError init(const size_t nof_queries, const size_t nof_fri_rounds, const size_t final_poly_size) { - if (nof_queries <= 0 || nof_fri_rounds <= 0) { - ICICLE_LOG_ERROR << "Number of queries and FRI rounds must be > 0. nof_queries = " << nof_queries - << ", nof_fri_rounds = " << nof_fri_rounds; - return eIcicleError::INVALID_ARGUMENT; - } - // Resize the matrix to hold nof_queries rows and nof_fri_rounds columns m_query_proofs.resize( 2 * nof_queries, diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index a095aa90eb..4ec5bd8961 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -10,6 +10,8 @@ #include "icicle/hash/hash.h" #include "icicle/hash/pow.h" +#define PRE_ALLOCATED_SPACE 1024 + namespace icicle { template @@ -20,7 +22,7 @@ namespace icicle { : m_transcript_config(transcript_config), m_prev_alpha(F::zero()), m_pow_nonce(0) { m_entry_0.clear(); - m_entry_0.reserve(1024); // pre-allocate some space + m_entry_0.reserve(PRE_ALLOCATED_SPACE); // pre-allocate some space build_entry_0(log_input_size); } @@ -33,7 +35,7 @@ namespace icicle { F get_alpha(const std::vector& merkle_commit, bool is_first_round, eIcicleError& err) { std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space + hash_input.reserve(PRE_ALLOCATED_SPACE); // pre-allocate some space // Build the round's hash input if (is_first_round) { @@ -55,7 +57,7 @@ namespace icicle { { // Prepare a buffer for hashing std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space + hash_input.reserve(PRE_ALLOCATED_SPACE); // pre-allocate some space build_hash_input_pow(hash_input); const Hash& hasher = m_transcript_config.get_hasher(); const PowConfig cfg; @@ -70,7 +72,7 @@ namespace icicle { { // Prepare a buffer for hashing std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space + hash_input.reserve(PRE_ALLOCATED_SPACE); // pre-allocate some space build_hash_input_pow(hash_input); const Hash& hasher = m_transcript_config.get_hasher(); const PowConfig cfg; @@ -99,7 +101,7 @@ namespace icicle { { // Prepare a buffer for hashing std::vector hash_input; - hash_input.reserve(1024); // pre-allocate some space + hash_input.reserve(PRE_ALLOCATED_SPACE); // pre-allocate some space // Build the hash input build_hash_input_query_phase(hash_input); diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 24fb6ea38f..cd184095cb 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -99,8 +99,8 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - if (fri_config.nof_queries <= 0) { - ICICLE_LOG_ERROR << "Number of queries must be > 0"; + if (fri_config.nof_queries <= 0 || fri_config.nof_queries > (input_size / fri_config.folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; return eIcicleError::INVALID_ARGUMENT; } if (fri_config.folding_factor != 2) { @@ -125,8 +125,11 @@ namespace icicle { const uint64_t output_store_min_layer, bool& valid /* OUT */) { - if (fri_config.nof_queries <= 0) { - ICICLE_LOG_ERROR << "Number of queries must be > 0"; + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + if (fri_config.nof_queries <= 0 || fri_config.nof_queries > ((1 << log_input_size) / fri_config.folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; return eIcicleError::INVALID_ARGUMENT; } if (fri_config.folding_factor != 2) { @@ -134,9 +137,6 @@ namespace icicle { // supporting other folding factors return eIcicleError::INVALID_ARGUMENT; } - const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); - const size_t final_poly_size = fri_proof.get_final_poly_size(); - const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); Fri verifier_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); @@ -155,8 +155,8 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - if (fri_config.nof_queries <= 0) { - ICICLE_LOG_ERROR << "Number of queries must be > 0"; + if (fri_config.nof_queries <= 0 || fri_config.nof_queries > (input_size / fri_config.folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; return eIcicleError::INVALID_ARGUMENT; } if (fri_config.folding_factor != 2) { @@ -181,8 +181,11 @@ namespace icicle { const uint64_t output_store_min_layer, bool& valid /* OUT */) { - if (fri_config.nof_queries <= 0) { - ICICLE_LOG_ERROR << "Number of queries must be > 0"; + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + if (fri_config.nof_queries <= 0 || fri_config.nof_queries > ((1 << log_input_size) / fri_config.folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; return eIcicleError::INVALID_ARGUMENT; } if (fri_config.folding_factor != 2) { @@ -190,9 +193,6 @@ namespace icicle { // supporting other folding factors return eIcicleError::INVALID_ARGUMENT; } - const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); - const size_t final_poly_size = fri_proof.get_final_poly_size(); - const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); Fri verifier_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index cec0e52f4a..17571c9cc5 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -687,7 +687,7 @@ TYPED_TEST(FieldTest, Fri) }; run(IcicleTestBase::reference_device(), false); - // run(IcicleTestBase::main_device(), false); + run(IcicleTestBase::main_device(), false); } } } @@ -766,14 +766,15 @@ TYPED_TEST(FieldTest, FriShouldFailCases) IcicleTestBase::reference_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 /*log_domain_size*/); - // // Main Device - // // Test invalid nof_queries - // run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); - // // Test invalid folding_factor - // run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); - // // Test invalid input size - // run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 - // /*log_domain_size*/); + // Main Device + // Test invalid nof_queries + run(IcicleTestBase::main_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); + // Test invalid folding_factor + run(IcicleTestBase::main_device(), 10 /*nof_queries*/, 16 /*folding_factor*/, log_input_size /*log_domain_size*/); + // Test invalid input size + run( + IcicleTestBase::main_device(), 10 /*nof_queries*/, 2 /*folding_factor*/, log_input_size - 1 + /*log_domain_size*/); } #endif // FRI From c1eb3185dcc2f50067155177fb0527033efc0bdb Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 14:26:05 +0200 Subject: [PATCH 116/127] Added more cases to FriShouldFailCases test. + some more code review fixes --- icicle/backend/cpu/include/cpu_fri_backend.h | 4 +- icicle/include/icicle/fri/fri.h | 6 +- icicle/include/icicle/fri/fri_proof.h | 9 +- icicle/include/icicle/fri/fri_transcript.h | 10 ++- icicle/src/fri/fri.cpp | 86 ++++++++++---------- icicle/tests/test_field_api.cpp | 56 +++++++++---- 6 files changed, 101 insertions(+), 70 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 44b02ee8c6..5c32d7395d 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -176,11 +176,11 @@ namespace icicle { size_t leaf_idx_sym = (query + (round_size >> 1)) % round_size; F* round_evals = m_fri_rounds.get_round_evals(round_idx); - MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); + MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); eIcicleError err = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof( round_evals, round_size, leaf_idx, false /* is_pruned */, MerkleTreeConfig(), proof_ref); if (err != eIcicleError::SUCCESS) return err; - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); + MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); eIcicleError err_sym = m_fri_rounds.get_merkle_tree(round_idx)->get_merkle_proof( round_evals, round_size, leaf_idx_sym, false /* is_pruned */, MerkleTreeConfig(), proof_ref_sym); if (err_sym != eIcicleError::SUCCESS) return err_sym; diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 3f8d8efd99..3bd4e10da9 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -312,7 +312,7 @@ namespace icicle { return false; } } else { - MerkleProof& proof_ref_folded = fri_proof.get_query_proof(2 * query_idx, round_idx + 1); + MerkleProof& proof_ref_folded = fri_proof.get_query_proof_slot(2 * query_idx, round_idx + 1); const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); if (leaf_data_folded_f != folded) { @@ -386,8 +386,8 @@ namespace icicle { size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; - MerkleProof& proof_ref = fri_proof.get_query_proof(2 * query_idx, round_idx); - MerkleProof& proof_ref_sym = fri_proof.get_query_proof(2 * query_idx + 1, round_idx); + MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); + MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 7c92ea0439..cb3b3873bb 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -47,13 +47,16 @@ namespace icicle { } /** - * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. + * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. Each query includes a proof for two values per round.. * + * This function returns a reference to a pre-allocated Merkle proof in the `m_query_proofs` array. + * The proof is initially empty and will be populated by another function responsible for generating + * the actual proof data. * @param query_idx Index of the query. * @param round_idx Index of the round (FRI round). * @return Reference to the Merkle proof at the specified position. */ - MerkleProof& get_query_proof(const size_t query_idx, const size_t round_idx) + MerkleProof& get_query_proof_slot(const size_t query_idx, const size_t round_idx) { if (query_idx < 0 || query_idx >= m_query_proofs.size()) { throw std::out_of_range("Invalid query index"); } if (round_idx < 0 || round_idx >= m_query_proofs[query_idx].size()) { @@ -68,7 +71,7 @@ namespace icicle { */ std::pair get_merkle_tree_root(const size_t round_idx) const { - return m_query_proofs[0][round_idx].get_root(); + return m_query_proofs[0/*query_idx*/][round_idx].get_root(); // Since all queries in the same round share the same root, we can just return root for query index 0 of the current round } /** diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index 4ec5bd8961..f21b395652 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -224,9 +224,12 @@ namespace icicle { /** * @brief Build the hash input for the query phase. * + * - If PoW is **enabled**: `hash_input = entry_0 || "nonce" || nonce` + * - If PoW is **disabled**: `hash_input = entry_0 || alpha_{n-1}` + * * @param hash_input (OUT) The byte vector that accumulates data to be hashed. */ - void build_hash_input_query_phase(std::vector& hash_input) + inline void build_hash_input_query_phase(std::vector& hash_input) { if (m_pow_nonce == 0) { append_data(hash_input, m_entry_0); @@ -241,9 +244,10 @@ namespace icicle { uint64_t bytes_to_uint_64(const std::vector& data) { uint64_t result = 0; - for (size_t i = 0; i < sizeof(uint64_t); i++) { - result |= static_cast(data[i]) << (i * 8); + if (data.size() < sizeof(uint64_t)) { + ICICLE_LOG_ERROR << "Insufficient data size for conversion to uint64_t"; } + std::memcpy(&result, data.data(), sizeof(uint64_t)); return result; } }; diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index cd184095cb..cc2857a5b3 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -27,8 +27,7 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -37,7 +36,6 @@ namespace icicle { merkle_trees.emplace_back(MerkleTree::create(layer_hashes, leaf_element_size, output_store_min_layer)); layer_hashes.pop_back(); } - // return create_fri_with_merkle_trees(folding_factor, stopping_degree, std::move(merkle_trees)); std::shared_ptr> backend; ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); @@ -69,8 +67,7 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - if (compress_hash_arity != 2) { ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; } + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -88,6 +85,33 @@ namespace icicle { #endif // EXT_FIELD +eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, const size_t folding_factor, const size_t compress_hash_arity){ + if (nof_queries <= 0 || nof_queries > (input_size / folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/folding_factor"; + return eIcicleError::INVALID_ARGUMENT; + } + if (folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + if (input_size == 0 || (input_size & (input_size - 1)) != 0) { + ICICLE_LOG_ERROR << "input_size must be a power of 2. input_size = "<< input_size; + return eIcicleError::INVALID_ARGUMENT; + } + if (folding_factor % compress_hash_arity != 0) { + ICICLE_LOG_ERROR << "folding_factor must be divisible by compress_hash_arity. " + << "folding_factor = " << folding_factor << ", compress_hash_arity = " << compress_hash_arity; + return eIcicleError::INVALID_ARGUMENT; + } + if (compress_hash_arity != 2) { + ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; + return eIcicleError::INVALID_ARGUMENT; + } + return eIcicleError::SUCCESS; +} + + template <> eIcicleError get_fri_proof_mt( const FriConfig& fri_config, @@ -99,15 +123,8 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - if (fri_config.nof_queries <= 0 || fri_config.nof_queries > (input_size / fri_config.folding_factor)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; - return eIcicleError::INVALID_ARGUMENT; - } - if (fri_config.folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } + eIcicleError err = check_if_valid(fri_config.nof_queries, input_size, fri_config.folding_factor, merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size()); + if (err != eIcicleError::SUCCESS){ return err; } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, @@ -128,15 +145,11 @@ namespace icicle { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - if (fri_config.nof_queries <= 0 || fri_config.nof_queries > ((1 << log_input_size) / fri_config.folding_factor)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; - return eIcicleError::INVALID_ARGUMENT; - } - if (fri_config.folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } + + size_t compress_hash_arity = + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS){ return err; } Fri verifier_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); @@ -155,15 +168,10 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - if (fri_config.nof_queries <= 0 || fri_config.nof_queries > (input_size / fri_config.folding_factor)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; - return eIcicleError::INVALID_ARGUMENT; - } - if (fri_config.folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } + size_t compress_hash_arity = + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = check_if_valid(fri_config.nof_queries, input_size, fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS){ return err; } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, @@ -184,15 +192,11 @@ namespace icicle { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - if (fri_config.nof_queries <= 0 || fri_config.nof_queries > ((1 << log_input_size) / fri_config.folding_factor)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/fri_config.folding_factor"; - return eIcicleError::INVALID_ARGUMENT; - } - if (fri_config.folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } + + size_t compress_hash_arity = + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS){ return err; } Fri verifier_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 17571c9cc5..80afe7a943 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -631,7 +631,7 @@ TYPED_TEST(FieldTest, Fri) auto run = [log_input_size, input_size, folding_factor, stopping_degree, output_store_min_layer, nof_queries, pow_bits, &scalars](const std::string& dev_type, bool measure) { Device dev = {dev_type, 0}; - icicle_set_device(dev); + ICICLE_CHECK(icicle_set_device(dev)); // Initialize ntt domain NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); @@ -697,19 +697,19 @@ TYPED_TEST(FieldTest, FriShouldFailCases) const int log_input_size = 10; const int log_stopping_size = 4; const size_t pow_bits = 0; - const size_t nof_queries = 4; - const size_t input_size = 1 << log_input_size; const size_t stopping_size = 1 << log_stopping_size; const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; - // Generate input polynomial evaluations - auto scalars = std::make_unique(input_size); - TypeParam::rand_host_many(scalars.get(), input_size); - auto run = [log_input_size, input_size, stopping_degree, output_store_min_layer, pow_bits, &scalars]( + auto run = [stopping_degree, output_store_min_layer, pow_bits]( const std::string& dev_type, const size_t nof_queries, const size_t folding_factor, - const size_t log_domain_size) { + const size_t log_domain_size, const size_t merkle_tree_arity, const size_t input_size) { + + // Generate input polynomial evaluations + auto scalars = std::make_unique(input_size); + TypeParam::rand_host_many(scalars.get(), input_size); + Device dev = {dev_type, 0}; icicle_set_device(dev); @@ -718,8 +718,6 @@ TYPED_TEST(FieldTest, FriShouldFailCases) ICICLE_CHECK(ntt_init_domain(scalar_t::omega(log_domain_size), init_domain_config)); // ===== Prover side ====== - uint64_t merkle_tree_arity = 2; // TODO SHANIE (future) - add support for other arities - // Define hashers for merkle tree Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B @@ -759,22 +757,44 @@ TYPED_TEST(FieldTest, FriShouldFailCases) }; // Reference Device - run(IcicleTestBase::reference_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/); + // Test invalid nof_queries + run(IcicleTestBase::reference_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/, 2 /*merkle_tree_arity*/, 1 << log_input_size /*input_size*/); + run(IcicleTestBase::main_device(), (1< Date: Wed, 12 Mar 2025 14:26:33 +0200 Subject: [PATCH 117/127] format --- icicle/include/icicle/fri/fri_proof.h | 9 ++- icicle/include/icicle/fri/fri_transcript.h | 4 +- icicle/src/fri/fri.cpp | 84 ++++++++++++---------- icicle/tests/test_field_api.cpp | 56 +++++++++------ 4 files changed, 87 insertions(+), 66 deletions(-) diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index cb3b3873bb..0fd49a1ecb 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -47,10 +47,11 @@ namespace icicle { } /** - * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. Each query includes a proof for two values per round.. + * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. Each query + * includes a proof for two values per round.. * * This function returns a reference to a pre-allocated Merkle proof in the `m_query_proofs` array. - * The proof is initially empty and will be populated by another function responsible for generating + * The proof is initially empty and will be populated by another function responsible for generating * the actual proof data. * @param query_idx Index of the query. * @param round_idx Index of the round (FRI round). @@ -71,7 +72,9 @@ namespace icicle { */ std::pair get_merkle_tree_root(const size_t round_idx) const { - return m_query_proofs[0/*query_idx*/][round_idx].get_root(); // Since all queries in the same round share the same root, we can just return root for query index 0 of the current round + return m_query_proofs[0 /*query_idx*/][round_idx] + .get_root(); // Since all queries in the same round share the same root, we can just return root for query index + // 0 of the current round } /** diff --git a/icicle/include/icicle/fri/fri_transcript.h b/icicle/include/icicle/fri/fri_transcript.h index f21b395652..377409ee80 100644 --- a/icicle/include/icicle/fri/fri_transcript.h +++ b/icicle/include/icicle/fri/fri_transcript.h @@ -244,9 +244,7 @@ namespace icicle { uint64_t bytes_to_uint_64(const std::vector& data) { uint64_t result = 0; - if (data.size() < sizeof(uint64_t)) { - ICICLE_LOG_ERROR << "Insufficient data size for conversion to uint64_t"; - } + if (data.size() < sizeof(uint64_t)) { ICICLE_LOG_ERROR << "Insufficient data size for conversion to uint64_t"; } std::memcpy(&result, data.data(), sizeof(uint64_t)); return result; } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index cc2857a5b3..4ef83da373 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -27,7 +27,7 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -67,7 +67,7 @@ namespace icicle { std::vector merkle_trees; merkle_trees.reserve(fold_rounds); size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); size_t first_merkle_tree_height = std::ceil(log_input_size / std::log2(compress_hash_arity)) + 1; std::vector layer_hashes(first_merkle_tree_height, merkle_tree_compress_hash); layer_hashes[0] = merkle_tree_leaves_hash; @@ -85,32 +85,33 @@ namespace icicle { #endif // EXT_FIELD -eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, const size_t folding_factor, const size_t compress_hash_arity){ - if (nof_queries <= 0 || nof_queries > (input_size / folding_factor)) { - ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/folding_factor"; - return eIcicleError::INVALID_ARGUMENT; - } - if (folding_factor != 2) { - ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when - // supporting other folding factors - return eIcicleError::INVALID_ARGUMENT; - } - if (input_size == 0 || (input_size & (input_size - 1)) != 0) { - ICICLE_LOG_ERROR << "input_size must be a power of 2. input_size = "<< input_size; - return eIcicleError::INVALID_ARGUMENT; - } - if (folding_factor % compress_hash_arity != 0) { - ICICLE_LOG_ERROR << "folding_factor must be divisible by compress_hash_arity. " - << "folding_factor = " << folding_factor << ", compress_hash_arity = " << compress_hash_arity; - return eIcicleError::INVALID_ARGUMENT; - } - if (compress_hash_arity != 2) { - ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; - return eIcicleError::INVALID_ARGUMENT; + eIcicleError check_if_valid( + const size_t nof_queries, const size_t input_size, const size_t folding_factor, const size_t compress_hash_arity) + { + if (nof_queries <= 0 || nof_queries > (input_size / folding_factor)) { + ICICLE_LOG_ERROR << "Number of queries must be > 0 and < input_size/folding_factor"; + return eIcicleError::INVALID_ARGUMENT; + } + if (folding_factor != 2) { + ICICLE_LOG_ERROR << "Currently only folding factor of 2 is supported"; // TODO SHANIE (future) - remove when + // supporting other folding factors + return eIcicleError::INVALID_ARGUMENT; + } + if (input_size == 0 || (input_size & (input_size - 1)) != 0) { + ICICLE_LOG_ERROR << "input_size must be a power of 2. input_size = " << input_size; + return eIcicleError::INVALID_ARGUMENT; + } + if (folding_factor % compress_hash_arity != 0) { + ICICLE_LOG_ERROR << "folding_factor must be divisible by compress_hash_arity. " + << "folding_factor = " << folding_factor << ", compress_hash_arity = " << compress_hash_arity; + return eIcicleError::INVALID_ARGUMENT; + } + if (compress_hash_arity != 2) { + ICICLE_LOG_ERROR << "Currently only compress hash arity of 2 is supported"; + return eIcicleError::INVALID_ARGUMENT; + } + return eIcicleError::SUCCESS; } - return eIcicleError::SUCCESS; -} - template <> eIcicleError get_fri_proof_mt( @@ -123,8 +124,10 @@ eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, c const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - eIcicleError err = check_if_valid(fri_config.nof_queries, input_size, fri_config.folding_factor, merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size()); - if (err != eIcicleError::SUCCESS){ return err; } + eIcicleError err = check_if_valid( + fri_config.nof_queries, input_size, fri_config.folding_factor, + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size()); + if (err != eIcicleError::SUCCESS) { return err; } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, @@ -145,11 +148,12 @@ eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, c const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); const size_t final_poly_size = fri_proof.get_final_poly_size(); const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - + size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - eIcicleError err = check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); - if (err != eIcicleError::SUCCESS){ return err; } + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = + check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS) { return err; } Fri verifier_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); @@ -169,9 +173,10 @@ eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, c FriProof& fri_proof /* OUT */) { size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - eIcicleError err = check_if_valid(fri_config.nof_queries, input_size, fri_config.folding_factor, compress_hash_arity); - if (err != eIcicleError::SUCCESS){ return err; } + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = + check_if_valid(fri_config.nof_queries, input_size, fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS) { return err; } const size_t log_input_size = std::log2(input_size); Fri prover_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, @@ -194,9 +199,10 @@ eIcicleError check_if_valid(const size_t nof_queries, const size_t input_size, c const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); size_t compress_hash_arity = - merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); - eIcicleError err = check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); - if (err != eIcicleError::SUCCESS){ return err; } + merkle_tree_compress_hash.default_input_chunk_size() / merkle_tree_compress_hash.output_size(); + eIcicleError err = + check_if_valid(fri_config.nof_queries, (1 << log_input_size), fri_config.folding_factor, compress_hash_arity); + if (err != eIcicleError::SUCCESS) { return err; } Fri verifier_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 80afe7a943..d0a1a42dee 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -701,11 +701,9 @@ TYPED_TEST(FieldTest, FriShouldFailCases) const size_t stopping_degree = stopping_size - 1; const uint64_t output_store_min_layer = 0; - auto run = [stopping_degree, output_store_min_layer, pow_bits]( const std::string& dev_type, const size_t nof_queries, const size_t folding_factor, const size_t log_domain_size, const size_t merkle_tree_arity, const size_t input_size) { - // Generate input polynomial evaluations auto scalars = std::make_unique(input_size); TypeParam::rand_host_many(scalars.get(), input_size); @@ -758,43 +756,59 @@ TYPED_TEST(FieldTest, FriShouldFailCases) // Reference Device // Test invalid nof_queries - run(IcicleTestBase::reference_device(), 0 /*nof_queries*/, 2 /*folding_factor*/, log_input_size /*log_domain_size*/, 2 /*merkle_tree_arity*/, 1 << log_input_size /*input_size*/); - run(IcicleTestBase::main_device(), (1< Date: Wed, 12 Mar 2025 15:25:32 +0200 Subject: [PATCH 118/127] update documentation --- .../version-3.5.0/icicle/primitives/fri.md | 104 +++++++++++------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md index 61d63723f0..7ecfa790d3 100644 --- a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md +++ b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md @@ -1,4 +1,4 @@ -# Sumcheck API Documentation +# FRI API Documentation ## Overview The Fast Reed-Solomon Interactive Oracle Proof of Proximity (FRI) protocol is used to efficiently verify that a given polynomial has a bounded degree. @@ -41,22 +41,15 @@ The polynomial size must be a power of 2 and is passed to the protocol in evalua ## C++ API -A Fri object can be created using the following function: -```cpp -// icicle/fri/fri.h -Fri fri_instance = create_fri( - input_size, folding_factor, stopping_degree, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer) -``` - -* `merkle_tree_leaves_hash`, `merkle_tree_compress_hash` and `output_store_min_layer` refer to the hashes used in the Merkle Trees built in each round of the folding. For further information about ICICLE's Merkle Trees, see [Merkle-Tree documentation](./merkle.md) and [Hash documentation](./merkle.md). -* Currently, only a folding factor of 2 is supported. -* Currently, only arity 2 is supported for `merkle_tree_compress_hash`. +### Configuration structs There are two key configuration structs related to the Fri protocol. -### FriConfig +#### FriConfig The `FriConfig` struct is used to specify parameters for the FRI protocol. It contains the following fields: - **`stream: icicleStreamHandle`**: The CUDA stream for asynchronous execution. If `nullptr`, the default stream is used. +- **`folding_factor: size_t`**: The factor by which the codeword is folded in each round. +- **`stopping_degree: size_t`**: The minimal polynomial degree at which folding stops. - **`pow_bits: size_t`**: Number of leading zeros required for proof-of-work. If set, the optional proof-of-work phase is executed. - **`nof_queries: size_t`**: Number of queries computed for each folded layer of FRI. - **`are_inputs_on_device: bool`**: If true, the input polynomials are stored on the device (e.g., GPU); otherwise, they remain on the host (e.g., CPU). @@ -68,15 +61,18 @@ The default values are: // icicle/fri/fri_config.h struct FriConfig { icicleStreamHandle stream = nullptr; - size_t pow_bits = 0; - size_t nof_queries = 1; - bool are_inputs_on_device =false; + size_t folding_factor = 2; + size_t stopping_degree = 0; + size_t pow_bits = 16; + size_t nof_queries = 100; + bool are_inputs_on_device = false; bool is_async = false; ConfigExtension* ext = nullptr; }; ``` +> **_NOTE:_** Currently, only a folding factor of 2 is supported. -### FriTranscriptConfig +#### FriTranscriptConfig The `FriTranscriptConfig` class is used to specify parameters for the Fiat-Shamir scheme used by the FRI protocol. It contains the following fields: - **`hasher: Hash`**: The hash function used to generate randomness for Fiat-Shamir. - **`domain_separator_label: std::vector`** @@ -86,7 +82,7 @@ The `FriTranscriptConfig` class is used to specify parameters for the - **`public_state: std::vector`** - **`seed_rng: TypeParam`**: The seed for initializing the RNG. -Note that the encoding is little endian. +> **_NOTE:_** the encoding is little endian. There are three constructors for `FriTranscriptConfig`: @@ -141,25 +137,40 @@ class FriProof The class has a default constructor `FriProof()` that takes no arguments. -The proof can be generated using the get_proof method from the `Fri` object: -```cpp -// icicle/fri/fri.h -eIcicleError get_proof( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - const F* input_data, - FriProof& fri_proof /* OUT */) const -``` +To generate a FRI proof using the Merkle Tree commit scheme, use one of the following functions: +1. **Directly call `get_fri_proof_mt`:** + ```cpp + template + eIcicleError get_fri_proof_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + const size_t input_size, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + FriProof& fri_proof /* OUT */); + ``` +2. **Use the `FRI` wrapper, which internally calls `get_fri_proof_mt`:** + ```cpp + FRI::get_proof_mt( ... ); + ``` + This approach calls `get_fri_proof_mt` internally but provides a more structured way to access it. - **`input_data: const F*`**: Evaluations of The input polynomial. - **`fri_proof: FriProof&`**: The output `FriProof` object containing the generated proof. +* `merkle_tree_leaves_hash`, `merkle_tree_compress_hash` and `output_store_min_layer` refer to the hashes used in the Merkle Trees built in each round of the folding. For further information about ICICLE's Merkle Trees, see [Merkle-Tree documentation](./merkle.md) and [Hash documentation](./merkle.md). + +> **_NOTE:_** `folding_factor` must be divisible by `merkle_tree_compress_hash`. + -Note: An NTT domain is used for proof generation, so before generating a proof, an NTT domain of at least the input_data size must be initialized. For more information see [NTT documentation](./ntt.md). +> **_NOTE:_** An NTT domain is used for proof generation, so before generating a proof, an NTT domain of at least the input_data size must be initialized. For more information see [NTT documentation](./ntt.md). ```cpp NTTInitDomainConfig init_domain_config = default_ntt_init_domain_config(); ntt_init_domain(scalar_t::omega(log_input_size), init_domain_config) ``` +::: #### Example: Generating a Proof @@ -173,9 +184,6 @@ uint64_t merkle_tree_arity = 2; Hash hash = Keccak256::create(sizeof(TypeParam)); // hash element -> 32B Hash compress = Keccak256::create(merkle_tree_arity * hash.output_size()); // hash every 64B to 32B -// Create fri -Fri prover_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); - // set transcript config const char* domain_separator_label = "domain_separator_label"; const char* round_challenge_label = "round_challenge_label"; @@ -192,11 +200,15 @@ FriTranscriptConfig transcript_config( FriConfig fri_config; fri_config.nof_queries = 100; fri_config.pow_bits = 16; +fri_config.folding_factor = 2; +fri_config.stopping_degree = 0; FriProof fri_proof; // get fri proof -prover_fri.get_proof(fri_config, transcript_config, scalars.get(), fri_proof); +eIcicleError err = FRI::get_proof_mt( + fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); +ICICLE_CHECK(err); // Release ntt domain ntt_release_domain(); @@ -204,26 +216,36 @@ ntt_release_domain(); ### Verifying Fri Proofs -To verify the proof, the verifier should use the verify method of the `Fri` object: +To verify the FRI proof using the Merkle Tree commit scheme, use one of the following functions: +1. **Directly call `verify_fri_mt`**: ```cpp // icicle/fri/fri.h -eIcicleError verify( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, - bool& valid /* OUT */) const +template +eIcicleError verify_fri_mt( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + FriProof& fri_proof, + Hash merkle_tree_leaves_hash, + Hash merkle_tree_compress_hash, + const uint64_t output_store_min_layer, + bool& valid /* OUT */); ``` +2. **Use the `FRI` wrapper, which internally calls `verify_fri_mt`:** + ```cpp + FRI::verify_mt( ... ); + ``` + > **_NOTE:_** `FriConfig` and `FriTranscriptConfig` used for generating the proof must be identical to the one used for verification. #### Example: Verifying a Proof ```cpp -Fri verifier_fri = create_fri(input_size, folding_factor, stopping_degree, hash, compress, output_store_min_layer); bool valid = false; -verifier_fri.verify(fri_config, transcript_config, fri_proof, valid); - +eIcicleError err = FRI::verify_mt( + fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); +ICICLE_CHECK(err); ASSERT_EQ(true, valid); // Ensure proof verification succeeds ``` -After calling `verifier_fri.verify`, the variable `valid` will be set to `true` if the proof is valid, and `false` otherwise. \ No newline at end of file +After calling `FRI::verify_mt`, the variable `valid` will be set to `true` if the proof is valid, and `false` otherwise. \ No newline at end of file From c2052914a41992ff898607efbf5aefa2eb8fbbbe Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 15:46:08 +0200 Subject: [PATCH 119/127] copy MerkleTree instead of using a reference (The MerkleTree class only holds a shared_ptr to MerkleTreeBackend, so copying is ok) --- icicle/backend/cpu/include/cpu_fri_backend.h | 2 +- icicle/backend/cpu/include/cpu_fri_rounds.h | 2 +- icicle/backend/cpu/src/field/cpu_fri.cpp | 2 +- icicle/src/fri/fri.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/icicle/backend/cpu/include/cpu_fri_backend.h b/icicle/backend/cpu/include/cpu_fri_backend.h index 5c32d7395d..667fe9ecca 100644 --- a/icicle/backend/cpu/include/cpu_fri_backend.h +++ b/icicle/backend/cpu/include/cpu_fri_backend.h @@ -24,7 +24,7 @@ namespace icicle { * @param merkle_trees A vector of MerkleTrees, tree per FRI round. */ CpuFriBackend(const size_t folding_factor, const size_t stopping_degree, std::vector merkle_trees) - : FriBackend(folding_factor, stopping_degree, std::move(merkle_trees)), + : FriBackend(folding_factor, stopping_degree, merkle_trees), m_nof_fri_rounds(this->m_merkle_trees.size()), m_log_input_size(this->m_merkle_trees.size() + std::log2(static_cast(stopping_degree + 1))), m_input_size(pow(2, m_log_input_size)), m_fri_rounds(this->m_merkle_trees, m_log_input_size) diff --git a/icicle/backend/cpu/include/cpu_fri_rounds.h b/icicle/backend/cpu/include/cpu_fri_rounds.h index 4df4a7fd46..594e5689e3 100644 --- a/icicle/backend/cpu/include/cpu_fri_rounds.h +++ b/icicle/backend/cpu/include/cpu_fri_rounds.h @@ -62,7 +62,7 @@ namespace icicle { std::vector> m_rounds_evals; // Holds MerkleTree for each round. m_merkle_trees[i] is the tree for round i. - std::vector& m_merkle_trees; + std::vector m_merkle_trees; }; } // namespace icicle diff --git a/icicle/backend/cpu/src/field/cpu_fri.cpp b/icicle/backend/cpu/src/field/cpu_fri.cpp index c7b70cd6f1..4217988097 100644 --- a/icicle/backend/cpu/src/field/cpu_fri.cpp +++ b/icicle/backend/cpu/src/field/cpu_fri.cpp @@ -14,7 +14,7 @@ namespace icicle { std::vector merkle_trees, std::shared_ptr>& backend /*OUT*/) { - backend = std::make_shared>(folding_factor, stopping_degree, std::move(merkle_trees)); + backend = std::make_shared>(folding_factor, stopping_degree, merkle_trees); return eIcicleError::SUCCESS; } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 4ef83da373..b646820275 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -37,7 +37,7 @@ namespace icicle { layer_hashes.pop_back(); } std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); + ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); // The MerkleTree class only holds a shared_ptr to MerkleTreeBackend, so copying is lightweight. Fri fri{backend}; return fri; @@ -77,7 +77,7 @@ namespace icicle { layer_hashes.pop_back(); } std::shared_ptr> backend; - ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, std::move(merkle_trees), backend)); + ICICLE_CHECK(FriExtFieldDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); Fri fri{backend}; return fri; From 39ab65f05cf4d1206840e372d894ac928c8486b8 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 15:47:58 +0200 Subject: [PATCH 120/127] format --- icicle/src/fri/fri.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index b646820275..230b5c3a7f 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -37,7 +37,9 @@ namespace icicle { layer_hashes.pop_back(); } std::shared_ptr> backend; - ICICLE_CHECK(FriDispatcher::execute(folding_factor, stopping_degree, merkle_trees, backend)); // The MerkleTree class only holds a shared_ptr to MerkleTreeBackend, so copying is lightweight. + ICICLE_CHECK(FriDispatcher::execute( + folding_factor, stopping_degree, merkle_trees, + backend)); // The MerkleTree class only holds a shared_ptr to MerkleTreeBackend, so copying is lightweight. Fri fri{backend}; return fri; From d69b33ec015523887f846d4f90e564a1cb5bffb6 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 17:05:09 +0200 Subject: [PATCH 121/127] FRI struct replaced with fri_merkle_tree namespace. const added to FriProof when calling verify --- icicle/include/icicle/fri/fri.h | 24 ++++++++++++------------ icicle/include/icicle/fri/fri_proof.h | 16 +++++++++++++++- icicle/src/fri/fri.cpp | 4 ++-- icicle/tests/test_field_api.cpp | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 3bd4e10da9..ffa412f37b 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -72,13 +72,13 @@ namespace icicle { eIcicleError verify_fri_mt( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, + const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, bool& valid /* OUT */); - struct FRI { + namespace fri_merkle_tree { template inline static eIcicleError get_proof_mt( const FriConfig& fri_config, @@ -99,7 +99,7 @@ namespace icicle { inline static eIcicleError verify_mt( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, + const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, @@ -156,7 +156,7 @@ namespace icicle { eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, + const FriProof& fri_proof, bool& valid /* OUT */) const { if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } @@ -201,7 +201,7 @@ namespace icicle { * @param alpha_values (OUT) Vector to store computed alpha values. */ eIcicleError update_transcript_and_generate_alphas_from_proof( - FriProof& fri_proof, + const FriProof& fri_proof, FriTranscript& transcript, const size_t nof_fri_rounds, std::vector& alpha_values) const @@ -228,7 +228,7 @@ namespace icicle { * @param pow_valid (OUT) Set to true if PoW verification succeeds. */ void check_pow_nonce_and_set_to_transcript( - FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const + const FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const { uint64_t proof_pow_nonce = fri_proof.get_pow_nonce(); pow_valid = transcript.verify_pow(proof_pow_nonce, fri_config.pow_bits); @@ -282,7 +282,7 @@ namespace icicle { * @return True if the collinearity check passes, false otherwise. */ bool collinearity_check( - FriProof& fri_proof, + const FriProof& fri_proof, const std::byte* leaf_data, const std::byte* leaf_data_sym, const size_t query_idx, @@ -312,7 +312,7 @@ namespace icicle { return false; } } else { - MerkleProof& proof_ref_folded = fri_proof.get_query_proof_slot(2 * query_idx, round_idx + 1); + const MerkleProof& proof_ref_folded = fri_proof.get_query_proof_slot(2 * query_idx, round_idx + 1); const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); if (leaf_data_folded_f != folded) { @@ -333,7 +333,7 @@ namespace icicle { * @return True if both proofs are valid, false otherwise. */ bool verify_merkle_proofs_for_query( - const MerkleTree& current_round_tree, MerkleProof& proof_ref, MerkleProof& proof_ref_sym) const + const MerkleTree& current_round_tree, const MerkleProof& proof_ref, const MerkleProof& proof_ref_sym) const { bool merkle_proof_valid = false; eIcicleError err = current_round_tree.verify(proof_ref, merkle_proof_valid); @@ -373,7 +373,7 @@ namespace icicle { */ eIcicleError verify_queries( - FriProof& fri_proof, + const FriProof& fri_proof, const size_t nof_queries, std::vector& queries_indicies, std::vector& alpha_values, @@ -386,8 +386,8 @@ namespace icicle { size_t query = queries_indicies[query_idx]; for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; - MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); - MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); + const MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); + const MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 0fd49a1ecb..8f93a4c792 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -48,7 +48,7 @@ namespace icicle { /** * @brief Get a reference to a specific Merkle proof for a given query index in a specific FRI round. Each query - * includes a proof for two values per round.. + * includes a proof for two values per round. * * This function returns a reference to a pre-allocated Merkle proof in the `m_query_proofs` array. * The proof is initially empty and will be populated by another function responsible for generating @@ -66,6 +66,20 @@ namespace icicle { return m_query_proofs[query_idx][round_idx]; } + /** + * @brief Get a const reference to a specific Merkle proof for a given query index in a specific FRI round. Each query + * includes a proof for two values per round. + */ + + const MerkleProof& get_query_proof_slot(const size_t query_idx, const size_t round_idx) const + { + if (query_idx < 0 || query_idx >= m_query_proofs.size()) { throw std::out_of_range("Invalid query index"); } + if (round_idx < 0 || round_idx >= m_query_proofs[query_idx].size()) { + throw std::out_of_range("Invalid round index"); + } + return m_query_proofs[query_idx][round_idx]; + } + /** * @brief Returns a pair containing the pointer to the merkle tree root data and its size. * @return A pair of (root data pointer, root size). diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 230b5c3a7f..398d64b6ea 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -141,7 +141,7 @@ namespace icicle { eIcicleError verify_fri_mt( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, + const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, @@ -190,7 +190,7 @@ namespace icicle { eIcicleError verify_fri_mt( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, - FriProof& fri_proof, + const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, const uint64_t output_store_min_layer, diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index d0a1a42dee..a172eb02c7 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -670,7 +670,7 @@ TYPED_TEST(FieldTest, Fri) oss << dev_type << " FRI proof"; } START_TIMER(FRIPROOF_sync) - eIcicleError err = FRI::get_proof_mt( + eIcicleError err = fri_merkle_tree::get_proof_mt( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); ICICLE_CHECK(err); END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); @@ -680,7 +680,7 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; - err = FRI::verify_mt( + err = fri_merkle_tree::verify_mt( fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); From 4da615c48db4430aa03860e6a6abc2130987def2 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 17:11:17 +0200 Subject: [PATCH 122/127] "output_store_min_layer" removed from verify --- docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md | 3 +-- icicle/include/icicle/fri/fri.h | 7 ++----- icicle/src/fri/fri.cpp | 6 ++---- icicle/tests/test_field_api.cpp | 4 ++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md index 7ecfa790d3..e860b4c260 100644 --- a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md +++ b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md @@ -228,7 +228,6 @@ eIcicleError verify_fri_mt( FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, bool& valid /* OUT */); ``` @@ -243,7 +242,7 @@ eIcicleError verify_fri_mt( ```cpp bool valid = false; eIcicleError err = FRI::verify_mt( - fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); + fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); // Ensure proof verification succeeds ``` diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index ffa412f37b..6bcd59b56e 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -35,7 +35,7 @@ namespace icicle { * @param input_size The number of evaluations in the input polynomial. * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer (Optional) The layer at which to store partial results. Default = 0. + * @param output_store_min_layer The layer at which to store partial results. Default = 0. * @param fri_proof (OUT) The generated FRI proof. * @return `eIcicleError` indicating success or failure of the proof generation. */ @@ -63,7 +63,6 @@ namespace icicle { * @param fri_proof The FRI proof to be verified. * @param merkle_tree_leaves_hash The hash function used for Merkle tree leaves. * @param merkle_tree_compress_hash The hash function used for compressing Merkle tree nodes. - * @param output_store_min_layer The layer at which to store partial results. * @param valid (OUT) Boolean flag indicating whether the proof is valid. * @return `eIcicleError` indicating success or failure of the verification process. */ @@ -75,7 +74,6 @@ namespace icicle { const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, bool& valid /* OUT */); namespace fri_merkle_tree { @@ -102,12 +100,11 @@ namespace icicle { const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, bool& valid /* OUT */) { return verify_fri_mt( fri_config, fri_transcript_config, fri_proof, merkle_tree_leaves_hash, merkle_tree_compress_hash, - output_store_min_layer, valid /* OUT */); + valid /* OUT */); } }; diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 398d64b6ea..8881e601e7 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -144,7 +144,6 @@ namespace icicle { const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, bool& valid /* OUT */) { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); @@ -158,7 +157,7 @@ namespace icicle { if (err != eIcicleError::SUCCESS) { return err; } Fri verifier_fri = create_fri( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, - merkle_tree_compress_hash, output_store_min_layer); + merkle_tree_compress_hash, 0); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } @@ -193,7 +192,6 @@ namespace icicle { const FriProof& fri_proof, Hash merkle_tree_leaves_hash, Hash merkle_tree_compress_hash, - const uint64_t output_store_min_layer, bool& valid /* OUT */) { const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); @@ -207,7 +205,7 @@ namespace icicle { if (err != eIcicleError::SUCCESS) { return err; } Fri verifier_fri = create_fri_ext( log_input_size, fri_config.folding_factor, fri_config.stopping_degree, merkle_tree_leaves_hash, - merkle_tree_compress_hash, output_store_min_layer); + merkle_tree_compress_hash, 0); return verifier_fri.verify(fri_config, fri_transcript_config, fri_proof, valid); } #endif diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index a172eb02c7..04af8c3661 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -681,7 +681,7 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; err = fri_merkle_tree::verify_mt( - fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); + fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); }; @@ -748,7 +748,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) // ===== Verifier side ====== bool valid = false; error = verify_fri_mt( - fri_config, transcript_config, fri_proof, hash, compress, output_store_min_layer, valid); + fri_config, transcript_config, fri_proof, hash, compress, valid); ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); From 56ed067df2d6454cb3807b0afd7609e2eb373172 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 17:26:55 +0200 Subject: [PATCH 123/127] change names to get_fri_proof_merkle_tree, fri_merkle_tree::prove, verify_fri_merkle_tree, fri_merkle_tree::verify --- .../version-3.5.0/icicle/primitives/fri.md | 24 +++++++++---------- icicle/include/icicle/fri/fri.h | 18 +++++--------- icicle/src/fri/fri.cpp | 8 +++---- icicle/tests/test_field_api.cpp | 8 +++---- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md index e860b4c260..eca1d39939 100644 --- a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md +++ b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md @@ -138,10 +138,10 @@ class FriProof The class has a default constructor `FriProof()` that takes no arguments. To generate a FRI proof using the Merkle Tree commit scheme, use one of the following functions: -1. **Directly call `get_fri_proof_mt`:** +1. **Directly call `get_fri_proof_merkle_tree`:** ```cpp template - eIcicleError get_fri_proof_mt( + eIcicleError get_fri_proof_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, @@ -151,11 +151,11 @@ To generate a FRI proof using the Merkle Tree commit scheme, use one of the foll const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */); ``` -2. **Use the `FRI` wrapper, which internally calls `get_fri_proof_mt`:** +2. **Use the `fri_merkle_tree` namespace, which internally calls `get_fri_proof_merkle_tree`:** ```cpp - FRI::get_proof_mt( ... ); + fri_merkle_tree::prove( ... ); ``` - This approach calls `get_fri_proof_mt` internally but provides a more structured way to access it. + This approach calls `get_fri_proof_merkle_tree` internally but provides a more structured way to access it. - **`input_data: const F*`**: Evaluations of The input polynomial. - **`fri_proof: FriProof&`**: The output `FriProof` object containing the generated proof. @@ -206,7 +206,7 @@ fri_config.stopping_degree = 0; FriProof fri_proof; // get fri proof -eIcicleError err = FRI::get_proof_mt( +eIcicleError err = fri_merkle_tree::prove( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); ICICLE_CHECK(err); @@ -218,11 +218,11 @@ ntt_release_domain(); To verify the FRI proof using the Merkle Tree commit scheme, use one of the following functions: -1. **Directly call `verify_fri_mt`**: +1. **Directly call `verify_fri_merkle_tree`**: ```cpp // icicle/fri/fri.h template -eIcicleError verify_fri_mt( +eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, FriProof& fri_proof, @@ -231,9 +231,9 @@ eIcicleError verify_fri_mt( bool& valid /* OUT */); ``` -2. **Use the `FRI` wrapper, which internally calls `verify_fri_mt`:** +2. **Use the `fri_merkle_tree` namespac, which internally calls `verify_fri_merkle_tree`:** ```cpp - FRI::verify_mt( ... ); + fri_merkle_tree::verify( ... ); ``` > **_NOTE:_** `FriConfig` and `FriTranscriptConfig` used for generating the proof must be identical to the one used for verification. @@ -241,10 +241,10 @@ eIcicleError verify_fri_mt( #### Example: Verifying a Proof ```cpp bool valid = false; -eIcicleError err = FRI::verify_mt( +eIcicleError err = fri_merkle_tree::verify( fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); // Ensure proof verification succeeds ``` -After calling `FRI::verify_mt`, the variable `valid` will be set to `true` if the proof is valid, and `false` otherwise. \ No newline at end of file +After calling `fri_merkle_tree::verify`, the variable `valid` will be set to `true` if the proof is valid, and `false` otherwise. \ No newline at end of file diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 6bcd59b56e..1684832932 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -16,12 +16,6 @@ namespace icicle { - /** - * @brief Forward declaration for the FRI class template. - */ - template - class Fri; - /** * @brief Generates a FRI proof using a binary Merkle tree structure. * @@ -41,7 +35,7 @@ namespace icicle { */ template - eIcicleError get_fri_proof_mt( + eIcicleError get_fri_proof_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, @@ -68,7 +62,7 @@ namespace icicle { */ template - eIcicleError verify_fri_mt( + eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, @@ -78,7 +72,7 @@ namespace icicle { namespace fri_merkle_tree { template - inline static eIcicleError get_proof_mt( + inline static eIcicleError prove( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, @@ -88,13 +82,13 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - return get_fri_proof_mt( + return get_fri_proof_merkle_tree( fri_config, fri_transcript_config, input_data, input_size, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer, fri_proof /* OUT */); } template - inline static eIcicleError verify_mt( + inline static eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, @@ -102,7 +96,7 @@ namespace icicle { Hash merkle_tree_compress_hash, bool& valid /* OUT */) { - return verify_fri_mt( + return verify_fri_merkle_tree( fri_config, fri_transcript_config, fri_proof, merkle_tree_leaves_hash, merkle_tree_compress_hash, valid /* OUT */); } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 8881e601e7..a8af45448f 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -116,7 +116,7 @@ namespace icicle { } template <> - eIcicleError get_fri_proof_mt( + eIcicleError get_fri_proof_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const scalar_t* input_data, @@ -138,7 +138,7 @@ namespace icicle { } template <> - eIcicleError verify_fri_mt( + eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, @@ -163,7 +163,7 @@ namespace icicle { #ifdef EXT_FIELD template <> - eIcicleError get_fri_proof_mt( + eIcicleError get_fri_proof_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const extension_t* input_data, @@ -186,7 +186,7 @@ namespace icicle { } template <> - eIcicleError verify_fri_mt( + eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 04af8c3661..1ba995f37a 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -670,7 +670,7 @@ TYPED_TEST(FieldTest, Fri) oss << dev_type << " FRI proof"; } START_TIMER(FRIPROOF_sync) - eIcicleError err = fri_merkle_tree::get_proof_mt( + eIcicleError err = fri_merkle_tree::prove( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); ICICLE_CHECK(err); END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); @@ -680,7 +680,7 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; - err = fri_merkle_tree::verify_mt( + err = fri_merkle_tree::verify( fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); @@ -738,7 +738,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) fri_config.stopping_degree = stopping_degree; FriProof fri_proof; - eIcicleError error = get_fri_proof_mt( + eIcicleError error = get_fri_proof_merkle_tree( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); // Release domain @@ -747,7 +747,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== bool valid = false; - error = verify_fri_mt( + error = verify_fri_merkle_tree( fri_config, transcript_config, fri_proof, hash, compress, valid); ASSERT_EQ(true, valid); } From d80d1806c758c37434f4d1ce33cee9c306bc0277 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 17:31:11 +0200 Subject: [PATCH 124/127] moved Fri class from fri.h to fri.cpp. --- icicle/include/icicle/fri/fri.h | 300 ------------------------ icicle/include/icicle/fri/fri_config.h | 12 +- icicle/src/fri/fri.cpp | 301 +++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 306 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 1684832932..75977c6394 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -102,304 +102,4 @@ namespace icicle { } }; - /** - * @brief Class for performing FRI operations. - * - * This class provides a high-level interface for constructing and managing a FRI proof. - * - * @tparam F The field type used in the FRI protocol. - */ - template - class Fri - { - public: - /** - * @brief Constructor for the Fri class. - * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. - */ - explicit Fri(std::shared_ptr> backend) : m_backend(std::move(backend)) {} - - /** - * @brief Generate a FRI proof from the given polynomial evaluations (or input data). - * @param fri_config Configuration for FRI operations (e.g., proof-of-work, queries). - * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). - * @param input_data Evaluations of the input polynomial. - * @param fri_proof Reference to a FriProof object (output). - * @return An eIcicleError indicating success or failure. - */ - eIcicleError get_proof( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - const F* input_data, - FriProof& fri_proof /* OUT */) const - { - return m_backend->get_proof(fri_config, fri_transcript_config, input_data, fri_proof); - } - - /** - * @brief Verify a FRI proof. - * @param fri_config Configuration for FRI operations. - * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). - * @param fri_proof The proof object to verify. - * @param valid (OUT) Set to true if verification succeeds, false otherwise. - * @return An eIcicleError indicating success or failure. - */ - eIcicleError verify( - const FriConfig& fri_config, - const FriTranscriptConfig& fri_transcript_config, - const FriProof& fri_proof, - bool& valid /* OUT */) const - { - if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } - - const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); - const size_t final_poly_size = fri_proof.get_final_poly_size(); - const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); - - FriTranscript transcript(fri_transcript_config, log_input_size); - std::vector alpha_values(nof_fri_rounds); - eIcicleError err = - update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); - if (err != eIcicleError::SUCCESS) { return err; } - - // Validate proof-of-work - if (fri_config.pow_bits != 0) { - bool pow_valid = false; - check_pow_nonce_and_set_to_transcript(fri_proof, transcript, fri_config, pow_valid); - if (!pow_valid) return eIcicleError::SUCCESS; // return with valid = false - } - - // verify queries - bool queries_valid = false; - std::vector queries_indicies = - transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size, err); - if (err != eIcicleError::SUCCESS) { return err; } - err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); - if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false - - valid = true; - return err; - } - - private: - std::shared_ptr> m_backend; - - /** - * @brief Updates the transcript with Merkle roots and generates alpha values for each round. - * @param fri_proof The proof object containing Merkle roots. - * @param transcript The transcript storing challenges. - * @param nof_fri_rounds Number of FRI rounds. - * @param alpha_values (OUT) Vector to store computed alpha values. - */ - eIcicleError update_transcript_and_generate_alphas_from_proof( - const FriProof& fri_proof, - FriTranscript& transcript, - const size_t nof_fri_rounds, - std::vector& alpha_values) const - { - for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { - auto [root_ptr, root_size] = fri_proof.get_merkle_tree_root(round_idx); - if (root_ptr == nullptr || root_size <= 0) { - ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; - } - std::vector merkle_commit(root_size); - std::memcpy(merkle_commit.data(), root_ptr, root_size); - eIcicleError err; - alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0, err); - if (err != eIcicleError::SUCCESS) { return err; } - } - return eIcicleError::SUCCESS; - } - - /** - * @brief Validates the proof-of-work nonce from the fri_proof and sets it in the transcript. - * @param fri_proof The proof containing the PoW nonce. - * @param transcript The transcript where the nonce is recorded. - * @param fri_config Configuration specifying required PoW bits. - * @param pow_valid (OUT) Set to true if PoW verification succeeds. - */ - void check_pow_nonce_and_set_to_transcript( - const FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const - { - uint64_t proof_pow_nonce = fri_proof.get_pow_nonce(); - pow_valid = transcript.verify_pow(proof_pow_nonce, fri_config.pow_bits); - transcript.set_pow_nonce(proof_pow_nonce); - } - - /** - * @brief Checks if the leaf index from the proof matches the expected index computed based on the transcript random - * generation. - * @param leaf_index Index extracted from the proof. - * @param leaf_index_sym Symmetric index extracted from the proof. - * @param query The query based on the transcript random generation. - * @param round_idx Current FRI round index. - * @param log_input_size Log of the initial input size. - * @return True if indices are consistent, false otherwise. - */ - bool leaf_index_consistency_check( - const uint64_t leaf_index, - const uint64_t leaf_index_sym, - const size_t query, - const size_t round_idx, - const uint32_t log_input_size) const - { - size_t round_size = (1ULL << (log_input_size - round_idx)); - size_t elem_idx = query % round_size; - size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; - if (__builtin_expect(elem_idx != leaf_index, 0)) { - ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; - return false; - } - if (__builtin_expect(elem_idx_sym != leaf_index_sym, 0)) { - ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; - return false; - } - return true; - } - - /** - * @brief Validates collinearity in the folding process for a specific round. - * This ensures that the folded value computed from the queried elements - * matches the expected value in the proof. - * @param fri_proof The proof object containing leaf data. - * @param leaf_data Pointer to the leaf data. - * @param leaf_data_sym Pointer to the symmetric leaf data. - * @param query_idx Index of the query being verified. - * @param query The query based on the transcript random generation. - * @param round_idx Current FRI round index. - * @param alpha_values Vector of alpha values for each round. - * @param log_input_size Log of the initial input size. - * @param primitive_root_inv Inverse primitive root used in calculations. - * @return True if the collinearity check passes, false otherwise. - */ - bool collinearity_check( - const FriProof& fri_proof, - const std::byte* leaf_data, - const std::byte* leaf_data_sym, - const size_t query_idx, - const size_t query, - const size_t round_idx, - std::vector& alpha_values, - const uint32_t log_input_size, - const S primitive_root_inv) const - { - const F& leaf_data_f = *reinterpret_cast(leaf_data); - const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); - size_t round_size = (1ULL << (log_input_size - round_idx)); - size_t elem_idx = query % round_size; - F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); - F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * - S::pow(primitive_root_inv, elem_idx * (1 << round_idx)); - F alpha = alpha_values[round_idx]; - F folded = l_even + (alpha * l_odd); - - const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); - const size_t final_poly_size = fri_proof.get_final_poly_size(); - if (round_idx == nof_fri_rounds - 1) { - const F* final_poly = fri_proof.get_final_poly(); - if (final_poly[query % final_poly_size] != folded) { - ICICLE_LOG_ERROR << " (last round) Collinearity check failed for query=" << query - << ", query_idx=" << query_idx << ", round=" << round_idx; - return false; - } - } else { - const MerkleProof& proof_ref_folded = fri_proof.get_query_proof_slot(2 * query_idx, round_idx + 1); - const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); - const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); - if (leaf_data_folded_f != folded) { - ICICLE_LOG_ERROR << "Collinearity check failed. query=" << query << ", query_idx=" << query_idx - << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" - << leaf_data_folded_f; - return false; - } - } - return true; - } - - /** - * @brief Verifies Merkle proofs for a given query. - * @param current_round_tree The Merkle tree corresponding to the round. - * @param proof_ref Merkle proof for the query. - * @param proof_ref_sym Merkle proof for the symmetric query. - * @return True if both proofs are valid, false otherwise. - */ - bool verify_merkle_proofs_for_query( - const MerkleTree& current_round_tree, const MerkleProof& proof_ref, const MerkleProof& proof_ref_sym) const - { - bool merkle_proof_valid = false; - eIcicleError err = current_round_tree.verify(proof_ref, merkle_proof_valid); - if (err != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "Merkle path verification returned err"; - return false; - } - if (!merkle_proof_valid) { - ICICLE_LOG_ERROR << "Merkle path verification failed"; - return false; - } - - merkle_proof_valid = false; - eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, merkle_proof_valid); - if (err_sym != eIcicleError::SUCCESS) { - ICICLE_LOG_ERROR << "Merkle path verification returned err"; - return false; - } - if (!merkle_proof_valid) { - ICICLE_LOG_ERROR << "Merkle path sym verification failed"; - return false; - } - return true; - } - - /** - * @brief Verifies all queries in the FRI proof. This includes: - * - Checking Merkle proofs for consistency. - * - Ensuring leaf indices in the proof match those derived from the transcript. - * - Validating collinearity in the folding process. - * @param fri_proof The proof object to verify. - * @param nof_queries The number of queries. - * @param queries_indices List of query indices to check. - * @param alpha_values Vector of alpha values for each round. - * @param queries_valid (OUT) Set to true if all queries pass verification. - * @return An eIcicleError indicating success or failure. - */ - - eIcicleError verify_queries( - const FriProof& fri_proof, - const size_t nof_queries, - std::vector& queries_indicies, - std::vector& alpha_values, - bool& queries_valid) const - { - const uint32_t log_input_size = - fri_proof.get_nof_fri_rounds() + static_cast(std::log2(fri_proof.get_final_poly_size())); - S primitive_root_inv = S::omega_inv(log_input_size); - for (size_t query_idx = 0; query_idx < nof_queries; query_idx++) { - size_t query = queries_indicies[query_idx]; - for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { - MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; - const MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); - const MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); - const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); - const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); - - if (!verify_merkle_proofs_for_query(current_round_tree, proof_ref, proof_ref_sym)) { - return eIcicleError::SUCCESS; // return with queries_valid = false - } - - if (!leaf_index_consistency_check(leaf_index, leaf_index_sym, query, round_idx, log_input_size)) { - return eIcicleError::SUCCESS; // return with queries_valid = false - } - - if (!collinearity_check( - fri_proof, leaf_data, leaf_data_sym, query_idx, query, round_idx, alpha_values, log_input_size, - primitive_root_inv)) { - return eIcicleError::SUCCESS; // return with queries_valid = false - } - } - } - queries_valid = true; - return eIcicleError::SUCCESS; - } - }; - } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index f8358923f3..1cc713d0de 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -14,15 +14,15 @@ namespace icicle { * It also supports backend-specific extensions for customization. */ struct FriConfig { - icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. Default is nullptr. + icicleStreamHandle stream = nullptr; // Stream for asynchronous execution. size_t folding_factor = 2; // The factor by which the codeword is folded in each round. size_t stopping_degree = 0; // The minimal polynomial degree at which folding stops. - size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. Default is 0. - size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. Default is 50. + size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. + size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. bool are_inputs_on_device = - false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). Default is false. - bool is_async = false; // True to run operations asynchronously, false to run synchronously. Default is false. - ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. Default is nullptr. + false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). + bool is_async = false; // True to run operations asynchronously, false to run synchronously. + ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. }; /** diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index a8af45448f..04c9a7167d 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -4,6 +4,307 @@ namespace icicle { + /** + * @brief Class for performing FRI operations. + * + * This class provides a high-level interface for constructing and managing a FRI proof. + * + * @tparam F The field type used in the FRI protocol. + */ + template + class Fri + { + public: + /** + * @brief Constructor for the Fri class. + * @param backend A shared pointer to the backend (FriBackend) responsible for FRI operations. + */ + explicit Fri(std::shared_ptr> backend) : m_backend(std::move(backend)) {} + + /** + * @brief Generate a FRI proof from the given polynomial evaluations (or input data). + * @param fri_config Configuration for FRI operations (e.g., proof-of-work, queries). + * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). + * @param input_data Evaluations of the input polynomial. + * @param fri_proof Reference to a FriProof object (output). + * @return An eIcicleError indicating success or failure. + */ + eIcicleError get_proof( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const F* input_data, + FriProof& fri_proof /* OUT */) const + { + return m_backend->get_proof(fri_config, fri_transcript_config, input_data, fri_proof); + } + + /** + * @brief Verify a FRI proof. + * @param fri_config Configuration for FRI operations. + * @param fri_transcript_config Configuration for encoding/hashing (Fiat-Shamir). + * @param fri_proof The proof object to verify. + * @param valid (OUT) Set to true if verification succeeds, false otherwise. + * @return An eIcicleError indicating success or failure. + */ + eIcicleError verify( + const FriConfig& fri_config, + const FriTranscriptConfig& fri_transcript_config, + const FriProof& fri_proof, + bool& valid /* OUT */) const + { + if (__builtin_expect(fri_config.nof_queries <= 0, 0)) { ICICLE_LOG_ERROR << "Number of queries must be > 0"; } + + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + const uint32_t log_input_size = nof_fri_rounds + static_cast(std::log2(final_poly_size)); + + FriTranscript transcript(fri_transcript_config, log_input_size); + std::vector alpha_values(nof_fri_rounds); + eIcicleError err = + update_transcript_and_generate_alphas_from_proof(fri_proof, transcript, nof_fri_rounds, alpha_values); + if (err != eIcicleError::SUCCESS) { return err; } + + // Validate proof-of-work + if (fri_config.pow_bits != 0) { + bool pow_valid = false; + check_pow_nonce_and_set_to_transcript(fri_proof, transcript, fri_config, pow_valid); + if (!pow_valid) return eIcicleError::SUCCESS; // return with valid = false + } + + // verify queries + bool queries_valid = false; + std::vector queries_indicies = + transcript.rand_queries_indicies(fri_config.nof_queries, final_poly_size, 1 << log_input_size, err); + if (err != eIcicleError::SUCCESS) { return err; } + err = verify_queries(fri_proof, fri_config.nof_queries, queries_indicies, alpha_values, queries_valid); + if (!queries_valid) return eIcicleError::SUCCESS; // return with valid = false + + valid = true; + return err; + } + + private: + std::shared_ptr> m_backend; + + /** + * @brief Updates the transcript with Merkle roots and generates alpha values for each round. + * @param fri_proof The proof object containing Merkle roots. + * @param transcript The transcript storing challenges. + * @param nof_fri_rounds Number of FRI rounds. + * @param alpha_values (OUT) Vector to store computed alpha values. + */ + eIcicleError update_transcript_and_generate_alphas_from_proof( + const FriProof& fri_proof, + FriTranscript& transcript, + const size_t nof_fri_rounds, + std::vector& alpha_values) const + { + for (size_t round_idx = 0; round_idx < nof_fri_rounds; ++round_idx) { + auto [root_ptr, root_size] = fri_proof.get_merkle_tree_root(round_idx); + if (root_ptr == nullptr || root_size <= 0) { + ICICLE_LOG_ERROR << "Failed to retrieve Merkle root for round " << round_idx; + } + std::vector merkle_commit(root_size); + std::memcpy(merkle_commit.data(), root_ptr, root_size); + eIcicleError err; + alpha_values[round_idx] = transcript.get_alpha(merkle_commit, round_idx == 0, err); + if (err != eIcicleError::SUCCESS) { return err; } + } + return eIcicleError::SUCCESS; + } + + /** + * @brief Validates the proof-of-work nonce from the fri_proof and sets it in the transcript. + * @param fri_proof The proof containing the PoW nonce. + * @param transcript The transcript where the nonce is recorded. + * @param fri_config Configuration specifying required PoW bits. + * @param pow_valid (OUT) Set to true if PoW verification succeeds. + */ + void check_pow_nonce_and_set_to_transcript( + const FriProof& fri_proof, FriTranscript& transcript, const FriConfig& fri_config, bool& pow_valid) const + { + uint64_t proof_pow_nonce = fri_proof.get_pow_nonce(); + pow_valid = transcript.verify_pow(proof_pow_nonce, fri_config.pow_bits); + transcript.set_pow_nonce(proof_pow_nonce); + } + + /** + * @brief Checks if the leaf index from the proof matches the expected index computed based on the transcript random + * generation. + * @param leaf_index Index extracted from the proof. + * @param leaf_index_sym Symmetric index extracted from the proof. + * @param query The query based on the transcript random generation. + * @param round_idx Current FRI round index. + * @param log_input_size Log of the initial input size. + * @return True if indices are consistent, false otherwise. + */ + bool leaf_index_consistency_check( + const uint64_t leaf_index, + const uint64_t leaf_index_sym, + const size_t query, + const size_t round_idx, + const uint32_t log_input_size) const + { + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + size_t elem_idx_sym = (query + (round_size >> 1)) % round_size; + if (__builtin_expect(elem_idx != leaf_index, 0)) { + ICICLE_LOG_ERROR << "Leaf index from proof doesn't match query expected index"; + return false; + } + if (__builtin_expect(elem_idx_sym != leaf_index_sym, 0)) { + ICICLE_LOG_ERROR << "Leaf index symmetry from proof doesn't match query expected index"; + return false; + } + return true; + } + + /** + * @brief Validates collinearity in the folding process for a specific round. + * This ensures that the folded value computed from the queried elements + * matches the expected value in the proof. + * @param fri_proof The proof object containing leaf data. + * @param leaf_data Pointer to the leaf data. + * @param leaf_data_sym Pointer to the symmetric leaf data. + * @param query_idx Index of the query being verified. + * @param query The query based on the transcript random generation. + * @param round_idx Current FRI round index. + * @param alpha_values Vector of alpha values for each round. + * @param log_input_size Log of the initial input size. + * @param primitive_root_inv Inverse primitive root used in calculations. + * @return True if the collinearity check passes, false otherwise. + */ + bool collinearity_check( + const FriProof& fri_proof, + const std::byte* leaf_data, + const std::byte* leaf_data_sym, + const size_t query_idx, + const size_t query, + const size_t round_idx, + std::vector& alpha_values, + const uint32_t log_input_size, + const S primitive_root_inv) const + { + const F& leaf_data_f = *reinterpret_cast(leaf_data); + const F& leaf_data_sym_f = *reinterpret_cast(leaf_data_sym); + size_t round_size = (1ULL << (log_input_size - round_idx)); + size_t elem_idx = query % round_size; + F l_even = (leaf_data_f + leaf_data_sym_f) * S::inv_log_size(1); + F l_odd = ((leaf_data_f - leaf_data_sym_f) * S::inv_log_size(1)) * + S::pow(primitive_root_inv, elem_idx * (1 << round_idx)); + F alpha = alpha_values[round_idx]; + F folded = l_even + (alpha * l_odd); + + const size_t nof_fri_rounds = fri_proof.get_nof_fri_rounds(); + const size_t final_poly_size = fri_proof.get_final_poly_size(); + if (round_idx == nof_fri_rounds - 1) { + const F* final_poly = fri_proof.get_final_poly(); + if (final_poly[query % final_poly_size] != folded) { + ICICLE_LOG_ERROR << " (last round) Collinearity check failed for query=" << query + << ", query_idx=" << query_idx << ", round=" << round_idx; + return false; + } + } else { + const MerkleProof& proof_ref_folded = fri_proof.get_query_proof_slot(2 * query_idx, round_idx + 1); + const auto [leaf_data_folded, leaf_size_folded, leaf_index_folded] = proof_ref_folded.get_leaf(); + const F& leaf_data_folded_f = *reinterpret_cast(leaf_data_folded); + if (leaf_data_folded_f != folded) { + ICICLE_LOG_ERROR << "Collinearity check failed. query=" << query << ", query_idx=" << query_idx + << ", round=" << round_idx << ".\nfolded_res = \t\t" << folded << "\nfolded_from_proof = \t" + << leaf_data_folded_f; + return false; + } + } + return true; + } + + /** + * @brief Verifies Merkle proofs for a given query. + * @param current_round_tree The Merkle tree corresponding to the round. + * @param proof_ref Merkle proof for the query. + * @param proof_ref_sym Merkle proof for the symmetric query. + * @return True if both proofs are valid, false otherwise. + */ + bool verify_merkle_proofs_for_query( + const MerkleTree& current_round_tree, const MerkleProof& proof_ref, const MerkleProof& proof_ref_sym) const + { + bool merkle_proof_valid = false; + eIcicleError err = current_round_tree.verify(proof_ref, merkle_proof_valid); + if (err != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Merkle path verification returned err"; + return false; + } + if (!merkle_proof_valid) { + ICICLE_LOG_ERROR << "Merkle path verification failed"; + return false; + } + + merkle_proof_valid = false; + eIcicleError err_sym = current_round_tree.verify(proof_ref_sym, merkle_proof_valid); + if (err_sym != eIcicleError::SUCCESS) { + ICICLE_LOG_ERROR << "Merkle path verification returned err"; + return false; + } + if (!merkle_proof_valid) { + ICICLE_LOG_ERROR << "Merkle path sym verification failed"; + return false; + } + return true; + } + + /** + * @brief Verifies all queries in the FRI proof. This includes: + * - Checking Merkle proofs for consistency. + * - Ensuring leaf indices in the proof match those derived from the transcript. + * - Validating collinearity in the folding process. + * @param fri_proof The proof object to verify. + * @param nof_queries The number of queries. + * @param queries_indices List of query indices to check. + * @param alpha_values Vector of alpha values for each round. + * @param queries_valid (OUT) Set to true if all queries pass verification. + * @return An eIcicleError indicating success or failure. + */ + + eIcicleError verify_queries( + const FriProof& fri_proof, + const size_t nof_queries, + std::vector& queries_indicies, + std::vector& alpha_values, + bool& queries_valid) const + { + const uint32_t log_input_size = + fri_proof.get_nof_fri_rounds() + static_cast(std::log2(fri_proof.get_final_poly_size())); + S primitive_root_inv = S::omega_inv(log_input_size); + for (size_t query_idx = 0; query_idx < nof_queries; query_idx++) { + size_t query = queries_indicies[query_idx]; + for (size_t round_idx = 0; round_idx < fri_proof.get_nof_fri_rounds(); ++round_idx) { + MerkleTree current_round_tree = m_backend->m_merkle_trees[round_idx]; + const MerkleProof& proof_ref = fri_proof.get_query_proof_slot(2 * query_idx, round_idx); + const MerkleProof& proof_ref_sym = fri_proof.get_query_proof_slot(2 * query_idx + 1, round_idx); + const auto [leaf_data, leaf_size, leaf_index] = proof_ref.get_leaf(); + const auto [leaf_data_sym, leaf_size_sym, leaf_index_sym] = proof_ref_sym.get_leaf(); + + if (!verify_merkle_proofs_for_query(current_round_tree, proof_ref, proof_ref_sym)) { + return eIcicleError::SUCCESS; // return with queries_valid = false + } + + if (!leaf_index_consistency_check(leaf_index, leaf_index_sym, query, round_idx, log_input_size)) { + return eIcicleError::SUCCESS; // return with queries_valid = false + } + + if (!collinearity_check( + fri_proof, leaf_data, leaf_data_sym, query_idx, query, round_idx, alpha_values, log_input_size, + primitive_root_inv)) { + return eIcicleError::SUCCESS; // return with queries_valid = false + } + } + } + queries_valid = true; + return eIcicleError::SUCCESS; + } + }; + + using FriFactoryScalar = FriFactoryImpl; ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryScalar); From 414b7dc0c90b36ef0865f70142b09b47a5a00f19 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 17:33:41 +0200 Subject: [PATCH 125/127] format --- icicle/include/icicle/fri/fri.h | 2 +- icicle/include/icicle/fri/fri_config.h | 7 +++---- icicle/include/icicle/fri/fri_proof.h | 4 ++-- icicle/src/fri/fri.cpp | 3 +-- icicle/tests/test_field_api.cpp | 8 ++++---- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index 75977c6394..c4090faf4d 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -100,6 +100,6 @@ namespace icicle { fri_config, fri_transcript_config, fri_proof, merkle_tree_leaves_hash, merkle_tree_compress_hash, valid /* OUT */); } - }; + }; // namespace fri_merkle_tree } // namespace icicle diff --git a/icicle/include/icicle/fri/fri_config.h b/icicle/include/icicle/fri/fri_config.h index 1cc713d0de..6173cd9bcb 100644 --- a/icicle/include/icicle/fri/fri_config.h +++ b/icicle/include/icicle/fri/fri_config.h @@ -19,10 +19,9 @@ namespace icicle { size_t stopping_degree = 0; // The minimal polynomial degree at which folding stops. size_t pow_bits = 16; // Number of leading zeros required for proof-of-work. size_t nof_queries = 100; // Number of queries, computed for each folded layer of FRI. - bool are_inputs_on_device = - false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). - bool is_async = false; // True to run operations asynchronously, false to run synchronously. - ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. + bool are_inputs_on_device = false; // True if inputs reside on the device (e.g., GPU), false if on the host (CPU). + bool is_async = false; // True to run operations asynchronously, false to run synchronously. + ConfigExtension* ext = nullptr; // Pointer to backend-specific configuration extensions. }; /** diff --git a/icicle/include/icicle/fri/fri_proof.h b/icicle/include/icicle/fri/fri_proof.h index 8f93a4c792..83778e6c50 100644 --- a/icicle/include/icicle/fri/fri_proof.h +++ b/icicle/include/icicle/fri/fri_proof.h @@ -67,8 +67,8 @@ namespace icicle { } /** - * @brief Get a const reference to a specific Merkle proof for a given query index in a specific FRI round. Each query - * includes a proof for two values per round. + * @brief Get a const reference to a specific Merkle proof for a given query index in a specific FRI round. Each + * query includes a proof for two values per round. */ const MerkleProof& get_query_proof_slot(const size_t query_idx, const size_t round_idx) const diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 04c9a7167d..22b50988a4 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -4,7 +4,7 @@ namespace icicle { - /** + /** * @brief Class for performing FRI operations. * * This class provides a high-level interface for constructing and managing a FRI proof. @@ -304,7 +304,6 @@ namespace icicle { } }; - using FriFactoryScalar = FriFactoryImpl; ICICLE_DISPATCHER_INST(FriDispatcher, fri_factory, FriFactoryScalar); diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 1ba995f37a..c064dc2d47 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -680,8 +680,8 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; - err = fri_merkle_tree::verify( - fri_config, transcript_config, fri_proof, hash, compress, valid); + err = + fri_merkle_tree::verify(fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); }; @@ -747,8 +747,8 @@ TYPED_TEST(FieldTest, FriShouldFailCases) if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== bool valid = false; - error = verify_fri_merkle_tree( - fri_config, transcript_config, fri_proof, hash, compress, valid); + error = + verify_fri_merkle_tree(fri_config, transcript_config, fri_proof, hash, compress, valid); ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); From d3789cb534b4b9028ad9d66cdfe907ba4a6d3ced Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 18:04:53 +0200 Subject: [PATCH 126/127] removed S from template --- .../version-3.5.0/icicle/primitives/fri.md | 20 +++++++++---------- icicle/include/icicle/fri/fri.h | 14 ++++++------- icicle/src/fri/fri.cpp | 8 ++++---- icicle/tests/test_field_api.cpp | 8 ++++---- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md index eca1d39939..5b6373e84f 100644 --- a/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md +++ b/docs/versioned_docs/version-3.5.0/icicle/primitives/fri.md @@ -138,10 +138,10 @@ class FriProof The class has a default constructor `FriProof()` that takes no arguments. To generate a FRI proof using the Merkle Tree commit scheme, use one of the following functions: -1. **Directly call `get_fri_proof_merkle_tree`:** +1. **Directly call `prove_fri_merkle_tree`:** ```cpp - template - eIcicleError get_fri_proof_merkle_tree( + template + eIcicleError prove_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, @@ -151,11 +151,11 @@ To generate a FRI proof using the Merkle Tree commit scheme, use one of the foll const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */); ``` -2. **Use the `fri_merkle_tree` namespace, which internally calls `get_fri_proof_merkle_tree`:** +2. **Use the `fri_merkle_tree` namespace, which internally calls `prove_fri_merkle_tree`:** ```cpp - fri_merkle_tree::prove( ... ); + fri_merkle_tree::prove( ... ); ``` - This approach calls `get_fri_proof_merkle_tree` internally but provides a more structured way to access it. + This approach calls `prove_fri_merkle_tree` internally but provides a more structured way to access it. - **`input_data: const F*`**: Evaluations of The input polynomial. - **`fri_proof: FriProof&`**: The output `FriProof` object containing the generated proof. @@ -206,7 +206,7 @@ fri_config.stopping_degree = 0; FriProof fri_proof; // get fri proof -eIcicleError err = fri_merkle_tree::prove( +eIcicleError err = fri_merkle_tree::prove( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); ICICLE_CHECK(err); @@ -221,7 +221,7 @@ To verify the FRI proof using the Merkle Tree commit scheme, use one of the foll 1. **Directly call `verify_fri_merkle_tree`**: ```cpp // icicle/fri/fri.h -template +template eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, @@ -233,7 +233,7 @@ eIcicleError verify_fri_merkle_tree( 2. **Use the `fri_merkle_tree` namespac, which internally calls `verify_fri_merkle_tree`:** ```cpp - fri_merkle_tree::verify( ... ); + fri_merkle_tree::verify( ... ); ``` > **_NOTE:_** `FriConfig` and `FriTranscriptConfig` used for generating the proof must be identical to the one used for verification. @@ -241,7 +241,7 @@ eIcicleError verify_fri_merkle_tree( #### Example: Verifying a Proof ```cpp bool valid = false; -eIcicleError err = fri_merkle_tree::verify( +eIcicleError err = fri_merkle_tree::verify( fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); // Ensure proof verification succeeds diff --git a/icicle/include/icicle/fri/fri.h b/icicle/include/icicle/fri/fri.h index c4090faf4d..80edb4cc0b 100644 --- a/icicle/include/icicle/fri/fri.h +++ b/icicle/include/icicle/fri/fri.h @@ -34,8 +34,8 @@ namespace icicle { * @return `eIcicleError` indicating success or failure of the proof generation. */ - template - eIcicleError get_fri_proof_merkle_tree( + template + eIcicleError prove_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const F* input_data, @@ -61,7 +61,7 @@ namespace icicle { * @return `eIcicleError` indicating success or failure of the verification process. */ - template + template eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, @@ -71,7 +71,7 @@ namespace icicle { bool& valid /* OUT */); namespace fri_merkle_tree { - template + template inline static eIcicleError prove( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, @@ -82,12 +82,12 @@ namespace icicle { const uint64_t output_store_min_layer, FriProof& fri_proof /* OUT */) { - return get_fri_proof_merkle_tree( + return prove_fri_merkle_tree( fri_config, fri_transcript_config, input_data, input_size, merkle_tree_leaves_hash, merkle_tree_compress_hash, output_store_min_layer, fri_proof /* OUT */); } - template + template inline static eIcicleError verify( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, @@ -96,7 +96,7 @@ namespace icicle { Hash merkle_tree_compress_hash, bool& valid /* OUT */) { - return verify_fri_merkle_tree( + return verify_fri_merkle_tree( fri_config, fri_transcript_config, fri_proof, merkle_tree_leaves_hash, merkle_tree_compress_hash, valid /* OUT */); } diff --git a/icicle/src/fri/fri.cpp b/icicle/src/fri/fri.cpp index 22b50988a4..495b7f9882 100644 --- a/icicle/src/fri/fri.cpp +++ b/icicle/src/fri/fri.cpp @@ -416,7 +416,7 @@ namespace icicle { } template <> - eIcicleError get_fri_proof_merkle_tree( + eIcicleError prove_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const scalar_t* input_data, @@ -438,7 +438,7 @@ namespace icicle { } template <> - eIcicleError verify_fri_merkle_tree( + eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, @@ -463,7 +463,7 @@ namespace icicle { #ifdef EXT_FIELD template <> - eIcicleError get_fri_proof_merkle_tree( + eIcicleError prove_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const extension_t* input_data, @@ -486,7 +486,7 @@ namespace icicle { } template <> - eIcicleError verify_fri_merkle_tree( + eIcicleError verify_fri_merkle_tree( const FriConfig& fri_config, const FriTranscriptConfig& fri_transcript_config, const FriProof& fri_proof, diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index c064dc2d47..3afa787913 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -670,7 +670,7 @@ TYPED_TEST(FieldTest, Fri) oss << dev_type << " FRI proof"; } START_TIMER(FRIPROOF_sync) - eIcicleError err = fri_merkle_tree::prove( + eIcicleError err = fri_merkle_tree::prove( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); ICICLE_CHECK(err); END_TIMER(FRIPROOF_sync, oss.str().c_str(), measure); @@ -681,7 +681,7 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; err = - fri_merkle_tree::verify(fri_config, transcript_config, fri_proof, hash, compress, valid); + fri_merkle_tree::verify(fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); }; @@ -738,7 +738,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) fri_config.stopping_degree = stopping_degree; FriProof fri_proof; - eIcicleError error = get_fri_proof_merkle_tree( + eIcicleError error = prove_fri_merkle_tree( fri_config, transcript_config, scalars.get(), input_size, hash, compress, output_store_min_layer, fri_proof); // Release domain @@ -748,7 +748,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) // ===== Verifier side ====== bool valid = false; error = - verify_fri_merkle_tree(fri_config, transcript_config, fri_proof, hash, compress, valid); + verify_fri_merkle_tree(fri_config, transcript_config, fri_proof, hash, compress, valid); ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT); From 58a8cc71e369a91f7a2c051b373aba2b168d1455 Mon Sep 17 00:00:00 2001 From: Shanie Winitz Date: Wed, 12 Mar 2025 18:05:43 +0200 Subject: [PATCH 127/127] format --- icicle/tests/test_field_api.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/icicle/tests/test_field_api.cpp b/icicle/tests/test_field_api.cpp index 3afa787913..509c3b8181 100644 --- a/icicle/tests/test_field_api.cpp +++ b/icicle/tests/test_field_api.cpp @@ -680,8 +680,7 @@ TYPED_TEST(FieldTest, Fri) // ===== Verifier side ====== bool valid = false; - err = - fri_merkle_tree::verify(fri_config, transcript_config, fri_proof, hash, compress, valid); + err = fri_merkle_tree::verify(fri_config, transcript_config, fri_proof, hash, compress, valid); ICICLE_CHECK(err); ASSERT_EQ(true, valid); }; @@ -747,8 +746,7 @@ TYPED_TEST(FieldTest, FriShouldFailCases) if (error == eIcicleError::SUCCESS) { // ===== Verifier side ====== bool valid = false; - error = - verify_fri_merkle_tree(fri_config, transcript_config, fri_proof, hash, compress, valid); + error = verify_fri_merkle_tree(fri_config, transcript_config, fri_proof, hash, compress, valid); ASSERT_EQ(true, valid); } ASSERT_EQ(error, eIcicleError::INVALID_ARGUMENT);

    U^Mcqr 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-3.4.0/icicle/primitives/image-3.png b/docs/versioned_docs/version-3.4.0/icicle/primitives/image-3.png new file mode 100644 index 0000000000000000000000000000000000000000..75d3b4329bb29f16889b7944e127ac1674abeed5 GIT binary patch literal 329359 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 z z1C5XG6#_qSvpBVLH!h6usSV}84HDq+DXn5JP(WMR8_)DeV{h}zY287AyM5>__VfG{ zswYr?qhr&`?A$(sim{4)5mvo1-}qEvb8{+mJ{z$xzPd@w-@x}aVXL#(i%uM$>|WMa z-;2^WH=QhRCoSN6YHLWz<3|;Y0QYbH#A}R2{EN4iW2^-#kMGOqnVC(XoDL`PKMcZG zEIVW!XM6REIH}vRtOE9G4U`{gjWM0%_h8xjRvCAL?!~{<0Ln7b*zN>)GSL_A%qxz^ zc~vsL(<0Z*;Y@Rz z*3*1tfI|w#-;?MW_G{DiYdiZ(33*tUjibtAX1%M3JqA5?S63JjMH{>@M(<$9K9>VR z^~5CMaW)%PtzIAegceB1-sM*lPkjN{!Inc~a=~mN^{+xX5piV;nhi4dcOyFsZ;{-^ z-{0z68p4E1){D@(vk+IiIQgA(@zW-P+L;}U-mdy}@=}*FbK&*@)sttoYGRAAzc!zv zouTX_k5Ut~s%t{3823auxC;`@U*LaCz`G`%Fuip;Lpms$dl}_e>kK;h0Q4L@yFduo z=r16V1~|U@cB*4tp|0o3^o5WI5`yJxP{BPorv4rd-kD?=V~M#&D5Dfphx6)XZCg z=bJ|vv?>ve-@3XA4WZd}pS2iD$OmUdF*f)PV)&>P22D;HaJxLS6Yx|K6@7J$+MCE_ zy1j+oLvmVMwYKZ{LdEWaB99|r)uKbV@-;55+U{VQWyPxUco!Si>j62iqM~elfWPuM}*Q{hqi@46OTea@>WSoNQrx z>W-|BiOJZIlGiV5(HNERTfJX1TWpc@b3LzzJ*lavh}hd_pTRm@Y@t0eNIjJ75urkP zJ#AVCVU{CEv7JU+UWk8uyjXpehMFHvT{HsjeYd{Oe0R?r zqU$7M3kx?9?7-(v{th1AVWe=-__w{O9g3cuI3uk1v-dI*%2WwAmDxV z7352ken{)7eK}--_z~Fvm+^GONL@_L!F`czXd85*56nUowc5fo?m}>BE2qWw%L$8q zqU>7U?JKyjZ9I4ldpVbxH=kI6T`gUsL6+BVs>J5 z=x)T2`4iFgt{$d=yLa57q=dO?p>RqehdWcZr4(sM|0NR;cW+VW#te>ucD{N2g0a1aZ zY@6hi>6`#`5JpTrB0BZ^PB<zF2D5%og=wqz+$`oN zg8J+8_$o6TyR692UxBZ^-CYFWX_P)bq*?|%%k2e5Y^DxEe@jIIhUhR{94LrAbfvn$ zPe2(zo{wCvZymheUo%=XBPrD{a);>KIf;=6FCe zG*61bN!kAEsL_L95YJL8f6eT2vf{p`%ibv6`NiHazcbagNW5fnp8CjFP_E)|u`rNK zK_MwWm7xw2QdDocAQbwBhOJOr6vFR(vpg6MV|q-{TfI&`(@%u{Y^0t@L`o{Vst`+l z>RepHdG3&lGzWiN+T7fHz`YZWkI#dj>IQZqu*ynLfx5c~30o_H{!W6zH)>E8c;LR3 zHI^7iK+ehI{YdY%BxfZB8;5S=#v_hSeV%4%P7jGS--GU$}JxShNAGl*_3 z&75PMdW48NzJQx5@!81ccGJ|O%Hz})>v_1!lh9V75IoSjt_CV>BF7GG8_`AUY!4Wnef zso!5!RxMgpLZL9H(=#^6oTADqEHAAO?k|s{8dklDbw5Im1a<9OWRN-oc0A?^%u7nz4rkK&})mqPYjJRM7d=Lr1b>&`42@mkRUyUFT&WpF3Li z^Qr5#=I!l1jYJSEG+!O@6bewvsjd=@1HrLowL|3mK8bsm#_g*hkc$YuD&`~L@nVC_ zuCJ{vq@58kt||U%*zL$Li(eFrO@Z!HAuDMw$H(#-V$D(HQ|*NJ7kv(=7Y)oO07+Ejq>YjW~5ZJ~- zPKk#+C7uKCa^)&)vd3d~U$ek$b*o;P=}pBG(6%34A8d!nfrpApf1fG%g6sNlyxYx| zM?DY!Lf99TuL266N;~7Ck`hx?hzeO{VO1f^;n()q>oE0ek%&@JQ6VYc{OOXNlq9hJ z^N1FTq1t3b8IHKXO3bK+K808yc`uukFbG#pZWJ}_9YVp#DD?-`PZARFs8pd!!CBdw~e%&@D=2Qt}P9)P|q(%Za_XEr=wFv#RV3Sy*x0Mdn z<@6?_&_AfaUtdj5A==#@hDIkpaXrWd>ngn4{Q0A!t2zU__D4k08{OyUy7#dSKUc)Z z21J6m|{iYz(=R7wOe}nQ^Em`O_7YNdi)?YivF_AQ!2c>S$k3Fu!(B@#Ct3PgGT!O+m#>kX}ncl|r65CcG> z&)EdU_{*4c$uw_LYz%aEEFkrcxWDCmxz27W=Rs#XeJWq3cbkv;8pvsGih{uP*5jo! z%2Gm%G<>xQKZv;84I|6Dy>^Qz@)KG9Eg5&C zJ02n&`eG?jusob~1;F_A1-zoBcJ?s(zIQIW&L=Z6I#ArIw=%A=xEo$JvQ8ubMxNu| zjeQ3C)bOt*HcO>VYz8DaZu9e1AST2oUyO6m zY`%*i4P4uEl~CYL6dSW}C!7lxsFMpGn2p$W-Wh%hhXOR;r>VHzEsvimO|g-m(@`D` zWn}-ZriL;W93;^Ma!Z9{e*N!*S|-aFcAhe=mxv2f0%Kuvu=&7!Lu~F02Nva!@k6P4 z^ZRY|QKXl4{lf;RupxM(2`I&^`9)oNAIR0aK9Sek_xAN2&4T8EaHL)BK78M-6hbKr z-mA#Ip%zDnF4BktElkcCPe5^0Y`5dNuHXB>%OgLpc3{;mXvNQ-=un1J7DW2TXmKZc ze+cBZ?^7T}TIk^&A({_XwZtiw9`vdDiGHd2K{Y-R$Erv|@~d$LswwNgMlKYNniYC0 z*y<%bGlz^gVLvpb8_K_yRaWFR7*WFp1OCYEL`;*eb3k#nU~oegksZv}^q_1uZzWZ8 zE_2hMTP{8dVcX@c?9KIrwX2KL^a^OycodfdF4=OC%Mp6qnaP-{>Hx@SqM1o7XT}Px~R6BCTo#IE*H`SIlsaoIua`mSS-^#WCDS`qzRI9s*LXWtX zekB_Ti6VC@8BFrx8f?bLj}$3c z{yr~2_~1w!sz73Ud$La|+}U`pWL)oGi8ybFFwoy`AZV})yOG0c15Id_9mh6n-~wPG zd{v5T#tL}%(j*RhQ>k}50B_e$AiQKtp1$|_*eL79qW6% z{+z4-vT=N|XJu`@%^pNiQCS&ec?gtFll|ZucjbtRlck+3{;@K%0st-G z?dAbMJO+yZ!k@eqSMKGVGh4^|-g>hzjkpLZaaf%}?ui{rTs*j@dE^cHNpObFM!Q7r z&B#{I;;>X^A+}%aCxUc(I1_|7B+2o4x1ABil@KKBUTntM{63cPtnTt(MDy>vLtOorNfuT!LXdRzt+}`~H%j#;w{Y#y0Qdxa&ZzwlM$(H`h4 zl6(-YOIwgbm!fh;IDKWK^I9)Jhp2ZriAjfDBQx3QZC6xF_U=N*VhAVRL*5yd(Ho6{ z3il1VxM?<1YqTmbAfEtgF-R8GCYC6&j3wKGFxDXy61H@c| zli6&b-NE5+M{+fl-pb+{h@VgWfjJn72o}nTa}LvXd4fY_>Tj78y40DEjL)!?^h1kgW@}nEUAav zy??ojZ@+H+K373$B;Y6yRW1(Keujx_HY^mZ`~w-^I9h}@PK0JOF#%kUR})Y=rlTDW zi4^dbjqk(%0jKaDNtE(Qc*ktl%&b05&6rrCre$go?lHOzu+T#(=Su#ALdZzLg**0# zw&K$Gxo%168+=JBJBj+K5#v7R@?c*^785R?#uHYBhUmv(@(q=aqcHWes}9*5SIetp zS2K*>N)E3lx=8VqG#UOD%%k+0pX|oChWjN(5#e{&)25bbvEZMQjmY!>xs)p^Cc22q z*%l&ple7`VYbCEeO6kS2!f96~rCAlZelFGa9IJeWyqB9!5VLzPB(quPh@R#tX$s|G z*Fm6F>1)G+mv@@(>(yNis=oCyw$2@=R&srdvZmK?8votkBKKwQQ15~0Mbwsf zr@RW^<%#S9syhbd9mK^!mgPHvj`g4VpSFriME#>n@-|87o{sdhu#=I3c|F%_*E%p? zoG;+MLc#MmxGp(Vj-|c)81az8k|kJvq_YyU)tIfBy?wTn2PYA>o4=Gpc4ZdLdP3Vq zI8J8V*IZRgnKp5p2nUx})TKWRnA*0V>2K!nvOv9Q@~71F~#0x#z1*=}J1z zP)GE&o1B97r%XbBm>alm_{)EFHq?FjDzVCZeKO@f%<(ZBxviK+M%#l;O&rGvzA)PE z;>&<2HT|&iF;y^Fe2{h7b(0YF-8%}bZiUw_F7h~n-gsZ%0dNyuo(;nTbrq3@`z1UY zu2bCQoKOJF#A2hgbac^EY5%Tc{B>p1gbK9lZ1%3LVrO?CGC4G&6Erhjhc6ABN+E(S z{8}C%QAmnr}3)}=oCHdLV zmtU@e1I165%MDD0q~3z5Jgvj9 ze4RvQut9w)+s`Q&uAFtlTpn@IU`@V&=4KlBC*R=b@wiPZ`5QfUhYzm0BdH%U0sV)MOoIX1E%0u*0uxw0o~C1kBZ=Xz{Z>4C6Z0=M7HV!fAN?w(+>W&@M1NZr$_x=iWtPMgHbH9ylMaY*bQuO=q= zY6EfX=2A}A$7gqv#T37NCl|!1QkE03n-d`AgIXJV#{tkFovD|M=5j+LDW)FbqE5;3 zn%m?#le9&PsFcr4hmQaLrieC?d3dO!w5CML6K>3XjDdW3zUe2M%F^t- zS;T4E^_9$|Vr%#~i}D8AVc0a7W55~`@G_#!Z#WQptG zbMA#ivwh45Zb#ps3^-g?(esW@#u%udt@H`4_U*Z(``MEzK(V17DYzPhkBM`w+QFo8 z+c(I%ZknOYWpIp!I?3$=m=*A^c^Mcq8pHp-2LGK=wEp6YDz{!BnfZ|pXT5}UN5JqBO zB}*l`OfE$MF^IwfvqfjaIYv>f&7dO<6*m|;^trvhYv#*q9pPZ-`s5ZiQ}k}4dG;*g z$J~?UMrCUgD}L0!vt4cw9noIIAg7k1&8Px(w`Lr1>krLh=ikpmG`=^8ES?fsl+}|& zgcN`!g{4$9w*Y(;uc;0&n8~|S?VcX%vpw1>bJlEbZDcpZsGLXur@9D6D3JfUcbMnP zzBAO25{Tk+azP0LCzOPo?G9NhoF+%qiGEx}9&1n_Q1$Z#Hty`_ zYfreG+}b!6a}*M|4MqiGltr8w2&XSDnm?1%^(c^TsRaIggn*;vlRd&(-6K8R_K92= zViv{C z{HRVGi1BStv5rQc+hnvwHu3PLT*!!ubGZXptFGwjT_YX|kBxLf#QvTPI@+1D-cDpcdv2|5L!3QX z!`y%bA^!WOth{`5aFILL=dVTnApAm_UE`e}8OlY)wBr3(O!AjqNw^2)G;Y@5s?`n; z96TbLSy(u{th>76g(bobkjd0W~WM8?tv57P4;yi|OJOiAG`) zH%}%M@`(OB0b%CjZ;5Ao^8Bow6~!mMj1!RJYVnvoc02u?D)r(xs}WyYmA5j9=zUi z0e3_=oK_`AL4NmVD6szfYGX0Yd>%V_X*L$5j5gd?`?+r4hDSeOuQD7dCS^a6>ibUC z*=VAGLvvC~e#Wa`@QS~O8%wE+z{LN8ER)78J7@#C;k_az2_oJX)Pub`m?4T(xm-%l z?qJZo4@mO@cb*+6$&=ybs#b8ln3O(+(+`T1KfOG8gjs%RpF$;Ek$f%;*WOfpj4PRO2T;RK-B&G*vx-&6B7 zS+F6^XAKCd5_&w4PQ6p&8_0xD_~qgbbJX_gTl$KQy&Q`<|L%|{TL-l z9f>VsOz8p$KTa*-eX6YY%Up{KaakX1z_I836;sipxTRo-V4cR;@lRckgpLvA6O2{L z`_DuWnSkn#CrtUw81LLn>sqX-&!-ld^hB1O6x%G{Ber;+_Bm80ws}LLj}pbyYw}e( z?ouXk9!s1{5d6xyxMJF=6E_$z`y9h-&>rcUwAcF_uf& z9oA3EVev@SI@I~^A+VSvpM{E_1ifZ?1-~fIBV7DoHdtKwsS>hUZ3dBgA726?YLphX zMr=KJTK{y35Bu$i&Xx|t1(@aiJm0nOS|1aX~+5E;5+$kcxd#I^Ae*hd|(M7-*YgyT#9Aba<}85^5CO>#ghi^`Fshk z;_FZCaC9ORq^ARxou*d6(stSk;k&)>>Sf)+?Yd2^E|lyajxWVMx+Ut7-BviZpPZiV zG0)l|$G+UhQffn9#n5}d$f@7;8JJf~ogVKmS`P9kio`$x63;a3*90!HNoWNlr*#}x zP~r@?+5Zd2)DFeV>o;1=RX~+SLA>?ZV4>*Et*;K5aj1)Xzd))Jy0^}=;6|BhZ7dfr zfgch0n)7`d59Q5=iSC~u-Hw~$nxC({m{aAh#y9<`U?Krc&B(RkS7v5z;+3$?W1*<^Sp4*laPp@W;Umrg-h z;G6O_2q`!ZmbXdzYC{g9vhVXU0f;kU%&O@uS4re4#2JP8DD5uX$f732s9|`E<-^ME zkqug=j~b#HeG0ti(Na4z?)SsI99f)tsLZQXN=nMsYT8o`AS+U zaf47T=NLmSE?A?yQ(0$vc4RHr4{cXN!tSF6t_WoNXhJF9uZwKRlAod~X}%rJ`|;_L zK*Vj37hBOxdUL*F1EsK`!v!8m#EXh5lf$wE8lQ!%8mR4&kbwag6e<5IO5w{DY`vaC|;yYmZlC}UchoombBU1w~1IH5~}d!7-}Y`KPNd?M&(Jcya5aQj>*_}Y+FKqS9v2+& zfsR9q|2*USM2dQMM_0510cVNyz>UC)c9x^~pBjevV~T=u);P{?_rac*7j_J(!jw}} zL9jI{{4-yJgj^t@{MQ`Mya5wh~RQRk5tV#tz&m$9yOazv1@p; z`$rZw{j;m8$ad%{#p5Hq2;DNZS8N4;Qx z;jSa7wd6rUm7TBx3(h#&)1^96(GwB}_+Vy?jM}2mZ=}mY8NzlS9HdP^rmeSrqz{^pGzCX zwyb+eVg7kBb$#X)RlEM(a=fl)osZAYx70PF7~O?5f%V^bBE}r0mQV_b{sr^_W}Uq~ z_!l@37Z*97C*>F#93WkMNpN0ypZG6~j1QX#@$(Ye-B!~3wVi#K6`yZw+eGm3dC3em zgZh@w&&(uhR5XJ=ol7iK{gMhNmfu)h5;7T>WdJoSyeTZ6XQ7|S%jSm~o$U^LK!Gx9 zNrX%edI;XYOsON9G0or;n@hHT>^_NY8UK?w{e9xdN2Q_}fpE=BT`;ap>U9-moC}U;0w~caD~9tVt0x zM6FOZBwc6@XnoZ0j2plZ6A70^r*YWiE)&9_r2e55#}RK3KnsiS%po?*THGZFdwQC~ z{)Gkw-az0HWWBGD=8l>mcqpSfvIY5veLB&58qWQ8`J;+eEFR=~(|M^{7X5}1C)Ny~ie=Ov z+V+P9`QL(r|LY%W@HA!3)}McLmjB1}|8DPpH$Jk6cK`;#p5bNIKa#@#gQfoe{Jt4D zZK~Ym8r>f{`EQ=|Kab6XK+{_nf#I5WFZ@5g-M>EOoecEU6pwS*e`=vU0Z28DaUU5r zBI-Yw{@>7`{}Vs?zZr=O6&UgMnX$t`ga2v26v9EMR*3QtsC}nj85Oau&S{3MDB?tDVOZY{^9Q`gX zZ0DLCmg8;J-0?pNL7Rr7=_t*h1Q?W#ifbTtM^b?2^OnChhE`g+7^fl$#scXFLbw*U|!$o1Y$J(1+|8?$<69#Y9XD35|sNA=0v| zmp`jMe+Qmdfsa-Za^b-N6%_~ZI|FZ)T1O0wgY`%-q6U=UC(U7>(wQbZuSub|Y>-Y=P+S=at z#}7GR{|ePwq`@adgX#*5z<^k+aihVU+G6Dpm!&5nDJ&KMGZ7V4_uFm~SOn<}IE=-7 z#Om;ZF*ME#`2dnl2Gdz-r<>=!?fIL<;1{Ld;VuTG3)DkN5Wx|F{#Nt=1F<8=oPGT_ zJbPQ)_{r40M#ve%ogV`ewqV{Ohz+9tbvWUSFMMZ8QI^>qDUL@$F$mZirKdD)Ih^dv zP}*cUS^4$QI$p;`G|$%D-RA)?B0dEvD2+{O?aj=*WIbDu=9w)J&fM&UT^Oy}M|G