This document covers the zero-knowledge specific design decisions and constraints.
- ZK Compatibility: All code must run inside a ZK-VM
- Determinism: Identical inputs must produce identical outputs
- Efficiency: Minimize cycle count for faster proofs
- Auditability: Clear separation of concerns
The core library is designed to work without the standard library:
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;ZK-VMs like RISC Zero run in constrained environments without:
- System calls
- File I/O
- Network access
- Thread spawning
- Random number generation
All computation must be pure and deterministic.
| Feature | Standard Rust | no_std for ZK |
|---|---|---|
| Collections | std::collections |
alloc::collections |
| Strings | String |
alloc::string::String |
| Errors | std::error::Error |
Custom error types |
| Hash Maps | HashMap |
BTreeMap (deterministic) |
| Randomness | rand |
Not allowed |
| Time | std::time |
Provided via input |
The InMemoryDB uses BTreeMap instead of HashMap:
pub struct InMemoryDB {
// BTreeMap provides deterministic iteration order
pub accounts: BTreeMap<Address, AccountState>,
}This ensures:
- Same accounts always hash to same state root
- Iteration order is consistent across runs
- No randomness from hash map bucket ordering
Bincode serialization is used for deterministic encoding:
fn hash_struct<T: Serialize>(value: &T) -> Hash {
let bytes = bincode::serialize(value).unwrap();
keccak256(&bytes)
}Floating point operations are NOT used because:
- Different hardware may produce different results
- Rounding can vary between implementations
- EVM uses fixed-point arithmetic (U256)
Every CPU instruction costs cycles in the ZK-VM. Optimizations include:
Only compute hashes when needed:
impl ExecutionOutput {
// Hash computed on demand, not stored
pub fn hash(&self) -> Hash {
hash_struct(self)
}
}Reuse buffers where possible:
// Bad: Allocates new vec each time
fn process(data: &[u8]) -> Vec<u8> {
data.to_vec()
}
// Good: Minimize allocations
fn process(data: &[u8], out: &mut Vec<u8>) {
out.clear();
out.extend_from_slice(data);
}Simple algorithms often perform better in ZK:
- Linear search over small sets vs hash lookup
- Iterative over recursive solutions
- Fixed-size buffers over dynamic allocation
The guest trusts the pre-state provided by the host, but commits to it:
let pre_state_root = input.pre_state.compute_state_root();
// This becomes part of the commitmentThe on-chain verifier can check this pre-state root against known chain state.
After execution, the post-state is committed:
let post_state_root = output.post_state.compute_state_root();
// Included in ExecutionCommitmentThis creates a verifiable chain: pre_state → execution → post_state
Block-level parameters are provided as input:
pub struct BlockEnv {
pub number: u64,
pub timestamp: u64,
pub gas_limit: u64,
pub coinbase: Address,
pub base_fee: U256,
pub prev_randao: Hash,
pub chain_id: u64,
}All values must be provided—no syscalls to query chain state.
Gas is tracked but not enforced at the network level:
pub struct ExecutionOutput {
pub gas_used: u64,
pub gas_refunded: u64,
// ...
}The proof shows how much gas was used, but doesn't prevent execution.
Errors in the guest abort proof generation:
fn main() {
let result = ZkExecutor::execute(input);
match result {
Ok((output, commitment)) => {
commit_output(&commitment);
}
Err(_) => {
// Panic aborts the ZK-VM
panic!("EVM execution failed");
}
}
}The host should validate inputs before proving to avoid wasted cycles.
The guest has limited memory available:
| Region | Size | Purpose |
|---|---|---|
| Stack | 1 MB | Local variables, call frames |
| Heap | ~256 MB | Dynamic allocations |
| Code | ~16 MB | Guest program ELF |
Large state sets may exceed memory limits.
The host must validate:
- Pre-state is authentic (matches on-chain)
- Transaction is well-formed
- Block parameters are correct
Groth16 requires a trusted setup:
- RISC Zero provides the setup
- Parameters are publicly verifiable
- Compromise would affect all proofs
ZK proofs can leak information through:
- Cycle count (execution time)
- Memory access patterns
Shadow-EVM doesn't currently mitigate these—consider if privacy is required.
Core library tests run in standard environment:
cargo test -p shadow-evm-coreFull ZK execution tests:
# Uses dev mode for speed
cargo test -p shadow-evm-host --features testProfile cycle usage:
RISC0_DEV_MODE=0 cargo run --release -- execute --input test.json --verbose- Precompiles: Accelerated crypto operations
- Memory Pooling: Reduce allocation overhead
- Parallel Proving: Split large computations
- Custom opcodes: Skip expensive operations when safe