diff --git a/crates/sdk/src/blocking/mock/mod.rs b/crates/sdk/src/blocking/mock/mod.rs index 9c54b0ec8a..b6fa825d00 100644 --- a/crates/sdk/src/blocking/mock/mod.rs +++ b/crates/sdk/src/blocking/mock/mod.rs @@ -19,7 +19,7 @@ use crate::{ cpu::CPUProverError, prover::{BaseProveRequest, ProveRequest, Prover}, }, - proof::verify_mock_public_inputs, + proof::{mock_status_code, verify_mock_public_inputs}, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey, SP1VerificationError, StatusCode, }; @@ -79,8 +79,19 @@ impl Prover for MockProver { &self, proof: &SP1ProofWithPublicValues, vkey: &SP1VerifyingKey, - _status_code: Option, + status_code: Option, ) -> Result<(), SP1VerificationError> { + let status_code = status_code.unwrap_or(StatusCode::SUCCESS); + let actual_status_code = mock_status_code(proof).map_err(|err| match &proof.proof { + SP1Proof::Core(_) => SP1VerificationError::Core(err), + SP1Proof::Compressed(_) => SP1VerificationError::Recursion(err), + SP1Proof::Plonk(_) => SP1VerificationError::Plonk(err), + SP1Proof::Groth16(_) => SP1VerificationError::Groth16(err), + })?; + if !status_code.is_accepted_code(actual_status_code) { + return Err(SP1VerificationError::UnexpectedExitCode(actual_status_code)); + } + match &proof.proof { SP1Proof::Plonk(PlonkBn254Proof { public_inputs, .. }) => { // Verify the mock Plonk proof by checking public inputs match. @@ -112,12 +123,17 @@ impl<'a> ProveRequest<'a, MockProver> for MockProveRequest<'a> { tracing::info!(mode = ?mode, "generating mock proof"); let mut req = prover.execute(pk.elf.clone(), stdin); req.context_builder = context_builder; - let (public_values, _) = req.run()?; - Ok(SP1ProofWithPublicValues::create_mock_proof( + let (public_values, report) = req.run()?; + let status_code = + u32::try_from(report.exit_code).ok().and_then(StatusCode::new).ok_or_else(|| { + anyhow::anyhow!("unsupported mock execution exit code: {}", report.exit_code) + })?; + Ok(SP1ProofWithPublicValues::create_mock_proof_with_status( &pk.vk, public_values, mode, prover.version(), + status_code, )) } } @@ -127,7 +143,7 @@ mod tests { use crate::{ blocking::prover::{ProveRequest, Prover}, utils::setup_logger, - SP1Stdin, + SP1Stdin, StatusCode, }; use super::MockProver; @@ -281,4 +297,66 @@ mod tests { let result = prover.verify(&proof, &pk.vk, None); assert!(result.is_err(), "Verification should fail with tampered public values"); } + + #[test] + fn test_mock_proof_panic_exit_status() { + setup_logger(); + let prover = MockProver::new(); + let pk = prover.setup(test_artifacts::PANIC_ELF).expect("failed to setup proving key"); + let stdin = SP1Stdin::new(); + + let modes = [ + crate::SP1ProofMode::Core, + crate::SP1ProofMode::Compressed, + crate::SP1ProofMode::Plonk, + crate::SP1ProofMode::Groth16, + ]; + + for mode in modes { + let proof = prover + .prove(&pk, stdin.clone()) + .mode(mode) + .expected_exit_code(StatusCode::PANIC) + .run() + .expect("failed to create panic mock proof"); + + prover + .verify(&proof, &pk.vk, StatusCode::new(1)) + .expect("failed to verify mock proof with panic exit code"); + + let result = prover.verify(&proof, &pk.vk, None); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(1)))); + + let result = prover.verify(&proof, &pk.vk, StatusCode::new(0)); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(1)))); + } + } + + #[test] + fn test_mock_proof_success_exit_status_rejects_panic_expectation() { + setup_logger(); + let prover = MockProver::new(); + let pk = prover.setup(test_artifacts::FIBONACCI_ELF).expect("failed to setup proving key"); + let stdin = SP1Stdin::new(); + + let modes = [ + crate::SP1ProofMode::Core, + crate::SP1ProofMode::Compressed, + crate::SP1ProofMode::Plonk, + crate::SP1ProofMode::Groth16, + ]; + + for mode in modes { + let proof = prover + .prove(&pk, stdin.clone()) + .mode(mode) + .run() + .expect("failed to create success mock proof"); + + prover.verify(&proof, &pk.vk, None).expect("failed to verify success mock proof"); + + let result = prover.verify(&proof, &pk.vk, StatusCode::new(1)); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(0)))); + } + } } diff --git a/crates/sdk/src/mock/mod.rs b/crates/sdk/src/mock/mod.rs index ea937abba2..3e96130657 100644 --- a/crates/sdk/src/mock/mod.rs +++ b/crates/sdk/src/mock/mod.rs @@ -16,7 +16,7 @@ use sp1_prover::{ }; use crate::{ - proof::verify_mock_public_inputs, + proof::{mock_status_code, verify_mock_public_inputs}, prover::{BaseProveRequest, ProveRequest}, Prover, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey, SP1VerificationError, StatusCode, }; @@ -89,8 +89,19 @@ impl Prover for MockProver { &self, proof: &SP1ProofWithPublicValues, vkey: &SP1VerifyingKey, - _status_code: Option, + status_code: Option, ) -> Result<(), SP1VerificationError> { + let status_code = status_code.unwrap_or(StatusCode::SUCCESS); + let actual_status_code = mock_status_code(proof).map_err(|err| match &proof.proof { + SP1Proof::Core(_) => SP1VerificationError::Core(err), + SP1Proof::Compressed(_) => SP1VerificationError::Recursion(err), + SP1Proof::Plonk(_) => SP1VerificationError::Plonk(err), + SP1Proof::Groth16(_) => SP1VerificationError::Groth16(err), + })?; + if !status_code.is_accepted_code(actual_status_code) { + return Err(SP1VerificationError::UnexpectedExitCode(actual_status_code)); + } + match &proof.proof { SP1Proof::Plonk(PlonkBn254Proof { public_inputs, .. }) => { // Verify the mock Plonk proof by checking public inputs match. @@ -134,13 +145,20 @@ impl<'a> IntoFuture for MockProveRequest<'a> { req.context_builder = context_builder; // Spawn blocking under the hood. - let (public_values, _) = req.await?; - - Ok(SP1ProofWithPublicValues::create_mock_proof( + let (public_values, report) = req.await?; + let status_code = u32::try_from(report.exit_code) + .ok() + .and_then(StatusCode::new) + .ok_or_else(|| { + anyhow::anyhow!("unsupported mock execution exit code: {}", report.exit_code) + })?; + + Ok(SP1ProofWithPublicValues::create_mock_proof_with_status( &pk.vk, public_values, mode, prover.version(), + status_code, )) }) } @@ -148,7 +166,9 @@ impl<'a> IntoFuture for MockProveRequest<'a> { #[cfg(test)] mod tests { - use crate::{prover::ProveRequest, utils::setup_logger, MockProver, Prover, SP1Stdin}; + use crate::{ + prover::ProveRequest, utils::setup_logger, MockProver, Prover, SP1Stdin, StatusCode, + }; /// Test mock proof creation and verification for all proof types. #[tokio::test] @@ -311,4 +331,68 @@ mod tests { let result = prover.verify(&proof, &pk.vk, None); assert!(result.is_err(), "Verification should fail with tampered public values"); } + + #[tokio::test] + async fn test_mock_proof_panic_exit_status() { + setup_logger(); + let prover = MockProver::new().await; + let pk = + prover.setup(test_artifacts::PANIC_ELF).await.expect("failed to setup proving key"); + let stdin = SP1Stdin::new(); + + let modes = [ + crate::SP1ProofMode::Core, + crate::SP1ProofMode::Compressed, + crate::SP1ProofMode::Plonk, + crate::SP1ProofMode::Groth16, + ]; + + for mode in modes { + let proof = prover + .prove(&pk, stdin.clone()) + .mode(mode) + .expected_exit_code(StatusCode::PANIC) + .await + .expect("failed to create panic mock proof"); + + prover + .verify(&proof, &pk.vk, StatusCode::new(1)) + .expect("failed to verify mock proof with panic exit code"); + + let result = prover.verify(&proof, &pk.vk, None); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(1)))); + + let result = prover.verify(&proof, &pk.vk, StatusCode::new(0)); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(1)))); + } + } + + #[tokio::test] + async fn test_mock_proof_success_exit_status_rejects_panic_expectation() { + setup_logger(); + let prover = MockProver::new().await; + let pk = + prover.setup(test_artifacts::FIBONACCI_ELF).await.expect("failed to setup proving key"); + let stdin = SP1Stdin::new(); + + let modes = [ + crate::SP1ProofMode::Core, + crate::SP1ProofMode::Compressed, + crate::SP1ProofMode::Plonk, + crate::SP1ProofMode::Groth16, + ]; + + for mode in modes { + let proof = prover + .prove(&pk, stdin.clone()) + .mode(mode) + .await + .expect("failed to create success mock proof"); + + prover.verify(&proof, &pk.vk, None).expect("failed to verify success mock proof"); + + let result = prover.verify(&proof, &pk.vk, StatusCode::new(1)); + assert!(matches!(result, Err(crate::SP1VerificationError::UnexpectedExitCode(0)))); + } + } } diff --git a/crates/sdk/src/proof.rs b/crates/sdk/src/proof.rs index 78ae3e8e05..4953d1c28e 100644 --- a/crates/sdk/src/proof.rs +++ b/crates/sdk/src/proof.rs @@ -12,10 +12,41 @@ use sp1_hypercube::create_dummy_recursion_proof; use sp1_primitives::io::SP1PublicValues; use sp1_prover::{Groth16Bn254Proof, HashableKey, PlonkBn254Proof, SP1VerifyingKey}; +use crate::StatusCode; + // Re-export the types from the verifier crate in order to avoid importing the verifier crate // for downstream dependencies. pub use sp1_verifier::{ProofFromNetwork, SP1Proof, SP1ProofMode}; +const MOCK_STATUS_TEE_PROOF_PREFIX: &[u8] = b"sp1-mock-status-v1:"; + +fn encode_mock_status_tee_proof(status_code: StatusCode) -> Vec { + let mut encoded = MOCK_STATUS_TEE_PROOF_PREFIX.to_vec(); + encoded.extend_from_slice(&status_code.as_u32().to_le_bytes()); + encoded +} + +pub(crate) fn mock_status_code(proof: &SP1ProofWithPublicValues) -> Result { + if let Some(tee_proof) = &proof.tee_proof { + if let Some(encoded_status) = tee_proof.strip_prefix(MOCK_STATUS_TEE_PROOF_PREFIX) { + let bytes: [u8; 4] = encoded_status + .try_into() + .map_err(|_| anyhow::anyhow!("invalid mock status metadata"))?; + return Ok(u32::from_le_bytes(bytes)); + } + } + + match &proof.proof { + SP1Proof::Plonk(plonk_proof) => { + plonk_proof.public_inputs[2].parse::().context("invalid mock Plonk exit code") + } + SP1Proof::Groth16(groth16_proof) => { + groth16_proof.public_inputs[2].parse::().context("invalid mock Groth16 exit code") + } + SP1Proof::Core(_) | SP1Proof::Compressed(_) => Ok(StatusCode::SUCCESS.as_u32()), + } +} + /// Verify that the mock proof's public inputs match the expected values. /// /// This is used by both the async and blocking mock provers to verify mock Plonk and Groth16 proofs. @@ -219,6 +250,25 @@ impl SP1ProofWithPublicValues { public_values: SP1PublicValues, mode: SP1ProofMode, sp1_version: &str, + ) -> Self { + Self::create_mock_proof_with_status( + vk, + public_values, + mode, + sp1_version, + StatusCode::SUCCESS, + ) + } + + /// Creates a mock proof for the specified proof mode from the public values and exit status. + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn create_mock_proof_with_status( + vk: &SP1VerifyingKey, + public_values: SP1PublicValues, + mode: SP1ProofMode, + sp1_version: &str, + status_code: StatusCode, ) -> Self { let sp1_version = sp1_version.to_string(); match mode { @@ -226,7 +276,8 @@ impl SP1ProofWithPublicValues { proof: SP1Proof::Core(vec![]), public_values, sp1_version, - tee_proof: None, + tee_proof: (status_code != StatusCode::SUCCESS) + .then(|| encode_mock_status_tee_proof(status_code)), }, SP1ProofMode::Compressed => { // Create a mock compressed proof with dummy values. @@ -235,14 +286,15 @@ impl SP1ProofWithPublicValues { proof: SP1Proof::Compressed(Box::new(dummy_proof)), public_values, sp1_version, - tee_proof: None, + tee_proof: (status_code != StatusCode::SUCCESS) + .then(|| encode_mock_status_tee_proof(status_code)), } } SP1ProofMode::Plonk => { // Create mock Plonk proof with correct public inputs. // public_inputs[0]: vkey_hash // public_inputs[1]: committed_values_digest (public_values hash) - // public_inputs[2]: exit_code (0 for success) + // public_inputs[2]: exit_code // public_inputs[3]: vk_root (0 for mock) // public_inputs[4]: proof_nonce (0 for mock) let vkey_hash = vk.hash_bn254().to_string(); @@ -252,7 +304,7 @@ impl SP1ProofWithPublicValues { public_inputs: [ vkey_hash, committed_values_digest, - "0".to_string(), // exit_code + status_code.as_u32().to_string(), "0".to_string(), // vk_root (mock) "0".to_string(), // proof_nonce (mock) ], @@ -269,7 +321,7 @@ impl SP1ProofWithPublicValues { // Create mock Groth16 proof with correct public inputs. // public_inputs[0]: vkey_hash // public_inputs[1]: committed_values_digest (public_values hash) - // public_inputs[2]: exit_code (0 for success) + // public_inputs[2]: exit_code // public_inputs[3]: vk_root (0 for mock) // public_inputs[4]: proof_nonce (0 for mock) let vkey_hash = vk.hash_bn254().to_string(); @@ -279,7 +331,7 @@ impl SP1ProofWithPublicValues { public_inputs: [ vkey_hash, committed_values_digest, - "0".to_string(), // exit_code + status_code.as_u32().to_string(), "0".to_string(), // vk_root (mock) "0".to_string(), // proof_nonce (mock) ],