initial commit

This commit is contained in:
Dmitriy Ryajov 2024-01-19 12:43:40 -06:00
commit 5a522239a6
No known key found for this signature in database
GPG Key ID: DA8C680CE7C657A4
10 changed files with 487 additions and 0 deletions

24
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Cargo Build & Test
on:
push:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Rust project - latest
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v3
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
.vscode/

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "circom-compat-ffi"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ark-circom = { git = "https://github.com/codex-storage/circom-compat.git" }
ark-crypto-primitives = { version = "=0.4.0" }
ark-ec = { version = "=0.4.1", default-features = false, features = ["parallel"] }
ark-ff = { version = "=0.4.1", default-features = false, features = ["parallel", "asm"] }
ark-std = { version = "=0.4.0", default-features = false, features = ["parallel"] }
ark-bn254 = { version = "=0.4.0" }
ark-groth16 = { version = "=0.4.0", default-features = false, features = ["parallel"] }
ark-poly = { version = "=0.4.1", default-features = false, features = ["parallel"] }
ark-relations = { version = "=0.4.0", default-features = false }
ark-serialize = { version = "=0.4.1", default-features = false }
ruint = { version = "1.7.0", features = ["serde", "num-bigint", "ark-ff"] }

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Codex Storage
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# circom-compat-ffi
circom-compat (ark-circom) ffi

32
cbindgen.toml Normal file
View File

@ -0,0 +1,32 @@
[parse]
parse_deps = false
expand = ["rust-circom-compat-ffi"]
# Configuration for name mangling
[export.mangle]
# Whether the types should be renamed during mangling, for example
# c_char -> CChar, etc.
rename_types = "CamelCase"
# Whether the underscores from the mangled name should be omitted.
remove_underscores = true
[struct]
# A rule to use to rename struct field names. The renaming assumes the input is
# the Rust standard snake_case, however it acccepts all the different rename_args
# inputs. This means many options here are no-ops or redundant.
#
# possible values (that actually do something):
# * "CamelCase": my_arg => myArg
# * "PascalCase": my_arg => MyArg
# * "GeckoCase": my_arg => mMyArg
# * "ScreamingSnakeCase": my_arg => MY_ARG
# * "None": apply no renaming
#
# technically possible values (that shouldn't have a purpose here):
# * "SnakeCase": apply no renaming
# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?)
# * "UpperCase": same as ScreamingSnakeCase in this context
# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context
#
# default: "None"
rename_fields = "CamelCase"

BIN
fixtures/mycircuit.r1cs Normal file

Binary file not shown.

BIN
fixtures/mycircuit.wasm Normal file

Binary file not shown.

370
src/ffi.rs Normal file
View File

@ -0,0 +1,370 @@
use std::{
any::Any,
ffi::{c_char, CStr},
fs::File,
os::raw::c_void,
panic::{catch_unwind, AssertUnwindSafe},
};
use ark_bn254::{Bn254, Fr};
use ark_circom::{read_zkey, CircomBuilder, CircomConfig};
use ark_crypto_primitives::snark::SNARK;
use ark_groth16::{prepare_verifying_key, Groth16, Proof, ProvingKey};
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use ark_std::rand::{rngs::ThreadRng, thread_rng};
use ruint::aliases::U256;
type GrothBn = Groth16<Bn254>;
pub const ERR_UNKNOWN: i32 = -1;
pub const ERR_OK: i32 = 0;
pub const ERR_WASM_PATH: i32 = 1;
pub const ERR_R1CS_PATH: i32 = 2;
pub const ERR_ZKEY_PATH: i32 = 3;
pub const ERR_INPUT_NAME: i32 = 4;
pub const ERR_INVALID_INPUT: i32 = 5;
pub const ERR_CANT_READ_ZKEY: i32 = 6;
pub const ERR_CIRCOM_BUILDER: i32 = 7;
pub const ERR_FAILED_TO_DESERIALIZE_PROOF: i32 = 8;
pub const ERR_FAILED_TO_DESERIALIZE_INPUTS: i32 = 9;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Buffer {
data: *const u8,
len: usize,
}
#[derive(Debug, Clone)]
// #[repr(C)]
struct CircomBn254 {
builder: *mut CircomBuilder<Bn254>,
proving_key: *mut ProvingKey<Bn254>,
_marker: core::marker::PhantomData<(*mut CircomBn254, core::marker::PhantomPinned)>,
}
#[derive(Debug, Clone)]
struct CircomCompatCtx {
circom: *mut c_void,
rng: ThreadRng,
_marker: core::marker::PhantomData<(*mut CircomCompatCtx, core::marker::PhantomPinned)>,
}
fn to_err_code(result: Result<(), Box<dyn Any + Send>>) -> i32 {
match result {
Ok(_) => ERR_OK,
Err(e) => match e.downcast_ref::<i32>() {
Some(e) => *e,
None => ERR_UNKNOWN,
},
}
}
/// # Safety
///
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn init_circom_compat(
r1cs_path: *const c_char,
wasm_path: *const c_char,
zkey_path: *const c_char,
ctx_ptr: &mut *mut CircomCompatCtx,
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let mut rng = thread_rng(); // TODO: use a shared rng - how?
let builder = CircomBuilder::new(
CircomConfig::<Bn254>::new(
CStr::from_ptr(wasm_path)
.to_str()
.map_err(|_| ERR_WASM_PATH)
.unwrap(),
CStr::from_ptr(r1cs_path)
.to_str()
.map_err(|_| ERR_R1CS_PATH)
.unwrap(),
)
.map_err(|_| ERR_CIRCOM_BUILDER)
.unwrap(),
);
let proving_key = if !zkey_path.is_null() {
let mut file = File::open(
CStr::from_ptr(zkey_path)
.to_str()
.map_err(|_| ERR_ZKEY_PATH)
.unwrap(),
)
.unwrap();
read_zkey(&mut file)
.map_err(|_| ERR_CANT_READ_ZKEY)
.unwrap()
.0
} else {
Groth16::<Bn254>::generate_random_parameters_with_reduction::<_>(
builder.setup(),
&mut rng,
)
.map_err(|_| ERR_UNKNOWN)
.unwrap()
};
let circom_bn254 = CircomBn254 {
builder: Box::into_raw(Box::new(builder)),
proving_key: Box::into_raw(Box::new(proving_key)),
_marker: core::marker::PhantomData,
};
let circom_compat_ctx = CircomCompatCtx {
circom: Box::into_raw(Box::new(circom_bn254)) as *mut c_void,
rng: rng,
_marker: core::marker::PhantomData,
};
*ctx_ptr = Box::into_raw(Box::new(circom_compat_ctx));
}));
to_err_code(result)
}
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn release_circom_compat(ctx_ptr: &mut *mut CircomCompatCtx) {
if !ctx_ptr.is_null() {
let ctx = &mut Box::from_raw(*ctx_ptr);
if !ctx.circom.is_null() {
let circom = &mut Box::from_raw(ctx.circom as *mut CircomBn254);
let _ = Box::from_raw(circom.builder);
let _ = Box::from_raw(circom.proving_key);
if !circom.builder.is_null() {
circom.builder = std::ptr::null_mut()
};
if !circom.proving_key.is_null() {
circom.proving_key = std::ptr::null_mut()
};
ctx.circom = std::ptr::null_mut();
*ctx_ptr = std::ptr::null_mut();
}
}
}
pub unsafe extern "C" fn release_buffer(buff_ptr: &mut *mut Buffer) {
if !buff_ptr.is_null() {
let buff = &mut Box::from_raw(*buff_ptr);
let _ = Box::from_raw(buff.data as *mut u8);
buff.data = std::ptr::null_mut();
buff.len = 0;
*buff_ptr = std::ptr::null_mut();
}
}
unsafe fn to_circom(ctx_ptr: *mut CircomCompatCtx) -> *mut CircomBn254 {
(*ctx_ptr).circom as *mut CircomBn254
}
/// # Safety
///
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn push_input_u256_array(
ctx_ptr: *mut CircomCompatCtx,
name_ptr: *const c_char,
input_ptr: *const u8,
len: usize,
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let name = CStr::from_ptr(name_ptr)
.to_str()
.map_err(|_| ERR_INPUT_NAME)
.unwrap();
let input = {
let slice = std::slice::from_raw_parts(input_ptr, len);
slice
.chunks(U256::BYTES)
.map(|c| U256::try_from_le_slice(c).ok_or(ERR_INVALID_INPUT).unwrap())
.collect::<Vec<U256>>()
};
let circom = &mut *to_circom(ctx_ptr);
input
.iter()
.for_each(|c| (*circom.builder).push_input(name, *c));
}));
to_err_code(result)
}
/// # Safety
///
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn prove_circuit(
ctx_ptr: *mut CircomCompatCtx,
proof_bytes_ptr: &mut *mut Buffer,
inputs_bytes_ptr: &mut *mut Buffer,
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let circom = &mut *to_circom(ctx_ptr);
let proving_key = &(*circom.proving_key);
let rng = &mut (*ctx_ptr).rng;
let circuit = (*circom.builder).clone().build().unwrap();
let inputs = circuit.get_public_inputs().unwrap();
let proof = GrothBn::prove(&proving_key, circuit, rng).unwrap();
let mut proof_bytes = Vec::new();
proof.serialize_compressed(&mut proof_bytes).unwrap();
let mut public_inputs_bytes = Vec::new();
inputs
.serialize_compressed(&mut public_inputs_bytes)
.unwrap();
// leak the buffers to avoid rust from freeing the pointed to data,
// clone to avoid bytes from being freed
let proof_slice = Box::leak(Box::new(proof_bytes.clone())).as_slice();
let proof_buff = Buffer {
data: proof_slice.as_ptr() as *const u8,
len: proof_bytes.len(),
};
// leak the buffers to avoid rust from freeing the pointed to data,
// clone to avoid bytes from being freed
let input_slice = Box::leak(Box::new(public_inputs_bytes.clone())).as_slice();
let input_buff = Buffer {
data: input_slice.as_ptr() as *const u8,
len: public_inputs_bytes.len(),
};
*proof_bytes_ptr = Box::into_raw(Box::new(proof_buff));
*inputs_bytes_ptr = Box::into_raw(Box::new(input_buff));
}));
to_err_code(result)
}
/// # Safety
///
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn verify_circuit(
ctx_ptr: *mut CircomCompatCtx,
proof_bytes_ptr: *const Buffer,
inputs_bytes_ptr: *const Buffer,
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let proof_bytes =
std::slice::from_raw_parts((*proof_bytes_ptr).data, (*proof_bytes_ptr).len);
let proof = Proof::<Bn254>::deserialize_compressed(proof_bytes)
.map_err(|_| ERR_FAILED_TO_DESERIALIZE_PROOF)
.unwrap();
let public_inputs_bytes =
std::slice::from_raw_parts((*inputs_bytes_ptr).data, (*inputs_bytes_ptr).len);
let public_inputs: Vec<Fr> =
CanonicalDeserialize::deserialize_compressed(public_inputs_bytes)
.map_err(|_| ERR_FAILED_TO_DESERIALIZE_INPUTS)
.unwrap();
let circom = &mut *to_circom(ctx_ptr);
let proving_key = &(*circom.proving_key);
let pvk = prepare_verifying_key(&proving_key.vk);
GrothBn::verify_proof(&pvk, &proof, &public_inputs)
.map_err(|e| e.to_string())
.unwrap();
}));
to_err_code(result)
}
macro_rules! build_fn
{
($name:tt, $($v:ident: $t:ty),*) => {
#[no_mangle]
#[allow(private_interfaces)]
pub unsafe extern "C" fn $name(
ctx_ptr: *mut CircomCompatCtx,
name_ptr: *const c_char,
input: $($t),*
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let name = CStr::from_ptr(name_ptr).to_str().unwrap();
let input = U256::from(input);
let circom = &mut *to_circom(ctx_ptr);
(*circom.builder).push_input(name, input);
}));
to_err_code(result)
}
};
}
build_fn!(push_input_numeric_i8, x: i8);
build_fn!(push_input_numeric_u8, x: u8);
build_fn!(push_input_numeric_i16, x: i16);
build_fn!(push_input_numeric_u16, x: u16);
build_fn!(push_input_numeric_i32, x: i32);
build_fn!(push_input_numeric_u32, x: u32);
build_fn!(push_input_numeric_u64, x: u64);
#[cfg(test)]
mod test {
use std::ffi::CString;
use super::*;
#[test]
fn groth16_proof() {
let r1cs_path = CString::new("./fixtures/mycircuit.r1cs".as_bytes()).unwrap();
let wasm_path = CString::new("./fixtures/mycircuit.wasm".as_bytes()).unwrap();
unsafe {
let mut ctx_ptr: *mut CircomCompatCtx = std::ptr::null_mut();
init_circom_compat(
r1cs_path.as_ptr(),
wasm_path.as_ptr(),
std::ptr::null(),
&mut ctx_ptr,
);
assert!(ctx_ptr != std::ptr::null_mut());
let a = CString::new("a".as_bytes()).unwrap();
push_input_numeric_i8(ctx_ptr, a.as_ptr(), 3);
let b = CString::new("b".as_bytes()).unwrap();
push_input_numeric_i8(ctx_ptr, b.as_ptr(), 3);
let mut proof_bytes_ptr: *mut Buffer = std::ptr::null_mut();
let mut inputs_bytes_ptr: *mut Buffer = std::ptr::null_mut();
assert!(prove_circuit(ctx_ptr, &mut proof_bytes_ptr, &mut inputs_bytes_ptr) == ERR_OK);
assert!(proof_bytes_ptr != std::ptr::null_mut());
assert!((*proof_bytes_ptr).data != std::ptr::null());
assert!((*proof_bytes_ptr).len > 0);
assert!(inputs_bytes_ptr != std::ptr::null_mut());
assert!((*inputs_bytes_ptr).data != std::ptr::null());
assert!((*inputs_bytes_ptr).len > 0);
assert!(verify_circuit(ctx_ptr, &(*proof_bytes_ptr), &(*inputs_bytes_ptr)) == ERR_OK);
release_buffer(&mut proof_bytes_ptr);
release_buffer(&mut inputs_bytes_ptr);
release_circom_compat(&mut ctx_ptr);
assert!(ctx_ptr == std::ptr::null_mut());
assert!(proof_bytes_ptr == std::ptr::null_mut());
assert!(inputs_bytes_ptr == std::ptr::null_mut());
};
}
}

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod ffi;