From 892a5510f565dd3f566fee5d8178c4682d99fd2d Mon Sep 17 00:00:00 2001 From: ashpect Date: Thu, 26 Mar 2026 02:34:03 +0530 Subject: [PATCH 01/12] feat: add serialization and deserialization --- provekit/common/src/file/io/bin.rs | 99 ++++++++++++++++++++++++++++++ provekit/common/src/file/io/mod.rs | 21 ++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/provekit/common/src/file/io/bin.rs b/provekit/common/src/file/io/bin.rs index cb5ff2f48..12ba1a530 100644 --- a/provekit/common/src/file/io/bin.rs +++ b/provekit/common/src/file/io/bin.rs @@ -156,6 +156,105 @@ pub fn read_bin Deserialize<'a>>( postcard::from_bytes(&uncompressed).context("while decoding from postcard") } +/// Serialize a value to bytes in the same format as `write_bin` (header + +/// compressed postcard). The output is byte-for-byte identical to what +/// `write_bin` would write to disk. +pub fn serialize_to_bytes( + value: &T, + format: [u8; 8], + (major, minor): (u16, u16), + compression: Compression, + hash_config: Option, +) -> Result> { + let postcard_data = postcard::to_allocvec(value).context("while encoding to postcard")?; + + let compressed_data = match compression { + Compression::Zstd => { + zstd::bulk::compress(&postcard_data, 3).context("while compressing with zstd")? + } + Compression::Xz => { + let mut buf = Vec::new(); + let mut encoder = xz2::write::XzEncoder::new(&mut buf, 6); + encoder + .write_all(&postcard_data) + .context("while compressing with xz")?; + encoder.finish().context("while finishing xz stream")?; + buf + } + }; + + let mut out = Vec::with_capacity(HEADER_SIZE + compressed_data.len()); + // Header: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) + out.put(MAGIC_BYTES); + out.put(&format[..]); + out.put_u16_le(major); + out.put_u16_le(minor); + out.put_u8(hash_config.map(|c| c.to_byte()).unwrap_or(0xff)); + out.extend_from_slice(&compressed_data); + + Ok(out) +} + +/// Deserialize a value from bytes produced by `serialize_to_bytes` or read +/// from a file written by `write_bin`. +pub fn deserialize_from_bytes Deserialize<'a>>( + data: &[u8], + format: [u8; 8], + (major, minor): (u16, u16), +) -> Result { + ensure!( + data.len() > HEADER_SIZE, + "Data too small ({} bytes, need at least {})", + data.len(), + HEADER_SIZE + 1 + ); + + let mut header = Bytes::copy_from_slice(&data[..HEADER_SIZE]); + ensure!( + header.get_bytes::<8>() == MAGIC_BYTES, + "Invalid magic bytes" + ); + ensure!(header.get_bytes::<8>() == format, "Invalid format"); + ensure!( + header.get_u16_le() == major, + "Incompatible format major version" + ); + ensure!( + header.get_u16_le() >= minor, + "Incompatible format minor version" + ); + let _hash_config_byte = header.get_u8(); + + let compressed = &data[HEADER_SIZE..]; + let uncompressed = decompress_bytes(compressed)?; + + postcard::from_bytes(&uncompressed).context("while decoding from postcard") +} + +/// Detect compression format from bytes and decompress. +fn decompress_bytes(data: &[u8]) -> Result> { + ensure!(data.len() >= 6, "Data too small to detect compression"); + + let is_zstd = data[..4] == ZSTD_MAGIC; + let is_xz = data[..6] == XZ_MAGIC; + + if is_zstd { + zstd::bulk::decompress(data, usize::MAX).context("while decompressing zstd data") + } else if is_xz { + let mut out = Vec::new(); + let mut decoder = xz2::read::XzDecoder::new(data); + decoder + .read_to_end(&mut out) + .context("while decompressing XZ data")?; + Ok(out) + } else { + anyhow::bail!( + "Unknown compression format (first bytes: {:02X?})", + &data[..data.len().min(6)] + ); + } +} + /// Peek at the first bytes to detect compression format, then /// stream-decompress. fn decompress_stream(reader: &mut BufReader) -> Result> { diff --git a/provekit/common/src/file/io/mod.rs b/provekit/common/src/file/io/mod.rs index 4d04680a3..049c984a7 100644 --- a/provekit/common/src/file/io/mod.rs +++ b/provekit/common/src/file/io/mod.rs @@ -5,7 +5,10 @@ mod json; use { self::{ - bin::{read_bin, read_hash_config as read_hash_config_bin, write_bin, Compression}, + bin::{ + deserialize_from_bytes, read_bin, read_hash_config as read_hash_config_bin, + serialize_to_bytes, write_bin, Compression, + }, buf_ext::BufExt, counting_writer::CountingWriter, json::{read_json, write_json}, @@ -134,6 +137,22 @@ pub fn read(path: &Path) -> Result { } } +/// Serialize a value to bytes in the same binary format as `write`. +/// +/// The output is byte-for-byte identical to what `write` produces on disk +/// (header + compressed postcard). Use `deserialize` to recover the value. +#[allow(private_bounds)] +pub fn serialize(value: &T) -> Result> { + let hash_config = value.maybe_hash_config(); + serialize_to_bytes(value, T::FORMAT, T::VERSION, T::COMPRESSION, hash_config) +} + +/// Deserialize a value from bytes produced by `serialize` or read from a file +/// written by `write`. +pub fn deserialize(data: &[u8]) -> Result { + deserialize_from_bytes(data, T::FORMAT, T::VERSION) +} + /// Read just the hash configuration from a file. #[instrument()] pub fn read_hash_config(path: &Path) -> Result { From 90232241fe2709c467445158c507b36c8c6e31a4 Mon Sep 17 00:00:00 2001 From: ashpect Date: Thu, 26 Mar 2026 02:34:24 +0530 Subject: [PATCH 02/12] feat: get noir abi from nps --- provekit/common/src/noir_proof_scheme.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/provekit/common/src/noir_proof_scheme.rs b/provekit/common/src/noir_proof_scheme.rs index a43377eae..7731d3c47 100644 --- a/provekit/common/src/noir_proof_scheme.rs +++ b/provekit/common/src/noir_proof_scheme.rs @@ -55,4 +55,12 @@ impl NoirProofScheme { let r1cs = self.r1cs(); (r1cs.num_constraints(), r1cs.num_witnesses()) } + + #[must_use] + pub fn abi(&self) -> &noirc_abi::Abi { + match self { + NoirProofScheme::Noir(d) => d.witness_generator.abi(), + NoirProofScheme::Mavros(d) => &d.abi, + } + } } From 116a8069f6d044c8fe0387d1c4191199961c4955 Mon Sep 17 00:00:00 2001 From: ashpect Date: Thu, 26 Mar 2026 02:36:30 +0530 Subject: [PATCH 03/12] feat: ffi v2.0 for provekit --- Cargo.lock | 4 + tooling/provekit-ffi/Cargo.toml | 7 +- tooling/provekit-ffi/src/ffi.rs | 635 +++++++++++++++++++++++------- tooling/provekit-ffi/src/lib.rs | 18 +- tooling/provekit-ffi/src/types.rs | 31 +- 5 files changed, 546 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc5371a21..6c59517cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4590,9 +4590,13 @@ version = "0.1.0" dependencies = [ "anyhow", "libc", + "noirc_abi", "parking_lot", + "postcard", "provekit-common", "provekit-prover", + "provekit-r1cs-compiler", + "provekit-verifier", "serde_json", ] diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index e0d1fe318..2f9a4fa83 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -15,9 +15,13 @@ crate-type = ["staticlib"] # Workspace crates provekit-common.workspace = true provekit-prover = { workspace = true, features = ["witness-generation", "parallel"] } +provekit-r1cs-compiler = { workspace = true } +provekit-verifier = { workspace = true } # 3rd party anyhow.workspace = true +noirc_abi.workspace = true +postcard.workspace = true serde_json.workspace = true parking_lot = "0.12" @@ -26,6 +30,3 @@ libc = "0.2" [lints] workspace = true - -[features] -default = [] diff --git a/tooling/provekit-ffi/src/ffi.rs b/tooling/provekit-ffi/src/ffi.rs index 686662808..8d7772718 100644 --- a/tooling/provekit-ffi/src/ffi.rs +++ b/tooling/provekit-ffi/src/ffi.rs @@ -1,13 +1,18 @@ -//! Main FFI functions for ProveKit. +//! Handle-based FFI functions for ProveKit. +//! +//! All functions use opaque `PKProver` / `PKVerifier` handles instead of file +//! paths. Proofs are always returned as bytes in a `PKBuf`. use { crate::{ - types::{PKBuf, PKError}, + types::{PKBuf, PKError, PKProver, PKVerifier}, utils::c_str_to_str, }, - anyhow::Result, - provekit_common::{file::read, Prover}, + noirc_abi::input_parser::Format, + provekit_common::{file, HashConfig, NoirProof, Prover, Verifier}, provekit_prover::Prove, + provekit_r1cs_compiler::NoirCompiler, + provekit_verifier::Verify, std::{ os::raw::{c_char, c_int}, panic, @@ -25,223 +30,579 @@ where panic::catch_unwind(f).unwrap_or(default) } -/// Prove a Noir program and write the proof to a file. +/// Initialize the ProveKit library. /// -/// # Arguments +/// Must be called once before using any other ProveKit functions. +#[no_mangle] +pub extern "C" fn pk_init() -> c_int { + provekit_common::register_ntt(); + PKError::Success.into() +} + +/// Configure the mmap-based memory allocator. /// -/// * `prover_path` - Path to the prepared proof scheme (.pkp file) -/// * `input_path` - Path to the witness/input values (.toml file) -/// * `out_path` - Path where to write the proof file (.np or .json) +/// MUST be called before `pk_init()` and before any allocations occur. /// -/// # Returns +/// # Safety /// -/// Returns `PKError::Success` on success, or an appropriate error code on -/// failure. +/// `swap_file_path` must be either NULL or a valid null-terminated C string. +#[no_mangle] +pub unsafe extern "C" fn pk_configure_memory( + ram_limit_bytes: usize, + use_file_backed: bool, + swap_file_path: *const c_char, +) -> c_int { + if ram_limit_bytes == 0 { + return PKError::InvalidInput.into(); + } + + if crate::mmap_allocator::configure_allocator(ram_limit_bytes, use_file_backed, swap_file_path) + { + PKError::Success.into() + } else { + PKError::InvalidInput.into() + } +} + +/// Get current memory statistics. /// /// # Safety /// -/// The caller must ensure that all path parameters are valid null-terminated C -/// strings. +/// All non-NULL pointers must be valid. #[no_mangle] -pub unsafe extern "C" fn pk_prove_to_file( - prover_path: *const c_char, - input_path: *const c_char, - out_path: *const c_char, +pub unsafe extern "C" fn pk_get_memory_stats( + ram_used: *mut usize, + swap_used: *mut usize, + peak_ram: *mut usize, ) -> c_int { - catch_panic(PKError::ProofError.into(), || { - let result = (|| -> Result<(), PKError> { - let prover_path = c_str_to_str(prover_path)?; - let input_path = c_str_to_str(input_path)?; - let out_path = c_str_to_str(out_path)?; + let (ram, swap, peak) = crate::mmap_allocator::get_stats(); - let prover: Prover = - read(Path::new(&prover_path)).map_err(|_| PKError::SchemeReadError)?; + if !ram_used.is_null() { + *ram_used = ram; + } + if !swap_used.is_null() { + *swap_used = swap; + } + if !peak_ram.is_null() { + *peak_ram = peak; + } - let proof = prover - .prove_with_toml(&input_path) - .map_err(|_| PKError::ProofError)?; + PKError::Success.into() +} + +// --------------------------------------------------------------------------- +// Prepare +// --------------------------------------------------------------------------- + +/// Compile a Noir circuit into prover and verifier handles. +/// +/// No files are written and both handles live in memory. The caller must free +/// each handle exactly once via `pk_free_prover` / `pk_free_verifier`. +/// +/// # Safety +/// +/// - `circuit_path` must be a valid null-terminated C string. +/// - `out_prover` and `out_verifier` must be valid, non-null pointers. +#[no_mangle] +pub unsafe extern "C" fn pk_prepare( + circuit_path: *const c_char, + out_prover: *mut *mut PKProver, + out_verifier: *mut *mut PKVerifier, +) -> c_int { + if out_prover.is_null() || out_verifier.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::CompilationError.into(), || { + *out_prover = std::ptr::null_mut(); + *out_verifier = std::ptr::null_mut(); - provekit_common::file::write(&proof, Path::new(&out_path)) - .map_err(|_| PKError::FileWriteError)?; + let result = (|| -> Result<(*mut PKProver, *mut PKVerifier), PKError> { + let circuit_path = c_str_to_str(circuit_path)?; - Ok(()) + let scheme = NoirCompiler::from_file(Path::new(&circuit_path), HashConfig::default()) + .map_err(|_| PKError::CompilationError)?; + + let prover = Prover::from_noir_proof_scheme(scheme.clone()); + let verifier = Verifier::from_noir_proof_scheme(scheme); + + let pk = Box::into_raw(Box::new(PKProver { prover })); + let vk = Box::into_raw(Box::new(PKVerifier { verifier })); + + Ok((pk, vk)) })(); match result { - Ok(()) => PKError::Success.into(), - Err(error) => error.into(), + Ok((pk, vk)) => { + *out_prover = pk; + *out_verifier = vk; + PKError::Success.into() + } + Err(e) => e.into(), } }) } -/// Prove a Noir program and return the proof as JSON string. -/// -/// This function is only available when the "json" feature is enabled. -/// -/// # Arguments -/// -/// * `scheme_path` - Path to the prepared proof scheme (.pkp file) -/// * `input_path` - Path to the witness/input values (.toml file) -/// * `out_buf` - Output buffer to store the JSON string +// --------------------------------------------------------------------------- +// Load (from file path) +// --------------------------------------------------------------------------- + +/// Load a prover scheme from a `.pkp` file. /// -/// # Returns +/// # Safety /// -/// Returns `PKError::Success` on success, or an appropriate error code on -/// failure. The caller must free the returned buffer using `pk_free_buf`. +/// - `path` must be a valid null-terminated C string. +/// - `out` must be a valid, non-null pointer. +/// - The returned handle must be freed via `pk_free_prover`. +#[no_mangle] +pub unsafe extern "C" fn pk_load_prover(path: *const c_char, out: *mut *mut PKProver) -> c_int { + if out.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::SchemeReadError.into(), || { + *out = std::ptr::null_mut(); + + let result = (|| -> Result<*mut PKProver, PKError> { + let path = c_str_to_str(path)?; + let prover: Prover = + file::read(Path::new(&path)).map_err(|_| PKError::SchemeReadError)?; + Ok(Box::into_raw(Box::new(PKProver { prover }))) + })(); + + match result { + Ok(handle) => { + *out = handle; + PKError::Success.into() + } + Err(e) => e.into(), + } + }) +} + +/// Load a verifier scheme from a `.pkv` file. /// /// # Safety /// -/// The caller must ensure that: -/// - `prover_path` and `input_path` are valid null-terminated C strings -/// - `out_buf` is a valid pointer to a `PKBuf` structure -/// - The returned buffer is freed using `pk_free_buf` +/// - `path` must be a valid null-terminated C string. +/// - `out` must be a valid, non-null pointer. +/// - The returned handle must be freed via `pk_free_verifier`. #[no_mangle] -pub unsafe extern "C" fn pk_prove_to_json( - prover_path: *const c_char, - input_path: *const c_char, - out_buf: *mut PKBuf, -) -> c_int { - if out_buf.is_null() { +pub unsafe extern "C" fn pk_load_verifier(path: *const c_char, out: *mut *mut PKVerifier) -> c_int { + if out.is_null() { return PKError::InvalidInput.into(); } - catch_panic(PKError::ProofError.into(), || { - // Safety: out_buf is guaranteed non-null by the check above - let out_buf = &mut *out_buf; + catch_panic(PKError::SchemeReadError.into(), || { + *out = std::ptr::null_mut(); - *out_buf = PKBuf::empty(); + let result = (|| -> Result<*mut PKVerifier, PKError> { + let path = c_str_to_str(path)?; + let verifier: Verifier = + file::read(Path::new(&path)).map_err(|_| PKError::SchemeReadError)?; + Ok(Box::into_raw(Box::new(PKVerifier { verifier }))) + })(); - let result = (|| -> Result, PKError> { - let prover_path = c_str_to_str(prover_path)?; - let input_path = c_str_to_str(input_path)?; + match result { + Ok(handle) => { + *out = handle; + PKError::Success.into() + } + Err(e) => e.into(), + } + }) +} - let prover: Prover = - read(Path::new(&prover_path)).map_err(|_| PKError::SchemeReadError)?; +// --------------------------------------------------------------------------- +// Load (from bytes) +// --------------------------------------------------------------------------- - let proof = prover - .prove_with_toml(&input_path) - .map_err(|_| PKError::ProofError)?; +/// Load a prover scheme from bytes (same format as `.pkp` files). +/// +/// # Safety +/// +/// - `ptr` must point to `len` valid bytes. +/// - `out` must be a valid, non-null pointer. +/// - The returned handle must be freed via `pk_free_prover`. +#[no_mangle] +pub unsafe extern "C" fn pk_load_prover_bytes( + ptr: *const u8, + len: usize, + out: *mut *mut PKProver, +) -> c_int { + if out.is_null() || ptr.is_null() || len == 0 { + return PKError::InvalidInput.into(); + } - let json_string = - serde_json::to_string(&proof).map_err(|_| PKError::SerializationError)?; + catch_panic(PKError::SchemeReadError.into(), || { + *out = std::ptr::null_mut(); - Ok(json_string.into_bytes()) + let result = (|| -> Result<*mut PKProver, PKError> { + let data = std::slice::from_raw_parts(ptr, len); + let prover: Prover = file::deserialize(data).map_err(|_| PKError::SchemeReadError)?; + Ok(Box::into_raw(Box::new(PKProver { prover }))) })(); match result { - Ok(json_bytes) => { - *out_buf = PKBuf::from_vec(json_bytes); + Ok(handle) => { + *out = handle; PKError::Success.into() } - Err(error) => error.into(), + Err(e) => e.into(), } }) } -/// Free a buffer allocated by ProveKit FFI functions. +/// Load a verifier scheme from bytes (same format as `.pkv` files). /// -/// # Arguments +/// # Safety /// -/// * `buf` - The buffer to free +/// - `ptr` must point to `len` valid bytes. +/// - `out` must be a valid, non-null pointer. +/// - The returned handle must be freed via `pk_free_verifier`. +#[no_mangle] +pub unsafe extern "C" fn pk_load_verifier_bytes( + ptr: *const u8, + len: usize, + out: *mut *mut PKVerifier, +) -> c_int { + if out.is_null() || ptr.is_null() || len == 0 { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::SchemeReadError.into(), || { + *out = std::ptr::null_mut(); + + let result = (|| -> Result<*mut PKVerifier, PKError> { + let data = std::slice::from_raw_parts(ptr, len); + let verifier: Verifier = + file::deserialize(data).map_err(|_| PKError::SchemeReadError)?; + Ok(Box::into_raw(Box::new(PKVerifier { verifier }))) + })(); + + match result { + Ok(handle) => { + *out = handle; + PKError::Success.into() + } + Err(e) => e.into(), + } + }) +} + +// --------------------------------------------------------------------------- +// Save (to file path) +// --------------------------------------------------------------------------- + +/// Save a prover scheme to a `.pkp` file. /// /// # Safety /// -/// The caller must ensure that: -/// - The buffer was allocated by a ProveKit FFI function -/// - The buffer is not used after calling this function -/// - This function is called exactly once for each allocated buffer +/// - `prover` must be a valid handle from `pk_prepare` or `pk_load_prover`. +/// - `path` must be a valid null-terminated C string. #[no_mangle] -pub unsafe extern "C" fn pk_free_buf(buf: PKBuf) { - if !buf.ptr.is_null() && buf.cap > 0 { - drop(Vec::from_raw_parts(buf.ptr, buf.len, buf.cap)); +pub unsafe extern "C" fn pk_save_prover(prover: *const PKProver, path: *const c_char) -> c_int { + if prover.is_null() { + return PKError::InvalidInput.into(); } + + catch_panic(PKError::FileWriteError.into(), || { + let result = (|| -> Result<(), PKError> { + let path = c_str_to_str(path)?; + file::write(&(*prover).prover, Path::new(&path)).map_err(|_| PKError::FileWriteError) + })(); + + match result { + Ok(()) => PKError::Success.into(), + Err(e) => e.into(), + } + }) } -/// Initialize the ProveKit library. -/// -/// This function should be called once before using any other ProveKit -/// functions. It sets up logging and other global state. +/// Save a verifier scheme to a `.pkv` file. /// -/// # Returns +/// # Safety /// -/// Returns `PKError::Success` on success. +/// - `verifier` must be a valid handle from `pk_prepare` or `pk_load_verifier`. +/// - `path` must be a valid null-terminated C string. #[no_mangle] -pub extern "C" fn pk_init() -> c_int { - // TODO: Initialize tracing/logging for FFI consumers. - provekit_common::register_ntt(); - PKError::Success.into() +pub unsafe extern "C" fn pk_save_verifier( + verifier: *const PKVerifier, + path: *const c_char, +) -> c_int { + if verifier.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::FileWriteError.into(), || { + let result = (|| -> Result<(), PKError> { + let path = c_str_to_str(path)?; + file::write(&(*verifier).verifier, Path::new(&path)) + .map_err(|_| PKError::FileWriteError) + })(); + + match result { + Ok(()) => PKError::Success.into(), + Err(e) => e.into(), + } + }) } -/// Configure the mmap-based memory allocator. +// --------------------------------------------------------------------------- +// Serialize (to bytes) +// --------------------------------------------------------------------------- + +/// Serialize a prover scheme to bytes (same format as `.pkp` files). /// -/// MUST be called before pk_init() and before any allocations occur. +/// # Safety /// -/// # Arguments +/// - `prover` must be a valid handle. +/// - `out_buf` must be a valid, non-null pointer. +/// - The returned buffer must be freed via `pk_free_buf`. +#[no_mangle] +pub unsafe extern "C" fn pk_serialize_prover( + prover: *const PKProver, + out_buf: *mut PKBuf, +) -> c_int { + if prover.is_null() || out_buf.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::SerializationError.into(), || { + let out_buf = &mut *out_buf; + *out_buf = PKBuf::empty(); + + match file::serialize(&(*prover).prover) { + Ok(bytes) => { + *out_buf = PKBuf::from_vec(bytes); + PKError::Success.into() + } + Err(_) => PKError::SerializationError.into(), + } + }) +} + +/// Serialize a verifier scheme to bytes (same format as `.pkv` files). /// -/// * `ram_limit_bytes` - Maximum RAM to use before swapping to file (must be > -/// 0) -/// * `use_file_backed` - Whether to use file-backed mmap when over RAM limit -/// * `swap_file_path` - Path to swap directory (NULL = use system temp dir) +/// # Safety /// -/// # Returns +/// - `verifier` must be a valid handle. +/// - `out_buf` must be a valid, non-null pointer. +/// - The returned buffer must be freed via `pk_free_buf`. +#[no_mangle] +pub unsafe extern "C" fn pk_serialize_verifier( + verifier: *const PKVerifier, + out_buf: *mut PKBuf, +) -> c_int { + if verifier.is_null() || out_buf.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::SerializationError.into(), || { + let out_buf = &mut *out_buf; + *out_buf = PKBuf::empty(); + + match file::serialize(&(*verifier).verifier) { + Ok(bytes) => { + *out_buf = PKBuf::from_vec(bytes); + PKError::Success.into() + } + Err(_) => PKError::SerializationError.into(), + } + }) +} + +// --------------------------------------------------------------------------- +// Prove +// --------------------------------------------------------------------------- + +/// Prove using a prover handle and a TOML input file. /// -/// Returns `PKError::Success` or `PKError::InvalidInput` if ram_limit_bytes is -/// 0. +/// Returns proof bytes in `out_proof`. The caller must free the buffer via +/// `pk_free_buf`. /// /// # Safety /// -/// The caller must ensure that `swap_file_path` is either NULL or a valid -/// null-terminated C string. +/// - `prover` must be a valid handle. +/// - `toml_path` must be a valid null-terminated C string. +/// - `out_proof` must be a valid, non-null pointer. #[no_mangle] -pub unsafe extern "C" fn pk_configure_memory( - ram_limit_bytes: usize, - use_file_backed: bool, - swap_file_path: *const c_char, +pub unsafe extern "C" fn pk_prove_toml( + prover: *const PKProver, + toml_path: *const c_char, + out_proof: *mut PKBuf, ) -> c_int { - if ram_limit_bytes == 0 { + if prover.is_null() || out_proof.is_null() { return PKError::InvalidInput.into(); } - if crate::mmap_allocator::configure_allocator(ram_limit_bytes, use_file_backed, swap_file_path) - { - PKError::Success.into() - } else { - PKError::InvalidInput.into() - } + catch_panic(PKError::ProofError.into(), || { + let out_proof = &mut *out_proof; + *out_proof = PKBuf::empty(); + + let result = (|| -> Result, PKError> { + let toml_path = c_str_to_str(toml_path)?; + + let fresh_prover = (*prover).prover.clone(); + let proof = fresh_prover + .prove_with_toml(Path::new(&toml_path)) + .map_err(|_| PKError::ProofError)?; + + postcard::to_allocvec(&proof).map_err(|_| PKError::SerializationError) + })(); + + match result { + Ok(bytes) => { + *out_proof = PKBuf::from_vec(bytes); + PKError::Success.into() + } + Err(e) => e.into(), + } + }) } -/// Get current memory statistics. +/// Prove using a prover handle and a JSON string of inputs. /// -/// # Arguments +/// The JSON must match the circuit's ABI. Example: +/// `{"x": "5", "y": "10"}` for `fn main(x: Field, y: Field)`. /// -/// * `ram_used` - Output: current RAM usage in bytes (can be NULL) -/// * `swap_used` - Output: current swap usage in bytes (can be NULL) -/// * `peak_ram` - Output: peak RAM usage in bytes (can be NULL) +/// Returns proof bytes in `out_proof`. The caller must free the buffer via +/// `pk_free_buf`. /// -/// # Returns +/// # Safety +/// +/// - `prover` must be a valid handle. +/// - `inputs_json` must be a valid null-terminated UTF-8 C string. +/// - `out_proof` must be a valid, non-null pointer. +#[no_mangle] +pub unsafe extern "C" fn pk_prove_json( + prover: *const PKProver, + inputs_json: *const c_char, + out_proof: *mut PKBuf, +) -> c_int { + if prover.is_null() || out_proof.is_null() { + return PKError::InvalidInput.into(); + } + + catch_panic(PKError::ProofError.into(), || { + let out_proof = &mut *out_proof; + *out_proof = PKBuf::empty(); + + let result = (|| -> Result, PKError> { + let json_str = c_str_to_str(inputs_json)?; + + // Get ABI from the prover to parse inputs + let abi = match &(*prover).prover { + Prover::Noir(p) => p.witness_generator.abi(), + Prover::Mavros(_) => return Err(PKError::InvalidInput), + }; + + let format = Format::from_ext("json").ok_or(PKError::InvalidInput)?; + let input_map = format + .parse(&json_str, abi) + .map_err(|_| PKError::WitnessReadError)?; + + let fresh_prover = (*prover).prover.clone(); + let proof = fresh_prover + .prove(input_map) + .map_err(|_| PKError::ProofError)?; + + postcard::to_allocvec(&proof).map_err(|_| PKError::SerializationError) + })(); + + match result { + Ok(bytes) => { + *out_proof = PKBuf::from_vec(bytes); + PKError::Success.into() + } + Err(e) => e.into(), + } + }) +} + +// --------------------------------------------------------------------------- +// Verify +// --------------------------------------------------------------------------- + +/// Verify a proof using a verifier handle. /// -/// Returns `PKError::Success`. +/// Returns `PKError::Success` (0) if valid, `PKError::ProofError` (4) if +/// invalid. /// /// # Safety /// -/// The caller must ensure that all non-NULL pointers are valid. +/// - `verifier` must be a valid handle. +/// - `proof_ptr` must point to `proof_len` valid bytes. #[no_mangle] -pub unsafe extern "C" fn pk_get_memory_stats( - ram_used: *mut usize, - swap_used: *mut usize, - peak_ram: *mut usize, +pub unsafe extern "C" fn pk_verify( + verifier: *const PKVerifier, + proof_ptr: *const u8, + proof_len: usize, ) -> c_int { - let (ram, swap, peak) = crate::mmap_allocator::get_stats(); - - if !ram_used.is_null() { - *ram_used = ram; + if verifier.is_null() || proof_ptr.is_null() || proof_len == 0 { + return PKError::InvalidInput.into(); } - if !swap_used.is_null() { - *swap_used = swap; + + catch_panic(PKError::ProofError.into(), || { + let result = (|| -> Result { + let proof_bytes = std::slice::from_raw_parts(proof_ptr, proof_len); + let proof: NoirProof = + postcard::from_bytes(proof_bytes).map_err(|_| PKError::SerializationError)?; + + let mut fresh_verifier = (*verifier).verifier.clone(); + match fresh_verifier.verify(&proof) { + Ok(()) => Ok(true), + Err(_) => Ok(false), + } + })(); + + match result { + Ok(true) => PKError::Success.into(), + Ok(false) => PKError::ProofError.into(), + Err(e) => e.into(), + } + }) +} + +// --------------------------------------------------------------------------- +// Cleanup +// --------------------------------------------------------------------------- + +/// Free a prover handle. +/// +/// # Safety +/// +/// `prover` must have been created by `pk_prepare` or `pk_load_prover` +/// and not yet freed. +#[no_mangle] +pub unsafe extern "C" fn pk_free_prover(prover: *mut PKProver) { + if !prover.is_null() { + drop(Box::from_raw(prover)); } - if !peak_ram.is_null() { - *peak_ram = peak; +} + +/// Free a verifier handle. +/// +/// # Safety +/// +/// `verifier` must have been created by `pk_prepare` or `pk_load_verifier` +/// and not yet freed. +#[no_mangle] +pub unsafe extern "C" fn pk_free_verifier(verifier: *mut PKVerifier) { + if !verifier.is_null() { + drop(Box::from_raw(verifier)); } +} - PKError::Success.into() +/// Free a buffer allocated by ProveKit FFI functions. +/// +/// # Safety +/// +/// The buffer must have been allocated by a ProveKit FFI function and must +/// not be used after this call. +#[no_mangle] +pub unsafe extern "C" fn pk_free_buf(buf: PKBuf) { + if !buf.ptr.is_null() && buf.cap > 0 { + drop(Vec::from_raw_parts(buf.ptr, buf.len, buf.cap)); + } } diff --git a/tooling/provekit-ffi/src/lib.rs b/tooling/provekit-ffi/src/lib.rs index e5babc9dd..7ababb52e 100644 --- a/tooling/provekit-ffi/src/lib.rs +++ b/tooling/provekit-ffi/src/lib.rs @@ -1,22 +1,24 @@ //! FFI bindings for ProveKit, enabling integration with multiple programming //! languages and platforms. //! -//! This crate provides C-compatible functions for loading Noir proof schemes, -//! reading witness inputs, and generating proofs that can be called from any +//! This crate provides C-compatible functions for compiling Noir circuits, +//! generating proofs, and verifying proofs. It can be called from any //! language that supports C FFI (Swift, Kotlin, Python, JavaScript, etc.). //! //! # Architecture //! -//! The FFI bindings are organized into several modules: -//! - `types`: Type definitions (PKBuf, PKError, etc.) -//! - `ffi`: Main FFI functions exposed via C ABI -//! - `utils`: Internal utility functions +//! The FFI uses opaque handles (`PKProver`, `PKVerifier`) instead of file +//! paths. The SDK creates handles via `pk_prepare` or `pk_load_*`, uses them +//! for proving/verifying, and frees them when done. //! //! # Usage //! //! 1. Call `pk_init()` once before using any other functions -//! 2. Use `pk_prove_to_file()` or `pk_prove_to_json()` to generate proofs -//! 3. Free any returned buffers using `pk_free_buf()` +//! 2. Call `pk_prepare()` to compile a circuit into prover + verifier handles +//! 3. Call `pk_prove_toml()` or `pk_prove_json()` to generate proofs +//! 4. Call `pk_verify()` to verify proofs +//! 5. Free handles with `pk_free_prover()` / `pk_free_verifier()` +//! 6. Free buffers with `pk_free_buf()` //! //! # Safety //! diff --git a/tooling/provekit-ffi/src/types.rs b/tooling/provekit-ffi/src/types.rs index a772d86be..409da27f2 100644 --- a/tooling/provekit-ffi/src/types.rs +++ b/tooling/provekit-ffi/src/types.rs @@ -1,6 +1,9 @@ //! Type definitions for ProveKit FFI bindings. -use std::{os::raw::c_int, ptr}; +use { + provekit_common::{Prover, Verifier}, + std::{os::raw::c_int, ptr}, +}; /// Buffer structure for returning data to foreign languages. /// The caller is responsible for freeing the buffer using `pk_free_buf`. @@ -54,6 +57,8 @@ pub enum PKError { Utf8Error = 6, /// File write error FileWriteError = 7, + /// Circuit compilation error + CompilationError = 8, } impl From for c_int { @@ -61,3 +66,27 @@ impl From for c_int { error as c_int } } + +/// Opaque handle to a compiled prover scheme. +/// +/// Holds a `Prover` that is cloned for each prove call (since `Prove::prove` +/// consumes `self`). Thread-safe: `Prover` is `Send + Sync`, and all access +/// through the handle is read-only (clone then use). +/// +/// Created by `pk_prepare` or `pk_load_prover`. Must be freed exactly once +/// via `pk_free_prover`. +pub struct PKProver { + pub(crate) prover: Prover, +} + +/// Opaque handle to a compiled verifier scheme. +/// +/// Holds a `Verifier` that is cloned for each verify call (since +/// `Verify::verify` consumes `whir_for_witness` via `.take()`). Thread-safe +/// for the same reasons as `PKProver`. +/// +/// Created by `pk_prepare` or `pk_load_verifier`. Must be freed exactly once +/// via `pk_free_verifier`. +pub struct PKVerifier { + pub(crate) verifier: Verifier, +} From 33cc945b44354e67895c8b2eea1638a18875c2a8 Mon Sep 17 00:00:00 2001 From: ashpect Date: Thu, 26 Mar 2026 02:59:18 +0530 Subject: [PATCH 04/12] chore: cleanup --- Cargo.lock | 1 - tooling/provekit-ffi/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c59517cc..85b2a6477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4597,7 +4597,6 @@ dependencies = [ "provekit-prover", "provekit-r1cs-compiler", "provekit-verifier", - "serde_json", ] [[package]] diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index 2f9a4fa83..05ca3828a 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -22,7 +22,6 @@ provekit-verifier = { workspace = true } anyhow.workspace = true noirc_abi.workspace = true postcard.workspace = true -serde_json.workspace = true parking_lot = "0.12" [target.'cfg(unix)'.dependencies] From fd99ed0a3fe48d6673c995c0363f937c669221c1 Mon Sep 17 00:00:00 2001 From: ashpect Date: Thu, 26 Mar 2026 03:24:43 +0530 Subject: [PATCH 05/12] fix: use streaming zstd decompression in deserialize_from_bytes --- provekit/common/src/file/io/bin.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/provekit/common/src/file/io/bin.rs b/provekit/common/src/file/io/bin.rs index 12ba1a530..c4732f9d5 100644 --- a/provekit/common/src/file/io/bin.rs +++ b/provekit/common/src/file/io/bin.rs @@ -239,7 +239,12 @@ fn decompress_bytes(data: &[u8]) -> Result> { let is_xz = data[..6] == XZ_MAGIC; if is_zstd { - zstd::bulk::decompress(data, usize::MAX).context("while decompressing zstd data") + let mut out = Vec::new(); + let mut decoder = zstd::Decoder::new(data).context("while initializing zstd decoder")?; + decoder + .read_to_end(&mut out) + .context("while decompressing zstd data")?; + Ok(out) } else if is_xz { let mut out = Vec::new(); let mut decoder = xz2::read::XzDecoder::new(data); From 43f75312ca0eb794c66ba5b48cc3ab827dd40b05 Mon Sep 17 00:00:00 2001 From: Aditya Bisht Date: Mon, 30 Mar 2026 11:32:19 -0700 Subject: [PATCH 06/12] feat: add cdylib crate type to provekit-ffi for Android dynamic linking --- tooling/provekit-ffi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index 05ca3828a..3e0ffa209 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true repository.workspace = true [lib] -crate-type = ["staticlib"] +crate-type = ["staticlib", "cdylib"] [dependencies] # Workspace crates From 9125495922d96dcd90f589d23141080e8a965d1d Mon Sep 17 00:00:00 2001 From: Aditya Bisht Date: Mon, 30 Mar 2026 11:41:06 -0700 Subject: [PATCH 07/12] feat: add release-mobile profile with opt-level z for iOS/Android builds --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index c73da926a..7259f6558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,11 @@ opt-level = 3 codegen-units = 1 lto = "fat" +# Size-optimized builds for mobile (iOS/Android): cargo build --profile release-mobile +[profile.release-mobile] +inherits = "release" +opt-level = "z" + # Fast release builds for development iteration: cargo build --profile release-fast [profile.release-fast] inherits = "release" From fdb43d2103042fe5f2fc3116237970101b87fc4a Mon Sep 17 00:00:00 2001 From: ashpect Date: Tue, 31 Mar 2026 00:18:33 +0530 Subject: [PATCH 08/12] fix: resolve comments --- Cargo.lock | 1 - provekit/common/src/file/io/bin.rs | 89 +++----- provekit/common/src/prover.rs | 8 + tooling/provekit-ffi/Cargo.toml | 1 - tooling/provekit-ffi/src/ffi.rs | 334 ++++++++++++++++++++--------- tooling/provekit-ffi/src/lib.rs | 8 +- tooling/provekit-ffi/src/types.rs | 38 +++- tooling/provekit-ffi/src/utils.rs | 12 +- 8 files changed, 322 insertions(+), 169 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85b2a6477..37318a25d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4592,7 +4592,6 @@ dependencies = [ "libc", "noirc_abi", "parking_lot", - "postcard", "provekit-common", "provekit-prover", "provekit-r1cs-compiler", diff --git a/provekit/common/src/file/io/bin.rs b/provekit/common/src/file/io/bin.rs index c4732f9d5..a092b9462 100644 --- a/provekit/common/src/file/io/bin.rs +++ b/provekit/common/src/file/io/bin.rs @@ -6,7 +6,7 @@ use { HashConfig, }, anyhow::{ensure, Context as _, Result}, - bytes::{Buf, BufMut as _, Bytes, BytesMut}, + bytes::{Buf, BufMut as _, Bytes}, serde::{Deserialize, Serialize}, std::{ fs::File, @@ -20,6 +20,12 @@ use { /// MINOR(2) = 20 const HASH_CONFIG_OFFSET: usize = 20; +/// Zstd compression level used for serialization. +const ZSTD_LEVEL: i32 = 3; + +/// XZ compression level used for serialization. +const XZ_LEVEL: u32 = 6; + /// Compression algorithm for binary file output. #[derive(Debug, Clone, Copy)] pub enum Compression { @@ -27,62 +33,45 @@ pub enum Compression { Xz, } +/// Compress data using the specified algorithm. +fn compress(data: &[u8], compression: Compression) -> Result> { + match compression { + Compression::Zstd => { + zstd::bulk::compress(data, ZSTD_LEVEL).context("while compressing with zstd") + } + Compression::Xz => { + let mut buf = Vec::new(); + let mut encoder = xz2::write::XzEncoder::new(&mut buf, XZ_LEVEL); + encoder + .write_all(data) + .context("while compressing with xz")?; + encoder.finish().context("while finishing xz stream")?; + Ok(buf) + } + } +} + /// Write a compressed binary file. #[instrument(skip(value))] pub fn write_bin( value: &T, path: &Path, format: [u8; 8], - (major, minor): (u16, u16), + version: (u16, u16), compression: Compression, hash_config: Option, ) -> Result<()> { - let postcard_data = postcard::to_allocvec(value).context("while encoding to postcard")?; - let uncompressed = postcard_data.len(); - - let compressed_data = match compression { - Compression::Zstd => { - zstd::bulk::compress(&postcard_data, 3).context("while compressing with zstd")? - } - Compression::Xz => { - let mut buf = Vec::new(); - let mut encoder = xz2::write::XzEncoder::new(&mut buf, 6); - encoder - .write_all(&postcard_data) - .context("while compressing with xz")?; - encoder.finish().context("while finishing xz stream")?; - buf - } - }; + let data = serialize_to_bytes(value, format, version, compression, hash_config)?; let mut file = File::create(path).context("while creating output file")?; - - // Write header: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) - let mut header = BytesMut::with_capacity(HEADER_SIZE); - header.put(MAGIC_BYTES); - header.put(&format[..]); - header.put_u16_le(major); - header.put_u16_le(minor); - header.put_u8(hash_config.map(|c| c.to_byte()).unwrap_or(0xff)); - - file.write_all(&header).context("while writing header")?; - - file.write_all(&compressed_data) - .context("while writing compressed data")?; - - let compressed = HEADER_SIZE + compressed_data.len(); - let size = file.metadata().map(|m| m.len()).ok(); + file.write_all(&data).context("while writing data")?; file.sync_all().context("while syncing output file")?; - drop(file); - let ratio = compressed as f64 / uncompressed as f64; info!( ?path, - size, - compressed, - uncompressed, - "Wrote {}B bytes to {path:?} ({ratio:.2} compression ratio)", - human(compressed as f64) + size = data.len(), + "Wrote {}B to {path:?}", + human(data.len() as f64) ); Ok(()) } @@ -167,21 +156,7 @@ pub fn serialize_to_bytes( hash_config: Option, ) -> Result> { let postcard_data = postcard::to_allocvec(value).context("while encoding to postcard")?; - - let compressed_data = match compression { - Compression::Zstd => { - zstd::bulk::compress(&postcard_data, 3).context("while compressing with zstd")? - } - Compression::Xz => { - let mut buf = Vec::new(); - let mut encoder = xz2::write::XzEncoder::new(&mut buf, 6); - encoder - .write_all(&postcard_data) - .context("while compressing with xz")?; - encoder.finish().context("while finishing xz stream")?; - buf - } - }; + let compressed_data = compress(&postcard_data, compression)?; let mut out = Vec::with_capacity(HEADER_SIZE + compressed_data.len()); // Header: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) diff --git a/provekit/common/src/prover.rs b/provekit/common/src/prover.rs index a8d09c9d2..9cf750282 100644 --- a/provekit/common/src/prover.rs +++ b/provekit/common/src/prover.rs @@ -6,6 +6,7 @@ use { HashConfig, MavrosProver, NoirElement, R1CS, }, acir::circuit::Program, + noirc_abi::Abi, serde::{Deserialize, Serialize}, }; @@ -53,6 +54,13 @@ impl Prover { } } + pub fn abi(&self) -> &Abi { + match self { + Prover::Noir(p) => p.witness_generator.abi(), + Prover::Mavros(p) => &p.abi, + } + } + pub fn size(&self) -> (usize, usize) { match self { Prover::Noir(p) => (p.r1cs.num_constraints(), p.r1cs.num_witnesses()), diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index 3e0ffa209..439861614 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -21,7 +21,6 @@ provekit-verifier = { workspace = true } # 3rd party anyhow.workspace = true noirc_abi.workspace = true -postcard.workspace = true parking_lot = "0.12" [target.'cfg(unix)'.dependencies] diff --git a/tooling/provekit-ffi/src/ffi.rs b/tooling/provekit-ffi/src/ffi.rs index 8d7772718..c0687cc75 100644 --- a/tooling/provekit-ffi/src/ffi.rs +++ b/tooling/provekit-ffi/src/ffi.rs @@ -1,11 +1,12 @@ //! Handle-based FFI functions for ProveKit. //! //! All functions use opaque `PKProver` / `PKVerifier` handles instead of file -//! paths. Proofs are always returned as bytes in a `PKBuf`. +//! paths. Proofs are returned as bytes in a `PKBuf` using the standard `.np` +//! binary format (header + compressed postcard), interoperable with CLI tools. use { crate::{ - types::{PKBuf, PKError, PKProver, PKVerifier}, + types::{PKBuf, PKProver, PKStatus, PKVerifier}, utils::c_str_to_str, }, noirc_abi::input_parser::Format, @@ -14,20 +15,74 @@ use { provekit_r1cs_compiler::NoirCompiler, provekit_verifier::Verify, std::{ + cell::RefCell, os::raw::{c_char, c_int}, panic, path::Path, }, }; +// --------------------------------------------------------------------------- +// Error capture (thread-local last-error pattern) +// --------------------------------------------------------------------------- + +thread_local! { + static LAST_ERROR: RefCell> = const { RefCell::new(None) }; +} + +pub(crate) fn set_last_error(msg: String) { + LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg)); +} + +fn clear_last_error() { + LAST_ERROR.with(|e| *e.borrow_mut() = None); +} + /// Catches panics and converts them to error codes to prevent unwinding across -/// FFI boundary. +/// FFI boundary. Captures panic payloads into the thread-local last-error +/// buffer, retrievable via `pk_get_last_error`. #[inline] fn catch_panic(default: T, f: F) -> T where F: FnOnce() -> T + panic::UnwindSafe, { - panic::catch_unwind(f).unwrap_or(default) + clear_last_error(); + match panic::catch_unwind(f) { + Ok(v) => v, + Err(payload) => { + let msg = payload + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "unknown panic".into()); + set_last_error(msg); + default + } + } +} + +/// Get the error message from the most recent failing FFI call. +/// +/// Returns the message as UTF-8 bytes in `out_buf`. The error is cleared +/// after this call. Returns an empty buffer if no error is stored. The caller +/// must free the buffer via `pk_free_buf`. +/// +/// # Safety +/// +/// - `out_buf` must be a valid, non-null pointer. +#[no_mangle] +pub unsafe extern "C" fn pk_get_last_error(out_buf: *mut PKBuf) -> c_int { + if out_buf.is_null() { + return PKStatus::InvalidInput.into(); + } + + // SAFETY: out_buf is guaranteed non-null by the check above. + let out = &mut *out_buf; + *out = LAST_ERROR.with(|e| match e.borrow_mut().take() { + Some(msg) => PKBuf::from_vec(msg.into_bytes()), + None => PKBuf::empty(), + }); + PKStatus::Success.into() } /// Initialize the ProveKit library. @@ -36,12 +91,13 @@ where #[no_mangle] pub extern "C" fn pk_init() -> c_int { provekit_common::register_ntt(); - PKError::Success.into() + PKStatus::Success.into() } /// Configure the mmap-based memory allocator. /// -/// MUST be called before `pk_init()` and before any allocations occur. +/// Optional. If called, MUST be invoked before `pk_init()` and before any +/// allocations occur. /// /// # Safety /// @@ -53,14 +109,15 @@ pub unsafe extern "C" fn pk_configure_memory( swap_file_path: *const c_char, ) -> c_int { if ram_limit_bytes == 0 { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } if crate::mmap_allocator::configure_allocator(ram_limit_bytes, use_file_backed, swap_file_path) { - PKError::Success.into() + PKStatus::Success.into() } else { - PKError::InvalidInput.into() + set_last_error("memory allocator configuration failed".into()); + PKStatus::InvalidInput.into() } } @@ -78,6 +135,7 @@ pub unsafe extern "C" fn pk_get_memory_stats( let (ram, swap, peak) = crate::mmap_allocator::get_stats(); if !ram_used.is_null() { + // SAFETY: caller guarantees non-null pointers are valid. *ram_used = ram; } if !swap_used.is_null() { @@ -87,7 +145,7 @@ pub unsafe extern "C" fn pk_get_memory_stats( *peak_ram = peak; } - PKError::Success.into() + PKStatus::Success.into() } // --------------------------------------------------------------------------- @@ -96,6 +154,9 @@ pub unsafe extern "C" fn pk_get_memory_stats( /// Compile a Noir circuit into prover and verifier handles. /// +/// `hash_config` selects the hash algorithm: 0 = Skyscraper (default), +/// 1 = SHA-256, 2 = Keccak, 3 = Blake3. +/// /// No files are written and both handles live in memory. The caller must free /// each handle exactly once via `pk_free_prover` / `pk_free_verifier`. /// @@ -106,37 +167,49 @@ pub unsafe extern "C" fn pk_get_memory_stats( #[no_mangle] pub unsafe extern "C" fn pk_prepare( circuit_path: *const c_char, + hash_config: c_int, out_prover: *mut *mut PKProver, out_verifier: *mut *mut PKVerifier, ) -> c_int { if out_prover.is_null() || out_verifier.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::CompilationError.into(), || { + catch_panic(PKStatus::CompilationError.into(), || { + // SAFETY: out_prover / out_verifier are guaranteed non-null above. *out_prover = std::ptr::null_mut(); *out_verifier = std::ptr::null_mut(); - let result = (|| -> Result<(*mut PKProver, *mut PKVerifier), PKError> { + let result = (|| -> Result<(*mut PKProver, *mut PKVerifier), PKStatus> { let circuit_path = c_str_to_str(circuit_path)?; - let scheme = NoirCompiler::from_file(Path::new(&circuit_path), HashConfig::default()) - .map_err(|_| PKError::CompilationError)?; + let hash = HashConfig::from_byte(hash_config.try_into().unwrap_or(u8::MAX)) + .ok_or_else(|| { + set_last_error( + "hash_config must be 0-3 (skyscraper, sha256, keccak, blake3)".into(), + ); + PKStatus::InvalidInput + })?; + + let scheme = NoirCompiler::from_file(Path::new(&circuit_path), hash).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::CompilationError + })?; let prover = Prover::from_noir_proof_scheme(scheme.clone()); let verifier = Verifier::from_noir_proof_scheme(scheme); - let pk = Box::into_raw(Box::new(PKProver { prover })); - let vk = Box::into_raw(Box::new(PKVerifier { verifier })); + let pk = Box::new(PKProver { prover }); + let vk = Box::new(PKVerifier { verifier }); - Ok((pk, vk)) + Ok((Box::into_raw(pk), Box::into_raw(vk))) })(); match result { Ok((pk, vk)) => { *out_prover = pk; *out_verifier = vk; - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -157,23 +230,26 @@ pub unsafe extern "C" fn pk_prepare( #[no_mangle] pub unsafe extern "C" fn pk_load_prover(path: *const c_char, out: *mut *mut PKProver) -> c_int { if out.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SchemeReadError.into(), || { + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. *out = std::ptr::null_mut(); - let result = (|| -> Result<*mut PKProver, PKError> { + let result = (|| -> Result<*mut PKProver, PKStatus> { let path = c_str_to_str(path)?; - let prover: Prover = - file::read(Path::new(&path)).map_err(|_| PKError::SchemeReadError)?; + let prover: Prover = file::read(Path::new(&path)).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; Ok(Box::into_raw(Box::new(PKProver { prover }))) })(); match result { Ok(handle) => { *out = handle; - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -190,23 +266,26 @@ pub unsafe extern "C" fn pk_load_prover(path: *const c_char, out: *mut *mut PKPr #[no_mangle] pub unsafe extern "C" fn pk_load_verifier(path: *const c_char, out: *mut *mut PKVerifier) -> c_int { if out.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SchemeReadError.into(), || { + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. *out = std::ptr::null_mut(); - let result = (|| -> Result<*mut PKVerifier, PKError> { + let result = (|| -> Result<*mut PKVerifier, PKStatus> { let path = c_str_to_str(path)?; - let verifier: Verifier = - file::read(Path::new(&path)).map_err(|_| PKError::SchemeReadError)?; + let verifier: Verifier = file::read(Path::new(&path)).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; Ok(Box::into_raw(Box::new(PKVerifier { verifier }))) })(); match result { Ok(handle) => { *out = handle; - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -231,22 +310,27 @@ pub unsafe extern "C" fn pk_load_prover_bytes( out: *mut *mut PKProver, ) -> c_int { if out.is_null() || ptr.is_null() || len == 0 { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SchemeReadError.into(), || { + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. *out = std::ptr::null_mut(); - let result = (|| -> Result<*mut PKProver, PKError> { + let result = (|| -> Result<*mut PKProver, PKStatus> { + // SAFETY: ptr/len validity is guaranteed by the caller (documented in # Safety). let data = std::slice::from_raw_parts(ptr, len); - let prover: Prover = file::deserialize(data).map_err(|_| PKError::SchemeReadError)?; + let prover: Prover = file::deserialize(data).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; Ok(Box::into_raw(Box::new(PKProver { prover }))) })(); match result { Ok(handle) => { *out = handle; - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -267,23 +351,27 @@ pub unsafe extern "C" fn pk_load_verifier_bytes( out: *mut *mut PKVerifier, ) -> c_int { if out.is_null() || ptr.is_null() || len == 0 { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SchemeReadError.into(), || { + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. *out = std::ptr::null_mut(); - let result = (|| -> Result<*mut PKVerifier, PKError> { + let result = (|| -> Result<*mut PKVerifier, PKStatus> { + // SAFETY: ptr/len validity is guaranteed by the caller (documented in # Safety). let data = std::slice::from_raw_parts(ptr, len); - let verifier: Verifier = - file::deserialize(data).map_err(|_| PKError::SchemeReadError)?; + let verifier: Verifier = file::deserialize(data).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; Ok(Box::into_raw(Box::new(PKVerifier { verifier }))) })(); match result { Ok(handle) => { *out = handle; - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -303,17 +391,21 @@ pub unsafe extern "C" fn pk_load_verifier_bytes( #[no_mangle] pub unsafe extern "C" fn pk_save_prover(prover: *const PKProver, path: *const c_char) -> c_int { if prover.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::FileWriteError.into(), || { - let result = (|| -> Result<(), PKError> { + catch_panic(PKStatus::FileWriteError.into(), || { + let result = (|| -> Result<(), PKStatus> { let path = c_str_to_str(path)?; - file::write(&(*prover).prover, Path::new(&path)).map_err(|_| PKError::FileWriteError) + // SAFETY: prover is guaranteed non-null and valid by caller contract. + file::write(&(*prover).prover, Path::new(&path)).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::FileWriteError + }) })(); match result { - Ok(()) => PKError::Success.into(), + Ok(()) => PKStatus::Success.into(), Err(e) => e.into(), } }) @@ -331,18 +423,21 @@ pub unsafe extern "C" fn pk_save_verifier( path: *const c_char, ) -> c_int { if verifier.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::FileWriteError.into(), || { - let result = (|| -> Result<(), PKError> { + catch_panic(PKStatus::FileWriteError.into(), || { + let result = (|| -> Result<(), PKStatus> { let path = c_str_to_str(path)?; - file::write(&(*verifier).verifier, Path::new(&path)) - .map_err(|_| PKError::FileWriteError) + // SAFETY: verifier is guaranteed non-null and valid by caller contract. + file::write(&(*verifier).verifier, Path::new(&path)).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::FileWriteError + }) })(); match result { - Ok(()) => PKError::Success.into(), + Ok(()) => PKStatus::Success.into(), Err(e) => e.into(), } }) @@ -365,19 +460,24 @@ pub unsafe extern "C" fn pk_serialize_prover( out_buf: *mut PKBuf, ) -> c_int { if prover.is_null() || out_buf.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SerializationError.into(), || { + catch_panic(PKStatus::SerializationError.into(), || { + // SAFETY: out_buf is guaranteed non-null by the check above. let out_buf = &mut *out_buf; *out_buf = PKBuf::empty(); + // SAFETY: prover is guaranteed non-null and valid by caller contract. match file::serialize(&(*prover).prover) { Ok(bytes) => { *out_buf = PKBuf::from_vec(bytes); - PKError::Success.into() + PKStatus::Success.into() + } + Err(e) => { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError.into() } - Err(_) => PKError::SerializationError.into(), } }) } @@ -395,19 +495,24 @@ pub unsafe extern "C" fn pk_serialize_verifier( out_buf: *mut PKBuf, ) -> c_int { if verifier.is_null() || out_buf.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::SerializationError.into(), || { + catch_panic(PKStatus::SerializationError.into(), || { + // SAFETY: out_buf is guaranteed non-null by the check above. let out_buf = &mut *out_buf; *out_buf = PKBuf::empty(); + // SAFETY: verifier is guaranteed non-null and valid by caller contract. match file::serialize(&(*verifier).verifier) { Ok(bytes) => { *out_buf = PKBuf::from_vec(bytes); - PKError::Success.into() + PKStatus::Success.into() + } + Err(e) => { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError.into() } - Err(_) => PKError::SerializationError.into(), } }) } @@ -433,28 +538,37 @@ pub unsafe extern "C" fn pk_prove_toml( out_proof: *mut PKBuf, ) -> c_int { if prover.is_null() || out_proof.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::ProofError.into(), || { + catch_panic(PKStatus::ProofError.into(), || { + // SAFETY: out_proof is guaranteed non-null by the check above. let out_proof = &mut *out_proof; *out_proof = PKBuf::empty(); - let result = (|| -> Result, PKError> { + let result = (|| -> Result, PKStatus> { let toml_path = c_str_to_str(toml_path)?; + // Clone is required: Prove::prove consumes self. + // SAFETY: prover is guaranteed non-null and valid by caller contract. let fresh_prover = (*prover).prover.clone(); let proof = fresh_prover .prove_with_toml(Path::new(&toml_path)) - .map_err(|_| PKError::ProofError)?; - - postcard::to_allocvec(&proof).map_err(|_| PKError::SerializationError) + .map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::ProofError + })?; + + file::serialize(&proof).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError + }) })(); match result { Ok(bytes) => { *out_proof = PKBuf::from_vec(bytes); - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -466,8 +580,12 @@ pub unsafe extern "C" fn pk_prove_toml( /// The JSON must match the circuit's ABI. Example: /// `{"x": "5", "y": "10"}` for `fn main(x: Field, y: Field)`. /// -/// Returns proof bytes in `out_proof`. The caller must free the buffer via -/// `pk_free_buf`. +/// Returns proof bytes in `out_proof` using the standard `.np` binary format. +/// The caller must free the buffer via `pk_free_buf`. +/// +/// Note: internally clones the full prover scheme per call since `prove()` +/// consumes `self`. This may be significant for large circuits on constrained +/// devices. /// /// # Safety /// @@ -481,39 +599,43 @@ pub unsafe extern "C" fn pk_prove_json( out_proof: *mut PKBuf, ) -> c_int { if prover.is_null() || out_proof.is_null() { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::ProofError.into(), || { + catch_panic(PKStatus::ProofError.into(), || { + // SAFETY: out_proof is guaranteed non-null by the check above. let out_proof = &mut *out_proof; *out_proof = PKBuf::empty(); - let result = (|| -> Result, PKError> { + let result = (|| -> Result, PKStatus> { let json_str = c_str_to_str(inputs_json)?; - // Get ABI from the prover to parse inputs - let abi = match &(*prover).prover { - Prover::Noir(p) => p.witness_generator.abi(), - Prover::Mavros(_) => return Err(PKError::InvalidInput), - }; + // SAFETY: prover is guaranteed non-null and valid by caller contract. + let abi = (*prover).prover.abi(); - let format = Format::from_ext("json").ok_or(PKError::InvalidInput)?; - let input_map = format - .parse(&json_str, abi) - .map_err(|_| PKError::WitnessReadError)?; + let format = Format::from_ext("json").ok_or(PKStatus::InvalidInput)?; + let input_map = format.parse(&json_str, abi).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::WitnessReadError + })?; + // Clone is required: Prove::prove consumes self. let fresh_prover = (*prover).prover.clone(); - let proof = fresh_prover - .prove(input_map) - .map_err(|_| PKError::ProofError)?; - - postcard::to_allocvec(&proof).map_err(|_| PKError::SerializationError) + let proof = fresh_prover.prove(input_map).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::ProofError + })?; + + file::serialize(&proof).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError + }) })(); match result { Ok(bytes) => { *out_proof = PKBuf::from_vec(bytes); - PKError::Success.into() + PKStatus::Success.into() } Err(e) => e.into(), } @@ -526,9 +648,16 @@ pub unsafe extern "C" fn pk_prove_json( /// Verify a proof using a verifier handle. /// -/// Returns `PKError::Success` (0) if valid, `PKError::ProofError` (4) if +/// Expects proof bytes in the standard `.np` binary format (as returned by +/// `pk_prove_toml` / `pk_prove_json`). +/// +/// Returns `PKStatus::Success` (0) if valid, `PKStatus::ProofError` (4) if /// invalid. /// +/// Note: internally clones the full verifier scheme per call since `verify()` +/// consumes internal state. This may be significant for large circuits on +/// constrained devices. +/// /// # Safety /// /// - `verifier` must be a valid handle. @@ -540,25 +669,33 @@ pub unsafe extern "C" fn pk_verify( proof_len: usize, ) -> c_int { if verifier.is_null() || proof_ptr.is_null() || proof_len == 0 { - return PKError::InvalidInput.into(); + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::ProofError.into(), || { - let result = (|| -> Result { + catch_panic(PKStatus::ProofError.into(), || { + let result = (|| -> Result { + // SAFETY: proof_ptr/proof_len validity is guaranteed by the caller. let proof_bytes = std::slice::from_raw_parts(proof_ptr, proof_len); - let proof: NoirProof = - postcard::from_bytes(proof_bytes).map_err(|_| PKError::SerializationError)?; + let proof: NoirProof = file::deserialize(proof_bytes).map_err(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError + })?; + // Clone is required: Verify::verify consumes internal state. + // SAFETY: verifier is guaranteed non-null and valid by caller contract. let mut fresh_verifier = (*verifier).verifier.clone(); match fresh_verifier.verify(&proof) { Ok(()) => Ok(true), - Err(_) => Ok(false), + Err(e) => { + set_last_error(format!("{e:#}")); + Ok(false) + } } })(); match result { - Ok(true) => PKError::Success.into(), - Ok(false) => PKError::ProofError.into(), + Ok(true) => PKStatus::Success.into(), + Ok(false) => PKStatus::ProofError.into(), Err(e) => e.into(), } }) @@ -577,6 +714,7 @@ pub unsafe extern "C" fn pk_verify( #[no_mangle] pub unsafe extern "C" fn pk_free_prover(prover: *mut PKProver) { if !prover.is_null() { + // SAFETY: caller guarantees this is a valid, non-freed handle. drop(Box::from_raw(prover)); } } @@ -590,6 +728,7 @@ pub unsafe extern "C" fn pk_free_prover(prover: *mut PKProver) { #[no_mangle] pub unsafe extern "C" fn pk_free_verifier(verifier: *mut PKVerifier) { if !verifier.is_null() { + // SAFETY: caller guarantees this is a valid, non-freed handle. drop(Box::from_raw(verifier)); } } @@ -603,6 +742,7 @@ pub unsafe extern "C" fn pk_free_verifier(verifier: *mut PKVerifier) { #[no_mangle] pub unsafe extern "C" fn pk_free_buf(buf: PKBuf) { if !buf.ptr.is_null() && buf.cap > 0 { + // SAFETY: buf was created by PKBuf::from_vec which used mem::forget. drop(Vec::from_raw_parts(buf.ptr, buf.len, buf.cap)); } } diff --git a/tooling/provekit-ffi/src/lib.rs b/tooling/provekit-ffi/src/lib.rs index 7ababb52e..d41f0b95b 100644 --- a/tooling/provekit-ffi/src/lib.rs +++ b/tooling/provekit-ffi/src/lib.rs @@ -14,11 +14,13 @@ //! # Usage //! //! 1. Call `pk_init()` once before using any other functions -//! 2. Call `pk_prepare()` to compile a circuit into prover + verifier handles +//! 2. Call `pk_prepare(path, hash_config, ...)` to compile a circuit, or +//! `pk_load_prover()` / `pk_load_verifier()` to load from files //! 3. Call `pk_prove_toml()` or `pk_prove_json()` to generate proofs //! 4. Call `pk_verify()` to verify proofs -//! 5. Free handles with `pk_free_prover()` / `pk_free_verifier()` -//! 6. Free buffers with `pk_free_buf()` +//! 5. On error, call `pk_get_last_error()` for a diagnostic message +//! 6. Free handles with `pk_free_prover()` / `pk_free_verifier()` +//! 7. Free buffers with `pk_free_buf()` //! //! # Safety //! diff --git a/tooling/provekit-ffi/src/types.rs b/tooling/provekit-ffi/src/types.rs index 409da27f2..c110df6d2 100644 --- a/tooling/provekit-ffi/src/types.rs +++ b/tooling/provekit-ffi/src/types.rs @@ -8,6 +8,7 @@ use { /// Buffer structure for returning data to foreign languages. /// The caller is responsible for freeing the buffer using `pk_free_buf`. #[repr(C)] +#[must_use = "this buffer must be freed with pk_free_buf"] pub struct PKBuf { /// Pointer to the data pub ptr: *mut u8, @@ -37,10 +38,10 @@ impl PKBuf { } } -/// Error codes returned by FFI functions +/// Status codes returned by FFI functions. #[repr(C)] #[derive(Debug)] -pub enum PKError { +pub enum PKStatus { /// Success Success = 0, /// Invalid input parameters (null pointers, etc.) @@ -49,7 +50,7 @@ pub enum PKError { SchemeReadError = 2, /// Failed to read witness/input file WitnessReadError = 3, - /// Failed to generate proof + /// Failed to generate or verify proof ProofError = 4, /// Failed to serialize output SerializationError = 5, @@ -61,9 +62,27 @@ pub enum PKError { CompilationError = 8, } -impl From for c_int { - fn from(error: PKError) -> Self { - error as c_int +impl std::fmt::Display for PKStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Success => "success", + Self::InvalidInput => "invalid input parameter", + Self::SchemeReadError => "failed to read proof scheme", + Self::WitnessReadError => "failed to read witness/input", + Self::ProofError => "proof generation or verification failed", + Self::SerializationError => "serialization failed", + Self::Utf8Error => "invalid UTF-8 in C string", + Self::FileWriteError => "file write failed", + Self::CompilationError => "circuit compilation failed", + }) + } +} + +impl std::error::Error for PKStatus {} + +impl From for c_int { + fn from(status: PKStatus) -> Self { + status as c_int } } @@ -90,3 +109,10 @@ pub struct PKProver { pub struct PKVerifier { pub(crate) verifier: Verifier, } + +// Compile-time assertion: PKProver and PKVerifier must be Send + Sync. +// If a future change adds a non-Send/Sync field, this will fail to compile. +#[allow(dead_code)] +trait AssertSendSync: Send + Sync {} +impl AssertSendSync for PKProver {} +impl AssertSendSync for PKVerifier {} diff --git a/tooling/provekit-ffi/src/utils.rs b/tooling/provekit-ffi/src/utils.rs index 64eb4130f..09c41b782 100644 --- a/tooling/provekit-ffi/src/utils.rs +++ b/tooling/provekit-ffi/src/utils.rs @@ -1,7 +1,7 @@ //! Utility functions for ProveKit FFI bindings. use { - crate::types::PKError, + crate::{ffi::set_last_error, types::PKStatus}, anyhow::Result, std::{ffi::CStr, os::raw::c_char}, }; @@ -15,12 +15,16 @@ use { /// /// The caller must ensure that `ptr` is a valid null-terminated C string /// that remains valid for the duration of this function call. -pub unsafe fn c_str_to_str(ptr: *const c_char) -> Result { +pub unsafe fn c_str_to_str(ptr: *const c_char) -> Result { if ptr.is_null() { - return Err(PKError::InvalidInput); + set_last_error("null pointer passed as C string".into()); + return Err(PKStatus::InvalidInput); } CStr::from_ptr(ptr) .to_str() .map(|s| s.to_owned()) - .map_err(|_| PKError::Utf8Error) + .map_err(|e| { + set_last_error(format!("invalid UTF-8 in C string: {e}")); + PKStatus::Utf8Error + }) } From e5409b52ff935ede3d8b9fa648fa9aae4b87810b Mon Sep 17 00:00:00 2001 From: Aditya Bisht Date: Mon, 30 Mar 2026 11:53:33 -0700 Subject: [PATCH 09/12] fix: keep staticlib only in provekit-ffi, use cargo rustc --crate-type cdylib for Android --- tooling/provekit-ffi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index 439861614..7223ec77c 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true repository.workspace = true [lib] -crate-type = ["staticlib", "cdylib"] +crate-type = ["staticlib"] [dependencies] # Workspace crates From 8bfd0c0d5303bc1d136d7e1ef7d9f91b1295c5b4 Mon Sep 17 00:00:00 2001 From: ashpect Date: Tue, 31 Mar 2026 00:36:35 +0530 Subject: [PATCH 10/12] cleanup: cargo fmt --- tooling/provekit-ffi/TEST_PLAN.md | 75 +++++++++++++++++++++++++++++++ tooling/provekit-ffi/src/ffi.rs | 6 ++- 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tooling/provekit-ffi/TEST_PLAN.md diff --git a/tooling/provekit-ffi/TEST_PLAN.md b/tooling/provekit-ffi/TEST_PLAN.md new file mode 100644 index 000000000..7d72b0ec6 --- /dev/null +++ b/tooling/provekit-ffi/TEST_PLAN.md @@ -0,0 +1,75 @@ +# provekit-ffi Test Plan + +## Context + +PR #384 rewrites provekit-ffi from file-path API to handle-based API (PKProver/PKVerifier). Review comments from Bisht13 have been addressed (PKError->PKStatus rename, file::serialize for proofs, Prover::abi(), hash_config param, pk_get_last_error, etc). Changes are unstaged on branch `ash/sdk`. + +There are currently ZERO tests for FFI functions. Only 2 unit tests exist for mmap_allocator. + +## What to test + +### 1. Error reporting (`pk_get_last_error`) +- Call an FFI function that fails (e.g., pk_load_prover with bad path) +- Verify pk_get_last_error returns a non-empty UTF-8 message +- Verify calling pk_get_last_error again returns empty (clears on read) +- Verify c_str_to_str null pointer sets error message +- Verify c_str_to_str invalid UTF-8 sets error message + +### 2. Prepare flow (`pk_prepare`) +- Compile a noir circuit (use `noir-examples/basic-2/target/basic_2.json`) +- Verify returns Success +- Verify out_prover and out_verifier are non-null +- Test invalid hash_config (e.g., 99) returns InvalidInput with error message +- Test null circuit_path returns error +- Free handles after + +### 3. Prove + Verify round-trip +- pk_prepare or pk_load_prover from basic-2 +- pk_prove_toml with basic-2 inputs -> get proof bytes +- pk_verify with matching verifier + proof bytes -> expect Success +- pk_verify with corrupted proof bytes -> expect ProofError +- Verify proof bytes are valid .np format (start with magic bytes) + +### 4. pk_prove_json +- Same as above but with JSON inputs: `{"x":"5","y":"10"}` (check basic-2 ABI) +- Verify Mavros provers aren't silently rejected (if testable) + +### 5. Serialize/Deserialize round-trip +- pk_prepare -> pk_serialize_prover -> pk_load_prover_bytes -> prove +- Verify the re-loaded prover produces valid proofs +- Same for verifier: pk_serialize_verifier -> pk_load_verifier_bytes -> verify + +### 6. Save/Load file round-trip +- pk_prepare -> pk_save_prover to temp file -> pk_load_prover from file +- Verify loaded prover works + +### 7. PKStatus codes +- Verify each error path returns the expected status code +- Null pointer args -> InvalidInput +- Bad file path -> SchemeReadError +- Invalid proof -> ProofError + +### 8. Cleanup +- pk_free_prover/pk_free_verifier/pk_free_buf don't crash +- Double-free protection (null after free) + +## How to test + +Write tests in `tooling/provekit-ffi/tests/ffi_integration.rs`. Call FFI functions directly from Rust (they're `unsafe extern "C"` but callable from Rust). Use `noir-examples/basic-2` as the test circuit — it's small and already has input files. + +The test file needs: +```rust +use provekit_ffi::{pk_init, pk_prepare, pk_prove_toml, pk_verify, ...}; +``` + +Call `pk_init()` once in a test setup. Use `std::ffi::CString` for C string args. Use temp dirs for file I/O tests. + +## Important notes + +- Branch: `ash/sdk` +- All review changes are unstaged (NOT committed) — check `git diff` to see current state +- The enum is now `PKStatus` (not `PKError`) +- `pk_prepare` now takes `hash_config: c_int` as second param (0=Skyscraper) +- Proofs use `file::serialize` format (standard .np binary), not raw postcard +- `postcard` was removed from FFI Cargo.toml deps +- Test circuit: `noir-examples/basic-2` — may need `cargo run -p provekit -- compile` first if json artifact doesn't exist diff --git a/tooling/provekit-ffi/src/ffi.rs b/tooling/provekit-ffi/src/ffi.rs index c0687cc75..fea67c613 100644 --- a/tooling/provekit-ffi/src/ffi.rs +++ b/tooling/provekit-ffi/src/ffi.rs @@ -318,7 +318,8 @@ pub unsafe extern "C" fn pk_load_prover_bytes( *out = std::ptr::null_mut(); let result = (|| -> Result<*mut PKProver, PKStatus> { - // SAFETY: ptr/len validity is guaranteed by the caller (documented in # Safety). + // SAFETY: ptr/len validity is guaranteed by the caller (documented in # + // Safety). let data = std::slice::from_raw_parts(ptr, len); let prover: Prover = file::deserialize(data).map_err(|e| { set_last_error(format!("{e:#}")); @@ -359,7 +360,8 @@ pub unsafe extern "C" fn pk_load_verifier_bytes( *out = std::ptr::null_mut(); let result = (|| -> Result<*mut PKVerifier, PKStatus> { - // SAFETY: ptr/len validity is guaranteed by the caller (documented in # Safety). + // SAFETY: ptr/len validity is guaranteed by the caller (documented in # + // Safety). let data = std::slice::from_raw_parts(ptr, len); let verifier: Verifier = file::deserialize(data).map_err(|e| { set_last_error(format!("{e:#}")); From be2fc0b7358fdaec271e053c99c2225a63dd70be Mon Sep 17 00:00:00 2001 From: ashpect Date: Tue, 31 Mar 2026 06:25:39 +0530 Subject: [PATCH 11/12] chore: postcard deps --- Cargo.toml | 1 + tooling/provekit-ffi/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7259f6558..bc53458e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "ansi"] } tracing-tracy = "=0.11.4" tracy-client = "=0.18.0" tracy-client-sys = "=0.24.3" +parking_lot = "0.12" xz2 = "0.1.7" zerocopy = "0.8.25" zeroize = "1.8.1" diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index 7223ec77c..3b78fd0d4 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -21,7 +21,7 @@ provekit-verifier = { workspace = true } # 3rd party anyhow.workspace = true noirc_abi.workspace = true -parking_lot = "0.12" +parking_lot.workspace = true [target.'cfg(unix)'.dependencies] libc = "0.2" From 18f6515cc10812d66ed5eaad4abba9b327f6ede0 Mon Sep 17 00:00:00 2001 From: ashpect Date: Tue, 31 Mar 2026 07:36:48 +0530 Subject: [PATCH 12/12] chore: cleanup --- Cargo.toml | 1 - tooling/provekit-ffi/TEST_PLAN.md | 75 ------------------------------- 2 files changed, 76 deletions(-) delete mode 100644 tooling/provekit-ffi/TEST_PLAN.md diff --git a/Cargo.toml b/Cargo.toml index bc53458e3..55413bdff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,6 @@ missing_docs_in_private_items = { level = "allow", priority = 1 } missing_safety_doc = { level = "deny", priority = 1 } [profile.release] -debug = true # Generate symbol info for profiling opt-level = 3 codegen-units = 1 lto = "fat" diff --git a/tooling/provekit-ffi/TEST_PLAN.md b/tooling/provekit-ffi/TEST_PLAN.md deleted file mode 100644 index 7d72b0ec6..000000000 --- a/tooling/provekit-ffi/TEST_PLAN.md +++ /dev/null @@ -1,75 +0,0 @@ -# provekit-ffi Test Plan - -## Context - -PR #384 rewrites provekit-ffi from file-path API to handle-based API (PKProver/PKVerifier). Review comments from Bisht13 have been addressed (PKError->PKStatus rename, file::serialize for proofs, Prover::abi(), hash_config param, pk_get_last_error, etc). Changes are unstaged on branch `ash/sdk`. - -There are currently ZERO tests for FFI functions. Only 2 unit tests exist for mmap_allocator. - -## What to test - -### 1. Error reporting (`pk_get_last_error`) -- Call an FFI function that fails (e.g., pk_load_prover with bad path) -- Verify pk_get_last_error returns a non-empty UTF-8 message -- Verify calling pk_get_last_error again returns empty (clears on read) -- Verify c_str_to_str null pointer sets error message -- Verify c_str_to_str invalid UTF-8 sets error message - -### 2. Prepare flow (`pk_prepare`) -- Compile a noir circuit (use `noir-examples/basic-2/target/basic_2.json`) -- Verify returns Success -- Verify out_prover and out_verifier are non-null -- Test invalid hash_config (e.g., 99) returns InvalidInput with error message -- Test null circuit_path returns error -- Free handles after - -### 3. Prove + Verify round-trip -- pk_prepare or pk_load_prover from basic-2 -- pk_prove_toml with basic-2 inputs -> get proof bytes -- pk_verify with matching verifier + proof bytes -> expect Success -- pk_verify with corrupted proof bytes -> expect ProofError -- Verify proof bytes are valid .np format (start with magic bytes) - -### 4. pk_prove_json -- Same as above but with JSON inputs: `{"x":"5","y":"10"}` (check basic-2 ABI) -- Verify Mavros provers aren't silently rejected (if testable) - -### 5. Serialize/Deserialize round-trip -- pk_prepare -> pk_serialize_prover -> pk_load_prover_bytes -> prove -- Verify the re-loaded prover produces valid proofs -- Same for verifier: pk_serialize_verifier -> pk_load_verifier_bytes -> verify - -### 6. Save/Load file round-trip -- pk_prepare -> pk_save_prover to temp file -> pk_load_prover from file -- Verify loaded prover works - -### 7. PKStatus codes -- Verify each error path returns the expected status code -- Null pointer args -> InvalidInput -- Bad file path -> SchemeReadError -- Invalid proof -> ProofError - -### 8. Cleanup -- pk_free_prover/pk_free_verifier/pk_free_buf don't crash -- Double-free protection (null after free) - -## How to test - -Write tests in `tooling/provekit-ffi/tests/ffi_integration.rs`. Call FFI functions directly from Rust (they're `unsafe extern "C"` but callable from Rust). Use `noir-examples/basic-2` as the test circuit — it's small and already has input files. - -The test file needs: -```rust -use provekit_ffi::{pk_init, pk_prepare, pk_prove_toml, pk_verify, ...}; -``` - -Call `pk_init()` once in a test setup. Use `std::ffi::CString` for C string args. Use temp dirs for file I/O tests. - -## Important notes - -- Branch: `ash/sdk` -- All review changes are unstaged (NOT committed) — check `git diff` to see current state -- The enum is now `PKStatus` (not `PKError`) -- `pk_prepare` now takes `hash_config: c_int` as second param (0=Skyscraper) -- Proofs use `file::serialize` format (standard .np binary), not raw postcard -- `postcard` was removed from FFI Cargo.toml deps -- Test circuit: `noir-examples/basic-2` — may need `cargo run -p provekit -- compile` first if json artifact doesn't exist