use serde::{Deserialize, Serialize};
use crate::engine_context_source_execution::{canonical_sha256, sha256_digest};
use crate::{
AcceptanceState, EngineContextSourceExecutionRequestV1, EngineContextSourceExecutionResponseV1,
ReceiptDocumentV1, ReceiptEvidenceKindV1, ReceiptTerminalStatusV1, ValidationError,
};
pub const MAX_ENGINE_SOURCE_RECEIPT_DOCUMENT_BYTES: usize = 1024 * 1024;
pub const MAX_ENGINE_SOURCE_EXECUTION_V2_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EngineContextSourceExecutionResponseV2 {
pub schema_version: u32,
pub execution: EngineContextSourceExecutionResponseV1,
pub receipt_document_json: String,
}
impl EngineContextSourceExecutionResponseV2 {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != 2
|| self.receipt_document_json.len() > MAX_ENGINE_SOURCE_RECEIPT_DOCUMENT_BYTES
{
return Err(invalid());
}
self.execution.validate()?;
let bytes = self.receipt_document_json.as_bytes();
let projection = &self.execution.canonical_receipt;
if sha256_digest(bytes)? != projection.receipt_digest {
return Err(invalid());
}
let document = ReceiptDocumentV1::from_canonical_bytes(bytes)?;
let lineage = &document.lineage;
let plan = &self.execution.execution_plan;
let invocation = &self.execution.invocation;
if document.receipt_id.as_str() != projection.receipt_id.as_str()
|| document.outcome.state != AcceptanceState::Unknown
|| document.status != ReceiptTerminalStatusV1::Succeeded
|| lineage.task_id != plan.task_id
|| lineage.plan_id != plan.plan_id
|| lineage.plan_ref != canonical_sha256(plan)?
|| lineage.invocation_id != invocation.invocation_id.as_str()
|| lineage.invocation_ref != canonical_sha256(invocation)?
|| !invocation.source_refs.iter().any(|reference| {
reference.as_str() == format!("task:{}", lineage.task_ref.as_str())
})
{
return Err(invalid());
}
let native = self
.execution
.observation
.receipt_link
.as_ref()
.ok_or_else(invalid)?;
let observation_digest = canonical_sha256(&self.execution.observation)?;
for (uri, digest) in [
(
format!("artifact://engine/receipts/{}", native.receipt_digest.hex()),
&native.receipt_digest,
),
("artifact://engine/observation".into(), &observation_digest),
] {
if !document.evidence_refs.iter().any(|evidence| {
evidence.uri.as_str() == uri
&& &evidence.digest == digest
&& evidence.kind == ReceiptEvidenceKindV1::Measurement
}) {
return Err(invalid());
}
}
let encoded = serde_json::to_vec(self).map_err(|_| invalid())?;
if encoded.len() > MAX_ENGINE_SOURCE_EXECUTION_V2_RESPONSE_BYTES {
return Err(invalid());
}
Ok(())
}
pub fn validate_against(
&self,
request: &EngineContextSourceExecutionRequestV1,
) -> Result<(), ValidationError> {
self.execution.validate_against(request)?;
self.validate()
}
}
fn invalid() -> ValidationError {
ValidationError::new("source execution v2 does not bind the exact canonical receipt")
}