Security Considerations
Critical security guidelines and threat models for production ZKVAULT deployments.
Threat Model
Attack Vectors
| Attack Type | Risk Level | Mitigation |
|---|---|---|
| Malicious Proof Submission | High | On-chain verification, input validation |
| Replay Attacks | High | Nullifiers, nonces, timestamps |
| Double Spending | Critical | Nullifier tracking, Merkle tree validation |
| Side-Channel Leaks | Medium | Constant-time operations, secure memory |
| Circuit Bugs | Critical | Formal verification, audits, testing |
| VK Substitution | High | VK hash commitment, authority checks |
Trust Assumptions
- Trusted Setup: Groth16 requires honest setup ceremony participant
- Circuit Correctness: Circuit must correctly encode intended logic
- Field Security: BN254 provides ~128-bit security
- Solana Runtime: Assumes honest validator majority
Circuit Security
Common Circuit Vulnerabilities
1. Under-Constrained Circuits
// VULNERABLE: Missing constraint allows any output
template Vulnerable() {
signal input a;
signal input b;
signal output c;
// BUG: No constraint on c!
// Prover can set c to any value
}
// SECURE: Properly constrained
template Secure() {
signal input a;
signal input b;
signal output c;
c <== a * b; // Constraint enforced
}2. Integer Overflow
// VULNERABLE: Addition can overflow field
template VulnerableAdd() {
signal input a;
signal input b;
signal output c;
c <== a + b; // Wraps around field modulus!
}
// SECURE: Range-checked addition
template SecureAdd() {
signal input a;
signal input b;
signal output c;
// Ensure inputs are in valid range
component rangeA = Num2Bits(252);
rangeA.in <== a;
component rangeB = Num2Bits(252);
rangeB.in <== b;
// Check sum doesn't overflow
component sum = Num2Bits(253);
sum.in <== a + b;
c <== a + b;
}3. Missing Input Validation
// VULNERABLE: No validation of merkle path
template VulnerableMerkle() {
signal input leaf;
signal input root;
signal input path[20];
// BUG: Doesn't verify path is valid!
root === leaf; // Wrong!
}
// SECURE: Proper Merkle verification
include "circomlib/poseidon.circom";
template SecureMerkle(levels) {
signal input leaf;
signal input root;
signal input pathElements[levels];
signal input pathIndices[levels];
component hashers[levels];
signal hashes[levels + 1];
hashes[0] <== leaf;
for (var i = 0; i < levels; i++) {
hashers[i] = Poseidon(2);
// Use path index to determine order
hashers[i].inputs[0] <== hashes[i] * (1 - pathIndices[i]) + pathElements[i] * pathIndices[i];
hashers[i].inputs[1] <== hashes[i] * pathIndices[i] + pathElements[i] * (1 - pathIndices[i]);
hashes[i + 1] <== hashers[i].out;
}
root === hashes[levels];
}Circuit Auditing Checklist
- All intermediate signals properly constrained
- No unconstrained outputs
- Range checks on all arithmetic operations
- Input validation for all public/private inputs
- No information leaks through constraint patterns
- Nullifier uniqueness enforced
- Replay protection implemented
- Edge cases handled (zero, max values)
Cryptographic Security
Setup Ceremony
// Multi-party computation for trusted setup
// REQUIREMENT: At least one participant must be honest
// Phase 1: Universal setup (reusable)
zkvault setup phase1 \
--output phase1.params \
--contribution-count 50 \
--entropy-source /dev/urandom
// Phase 2: Circuit-specific
zkvault setup phase2 \
--circuit circuit.r1cs \
--phase1 phase1.params \
--output vk.json
// Verification: Anyone can verify contributions
zkvault setup verify-ceremony \
--phase1 phase1.params \
--phase2 vk.json
// Output includes contribution hashes for transparencyNullifier Security
// Secure nullifier generation
function generateNullifier(secret: bigint, commitment: bigint): bigint {
// Use cryptographic hash to derive nullifier
const nullifier = poseidon([secret, commitment]);
// IMPORTANT: Never reuse nullifiers
// Each commitment must have unique nullifier
return nullifier;
}
// On-chain nullifier tracking
#[account]
pub struct NullifierSet {
pub nullifiers: Vec<[u8; 32]>,
pub merkle_tree: MerkleTree,
}
pub fn check_nullifier(nullifier: [u8; 32]) -> Result<()> {
require!(
!NULLIFIER_SET.contains(&nullifier),
ErrorCode::NullifierAlreadyUsed
);
NULLIFIER_SET.insert(nullifier);
Ok(())
}Randomness Requirements
// CRITICAL: Use cryptographically secure randomness
// WRONG: Predictable randomness
const bad_random = Math.random();
// CORRECT: Cryptographically secure
import { randomBytes } from 'crypto';
const secure_random = randomBytes(32);
// For proving: blinding factors must be random
const blindingFactor = BigInt('0x' + randomBytes(32).toString('hex'));
// For nullifiers: use unpredictable secrets
const secret = generateSecureSecret();
const nullifier = poseidon([secret, commitment]);Solana Program Security
Account Validation
use anchor_lang::prelude::*;
#[program]
pub mod secure_zkvault {
pub fn submit_proof(
ctx: Context<SubmitProof>,
proof: Vec<u8>,
public_inputs: Vec<u64>,
) -> Result<()> {
// 1. Verify signer authorization
require!(
ctx.accounts.authority.key() == ctx.accounts.vault.authority,
ErrorCode::Unauthorized
);
// 2. Validate vault state
require!(
ctx.accounts.vault.is_active && !ctx.accounts.vault.is_frozen,
ErrorCode::VaultNotActive
);
// 3. Validate proof size
require!(
proof.len() == EXPECTED_PROOF_SIZE,
ErrorCode::InvalidProofSize
);
// 4. Check for replay attacks
require!(
!ctx.accounts.vault.processed_proofs.contains(&compute_proof_hash(&proof)),
ErrorCode::ProofAlreadyProcessed
);
// 5. Validate public inputs
require!(
public_inputs.len() == ctx.accounts.vault.expected_input_count,
ErrorCode::InvalidInputCount
);
// 6. Verify PDA derivation
let (expected_pda, bump) = Pubkey::find_program_address(
&[b"vault", ctx.accounts.authority.key().as_ref()],
ctx.program_id
);
require!(
ctx.accounts.vault.key() == expected_pda,
ErrorCode::InvalidPDA
);
// Proceed with verification...
Ok(())
}
}Reentrancy Protection
// Use state flags to prevent reentrancy
#[account]
pub struct Vault {
pub is_locked: bool,
// ... other fields
}
pub fn verify_proof(ctx: Context<VerifyProof>) -> Result<()> {
let vault = &mut ctx.accounts.vault;
// Check lock
require!(!vault.is_locked, ErrorCode::Reentrant);
// Set lock
vault.is_locked = true;
// Perform verification and state changes
// ...
// Release lock
vault.is_locked = false;
Ok(())
}Integer Overflow Protection
use anchor_lang::prelude::*;
pub fn safe_add(a: u64, b: u64) -> Result<u64> {
a.checked_add(b)
.ok_or(ErrorCode::IntegerOverflow.into())
}
pub fn safe_mul(a: u64, b: u64) -> Result<u64> {
a.checked_mul(b)
.ok_or(ErrorCode::IntegerOverflow.into())
}
// Use in program
pub fn update_stats(ctx: Context<UpdateStats>, amount: u64) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.total_verified = safe_add(vault.total_verified, 1)?;
vault.total_amount = safe_add(vault.total_amount, amount)?;
Ok(())
}Key Management
Verification Key Security
// Store VK hash on-chain for integrity
#[account]
pub struct Vault {
pub vk_hash: [u8; 32],
// ...
}
pub fn update_verification_key(
ctx: Context<UpdateVK>,
new_vk: Vec<u8>,
) -> Result<()> {
// Only authority can update
require!(
ctx.accounts.authority.key() == ctx.accounts.vault.authority,
ErrorCode::Unauthorized
);
// Compute hash
let vk_hash = hash(&new_vk);
// Verify VK format
validate_vk_format(&new_vk)?;
// Update
ctx.accounts.vault.vk_hash = vk_hash;
ctx.accounts.vk_account.data = new_vk;
emit!(VKUpdated {
vault: ctx.accounts.vault.key(),
new_vk_hash: vk_hash,
});
Ok(())
}Private Key Protection
// NEVER log or expose private keys
// WRONG:
console.log('Secret:', secret);
console.log('Private key:', privateKey);
// CORRECT:
console.log('Public commitment:', commitment);
console.log('Nullifier (public):', nullifier);
// Clear sensitive data after use
function secureClear(buffer: Uint8Array) {
crypto.getRandomValues(buffer); // Overwrite with random
buffer.fill(0); // Then zero
}
// Use secure memory for sensitive operations
import { SecureBuffer } from '@zkvault/secure-memory';
const witness = new SecureBuffer(witnessData);
try {
const proof = await prover.prove(circuit, witness);
return proof;
} finally {
witness.clear(); // Always clear
}Monitoring and Incident Response
Security Monitoring
// Monitor for suspicious activity
const securityMetrics = {
failedVerifications: 0,
nullifierCollisions: 0,
invalidProofSubmissions: 0,
unauthorizedAccess: 0,
};
function monitorSecurity() {
// Alert on anomalies
if (securityMetrics.failedVerifications > THRESHOLD) {
alertSecurityTeam('High verification failure rate');
}
if (securityMetrics.nullifierCollisions > 0) {
alertSecurityTeam('CRITICAL: Nullifier collision detected!');
freezeVault();
}
// Log security events
logSecurityEvent({
timestamp: Date.now(),
metrics: securityMetrics,
});
}Emergency Procedures
// Implement circuit breaker pattern
#[account]
pub struct Vault {
pub is_frozen: bool,
pub freeze_reason: Option<String>,
pub frozen_at: Option<i64>,
}
pub fn emergency_freeze(
ctx: Context<EmergencyFreeze>,
reason: String,
) -> Result<()> {
// Only authority or guardian can freeze
require!(
ctx.accounts.signer.key() == ctx.accounts.vault.authority
|| ctx.accounts.signer.key() == ctx.accounts.vault.guardian,
ErrorCode::Unauthorized
);
let vault = &mut ctx.accounts.vault;
vault.is_frozen = true;
vault.freeze_reason = Some(reason.clone());
vault.frozen_at = Some(Clock::get()?.unix_timestamp);
emit!(VaultFrozen {
vault: vault.key(),
reason,
frozen_by: ctx.accounts.signer.key(),
});
Ok(())
}
// All operations check freeze status
pub fn submit_proof(ctx: Context<SubmitProof>) -> Result<()> {
require!(
!ctx.accounts.vault.is_frozen,
ErrorCode::VaultFrozen
);
// ...
}Audit Recommendations
Pre-Deployment Audits
- Circuit Audit: Formal verification of circuit logic
- Cryptography Audit: Review of proof system implementation
- Smart Contract Audit: Solana program security review
- Integration Testing: End-to-end security testing
- Penetration Testing: Attack simulation
Recommended Auditors
- Trail of Bits (circuit + smart contract)
- Least Authority (cryptography)
- Zellic (Solana programs)
- Runtime Verification (formal verification)