use crate::envelope::PublishEnvelope;
use crate::error::{TrazaeoError, TrazaeoResult};
use crate::proof_log::{
build_proof_log_commitment, verify_proof_log_commitment_linkage, ProofLogCommitment,
ProofLogPublishResult, ProofLogReceipt,
};
use crate::solana::{
cluster_name, commit_anchor_v1, get_anchor_account_by_pda, get_transaction, program_id,
CommitAnchorV1Payload, SolanaClient, TxResult,
};
#[derive(Debug, Clone, Copy)]
pub struct PublishProofLogCommitRequest<'a> {
pub envelope: &'a PublishEnvelope,
pub client: &'a SolanaClient,
pub attestor_pubkey: [u8; 32],
pub committed_at: &'a str,
pub committed_unix_seconds: i64,
pub prev_entry_hash: [u8; 32],
pub attestor_key_ref: &'a str,
}
fn build_proof_log_receipt(tx: &TxResult) -> ProofLogReceipt {
ProofLogReceipt {
entry_id: tx.signature.clone(),
network: tx.cluster.clone(),
verifier_ref: tx.program_id.clone(),
inclusion_height: tx.slot,
finalized: tx.finalized,
locator: tx.anchor_account_pda.clone(),
}
}
pub fn verify_proof_log_commitment_onchain(
envelope: &PublishEnvelope,
commitment: &ProofLogCommitment,
client: &SolanaClient,
expected_cluster: &str,
expected_program_id: &str,
) -> TrazaeoResult<()> {
verify_proof_log_commitment_linkage(envelope, commitment).map_err(|_| {
TrazaeoError::invalid_input(
"verify proof log linkage onchain",
"proof-log linkage mismatch against envelope",
)
})?;
let tx = get_transaction(client, &commitment.entry_id)?.ok_or_else(|| {
TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log entry not found on chain",
)
})?;
if !tx.finalized {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log entry not finalized",
));
}
if tx.cluster != expected_cluster {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log entry cluster mismatch",
));
}
if tx.program_id != expected_program_id {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log entry program mismatch",
));
}
let account = get_anchor_account_by_pda(client, &tx.anchor_account_pda)?.ok_or_else(|| {
TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account not found",
)
})?;
if hex::encode(account.anchored_envelope_hash) != commitment.envelope_hash {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account envelope hash mismatch",
));
}
if hex::encode(account.anchored_checkpoint_hash) != commitment.checkpoint_hash {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account checkpoint hash mismatch",
));
}
if hex::encode(account.anchored_root_hash) != commitment.log_root_hash {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account log root hash mismatch",
));
}
Ok(())
}
pub fn commit_publish_proof_log(
request: PublishProofLogCommitRequest<'_>,
) -> TrazaeoResult<ProofLogPublishResult> {
let anchored_envelope_hash = blake3::hash(&request.envelope.canonical_signed_bytes());
let anchored_checkpoint_hash = hex::decode(&request.envelope.checkpoint_manifest_hash)
.map_err(|e| {
TrazaeoError::invalid_input(
"commit publish proof log",
format!("invalid checkpoint_manifest_hash hex: {e}"),
)
})?;
let anchored_checkpoint_hash: [u8; 32] = anchored_checkpoint_hash
.as_slice()
.try_into()
.map_err(|_| {
TrazaeoError::invalid_input(
"commit publish proof log",
"checkpoint_manifest_hash must be exactly 32 bytes",
)
})?;
let anchored_log_root_hash =
hex::decode(&request.envelope.checkpoint_log_root_hash).map_err(|e| {
TrazaeoError::invalid_input(
"commit publish proof log",
format!("invalid checkpoint_log_root_hash hex: {e}"),
)
})?;
let anchored_log_root_hash: [u8; 32] =
anchored_log_root_hash.as_slice().try_into().map_err(|_| {
TrazaeoError::invalid_input(
"commit publish proof log",
"checkpoint_log_root_hash must be exactly 32 bytes",
)
})?;
let payload = CommitAnchorV1Payload {
schema_version: 1,
anchored_envelope_hash: *anchored_envelope_hash.as_bytes(),
anchored_checkpoint_hash,
anchored_root_hash: anchored_log_root_hash,
anchored_unix_seconds: request.committed_unix_seconds,
prev_anchor_hash: request.prev_entry_hash,
};
let tx = commit_anchor_v1(request.client, request.attestor_pubkey, payload)?;
let commitment = build_proof_log_commitment(
request.envelope,
request.committed_at,
&tx.signature,
request.attestor_key_ref,
);
verify_proof_log_commitment_onchain(
request.envelope,
&commitment,
request.client,
&cluster_name(request.client),
&program_id(request.client),
)?;
Ok(ProofLogPublishResult {
commitment,
receipt: build_proof_log_receipt(&tx),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::{Attestation, PublishEnvelope};
use crate::solana::{commit_anchor_v1, CommitAnchorV1Payload, SolanaConfig};
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_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 onchain_proof_log_verification_happy_path() {
let env = publish();
let client = crate::solana::init_solana_client(&SolanaConfig {
cluster: "solana-testnet".to_string(),
program_id: "program-1".to_string(),
});
let payload = CommitAnchorV1Payload {
schema_version: 1,
anchored_envelope_hash: hex::decode(hex::encode(
blake3::hash(&env.canonical_signed_bytes()).as_bytes(),
))
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_checkpoint_hash: hex::decode(&env.checkpoint_manifest_hash)
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_root_hash: hex::decode(&env.checkpoint_log_root_hash)
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_unix_seconds: 1700000000,
prev_anchor_hash: [0u8; 32],
};
let tx = commit_anchor_v1(&client, [7u8; 32], payload).expect("commit");
let commitment = ProofLogCommitment {
entry_id: tx.signature,
envelope_hash: hex::encode(blake3::hash(&env.canonical_signed_bytes()).as_bytes()),
checkpoint_hash: env.checkpoint_manifest_hash.clone(),
log_root_hash: env.checkpoint_log_root_hash.clone(),
committed_at: "2026-01-01T00:00:00Z".to_string(),
attestor_key_ref: "attestor-key".to_string(),
};
assert!(verify_proof_log_commitment_onchain(
&env,
&commitment,
&client,
"solana-testnet",
"program-1"
)
.is_ok());
}
#[test]
fn onchain_proof_log_verification_rejects_wrong_cluster() {
let env = publish();
let client = crate::solana::init_solana_client(&SolanaConfig {
cluster: "solana-testnet".to_string(),
program_id: "program-1".to_string(),
});
let payload = CommitAnchorV1Payload {
schema_version: 1,
anchored_envelope_hash: hex::decode(hex::encode(
blake3::hash(&env.canonical_signed_bytes()).as_bytes(),
))
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_checkpoint_hash: hex::decode(&env.checkpoint_manifest_hash)
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_root_hash: hex::decode(&env.checkpoint_log_root_hash)
.expect("hex decode")
.try_into()
.expect("fixed array"),
anchored_unix_seconds: 1700000000,
prev_anchor_hash: [0u8; 32],
};
let tx = commit_anchor_v1(&client, [7u8; 32], payload).expect("commit");
let commitment = ProofLogCommitment {
entry_id: tx.signature,
envelope_hash: hex::encode(blake3::hash(&env.canonical_signed_bytes()).as_bytes()),
checkpoint_hash: env.checkpoint_manifest_hash.clone(),
log_root_hash: env.checkpoint_log_root_hash.clone(),
committed_at: "2026-01-01T00:00:00Z".to_string(),
attestor_key_ref: "attestor-key".to_string(),
};
assert!(verify_proof_log_commitment_onchain(
&env,
&commitment,
&client,
"solana-mainnet",
"program-1"
)
.is_err());
}
#[test]
fn commit_publish_proof_log_returns_verified_result() {
let env = publish();
let client = crate::solana::init_solana_client(&SolanaConfig {
cluster: "solana-testnet".to_string(),
program_id: "program-1".to_string(),
});
let result = commit_publish_proof_log(PublishProofLogCommitRequest {
envelope: &env,
client: &client,
attestor_pubkey: [7u8; 32],
committed_at: "2026-01-01T00:00:00Z",
committed_unix_seconds: 1700000000,
prev_entry_hash: [0u8; 32],
attestor_key_ref: "attestor-key",
})
.expect("commit publish proof log");
assert!(verify_proof_log_commitment_linkage(&env, &result.commitment).is_ok());
assert!(verify_proof_log_commitment_onchain(
&env,
&result.commitment,
&client,
"solana-testnet",
"program-1"
)
.is_ok());
}
}