use crate::envelope::PublishEnvelope;
use crate::error::{TrazaeoError, TrazaeoResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofLogCommitment {
#[serde(rename = "proof_log_entry_id")]
pub entry_id: String,
#[serde(rename = "committed_envelope_hash")]
pub envelope_hash: String,
#[serde(rename = "committed_checkpoint_hash")]
pub checkpoint_hash: String,
#[serde(rename = "committed_log_root_hash")]
pub log_root_hash: String,
#[serde(rename = "committed_at")]
pub committed_at: String,
pub attestor_key_ref: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofLogReceipt {
#[serde(rename = "entry_id")]
pub entry_id: String,
#[serde(rename = "network")]
pub network: String,
#[serde(rename = "verifier_ref")]
pub verifier_ref: String,
#[serde(rename = "inclusion_height")]
pub inclusion_height: u64,
pub finalized: bool,
#[serde(rename = "locator")]
pub locator: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofLogPublishResult {
#[serde(rename = "proof_log_commitment")]
pub commitment: ProofLogCommitment,
#[serde(rename = "proof_log_receipt")]
pub receipt: ProofLogReceipt,
}
pub fn build_proof_log_commitment(
envelope: &PublishEnvelope,
committed_at: &str,
entry_id: &str,
attestor_key_ref: &str,
) -> ProofLogCommitment {
let envelope_hash = hex::encode(blake3::hash(&envelope.canonical_signed_bytes()).as_bytes());
ProofLogCommitment {
entry_id: entry_id.to_string(),
envelope_hash,
checkpoint_hash: envelope.checkpoint_manifest_hash.clone(),
log_root_hash: envelope.checkpoint_log_root_hash.clone(),
committed_at: committed_at.to_string(),
attestor_key_ref: attestor_key_ref.to_string(),
}
}
pub fn verify_proof_log_commitment_linkage(
envelope: &PublishEnvelope,
commitment: &ProofLogCommitment,
) -> TrazaeoResult<()> {
let expected = hex::encode(blake3::hash(&envelope.canonical_signed_bytes()).as_bytes());
let matches = expected == commitment.envelope_hash
&& envelope.checkpoint_manifest_hash == commitment.checkpoint_hash
&& envelope.checkpoint_log_root_hash == commitment.log_root_hash
&& !commitment.entry_id.trim().is_empty();
if matches {
Ok(())
} else {
Err(TrazaeoError::invalid_input(
"verify proof log commitment linkage",
"proof log commitment mismatch against envelope",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::{Attestation, PublishEnvelope};
use serde_json::json;
fn publish() -> PublishEnvelope {
PublishEnvelope {
schema_version: "1.0.0".to_string(),
envelope_type: "publish".to_string(),
issued_at: "2026-01-01T00:00:00Z".to_string(),
subject_id: "publish-1".to_string(),
dataset_id: "sst".to_string(),
dataset_version: "v1".to_string(),
input_refs: vec!["obj://in".to_string()],
output_refs: vec!["obj://out".to_string()],
published_artifacts: vec![crate::checkpoint::CheckpointArtifact {
artifact_id: "artifact-1".to_string(),
content_root_hash:
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
content_descriptor_ref: None,
content_descriptor_hash: None,
media_type: "application/vnd+zarr".to_string(),
}],
primary_artifact_id: "artifact-1".to_string(),
checkpoint_manifest_ref: "checkpoint://1".to_string(),
checkpoint_manifest_hash:
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
checkpoint_id: "checkpoint-1".to_string(),
checkpoint_log_root_hash:
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
lineage_refs: vec!["capture://1".to_string()],
verification_policy_id: "verify-default".to_string(),
attestations: vec![Attestation {
signer_id: "s".to_string(),
key_id: "k".to_string(),
signature: "sig".to_string(),
signed_at: "2026-01-01T00:00:00Z".to_string(),
}],
key_id: "key-1".to_string(),
stac_refs: vec![],
reward_context_ref: None,
reward_context_hash: None,
provenance_start_mode: "transport_capture".to_string(),
bootstrap_origin_label: None,
reward_eligible: false,
}
}
#[test]
fn proof_log_commitment_links_to_publish_envelope() {
let env = publish();
let commitment =
build_proof_log_commitment(&env, "2026-01-01T00:01:00Z", "tx-1", "attestor-key");
assert!(verify_proof_log_commitment_linkage(&env, &commitment).is_ok());
}
#[test]
fn proof_log_commitment_linkage_fails_when_root_mismatch() {
let env = publish();
let mut commitment =
build_proof_log_commitment(&env, "2026-01-01T00:01:00Z", "tx-1", "attestor-key");
commitment.log_root_hash = "bad".to_string();
assert!(verify_proof_log_commitment_linkage(&env, &commitment).is_err());
}
#[test]
fn proof_log_publish_result_uses_proof_log_json_shape() {
let result = ProofLogPublishResult {
commitment: ProofLogCommitment {
entry_id: "tx-1".to_string(),
envelope_hash: "aa".repeat(32),
checkpoint_hash: "bb".repeat(32),
log_root_hash: "cc".repeat(32),
committed_at: "2026-01-01T00:01:00Z".to_string(),
attestor_key_ref: "attestor-key".to_string(),
},
receipt: ProofLogReceipt {
entry_id: "tx-1".to_string(),
network: "solana-devnet".to_string(),
verifier_ref: "program-1".to_string(),
inclusion_height: 7,
finalized: true,
locator: "pda-1".to_string(),
},
};
let value = serde_json::to_value(result).expect("serialize proof-log result");
assert_eq!(
value,
json!({
"proof_log_commitment": {
"proof_log_entry_id": "tx-1",
"committed_envelope_hash": "aa".repeat(32),
"committed_checkpoint_hash": "bb".repeat(32),
"committed_log_root_hash": "cc".repeat(32),
"committed_at": "2026-01-01T00:01:00Z",
"attestor_key_ref": "attestor-key",
},
"proof_log_receipt": {
"entry_id": "tx-1",
"network": "solana-devnet",
"verifier_ref": "program-1",
"inclusion_height": 7,
"finalized": true,
"locator": "pda-1",
}
})
);
}
}