Conversation
There was a problem hiding this comment.
Pull request overview
Adds new Rust staticlib “glue” layers for SP1 and Ziren proving/verification, wires them into the Zig state-proving manager and build system, and implements OpenVM receipt verification (previously a stub).
Changes:
- Add
sp1-glueandziren-gluecrates exposing*_prove/*_verifyC-ABI entrypoints and include them in the Rust workspace. - Implement
openvm_verifyby deserializing a proof package, checking an ELF hash, and verifying the proof with OpenVM SDK. - Extend Zig build + manager plumbing to select/link SP1/Ziren libraries and route prove/verify calls.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/ziren-glue/src/lib.rs | New Ziren prover/verifier FFI surface and proof packaging. |
| rust/ziren-glue/Cargo.toml | New crate definition + Ziren SDK dependency. |
| rust/sp1-glue/src/lib.rs | New SP1 prover/verifier FFI surface and proof packaging. |
| rust/sp1-glue/Cargo.toml | New crate definition + SP1 SDK dependency. |
| rust/openvm-glue/src/lib.rs | Replace OpenVM verify stub with real verification flow. |
| rust/openvm-glue/Cargo.toml | Add openvm-circuit dependency to deserialize proof type. |
| rust/Cargo.toml | Add new workspace members + add SP1/Ziren build profiles. |
| pkgs/state-proving-manager/src/manager.zig | Add SP1/Ziren options and route calls via conditional externs. |
| build.zig | Add SP1/Ziren prover choices, link the right Rust archives, and build with the right Cargo profiles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Deserialize the ZKM proof | ||
| let proof: ZKMProofWithPublicValues = match bincode::deserialize(&proof_package.proof_bytes) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("ziren_verify: failed to deserialize proof: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Deserialize the verifying key | ||
| let vk: ZKMVerifyingKey = match bincode::deserialize(&proof_package.vk_bytes) { | ||
| Ok(k) => k, | ||
| Err(e) => { | ||
| eprintln!("ziren_verify: failed to deserialize verifying key: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Verify the proof using the ZKM SDK | ||
| let client = ProverClient::new(); | ||
| match client.verify(&proof, &vk) { |
There was a problem hiding this comment.
ziren_verify trusts a verifying key provided inside the untrusted receipt (vk_bytes). An attacker can set elf_hash to match the local ELF but supply a (proof, vk) pair for a different program/circuit, and verification will still succeed because nothing ties vk to binary_path. Derive the verifying key from the ELF on the verifier side (e.g., run client.setup(&elf_bytes) and use/compare the resulting vk), or otherwise authenticate the verifying key out-of-band instead of accepting it from the receipt.
| // Deserialize the ZKM proof | |
| let proof: ZKMProofWithPublicValues = match bincode::deserialize(&proof_package.proof_bytes) { | |
| Ok(p) => p, | |
| Err(e) => { | |
| eprintln!("ziren_verify: failed to deserialize proof: {}", e); | |
| return false; | |
| } | |
| }; | |
| // Deserialize the verifying key | |
| let vk: ZKMVerifyingKey = match bincode::deserialize(&proof_package.vk_bytes) { | |
| Ok(k) => k, | |
| Err(e) => { | |
| eprintln!("ziren_verify: failed to deserialize verifying key: {}", e); | |
| return false; | |
| } | |
| }; | |
| // Verify the proof using the ZKM SDK | |
| let client = ProverClient::new(); | |
| match client.verify(&proof, &vk) { | |
| // Derive the verifying key from the local ELF to avoid trusting vk_bytes from the receipt | |
| let client = ProverClient::new(); | |
| let (_proving_key, vk_from_elf) = match client.setup(&elf_bytes) { | |
| Ok(result) => result, | |
| Err(e) => { | |
| eprintln!("ziren_verify: failed to derive verifying key from ELF: {}", e); | |
| return false; | |
| } | |
| }; | |
| // Deserialize the ZKM proof | |
| let proof: ZKMProofWithPublicValues = match bincode::deserialize(&proof_package.proof_bytes) { | |
| Ok(p) => p, | |
| Err(e) => { | |
| eprintln!("ziren_verify: failed to deserialize proof: {}", e); | |
| return false; | |
| } | |
| }; | |
| // Verify the proof using the ZKM SDK and the verifying key derived from the ELF | |
| match client.verify(&proof, &vk_from_elf) { |
| // Deserialize the proof package from receipt bytes | ||
| let proof_package: ZirenProofPackage = match bincode::deserialize(receipt_slice) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("ziren_verify: failed to deserialize proof package: {}", e); | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
bincode::deserialize(receipt_slice) is performed on untrusted input without any size limit. Because ZirenProofPackage contains Vec<u8> fields, a crafted receipt can request huge allocations and cause OOM/DoS during verification. Consider using bincode::DefaultOptions::new().with_limit(...) (or an equivalent bounded deserializer) and reject receipts exceeding a maximum expected size.
| // Deserialize the proof package from receipt bytes | ||
| let proof_package: OpenVMProofPackage = match bincode::deserialize(receipt_slice) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: failed to deserialize proof package: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Verify ELF hash: read the binary ELF and check it matches the hash in the proof | ||
| let elf_bytes = match fs::read(binary_path) { | ||
| Ok(b) => b, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: failed to read ELF at {}: {}", binary_path, e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| let mut hasher = Sha256::new(); | ||
| hasher.update(&elf_bytes); | ||
| let computed_hash = hasher.finalize().to_vec(); | ||
|
|
||
| if computed_hash != proof_package.elf_hash { | ||
| eprintln!("openvm_verify: ELF hash mismatch — proof was generated for a different binary"); | ||
| return false; | ||
| } | ||
|
|
||
| // Deserialize the continuation VM proof | ||
| let proof: ContinuationVmProof<SC> = match bincode::deserialize(&proof_package.proof_bytes) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: failed to deserialize proof: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Deserialize the app verifying key | ||
| let app_vk: AppVerifyingKey = match bincode::deserialize(&proof_package.vk_bytes) { | ||
| Ok(k) => k, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: failed to deserialize verifying key: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Verify the proof using the SDK | ||
| let sdk = Sdk::new(); | ||
| match sdk.verify_app_proof(&app_vk, &proof) { | ||
| Ok(_) => true, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: proof verification failed: {}", e); | ||
| false | ||
| } | ||
| } |
There was a problem hiding this comment.
openvm_verify accepts vk_bytes from the untrusted receipt and verifies the proof against that key. Because the ELF hash check is independent of the verifying key, a malicious receipt can set elf_hash to match the local ELF while supplying a (proof, vk) pair for a different circuit, and verification can still succeed. The verifier should derive the expected AppVerifyingKey from the ELF/config on its side (or compare against a trusted/precomputed key) rather than trusting vk_bytes from the receipt.
| // Deserialize the proof package from receipt bytes | ||
| let proof_package: OpenVMProofPackage = match bincode::deserialize(receipt_slice) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("openvm_verify: failed to deserialize proof package: {}", e); | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
bincode::deserialize(receipt_slice) is called on untrusted receipt bytes without a size limit. Since OpenVMProofPackage contains Vec<u8> fields, a crafted receipt can trigger large allocations and OOM/DoS during verification. Consider switching to bounded deserialization (e.g., bincode::DefaultOptions::new().with_limit(...)) and rejecting oversized receipts early.
| // Deserialize the verifying key | ||
| let vk: SP1VerifyingKey = match bincode::deserialize(&proof_package.vk_bytes) { | ||
| Ok(k) => k, | ||
| Err(e) => { | ||
| eprintln!("sp1_verify: failed to deserialize verifying key: {}", e); | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // Verify the proof using the SP1 SDK | ||
| let client = ProverClient::new(); | ||
| match client.verify(&proof, &vk) { | ||
| Ok(()) => true, |
There was a problem hiding this comment.
sp1_verify trusts a verifying key provided inside the untrusted receipt (vk_bytes). An attacker can set elf_hash to match the local ELF but supply a (proof, vk) pair for a different program/circuit, and verification will still succeed because nothing ties vk to binary_path. Derive the verifying key from the ELF on the verifier side (e.g., run client.setup(&elf_bytes) and use/compare the resulting vk), or otherwise authenticate the verifying key out-of-band instead of accepting it from the receipt.
| // Deserialize the proof package from receipt bytes | ||
| let proof_package: SP1ProofPackage = match bincode::deserialize(receipt_slice) { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| eprintln!("sp1_verify: failed to deserialize proof package: {}", e); | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
bincode::deserialize(receipt_slice) is performed on untrusted input without any size limit. Because SP1ProofPackage contains Vec<u8> fields, a crafted receipt can request huge allocations and cause OOM/DoS during verification. Consider using bincode::DefaultOptions::new().with_limit(...) (or an equivalent bounded deserializer) and reject receipts exceeding a maximum expected size.
e88ce57 to
e9640be
Compare
No description provided.