Integration Best Practices
Production-ready patterns and recommendations for integrating ZKVAULT into your Solana applications.
Architecture Patterns
Client-Side vs Server-Side Proving
Choose the right proving architecture based on your security and performance requirements.
| Aspect | Client-Side | Server-Side |
|---|---|---|
| Security | Higher (witness never leaves device) | Requires trusted server |
| Performance | Varies by device | Consistent, optimized |
| UX | May block UI during proving | Async, non-blocking |
| Cost | Free (user's compute) | Server infrastructure costs |
// Client-side proving (recommended for sensitive data)
async function clientSideProve(privateData: any) {
const circuit = await zkvault.circuit.load('myCircuit');
const witness = circuit.calculateWitness(privateData);
// Proving happens in browser
const proof = await zkvault.prover.prove(circuit, witness);
// Only proof and public inputs sent to chain
return proof;
}
// Server-side proving (for complex circuits)
async function serverSideProve(publicData: any) {
const response = await fetch('/api/prove', {
method: 'POST',
body: JSON.stringify({ publicData }),
});
const { proof, publicInputs } = await response.json();
return { proof, publicInputs };
}
// Hybrid approach: witness generation client-side, proving server-side
async function hybridProve(privateData: any) {
// Generate witness locally (keeps private data private)
const circuit = await zkvault.circuit.load('myCircuit');
const witness = circuit.calculateWitness(privateData);
// Send only witness to server for proving
const response = await fetch('/api/prove-from-witness', {
method: 'POST',
body: JSON.stringify({ witness: witness.toHex() }),
});
return await response.json();
}Error Handling
Robust Error Recovery
import { ZKVaultError, ErrorCode } from '@zkvault/sdk';
async function submitProofWithRetry(
proof: Proof,
maxRetries = 3
): Promise<string> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const signature = await zkvault.program.methods
.submitProof(proof.bytes, proof.publicInputs, metadata)
.accounts({ vault: vaultPda })
.rpc();
return signature;
} catch (error) {
if (error instanceof ZKVaultError) {
switch (error.code) {
case ErrorCode.INVALID_PROOF_FORMAT:
// Don't retry - proof is malformed
throw error;
case ErrorCode.INSUFFICIENT_COMPUTE_UNITS:
// Retry with higher CU limit
console.log(`Attempt ${attempt + 1}: Insufficient CU, retrying...`);
await increaseComputeUnits();
continue;
case ErrorCode.NETWORK_ERROR:
// Retry with exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt + 1}: Network error, retrying in ${delay}ms...`);
await sleep(delay);
continue;
case ErrorCode.VAULT_FROZEN:
// Don't retry - vault is frozen
throw new Error('Vault is frozen. Contact administrator.');
default:
throw error;
}
}
// Unknown error
if (attempt === maxRetries - 1) throw error;
}
}
throw new Error('Max retries exceeded');
}Graceful Degradation
// Fallback when ZK proof generation fails
async function submitWithFallback(data: SensitiveData) {
try {
// Try ZK proof flow
const proof = await generateProof(data);
await submitProof(proof);
return { method: 'zk-proof', status: 'success' };
} catch (error) {
console.warn('ZK proof failed, falling back to trusted oracle:', error);
// Fallback: Submit via trusted oracle with cryptographic attestation
const attestation = await trustedOracle.attest(data);
await submitAttestation(attestation);
return { method: 'oracle-attestation', status: 'fallback' };
}
}Performance Optimization
Proof Caching Strategy
import { LRUCache } from 'lru-cache';
// Cache proofs by input hash
const proofCache = new LRUCache<string, Proof>({
max: 100,
ttl: 1000 * 60 * 60, // 1 hour
updateAgeOnGet: true,
});
async function getOrGenerateProof(input: any): Promise<Proof> {
const inputHash = hashInput(input);
// Check cache first
const cached = proofCache.get(inputHash);
if (cached) {
console.log('Proof cache hit');
return cached;
}
// Generate new proof
console.log('Proof cache miss, generating...');
const proof = await zkvault.prover.prove(circuit, input);
// Cache for future use
proofCache.set(inputHash, proof);
return proof;
}
// Pre-generate common proofs
async function warmupCache(commonInputs: any[]) {
console.log('Warming up proof cache...');
await Promise.all(
commonInputs.map(input => getOrGenerateProof(input))
);
console.log(`Cache warmed up with ${commonInputs.length} proofs`);
}Circuit Optimization
// BAD: Inefficient constraint usage
template Inefficient() {
signal input a;
signal input b;
signal output c;
signal intermediate1 <== a * a;
signal intermediate2 <== b * b;
signal intermediate3 <== intermediate1 + intermediate2;
c <== intermediate3 * intermediate3; // Many intermediate signals
}
// GOOD: Optimized constraint usage
template Efficient() {
signal input a;
signal input b;
signal output c;
signal a2 <== a * a;
signal b2 <== b * b;
c <== (a2 + b2) * (a2 + b2); // Fewer constraints
}
// Use library templates when possible
include "circomlib/comparators.circom";
include "circomlib/bitify.circom";
// These are highly optimized
template OptimizedComparison() {
signal input a;
signal input b;
signal output result;
component lt = LessThan(252);
lt.in[0] <== a;
lt.in[1] <== b;
result <== lt.out;
}Parallel Processing
// Process multiple proofs in parallel
async function batchGenerateProofs(inputs: any[]): Promise<Proof[]> {
const workers = navigator.hardwareConcurrency || 4;
const chunks = chunkArray(inputs, Math.ceil(inputs.length / workers));
const proofChunks = await Promise.all(
chunks.map(async (chunk) => {
return Promise.all(
chunk.map(input => zkvault.prover.prove(circuit, input))
);
})
);
return proofChunks.flat();
}
// Worker-based proving for non-blocking UI
const proverWorker = new Worker('/workers/prover.js');
function proveInWorker(input: any): Promise<Proof> {
return new Promise((resolve, reject) => {
proverWorker.postMessage({ type: 'prove', input });
proverWorker.onmessage = (e) => {
if (e.data.type === 'proof-complete') {
resolve(e.data.proof);
} else if (e.data.type === 'error') {
reject(new Error(e.data.error));
}
};
});
}Security Best Practices
Input Validation
// Always validate inputs before proving
function validateProofInputs(input: ProofInput): boolean {
// Check input ranges
if (input.amount < 0 || input.amount > MAX_AMOUNT) {
throw new Error('Amount out of valid range');
}
// Verify nullifiers are unique
if (usedNullifiers.has(input.nullifier)) {
throw new Error('Nullifier already used (double-spend attempt)');
}
// Validate Merkle proof
if (!verifyMerkleProof(input.merkleProof, input.commitment)) {
throw new Error('Invalid Merkle proof');
}
// Check timestamp freshness
const age = Date.now() - input.timestamp;
if (age > MAX_PROOF_AGE) {
throw new Error('Proof inputs too old');
}
return true;
}
// Sanitize user inputs
function sanitizeInput(userInput: string): bigint {
// Remove any non-numeric characters
const cleaned = userInput.replace(/[^0-9]/g, '');
// Convert to bigint and validate range
const value = BigInt(cleaned);
if (value < 0n || value >= FIELD_MODULUS) {
throw new Error('Input value out of field range');
}
return value;
}Secure Key Management
// Store keys securely in the application
// For client-side: use secure storage
import { SecureStore } from '@zkvault/secure-storage';
async function storeWitness(witness: Witness) {
// Store encrypted in browser's secure storage
await SecureStore.set('witness', witness, {
encryption: 'AES-256-GCM',
authentication: true,
});
}
async function getWitness(): Promise<Witness> {
return await SecureStore.get('witness');
}
// Clear sensitive data after use
function clearSensitiveData() {
// Overwrite memory before garbage collection
if (witness) {
witness.data.fill(0);
witness = null;
}
}
// Use secure keypair storage
const keypair = await loadKeypairFromSecureStorage();Access Control
// Implement multi-layer access control
async function submitProofWithAccessControl(
proof: Proof,
user: PublicKey
) {
// 1. Verify user has permission
const hasPermission = await checkUserPermission(user, 'submit_proof');
if (!hasPermission) {
throw new Error('Insufficient permissions');
}
// 2. Rate limiting
const rateLimitOk = await checkRateLimit(user);
if (!rateLimitOk) {
throw new Error('Rate limit exceeded');
}
// 3. Verify proof format
const isValidFormat = validateProofFormat(proof);
if (!isValidFormat) {
throw new Error('Invalid proof format');
}
// 4. Check vault access policy
const vault = await getVault(vaultPda);
const canAccess = await vault.checkAccess(user);
if (!canAccess) {
throw new Error('Vault access denied');
}
// 5. Submit proof
return await zkvault.program.methods
.submitProof(proof.bytes, proof.publicInputs)
.accounts({ vault: vaultPda, authority: user })
.rpc();
}Testing Strategies
Comprehensive Test Coverage
import { expect } from 'chai';
import { ZKVaultTestHarness } from '@zkvault/testing';
describe('ZK Proof Integration', () => {
let harness: ZKVaultTestHarness;
before(async () => {
harness = await ZKVaultTestHarness.setup({
network: 'localnet',
circuits: ['./circuits/my_circuit.circom'],
});
});
it('generates valid proof for correct inputs', async () => {
const input = { a: 5, b: 10, c: 50 };
const proof = await harness.prove('my_circuit', input);
const isValid = await harness.verify(proof);
expect(isValid).to.be.true;
});
it('rejects invalid proofs', async () => {
const input = { a: 5, b: 10, c: 99 }; // Wrong output
await expect(
harness.prove('my_circuit', input)
).to.be.rejectedWith('Constraint not satisfied');
});
it('handles on-chain verification', async () => {
const input = { a: 5, b: 10, c: 50 };
const proof = await harness.prove('my_circuit', input);
const signature = await harness.submitProof(proof);
const status = await harness.getProofStatus(signature);
expect(status.verified).to.be.true;
expect(status.result).to.equal('valid');
});
it('respects compute unit limits', async () => {
const largeInput = generateLargeInput();
const proof = await harness.prove('complex_circuit', largeInput);
const estimate = await harness.estimateComputeUnits(proof);
expect(estimate).to.be.lessThan(400_000); // Under Solana limit
});
});Load Testing
// Simulate high-load scenarios
async function loadTest() {
const concurrency = 50;
const iterations = 100;
console.log(`Starting load test: ${concurrency} concurrent, ${iterations} iterations`);
const results = {
success: 0,
failure: 0,
totalTime: 0,
};
for (let i = 0; i < iterations; i++) {
const batch = Array(concurrency).fill(null).map((_, j) => ({
a: i * concurrency + j,
b: (i * concurrency + j) * 2,
}));
const startTime = Date.now();
const batchResults = await Promise.allSettled(
batch.map(input => generateAndSubmitProof(input))
);
const batchTime = Date.now() - startTime;
results.totalTime += batchTime;
batchResults.forEach(result => {
if (result.status === 'fulfilled') {
results.success++;
} else {
results.failure++;
console.error('Batch error:', result.reason);
}
});
console.log(`Iteration ${i + 1}/${iterations}: ${batchResults.filter(r => r.status === 'fulfilled').length}/${concurrency} succeeded`);
}
console.log('Load test complete:');
console.log(` Success: ${results.success}`);
console.log(` Failure: ${results.failure}`);
console.log(` Total time: ${results.totalTime}ms`);
console.log(` Avg time per proof: ${results.totalTime / (iterations * concurrency)}ms`);
}Monitoring and Observability
Instrumentation
import { metrics } from '@zkvault/monitoring';
// Track proof generation metrics
async function instrumentedProve(input: any): Promise<Proof> {
const startTime = Date.now();
try {
const proof = await zkvault.prover.prove(circuit, input);
const duration = Date.now() - startTime;
metrics.histogram('proof_generation_duration_ms', duration);
metrics.increment('proofs_generated_total', { status: 'success' });
return proof;
} catch (error) {
metrics.increment('proofs_generated_total', { status: 'error' });
metrics.increment('proof_errors_total', { error: error.name });
throw error;
}
}
// Monitor verification success rates
async function instrumentedVerify(proof: Proof): Promise<boolean> {
try {
const result = await zkvault.verify(proof);
metrics.increment('proofs_verified_total', {
result: result ? 'valid' : 'invalid',
});
return result;
} catch (error) {
metrics.increment('verification_errors_total', { error: error.name });
throw error;
}
}Deployment Checklist
Pre-Production Validation
- ✓ All circuits audited by cryptography experts
- ✓ Verification keys generated with secure setup ceremony
- ✓ Integration tests passing on devnet
- ✓ Load tests demonstrate adequate performance
- ✓ Error handling covers all failure modes
- ✓ Monitoring and alerting configured
- ✓ Rollback procedure documented
- ✓ Security audit completed
- ✓ Documentation updated
Go-Live Process
# 1. Deploy to mainnet-beta
anchor build --verifiable
anchor deploy --provider.cluster mainnet-beta
# 2. Upload verification keys
zkvault vk upload \
--vault <MAINNET_VAULT> \
--vk-file ./keys/production_vk.json \
--network mainnet-beta
# 3. Verify integration
npm run test:integration -- --network mainnet-beta
# 4. Enable monitoring
npm run monitoring:enable -- --network mainnet-beta
# 5. Gradual rollout
# Start with 1% of traffic
npm run deploy:canary -- --traffic 0.01
# Monitor for 24 hours, then increase
npm run deploy:canary -- --traffic 0.10
npm run deploy:canary -- --traffic 0.50
npm run deploy:canary -- --traffic 1.00