diff --git a/Cargo.lock b/Cargo.lock index fc5371a21..37318a25d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4590,10 +4590,12 @@ version = "0.1.0" dependencies = [ "anyhow", "libc", + "noirc_abi", "parking_lot", "provekit-common", "provekit-prover", - "serde_json", + "provekit-r1cs-compiler", + "provekit-verifier", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c73da926a..55413bdff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,11 +61,15 @@ 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" +# 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" @@ -143,6 +147,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/provekit/common/src/file/io/bin.rs b/provekit/common/src/file/io/bin.rs index cb5ff2f48..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(()) } @@ -156,6 +145,96 @@ 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 = 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) + 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 { + 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); + 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 { 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, + } + } } 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 e0d1fe318..3b78fd0d4 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -15,17 +15,16 @@ 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 -serde_json.workspace = true -parking_lot = "0.12" +noirc_abi.workspace = true +parking_lot.workspace = true [target.'cfg(unix)'.dependencies] 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..fea67c613 100644 --- a/tooling/provekit-ffi/src/ffi.rs +++ b/tooling/provekit-ffi/src/ffi.rs @@ -1,247 +1,750 @@ -//! Main FFI functions for ProveKit. +//! Handle-based FFI functions for ProveKit. +//! +//! All functions use opaque `PKProver` / `PKVerifier` handles instead of file +//! 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}, + types::{PKBuf, PKProver, PKStatus, 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::{ + 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 + } + } } -/// Prove a Noir program and write the proof to a file. +/// Get the error message from the most recent failing FFI call. /// -/// # Arguments +/// 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`. /// -/// * `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) +/// # 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. +/// +/// Must be called once before using any other ProveKit functions. +#[no_mangle] +pub extern "C" fn pk_init() -> c_int { + provekit_common::register_ntt(); + PKStatus::Success.into() +} + +/// Configure the mmap-based memory allocator. +/// +/// Optional. If called, MUST be invoked before `pk_init()` and before any +/// allocations occur. +/// +/// # Safety +/// +/// `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 PKStatus::InvalidInput.into(); + } + + if crate::mmap_allocator::configure_allocator(ram_limit_bytes, use_file_backed, swap_file_path) + { + PKStatus::Success.into() + } else { + set_last_error("memory allocator configuration failed".into()); + PKStatus::InvalidInput.into() + } +} + +/// Get current memory statistics. +/// +/// # Safety +/// +/// All non-NULL pointers must be valid. +#[no_mangle] +pub unsafe extern "C" fn pk_get_memory_stats( + ram_used: *mut usize, + swap_used: *mut usize, + peak_ram: *mut usize, +) -> c_int { + 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() { + *swap_used = swap; + } + if !peak_ram.is_null() { + *peak_ram = peak; + } + + PKStatus::Success.into() +} + +// --------------------------------------------------------------------------- +// Prepare +// --------------------------------------------------------------------------- + +/// Compile a Noir circuit into prover and verifier handles. /// -/// # Returns +/// `hash_config` selects the hash algorithm: 0 = Skyscraper (default), +/// 1 = SHA-256, 2 = Keccak, 3 = Blake3. /// -/// Returns `PKError::Success` on success, or an appropriate error code on -/// failure. +/// 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 /// -/// The caller must ensure that all path parameters are valid null-terminated C -/// strings. +/// - `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_prove_to_file( - prover_path: *const c_char, - input_path: *const c_char, - out_path: *const c_char, +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 { - 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)?; + if out_prover.is_null() || out_verifier.is_null() { + return PKStatus::InvalidInput.into(); + } - let prover: Prover = - read(Path::new(&prover_path)).map_err(|_| PKError::SchemeReadError)?; + 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 proof = prover - .prove_with_toml(&input_path) - .map_err(|_| PKError::ProofError)?; + let result = (|| -> Result<(*mut PKProver, *mut PKVerifier), PKStatus> { + let circuit_path = c_str_to_str(circuit_path)?; - provekit_common::file::write(&proof, Path::new(&out_path)) - .map_err(|_| PKError::FileWriteError)?; + 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 + })?; - Ok(()) + 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::new(PKProver { prover }); + let vk = Box::new(PKVerifier { verifier }); + + Ok((Box::into_raw(pk), Box::into_raw(vk))) })(); match result { - Ok(()) => PKError::Success.into(), - Err(error) => error.into(), + Ok((pk, vk)) => { + *out_prover = pk; + *out_verifier = vk; + PKStatus::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. +// --------------------------------------------------------------------------- +// Load (from file path) +// --------------------------------------------------------------------------- + +/// Load a prover scheme from a `.pkp` file. /// -/// # Arguments +/// # Safety /// -/// * `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 +/// - `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 PKStatus::InvalidInput.into(); + } + + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. + *out = std::ptr::null_mut(); + + let result = (|| -> Result<*mut PKProver, PKStatus> { + let path = c_str_to_str(path)?; + 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; + PKStatus::Success.into() + } + Err(e) => e.into(), + } + }) +} + +/// Load a verifier scheme from a `.pkv` 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_verifier`. +#[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 PKStatus::InvalidInput.into(); + } + + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. + *out = std::ptr::null_mut(); + + let result = (|| -> Result<*mut PKVerifier, PKStatus> { + let path = c_str_to_str(path)?; + 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; + PKStatus::Success.into() + } + Err(e) => e.into(), + } + }) +} + +// --------------------------------------------------------------------------- +// Load (from bytes) +// --------------------------------------------------------------------------- + +/// Load a prover scheme from bytes (same format as `.pkp` files). /// /// # 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` +/// - `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_prove_to_json( - prover_path: *const c_char, - input_path: *const c_char, - out_buf: *mut PKBuf, +pub unsafe extern "C" fn pk_load_prover_bytes( + ptr: *const u8, + len: usize, + out: *mut *mut PKProver, ) -> c_int { - if out_buf.is_null() { - return PKError::InvalidInput.into(); + if out.is_null() || ptr.is_null() || len == 0 { + return PKStatus::InvalidInput.into(); } - catch_panic(PKError::ProofError.into(), || { - // Safety: out_buf is guaranteed non-null by the check above - let out_buf = &mut *out_buf; - - *out_buf = PKBuf::empty(); + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. + *out = std::ptr::null_mut(); - let result = (|| -> Result, PKError> { - let prover_path = c_str_to_str(prover_path)?; - let input_path = c_str_to_str(input_path)?; + 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(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; + Ok(Box::into_raw(Box::new(PKProver { prover }))) + })(); - let prover: Prover = - read(Path::new(&prover_path)).map_err(|_| PKError::SchemeReadError)?; + match result { + Ok(handle) => { + *out = handle; + PKStatus::Success.into() + } + Err(e) => e.into(), + } + }) +} - let proof = prover - .prove_with_toml(&input_path) - .map_err(|_| PKError::ProofError)?; +/// Load a verifier scheme from bytes (same format as `.pkv` 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_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 PKStatus::InvalidInput.into(); + } - let json_string = - serde_json::to_string(&proof).map_err(|_| PKError::SerializationError)?; + catch_panic(PKStatus::SchemeReadError.into(), || { + // SAFETY: out is guaranteed non-null above. + *out = std::ptr::null_mut(); - Ok(json_string.into_bytes()) + 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(|e| { + set_last_error(format!("{e:#}")); + PKStatus::SchemeReadError + })?; + Ok(Box::into_raw(Box::new(PKVerifier { verifier }))) })(); match result { - Ok(json_bytes) => { - *out_buf = PKBuf::from_vec(json_bytes); - PKError::Success.into() + Ok(handle) => { + *out = handle; + PKStatus::Success.into() } - Err(error) => error.into(), + Err(e) => e.into(), } }) } -/// Free a buffer allocated by ProveKit FFI functions. +// --------------------------------------------------------------------------- +// Save (to file path) +// --------------------------------------------------------------------------- + +/// Save a prover scheme to a `.pkp` file. /// -/// # Arguments +/// # Safety /// -/// * `buf` - The buffer to free +/// - `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_save_prover(prover: *const PKProver, path: *const c_char) -> c_int { + if prover.is_null() { + return PKStatus::InvalidInput.into(); + } + + catch_panic(PKStatus::FileWriteError.into(), || { + let result = (|| -> Result<(), PKStatus> { + let path = c_str_to_str(path)?; + // 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(()) => PKStatus::Success.into(), + Err(e) => e.into(), + } + }) +} + +/// Save a verifier scheme to a `.pkv` 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 +/// - `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 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_verifier( + verifier: *const PKVerifier, + path: *const c_char, +) -> c_int { + if verifier.is_null() { + return PKStatus::InvalidInput.into(); } + + catch_panic(PKStatus::FileWriteError.into(), || { + let result = (|| -> Result<(), PKStatus> { + let path = c_str_to_str(path)?; + // 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(()) => PKStatus::Success.into(), + Err(e) => e.into(), + } + }) } -/// Initialize the ProveKit library. +// --------------------------------------------------------------------------- +// Serialize (to bytes) +// --------------------------------------------------------------------------- + +/// Serialize a prover scheme to bytes (same format as `.pkp` files). /// -/// This function should be called once before using any other ProveKit -/// functions. It sets up logging and other global state. +/// # Safety /// -/// # Returns +/// - `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 PKStatus::InvalidInput.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); + PKStatus::Success.into() + } + Err(e) => { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError.into() + } + } + }) +} + +/// Serialize a verifier scheme to bytes (same format as `.pkv` files). +/// +/// # Safety /// -/// Returns `PKError::Success` on success. +/// - `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 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_serialize_verifier( + verifier: *const PKVerifier, + out_buf: *mut PKBuf, +) -> c_int { + if verifier.is_null() || out_buf.is_null() { + return PKStatus::InvalidInput.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); + PKStatus::Success.into() + } + Err(e) => { + set_last_error(format!("{e:#}")); + PKStatus::SerializationError.into() + } + } + }) } -/// Configure the mmap-based memory allocator. +// --------------------------------------------------------------------------- +// Prove +// --------------------------------------------------------------------------- + +/// Prove using a prover handle and a TOML input file. /// -/// MUST be called before pk_init() and before any allocations occur. +/// Returns proof bytes in `out_proof`. The caller must free the buffer via +/// `pk_free_buf`. +/// +/// # Safety /// -/// # Arguments +/// - `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_prove_toml( + prover: *const PKProver, + toml_path: *const c_char, + out_proof: *mut PKBuf, +) -> c_int { + if prover.is_null() || out_proof.is_null() { + return PKStatus::InvalidInput.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, 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(|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); + PKStatus::Success.into() + } + Err(e) => e.into(), + } + }) +} + +/// Prove using a prover handle and a JSON string of inputs. /// -/// * `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) +/// The JSON must match the circuit's ABI. Example: +/// `{"x": "5", "y": "10"}` for `fn main(x: Field, y: Field)`. /// -/// # Returns +/// Returns proof bytes in `out_proof` using the standard `.np` binary format. +/// The caller must free the buffer via `pk_free_buf`. /// -/// Returns `PKError::Success` or `PKError::InvalidInput` if ram_limit_bytes is -/// 0. +/// Note: internally clones the full prover scheme per call since `prove()` +/// consumes `self`. This may be significant for large circuits on constrained +/// devices. /// /// # 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. +/// - `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_configure_memory( - ram_limit_bytes: usize, - use_file_backed: bool, - swap_file_path: *const c_char, +pub unsafe extern "C" fn pk_prove_json( + prover: *const PKProver, + inputs_json: *const c_char, + out_proof: *mut PKBuf, ) -> c_int { - if ram_limit_bytes == 0 { - return PKError::InvalidInput.into(); + if prover.is_null() || out_proof.is_null() { + return PKStatus::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(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, PKStatus> { + let json_str = c_str_to_str(inputs_json)?; + + // SAFETY: prover is guaranteed non-null and valid by caller contract. + let abi = (*prover).prover.abi(); + + 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(|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); + PKStatus::Success.into() + } + Err(e) => e.into(), + } + }) } -/// Get current memory statistics. -/// -/// # Arguments +// --------------------------------------------------------------------------- +// Verify +// --------------------------------------------------------------------------- + +/// Verify a proof using a verifier handle. /// -/// * `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) +/// Expects proof bytes in the standard `.np` binary format (as returned by +/// `pk_prove_toml` / `pk_prove_json`). /// -/// # Returns +/// Returns `PKStatus::Success` (0) if valid, `PKStatus::ProofError` (4) if +/// invalid. /// -/// Returns `PKError::Success`. +/// 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 /// -/// 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 PKStatus::InvalidInput.into(); } - if !swap_used.is_null() { - *swap_used = swap; + + 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 = 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(e) => { + set_last_error(format!("{e:#}")); + Ok(false) + } + } + })(); + + match result { + Ok(true) => PKStatus::Success.into(), + Ok(false) => PKStatus::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() { + // SAFETY: caller guarantees this is a valid, non-freed handle. + 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() { + // SAFETY: caller guarantees this is a valid, non-freed handle. + 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 { + // 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 e5babc9dd..d41f0b95b 100644 --- a/tooling/provekit-ffi/src/lib.rs +++ b/tooling/provekit-ffi/src/lib.rs @@ -1,22 +1,26 @@ //! 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(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. 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 a772d86be..c110df6d2 100644 --- a/tooling/provekit-ffi/src/types.rs +++ b/tooling/provekit-ffi/src/types.rs @@ -1,10 +1,14 @@ //! 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`. #[repr(C)] +#[must_use = "this buffer must be freed with pk_free_buf"] pub struct PKBuf { /// Pointer to the data pub ptr: *mut u8, @@ -34,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.) @@ -46,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, @@ -54,10 +58,61 @@ pub enum PKError { Utf8Error = 6, /// File write error FileWriteError = 7, + /// Circuit compilation error + 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 + } +} + +/// 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, +} + +// 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 + }) }