commit 23325e48422212ad4e978047c44fa1efe6fb0151
parent 095a49de28ae59288184419579c7d3a7b77a4e80
Author: triesap <tyson@radroots.org>
Date: Tue, 19 May 2026 07:52:50 +0000
sp1: bind proof identity
- add distinct SP1 program identity to trade proof public values
- derive host artifact program hash from the guest ELF
- reject SP1 program and verifying-key mismatches
- validate guest and host SP1 identity lanes
Diffstat:
2 files changed, 166 insertions(+), 15 deletions(-)
diff --git a/crates/sp1_guest_trade/src/lib.rs b/crates/sp1_guest_trade/src/lib.rs
@@ -9,6 +9,7 @@ pub const RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION: u32 = 1;
pub const RADROOTS_SP1_TRADE_PROTOCOL_VERSION: &str = "radroots.trade.v1";
pub const RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH: &str =
"0x3d8f7f463904d71f2d0d14b1551450756697e51c7b658e10c6d5c20a7bc61f08";
+pub const RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET: &str = "trade.order_acceptance.v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -36,6 +37,7 @@ pub struct RadrootsSp1TradeProofPublicValues {
pub statement_type: RadrootsSp1TradeProofStatementType,
pub radroots_protocol_version: String,
pub reducer_program_hash: String,
+ pub sp1_program_hash: Option<String>,
pub sp1_verifying_key_hash: Option<String>,
pub event_set_root: String,
pub listing_addr_hash: Option<String>,
@@ -120,6 +122,7 @@ pub struct RadrootsSp1TradeOrderAcceptanceWitness {
pub previous_state_root: Option<String>,
pub reducer_program_hash: String,
pub radroots_protocol_version: String,
+ pub sp1_program_hash: Option<String>,
pub sp1_verifying_key_hash: Option<String>,
}
@@ -214,6 +217,7 @@ pub fn reduce_order_acceptance_public_values(
statement_type: RadrootsSp1TradeProofStatementType::TradeTransition,
radroots_protocol_version: witness.radroots_protocol_version.clone(),
reducer_program_hash: witness.reducer_program_hash.clone(),
+ sp1_program_hash: witness.sp1_program_hash.clone(),
sp1_verifying_key_hash: witness.sp1_verifying_key_hash.clone(),
event_set_root,
listing_addr_hash: Some(hash_bytes(
@@ -293,6 +297,9 @@ fn validate_witness_header(
if let Some(hash) = &witness.sp1_verifying_key_hash {
validate_hash32(hash, "sp1_verifying_key_hash")?;
}
+ if let Some(hash) = &witness.sp1_program_hash {
+ validate_hash32(hash, "sp1_program_hash")?;
+ }
Ok(())
}
@@ -455,6 +462,9 @@ fn validate_public_values(
"radroots_protocol_version",
)?;
validate_hash32(&public_values.reducer_program_hash, "reducer_program_hash")?;
+ if let Some(hash) = &public_values.sp1_program_hash {
+ validate_hash32(hash, "sp1_program_hash")?;
+ }
if let Some(hash) = &public_values.sp1_verifying_key_hash {
validate_hash32(hash, "sp1_verifying_key_hash")?;
}
@@ -611,6 +621,9 @@ mod tests {
previous_state_root: None,
reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
+ sp1_program_hash: Some(
+ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
+ ),
sp1_verifying_key_hash: Some(
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
),
diff --git a/crates/sp1_host_trade/src/lib.rs b/crates/sp1_host_trade/src/lib.rs
@@ -107,6 +107,10 @@ pub enum RadrootsSp1TradeHostError {
Sp1ProofModeMismatch,
#[error("SP1 verifying key hash mismatch")]
Sp1VerifyingKeyHashMismatch,
+ #[error("SP1 program hash mismatch")]
+ Sp1ProgramHashMismatch,
+ #[error("SP1 program hash is missing")]
+ MissingSp1ProgramHash,
#[error("proof artifact encoding failed")]
ProofEncoding,
}
@@ -146,6 +150,11 @@ pub fn order_acceptance_guest_elf() -> sp1_sdk::Elf {
}
#[cfg(feature = "expensive_proofs")]
+pub fn sp1_program_hash_for_order_acceptance_guest() -> String {
+ sp1_program_hash_for_elf(&order_acceptance_guest_elf())
+}
+
+#[cfg(feature = "expensive_proofs")]
pub async fn execute_order_acceptance_sp1_public_values(
witness: &RadrootsSp1TradeOrderAcceptanceWitness,
) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> {
@@ -159,9 +168,14 @@ pub async fn execute_order_acceptance_sp1_public_values_with_elf(
) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> {
use sp1_sdk::{Prover, ProverClient, SP1Stdin, StatusCode};
- let expected = execute_order_acceptance_public_values(witness)?;
+ let witness = witness_with_sp1_identity(
+ witness,
+ Some(sp1_program_hash_for_elf(&elf)),
+ witness.sp1_verifying_key_hash.clone(),
+ )?;
+ let expected = execute_order_acceptance_public_values(&witness)?;
let mut stdin = SP1Stdin::new();
- stdin.write(witness);
+ stdin.write(&witness);
let client = ProverClient::builder().light().build().await;
let (public_values, report) = client
.execute(elf, stdin)
@@ -200,14 +214,19 @@ pub async fn generate_order_acceptance_sp1_proof(
};
let sp1_mode = sp1_proof_mode(mode)?;
- let expected = execute_order_acceptance_public_values(witness)?;
let client = ProverClient::builder().cpu().build().await;
+ let elf = order_acceptance_guest_elf();
+ let sp1_program_hash = sp1_program_hash_for_elf(&elf);
let pk = client
- .setup(order_acceptance_guest_elf())
+ .setup(elf)
.await
.map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?;
+ let verifying_key_hash = pk.verifying_key().bytes32();
+ let witness =
+ witness_with_sp1_identity(witness, Some(sp1_program_hash), Some(verifying_key_hash))?;
+ let expected = execute_order_acceptance_public_values(&witness)?;
let mut stdin = SP1Stdin::new();
- stdin.write(witness);
+ stdin.write(&witness);
let proof = client
.prove(&pk, stdin)
.mode(sp1_mode)
@@ -228,12 +247,7 @@ pub async fn generate_order_acceptance_sp1_proof(
}
let proof_bytes =
bincode::serialize(&proof).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?;
- let proof = proof_artifact_for_real_sp1_execution(
- &execution,
- mode,
- pk.verifying_key().bytes32(),
- &proof_bytes,
- )?;
+ let proof = proof_artifact_for_real_sp1_execution(&execution, mode, &proof_bytes)?;
verify_order_acceptance_proof_artifact(&execution, &proof)?;
Ok(RadrootsSp1TradeProofBundle { execution, proof })
}
@@ -263,6 +277,10 @@ pub async fn verify_order_acceptance_sp1_proof_artifact(
if artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) {
return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch);
}
+ let sp1_program_hash = sp1_program_hash_for_order_acceptance_guest();
+ if artifact.program_hash.as_deref() != Some(sp1_program_hash.as_str()) {
+ return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch);
+ }
client
.verify(&proof, pk.verifying_key(), Some(StatusCode::SUCCESS))
.map_err(|error| {
@@ -311,6 +329,16 @@ pub fn verify_order_acceptance_proof_artifact(
if artifact.inline_proof_base64.is_none() && artifact.proof_reference.is_none() {
return Err(RadrootsSp1TradeHostError::MissingProofMaterial);
}
+ if artifact.program_hash.as_deref()
+ != execution.public_values.sp1_program_hash.as_deref()
+ {
+ return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch);
+ }
+ if artifact.verifying_key_hash.as_deref()
+ != execution.public_values.sp1_verifying_key_hash.as_deref()
+ {
+ return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch);
+ }
}
}
Ok(())
@@ -380,7 +408,6 @@ fn proof_artifact_for_execution(
fn proof_artifact_for_real_sp1_execution(
execution: &RadrootsSp1TradePublicValuesExecution,
mode: RadrootsSp1TradeProofMode,
- verifying_key_hash: String,
proof_bytes: &[u8],
) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> {
let system = mode.proof_system();
@@ -390,12 +417,18 @@ fn proof_artifact_for_real_sp1_execution(
let mut artifact = RadrootsSp1TradeProofArtifact {
inline_proof_base64: Some(base64::engine::general_purpose::STANDARD.encode(proof_bytes)),
mode: mode.mode_label().map(str::to_string),
- program_hash: Some(execution.public_values.reducer_program_hash.clone()),
+ program_hash: Some(
+ execution
+ .public_values
+ .sp1_program_hash
+ .clone()
+ .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?,
+ ),
proof_digest: String::new(),
proof_reference: None,
public_values_hash: execution.public_values_hash.clone(),
system,
- verifying_key_hash: Some(verifying_key_hash),
+ verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(),
};
artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?;
Ok(artifact)
@@ -434,6 +467,56 @@ fn hex_lower(bytes: &[u8]) -> String {
}
#[cfg(feature = "expensive_proofs")]
+fn sp1_program_hash_for_elf(elf: &sp1_sdk::Elf) -> String {
+ let bytes: &[u8] = match elf {
+ sp1_sdk::Elf::Static(bytes) => bytes,
+ sp1_sdk::Elf::Dynamic(bytes) => bytes,
+ };
+ hash_bytes("radroots:sp1-guest-elf:v1", bytes)
+}
+
+#[cfg(feature = "expensive_proofs")]
+fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(domain.as_bytes());
+ hasher.update(bytes);
+ format!("0x{}", hex_lower(hasher.finalize().as_slice()))
+}
+
+#[cfg(feature = "expensive_proofs")]
+fn witness_with_sp1_identity(
+ witness: &RadrootsSp1TradeOrderAcceptanceWitness,
+ sp1_program_hash: Option<String>,
+ sp1_verifying_key_hash: Option<String>,
+) -> Result<RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeHostError> {
+ if let (Some(existing), Some(actual)) = (
+ witness.sp1_program_hash.as_deref(),
+ sp1_program_hash.as_deref(),
+ ) {
+ if existing != actual {
+ return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch);
+ }
+ }
+ if let (Some(existing), Some(actual)) = (
+ witness.sp1_verifying_key_hash.as_deref(),
+ sp1_verifying_key_hash.as_deref(),
+ ) {
+ if existing != actual {
+ return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch);
+ }
+ }
+
+ let mut bound = witness.clone();
+ if let Some(hash) = sp1_program_hash {
+ bound.sp1_program_hash = Some(hash);
+ }
+ if let Some(hash) = sp1_verifying_key_hash {
+ bound.sp1_verifying_key_hash = Some(hash);
+ }
+ Ok(bound)
+}
+
+#[cfg(feature = "expensive_proofs")]
fn public_values_prefix(bytes: &[u8]) -> String {
const PREFIX_LEN: usize = 32;
hex_lower(&bytes[..bytes.len().min(PREFIX_LEN)])
@@ -600,6 +683,9 @@ mod tests {
previous_state_root: None,
reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
+ sp1_program_hash: Some(
+ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
+ ),
sp1_verifying_key_hash: Some(
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
),
@@ -713,6 +799,58 @@ mod tests {
}
#[test]
+ fn sp1_artifact_program_hash_must_match_public_values_identity() {
+ let mut input = witness();
+ input.sp1_program_hash =
+ Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string());
+ let execution =
+ super::execute_order_acceptance_public_values(&input).expect("deterministic execution");
+ let mut artifact = super::RadrootsSp1TradeProofArtifact {
+ inline_proof_base64: Some("cHJvb2Y=".to_string()),
+ mode: Some("core".to_string()),
+ program_hash: Some(
+ "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(),
+ ),
+ proof_digest: String::new(),
+ proof_reference: None,
+ public_values_hash: execution.public_values_hash.clone(),
+ system: RadrootsValidationReceiptProofSystem::Sp1Core,
+ verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(),
+ };
+ artifact.proof_digest =
+ super::proof_digest_for_execution(&execution, &artifact).expect("proof digest");
+ let err = verify_order_acceptance_proof_artifact(&execution, &artifact)
+ .expect_err("program hash mismatch");
+ assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProgramHashMismatch);
+ }
+
+ #[test]
+ fn sp1_artifact_program_hash_is_distinct_from_reducer_hash() {
+ let mut input = witness();
+ input.sp1_program_hash =
+ Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string());
+ let execution =
+ super::execute_order_acceptance_public_values(&input).expect("deterministic execution");
+ let mut artifact = super::RadrootsSp1TradeProofArtifact {
+ inline_proof_base64: Some("cHJvb2Y=".to_string()),
+ mode: Some("core".to_string()),
+ program_hash: execution.public_values.sp1_program_hash.clone(),
+ proof_digest: String::new(),
+ proof_reference: None,
+ public_values_hash: execution.public_values_hash.clone(),
+ system: RadrootsValidationReceiptProofSystem::Sp1Core,
+ verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(),
+ };
+ artifact.proof_digest =
+ super::proof_digest_for_execution(&execution, &artifact).expect("proof digest");
+ verify_order_acceptance_proof_artifact(&execution, &artifact).expect("artifact verifies");
+ assert_ne!(
+ artifact.program_hash.as_deref(),
+ Some(execution.public_values.reducer_program_hash.as_str())
+ );
+ }
+
+ #[test]
fn none_proof_mode_builds_deterministic_reducer_receipt() {
let mut input = witness();
input.sp1_verifying_key_hash = None;
@@ -785,7 +923,7 @@ mod tests {
let mut missing = super::RadrootsSp1TradeProofArtifact {
inline_proof_base64: None,
mode: Some("core".to_string()),
- program_hash: Some(execution.public_values.reducer_program_hash.clone()),
+ program_hash: execution.public_values.sp1_program_hash.clone(),
proof_digest: "0x00".to_string(),
proof_reference: None,
public_values_hash: execution.public_values_hash.clone(),