Know the execution target
The generated contract uses the BLS12-381 G1 MSM precompile at 0x0c and pairing precompile at 0x0f, as defined by EIP-2537. Deploy only to a network and EVM revision where those precompiles are active. An unavailable precompile is treated as verification failure.
The verifier is circuit-specific because it embeds the Groth16 verification key and exact public-input count. A new trusted setup or circuit shape produces a new key and therefore requires a newly generated contract.
Generate the circuit-specific verifier
Persist the canonical verification key with VerifyingKey::to_bytes(), then run the repository exporter. Select the same supported curve backend used by the rest of the application.
cargo run --release -p dusk-groth16 \
--bin dusk-groth16-solidity \
--no-default-features \
--features=bls-backend-blst,std -- \
generate-verifier \
verifying-key.bin \
ProductVerifier \
ProductVerifier.solThe same operation is available as the allocation-only Rust API:
let source = verifying_key.solidity_verifier("ProductVerifier")?;
std::fs::write("ProductVerifier.sol", source)?;Convert the proof transport
The crate's canonical Groth16 proof remains a 192-byte compressed (A, B, C) encoding. EIP-2537 consumes uncompressed, big-endian field elements, so the generated contract instead accepts a 512-byte transport containing (-A, B, C):
- bytes 0–127: -A, an EIP-2537 G1 point;
- bytes 128–383: B, an EIP-2537 G2 point in c0 || c1 coefficient order;
- bytes 384–511: C, an EIP-2537 G1 point.
Convert a proof directly in Rust with proof.to_eip2537(), or convert a saved canonical proof:
cargo run --release -p dusk-groth16 \
--bin dusk-groth16-solidity \
--no-default-features \
--features=bls-backend-blst,std -- \
encode-proof proof.bin proof.eip2537.binPreserve the public statement
bool valid = verifier.verifyProof(
eip2537Proof,
publicInputs
);The ABI is verifyProof(bytes,uint256[]). Public inputs must be canonical BLS scalar-field values and must appear in Composer emission order—the same order returned by the Rust prover and expected by the Rust verifier. Build these values from trusted application state rather than accepting an untrusted array merely because it arrived next to a proof.
Exercise the cross-language boundary
The checked-in Foundry project compiles a deterministic circuit-specific verifier and verifies a proof generated by the Rust stack. It also covers well-formed but incorrect proofs, wrong public inputs, non-canonical scalars, incorrect lengths, identity proof elements, non-canonical base-field coordinates, off-curve points, and wrong-subgroup points in every proof element.
make solidity-fmt CHECK=1
make solidity-test
forge build --root crates/groth16/tests/solidity \
test/fixtures/SquareVerifier.sol --sizesFoundry must support the Prague EVM revision. The deterministic fixture setup seed is for reproducible tests only and must never be used for production keys.
Review the security boundaries
- The current Groth16 setup is circuit-specific and single-party. Generate setup randomness securely and destroy it; MPC ceremony flows are not implemented.
- The contract rejects identity proof elements and public inputs outside the scalar field. The target EVM's EIP-2537 implementation validates every base-field coordinate, curve equation, and subgroup for proof and verification-key points. The contract checks call success and exact return sizes and fails closed.
- Malformed EIP-2537 input burns all gas forwarded to a precompile. Generated contracts cap forwarded gas according to the finalized operation schedule.
- Verification-key query points are embedded in runtime bytecode. Circuits with many public inputs must be checked against the target chain's contract-size limit.
- The verifier does not prove that the embedded key came from a trustworthy setup or that its parameters are algebraically consistent. Key provenance and setup integrity remain application responsibilities.
- The generator and contract are licensed under MPL-2.0. They have not yet undergone a dedicated security audit.