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
- Node.js Process Execution: Run SnarkJS as external process
- JNI/JNA Bridge: Native integration through JavaScript engines
- REST API Wrapper: Expose SnarkJS as microservice
- 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
- Circuit Security: Audit circuits for vulnerabilities
- Trusted Setup: Use secure multi-party computation for setup
- Input Validation: Validate all inputs before proof generation
- Resource Management: Set timeouts for proof generation
- Error Handling: Don't reveal sensitive information in errors
- Monitoring: Monitor proof generation success rates and performance
- 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.