commit ab3fdc5c4c9f2a94eeb5c4762d3da578d5c2bf7a
parent 59349f5ebe5a386540c4426bb2946d397c23bba2
Author: triesap <tyson@radroots.org>
Date: Thu, 21 May 2026 00:26:54 +0000
validation: trust receipt worker evidence
- add explicit trusted RHI worker pubkey runtime configuration
- split trusted worker evidence from informational untrusted evidence output
- batch validation receipt list evidence hydration for trusted worker keys
- map proof material errors to specific stable CLI reason codes
Diffstat:
13 files changed, 413 insertions(+), 89 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -40,7 +40,7 @@ radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
radroots_sdk = { path = "../lib/crates/sdk", features = ["radrootsd-client", "relay-client", "signing"] }
radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] }
radroots_sql_core = { path = "../lib/crates/sql_core", features = ["native"] }
-radroots_sp1_host_trade = { path = "../lib/crates/sp1_host_trade", features = ["expensive_proofs"] }
+radroots_sp1_host_trade = { path = "../lib/crates/sp1_host_trade", features = ["sp1_verify"] }
radroots_trade = { path = "../lib/crates/trade" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diff --git a/src/operation_basket.rs b/src/operation_basket.rs
@@ -1462,6 +1462,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_core.rs b/src/operation_core.rs
@@ -1235,6 +1235,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_farm.rs b/src/operation_farm.rs
@@ -571,6 +571,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_listing.rs b/src/operation_listing.rs
@@ -553,6 +553,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_market.rs b/src/operation_market.rs
@@ -697,6 +697,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_order.rs b/src/operation_order.rs
@@ -2132,6 +2132,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/operation_runtime.rs b/src/operation_runtime.rs
@@ -418,6 +418,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -53,6 +53,7 @@ const ENV_HYF_ENABLED: &str = "RADROOTS_HYF_ENABLED";
const ENV_HYF_EXECUTABLE: &str = "RADROOTS_HYF_EXECUTABLE";
const ENV_RPC_URL: &str = "RADROOTS_RPC_URL";
const ENV_RPC_BEARER_TOKEN: &str = "RADROOTS_RPC_BEARER_TOKEN";
+const ENV_TRUSTED_RHI_WORKER_PUBKEYS: &str = "RADROOTS_TRUSTED_RHI_WORKER_PUBKEYS";
const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_OUTPUT,
ENV_CLI_LOG_FILTER,
@@ -76,6 +77,7 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_HYF_EXECUTABLE,
ENV_RPC_URL,
ENV_RPC_BEARER_TOKEN,
+ ENV_TRUSTED_RHI_WORKER_PUBKEYS,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -365,6 +367,11 @@ pub struct RpcConfig {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RhiConfig {
+ pub trusted_worker_pubkeys: Vec<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConfig {
pub output: OutputConfig,
pub interaction: InteractionConfig,
@@ -381,6 +388,7 @@ pub struct RuntimeConfig {
pub myc: MycConfig,
pub hyf: HyfConfig,
pub rpc: RpcConfig,
+ pub rhi: RhiConfig,
pub capability_bindings: Vec<CapabilityBindingConfig>,
}
@@ -406,6 +414,7 @@ struct CliConfigFile {
myc: Option<MycFileConfig>,
hyf: Option<HyfFileConfig>,
rpc: Option<RpcFileConfig>,
+ rhi: Option<RhiFileConfig>,
capability_binding: Option<Vec<CapabilityBindingFileConfig>>,
}
@@ -426,6 +435,11 @@ struct RpcFileConfig {
}
#[derive(Debug, Default, Deserialize)]
+struct RhiFileConfig {
+ trusted_worker_pubkeys: Option<Vec<String>>,
+}
+
+#[derive(Debug, Default, Deserialize)]
struct MycFileConfig {
executable: Option<PathBuf>,
status_timeout_ms: Option<u64>,
@@ -674,6 +688,12 @@ impl RuntimeConfig {
app_config.as_ref(),
workspace_config.as_ref(),
)?,
+ rhi: resolve_rhi_config(
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?,
})
}
@@ -826,6 +846,65 @@ fn resolve_rpc_config(
})
}
+fn resolve_rhi_config(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> Result<RhiConfig, RuntimeError> {
+ let trusted_worker_pubkeys =
+ if let Some(value) = env_value(env, env_file, &[ENV_TRUSTED_RHI_WORKER_PUBKEYS]) {
+ parse_pubkey_env_value(value.as_str(), ENV_TRUSTED_RHI_WORKER_PUBKEYS)?
+ } else if let Some(values) = user_config
+ .and_then(|config| config.rhi.as_ref())
+ .and_then(|rhi| rhi.trusted_worker_pubkeys.clone())
+ {
+ normalize_pubkeys(values, "user config [rhi].trusted_worker_pubkeys")?
+ } else if let Some(values) = workspace_config
+ .and_then(|config| config.rhi.as_ref())
+ .and_then(|rhi| rhi.trusted_worker_pubkeys.clone())
+ {
+ normalize_pubkeys(values, "workspace config [rhi].trusted_worker_pubkeys")?
+ } else {
+ Vec::new()
+ };
+
+ Ok(RhiConfig {
+ trusted_worker_pubkeys,
+ })
+}
+
+fn parse_pubkey_env_value(value: &str, key: &str) -> Result<Vec<String>, RuntimeError> {
+ let entries = value
+ .split(',')
+ .map(str::trim)
+ .filter(|entry| !entry.is_empty())
+ .map(ToOwned::to_owned)
+ .collect::<Vec<_>>();
+ normalize_pubkeys(entries, key)
+}
+
+fn normalize_pubkeys(values: Vec<String>, source: &str) -> Result<Vec<String>, RuntimeError> {
+ let mut normalized = Vec::new();
+ for value in values {
+ let pubkey = validate_pubkey(value.as_str(), source)?;
+ if !normalized.iter().any(|existing| existing == &pubkey) {
+ normalized.push(pubkey);
+ }
+ }
+ Ok(normalized)
+}
+
+fn validate_pubkey(value: &str, source: &str) -> Result<String, RuntimeError> {
+ let trimmed = value.trim();
+ if trimmed.len() != 64 || !trimmed.chars().all(|char| char.is_ascii_hexdigit()) {
+ return Err(RuntimeError::Config(format!(
+ "{source} must contain 64-character hex Nostr public keys"
+ )));
+ }
+ Ok(trimmed.to_ascii_lowercase())
+}
+
fn resolve_capability_bindings(
user_config: Option<&CliConfigFile>,
workspace_config: Option<&CliConfigFile>,
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -17216,6 +17216,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -366,6 +366,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: bindings,
}
}
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -2482,6 +2482,9 @@ mod tests {
url: "http://127.0.0.1:7070".into(),
bridge_bearer_token: None,
},
+ rhi: crate::runtime::config::RhiConfig {
+ trusted_worker_pubkeys: Vec::new(),
+ },
capability_bindings: Vec::new(),
}
}
diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs
@@ -1,3 +1,5 @@
+use std::collections::{BTreeMap, BTreeSet};
+
use radroots_events::kinds::{
KIND_TRADE_VALIDATION_RECEIPT, KIND_WORKER_TRADE_TRANSITION_PROOF_RES,
};
@@ -137,6 +139,7 @@ pub struct ValidationReceiptProofVerificationView {
pub proof_reference: Option<String>,
pub inline_proof_present: bool,
pub worker_evidence: Option<ValidationReceiptWorkerEvidenceView>,
+ pub untrusted_worker_evidence: Option<ValidationReceiptWorkerEvidenceView>,
pub reason_code: Option<String>,
pub reason: Option<String>,
}
@@ -144,6 +147,7 @@ pub struct ValidationReceiptProofVerificationView {
#[derive(Debug, Clone, Serialize)]
pub struct ValidationReceiptWorkerEvidenceView {
pub result_event_id: String,
+ pub author: String,
pub status: String,
pub prover_backend: String,
pub proof_mode: String,
@@ -155,6 +159,12 @@ pub struct ValidationReceiptWorkerEvidenceView {
pub public_values_hash: String,
}
+#[derive(Clone, Debug, Default)]
+struct ValidationReceiptWorkerEvidenceSelection {
+ trusted: Option<ValidationReceiptWorkerEvidenceView>,
+ untrusted: Option<ValidationReceiptWorkerEvidenceView>,
+}
+
#[derive(Debug, Clone, Serialize)]
pub struct ValidationReceiptSummaryView {
pub resource: ValidationReceiptResourceView,
@@ -189,15 +199,23 @@ enum ValidationReceiptCommandIntent {
#[derive(Debug, Deserialize)]
struct ValidationReceiptWorkerResultPayload {
cryptographic_proof_verified: bool,
+ decision_event_id: Option<String>,
+ event_set_root: Option<String>,
+ listing_event_id: Option<String>,
+ order_id: Option<String>,
proof_generated: bool,
proof_mode: String,
proof_system: String,
public_values_hash: String,
prover_backend: String,
+ receipt_kind: Option<u32>,
receipt_event_id: String,
+ reducer_output_root: Option<String>,
+ request_event_id: Option<String>,
sp1_execute_checked: bool,
sp1_execute_public_values_hash: Option<String>,
status: String,
+ worker_role: Option<String>,
}
pub fn get(
@@ -347,7 +365,8 @@ fn inspected_event_view(
Ok(verified) => {
let event_id = converted.id.clone();
let order_id = verified.tags.order_id.clone();
- let proof_verification = proof_verification_view(config, &event_id, &verified.receipt);
+ let proof_verification =
+ proof_verification_view(config, &event_id, &verified.receipt, &verified.tags);
let reason_code =
(!failed_relays.is_empty()).then_some("relay_fetch_partial".to_owned());
let accepted = match intent {
@@ -436,7 +455,7 @@ fn list_from_fetch_receipt(
.cmp(&right.created_at.as_secs())
.then_with(|| left.id.to_hex().cmp(&right.id.to_hex()))
});
- let mut receipts = Vec::new();
+ let mut verified_receipts = Vec::new();
let mut invalid_receipts = Vec::new();
for event in events {
@@ -449,29 +468,7 @@ fn list_from_fetch_receipt(
},
) {
Ok(verified) => {
- let proof_verification =
- proof_verification_view(config, &converted.id, &verified.receipt);
- if proof_state_is_invalid(proof_verification.state.as_str()) {
- invalid_receipts.push(ValidationReceiptInvalidCandidateView {
- receipt_event_id: converted.id,
- kind: converted.kind,
- reason_code: proof_verification
- .reason_code
- .clone()
- .unwrap_or_else(|| proof_verification.state.clone()),
- reason: proof_verification.reason.clone().unwrap_or_else(|| {
- "validation receipt proof material did not verify".to_owned()
- }),
- proof_verification: Some(proof_verification),
- });
- } else {
- receipts.push(summary_view(
- &converted,
- &verified.receipt,
- &verified.tags,
- &proof_verification,
- ));
- }
+ verified_receipts.push((converted, verified.receipt, verified.tags));
}
Err(error) => {
let reason_code = validation_receipt_invalid_reason_code(&error);
@@ -486,6 +483,41 @@ fn list_from_fetch_receipt(
}
}
+ let evidence_bindings = verified_receipts
+ .iter()
+ .map(|(event, receipt, tags)| WorkerEvidenceReceiptBinding {
+ receipt_event_id: event.id.as_str(),
+ receipt,
+ tags,
+ })
+ .collect::<Vec<_>>();
+ let mut worker_evidence = worker_evidence_for_receipts(config, &evidence_bindings);
+ let mut receipts = Vec::new();
+ for (event, receipt, tags) in verified_receipts {
+ let proof_verification = proof_verification_view_for_receipt(
+ &receipt,
+ worker_evidence
+ .remove(event.id.as_str())
+ .unwrap_or_default(),
+ );
+ if proof_state_is_invalid(proof_verification.state.as_str()) {
+ invalid_receipts.push(ValidationReceiptInvalidCandidateView {
+ receipt_event_id: event.id,
+ kind: event.kind,
+ reason_code: proof_verification
+ .reason_code
+ .clone()
+ .unwrap_or_else(|| proof_verification.state.clone()),
+ reason: proof_verification.reason.clone().unwrap_or_else(|| {
+ "validation receipt proof material did not verify".to_owned()
+ }),
+ proof_verification: Some(proof_verification),
+ });
+ } else {
+ receipts.push(summary_view(&event, &receipt, &tags, &proof_verification));
+ }
+ }
+
let failed_relays = relay_failures(failed_relays);
let valid_count = receipts.len();
let invalid_count = invalid_receipts.len();
@@ -732,19 +764,21 @@ fn proof_verification_view(
config: &RuntimeConfig,
receipt_event_id: &str,
receipt: &RadrootsTradeValidationReceipt,
+ tags: &RadrootsValidationReceiptTags,
) -> ValidationReceiptProofVerificationView {
- let worker_evidence = worker_evidence_for_receipt(config, receipt_event_id, receipt);
+ let worker_evidence = worker_evidence_for_receipt(config, receipt_event_id, receipt, tags);
proof_verification_view_for_receipt(receipt, worker_evidence)
}
fn proof_verification_view_for_receipt(
receipt: &RadrootsTradeValidationReceipt,
- worker_evidence: Option<ValidationReceiptWorkerEvidenceView>,
+ worker_evidence: ValidationReceiptWorkerEvidenceSelection,
) -> ValidationReceiptProofVerificationView {
let proof = &receipt.proof;
let cryptographic_proof_required = proof.system != RadrootsValidationReceiptProofSystem::None;
if proof.system == RadrootsValidationReceiptProofSystem::None {
let state = if worker_evidence
+ .trusted
.as_ref()
.is_some_and(|evidence| evidence.sp1_execute_checked)
{
@@ -765,7 +799,8 @@ fn proof_verification_view_for_receipt(
verifying_key_hash: proof.verifying_key_hash.clone(),
proof_reference: proof.proof_reference.clone(),
inline_proof_present: proof.inline_proof_base64.is_some(),
- worker_evidence,
+ worker_evidence: worker_evidence.trusted,
+ untrusted_worker_evidence: worker_evidence.untrusted,
reason_code: None,
reason: None,
};
@@ -818,7 +853,8 @@ fn proof_verification_view_for_receipt(
verifying_key_hash: proof.verifying_key_hash.clone(),
proof_reference: proof.proof_reference.clone(),
inline_proof_present: proof.inline_proof_base64.is_some(),
- worker_evidence,
+ worker_evidence: worker_evidence.trusted,
+ untrusted_worker_evidence: worker_evidence.untrusted,
reason_code: None,
reason: None,
},
@@ -840,7 +876,7 @@ fn proof_verification_view_for_receipt(
fn sp1_unverified_proof_view(
receipt: &RadrootsTradeValidationReceipt,
- worker_evidence: Option<ValidationReceiptWorkerEvidenceView>,
+ worker_evidence: ValidationReceiptWorkerEvidenceSelection,
state: &str,
public_values_hash_binding: &str,
proof_metadata_binding: &str,
@@ -861,7 +897,8 @@ fn sp1_unverified_proof_view(
verifying_key_hash: proof.verifying_key_hash.clone(),
proof_reference: proof.proof_reference.clone(),
inline_proof_present: proof.inline_proof_base64.is_some(),
- worker_evidence,
+ worker_evidence: worker_evidence.trusted,
+ untrusted_worker_evidence: worker_evidence.untrusted,
reason_code: reason_code.map(str::to_owned),
reason: reason.map(str::to_owned),
}
@@ -871,9 +908,13 @@ fn validation_receipt_invalid_reason_code(error: &RadrootsValidationReceiptError
use radroots_trade::validation_receipt::RadrootsValidationReceiptError;
match error {
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") => {
+ RadrootsValidationReceiptError::InvalidProofMetadata("proof.material")
+ | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => {
"sp1_proof_material_missing"
}
+ RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => {
+ "sp1_proof_material_conflict"
+ }
RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") => {
"sp1_inline_proof_invalid"
}
@@ -901,11 +942,17 @@ fn invalid_proof_verification_view(
) -> Option<ValidationReceiptProofVerificationView> {
let reason_code = validation_receipt_invalid_reason_code(error);
let (state, public_values_hash_binding, proof_metadata_binding) = match error {
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") => (
+ RadrootsValidationReceiptError::InvalidProofMetadata("proof.material")
+ | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => (
"sp1_proof_material_missing",
"unverified",
"missing_proof_material",
),
+ RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => (
+ "sp1_proof_material_conflict",
+ "unverified",
+ "conflicting_proof_material",
+ ),
RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64")
| RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference")
| RadrootsValidationReceiptError::InvalidProofMetadata("proof.mode")
@@ -945,6 +992,7 @@ fn invalid_proof_verification_view(
proof_reference: None,
inline_proof_present: false,
worker_evidence: None,
+ untrusted_worker_evidence: None,
reason_code: Some(reason_code.to_owned()),
reason: Some(error.to_string()),
})
@@ -971,6 +1019,12 @@ fn proof_state_from_sp1_error(error: &RadrootsSp1TradeHostError) -> MappedSp1Pro
proof_metadata_binding: "missing_proof_material",
reason_code: "sp1_proof_material_missing",
},
+ RadrootsSp1TradeHostError::ProofMaterialConflict => MappedSp1ProofError {
+ state: "sp1_proof_material_conflict",
+ public_values_hash_binding: "unverified",
+ proof_metadata_binding: "conflicting_proof_material",
+ reason_code: "sp1_proof_material_conflict",
+ },
RadrootsSp1TradeHostError::PublicValuesHashMismatch
| RadrootsSp1TradeHostError::Sp1PublicValuesMismatch
| RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch(_) => MappedSp1ProofError {
@@ -1020,6 +1074,7 @@ fn proof_state_is_invalid(state: &str) -> bool {
matches!(
state,
"sp1_proof_material_missing"
+ | "sp1_proof_material_conflict"
| "sp1_public_values_mismatch"
| "sp1_program_hash_mismatch"
| "sp1_verifying_key_hash_mismatch"
@@ -1035,55 +1090,157 @@ fn proof_state_is_verification_success(state: &str) -> bool {
}
fn validation_receipt_worker_result_filter(
- receipt_event_id: &str,
+ receipt_event_ids: Vec<String>,
) -> Result<RadrootsNostrFilter, String> {
let filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(
KIND_WORKER_TRADE_TRANSITION_PROOF_RES as u16,
));
- radroots_nostr_filter_tag(filter, "e", vec![receipt_event_id.to_owned()])
+ radroots_nostr_filter_tag(filter, "e", receipt_event_ids)
.map_err(|error| format!("build validation receipt worker result filter: {error}"))
}
+struct WorkerEvidenceReceiptBinding<'a> {
+ receipt_event_id: &'a str,
+ receipt: &'a RadrootsTradeValidationReceipt,
+ tags: &'a RadrootsValidationReceiptTags,
+}
+
fn worker_evidence_for_receipt(
config: &RuntimeConfig,
receipt_event_id: &str,
receipt: &RadrootsTradeValidationReceipt,
-) -> Option<ValidationReceiptWorkerEvidenceView> {
- let filter = validation_receipt_worker_result_filter(receipt_event_id).ok()?;
- let fetch_receipt = fetch_events_from_relays(&config.relay.urls, filter).ok()?;
- let mut evidence = fetch_receipt
- .events
+ tags: &RadrootsValidationReceiptTags,
+) -> ValidationReceiptWorkerEvidenceSelection {
+ let bindings = [WorkerEvidenceReceiptBinding {
+ receipt_event_id,
+ receipt,
+ tags,
+ }];
+ worker_evidence_for_receipts(config, &bindings)
+ .remove(receipt_event_id)
+ .unwrap_or_default()
+}
+
+fn worker_evidence_for_receipts(
+ config: &RuntimeConfig,
+ bindings: &[WorkerEvidenceReceiptBinding<'_>],
+) -> BTreeMap<String, ValidationReceiptWorkerEvidenceSelection> {
+ if config.rhi.trusted_worker_pubkeys.is_empty() || bindings.is_empty() {
+ return BTreeMap::new();
+ }
+ let receipt_event_ids = bindings
+ .iter()
+ .map(|binding| binding.receipt_event_id.to_owned())
+ .collect::<Vec<_>>();
+ let filter = match validation_receipt_worker_result_filter(receipt_event_ids) {
+ Ok(filter) => filter,
+ Err(_) => return BTreeMap::new(),
+ };
+ let fetch_receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
+ Ok(fetch_receipt) => fetch_receipt,
+ Err(_) => return BTreeMap::new(),
+ };
+ let binding_by_receipt_id = bindings
+ .iter()
+ .map(|binding| (binding.receipt_event_id, binding))
+ .collect::<BTreeMap<_, _>>();
+ let trusted_pubkeys = config
+ .rhi
+ .trusted_worker_pubkeys
+ .iter()
+ .map(|pubkey| pubkey.to_ascii_lowercase())
+ .collect::<BTreeSet<_>>();
+ let mut by_receipt =
+ BTreeMap::<String, Vec<(u64, String, bool, ValidationReceiptWorkerEvidenceView)>>::new();
+
+ for event in fetch_receipt.events {
+ let payload =
+ match serde_json::from_str::<ValidationReceiptWorkerResultPayload>(&event.content) {
+ Ok(payload) => payload,
+ Err(_) => continue,
+ };
+ let Some(binding) = binding_by_receipt_id.get(payload.receipt_event_id.as_str()) else {
+ continue;
+ };
+ let converted = radroots_event_from_nostr(&event);
+ let author = converted.author.to_ascii_lowercase();
+ let trusted_author = trusted_pubkeys.contains(author.as_str());
+ let bound = worker_payload_binds_receipt(&payload, binding);
+ let trusted = trusted_author && bound;
+ let receipt_event_id = payload.receipt_event_id.clone();
+ let result_event_id = event.id.to_hex();
+ let view = ValidationReceiptWorkerEvidenceView {
+ result_event_id: result_event_id.clone(),
+ author,
+ status: payload.status,
+ prover_backend: payload.prover_backend,
+ proof_mode: payload.proof_mode,
+ proof_system: payload.proof_system,
+ proof_generated: payload.proof_generated,
+ sp1_execute_checked: payload.sp1_execute_checked,
+ sp1_execute_public_values_hash: payload.sp1_execute_public_values_hash,
+ cryptographic_proof_verified: payload.cryptographic_proof_verified,
+ public_values_hash: payload.public_values_hash,
+ };
+ by_receipt.entry(receipt_event_id).or_default().push((
+ event.created_at.as_secs(),
+ result_event_id,
+ trusted,
+ view,
+ ));
+ }
+
+ by_receipt
.into_iter()
- .filter_map(|event| {
- let payload =
- serde_json::from_str::<ValidationReceiptWorkerResultPayload>(&event.content)
- .ok()?;
- if payload.receipt_event_id != receipt_event_id
- || payload.public_values_hash != receipt.public_values_hash
- || payload.proof_system != receipt.proof.system.as_str()
- {
- return None;
+ .map(|(receipt_event_id, mut candidates)| {
+ candidates.sort_by(|left, right| {
+ left.0
+ .cmp(&right.0)
+ .then_with(|| left.1.cmp(&right.1))
+ .then_with(|| left.2.cmp(&right.2))
+ });
+ let mut selection = ValidationReceiptWorkerEvidenceSelection::default();
+ for (_, _, trusted, view) in candidates.into_iter().rev() {
+ if trusted && selection.trusted.is_none() {
+ selection.trusted = Some(view);
+ } else if !trusted && selection.untrusted.is_none() {
+ selection.untrusted = Some(view);
+ }
+ if selection.trusted.is_some() && selection.untrusted.is_some() {
+ break;
+ }
}
- Some((
- event.created_at.as_secs(),
- event.id.to_hex(),
- ValidationReceiptWorkerEvidenceView {
- result_event_id: event.id.to_hex(),
- status: payload.status,
- prover_backend: payload.prover_backend,
- proof_mode: payload.proof_mode,
- proof_system: payload.proof_system,
- proof_generated: payload.proof_generated,
- sp1_execute_checked: payload.sp1_execute_checked,
- sp1_execute_public_values_hash: payload.sp1_execute_public_values_hash,
- cryptographic_proof_verified: payload.cryptographic_proof_verified,
- public_values_hash: payload.public_values_hash,
- },
- ))
+ (receipt_event_id, selection)
})
- .collect::<Vec<_>>();
- evidence.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
- evidence.pop().map(|(_, _, view)| view)
+ .collect()
+}
+
+fn worker_payload_binds_receipt(
+ payload: &ValidationReceiptWorkerResultPayload,
+ binding: &WorkerEvidenceReceiptBinding<'_>,
+) -> bool {
+ let receipt = binding.receipt;
+ let tags = binding.tags;
+ payload.status == "succeeded"
+ && payload.worker_role.as_deref() == Some("non_authoritative_prover")
+ && payload.receipt_kind == Some(KIND_TRADE_VALIDATION_RECEIPT)
+ && payload.receipt_event_id == binding.receipt_event_id
+ && payload.order_id.as_deref() == Some(tags.order_id.as_str())
+ && payload.listing_event_id.as_deref() == Some(tags.root_event_id.as_str())
+ && payload.event_set_root.as_deref() == Some(tags.event_set_root.as_str())
+ && payload.reducer_output_root.as_deref() == Some(tags.reducer_output_root.as_str())
+ && payload.request_event_id.as_deref() == Some(tags.root_event_id.as_str())
+ && payload.decision_event_id.as_deref() == Some(tags.target_event_id.as_str())
+ && payload.public_values_hash == receipt.public_values_hash
+ && payload.proof_system == receipt.proof.system.as_str()
+ && payload.proof_mode == receipt.proof.mode.as_deref().unwrap_or("none")
+ && payload.proof_generated
+ == (receipt.proof.system != RadrootsValidationReceiptProofSystem::None)
+ && payload.cryptographic_proof_verified == payload.proof_generated
+ && !payload.prover_backend.trim().is_empty()
+ && payload.sp1_execute_checked
+ && payload.sp1_execute_public_values_hash.as_deref()
+ == Some(receipt.public_values_hash.as_str())
}
fn summary_view(
@@ -1132,8 +1289,8 @@ fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> {
#[cfg(test)]
mod tests {
use super::{
- ValidationReceiptWorkerEvidenceView, proof_verification_view_for_receipt,
- validation_receipt_invalid_reason_code,
+ ValidationReceiptWorkerEvidenceSelection, ValidationReceiptWorkerEvidenceView,
+ proof_verification_view_for_receipt, validation_receipt_invalid_reason_code,
};
use radroots_trade::validation_receipt::{
RadrootsTradeValidationReceipt, RadrootsValidationReceiptError,
@@ -1198,7 +1355,10 @@ mod tests {
#[test]
fn none_receipts_report_deterministic_verification_without_crypto_claim() {
- let view = proof_verification_view_for_receipt(&deterministic_receipt(), None);
+ let view = proof_verification_view_for_receipt(
+ &deterministic_receipt(),
+ ValidationReceiptWorkerEvidenceSelection::default(),
+ );
assert_eq!(view.state, "deterministic_receipt_verified");
assert!(!view.cryptographic_proof_required);
@@ -1209,21 +1369,28 @@ mod tests {
fn none_receipts_surface_advisory_sp1_execute_evidence() {
let view = proof_verification_view_for_receipt(
&deterministic_receipt(),
- Some(ValidationReceiptWorkerEvidenceView {
- result_event_id: "result-1".to_owned(),
- status: "succeeded".to_owned(),
- prover_backend: "local_execute".to_owned(),
- proof_mode: "none".to_owned(),
- proof_system: "none".to_owned(),
- proof_generated: false,
- sp1_execute_checked: true,
- sp1_execute_public_values_hash: Some(
- "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(),
- ),
- cryptographic_proof_verified: false,
- public_values_hash:
- "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(),
- }),
+ ValidationReceiptWorkerEvidenceSelection {
+ trusted: Some(ValidationReceiptWorkerEvidenceView {
+ result_event_id: "result-1".to_owned(),
+ author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ .to_owned(),
+ status: "succeeded".to_owned(),
+ prover_backend: "local_execute".to_owned(),
+ proof_mode: "none".to_owned(),
+ proof_system: "none".to_owned(),
+ proof_generated: false,
+ sp1_execute_checked: true,
+ sp1_execute_public_values_hash: Some(
+ "0x5555555555555555555555555555555555555555555555555555555555555555"
+ .to_owned(),
+ ),
+ cryptographic_proof_verified: false,
+ public_values_hash:
+ "0x5555555555555555555555555555555555555555555555555555555555555555"
+ .to_owned(),
+ }),
+ untrusted: None,
+ },
);
assert_eq!(view.state, "sp1_execute_checked");
@@ -1232,12 +1399,48 @@ mod tests {
}
#[test]
+ fn untrusted_worker_evidence_does_not_upgrade_deterministic_receipts() {
+ let view = proof_verification_view_for_receipt(
+ &deterministic_receipt(),
+ ValidationReceiptWorkerEvidenceSelection {
+ trusted: None,
+ untrusted: Some(ValidationReceiptWorkerEvidenceView {
+ result_event_id: "result-1".to_owned(),
+ author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ .to_owned(),
+ status: "succeeded".to_owned(),
+ prover_backend: "local_execute".to_owned(),
+ proof_mode: "none".to_owned(),
+ proof_system: "none".to_owned(),
+ proof_generated: false,
+ sp1_execute_checked: true,
+ sp1_execute_public_values_hash: Some(
+ "0x5555555555555555555555555555555555555555555555555555555555555555"
+ .to_owned(),
+ ),
+ cryptographic_proof_verified: false,
+ public_values_hash:
+ "0x5555555555555555555555555555555555555555555555555555555555555555"
+ .to_owned(),
+ }),
+ },
+ );
+
+ assert_eq!(view.state, "deterministic_receipt_verified");
+ assert!(view.worker_evidence.is_none());
+ assert!(view.untrusted_worker_evidence.is_some());
+ }
+
+ #[test]
fn sp1_receipts_with_references_report_unresolved_without_crypto_claim() {
let mut receipt = receipt_with_proof(sp1_proof_with_material());
receipt.proof.inline_proof_base64 = None;
receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64)));
- let view = proof_verification_view_for_receipt(&receipt, None);
+ let view = proof_verification_view_for_receipt(
+ &receipt,
+ ValidationReceiptWorkerEvidenceSelection::default(),
+ );
assert_eq!(view.state, "sp1_reference_unresolved");
assert!(view.cryptographic_proof_required);
@@ -1249,7 +1452,7 @@ mod tests {
fn invalid_inline_sp1_material_reports_invalid_proof_state() {
let view = proof_verification_view_for_receipt(
&receipt_with_proof(sp1_proof_with_material()),
- None,
+ ValidationReceiptWorkerEvidenceSelection::default(),
);
assert_eq!(view.state, "sp1_proof_invalid");
@@ -1267,6 +1470,18 @@ mod tests {
"sp1_proof_material_missing"
);
assert_eq!(
+ validation_receipt_invalid_reason_code(
+ &RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing")
+ ),
+ "sp1_proof_material_missing"
+ );
+ assert_eq!(
+ validation_receipt_invalid_reason_code(
+ &RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict")
+ ),
+ "sp1_proof_material_conflict"
+ );
+ assert_eq!(
validation_receipt_invalid_reason_code(&RadrootsValidationReceiptError::TagMismatch(
"public_values_hash"
)),