|
| 1 | +//! API: Export the SP1 shrink-verifier relation to LF-targeted R1LF + witness bundle. |
| 2 | +//! |
| 3 | +//! This is a **library** counterpart of the `dump_shrink_verify_constraints` binary, intended for |
| 4 | +//! downstream repos (like PVUGC) to call in-process (no subprocesses, no log scraping). |
| 5 | +//! |
| 6 | +//! Notes: |
| 7 | +//! - This is research plumbing; it writes the same on-disk formats that LF+ expects today. |
| 8 | +//! - Program selection uses the same environment variables as the binary: |
| 9 | +//! - `ELF_PATH` (optional; otherwise uses the default fibonacci example) |
| 10 | +//! - `ELF_STDIN_U32` (optional; default 10) |
| 11 | +//! - Optional caching (same as the binary): |
| 12 | +//! - `SHRINK_PROOF_CACHE=/path/to/shrink_proof.bin` |
| 13 | +//! - `REBUILD_SHRINK_PROOF=1` to force rebuild even if cache exists |
| 14 | +
|
| 15 | +use std::borrow::Borrow; |
| 16 | +use std::io::Write; |
| 17 | +use std::path::Path; |
| 18 | + |
| 19 | +use p3_baby_bear::{BabyBear, DiffusionMatrixBabyBear}; |
| 20 | +use p3_field::{PrimeField32, PrimeField64}; |
| 21 | +use sp1_core_executor::SP1Context; |
| 22 | +use sp1_core_machine::io::SP1Stdin; |
| 23 | +use sp1_core_machine::reduce::SP1ReduceProof; |
| 24 | +use sp1_recursion_circuit::machine::{PublicValuesOutputDigest, SP1CompressWithVKeyWitnessValues}; |
| 25 | +use sp1_recursion_circuit::witness::Witnessable; |
| 26 | +use sp1_recursion_compiler::config::InnerConfig; |
| 27 | +use sp1_recursion_compiler::ir::Builder; |
| 28 | +use sp1_recursion_compiler::r1cs::lf::lift_r1cs_to_lf_with_linear_carries_and_witness; |
| 29 | +use sp1_recursion_compiler::r1cs::R1CSCompiler; |
| 30 | +use sp1_recursion_compiler::{circuit::AsmCompiler, ir::DslIrProgram}; |
| 31 | +use sp1_recursion_core::Runtime; |
| 32 | +use sp1_stark::baby_bear_poseidon2::BabyBearPoseidon2; |
| 33 | +use sp1_stark::{StarkGenericConfig, SP1ProverOpts}; |
| 34 | + |
| 35 | +use crate::{utils::words_to_bytes, InnerSC, ShrinkAir, SP1Prover}; |
| 36 | + |
| 37 | +/// Export the shrink-verifier R1LF and witness bundle to the given paths. |
| 38 | +pub fn export_shrink_verifier(r1lf_path: &Path, witness_bundle_path: &Path) -> anyhow::Result<()> { |
| 39 | + // Build a concrete shrink proof input (vk+proof+merkle) so we can materialize a full witness. |
| 40 | + let prover: SP1Prover = SP1Prover::new(); |
| 41 | + let input_with_merkle = build_input_with_merkle(&prover)?; |
| 42 | + |
| 43 | + // Build verifier circuit ops with the real input (keeps shape identical to shape-only build). |
| 44 | + let machine_verified = ShrinkAir::shrink_machine(InnerSC::compressed()); |
| 45 | + let mut builder = Builder::<InnerConfig>::default(); |
| 46 | + let input = input_with_merkle.read(&mut builder); |
| 47 | + sp1_recursion_circuit::machine::SP1CompressRootVerifierWithVKey::verify( |
| 48 | + &mut builder, |
| 49 | + &machine_verified, |
| 50 | + input, |
| 51 | + true, |
| 52 | + PublicValuesOutputDigest::Reduce, |
| 53 | + ); |
| 54 | + let block = builder.into_root_block(); |
| 55 | + |
| 56 | + // Compile the same block and execute it in recursion runtime to fill memory. |
| 57 | + let dsl_program = unsafe { DslIrProgram::new_unchecked(block.clone()) }; |
| 58 | + let mut asm = AsmCompiler::<InnerConfig>::default(); |
| 59 | + let program = std::sync::Arc::new(asm.compile(dsl_program)); |
| 60 | + |
| 61 | + type F = <InnerSC as StarkGenericConfig>::Val; |
| 62 | + type EF = <InnerSC as StarkGenericConfig>::Challenge; |
| 63 | + let mut runtime = Runtime::<F, EF, DiffusionMatrixBabyBear>::new( |
| 64 | + program.clone(), |
| 65 | + sp1_stark::BabyBearPoseidon2Inner::new().perm, |
| 66 | + ); |
| 67 | + let mut witness_blocks = Vec::new(); |
| 68 | + Witnessable::<InnerConfig>::write(&input_with_merkle, &mut witness_blocks); |
| 69 | + let witness_blocks_for_fill = witness_blocks.clone(); |
| 70 | + runtime.witness_stream = witness_blocks.into(); |
| 71 | + runtime.run()?; |
| 72 | + |
| 73 | + // Compile to R1CS and generate the full witness in one pass. |
| 74 | + let hint_pos = std::cell::Cell::new(0usize); |
| 75 | + let mut next_hint_felt = || -> Option<BabyBear> { |
| 76 | + let pos = hint_pos.get(); |
| 77 | + let blk = witness_blocks_for_fill.get(pos)?; |
| 78 | + hint_pos.set(pos + 1); |
| 79 | + Some(blk.0[0]) |
| 80 | + }; |
| 81 | + let mut next_hint_ext = || -> Option<[BabyBear; 4]> { |
| 82 | + let pos = hint_pos.get(); |
| 83 | + let blk = witness_blocks_for_fill.get(pos)?; |
| 84 | + hint_pos.set(pos + 1); |
| 85 | + Some([blk.0[0], blk.0[1], blk.0[2], blk.0[3]]) |
| 86 | + }; |
| 87 | + let mut get_value = |id: &str| -> Option<BabyBear> { |
| 88 | + // Minimal subset of `parse_mem_id` logic: handle felt/var/ptr/ext... in the same shapes |
| 89 | + // the compiler emits. This matches the exporter binary. |
| 90 | + let (addr_u64, limb) = parse_mem_id(id)?; |
| 91 | + let vaddr: usize = addr_u64.try_into().ok()?; |
| 92 | + let &paddr = asm.virtual_to_physical.get(vaddr)?; |
| 93 | + let entry = runtime.memory.mr(paddr); |
| 94 | + entry.val.0.get(limb).copied() |
| 95 | + }; |
| 96 | + let (c, w_bb) = R1CSCompiler::<InnerConfig>::compile_with_witness( |
| 97 | + block.ops.clone(), |
| 98 | + &mut get_value, |
| 99 | + &mut next_hint_felt, |
| 100 | + &mut next_hint_ext, |
| 101 | + ); |
| 102 | + |
| 103 | + // Lift to LF-targeted R1LF and compute witness (u64) for that lifted instance. |
| 104 | + let (r1lf, _stats, w_lf_u64) = |
| 105 | + lift_r1cs_to_lf_with_linear_carries_and_witness(&c.r1cs, &w_bb)?; |
| 106 | + |
| 107 | + // Write R1LF. |
| 108 | + r1lf.save_to_file( |
| 109 | + r1lf_path |
| 110 | + .to_str() |
| 111 | + .ok_or_else(|| anyhow::anyhow!("non-utf8 r1lf path"))?, |
| 112 | + )?; |
| 113 | + |
| 114 | + // Extract (vk_hash, committed_values_digest) and write witness bundle. |
| 115 | + let (vk_hash, committed_values_digest) = extract_public_inputs_from_shrink(&input_with_merkle); |
| 116 | + write_witness_bundle(witness_bundle_path, &r1lf, &w_lf_u64, &vk_hash, &committed_values_digest)?; |
| 117 | + |
| 118 | + Ok(()) |
| 119 | +} |
| 120 | + |
| 121 | +fn build_input_with_merkle( |
| 122 | + prover: &SP1Prover, |
| 123 | +) -> anyhow::Result<SP1CompressWithVKeyWitnessValues<BabyBearPoseidon2>> { |
| 124 | + // Cache the shrink proof (vk + proof) to avoid regenerating it between runs. |
| 125 | + let cache_path = std::env::var("SHRINK_PROOF_CACHE").ok(); |
| 126 | + let force_rebuild = std::env::var("REBUILD_SHRINK_PROOF").ok().as_deref() == Some("1"); |
| 127 | + |
| 128 | + if let (Some(path), false) = (cache_path.as_deref(), force_rebuild) { |
| 129 | + if std::path::Path::new(path).exists() { |
| 130 | + let file = std::fs::File::open(path)?; |
| 131 | + let shrink: SP1ReduceProof<InnerSC> = bincode::deserialize_from(file)?; |
| 132 | + let input = sp1_recursion_circuit::machine::SP1CompressWitnessValues { |
| 133 | + vks_and_proofs: vec![(shrink.vk.clone(), shrink.proof.clone())], |
| 134 | + is_complete: true, |
| 135 | + }; |
| 136 | + return Ok(prover.make_merkle_proofs(input)); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + let elf_bytes: Vec<u8> = if let Ok(path) = std::env::var("ELF_PATH") { |
| 141 | + std::fs::read(&path)? |
| 142 | + } else { |
| 143 | + load_default_fibonacci_elf_bytes()? |
| 144 | + }; |
| 145 | + let opts = SP1ProverOpts::auto(); |
| 146 | + let context = SP1Context::default(); |
| 147 | + |
| 148 | + let (_, pk_d, program, vk) = prover.setup(&elf_bytes); |
| 149 | + let mut stdin = SP1Stdin::new(); |
| 150 | + let stdin_u32: u32 = std::env::var("ELF_STDIN_U32") |
| 151 | + .ok() |
| 152 | + .as_deref() |
| 153 | + .map(|s| s.parse().expect("failed to parse ELF_STDIN_U32 as u32")) |
| 154 | + .unwrap_or(10); |
| 155 | + stdin.write(&stdin_u32); |
| 156 | + let core_proof = prover.prove_core(&pk_d, program, &stdin, opts, context)?; |
| 157 | + let compressed = prover.compress(&vk, core_proof, vec![], opts)?; |
| 158 | + let shrink = prover.shrink(compressed, opts)?; |
| 159 | + |
| 160 | + let input = sp1_recursion_circuit::machine::SP1CompressWitnessValues { |
| 161 | + vks_and_proofs: vec![(shrink.vk.clone(), shrink.proof.clone())], |
| 162 | + is_complete: true, |
| 163 | + }; |
| 164 | + let input_with_merkle = prover.make_merkle_proofs(input); |
| 165 | + |
| 166 | + if let Some(path) = cache_path.as_deref() { |
| 167 | + let shrink = SP1ReduceProof::<InnerSC> { |
| 168 | + vk: shrink.vk, |
| 169 | + proof: shrink.proof, |
| 170 | + }; |
| 171 | + let file = std::fs::File::create(path)?; |
| 172 | + bincode::serialize_into(file, &shrink)?; |
| 173 | + } |
| 174 | + |
| 175 | + Ok(input_with_merkle) |
| 176 | +} |
| 177 | + |
| 178 | +fn load_default_fibonacci_elf_bytes() -> anyhow::Result<Vec<u8>> { |
| 179 | + // Mirror the binary's behavior: prefer the already-built fibonacci example ELF if present, |
| 180 | + // otherwise build it once. |
| 181 | + let prover_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
| 182 | + let examples_dir = prover_dir.join("../../examples"); |
| 183 | + let elf_path = examples_dir.join( |
| 184 | + "target/elf-compilation/riscv32im-succinct-zkvm-elf/release/fibonacci-program", |
| 185 | + ); |
| 186 | + if elf_path.exists() { |
| 187 | + return Ok(std::fs::read(&elf_path)?); |
| 188 | + } |
| 189 | + let status = std::process::Command::new("cargo") |
| 190 | + .arg("build") |
| 191 | + .arg("-p") |
| 192 | + .arg("fibonacci-script") |
| 193 | + .arg("--release") |
| 194 | + .current_dir(&examples_dir) |
| 195 | + .status()?; |
| 196 | + if !status.success() { |
| 197 | + anyhow::bail!( |
| 198 | + "failed to build default fibonacci ELF; run (cd sp1/examples && cargo build -p fibonacci-script --release) or set ELF_PATH" |
| 199 | + ); |
| 200 | + } |
| 201 | + Ok(std::fs::read(&elf_path)?) |
| 202 | +} |
| 203 | + |
| 204 | +fn extract_public_inputs_from_shrink( |
| 205 | + input: &SP1CompressWithVKeyWitnessValues<BabyBearPoseidon2>, |
| 206 | +) -> ([u8; 32], [u8; 32]) { |
| 207 | + let (vk, proof) = input |
| 208 | + .compress_val |
| 209 | + .vks_and_proofs |
| 210 | + .first() |
| 211 | + .expect("expected one shrink proof"); |
| 212 | + let vk_hash = vk.bytes32_raw(); |
| 213 | + let pv: &sp1_recursion_core::air::RecursionPublicValues<BabyBear> = |
| 214 | + proof.public_values.as_slice().borrow(); |
| 215 | + let bytes = words_to_bytes(&pv.committed_value_digest); |
| 216 | + let mut committed_values_digest = [0u8; 32]; |
| 217 | + for (i, b) in bytes.iter().enumerate().take(32) { |
| 218 | + committed_values_digest[i] = b.as_canonical_u32() as u8; |
| 219 | + } |
| 220 | + (vk_hash, committed_values_digest) |
| 221 | +} |
| 222 | + |
| 223 | +fn write_witness_bundle( |
| 224 | + path: &Path, |
| 225 | + r1lf: &sp1_recursion_compiler::r1cs::lf::R1CSLf, |
| 226 | + witness: &[u64], |
| 227 | + vk_hash: &[u8; 32], |
| 228 | + committed_values_digest: &[u8; 32], |
| 229 | +) -> anyhow::Result<()> { |
| 230 | + const MAGIC: &[u8; 4] = b"SP1W"; |
| 231 | + const VERSION: u32 = 1; |
| 232 | + let file = std::fs::File::create(path)?; |
| 233 | + let mut w = std::io::BufWriter::with_capacity(256 * 1024 * 1024, file); |
| 234 | + w.write_all(MAGIC)?; |
| 235 | + w.write_all(&VERSION.to_le_bytes())?; |
| 236 | + w.write_all(&r1lf.digest())?; |
| 237 | + let len = witness.len() as u64; |
| 238 | + w.write_all(&len.to_le_bytes())?; |
| 239 | + w.write_all(vk_hash)?; |
| 240 | + w.write_all(committed_values_digest)?; |
| 241 | + write_u64le_to(&mut w, witness); |
| 242 | + w.flush()?; |
| 243 | + Ok(()) |
| 244 | +} |
| 245 | + |
| 246 | +fn write_u64le_to(w: &mut impl std::io::Write, xs: &[u64]) { |
| 247 | + let mut buf = vec![0u8; 8 * 1024 * 1024]; // 8MB |
| 248 | + let mut i = 0usize; |
| 249 | + while i < xs.len() { |
| 250 | + let take = ((buf.len() / 8).min(xs.len() - i)) as usize; |
| 251 | + for j in 0..take { |
| 252 | + let off = j * 8; |
| 253 | + buf[off..off + 8].copy_from_slice(&xs[i + j].to_le_bytes()); |
| 254 | + } |
| 255 | + w.write_all(&buf[..take * 8]).expect("write witness chunk"); |
| 256 | + i += take; |
| 257 | + } |
| 258 | +} |
| 259 | + |
| 260 | +fn parse_mem_id(id: &str) -> Option<(u64, usize)> { |
| 261 | + if let Some(rest) = id.strip_prefix("felt") { |
| 262 | + let n: u64 = rest.parse().ok()?; |
| 263 | + return Some((n, 0)); |
| 264 | + } |
| 265 | + if let Some(rest) = id.strip_prefix("var") { |
| 266 | + let n: u64 = rest.parse().ok()?; |
| 267 | + return Some((n, 0)); |
| 268 | + } |
| 269 | + if let Some(rest) = id.strip_prefix("ptr") { |
| 270 | + let n: u64 = rest.parse().ok()?; |
| 271 | + return Some((n, 0)); |
| 272 | + } |
| 273 | + if let Some(rest) = id.strip_prefix("ext") { |
| 274 | + let (a, limb) = rest.split_once("__")?; |
| 275 | + let n: u64 = a.parse().ok()?; |
| 276 | + let limb: usize = limb.parse().ok()?; |
| 277 | + return Some((n, limb)); |
| 278 | + } |
| 279 | + None |
| 280 | +} |
| 281 | + |
0 commit comments