Skip to content

Commit d9cd401

Browse files
feat(vapp): support multiple versions of SP1
1 parent 9d38c9b commit d9cd401

File tree

3 files changed

+310
-14
lines changed

3 files changed

+310
-14
lines changed

crates/vapp/src/state.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,8 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
406406
let auctioneer = Address::try_from(body.auctioneer.as_slice())
407407
.map_err(|_| VAppPanic::AddressDeserializationFailed)?;
408408

409-
// Validate that the from account has sufficient balance for transfer + auctioneer fee.
409+
// Validate that the from account has sufficient balance for transfer + auctioneer
410+
// fee.
410411
debug!("validate from account has sufficient balance");
411412
let balance = self.accounts.entry(from)?.or_default().get_balance();
412413
let total_amount = u256::add(amount, auctioneer_fee)?;
@@ -816,14 +817,21 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
816817
)?;
817818
let mode = ProofMode::try_from(request.mode)
818819
.map_err(|_| VAppPanic::UnsupportedProofMode { mode: request.mode })?;
819-
match mode {
820-
ProofMode::Compressed => {
820+
821+
// Parse version from the request (format: "sp1-v5.0.0", "sp1-v6.0.0", etc) to
822+
// check if it is the primary supported version.
823+
let is_primary_version = request.version.starts_with("sp1-v5");
824+
825+
match (is_primary_version, mode) {
826+
// Only the primary version with Compressed uses native SP1 verification.
827+
(true, ProofMode::Compressed) => {
821828
let verifier = V::default();
822829
verifier
823830
.verify(vk, public_values_hash)
824831
.map_err(|_| VAppPanic::InvalidProof)?;
825832
}
826-
ProofMode::Groth16 | ProofMode::Plonk => {
833+
// Non-primary Compressed and supported non-Compressed modes use signature verification.
834+
(false, ProofMode::Compressed) | (_, ProofMode::Groth16) | (_, ProofMode::Plonk) => {
827835
let verify =
828836
clear.verify.as_ref().ok_or(VAppPanic::MissingVerifierSignature)?;
829837
let fulfillment_id = fulfill_body
@@ -834,6 +842,7 @@ impl<A: Storage<Address, Account>, R: Storage<RequestId, bool>> VAppState<A, R>
834842
return Err(VAppPanic::InvalidVerifierSignature);
835843
}
836844
}
845+
// Unsupported proof modes.
837846
_ => {
838847
return Err(VAppPanic::UnsupportedProofMode { mode: request.mode });
839848
}

crates/vapp/tests/clear.rs

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -984,10 +984,10 @@ fn test_clear_invalid_bid_amount_parsing() {
984984
let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
985985
test.state.execute::<MockVerifier>(&create_prover_tx).unwrap();
986986

987-
// For this test we need to create a transaction where parsing fails before signature validation.
988-
// Since the VApp validates signatures before parsing amounts, we can't easily test U256ParseError
989-
// for bid amounts by modifying an already-signed transaction. Instead, this test demonstrates
990-
// that signature validation happens first.
987+
// For this test we need to create a transaction where parsing fails before signature
988+
// validation. Since the VApp validates signatures before parsing amounts, we can't easily
989+
// test U256ParseError for bid amounts by modifying an already-signed transaction. Instead,
990+
// this test demonstrates that signature validation happens first.
991991
let mut clear_tx = create_clear_tx(
992992
&test.requester,
993993
&test.fulfiller,
@@ -1379,9 +1379,10 @@ fn test_clear_invalid_settle_signature() {
13791379
clear.settle.signature[0] ^= 0xFF;
13801380
}
13811381

1382-
// Execute should fail with AuctioneerMismatch because corrupted signature recovers wrong address.
1382+
// Execute should fail with AuctioneerMismatch because corrupted signature recovers wrong
1383+
// address.
13831384
let result = test.state.execute::<MockVerifier>(&clear_tx);
1384-
assert!(matches!(result, Err(VAppPanic::AuctioneerMismatch { .. })));
1385+
assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. })));
13851386
}
13861387

13871388
#[test]
@@ -1423,9 +1424,9 @@ fn test_clear_invalid_execute_signature() {
14231424
clear.execute.signature[0] ^= 0xFF;
14241425
}
14251426

1426-
// Execute should fail with InvalidSignature because corrupted signature cannot be verified.
1427+
// Execute should fail with ExecutorMismatch because corrupted signature recovers to wrong address.
14271428
let result = test.state.execute::<MockVerifier>(&clear_tx);
1428-
assert!(matches!(result, Err(VAppPanic::InvalidSignature { .. })));
1429+
assert!(matches!(result, Err(VAppPanic::ExecutorMismatch { .. })));
14291430
}
14301431

14311432
#[test]
@@ -2739,3 +2740,133 @@ fn test_clear_invalid_fulfill_variant() {
27392740
let result = test.state.execute::<RejectVerifier>(&clear_tx);
27402741
assert!(matches!(result, Err(VAppPanic::InvalidTransactionVariant)));
27412742
}
2743+
2744+
#[test]
2745+
fn test_clear_v6_compressed_uses_signature_verification() {
2746+
let mut test = setup();
2747+
2748+
// Setup: Deposit funds for requester and create prover.
2749+
let requester_address = test.requester.address();
2750+
let prover_address = test.fulfiller.address();
2751+
let amount = U256::from(100_000_000);
2752+
2753+
let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1);
2754+
test.state.execute::<MockVerifier>(&deposit_tx).unwrap();
2755+
2756+
let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
2757+
test.state.execute::<MockVerifier>(&create_prover_tx).unwrap();
2758+
2759+
// Create v6 compressed clear transaction with verifier signature (should use signature
2760+
// verification).
2761+
let clear_tx = create_clear_tx_with_version(
2762+
&test.requester,
2763+
&test.fulfiller,
2764+
&test.fulfiller,
2765+
&test.auctioneer,
2766+
&test.executor,
2767+
&test.verifier,
2768+
1,
2769+
U256::from(50_000),
2770+
1,
2771+
1,
2772+
1,
2773+
1,
2774+
ProofMode::Compressed,
2775+
ExecutionStatus::Executed,
2776+
true, // needs_verifier_signature - this is key for v6
2777+
"sp1-v6.0.0",
2778+
);
2779+
2780+
// Execute clear transaction - should succeed using signature verification.
2781+
let receipt = test.state.execute::<MockVerifier>(&clear_tx).unwrap();
2782+
2783+
// Verify balances after clear - requester pays, prover receives.
2784+
let expected_cost = U256::from(50_000_000);
2785+
let expected_requester_balance = amount - expected_cost;
2786+
2787+
assert_account_balance(&mut test, requester_address, expected_requester_balance);
2788+
assert_account_balance(&mut test, prover_address, expected_cost);
2789+
2790+
// Clear transactions don't return receipts.
2791+
assert!(receipt.is_none());
2792+
}
2793+
2794+
#[test]
2795+
fn test_clear_v6_compressed_without_signature_fails() {
2796+
let mut test = setup();
2797+
2798+
// Setup: Deposit funds for requester and create prover.
2799+
let requester_address = test.requester.address();
2800+
let prover_address = test.fulfiller.address();
2801+
let amount = U256::from(100_000_000);
2802+
2803+
let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1);
2804+
test.state.execute::<MockVerifier>(&deposit_tx).unwrap();
2805+
2806+
let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
2807+
test.state.execute::<MockVerifier>(&create_prover_tx).unwrap();
2808+
2809+
// Create v6 compressed clear transaction without verifier signature (should fail).
2810+
let clear_tx = create_clear_tx_with_version(
2811+
&test.requester,
2812+
&test.fulfiller,
2813+
&test.fulfiller,
2814+
&test.auctioneer,
2815+
&test.executor,
2816+
&test.verifier,
2817+
1,
2818+
U256::from(50_000),
2819+
1,
2820+
1,
2821+
1,
2822+
1,
2823+
ProofMode::Compressed,
2824+
ExecutionStatus::Executed,
2825+
false, // needs_verifier_signature = false, should cause failure for v6
2826+
"sp1-v6.0.0",
2827+
);
2828+
2829+
// Execute should fail with MissingVerifierSignature since v6 compressed needs signature
2830+
// verification.
2831+
let result = test.state.execute::<MockVerifier>(&clear_tx);
2832+
assert!(matches!(result, Err(VAppPanic::MissingVerifierSignature)));
2833+
}
2834+
2835+
#[test]
2836+
fn test_clear_unsupported_proof_mode_core() {
2837+
let mut test = setup();
2838+
2839+
// Setup: Deposit funds for requester and create prover.
2840+
let requester_address = test.requester.address();
2841+
let prover_address = test.fulfiller.address();
2842+
let amount = U256::from(100_000_000);
2843+
2844+
let deposit_tx = deposit_tx(requester_address, amount, 0, 1, 1);
2845+
test.state.execute::<MockVerifier>(&deposit_tx).unwrap();
2846+
2847+
let create_prover_tx = create_prover_tx(prover_address, prover_address, U256::ZERO, 1, 2, 2);
2848+
test.state.execute::<MockVerifier>(&create_prover_tx).unwrap();
2849+
2850+
// Create clear transaction with Core proof mode (unsupported).
2851+
let clear_tx = create_clear_tx(
2852+
&test.requester,
2853+
&test.fulfiller,
2854+
&test.fulfiller,
2855+
&test.auctioneer,
2856+
&test.executor,
2857+
&test.verifier,
2858+
1,
2859+
U256::from(50_000),
2860+
1,
2861+
1,
2862+
1,
2863+
1,
2864+
ProofMode::Core,
2865+
ExecutionStatus::Executed,
2866+
false,
2867+
);
2868+
2869+
// Execute should fail with UnsupportedProofMode.
2870+
let result = test.state.execute::<MockVerifier>(&clear_tx);
2871+
assert!(matches!(result, Err(VAppPanic::UnsupportedProofMode { .. })));
2872+
}

crates/vapp/tests/common/mod.rs

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ pub fn create_clear_tx_with_options(
572572
nonce: request_nonce,
573573
vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683")
574574
.unwrap(),
575-
version: "sp1-v3.0.0".to_string(),
575+
version: "sp1-v5.0.0".to_string(),
576576
mode: proof_mode as i32,
577577
strategy: FulfillmentStrategy::Auction as i32,
578578
stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9"
@@ -883,7 +883,7 @@ pub fn create_clear_tx_with_public_values_hash(
883883
nonce: request_nonce,
884884
vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683")
885885
.unwrap(),
886-
version: "sp1-v3.0.0".to_string(),
886+
version: "sp1-v5.0.0".to_string(),
887887
mode: proof_mode as i32,
888888
strategy: FulfillmentStrategy::Auction as i32,
889889
stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9"
@@ -1061,3 +1061,159 @@ pub fn create_clear_tx_with_mismatched_auctioneer(
10611061

10621062
tx
10631063
}
1064+
1065+
/// Creates a clear transaction with a specific version string.
1066+
#[allow(clippy::too_many_arguments)]
1067+
pub fn create_clear_tx_with_version(
1068+
requester_signer: &PrivateKeySigner,
1069+
bidder_signer: &PrivateKeySigner,
1070+
fulfiller_signer: &PrivateKeySigner,
1071+
auctioneer_signer: &PrivateKeySigner,
1072+
executor_signer: &PrivateKeySigner,
1073+
verifier_signer: &PrivateKeySigner,
1074+
request_nonce: u64,
1075+
bid_amount: U256,
1076+
bid_nonce: u64,
1077+
settle_nonce: u64,
1078+
fulfill_nonce: u64,
1079+
execute_nonce: u64,
1080+
proof_mode: ProofMode,
1081+
execution_status: ExecutionStatus,
1082+
needs_verifier_signature: bool,
1083+
version: &str,
1084+
) -> VAppTransaction {
1085+
use spn_network_types::HashableWithSender;
1086+
1087+
// Create request body with custom version.
1088+
let request_body = RequestProofRequestBody {
1089+
nonce: request_nonce,
1090+
vk_hash: hex::decode("005b97bb81b9ed64f9321049013a56d9633c115b076ae4144f2622d0da13d683")
1091+
.unwrap(),
1092+
version: version.to_string(),
1093+
mode: proof_mode as i32,
1094+
strategy: FulfillmentStrategy::Auction as i32,
1095+
stdin_uri: "s3://spn-artifacts-production3/stdins/artifact_01jqcgtjr7es883amkx30sqkg9"
1096+
.to_string(),
1097+
deadline: 1000,
1098+
cycle_limit: 1000,
1099+
gas_limit: 10000,
1100+
min_auction_period: 0,
1101+
whitelist: vec![],
1102+
domain: SPN_MAINNET_V1_DOMAIN.to_vec(),
1103+
auctioneer: auctioneer_signer.address().to_vec(),
1104+
executor: executor_signer.address().to_vec(),
1105+
verifier: verifier_signer.address().to_vec(),
1106+
public_values_hash: None,
1107+
base_fee: "0".to_string(),
1108+
max_price_per_pgu: "100000".to_string(),
1109+
variant: TransactionVariant::RequestVariant as i32,
1110+
treasury: signer("treasury").address().to_vec(),
1111+
};
1112+
1113+
// Compute the request ID from the request body and signer.
1114+
let request_id = request_body
1115+
.hash_with_signer(requester_signer.address().as_slice())
1116+
.expect("Failed to hash request body");
1117+
1118+
// Create and sign request.
1119+
let request = RequestProofRequest {
1120+
format: MessageFormat::Binary as i32,
1121+
signature: proto_sign(requester_signer, &request_body).as_bytes().to_vec(),
1122+
body: Some(request_body),
1123+
};
1124+
1125+
// Create bid body with computed request ID.
1126+
let bid_body = BidRequestBody {
1127+
nonce: bid_nonce,
1128+
request_id: request_id.to_vec(),
1129+
amount: bid_amount.to_string(),
1130+
domain: SPN_MAINNET_V1_DOMAIN.to_vec(),
1131+
prover: bidder_signer.address().to_vec(),
1132+
variant: TransactionVariant::BidVariant as i32,
1133+
};
1134+
1135+
// Create and sign bid.
1136+
let bid = BidRequest {
1137+
format: MessageFormat::Binary as i32,
1138+
signature: proto_sign(bidder_signer, &bid_body).as_bytes().to_vec(),
1139+
body: Some(bid_body),
1140+
};
1141+
1142+
// Create settle body with computed request ID.
1143+
let settle_body = SettleRequestBody {
1144+
nonce: settle_nonce,
1145+
request_id: request_id.to_vec(),
1146+
winner: bidder_signer.address().to_vec(),
1147+
domain: SPN_MAINNET_V1_DOMAIN.to_vec(),
1148+
variant: TransactionVariant::SettleVariant as i32,
1149+
};
1150+
1151+
// Create and sign settle.
1152+
let settle = SettleRequest {
1153+
format: MessageFormat::Binary as i32,
1154+
signature: proto_sign(auctioneer_signer, &settle_body).as_bytes().to_vec(),
1155+
body: Some(settle_body),
1156+
};
1157+
1158+
// Create execute body with computed request ID.
1159+
let execute_body = ExecuteProofRequestBody {
1160+
nonce: execute_nonce,
1161+
request_id: request_id.to_vec(),
1162+
execution_status: execution_status as i32,
1163+
public_values_hash: Some([0; 32].to_vec()), // Dummy public values hash
1164+
cycles: Some(1000),
1165+
pgus: Some(1000),
1166+
domain: SPN_MAINNET_V1_DOMAIN.to_vec(),
1167+
punishment: None,
1168+
failure_cause: None,
1169+
variant: TransactionVariant::ExecuteVariant as i32,
1170+
};
1171+
1172+
// Create and sign execute.
1173+
let execute = ExecuteProofRequest {
1174+
format: MessageFormat::Binary as i32,
1175+
signature: proto_sign(executor_signer, &execute_body).as_bytes().to_vec(),
1176+
body: Some(execute_body),
1177+
};
1178+
1179+
// Create fulfill body with computed request ID.
1180+
let fulfill_body = FulfillProofRequestBody {
1181+
nonce: fulfill_nonce,
1182+
request_id: request_id.to_vec(),
1183+
proof: vec![],
1184+
domain: SPN_MAINNET_V1_DOMAIN.to_vec(),
1185+
variant: TransactionVariant::FulfillVariant as i32,
1186+
reserved_metadata: None,
1187+
};
1188+
1189+
// Create fulfill request.
1190+
let fulfill = FulfillProofRequest {
1191+
format: MessageFormat::Binary as i32,
1192+
signature: proto_sign(fulfiller_signer, &fulfill_body).as_bytes().to_vec(),
1193+
body: Some(fulfill_body),
1194+
};
1195+
1196+
// Add verifier signature if required.
1197+
let verify = if needs_verifier_signature {
1198+
let fulfill_id = fulfill
1199+
.body
1200+
.as_ref()
1201+
.unwrap()
1202+
.hash_with_signer(fulfiller_signer.address().as_slice())
1203+
.expect("Failed to hash fulfill body");
1204+
use alloy::signers::SignerSync;
1205+
Some(verifier_signer.sign_message_sync(&fulfill_id).unwrap().as_bytes().to_vec())
1206+
} else {
1207+
None
1208+
};
1209+
1210+
VAppTransaction::Clear(ClearTransaction {
1211+
request,
1212+
bid,
1213+
settle,
1214+
execute,
1215+
fulfill: Some(fulfill),
1216+
verify,
1217+
vk: None,
1218+
})
1219+
}

0 commit comments

Comments
 (0)