Skip to main content

treeship_core/merkle/
checkpoint.rs

1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use serde::{Deserialize, Serialize};
4
5use crate::attestation::{Signer, SignerError};
6use crate::statements::unix_to_rfc3339;
7
8use super::tree::MerkleTree;
9
10/// A signed snapshot of the Merkle tree at a point in time.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Checkpoint {
13    pub index: u64,
14    /// Root hash in `sha256:<hex>` format.
15    pub root: String,
16    pub tree_size: usize,
17    pub height: usize,
18    /// RFC 3339 timestamp.
19    pub signed_at: String,
20    /// Key ID of the signer.
21    pub signer: String,
22    /// Base64url-encoded public key bytes.
23    pub public_key: String,
24    /// Base64url-encoded Ed25519 signature of the canonical form.
25    pub signature: String,
26    /// Merkle algorithm used. Missing = v1 (sha256-duplicate-last).
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub algorithm: Option<String>,
29    /// Optional ZK chain proof result (added when proof is ready).
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub zk_proof: Option<ChainProofSummary>,
32}
33
34/// Summary of a RISC Zero chain proof, embedded in a Merkle checkpoint.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ChainProofSummary {
37    pub image_id: String,
38    pub all_signatures_valid: bool,
39    pub chain_intact: bool,
40    pub approval_nonces_matched: bool,
41    pub artifact_count: u64,
42    pub public_key_digest: String,
43    pub proved_at: String,
44}
45
46/// Errors from checkpoint creation.
47#[derive(Debug)]
48pub enum CheckpointError {
49    EmptyTree,
50    Signing(SignerError),
51}
52
53impl std::fmt::Display for CheckpointError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::EmptyTree => write!(f, "cannot checkpoint an empty tree"),
57            Self::Signing(e) => write!(f, "checkpoint signing failed: {}", e),
58        }
59    }
60}
61
62impl std::error::Error for CheckpointError {}
63impl From<SignerError> for CheckpointError {
64    fn from(e: SignerError) -> Self {
65        Self::Signing(e)
66    }
67}
68
69impl Checkpoint {
70    /// Create a signed checkpoint from the current tree state.
71    ///
72    /// The canonical form for signing is: `{root}|{tree_size}|{signed_at}`
73    pub fn create(
74        index: u64,
75        tree: &MerkleTree,
76        signer: &dyn Signer,
77    ) -> Result<Self, CheckpointError> {
78        let root_bytes = tree.root().ok_or(CheckpointError::EmptyTree)?;
79        let root = format!("sha256:{}", hex::encode(root_bytes));
80
81        let secs = std::time::SystemTime::now()
82            .duration_since(std::time::UNIX_EPOCH)
83            .unwrap_or_default()
84            .as_secs();
85        let signed_at = unix_to_rfc3339(secs);
86
87        let canonical = format!("{}|{}|{}|{}|{}|{}", index, root, tree.len(), tree.height(), signer.key_id(), signed_at);
88        let sig_bytes = signer.sign(canonical.as_bytes())?;
89        let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
90        let public_key = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
91
92        Ok(Self {
93            index,
94            root,
95            tree_size: tree.len(),
96            height: tree.height(),
97            signed_at,
98            signer: signer.key_id().to_string(),
99            public_key,
100            signature,
101            algorithm: Some(super::tree::MERKLE_ALGORITHM_V2.to_string()),
102            zk_proof: None,
103        })
104    }
105
106    /// Verify the checkpoint signature. Returns `false` on any failure
107    /// (bad encoding, wrong key size, invalid signature). Never panics.
108    pub fn verify(&self) -> bool {
109        let pub_bytes = match URL_SAFE_NO_PAD.decode(&self.public_key) {
110            Ok(b) => b,
111            Err(_) => return false,
112        };
113        let pub_array: [u8; 32] = match pub_bytes.as_slice().try_into() {
114            Ok(a) => a,
115            Err(_) => return false,
116        };
117        let vk = match VerifyingKey::from_bytes(&pub_array) {
118            Ok(k) => k,
119            Err(_) => return false,
120        };
121
122        let canonical = format!("{}|{}|{}|{}|{}|{}", self.index, self.root, self.tree_size, self.height, self.signer, self.signed_at);
123
124        let sig_bytes = match URL_SAFE_NO_PAD.decode(&self.signature) {
125            Ok(b) => b,
126            Err(_) => return false,
127        };
128        let sig_array: [u8; 64] = match sig_bytes.as_slice().try_into() {
129            Ok(a) => a,
130            Err(_) => return false,
131        };
132        let sig = Signature::from_bytes(&sig_array);
133
134        vk.verify(canonical.as_bytes(), &sig).is_ok()
135    }
136}