trazaeo 0.5.6

Open-source provenance SDK and specification for verifiable EO and climate data workflows
Documentation
//! Portable proof-log data model and linkage checks.
//!
//! This module intentionally owns the chain-agnostic commitment and receipt
//! shapes. Solana transport and RPC behavior live in `onchain` and
//! `adaptors::proof_log`, so the core provenance model stays reusable for
//! non-Solana proof-log backends.

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,
}

/// Creates the portable proof-log commitment for a publish envelope.
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(),
    }
}

/// Verifies that a portable proof-log commitment matches the publish envelope.
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![],
            ogc_refs: vec![],
            c2pa_manifest_ref: None,
            c2pa_manifest_hash: None,
            c2pa_ingredients: vec![],
            c2pa_actions: 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",
                }
            })
        );
    }
}