diff --git a/.github/actions/compile-witness-generator/action.yml b/.github/actions/compile-witness-generator/action.yml index b7781e8..16a5866 100644 --- a/.github/actions/compile-witness-generator/action.yml +++ b/.github/actions/compile-witness-generator/action.yml @@ -83,6 +83,7 @@ runs: cp "${SOURCES_ROOT}/circom_adapter.hpp" "${CIRCUIT_CPP_PATH}/circom_adapter.hpp" cp "${SOURCES_ROOT}/circom_fwd.hpp" "${CIRCUIT_CPP_PATH}/circom_fwd.hpp" cp "${SOURCES_ROOT}/types.hpp" "${CIRCUIT_CPP_PATH}/types.hpp" + cp "${SOURCES_ROOT}/assert.h" "${CIRCUIT_CPP_PATH}/assert.h" # TODO: Instead of replace, make a fork that generates the appropriate Makefile - name: Replace ${{ inputs.circuit-name-display }}'s Makefile diff --git a/justfile b/justfile index a81c249..d6efac5 100644 --- a/justfile +++ b/justfile @@ -35,7 +35,7 @@ poq: check-circom # circom-generated main() has no return on the success path; patch it before -O3 turns it into an infinite loop {{sed_i}} ':a;N;$!ba;s/\n}\n\n*$/\n return 0;\n}/' blend/poq_cpp/main.cpp cp -r {{src}}/poq blend/poq_cpp/poq - cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp blend/poq_cpp/ + cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp {{src}}/assert.h blend/poq_cpp/ cp {{ci_makefile}} blend/poq_cpp/Makefile cp blend/test_ffi.cpp blend/poq_cpp/test_ffi.cpp make -C blend/poq_cpp PROJECT=poq linux-lib @@ -51,7 +51,7 @@ pol: check-circom # circom-generated main() has no return on the success path; patch it before -O3 turns it into an infinite loop {{sed_i}} ':a;N;$!ba;s/\n}\n\n*$/\n return 0;\n}/' mantle/pol_cpp/main.cpp cp -r {{src}}/pol mantle/pol_cpp/pol - cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp mantle/pol_cpp/ + cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp {{src}}/assert.h mantle/pol_cpp/ cp {{ci_makefile}} mantle/pol_cpp/Makefile cp mantle/test_pol.cpp mantle/pol_cpp/test_pol.cpp make -C mantle/pol_cpp PROJECT=pol linux-lib @@ -67,7 +67,7 @@ poc: check-circom # circom-generated main() has no return on the success path; patch it before -O3 turns it into an infinite loop {{sed_i}} ':a;N;$!ba;s/\n}\n\n*$/\n return 0;\n}/' mantle/poc_cpp/main.cpp cp -r {{src}}/poc mantle/poc_cpp/poc - cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp mantle/poc_cpp/ + cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp {{src}}/assert.h mantle/poc_cpp/ cp {{ci_makefile}} mantle/poc_cpp/Makefile cp mantle/test_poc.cpp mantle/poc_cpp/test_poc.cpp make -C mantle/poc_cpp PROJECT=poc linux-lib @@ -83,7 +83,7 @@ signature: check-circom # circom-generated main() has no return on the success path; patch it before -O3 turns it into an infinite loop {{sed_i}} ':a;N;$!ba;s/\n}\n\n*$/\n return 0;\n}/' mantle/signature_cpp/main.cpp cp -r {{src}}/signature mantle/signature_cpp/signature - cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp mantle/signature_cpp/ + cp {{src}}/circom_adapter.cpp {{src}}/circom_adapter.hpp {{src}}/circom_fwd.hpp {{src}}/types.hpp {{src}}/assert.h mantle/signature_cpp/ cp {{ci_makefile}} mantle/signature_cpp/Makefile cp mantle/test_signature.cpp mantle/signature_cpp/test_signature.cpp make -C mantle/signature_cpp PROJECT=signature linux-lib diff --git a/rust/logos-blockchain-circuits-poc-sys/src/native.rs b/rust/logos-blockchain-circuits-poc-sys/src/native.rs index 959926f..9933d7a 100644 --- a/rust/logos-blockchain-circuits-poc-sys/src/native.rs +++ b/rust/logos-blockchain-circuits-poc-sys/src/native.rs @@ -60,6 +60,29 @@ mod tests { static INPUTS: LazyLock = LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample.input.json")); + #[test] + fn test_generate_witness_invalid_json_returns_err() { + let input = PocWitnessInput::new("{".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_missing_inputs_returns_err() { + let input = PocWitnessInput::new("{}".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_constraint_violation_returns_err() { + let json = std::fs::read_to_string(&*INPUTS).unwrap(); + let bad_json = json.replace( + "\"voucher_root\": \"20810875415353676096192834577269613981524168537821543897016159330974871397924\"", + "\"voucher_root\": \"1\"", + ); + let input = PocWitnessInput::new(bad_json).unwrap(); + assert!(generate_witness(&input).is_err()); + } + #[test] fn test_generate_witness() { let dat = LIB_DIR.join("witness_generator"); diff --git a/rust/logos-blockchain-circuits-pol-sys/src/native.rs b/rust/logos-blockchain-circuits-pol-sys/src/native.rs index 62c3d43..ca465b9 100644 --- a/rust/logos-blockchain-circuits-pol-sys/src/native.rs +++ b/rust/logos-blockchain-circuits-pol-sys/src/native.rs @@ -60,6 +60,29 @@ mod tests { static INPUTS: LazyLock = LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample.input.json")); + #[test] + fn test_generate_witness_invalid_json_returns_err() { + let input = PolWitnessInput::new("{".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_missing_inputs_returns_err() { + let input = PolWitnessInput::new("{}".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_constraint_violation_returns_err() { + let json = std::fs::read_to_string(&*INPUTS).unwrap(); + let bad_json = json.replace( + "\"ledger_aged\": \"9907496234164738674719754286318998202143315407023653151112941050435603056651\"", + "\"ledger_aged\": \"1\"", + ); + let input = PolWitnessInput::new(bad_json).unwrap(); + assert!(generate_witness(&input).is_err()); + } + #[test] fn test_generate_witness() { let dat = LIB_DIR.join("witness_generator"); diff --git a/rust/logos-blockchain-circuits-poq-sys/src/native.rs b/rust/logos-blockchain-circuits-poq-sys/src/native.rs index 1570c7e..2e51eb2 100644 --- a/rust/logos-blockchain-circuits-poq-sys/src/native.rs +++ b/rust/logos-blockchain-circuits-poq-sys/src/native.rs @@ -60,6 +60,31 @@ mod tests { static INPUTS: LazyLock = LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample.input.json")); + #[test] + fn test_generate_witness_invalid_json_returns_err() { + let input = PoqWitnessInput::new("{".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_missing_inputs_returns_err() { + let input = PoqWitnessInput::new("{}".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_constraint_violation_returns_err() { + let json = std::fs::read_to_string(&*INPUTS).unwrap(); + // Swap core_root for a wrong value; the Merkle path no longer verifies, + // so is_registered.out = 0 and the constraint at circom line 108 fires. + let bad_json = json.replace( + "\"core_root\": \"20423847801203321296654759690878714805328777188198550442842378955293864405749\"", + "\"core_root\": \"1\"", + ); + let input = PoqWitnessInput::new(bad_json).unwrap(); + assert!(generate_witness(&input).is_err()); + } + #[test] fn test_generate_witness() { let dat = LIB_DIR.join("witness_generator"); diff --git a/rust/logos-blockchain-circuits-signature-sys/src/native.rs b/rust/logos-blockchain-circuits-signature-sys/src/native.rs index 62f710b..c2c847f 100644 --- a/rust/logos-blockchain-circuits-signature-sys/src/native.rs +++ b/rust/logos-blockchain-circuits-signature-sys/src/native.rs @@ -65,6 +65,18 @@ mod tests { static INPUTS: LazyLock = LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample.input.json")); + #[test] + fn test_generate_witness_invalid_json_returns_err() { + let input = SignatureWitnessInput::new("{".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + + #[test] + fn test_generate_witness_missing_inputs_returns_err() { + let input = SignatureWitnessInput::new("{}".to_owned()).unwrap(); + assert!(generate_witness(&input).is_err()); + } + #[test] fn test_generate_witness() { let dat = LIB_DIR.join("witness_generator"); diff --git a/rust/logos-blockchain-circuits-tests/tests/conflicts.rs b/rust/logos-blockchain-circuits-tests/tests/conflicts.rs index 5d58d5d..446718e 100644 --- a/rust/logos-blockchain-circuits-tests/tests/conflicts.rs +++ b/rust/logos-blockchain-circuits-tests/tests/conflicts.rs @@ -22,4 +22,23 @@ mod tests { let poq_result = lbc_poq_sys::generate_witness(&inputs_json); assert!(poq_result.is_ok()); } + + #[test] + fn test_concurrent_poq_calls() { + let inputs_json_raw = std::fs::read_to_string(inputs::POQ.as_path()).unwrap(); + + let handles: Vec<_> = (0..4) + .map(|_| { + let json = inputs_json_raw.clone(); + std::thread::spawn(move || { + let input = PoqWitnessInput::new(json).unwrap(); + lbc_poq_sys::generate_witness(&input) + }) + }) + .collect(); + + for h in handles { + assert!(h.join().unwrap().is_ok()); + } + } } diff --git a/src/assert.h b/src/assert.h new file mode 100644 index 0000000..7bd2209 --- /dev/null +++ b/src/assert.h @@ -0,0 +1,27 @@ +// Assert-to-throw shim for circom-generated circuit code. +// +// Problem +// ------- +// Circom generates C++ code that calls the standard assert() macro to enforce +// circuit constraints (e.g. `assert(Fr_isTrue(&expaux[0]))`). When compiled +// into a standalone binary, a failing assert aborts the subprocess and the +// caller receives a non-zero exit code; an error. When compiled into a static +// library and linked into the caller's process, the same abort kills the entire +// process. A library must never call abort() on its caller. +// +// Mechanism +// --------- +// This file is copied into each circuit's build directory as assert.h. The +// Makefile already passes -I. so the compiler finds this file before the +// system assert.h when the generated code does `#include `. +// #include_next pulls in the real system header (so all +// declarations are present), then we redefine the assert macro to throw a +// std::runtime_error instead of calling abort(). #pragma once prevents a +// second include from re-running and undoing the redefinition. +#pragma once +#include_next +#undef assert +#include +#define assert(cond) \ + ((cond) ? void(0) : throw std::runtime_error( \ + std::string("Circuit constraint violated in ") + __FILE__ + ":" + std::to_string(__LINE__) + ": " + #cond)) diff --git a/src/poc/ffi.cpp b/src/poc/ffi.cpp index 511a1f3..d5a25d4 100644 --- a/src/poc/ffi.cpp +++ b/src/poc/ffi.cpp @@ -92,7 +92,13 @@ static Status generate_witness_impl(const WitnessInput* input, Bytes* output) { Circom_Circuit* circuit = loadCircuit(circuit_bytes); Circom_CalcWit* ctx = new Circom_CalcWit(circuit); - loadJson(ctx, input->inputs_json); + try { + loadJson(ctx, input->inputs_json); + } catch (...) { + delete ctx; + delete circuit; + throw; + } if (ctx->getRemaingInputsToBeSet()!=0) { const std::string message = "Not all inputs have been set. Only " + std::to_string(get_main_input_signal_no()-ctx->getRemaingInputsToBeSet()) + " out of " + std::to_string(get_main_input_signal_no()) + "."; delete ctx; diff --git a/src/pol/ffi.cpp b/src/pol/ffi.cpp index e4404ba..4a9cac2 100644 --- a/src/pol/ffi.cpp +++ b/src/pol/ffi.cpp @@ -92,7 +92,13 @@ static Status generate_witness_impl(const WitnessInput* input, Bytes* output) { Circom_Circuit* circuit = loadCircuit(circuit_bytes); Circom_CalcWit* ctx = new Circom_CalcWit(circuit); - loadJson(ctx, input->inputs_json); + try { + loadJson(ctx, input->inputs_json); + } catch (...) { + delete ctx; + delete circuit; + throw; + } if (ctx->getRemaingInputsToBeSet()!=0) { const std::string message = "Not all inputs have been set. Only " + std::to_string(get_main_input_signal_no()-ctx->getRemaingInputsToBeSet()) + " out of " + std::to_string(get_main_input_signal_no()) + "."; delete ctx; diff --git a/src/poq/ffi.cpp b/src/poq/ffi.cpp index eb5ebc9..7715c89 100644 --- a/src/poq/ffi.cpp +++ b/src/poq/ffi.cpp @@ -92,7 +92,13 @@ static Status generate_witness_impl(const WitnessInput* input, Bytes* output) { Circom_Circuit* circuit = loadCircuit(circuit_bytes); Circom_CalcWit* ctx = new Circom_CalcWit(circuit); - loadJson(ctx, input->inputs_json); + try { + loadJson(ctx, input->inputs_json); + } catch (...) { + delete ctx; + delete circuit; + throw; + } if (ctx->getRemaingInputsToBeSet()!=0) { const std::string message = "Not all inputs have been set. Only " + std::to_string(get_main_input_signal_no()-ctx->getRemaingInputsToBeSet()) + " out of " + std::to_string(get_main_input_signal_no()) + "."; delete ctx; diff --git a/src/signature/ffi.cpp b/src/signature/ffi.cpp index fdd05f6..94a7990 100644 --- a/src/signature/ffi.cpp +++ b/src/signature/ffi.cpp @@ -92,7 +92,13 @@ static Status generate_witness_impl(const WitnessInput* input, Bytes* output) { Circom_Circuit* circuit = loadCircuit(circuit_bytes); Circom_CalcWit* ctx = new Circom_CalcWit(circuit); - loadJson(ctx, input->inputs_json); + try { + loadJson(ctx, input->inputs_json); + } catch (...) { + delete ctx; + delete circuit; + throw; + } if (ctx->getRemaingInputsToBeSet()!=0) { const std::string message = "Not all inputs have been set. Only " + std::to_string(get_main_input_signal_no()-ctx->getRemaingInputsToBeSet()) + " out of " + std::to_string(get_main_input_signal_no()) + "."; delete ctx;