witnesscalc_adapter/
lib.rs

1pub use paste;
2pub use serde_json;
3use std::{
4    env, fs,
5    path::{Path, PathBuf},
6    process::Command,
7};
8
9pub mod convert_type;
10pub use convert_type::*;
11
12#[doc(hidden)]
13pub mod __macro_deps {
14    pub use anyhow;
15}
16
17/// Macro to generate a witness for a given circuit
18#[macro_export]
19macro_rules! witness {
20    ($x: ident) => {
21        $crate::paste::paste! {
22            #[allow(non_upper_case_globals)]
23            const [<$x _CIRCUIT_DATA>]: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/witnesscalc/src/", stringify!($x), ".dat"));
24            #[link(name = "witnesscalc_" [<$x>], kind = "static")]
25            extern "C" {
26                fn [<witnesscalc_ $x>](
27                    circuit_buffer: *const std::os::raw::c_char,
28                    circuit_size: std::ffi::c_ulong,
29                    json_buffer: *const std::os::raw::c_char,
30                    json_size: std::ffi::c_ulong,
31                    wtns_buffer: *mut std::os::raw::c_char,
32                    wtns_size: *mut std::ffi::c_ulong,
33                    error_msg: *mut std::os::raw::c_char,
34                    error_msg_maxsize: std::ffi::c_ulong,
35                ) -> std::ffi::c_int;
36            }
37        }
38        $crate::paste::item! {
39            pub fn [<$x _witness>](json_input: &str) -> $crate::__macro_deps::anyhow::Result<Vec<u8>> {
40                // FFI return codes
41                const WITNESSCALC_OK: std::ffi::c_int = 0x0;
42                const WITNESSCALC_ERROR_SHORT_BUFFER: std::ffi::c_int = 0x2;
43
44                println!("Generating witness for circuit {}", stringify!($x));
45                unsafe {
46                    let json_input = std::ffi::CString::new(json_input).map_err(|e| $crate::__macro_deps::anyhow::anyhow!("Failed to convert JSON input to CString: {}", e))?;
47                    let json_size = json_input.as_bytes().len() as std::ffi::c_ulong;
48
49                    let circuit_buffer = [<$x _CIRCUIT_DATA>].as_ptr() as *const std::ffi::c_char;
50                    let circuit_size = [<$x _CIRCUIT_DATA>].len() as std::ffi::c_ulong;
51
52                    let mut error_msg = vec![0u8; 256]; // Error message buffer
53                    let error_msg_ptr = error_msg.as_mut_ptr() as *mut std::ffi::c_char;
54
55                    // Two-pass dynamic allocation:
56                    // Pass 1: Probe with small buffer to query required size
57                    let mut probe_buffer = vec![0u8; 1024]; // 1 KB probe buffer
58                    let mut wtns_size: std::ffi::c_ulong = probe_buffer.len() as std::ffi::c_ulong;
59
60                    let result = [<witnesscalc_ $x>](
61                        circuit_buffer,
62                        circuit_size,
63                        json_input.as_ptr(),
64                        json_size,
65                        probe_buffer.as_mut_ptr() as *mut _,
66                        &mut wtns_size as *mut _,
67                        error_msg_ptr,
68                        error_msg.len() as u64,
69                    );
70
71                    // Pass 2: If buffer too small, allocate exact size and retry
72                    let final_buffer = if result == WITNESSCALC_ERROR_SHORT_BUFFER {
73                        // wtns_size now contains the required minimum size
74                        let required_size = wtns_size as usize;
75                        println!("Witness requires {} bytes, allocating and retrying...", required_size);
76
77                        let mut wtns_buffer = vec![0u8; required_size];
78                        let mut wtns_size: std::ffi::c_ulong = required_size as std::ffi::c_ulong;
79
80                        let result = [<witnesscalc_ $x>](
81                            circuit_buffer,
82                            circuit_size,
83                            json_input.as_ptr(),
84                            json_size,
85                            wtns_buffer.as_mut_ptr() as *mut _,
86                            &mut wtns_size as *mut _,
87                            error_msg_ptr,
88                            error_msg.len() as u64,
89                        );
90
91                        if result != WITNESSCALC_OK {
92                            let error_string = std::ffi::CStr::from_ptr(error_msg_ptr)
93                                .to_string_lossy()
94                                .into_owned();
95                            return Err($crate::__macro_deps::anyhow::anyhow!("Witness generation failed: {}", error_string));
96                        }
97
98                        wtns_buffer[..wtns_size as usize].to_vec()
99                    } else if result == WITNESSCALC_OK {
100                        // Success on first try with probe buffer (small witness)
101                        probe_buffer[..wtns_size as usize].to_vec()
102                    } else {
103                        // Other error
104                        let error_string = std::ffi::CStr::from_ptr(error_msg_ptr)
105                            .to_string_lossy()
106                            .into_owned();
107                        return Err($crate::__macro_deps::anyhow::anyhow!("Witness generation failed: {}", error_string));
108                    };
109
110                    Ok(final_buffer)
111                }
112            }
113        }
114    };
115}
116
117const WITNESSCALC_BUILD_SCRIPT: &str = include_str!("../clone_witnesscalc.sh");
118
119pub fn build_and_link(circuits_dir: &str) {
120    let target = env::var("TARGET").expect("Cargo did not provide the TARGET environment variable");
121
122    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
123    let lib_dir = Path::new(&out_dir)
124        .join("witnesscalc")
125        .join("package")
126        .join("lib");
127
128    if !Path::is_dir(Path::new(circuits_dir)) {
129        panic!("circuits_dir must be a directory");
130    }
131    println!("cargo:rerun-if-changed={}", circuits_dir);
132
133    let witnesscalc_path = Path::new(&out_dir).join(Path::new("witnesscalc"));
134    // If the witnesscalc repo is not cloned, clone it
135    if !witnesscalc_path.exists() {
136        let witnesscalc_script_path = Path::new(&out_dir).join(Path::new("clone_witnesscalc.sh"));
137        fs::write(&witnesscalc_script_path, WITNESSCALC_BUILD_SCRIPT)
138            .expect("Failed to write build script");
139        Command::new("sh")
140            .arg(witnesscalc_script_path.to_str().unwrap())
141            .spawn()
142            .expect("Failed to spawn witnesscalc build")
143            .wait()
144            .expect("witnesscalc build errored");
145    }
146
147    println!("Detected target: {}", target);
148    //For possible options see witnesscalc/build_gmp.sh
149    let gmp_build_target = match target.as_str() {
150        "aarch64-apple-ios" => "ios",
151        "aarch64-apple-ios-sim" => "ios_simulator",
152        "x86_64-apple-ios" => "ios_simulator",
153        "x86_64-linux-android" => "android_x86_64",
154        "i686-linux-android" => "android_x86_64",
155        "armv7-linux-androideabi" => "android",
156        "aarch64-linux-android" => "android",
157        "aarch64-apple-darwin" => "host", //Use "host" for M Macs, macos_arm64 would fail the subsequent build
158        _ => "host",
159    };
160
161    let gmp_lib_folder = match target.as_str() {
162        "aarch64-apple-ios" => "package_ios_arm64",
163        "aarch64-apple-ios-sim" => "package_iphone_simulator_arm64",
164        "x86_64-apple-ios" => "package_iphone_simulator_x86_64",
165        "x86_64-linux-android" => "package_android_x86_64",
166        "i686-linux-android" => "package_android_x86_64",
167        "armv7-linux-androideabi" => "package_android_arm64",
168        "aarch64-linux-android" => "package_android_arm64",
169        _ => "package",
170    };
171    //For possible options see witnesscalc/Makefile
172    let witnesscalc_build_target = match target.as_str() {
173        "aarch64-apple-ios" => "ios",
174        "aarch64-apple-ios-sim" => "ios_simulator_arm64",
175        "x86_64-apple-ios" => "ios_simulator_x86_64",
176        "x86_64-linux-android" => "android_x86_64",
177        "i686-linux-android" => "android_x86_64",
178        "armv7-linux-androideabi" => "android",
179        "aarch64-linux-android" => "android",
180        "aarch64-apple-darwin" => "arm64_host",
181        _ => "host",
182    };
183
184    // If the witnesscalc library is not built, build it
185    let gmp_dir = witnesscalc_path.join("depends").join("gmp");
186    let target_dir = gmp_dir.join(gmp_lib_folder);
187    if !target_dir.exists() {
188        Command::new("bash")
189            .current_dir(&witnesscalc_path)
190            .arg("./build_gmp.sh")
191            .arg(gmp_build_target)
192            .spawn()
193            .expect("Failed to spawn build_gmp.sh")
194            .wait()
195            .expect("build_gmp.sh errored");
196    }
197
198    //find all the .cpp files in the circuits_dir
199    let circuit_files = fs::read_dir(circuits_dir)
200        .expect("Failed to read circuits directory")
201        .map(|entry| entry.unwrap().path())
202        .filter(|path| path.extension().is_some() && path.extension().unwrap() == "cpp")
203        .collect::<Vec<_>>();
204
205    let mut v2_1_0_circuit_files: Vec<PathBuf> = Vec::new();
206    let mut v2_2_0_circuit_files: Vec<PathBuf> = Vec::new();
207
208    // Copy each circuit .cpp and .dat into witnesscalc/src, replacing any existing files
209    circuit_files.iter().for_each(|path| {
210        let circuit_name = path.file_stem().unwrap().to_str().unwrap();
211        let circuit_dat = path.with_extension("dat");
212        let circuit_dat_name = circuit_dat.file_name().unwrap().to_str().unwrap();
213        let circuit_dat_dest = witnesscalc_path.join("src").join(circuit_dat_name);
214        fs::copy(&circuit_dat, &circuit_dat_dest).expect("Failed to copy circuit .dat file");
215        //For each .cpp file, do the following: find the last include statement (should be #include "calcwit.hpp") and insert the following on the next line: namespace CIRCUIT_NAME {. Then, insert the closing } at the end of the file:
216        let circuit_cpp = fs::read_to_string(path).expect("Failed to read circuit .cpp file");
217        let circuit_cpp = circuit_cpp.replace(
218            "#include \"calcwit.hpp\"",
219            "#include \"calcwit.hpp\"\nnamespace CIRCUIT_NAME {",
220        );
221        let circuit_cpp = circuit_cpp + "\n}";
222        let circuit_cpp_name = witnesscalc_path.join("src").join(circuit_name);
223        let circuit_cpp_dest = circuit_cpp_name.with_extension("cpp");
224        fs::write(&circuit_cpp_dest, &circuit_cpp).expect("Failed to write circuit .cpp file");
225
226        let circuit_cpp_str = &circuit_cpp;
227        if circuit_cpp_str.contains("uint get_size_of_bus_field_map() {return 0;}") {
228            v2_2_0_circuit_files.push(path.clone());
229        } else {
230            v2_1_0_circuit_files.push(path.clone());
231        }
232    });
233
234    build_for_circuits_with_different_versions(
235        &v2_1_0_circuit_files,
236        &witnesscalc_path,
237        &witnesscalc_build_target,
238    );
239    if v2_2_0_circuit_files.len() > 0 {
240        Command::new("git")
241            .arg("checkout")
242            .arg("v2.2.0")
243            .current_dir(&witnesscalc_path)
244            .spawn()
245            .expect("Failed to spawn git checkout v2.2.0")
246            .wait()
247            .expect("git checkout v2.2.0 errored");
248        build_for_circuits_with_different_versions(
249            &v2_2_0_circuit_files,
250            &witnesscalc_path,
251            &witnesscalc_build_target,
252        );
253    }
254
255    // Link the C++ standard library. This is necessary for Rust tests to run on the host,
256    // non-host targets may require a specific way of linking (e.g., through linking flags in xcode)
257    #[cfg(target_os = "macos")]
258    {
259        println!("cargo:rustc-link-lib=c++"); // macOS default
260    }
261    #[cfg(not(target_os = "macos"))]
262    {
263        println!("cargo:rustc-link-lib=stdc++"); // Linux or other platforms
264    }
265    // Link the gmp and fr libraries
266    println!("cargo:rustc-link-lib=static=gmp");
267    println!("cargo:rustc-link-lib=static=fr");
268    // Specify the path to the witnesscalc library for the linker
269    println!(
270        "cargo:rustc-link-search=native={}",
271        lib_dir.to_string_lossy()
272    );
273
274    if !(env::var("CARGO_CFG_TARGET_OS").unwrap().contains("ios")
275        || env::var("CARGO_CFG_TARGET_OS").unwrap().contains("android"))
276    {
277        println!("cargo:rustc-link-lib=dylib=fr");
278        println!("cargo:rustc-link-lib=dylib=gmp");
279    }
280}
281
282fn build_for_circuits_with_different_versions(
283    circuit_files: &Vec<PathBuf>,
284    witnesscalc_path: &Path,
285    witnesscalc_build_target: &str,
286) {
287    circuit_files.iter().for_each(|path| {
288        let circuit_name = path.file_stem().unwrap().to_str().unwrap();
289        //Find a witnesscalc_template.cpp template file in the src. Replace all the @CIRCUIT_NAME@ inside it with the circuit name and write it to the src directory, replacing "template" in the name with the circuit name
290        let template_path = witnesscalc_path
291            .join("src")
292            .join("witnesscalc_template.cpp");
293        let template = fs::read_to_string(&template_path).expect("Failed to read template file");
294        let template = template.replace("@CIRCUIT_NAME@", circuit_name);
295        let template_dest = witnesscalc_path
296            .join("src")
297            .join(format!("witnesscalc_{}.cpp", circuit_name));
298        fs::write(&template_dest, template).expect("Failed to write the templated .cpp file");
299        //Find a witnesscalc_template.h template file in the src. Replace all the @CIRCUIT_NAME@ inside it with the circuit name, @CIRCUIT_NAME_CAPS@ with the capitalized name, and write it to the src directory, replacing "template" in the name with the circuit name
300        let template_path = witnesscalc_path.join("src").join("witnesscalc_template.h");
301        let template = fs::read_to_string(&template_path).expect("Failed to read template file");
302        let template = template
303            .replace("@CIRCUIT_NAME@", circuit_name)
304            .replace("@CIRCUIT_NAME_CAPS@", &circuit_name.to_uppercase());
305        let template_dest = witnesscalc_path
306            .join("src")
307            .join(format!("witnesscalc_{}.h", circuit_name));
308        fs::write(&template_dest, template).expect("Failed to write the templated .h file");
309    });
310
311    //the circuit name list would look like "circuit1;circuit2;circuit3"
312    let circuit_names = circuit_files
313        .iter()
314        .map(|path| path.file_stem().unwrap().to_str().unwrap())
315        .collect::<Vec<_>>();
316
317    let circuit_names_semicolon = circuit_names.join(";");
318
319    let make_process = Command::new("make")
320        .env("CIRCUIT_NAMES", circuit_names_semicolon)
321        .arg(witnesscalc_build_target)
322        .current_dir(&witnesscalc_path)
323        .output()
324        .expect("Failed to execute make arm64_host");
325
326    if !make_process.status.success() {
327        eprintln!(
328            "Make command failed with exit code: {}",
329            make_process.status
330        );
331        eprintln!("stdout: {}", String::from_utf8_lossy(&make_process.stdout));
332        eprintln!("stderr: {}", String::from_utf8_lossy(&make_process.stderr));
333
334        // Check if any of the required libraries were actually built despite the error
335        let lib_dir = witnesscalc_path.join("package").join("lib");
336        let mut all_libs_exist = true;
337
338        for circuit_name in &circuit_names {
339            let lib_path = lib_dir.join(format!("libwitnesscalc_{}.a", circuit_name));
340            if !lib_path.exists() {
341                eprintln!("Warning: Library {} was not built", lib_path.display());
342                all_libs_exist = false;
343            }
344        }
345
346        if !all_libs_exist {
347            panic!("Make command failed and required libraries are missing");
348        } else {
349            eprintln!("Warning: Make command failed but required libraries exist. Continuing...");
350        }
351    }
352
353    // Link the witnesscalc library for the circuit
354    circuit_names.iter().for_each(|circuit_name| {
355        println!("cargo:rustc-link-lib=static=witnesscalc_{}", circuit_name);
356    });
357
358    if !(env::var("CARGO_CFG_TARGET_OS").unwrap().contains("ios")
359        || env::var("CARGO_CFG_TARGET_OS").unwrap().contains("android"))
360    {
361        circuit_names.iter().for_each(|circuit_name| {
362            println!("cargo:rustc-link-lib=dylib=witnesscalc_{}", circuit_name);
363        });
364    }
365}