From d9cd40196e3886f090d2ab919f7b7f0405ce49db Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Wed, 28 Jan 2026 17:03:57 -0800 Subject: [PATCH 1/2] feat(vapp): support multiple versions of SP1 --- crates/vapp/src/state.rs | 17 +++- crates/vapp/tests/clear.rs | 147 +++++++++++++++++++++++++++-- crates/vapp/tests/common/mod.rs | 160 +++++++++++++++++++++++++++++++- 3 files changed, 310 insertions(+), 14 deletions(-) diff --git a/crates/vapp/src/state.rs b/crates/vapp/src/state.rs index 2913a69f..1ef49237 100644 --- a/crates/vapp/src/state.rs +++ b/crates/vapp/src/state.rs @@ -406,7 +406,8 @@ impl, R: Storage> VAppState let auctioneer = Address::try_from(body.auctioneer.as_slice()) .map_err(|_| VAppPanic::AddressDeserializationFailed)?; - // Validate that the from account has sufficient balance for transfer + auctioneer fee. + // Validate that the from account has sufficient balance for transfer + auctioneer + // fee. debug!("validate from account has sufficient balance"); let balance = self.accounts.entry(from)?.or_default().get_balance(); let total_amount = u256::add(amount, auctioneer_fee)?; @@ -816,14 +817,21 @@ impl, R: Storage> VAppState )?; let mode = ProofMode::try_from(request.mode) .map_err(|_| VAppPanic::UnsupportedProofMode { mode: request.mode })?; - match mode { - ProofMode::Compressed => { + + // Parse version from the request (format: "sp1-v5.0.0", "sp1-v6.0.0", etc) to + // check if it is the primary supported version. + let is_primary_version = request.version.starts_with("sp1-v5"); + + match (is_primary_version, mode) { + // Only the primary version with Compressed uses native SP1 verification. + (true, ProofMode::Compressed) => { let verifier = V::default(); verifier .verify(vk, public_values_hash) .map_err(|_| VAppPanic::InvalidProof)?; } - ProofMode::Groth16 | ProofMode::Plonk => { + // Non-primary Compressed and supported non-Compressed modes use signature verification. + (false, ProofMode::Compressed) | (_, ProofMode::Groth16) | (_, ProofMode::Plonk) => { let verify = clear.verify.as_ref().ok_or(VAppPanic::MissingVerifierSignature)?; let fulfillment_id = fulfill_body @@ -834,6 +842,7 @@ impl, R: Storage> VAppState return Err(VAppPanic::InvalidVerifierSignature); } } + // Unsupported proof modes. _ => { return Err(VAppPanic::UnsupportedProofMode { mode: request.mode }); } diff --git a/crates/vapp/tests/clear.rs b/crates/vapp/tests/clear.rs index d1b3a47d..8f587485 100644 --- a/crates/vapp/tests/clear.rs +++ b/crates/vapp/tests/clear.rs @@ -984,10 +984,10 @@ fn test_clear_invalid_bid_amount_parsing() { let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2); test.state.execute::(&create_prover_tx).unwrap(); - // For this test we need to create a transaction where parsing fails before signature validation. - // Since the VApp validates signatures before parsing amounts, we can't easily test U256ParseError - // for bid amounts by modifying an already-signed transaction. Instead, this test demonstrates - // that signature validation happens first. + // For this test we need to create a transaction where parsing fails before signature + // validation. Since the VApp validates signatures before parsing amounts, we can't easily + // test U256ParseError for bid amounts by modifying an already-signed transaction. Instead, + // this test demonstrates that signature validation happens first. let mut clear_tx = create_clear_tx( &test.requester, &test.fulfiller, @@ -1379,9 +1379,10 @@ fn test_clear_invalid_settle_signature() { clear.settle.signature[0] ^= 0xFF; } - // Execute should fail with AuctioneerMismatch because corrupted signature recovers wrong address. + // Execute should fail with AuctioneerMismatch because corrupted signature recovers wrong + // address. let result = test.state.execute::(&clear_tx); - assert!(matches!(result, Err(VAppPanic::AuctioneerMismatch { .. }))); + assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. }))); } #[test] @@ -1423,9 +1424,9 @@ fn test_clear_invalid_execute_signature() { clear.execute.signature[0] ^= 0xFF; } - // Execute should fail with InvalidSignature because corrupted signature cannot be verified. + // Execute should fail with ExecutorMismatch because corrupted signature recovers to wrong address. let result = test.state.execute::(&clear_tx); - assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. }))); + assert!(matches!(result, Err(VAppPanic::ExecutorMismatch { .. }))); } #[test] @@ -2739,3 +2740,133 @@ fn test_clear_invalid_fulfill_variant() { let result = test.state.execute::(&clear_tx); assert!(matches!(result, Err(VAppPanic::InvalidTransactionVariant))); } + +#[test] +fn test_clear_v6_compressed_uses_signature_verification() { + let mut test = setup(); + + // Setup: Deposit funds for requester and create prover. + let requester_address = test.requester.address(); + let prover_address = test.fulfiller.address(); + let amount = U256::from(100_000_000); + + let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1); + test.state.execute::(&deposit_tx).unwrap(); + + let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2); + test.state.execute::(&create_prover_tx).unwrap(); + + // Create v6 compressed clear transaction with verifier signature (should use signature + // verification). + let clear_tx = create_clear_tx_with_version( + &test.requester, + &test.fulfiller, + &test.fulfiller, + &test.auctioneer, + &test.executor, + &test.verifier, + 1, + U256::from(50_000), + 1, + 1, + 1, + 1, + ProofMode::Compressed, + ExecutionStatus::Executed, + true, // needs_verifier_signature - this is key for v6 + "sp1-v6.0.0", + ); + + // Execute clear transaction - should succeed using signature verification. + let receipt = test.state.execute::(&clear_tx).unwrap(); + + // Verify balances after clear - requester pays, prover receives. + let expected_cost = U256::from(50_000_000); + let expected_requester_balance = amount - expected_cost; + + assert_account_balance(&mut test, requester_address, expected_requester_balance); + assert_account_balance(&mut test, prover_address, expected_cost); + + // Clear transactions don't return receipts. + assert!(receipt.is_none()); +} + +#[test] +fn test_clear_v6_compressed_without_signature_fails() { + let mut test = setup(); + + // Setup: Deposit funds for requester and create prover. + let requester_address = test.requester.address(); + let prover_address = test.fulfiller.address(); + let amount = U256::from(100_000_000); + + let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1); + test.state.execute::(&deposit_tx).unwrap(); + + let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2); + test.state.execute::(&create_prover_tx).unwrap(); + + // Create v6 compressed clear transaction without verifier signature (should fail). + let clear_tx = create_clear_tx_with_version( + &test.requester, + &test.fulfiller, + &test.fulfiller, + &test.auctioneer, + &test.executor, + &test.verifier, + 1, + U256::from(50_000), + 1, + 1, + 1, + 1, + ProofMode::Compressed, + ExecutionStatus::Executed, + false, // needs_verifier_signature = false, should cause failure for v6 + "sp1-v6.0.0", + ); + + // Execute should fail with MissingVerifierSignature since v6 compressed needs signature + // verification. + let result = test.state.execute::(&clear_tx); + assert!(matches!(result, Err(VAppPanic::MissingVerifierSignature))); +} + +#[test] +fn test_clear_unsupported_proof_mode_core() { + let mut test = setup(); + + // Setup: Deposit funds for requester and create prover. + let requester_address = test.requester.address(); + let prover_address = test.fulfiller.address(); + let amount = U256::from(100_000_000); + + let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1); + test.state.execute::(&deposit_tx).unwrap(); + + let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2); + test.state.execute::(&create_prover_tx).unwrap(); + + // Create clear transaction with Core proof mode (unsupported). + let clear_tx = create_clear_tx( + &test.requester, + &test.fulfiller, + &test.fulfiller, + &test.auctioneer, + &test.executor, + &test.verifier, + 1, + U256::from(50_000), + 1, + 1, + 1, + 1, + ProofMode::Core, + ExecutionStatus::Executed, + false, + ); + + // Execute should fail with UnsupportedProofMode. + let result = test.state.execute::(&clear_tx); + assert!(matches!(result, Err(VAppPanic::UnsupportedProofMode { .. }))); +} diff --git a/crates/vapp/tests/common/mod.rs b/crates/vapp/tests/common/mod.rs index 9dfc48d1..dbc8f8ba 100644 --- a/crates/vapp/tests/common/mod.rs +++ b/crates/vapp/tests/common/mod.rs @@ -572,7 +572,7 @@ pub fn create_clear_tx_with_options( nonce: request_nonce, vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683") .unwrap(), - version: "sp1-v3.0.0".to_string(), + version: "sp1-v5.0.0".to_string(), mode: proof_mode as i32, strategy: FulfillmentStrategy::Auction as i32, stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9" @@ -883,7 +883,7 @@ pub fn create_clear_tx_with_public_values_hash( nonce: request_nonce, vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683") .unwrap(), - version: "sp1-v3.0.0".to_string(), + version: "sp1-v5.0.0".to_string(), mode: proof_mode as i32, strategy: FulfillmentStrategy::Auction as i32, stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9" @@ -1061,3 +1061,159 @@ pub fn create_clear_tx_with_mismatched_auctioneer( tx } + +/// Creates a clear transaction with a specific version string. +#[allow(clippy::too_many_arguments)] +pub fn create_clear_tx_with_version( + requester_signer: &PrivateKeySigner, + bidder_signer: &PrivateKeySigner, + fulfiller_signer: &PrivateKeySigner, + auctioneer_signer: &PrivateKeySigner, + executor_signer: &PrivateKeySigner, + verifier_signer: &PrivateKeySigner, + request_nonce: u64, + bid_amount: U256, + bid_nonce: u64, + settle_nonce: u64, + fulfill_nonce: u64, + execute_nonce: u64, + proof_mode: ProofMode, + execution_status: ExecutionStatus, + needs_verifier_signature: bool, + version: &str, +) -> VAppTransaction { + use spn_network_types::HashableWithSender; + + // Create request body with custom version. + let request_body = RequestProofRequestBody { + nonce: request_nonce, + vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683") + .unwrap(), + version: version.to_string(), + mode: proof_mode as i32, + strategy: FulfillmentStrategy::Auction as i32, + stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9" + .to_string(), + deadline: 1000, + cycle_limit: 1000, + gas_limit: 10000, + min_auction_period: 0, + whitelist: vec![], + domain: SPN_MAINNET_V1_DOMAIN.to_vec(), + auctioneer: auctioneer_signer.address().to_vec(), + executor: executor_signer.address().to_vec(), + verifier: verifier_signer.address().to_vec(), + public_values_hash: None, + base_fee: "0".to_string(), + max_price_per_pgu: "100000".to_string(), + variant: TransactionVariant::RequestVariant as i32, + treasury: signer("treasury").address().to_vec(), + }; + + // Compute the request ID from the request body and signer. + let request_id = request_body + .hash_with_signer(requester_signer.address().as_slice()) + .expect("Failed to hash request body"); + + // Create and sign request. + let request = RequestProofRequest { + format: MessageFormat::Binary as i32, + signature: proto_sign(requester_signer, &request_body).as_bytes().to_vec(), + body: Some(request_body), + }; + + // Create bid body with computed request ID. + let bid_body = BidRequestBody { + nonce: bid_nonce, + request_id: request_id.to_vec(), + amount: bid_amount.to_string(), + domain: SPN_MAINNET_V1_DOMAIN.to_vec(), + prover: bidder_signer.address().to_vec(), + variant: TransactionVariant::BidVariant as i32, + }; + + // Create and sign bid. + let bid = BidRequest { + format: MessageFormat::Binary as i32, + signature: proto_sign(bidder_signer, &bid_body).as_bytes().to_vec(), + body: Some(bid_body), + }; + + // Create settle body with computed request ID. + let settle_body = SettleRequestBody { + nonce: settle_nonce, + request_id: request_id.to_vec(), + winner: bidder_signer.address().to_vec(), + domain: SPN_MAINNET_V1_DOMAIN.to_vec(), + variant: TransactionVariant::SettleVariant as i32, + }; + + // Create and sign settle. + let settle = SettleRequest { + format: MessageFormat::Binary as i32, + signature: proto_sign(auctioneer_signer, &settle_body).as_bytes().to_vec(), + body: Some(settle_body), + }; + + // Create execute body with computed request ID. + let execute_body = ExecuteProofRequestBody { + nonce: execute_nonce, + request_id: request_id.to_vec(), + execution_status: execution_status as i32, + public_values_hash: Some([0; 32].to_vec()), // Dummy public values hash + cycles: Some(1000), + pgus: Some(1000), + domain: SPN_MAINNET_V1_DOMAIN.to_vec(), + punishment: None, + failure_cause: None, + variant: TransactionVariant::ExecuteVariant as i32, + }; + + // Create and sign execute. + let execute = ExecuteProofRequest { + format: MessageFormat::Binary as i32, + signature: proto_sign(executor_signer, &execute_body).as_bytes().to_vec(), + body: Some(execute_body), + }; + + // Create fulfill body with computed request ID. + let fulfill_body = FulfillProofRequestBody { + nonce: fulfill_nonce, + request_id: request_id.to_vec(), + proof: vec![], + domain: SPN_MAINNET_V1_DOMAIN.to_vec(), + variant: TransactionVariant::FulfillVariant as i32, + reserved_metadata: None, + }; + + // Create fulfill request. + let fulfill = FulfillProofRequest { + format: MessageFormat::Binary as i32, + signature: proto_sign(fulfiller_signer, &fulfill_body).as_bytes().to_vec(), + body: Some(fulfill_body), + }; + + // Add verifier signature if required. + let verify = if needs_verifier_signature { + let fulfill_id = fulfill + .body + .as_ref() + .unwrap() + .hash_with_signer(fulfiller_signer.address().as_slice()) + .expect("Failed to hash fulfill body"); + use alloy::signers::SignerSync; + Some(verifier_signer.sign_message_sync(&fulfill_id).unwrap().as_bytes().to_vec()) + } else { + None + }; + + VAppTransaction::Clear(ClearTransaction { + request, + bid, + settle, + execute, + fulfill: Some(fulfill), + verify, + vk: None, + }) +} From 3b177489a3cf6df452ce142bc401913a658bf2c4 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Wed, 28 Jan 2026 17:45:14 -0800 Subject: [PATCH 2/2] fix: make fmt happy --- crates/vapp/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vapp/src/state.rs b/crates/vapp/src/state.rs index 1ef49237..643c1d82 100644 --- a/crates/vapp/src/state.rs +++ b/crates/vapp/src/state.rs @@ -831,7 +831,7 @@ impl, R: Storage> VAppState .map_err(|_| VAppPanic::InvalidProof)?; } // Non-primary Compressed and supported non-Compressed modes use signature verification. - (false, ProofMode::Compressed) | (_, ProofMode::Groth16) | (_, ProofMode::Plonk) => { + (false, ProofMode::Compressed) | (_, ProofMode::Groth16 | ProofMode::Plonk) => { let verify = clear.verify.as_ref().ok_or(VAppPanic::MissingVerifierSignature)?; let fulfillment_id = fulfill_body