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::{
attestor_key_ref, cluster_name, commit_anchor_v1, derive_anchor_account_pda,
get_anchor_account_by_pda, get_transaction, program_id, verify_anchor_transaction_signature,
CommitAnchorV1Payload, LocalAttestorSigner, SolanaClient, TxResult,
};
#[derive(Debug, Clone, Copy)]
pub struct PublishProofLogCommitRequest<'a> {
pub envelope: &'a PublishEnvelope,
pub client: &'a SolanaClient,
pub attestor_signing_seed: [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,
expected_attestor_pubkey: &[u8; 32],
) -> 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 expected_attestor_key_ref = attestor_key_ref(expected_attestor_pubkey);
if commitment.attestor_key_ref != expected_attestor_key_ref {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log attestor key 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",
));
}
if &account.attestor_pubkey != expected_attestor_pubkey {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account attestor mismatch",
));
}
let (expected_pda, expected_bump) = derive_anchor_account_pda(
expected_program_id,
expected_attestor_pubkey,
&account.anchored_envelope_hash,
);
if tx.anchor_account_pda != expected_pda || account.bump != expected_bump {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account pda mismatch",
));
}
if account.anchored_slot != tx.slot {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log account slot mismatch",
));
}
if !verify_anchor_transaction_signature(&tx, &account) {
return Err(TrazaeoError::external(
"verify proof log linkage onchain",
"proof-log attestor signature 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 signer = LocalAttestorSigner::from_seed(request.attestor_signing_seed);
let expected_attestor_pubkey = signer.attestor_pubkey();
let expected_attestor_key_ref = attestor_key_ref(&expected_attestor_pubkey);
if request.attestor_key_ref != expected_attestor_key_ref {
return Err(TrazaeoError::invalid_input(
"commit publish proof log",
"attestor_key_ref must be the hex-encoded attestor public key",
));
}
let tx = commit_anchor_v1(request.client, &signer, payload)?;
let commitment = build_proof_log_commitment(
request.envelope,
request.committed_at,
&tx.signature,
&expected_attestor_key_ref,
);
verify_proof_log_commitment_onchain(
request.envelope,
&commitment,
request.client,
&cluster_name(request.client),
&program_id(request.client),
&expected_attestor_pubkey,
)?;
Ok(ProofLogPublishResult {
commitment,
receipt: build_proof_log_receipt(&tx),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::{Attestation, PublishEnvelope};
use crate::solana::{
attestor_key_ref, commit_anchor_v1, CommitAnchorV1Payload, LocalAttestorSigner,
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 signer = LocalAttestorSigner::from_seed([7u8; 32]);
let attestor_pubkey = signer.attestor_pubkey();
let tx = commit_anchor_v1(&client, &signer, 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_ref(&attestor_pubkey),
};
assert!(verify_proof_log_commitment_onchain(
&env,
&commitment,
&client,
"solana-testnet",
"program-1",
&attestor_pubkey
)
.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 signer = LocalAttestorSigner::from_seed([7u8; 32]);
let attestor_pubkey = signer.attestor_pubkey();
let tx = commit_anchor_v1(&client, &signer, 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_ref(&attestor_pubkey),
};
assert!(verify_proof_log_commitment_onchain(
&env,
&commitment,
&client,
"solana-mainnet",
"program-1",
&attestor_pubkey
)
.is_err());
}
#[test]
fn onchain_proof_log_verification_rejects_untrusted_attestor_anchor() {
let env = publish();
let client = crate::solana::init_solana_client(&SolanaConfig {
cluster: "solana-testnet".to_string(),
program_id: "program-1".to_string(),
});
let attacker_signer = LocalAttestorSigner::from_seed([0x42u8; 32]);
let trusted_pubkey = LocalAttestorSigner::from_seed([0x99u8; 32]).attestor_pubkey();
let payload = CommitAnchorV1Payload {
schema_version: 1,
anchored_envelope_hash: *blake3::hash(&env.canonical_signed_bytes()).as_bytes(),
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, &attacker_signer, payload).expect("attacker 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_ref(&trusted_pubkey),
};
assert!(verify_proof_log_commitment_onchain(
&env,
&commitment,
&client,
"solana-testnet",
"program-1",
&trusted_pubkey
)
.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_signing_seed: [7u8; 32],
committed_at: "2026-01-01T00:00:00Z",
committed_unix_seconds: 1700000000,
prev_entry_hash: [0u8; 32],
attestor_key_ref: &attestor_key_ref(
&LocalAttestorSigner::from_seed([7u8; 32]).attestor_pubkey(),
),
})
.expect("commit publish proof log");
let attestor_pubkey = LocalAttestorSigner::from_seed([7u8; 32]).attestor_pubkey();
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",
&attestor_pubkey
)
.is_ok());
}
}