├── src ├── chips │ ├── cpu │ │ ├── mod.rs │ │ ├── columns.rs │ │ ├── trace.rs │ │ └── air.rs │ ├── mod.rs │ ├── syscall │ │ ├── mod.rs │ │ ├── poseidon.rs │ │ └── sha256.rs │ ├── range │ │ └── mod.rs │ └── memory │ │ └── mod.rs ├── lib.rs ├── proof.rs ├── trace.rs ├── machine.rs ├── main.rs ├── prover.rs └── verifier.rs ├── LICENSE-MIT ├── benches └── prover_bench.rs ├── Cargo.toml ├── README.md └── LICENSE-APACHE /src/chips/cpu/mod.rs: -------------------------------------------------------------------------------- 1 | //! CPU Chip implementation 2 | //! 3 | //! The CPU chip handles ZK IR instruction execution with ~32 trace columns. 4 | 5 | mod air; 6 | mod columns; 7 | mod trace; 8 | 9 | pub use air::CpuChip; 10 | pub use columns::CpuColumns; 11 | pub use trace::generate_cpu_trace; 12 | -------------------------------------------------------------------------------- /src/chips/mod.rs: -------------------------------------------------------------------------------- 1 | //! Chip implementations for the ZK IR prover 2 | //! 3 | //! The prover uses a multi-chip architecture: 4 | //! - CPU Chip: Main execution with ~32 trace columns 5 | //! - Memory Chip: Memory consistency via sorted trace 6 | //! - Range Check Chip: 32-bit value validation 7 | //! - Syscall Chips: Dedicated chips for crypto operations 8 | 9 | pub mod cpu; 10 | pub mod memory; 11 | pub mod range; 12 | pub mod syscall; 13 | 14 | pub use cpu::CpuChip; 15 | pub use memory::MemoryChip; 16 | pub use range::RangeCheckChip; 17 | pub use syscall::{Poseidon2Chip, Sha256Chip}; 18 | -------------------------------------------------------------------------------- /src/chips/syscall/mod.rs: -------------------------------------------------------------------------------- 1 | //! Syscall Chips for cryptographic operations 2 | //! 3 | //! Dedicated chips for expensive cryptographic operations: 4 | //! - Poseidon2: Hash function (~200 constraints per hash) 5 | //! - SHA256: Hash function (~20,000 constraints per block) 6 | 7 | mod poseidon; 8 | mod sha256; 9 | 10 | pub use poseidon::Poseidon2Chip; 11 | pub use sha256::Sha256Chip; 12 | 13 | /// Common interface for syscall chips 14 | pub trait SyscallChip { 15 | /// Syscall code this chip handles 16 | fn syscall_code(&self) -> u32; 17 | 18 | /// Number of constraints per invocation 19 | fn constraints_per_call(&self) -> usize; 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ZK IR Prover v2.1 2 | //! 3 | //! STARK prover for ZK IR using Plonky3 with Baby Bear field. 4 | //! 5 | //! # Architecture 6 | //! 7 | //! The prover uses a multi-chip design: 8 | //! - CPU Chip: Executes ZK IR instructions (~32 trace columns) 9 | //! - Memory Chip: Enforces memory consistency 10 | //! - Range Check Chip: Validates 32-bit values 11 | //! - Syscall Chips: Dedicated chips for cryptographic operations 12 | 13 | pub mod chips; 14 | pub mod machine; 15 | pub mod proof; 16 | pub mod prover; 17 | pub mod trace; 18 | pub mod verifier; 19 | 20 | pub use machine::ZkIrMachine; 21 | pub use proof::{Proof, PublicInputs}; 22 | pub use prover::{Prover, ProverConfig}; 23 | pub use trace::ExecutionTrace; 24 | pub use verifier::Verifier; 25 | 26 | use p3_baby_bear::BabyBear; 27 | 28 | /// The field type used throughout the prover (Baby Bear: p = 2^31 - 2^27 + 1) 29 | pub type F = BabyBear; 30 | 31 | /// Baby Bear prime: 2^31 - 2^27 + 1 = 2013265921 32 | pub const BABY_BEAR_PRIME: u32 = 2013265921; 33 | 34 | /// Number of registers in the ZK IR VM 35 | pub const NUM_REGISTERS: usize = 32; 36 | 37 | /// Word size in bytes 38 | pub const WORD_SIZE: usize = 4; 39 | -------------------------------------------------------------------------------- /benches/prover_bench.rs: -------------------------------------------------------------------------------- 1 | //! Benchmarks for the ZK IR prover 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId}; 4 | 5 | use zkir_prover::{ExecutionTrace, Prover, ProverConfig}; 6 | use zkir_prover::trace::Step; 7 | 8 | /// Create a dummy execution trace with the given number of steps 9 | fn create_dummy_trace(num_steps: usize) -> ExecutionTrace { 10 | let mut trace = ExecutionTrace::new([0u8; 32]); 11 | 12 | for i in 0..num_steps { 13 | trace.steps.push(Step { 14 | pc: (i * 4) as u32, 15 | cycle: i as u64, 16 | opcode: 0b0110011, // ALU 17 | rd: 1, 18 | rs1: 2, 19 | rs2: 3, 20 | imm: 0, 21 | funct: 0, 22 | registers: [0u32; 32], 23 | }); 24 | } 25 | 26 | trace 27 | } 28 | 29 | fn bench_trace_generation(c: &mut Criterion) { 30 | let mut group = c.benchmark_group("trace_generation"); 31 | 32 | for size in [1 << 10, 1 << 14, 1 << 16] { 33 | group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { 34 | let trace = create_dummy_trace(size); 35 | let prover = Prover::new(ProverConfig::fast()); 36 | 37 | b.iter(|| { 38 | let _ = black_box(prover.prove(&trace)); 39 | }); 40 | }); 41 | } 42 | 43 | group.finish(); 44 | } 45 | 46 | fn bench_proving(c: &mut Criterion) { 47 | let mut group = c.benchmark_group("proving"); 48 | group.sample_size(10); // Proving is slow, use fewer samples 49 | 50 | for size in [1 << 10, 1 << 14] { 51 | group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { 52 | let trace = create_dummy_trace(size); 53 | let prover = Prover::new(ProverConfig::fast()); 54 | 55 | b.iter(|| { 56 | let proof = prover.prove(&trace).unwrap(); 57 | black_box(proof) 58 | }); 59 | }); 60 | } 61 | 62 | group.finish(); 63 | } 64 | 65 | criterion_group!(benches, bench_trace_generation, bench_proving); 66 | criterion_main!(benches); 67 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zkir-prover" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["seceq"] 6 | description = "STARK prover for ZK IR using Plonky3 with Baby Bear field" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/seceq/zkir-prover" 9 | 10 | [dependencies] 11 | # Plonky3 - STARK proving system (pinned to stable release) 12 | p3-air = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 13 | p3-baby-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 14 | p3-challenger = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 15 | p3-commit = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 16 | p3-dft = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 17 | p3-field = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 18 | p3-fri = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 19 | p3-keccak = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 20 | p3-matrix = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 21 | p3-merkle-tree = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 22 | p3-poseidon2 = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 23 | p3-symmetric = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 24 | p3-uni-stark = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 25 | p3-util = { git = "https://github.com/Plonky3/Plonky3.git", rev = "1ba4e5c" } 26 | 27 | # CLI 28 | clap = { version = "4", features = ["derive"] } 29 | 30 | # Serialization 31 | serde = { version = "1", features = ["derive"] } 32 | serde_json = "1" 33 | bincode = "1" 34 | 35 | # Error handling 36 | thiserror = "1" 37 | anyhow = "1" 38 | 39 | # Logging 40 | tracing = "0.1" 41 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 42 | 43 | # Utilities 44 | rand = "0.8" 45 | hex = "0.4" 46 | byteorder = "1" 47 | 48 | [dev-dependencies] 49 | criterion = "0.5" 50 | 51 | [[bench]] 52 | name = "prover_bench" 53 | harness = false 54 | 55 | [profile.release] 56 | lto = true 57 | codegen-units = 1 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZK IR Prover 2 | 3 | STARK prover for [ZK IR](https://github.com/seceq/zkir) using Plonky3 with Baby Bear field. 4 | 5 | ## Overview 6 | 7 | ZK IR Prover generates STARK proofs from execution traces of ZK IR programs. It uses a multi-chip architecture optimized for the 32-bit ZK IR instruction set. 8 | 9 | ### Key Features 10 | 11 | - **32-bit only** - No field registers, all operations on 32-bit integers 12 | - **Baby Bear field** - 31-bit prime (p = 2^31 - 2^27 + 1) 13 | - **Syscall-based crypto** - Dedicated chips for Poseidon2, SHA256 14 | - **~32 trace columns** - Minimal CPU chip design 15 | 16 | ### Architecture 17 | 18 | ``` 19 | ┌─────────────────────────────────────────────────────────────────────────┐ 20 | │ ZK IR Prover │ 21 | ├─────────────────────────────────────────────────────────────────────────┤ 22 | │ ExecutionTrace Multi-Chip Machine Proof │ 23 | │ ┌──────────────┐ ┌─────────────────────┐ ┌──────────────┐ │ 24 | │ │ Steps │ │ ┌───────────────┐ │ │ STARK Proof │ │ 25 | │ │ Syscalls │──────►│ │ CPU Chip │ │──────►│ │ │ 26 | │ │ Memory Log │ │ │ (32 columns) │ │ │ ~50-100 KB │ │ 27 | │ │ I/O │ │ └───────────────┘ │ │ │ │ 28 | │ └──────────────┘ │ ┌───────────────┐ │ │ Verify: ~10ms│ │ 29 | │ │ │ Memory Chip │ │ └──────────────┘ │ 30 | │ │ └───────────────┘ │ │ 31 | │ │ ┌───────────────┐ │ │ 32 | │ │ │ Syscall Chips │ │ │ 33 | │ │ │ (Poseidon,etc)│ │ │ 34 | │ │ └───────────────┘ │ │ 35 | │ └─────────────────────┘ │ 36 | └─────────────────────────────────────────────────────────────────────────┘ 37 | ``` 38 | 39 | ## Installation 40 | 41 | ```bash 42 | git clone https://github.com/seceq/zkir-prover 43 | cd zkir-prover 44 | cargo build --release 45 | ``` 46 | 47 | ## Usage 48 | 49 | ### Generate a proof 50 | 51 | ```bash 52 | zkir-prover prove --trace execution.zktrace -o proof.zkproof 53 | ``` 54 | 55 | ### Verify a proof 56 | 57 | ```bash 58 | zkir-prover verify proof.zkproof 59 | ``` 60 | 61 | ### Show proof info 62 | 63 | ```bash 64 | zkir-prover info proof.zkproof 65 | ``` 66 | 67 | ### Security levels 68 | 69 | - `--security fast` - ~80 bits, for testing 70 | - `--security default` - ~100 bits 71 | - `--security high` - ~128 bits 72 | 73 | ## Performance Estimates 74 | 75 | | Trace Size | Prove Time | Verify Time | Proof Size | 76 | |------------|------------|-------------|------------| 77 | | 2^16 (64K) | ~0.3s | ~10ms | ~50KB | 78 | | 2^20 (1M) | ~5s | ~15ms | ~80KB | 79 | | 2^24 (16M) | ~1.5min | ~20ms | ~120KB | 80 | 81 | ## Related Projects 82 | 83 | - [zkir](https://github.com/seceq/zkir) - ZK IR specification 84 | - [zkir-llvm](https://github.com/seceq/zkir-llvm) - LLVM to ZK IR compiler 85 | - [Plonky3](https://github.com/Plonky3/Plonky3) - STARK proving system 86 | 87 | ## License 88 | 89 | Licensed under either of: 90 | 91 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 92 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 93 | 94 | at your option. 95 | -------------------------------------------------------------------------------- /src/proof.rs: -------------------------------------------------------------------------------- 1 | //! Proof types and serialization 2 | 3 | use std::path::Path; 4 | 5 | use anyhow::Result; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// STARK proof for ZK IR execution 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | pub struct Proof { 11 | /// Commitments to trace polynomials 12 | pub trace_commitments: Vec, 13 | 14 | /// FRI proof 15 | pub fri_proof: FriProof, 16 | 17 | /// Opening proofs for evaluations 18 | pub openings: Vec, 19 | 20 | /// Public inputs and outputs 21 | pub public_inputs: PublicInputs, 22 | 23 | /// Prover configuration used 24 | pub config: ProofConfig, 25 | } 26 | 27 | impl Proof { 28 | /// Load a proof from a file 29 | pub fn load(path: &Path) -> Result { 30 | let data = std::fs::read(path)?; 31 | let proof: Self = bincode::deserialize(&data)?; 32 | Ok(proof) 33 | } 34 | 35 | /// Save the proof to a file 36 | pub fn save(&self, path: &Path) -> Result<()> { 37 | let data = bincode::serialize(self)?; 38 | std::fs::write(path, data)?; 39 | Ok(()) 40 | } 41 | 42 | /// Size of the serialized proof in bytes 43 | pub fn size_bytes(&self) -> usize { 44 | bincode::serialized_size(self).unwrap_or(0) as usize 45 | } 46 | } 47 | 48 | /// Commitment to a trace polynomial 49 | #[derive(Clone, Debug, Serialize, Deserialize)] 50 | pub struct TraceCommitment { 51 | /// Chip name this commitment is for 52 | pub chip: String, 53 | /// Merkle root of the committed polynomial 54 | pub root: [u8; 32], 55 | } 56 | 57 | /// FRI proof for low-degree testing 58 | #[derive(Clone, Debug, Serialize, Deserialize)] 59 | pub struct FriProof { 60 | /// Commitments to each FRI layer 61 | pub layer_commitments: Vec<[u8; 32]>, 62 | /// Final polynomial coefficients 63 | pub final_poly: Vec, 64 | /// Query round proofs 65 | pub query_proofs: Vec, 66 | /// Proof of work nonce (grinding) 67 | pub pow_nonce: u64, 68 | } 69 | 70 | /// Proof for a single FRI query 71 | #[derive(Clone, Debug, Serialize, Deserialize)] 72 | pub struct FriQueryProof { 73 | /// Query index 74 | pub index: usize, 75 | /// Values at queried positions 76 | pub values: Vec, 77 | /// Merkle authentication paths 78 | pub merkle_paths: Vec>, 79 | } 80 | 81 | /// Opening proof for polynomial evaluations 82 | #[derive(Clone, Debug, Serialize, Deserialize)] 83 | pub struct Opening { 84 | /// Evaluation point 85 | pub point: u64, 86 | /// Values of polynomials at this point 87 | pub values: Vec, 88 | /// Merkle authentication path 89 | pub merkle_path: Vec<[u8; 32]>, 90 | } 91 | 92 | /// Public inputs and outputs for verification 93 | #[derive(Clone, Debug, Serialize, Deserialize)] 94 | pub struct PublicInputs { 95 | /// Hash of the program being proven 96 | pub program_hash: [u8; 32], 97 | /// Public inputs to the program 98 | pub inputs: Vec, 99 | /// Public outputs from the program 100 | pub outputs: Vec, 101 | /// Number of execution cycles 102 | pub num_cycles: u64, 103 | } 104 | 105 | /// Proof configuration metadata 106 | #[derive(Clone, Debug, Serialize, Deserialize)] 107 | pub struct ProofConfig { 108 | /// Log2 of LDE blowup factor 109 | pub log_blowup: usize, 110 | /// Number of FRI queries 111 | pub num_queries: usize, 112 | /// Proof of work bits 113 | pub pow_bits: usize, 114 | } 115 | -------------------------------------------------------------------------------- /src/trace.rs: -------------------------------------------------------------------------------- 1 | //! Execution trace types for the ZK IR VM 2 | 3 | use std::path::Path; 4 | 5 | use anyhow::Result; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::NUM_REGISTERS; 9 | 10 | /// A single step of execution 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | pub struct Step { 13 | /// Program counter 14 | pub pc: u32, 15 | /// Cycle number 16 | pub cycle: u64, 17 | /// Opcode 18 | pub opcode: u8, 19 | /// Destination register 20 | pub rd: u8, 21 | /// Source register 1 22 | pub rs1: u8, 23 | /// Source register 2 24 | pub rs2: u8, 25 | /// Immediate value 26 | pub imm: i32, 27 | /// Function code (funct3 + funct7) 28 | pub funct: u8, 29 | /// Register file state after this step 30 | pub registers: [u32; NUM_REGISTERS], 31 | } 32 | 33 | /// A memory access record 34 | #[derive(Clone, Debug, Serialize, Deserialize)] 35 | pub struct MemoryAccess { 36 | /// Memory address 37 | pub address: u32, 38 | /// Cycle when access occurred 39 | pub cycle: u64, 40 | /// Value read or written 41 | pub value: u32, 42 | /// True if write, false if read 43 | pub is_write: bool, 44 | } 45 | 46 | /// A syscall record 47 | #[derive(Clone, Debug, Serialize, Deserialize)] 48 | pub struct SyscallRecord { 49 | /// Syscall code 50 | pub code: u32, 51 | /// Cycle when syscall was invoked 52 | pub cycle: u64, 53 | /// Input data (depends on syscall type) 54 | pub inputs: Vec, 55 | /// Output data (depends on syscall type) 56 | pub outputs: Vec, 57 | } 58 | 59 | /// Syscall codes 60 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 61 | #[repr(u32)] 62 | pub enum SyscallCode { 63 | Poseidon2 = 0x01, 64 | Keccak256 = 0x02, 65 | Sha256 = 0x03, 66 | Blake3 = 0x04, 67 | EcdsaVerify = 0x10, 68 | Ed25519Verify = 0x11, 69 | BigintAdd = 0x20, 70 | BigintMul = 0x21, 71 | } 72 | 73 | /// Complete execution trace 74 | #[derive(Clone, Debug, Serialize, Deserialize)] 75 | pub struct ExecutionTrace { 76 | /// Program bytecode hash 77 | pub program_hash: [u8; 32], 78 | /// Public inputs 79 | pub inputs: Vec, 80 | /// Public outputs 81 | pub outputs: Vec, 82 | /// Execution steps 83 | pub steps: Vec, 84 | /// Memory accesses (in execution order) 85 | pub memory_log: Vec, 86 | /// Syscall records 87 | pub syscalls: Vec, 88 | } 89 | 90 | impl ExecutionTrace { 91 | /// Create a new empty execution trace 92 | pub fn new(program_hash: [u8; 32]) -> Self { 93 | Self { 94 | program_hash, 95 | inputs: Vec::new(), 96 | outputs: Vec::new(), 97 | steps: Vec::new(), 98 | memory_log: Vec::new(), 99 | syscalls: Vec::new(), 100 | } 101 | } 102 | 103 | /// Load an execution trace from a file 104 | pub fn load(path: &Path) -> Result { 105 | let data = std::fs::read(path)?; 106 | let trace: Self = bincode::deserialize(&data)?; 107 | Ok(trace) 108 | } 109 | 110 | /// Save the execution trace to a file 111 | pub fn save(&self, path: &Path) -> Result<()> { 112 | let data = bincode::serialize(self)?; 113 | std::fs::write(path, data)?; 114 | Ok(()) 115 | } 116 | 117 | /// Number of execution cycles 118 | pub fn num_cycles(&self) -> u64 { 119 | self.steps.last().map(|s| s.cycle).unwrap_or(0) 120 | } 121 | 122 | /// Get memory accesses sorted by (address, cycle) for the memory chip 123 | pub fn sorted_memory_log(&self) -> Vec { 124 | let mut sorted = self.memory_log.clone(); 125 | sorted.sort_by(|a, b| { 126 | a.address.cmp(&b.address).then(a.cycle.cmp(&b.cycle)) 127 | }); 128 | sorted 129 | } 130 | 131 | /// Get syscalls by type 132 | pub fn syscalls_by_code(&self, code: SyscallCode) -> Vec<&SyscallRecord> { 133 | self.syscalls 134 | .iter() 135 | .filter(|s| s.code == code as u32) 136 | .collect() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/machine.rs: -------------------------------------------------------------------------------- 1 | //! Multi-chip machine orchestrating all chips 2 | 3 | use p3_field::Field; 4 | use p3_matrix::dense::RowMajorMatrix; 5 | 6 | use crate::chips::{CpuChip, MemoryChip, Poseidon2Chip, RangeCheckChip, Sha256Chip}; 7 | use crate::trace::ExecutionTrace; 8 | use crate::F; 9 | 10 | /// Multi-chip machine for ZK IR proving 11 | pub struct ZkIrMachine { 12 | /// CPU chip for instruction execution 13 | pub cpu: CpuChip, 14 | /// Memory chip for consistency checking 15 | pub memory: MemoryChip, 16 | /// Range check chip for 32-bit validation 17 | pub range: RangeCheckChip, 18 | /// Poseidon2 chip for hash syscalls 19 | pub poseidon: Poseidon2Chip, 20 | /// SHA256 chip for hash syscalls 21 | pub sha256: Sha256Chip, 22 | } 23 | 24 | impl Default for ZkIrMachine { 25 | fn default() -> Self { 26 | Self::new() 27 | } 28 | } 29 | 30 | impl ZkIrMachine { 31 | /// Create a new machine with default configuration 32 | pub fn new() -> Self { 33 | Self { 34 | cpu: CpuChip, 35 | memory: MemoryChip, 36 | range: RangeCheckChip::default(), 37 | poseidon: Poseidon2Chip::default(), 38 | sha256: Sha256Chip::default(), 39 | } 40 | } 41 | 42 | /// Generate all chip traces from an execution trace 43 | pub fn generate_traces(&self, trace: &ExecutionTrace) -> MachineTraces { 44 | // Generate individual chip traces 45 | let cpu_trace = self.cpu.generate_trace(trace); 46 | let memory_trace = self.memory.generate_trace(trace); 47 | 48 | // Collect all values that need range checking 49 | let range_values = self.collect_range_check_values(trace); 50 | let range_trace = self.range.generate_trace(&range_values); 51 | 52 | // Generate syscall traces 53 | let poseidon_trace = self.poseidon.generate_trace(&trace.syscalls); 54 | let sha256_trace = self.sha256.generate_trace(&trace.syscalls); 55 | 56 | MachineTraces { 57 | cpu: cpu_trace, 58 | memory: memory_trace, 59 | range: range_trace, 60 | poseidon: poseidon_trace, 61 | sha256: sha256_trace, 62 | } 63 | } 64 | 65 | /// Collect all values that need to be range-checked 66 | fn collect_range_check_values(&self, trace: &ExecutionTrace) -> Vec { 67 | let mut values = Vec::new(); 68 | 69 | // Register values 70 | for step in &trace.steps { 71 | values.extend_from_slice(&step.registers); 72 | } 73 | 74 | // Memory values 75 | for access in &trace.memory_log { 76 | values.push(access.address); 77 | values.push(access.value); 78 | } 79 | 80 | // Remove duplicates for efficiency 81 | values.sort(); 82 | values.dedup(); 83 | values 84 | } 85 | 86 | /// Get information about this machine's chips 87 | pub fn chip_info(&self) -> Vec { 88 | vec![ 89 | ChipInfo { 90 | name: "CPU", 91 | num_columns: 32, 92 | constraints_per_row: 48, 93 | }, 94 | ChipInfo { 95 | name: "Memory", 96 | num_columns: 7, 97 | constraints_per_row: 10, 98 | }, 99 | ChipInfo { 100 | name: "Range", 101 | num_columns: 6, 102 | constraints_per_row: 5, 103 | }, 104 | ChipInfo { 105 | name: "Poseidon2", 106 | num_columns: 35, 107 | constraints_per_row: 200, 108 | }, 109 | ChipInfo { 110 | name: "SHA256", 111 | num_columns: 20, 112 | constraints_per_row: 300, // per round, ~20k total per block 113 | }, 114 | ] 115 | } 116 | } 117 | 118 | /// All traces generated by the machine 119 | pub struct MachineTraces { 120 | pub cpu: RowMajorMatrix, 121 | pub memory: RowMajorMatrix, 122 | pub range: RowMajorMatrix, 123 | pub poseidon: RowMajorMatrix, 124 | pub sha256: RowMajorMatrix, 125 | } 126 | 127 | /// Information about a chip 128 | #[derive(Clone, Debug)] 129 | pub struct ChipInfo { 130 | pub name: &'static str, 131 | pub num_columns: usize, 132 | pub constraints_per_row: usize, 133 | } 134 | -------------------------------------------------------------------------------- /src/chips/range/mod.rs: -------------------------------------------------------------------------------- 1 | //! Range Check Chip implementation 2 | //! 3 | //! Validates that values are within valid 32-bit range using a lookup argument. 4 | //! Uses a multiplicative lookup table for efficiency. 5 | 6 | use std::borrow::{Borrow, BorrowMut}; 7 | use std::ops::Deref; 8 | 9 | use p3_air::{Air, AirBuilder, BaseAir}; 10 | use p3_field::{Field, FieldAlgebra}; 11 | use p3_matrix::dense::RowMajorMatrix; 12 | use p3_matrix::Matrix; 13 | 14 | /// Range check trace columns 15 | #[repr(C)] 16 | #[derive(Clone, Copy, Debug, Default)] 17 | pub struct RangeCheckColumns { 18 | /// Value to check (decomposed into bytes) 19 | pub value: T, 20 | /// Byte 0 (least significant) 21 | pub byte0: T, 22 | /// Byte 1 23 | pub byte1: T, 24 | /// Byte 2 25 | pub byte2: T, 26 | /// Byte 3 (most significant) 27 | pub byte3: T, 28 | /// Multiplicity: how many times this value appears 29 | pub multiplicity: T, 30 | } 31 | 32 | /// Number of columns in the range check trace 33 | pub const RANGE_CHECK_NUM_COLUMNS: usize = 6; 34 | 35 | impl RangeCheckColumns { 36 | pub const NUM_COLUMNS: usize = RANGE_CHECK_NUM_COLUMNS; 37 | } 38 | 39 | impl Borrow> for [T; RANGE_CHECK_NUM_COLUMNS] { 40 | fn borrow(&self) -> &RangeCheckColumns { 41 | unsafe { &*(self.as_ptr() as *const RangeCheckColumns) } 42 | } 43 | } 44 | 45 | impl BorrowMut> for [T; RANGE_CHECK_NUM_COLUMNS] { 46 | fn borrow_mut(&mut self) -> &mut RangeCheckColumns { 47 | unsafe { &mut *(self.as_mut_ptr() as *mut RangeCheckColumns) } 48 | } 49 | } 50 | 51 | /// Range Check Chip for validating 32-bit values 52 | pub struct RangeCheckChip { 53 | /// Maximum number of bits to check 54 | pub max_bits: usize, 55 | } 56 | 57 | impl Default for RangeCheckChip { 58 | fn default() -> Self { 59 | Self { max_bits: 32 } 60 | } 61 | } 62 | 63 | impl BaseAir for RangeCheckChip { 64 | fn width(&self) -> usize { 65 | RangeCheckColumns::::NUM_COLUMNS 66 | } 67 | } 68 | 69 | impl Air for RangeCheckChip { 70 | fn eval(&self, builder: &mut AB) { 71 | let main = builder.main(); 72 | let local_slice = main.row_slice(0); 73 | 74 | let local_arr: &[AB::Var; RANGE_CHECK_NUM_COLUMNS] = local_slice.deref().try_into().unwrap(); 75 | let local: &RangeCheckColumns = local_arr.borrow(); 76 | 77 | // Value decomposition: value = byte0 + 256*byte1 + 256^2*byte2 + 256^3*byte3 78 | let reconstructed = local.byte0.into() 79 | + local.byte1.into() * AB::Expr::from_canonical_u32(256) 80 | + local.byte2.into() * AB::Expr::from_canonical_u32(256 * 256) 81 | + local.byte3.into() * AB::Expr::from_canonical_u32(256 * 256 * 256); 82 | 83 | builder.assert_eq(local.value, reconstructed); 84 | 85 | // Each byte is in range [0, 255] (enforced via lookup table argument) 86 | // The lookup table contains all values 0..255 87 | // Each byte column must be a member of this table 88 | 89 | // For now, we use a degree-256 constraint (product check) 90 | // In practice, this would be done via a log-derivative lookup argument 91 | } 92 | } 93 | 94 | impl RangeCheckChip { 95 | pub fn new(max_bits: usize) -> Self { 96 | Self { max_bits } 97 | } 98 | 99 | /// Generate the range check trace 100 | pub fn generate_trace(&self, values_to_check: &[u32]) -> RowMajorMatrix { 101 | let num_values = values_to_check.len(); 102 | let trace_len = num_values.next_power_of_two().max(2); 103 | 104 | let mut trace_values = vec![F::ZERO; trace_len * RangeCheckColumns::::NUM_COLUMNS]; 105 | 106 | for (i, &value) in values_to_check.iter().enumerate() { 107 | let row_offset = i * RangeCheckColumns::::NUM_COLUMNS; 108 | let row: &mut [F; RANGE_CHECK_NUM_COLUMNS] = (&mut trace_values[row_offset..row_offset + RangeCheckColumns::::NUM_COLUMNS]).try_into().unwrap(); 109 | let cols: &mut RangeCheckColumns = row.borrow_mut(); 110 | 111 | cols.value = F::from_canonical_u32(value); 112 | cols.byte0 = F::from_canonical_u32(value & 0xFF); 113 | cols.byte1 = F::from_canonical_u32((value >> 8) & 0xFF); 114 | cols.byte2 = F::from_canonical_u32((value >> 16) & 0xFF); 115 | cols.byte3 = F::from_canonical_u32((value >> 24) & 0xFF); 116 | cols.multiplicity = F::ONE; 117 | } 118 | 119 | RowMajorMatrix::new(trace_values, RangeCheckColumns::::NUM_COLUMNS) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/chips/cpu/columns.rs: -------------------------------------------------------------------------------- 1 | //! CPU trace column definitions (~32 columns) 2 | 3 | use std::borrow::{Borrow, BorrowMut}; 4 | 5 | /// CPU trace columns 6 | /// 7 | /// Total: 32 columns organized into logical groups 8 | #[repr(C)] 9 | #[derive(Clone, Copy, Debug, Default)] 10 | pub struct CpuColumns { 11 | // === State (4 columns) === 12 | /// Program counter 13 | pub pc: T, 14 | /// Next program counter 15 | pub next_pc: T, 16 | /// Cycle counter 17 | pub cycle: T, 18 | /// Halt flag (1 when halted) 19 | pub is_halted: T, 20 | 21 | // === Instruction decode (6 columns) === 22 | /// 7-bit opcode 23 | pub opcode: T, 24 | /// Destination register (rd) 25 | pub rd: T, 26 | /// Source register 1 (rs1) 27 | pub rs1: T, 28 | /// Source register 2 (rs2) 29 | pub rs2: T, 30 | /// Immediate value 31 | pub imm: T, 32 | /// Function code (funct3 + funct7 combined) 33 | pub funct: T, 34 | 35 | // === Operand values (3 columns) === 36 | /// Value of rs1 register 37 | pub rs1_val: T, 38 | /// Value of rs2 register 39 | pub rs2_val: T, 40 | /// Value to write to rd register 41 | pub rd_val: T, 42 | 43 | // === Opcode flags (12 columns, one-hot) === 44 | /// ALU operation (ADD, SUB, MUL, etc.) 45 | pub is_alu: T, 46 | /// ALU immediate operation (ADDI, etc.) 47 | pub is_alu_imm: T, 48 | /// Branch instruction (BEQ, BNE, etc.) 49 | pub is_branch: T, 50 | /// Jump instruction (JAL, JALR) 51 | pub is_jump: T, 52 | /// Load instruction (LW, LH, LB) 53 | pub is_load: T, 54 | /// Store instruction (SW, SH, SB) 55 | pub is_store: T, 56 | /// LUI or AUIPC 57 | pub is_lui_auipc: T, 58 | /// System instruction (ECALL, EBREAK) 59 | pub is_system: T, 60 | /// ZK custom instruction (ASSERT, COMMIT) 61 | pub is_zk_custom: T, 62 | /// ZK I/O instruction (READ, WRITE) 63 | pub is_zk_io: T, 64 | /// HALT instruction 65 | pub is_halt: T, 66 | /// NOP (padding rows) 67 | pub is_nop: T, 68 | 69 | // === ALU operation (4 columns) === 70 | /// ALU operation selector 71 | pub alu_op: T, 72 | /// ALU result 73 | pub alu_result: T, 74 | /// Branch condition result (1 if taken) 75 | pub branch_taken: T, 76 | /// Comparison result (for SLT, etc.) 77 | pub comparison_result: T, 78 | 79 | // === Memory (3 columns) === 80 | /// Memory address for load/store 81 | pub mem_addr: T, 82 | /// Memory value 83 | pub mem_val: T, 84 | /// Memory operation type (1 = write, 0 = read) 85 | pub mem_is_write: T, 86 | } 87 | 88 | /// Number of columns in the CPU trace 89 | pub const CPU_NUM_COLUMNS: usize = 32; 90 | 91 | impl CpuColumns { 92 | /// Number of columns in the CPU trace 93 | pub const NUM_COLUMNS: usize = CPU_NUM_COLUMNS; 94 | } 95 | 96 | impl CpuColumns { 97 | /// Get all opcode flag columns as a slice 98 | pub fn opcode_flags(&self) -> [T; 12] { 99 | [ 100 | self.is_alu, 101 | self.is_alu_imm, 102 | self.is_branch, 103 | self.is_jump, 104 | self.is_load, 105 | self.is_store, 106 | self.is_lui_auipc, 107 | self.is_system, 108 | self.is_zk_custom, 109 | self.is_zk_io, 110 | self.is_halt, 111 | self.is_nop, 112 | ] 113 | } 114 | } 115 | 116 | // Allow converting between CpuColumns and [T; 32] 117 | impl Borrow> for [T; CPU_NUM_COLUMNS] { 118 | fn borrow(&self) -> &CpuColumns { 119 | // Safety: CpuColumns is repr(C) and has exactly NUM_COLUMNS fields of type T 120 | unsafe { &*(self.as_ptr() as *const CpuColumns) } 121 | } 122 | } 123 | 124 | impl BorrowMut> for [T; CPU_NUM_COLUMNS] { 125 | fn borrow_mut(&mut self) -> &mut CpuColumns { 126 | unsafe { &mut *(self.as_mut_ptr() as *mut CpuColumns) } 127 | } 128 | } 129 | 130 | impl Borrow<[T; CPU_NUM_COLUMNS]> for CpuColumns { 131 | fn borrow(&self) -> &[T; CPU_NUM_COLUMNS] { 132 | unsafe { &*(self as *const CpuColumns as *const [T; CPU_NUM_COLUMNS]) } 133 | } 134 | } 135 | 136 | /// ALU operation codes 137 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 138 | #[repr(u8)] 139 | pub enum AluOp { 140 | Add = 0, 141 | Sub = 1, 142 | And = 2, 143 | Or = 3, 144 | Xor = 4, 145 | Sll = 5, // Shift left logical 146 | Srl = 6, // Shift right logical 147 | Sra = 7, // Shift right arithmetic 148 | Slt = 8, // Set less than (signed) 149 | Sltu = 9, // Set less than (unsigned) 150 | Mul = 10, 151 | Div = 11, 152 | Divu = 12, 153 | Rem = 13, 154 | Remu = 14, 155 | } 156 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! ZK IR Prover CLI 2 | 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use clap::{Parser, Subcommand}; 7 | use tracing::info; 8 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 9 | 10 | use zkir_prover::{ExecutionTrace, Proof, Prover, ProverConfig, Verifier}; 11 | 12 | #[derive(Parser)] 13 | #[command(name = "zkir-prover")] 14 | #[command(about = "STARK prover for ZK IR", long_about = None)] 15 | struct Cli { 16 | #[command(subcommand)] 17 | command: Commands, 18 | 19 | /// Enable verbose output 20 | #[arg(short, long, global = true)] 21 | verbose: bool, 22 | } 23 | 24 | #[derive(Subcommand)] 25 | enum Commands { 26 | /// Generate a STARK proof from an execution trace 27 | Prove { 28 | /// Path to the execution trace file 29 | #[arg(short, long)] 30 | trace: PathBuf, 31 | 32 | /// Output path for the proof (defaults to .zkproof) 33 | #[arg(short, long)] 34 | output: Option, 35 | 36 | /// Security level: fast, default, or high 37 | #[arg(long, default_value = "default")] 38 | security: String, 39 | }, 40 | 41 | /// Verify a STARK proof 42 | Verify { 43 | /// Path to the proof file 44 | proof: PathBuf, 45 | }, 46 | 47 | /// Show information about a proof 48 | Info { 49 | /// Path to the proof file 50 | proof: PathBuf, 51 | }, 52 | } 53 | 54 | fn main() -> Result<()> { 55 | let cli = Cli::parse(); 56 | 57 | // Initialize tracing 58 | let filter = if cli.verbose { 59 | EnvFilter::new("debug") 60 | } else { 61 | EnvFilter::new("info") 62 | }; 63 | 64 | tracing_subscriber::registry() 65 | .with(filter) 66 | .with(tracing_subscriber::fmt::layer()) 67 | .init(); 68 | 69 | match cli.command { 70 | Commands::Prove { 71 | trace, 72 | output, 73 | security, 74 | } => { 75 | cmd_prove(trace, output, security)?; 76 | } 77 | Commands::Verify { proof } => { 78 | cmd_verify(proof)?; 79 | } 80 | Commands::Info { proof } => { 81 | cmd_info(proof)?; 82 | } 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | fn cmd_prove(trace_path: PathBuf, output: Option, security: String) -> Result<()> { 89 | info!("Loading execution trace from {:?}", trace_path); 90 | 91 | let config = match security.as_str() { 92 | "fast" => ProverConfig::fast(), 93 | "default" => ProverConfig::default(), 94 | "high" => ProverConfig::high(), 95 | _ => { 96 | anyhow::bail!("Unknown security level: {}. Use fast, default, or high", security); 97 | } 98 | }; 99 | 100 | info!("Using security level: {} ({:?})", security, config); 101 | 102 | // Load execution trace 103 | let trace = ExecutionTrace::load(&trace_path)?; 104 | info!("Loaded trace with {} cycles", trace.num_cycles()); 105 | 106 | // Generate proof 107 | let prover = Prover::new(config); 108 | info!("Generating proof..."); 109 | 110 | let proof = prover.prove(&trace)?; 111 | 112 | // Determine output path 113 | let output_path = output.unwrap_or_else(|| { 114 | let mut p = trace_path.clone(); 115 | p.set_extension("zkproof"); 116 | p 117 | }); 118 | 119 | // Save proof 120 | proof.save(&output_path)?; 121 | info!("Proof saved to {:?}", output_path); 122 | info!("Proof size: {} bytes", proof.size_bytes()); 123 | 124 | Ok(()) 125 | } 126 | 127 | fn cmd_verify(proof_path: PathBuf) -> Result<()> { 128 | info!("Loading proof from {:?}", proof_path); 129 | 130 | let proof = Proof::load(&proof_path)?; 131 | let verifier = Verifier::new(); 132 | 133 | info!("Verifying proof..."); 134 | match verifier.verify(&proof) { 135 | Ok(()) => { 136 | info!("Proof is VALID"); 137 | println!("Verification: PASSED"); 138 | } 139 | Err(e) => { 140 | info!("Proof is INVALID: {}", e); 141 | println!("Verification: FAILED - {}", e); 142 | std::process::exit(1); 143 | } 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | fn cmd_info(proof_path: PathBuf) -> Result<()> { 150 | let proof = Proof::load(&proof_path)?; 151 | 152 | println!("Proof Information"); 153 | println!("================="); 154 | println!("Size: {} bytes", proof.size_bytes()); 155 | println!(); 156 | println!("Public Inputs:"); 157 | println!(" Program hash: {}", hex::encode(&proof.public_inputs.program_hash)); 158 | println!(" Num cycles: {}", proof.public_inputs.num_cycles); 159 | println!(" Inputs: {:?}", proof.public_inputs.inputs); 160 | println!(" Outputs: {:?}", proof.public_inputs.outputs); 161 | 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /src/prover.rs: -------------------------------------------------------------------------------- 1 | //! STARK prover implementation 2 | 3 | use anyhow::Result; 4 | use thiserror::Error; 5 | use tracing::info; 6 | 7 | use crate::machine::ZkIrMachine; 8 | use crate::proof::{Proof, ProofConfig, PublicInputs}; 9 | use crate::trace::ExecutionTrace; 10 | 11 | /// Prover errors 12 | #[derive(Debug, Error)] 13 | pub enum ProverError { 14 | #[error("Empty trace: no execution steps")] 15 | EmptyTrace, 16 | 17 | #[error("Trace too large: {0} steps exceeds maximum")] 18 | TraceTooLarge(usize), 19 | 20 | #[error("Invalid trace: {0}")] 21 | InvalidTrace(String), 22 | 23 | #[error("Proving failed: {0}")] 24 | ProvingFailed(String), 25 | } 26 | 27 | /// Prover configuration 28 | #[derive(Clone, Debug)] 29 | pub struct ProverConfig { 30 | /// Log2 of LDE blowup factor (2 = 4x, 3 = 8x, 4 = 16x) 31 | pub log_blowup: usize, 32 | /// Number of FRI queries 33 | pub num_queries: usize, 34 | /// Proof of work bits for grinding 35 | pub pow_bits: usize, 36 | } 37 | 38 | impl ProverConfig { 39 | /// Fast configuration for testing (~80 bits security) 40 | pub fn fast() -> Self { 41 | Self { 42 | log_blowup: 2, // 4x 43 | num_queries: 20, 44 | pow_bits: 8, 45 | } 46 | } 47 | 48 | /// Default configuration (~100 bits security) 49 | pub fn default() -> Self { 50 | Self { 51 | log_blowup: 3, // 8x 52 | num_queries: 28, 53 | pow_bits: 16, 54 | } 55 | } 56 | 57 | /// High security configuration (~128 bits) 58 | pub fn high() -> Self { 59 | Self { 60 | log_blowup: 4, // 16x 61 | num_queries: 50, 62 | pow_bits: 20, 63 | } 64 | } 65 | } 66 | 67 | impl Default for ProverConfig { 68 | fn default() -> Self { 69 | Self::default() 70 | } 71 | } 72 | 73 | /// STARK prover for ZK IR 74 | pub struct Prover { 75 | /// Prover configuration 76 | config: ProverConfig, 77 | /// Multi-chip machine 78 | machine: ZkIrMachine, 79 | } 80 | 81 | impl Prover { 82 | /// Create a new prover with the given configuration 83 | pub fn new(config: ProverConfig) -> Self { 84 | Self { 85 | config, 86 | machine: ZkIrMachine::new(), 87 | } 88 | } 89 | 90 | /// Generate a STARK proof from an execution trace 91 | pub fn prove(&self, trace: &ExecutionTrace) -> Result { 92 | // Validate trace 93 | if trace.steps.is_empty() { 94 | return Err(ProverError::EmptyTrace); 95 | } 96 | 97 | info!("Generating traces for {} steps", trace.steps.len()); 98 | 99 | // Generate chip traces 100 | let traces = self.machine.generate_traces(trace); 101 | 102 | info!( 103 | "Generated traces - CPU: {}x{}, Memory: {}x{}, Range: {}x{}", 104 | traces.cpu.height(), 105 | traces.cpu.width(), 106 | traces.memory.height(), 107 | traces.memory.width(), 108 | traces.range.height(), 109 | traces.range.width(), 110 | ); 111 | 112 | // Build public inputs 113 | let public_inputs = PublicInputs { 114 | program_hash: trace.program_hash, 115 | inputs: trace.inputs.clone(), 116 | outputs: trace.outputs.clone(), 117 | num_cycles: trace.num_cycles(), 118 | }; 119 | 120 | // TODO: Implement actual STARK proving 121 | // 1. Commit to trace polynomials 122 | // 2. Compute constraint polynomials 123 | // 3. Commit to quotient polynomial 124 | // 4. Run FRI protocol 125 | // 5. Open at random points 126 | 127 | // Placeholder proof 128 | let proof = Proof { 129 | trace_commitments: vec![], 130 | fri_proof: crate::proof::FriProof { 131 | layer_commitments: vec![], 132 | final_poly: vec![], 133 | query_proofs: vec![], 134 | pow_nonce: 0, 135 | }, 136 | openings: vec![], 137 | public_inputs, 138 | config: ProofConfig { 139 | log_blowup: self.config.log_blowup, 140 | num_queries: self.config.num_queries, 141 | pow_bits: self.config.pow_bits, 142 | }, 143 | }; 144 | 145 | Ok(proof) 146 | } 147 | } 148 | 149 | /// Trait for objects that can compute their trace width 150 | pub trait TraceWidth { 151 | fn width(&self) -> usize; 152 | fn height(&self) -> usize; 153 | } 154 | 155 | impl TraceWidth for p3_matrix::dense::RowMajorMatrix { 156 | fn width(&self) -> usize { 157 | p3_matrix::Matrix::width(self) 158 | } 159 | 160 | fn height(&self) -> usize { 161 | p3_matrix::Matrix::height(self) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/chips/syscall/poseidon.rs: -------------------------------------------------------------------------------- 1 | //! Poseidon2 Chip implementation 2 | //! 3 | //! Implements Poseidon2 hash function constraints. 4 | //! ~200 constraints per hash invocation. 5 | 6 | use p3_air::{Air, AirBuilder, BaseAir}; 7 | use p3_field::Field; 8 | use p3_matrix::dense::RowMajorMatrix; 9 | use p3_matrix::Matrix; 10 | 11 | use super::SyscallChip; 12 | use crate::trace::{SyscallCode, SyscallRecord}; 13 | 14 | /// Poseidon2 state width 15 | pub const POSEIDON2_WIDTH: usize = 16; 16 | /// Number of full rounds 17 | pub const POSEIDON2_FULL_ROUNDS: usize = 8; 18 | /// Number of partial rounds 19 | pub const POSEIDON2_PARTIAL_ROUNDS: usize = 14; 20 | 21 | /// Poseidon2 trace columns 22 | #[repr(C)] 23 | #[derive(Clone, Copy, Debug)] 24 | pub struct Poseidon2Columns { 25 | /// Cycle when syscall was invoked 26 | pub cycle: T, 27 | /// Round number (0 to FULL_ROUNDS + PARTIAL_ROUNDS) 28 | pub round: T, 29 | /// Is this a full round? 30 | pub is_full_round: T, 31 | /// Current state (16 field elements) 32 | pub state: [T; POSEIDON2_WIDTH], 33 | /// State after S-box application 34 | pub state_after_sbox: [T; POSEIDON2_WIDTH], 35 | } 36 | 37 | impl Default for Poseidon2Columns { 38 | fn default() -> Self { 39 | Self { 40 | cycle: T::default(), 41 | round: T::default(), 42 | is_full_round: T::default(), 43 | state: [T::default(); POSEIDON2_WIDTH], 44 | state_after_sbox: [T::default(); POSEIDON2_WIDTH], 45 | } 46 | } 47 | } 48 | 49 | impl Poseidon2Columns { 50 | pub const NUM_COLUMNS: usize = 3 + POSEIDON2_WIDTH * 2; 51 | } 52 | 53 | /// Poseidon2 Chip for hash operations 54 | pub struct Poseidon2Chip { 55 | /// Round constants 56 | pub round_constants: Vec<[u32; POSEIDON2_WIDTH]>, 57 | /// MDS matrix (internal linear layer) 58 | pub mds_matrix: [[u32; POSEIDON2_WIDTH]; POSEIDON2_WIDTH], 59 | } 60 | 61 | impl Default for Poseidon2Chip { 62 | fn default() -> Self { 63 | Self::new() 64 | } 65 | } 66 | 67 | impl Poseidon2Chip { 68 | pub fn new() -> Self { 69 | // Initialize with placeholder constants 70 | // Real implementation would use proper Poseidon2 constants for Baby Bear 71 | let num_rounds = POSEIDON2_FULL_ROUNDS + POSEIDON2_PARTIAL_ROUNDS; 72 | let round_constants = vec![[0u32; POSEIDON2_WIDTH]; num_rounds]; 73 | let mds_matrix = [[0u32; POSEIDON2_WIDTH]; POSEIDON2_WIDTH]; 74 | 75 | Self { 76 | round_constants, 77 | mds_matrix, 78 | } 79 | } 80 | } 81 | 82 | impl SyscallChip for Poseidon2Chip { 83 | fn syscall_code(&self) -> u32 { 84 | SyscallCode::Poseidon2 as u32 85 | } 86 | 87 | fn constraints_per_call(&self) -> usize { 88 | 200 89 | } 90 | } 91 | 92 | impl BaseAir for Poseidon2Chip { 93 | fn width(&self) -> usize { 94 | Poseidon2Columns::::NUM_COLUMNS 95 | } 96 | } 97 | 98 | impl Air for Poseidon2Chip { 99 | fn eval(&self, builder: &mut AB) { 100 | let main = builder.main(); 101 | let _local = main.row_slice(0); 102 | let _next = main.row_slice(1); 103 | 104 | // Poseidon2 round constraints: 105 | // 1. S-box: x^7 (or x^5 depending on field) 106 | // 2. Linear layer (MDS matrix multiplication) 107 | // 3. Add round constants 108 | 109 | // For Baby Bear, we use S-box x^7 110 | // state_after_sbox[i] = state[i]^7 111 | 112 | // Full rounds: apply S-box to all elements 113 | // Partial rounds: apply S-box only to first element 114 | 115 | // This is a simplified placeholder - full implementation would have: 116 | // - S-box constraints for each element 117 | // - MDS matrix multiplication constraints 118 | // - Round constant addition 119 | // - Transition constraints between rounds 120 | } 121 | } 122 | 123 | impl Poseidon2Chip { 124 | /// Generate trace for Poseidon2 syscalls 125 | pub fn generate_trace(&self, syscalls: &[SyscallRecord]) -> RowMajorMatrix { 126 | let poseidon_calls: Vec<_> = syscalls 127 | .iter() 128 | .filter(|s| s.code == SyscallCode::Poseidon2 as u32) 129 | .collect(); 130 | 131 | let num_rounds = POSEIDON2_FULL_ROUNDS + POSEIDON2_PARTIAL_ROUNDS; 132 | let rows_per_call = num_rounds; 133 | let total_rows = poseidon_calls.len() * rows_per_call; 134 | let trace_len = total_rows.next_power_of_two().max(2); 135 | 136 | let values = vec![F::ZERO; trace_len * Poseidon2Columns::::NUM_COLUMNS]; 137 | 138 | // TODO: Populate trace with actual Poseidon2 computation 139 | // For each syscall: 140 | // - Initialize state from inputs 141 | // - Compute each round 142 | // - Store intermediate states 143 | 144 | RowMajorMatrix::new(values, Poseidon2Columns::::NUM_COLUMNS) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/verifier.rs: -------------------------------------------------------------------------------- 1 | //! STARK verifier implementation 2 | 3 | use thiserror::Error; 4 | use tracing::info; 5 | 6 | use crate::proof::Proof; 7 | 8 | /// Verification errors 9 | #[derive(Debug, Error)] 10 | pub enum VerifyError { 11 | #[error("Invalid proof format: {0}")] 12 | InvalidFormat(String), 13 | 14 | #[error("FRI verification failed: {0}")] 15 | FriVerificationFailed(String), 16 | 17 | #[error("Constraint check failed at {chip} row {row}: {constraint}")] 18 | ConstraintFailed { 19 | chip: String, 20 | row: usize, 21 | constraint: String, 22 | }, 23 | 24 | #[error("Merkle path verification failed")] 25 | MerklePathFailed, 26 | 27 | #[error("Proof of work verification failed")] 28 | PowFailed, 29 | 30 | #[error("Public input mismatch: {0}")] 31 | PublicInputMismatch(String), 32 | } 33 | 34 | /// STARK verifier for ZK IR proofs 35 | pub struct Verifier; 36 | 37 | impl Default for Verifier { 38 | fn default() -> Self { 39 | Self::new() 40 | } 41 | } 42 | 43 | impl Verifier { 44 | /// Create a new verifier 45 | pub fn new() -> Self { 46 | Self 47 | } 48 | 49 | /// Verify a STARK proof 50 | pub fn verify(&self, proof: &Proof) -> Result<(), VerifyError> { 51 | info!("Verifying proof for {} cycles", proof.public_inputs.num_cycles); 52 | 53 | // TODO: Implement actual verification 54 | // 1. Verify proof of work 55 | self.verify_pow(proof)?; 56 | 57 | // 2. Reconstruct challenges from Fiat-Shamir 58 | let _challenges = self.compute_challenges(proof); 59 | 60 | // 3. Verify FRI proof 61 | self.verify_fri(proof)?; 62 | 63 | // 4. Verify constraint evaluations 64 | self.verify_constraints(proof)?; 65 | 66 | // 5. Verify public inputs match committed values 67 | self.verify_public_inputs(proof)?; 68 | 69 | info!("Proof verified successfully"); 70 | Ok(()) 71 | } 72 | 73 | /// Verify proof of work 74 | fn verify_pow(&self, proof: &Proof) -> Result<(), VerifyError> { 75 | // TODO: Verify that hash(proof_data || pow_nonce) has required leading zeros 76 | let _pow_bits = proof.config.pow_bits; 77 | let _nonce = proof.fri_proof.pow_nonce; 78 | 79 | // Placeholder - always passes 80 | Ok(()) 81 | } 82 | 83 | /// Compute challenges using Fiat-Shamir 84 | fn compute_challenges(&self, proof: &Proof) -> Challenges { 85 | // TODO: Hash transcript to derive challenges 86 | // Include: trace commitments, public inputs, etc. 87 | Challenges { 88 | alpha: 0, 89 | beta: 0, 90 | gamma: 0, 91 | query_indices: vec![], 92 | } 93 | } 94 | 95 | /// Verify FRI proof 96 | fn verify_fri(&self, proof: &Proof) -> Result<(), VerifyError> { 97 | // TODO: Implement FRI verification 98 | // 1. Verify each layer's folding is correct 99 | // 2. Verify final polynomial has correct degree 100 | // 3. Verify query openings 101 | 102 | if proof.fri_proof.query_proofs.len() < proof.config.num_queries { 103 | return Err(VerifyError::FriVerificationFailed(format!( 104 | "Expected {} queries, got {}", 105 | proof.config.num_queries, 106 | proof.fri_proof.query_proofs.len() 107 | ))); 108 | } 109 | 110 | Ok(()) 111 | } 112 | 113 | /// Verify constraint evaluations 114 | fn verify_constraints(&self, proof: &Proof) -> Result<(), VerifyError> { 115 | // TODO: Verify that constraint polynomials evaluate to zero 116 | // at the claimed points 117 | 118 | // For each opening: 119 | // 1. Verify Merkle path 120 | // 2. Compute constraint evaluation from opened values 121 | // 3. Check that quotient * vanishing = constraint 122 | 123 | for opening in &proof.openings { 124 | self.verify_merkle_path(opening)?; 125 | } 126 | 127 | Ok(()) 128 | } 129 | 130 | /// Verify a Merkle authentication path 131 | fn verify_merkle_path(&self, opening: &crate::proof::Opening) -> Result<(), VerifyError> { 132 | // TODO: Implement Merkle path verification 133 | // Recompute root from leaf and path, compare to commitment 134 | Ok(()) 135 | } 136 | 137 | /// Verify public inputs match the proof 138 | fn verify_public_inputs(&self, proof: &Proof) -> Result<(), VerifyError> { 139 | // TODO: Verify that public inputs are correctly committed 140 | // in the trace 141 | 142 | // Check program hash is valid 143 | if proof.public_inputs.program_hash == [0u8; 32] { 144 | return Err(VerifyError::PublicInputMismatch( 145 | "Program hash cannot be zero".to_string(), 146 | )); 147 | } 148 | 149 | Ok(()) 150 | } 151 | } 152 | 153 | /// Challenges derived via Fiat-Shamir 154 | struct Challenges { 155 | /// Challenge for combining constraints 156 | alpha: u64, 157 | /// Challenge for permutation argument 158 | beta: u64, 159 | /// Challenge for lookup argument 160 | gamma: u64, 161 | /// Indices for FRI queries 162 | query_indices: Vec, 163 | } 164 | -------------------------------------------------------------------------------- /src/chips/cpu/trace.rs: -------------------------------------------------------------------------------- 1 | //! CPU trace generation from execution trace 2 | 3 | use std::borrow::BorrowMut; 4 | 5 | use p3_field::Field; 6 | use p3_matrix::dense::RowMajorMatrix; 7 | 8 | use super::columns::{CpuColumns, CPU_NUM_COLUMNS}; 9 | use crate::trace::{ExecutionTrace, Step}; 10 | 11 | /// Opcode constants matching ZK IR spec 12 | pub mod opcodes { 13 | // R-type (register-register) 14 | pub const OP_ALU: u8 = 0b0110011; 15 | // I-type (immediate) 16 | pub const OP_ALU_IMM: u8 = 0b0010011; 17 | pub const OP_LOAD: u8 = 0b0000011; 18 | pub const OP_JALR: u8 = 0b1100111; 19 | // S-type (store) 20 | pub const OP_STORE: u8 = 0b0100011; 21 | // B-type (branch) 22 | pub const OP_BRANCH: u8 = 0b1100011; 23 | // U-type (upper immediate) 24 | pub const OP_LUI: u8 = 0b0110111; 25 | pub const OP_AUIPC: u8 = 0b0010111; 26 | // J-type (jump) 27 | pub const OP_JAL: u8 = 0b1101111; 28 | // System 29 | pub const OP_SYSTEM: u8 = 0b1110011; 30 | // ZK custom 31 | pub const OP_ZK_CUSTOM: u8 = 0b0001011; 32 | pub const OP_ZK_IO: u8 = 0b0101011; 33 | pub const OP_HALT: u8 = 0b1111111; 34 | } 35 | 36 | /// Generate the CPU trace from an execution trace 37 | pub fn generate_cpu_trace(trace: &ExecutionTrace) -> RowMajorMatrix { 38 | let num_steps = trace.steps.len(); 39 | // Pad to next power of 2 40 | let trace_len = num_steps.next_power_of_two().max(2); 41 | 42 | let mut values = vec![F::ZERO; trace_len * CpuColumns::::NUM_COLUMNS]; 43 | 44 | for (i, step) in trace.steps.iter().enumerate() { 45 | let row_offset = i * CpuColumns::::NUM_COLUMNS; 46 | let row = &mut values[row_offset..row_offset + CpuColumns::::NUM_COLUMNS]; 47 | populate_row_from_step::(row, step, i == num_steps - 1); 48 | } 49 | 50 | // Fill padding rows with NOP 51 | for i in num_steps..trace_len { 52 | let row_offset = i * CpuColumns::::NUM_COLUMNS; 53 | let row = &mut values[row_offset..row_offset + CpuColumns::::NUM_COLUMNS]; 54 | populate_nop_row::(row, i as u64); 55 | } 56 | 57 | RowMajorMatrix::new(values, CpuColumns::::NUM_COLUMNS) 58 | } 59 | 60 | fn populate_row_from_step(row: &mut [F], step: &Step, is_last: bool) { 61 | let row_arr: &mut [F; CPU_NUM_COLUMNS] = row.try_into().unwrap(); 62 | let cols: &mut CpuColumns = row_arr.borrow_mut(); 63 | 64 | // State 65 | cols.pc = F::from_canonical_u32(step.pc); 66 | cols.cycle = F::from_canonical_u64(step.cycle); 67 | 68 | // Instruction decode 69 | cols.opcode = F::from_canonical_u32(step.opcode as u32); 70 | cols.rd = F::from_canonical_u32(step.rd as u32); 71 | cols.rs1 = F::from_canonical_u32(step.rs1 as u32); 72 | cols.rs2 = F::from_canonical_u32(step.rs2 as u32); 73 | cols.imm = F::from_canonical_u32(step.imm as u32); 74 | cols.funct = F::from_canonical_u32(step.funct as u32); 75 | 76 | // Operand values 77 | cols.rs1_val = F::from_canonical_u32(step.registers[step.rs1 as usize]); 78 | cols.rs2_val = F::from_canonical_u32(step.registers[step.rs2 as usize]); 79 | cols.rd_val = F::from_canonical_u32(step.registers[step.rd as usize]); 80 | 81 | // Set opcode flags (one-hot) 82 | reset_flags(cols); 83 | match step.opcode { 84 | opcodes::OP_ALU => cols.is_alu = F::ONE, 85 | opcodes::OP_ALU_IMM => cols.is_alu_imm = F::ONE, 86 | opcodes::OP_BRANCH => cols.is_branch = F::ONE, 87 | opcodes::OP_JAL | opcodes::OP_JALR => cols.is_jump = F::ONE, 88 | opcodes::OP_LOAD => cols.is_load = F::ONE, 89 | opcodes::OP_STORE => cols.is_store = F::ONE, 90 | opcodes::OP_LUI | opcodes::OP_AUIPC => cols.is_lui_auipc = F::ONE, 91 | opcodes::OP_SYSTEM => cols.is_system = F::ONE, 92 | opcodes::OP_ZK_CUSTOM => cols.is_zk_custom = F::ONE, 93 | opcodes::OP_ZK_IO => cols.is_zk_io = F::ONE, 94 | opcodes::OP_HALT => { 95 | cols.is_halt = F::ONE; 96 | cols.is_halted = F::ONE; 97 | } 98 | _ => cols.is_nop = F::ONE, 99 | } 100 | 101 | // Compute next_pc (simplified - actual implementation would check all cases) 102 | cols.next_pc = F::from_canonical_u32(step.pc.wrapping_add(4)); 103 | 104 | // ALU operation (would need to decode from funct) 105 | cols.alu_op = F::ZERO; // Placeholder 106 | cols.alu_result = cols.rd_val; // Simplified 107 | 108 | // Handle halt 109 | if is_last || step.opcode == opcodes::OP_HALT { 110 | cols.is_halted = F::ONE; 111 | cols.next_pc = cols.pc; 112 | } 113 | } 114 | 115 | fn populate_nop_row(row: &mut [F], cycle: u64) { 116 | let row_arr: &mut [F; CPU_NUM_COLUMNS] = row.try_into().unwrap(); 117 | let cols: &mut CpuColumns = row_arr.borrow_mut(); 118 | 119 | cols.cycle = F::from_canonical_u64(cycle); 120 | reset_flags(cols); 121 | cols.is_nop = F::ONE; 122 | } 123 | 124 | fn reset_flags(cols: &mut CpuColumns) { 125 | cols.is_alu = F::ZERO; 126 | cols.is_alu_imm = F::ZERO; 127 | cols.is_branch = F::ZERO; 128 | cols.is_jump = F::ZERO; 129 | cols.is_load = F::ZERO; 130 | cols.is_store = F::ZERO; 131 | cols.is_lui_auipc = F::ZERO; 132 | cols.is_system = F::ZERO; 133 | cols.is_zk_custom = F::ZERO; 134 | cols.is_zk_io = F::ZERO; 135 | cols.is_halt = F::ZERO; 136 | cols.is_nop = F::ZERO; 137 | } 138 | -------------------------------------------------------------------------------- /src/chips/memory/mod.rs: -------------------------------------------------------------------------------- 1 | //! Memory Chip implementation 2 | //! 3 | //! Enforces memory consistency using a sorted trace approach. 4 | //! Memory accesses are sorted by (address, cycle), and constraints ensure 5 | //! that reads return the most recently written value. 6 | 7 | use std::borrow::{Borrow, BorrowMut}; 8 | use std::ops::Deref; 9 | 10 | use p3_air::{Air, AirBuilder, BaseAir}; 11 | use p3_field::{Field, FieldAlgebra}; 12 | use p3_matrix::dense::RowMajorMatrix; 13 | use p3_matrix::Matrix; 14 | 15 | use crate::trace::ExecutionTrace; 16 | 17 | /// Memory trace columns 18 | #[repr(C)] 19 | #[derive(Clone, Copy, Debug, Default)] 20 | pub struct MemoryColumns { 21 | /// Memory address 22 | pub address: T, 23 | /// Cycle when access occurred 24 | pub cycle: T, 25 | /// Value read or written 26 | pub value: T, 27 | /// 1 if write, 0 if read 28 | pub is_write: T, 29 | 30 | // Helper columns for constraints 31 | /// 1 if this row has the same address as the next row 32 | pub same_addr_as_next: T, 33 | /// Inverse of (next_addr - addr) when addresses differ, used for range check 34 | pub addr_diff_inv: T, 35 | /// Inverse of (next_cycle - cycle) when same address 36 | pub cycle_diff_inv: T, 37 | } 38 | 39 | /// Number of columns in the memory trace 40 | pub const MEMORY_NUM_COLUMNS: usize = 7; 41 | 42 | impl MemoryColumns { 43 | pub const NUM_COLUMNS: usize = MEMORY_NUM_COLUMNS; 44 | } 45 | 46 | impl Borrow> for [T; MEMORY_NUM_COLUMNS] { 47 | fn borrow(&self) -> &MemoryColumns { 48 | unsafe { &*(self.as_ptr() as *const MemoryColumns) } 49 | } 50 | } 51 | 52 | impl BorrowMut> for [T; MEMORY_NUM_COLUMNS] { 53 | fn borrow_mut(&mut self) -> &mut MemoryColumns { 54 | unsafe { &mut *(self.as_mut_ptr() as *mut MemoryColumns) } 55 | } 56 | } 57 | 58 | /// Memory Chip enforcing read/write consistency 59 | pub struct MemoryChip; 60 | 61 | impl BaseAir for MemoryChip { 62 | fn width(&self) -> usize { 63 | MemoryColumns::::NUM_COLUMNS 64 | } 65 | } 66 | 67 | impl Air for MemoryChip { 68 | fn eval(&self, builder: &mut AB) { 69 | let main = builder.main(); 70 | let local_slice = main.row_slice(0); 71 | let next_slice = main.row_slice(1); 72 | 73 | let local_arr: &[AB::Var; MEMORY_NUM_COLUMNS] = local_slice.deref().try_into().unwrap(); 74 | let next_arr: &[AB::Var; MEMORY_NUM_COLUMNS] = next_slice.deref().try_into().unwrap(); 75 | let local: &MemoryColumns = local_arr.borrow(); 76 | let next: &MemoryColumns = next_arr.borrow(); 77 | 78 | // Boolean constraints 79 | builder.assert_zero(local.is_write.into() * (AB::Expr::ONE - local.is_write.into())); 80 | builder.assert_zero( 81 | local.same_addr_as_next.into() * (AB::Expr::ONE - local.same_addr_as_next.into()), 82 | ); 83 | 84 | // Address ordering: addresses are non-decreasing 85 | // When same_addr_as_next = 1: next.address = local.address 86 | builder 87 | .when_transition() 88 | .when(local.same_addr_as_next) 89 | .assert_eq(next.address, local.address); 90 | 91 | // When same_addr_as_next = 0: next.address > local.address 92 | // (enforced via range check on next.address - local.address - 1) 93 | 94 | // Cycle ordering within same address: cycles must be strictly increasing 95 | // Enforced via range check on (next.cycle - local.cycle - 1) 96 | builder 97 | .when_transition() 98 | .when(local.same_addr_as_next) 99 | .assert_zero( 100 | (next.cycle.into() - local.cycle.into() - AB::Expr::ONE) * local.cycle_diff_inv.into() 101 | - AB::Expr::ONE, 102 | ); 103 | 104 | // Read consistency: reads return last written value 105 | // If next row is a read at same address, its value must equal current value 106 | let next_is_read: AB::Expr = AB::Expr::ONE - next.is_write.into(); 107 | 108 | builder 109 | .when_transition() 110 | .when(local.same_addr_as_next) 111 | .when(next_is_read) 112 | .assert_eq(next.value, local.value); 113 | 114 | // First access to an address must be a write (or value must be zero) 115 | // TODO: implement constraint for initial memory state 116 | } 117 | } 118 | 119 | impl MemoryChip { 120 | /// Generate the memory trace sorted by (address, cycle) 121 | pub fn generate_trace(&self, trace: &ExecutionTrace) -> RowMajorMatrix { 122 | let sorted = trace.sorted_memory_log(); 123 | let num_accesses = sorted.len(); 124 | let trace_len = num_accesses.next_power_of_two().max(2); 125 | 126 | let mut values = vec![F::ZERO; trace_len * MemoryColumns::::NUM_COLUMNS]; 127 | 128 | for (i, access) in sorted.iter().enumerate() { 129 | let row_offset = i * MemoryColumns::::NUM_COLUMNS; 130 | let row: &mut [F; MEMORY_NUM_COLUMNS] = (&mut values[row_offset..row_offset + MemoryColumns::::NUM_COLUMNS]).try_into().unwrap(); 131 | let cols: &mut MemoryColumns = row.borrow_mut(); 132 | 133 | cols.address = F::from_canonical_u32(access.address); 134 | cols.cycle = F::from_canonical_u64(access.cycle); 135 | cols.value = F::from_canonical_u32(access.value); 136 | cols.is_write = if access.is_write { F::ONE } else { F::ZERO }; 137 | 138 | // Check if next row has same address 139 | if i + 1 < sorted.len() { 140 | cols.same_addr_as_next = if sorted[i + 1].address == access.address { 141 | F::ONE 142 | } else { 143 | F::ZERO 144 | }; 145 | } 146 | } 147 | 148 | RowMajorMatrix::new(values, MemoryColumns::::NUM_COLUMNS) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/chips/cpu/air.rs: -------------------------------------------------------------------------------- 1 | //! CPU AIR (Algebraic Intermediate Representation) constraints 2 | 3 | use std::borrow::Borrow; 4 | use std::ops::Deref; 5 | 6 | use p3_air::{Air, AirBuilder, BaseAir}; 7 | use p3_field::{Field, FieldAlgebra}; 8 | use p3_matrix::dense::RowMajorMatrix; 9 | use p3_matrix::Matrix; 10 | 11 | use super::columns::{CpuColumns, CPU_NUM_COLUMNS}; 12 | 13 | /// CPU Chip for ZK IR execution 14 | pub struct CpuChip; 15 | 16 | impl BaseAir for CpuChip { 17 | fn width(&self) -> usize { 18 | CpuColumns::::NUM_COLUMNS 19 | } 20 | } 21 | 22 | impl Air for CpuChip { 23 | fn eval(&self, builder: &mut AB) { 24 | let main = builder.main(); 25 | let local_slice = main.row_slice(0); 26 | let next_slice = main.row_slice(1); 27 | 28 | // Convert row slices to typed columns 29 | let local_arr: &[AB::Var; CPU_NUM_COLUMNS] = local_slice.deref().try_into().unwrap(); 30 | let next_arr: &[AB::Var; CPU_NUM_COLUMNS] = next_slice.deref().try_into().unwrap(); 31 | let local: &CpuColumns = local_arr.borrow(); 32 | let next: &CpuColumns = next_arr.borrow(); 33 | 34 | // Opcode flags must be one-hot (exactly one set) 35 | let flag_sum = local.is_alu.into() 36 | + local.is_alu_imm.into() 37 | + local.is_branch.into() 38 | + local.is_jump.into() 39 | + local.is_load.into() 40 | + local.is_store.into() 41 | + local.is_lui_auipc.into() 42 | + local.is_system.into() 43 | + local.is_zk_custom.into() 44 | + local.is_zk_io.into() 45 | + local.is_halt.into() 46 | + local.is_nop.into(); 47 | 48 | builder.assert_one(flag_sum); 49 | 50 | // Boolean constraints for flags 51 | self.assert_bool(builder, local.is_alu); 52 | self.assert_bool(builder, local.is_alu_imm); 53 | self.assert_bool(builder, local.is_branch); 54 | self.assert_bool(builder, local.is_jump); 55 | self.assert_bool(builder, local.is_load); 56 | self.assert_bool(builder, local.is_store); 57 | self.assert_bool(builder, local.is_lui_auipc); 58 | self.assert_bool(builder, local.is_system); 59 | self.assert_bool(builder, local.is_zk_custom); 60 | self.assert_bool(builder, local.is_zk_io); 61 | self.assert_bool(builder, local.is_halt); 62 | self.assert_bool(builder, local.is_nop); 63 | self.assert_bool(builder, local.is_halted); 64 | self.assert_bool(builder, local.branch_taken); 65 | self.assert_bool(builder, local.mem_is_write); 66 | 67 | // ALU operations write result to rd 68 | builder 69 | .when(local.is_alu) 70 | .assert_eq(local.rd_val, local.alu_result); 71 | 72 | // ALU immediate operations 73 | builder 74 | .when(local.is_alu_imm) 75 | .assert_eq(local.rd_val, local.alu_result); 76 | 77 | // PC transitions 78 | let pc_plus_4: AB::Expr = local.pc.into() + AB::Expr::from_canonical_u32(4); 79 | let pc_plus_imm: AB::Expr = local.pc.into() + local.imm.into(); 80 | 81 | // Sequential instructions: next_pc = pc + 4 82 | let is_sequential: AB::Expr = local.is_alu.into() 83 | + local.is_alu_imm.into() 84 | + local.is_load.into() 85 | + local.is_store.into() 86 | + local.is_lui_auipc.into() 87 | + local.is_zk_custom.into() 88 | + local.is_zk_io.into(); 89 | 90 | builder 91 | .when(is_sequential.clone()) 92 | .assert_eq(local.next_pc, pc_plus_4.clone()); 93 | 94 | // Branch logic: next_pc = branch_taken ? pc+imm : pc+4 95 | // When branch taken: next_pc = pc + imm 96 | // When not taken: next_pc = pc + 4 97 | let branch_target: AB::Expr = local.branch_taken.into() * pc_plus_imm.clone() 98 | + (AB::Expr::ONE - local.branch_taken.into()) * pc_plus_4.clone(); 99 | 100 | builder 101 | .when(local.is_branch) 102 | .assert_eq(local.next_pc, branch_target); 103 | 104 | // Jump instructions save return address in rd 105 | builder 106 | .when(local.is_jump) 107 | .assert_eq(local.rd_val, pc_plus_4.clone()); 108 | 109 | // JAL: next_pc = pc + imm (handled by funct disambiguation) 110 | // JALR: next_pc = rs1_val + imm (need additional constraint) 111 | 112 | // Halt behavior 113 | builder 114 | .when(local.is_halt) 115 | .assert_eq(local.next_pc, local.pc); 116 | 117 | // Once halted, stay halted 118 | builder 119 | .when(local.is_halted) 120 | .assert_one(next.is_halted); 121 | 122 | // PC continuity: next.pc = local.next_pc (except for padding) 123 | let not_halted_or_nop: AB::Expr = 124 | AB::Expr::ONE - local.is_halt.into() - local.is_nop.into(); 125 | 126 | builder 127 | .when_transition() 128 | .when(not_halted_or_nop) 129 | .assert_eq(next.pc, local.next_pc); 130 | 131 | // Cycle counter increments each non-NOP step 132 | builder 133 | .when_transition() 134 | .when(AB::Expr::ONE - local.is_nop.into()) 135 | .assert_eq(next.cycle, local.cycle.into() + AB::Expr::ONE); 136 | 137 | // Register r0 is always zero (enforced via register file lookup) 138 | 139 | // Memory constraints (linked via permutation with memory chip) 140 | // Load: mem_is_write = 0, mem_addr = rs1_val + imm, rd_val = mem_val 141 | builder.when(local.is_load).assert_zero(local.mem_is_write); 142 | 143 | // Store: mem_is_write = 1, mem_addr = rs1_val + imm, mem_val = rs2_val 144 | builder.when(local.is_store).assert_one(local.mem_is_write); 145 | } 146 | } 147 | 148 | impl CpuChip { 149 | /// Assert that a value is boolean (0 or 1) 150 | fn assert_bool(&self, builder: &mut AB, val: AB::Var) { 151 | // val * (1 - val) = 0 152 | builder.assert_zero(val.into() * (AB::Expr::ONE - val.into())); 153 | } 154 | 155 | /// Generate the trace matrix for this chip 156 | pub fn generate_trace( 157 | &self, 158 | trace: &crate::ExecutionTrace, 159 | ) -> RowMajorMatrix { 160 | super::trace::generate_cpu_trace(trace) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/chips/syscall/sha256.rs: -------------------------------------------------------------------------------- 1 | //! SHA256 Chip implementation 2 | //! 3 | //! Implements SHA256 hash function constraints. 4 | //! ~20,000 constraints per 512-bit block. 5 | 6 | use p3_air::{Air, AirBuilder, BaseAir}; 7 | use p3_field::{Field, FieldAlgebra}; 8 | use p3_matrix::dense::RowMajorMatrix; 9 | use p3_matrix::Matrix; 10 | 11 | use super::SyscallChip; 12 | use crate::trace::{SyscallCode, SyscallRecord}; 13 | 14 | /// SHA256 block size in 32-bit words 15 | pub const SHA256_BLOCK_WORDS: usize = 16; 16 | /// SHA256 rounds per block 17 | pub const SHA256_ROUNDS: usize = 64; 18 | /// SHA256 hash size in 32-bit words 19 | pub const SHA256_HASH_WORDS: usize = 8; 20 | 21 | /// SHA256 trace columns for one round 22 | #[repr(C)] 23 | #[derive(Clone, Copy, Debug)] 24 | pub struct Sha256Columns { 25 | /// Cycle when syscall was invoked 26 | pub cycle: T, 27 | /// Block index (for multi-block messages) 28 | pub block_idx: T, 29 | /// Round number (0-63) 30 | pub round: T, 31 | 32 | /// Working variables a-h 33 | pub a: T, 34 | pub b: T, 35 | pub c: T, 36 | pub d: T, 37 | pub e: T, 38 | pub f: T, 39 | pub g: T, 40 | pub h: T, 41 | 42 | /// Message schedule word for this round 43 | pub w: T, 44 | /// Round constant 45 | pub k: T, 46 | 47 | // Intermediate values for constraint efficiency 48 | /// Ch(e, f, g) = (e AND f) XOR (NOT e AND g) 49 | pub ch: T, 50 | /// Maj(a, b, c) = (a AND b) XOR (a AND c) XOR (b AND c) 51 | pub maj: T, 52 | /// Sigma0(a) 53 | pub sigma0: T, 54 | /// Sigma1(e) 55 | pub sigma1: T, 56 | /// temp1 = h + sigma1 + ch + k + w 57 | pub temp1: T, 58 | /// temp2 = sigma0 + maj 59 | pub temp2: T, 60 | } 61 | 62 | impl Default for Sha256Columns { 63 | fn default() -> Self { 64 | Self { 65 | cycle: T::default(), 66 | block_idx: T::default(), 67 | round: T::default(), 68 | a: T::default(), 69 | b: T::default(), 70 | c: T::default(), 71 | d: T::default(), 72 | e: T::default(), 73 | f: T::default(), 74 | g: T::default(), 75 | h: T::default(), 76 | w: T::default(), 77 | k: T::default(), 78 | ch: T::default(), 79 | maj: T::default(), 80 | sigma0: T::default(), 81 | sigma1: T::default(), 82 | temp1: T::default(), 83 | temp2: T::default(), 84 | } 85 | } 86 | } 87 | 88 | impl Sha256Columns { 89 | pub const NUM_COLUMNS: usize = 20; 90 | } 91 | 92 | /// SHA256 round constants 93 | pub const SHA256_K: [u32; 64] = [ 94 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 95 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 96 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 97 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 98 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 99 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 100 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 101 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, 102 | ]; 103 | 104 | /// SHA256 initial hash values 105 | pub const SHA256_H: [u32; 8] = [ 106 | 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 107 | 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, 108 | ]; 109 | 110 | /// SHA256 Chip for hash operations 111 | pub struct Sha256Chip; 112 | 113 | impl Default for Sha256Chip { 114 | fn default() -> Self { 115 | Self::new() 116 | } 117 | } 118 | 119 | impl Sha256Chip { 120 | pub fn new() -> Self { 121 | Self 122 | } 123 | } 124 | 125 | impl SyscallChip for Sha256Chip { 126 | fn syscall_code(&self) -> u32 { 127 | SyscallCode::Sha256 as u32 128 | } 129 | 130 | fn constraints_per_call(&self) -> usize { 131 | 20_000 132 | } 133 | } 134 | 135 | impl BaseAir for Sha256Chip { 136 | fn width(&self) -> usize { 137 | Sha256Columns::::NUM_COLUMNS 138 | } 139 | } 140 | 141 | impl Air for Sha256Chip { 142 | fn eval(&self, builder: &mut AB) { 143 | let main = builder.main(); 144 | let local = main.row_slice(0); 145 | let next = main.row_slice(1); 146 | 147 | // SHA256 round constraints: 148 | // 1. Ch computation: ch = (e & f) ^ (~e & g) 149 | // 2. Maj computation: maj = (a & b) ^ (a & c) ^ (b & c) 150 | // 3. Sigma0: rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22) 151 | // 4. Sigma1: rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25) 152 | // 5. temp1 = h + sigma1 + ch + k + w 153 | // 6. temp2 = sigma0 + maj 154 | // 7. State update: 155 | // h' = g, g' = f, f' = e, e' = d + temp1 156 | // d' = c, c' = b, b' = a, a' = temp1 + temp2 157 | 158 | // Note: Bit decomposition constraints are needed for rotations and XOR 159 | // This is where the ~20,000 constraints come from 160 | 161 | // Transition constraints for state update 162 | builder.when_transition().assert_eq( 163 | // next.a should equal temp1 + temp2 164 | // (simplified - actual implementation would include bit operations) 165 | AB::Expr::ZERO, 166 | AB::Expr::ZERO, 167 | ); 168 | } 169 | } 170 | 171 | impl Sha256Chip { 172 | /// Generate trace for SHA256 syscalls 173 | pub fn generate_trace(&self, syscalls: &[SyscallRecord]) -> RowMajorMatrix { 174 | let sha_calls: Vec<_> = syscalls 175 | .iter() 176 | .filter(|s| s.code == SyscallCode::Sha256 as u32) 177 | .collect(); 178 | 179 | // Each block requires 64 rounds 180 | let rows_per_block = SHA256_ROUNDS; 181 | // Estimate total blocks (assuming 512-bit input per call for simplicity) 182 | let total_rows = sha_calls.len() * rows_per_block; 183 | let trace_len = total_rows.next_power_of_two().max(2); 184 | 185 | let values = vec![F::ZERO; trace_len * Sha256Columns::::NUM_COLUMNS]; 186 | 187 | // TODO: Populate trace with actual SHA256 computation 188 | // For each syscall: 189 | // - Pad input to 512-bit blocks 190 | // - Initialize hash state 191 | // - For each block: 192 | // - Compute message schedule 193 | // - Execute 64 rounds 194 | // - Update hash state 195 | 196 | RowMajorMatrix::new(values, Sha256Columns::::NUM_COLUMNS) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to the Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no theory of 154 | liability, whether in contract, strict liability, or tort 155 | (including negligence or otherwise) arising in any way out of 156 | the use or inability to use the Work (even if such Holder or other 157 | party has been advised of the possibility of such damages), shall 158 | any Contributor be liable to You for damages, including any direct, 159 | indirect, special, incidental, or consequential damages of any 160 | character arising as a result of this License or out of the use or 161 | inability to use the Work (including but not limited to damages for 162 | loss of goodwill, work stoppage, computer failure or malfunction, or 163 | any and all other commercial damages or losses), even if such 164 | Contributor has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | --------------------------------------------------------------------------------