Skip to content

Commit cf6d272

Browse files
author
jagdeep sidhu
committed
api rework
1 parent 06b9bb4 commit cf6d272

File tree

3 files changed

+300
-0
lines changed

3 files changed

+300
-0
lines changed

crates/prover/src/bin/dump_shrink_verify_constraints.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,24 @@ fn audit_no_wrap_frog64_lifted(r1lf: &sp1_recursion_compiler::r1cs::lf::R1CSLf)
703703
}
704704

705705
fn main() {
706+
// API-driven fast path: if the caller requested the witness bundle export, use the library API
707+
// (so downstream crates can share the exact same code path as this binary).
708+
//
709+
// We intentionally keep the legacy code below for shape-only debugging, opcode histograms,
710+
// and audit modes.
711+
if let (Ok(r1lf_path), Ok(witness_path)) = (std::env::var("SP1_R1LF"), std::env::var("SP1_WITNESS"))
712+
{
713+
println!("[sp1-prover] exporting shrink verifier via API...");
714+
sp1_prover::shrink_export::export_shrink_verifier(
715+
std::path::Path::new(&r1lf_path),
716+
std::path::Path::new(&witness_path),
717+
)
718+
.expect("export_shrink_verifier");
719+
println!("[sp1-prover] wrote SP1_R1LF={r1lf_path}");
720+
println!("[sp1-prover] wrote SP1_WITNESS={witness_path}");
721+
return;
722+
}
723+
706724
println!("=========================================================");
707725
println!("SP1 Shrink Verifier → R1CS Compilation");
708726
println!("=========================================================\n");

crates/prover/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod shapes;
1818
pub mod types;
1919
pub mod utils;
2020
pub mod verify;
21+
pub mod shrink_export;
2122

2223
use std::{
2324
borrow::Borrow,

crates/prover/src/shrink_export.rs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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

Comments
 (0)