SP1 and zkVMs: A Security Auditor's Guide
Zero-Knowledge Virtual Machines (zkVMs) like SP1 are becoming increasingly prevalent in blockchain infrastructure, particularly for rollups and cross-chain protocols. As a security auditor, understanding these systems is crucial for identifying potential vulnerabilities and ensuring the integrity of zero-knowledge proof systems.
This guide provides a comprehensive overview of SP1's architecture, common security considerations, and practical auditing approaches specifically tailored for security professionals.
Pre-Requisites
To be able to review SP1 programs, in addition to this blog you need to have:
- An understanding of the source code language. This is usually Rust but SP1 programs can be written in anything that compiles to RISC-V. For example if the project is written in C++ you'll need to know C++ ...
- A strong understanding of the security considerations specific to the source code language. For example in Rust this is error handling, panics, memory allocations etc.
You do not need to have a complete knowledge of the underlying mathematics of Zero Knowledge proving systems, but it will help!
What is SP1 and Succinct Prover Network?
SP1 is a zero-knowledge virtual machine (zkVM) developed by Succinct Labs that allows developers to prove the execution of arbitrary Rust programs. Unlike traditional zero-knowledge proof systems that require writing circuits in specialised languages, SP1 enables developers to write standard Rust code and generate cryptographic proofs of its correct execution.
The Succinct Prover Network is a protocol where requesters can outsource the expensive proving to dedicated providers. These providers are rewarded in Succinct tokens ($PROVE) for their work.
SP1's Architecture: CPU-Like Design for Zero-Knowledge
SP1 is architected like a CPU that executes standard RISC-V programs, but with one crucial difference: every instruction execution is cryptographically proven. Here's how the system works:
1. Compilation: Rust code is compiled to standard RISC-V ELF binaries (the same format used by real RISC-V processors).
2. Execution & Proving: The SP1 zkVM executes these binaries instruction-by-instruction, generating STARK proofs that demonstrate correct execution of the program and attests to the public value.
3. Proof Optimisation: The STARK proofs are then "wrapped" into SNARKs, which are much smaller and cheaper to verify onchain.
4. Verification: Anyone can verify the final SNARK proof to confirm the program executed correctly, without needing to re-run the computation.
The SP1 Pipeline:
Stage | Input | Process | Output | Benefit |
---|---|---|---|---|
1 | Rust Code | Standard Compilation | RISC-V ELF | Developer Familiarity |
2 | RISC-V ELF | SP1 zkVM Execution | STARK Proof | Proven Execution |
3 | STARK Proof | Proof Wrapping | SNARK | Fast Proving |
4 | SNARK | Mathematical Verification | Trust Guarantee | Cheap Verification |
Key Benefits:
- Developer Familiarity: Use standard Rust (or other language) toolchain and libraries
- Performance: STARK proofs are generated efficiently for RISC-V instructions (though still not as efficient as application-specific circuits)
- Cost-Effective: SNARK wrapping makes onchain verification affordable
- Compatibility: Existing Rust codebases can be adapted with reduced overheads
This approach makes zero-knowledge proofs more accessible to developers while maintaining cryptographic guarantees of correctness and privacy.
Prover vs Verifier: Two Sides of the Proof System
Understanding the distinction between provers and verifiers is fundamental to zkVM security:
Prover
- Role: Executes the guest program and generates the zero-knowledge proof
- Trust Model: Considered potentially malicious or compromised
- Capabilities: Can provide arbitrary inputs to the guest program, control the host environment
- Output: Produces a cryptographic proof (receipt) attesting to correct execution
- Security Implication: Should not be able to fake a valid proof, but can manipulate inputs and host behaviour
Verifier
- Role: Validates the cryptographic proof without re-executing the program
- Trust Model: Relies only on cryptographic assumptions, not on the prover's honesty
- Input: The proof (receipt) and the program's verification key
- Output: Boolean result indicating proof validity
- Security Guarantee: If verification succeeds, the claimed computation definitely occurred correctly
Host vs Guest: Architectural Separation
SP1 programs are structured with a clear separation between trusted and untrusted components. Crucially, both host and guest code only execute during proving, the verifier performs pure mathematical verification without executing any source code.
Host Program
The host is the untrusted orchestrator that:
- Runs in a standard execution environment (your OS)
- Prepares inputs for the guest program
- Invokes the SP1 zkVM to generate proofs
- Is NOT part of the cryptographic proof
- Can be controlled by a malicious prover
Security Implication: Anything the host does is outside the cryptographic guarantees. A malicious actor controlling the host can provide arbitrary inputs to the guest program.
Guest Program
Guest programs runs inside a VM, they do not have access to an operating system. That means no internet connections, databases, files or operating system calls. These tasks must all be done by the host and shared as untrusted input.
The guest is the trusted program that:
- Contains the logic you want to prove
- Runs inside the SP1 zkVM (RISC-V environment)
- Derives a unique verification key (program commitment)
- Has its execution cryptographically proven
- Operates in a 32-bit RISC-V environment
Security Guarantee: Only the guest program's execution is proven. The verifier's mathematical checks validate only the guest code's execution, host code behaviour is never cryptographically verified. If verification succeeds, you can trust that this specific guest code executed correctly to produce the claimed outputs.
SP1 Code Patterns and Security Implications
Before diving into specific security considerations, let's examine the fundamental code patterns used in SP1 programs and their security implications.
Entry Point Declaration - Guest Program Starting Point
sp1_zkvm::entrypoint!(main);
- Purpose: Designates the function where the guest program begins the execution which will be proven
- Security Note: Only code reachable from this entry point is included in the proof
Reading Untrusted Input
let input_data = sp1_zkvm::io::read::<MyStruct>();
- Purpose: Reads private input data from the host
- Critical Security Consideration: This data is completely untrusted
- Best Practice: Always validate input data within the guest program
Committing Public Output
sp1_zkvm::io::commit_slice(&output_data);
- Purpose: Makes data publicly verifiable as part of the proof
- Security Guarantee: Committed data is cryptographically bound to the proof
- Use Case: State transitions, computation results that need public verification
Security Considerations for Auditors
1. All Input Data is Untrusted
Risk: The host can provide arbitrary inputs to the guest program.
Mitigation: Guest programs must validate all input data, including:
- Range checks for numerical values
- Length limits for collections
- Format validation for structured data
- Business logic constraints
Example - Input Validation Patterns:
sp1_zkvm::entrypoint!(main);
pub fn main() {
// Read potentially malicious inputs
let user_id = sp1_zkvm::io::read::<u32>();
let transfer_amount = sp1_zkvm::io::read::<u64>();
let recipient_address = sp1_zkvm::io::read::<[u8; 20]>();
let data_buffer = sp1_zkvm::io::read::<Vec<u8>>();
// CRITICAL: Validate all inputs
assert!(user_id > 0 && user_id <= 1_000_000, "Invalid user ID range");
assert!(transfer_amount > 0 && transfer_amount <= 1_000_000_000, "Invalid transfer amount");
assert!(!recipient_address.iter().all(|&b| b == 0), "Zero address not allowed");
assert!(data_buffer.len() <= 1024, "Data buffer too large");
// Additional business logic validation
assert!(transfer_amount >= 1000, "Minimum transfer amount not met");
// Now safely process the validated inputs
process_transfer(user_id, transfer_amount, recipient_address, data_buffer);
}
2. Only Guest Code is Proven
Risk: Host code can contain bugs or malicious logic that isn't covered by the proof.
Audit Focus: Ensure critical logic is implemented in the guest program, not the host.
Example - Proper Logic Separation:
// ❌ BAD: Critical logic in host (not proven)
fn host_main() {
let balance = get_user_balance(); // Host logic - not proven!
let amount = 1000;
if balance >= amount { // Critical check in host - vulnerable!
let proof_input = ProofInput { balance, amount };
generate_proof(proof_input);
}
}
// ✅ GOOD: Critical logic in guest (proven)
sp1_zkvm::entrypoint!(main);
pub fn main() {
let balance = sp1_zkvm::io::read::<u64>(); // Untrusted input
let amount = sp1_zkvm::io::read::<u64>();
// Critical validation happens in guest - cryptographically proven
assert!(balance >= amount, "Insufficient balance");
let new_balance = balance.checked_sub(amount).unwrap();
sp1_zkvm::io::commit(&new_balance);
}
3. 32-bit vs 64-bit Architecture Differences
Risk: SP1 uses 32-bit RISC-V, which can cause issues when porting from 64-bit systems.
Important Note: The Baby Bear field used by SP1's cryptographic system (~2^31) does NOT affect guest program integer type, usize
remains a full 32-bit unsigned integer (0 to 2^32 - 1).
Common Issues:
usize
is always 32 bits in the guest (regardless of cryptographic field size)- Pointer arithmetic differences
- Memory addressing limitations
- Integer overflow in calculations that work fine on 64-bit systems
Example - 32-bit Architecture Pitfalls:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let large_array_size = sp1_zkvm::io::read::<u64>();
// ❌ DANGEROUS: Potential truncation on 32-bit system
let vec_size = large_array_size as usize; // Silent truncation if > 2^32
let mut data = Vec::with_capacity(vec_size);
// ✅ SAFE: Explicit bounds checking for 32-bit environment
assert!(large_array_size <= u32::MAX as u64, "Array size too large for 32-bit system");
let safe_size: usize = large_array_size as usize;
let mut safe_data = Vec::with_capacity(safe_size);
// ❌ DANGEROUS: Large pointer arithmetic
let base_ptr = safe_data.as_ptr();
let offset = large_array_size; // Could be > usize::MAX
// let dangerous_ptr = unsafe { base_ptr.add(offset as usize) }; // Undefined behaviour
// ✅ SAFE: Check bounds before pointer arithmetic
assert!(offset < safe_data.len() as u64, "Offset exceeds array bounds");
let safe_ptr = unsafe { base_ptr.add(offset as usize) };
}
4. Third-Party Dependencies and Compatibility
Risk: Libraries and dependencies written for general platforms may assume access to the OS, 64-bit architecture, or other system behaviours that do not hold inside the SP1 zkVM. Using these dependencies unmodified can introduce subtle bugs, undefined behaviour, or security issues when compiled for RISC-V and executed in the zkVM.
Common Problem Patterns:
- Dependencies that call into the operating system (I/O, threading, file access, randomness) which are unavailable or behave differently in the zkVM
- Libraries that assume 64-bit types, pointer sizes, or rely on undefined integer-width behaviour
- Unsafe code or FFI that depends on native platform ABIs
- Use of system-specific features (clock/timers, environment variables, file descriptors)
Mitigations:
- Review dependencies: prefer small, well-understood crates or vendored copies that you can inspect and adapt
- Replace or stub OS-level behaviour with zkVM-compatible implementations (for example, use deterministic RNG provided by the host or framework ABI)
- Add explicit tests compiled for the RISC-V target and run them under the SP1 tooling or emulator
- Avoid or carefully review unsafe/FFI code; ensure ABI compatibility and deterministic behaviour
- Document any accepted platform assumptions and include them in the verification process
Audit Focus: Treat third-party code as untrusted with respect to SP1 constraints — verify it for deterministic, 32-bit-safe behaviour and remove any hidden platform assumptions.
5. Integer Overflow Vulnerabilities
Risk: Rust's default integer overflow behaviour differs between debug and release modes.
SP1-Specific Recommendation: Since panics are desirable in guest code, enable overflow checks in release mode by adding to the guest program's Cargo.toml
(not the host program):
[profile.release]
overflow-checks = true
Best Practices:
- Use checked arithmetic (
checked_add
,checked_mul
, etc.) - Explicitly handle overflow cases
- Be especially careful with user-provided numerical inputs
Type Casting Vulnerabilities: Overflow checks do NOT catch type casting issues, these silent truncations must be manually validated:
// 🌶️ Dangerous: Silent truncation
let large_value: u64 = u64::MAX;
let truncated: u32 = large_value as u32; // Silent loss of data
// Safe: Explicit validation
let safe_cast: u32 = large_value.try_into()
.expect("Value too large for u32"); // Will panic appropriately
6. Verification Key Management
What is a Verification Key: When SP1 programs are written and compiled, part of the compilation generates two keys (both are public key, anyone can find them). The first key is rather large and given to the prover, it's called the proving key. The proving key is used in proof generation and is only valid for a specific program. The second key is much smaller and is called the verification key, this key is required when verifying an SP1 program. Similarly, the verifier key is only valid for a specific program. Both the verifier key and proving key are derived from the source code, changing the source code changes the keys. Proofs will only verify correctly if the proof generation using the proving key is validated against the correct verifying key.
Critical Security Property: Each guest program has a unique verification key derived from its compiled binary.
Risks:
- Using wrong verification keys during proof validation
- Accepting proofs for outdated or vulnerable program versions
- Key substitution attacks
Audit Checklist:
- Verify that the correct verification key is used - the derivation of keys is deterministic for each program, as an auditor you should check this by regenerating them.
- Ensure keys are constant or follow correct update procedures - updating your key is the same as replacing the entire SP1 program
7. Public vs Private Data Leakage
Risk: Accidentally revealing private information through public outputs.
Best Practices:
- Only commit necessary data to public outputs
- Use zero-knowledge friendly data structures when needed
- Audit all
commit
andcommit_slice
calls
Example - Information Leakage Patterns:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let private_key = sp1_zkvm::io::read::<[u8; 32]>();
let user_balance = sp1_zkvm::io::read::<u64>();
let user_age = sp1_zkvm::io::read::<u32>();
let transaction_amount = sp1_zkvm::io::read::<u64>();
let salt = sp1_zkvm::io::read::<u128>();
// Verify user can make transaction (private computation)
let has_sufficient_balance = user_balance >= transaction_amount;
let is_adult = user_age >= 18;
let can_transact = has_sufficient_balance && is_adult;
// ❌ DANGEROUS: Leaking private information
sp1_zkvm::io::commit(&user_balance); // Reveals exact balance!
sp1_zkvm::io::commit(&user_age); // Reveals exact age!
sp1_zkvm::io::commit(&private_key); // Catastrophic leak!
// ✅ GOOD: Only commit necessary public information
sp1_zkvm::io::commit(&can_transact); // Only reveals boolean result
// ✅ ALTERNATIVE: Commit transaction hash instead of details (add random salt to prevent preimage brute force attacks)
let tx_hash = hash_transaction(transaction_amount, salt);
sp1_zkvm::io::commit(&tx_hash);
// ✅ GOOD: Commit proof of range without revealing exact value
let balance_sufficient = user_balance >= 1000; // Proves balance > threshold
sp1_zkvm::io::commit(&balance_sufficient);
}
8. Proof Replay and Uniqueness
Risk: Valid proofs might be replayed in unintended contexts.
Mitigations:
- Include context-specific data (block numbers, timestamps) in proofs and commit these to the public values
- Use nonces or unique identifiers
- Implement proper proof invalidation mechanisms
9. Liveness Bugs and Panic Handling
Key Distinction: Panics in host vs guest code have fundamentally different security implications.
Host Panics:
- Risk: Cause the proving process to fail, creating liveness issues
- Impact: Prevent valid proofs from being generated
- Best Practice: Implement proper error handling in host code
Guest Panics:
- Security Feature: Panics are the optimal way to handle invalid conditions in guest code
- Benefit: Prevent generation of proofs for invalid computations
- Design Pattern: Use
assert!
,panic!
, orunwrap()
to fail fast on invalid inputs
Critical Balance:
- Wanted: Guest panics on invalid inputs (security)
- Unwanted: Guest panics on valid inputs (liveness bug)
Audit Focus:
// ✅ Good: Panic on invalid conditions
assert!(user_balance >= withdrawal_amount, "Insufficient funds");
// ❌ Bad: Panic on valid edge cases that should be handled
let result = valid_computation().unwrap(); // Could panic on valid but unexpected inputs
10. Resource Exhaustion Attacks
Proving is expensive, in time and often money. The Succinct Prover Network still charges requesters for proving if the request is invalid. That means DoS vectors can cause financial cost to protocols.
Risk: Malicious inputs can cause excessive memory or computation consumption.
Common Attack Vectors:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let size = sp1_zkvm::io::read::<usize>();
// ❌ DANGEROUS: Unchecked memory allocation
let mut vec = Vec::with_capacity(size); // Could allocate gigabytes!
// ✅ SAFE: Bounded allocation
const MAX_SIZE: usize = 1_000_000; // 1MB limit
assert!(size <= MAX_SIZE, "Input size exceeds limit");
let mut safe_vec = Vec::with_capacity(size);
}
11. External Validation Requirements
Critical Principle: Some properties cannot or should not be validated inside the guest program.
Why External Validation is Needed:
- Computational efficiency (some checks are expensive in zkVM)
- Access limitations (guest cannot access external systems)
- Trust boundaries (some data sources are inherently external)
Common Example - Ethereum Block Hash Validation:
sp1_zkvm::entrypoint!(main);
pub fn main() {
// Read inputs from host
let block_hash = sp1_zkvm::io::read::<[u8; 32]>();
let merkle_proof = sp1_zkvm::io::read::<MerkleProof>();
let account_data = sp1_zkvm::io::read::<AccountData>();
// Can validate: Merkle proof against state root
assert!(merkle_proof.verify(&block_hash, &account_data), "Invalid merkle proof");
// Cannot validate: Whether block_hash is a valid Ethereum block
// Malicious prover could provide fake block_hash with crafted state root
// Must commit block_hash to public outputs for external validation
sp1_zkvm::io::commit(&block_hash);
sp1_zkvm::io::commit(&account_data);
}
Verifier-Side Validation Required:
// In Solidity smart contract verifier
function verifyAccountProof(bytes32 proofData) external {
(bytes32 blockHash, AccountData memory account) = abi.decode(proofData, (bytes32, AccountData));
// External validation: Check block hash is recent and valid
require(blockHash == blockhash(block.number - 1), "Invalid or stale block hash");
require(block.number - 1 > 0, "Genesis block not supported");
// Now we can trust the SP1 proof validated the merkle inclusion correctly
}
Audit Methodology:
- Identify Unvalidatable Properties in SP1: Look for data that requires external validation
- Review Public Commitment: Verify these values are committed to public outputs
- Check Verifier Logic: Confirm external validation exists in the verification code
- Common Unvalidatable Properties:
- Blockchain state (block hashes, state root hashes)
- External API responses
- Real-world events or oracles
- Cross-chain state
- Public Keys
Security Risk: If unvalidatable properties are not committed publicly and validated externally, malicious provers can provide arbitrary values and still generate valid proofs.
Practical Audit Approach
When auditing SP1 programs:
- Identify the Trust Boundary: Clearly distinguish between host and guest code
- Trace Input Validation: Follow all
io::read()
calls and ensure proper validation - Review Public Outputs: Examine all
io::commit()
calls for information leakage - Check Arithmetic: Look for potential overflows, especially in financial calculations
- Verify Determinism: Ensure all operations produce consistent results
- Validate Key Management: Confirm proper verification key handling in validators
- Test Edge Cases: Pay special attention to boundary conditions and error handling
Practical Insights from Security Auditing Experience
SP1 Program Structure Pattern
Most SP1 programs follow a three-phase logical structure (though not necessarily implemented linearly):
(a) Loading Initial State and Private Inputs
Since zkVMs cannot access external databases or APIs, all initial state must be loaded from the host into the guest. This phase is critical for security.
sp1_zkvm::entrypoint!(main);
pub fn main() {
// Phase 1: Load and validate all inputs
let merkle_root = sp1_zkvm::io::read::<[u8; 32]>();
let account_proofs = sp1_zkvm::io::read::<Vec<AccountProof>>();
let transactions = sp1_zkvm::io::read::<Vec<Transaction>>();
// CRITICAL: Validate initial state integrity
assert!(verify_merkle_root(&merkle_root, &account_proofs), "Invalid state root");
// Commit initial state hash for external verification
sp1_zkvm::io::commit_slice(&merkle_root);
}
(b) State Transition Logic
This is where the core computation happens, transforming initial state + private inputs → output state. Requires careful validation of edge cases and business logic.
// Phase 2: Execute state transitions
let mut new_state = initial_state.clone();
for transaction in transactions {
// Validate each transition step
assert!(transaction.amount > 0, "Invalid transaction amount");
assert!(new_state.get_balance(transaction.from) >= transaction.amount, "Insufficient balance");
// Apply state transition
new_state.transfer(transaction.from, transaction.to, transaction.amount);
}
(c) Outputting Public Values
Final phase commits the results to public outputs, which must then be validated by the verifier alongside the SP1 mathematical proof.
// Phase 3: Commit outputs for verification
let final_state_root = new_state.compute_root();
sp1_zkvm::io::commit_slice(&final_state_root);
sp1_zkvm::io::commit(&transaction_count);
The "Underconstrained" Problem in zkVMs
The most common vulnerability pattern in zero-knowledge circuits is "underconstrained circuits", insufficient validation that allows malicious inputs to produce valid proofs. This same problem applies to zkVM programs.
Common Underconstrained Patterns:
sp1_zkvm::entrypoint!(main);
pub fn main() {
let user_balance = sp1_zkvm::io::read::<u64>();
let withdrawal = sp1_zkvm::io::read::<u64>();
// ❌ UNDERCONSTRAINED: Missing critical validations
let new_balance = user_balance - withdrawal; // No overflow check!
sp1_zkvm::io::commit(&new_balance);
// ✅ PROPERLY CONSTRAINED: Comprehensive validation
assert!(user_balance >= withdrawal, "Insufficient balance");
assert!(withdrawal > 0, "Invalid withdrawal amount");
assert!(withdrawal <= MAX_WITHDRAWAL, "Exceeds withdrawal limit");
let new_balance = user_balance.checked_sub(withdrawal)
.expect("Arithmetic overflow in balance calculation");
sp1_zkvm::io::commit(&new_balance);
}
Audit Strategy: Systematically verify that every private input has appropriate constraints that prevent malicious state transitions. Ask: "What happens if this input is maliciously crafted?"
Security Audit Quick Reference
Common Vulnerability Patterns Checklist
Input Validation:
All sp1_zkvm::io::read()
calls followed by validation
Range checks on numerical inputs
Length limits on collections and strings
Business logic constraints enforced
Architecture:
Program verifier key managed securely
Critical logic implemented in guest (not host)
Proper separation of trusted/untrusted components
Host code has appropriate error handling
Third-party dependencies audited for SP1/32-bit compatibility
Data Handling:
No private data leaked through commit()
calls
External validation for unverifiable properties
Appropriate use of public values vs private inputs
Resource Management:
Memory allocations are bounded
Computation loops have reasonable limits
No potential for resource exhaustion attacks
Overflow Protection:
Guest Cargo.toml
has overflow-checks = true
Use of checked arithmetic where appropriate
Careful handling of type casting (u64 → u32)
Red Flags to Watch For
// Immediate security concerns requiring attention:
sp1_zkvm::io::read::<Vec<T>>(); // No length validation
user_input as usize; // Potential silent truncation
Vec::with_capacity(size); // Unbounded allocation
balance - amount; // No overflow check
sp1_zkvm::io::commit(&secret); // Information leakage
Mathematical Knowledge Requirements for Auditing
Good News: Deep mathematical knowledge of STARKs and SNARKs is not necessary to effectively audit SP1 programs. Most security vulnerabilities occur at the application logic level, not in the cryptographic primitives.
What You Need to Know:
- Basic Concept: Zero-knowledge proofs allow proving computation correctness without revealing private inputs
- Trust Model: The cryptographic math is sound (assuming no cryptographic breaks), but the program logic can still be flawed
- Proof Scope: Only the guest program execution is proven, input validation and business logic are your responsibility
What You Don't Need:
- Finite field arithmetic details
- Polynomial commitment schemes
- STARK/SNARK construction internals
- Cryptographic protocol security proofs
Focus Your Audit On:
// This is where bugs live - not in the cryptography
sp1_zkvm::entrypoint!(main);
pub fn main() {
let input = sp1_zkvm::io::read::<UserInput>();
// BUG TERRITORY: Application logic vulnerabilities
if input.user_type == "admin" { // String comparison vulnerability?
grant_admin_privileges(); // Logic flaw?
}
let result = process_payment(input.amount); // Integer overflow?
sp1_zkvm::io::commit(&result); // Information leakage?
}
// The SP1 zkVM handles the cryptographic proving automatically
Key Insight: Treat SP1 programs like any other critical system code, focus on input validation, business logic correctness, and proper error handling. The zero-knowledge cryptography is handled by the SP1 framework and is not typically where security issues arise.
Additional Resources
SP1 Documentation and Learning Materials (Recommended)
- SP1 Official Documentation - Comprehensive guide to SP1 development
- SP1 GitHub Repository - Source code, examples, and issue tracking
- Succinct Blog - The latest updates, case studies, and technical deep dives
- SP1 Examples Repository - Practical code examples and templates
- RareSkills ZK Bootcamp - General zero-knowledge development course
- ZK Podcast - Interviews and discussions on zero-knowledge technology
Zero-Knowledge Security Research (Optional - math + circuits)
- Common Vulnerabilities in ZK Proof Systems - Note: Mostly circuit-specific bugs which are handled by SP1's design team, not application developers
- ZK Security Database - Community-maintained database of ZK vulnerabilities and exploits
- Trail of Bits ZK Security Research - Professional security research on zero-knowledge systems
- 0xPARC ZK Learning Resources - Educational materials and research on ZK applications
- A16Z Crypto ZK Canon - Curated list of ZK papers and resources
- ZK-STARKs Overview - Background on STARK technology (optional reading)
- ZK Whiteboard Sessions - Technical video explanations of ZK concepts
Cryptographic Foundations (Optional - math)
- Introduction to Cryptography - Beginner-friendly introduction to cryptography fundamentals (no prior knowledge required)
- Moon Math Manual - All the ZK math you can dream of
- Vitalik's PLONK Explainer - Deep dive into SNARK construction
- Groth16 Paper - The foundational SNARK construction used in many systems
- STARK Paper - The STARK academic paper
Note: The cryptographic resources are provided for context, but remember that deep mathematical knowledge is not required for effective SP1 program auditing. Focus your study time on Rust security patterns and the SP1-specific considerations outlined in this guide.
Conclusion
SP1 and zkVMs provide powerful tools for creating verifiable computation systems, but they require careful security consideration. The separation between trusted guest programs and untrusted host environments creates unique attack vectors that traditional code audits might miss.
Key Takeaway: The cryptographic proof only guarantees that the guest program executed correctly, it doesn't guarantee that the program's logic is secure or that the inputs were legitimate. Thorough validation and proper architectural design remain essential for building secure zkVM applications.
By understanding these architectural patterns and potential pitfalls, security auditors can ensure that zkVM-based systems maintain their intended security properties.