google-site-verification: google61fe8ba583a51912.html
Zero-Knowledge Proofs with SnarkJS in Java

Overview

Zero-Knowledge Proofs (ZKPs) allow one party to prove they know a value without revealing the value itself. SnarkJS is a JavaScript library for zk-SNARKs, but can be integrated with Java applications for generating and verifying proofs.

Architecture

Integration Approaches

  1. Node.js Process Execution: Run SnarkJS as external process
  2. JNI/JNA Bridge: Native integration through JavaScript engines
  3. REST API Wrapper: Expose SnarkJS as microservice
  4. WebAssembly (WASM): Compile circuits to WASM

Dependencies

<dependencies> <!-- Process execution --> <dependency> <groupId>org.zeroturnaround</groupId> <artifactId>zt-exec</artifactId> <version>1.12</version> </dependency> <!-- JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- HTTP client for REST approach --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- GraalVM JS integration --> <dependency> <groupId>org.graalvm.js</groupId> <artifactId>js</artifactId> <version>23.0.0</version> </dependency> <!-- Web3j for blockchain integration --> <dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.9.7</version> </dependency> </dependencies>

Core Implementation

1. SnarkJS Process Executor

@Service public class SnarkJSProcessExecutor { private final String snarkjsPath; private final String circuitsBasePath; public SnarkJSProcessExecutor(@Value("${snarkjs.path:snarkjs}") String snarkjsPath, @Value("${circuits.path:./circuits}") String circuitsBasePath) { this.snarkjsPath = snarkjsPath; this.circuitsBasePath = circuitsBasePath; } public ZKPResult generateProof(String circuitName, Map<String, Object> inputs) throws IOException, InterruptedException { // Create input file String inputFile = createInputFile(circuitName, inputs); // Generate witness generateWitness(circuitName, inputFile); // Generate proof return executeProofGeneration(circuitName); } private String createInputFile(String circuitName, Map<String, Object> inputs) throws IOException { String inputPath = circuitsBasePath + "/" + circuitName + "/input.json"; ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(new File(inputPath), inputs); return inputPath; } private void generateWitness(String circuitName, String inputFile) throws IOException, InterruptedException { String circuitFile = circuitsBasePath + "/" + circuitName + "/circuit.wasm"; String witnessFile = circuitsBasePath + "/" + circuitName + "/witness.wtns"; ProcessBuilder builder = new ProcessBuilder( "node", "generate_witness.js", circuitFile, inputFile, witnessFile ); builder.directory(new File(circuitsBasePath + "/" + circuitName)); Process process = builder.start(); int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("Witness generation failed with exit code: " + exitCode); } } private ZKPResult executeProofGeneration(String circuitName) throws IOException, InterruptedException { String provingKey = circuitsBasePath + "/" + circuitName + "/proving_key.zkey"; String witnessFile = circuitsBasePath + "/" + circuitName + "/witness.wtns"; String proofFile = circuitsBasePath + "/" + circuitName + "/proof.json"; String publicFile = circuitsBasePath + "/" + circuitName + "/public.json"; // Execute snarkjs proof generation ProcessBuilder proofBuilder = new ProcessBuilder( snarkjsPath, "groth16", "prove", provingKey, witnessFile, proofFile, publicFile ); Process proofProcess = proofBuilder.start(); int proofExitCode = proofProcess.waitFor(); if (proofExitCode != 0) { throw new RuntimeException("Proof generation failed"); } // Read generated proof and public signals ObjectMapper mapper = new ObjectMapper(); Proof proof = mapper.readValue(new File(proofFile), Proof.class); List<Object> publicSignals = mapper.readValue( new File(publicFile), new TypeReference<List<Object>>() {} ); return new ZKPResult(proof, publicSignals); } public boolean verifyProof(String circuitName, Proof proof, List<Object> publicSignals) throws IOException, InterruptedException { String verificationKey = circuitsBasePath + "/" + circuitName + "/verification_key.json"; // Create temporary files for proof and public signals String tempProofFile = createTempProofFile(proof); String tempPublicFile = createTempPublicFile(publicSignals); ProcessBuilder verifyBuilder = new ProcessBuilder( snarkjsPath, "groth16", "verify", verificationKey, tempPublicFile, tempProofFile ); Process verifyProcess = verifyBuilder.start(); // Read verification output String output = new String(verifyProcess.getInputStream().readAllBytes()); int verifyExitCode = verifyProcess.waitFor(); // Cleanup temp files new File(tempProofFile).delete(); new File(tempPublicFile).delete(); return verifyExitCode == 0 && output.trim().equals("OK"); } }

2. Circuit Management Service

@Service public class CircuitManager { private final Map<String, CircuitMetadata> circuits; private final String circuitsBasePath; public CircuitManager(@Value("${circuits.path:./circuits}") String circuitsBasePath) { this.circuitsBasePath = circuitsBasePath; this.circuits = new HashMap<>(); loadCircuitMetadata(); } private void loadCircuitMetadata() { File circuitsDir = new File(circuitsBasePath); if (!circuitsDir.exists()) return; for (File circuitDir : circuitsDir.listFiles(File::isDirectory)) { String circuitName = circuitDir.getName(); CircuitMetadata metadata = loadCircuitMetadata(circuitName); circuits.put(circuitName, metadata); } } private CircuitMetadata loadCircuitMetadata(String circuitName) { try { String metadataPath = circuitsBasePath + "/" + circuitName + "/metadata.json"; ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(new File(metadataPath), CircuitMetadata.class); } catch (IOException e) { return new CircuitMetadata(circuitName, CircuitStatus.UNKNOWN, new HashMap<>()); } } public CircuitCompilationResult compileCircuit(String circuitName, String circuitCode) throws IOException, InterruptedException { // Create circuit directory String circuitDir = circuitsBasePath + "/" + circuitName; new File(circuitDir).mkdirs(); // Write circuit file String circuitFile = circuitDir + "/circuit.circom"; Files.write(Paths.get(circuitFile), circuitCode.getBytes()); // Compile circuit using circom ProcessBuilder compileBuilder = new ProcessBuilder( "circom", circuitFile, "--wasm", "--r1cs", "--sym" ); compileBuilder.directory(new File(circuitDir)); Process compileProcess = compileBuilder.start(); int compileExitCode = compileProcess.waitFor(); if (compileExitCode != 0) { throw new RuntimeException("Circuit compilation failed"); } // Generate setup (trusted setup) return performTrustedSetup(circuitName); } private CircuitCompilationResult performTrustedSetup(String circuitName) throws IOException, InterruptedException { String circuitDir = circuitsBasePath + "/" + circuitName; String r1csFile = circuitDir + "/circuit.r1cs"; // Phase 1 (powers of tau) - usually done once for multiple circuits ProcessBuilder phase1Builder = new ProcessBuilder( "snarkjs", "powersoftau", "new", "bn128", "12", "pot12_0000.ptau", "-v" ); phase1Builder.directory(new File(circuitDir)); Process phase1Process = phase1Builder.start(); phase1Process.waitFor(); // ... more setup steps return new CircuitCompilationResult(circuitName, CircuitStatus.COMPILED, "Setup completed"); } }

3. ZKP Service Abstraction

@Service public class ZKProofService { private final SnarkJSProcessExecutor snarkjs; private final CircuitManager circuitManager; public ZKProofService(SnarkJSProcessExecutor snarkjs, CircuitManager circuitManager) { this.snarkjs = snarkjs; this.circuitManager = circuitManager; } public ProofResult generateProof(String circuitName, Map<String, Object> privateInputs) { try { // Validate circuit exists if (!circuitManager.circuitExists(circuitName)) { throw new CircuitNotFoundException("Circuit not found: " + circuitName); } // Validate inputs against circuit schema validateInputs(circuitName, privateInputs); // Generate proof ZKPResult zkpResult = snarkjs.generateProof(circuitName, privateInputs); return new ProofResult( zkpResult.getProof(), zkpResult.getPublicSignals(), circuitName, Instant.now() ); } catch (Exception e) { throw new ProofGenerationException("Failed to generate proof", e); } } public boolean verifyProof(String circuitName, Proof proof, List<Object> publicSignals) { try { return snarkjs.verifyProof(circuitName, proof, publicSignals); } catch (Exception e) { throw new ProofVerificationException("Failed to verify proof", e); } } public ProofResult generateRangeProof(BigInteger value, BigInteger min, BigInteger max) { String circuitName = "range_proof"; Map<String, Object> inputs = new HashMap<>(); inputs.put("value", value.toString()); inputs.put("min", min.toString()); inputs.put("max", max.toString()); return generateProof(circuitName, inputs); } public ProofResult generateMerkleProof(List<String> elements, String leaf, String root) { String circuitName = "merkle_proof"; Map<String, Object> inputs = new HashMap<>(); inputs.put("elements", elements); inputs.put("leaf", leaf); inputs.put("root", root); return generateProof(circuitName, inputs); } public ProofResult generateBalanceProof(String account, BigInteger balance, BigInteger threshold) { String circuitName = "balance_proof"; Map<String, Object> inputs = new HashMap<>(); inputs.put("account", account); inputs.put("balance", balance.toString()); inputs.put("threshold", threshold.toString()); return generateProof(circuitName, inputs); } private void validateInputs(String circuitName, Map<String, Object> inputs) { CircuitMetadata metadata = circuitManager.getCircuitMetadata(circuitName); Map<String, String> inputSchema = metadata.getInputSchema(); for (String requiredInput : inputSchema.keySet()) { if (!inputs.containsKey(requiredInput)) { throw new InvalidInputException("Missing required input: " + requiredInput); } } } }

Advanced Integration

1. GraalVM JavaScript Integration

@Service public class SnarkJSGraalVMService { private final Context jsContext; public SnarkJSGraalVMService() { this.jsContext = Context.newBuilder("js") .allowAllAccess(true) .build(); } public void initializeSnarkJS() { // Load SnarkJS library try { String snarkjsSource = Files.readString(Paths.get("node_modules/snarkjs/snarkjs.js")); jsContext.eval("js", snarkjsSource); } catch (IOException e) { throw new RuntimeException("Failed to load SnarkJS", e); } } public ProofResult generateProofWithGraalVM(String circuitName, Map<String, Object> inputs) { Value snarkjs = jsContext.getBindings("js").getMember("snarkjs"); // Convert Java inputs to JavaScript Value jsInputs = convertToJSValue(inputs); // Execute proof generation Value result = snarkjs.invokeMember("groth16", "fullProve", jsInputs, circuitName + "/circuit.wasm", circuitName + "/proving_key.zkey"); return convertToProofResult(result); } private Value convertToJSValue(Map<String, Object> inputs) { Value jsObject = jsContext.eval("js", "({})"); for (Map.Entry<String, Object> entry : inputs.entrySet()) { jsObject.putMember(entry.getKey(), convertValue(entry.getValue())); } return jsObject; } private Value convertValue(Object value) { if (value instanceof String) { return jsContext.asValue(value); } else if (value instanceof Number) { return jsContext.asValue(value.toString()); } else if (value instanceof List) { Value jsArray = jsContext.eval("js", "[]"); List<?> list = (List<?>) value; for (int i = 0; i < list.size(); i++) { jsArray.setArrayElement(i, convertValue(list.get(i))); } return jsArray; } return jsContext.asValue(value.toString()); } }

2. REST API Wrapper

@RestController @RequestMapping("/api/zkp") public class ZKPController { private final ZKProofService zkpService; public ZKPController(ZKProofService zkpService) { this.zkpService = zkpService; } @PostMapping("/proof/generate") public ResponseEntity<ProofResponse> generateProof(@RequestBody ProofRequest request) { try { ProofResult result = zkpService.generateProof( request.getCircuitName(), request.getInputs() ); ProofResponse response = new ProofResponse( result.getProof(), result.getPublicSignals(), result.getCircuitName(), result.getTimestamp() ); return ResponseEntity.ok(response); } catch (CircuitNotFoundException e) { return ResponseEntity.badRequest().build(); } catch (ProofGenerationException e) { return ResponseEntity.status(500).build(); } } @PostMapping("/proof/verify") public ResponseEntity<VerificationResponse> verifyProof(@RequestBody VerificationRequest request) { try { boolean isValid = zkpService.verifyProof( request.getCircuitName(), request.getProof(), request.getPublicSignals() ); VerificationResponse response = new VerificationResponse(isValid); return ResponseEntity.ok(response); } catch (ProofVerificationException e) { return ResponseEntity.status(500).build(); } } @PostMapping("/circuit/compile") public ResponseEntity<CircuitResponse> compileCircuit(@RequestBody CircuitCompileRequest request) { try { CircuitCompilationResult result = zkpService.compileCircuit( request.getCircuitName(), request.getCircuitCode() ); CircuitResponse response = new CircuitResponse( result.getCircuitName(), result.getStatus(), result.getMessage() ); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } }

Use Case Implementations

1. Anonymous Voting System

@Service public class AnonymousVotingService { private final ZKProofService zkpService; private final VotingRepository votingRepository; public AnonymousVotingService(ZKProofService zkpService, VotingRepository votingRepository) { this.zkpService = zkpService; this.votingRepository = votingRepository; } public VotingReceipt castVote(String voterId, String candidateId, String secret) { // Generate proof that voter is eligible without revealing identity Map<String, Object> inputs = new HashMap<>(); inputs.put("voterId", voterId); inputs.put("candidateId", candidateId); inputs.put("secret", secret); inputs.put("voterRoot", getVoterRootHash()); ProofResult proof = zkpService.generateProof("voting_circuit", inputs); // Store vote commitment VoteCommitment commitment = new VoteCommitment( UUID.randomUUID().toString(), proof.getPublicSignals().get(0).toString(), // commitment Instant.now() ); votingRepository.save(commitment); return new VotingReceipt(commitment.getId(), proof); } public boolean verifyVote(String commitment, Proof proof) { List<Object> publicSignals = List.of(commitment); return zkpService.verifyProof("voting_circuit", proof, publicSignals); } private String getVoterRootHash() { // Return Merkle root of eligible voters return "0x..."; } }

2. Private Transaction System

@Service public class PrivateTransactionService { private final ZKProofService zkpService; private final BlockchainService blockchainService; public PrivateTransactionService(ZKProofService zkpService, BlockchainService blockchainService) { this.zkpService = zkpService; this.blockchainService = blockchainService; } public TransactionProof createPrivateTransaction( String from, String to, BigInteger amount, BigInteger balance) { // Generate proof that sender has sufficient balance Map<String, Object> inputs = new HashMap<>(); inputs.put("from", from); inputs.put("to", to); inputs.put("amount", amount.toString()); inputs.put("balance", balance.toString()); inputs.put("balanceCommitment", calculateBalanceCommitment(from, balance)); ProofResult proof = zkpService.generateProof("transaction_circuit", inputs); // Create transaction with proof ZKTransaction transaction = new ZKTransaction( from, to, amount, proof.getPublicSignals(), proof.getProof() ); // Submit to blockchain String txHash = blockchainService.submitTransaction(transaction); return new TransactionProof(txHash, proof); } public boolean verifyTransaction(String from, BigInteger amount, Proof proof, List<Object> publicSignals) { return zkpService.verifyProof("transaction_circuit", proof, publicSignals); } private String calculateBalanceCommitment(String account, BigInteger balance) { // Calculate Pedersen commitment or similar return "0x..." + balance.toString(16); } }

Configuration and Security

application.yml

zkp: snarkjs: path: snarkjs timeout: 300000 circuits: path: ./circuits auto-compile: true security: max-proof-size: 1048576 allowed-circuits: - range_proof - merkle_proof - balance_proof - voting_circuit - transaction_circuit

Security Configuration

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/zkp/proof/generate").hasRole("USER") .antMatchers("/api/zkp/circuit/compile").hasRole("ADMIN") .anyRequest().authenticated() .and() .httpBasic(); } }

Testing

@SpringBootTest class ZKPTest { @Autowired private ZKProofService zkpService; @Test void testRangeProof() { Map<String, Object> inputs = new HashMap<>(); inputs.put("value", "50"); inputs.put("min", "0"); inputs.put("max", "100"); ProofResult result = zkpService.generateProof("range_proof", inputs); assertNotNull(result.getProof()); assertTrue(zkpService.verifyProof("range_proof", result.getProof(), result.getPublicSignals())); } @Test void testMerkleProof() { List<String> elements = Arrays.asList("a", "b", "c", "d"); String leaf = "b"; String root = calculateMerkleRoot(elements); Map<String, Object> inputs = new HashMap<>(); inputs.put("elements", elements); inputs.put("leaf", leaf); inputs.put("root", root); ProofResult result = zkpService.generateProof("merkle_proof", inputs); assertNotNull(result.getProof()); assertTrue(zkpService.verifyProof("merkle_proof", result.getProof(), result.getPublicSignals())); } }

Performance Optimization

@Service public class ZKPCacheService { private final Cache<String, ProofResult> proofCache; private final Cache<String, Boolean> verificationCache; public ZKPCacheService() { this.proofCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(1, TimeUnit.HOURS) .build(); this.verificationCache = Caffeine.newBuilder() .maximumSize(5000) .expireAfterWrite(24, TimeUnit.HOURS) .build(); } public ProofResult getCachedProof(String cacheKey, Supplier<ProofResult> proofSupplier) { return proofCache.get(cacheKey, k -> proofSupplier.get()); } public Boolean getCachedVerification(String cacheKey, Supplier<Boolean> verificationSupplier) { return verificationCache.get(cacheKey, k -> verificationSupplier.get()); } public String generateCacheKey(String circuitName, Map<String, Object> inputs) { try { ObjectMapper mapper = new ObjectMapper(); String inputJson = mapper.writeValueAsString(inputs); return circuitName + "_" + Hashing.sha256().hashString(inputJson, StandardCharsets.UTF_8); } catch (JsonProcessingException e) { throw new RuntimeException("Cache key generation failed", e); } } }

Best Practices

  1. Circuit Security: Audit circuits for vulnerabilities
  2. Trusted Setup: Use secure multi-party computation for setup
  3. Input Validation: Validate all inputs before proof generation
  4. Resource Management: Set timeouts for proof generation
  5. Error Handling: Don't reveal sensitive information in errors
  6. Monitoring: Monitor proof generation success rates and performance
  7. Key Management: Secure proving and verification keys

This implementation provides a comprehensive foundation for integrating SnarkJS with Java applications, enabling zero-knowledge proof generation and verification for various use cases while maintaining security and performance.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper