Skip to main content

chio_kernel/
checkpoint.rs

1//! Merkle-committed receipt batch checkpointing.
2//!
3//! Produces signed kernel checkpoint statements that commit a batch of receipts
4//! to a Merkle root. Inclusion proofs allow verifying that a specific receipt
5//! was part of a batch without replaying the entire log.
6//!
7//! Schema: "chio.checkpoint_statement.v1"
8
9use std::collections::{BTreeMap, BTreeSet};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use chio_core::canonical::canonical_json_bytes;
13use chio_core::crypto::{Keypair, PublicKey, Signature, SigningAlgorithm};
14use chio_core::hashing::sha256_hex;
15use chio_core::hashing::Hash;
16use chio_core::merkle::{MerkleProof, MerkleTree};
17use chio_core::receipt::{
18    CheckpointPublicationIdentityKind, CheckpointPublicationTrustAnchorBinding,
19};
20use serde::{Deserialize, Serialize};
21
22use crate::ReceiptStoreError;
23
24pub const CHECKPOINT_SCHEMA: &str = "chio.checkpoint_statement.v1";
25pub const CHECKPOINT_PUBLICATION_SCHEMA: &str = "chio.checkpoint_publication.v1";
26pub const CHECKPOINT_WITNESS_SCHEMA: &str = "chio.checkpoint_witness.v1";
27pub const CHECKPOINT_CONSISTENCY_PROOF_SCHEMA: &str = "chio.checkpoint_consistency_proof.v1";
28pub const CHECKPOINT_EQUIVOCATION_SCHEMA: &str = "chio.checkpoint_equivocation.v1";
29
30#[must_use]
31pub fn is_supported_checkpoint_schema(schema: &str) -> bool {
32    schema == CHECKPOINT_SCHEMA
33}
34
35/// Error type for checkpoint operations.
36#[derive(Debug, thiserror::Error)]
37pub enum CheckpointError {
38    #[error("merkle error: {0}")]
39    Merkle(#[from] chio_core::Error),
40    #[error("serialization error: {0}")]
41    Serialization(String),
42    #[error("signing error: {0}")]
43    Signing(String),
44    #[error("sqlite error: {0}")]
45    Sqlite(#[from] rusqlite::Error),
46    #[error("json error: {0}")]
47    Json(#[from] serde_json::Error),
48    #[error("receipt store error: {0}")]
49    ReceiptStore(#[from] ReceiptStoreError),
50    #[error("invalid checkpoint: {0}")]
51    Invalid(String),
52    #[error("checkpoint signature verification failed")]
53    InvalidSignature,
54    #[error("checkpoint continuity error: {0}")]
55    Continuity(String),
56}
57
58/// The signed body of a kernel checkpoint statement.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct KernelCheckpointBody {
61    /// Schema identifier for new checkpoint issuance.
62    pub schema: String,
63    /// Monotonic checkpoint counter.
64    pub checkpoint_seq: u64,
65    /// First receipt seq in this batch.
66    pub batch_start_seq: u64,
67    /// Last receipt seq in this batch.
68    pub batch_end_seq: u64,
69    /// Number of leaves in the Merkle tree.
70    pub tree_size: usize,
71    /// Root from MerkleTree::from_leaves.
72    pub merkle_root: Hash,
73    /// Unix timestamp (seconds) when the checkpoint was issued.
74    pub issued_at: u64,
75    /// The kernel's signing key (public).
76    pub kernel_key: PublicKey,
77    /// Hash of the immediately preceding checkpoint body when this checkpoint extends a prior batch.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub previous_checkpoint_sha256: Option<String>,
80}
81
82/// A signed kernel checkpoint statement.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct KernelCheckpoint {
85    /// The signed body.
86    pub body: KernelCheckpointBody,
87    /// Ed25519 signature over canonical JSON of `body`.
88    pub signature: Signature,
89}
90
91/// A Merkle inclusion proof for a receipt within a checkpoint batch.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ReceiptInclusionProof {
94    /// Which checkpoint this proof is for.
95    pub checkpoint_seq: u64,
96    /// The seq of the receipt being proved.
97    pub receipt_seq: u64,
98    /// Index of this receipt in the Merkle leaf array.
99    pub leaf_index: usize,
100    /// The Merkle root this proof is against.
101    pub merkle_root: Hash,
102    /// The audit path proof.
103    pub proof: MerkleProof,
104}
105
106impl ReceiptInclusionProof {
107    /// Verify that `receipt_canonical_bytes` is included in the batch.
108    #[must_use]
109    pub fn verify(&self, receipt_canonical_bytes: &[u8], expected_root: &Hash) -> bool {
110        self.proof.verify(receipt_canonical_bytes, expected_root)
111    }
112}
113
114/// A deterministic publication record derived from a signed checkpoint.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct CheckpointPublication {
117    /// Local log identity derived from the checkpoint signing key until an
118    /// explicit persisted transparency log ID is available.
119    pub log_id: String,
120    /// Schema identifier for derived publication records.
121    pub schema: String,
122    /// Monotonic checkpoint counter.
123    pub checkpoint_seq: u64,
124    /// Canonical SHA-256 digest of the signed checkpoint body.
125    pub checkpoint_sha256: String,
126    /// Merkle root published by the checkpoint.
127    pub merkle_root: Hash,
128    /// Timestamp when the checkpoint was issued/published.
129    pub published_at: u64,
130    /// The kernel key that signed the checkpoint.
131    pub kernel_key: PublicKey,
132    /// Cumulative log size derived from the covered entry sequence range.
133    pub log_tree_size: u64,
134    /// First entry sequence covered by this checkpoint batch.
135    pub entry_start_seq: u64,
136    /// Last entry sequence covered by this checkpoint batch.
137    pub entry_end_seq: u64,
138    /// Digest of the predecessor checkpoint body when present.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub previous_checkpoint_sha256: Option<String>,
141    /// Declared verifier material when this publication is tied to a typed
142    /// publication path and explicit trust-anchor policy.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub trust_anchor_binding: Option<CheckpointPublicationTrustAnchorBinding>,
145}
146
147/// A deterministic witness record derived from a checkpoint's predecessor digest.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct CheckpointWitness {
150    /// Local log identity derived from the checkpoint signing key.
151    pub log_id: String,
152    /// Schema identifier for derived witness records.
153    pub schema: String,
154    /// The checkpoint being witnessed.
155    pub checkpoint_seq: u64,
156    /// Canonical SHA-256 digest of the witnessed checkpoint body.
157    pub checkpoint_sha256: String,
158    /// The later checkpoint that cites the witnessed checkpoint digest.
159    pub witness_checkpoint_seq: u64,
160    /// Canonical SHA-256 digest of the witness checkpoint body.
161    pub witness_checkpoint_sha256: String,
162    /// Timestamp from the witness checkpoint body.
163    pub witnessed_at: u64,
164}
165
166/// A deterministic prefix-growth proof derived from checkpoint continuity.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct CheckpointConsistencyProof {
169    /// Schema identifier for derived consistency proof records.
170    pub schema: String,
171    /// Local log identity derived from the checkpoint signing key.
172    pub log_id: String,
173    /// Earlier checkpoint sequence in the proven prefix chain.
174    pub from_checkpoint_seq: u64,
175    /// Later checkpoint sequence in the proven prefix chain.
176    pub to_checkpoint_seq: u64,
177    /// Canonical SHA-256 digest of the earlier checkpoint body.
178    pub from_checkpoint_sha256: String,
179    /// Canonical SHA-256 digest of the later checkpoint body.
180    pub to_checkpoint_sha256: String,
181    /// Cumulative log size before the append.
182    pub from_log_tree_size: u64,
183    /// Cumulative log size after the append.
184    pub to_log_tree_size: u64,
185    /// First entry sequence appended by the later checkpoint.
186    pub appended_entry_start_seq: u64,
187    /// Last entry sequence appended by the later checkpoint.
188    pub appended_entry_end_seq: u64,
189}
190
191/// Classifies a conflicting checkpoint observation.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum CheckpointEquivocationKind {
195    /// Two distinct checkpoints claim the same checkpoint sequence.
196    ConflictingCheckpointSeq,
197    /// Two distinct checkpoints claim the same log and cumulative tree size.
198    ConflictingLogTreeSize,
199    /// Two distinct checkpoints cite the same predecessor digest.
200    ConflictingPredecessorWitness,
201}
202
203/// A deterministic conflict record derived from multiple checkpoint statements.
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
205pub struct CheckpointEquivocation {
206    /// Schema identifier for derived equivocation records.
207    pub schema: String,
208    /// Which transparency rule was violated.
209    pub kind: CheckpointEquivocationKind,
210    /// Local log identity when the conflict can be tied to one derived log.
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub log_id: Option<String>,
213    /// Shared cumulative log size when the conflict is a tree-size fork.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub log_tree_size: Option<u64>,
216    /// The first conflicting checkpoint sequence.
217    pub first_checkpoint_seq: u64,
218    /// The second conflicting checkpoint sequence.
219    pub second_checkpoint_seq: u64,
220    /// Canonical SHA-256 digest of the first checkpoint body.
221    pub first_checkpoint_sha256: String,
222    /// Canonical SHA-256 digest of the second checkpoint body.
223    pub second_checkpoint_sha256: String,
224    /// Shared predecessor digest when the conflict is a witness fork.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub previous_checkpoint_sha256: Option<String>,
227}
228
229/// Derived transparency records for a set of checkpoints.
230#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
231pub struct CheckpointTransparencySummary {
232    /// Publication records for each checkpoint.
233    pub publications: Vec<CheckpointPublication>,
234    /// Witness records derived from predecessor-digest links.
235    pub witnesses: Vec<CheckpointWitness>,
236    /// Prefix-growth proofs derived from contiguous checkpoint extensions.
237    pub consistency_proofs: Vec<CheckpointConsistencyProof>,
238    /// Conflict records derived from contradictory checkpoints.
239    pub equivocations: Vec<CheckpointEquivocation>,
240}
241
242#[must_use]
243pub fn checkpoint_log_id(checkpoint: &KernelCheckpoint) -> String {
244    let log_key_bytes: Vec<u8> = match checkpoint.body.kernel_key.algorithm() {
245        SigningAlgorithm::Ed25519 => checkpoint.body.kernel_key.as_bytes().to_vec(),
246        SigningAlgorithm::P256 | SigningAlgorithm::P384 => {
247            checkpoint.body.kernel_key.to_hex().into_bytes()
248        }
249    };
250    format!("local-log-{}", sha256_hex(&log_key_bytes))
251}
252
253#[must_use]
254pub fn checkpoint_log_tree_size(body: &KernelCheckpointBody) -> u64 {
255    body.batch_end_seq
256}
257
258fn checkpoint_batch_entry_count(body: &KernelCheckpointBody) -> Result<u64, CheckpointError> {
259    body.batch_end_seq
260        .checked_sub(body.batch_start_seq)
261        .and_then(|count| count.checked_add(1))
262        .ok_or_else(|| {
263            CheckpointError::Invalid(format!(
264                "invalid checkpoint entry range {}-{}",
265                body.batch_start_seq, body.batch_end_seq
266            ))
267        })
268}
269
270/// Return the canonical SHA-256 digest for a checkpoint body.
271pub fn checkpoint_body_sha256(body: &KernelCheckpointBody) -> Result<String, CheckpointError> {
272    let body_bytes =
273        canonical_json_bytes(body).map_err(|e| CheckpointError::Serialization(e.to_string()))?;
274    Ok(sha256_hex(&body_bytes))
275}
276
277/// Build a deterministic publication record from a signed checkpoint.
278pub fn build_checkpoint_publication(
279    checkpoint: &KernelCheckpoint,
280) -> Result<CheckpointPublication, CheckpointError> {
281    validate_checkpoint(checkpoint)?;
282    Ok(CheckpointPublication {
283        log_id: checkpoint_log_id(checkpoint),
284        schema: CHECKPOINT_PUBLICATION_SCHEMA.to_string(),
285        checkpoint_seq: checkpoint.body.checkpoint_seq,
286        checkpoint_sha256: checkpoint_body_sha256(&checkpoint.body)?,
287        merkle_root: checkpoint.body.merkle_root,
288        published_at: checkpoint.body.issued_at,
289        kernel_key: checkpoint.body.kernel_key.clone(),
290        log_tree_size: checkpoint_log_tree_size(&checkpoint.body),
291        entry_start_seq: checkpoint.body.batch_start_seq,
292        entry_end_seq: checkpoint.body.batch_end_seq,
293        previous_checkpoint_sha256: checkpoint.body.previous_checkpoint_sha256.clone(),
294        trust_anchor_binding: None,
295    })
296}
297
298/// Build a deterministic publication record that is explicitly bound to
299/// declared trust-anchor verifier material.
300pub fn build_trust_anchored_checkpoint_publication(
301    checkpoint: &KernelCheckpoint,
302    trust_anchor_binding: CheckpointPublicationTrustAnchorBinding,
303) -> Result<CheckpointPublication, CheckpointError> {
304    trust_anchor_binding
305        .validate()
306        .map_err(|error| CheckpointError::Invalid(error.to_string()))?;
307    let publication = build_checkpoint_publication(checkpoint)?;
308    if trust_anchor_binding.publication_identity.kind == CheckpointPublicationIdentityKind::LocalLog
309        && trust_anchor_binding.publication_identity.identity != publication.log_id
310    {
311        return Err(CheckpointError::Invalid(format!(
312            "checkpoint publication local_log identity {} does not match log_id {}",
313            trust_anchor_binding.publication_identity.identity, publication.log_id
314        )));
315    }
316    let mut publication = publication;
317    publication.trust_anchor_binding = Some(trust_anchor_binding);
318    Ok(publication)
319}
320
321/// Build a deterministic witness record when `witness_checkpoint` cites `checkpoint`.
322pub fn build_checkpoint_witness(
323    checkpoint: &KernelCheckpoint,
324    witness_checkpoint: &KernelCheckpoint,
325) -> Result<CheckpointWitness, CheckpointError> {
326    validate_checkpoint(checkpoint)?;
327    validate_checkpoint(witness_checkpoint)?;
328
329    let checkpoint_sha256 = checkpoint_body_sha256(&checkpoint.body)?;
330    let witness_checkpoint_sha256 = checkpoint_body_sha256(&witness_checkpoint.body)?;
331    let Some(previous_checkpoint_sha256) = witness_checkpoint
332        .body
333        .previous_checkpoint_sha256
334        .as_deref()
335    else {
336        return Err(CheckpointError::Continuity(format!(
337            "checkpoint {} does not cite a predecessor digest",
338            witness_checkpoint.body.checkpoint_seq
339        )));
340    };
341    if previous_checkpoint_sha256 != checkpoint_sha256 {
342        return Err(CheckpointError::Continuity(format!(
343            "checkpoint {} does not witness checkpoint {}",
344            witness_checkpoint.body.checkpoint_seq, checkpoint.body.checkpoint_seq
345        )));
346    }
347
348    Ok(CheckpointWitness {
349        log_id: checkpoint_log_id(checkpoint),
350        schema: CHECKPOINT_WITNESS_SCHEMA.to_string(),
351        checkpoint_seq: checkpoint.body.checkpoint_seq,
352        checkpoint_sha256,
353        witness_checkpoint_seq: witness_checkpoint.body.checkpoint_seq,
354        witness_checkpoint_sha256,
355        witnessed_at: witness_checkpoint.body.issued_at,
356    })
357}
358
359/// Build a deterministic consistency proof when `current` cleanly extends `previous`.
360pub fn build_checkpoint_consistency_proof(
361    previous: &KernelCheckpoint,
362    current: &KernelCheckpoint,
363) -> Result<CheckpointConsistencyProof, CheckpointError> {
364    validate_checkpoint_predecessor(previous, current)?;
365    let previous_log_id = checkpoint_log_id(previous);
366    let current_log_id = checkpoint_log_id(current);
367    if previous_log_id != current_log_id {
368        return Err(CheckpointError::Continuity(format!(
369            "checkpoint {} derives log_id {} but predecessor {} derives {}",
370            current.body.checkpoint_seq,
371            current_log_id,
372            previous.body.checkpoint_seq,
373            previous_log_id
374        )));
375    }
376
377    Ok(CheckpointConsistencyProof {
378        schema: CHECKPOINT_CONSISTENCY_PROOF_SCHEMA.to_string(),
379        log_id: current_log_id,
380        from_checkpoint_seq: previous.body.checkpoint_seq,
381        to_checkpoint_seq: current.body.checkpoint_seq,
382        from_checkpoint_sha256: checkpoint_body_sha256(&previous.body)?,
383        to_checkpoint_sha256: checkpoint_body_sha256(&current.body)?,
384        from_log_tree_size: checkpoint_log_tree_size(&previous.body),
385        to_log_tree_size: checkpoint_log_tree_size(&current.body),
386        appended_entry_start_seq: current.body.batch_start_seq,
387        appended_entry_end_seq: current.body.batch_end_seq,
388    })
389}
390
391/// Verify that a consistency proof matches a concrete checkpoint extension.
392pub fn verify_checkpoint_consistency_proof(
393    previous: &KernelCheckpoint,
394    current: &KernelCheckpoint,
395    proof: &CheckpointConsistencyProof,
396) -> Result<bool, CheckpointError> {
397    Ok(*proof == build_checkpoint_consistency_proof(previous, current)?)
398}
399
400#[allow(clippy::too_many_arguments)]
401fn ordered_equivocation(
402    kind: CheckpointEquivocationKind,
403    log_id: Option<String>,
404    log_tree_size: Option<u64>,
405    first_seq: u64,
406    first_sha256: String,
407    second_seq: u64,
408    second_sha256: String,
409    previous_checkpoint_sha256: Option<String>,
410) -> CheckpointEquivocation {
411    if (first_seq, first_sha256.as_str()) <= (second_seq, second_sha256.as_str()) {
412        CheckpointEquivocation {
413            schema: CHECKPOINT_EQUIVOCATION_SCHEMA.to_string(),
414            kind,
415            log_id,
416            log_tree_size,
417            first_checkpoint_seq: first_seq,
418            second_checkpoint_seq: second_seq,
419            first_checkpoint_sha256: first_sha256,
420            second_checkpoint_sha256: second_sha256,
421            previous_checkpoint_sha256,
422        }
423    } else {
424        CheckpointEquivocation {
425            schema: CHECKPOINT_EQUIVOCATION_SCHEMA.to_string(),
426            kind,
427            log_id,
428            log_tree_size,
429            first_checkpoint_seq: second_seq,
430            second_checkpoint_seq: first_seq,
431            first_checkpoint_sha256: second_sha256,
432            second_checkpoint_sha256: first_sha256,
433            previous_checkpoint_sha256,
434        }
435    }
436}
437
438/// Detect whether two checkpoints conflict under Chio transparency semantics.
439pub fn detect_checkpoint_equivocation(
440    first: &KernelCheckpoint,
441    second: &KernelCheckpoint,
442) -> Result<Option<CheckpointEquivocation>, CheckpointError> {
443    validate_checkpoint(first)?;
444    validate_checkpoint(second)?;
445
446    let first_sha256 = checkpoint_body_sha256(&first.body)?;
447    let second_sha256 = checkpoint_body_sha256(&second.body)?;
448    if first_sha256 == second_sha256 {
449        return Ok(None);
450    }
451
452    let first_log_id = checkpoint_log_id(first);
453    let second_log_id = checkpoint_log_id(second);
454    let first_log_tree_size = checkpoint_log_tree_size(&first.body);
455    let second_log_tree_size = checkpoint_log_tree_size(&second.body);
456
457    if first.body.checkpoint_seq == second.body.checkpoint_seq {
458        return Ok(Some(ordered_equivocation(
459            CheckpointEquivocationKind::ConflictingCheckpointSeq,
460            (first_log_id == second_log_id).then_some(first_log_id.clone()),
461            (first_log_tree_size == second_log_tree_size).then_some(first_log_tree_size),
462            first.body.checkpoint_seq,
463            first_sha256,
464            second.body.checkpoint_seq,
465            second_sha256,
466            first
467                .body
468                .previous_checkpoint_sha256
469                .clone()
470                .or_else(|| second.body.previous_checkpoint_sha256.clone()),
471        )));
472    }
473
474    if first_log_id == second_log_id && first_log_tree_size == second_log_tree_size {
475        return Ok(Some(ordered_equivocation(
476            CheckpointEquivocationKind::ConflictingLogTreeSize,
477            Some(first_log_id),
478            Some(first_log_tree_size),
479            first.body.checkpoint_seq,
480            first_sha256,
481            second.body.checkpoint_seq,
482            second_sha256,
483            first
484                .body
485                .previous_checkpoint_sha256
486                .clone()
487                .or_else(|| second.body.previous_checkpoint_sha256.clone()),
488        )));
489    }
490
491    if first.body.previous_checkpoint_sha256.is_some()
492        && first.body.previous_checkpoint_sha256 == second.body.previous_checkpoint_sha256
493    {
494        return Ok(Some(ordered_equivocation(
495            CheckpointEquivocationKind::ConflictingPredecessorWitness,
496            (first_log_id == second_log_id).then_some(first_log_id),
497            None,
498            first.body.checkpoint_seq,
499            first_sha256,
500            second.body.checkpoint_seq,
501            second_sha256,
502            first.body.previous_checkpoint_sha256.clone(),
503        )));
504    }
505
506    Ok(None)
507}
508
509/// Render a checkpoint conflict as a stable, human-readable description.
510#[must_use]
511pub fn describe_checkpoint_equivocation(equivocation: &CheckpointEquivocation) -> String {
512    match equivocation.kind {
513        CheckpointEquivocationKind::ConflictingCheckpointSeq => format!(
514            "checkpoint_seq {} has conflicting digests {} and {}",
515            equivocation.first_checkpoint_seq,
516            equivocation.first_checkpoint_sha256,
517            equivocation.second_checkpoint_sha256
518        ),
519        CheckpointEquivocationKind::ConflictingLogTreeSize => format!(
520            "log {} has conflicting checkpoints at cumulative tree size {}: {} ({}) vs {} ({})",
521            equivocation.log_id.as_deref().unwrap_or("<unknown>"),
522            equivocation.log_tree_size.unwrap_or_default(),
523            equivocation.first_checkpoint_seq,
524            equivocation.first_checkpoint_sha256,
525            equivocation.second_checkpoint_seq,
526            equivocation.second_checkpoint_sha256
527        ),
528        CheckpointEquivocationKind::ConflictingPredecessorWitness => format!(
529            "predecessor digest {} is witnessed by conflicting checkpoints {} ({}) and {} ({})",
530            equivocation
531                .previous_checkpoint_sha256
532                .as_deref()
533                .unwrap_or("<missing>"),
534            equivocation.first_checkpoint_seq,
535            equivocation.first_checkpoint_sha256,
536            equivocation.second_checkpoint_seq,
537            equivocation.second_checkpoint_sha256
538        ),
539    }
540}
541
542/// Derive publication, witness, and equivocation records from a checkpoint set.
543pub fn build_checkpoint_transparency(
544    checkpoints: &[KernelCheckpoint],
545) -> Result<CheckpointTransparencySummary, CheckpointError> {
546    let mut publications = Vec::with_capacity(checkpoints.len());
547    let mut by_digest = BTreeMap::<String, &KernelCheckpoint>::new();
548
549    for checkpoint in checkpoints {
550        publications.push(build_checkpoint_publication(checkpoint)?);
551        by_digest.insert(checkpoint_body_sha256(&checkpoint.body)?, checkpoint);
552    }
553
554    publications.sort_by_key(|publication| publication.checkpoint_seq);
555
556    let mut equivocations = Vec::new();
557    for (index, checkpoint) in checkpoints.iter().enumerate() {
558        for conflicting in checkpoints.iter().skip(index + 1) {
559            if let Some(equivocation) = detect_checkpoint_equivocation(checkpoint, conflicting)? {
560                equivocations.push(equivocation);
561            }
562        }
563    }
564    equivocations.sort();
565    equivocations.dedup();
566    let equivocated_digests = equivocations
567        .iter()
568        .flat_map(|equivocation| {
569            [
570                equivocation.first_checkpoint_sha256.clone(),
571                equivocation.second_checkpoint_sha256.clone(),
572            ]
573        })
574        .collect::<BTreeSet<_>>();
575
576    let mut witnesses = Vec::new();
577    let mut consistency_proofs = Vec::new();
578    for checkpoint in checkpoints {
579        let Some(previous_checkpoint_sha256) =
580            checkpoint.body.previous_checkpoint_sha256.as_deref()
581        else {
582            continue;
583        };
584        if let Some(previous) = by_digest.get(previous_checkpoint_sha256) {
585            let checkpoint_sha256 = checkpoint_body_sha256(&checkpoint.body)?;
586            if let Err(error) = validate_checkpoint_predecessor(previous, checkpoint) {
587                if equivocated_digests.contains(&checkpoint_sha256) {
588                    continue;
589                }
590                return Err(error);
591            }
592            witnesses.push(build_checkpoint_witness(previous, checkpoint)?);
593            if checkpoint_log_id(previous) == checkpoint_log_id(checkpoint) {
594                consistency_proofs.push(build_checkpoint_consistency_proof(previous, checkpoint)?);
595            }
596        }
597    }
598    witnesses.sort_by_key(|witness| (witness.witness_checkpoint_seq, witness.checkpoint_seq));
599    consistency_proofs.sort_by_key(|proof| (proof.to_checkpoint_seq, proof.from_checkpoint_seq));
600
601    Ok(CheckpointTransparencySummary {
602        publications,
603        witnesses,
604        consistency_proofs,
605        equivocations,
606    })
607}
608
609/// Validate that a checkpoint set is transparency-safe and fork-free.
610pub fn validate_checkpoint_transparency(
611    checkpoints: &[KernelCheckpoint],
612) -> Result<CheckpointTransparencySummary, CheckpointError> {
613    let transparency = build_checkpoint_transparency(checkpoints)?;
614    if let Some(equivocation) = transparency.equivocations.first() {
615        return Err(CheckpointError::Continuity(format!(
616            "checkpoint equivocation detected: {}",
617            describe_checkpoint_equivocation(equivocation)
618        )));
619    }
620
621    let mut by_digest = BTreeMap::<String, &KernelCheckpoint>::new();
622    for checkpoint in checkpoints {
623        by_digest.insert(checkpoint_body_sha256(&checkpoint.body)?, checkpoint);
624    }
625    for checkpoint in checkpoints {
626        let Some(previous_checkpoint_sha256) =
627            checkpoint.body.previous_checkpoint_sha256.as_deref()
628        else {
629            continue;
630        };
631        if let Some(previous) = by_digest.get(previous_checkpoint_sha256) {
632            validate_checkpoint_predecessor(previous, checkpoint)?;
633        }
634    }
635
636    Ok(transparency)
637}
638
639/// Verify that supplied transparency records match the signed checkpoint set.
640///
641/// Valid trust-anchor bindings are preserved in the returned summary so callers
642/// can safely project publication state without collapsing back to raw
643/// checkpoint-only records.
644pub fn verify_checkpoint_transparency_records(
645    checkpoints: &[KernelCheckpoint],
646    supplied: &CheckpointTransparencySummary,
647) -> Result<CheckpointTransparencySummary, CheckpointError> {
648    let derived = validate_checkpoint_transparency(checkpoints)?;
649    let checkpoints_by_seq = checkpoints
650        .iter()
651        .map(|checkpoint| (checkpoint.body.checkpoint_seq, checkpoint))
652        .collect::<BTreeMap<_, _>>();
653    let derived_publications = derived
654        .publications
655        .iter()
656        .map(|publication| (publication.checkpoint_seq, publication))
657        .collect::<BTreeMap<_, _>>();
658
659    if supplied.publications.len() != derived.publications.len() {
660        return Err(CheckpointError::Continuity(
661            "checkpoint publication records do not match the signed checkpoint set".to_string(),
662        ));
663    }
664
665    let mut normalized_publications = Vec::with_capacity(supplied.publications.len());
666    let mut matched_checkpoint_seqs = BTreeSet::new();
667    for publication in &supplied.publications {
668        if !matched_checkpoint_seqs.insert(publication.checkpoint_seq) {
669            return Err(CheckpointError::Continuity(format!(
670                "duplicate checkpoint publication record for checkpoint {}",
671                publication.checkpoint_seq
672            )));
673        }
674        let Some(derived_publication) = derived_publications
675            .get(&publication.checkpoint_seq)
676            .copied()
677        else {
678            return Err(CheckpointError::Continuity(
679                "checkpoint publication records do not match the signed checkpoint set".to_string(),
680            ));
681        };
682        let expected = match publication.trust_anchor_binding.clone() {
683            Some(binding) => {
684                let checkpoint = checkpoints_by_seq
685                    .get(&publication.checkpoint_seq)
686                    .copied()
687                    .ok_or_else(|| {
688                        CheckpointError::Continuity(format!(
689                            "checkpoint publication {} references a missing checkpoint",
690                            publication.checkpoint_seq
691                        ))
692                    })?;
693                build_trust_anchored_checkpoint_publication(checkpoint, binding)?
694            }
695            None => (*derived_publication).clone(),
696        };
697        if publication != &expected {
698            return Err(CheckpointError::Continuity(
699                "checkpoint publication records do not match the signed checkpoint set".to_string(),
700            ));
701        }
702        normalized_publications.push(expected);
703    }
704    if matched_checkpoint_seqs.len() != derived_publications.len() {
705        return Err(CheckpointError::Continuity(
706            "checkpoint publication records do not cover the signed checkpoint set".to_string(),
707        ));
708    }
709
710    if supplied.witnesses != derived.witnesses {
711        return Err(CheckpointError::Continuity(
712            "checkpoint witness records do not match the signed checkpoint set".to_string(),
713        ));
714    }
715    if supplied.consistency_proofs != derived.consistency_proofs {
716        return Err(CheckpointError::Continuity(
717            "checkpoint consistency proof records do not match the signed checkpoint set"
718                .to_string(),
719        ));
720    }
721    if supplied.equivocations != derived.equivocations {
722        return Err(CheckpointError::Continuity(
723            "checkpoint equivocation records do not match the signed checkpoint set".to_string(),
724        ));
725    }
726
727    Ok(CheckpointTransparencySummary {
728        publications: normalized_publications,
729        witnesses: supplied.witnesses.clone(),
730        consistency_proofs: supplied.consistency_proofs.clone(),
731        equivocations: supplied.equivocations.clone(),
732    })
733}
734
735/// Verify that `current` explicitly extends `previous`.
736pub fn verify_checkpoint_continuity(
737    previous: &KernelCheckpoint,
738    current: &KernelCheckpoint,
739) -> Result<bool, CheckpointError> {
740    match validate_checkpoint_predecessor(previous, current) {
741        Ok(()) => Ok(true),
742        Err(CheckpointError::Continuity(_)) => Ok(false),
743        Err(error) => Err(error),
744    }
745}
746
747/// Return the current Unix timestamp in seconds.
748fn unix_now() -> u64 {
749    SystemTime::now()
750        .duration_since(UNIX_EPOCH)
751        .map(|d| d.as_secs())
752        .unwrap_or(0)
753}
754
755/// Build a signed kernel checkpoint from a batch of canonical receipt bytes.
756///
757/// `receipt_canonical_bytes_batch` must not be empty.
758pub fn build_checkpoint(
759    checkpoint_seq: u64,
760    batch_start_seq: u64,
761    batch_end_seq: u64,
762    receipt_canonical_bytes_batch: &[Vec<u8>],
763    keypair: &Keypair,
764) -> Result<KernelCheckpoint, CheckpointError> {
765    build_checkpoint_with_previous(
766        checkpoint_seq,
767        batch_start_seq,
768        batch_end_seq,
769        receipt_canonical_bytes_batch,
770        keypair,
771        None,
772    )
773}
774
775/// Build a signed kernel checkpoint that explicitly links to the previous checkpoint when provided.
776pub fn build_checkpoint_with_previous(
777    checkpoint_seq: u64,
778    batch_start_seq: u64,
779    batch_end_seq: u64,
780    receipt_canonical_bytes_batch: &[Vec<u8>],
781    keypair: &Keypair,
782    previous_checkpoint: Option<&KernelCheckpoint>,
783) -> Result<KernelCheckpoint, CheckpointError> {
784    let tree = MerkleTree::from_leaves(receipt_canonical_bytes_batch)?;
785    let merkle_root = tree.root();
786    let body = KernelCheckpointBody {
787        schema: CHECKPOINT_SCHEMA.to_string(),
788        checkpoint_seq,
789        batch_start_seq,
790        batch_end_seq,
791        tree_size: tree.leaf_count(),
792        merkle_root,
793        issued_at: unix_now(),
794        kernel_key: keypair.public_key(),
795        previous_checkpoint_sha256: previous_checkpoint
796            .map(|checkpoint| checkpoint_body_sha256(&checkpoint.body))
797            .transpose()?,
798    };
799    let body_bytes =
800        canonical_json_bytes(&body).map_err(|e| CheckpointError::Serialization(e.to_string()))?;
801    let signature = keypair.sign(&body_bytes);
802    Ok(KernelCheckpoint { body, signature })
803}
804
805/// Build an inclusion proof for a leaf in an already-built MerkleTree.
806pub fn build_inclusion_proof(
807    tree: &MerkleTree,
808    leaf_index: usize,
809    checkpoint_seq: u64,
810    receipt_seq: u64,
811) -> Result<ReceiptInclusionProof, CheckpointError> {
812    let proof = tree.inclusion_proof(leaf_index)?;
813    Ok(ReceiptInclusionProof {
814        checkpoint_seq,
815        receipt_seq,
816        leaf_index,
817        merkle_root: tree.root(),
818        proof,
819    })
820}
821
822/// Verify the signature on a KernelCheckpoint.
823///
824/// Returns `Ok(true)` if the signature is valid.
825pub fn verify_checkpoint_signature(checkpoint: &KernelCheckpoint) -> Result<bool, CheckpointError> {
826    let body_bytes = canonical_json_bytes(&checkpoint.body)
827        .map_err(|e| CheckpointError::Serialization(e.to_string()))?;
828    Ok(checkpoint
829        .body
830        .kernel_key
831        .verify(&body_bytes, &checkpoint.signature))
832}
833
834/// Validate the integrity of a single checkpoint statement.
835pub fn validate_checkpoint(checkpoint: &KernelCheckpoint) -> Result<(), CheckpointError> {
836    if !is_supported_checkpoint_schema(&checkpoint.body.schema) {
837        return Err(CheckpointError::Invalid(format!(
838            "unsupported checkpoint schema {}",
839            checkpoint.body.schema
840        )));
841    }
842    if checkpoint.body.checkpoint_seq == 0 {
843        return Err(CheckpointError::Invalid(
844            "checkpoint_seq must be greater than zero".to_string(),
845        ));
846    }
847    if checkpoint.body.batch_start_seq == 0 {
848        return Err(CheckpointError::Invalid(
849            "batch_start_seq must be greater than zero".to_string(),
850        ));
851    }
852    if checkpoint.body.batch_end_seq < checkpoint.body.batch_start_seq {
853        return Err(CheckpointError::Invalid(format!(
854            "batch_end_seq {} is less than batch_start_seq {}",
855            checkpoint.body.batch_end_seq, checkpoint.body.batch_start_seq
856        )));
857    }
858    if checkpoint.body.tree_size == 0 {
859        return Err(CheckpointError::Invalid(
860            "tree_size must be greater than zero".to_string(),
861        ));
862    }
863    let expected_tree_size = checkpoint_batch_entry_count(&checkpoint.body)?;
864    if u64::try_from(checkpoint.body.tree_size).ok() != Some(expected_tree_size) {
865        return Err(CheckpointError::Invalid(format!(
866            "tree_size {} does not match covered entry count {} for range {}-{}",
867            checkpoint.body.tree_size,
868            expected_tree_size,
869            checkpoint.body.batch_start_seq,
870            checkpoint.body.batch_end_seq
871        )));
872    }
873    if !verify_checkpoint_signature(checkpoint)? {
874        return Err(CheckpointError::InvalidSignature);
875    }
876    Ok(())
877}
878
879/// Validate that `checkpoint` cleanly extends `predecessor`.
880pub fn validate_checkpoint_predecessor(
881    predecessor: &KernelCheckpoint,
882    checkpoint: &KernelCheckpoint,
883) -> Result<(), CheckpointError> {
884    validate_checkpoint(predecessor)?;
885    validate_checkpoint(checkpoint)?;
886
887    let expected_checkpoint_seq =
888        predecessor
889            .body
890            .checkpoint_seq
891            .checked_add(1)
892            .ok_or_else(|| {
893                CheckpointError::Continuity("predecessor checkpoint_seq overflowed u64".to_string())
894            })?;
895    if checkpoint.body.checkpoint_seq != expected_checkpoint_seq {
896        return Err(CheckpointError::Continuity(format!(
897            "checkpoint_seq {} does not immediately follow predecessor {}",
898            checkpoint.body.checkpoint_seq, predecessor.body.checkpoint_seq
899        )));
900    }
901
902    let expected_batch_start = predecessor
903        .body
904        .batch_end_seq
905        .checked_add(1)
906        .ok_or_else(|| {
907            CheckpointError::Continuity("predecessor batch_end_seq overflowed u64".to_string())
908        })?;
909    if checkpoint.body.batch_start_seq != expected_batch_start {
910        return Err(CheckpointError::Continuity(format!(
911            "batch_start_seq {} does not immediately follow predecessor batch_end_seq {}",
912            checkpoint.body.batch_start_seq, predecessor.body.batch_end_seq
913        )));
914    }
915
916    if let Some(previous_checkpoint_sha256) = checkpoint.body.previous_checkpoint_sha256.as_deref()
917    {
918        let expected_previous_checkpoint_sha256 = checkpoint_body_sha256(&predecessor.body)?;
919        if previous_checkpoint_sha256 != expected_previous_checkpoint_sha256 {
920            return Err(CheckpointError::Continuity(format!(
921                "checkpoint {} does not match predecessor digest {}",
922                checkpoint.body.checkpoint_seq, expected_previous_checkpoint_sha256
923            )));
924        }
925    }
926
927    Ok(())
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    fn make_receipt_bytes(n: usize) -> Vec<Vec<u8>> {
935        (0..n)
936            .map(|i| format!("{{\"receipt_id\":\"rcpt-{i:04}\",\"seq\":{i}}}").into_bytes())
937            .collect()
938    }
939
940    #[test]
941    fn build_checkpoint_100_has_tree_size_100() {
942        let kp = Keypair::generate();
943        let batch = make_receipt_bytes(100);
944        let cp = build_checkpoint(1, 1, 100, &batch, &kp).expect("build_checkpoint failed");
945        assert_eq!(cp.body.tree_size, 100);
946    }
947
948    #[test]
949    fn build_checkpoint_signature_verifies() {
950        let kp = Keypair::generate();
951        let batch = make_receipt_bytes(10);
952        let cp = build_checkpoint(1, 1, 10, &batch, &kp).expect("build_checkpoint failed");
953        assert!(
954            verify_checkpoint_signature(&cp).expect("verify failed"),
955            "signature should be valid"
956        );
957    }
958
959    #[test]
960    fn build_checkpoint_wrong_key_fails_verification() {
961        let kp1 = Keypair::generate();
962        let kp2 = Keypair::generate();
963        let batch = make_receipt_bytes(5);
964        let mut cp = build_checkpoint(1, 1, 5, &batch, &kp1).expect("build_checkpoint failed");
965        // Replace the kernel_key with a different key -- signature no longer matches.
966        cp.body.kernel_key = kp2.public_key();
967        assert!(
968            !verify_checkpoint_signature(&cp).expect("verify call failed"),
969            "tampered key should fail"
970        );
971    }
972
973    #[test]
974    fn build_checkpoint_single_receipt() {
975        let kp = Keypair::generate();
976        let batch = make_receipt_bytes(1);
977        let cp = build_checkpoint(1, 1, 1, &batch, &kp).expect("build_checkpoint failed");
978        assert_eq!(cp.body.tree_size, 1);
979        assert!(
980            verify_checkpoint_signature(&cp).expect("verify failed"),
981            "single-receipt checkpoint should have valid signature"
982        );
983    }
984
985    #[test]
986    fn build_checkpoint_single_receipt_merkle_root_equals_leaf_hash() {
987        // Degenerate case: a single-receipt batch must produce a Merkle root
988        // equal to the leaf hash of that receipt's canonical bytes (per RFC 6962:
989        // LeafHash(bytes) = SHA256(0x00 || bytes)).
990        use chio_core::merkle::leaf_hash;
991
992        let kp = Keypair::generate();
993        let leaf_bytes = b"single-receipt-canonical-bytes";
994        let batch = vec![leaf_bytes.to_vec()];
995        let cp = build_checkpoint(1, 1, 1, &batch, &kp).expect("build_checkpoint failed");
996
997        let expected_root = leaf_hash(leaf_bytes);
998        assert_eq!(
999            cp.body.merkle_root, expected_root,
1000            "single-receipt checkpoint merkle_root must equal leaf_hash of the receipt bytes"
1001        );
1002        assert_eq!(cp.body.tree_size, 1);
1003        assert!(
1004            verify_checkpoint_signature(&cp).expect("verify failed"),
1005            "single-receipt checkpoint signature should verify"
1006        );
1007    }
1008
1009    #[test]
1010    fn schema_is_v1() {
1011        let kp = Keypair::generate();
1012        let batch = make_receipt_bytes(3);
1013        let cp = build_checkpoint(1, 1, 3, &batch, &kp).expect("build_checkpoint failed");
1014        assert_eq!(cp.body.schema, CHECKPOINT_SCHEMA);
1015        assert!(cp.body.previous_checkpoint_sha256.is_none());
1016    }
1017
1018    #[test]
1019    fn build_checkpoint_with_previous_sets_continuity_hash() {
1020        let kp = Keypair::generate();
1021        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp)
1022            .expect("first checkpoint build failed");
1023        let second =
1024            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1025                .expect("second checkpoint build failed");
1026        let expected_previous_checkpoint_sha256 =
1027            checkpoint_body_sha256(&first.body).expect("previous digest");
1028
1029        assert_eq!(
1030            second.body.previous_checkpoint_sha256.as_deref(),
1031            Some(expected_previous_checkpoint_sha256.as_str())
1032        );
1033        assert!(
1034            verify_checkpoint_continuity(&first, &second).expect("continuity verification"),
1035            "second checkpoint should extend the first"
1036        );
1037    }
1038
1039    #[test]
1040    fn build_checkpoint_transparency_derives_publications_and_witnesses() {
1041        let kp = Keypair::generate();
1042        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build first");
1043        let second =
1044            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1045                .expect("build second");
1046
1047        let transparency =
1048            validate_checkpoint_transparency(&[first.clone(), second.clone()]).expect("summary");
1049
1050        assert_eq!(transparency.publications.len(), 2);
1051        assert_eq!(transparency.witnesses.len(), 1);
1052        assert_eq!(transparency.consistency_proofs.len(), 1);
1053        assert!(transparency.equivocations.is_empty());
1054        assert_eq!(
1055            transparency.publications[0].log_id,
1056            checkpoint_log_id(&first)
1057        );
1058        assert_eq!(transparency.publications[0].log_tree_size, 3);
1059        assert_eq!(transparency.publications[1].entry_start_seq, 4);
1060        assert_eq!(transparency.publications[1].entry_end_seq, 6);
1061        assert_eq!(
1062            transparency.publications[0].checkpoint_sha256,
1063            checkpoint_body_sha256(&first.body).expect("first digest")
1064        );
1065        assert_eq!(transparency.witnesses[0].log_id, checkpoint_log_id(&first));
1066        assert_eq!(transparency.witnesses[0].checkpoint_seq, 1);
1067        assert_eq!(transparency.witnesses[0].witness_checkpoint_seq, 2);
1068        assert_eq!(transparency.consistency_proofs[0].from_log_tree_size, 3);
1069        assert_eq!(transparency.consistency_proofs[0].to_log_tree_size, 6);
1070    }
1071
1072    #[test]
1073    fn checkpoint_log_id_preserves_historical_ed25519_hashing() {
1074        let kp = Keypair::generate();
1075        let checkpoint =
1076            build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build checkpoint");
1077
1078        assert_eq!(
1079            checkpoint_log_id(&checkpoint),
1080            format!("local-log-{}", sha256_hex(kp.public_key().as_bytes()))
1081        );
1082    }
1083
1084    #[test]
1085    fn build_trust_anchored_checkpoint_publication_records_binding() {
1086        let kp = Keypair::generate();
1087        let checkpoint =
1088            build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build checkpoint");
1089        let publication = build_trust_anchored_checkpoint_publication(
1090            &checkpoint,
1091            CheckpointPublicationTrustAnchorBinding {
1092                publication_identity: chio_core::receipt::CheckpointPublicationIdentity::new(
1093                    chio_core::receipt::CheckpointPublicationIdentityKind::TransparencyService,
1094                    "transparency.example/checkpoints/1",
1095                ),
1096                trust_anchor_identity: chio_core::receipt::CheckpointTrustAnchorIdentity::new(
1097                    chio_core::receipt::CheckpointTrustAnchorIdentityKind::Did,
1098                    "did:chio:operator-root",
1099                ),
1100                trust_anchor_ref: "chio_checkpoint_witness_chain".to_string(),
1101                signer_cert_ref: "did:web:chio.example#checkpoint-signer".to_string(),
1102                publication_profile_version: "phase4-preview.v1".to_string(),
1103            },
1104        )
1105        .expect("build trust-anchored publication");
1106
1107        assert_eq!(
1108            publication
1109                .trust_anchor_binding
1110                .as_ref()
1111                .expect("binding")
1112                .trust_anchor_ref,
1113            "chio_checkpoint_witness_chain"
1114        );
1115        assert_eq!(
1116            publication
1117                .trust_anchor_binding
1118                .as_ref()
1119                .expect("binding")
1120                .publication_identity
1121                .identity,
1122            "transparency.example/checkpoints/1"
1123        );
1124        assert_eq!(publication.log_id, checkpoint_log_id(&checkpoint));
1125    }
1126
1127    #[test]
1128    fn verify_checkpoint_transparency_records_rejects_duplicate_publication_coverage() {
1129        let kp = Keypair::generate();
1130        let first =
1131            build_checkpoint(1, 1, 2, &make_receipt_bytes(2), &kp).expect("first checkpoint");
1132        let second =
1133            build_checkpoint_with_previous(2, 3, 4, &make_receipt_bytes(2), &kp, Some(&first))
1134                .expect("second checkpoint");
1135        let derived = validate_checkpoint_transparency(&[first.clone(), second.clone()])
1136            .expect("transparency");
1137        let supplied = CheckpointTransparencySummary {
1138            publications: vec![
1139                derived.publications[0].clone(),
1140                derived.publications[0].clone(),
1141            ],
1142            witnesses: derived.witnesses.clone(),
1143            consistency_proofs: derived.consistency_proofs.clone(),
1144            equivocations: derived.equivocations.clone(),
1145        };
1146
1147        let error = verify_checkpoint_transparency_records(&[first, second], &supplied)
1148            .expect_err("duplicate publication coverage should fail");
1149        assert!(
1150            error
1151                .to_string()
1152                .contains("duplicate checkpoint publication record"),
1153            "unexpected error: {error}"
1154        );
1155    }
1156
1157    #[test]
1158    fn build_trust_anchored_checkpoint_publication_rejects_invalid_binding() {
1159        let kp = Keypair::generate();
1160        let checkpoint =
1161            build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build checkpoint");
1162        let error = build_trust_anchored_checkpoint_publication(
1163            &checkpoint,
1164            CheckpointPublicationTrustAnchorBinding {
1165                publication_identity: chio_core::receipt::CheckpointPublicationIdentity::new(
1166                    chio_core::receipt::CheckpointPublicationIdentityKind::TransparencyService,
1167                    "",
1168                ),
1169                trust_anchor_identity: chio_core::receipt::CheckpointTrustAnchorIdentity::new(
1170                    chio_core::receipt::CheckpointTrustAnchorIdentityKind::Did,
1171                    "did:chio:operator-root",
1172                ),
1173                trust_anchor_ref: "chio_checkpoint_witness_chain".to_string(),
1174                signer_cert_ref: "".to_string(),
1175                publication_profile_version: "phase4-preview.v1".to_string(),
1176            },
1177        )
1178        .expect_err("blank signer certificate ref must be rejected");
1179        assert!(error.to_string().contains("publication_identity.identity"));
1180    }
1181
1182    #[test]
1183    fn build_trust_anchored_checkpoint_publication_rejects_mismatched_local_log_identity() {
1184        let kp = Keypair::generate();
1185        let checkpoint =
1186            build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build checkpoint");
1187        let error = build_trust_anchored_checkpoint_publication(
1188            &checkpoint,
1189            CheckpointPublicationTrustAnchorBinding {
1190                publication_identity: chio_core::receipt::CheckpointPublicationIdentity::new(
1191                    chio_core::receipt::CheckpointPublicationIdentityKind::LocalLog,
1192                    "local-log-not-the-real-one",
1193                ),
1194                trust_anchor_identity: chio_core::receipt::CheckpointTrustAnchorIdentity::new(
1195                    chio_core::receipt::CheckpointTrustAnchorIdentityKind::OperatorRoot,
1196                    "chio-operator-root",
1197                ),
1198                trust_anchor_ref: "chio_checkpoint_witness_chain".to_string(),
1199                signer_cert_ref: "did:web:chio.example#checkpoint-signer".to_string(),
1200                publication_profile_version: "phase4-preview.v1".to_string(),
1201            },
1202        )
1203        .expect_err("mismatched local log identity must be rejected");
1204        assert!(error.to_string().contains("does not match log_id"));
1205    }
1206
1207    #[test]
1208    fn detect_checkpoint_equivocation_reports_conflicting_sequence() {
1209        let kp = Keypair::generate();
1210        let first = build_checkpoint(1, 1, 2, &[b"one".to_vec(), b"two".to_vec()], &kp)
1211            .expect("first checkpoint");
1212        let conflicting = build_checkpoint(1, 1, 2, &[b"one".to_vec(), b"changed".to_vec()], &kp)
1213            .expect("conflicting checkpoint");
1214
1215        let equivocation = detect_checkpoint_equivocation(&first, &conflicting)
1216            .expect("equivocation detection")
1217            .expect("expected conflict");
1218        assert_eq!(
1219            equivocation.kind,
1220            CheckpointEquivocationKind::ConflictingCheckpointSeq
1221        );
1222        assert_eq!(equivocation.first_checkpoint_seq, 1);
1223        assert_eq!(equivocation.second_checkpoint_seq, 1);
1224    }
1225
1226    #[test]
1227    fn checkpoint_rejects_same_log_same_tree_size_fork() {
1228        let kp = Keypair::generate();
1229        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("first");
1230        let second =
1231            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1232                .expect("second");
1233        let fork = build_checkpoint_with_previous(
1234            9,
1235            1,
1236            6,
1237            &[
1238                b"fork-one".to_vec(),
1239                b"fork-two".to_vec(),
1240                b"fork-three".to_vec(),
1241                b"fork-four".to_vec(),
1242                b"fork-five".to_vec(),
1243                b"fork-six".to_vec(),
1244            ],
1245            &kp,
1246            None,
1247        )
1248        .expect("fork");
1249
1250        let error = validate_checkpoint_transparency(&[first, second, fork])
1251            .expect_err("same-log same-tree-size fork should fail");
1252        assert!(
1253            error.to_string().contains("cumulative tree size 6"),
1254            "unexpected error: {error}"
1255        );
1256    }
1257
1258    #[test]
1259    fn checkpoint_consistency_proof_verifies_prefix_growth() {
1260        let kp = Keypair::generate();
1261        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("first");
1262        let second =
1263            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1264                .expect("second");
1265
1266        let proof = build_checkpoint_consistency_proof(&first, &second).expect("proof");
1267        assert_eq!(proof.log_id, checkpoint_log_id(&first));
1268        assert_eq!(proof.from_log_tree_size, 3);
1269        assert_eq!(proof.to_log_tree_size, 6);
1270        assert_eq!(proof.appended_entry_start_seq, 4);
1271        assert_eq!(proof.appended_entry_end_seq, 6);
1272        assert!(
1273            verify_checkpoint_consistency_proof(&first, &second, &proof).expect("verify proof"),
1274            "prefix-growth proof should verify"
1275        );
1276    }
1277
1278    #[test]
1279    fn inclusion_proof_verifies_for_leaf_n() {
1280        let batch = make_receipt_bytes(10);
1281        let tree = MerkleTree::from_leaves(&batch).expect("tree build failed");
1282        let root = tree.root();
1283        let proof = build_inclusion_proof(&tree, 5, 1, 6).expect("proof failed");
1284        assert!(
1285            proof.verify(&batch[5], &root),
1286            "inclusion proof should verify"
1287        );
1288    }
1289
1290    #[test]
1291    fn inclusion_proof_tampered_bytes_fail() {
1292        let batch = make_receipt_bytes(10);
1293        let tree = MerkleTree::from_leaves(&batch).expect("tree build failed");
1294        let root = tree.root();
1295        let proof = build_inclusion_proof(&tree, 5, 1, 6).expect("proof failed");
1296        assert!(
1297            !proof.verify(b"tampered bytes that are not in the tree", &root),
1298            "tampered bytes should not verify"
1299        );
1300    }
1301
1302    #[test]
1303    fn inclusion_proof_all_100_leaves_verify() {
1304        let batch = make_receipt_bytes(100);
1305        let tree = MerkleTree::from_leaves(&batch).expect("tree build failed");
1306        let root = tree.root();
1307        for i in 0..100 {
1308            let proof = build_inclusion_proof(&tree, i, 1, i as u64 + 1).expect("proof failed");
1309            assert!(
1310                proof.verify(&batch[i], &root),
1311                "leaf {i} inclusion proof failed"
1312            );
1313        }
1314    }
1315
1316    #[test]
1317    fn checkpoint_body_schema_field() {
1318        let kp = Keypair::generate();
1319        let batch = make_receipt_bytes(5);
1320        let cp = build_checkpoint(7, 101, 105, &batch, &kp).expect("build failed");
1321        let json = serde_json::to_string(&cp.body).expect("serialize failed");
1322        assert!(
1323            json.contains(CHECKPOINT_SCHEMA),
1324            "JSON should contain schema string"
1325        );
1326    }
1327
1328    #[test]
1329    fn checkpoint_schema_support_matches_current_v1() {
1330        assert!(is_supported_checkpoint_schema(CHECKPOINT_SCHEMA));
1331    }
1332
1333    #[test]
1334    fn kernel_checkpoint_serde_roundtrip() {
1335        let kp = Keypair::generate();
1336        let batch = make_receipt_bytes(5);
1337        let cp = build_checkpoint(1, 1, 5, &batch, &kp).expect("build failed");
1338        let json = serde_json::to_string(&cp).expect("serialize failed");
1339        let restored: KernelCheckpoint = serde_json::from_str(&json).expect("deserialize failed");
1340        assert_eq!(cp.body.checkpoint_seq, restored.body.checkpoint_seq);
1341        assert_eq!(cp.body.tree_size, restored.body.tree_size);
1342        assert_eq!(cp.signature.to_hex(), restored.signature.to_hex());
1343        // Verify signature still works after roundtrip.
1344        assert!(
1345            verify_checkpoint_signature(&restored).expect("verify failed"),
1346            "roundtripped checkpoint signature should verify"
1347        );
1348    }
1349
1350    #[test]
1351    fn validate_checkpoint_rejects_zero_checkpoint_seq() {
1352        let kp = Keypair::generate();
1353        let batch = make_receipt_bytes(3);
1354        let mut checkpoint = build_checkpoint(1, 1, 3, &batch, &kp).expect("build failed");
1355        checkpoint.body.checkpoint_seq = 0;
1356
1357        let error = validate_checkpoint(&checkpoint).expect_err("checkpoint should be invalid");
1358        assert!(
1359            error
1360                .to_string()
1361                .contains("checkpoint_seq must be greater than zero"),
1362            "unexpected error: {error}"
1363        );
1364    }
1365
1366    #[test]
1367    fn validate_checkpoint_rejects_tampered_signature() {
1368        let kp = Keypair::generate();
1369        let batch = make_receipt_bytes(3);
1370        let mut checkpoint = build_checkpoint(1, 1, 3, &batch, &kp).expect("build failed");
1371        checkpoint.body.issued_at = checkpoint.body.issued_at.saturating_add(1);
1372
1373        let error = validate_checkpoint(&checkpoint).expect_err("checkpoint should be invalid");
1374        assert!(
1375            matches!(error, CheckpointError::InvalidSignature),
1376            "unexpected error: {error}"
1377        );
1378    }
1379
1380    #[test]
1381    fn validate_checkpoint_rejects_tree_size_that_does_not_match_entry_range() {
1382        let kp = Keypair::generate();
1383        let batch = make_receipt_bytes(3);
1384        let mut checkpoint = build_checkpoint(1, 1, 3, &batch, &kp).expect("build failed");
1385        checkpoint.body.tree_size = 2;
1386        checkpoint.signature =
1387            kp.sign(&canonical_json_bytes(&checkpoint.body).expect("canonical checkpoint body"));
1388
1389        let error = validate_checkpoint(&checkpoint).expect_err("checkpoint should be invalid");
1390        assert!(
1391            error
1392                .to_string()
1393                .contains("tree_size 2 does not match covered entry count 3"),
1394            "unexpected error: {error}"
1395        );
1396    }
1397
1398    #[test]
1399    fn validate_checkpoint_predecessor_accepts_contiguous_batches() {
1400        let kp = Keypair::generate();
1401        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build failed");
1402        let second =
1403            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1404                .expect("build failed");
1405
1406        validate_checkpoint_predecessor(&first, &second).expect("continuity should hold");
1407    }
1408
1409    #[test]
1410    fn validate_checkpoint_predecessor_rejects_batch_gap() {
1411        let kp = Keypair::generate();
1412        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build failed");
1413        let second =
1414            build_checkpoint_with_previous(2, 5, 6, &make_receipt_bytes(2), &kp, Some(&first))
1415                .expect("build failed");
1416
1417        let error =
1418            validate_checkpoint_predecessor(&first, &second).expect_err("continuity should fail");
1419        assert!(
1420            error.to_string().contains("does not immediately follow"),
1421            "unexpected error: {error}"
1422        );
1423    }
1424
1425    #[test]
1426    fn validate_checkpoint_predecessor_rejects_wrong_predecessor_digest() {
1427        let kp = Keypair::generate();
1428        let first = build_checkpoint(1, 1, 3, &make_receipt_bytes(3), &kp).expect("build failed");
1429        let mut second =
1430            build_checkpoint_with_previous(2, 4, 6, &make_receipt_bytes(3), &kp, Some(&first))
1431                .expect("build failed");
1432        second.body.previous_checkpoint_sha256 = Some("not-the-real-digest".to_string());
1433        second.signature =
1434            kp.sign(&canonical_json_bytes(&second.body).expect("canonical second checkpoint body"));
1435
1436        let error =
1437            validate_checkpoint_predecessor(&first, &second).expect_err("continuity should fail");
1438        assert!(
1439            error
1440                .to_string()
1441                .contains("does not match predecessor digest"),
1442            "unexpected error: {error}"
1443        );
1444    }
1445
1446    #[test]
1447    fn validate_checkpoint_transparency_rejects_predecessor_fork() {
1448        let kp = Keypair::generate();
1449        let first = build_checkpoint(1, 1, 2, &[b"one".to_vec(), b"two".to_vec()], &kp)
1450            .expect("first checkpoint");
1451        let second = build_checkpoint_with_previous(
1452            2,
1453            3,
1454            4,
1455            &[b"three".to_vec(), b"four".to_vec()],
1456            &kp,
1457            Some(&first),
1458        )
1459        .expect("second checkpoint");
1460        let mut fork = build_checkpoint_with_previous(
1461            3,
1462            5,
1463            6,
1464            &[b"five".to_vec(), b"six".to_vec()],
1465            &kp,
1466            Some(&first),
1467        )
1468        .expect("fork checkpoint");
1469        fork.signature =
1470            kp.sign(&canonical_json_bytes(&fork.body).expect("canonical fork checkpoint body"));
1471
1472        let error = validate_checkpoint_transparency(&[first, second, fork])
1473            .expect_err("forked checkpoint set should fail");
1474        assert!(
1475            error
1476                .to_string()
1477                .contains("checkpoint equivocation detected"),
1478            "unexpected error: {error}"
1479        );
1480    }
1481}