Skip to main content

sp1_prover/worker/node/full/
mod.rs

1use std::sync::Arc;
2
3mod init;
4
5pub use init::SP1LocalNodeBuilder;
6
7use either::Either;
8use mti::prelude::{MagicTypeIdExt, V7};
9use sp1_core_executor::{ExecutionReport, SP1Context};
10use sp1_core_machine::io::SP1Stdin;
11use sp1_hypercube::{SP1PcsProofOuter, SP1VerifyingKey, SP1WrapProof};
12use sp1_primitives::{io::SP1PublicValues, SP1OuterGlobalContext};
13use sp1_prover_types::{
14    network_base_types::ProofMode, Artifact, ArtifactClient, ArtifactType, InMemoryArtifactClient,
15    TaskStatus, TaskType,
16};
17pub use sp1_verifier::{ProofFromNetwork, SP1Proof};
18use tokio::task::JoinSet;
19use tracing::{instrument, Instrument};
20
21use crate::{
22    shapes::DEFAULT_ARITY,
23    worker::{
24        LocalWorkerClient, ProofId, RawTaskRequest, RequesterId, SP1NodeCore, TaskContext,
25        VkeyMapControllerInput, VkeyMapControllerOutput, WorkerClient,
26    },
27};
28
29pub(crate) struct SP1NodeInner {
30    artifact_client: InMemoryArtifactClient,
31    worker_client: LocalWorkerClient,
32    core: SP1NodeCore,
33    _tasks: JoinSet<()>,
34}
35
36pub struct SP1LocalNode {
37    inner: Arc<SP1NodeInner>,
38}
39
40impl Clone for SP1LocalNode {
41    fn clone(&self) -> Self {
42        Self { inner: self.inner.clone() }
43    }
44}
45
46impl SP1LocalNode {
47    pub fn core(&self) -> &SP1NodeCore {
48        &self.inner.core
49    }
50
51    #[instrument(name = "execute_program", skip_all)]
52    pub async fn execute(
53        &self,
54        elf: &[u8],
55        stdin: SP1Stdin,
56        context: SP1Context<'static>,
57    ) -> anyhow::Result<(SP1PublicValues, [u8; 32], ExecutionReport)> {
58        self.inner.core.execute(elf, stdin, context).await
59    }
60
61    pub async fn setup(&self, elf: &[u8]) -> anyhow::Result<SP1VerifyingKey> {
62        let elf_artifact = self.inner.artifact_client.create_artifact()?;
63        self.inner.artifact_client.upload_program(&elf_artifact, elf.to_vec()).await?;
64
65        // Create a setup task and wait for the vk
66        let vk_artifact = self.inner.artifact_client.create_artifact()?;
67        let context = TaskContext {
68            proof_id: ProofId::new("core_proof"),
69            parent_id: None,
70            parent_context: None,
71            requester_id: RequesterId::new("local node"),
72        };
73        let setup_request = RawTaskRequest {
74            inputs: vec![elf_artifact.clone()],
75            outputs: vec![vk_artifact.clone()],
76            context: context.clone(),
77        };
78        tracing::trace!("submitting setup task");
79        let setup_id =
80            self.inner.worker_client.submit_task(TaskType::SetupVkey, setup_request).await?;
81        // Wait for the setup task to finish
82        let subscriber =
83            self.inner.worker_client.subscriber(context.proof_id.clone()).await?.per_task();
84        let status =
85            subscriber.wait_task(setup_id).instrument(tracing::debug_span!("setup task")).await?;
86        if status != TaskStatus::Succeeded {
87            return Err(anyhow::anyhow!("setup task failed"));
88        }
89        tracing::trace!("setup task succeeded");
90        // Download the vk
91        let vk = self.inner.artifact_client.download::<SP1VerifyingKey>(&vk_artifact).await?;
92
93        // Clean up the artifacts
94        self.inner.artifact_client.try_delete(&elf_artifact, ArtifactType::Program).await?;
95        self.inner
96            .artifact_client
97            .try_delete(&vk_artifact, ArtifactType::UnspecifiedArtifactType)
98            .await?;
99
100        Ok(vk)
101    }
102
103    pub async fn prove(
104        &self,
105        elf: &[u8],
106        stdin: SP1Stdin,
107        context: SP1Context<'static>,
108    ) -> anyhow::Result<ProofFromNetwork> {
109        self.prove_with_mode(elf, stdin, context, ProofMode::Compressed).await
110    }
111
112    pub async fn build_vks(
113        &self,
114        range_or_limit: Option<Either<Vec<usize>, usize>>,
115        chunk_size: usize,
116    ) -> anyhow::Result<VkeyMapControllerOutput> {
117        let vk_controller_artifact = self.inner.artifact_client.create_artifact()?;
118        let input =
119            VkeyMapControllerInput { range_or_limit, chunk_size, reduce_batch_size: DEFAULT_ARITY };
120        self.inner.artifact_client.upload(&vk_controller_artifact, input).await?;
121
122        let output_artifact = self.inner.artifact_client.create_artifact()?;
123
124        let proof_id = ProofId::new("proof".create_type_id::<V7>().to_string());
125
126        let request = RawTaskRequest {
127            inputs: vec![vk_controller_artifact.clone()],
128            outputs: vec![output_artifact.clone()],
129            context: TaskContext {
130                proof_id: proof_id.clone(),
131                parent_id: None,
132                parent_context: None,
133                requester_id: RequesterId::new(format!("local-node-{}", std::process::id())),
134            },
135        };
136
137        let task_id =
138            self.inner.worker_client.submit_task(TaskType::UtilVkeyMapController, request).await?;
139        let subscriber = self.inner.worker_client.subscriber(proof_id).await?.per_task();
140        let status = subscriber.wait_task(task_id).await?;
141        if status != TaskStatus::Succeeded {
142            return Err(anyhow::anyhow!("controller task failed"));
143        }
144
145        // Clean up the input artifacts
146        self.inner
147            .artifact_client
148            .try_delete(&vk_controller_artifact, ArtifactType::Program)
149            .await?;
150
151        // Download the output proof and return it.
152        let output = self
153            .inner
154            .artifact_client
155            .download::<VkeyMapControllerOutput>(&output_artifact)
156            .await?;
157
158        // Clean up the output artifact
159        self.inner
160            .artifact_client
161            .try_delete(&output_artifact, ArtifactType::UnspecifiedArtifactType)
162            .await?;
163
164        Ok(output)
165    }
166
167    #[instrument(name = "prove", skip_all, fields(mode = ?mode))]
168    pub async fn prove_with_mode(
169        &self,
170        elf: &[u8],
171        stdin: SP1Stdin,
172        context: SP1Context<'static>,
173        mode: ProofMode,
174    ) -> anyhow::Result<ProofFromNetwork> {
175        let elf_artifact = self.inner.artifact_client.create_artifact()?;
176        self.inner.artifact_client.upload_program(&elf_artifact, elf.to_vec()).await?;
177
178        let proof_nonce_artifact = self.inner.artifact_client.create_artifact()?;
179        self.inner
180            .artifact_client
181            .upload::<[u32; 4]>(&proof_nonce_artifact, context.proof_nonce)
182            .await?;
183
184        let stdin_artifact = self.inner.artifact_client.create_artifact()?;
185        self.inner
186            .artifact_client
187            .upload_with_type(&stdin_artifact, ArtifactType::Stdin, stdin)
188            .await?;
189
190        let mode_artifact = Artifact((mode as i32).to_string());
191
192        // Create an artifact for the output
193        let output_artifact = self.inner.artifact_client.create_artifact()?;
194
195        let proof_id = ProofId::new("proof".create_type_id::<V7>().to_string());
196        let request = RawTaskRequest {
197            inputs: vec![
198                elf_artifact.clone(),
199                stdin_artifact.clone(),
200                mode_artifact.clone(),
201                proof_nonce_artifact.clone(),
202            ],
203            outputs: vec![output_artifact.clone()],
204            context: TaskContext {
205                proof_id: proof_id.clone(),
206                parent_id: None,
207                parent_context: None,
208                requester_id: RequesterId::new(format!("local-node-{}", std::process::id())),
209            },
210        };
211
212        let task_id = self.inner.worker_client.submit_task(TaskType::Controller, request).await?;
213        let subscriber = self.inner.worker_client.subscriber(proof_id).await?.per_task();
214        let status = subscriber.wait_task(task_id).await?;
215        if status != TaskStatus::Succeeded {
216            return Err(anyhow::anyhow!("controller task failed"));
217        }
218
219        // Clean up the input artifacts
220        self.inner.artifact_client.try_delete(&elf_artifact, ArtifactType::Program).await?;
221        self.inner.artifact_client.try_delete(&stdin_artifact, ArtifactType::Stdin).await?;
222
223        // Download the output proof and return it.
224        let proof =
225            self.inner.artifact_client.download::<ProofFromNetwork>(&output_artifact).await?;
226        // Clean up the output artifact
227        self.inner
228            .artifact_client
229            .try_delete(&output_artifact, ArtifactType::UnspecifiedArtifactType)
230            .await?;
231
232        self.inner
233            .artifact_client
234            .try_delete(&proof_nonce_artifact, ArtifactType::UnspecifiedArtifactType)
235            .await?;
236
237        Ok(proof)
238    }
239
240    pub fn verify(&self, vk: &SP1VerifyingKey, proof: &SP1Proof) -> anyhow::Result<()> {
241        self.inner.core.verify(vk, proof)
242    }
243
244    #[cfg(test)]
245    #[allow(dead_code)]
246    pub(crate) fn wrap_vk(&self) -> &sp1_hypercube::MachineVerifyingKey<SP1OuterGlobalContext> {
247        self.inner.core.wrap_vk()
248    }
249
250    /// Convert the given compressed proof to a proof that can be verified by the groth16 circuit.
251    pub async fn shrink_wrap(
252        &self,
253        compressed_proof: &SP1Proof,
254    ) -> anyhow::Result<SP1WrapProof<SP1OuterGlobalContext, SP1PcsProofOuter>> {
255        let compressed_proof = match compressed_proof {
256            SP1Proof::Compressed(proof) => *proof.clone(),
257            _ => return Err(anyhow::anyhow!("given proof is not a compressed proof")),
258        };
259        // Upload the compressed proof to the artifact client
260        let compressed_proof_artifact = self.inner.artifact_client.create_artifact()?;
261        self.inner.artifact_client.upload(&compressed_proof_artifact, compressed_proof).await?;
262
263        // Create an artifact for the output
264        let output_artifact = self.inner.artifact_client.create_artifact()?;
265
266        // Create a task request for the shrink wrap task
267        let proof_id = ProofId::new("shrink wrap".create_type_id::<V7>().to_string());
268        let request = RawTaskRequest {
269            inputs: vec![compressed_proof_artifact.clone()],
270            outputs: vec![output_artifact.clone()],
271            context: TaskContext {
272                proof_id: proof_id.clone(),
273                parent_id: None,
274                parent_context: None,
275                requester_id: RequesterId::new(format!("local-node-{}", std::process::id())),
276            },
277        };
278
279        let task_id = self.inner.worker_client.submit_task(TaskType::ShrinkWrap, request).await?;
280        // Wait for the task to finish
281        let subscriber = self.inner.worker_client.subscriber(proof_id).await?.per_task();
282        let status = subscriber.wait_task(task_id).await?;
283        if status != TaskStatus::Succeeded {
284            return Err(anyhow::anyhow!("shrink wrap task failed"));
285        }
286
287        // Download the output proof and return it.
288        let proof = self
289            .inner
290            .artifact_client
291            .download::<SP1WrapProof<SP1OuterGlobalContext, SP1PcsProofOuter>>(&output_artifact)
292            .await?;
293        // Clean up the input and output artifacts
294        tokio::try_join!(
295            self.inner
296                .artifact_client
297                .try_delete(&compressed_proof_artifact, ArtifactType::UnspecifiedArtifactType),
298            self.inner
299                .artifact_client
300                .try_delete(&output_artifact, ArtifactType::UnspecifiedArtifactType)
301        )?;
302
303        Ok(proof)
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use serial_test::serial;
310    use sp1_core_machine::utils::setup_logger;
311
312    use crate::CpuSP1ProverComponents;
313    use sp1_hypercube::HashableKey;
314
315    use crate::worker::{cpu_worker_builder, SP1LocalNodeBuilder, SP1WorkerBuilder};
316
317    use super::*;
318
319    async fn run_e2e_node_test(
320        builder: SP1WorkerBuilder<CpuSP1ProverComponents>,
321    ) -> anyhow::Result<()> {
322        let elf = test_artifacts::FIBONACCI_ELF;
323        let stdin = SP1Stdin::default();
324        let mode = ProofMode::Compressed;
325
326        let client =
327            SP1LocalNodeBuilder::from_worker_client_builder(builder).build().await.unwrap();
328
329        let proof_nonce = [0x6284, 0xC0DE, 0x4242, 0xCAFE];
330
331        let time = tokio::time::Instant::now();
332        let context = SP1Context { proof_nonce, ..Default::default() };
333
334        let (_, _, report) = client.execute(&elf, stdin.clone(), context.clone()).await.unwrap();
335
336        let execute_time = time.elapsed();
337        let cycles = report.total_instruction_count() as usize;
338        tracing::info!(
339            "execute time: {:?}, cycles: {}, gas: {:?}",
340            execute_time,
341            cycles,
342            report.gas()
343        );
344
345        let time = tokio::time::Instant::now();
346        let vk = client.setup(&elf).await.unwrap();
347        let setup_time = time.elapsed();
348        tracing::info!("setup time: {:?}", setup_time);
349
350        let time = tokio::time::Instant::now();
351
352        tracing::info!("proving with mode: {mode:?}");
353        let proof = client
354            .prove_with_mode(&elf, stdin.clone(), context.clone(), mode)
355            .await
356            .expect("proof failed");
357        let proof_time = time.elapsed();
358        tracing::info!("proof time: {:?}", proof_time);
359
360        // Verify the proof
361        tokio::task::spawn_blocking(move || client.verify(&vk, &proof.proof).unwrap())
362            .await
363            .unwrap();
364
365        Ok(())
366    }
367
368    #[tokio::test]
369    #[serial]
370    async fn test_e2e_node() -> anyhow::Result<()> {
371        setup_logger();
372        run_e2e_node_test(cpu_worker_builder()).await
373    }
374
375    #[tokio::test]
376    #[cfg(feature = "experimental")]
377    #[serial]
378    async fn test_e2e_node_experimental() -> anyhow::Result<()> {
379        setup_logger();
380        run_e2e_node_test(cpu_worker_builder().without_vk_verification()).await
381    }
382
383    #[tokio::test]
384    #[serial]
385    #[ignore = "only run to write the vk root and num keys to a file"]
386    async fn make_verifier_vks() -> anyhow::Result<()> {
387        setup_logger();
388
389        let client = SP1LocalNodeBuilder::from_worker_client_builder(cpu_worker_builder())
390            .build()
391            .await
392            .unwrap();
393
394        let recursion_vks = client.core().recursion_vks();
395
396        let mut file = std::fs::File::create("../verifier/vk-artifacts/verifier_vks.bin")?;
397
398        bincode::serialize_into(&mut file, &recursion_vks)?;
399        Ok(())
400    }
401
402    #[tokio::test]
403    #[serial]
404    async fn test_e2e_groth16_node() -> anyhow::Result<()> {
405        setup_logger();
406
407        let elf = test_artifacts::FIBONACCI_ELF;
408        let stdin = SP1Stdin::default();
409        let mode = ProofMode::Groth16;
410
411        let client = SP1LocalNodeBuilder::from_worker_client_builder(cpu_worker_builder())
412            .build()
413            .await
414            .unwrap();
415
416        let time = tokio::time::Instant::now();
417        let context = SP1Context::default();
418        let (_, _, report) = client.execute(&elf, stdin.clone(), context.clone()).await.unwrap();
419        let execute_time = time.elapsed();
420        let cycles = report.total_instruction_count() as usize;
421        tracing::info!(
422            "execute time: {:?}, cycles: {}, gas: {:?}",
423            execute_time,
424            cycles,
425            report.gas()
426        );
427
428        let time = tokio::time::Instant::now();
429        let vk = client.setup(&elf).await.unwrap();
430        let setup_time = time.elapsed();
431        tracing::info!("setup time: {:?}", setup_time);
432
433        let time = tokio::time::Instant::now();
434
435        tracing::info!("proving with mode: {mode:?}");
436        let proof = client.prove_with_mode(&elf, stdin, context, mode).await.unwrap();
437        let proof_time = time.elapsed();
438        tracing::info!("proof time: {:?}", proof_time);
439
440        // Verify the proof
441        tokio::task::spawn_blocking(move || client.verify(&vk, &proof.proof).unwrap())
442            .await
443            .unwrap();
444
445        Ok(())
446    }
447
448    /// Test that changing the vk_root (by modifying a vk_map entry) does NOT change the wrap VK
449    /// or the Groth16 circuit. This confirms that vk_root is purely a witness variable.
450    ///
451    /// Note: Before running this test with `SP1_CIRCUIT_MODE=dev`, ensure that there are circuit
452    /// artifacts for the current wrap VK (either on S3 or locally cached). Otherwise, the test
453    /// will rebuild circuit artifacts locally, which may cause the test to pass incorrectly.
454    #[tokio::test]
455    #[serial]
456    #[cfg(feature = "experimental")]
457    #[ignore = "sanity check test; see doc-comment for proper usage"]
458    async fn test_e2e_groth16_node_modified_vk_root() -> anyhow::Result<()> {
459        use slop_algebra::AbstractField;
460        use sp1_primitives::SP1Field;
461        use sp1_recursion_executor::DIGEST_SIZE;
462        use std::collections::BTreeMap;
463        use std::io::Write;
464
465        setup_logger();
466
467        // Step 1: Load the original vk_map and modify the first entry.
468        let original_map: BTreeMap<[SP1Field; DIGEST_SIZE], usize> =
469            bincode::deserialize(include_bytes!("../../../vk_map.bin"))
470                .expect("failed to deserialize vk_map.bin");
471        tracing::info!("original vk_map has {} entries", original_map.len());
472
473        let mut modified_map = original_map.clone();
474        let first_key = *modified_map.keys().next().unwrap();
475        let first_val = modified_map.remove(&first_key).unwrap();
476        let mut new_key = first_key;
477        new_key[0] += SP1Field::one();
478        modified_map.insert(new_key, first_val);
479        assert_eq!(modified_map.len(), original_map.len(), "map size should not change");
480
481        // Step 2: Write modified vk_map to a temp file.
482        let temp_dir = tempfile::tempdir()?;
483        let vk_map_path = temp_dir.path().join("modified_vk_map.bin");
484        {
485            let mut file = std::fs::File::create(&vk_map_path)?;
486            bincode::serialize_into(&mut file, &modified_map)?;
487            file.flush()?;
488        }
489        tracing::info!("wrote modified vk_map to {:?}", vk_map_path);
490
491        // Step 3: Build the prover with the modified vk_map (vk_verification stays ON).
492        let builder =
493            cpu_worker_builder().with_vk_map_path(vk_map_path.to_str().unwrap().to_string());
494
495        let elf = test_artifacts::FIBONACCI_ELF;
496        let stdin = SP1Stdin::default();
497        let mode = ProofMode::Groth16;
498
499        let client =
500            SP1LocalNodeBuilder::from_worker_client_builder(builder).build().await.unwrap();
501
502        // Step 4: Verify that the wrap VK hasn't changed by checking the Groth16 artifacts
503        // cache directory. If the wrap VK is the same, the same cache dir
504        // (based on wrap VK hash) will be used and the existing artifacts reused.
505        let time = tokio::time::Instant::now();
506        let vk = client.setup(&elf).await.unwrap();
507        tracing::info!("setup time: {:?}", time.elapsed());
508
509        let time = tokio::time::Instant::now();
510        let context = SP1Context::default();
511        tracing::info!("proving with mode: {mode:?} (modified vk_root)");
512        let proof = client.prove_with_mode(&elf, stdin, context, mode).await.unwrap();
513        tracing::info!("proof time: {:?}", time.elapsed());
514
515        // Step 5: Verify the proof. This uses the cached Groth16 artifacts
516        // (from the previous test_e2e_groth16_node run) since the wrap VK is unchanged.
517        // If the Groth16 circuit had changed, verification would fail.
518        tokio::task::spawn_blocking(move || {
519            client.verify(&vk, &proof.proof).unwrap();
520            tracing::info!("verification with modified vk_root PASSED");
521        })
522        .await
523        .unwrap();
524
525        Ok(())
526    }
527
528    #[tokio::test]
529    #[serial]
530    async fn test_node_deferred_compress() -> anyhow::Result<()> {
531        setup_logger();
532
533        let client = SP1LocalNodeBuilder::from_worker_client_builder(cpu_worker_builder())
534            .build()
535            .await
536            .unwrap();
537
538        // Test program which proves the Keccak-256 hash of various inputs.
539        let keccak_elf = test_artifacts::KECCAK256_ELF;
540
541        // Test program which verifies proofs of a vkey and a list of committed inputs.
542        let verify_elf = test_artifacts::VERIFY_PROOF_ELF;
543
544        tracing::info!("setup keccak elf");
545        let keccak_vk = client.setup(&keccak_elf).await?;
546
547        tracing::info!("setup verify elf");
548        let verify_vk = client.setup(&verify_elf).await?;
549
550        tracing::info!("prove subproof 1");
551        let mut stdin = SP1Stdin::new();
552        stdin.write(&1usize);
553        stdin.write(&vec![0u8, 0, 0]);
554        let context = SP1Context::default();
555        let deferred_proof_1 = client
556            .prove_with_mode(&keccak_elf, stdin, context.clone(), ProofMode::Compressed)
557            .await?;
558        let pv_1 = deferred_proof_1.public_values.as_slice().to_vec().clone();
559
560        // Generate a second proof of keccak of various inputs.
561        tracing::info!("prove subproof 2");
562        let mut stdin = SP1Stdin::new();
563        stdin.write(&3usize);
564        stdin.write(&vec![0u8, 1, 2]);
565        stdin.write(&vec![2, 3, 4]);
566        stdin.write(&vec![5, 6, 7]);
567        let deferred_proof_2 = client
568            .prove_with_mode(&keccak_elf, stdin, context.clone(), ProofMode::Compressed)
569            .await?;
570        let pv_2 = deferred_proof_2.public_values.as_slice().to_vec().clone();
571
572        let deferred_reduce_1 = match deferred_proof_1.proof {
573            SP1Proof::Compressed(proof) => *proof,
574            _ => return Err(anyhow::anyhow!("deferred proof 1 is not a compressed proof")),
575        };
576        let deferred_reduce_2 = match deferred_proof_2.proof {
577            SP1Proof::Compressed(proof) => *proof,
578            _ => return Err(anyhow::anyhow!("deferred proof 2 is not a compressed proof")),
579        };
580
581        // Exercise deferred proof verification during execute.
582        let mut invalid_proof = deferred_reduce_1.clone();
583        invalid_proof.proof.public_values.clear();
584        let mut execute_stdin = SP1Stdin::new();
585        let vkey_digest = keccak_vk.hash_u32();
586        execute_stdin.write(&vkey_digest);
587        execute_stdin.write(&vec![pv_1.clone(), pv_2.clone(), pv_2.clone()]);
588        execute_stdin.write_proof(invalid_proof, keccak_vk.vk.clone());
589        execute_stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
590        execute_stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
591
592        let execute_result = client.execute(&verify_elf, execute_stdin, context.clone()).await;
593        let err = execute_result.expect_err("expected deferred proof verification to fail");
594        assert!(
595            err.to_string().contains("deferred proof 0 failed verification"),
596            "unexpected error: {err}"
597        );
598
599        // Execute verify program with deferred proof verification enabled and valid proofs.
600        let mut execute_stdin = SP1Stdin::new();
601        let vkey_digest = keccak_vk.hash_u32();
602        execute_stdin.write(&vkey_digest);
603        execute_stdin.write(&vec![pv_1.clone(), pv_2.clone(), pv_2.clone()]);
604        execute_stdin.write_proof(deferred_reduce_1.clone(), keccak_vk.vk.clone());
605        execute_stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
606        execute_stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
607
608        let (_execute_pv, _execute_digest, execute_report) =
609            client.execute(&verify_elf, execute_stdin, context.clone()).await?;
610        assert_eq!(execute_report.exit_code, 0);
611
612        // Run verify program with keccak vkey, subproofs, and their committed values.
613        let mut stdin = SP1Stdin::new();
614        let vkey_digest = keccak_vk.hash_u32();
615        stdin.write(&vkey_digest);
616        stdin.write(&vec![pv_1.clone(), pv_2.clone(), pv_2.clone()]);
617        stdin.write_proof(deferred_reduce_1.clone(), keccak_vk.vk.clone());
618        stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
619        stdin.write_proof(deferred_reduce_2.clone(), keccak_vk.vk.clone());
620
621        tracing::info!("proving verify program (core)");
622        let verify_proof =
623            client.prove_with_mode(&verify_elf, stdin, context, ProofMode::Compressed).await?;
624
625        tracing::info!("verifying verify proof");
626        tokio::task::spawn_blocking(move || {
627            client.verify(&verify_vk, &verify_proof.proof).unwrap()
628        })
629        .await
630        .unwrap();
631
632        Ok(())
633    }
634}