Security Considerations

Critical security guidelines and threat models for production ZKVAULT deployments.

Threat Model

Attack Vectors

Attack TypeRisk LevelMitigation
Malicious Proof SubmissionHighOn-chain verification, input validation
Replay AttacksHighNullifiers, nonces, timestamps
Double SpendingCriticalNullifier tracking, Merkle tree validation
Side-Channel LeaksMediumConstant-time operations, secure memory
Circuit BugsCriticalFormal verification, audits, testing
VK SubstitutionHighVK 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 transparency

Nullifier 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)

Security Resources