1use 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct KernelCheckpointBody {
61 pub schema: String,
63 pub checkpoint_seq: u64,
65 pub batch_start_seq: u64,
67 pub batch_end_seq: u64,
69 pub tree_size: usize,
71 pub merkle_root: Hash,
73 pub issued_at: u64,
75 pub kernel_key: PublicKey,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub previous_checkpoint_sha256: Option<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct KernelCheckpoint {
85 pub body: KernelCheckpointBody,
87 pub signature: Signature,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ReceiptInclusionProof {
94 pub checkpoint_seq: u64,
96 pub receipt_seq: u64,
98 pub leaf_index: usize,
100 pub merkle_root: Hash,
102 pub proof: MerkleProof,
104}
105
106impl ReceiptInclusionProof {
107 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct CheckpointPublication {
117 pub log_id: String,
120 pub schema: String,
122 pub checkpoint_seq: u64,
124 pub checkpoint_sha256: String,
126 pub merkle_root: Hash,
128 pub published_at: u64,
130 pub kernel_key: PublicKey,
132 pub log_tree_size: u64,
134 pub entry_start_seq: u64,
136 pub entry_end_seq: u64,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub previous_checkpoint_sha256: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub trust_anchor_binding: Option<CheckpointPublicationTrustAnchorBinding>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct CheckpointWitness {
150 pub log_id: String,
152 pub schema: String,
154 pub checkpoint_seq: u64,
156 pub checkpoint_sha256: String,
158 pub witness_checkpoint_seq: u64,
160 pub witness_checkpoint_sha256: String,
162 pub witnessed_at: u64,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct CheckpointConsistencyProof {
169 pub schema: String,
171 pub log_id: String,
173 pub from_checkpoint_seq: u64,
175 pub to_checkpoint_seq: u64,
177 pub from_checkpoint_sha256: String,
179 pub to_checkpoint_sha256: String,
181 pub from_log_tree_size: u64,
183 pub to_log_tree_size: u64,
185 pub appended_entry_start_seq: u64,
187 pub appended_entry_end_seq: u64,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum CheckpointEquivocationKind {
195 ConflictingCheckpointSeq,
197 ConflictingLogTreeSize,
199 ConflictingPredecessorWitness,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
205pub struct CheckpointEquivocation {
206 pub schema: String,
208 pub kind: CheckpointEquivocationKind,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub log_id: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub log_tree_size: Option<u64>,
216 pub first_checkpoint_seq: u64,
218 pub second_checkpoint_seq: u64,
220 pub first_checkpoint_sha256: String,
222 pub second_checkpoint_sha256: String,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub previous_checkpoint_sha256: Option<String>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
231pub struct CheckpointTransparencySummary {
232 pub publications: Vec<CheckpointPublication>,
234 pub witnesses: Vec<CheckpointWitness>,
236 pub consistency_proofs: Vec<CheckpointConsistencyProof>,
238 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
270pub 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
277pub 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
298pub 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
321pub 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
359pub 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(¤t.body)?,
384 from_log_tree_size: checkpoint_log_tree_size(&previous.body),
385 to_log_tree_size: checkpoint_log_tree_size(¤t.body),
386 appended_entry_start_seq: current.body.batch_start_seq,
387 appended_entry_end_seq: current.body.batch_end_seq,
388 })
389}
390
391pub 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
438pub 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#[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
542pub 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
609pub 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
639pub 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
735pub 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
747fn unix_now() -> u64 {
749 SystemTime::now()
750 .duration_since(UNIX_EPOCH)
751 .map(|d| d.as_secs())
752 .unwrap_or(0)
753}
754
755pub 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
775pub 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
805pub 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
822pub 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
834pub 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
879pub 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 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 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 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}