Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions crates/vapp/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
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)?;
Expand Down Expand Up @@ -816,14 +817,21 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
)?;
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
Expand All @@ -834,6 +842,7 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
return Err(VAppPanic::InvalidVerifierSignature);
}
}
// Unsupported proof modes.
_ => {
return Err(VAppPanic::UnsupportedProofMode { mode: request.mode });
}
Expand Down
147 changes: 139 additions & 8 deletions crates/vapp/tests/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<MockVerifier>(&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,
Expand Down Expand Up @@ -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::<MockVerifier>(&clear_tx);
assert!(matches!(result, Err(VAppPanic::AuctioneerMismatch { .. })));
assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. })));
}

#[test]
Expand Down Expand Up @@ -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::<MockVerifier>(&clear_tx);
assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. })));
assert!(matches!(result, Err(VAppPanic::ExecutorMismatch { .. })));
}

#[test]
Expand Down Expand Up @@ -2739,3 +2740,133 @@ fn test_clear_invalid_fulfill_variant() {
let result = test.state.execute::<RejectVerifier>(&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::<MockVerifier>(&deposit_tx).unwrap();

let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
test.state.execute::<MockVerifier>(&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::<MockVerifier>(&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::<MockVerifier>(&deposit_tx).unwrap();

let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
test.state.execute::<MockVerifier>(&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::<MockVerifier>(&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::<MockVerifier>(&deposit_tx).unwrap();

let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
test.state.execute::<MockVerifier>(&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::<MockVerifier>(&clear_tx);
assert!(matches!(result, Err(VAppPanic::UnsupportedProofMode { .. })));
}
160 changes: 158 additions & 2 deletions crates/vapp/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
})
}
Loading