treeship-core 0.10.4

Portable trust receipts for agent workflows - core library
Documentation
//! Merkle tree module: append-only binary Merkle tree with checkpoints
//! and inclusion proofs for batch integrity verification.

pub mod tree;
pub mod checkpoint;
pub mod proof;

pub use tree::{
    MerkleTree, MerkleError, InclusionProof, Direction, ProofStep,
    MERKLE_ALGORITHM_V1, MERKLE_ALGORITHM_V2, MERKLE_VERSION_V1, MERKLE_VERSION_V2,
    default_merkle_version_v1,
};
pub use checkpoint::{
    Checkpoint, CheckpointError,
    CANONICAL_VERSION_V1, CANONICAL_VERSION_V2, CANONICAL_VERSION_V3,
};
pub use proof::{ProofFile, ArtifactSummary};

#[cfg(test)]
mod tests {
    use super::*;
    use crate::attestation::{Ed25519Signer, Signer};
    use crate::trust::{encode_ed25519_pubkey, TrustRoot, TrustRootKind, TrustRootStore};

    fn trust_for(signer: &Ed25519Signer) -> TrustRootStore {
        use ed25519_dalek::VerifyingKey;
        let pk_bytes: [u8; 32] = signer.public_key_bytes().try_into().unwrap();
        let vk = VerifyingKey::from_bytes(&pk_bytes).unwrap();
        TrustRootStore::with_roots(vec![TrustRoot {
            key_id:     signer.key_id().to_string(),
            public_key: encode_ed25519_pubkey(&vk),
            kind:       TrustRootKind::HubCheckpoint,
            label:      "test".into(),
            added_at:   "2026-05-15T00:00:00Z".into(),
        }])
    }

    #[test]
    fn checkpoint_signs_and_verifies() {
        let mut tree = MerkleTree::new();
        tree.append("art_a");
        tree.append("art_b");

        let signer = Ed25519Signer::generate("key_test").unwrap();
        let trust = trust_for(&signer);
        let checkpoint = Checkpoint::create(1, &tree, &signer).unwrap();

        assert!(checkpoint.verify(&trust));
    }

    #[test]
    fn tampered_checkpoint_fails() {
        let mut tree = MerkleTree::new();
        tree.append("art_a");

        let signer = Ed25519Signer::generate("key_test").unwrap();
        let trust = trust_for(&signer);
        let mut checkpoint = Checkpoint::create(1, &tree, &signer).unwrap();

        checkpoint.tree_size = 999; // tamper

        assert!(!checkpoint.verify(&trust));
    }

    #[test]
    fn proof_file_round_trips_json() {
        let mut tree = MerkleTree::new();
        tree.append("art_a");
        tree.append("art_b");

        let signer = Ed25519Signer::generate("key_test").unwrap();
        let checkpoint = Checkpoint::create(1, &tree, &signer).unwrap();
        let inclusion_proof = tree.inclusion_proof(1).unwrap();

        let file = ProofFile {
            artifact_id: "art_b".to_string(),
            artifact_summary: ArtifactSummary {
                actor: "agent://test".to_string(),
                action: "test.run".to_string(),
                timestamp: "2026-03-26T00:00:00Z".to_string(),
                key_id: "key_test".to_string(),
            },
            inclusion_proof,
            checkpoint,
        };

        let json = serde_json::to_string(&file).unwrap();
        let restored: ProofFile = serde_json::from_str(&json).unwrap();
        assert_eq!(restored.artifact_id, "art_b");
    }
}