diff --git a/barretenberg/cpp/pil/vm2/constants_gen.pil b/barretenberg/cpp/pil/vm2/constants_gen.pil index 2029daa6bb0a..235af2d2fa64 100644 --- a/barretenberg/cpp/pil/vm2/constants_gen.pil +++ b/barretenberg/cpp/pil/vm2/constants_gen.pil @@ -165,9 +165,9 @@ namespace constants; pol UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE = 144; pol GRUMPKIN_ONE_X = 1; pol GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673675138860; - pol DOM_SEP__NOTE_HASH_NONCE = 1721808740; - pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; pol DOM_SEP__SILOED_NOTE_HASH = 3361878420; + pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; + pol DOM_SEP__NOTE_HASH_NONCE = 1721808740; pol DOM_SEP__SILOED_NULLIFIER = 57496191; pol DOM_SEP__PUBLIC_LEAF_SLOT = 1247650290; pol DOM_SEP__PUBLIC_STORAGE_MAP_SLOT = 4015149901; diff --git a/barretenberg/cpp/src/barretenberg/api/api_avm.hpp b/barretenberg/cpp/src/barretenberg/api/api_avm.hpp index e21e383c203e..b43a1ccbed0e 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_avm.hpp +++ b/barretenberg/cpp/src/barretenberg/api/api_avm.hpp @@ -1,8 +1,40 @@ #pragma once +#include #include +#include + +#include "barretenberg/ecc/curves/bn254/fr.hpp" namespace bb { +/** + * @brief Result of in-memory AVM proving. + */ +struct AvmProveResult { + std::vector proof; + bool verified; +}; + +/** + * @brief Prove an AVM transaction from serialized inputs (msgpack bytes). + * After proving, the generated proof is also verified. If verification fails, verified is set to false. + */ +AvmProveResult avm_prove_from_bytes(std::vector inputs); + +/** + * @brief Verify an AVM proof from serialized data. + * @param proof The proof as a vector of field elements. + * @param public_inputs Serialized public inputs (msgpack bytes). + * @return true if verification succeeds. + */ +bool avm_verify_from_bytes(std::vector proof, std::vector public_inputs); + +/** + * @brief Check the AVM circuit from serialized inputs (msgpack bytes). + * @return true if the circuit check passes. + */ +bool avm_check_circuit_from_bytes(std::vector inputs); + // Global flag indicating AVM support is available extern const bool avm_enabled; diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.cpp new file mode 100644 index 000000000000..d459fe198127 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.cpp @@ -0,0 +1,24 @@ +#include "barretenberg/bbapi/bbapi_avm.hpp" +#include "barretenberg/api/api_avm.hpp" + +namespace bb::bbapi { + +AvmProve::Response AvmProve::execute(const BBApiRequest& /*request*/) && +{ + auto result = avm_prove_from_bytes(std::move(inputs)); + return Response{ .proof = std::move(result.proof), .verified = result.verified }; +} + +AvmVerify::Response AvmVerify::execute(const BBApiRequest& /*request*/) && +{ + bool verified = avm_verify_from_bytes(std::move(proof), std::move(public_inputs)); + return Response{ .verified = verified }; +} + +AvmCheckCircuit::Response AvmCheckCircuit::execute(const BBApiRequest& /*request*/) && +{ + bool passed = avm_check_circuit_from_bytes(std::move(inputs)); + return Response{ .passed = passed }; +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.hpp new file mode 100644 index 000000000000..56df5afd1a16 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_avm.hpp @@ -0,0 +1,86 @@ +#pragma once +/** + * @file bbapi_avm.hpp + * @brief AVM-specific command definitions for the Barretenberg RPC API. + * + * This file contains command structures for AVM operations including proving, + * verification, and circuit checking. When built with bb (non-AVM), these + * commands return an error response. When built with bb-avm, they work normally. + */ +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/common/named_union.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include +#include + +namespace bb::bbapi { + +/** + * @struct AvmProve + * @brief Prove an AVM transaction from serialized inputs. + * The inputs are opaque msgpack bytes of AvmProvingInputs. + * After proving, the generated proof is also verified. + */ +struct AvmProve { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmProve"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmProveResponse"; + + std::vector proof; + bool verified; + SERIALIZATION_FIELDS(proof, verified); + bool operator==(const Response&) const = default; + }; + + std::vector inputs; + SERIALIZATION_FIELDS(inputs); + Response execute(const BBApiRequest& request = {}) &&; + bool operator==(const AvmProve&) const = default; +}; + +/** + * @struct AvmVerify + * @brief Verify an AVM proof against serialized public inputs. + */ +struct AvmVerify { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmVerify"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmVerifyResponse"; + + bool verified; + SERIALIZATION_FIELDS(verified); + bool operator==(const Response&) const = default; + }; + + std::vector proof; + std::vector public_inputs; + SERIALIZATION_FIELDS(proof, public_inputs); + Response execute(const BBApiRequest& request = {}) &&; + bool operator==(const AvmVerify&) const = default; +}; + +/** + * @struct AvmCheckCircuit + * @brief Check the AVM circuit from serialized inputs. + */ +struct AvmCheckCircuit { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmCheckCircuit"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "AvmCheckCircuitResponse"; + + bool passed; + SERIALIZATION_FIELDS(passed); + bool operator==(const Response&) const = default; + }; + + std::vector inputs; + SERIALIZATION_FIELDS(inputs); + Response execute(const BBApiRequest& request = {}) &&; + bool operator==(const AvmCheckCircuit&) const = default; +}; + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp index 7c2db20e993d..34b2cf7c1442 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp @@ -1,5 +1,6 @@ #pragma once +#include "barretenberg/bbapi/bbapi_avm.hpp" #include "barretenberg/bbapi/bbapi_chonk.hpp" #include "barretenberg/bbapi/bbapi_crypto.hpp" #include "barretenberg/bbapi/bbapi_ecc.hpp" @@ -13,7 +14,10 @@ namespace bb::bbapi { -using Command = NamedUnion; using CommandResponse = NamedUnion inputs) +{ + avm2::AvmAPI avm; + auto proving_inputs = avm2::AvmAPI::ProvingInputs::from(inputs); + auto proof = avm.prove(proving_inputs); + + print_avm_stats(); + + // NOTE: Temporarily we also verify after proving (matching avm_prove behavior). + info("verifying..."); + bool verified = avm.verify(proof, proving_inputs.public_inputs); + info("verification: ", verified ? "success" : "failure"); + + return AvmProveResult{ .proof = std::move(proof), .verified = verified }; +} + +bool avm_verify_from_bytes(std::vector proof, std::vector public_inputs) +{ + auto pi = avm2::PublicInputs::from(public_inputs); + + avm2::AvmAPI avm; + bool res = avm.verify(proof, pi); + info("verification: ", res ? "success" : "failure"); + + print_avm_stats(); + return res; +} + +bool avm_check_circuit_from_bytes(std::vector inputs) +{ + avm2::AvmAPI avm; + auto proving_inputs = avm2::AvmAPI::ProvingInputs::from(inputs); + + bool res = avm.check_circuit(proving_inputs); + info("circuit check: ", res ? "success" : "failure"); + + print_avm_stats(); + return res; +} + } // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp index 1939294f3582..76a8978f2ddd 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp @@ -257,9 +257,9 @@ #define UPDATES_DELAYED_PUBLIC_MUTABLE_VALUES_LEN 3 #define UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE 144 #define DEFAULT_MAX_DEBUG_LOG_MEMORY_READS 125000 -#define DOM_SEP__NOTE_HASH_NONCE 1721808740UL -#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL #define DOM_SEP__SILOED_NOTE_HASH 3361878420UL +#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL +#define DOM_SEP__NOTE_HASH_NONCE 1721808740UL #define DOM_SEP__SILOED_NULLIFIER 57496191UL #define DOM_SEP__PUBLIC_LEAF_SLOT 1247650290UL #define DOM_SEP__PUBLIC_STORAGE_MAP_SLOT 4015149901UL diff --git a/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.cpp b/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.cpp index 5a3469d4ab5e..656c9d58c194 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.cpp @@ -34,4 +34,20 @@ void avm_write_verification_key([[maybe_unused]] const std::filesystem::path& ou throw_or_abort("AVM is not supported in this build. Use the 'bb-avm' binary with full AVM support."); } +AvmProveResult avm_prove_from_bytes([[maybe_unused]] std::vector inputs) +{ + throw_or_abort("AVM is not supported in this build. Use the 'bb-avm' binary with full AVM support."); +} + +bool avm_verify_from_bytes([[maybe_unused]] std::vector proof, + [[maybe_unused]] std::vector public_inputs) +{ + throw_or_abort("AVM is not supported in this build. Use the 'bb-avm' binary with full AVM support."); +} + +bool avm_check_circuit_from_bytes([[maybe_unused]] std::vector inputs) +{ + throw_or_abort("AVM is not supported in this build. Use the 'bb-avm' binary with full AVM support."); +} + } // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.hpp b/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.hpp index 00af7d8222f9..5fd09593c915 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2_stub/api_avm.hpp @@ -1,5 +1,9 @@ #pragma once +#include #include +#include + +#include "barretenberg/ecc/curves/bn254/fr.hpp" namespace bb { @@ -31,4 +35,13 @@ void avm_simulate(const std::filesystem::path& inputs_path); */ void avm_write_verification_key(const std::filesystem::path& output_path); +struct AvmProveResult { + std::vector proof; + bool verified; +}; + +AvmProveResult avm_prove_from_bytes(std::vector inputs); +bool avm_verify_from_bytes(std::vector proof, std::vector public_inputs); +bool avm_check_circuit_from_bytes(std::vector inputs); + } // namespace bb diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index bc9c9c46cb9e..d1bf75804bc2 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -611,6 +611,12 @@ pub global RECURSIVE_ROLLUP_HONK_PROOF_LENGTH: u32 = RECURSIVE_PROOF_LENGTH + IPA_CLAIM_SIZE + IPA_PROOF_LENGTH; pub global NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH: u32 = RECURSIVE_ROLLUP_HONK_PROOF_LENGTH; pub global CHONK_PROOF_LENGTH: u32 = 1632; +// Sub-proof sizes within a Chonk proof (must match C++ ChonkProof::from_field_elements split) +// Verified: 407 + 28 + 42 + 608 + 64 + 483 = 1632 +pub global CHONK_MEGA_ZK_PROOF_LENGTH_WITHOUT_PUB_INPUTS: u32 = 407; +pub global CHONK_MERGE_PROOF_LENGTH: u32 = 42; +pub global CHONK_ECCVM_PROOF_LENGTH: u32 = 608; +pub global CHONK_TRANSLATOR_PROOF_LENGTH: u32 = 483; pub global ULTRA_VK_LENGTH_IN_FIELDS: u32 = 115; // size of an Ultra verification key pub global MEGA_VK_LENGTH_IN_FIELDS: u32 = 127; // size of a Mega verification key diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/delayed_public_mutable/delayed_public_mutable_values/test.nr b/noir-projects/noir-protocol-circuits/crates/types/src/delayed_public_mutable/delayed_public_mutable_values/test.nr index acfb537c9869..c4d409d11f0b 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/delayed_public_mutable/delayed_public_mutable_values/test.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/delayed_public_mutable/delayed_public_mutable_values/test.nr @@ -74,7 +74,7 @@ unconstrained fn packed_delayed_public_mutable_values_match_typescript() { let pre_value = MockStruct { a: 1, b: 2 }; let post_value = MockStruct { a: 3, b: 4 }; - let sdc = ScheduledDelayChange::<0u64>::new(Option::some(1), Option::some(50), 2); + let sdc = ScheduledDelayChange::<0_u64>::new(Option::some(1), Option::some(50), 2); let svc = ScheduledValueChange::new(pre_value, post_value, 50); let dpmv = DelayedPublicMutableValues::new(svc, sdc); diff --git a/yarn-project/bb-prover/package.json b/yarn-project/bb-prover/package.json index 04774fdf6ea8..9bf4cc047e44 100644 --- a/yarn-project/bb-prover/package.json +++ b/yarn-project/bb-prover/package.json @@ -9,7 +9,8 @@ "./client": "./dest/prover/client/bb_private_kernel_prover.js", "./verifier": "./dest/verifier/index.js", "./test": "./dest/test/index.js", - "./config": "./dest/config.js" + "./config": "./dest/config.js", + "./debug": "./dest/bb/bb_js_debug.js" }, "bin": { "bb-cli": "./dest/bb/index.js" diff --git a/yarn-project/bb-prover/src/bb/bb_js_backend.ts b/yarn-project/bb-prover/src/bb/bb_js_backend.ts new file mode 100644 index 000000000000..4d6f691610b4 --- /dev/null +++ b/yarn-project/bb-prover/src/bb/bb_js_backend.ts @@ -0,0 +1,325 @@ +import { type BackendOptions, BackendType, Barretenberg, type ChonkProof } from '@aztec/bb.js'; +import { + CHONK_ECCVM_PROOF_LENGTH, + CHONK_MEGA_ZK_PROOF_LENGTH_WITHOUT_PUB_INPUTS, + CHONK_MERGE_PROOF_LENGTH, + CHONK_TRANSLATOR_PROOF_LENGTH, + HIDING_KERNEL_IO_PUBLIC_INPUTS_SIZE, + IPA_PROOF_LENGTH, +} from '@aztec/constants'; +import type { LogFn, Logger } from '@aztec/foundation/log'; +import { Timer } from '@aztec/foundation/timer'; + +import type { UltraHonkFlavor } from '../honk.js'; + +/** + * Maps UltraHonkFlavor to the bb.js ProofSystemSettings. + * All server-side proofs use disableZk: true. + */ +function getProofSettings(flavor: UltraHonkFlavor) { + const base = { disableZk: true, optimizedSolidityVerifier: false }; + switch (flavor) { + case 'ultra_honk': + return { ...base, oracleHashType: 'poseidon2' as const, ipaAccumulation: false }; + case 'ultra_keccak_honk': + return { ...base, oracleHashType: 'keccak' as const, ipaAccumulation: false }; + case 'ultra_starknet_honk': + return { ...base, oracleHashType: 'starknet' as const, ipaAccumulation: false }; + case 'ultra_rollup_honk': + return { ...base, oracleHashType: 'poseidon2' as const, ipaAccumulation: true }; + } +} + +/** Result of a successful proof generation via bb.js. */ +export type BBJsProofResult = { + /** Proof fields as 32-byte Uint8Arrays. */ + proofFields: Uint8Array[]; + /** Public input fields as 32-byte Uint8Arrays. */ + publicInputFields: Uint8Array[]; + /** Duration of the proving operation in ms. */ + durationMs: number; +}; + +/** Public API surface of a bb.js instance, used by the factory and debug wrapper. */ +export interface BBJsApi { + generateProof( + circuitName: string, + bytecode: Uint8Array, + verificationKey: Uint8Array, + witness: Uint8Array, + flavor: UltraHonkFlavor, + ): Promise; + verifyProof( + proofFields: Uint8Array[], + verificationKey: Uint8Array, + publicInputFields: Uint8Array[], + flavor: UltraHonkFlavor, + ): Promise<{ verified: boolean; durationMs: number }>; + verifyChonkProof( + fieldsWithPublicInputs: Uint8Array[], + verificationKey: Uint8Array, + numCustomPublicInputs: number, + ): Promise<{ verified: boolean; durationMs: number }>; + computeGateCount( + circuitName: string, + bytecode: Uint8Array, + flavor: UltraHonkFlavor | 'mega_honk', + ): Promise<{ circuitSize: number; durationMs: number }>; + generateContract(verificationKey: Uint8Array): Promise<{ solidityCode: string; durationMs: number }>; + /** Generate an AVM proof from serialized inputs. Returns proof fields and verification result. */ + generateAvmProof(inputs: Uint8Array): Promise<{ proof: Uint8Array[]; verified: boolean; durationMs: number }>; + /** Verify an AVM proof against serialized public inputs. */ + verifyAvmProof(proof: Uint8Array[], publicInputs: Uint8Array): Promise<{ verified: boolean; durationMs: number }>; + /** Check the AVM circuit from serialized inputs. */ + checkAvmCircuit(inputs: Uint8Array): Promise<{ passed: boolean; durationMs: number }>; + destroy(): Promise; +} + +/** + * Thin wrapper around a single Barretenberg instance. + * Each instance spawns its own bb process via the NativeUnixSocket backend. + */ +export class BBJsInstance implements BBJsApi { + private constructor(private api: Barretenberg) {} + + /** Creates a new Barretenberg instance connected to a fresh bb process. */ + static async create(bbPath: string, logger?: LogFn, threads?: number): Promise { + const options: BackendOptions = { + bbPath, + backend: BackendType.NativeUnixSocket, + logger, + }; + if (threads !== undefined) { + options.threads = threads; + } + const api = await Barretenberg.new(options); + return new BBJsInstance(api); + } + + /** + * Generate an UltraHonk proof for a circuit. + * @param circuitName - Identifier for the circuit (used by bb internally). + * @param bytecode - Uncompressed ACIR bytecode. + * @param verificationKey - The circuit's verification key bytes. + * @param witness - Uncompressed witness bytes. + * @param flavor - The UltraHonk flavor to use. + */ + async generateProof( + circuitName: string, + bytecode: Uint8Array, + verificationKey: Uint8Array, + witness: Uint8Array, + flavor: UltraHonkFlavor, + ): Promise { + const timer = new Timer(); + const result = await this.api.circuitProve({ + circuit: { + name: circuitName, + bytecode, + verificationKey, + }, + witness, + settings: getProofSettings(flavor), + }); + return { + proofFields: result.proof, + publicInputFields: result.publicInputs, + durationMs: timer.ms(), + }; + } + + /** + * Verify an UltraHonk proof. + * @param proofFields - Proof fields as 32-byte Uint8Arrays. + * @param verificationKey - The VK bytes. + * @param publicInputFields - Public input fields as 32-byte Uint8Arrays. + * @param flavor - The UltraHonk flavor. + * @returns Whether the proof is valid. + */ + async verifyProof( + proofFields: Uint8Array[], + verificationKey: Uint8Array, + publicInputFields: Uint8Array[], + flavor: UltraHonkFlavor, + ): Promise<{ verified: boolean; durationMs: number }> { + const timer = new Timer(); + const result = await this.api.circuitVerify({ + verificationKey, + publicInputs: publicInputFields, + proof: proofFields, + settings: getProofSettings(flavor), + }); + return { verified: result.verified, durationMs: timer.ms() }; + } + + /** + * Compute circuit gate count / circuit size. + * @param circuitName - Identifier for the circuit. + * @param bytecode - Uncompressed ACIR bytecode. + * @param flavor - 'mega_honk' for chonk circuits, or an UltraHonk flavor. + * @returns The dyadic circuit size (next power of 2 of gate count). + */ + async computeGateCount( + circuitName: string, + bytecode: Uint8Array, + flavor: UltraHonkFlavor | 'mega_honk', + ): Promise<{ circuitSize: number; durationMs: number }> { + const timer = new Timer(); + if (flavor === 'mega_honk') { + const result = await this.api.chonkStats({ + circuit: { name: circuitName, bytecode }, + includeGatesPerOpcode: false, + }); + return { circuitSize: result.circuitSize, durationMs: timer.ms() }; + } + const result = await this.api.circuitStats({ + circuit: { name: circuitName, bytecode, verificationKey: new Uint8Array(0) }, + includeGatesPerOpcode: false, + settings: getProofSettings(flavor), + }); + return { circuitSize: result.numGatesDyadic, durationMs: timer.ms() }; + } + + /** + * Generate a Solidity verifier contract from a verification key. + * @param verificationKey - The VK bytes. + * @returns The Solidity source code. + */ + async generateContract(verificationKey: Uint8Array): Promise<{ solidityCode: string; durationMs: number }> { + const timer = new Timer(); + const result = await this.api.circuitWriteSolidityVerifier({ + verificationKey, + settings: { + ipaAccumulation: false, + oracleHashType: 'poseidon2', + disableZk: true, + optimizedSolidityVerifier: false, + }, + }); + return { solidityCode: result.solidityCode, durationMs: timer.ms() }; + } + + /** + * Verify a Chonk (IVC) proof by splitting flat fields into the structured ChonkProof format. + * Mirrors C++ ChonkProof::from_field_elements() logic. + * @param fieldsWithPublicInputs - Flat proof fields as 32-byte Uint8Arrays (public inputs prepended). + * @param verificationKey - The VK bytes. + * @param numCustomPublicInputs - Number of custom public inputs beyond HidingKernelIO. + */ + async verifyChonkProof( + fieldsWithPublicInputs: Uint8Array[], + verificationKey: Uint8Array, + numCustomPublicInputs: number, + ): Promise<{ verified: boolean; durationMs: number }> { + const timer = new Timer(); + const proof = splitChonkProofToStructured(fieldsWithPublicInputs, numCustomPublicInputs); + const result = await this.api.chonkVerify({ proof, vk: verificationKey }); + return { verified: result.valid, durationMs: timer.ms() }; + } + + /** Generate an AVM proof from serialized inputs. */ + async generateAvmProof(inputs: Uint8Array): Promise<{ proof: Uint8Array[]; verified: boolean; durationMs: number }> { + const timer = new Timer(); + const result = await this.api.avmProve({ inputs }); + return { proof: result.proof, verified: result.verified, durationMs: timer.ms() }; + } + + /** Verify an AVM proof against serialized public inputs. */ + async verifyAvmProof( + proof: Uint8Array[], + publicInputs: Uint8Array, + ): Promise<{ verified: boolean; durationMs: number }> { + const timer = new Timer(); + const result = await this.api.avmVerify({ proof, publicInputs }); + return { verified: result.verified, durationMs: timer.ms() }; + } + + /** Check the AVM circuit from serialized inputs. */ + async checkAvmCircuit(inputs: Uint8Array): Promise<{ passed: boolean; durationMs: number }> { + const timer = new Timer(); + const result = await this.api.avmCheckCircuit({ inputs }); + return { passed: result.passed, durationMs: timer.ms() }; + } + + /** Destroy this instance and kill the underlying bb process. */ + async destroy(): Promise { + await this.api.destroy(); + } +} + +/** + * Factory for managing BBJsInstance lifecycle. + * Provides fresh instances for proving (each spawns a new bb process) + * and can pool instances for verification. + */ +export class BBJsProverFactory { + constructor( + private bbPath: string, + private logger?: Logger, + private threads?: number, + private debugDir?: string, + ) {} + + /** + * Run an operation with a fresh Barretenberg instance. + * The instance is created before the operation and destroyed after. + * Suitable for proving where process startup is negligible relative to proof time. + */ + async withFreshInstance(fn: (instance: BBJsApi) => Promise): Promise { + const logFn = this.logger ? (msg: string) => this.logger!.verbose(`bb.js - ${msg}`) : undefined; + const raw = await BBJsInstance.create(this.bbPath, logFn, this.threads); + const instance = await this.maybeWrapDebug(raw); + try { + return await fn(instance); + } finally { + await instance.destroy(); + } + } + + /** + * Run a verification operation. + * Currently creates a fresh instance per call (matches current behavior of spawning bb per verification). + * Can be extended to use a pool if needed. + */ + withVerifierInstance(fn: (instance: BBJsApi) => Promise): Promise { + return this.withFreshInstance(fn); + } + + /** Wrap the instance in a debug wrapper if debugDir is configured. */ + private async maybeWrapDebug(instance: BBJsInstance): Promise { + if (this.debugDir && this.logger) { + const { DebugBBJsInstance } = await import('./bb_js_debug.js'); + return new DebugBBJsInstance(instance, this.debugDir, this.bbPath, this.logger); + } + return instance; + } +} + +/** + * Split a flat Chonk proof field array into the structured ChonkProof format expected by bb.js chonkVerify. + * Mirrors C++ ChonkProof::from_field_elements() in barretenberg/cpp/src/barretenberg/chonk/chonk_proof.cpp. + */ +function splitChonkProofToStructured(fields: Uint8Array[], numCustomPublicInputs: number): ChonkProof { + let offset = 0; + + const megaSize = + CHONK_MEGA_ZK_PROOF_LENGTH_WITHOUT_PUB_INPUTS + HIDING_KERNEL_IO_PUBLIC_INPUTS_SIZE + numCustomPublicInputs; + const megaProof = fields.slice(offset, offset + megaSize); + offset += megaSize; + + const mergeProof = fields.slice(offset, offset + CHONK_MERGE_PROOF_LENGTH); + offset += CHONK_MERGE_PROOF_LENGTH; + + const eccvmProof = fields.slice(offset, offset + CHONK_ECCVM_PROOF_LENGTH); + offset += CHONK_ECCVM_PROOF_LENGTH; + + const ipaProof = fields.slice(offset, offset + IPA_PROOF_LENGTH); + offset += IPA_PROOF_LENGTH; + + const translatorProof = fields.slice(offset, offset + CHONK_TRANSLATOR_PROOF_LENGTH); + + return { + megaProof, + goblinProof: { mergeProof, eccvmProof, ipaProof, translatorProof }, + }; +} diff --git a/yarn-project/bb-prover/src/bb/bb_js_debug.ts b/yarn-project/bb-prover/src/bb/bb_js_debug.ts new file mode 100644 index 000000000000..ecb2a168414c --- /dev/null +++ b/yarn-project/bb-prover/src/bb/bb_js_debug.ts @@ -0,0 +1,227 @@ +import type { Logger } from '@aztec/foundation/log'; + +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { gzipSync } from 'zlib'; + +import type { UltraHonkFlavor } from '../honk.js'; +import type { BBJsApi, BBJsProofResult } from './bb_js_backend.js'; + +/** + * Maps UltraHonk flavors to the CLI flags used by the bb binary. + * The CLI always uses `--scheme ultra_honk`; flavors are expressed via + * `--oracle_hash` and `--ipa_accumulation`. + */ +function getCliFlags(flavor: UltraHonkFlavor): string { + const base = '--scheme ultra_honk --disable_zk'; + switch (flavor) { + case 'ultra_honk': + return `${base} --oracle_hash poseidon2`; + case 'ultra_keccak_honk': + return `${base} --oracle_hash keccak`; + case 'ultra_starknet_honk': + return `${base} --oracle_hash starknet`; + case 'ultra_rollup_honk': + return `${base} --oracle_hash poseidon2 --ipa_accumulation`; + } +} + +/** Concatenate an array of 32-byte field elements into a single buffer. */ +function concatFields(fields: Uint8Array[]): Buffer { + const totalLen = fields.reduce((sum, f) => sum + f.length, 0); + const buf = Buffer.alloc(totalLen); + let offset = 0; + for (const f of fields) { + buf.set(f, offset); + offset += f.length; + } + return buf; +} + +/** + * Wraps a BBJsApi instance to write debug files and log equivalent CLI commands. + * Activated when BB_DEBUG_OUTPUT_DIR is set. Each operation writes its inputs + * and outputs to a numbered subdirectory, enabling offline reproduction. + */ +export class DebugBBJsInstance implements BBJsApi { + private counter = 0; + + constructor( + private inner: BBJsApi, + private debugDir: string, + private bbPath: string, + private logger: Logger, + ) {} + + private nextDir(prefix: string): string { + this.counter++; + const padded = String(this.counter).padStart(3, '0'); + return path.join(this.debugDir, `${prefix}-${padded}`); + } + + /** Write a command string to both the logger and a command.sh file in the given directory. */ + private async logCommand(dir: string, command: string): Promise { + this.logger.info(`Executing BB with: ${command}`); + await fs.writeFile(path.join(dir, 'command.sh'), `#!/bin/bash\n${command}\n`, { mode: 0o755 }); + } + + async generateProof( + circuitName: string, + bytecode: Uint8Array, + verificationKey: Uint8Array, + witness: Uint8Array, + flavor: UltraHonkFlavor, + ): Promise { + const dir = this.nextDir(circuitName); + await fs.mkdir(dir, { recursive: true }); + + const bytecodePath = path.join(dir, `${circuitName}-bytecode.gz`); + const vkPath = path.join(dir, `${circuitName}-vk`); + const witnessPath = path.join(dir, 'partial-witness.gz'); + + await Promise.all([ + fs.writeFile(bytecodePath, gzipSync(bytecode)), + fs.writeFile(vkPath, verificationKey), + fs.writeFile(witnessPath, gzipSync(witness)), + ]); + + const flags = getCliFlags(flavor); + await this.logCommand( + dir, + `${this.bbPath} prove ${flags} -o ${dir} -b ${bytecodePath} -k ${vkPath} -w ${witnessPath}`, + ); + + const result = await this.inner.generateProof(circuitName, bytecode, verificationKey, witness, flavor); + + const proofBuf = concatFields(result.proofFields); + const publicInputsBuf = concatFields(result.publicInputFields); + await Promise.all([ + fs.writeFile(path.join(dir, 'proof'), proofBuf), + fs.writeFile(path.join(dir, 'public_inputs'), publicInputsBuf), + ]); + + return result; + } + + async verifyProof( + proofFields: Uint8Array[], + verificationKey: Uint8Array, + publicInputFields: Uint8Array[], + flavor: UltraHonkFlavor, + ): Promise<{ verified: boolean; durationMs: number }> { + const dir = this.nextDir(`verify-${flavor}`); + await fs.mkdir(dir, { recursive: true }); + + const proofPath = path.join(dir, 'proof'); + const vkPath = path.join(dir, 'vk'); + const publicInputsPath = path.join(dir, 'public_inputs'); + + const proofBuf = concatFields(proofFields); + const publicInputsBuf = concatFields(publicInputFields); + await Promise.all([ + fs.writeFile(proofPath, proofBuf), + fs.writeFile(vkPath, verificationKey), + fs.writeFile(publicInputsPath, publicInputsBuf), + ]); + + const flags = getCliFlags(flavor); + await this.logCommand(dir, `${this.bbPath} verify ${flags} -p ${proofPath} -k ${vkPath} -i ${publicInputsPath}`); + + return this.inner.verifyProof(proofFields, verificationKey, publicInputFields, flavor); + } + + async verifyChonkProof( + fieldsWithPublicInputs: Uint8Array[], + verificationKey: Uint8Array, + numCustomPublicInputs: number, + ): Promise<{ verified: boolean; durationMs: number }> { + const dir = this.nextDir('verify-chonk'); + await fs.mkdir(dir, { recursive: true }); + + const proofPath = path.join(dir, 'proof'); + const vkPath = path.join(dir, 'vk'); + + const proofBuf = concatFields(fieldsWithPublicInputs); + await Promise.all([fs.writeFile(proofPath, proofBuf), fs.writeFile(vkPath, verificationKey)]); + + await this.logCommand(dir, `${this.bbPath} verify --scheme chonk -p ${proofPath} -k ${vkPath} -v`); + + return this.inner.verifyChonkProof(fieldsWithPublicInputs, verificationKey, numCustomPublicInputs); + } + + async computeGateCount( + circuitName: string, + bytecode: Uint8Array, + flavor: UltraHonkFlavor | 'mega_honk', + ): Promise<{ circuitSize: number; durationMs: number }> { + const dir = this.nextDir(`gates-${circuitName}`); + await fs.mkdir(dir, { recursive: true }); + + const bytecodePath = path.join(dir, `${circuitName}-bytecode.gz`); + await fs.writeFile(bytecodePath, gzipSync(bytecode)); + + if (flavor === 'mega_honk') { + await this.logCommand(dir, `${this.bbPath} gates --scheme chonk -b ${bytecodePath}`); + } else { + const flags = getCliFlags(flavor); + await this.logCommand(dir, `${this.bbPath} gates ${flags} -b ${bytecodePath}`); + } + + return this.inner.computeGateCount(circuitName, bytecode, flavor); + } + + async generateAvmProof(inputs: Uint8Array): Promise<{ proof: Uint8Array[]; verified: boolean; durationMs: number }> { + const dir = this.nextDir('avm-prove'); + await fs.mkdir(dir, { recursive: true }); + + const inputsPath = path.join(dir, 'avm_inputs.bin'); + await fs.writeFile(inputsPath, inputs); + + await this.logCommand(dir, `${this.bbPath} avm_prove --avm-inputs ${inputsPath} -o ${dir}`); + + const result = await this.inner.generateAvmProof(inputs); + + const proofBuf = concatFields(result.proof); + await fs.writeFile(path.join(dir, 'proof'), proofBuf); + + return result; + } + + async verifyAvmProof( + proof: Uint8Array[], + publicInputs: Uint8Array, + ): Promise<{ verified: boolean; durationMs: number }> { + const dir = this.nextDir('avm-verify'); + await fs.mkdir(dir, { recursive: true }); + + const proofPath = path.join(dir, 'proof'); + const publicInputsPath = path.join(dir, 'avm_public_inputs.bin'); + + const proofBuf = concatFields(proof); + await Promise.all([fs.writeFile(proofPath, proofBuf), fs.writeFile(publicInputsPath, publicInputs)]); + + await this.logCommand(dir, `${this.bbPath} avm_verify -p ${proofPath} --avm-public-inputs ${publicInputsPath}`); + + return this.inner.verifyAvmProof(proof, publicInputs); + } + + async checkAvmCircuit(inputs: Uint8Array): Promise<{ passed: boolean; durationMs: number }> { + const dir = this.nextDir('avm-check-circuit'); + await fs.mkdir(dir, { recursive: true }); + + const inputsPath = path.join(dir, 'avm_inputs.bin'); + await fs.writeFile(inputsPath, inputs); + + await this.logCommand(dir, `${this.bbPath} avm_check_circuit --avm-inputs ${inputsPath}`); + + return this.inner.checkAvmCircuit(inputs); + } + + generateContract(verificationKey: Uint8Array): Promise<{ solidityCode: string; durationMs: number }> { + return this.inner.generateContract(verificationKey); + } + + destroy(): Promise { + return this.inner.destroy(); + } +} diff --git a/yarn-project/bb-prover/src/bb/execute.ts b/yarn-project/bb-prover/src/bb/execute.ts index a2d5d2e1df73..023dfd1e4d13 100644 --- a/yarn-project/bb-prover/src/bb/execute.ts +++ b/yarn-project/bb-prover/src/bb/execute.ts @@ -1,15 +1,12 @@ -import { sha256 } from '@aztec/foundation/crypto/sha256'; import type { LogFn, Logger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import type { AvmCircuitInputs, AvmCircuitPublicInputs } from '@aztec/stdlib/avm'; import * as proc from 'child_process'; import { promises as fs } from 'fs'; -import { basename, dirname, join } from 'path'; +import { join } from 'path'; import readline from 'readline'; -import type { UltraHonkFlavor } from '../honk.js'; - export const VK_FILENAME = 'vk'; export const PUBLIC_INPUTS_FILENAME = 'public_inputs'; export const PROOF_FILENAME = 'proof'; @@ -60,6 +57,7 @@ export const DEFAULT_BB_VERIFY_CONCURRENCY = 4; * @param command - The command to execute * @param args - The arguments to pass * @param logger - A log function + * @param concurrency - An optional concurrency setting * @param timeout - An optional timeout before killing the BB process * @param resultParser - An optional handler for detecting success or failure * @returns The completed partial witness outputted from the circuit @@ -119,173 +117,6 @@ export function executeBB( }).catch(_ => ({ status: BB_RESULT.FAILURE, exitCode: -1, signal: undefined })); } -export async function executeBbChonkProof( - pathToBB: string, - workingDirectory: string, - inputsPath: string, - log: LogFn, - writeVk = false, -): Promise { - // Check that the working directory exists - try { - await fs.access(workingDirectory); - } catch { - return { status: BB_RESULT.FAILURE, reason: `Working directory ${workingDirectory} does not exist` }; - } - - // The proof is written to e.g. /workingDirectory/proof - const outputPath = `${workingDirectory}`; - - const binaryPresent = await fs - .access(pathToBB, fs.constants.R_OK) - .then(_ => true) - .catch(_ => false); - if (!binaryPresent) { - return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` }; - } - - try { - // Write the bytecode to the working directory - log(`inputsPath ${inputsPath}`); - const timer = new Timer(); - const logFunction = (message: string) => { - log(`bb - ${message}`); - }; - - const args = ['-o', outputPath, '--ivc_inputs_path', inputsPath, '-v', '--scheme', 'chonk']; - if (writeVk) { - args.push('--write_vk'); - } - const result = await executeBB(pathToBB, 'prove', args, logFunction); - const durationMs = timer.ms(); - - if (result.status == BB_RESULT.SUCCESS) { - return { - status: BB_RESULT.SUCCESS, - durationMs, - proofPath: `${outputPath}`, - pkPath: undefined, - vkDirectoryPath: `${outputPath}`, - }; - } - // Not a great error message here but it is difficult to decipher what comes from bb - return { - status: BB_RESULT.FAILURE, - reason: `Failed to generate proof. Exit code ${result.exitCode}. Signal ${result.signal}.`, - retry: !!result.signal, - }; - } catch (error) { - return { status: BB_RESULT.FAILURE, reason: `${error}` }; - } -} - -function getArgs(flavor: UltraHonkFlavor) { - switch (flavor) { - case 'ultra_honk': { - return ['--scheme', 'ultra_honk', '--oracle_hash', 'poseidon2']; - } - case 'ultra_keccak_honk': { - return ['--scheme', 'ultra_honk', '--oracle_hash', 'keccak']; - } - case 'ultra_starknet_honk': { - return ['--scheme', 'ultra_honk', '--oracle_hash', 'starknet']; - } - case 'ultra_rollup_honk': { - return ['--scheme', 'ultra_honk', '--oracle_hash', 'poseidon2', '--ipa_accumulation']; - } - } -} - -/** - * Used for generating proofs of noir circuits. - * It is assumed that the working directory is a temporary and/or random directory used solely for generating this proof. - * @param pathToBB - The full path to the bb binary - * @param workingDirectory - A working directory for use by bb - * @param circuitName - An identifier for the circuit - * @param bytecode - The compiled circuit bytecode - * @param inputWitnessFile - The circuit input witness - * @param log - A logging function - * @returns An object containing a result indication, the location of the proof and the duration taken - */ -export async function generateProof( - pathToBB: string, - workingDirectory: string, - circuitName: string, - bytecode: Buffer, - verificationKey: Buffer, - inputWitnessFile: string, - flavor: UltraHonkFlavor, - log: Logger, -): Promise { - // Check that the working directory exists - try { - await fs.access(workingDirectory); - } catch { - return { status: BB_RESULT.FAILURE, reason: `Working directory ${workingDirectory} does not exist` }; - } - - // The bytecode is written to e.g. /workingDirectory/ParityBaseArtifact-bytecode - const bytecodePath = `${workingDirectory}/${circuitName}-bytecode`; - const vkPath = `${workingDirectory}/${circuitName}-vk`; - - // The proof is written to e.g. /workingDirectory/ultra_honk/proof - const outputPath = `${workingDirectory}`; - - const binaryPresent = await fs - .access(pathToBB, fs.constants.R_OK) - .then(_ => true) - .catch(_ => false); - if (!binaryPresent) { - return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` }; - } - - try { - // Write the bytecode and vk to the working directory - await Promise.all([fs.writeFile(bytecodePath, bytecode), fs.writeFile(vkPath, verificationKey)]); - const args = getArgs(flavor).concat([ - '--disable_zk', - '-o', - outputPath, - '-b', - bytecodePath, - '-k', - vkPath, - '-w', - inputWitnessFile, - '-v', - ]); - const loggingArg = log.level === 'debug' || log.level === 'trace' ? '-d' : log.level === 'verbose' ? '-v' : ''; - if (loggingArg !== '') { - args.push(loggingArg); - } - - const timer = new Timer(); - const logFunction = (message: string) => { - log.info(`${circuitName} BB out - ${message}`); - }; - const result = await executeBB(pathToBB, `prove`, args, logFunction); - const duration = timer.ms(); - - if (result.status == BB_RESULT.SUCCESS) { - return { - status: BB_RESULT.SUCCESS, - durationMs: duration, - proofPath: `${outputPath}`, - pkPath: undefined, - vkDirectoryPath: `${outputPath}`, - }; - } - // Not a great error message here but it is difficult to decipher what comes from bb - return { - status: BB_RESULT.FAILURE, - reason: `Failed to generate proof. Exit code ${result.exitCode}. Signal ${result.signal}.`, - retry: !!result.signal, - }; - } catch (error) { - return { status: BB_RESULT.FAILURE, reason: `${error}` }; - } -} - /** * Used for generating AVM proofs. * It is assumed that the working directory is a temporary and/or random directory used solely for generating this proof. @@ -369,47 +200,6 @@ export async function generateAvmProof( } } -/** - * Used for verifying proofs of noir circuits - * @param pathToBB - The full path to the bb binary - * @param proofFullPath - The full path to the proof to be verified - * @param verificationKeyPath - The full path to the circuit verification key - * @param logger - A logger - * @returns An object containing a result indication and duration taken - */ -export async function verifyProof( - pathToBB: string, - proofFullPath: string, - verificationKeyPath: string, - ultraHonkFlavor: UltraHonkFlavor, - logger: Logger, -): Promise { - // Specify the public inputs path in the case of UH verification. - // Take proofFullPath and remove the suffix past the / to get the directory. - const proofDir = proofFullPath.substring(0, proofFullPath.lastIndexOf('/')); - const publicInputsFullPath = join(proofDir, '/public_inputs'); - logger.debug(`public inputs path: ${publicInputsFullPath}`); - - const args = [ - '-p', - proofFullPath, - '-k', - verificationKeyPath, - '-i', - publicInputsFullPath, - '--disable_zk', - ...getArgs(ultraHonkFlavor), - ]; - - let concurrency = DEFAULT_BB_VERIFY_CONCURRENCY; - - if (process.env.VERIFY_HARDWARE_CONCURRENCY) { - concurrency = parseInt(process.env.VERIFY_HARDWARE_CONCURRENCY, 10); - } - - return await verifyProofInternal(pathToBB, `verify`, args, logger, concurrency); -} - export async function verifyAvmProof( pathToBB: string, workingDirectory: string, @@ -435,34 +225,6 @@ export async function verifyAvmProof( return await verifyProofInternal(pathToBB, 'avm_verify', args, logger); } -/** - * Verifies a ChonkProof - * TODO(#7370) The verification keys should be supplied separately - * @param pathToBB - The full path to the bb binary - * @param targetPath - The path to the folder with the proof, accumulator, and verification keys - * @param logger - A logger - * @param concurrency - The number of threads to use for the verification - * @returns An object containing a result indication and duration taken - */ -export async function verifyChonkProof( - pathToBB: string, - proofPath: string, - keyPath: string, - logger: Logger, - concurrency = 1, -): Promise { - const binaryPresent = await fs - .access(pathToBB, fs.constants.R_OK) - .then(_ => true) - .catch(_ => false); - if (!binaryPresent) { - return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` }; - } - - const args = ['--scheme', 'chonk', '-p', proofPath, '-k', keyPath, '-v']; - return await verifyProofInternal(pathToBB, 'verify', args, logger, concurrency); -} - /** * Used for verifying proofs with BB * @param pathToBB - The full path to the bb binary @@ -512,176 +274,3 @@ async function verifyProofInternal( return { status: BB_RESULT.FAILURE, reason: `${error}` }; } } - -export async function generateContractForVerificationKey( - pathToBB: string, - vkFilePath: string, - contractPath: string, - log: LogFn, -): Promise { - const binaryPresent = await fs - .access(pathToBB, fs.constants.R_OK) - .then(_ => true) - .catch(_ => false); - - if (!binaryPresent) { - return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` }; - } - - const outputDir = dirname(contractPath); - const contractName = basename(contractPath); - // cache contract generation based on vk file and contract name - const cacheKey = sha256(Buffer.concat([Buffer.from(contractName), await fs.readFile(vkFilePath)])); - - await fs.mkdir(outputDir, { recursive: true }); - - const res = await fsCache(outputDir, cacheKey, log, false, async () => { - try { - const args = ['--scheme', 'ultra_honk', '-k', vkFilePath, '-o', contractPath, '-v']; - const timer = new Timer(); - const result = await executeBB(pathToBB, 'contract', args, log); - const duration = timer.ms(); - if (result.status == BB_RESULT.SUCCESS) { - return { status: BB_RESULT.SUCCESS, durationMs: duration, contractPath }; - } - // Not a great error message here but it is difficult to decipher what comes from bb - return { - status: BB_RESULT.FAILURE, - reason: `Failed to write verifier contract. Exit code ${result.exitCode}. Signal ${result.signal}.`, - retry: !!result.signal, - }; - } catch (error) { - return { status: BB_RESULT.FAILURE, reason: `${error}` }; - } - }); - - if (!res) { - return { - status: BB_RESULT.ALREADY_PRESENT, - durationMs: 0, - contractPath, - }; - } - - return res; -} - -/** - * Compute bb gate count for a given circuit - * @param pathToBB - The full path to the bb binary - * @param workingDirectory - A temporary directory for writing the bytecode - * @param circuitName - The name of the circuit - * @param bytecode - The bytecode of the circuit - * @param flavor - The flavor of the backend - mega_honk or ultra_honk variants - * @returns An object containing the status, gate count, and time taken - */ -export async function computeGateCountForCircuit( - pathToBB: string, - workingDirectory: string, - circuitName: string, - bytecode: Buffer, - flavor: UltraHonkFlavor | 'mega_honk', - log: LogFn, -): Promise { - // Check that the working directory exists - try { - await fs.access(workingDirectory); - } catch { - return { status: BB_RESULT.FAILURE, reason: `Working directory ${workingDirectory} does not exist` }; - } - - // The bytecode is written to e.g. /workingDirectory/ParityBaseArtifact-bytecode - const bytecodePath = `${workingDirectory}/${circuitName}-bytecode`; - - const binaryPresent = await fs - .access(pathToBB, fs.constants.R_OK) - .then(_ => true) - .catch(_ => false); - if (!binaryPresent) { - return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` }; - } - - // Accumulate the stdout from bb - let stdout = ''; - const logHandler = (message: string) => { - stdout += message; - log(message); - }; - - try { - // Write the bytecode to the working directory - await fs.writeFile(bytecodePath, bytecode); - const timer = new Timer(); - - const result = await executeBB( - pathToBB, - 'gates', - ['--scheme', flavor === 'mega_honk' ? 'chonk' : 'ultra_honk', '-b', bytecodePath, '-v'], - logHandler, - ); - const duration = timer.ms(); - - if (result.status == BB_RESULT.SUCCESS) { - // Look for "circuit_size" in the stdout and parse the number - const circuitSizeMatch = stdout.match(/circuit_size": (\d+)/); - if (!circuitSizeMatch) { - return { status: BB_RESULT.FAILURE, reason: 'Failed to parse circuit_size from bb gates stdout.' }; - } - const circuitSize = parseInt(circuitSizeMatch[1]); - - return { - status: BB_RESULT.SUCCESS, - durationMs: duration, - circuitSize: circuitSize, - }; - } - - return { status: BB_RESULT.FAILURE, reason: 'Failed getting the gate count.' }; - } catch (error) { - return { status: BB_RESULT.FAILURE, reason: `${error}` }; - } -} - -const CACHE_FILENAME = '.cache'; -async function fsCache( - dir: string, - expectedCacheKey: Buffer, - logger: LogFn, - force: boolean, - action: () => Promise, -): Promise { - const cacheFilePath = join(dir, CACHE_FILENAME); - - let run: boolean; - if (force) { - run = true; - } else { - try { - run = !expectedCacheKey.equals(await fs.readFile(cacheFilePath)); - } catch (err: any) { - if (err && 'code' in err && err.code === 'ENOENT') { - // cache file doesn't exist, swallow error and run - run = true; - } else { - throw err; - } - } - } - - let res: T | undefined; - if (run) { - logger(`Cache miss or forced run. Running operation in ${dir}...`); - res = await action(); - } else { - logger(`Cache hit. Skipping operation in ${dir}...`); - } - - try { - await fs.writeFile(cacheFilePath, expectedCacheKey); - } catch { - logger(`Couldn't write cache data to ${cacheFilePath}. Skipping cache...`); - // ignore - } - - return res; -} diff --git a/yarn-project/bb-prover/src/config.ts b/yarn-project/bb-prover/src/config.ts index 60a33c9a67b6..5d78762bb11c 100644 --- a/yarn-project/bb-prover/src/config.ts +++ b/yarn-project/bb-prover/src/config.ts @@ -5,6 +5,8 @@ export interface BBConfig { bbSkipCleanup: boolean; numConcurrentIVCVerifiers: number; bbIVCConcurrency: number; + /** When set, bb.js operations write input/output files and log equivalent CLI commands to this directory. */ + bbDebugOutputDir?: string; } export interface ACVMConfig { diff --git a/yarn-project/bb-prover/src/index.ts b/yarn-project/bb-prover/src/index.ts index 692c6007f391..723d673954ff 100644 --- a/yarn-project/bb-prover/src/index.ts +++ b/yarn-project/bb-prover/src/index.ts @@ -3,6 +3,7 @@ export * from './test/index.js'; export * from './verifier/index.js'; export * from './config.js'; export * from './bb/execute.js'; +export * from './bb/bb_js_backend.js'; export * from './honk.js'; export * from './verification_key/verification_key_data.js'; diff --git a/yarn-project/bb-prover/src/prover/proof_utils.ts b/yarn-project/bb-prover/src/prover/proof_utils.ts index 8d2e0091d2a4..ac7f74e75285 100644 --- a/yarn-project/bb-prover/src/prover/proof_utils.ts +++ b/yarn-project/bb-prover/src/prover/proof_utils.ts @@ -113,3 +113,43 @@ export async function readProofsFromOutputDirectory proofLength, ); } + +/** + * Construct a RecursiveProof from in-memory proof and public input field arrays + * returned by the bb.js circuitProve API, without reading from files. + * + * @param proofFields - Proof fields as 32-byte Uint8Arrays from circuitProve. + * @param publicInputFields - Public input fields as 32-byte Uint8Arrays from circuitProve. + * @param vkData - Verification key data for the circuit. + * @param proofLength - Expected proof field count. + */ +export function constructRecursiveProofFromBuffers( + proofFields: Uint8Array[], + publicInputFields: Uint8Array[], + vkData: VerificationKeyData, + proofLength: PROOF_LENGTH, +): RecursiveProof { + assert( + proofLength == NESTED_RECURSIVE_PROOF_LENGTH || + proofLength == NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH || + proofLength == ULTRA_KECCAK_PROOF_LENGTH, + `Proof length must be one of the expected proof lengths, received ${proofLength}`, + ); + + // Convert Uint8Array fields to Fr instances + const proofFieldsFr = proofFields.map(f => Fr.fromBuffer(Buffer.from(f))); + + assert( + proofFieldsFr.length == proofLength, + `Proof fields length mismatch: ${proofFieldsFr.length} != ${proofLength}`, + ); + + // Reconstruct the binary proof with public inputs prepended (same format as file-based path) + const binaryPublicInputs = Buffer.concat(publicInputFields.map(f => Buffer.from(f))); + const binaryProof = Buffer.concat(proofFields.map(f => Buffer.from(f))); + const binaryProofWithPublicInputs = Buffer.concat([binaryPublicInputs, binaryProof]); + + const numPublicInputs = getNumCustomPublicInputs(proofLength, vkData); + + return new RecursiveProof(proofFieldsFr, new Proof(binaryProofWithPublicInputs, numPublicInputs), true, proofLength); +} diff --git a/yarn-project/bb-prover/src/prover/server/bb_prover.ts b/yarn-project/bb-prover/src/prover/server/bb_prover.ts index 46dfc97ee6ca..d6b8c19cb812 100644 --- a/yarn-project/bb-prover/src/prover/server/bb_prover.ts +++ b/yarn-project/bb-prover/src/prover/server/bb_prover.ts @@ -9,7 +9,6 @@ import { import { Fr } from '@aztec/foundation/curves/bn254'; import { runInDirectory } from '@aztec/foundation/fs'; import { createLogger } from '@aztec/foundation/log'; -import { BufferReader } from '@aztec/foundation/serialize'; import { type ServerProtocolArtifact, convertBlockMergeRollupOutputsFromWitnessMap, @@ -88,24 +87,15 @@ import { VerificationKeyData } from '@aztec/stdlib/vks'; import { Attributes, type TelemetryClient, getTelemetryClient, trackSpan } from '@aztec/telemetry-client'; import { promises as fs } from 'fs'; +import { ungzip } from 'pako'; import * as path from 'path'; -import { - type BBFailure, - type BBSuccess, - BB_RESULT, - PROOF_FILENAME, - PUBLIC_INPUTS_FILENAME, - VK_FILENAME, - generateAvmProof, - generateProof, - verifyAvmProof, - verifyProof, -} from '../../bb/execute.js'; +import { type BBJsProofResult, BBJsProverFactory } from '../../bb/bb_js_backend.js'; +import { BB_RESULT } from '../../bb/execute.js'; import type { ACVMConfig, BBConfig } from '../../config.js'; import { getUltraHonkFlavorForCircuit } from '../../honk.js'; import { ProverInstrumentation } from '../../instrumentation.js'; -import { readProofsFromOutputDirectory } from '../proof_utils.js'; +import { constructRecursiveProofFromBuffers } from '../proof_utils.js'; const logger = createLogger('bb-prover'); @@ -119,12 +109,14 @@ export interface BBProverConfig extends BBConfig, ACVMConfig { */ export class BBNativeRollupProver implements ServerCircuitProver { private instrumentation: ProverInstrumentation; + private bbJsFactory: BBJsProverFactory; constructor( private config: BBProverConfig, telemetry: TelemetryClient, ) { this.instrumentation = new ProverInstrumentation(telemetry, 'BBNativeRollupProver'); + this.bbJsFactory = new BBJsProverFactory(config.bbBinaryPath, logger, undefined, config.bbDebugOutputDir); } get tracer() { @@ -136,7 +128,7 @@ export class BBNativeRollupProver implements ServerCircuitProver { await fs.mkdir(config.acvmWorkingDirectory, { recursive: true }); await fs.access(config.bbBinaryPath, fs.constants.R_OK); await fs.mkdir(config.bbWorkingDirectory, { recursive: true }); - logger.info(`Using native BB at ${config.bbBinaryPath} and working directory ${config.bbWorkingDirectory}`); + logger.info(`Using bb.js API with binary at ${config.bbBinaryPath}`); logger.info(`Using native ACVM at ${config.acvmBinaryPath} and working directory ${config.acvmWorkingDirectory}`); return new BBNativeRollupProver(config, telemetry); @@ -453,12 +445,11 @@ export class BBNativeRollupProver implements ServerCircuitProver { convertInput: (input: Input) => WitnessMap, convertOutput: (outputWitness: WitnessMap) => Output, workingDirectory: string, - ): Promise<{ circuitOutput: Output; provingResult: BBSuccess }> { - // Have the ACVM write the partial witness here + ): Promise<{ circuitOutput: Output; proofResult: BBJsProofResult }> { + // Have the ACVM write the partial witness here (still needs a temp directory) const outputWitnessFile = path.join(workingDirectory, 'partial-witness.gz'); // Generate the partial witness using the ACVM - // A further temp directory will be created beneath ours and then cleaned up after the partial witness has been copied to our specified location const simulator = new NativeACVMSimulator( this.config.acvmWorkingDirectory, this.config.acvmBinaryPath, @@ -471,7 +462,7 @@ export class BBNativeRollupProver implements ServerCircuitProver { logger.debug(`Generating witness data for ${circuitType}`); const inputWitness = convertInput(input); - const foreignCallHandler = undefined; // We don't handle foreign calls in the native ACVM simulator + const foreignCallHandler = undefined; const witnessResult = await simulator.executeProtocolCircuit(inputWitness, artifact, foreignCallHandler); const output = convertOutput(witnessResult.witness); @@ -488,75 +479,88 @@ export class BBNativeRollupProver implements ServerCircuitProver { eventName: 'circuit-witness-generation', } satisfies CircuitWitnessGenerationStats); - // Now prove the circuit from the generated witness - logger.debug(`Proving ${circuitType}...`); - - const provingResult = await generateProof( - this.config.bbBinaryPath, - workingDirectory, - circuitType, - Buffer.from(artifact.bytecode, 'base64'), - this.getVerificationKeyDataForCircuit(circuitType).keyAsBytes, - outputWitnessFile, - getUltraHonkFlavorForCircuit(circuitType), - logger, - ); - - if (provingResult.status === BB_RESULT.FAILURE) { - logger.error(`Failed to generate proof for ${circuitType}: ${provingResult.reason}`); - throw new ProvingError(provingResult.reason, provingResult, provingResult.retry); + // Read and decompress the witness for bb.js + const witnessGz = await fs.readFile(outputWitnessFile); + const witness = ungzip(witnessGz); + + // Decompress bytecode for bb.js + const bytecode = ungzip(Buffer.from(artifact.bytecode, 'base64')); + + // Prove the circuit via bb.js API (spawns a fresh bb process per proof) + logger.debug(`Proving ${circuitType} via bb.js...`); + + let proofResult: BBJsProofResult; + try { + proofResult = await this.bbJsFactory.withFreshInstance(instance => + instance.generateProof( + circuitType, + bytecode, + this.getVerificationKeyDataForCircuit(circuitType).keyAsBytes, + witness, + getUltraHonkFlavorForCircuit(circuitType), + ), + ); + } catch (error) { + throw new ProvingError(`Failed to generate proof for ${circuitType}: ${error}`); } return { circuitOutput: output, - provingResult, + proofResult, }; } - private async generateAvmProofWithBB(input: AvmCircuitInputs, workingDirectory: string): Promise { + private async createAvmProof( + input: AvmCircuitInputs, + ): Promise> { logger.info(`Proving avm-circuit for TX ${input.hints.tx.hash}...`); - const provingResult = await generateAvmProof(this.config.bbBinaryPath, workingDirectory, input, logger); + const inputsBuffer = input.serializeWithMessagePack(); + const { + proof: proofFieldArrays, + verified, + durationMs, + } = await this.bbJsFactory.withFreshInstance(instance => instance.generateAvmProof(inputsBuffer)); - if (provingResult.status === BB_RESULT.FAILURE) { - logger.error(`Failed to generate AVM proof for TX ${input.hints.tx.hash}: ${provingResult.reason}`); - throw new ProvingError(provingResult.reason, provingResult, provingResult.retry); + if (!verified) { + throw new ProvingError('AVM proof verification failed after proving'); } - return provingResult; - } + // Convert Uint8Array[] (32-byte field elements) to Fr[] + const proofFields = proofFieldArrays.map(f => Fr.fromBuffer(Buffer.from(f))); - private async createAvmProof( - input: AvmCircuitInputs, - ): Promise> { - const operation = async (bbWorkingDirectory: string) => { - const provingResult = await this.generateAvmProofWithBB(input, bbWorkingDirectory); + // Pad to fixed size (during development the proof length may vary) + if (proofFields.length > AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED) { + throw new Error( + `Proof has ${proofFields.length} fields, expected no more than ${AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED}.`, + ); + } + const proofFieldsPadded = proofFields.concat( + Array(AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED - proofFields.length).fill(new Fr(0)), + ); - const avmProof = await this.readAvmProofAsFields(provingResult.proofPath!); + // Build the binary proof from the raw field data + const rawProofBuffer = Buffer.concat(proofFieldArrays.map(f => Buffer.from(f))); + const binaryProof = new Proof(rawProofBuffer, /*numPublicInputs=*/ 0); + const avmProof = new RecursiveProof(proofFieldsPadded, binaryProof, true, AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED); - const circuitType = 'avm-circuit' as const; - const appCircuitName = 'unknown' as const; - this.instrumentation.recordAvmDuration('provingDuration', appCircuitName, provingResult.durationMs); - this.instrumentation.recordAvmSize('proofSize', appCircuitName, avmProof.binaryProof.buffer.length); + const circuitType = 'avm-circuit' as const; + const appCircuitName = 'unknown' as const; + this.instrumentation.recordAvmDuration('provingDuration', appCircuitName, durationMs); + this.instrumentation.recordAvmSize('proofSize', appCircuitName, avmProof.binaryProof.buffer.length); - logger.info( - `Generated proof for ${circuitType}(${input.hints.tx.hash}) in ${Math.ceil(provingResult.durationMs)} ms`, - { - circuitName: circuitType, - appCircuitName: input.hints.tx.hash, - // does not include reading the proof from disk - duration: provingResult.durationMs, - proofSize: avmProof.binaryProof.buffer.length, - eventName: 'circuit-proving', - inputSize: input.serializeWithMessagePack().length, - circuitSize: 1 << 21, - numPublicInputs: 0, - } satisfies CircuitProvingStats, - ); + logger.info(`Generated proof for ${circuitType}(${input.hints.tx.hash}) in ${Math.ceil(durationMs)} ms`, { + circuitName: circuitType, + appCircuitName: input.hints.tx.hash, + duration: durationMs, + proofSize: avmProof.binaryProof.buffer.length, + eventName: 'circuit-proving', + inputSize: inputsBuffer.length, + circuitSize: 1 << 21, + numPublicInputs: 0, + } satisfies CircuitProvingStats); - return avmProof; - }; - return await this.runInDirectory(operation); + return avmProof; } /** @@ -579,33 +583,38 @@ export class BBNativeRollupProver implements ServerCircuitProver { convertInput: (input: CircuitInputType) => WitnessMap, convertOutput: (outputWitness: WitnessMap) => CircuitOutputType, ): Promise<{ circuitOutput: CircuitOutputType; proof: RecursiveProof }> { - // this probably is gonna need to call chonk - const operation = async (bbWorkingDirectory: string) => { - const { provingResult, circuitOutput: output } = await this.generateProofWithBB( + // Still need runInDirectory for ACVM witness generation temp files + const operation = async (workingDirectory: string) => { + const { proofResult, circuitOutput: output } = await this.generateProofWithBB( input, circuitType, convertInput, convertOutput, - bbWorkingDirectory, + workingDirectory, ); const vkData = this.getVerificationKeyDataForCircuit(circuitType); - // Read the proof as fields - const proof = await readProofsFromOutputDirectory(provingResult.proofPath!, vkData, proofLength, logger); + // Construct proof from in-memory buffers (no file I/O needed) + const proof = constructRecursiveProofFromBuffers( + proofResult.proofFields, + proofResult.publicInputFields, + vkData, + proofLength, + ); const circuitName = mapProtocolArtifactNameToCircuitName(circuitType); - this.instrumentation.recordDuration('provingDuration', circuitName, provingResult.durationMs); + this.instrumentation.recordDuration('provingDuration', circuitName, proofResult.durationMs); this.instrumentation.recordSize('proofSize', circuitName, proof.binaryProof.buffer.length); this.instrumentation.recordSize('circuitPublicInputCount', circuitName, vkData.numPublicInputs); this.instrumentation.recordSize('circuitSize', circuitName, vkData.circuitSize); logger.info( - `Generated proof for ${circuitType} in ${Math.ceil(provingResult.durationMs)} ms, size: ${ + `Generated proof for ${circuitType} in ${Math.ceil(proofResult.durationMs)} ms, size: ${ proof.proof.length } fields`, { circuitName, circuitSize: vkData.circuitSize, - duration: provingResult.durationMs, + duration: proofResult.durationMs, inputSize: output.toBuffer().length, proofSize: proof.binaryProof.buffer.length, eventName: 'circuit-proving', @@ -622,49 +631,59 @@ export class BBNativeRollupProver implements ServerCircuitProver { } /** - * Verifies a proof, will generate the verification key if one is not cached internally + * Verifies a proof via bb.js API (no temp files needed). * @param circuitType - The type of circuit whose proof is to be verified * @param proof - The proof to be verified */ public async verifyProof(circuitType: ServerProtocolArtifact, proof: Proof) { const verificationKey = this.getVerificationKeyDataForCircuit(circuitType); - return await this.verifyInternal(proof, verificationKey, (proofPath, vkPath) => - verifyProof(this.config.bbBinaryPath, proofPath, vkPath, getUltraHonkFlavorForCircuit(circuitType), logger), - ); + const flavor = getUltraHonkFlavorForCircuit(circuitType); + + // Split proof buffer into public input fields and proof fields (32-byte each) + const publicInputFields = splitBufferToFieldArrays(proof.buffer.subarray(0, proof.numPublicInputs * 32)); + const proofFields = splitBufferToFieldArrays(proof.buffer.subarray(proof.numPublicInputs * 32)); + + let verified: boolean; + let durationMs: number; + try { + ({ verified, durationMs } = await this.bbJsFactory.withVerifierInstance(instance => + instance.verifyProof(proofFields, verificationKey.keyAsBytes, publicInputFields, flavor), + )); + } catch (error) { + throw new ProvingError(`Failed to verify proof for ${circuitType}: ${error}`); + } + + if (!verified) { + throw new ProvingError( + 'Failed to verify proof from key!', + { status: BB_RESULT.FAILURE, reason: 'Verification failed' }, + false, + ); + } + + logger.info(`Successfully verified proof from key in ${durationMs} ms`); } + /** Verify an AVM proof via bb.js API. */ public async verifyAvmProof(proof: Proof, publicInputs: AvmCircuitPublicInputs) { - return await this.verifyInternal(proof, /*verificationKey=*/ undefined, (proofPath, /*unused*/ _vkPath) => - verifyAvmProof(this.config.bbBinaryPath, this.config.bbWorkingDirectory, proofPath, publicInputs, logger), + // For AVM proofs, numPublicInputs is 0, so the full buffer is the proof. + const proofBuffer = proof.buffer.subarray(proof.numPublicInputs * 32); + // Split the raw proof buffer into 32-byte field element arrays + const proofFields: Uint8Array[] = []; + for (let i = 0; i < proofBuffer.length; i += Fr.SIZE_IN_BYTES) { + proofFields.push(new Uint8Array(proofBuffer.subarray(i, i + Fr.SIZE_IN_BYTES))); + } + const piBuffer = publicInputs.serializeWithMessagePack(); + + const { verified, durationMs } = await this.bbJsFactory.withVerifierInstance(instance => + instance.verifyAvmProof(proofFields, piBuffer), ); - } - private async verifyInternal( - proof: Proof, - verificationKey: { keyAsBytes: Buffer } | undefined, - verificationFunction: (proofPath: string, vkPath: string) => Promise, - ) { - const operation = async (bbWorkingDirectory: string) => { - const publicInputsFileName = path.join(bbWorkingDirectory, PUBLIC_INPUTS_FILENAME); - const proofFileName = path.join(bbWorkingDirectory, PROOF_FILENAME); - const verificationKeyPath = path.join(bbWorkingDirectory, VK_FILENAME); - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/13189): Put this proof parsing logic in the proof class. - await fs.writeFile(publicInputsFileName, proof.buffer.subarray(0, proof.numPublicInputs * 32)); - await fs.writeFile(proofFileName, proof.buffer.subarray(proof.numPublicInputs * 32)); - if (verificationKey !== undefined) { - await fs.writeFile(verificationKeyPath, verificationKey.keyAsBytes); - } - - const result = await verificationFunction(proofFileName, verificationKeyPath); - - if (result.status === BB_RESULT.FAILURE) { - const errorMessage = `Failed to verify proof from key!`; - throw new ProvingError(errorMessage, result, result.retry); - } - - logger.info(`Successfully verified proof from key in ${result.durationMs} ms`); - }; - await this.runInDirectory(operation); + if (!verified) { + throw new ProvingError('Failed to verify AVM proof!'); + } + + logger.info(`Successfully verified AVM proof in ${durationMs} ms`); } /** @@ -680,29 +699,6 @@ export class BBNativeRollupProver implements ServerCircuitProver { return vk; } - private async readAvmProofAsFields( - proofFilename: string, - ): Promise> { - const rawProofBuffer = await fs.readFile(proofFilename); - const reader = BufferReader.asReader(rawProofBuffer); - const proofFields = reader.readArray(rawProofBuffer.length / Fr.SIZE_IN_BYTES, Fr); - - // We extend to a fixed-size padded proof as during development any new AVM circuit column changes the - // proof length and we do not have a mechanism to feedback a cpp constant to noir/TS. - // TODO(#13390): Revive a non-padded AVM proof - if (proofFields.length > AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED) { - throw new Error( - `Proof has ${proofFields.length} fields, expected no more than ${AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED}.`, - ); - } - const proofFieldsPadded = proofFields.concat( - Array(AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED - proofFields.length).fill(new Fr(0)), - ); - - const proof = new Proof(rawProofBuffer, /*numPublicInputs=*/ 0); - return new RecursiveProof(proofFieldsPadded, proof, true, AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED); - } - private runInDirectory(fn: (dir: string) => Promise) { return runInDirectory( this.config.bbWorkingDirectory, @@ -716,3 +712,12 @@ export class BBNativeRollupProver implements ServerCircuitProver { ); } } + +/** Split a buffer into 32-byte Uint8Array field elements. */ +function splitBufferToFieldArrays(buffer: Buffer): Uint8Array[] { + const fields: Uint8Array[] = []; + for (let i = 0; i < buffer.length; i += 32) { + fields.push(new Uint8Array(buffer.subarray(i, i + 32))); + } + return fields; +} diff --git a/yarn-project/bb-prover/src/verifier/bb_verifier.ts b/yarn-project/bb-prover/src/verifier/bb_verifier.ts index 3632f30f09b5..f00055bc1429 100644 --- a/yarn-project/bb-prover/src/verifier/bb_verifier.ts +++ b/yarn-project/bb-prover/src/verifier/bb_verifier.ts @@ -1,4 +1,4 @@ -import { runInDirectory } from '@aztec/foundation/fs'; +import { HIDING_KERNEL_IO_PUBLIC_INPUTS_SIZE } from '@aztec/constants'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { ProtocolCircuitVks } from '@aztec/noir-protocol-circuits-types/server/vks'; @@ -15,25 +15,20 @@ import { Tx } from '@aztec/stdlib/tx'; import type { VerificationKeyData } from '@aztec/stdlib/vks'; import { promises as fs } from 'fs'; -import * as path from 'path'; -import { - BB_RESULT, - PROOF_FILENAME, - PUBLIC_INPUTS_FILENAME, - VK_FILENAME, - verifyChonkProof, - verifyProof, -} from '../bb/execute.js'; +import { BBJsProverFactory } from '../bb/bb_js_backend.js'; import type { BBConfig } from '../config.js'; import { getUltraHonkFlavorForCircuit } from '../honk.js'; -import { writeChonkProofToPath } from '../prover/proof_utils.js'; export class BBCircuitVerifier implements ClientProtocolCircuitVerifier { + private bbJsFactory: BBJsProverFactory; + private constructor( private config: BBConfig, private logger: Logger, - ) {} + ) { + this.bbJsFactory = new BBJsProverFactory(config.bbBinaryPath, logger, undefined, config.bbDebugOutputDir); + } public stop(): Promise { return Promise.resolve(); @@ -55,87 +50,75 @@ export class BBCircuitVerifier implements ClientProtocolCircuitVerifier { return vk; } + /** Verify an UltraHonk proof via bb.js API (no temp files). */ public async verifyProofForCircuit(circuit: ServerProtocolArtifact, proof: Proof) { - const operation = async (bbWorkingDirectory: string) => { - const publicInputsFileName = path.join(bbWorkingDirectory, PUBLIC_INPUTS_FILENAME); - const proofFileName = path.join(bbWorkingDirectory, PROOF_FILENAME); - const verificationKeyPath = path.join(bbWorkingDirectory, VK_FILENAME); - const verificationKey = this.getVerificationKeyData(circuit); + const verificationKey = this.getVerificationKeyData(circuit); + const flavor = getUltraHonkFlavorForCircuit(circuit); - this.logger.debug(`${circuit} Verifying with key: ${verificationKey.keyAsFields.hash.toString()}`); + this.logger.debug(`${circuit} Verifying with key: ${verificationKey.keyAsFields.hash.toString()}`); - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/13189): Put this proof parsing logic in the proof class. - await fs.writeFile(publicInputsFileName, proof.buffer.slice(0, proof.numPublicInputs * 32)); - await fs.writeFile(proofFileName, proof.buffer.slice(proof.numPublicInputs * 32)); - await fs.writeFile(verificationKeyPath, verificationKey.keyAsBytes); + // Split proof buffer into public input fields and proof fields (32-byte each) + const publicInputFields = splitBufferToFieldArrays(proof.buffer.subarray(0, proof.numPublicInputs * 32)); + const proofFields = splitBufferToFieldArrays(proof.buffer.subarray(proof.numPublicInputs * 32)); - const result = await verifyProof( - this.config.bbBinaryPath, - proofFileName, - verificationKeyPath!, - getUltraHonkFlavorForCircuit(circuit), - this.logger, - ); + const { verified, durationMs } = await this.bbJsFactory.withVerifierInstance(instance => + instance.verifyProof(proofFields, verificationKey.keyAsBytes, publicInputFields, flavor), + ); - if (result.status === BB_RESULT.FAILURE) { - const errorMessage = `Failed to verify ${circuit} proof!`; - throw new Error(errorMessage); - } + if (!verified) { + throw new Error(`Failed to verify ${circuit} proof!`); + } - this.logger.debug(`${circuit} verification successful`, { - circuitName: mapProtocolArtifactNameToCircuitName(circuit), - duration: result.durationMs, - eventName: 'circuit-verification', - proofType: 'ultra-honk', - } satisfies CircuitVerificationStats); - }; - await runInDirectory(this.config.bbWorkingDirectory, operation, this.config.bbSkipCleanup, this.logger); + this.logger.debug(`${circuit} verification successful`, { + circuitName: mapProtocolArtifactNameToCircuitName(circuit), + duration: durationMs, + eventName: 'circuit-verification', + proofType: 'ultra-honk', + } satisfies CircuitVerificationStats); } + /** Verify a Chonk (IVC) proof from a transaction via bb.js API. */ public async verifyProof(tx: Tx): Promise { const proofType = 'Chonk'; try { const totalTimer = new Timer(); - let verificationDuration = 0; const circuit: ClientProtocolArtifact = tx.data.forPublic ? 'HidingKernelToPublic' : 'HidingKernelToRollup'; + const verificationKey = this.getVerificationKeyData(circuit); + const numCustomPublicInputs = verificationKey.numPublicInputs - HIDING_KERNEL_IO_PUBLIC_INPUTS_SIZE; + + // Reconstruct the full proof with public inputs prepended, then convert Fr[] to Uint8Array[] + const proofWithPubInputs = tx.chonkProof.attachPublicInputs(tx.data.publicInputs().toFields()); + const fieldsAsBuffers = proofWithPubInputs.fieldsWithPublicInputs.map(f => new Uint8Array(f.toBuffer())); - // Block below is almost copy-pasted from verifyProofForCircuit - const operation = async (bbWorkingDirectory: string) => { - const proofPath = path.join(bbWorkingDirectory, PROOF_FILENAME); - await writeChonkProofToPath(tx.chonkProof.attachPublicInputs(tx.data.publicInputs().toFields()), proofPath); - - const verificationKeyPath = path.join(bbWorkingDirectory, VK_FILENAME); - const verificationKey = this.getVerificationKeyData(circuit); - await fs.writeFile(verificationKeyPath, verificationKey.keyAsBytes); - - const timer = new Timer(); - const result = await verifyChonkProof( - this.config.bbBinaryPath, - proofPath, - verificationKeyPath, - this.logger, - this.config.bbIVCConcurrency, - ); - verificationDuration = timer.ms(); - - if (result.status === BB_RESULT.FAILURE) { - const errorMessage = `Failed to verify ${proofType} proof for ${circuit}!`; - throw new Error(errorMessage); - } - - this.logger.debug(`${proofType} verification successful`, { - circuitName: mapProtocolArtifactNameToCircuitName(circuit), - duration: result.durationMs, - eventName: 'circuit-verification', - proofType: 'chonk', - } satisfies CircuitVerificationStats); - }; - await runInDirectory(this.config.bbWorkingDirectory, operation, this.config.bbSkipCleanup, this.logger); - return { valid: true, durationMs: verificationDuration, totalDurationMs: totalTimer.ms() }; + const { verified, durationMs } = await this.bbJsFactory.withVerifierInstance(instance => + instance.verifyChonkProof(fieldsAsBuffers, verificationKey.keyAsBytes, numCustomPublicInputs), + ); + + if (!verified) { + throw new Error(`Failed to verify ${proofType} proof for ${circuit}!`); + } + + this.logger.debug(`${proofType} verification successful`, { + circuitName: mapProtocolArtifactNameToCircuitName(circuit), + duration: durationMs, + eventName: 'circuit-verification', + proofType: 'chonk', + } satisfies CircuitVerificationStats); + + return { valid: true, durationMs, totalDurationMs: totalTimer.ms() }; } catch (err) { this.logger.warn(`Failed to verify ${proofType} proof for tx ${tx.getTxHash().toString()}: ${String(err)}`); return { valid: false, durationMs: 0, totalDurationMs: 0 }; } } } + +/** Split a buffer into 32-byte Uint8Array field elements. */ +function splitBufferToFieldArrays(buffer: Buffer): Uint8Array[] { + const fields: Uint8Array[] = []; + for (let i = 0; i < buffer.length; i += 32) { + fields.push(new Uint8Array(buffer.subarray(i, i + 32))); + } + return fields; +} diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index a12776718ca3..e90634d76519 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -237,6 +237,10 @@ export const ULTRA_KECCAK_PROOF_LENGTH = 331; export const RECURSIVE_ROLLUP_HONK_PROOF_LENGTH = 519; export const NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH = 519; export const CHONK_PROOF_LENGTH = 1632; +export const CHONK_MEGA_ZK_PROOF_LENGTH_WITHOUT_PUB_INPUTS = 407; +export const CHONK_MERGE_PROOF_LENGTH = 42; +export const CHONK_ECCVM_PROOF_LENGTH = 608; +export const CHONK_TRANSLATOR_PROOF_LENGTH = 483; export const ULTRA_VK_LENGTH_IN_FIELDS = 115; export const MEGA_VK_LENGTH_IN_FIELDS = 127; export const CHONK_VK_LENGTH_IN_FIELDS = 127; @@ -505,12 +509,13 @@ export const GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673 export const DEFAULT_MAX_DEBUG_LOG_MEMORY_READS = 125000; export enum DomainSeparator { NOTE_HASH = 116501019, - NOTE_HASH_NONCE = 1721808740, - UNIQUE_NOTE_HASH = 226850429, SILOED_NOTE_HASH = 3361878420, + UNIQUE_NOTE_HASH = 226850429, + NOTE_HASH_NONCE = 1721808740, + SINGLE_USE_CLAIM_NULLIFIER = 1465998995, NOTE_NULLIFIER = 50789342, - MESSAGE_NULLIFIER = 3754509616, SILOED_NULLIFIER = 57496191, + MESSAGE_NULLIFIER = 3754509616, PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901, diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 690d9ceacf01..e716d56f6440 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -23,6 +23,7 @@ export type EnvVar = | 'BB_WORKING_DIRECTORY' | 'BB_NUM_IVC_VERIFIERS' | 'BB_IVC_CONCURRENCY' + | 'BB_DEBUG_OUTPUT_DIR' | 'BOOTSTRAP_NODES' | 'BLOB_ARCHIVE_API_URL' | 'BLOB_FILE_STORE_URLS' diff --git a/yarn-project/ivc-integration/src/bb_js_debug.test.ts b/yarn-project/ivc-integration/src/bb_js_debug.test.ts new file mode 100644 index 000000000000..ec8916a1b88d --- /dev/null +++ b/yarn-project/ivc-integration/src/bb_js_debug.test.ts @@ -0,0 +1,210 @@ +/** + * Tests that the bb.js debug wrapper writes files that are compatible with the bb CLI. + * Generates a proof via bb.js with the debug wrapper enabled, then: + * 1. Runs `bb prove` via CLI using the same inputs → compares proof output + * 2. Runs `bb verify` via CLI using the proof files → checks verification passes + */ +import { BBJsInstance, type BBJsProofResult, executeBB } from '@aztec/bb-prover'; +import { DebugBBJsInstance } from '@aztec/bb-prover/debug'; +import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import { createLogger } from '@aztec/foundation/log'; +import { Noir } from '@aztec/noir-noir_js'; +import { ServerCircuitArtifacts } from '@aztec/noir-protocol-circuits-types/server'; +import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; +import { ParityBasePrivateInputs } from '@aztec/stdlib/parity'; + +import { jest } from '@jest/globals'; +import * as fs from 'fs/promises'; +import { ungzip } from 'pako'; +import * as path from 'path'; + +const logger = createLogger('ivc-integration:test:bb-js-debug'); + +jest.setTimeout(120_000); + +const BB_PATH = path.resolve( + path.join(path.dirname(new URL(import.meta.url).pathname), '../../../barretenberg/cpp/build/bin/bb'), +); + +describe('BB.js Debug Wrapper', () => { + let debugDir: string; + let bytecode: Uint8Array; + let witness: Uint8Array; + let vkBytes: Uint8Array; + let bbJsResult: BBJsProofResult; + + beforeAll(async () => { + // Verify bb binary exists + await fs.access(BB_PATH); + + // Create a temporary debug output directory + debugDir = await fs.mkdtemp(path.join(process.env.BB_WORKING_DIRECTORY || '/tmp', 'bb-debug-test-')); + + // Generate base parity inputs (same approach as base_parity_inputs.test.ts) + const l1ToL2Messages = new Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(null).map(() => Fr.random()); + const vkTreeRoot = getVKTreeRoot(); + const baseParityInputs = ParityBasePrivateInputs.fromSlice(l1ToL2Messages, 0, vkTreeRoot); + + const noirInputs = { + msgs: baseParityInputs.msgs.map(m => m.toString()), + // eslint-disable-next-line camelcase + vk_tree_root: baseParityInputs.vkTreeRoot.toString(), + }; + + const artifact = ServerCircuitArtifacts.ParityBaseArtifact; + + // Execute circuit with Noir JS to generate witness + logger.info('Generating witness via Noir JS...'); + const program = new Noir(artifact as any); + const { witness: compressedWitness } = await program.execute({ inputs: noirInputs }); + + // Decompress for bb.js (it expects raw bytes) + bytecode = ungzip(Buffer.from(artifact.bytecode, 'base64')); + witness = ungzip(compressedWitness); + vkBytes = Buffer.from(artifact.verificationKey!.bytes, 'hex'); + + // Generate proof via bb.js with debug wrapper + logger.info('Generating proof via bb.js with debug wrapper...'); + const raw = await BBJsInstance.create(BB_PATH, (msg: string) => logger.verbose(`bb.js - ${msg}`)); + const debug = new DebugBBJsInstance(raw, debugDir, BB_PATH, logger); + + try { + bbJsResult = await debug.generateProof('ParityBase', bytecode, vkBytes, witness, 'ultra_honk'); + logger.info( + `bb.js proof generated: ${bbJsResult.proofFields.length} proof fields, ${bbJsResult.publicInputFields.length} public input fields`, + ); + } finally { + await debug.destroy(); + } + }); + + afterAll(async () => { + // Clean up debug dir + if (debugDir) { + await fs.rm(debugDir, { recursive: true, force: true }); + } + }); + + it('writes correct debug files and command.sh', async () => { + const opDir = path.join(debugDir, 'ParityBase-001'); + + // Check all expected files exist + const files = await fs.readdir(opDir); + expect(files).toContain('ParityBase-bytecode.gz'); + expect(files).toContain('ParityBase-vk'); + expect(files).toContain('partial-witness.gz'); + expect(files).toContain('proof'); + expect(files).toContain('public_inputs'); + expect(files).toContain('command.sh'); + + // Check command.sh contains the expected prove command + const command = await fs.readFile(path.join(opDir, 'command.sh'), 'utf-8'); + expect(command).toContain('prove'); + expect(command).toContain('--scheme ultra_honk'); + expect(command).toContain('--oracle_hash poseidon2'); + expect(command).toContain('--disable_zk'); + expect(command).toContain('-b'); + expect(command).toContain('-k'); + expect(command).toContain('-w'); + expect(command).toContain('-o'); + }); + + it('CLI bb prove reproduces the same proof as bb.js', async () => { + const opDir = path.join(debugDir, 'ParityBase-001'); + + // Create a separate output directory for the CLI proof + const cliOutputDir = path.join(debugDir, 'cli-prove-output'); + await fs.mkdir(cliOutputDir, { recursive: true }); + + // Run the bb prove command using the same input files + const bytecodePath = path.join(opDir, 'ParityBase-bytecode.gz'); + const vkPath = path.join(opDir, 'ParityBase-vk'); + const witnessPath = path.join(opDir, 'partial-witness.gz'); + + const logFn = (msg: string) => logger.verbose(`bb-cli - ${msg}`); + const result = await executeBB( + BB_PATH, + 'prove', + [ + '--scheme', + 'ultra_honk', + '--oracle_hash', + 'poseidon2', + '--disable_zk', + '-b', + bytecodePath, + '-k', + vkPath, + '-w', + witnessPath, + '-o', + cliOutputDir, + ], + logFn, + ); + + expect(result.status).toBe(0); // BB_RESULT.SUCCESS + + // Read CLI proof output + const cliProof = await fs.readFile(path.join(cliOutputDir, 'proof')); + const cliPublicInputs = await fs.readFile(path.join(cliOutputDir, 'public_inputs')); + + // Read bb.js proof output (written by debug wrapper) + const bbJsProof = await fs.readFile(path.join(opDir, 'proof')); + const bbJsPublicInputs = await fs.readFile(path.join(opDir, 'public_inputs')); + + // With --disable_zk, proofs are deterministic — they should match exactly + expect(Buffer.compare(cliProof, bbJsProof)).toBe(0); + expect(Buffer.compare(cliPublicInputs, bbJsPublicInputs)).toBe(0); + }); + + it('CLI bb verify succeeds with debug output files', async () => { + const opDir = path.join(debugDir, 'ParityBase-001'); + + // We need the VK produced by the prover (not the input VK). + // The prove command writes a VK only with --write_vk. Instead, we write_vk separately + // then verify using those files. Or we can just use the proof + public_inputs + VK + // from the CLI prove output which also writes them. + // Actually, let's write the VK first and then verify. + const vkDir = path.join(debugDir, 'cli-vk-output'); + await fs.mkdir(vkDir, { recursive: true }); + + const bytecodePath = path.join(opDir, 'ParityBase-bytecode.gz'); + + // Generate VK via CLI + const logFn = (msg: string) => logger.verbose(`bb-cli - ${msg}`); + const vkResult = await executeBB( + BB_PATH, + 'write_vk', + ['--scheme', 'ultra_honk', '--oracle_hash', 'poseidon2', '--disable_zk', '-b', bytecodePath, '-o', vkDir], + logFn, + ); + expect(vkResult.status).toBe(0); + + // Verify using the proof and public_inputs written by the debug wrapper + const proofPath = path.join(opDir, 'proof'); + const publicInputsPath = path.join(opDir, 'public_inputs'); + const vkPath = path.join(vkDir, 'vk'); + + const verifyResult = await executeBB( + BB_PATH, + 'verify', + [ + '--scheme', + 'ultra_honk', + '--oracle_hash', + 'poseidon2', + '--disable_zk', + '-p', + proofPath, + '-k', + vkPath, + '-i', + publicInputsPath, + ], + logFn, + ); + expect(verifyResult.status).toBe(0); + }); +}); diff --git a/yarn-project/ivc-integration/src/prove_native.ts b/yarn-project/ivc-integration/src/prove_native.ts index 3aeeb260fdbe..6adc8c3282b3 100644 --- a/yarn-project/ivc-integration/src/prove_native.ts +++ b/yarn-project/ivc-integration/src/prove_native.ts @@ -1,14 +1,10 @@ import { + BBJsProverFactory, BB_RESULT, - PROOF_FILENAME, - PUBLIC_INPUTS_FILENAME, type UltraHonkFlavor, - VK_FILENAME, + constructRecursiveProofFromBuffers, generateAvmProof, - generateProof, - readProofsFromOutputDirectory, verifyAvmProof, - verifyProof, } from '@aztec/bb-prover'; import { AVM_V2_PROOF_LENGTH_IN_FIELDS_PADDED, @@ -27,6 +23,7 @@ import { Proof, RecursiveProof } from '@aztec/stdlib/proofs'; import { VerificationKeyAsFields, VerificationKeyData } from '@aztec/stdlib/vks'; import * as fs from 'fs/promises'; +import { ungzip } from 'pako'; import * as path from 'path'; export async function proofBytesToRecursiveProof( @@ -49,60 +46,50 @@ export async function proofBytesToRecursiveProof( return new RecursiveProof(fieldsWithoutPublicInputs, proof, true, CHONK_PROOF_LENGTH); } -async function verifyProofWithKey( - pathToBB: string, - workingDirectory: string, - verificationKey: VerificationKeyData, - proof: Proof, - flavor: UltraHonkFlavor, - logger: Logger, -) { - const publicInputsFileName = path.join(workingDirectory, PUBLIC_INPUTS_FILENAME); - const proofFileName = path.join(workingDirectory, PROOF_FILENAME); - const verificationKeyPath = path.join(workingDirectory, VK_FILENAME); - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/13189): Put this proof parsing logic in the proof class. - await fs.writeFile(publicInputsFileName, proof.buffer.slice(0, proof.numPublicInputs * 32)); - await fs.writeFile(proofFileName, proof.buffer.slice(proof.numPublicInputs * 32)); - await fs.writeFile(verificationKeyPath, verificationKey.keyAsBytes); - - const result = await verifyProof(pathToBB, proofFileName, verificationKeyPath, flavor, logger); - if (result.status === BB_RESULT.FAILURE) { - throw new Error(`Failed to verify proof from key!`); - } - logger.info(`Successfully verified proof from key in ${result.durationMs} ms`); -} - async function proveRollupCircuit( name: string, pathToBB: string, - workingDirectory: string, + _workingDirectory: string, circuit: NoirCompiledCircuit, witness: Uint8Array, logger: Logger, flavor: T, proofLength: ProofLength, ) { - await fs.writeFile(path.join(workingDirectory, 'witness.gz'), witness); + const factory = new BBJsProverFactory(pathToBB, logger); + + // Decompress witness and bytecode for bb.js + const decompressedWitness = ungzip(witness); + const bytecode = ungzip(Buffer.from(circuit.bytecode, 'base64')); const vkBuffer = Buffer.from(circuit.verificationKey.bytes, 'hex'); - const proofResult = await generateProof( - pathToBB, - workingDirectory, - name, - Buffer.from(circuit.bytecode, 'base64'), - vkBuffer, - path.join(workingDirectory, 'witness.gz'), - flavor, - logger, - ); - if (proofResult.status != BB_RESULT.SUCCESS) { - throw new Error(`Failed to generate proof for ${name} with flavor ${flavor}`); - } + // Generate proof via bb.js + const proofResult = await factory.withFreshInstance(instance => + instance.generateProof(name, bytecode, vkBuffer, decompressedWitness, flavor), + ); const vk = await VerificationKeyData.fromFrBuffer(vkBuffer); - const proof = await readProofsFromOutputDirectory(proofResult.proofPath!, vk, proofLength, logger); - await verifyProofWithKey(pathToBB, workingDirectory, vk, proof.binaryProof, flavor, logger); + // Construct proof from in-memory buffers + const proof = constructRecursiveProofFromBuffers( + proofResult.proofFields, + proofResult.publicInputFields, + vk, + proofLength, + ); + + // Verify the proof via bb.js + const publicInputFields = proofResult.publicInputFields; + const proofFields = proofResult.proofFields; + + const { verified } = await factory.withVerifierInstance(instance => + instance.verifyProof(proofFields, vk.keyAsBytes, publicInputFields, flavor), + ); + + if (!verified) { + throw new Error(`Failed to verify proof from key!`); + } + logger.info(`Successfully verified proof from key`); return makeProofAndVerificationKey(proof, vk); } @@ -147,6 +134,7 @@ export function proveKeccakHonk( ); } +/** AVM proving still uses direct binary execution (no bb.js equivalent). */ export async function proveAvm( avmCircuitInputs: AvmCircuitInputs, workingDirectory: string, diff --git a/yarn-project/prover-client/src/config.ts b/yarn-project/prover-client/src/config.ts index 658558f9eb44..2aefba1a1a90 100644 --- a/yarn-project/prover-client/src/config.ts +++ b/yarn-project/prover-client/src/config.ts @@ -52,6 +52,11 @@ export const bbConfigMappings: ConfigMappingsType = { description: 'Number of threads to use for IVC verification', ...numberConfigHelper(1), }, + bbDebugOutputDir: { + env: 'BB_DEBUG_OUTPUT_DIR', + description: + 'When set, bb.js operations write input/output files and log equivalent CLI commands to this directory', + }, }; export const proverClientConfigMappings: ConfigMappingsType = {