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::{leaf_hash, verify_consistency_proof, MerkleProof, MerkleTree};
17use chio_core::receipt::{
18 checkpoint::CheckpointPublicationIdentityKind,
19 checkpoint::CheckpointPublicationTrustAnchorBinding,
20};
21use serde::{Deserialize, Deserializer, Serialize};
22
23use crate::ReceiptStoreError;
24
25#[path = "checkpoint/consistency.rs"]
26mod consistency;
27pub use consistency::CheckpointConsistencyAnchor;
28use consistency::{chain_leaf_is_committed, consistency_prefix_is_anchored};
29
30#[cfg(test)]
31std::thread_local! {
32 static CHECKPOINT_SIGNATURE_VERIFICATION_COUNT: std::cell::Cell<usize> =
33 const { std::cell::Cell::new(0) };
34 static CHECKPOINT_EQUIVOCATION_INSPECTION_COUNT: std::cell::Cell<usize> =
35 const { std::cell::Cell::new(0) };
36}
37
38pub const CHECKPOINT_SCHEMA_V1: &str = "chio.checkpoint_statement.v1";
40pub const CHECKPOINT_SCHEMA_V2: &str = "chio.checkpoint_statement.v2";
42pub const CHECKPOINT_SCHEMA: &str = CHECKPOINT_SCHEMA_V2;
44pub const CHECKPOINT_PUBLICATION_SCHEMA: &str = "chio.checkpoint_publication.v1";
45pub const CHECKPOINT_WITNESS_SCHEMA: &str = "chio.checkpoint_witness.v1";
46pub const CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V1: &str = "chio.checkpoint_consistency_proof.v1";
48pub const CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V2: &str = "chio.checkpoint_consistency_proof.v2";
50pub const CHECKPOINT_CONSISTENCY_PROOF_SCHEMA: &str = CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V2;
52pub const CHECKPOINT_EQUIVOCATION_SCHEMA: &str = "chio.checkpoint_equivocation.v1";
53
54#[must_use]
55pub fn is_supported_checkpoint_schema(schema: &str) -> bool {
56 matches!(schema, CHECKPOINT_SCHEMA_V1 | CHECKPOINT_SCHEMA_V2)
57}
58
59fn deserialize_non_null_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
60where
61 D: Deserializer<'de>,
62 T: serde::de::DeserializeOwned,
63{
64 let value = serde_json::Value::deserialize(deserializer)?;
65 if value.is_null() {
66 return Err(<D::Error as serde::de::Error>::custom(
67 "explicit null is not permitted; omit the optional field",
68 ));
69 }
70 T::deserialize(value)
71 .map(Some)
72 .map_err(<D::Error as serde::de::Error>::custom)
73}
74
75#[derive(Debug, thiserror::Error)]
77pub enum CheckpointError {
78 #[error("merkle error: {0}")]
79 Merkle(#[from] chio_core::Error),
80 #[error("serialization error: {0}")]
81 Serialization(String),
82 #[error("signing error: {0}")]
83 Signing(String),
84 #[error("sqlite error: {0}")]
85 Sqlite(#[from] rusqlite::Error),
86 #[error("json error: {0}")]
87 Json(#[from] serde_json::Error),
88 #[error("receipt store error: {0}")]
89 ReceiptStore(#[from] ReceiptStoreError),
90 #[error("invalid checkpoint: {0}")]
91 Invalid(String),
92 #[error("checkpoint signature verification failed")]
93 InvalidSignature,
94 #[error("checkpoint continuity error: {0}")]
95 Continuity(String),
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct KernelCheckpointBody {
102 pub schema: String,
104 pub checkpoint_seq: u64,
106 pub batch_start_seq: u64,
108 pub batch_end_seq: u64,
110 pub tree_size: usize,
112 pub merkle_root: Hash,
114 pub issued_at: u64,
116 pub kernel_key: PublicKey,
118 #[serde(
120 default,
121 skip_serializing_if = "Option::is_none",
122 deserialize_with = "deserialize_non_null_option"
123 )]
124 pub previous_checkpoint_sha256: Option<String>,
125 #[serde(
132 default,
133 skip_serializing_if = "Option::is_none",
134 deserialize_with = "deserialize_non_null_option"
135 )]
136 pub chain_root: Option<Hash>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct KernelCheckpoint {
143 pub body: KernelCheckpointBody,
145 pub signature: Signature,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ReceiptInclusionProof {
152 pub checkpoint_seq: u64,
154 pub receipt_seq: u64,
156 pub leaf_index: usize,
158 pub merkle_root: Hash,
160 pub proof: MerkleProof,
162}
163
164impl ReceiptInclusionProof {
165 #[must_use]
167 pub fn verify(&self, receipt_canonical_bytes: &[u8], expected_root: &Hash) -> bool {
168 self.proof.verify(receipt_canonical_bytes, expected_root)
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct CheckpointPublication {
175 pub log_id: String,
178 pub schema: String,
180 pub checkpoint_seq: u64,
182 pub checkpoint_sha256: String,
184 pub merkle_root: Hash,
186 pub published_at: u64,
188 pub kernel_key: PublicKey,
190 pub log_tree_size: u64,
192 pub entry_start_seq: u64,
194 pub entry_end_seq: u64,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub previous_checkpoint_sha256: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub trust_anchor_binding: Option<CheckpointPublicationTrustAnchorBinding>,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct CheckpointWitness {
208 pub log_id: String,
210 pub schema: String,
212 pub checkpoint_seq: u64,
214 pub checkpoint_sha256: String,
216 pub witness_checkpoint_seq: u64,
218 pub witness_checkpoint_sha256: String,
220 pub witnessed_at: u64,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(deny_unknown_fields)]
232pub struct CheckpointConsistencyProof {
233 pub schema: String,
235 pub log_id: String,
237 pub from_checkpoint_seq: u64,
239 pub to_checkpoint_seq: u64,
241 pub from_checkpoint_sha256: String,
243 pub to_checkpoint_sha256: String,
245 pub from_log_tree_size: u64,
247 pub to_log_tree_size: u64,
249 pub appended_entry_start_seq: u64,
251 pub appended_entry_end_seq: u64,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub from_chain_root: Option<Hash>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub to_chain_root: Option<Hash>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub chain_proof_hashes: Vec<Hash>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub from_leaf_inclusion: Option<MerkleProof>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub to_leaf_inclusion: Option<MerkleProof>,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
279#[serde(rename_all = "snake_case")]
280pub enum CheckpointEquivocationKind {
281 ConflictingCheckpointSeq,
283 ConflictingLogTreeSize,
285 ConflictingPredecessorWitness,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
291pub struct CheckpointEquivocation {
292 pub schema: String,
294 pub kind: CheckpointEquivocationKind,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub log_id: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub log_tree_size: Option<u64>,
302 pub first_checkpoint_seq: u64,
304 pub second_checkpoint_seq: u64,
306 pub first_checkpoint_sha256: String,
308 pub second_checkpoint_sha256: String,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub previous_checkpoint_sha256: Option<String>,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
317pub struct CheckpointTransparencySummary {
318 pub publications: Vec<CheckpointPublication>,
320 pub witnesses: Vec<CheckpointWitness>,
322 pub consistency_proofs: Vec<CheckpointConsistencyProof>,
324 pub equivocations: Vec<CheckpointEquivocation>,
326}
327
328#[must_use]
329pub fn checkpoint_log_id(checkpoint: &KernelCheckpoint) -> String {
330 let log_key_bytes: Vec<u8> = match checkpoint.body.kernel_key.algorithm() {
331 SigningAlgorithm::Ed25519 => checkpoint.body.kernel_key.as_bytes().to_vec(),
332 SigningAlgorithm::P256 | SigningAlgorithm::P384 | SigningAlgorithm::Hybrid => {
333 checkpoint.body.kernel_key.to_hex().into_bytes()
334 }
335 };
336 format!("local-log-{}", sha256_hex(&log_key_bytes))
337}
338
339#[must_use]
340pub fn checkpoint_log_tree_size(body: &KernelCheckpointBody) -> u64 {
341 body.batch_end_seq
342}
343
344fn checkpoint_batch_entry_count(body: &KernelCheckpointBody) -> Result<u64, CheckpointError> {
345 body.batch_end_seq
346 .checked_sub(body.batch_start_seq)
347 .and_then(|count| count.checked_add(1))
348 .ok_or_else(|| {
349 CheckpointError::Invalid(format!(
350 "invalid checkpoint entry range {}-{}",
351 body.batch_start_seq, body.batch_end_seq
352 ))
353 })
354}
355
356pub fn checkpoint_body_sha256(body: &KernelCheckpointBody) -> Result<String, CheckpointError> {
358 let body_bytes =
359 canonical_json_bytes(body).map_err(|e| CheckpointError::Serialization(e.to_string()))?;
360 Ok(sha256_hex(&body_bytes))
361}
362
363#[derive(Serialize)]
369struct CheckpointChainLeaf {
370 checkpoint_seq: u64,
371 batch_start_seq: u64,
372 batch_end_seq: u64,
373 merkle_root: Hash,
374}
375
376pub fn checkpoint_chain_leaf_hash_from_parts(
378 checkpoint_seq: u64,
379 batch_start_seq: u64,
380 batch_end_seq: u64,
381 merkle_root: Hash,
382) -> Result<Hash, CheckpointError> {
383 let leaf = CheckpointChainLeaf {
384 checkpoint_seq,
385 batch_start_seq,
386 batch_end_seq,
387 merkle_root,
388 };
389 let leaf_bytes =
390 canonical_json_bytes(&leaf).map_err(|e| CheckpointError::Serialization(e.to_string()))?;
391 Ok(leaf_hash(&leaf_bytes))
392}
393
394pub fn checkpoint_chain_leaf_hash(body: &KernelCheckpointBody) -> Result<Hash, CheckpointError> {
396 checkpoint_chain_leaf_hash_from_parts(
397 body.checkpoint_seq,
398 body.batch_start_seq,
399 body.batch_end_seq,
400 body.merkle_root,
401 )
402}
403
404#[derive(Debug, Clone, Default, PartialEq, Eq)]
411pub struct CheckpointChainFrontier {
412 subtrees: Vec<(Hash, u64)>,
414}
415
416impl CheckpointChainFrontier {
417 #[must_use]
419 pub fn empty() -> Self {
420 Self::default()
421 }
422
423 #[must_use]
426 pub fn from_leaves(chain_leaf_hashes: &[Hash]) -> Self {
427 let mut frontier = Self::empty();
428 for leaf in chain_leaf_hashes {
429 frontier.append(*leaf);
430 }
431 frontier
432 }
433
434 #[must_use]
436 pub fn leaf_count(&self) -> u64 {
437 self.subtrees.iter().map(|(_, span)| *span).sum()
438 }
439
440 pub fn append(&mut self, chain_leaf_hash: Hash) {
443 self.subtrees.push((chain_leaf_hash, 1));
444 while self.subtrees.len() >= 2 {
445 let (right, right_span) = self.subtrees[self.subtrees.len() - 1];
446 let (left, left_span) = self.subtrees[self.subtrees.len() - 2];
447 if left_span != right_span {
448 break;
449 }
450 self.subtrees.truncate(self.subtrees.len() - 2);
451 self.subtrees
452 .push((chio_core::merkle::node_hash(&left, &right), left_span * 2));
453 }
454 }
455
456 #[cfg(test)]
459 #[must_use]
460 pub fn subtree_count_for_test(&self) -> usize {
461 self.subtrees.len()
462 }
463
464 #[must_use]
469 pub fn root(&self) -> Option<Hash> {
470 let (last, _) = *self.subtrees.last()?;
471 Some(
472 self.subtrees[..self.subtrees.len() - 1]
473 .iter()
474 .rev()
475 .fold(last, |acc, (subtree, _)| {
476 chio_core::merkle::node_hash(subtree, &acc)
477 }),
478 )
479 }
480}
481
482pub fn checkpoint_chain_root(chain_leaf_hashes: &[Hash]) -> Result<Hash, CheckpointError> {
485 Ok(MerkleTree::from_hashes(chain_leaf_hashes.to_vec())?.root())
486}
487
488pub fn build_checkpoint_publication(
490 checkpoint: &KernelCheckpoint,
491) -> Result<CheckpointPublication, CheckpointError> {
492 validate_checkpoint(checkpoint)?;
493 Ok(CheckpointPublication {
494 log_id: checkpoint_log_id(checkpoint),
495 schema: CHECKPOINT_PUBLICATION_SCHEMA.to_string(),
496 checkpoint_seq: checkpoint.body.checkpoint_seq,
497 checkpoint_sha256: checkpoint_body_sha256(&checkpoint.body)?,
498 merkle_root: checkpoint.body.merkle_root,
499 published_at: checkpoint.body.issued_at,
500 kernel_key: checkpoint.body.kernel_key.clone(),
501 log_tree_size: checkpoint_log_tree_size(&checkpoint.body),
502 entry_start_seq: checkpoint.body.batch_start_seq,
503 entry_end_seq: checkpoint.body.batch_end_seq,
504 previous_checkpoint_sha256: checkpoint.body.previous_checkpoint_sha256.clone(),
505 trust_anchor_binding: None,
506 })
507}
508
509pub fn bind_checkpoint_publication_trust_anchor(
511 publication: CheckpointPublication,
512 trust_anchor_binding: CheckpointPublicationTrustAnchorBinding,
513) -> Result<CheckpointPublication, CheckpointError> {
514 trust_anchor_binding
515 .validate()
516 .map_err(|error| CheckpointError::Invalid(error.to_string()))?;
517 bind_checkpoint_publication_trust_anchor_after_validation(publication, trust_anchor_binding)
518}
519
520fn bind_checkpoint_publication_trust_anchor_after_validation(
521 mut publication: CheckpointPublication,
522 trust_anchor_binding: CheckpointPublicationTrustAnchorBinding,
523) -> Result<CheckpointPublication, CheckpointError> {
524 if trust_anchor_binding.publication_identity.kind == CheckpointPublicationIdentityKind::LocalLog
525 && trust_anchor_binding.publication_identity.identity != publication.log_id
526 {
527 return Err(CheckpointError::Invalid(format!(
528 "checkpoint publication local_log identity {} does not match log_id {}",
529 trust_anchor_binding.publication_identity.identity, publication.log_id
530 )));
531 }
532 publication.trust_anchor_binding = Some(trust_anchor_binding);
533 Ok(publication)
534}
535
536pub fn build_trust_anchored_checkpoint_publication(
539 checkpoint: &KernelCheckpoint,
540 trust_anchor_binding: CheckpointPublicationTrustAnchorBinding,
541) -> Result<CheckpointPublication, CheckpointError> {
542 trust_anchor_binding
543 .validate()
544 .map_err(|error| CheckpointError::Invalid(error.to_string()))?;
545 let publication = build_checkpoint_publication(checkpoint)?;
546 bind_checkpoint_publication_trust_anchor_after_validation(publication, trust_anchor_binding)
547}
548
549pub fn build_checkpoint_witness(
551 checkpoint: &KernelCheckpoint,
552 witness_checkpoint: &KernelCheckpoint,
553) -> Result<CheckpointWitness, CheckpointError> {
554 validate_checkpoint(checkpoint)?;
555 validate_checkpoint(witness_checkpoint)?;
556
557 let checkpoint_sha256 = checkpoint_body_sha256(&checkpoint.body)?;
558 let witness_checkpoint_sha256 = checkpoint_body_sha256(&witness_checkpoint.body)?;
559 let Some(previous_checkpoint_sha256) = witness_checkpoint
560 .body
561 .previous_checkpoint_sha256
562 .as_deref()
563 else {
564 return Err(CheckpointError::Continuity(format!(
565 "checkpoint {} does not cite a predecessor digest",
566 witness_checkpoint.body.checkpoint_seq
567 )));
568 };
569 if previous_checkpoint_sha256 != checkpoint_sha256 {
570 return Err(CheckpointError::Continuity(format!(
571 "checkpoint {} does not witness checkpoint {}",
572 witness_checkpoint.body.checkpoint_seq, checkpoint.body.checkpoint_seq
573 )));
574 }
575
576 Ok(CheckpointWitness {
577 log_id: checkpoint_log_id(checkpoint),
578 schema: CHECKPOINT_WITNESS_SCHEMA.to_string(),
579 checkpoint_seq: checkpoint.body.checkpoint_seq,
580 checkpoint_sha256,
581 witness_checkpoint_seq: witness_checkpoint.body.checkpoint_seq,
582 witness_checkpoint_sha256,
583 witnessed_at: witness_checkpoint.body.issued_at,
584 })
585}
586
587fn require_chain_root(checkpoint: &KernelCheckpoint) -> Result<Hash, CheckpointError> {
588 checkpoint.body.chain_root.ok_or_else(|| {
589 CheckpointError::Continuity(format!(
590 "checkpoint {} carries no chain commitment; consistency is unverifiable",
591 checkpoint.body.checkpoint_seq
592 ))
593 })
594}
595
596fn chain_tree_size(checkpoint: &KernelCheckpoint) -> Result<usize, CheckpointError> {
597 usize::try_from(checkpoint.body.checkpoint_seq).map_err(|_| {
598 CheckpointError::Invalid(format!(
599 "checkpoint_seq {} exceeds the addressable chain size",
600 checkpoint.body.checkpoint_seq
601 ))
602 })
603}
604
605fn validate_chain_leaves_for_pair(
608 previous: &KernelCheckpoint,
609 current: &KernelCheckpoint,
610 chain_leaf_hashes: &[Hash],
611) -> Result<(usize, usize), CheckpointError> {
612 let from_size = chain_tree_size(previous)?;
613 let to_size = chain_tree_size(current)?;
614 if from_size == 0 || from_size >= to_size {
618 return Err(CheckpointError::Continuity(format!(
619 "checkpoint {} does not extend checkpoint {} in chain order",
620 current.body.checkpoint_seq, previous.body.checkpoint_seq
621 )));
622 }
623 if chain_leaf_hashes.len() < to_size {
624 return Err(CheckpointError::Continuity(format!(
625 "chain leaf count {} does not reach checkpoint {} chain size {}",
626 chain_leaf_hashes.len(),
627 current.body.checkpoint_seq,
628 to_size
629 )));
630 }
631 if chain_leaf_hashes[from_size - 1] != checkpoint_chain_leaf_hash(&previous.body)? {
632 return Err(CheckpointError::Continuity(format!(
633 "chain leaf {} does not match the predecessor checkpoint body",
634 previous.body.checkpoint_seq
635 )));
636 }
637 if chain_leaf_hashes[to_size - 1] != checkpoint_chain_leaf_hash(¤t.body)? {
638 return Err(CheckpointError::Continuity(format!(
639 "chain leaf {} does not match the checkpoint body",
640 current.body.checkpoint_seq
641 )));
642 }
643 Ok((from_size, to_size))
644}
645
646fn validate_checkpoint_chain_commitments(
647 previous: &KernelCheckpoint,
648 current: &KernelCheckpoint,
649 chain_leaf_hashes: &[Hash],
650 chain_tree: &MerkleTree,
651) -> Result<(usize, usize), CheckpointError> {
652 let (from_size, to_size) =
653 validate_chain_leaves_for_pair(previous, current, chain_leaf_hashes)?;
654 if let Some(from_chain_root) = previous.body.chain_root {
655 if chain_tree.prefix_root(from_size)? != from_chain_root {
656 return Err(CheckpointError::Continuity(format!(
657 "predecessor {} chain_root does not match the retained checkpoint chain",
658 previous.body.checkpoint_seq
659 )));
660 }
661 }
662 if let Some(to_chain_root) = current.body.chain_root {
663 if chain_tree.prefix_root(to_size)? != to_chain_root {
664 return Err(CheckpointError::Continuity(format!(
665 "checkpoint {} chain_root does not extend the retained checkpoint chain",
666 current.body.checkpoint_seq
667 )));
668 }
669 }
670 Ok((from_size, to_size))
671}
672
673fn build_checkpoint_consistency_proof_from_tree(
674 previous: &KernelCheckpoint,
675 current: &KernelCheckpoint,
676 chain_leaf_hashes: &[Hash],
677 chain_tree: &MerkleTree,
678) -> Result<CheckpointConsistencyProof, CheckpointError> {
679 validate_checkpoint_predecessor(previous, current)?;
680 let previous = ValidatedCheckpoint::after_validation(previous)?;
681 let current = ValidatedCheckpoint::after_validation(current)?;
682 build_checkpoint_consistency_proof_from_validated(
683 &previous,
684 ¤t,
685 chain_leaf_hashes,
686 chain_tree,
687 )
688}
689
690fn build_checkpoint_consistency_proof_from_validated(
691 previous: &ValidatedCheckpoint<'_>,
692 current: &ValidatedCheckpoint<'_>,
693 chain_leaf_hashes: &[Hash],
694 chain_tree: &MerkleTree,
695) -> Result<CheckpointConsistencyProof, CheckpointError> {
696 validate_checkpoint_predecessor_link(
697 previous.checkpoint,
698 &previous.checkpoint_sha256,
699 current.checkpoint,
700 )?;
701 if previous.log_id != current.log_id {
702 return Err(CheckpointError::Continuity(format!(
703 "checkpoint {} derives log_id {} but predecessor {} derives {}",
704 current.checkpoint.body.checkpoint_seq,
705 current.log_id,
706 previous.checkpoint.body.checkpoint_seq,
707 previous.log_id
708 )));
709 }
710
711 let from_chain_root = require_chain_root(previous.checkpoint)?;
712 let to_chain_root = require_chain_root(current.checkpoint)?;
713 let (from_size, to_size) = validate_checkpoint_chain_commitments(
714 previous.checkpoint,
715 current.checkpoint,
716 chain_leaf_hashes,
717 chain_tree,
718 )?;
719 let chain_proof_hashes = chain_tree.consistency_proof_between(from_size, to_size)?;
720 let from_leaf_inclusion = chain_tree.inclusion_proof_at_size(from_size - 1, from_size)?;
721 let to_leaf_inclusion = chain_tree.inclusion_proof_at_size(to_size - 1, to_size)?;
722
723 Ok(CheckpointConsistencyProof {
724 schema: CHECKPOINT_CONSISTENCY_PROOF_SCHEMA.to_string(),
725 log_id: current.log_id.clone(),
726 from_checkpoint_seq: previous.checkpoint.body.checkpoint_seq,
727 to_checkpoint_seq: current.checkpoint.body.checkpoint_seq,
728 from_checkpoint_sha256: previous.checkpoint_sha256.clone(),
729 to_checkpoint_sha256: current.checkpoint_sha256.clone(),
730 from_log_tree_size: previous.log_tree_size,
731 to_log_tree_size: current.log_tree_size,
732 appended_entry_start_seq: current.checkpoint.body.batch_start_seq,
733 appended_entry_end_seq: current.checkpoint.body.batch_end_seq,
734 from_chain_root: Some(from_chain_root),
735 to_chain_root: Some(to_chain_root),
736 chain_proof_hashes,
737 from_leaf_inclusion: Some(from_leaf_inclusion),
738 to_leaf_inclusion: Some(to_leaf_inclusion),
739 })
740}
741
742fn build_legacy_checkpoint_consistency_record_from_validated(
743 previous: &ValidatedCheckpoint<'_>,
744 current: &ValidatedCheckpoint<'_>,
745) -> Result<CheckpointConsistencyProof, CheckpointError> {
746 validate_checkpoint_predecessor_link(
747 previous.checkpoint,
748 &previous.checkpoint_sha256,
749 current.checkpoint,
750 )?;
751 if previous.log_id != current.log_id {
752 return Err(CheckpointError::Continuity(format!(
753 "checkpoint {} derives log_id {} but predecessor {} derives {}",
754 current.checkpoint.body.checkpoint_seq,
755 current.log_id,
756 previous.checkpoint.body.checkpoint_seq,
757 previous.log_id
758 )));
759 }
760 if previous.checkpoint.body.schema != CHECKPOINT_SCHEMA_V1
761 || current.checkpoint.body.schema != CHECKPOINT_SCHEMA_V1
762 || previous.checkpoint.body.chain_root.is_some()
763 || current.checkpoint.body.chain_root.is_some()
764 {
765 return Err(CheckpointError::Invalid(
766 "legacy consistency records require two v1 checkpoints without chain commitments"
767 .to_string(),
768 ));
769 }
770
771 Ok(CheckpointConsistencyProof {
772 schema: CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V1.to_string(),
773 log_id: current.log_id.clone(),
774 from_checkpoint_seq: previous.checkpoint.body.checkpoint_seq,
775 to_checkpoint_seq: current.checkpoint.body.checkpoint_seq,
776 from_checkpoint_sha256: previous.checkpoint_sha256.clone(),
777 to_checkpoint_sha256: current.checkpoint_sha256.clone(),
778 from_log_tree_size: previous.log_tree_size,
779 to_log_tree_size: current.log_tree_size,
780 appended_entry_start_seq: current.checkpoint.body.batch_start_seq,
781 appended_entry_end_seq: current.checkpoint.body.batch_end_seq,
782 from_chain_root: None,
783 to_chain_root: None,
784 chain_proof_hashes: Vec::new(),
785 from_leaf_inclusion: None,
786 to_leaf_inclusion: None,
787 })
788}
789
790pub fn build_checkpoint_consistency_proof(
799 previous: &KernelCheckpoint,
800 current: &KernelCheckpoint,
801 chain_leaf_hashes: &[Hash],
802) -> Result<CheckpointConsistencyProof, CheckpointError> {
803 let to_size = chain_tree_size(current)?;
804 if chain_leaf_hashes.len() != to_size {
805 return Err(CheckpointError::Continuity(format!(
806 "chain leaf count {} does not match checkpoint {} chain size {}",
807 chain_leaf_hashes.len(),
808 current.body.checkpoint_seq,
809 to_size
810 )));
811 }
812 let tree = MerkleTree::from_hashes(chain_leaf_hashes.to_vec())?;
813 build_checkpoint_consistency_proof_from_tree(previous, current, chain_leaf_hashes, &tree)
814}
815
816pub fn verify_checkpoint_consistency_proof(
822 previous: &KernelCheckpoint,
823 current: &KernelCheckpoint,
824 proof: &CheckpointConsistencyProof,
825) -> Result<bool, CheckpointError> {
826 verify_checkpoint_consistency_proof_with_anchor(
827 previous,
828 current,
829 proof,
830 CheckpointConsistencyAnchor::Genesis,
831 )
832}
833
834pub fn verify_checkpoint_consistency_proof_with_anchor(
845 previous: &KernelCheckpoint,
846 current: &KernelCheckpoint,
847 proof: &CheckpointConsistencyProof,
848 anchor: CheckpointConsistencyAnchor<'_>,
849) -> Result<bool, CheckpointError> {
850 validate_checkpoint_predecessor(previous, current)?;
851 let previous_log_id = checkpoint_log_id(previous);
852 let current_log_id = checkpoint_log_id(current);
853 if previous_log_id != current_log_id {
854 return Err(CheckpointError::Continuity(format!(
855 "checkpoint {} derives log_id {} but predecessor {} derives {}",
856 current.body.checkpoint_seq,
857 current_log_id,
858 previous.body.checkpoint_seq,
859 previous_log_id
860 )));
861 }
862 let metadata_matches = proof.log_id == current_log_id
863 && proof.from_checkpoint_seq == previous.body.checkpoint_seq
864 && proof.to_checkpoint_seq == current.body.checkpoint_seq
865 && proof.from_checkpoint_sha256 == checkpoint_body_sha256(&previous.body)?
866 && proof.to_checkpoint_sha256 == checkpoint_body_sha256(¤t.body)?
867 && proof.from_log_tree_size == checkpoint_log_tree_size(&previous.body)
868 && proof.to_log_tree_size == checkpoint_log_tree_size(¤t.body)
869 && proof.appended_entry_start_seq == current.body.batch_start_seq
870 && proof.appended_entry_end_seq == current.body.batch_end_seq;
871 if !metadata_matches {
872 return Ok(false);
873 }
874
875 if proof.schema == CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V1 {
876 if previous.body.schema != CHECKPOINT_SCHEMA_V1
877 || current.body.schema != CHECKPOINT_SCHEMA_V1
878 {
879 return Err(CheckpointError::Invalid(
880 "legacy v1 consistency records apply only to v1 checkpoints".to_string(),
881 ));
882 }
883 return Ok(proof.from_chain_root.is_none()
884 && proof.to_chain_root.is_none()
885 && proof.chain_proof_hashes.is_empty()
886 && proof.from_leaf_inclusion.is_none()
887 && proof.to_leaf_inclusion.is_none());
888 }
889 if proof.schema != CHECKPOINT_CONSISTENCY_PROOF_SCHEMA_V2 {
890 return Err(CheckpointError::Invalid(format!(
891 "unsupported consistency proof schema {}",
892 proof.schema
893 )));
894 }
895 let from_chain_root = require_chain_root(previous)?;
896 let to_chain_root = require_chain_root(current)?;
897 if proof.from_chain_root != Some(from_chain_root) || proof.to_chain_root != Some(to_chain_root)
898 {
899 return Ok(false);
900 }
901 let (Some(from_leaf_inclusion), Some(to_leaf_inclusion)) = (
902 proof.from_leaf_inclusion.as_ref(),
903 proof.to_leaf_inclusion.as_ref(),
904 ) else {
905 return Ok(false);
906 };
907
908 let from_size = chain_tree_size(previous)?;
914 let to_size = chain_tree_size(current)?;
915 if !chain_leaf_is_committed(
916 from_leaf_inclusion,
917 from_size,
918 checkpoint_chain_leaf_hash(&previous.body)?,
919 &from_chain_root,
920 ) || !chain_leaf_is_committed(
921 to_leaf_inclusion,
922 to_size,
923 checkpoint_chain_leaf_hash(¤t.body)?,
924 &to_chain_root,
925 ) {
926 return Ok(false);
927 }
928
929 if !consistency_prefix_is_anchored(previous, from_size, &from_chain_root, anchor)? {
930 return Ok(false);
931 }
932
933 Ok(verify_consistency_proof(
934 from_size,
935 to_size,
936 &from_chain_root,
937 &to_chain_root,
938 &proof.chain_proof_hashes,
939 ))
940}
941
942#[allow(clippy::too_many_arguments)]
943fn ordered_equivocation(
944 kind: CheckpointEquivocationKind,
945 log_id: Option<String>,
946 log_tree_size: Option<u64>,
947 first_seq: u64,
948 first_sha256: String,
949 second_seq: u64,
950 second_sha256: String,
951 previous_checkpoint_sha256: Option<String>,
952) -> CheckpointEquivocation {
953 if (first_seq, first_sha256.as_str()) <= (second_seq, second_sha256.as_str()) {
954 CheckpointEquivocation {
955 schema: CHECKPOINT_EQUIVOCATION_SCHEMA.to_string(),
956 kind,
957 log_id,
958 log_tree_size,
959 first_checkpoint_seq: first_seq,
960 second_checkpoint_seq: second_seq,
961 first_checkpoint_sha256: first_sha256,
962 second_checkpoint_sha256: second_sha256,
963 previous_checkpoint_sha256,
964 }
965 } else {
966 CheckpointEquivocation {
967 schema: CHECKPOINT_EQUIVOCATION_SCHEMA.to_string(),
968 kind,
969 log_id,
970 log_tree_size,
971 first_checkpoint_seq: second_seq,
972 second_checkpoint_seq: first_seq,
973 first_checkpoint_sha256: second_sha256,
974 second_checkpoint_sha256: first_sha256,
975 previous_checkpoint_sha256,
976 }
977 }
978}
979
980#[derive(Debug)]
981struct ValidatedCheckpoint<'a> {
982 checkpoint: &'a KernelCheckpoint,
983 checkpoint_sha256: String,
984 log_id: String,
985 log_tree_size: u64,
986}
987
988impl<'a> ValidatedCheckpoint<'a> {
989 fn validate(checkpoint: &'a KernelCheckpoint) -> Result<Self, CheckpointError> {
990 validate_checkpoint(checkpoint)?;
991 Self::after_validation(checkpoint)
992 }
993
994 fn after_validation(checkpoint: &'a KernelCheckpoint) -> Result<Self, CheckpointError> {
995 Ok(Self {
996 checkpoint,
997 checkpoint_sha256: checkpoint_body_sha256(&checkpoint.body)?,
998 log_id: checkpoint_log_id(checkpoint),
999 log_tree_size: checkpoint_log_tree_size(&checkpoint.body),
1000 })
1001 }
1002}
1003
1004fn detect_checkpoint_equivocation_from_validated(
1005 first: &ValidatedCheckpoint<'_>,
1006 second: &ValidatedCheckpoint<'_>,
1007) -> Option<CheckpointEquivocation> {
1008 #[cfg(test)]
1009 CHECKPOINT_EQUIVOCATION_INSPECTION_COUNT.with(|count| {
1010 count.set(count.get().saturating_add(1));
1011 });
1012 if first.checkpoint_sha256 == second.checkpoint_sha256 {
1013 return None;
1014 }
1015
1016 if first.checkpoint.body.checkpoint_seq == second.checkpoint.body.checkpoint_seq {
1017 return Some(ordered_equivocation(
1018 CheckpointEquivocationKind::ConflictingCheckpointSeq,
1019 (first.log_id == second.log_id).then(|| first.log_id.clone()),
1020 (first.log_tree_size == second.log_tree_size).then_some(first.log_tree_size),
1021 first.checkpoint.body.checkpoint_seq,
1022 first.checkpoint_sha256.clone(),
1023 second.checkpoint.body.checkpoint_seq,
1024 second.checkpoint_sha256.clone(),
1025 first
1026 .checkpoint
1027 .body
1028 .previous_checkpoint_sha256
1029 .clone()
1030 .or_else(|| second.checkpoint.body.previous_checkpoint_sha256.clone()),
1031 ));
1032 }
1033
1034 if first.log_id == second.log_id && first.log_tree_size == second.log_tree_size {
1035 return Some(ordered_equivocation(
1036 CheckpointEquivocationKind::ConflictingLogTreeSize,
1037 Some(first.log_id.clone()),
1038 Some(first.log_tree_size),
1039 first.checkpoint.body.checkpoint_seq,
1040 first.checkpoint_sha256.clone(),
1041 second.checkpoint.body.checkpoint_seq,
1042 second.checkpoint_sha256.clone(),
1043 first
1044 .checkpoint
1045 .body
1046 .previous_checkpoint_sha256
1047 .clone()
1048 .or_else(|| second.checkpoint.body.previous_checkpoint_sha256.clone()),
1049 ));
1050 }
1051
1052 if first.checkpoint.body.previous_checkpoint_sha256.is_some()
1053 && first.checkpoint.body.previous_checkpoint_sha256
1054 == second.checkpoint.body.previous_checkpoint_sha256
1055 {
1056 return Some(ordered_equivocation(
1057 CheckpointEquivocationKind::ConflictingPredecessorWitness,
1058 (first.log_id == second.log_id).then(|| first.log_id.clone()),
1059 None,
1060 first.checkpoint.body.checkpoint_seq,
1061 first.checkpoint_sha256.clone(),
1062 second.checkpoint.body.checkpoint_seq,
1063 second.checkpoint_sha256.clone(),
1064 first.checkpoint.body.previous_checkpoint_sha256.clone(),
1065 ));
1066 }
1067
1068 None
1069}
1070
1071pub fn detect_checkpoint_equivocation(
1073 first: &KernelCheckpoint,
1074 second: &KernelCheckpoint,
1075) -> Result<Option<CheckpointEquivocation>, CheckpointError> {
1076 validate_checkpoint(first)?;
1077 validate_checkpoint(second)?;
1078 let first = ValidatedCheckpoint::after_validation(first)?;
1079 let second = ValidatedCheckpoint::after_validation(second)?;
1080 Ok(detect_checkpoint_equivocation_from_validated(
1081 &first, &second,
1082 ))
1083}
1084
1085#[must_use]
1087pub fn describe_checkpoint_equivocation(equivocation: &CheckpointEquivocation) -> String {
1088 match equivocation.kind {
1089 CheckpointEquivocationKind::ConflictingCheckpointSeq => format!(
1090 "checkpoint_seq {} has conflicting digests {} and {}",
1091 equivocation.first_checkpoint_seq,
1092 equivocation.first_checkpoint_sha256,
1093 equivocation.second_checkpoint_sha256
1094 ),
1095 CheckpointEquivocationKind::ConflictingLogTreeSize => format!(
1096 "log {} has conflicting checkpoints at cumulative tree size {}: {} ({}) vs {} ({})",
1097 equivocation.log_id.as_deref().unwrap_or("<unknown>"),
1098 equivocation.log_tree_size.unwrap_or_default(),
1099 equivocation.first_checkpoint_seq,
1100 equivocation.first_checkpoint_sha256,
1101 equivocation.second_checkpoint_seq,
1102 equivocation.second_checkpoint_sha256
1103 ),
1104 CheckpointEquivocationKind::ConflictingPredecessorWitness => format!(
1105 "predecessor digest {} is witnessed by conflicting checkpoints {} ({}) and {} ({})",
1106 equivocation
1107 .previous_checkpoint_sha256
1108 .as_deref()
1109 .unwrap_or("<missing>"),
1110 equivocation.first_checkpoint_seq,
1111 equivocation.first_checkpoint_sha256,
1112 equivocation.second_checkpoint_seq,
1113 equivocation.second_checkpoint_sha256
1114 ),
1115 }
1116}
1117
1118fn checkpoint_publication_from_validated(
1119 checkpoint: &ValidatedCheckpoint<'_>,
1120) -> CheckpointPublication {
1121 CheckpointPublication {
1122 log_id: checkpoint.log_id.clone(),
1123 schema: CHECKPOINT_PUBLICATION_SCHEMA.to_string(),
1124 checkpoint_seq: checkpoint.checkpoint.body.checkpoint_seq,
1125 checkpoint_sha256: checkpoint.checkpoint_sha256.clone(),
1126 merkle_root: checkpoint.checkpoint.body.merkle_root,
1127 published_at: checkpoint.checkpoint.body.issued_at,
1128 kernel_key: checkpoint.checkpoint.body.kernel_key.clone(),
1129 log_tree_size: checkpoint.log_tree_size,
1130 entry_start_seq: checkpoint.checkpoint.body.batch_start_seq,
1131 entry_end_seq: checkpoint.checkpoint.body.batch_end_seq,
1132 previous_checkpoint_sha256: checkpoint
1133 .checkpoint
1134 .body
1135 .previous_checkpoint_sha256
1136 .clone(),
1137 trust_anchor_binding: None,
1138 }
1139}
1140
1141fn checkpoint_witness_from_validated(
1142 checkpoint: &ValidatedCheckpoint<'_>,
1143 witness_checkpoint: &ValidatedCheckpoint<'_>,
1144) -> CheckpointWitness {
1145 CheckpointWitness {
1146 log_id: checkpoint.log_id.clone(),
1147 schema: CHECKPOINT_WITNESS_SCHEMA.to_string(),
1148 checkpoint_seq: checkpoint.checkpoint.body.checkpoint_seq,
1149 checkpoint_sha256: checkpoint.checkpoint_sha256.clone(),
1150 witness_checkpoint_seq: witness_checkpoint.checkpoint.body.checkpoint_seq,
1151 witness_checkpoint_sha256: witness_checkpoint.checkpoint_sha256.clone(),
1152 witnessed_at: witness_checkpoint.checkpoint.body.issued_at,
1153 }
1154}
1155
1156#[derive(Default)]
1157struct EquivocationKeyBucket {
1158 first_digest_by_position: BTreeMap<usize, String>,
1159 last_position_by_digest: BTreeMap<String, usize>,
1160}
1161
1162fn index_equivocation_key<K: Ord>(
1163 index: &mut BTreeMap<K, EquivocationKeyBucket>,
1164 key: K,
1165 checkpoint_sha256: &str,
1166 position: usize,
1167 candidate_pairs: &mut BTreeSet<(String, String)>,
1168) {
1169 let bucket = index.entry(key).or_default();
1170 match bucket
1171 .last_position_by_digest
1172 .get(checkpoint_sha256)
1173 .copied()
1174 {
1175 None => {
1176 candidate_pairs.extend(
1177 bucket
1178 .last_position_by_digest
1179 .keys()
1180 .map(|digest| (digest.clone(), checkpoint_sha256.to_string())),
1181 );
1182 bucket
1183 .first_digest_by_position
1184 .insert(position, checkpoint_sha256.to_string());
1185 }
1186 Some(previous_position) => {
1187 candidate_pairs.extend(
1188 bucket
1189 .first_digest_by_position
1190 .range((
1191 std::ops::Bound::Excluded(previous_position),
1192 std::ops::Bound::Excluded(position),
1193 ))
1194 .map(|(_, digest)| (digest.clone(), checkpoint_sha256.to_string())),
1195 );
1196 }
1197 }
1198 bucket
1199 .last_position_by_digest
1200 .insert(checkpoint_sha256.to_string(), position);
1201}
1202
1203struct DerivedCheckpointTransparency<'a> {
1204 summary: CheckpointTransparencySummary,
1205 checkpoints: Vec<ValidatedCheckpoint<'a>>,
1206}
1207
1208fn derive_checkpoint_transparency(
1209 checkpoints: &[KernelCheckpoint],
1210) -> Result<DerivedCheckpointTransparency<'_>, CheckpointError> {
1211 let checkpoints = checkpoints
1212 .iter()
1213 .map(ValidatedCheckpoint::validate)
1214 .collect::<Result<Vec<_>, _>>()?;
1215 let mut publications = checkpoints
1216 .iter()
1217 .map(checkpoint_publication_from_validated)
1218 .collect::<Vec<_>>();
1219 publications.sort_by_key(|publication| publication.checkpoint_seq);
1220
1221 let mut by_checkpoint_seq = BTreeMap::<u64, EquivocationKeyBucket>::new();
1225 let mut by_log_tree_size = BTreeMap::<(String, u64), EquivocationKeyBucket>::new();
1226 let mut by_predecessor_digest = BTreeMap::<String, EquivocationKeyBucket>::new();
1227 let mut candidate_pairs = BTreeSet::<(String, String)>::new();
1228 for (position, checkpoint) in checkpoints.iter().enumerate() {
1229 index_equivocation_key(
1230 &mut by_checkpoint_seq,
1231 checkpoint.checkpoint.body.checkpoint_seq,
1232 &checkpoint.checkpoint_sha256,
1233 position,
1234 &mut candidate_pairs,
1235 );
1236 index_equivocation_key(
1237 &mut by_log_tree_size,
1238 (checkpoint.log_id.clone(), checkpoint.log_tree_size),
1239 &checkpoint.checkpoint_sha256,
1240 position,
1241 &mut candidate_pairs,
1242 );
1243 if let Some(previous_checkpoint_sha256) = checkpoint
1244 .checkpoint
1245 .body
1246 .previous_checkpoint_sha256
1247 .clone()
1248 {
1249 index_equivocation_key(
1250 &mut by_predecessor_digest,
1251 previous_checkpoint_sha256,
1252 &checkpoint.checkpoint_sha256,
1253 position,
1254 &mut candidate_pairs,
1255 );
1256 }
1257 }
1258
1259 let by_digest = checkpoints
1260 .iter()
1261 .enumerate()
1262 .map(|(position, checkpoint)| (checkpoint.checkpoint_sha256.clone(), position))
1263 .collect::<BTreeMap<_, _>>();
1264 let mut equivocations = candidate_pairs
1265 .into_iter()
1266 .map(|(first_digest, second_digest)| {
1267 let first = by_digest.get(&first_digest).copied().ok_or_else(|| {
1268 CheckpointError::Invalid(
1269 "equivocation index references a missing first checkpoint digest".to_string(),
1270 )
1271 })?;
1272 let second = by_digest.get(&second_digest).copied().ok_or_else(|| {
1273 CheckpointError::Invalid(
1274 "equivocation index references a missing second checkpoint digest".to_string(),
1275 )
1276 })?;
1277 Ok(detect_checkpoint_equivocation_from_validated(
1278 &checkpoints[first],
1279 &checkpoints[second],
1280 ))
1281 })
1282 .collect::<Result<Vec<_>, CheckpointError>>()?
1283 .into_iter()
1284 .flatten()
1285 .collect::<Vec<_>>();
1286 equivocations.sort();
1287 equivocations.dedup();
1288 let equivocated_digests = equivocations
1289 .iter()
1290 .flat_map(|equivocation| {
1291 [
1292 equivocation.first_checkpoint_sha256.clone(),
1293 equivocation.second_checkpoint_sha256.clone(),
1294 ]
1295 })
1296 .collect::<BTreeSet<_>>();
1297
1298 let mut by_seq = BTreeMap::<u64, Vec<usize>>::new();
1304 for (position, checkpoint) in checkpoints.iter().enumerate() {
1305 by_seq
1306 .entry(checkpoint.checkpoint.body.checkpoint_seq)
1307 .or_default()
1308 .push(position);
1309 }
1310 let mut chain_leaf_hashes = Vec::new();
1311 let mut next_seq = 1u64;
1312 while let Some([single]) = by_seq.get(&next_seq).map(Vec::as_slice) {
1313 chain_leaf_hashes.push(checkpoint_chain_leaf_hash(
1314 &checkpoints[*single].checkpoint.body,
1315 )?);
1316 let Some(following) = next_seq.checked_add(1) else {
1317 break;
1318 };
1319 next_seq = following;
1320 }
1321 let chain_tree = (!chain_leaf_hashes.is_empty())
1322 .then(|| MerkleTree::from_hashes(chain_leaf_hashes.clone()))
1323 .transpose()?;
1324
1325 let mut witnesses = Vec::new();
1326 let mut consistency_proofs = Vec::new();
1327 for checkpoint in &checkpoints {
1328 let Some(previous_checkpoint_sha256) = checkpoint
1329 .checkpoint
1330 .body
1331 .previous_checkpoint_sha256
1332 .as_deref()
1333 else {
1334 continue;
1335 };
1336 if let Some(previous_position) = by_digest.get(previous_checkpoint_sha256) {
1337 let previous = &checkpoints[*previous_position];
1338 if let Err(error) = validate_checkpoint_predecessor_link(
1339 previous.checkpoint,
1340 &previous.checkpoint_sha256,
1341 checkpoint.checkpoint,
1342 ) {
1343 if equivocated_digests.contains(&checkpoint.checkpoint_sha256) {
1344 continue;
1345 }
1346 return Err(error);
1347 }
1348 witnesses.push(checkpoint_witness_from_validated(previous, checkpoint));
1349 let to_size = chain_tree_size(checkpoint.checkpoint)?;
1350 if let Some(tree) = chain_tree.as_ref() {
1351 if chain_leaf_hashes.len() >= to_size {
1352 validate_checkpoint_chain_commitments(
1356 previous.checkpoint,
1357 checkpoint.checkpoint,
1358 &chain_leaf_hashes,
1359 tree,
1360 )?;
1361 if previous.log_id == checkpoint.log_id {
1362 if previous.checkpoint.body.schema == CHECKPOINT_SCHEMA_V1
1363 && checkpoint.checkpoint.body.schema == CHECKPOINT_SCHEMA_V1
1364 {
1365 consistency_proofs.push(
1366 build_legacy_checkpoint_consistency_record_from_validated(
1367 previous, checkpoint,
1368 )?,
1369 );
1370 } else if previous.checkpoint.body.chain_root.is_some()
1371 && checkpoint.checkpoint.body.chain_root.is_some()
1372 {
1373 consistency_proofs.push(
1374 build_checkpoint_consistency_proof_from_validated(
1375 previous,
1376 checkpoint,
1377 &chain_leaf_hashes,
1378 tree,
1379 )?,
1380 );
1381 }
1382 }
1383 }
1384 }
1385 }
1386 }
1387 witnesses.sort_by_key(|witness| (witness.witness_checkpoint_seq, witness.checkpoint_seq));
1388 consistency_proofs.sort_by_key(|proof| (proof.to_checkpoint_seq, proof.from_checkpoint_seq));
1389
1390 Ok(DerivedCheckpointTransparency {
1391 summary: CheckpointTransparencySummary {
1392 publications,
1393 witnesses,
1394 consistency_proofs,
1395 equivocations,
1396 },
1397 checkpoints,
1398 })
1399}
1400
1401pub fn build_checkpoint_transparency(
1403 checkpoints: &[KernelCheckpoint],
1404) -> Result<CheckpointTransparencySummary, CheckpointError> {
1405 Ok(derive_checkpoint_transparency(checkpoints)?.summary)
1406}
1407
1408pub fn validate_checkpoint_transparency(
1415 checkpoints: &[KernelCheckpoint],
1416) -> Result<CheckpointTransparencySummary, CheckpointError> {
1417 let mut checkpoint_seqs = BTreeSet::new();
1418 for checkpoint in checkpoints {
1419 if !checkpoint_seqs.insert(checkpoint.body.checkpoint_seq) {
1420 return Err(CheckpointError::Continuity(format!(
1421 "duplicate checkpoint sequence {}",
1422 checkpoint.body.checkpoint_seq
1423 )));
1424 }
1425 }
1426
1427 let derived = derive_checkpoint_transparency(checkpoints)?;
1428 let transparency = derived.summary;
1429 if let Some(equivocation) = transparency.equivocations.first() {
1430 return Err(CheckpointError::Continuity(format!(
1431 "checkpoint equivocation detected: {}",
1432 describe_checkpoint_equivocation(equivocation)
1433 )));
1434 }
1435
1436 let by_digest = derived
1437 .checkpoints
1438 .iter()
1439 .enumerate()
1440 .map(|(position, checkpoint)| (checkpoint.checkpoint_sha256.as_str(), position))
1441 .collect::<BTreeMap<_, _>>();
1442 for checkpoint in &derived.checkpoints {
1443 let Some(previous_checkpoint_sha256) = checkpoint
1444 .checkpoint
1445 .body
1446 .previous_checkpoint_sha256
1447 .as_deref()
1448 else {
1449 if checkpoint.checkpoint.body.checkpoint_seq != 1 {
1450 return Err(CheckpointError::Continuity(format!(
1451 "checkpoint {} is not connected to checkpoint 1",
1452 checkpoint.checkpoint.body.checkpoint_seq
1453 )));
1454 }
1455 if checkpoint.checkpoint.body.batch_start_seq != 1 {
1456 return Err(CheckpointError::Continuity(format!(
1457 "checkpoint 1 must start at receipt 1, got {}",
1458 checkpoint.checkpoint.body.batch_start_seq
1459 )));
1460 }
1461 continue;
1462 };
1463 let previous = by_digest.get(previous_checkpoint_sha256).ok_or_else(|| {
1464 CheckpointError::Continuity(format!(
1465 "checkpoint {} has unresolved predecessor {}",
1466 checkpoint.checkpoint.body.checkpoint_seq, previous_checkpoint_sha256
1467 ))
1468 })?;
1469 let previous = &derived.checkpoints[*previous];
1470 validate_checkpoint_predecessor_link(
1471 previous.checkpoint,
1472 &previous.checkpoint_sha256,
1473 checkpoint.checkpoint,
1474 )?;
1475 }
1476
1477 Ok(transparency)
1478}
1479
1480pub fn verify_checkpoint_transparency_records(
1486 checkpoints: &[KernelCheckpoint],
1487 supplied: &CheckpointTransparencySummary,
1488) -> Result<CheckpointTransparencySummary, CheckpointError> {
1489 let derived = validate_checkpoint_transparency(checkpoints)?;
1490 let derived_publications = derived
1491 .publications
1492 .iter()
1493 .map(|publication| (publication.checkpoint_seq, publication))
1494 .collect::<BTreeMap<_, _>>();
1495
1496 if supplied.publications.len() != derived.publications.len() {
1497 return Err(CheckpointError::Continuity(
1498 "checkpoint publication records do not match the signed checkpoint set".to_string(),
1499 ));
1500 }
1501
1502 let mut normalized_publications = Vec::with_capacity(supplied.publications.len());
1503 let mut matched_checkpoint_seqs = BTreeSet::new();
1504 for publication in &supplied.publications {
1505 if !matched_checkpoint_seqs.insert(publication.checkpoint_seq) {
1506 return Err(CheckpointError::Continuity(format!(
1507 "duplicate checkpoint publication record for checkpoint {}",
1508 publication.checkpoint_seq
1509 )));
1510 }
1511 let Some(derived_publication) = derived_publications
1512 .get(&publication.checkpoint_seq)
1513 .copied()
1514 else {
1515 return Err(CheckpointError::Continuity(
1516 "checkpoint publication records do not match the signed checkpoint set".to_string(),
1517 ));
1518 };
1519 let expected = match publication.trust_anchor_binding.clone() {
1520 Some(binding) => {
1521 bind_checkpoint_publication_trust_anchor((*derived_publication).clone(), binding)?
1522 }
1523 None => (*derived_publication).clone(),
1524 };
1525 if publication != &expected {
1526 return Err(CheckpointError::Continuity(
1527 "checkpoint publication records do not match the signed checkpoint set".to_string(),
1528 ));
1529 }
1530 normalized_publications.push(expected);
1531 }
1532 if matched_checkpoint_seqs.len() != derived_publications.len() {
1533 return Err(CheckpointError::Continuity(
1534 "checkpoint publication records do not cover the signed checkpoint set".to_string(),
1535 ));
1536 }
1537
1538 if supplied.witnesses != derived.witnesses {
1539 return Err(CheckpointError::Continuity(
1540 "checkpoint witness records do not match the signed checkpoint set".to_string(),
1541 ));
1542 }
1543 if supplied.consistency_proofs != derived.consistency_proofs {
1544 return Err(CheckpointError::Continuity(
1545 "checkpoint consistency proof records do not match the signed checkpoint set"
1546 .to_string(),
1547 ));
1548 }
1549 if supplied.equivocations != derived.equivocations {
1550 return Err(CheckpointError::Continuity(
1551 "checkpoint equivocation records do not match the signed checkpoint set".to_string(),
1552 ));
1553 }
1554
1555 Ok(CheckpointTransparencySummary {
1556 publications: normalized_publications,
1557 witnesses: supplied.witnesses.clone(),
1558 consistency_proofs: supplied.consistency_proofs.clone(),
1559 equivocations: supplied.equivocations.clone(),
1560 })
1561}
1562
1563pub fn verify_checkpoint_continuity(
1565 previous: &KernelCheckpoint,
1566 current: &KernelCheckpoint,
1567) -> Result<bool, CheckpointError> {
1568 match validate_checkpoint_predecessor(previous, current) {
1569 Ok(()) => Ok(true),
1570 Err(CheckpointError::Continuity(_)) => Ok(false),
1571 Err(error) => Err(error),
1572 }
1573}
1574
1575fn unix_now() -> u64 {
1577 SystemTime::now()
1578 .duration_since(UNIX_EPOCH)
1579 .map(|d| d.as_secs())
1580 .unwrap_or(0)
1581}
1582
1583pub fn build_checkpoint(
1589 checkpoint_seq: u64,
1590 batch_start_seq: u64,
1591 batch_end_seq: u64,
1592 receipt_canonical_bytes_batch: &[Vec<u8>],
1593 keypair: &Keypair,
1594) -> Result<KernelCheckpoint, CheckpointError> {
1595 build_checkpoint_with_previous(
1596 checkpoint_seq,
1597 batch_start_seq,
1598 batch_end_seq,
1599 receipt_canonical_bytes_batch,
1600 keypair,
1601 None,
1602 &[],
1603 )
1604}
1605
1606pub fn build_checkpoint_with_previous(
1615 checkpoint_seq: u64,
1616 batch_start_seq: u64,
1617 batch_end_seq: u64,
1618 receipt_canonical_bytes_batch: &[Vec<u8>],
1619 keypair: &Keypair,
1620 previous_checkpoint: Option<&KernelCheckpoint>,
1621 prior_chain_leaf_hashes: &[Hash],
1622) -> Result<KernelCheckpoint, CheckpointError> {
1623 build_checkpoint_with_chain_frontier(
1624 checkpoint_seq,
1625 batch_start_seq,
1626 batch_end_seq,
1627 receipt_canonical_bytes_batch,
1628 keypair,
1629 previous_checkpoint,
1630 &CheckpointChainFrontier::from_leaves(prior_chain_leaf_hashes),
1631 )
1632}
1633
1634#[allow(clippy::too_many_arguments)]
1642pub fn build_checkpoint_with_chain_frontier(
1643 checkpoint_seq: u64,
1644 batch_start_seq: u64,
1645 batch_end_seq: u64,
1646 receipt_canonical_bytes_batch: &[Vec<u8>],
1647 keypair: &Keypair,
1648 previous_checkpoint: Option<&KernelCheckpoint>,
1649 prior_chain: &CheckpointChainFrontier,
1650) -> Result<KernelCheckpoint, CheckpointError> {
1651 let tree = MerkleTree::from_leaves(receipt_canonical_bytes_batch)?;
1652 let merkle_root = tree.root();
1653 let covered_entries = batch_end_seq
1654 .checked_sub(batch_start_seq)
1655 .and_then(|count| count.checked_add(1))
1656 .ok_or_else(|| {
1657 CheckpointError::Invalid(format!(
1658 "invalid checkpoint entry range {batch_start_seq}-{batch_end_seq}"
1659 ))
1660 })?;
1661 if usize::try_from(covered_entries).ok() != Some(tree.leaf_count()) {
1662 return Err(CheckpointError::Invalid(format!(
1663 "receipt batch length {} does not match covered entry count {} for range {}-{}",
1664 tree.leaf_count(),
1665 covered_entries,
1666 batch_start_seq,
1667 batch_end_seq
1668 )));
1669 }
1670
1671 let own_chain_leaf = checkpoint_chain_leaf_hash_from_parts(
1672 checkpoint_seq,
1673 batch_start_seq,
1674 batch_end_seq,
1675 merkle_root,
1676 )?;
1677 let chain_root = match previous_checkpoint {
1678 None => {
1679 if prior_chain.leaf_count() != 0 {
1680 return Err(CheckpointError::Invalid(
1681 "prior chain leaves supplied without a previous checkpoint".to_string(),
1682 ));
1683 }
1684 (checkpoint_seq == 1)
1685 .then(|| checkpoint_chain_root(&[own_chain_leaf]))
1686 .transpose()?
1687 }
1688 Some(previous) => {
1689 validate_checkpoint(previous)?;
1690 validate_checkpoint_successor_position(previous, checkpoint_seq, batch_start_seq)?;
1691 if prior_chain.leaf_count() != previous.body.checkpoint_seq {
1692 return Err(CheckpointError::Continuity(format!(
1693 "prior chain covers {} leaves but predecessor is checkpoint {}",
1694 prior_chain.leaf_count(),
1695 previous.body.checkpoint_seq
1696 )));
1697 }
1698 if let Some(previous_chain_root) = previous.body.chain_root {
1702 if prior_chain.root() != Some(previous_chain_root) {
1703 return Err(CheckpointError::Continuity(format!(
1704 "predecessor {} chain_root does not match the supplied chain",
1705 previous.body.checkpoint_seq
1706 )));
1707 }
1708 }
1709 let mut chain = prior_chain.clone();
1710 chain.append(own_chain_leaf);
1711 chain.root()
1712 }
1713 };
1714
1715 let body = KernelCheckpointBody {
1716 schema: CHECKPOINT_SCHEMA.to_string(),
1717 checkpoint_seq,
1718 batch_start_seq,
1719 batch_end_seq,
1720 tree_size: tree.leaf_count(),
1721 merkle_root,
1722 issued_at: unix_now(),
1723 kernel_key: keypair.public_key(),
1724 previous_checkpoint_sha256: previous_checkpoint
1725 .map(|checkpoint| checkpoint_body_sha256(&checkpoint.body))
1726 .transpose()?,
1727 chain_root,
1728 };
1729 let body_bytes =
1730 canonical_json_bytes(&body).map_err(|e| CheckpointError::Serialization(e.to_string()))?;
1731 let signature = keypair.sign(&body_bytes);
1732 Ok(KernelCheckpoint { body, signature })
1733}
1734
1735pub fn build_inclusion_proof(
1737 tree: &MerkleTree,
1738 leaf_index: usize,
1739 checkpoint_seq: u64,
1740 receipt_seq: u64,
1741) -> Result<ReceiptInclusionProof, CheckpointError> {
1742 let proof = tree.inclusion_proof(leaf_index)?;
1743 Ok(ReceiptInclusionProof {
1744 checkpoint_seq,
1745 receipt_seq,
1746 leaf_index,
1747 merkle_root: tree.root(),
1748 proof,
1749 })
1750}
1751
1752pub fn verify_checkpoint_signature(checkpoint: &KernelCheckpoint) -> Result<bool, CheckpointError> {
1756 #[cfg(test)]
1757 CHECKPOINT_SIGNATURE_VERIFICATION_COUNT.with(|count| {
1758 count.set(count.get().saturating_add(1));
1759 });
1760 let body_bytes = canonical_json_bytes(&checkpoint.body)
1761 .map_err(|e| CheckpointError::Serialization(e.to_string()))?;
1762 Ok(checkpoint
1763 .body
1764 .kernel_key
1765 .verify(&body_bytes, &checkpoint.signature))
1766}
1767
1768#[cfg(test)]
1769fn checkpoint_signature_verification_count_for_test() -> usize {
1770 CHECKPOINT_SIGNATURE_VERIFICATION_COUNT.with(std::cell::Cell::get)
1771}
1772
1773#[cfg(test)]
1774fn checkpoint_equivocation_inspection_count_for_test() -> usize {
1775 CHECKPOINT_EQUIVOCATION_INSPECTION_COUNT.with(std::cell::Cell::get)
1776}
1777
1778pub fn validate_checkpoint(checkpoint: &KernelCheckpoint) -> Result<(), CheckpointError> {
1780 if !is_supported_checkpoint_schema(&checkpoint.body.schema) {
1781 return Err(CheckpointError::Invalid(format!(
1782 "unsupported checkpoint schema {}",
1783 checkpoint.body.schema
1784 )));
1785 }
1786 if checkpoint.body.checkpoint_seq == 0 {
1787 return Err(CheckpointError::Invalid(
1788 "checkpoint_seq must be greater than zero".to_string(),
1789 ));
1790 }
1791 if checkpoint.body.batch_start_seq == 0 {
1792 return Err(CheckpointError::Invalid(
1793 "batch_start_seq must be greater than zero".to_string(),
1794 ));
1795 }
1796 if checkpoint.body.batch_end_seq < checkpoint.body.batch_start_seq {
1797 return Err(CheckpointError::Invalid(format!(
1798 "batch_end_seq {} is less than batch_start_seq {}",
1799 checkpoint.body.batch_end_seq, checkpoint.body.batch_start_seq
1800 )));
1801 }
1802 if checkpoint.body.tree_size == 0 {
1803 return Err(CheckpointError::Invalid(
1804 "tree_size must be greater than zero".to_string(),
1805 ));
1806 }
1807 if checkpoint.body.issued_at == 0 {
1808 return Err(CheckpointError::Invalid(
1809 "issued_at must be greater than zero".to_string(),
1810 ));
1811 }
1812 if checkpoint
1813 .body
1814 .previous_checkpoint_sha256
1815 .as_deref()
1816 .is_some_and(|digest| !is_lowercase_sha256(digest))
1817 {
1818 return Err(CheckpointError::Invalid(
1819 "previous_checkpoint_sha256 must be 64 lowercase hex characters".to_string(),
1820 ));
1821 }
1822 let expected_tree_size = checkpoint_batch_entry_count(&checkpoint.body)?;
1823 if u64::try_from(checkpoint.body.tree_size).ok() != Some(expected_tree_size) {
1824 return Err(CheckpointError::Invalid(format!(
1825 "tree_size {} does not match covered entry count {} for range {}-{}",
1826 checkpoint.body.tree_size,
1827 expected_tree_size,
1828 checkpoint.body.batch_start_seq,
1829 checkpoint.body.batch_end_seq
1830 )));
1831 }
1832 if checkpoint.body.schema == CHECKPOINT_SCHEMA_V1 && checkpoint.body.chain_root.is_some() {
1833 return Err(CheckpointError::Invalid(
1834 "v1 checkpoint statements cannot carry chain_root".to_string(),
1835 ));
1836 }
1837 if checkpoint.body.schema == CHECKPOINT_SCHEMA_V2
1838 && checkpoint.body.checkpoint_seq == 1
1839 && checkpoint.body.chain_root.is_none()
1840 {
1841 return Err(CheckpointError::Invalid(
1842 "v2 checkpoint 1 must carry chain_root".to_string(),
1843 ));
1844 }
1845 if let Some(chain_root) = checkpoint.body.chain_root {
1846 if checkpoint.body.checkpoint_seq == 1
1847 && chain_root
1848 != checkpoint_chain_root(&[checkpoint_chain_leaf_hash(&checkpoint.body)?])?
1849 {
1850 return Err(CheckpointError::Invalid(
1851 "chain_root of the first checkpoint does not commit its own chain leaf".to_string(),
1852 ));
1853 }
1854 }
1855 if !verify_checkpoint_signature(checkpoint)? {
1856 return Err(CheckpointError::InvalidSignature);
1857 }
1858 Ok(())
1859}
1860
1861fn is_lowercase_sha256(value: &str) -> bool {
1862 value.len() == 64
1863 && value
1864 .bytes()
1865 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1866}
1867
1868fn validate_checkpoint_successor_position(
1869 predecessor: &KernelCheckpoint,
1870 checkpoint_seq: u64,
1871 batch_start_seq: u64,
1872) -> Result<(), CheckpointError> {
1873 let expected_checkpoint_seq =
1874 predecessor
1875 .body
1876 .checkpoint_seq
1877 .checked_add(1)
1878 .ok_or_else(|| {
1879 CheckpointError::Continuity("predecessor checkpoint_seq overflowed u64".to_string())
1880 })?;
1881 if checkpoint_seq != expected_checkpoint_seq {
1882 return Err(CheckpointError::Continuity(format!(
1883 "checkpoint_seq {} does not immediately follow predecessor {}",
1884 checkpoint_seq, predecessor.body.checkpoint_seq
1885 )));
1886 }
1887
1888 let expected_batch_start = predecessor
1889 .body
1890 .batch_end_seq
1891 .checked_add(1)
1892 .ok_or_else(|| {
1893 CheckpointError::Continuity("predecessor batch_end_seq overflowed u64".to_string())
1894 })?;
1895 if batch_start_seq != expected_batch_start {
1896 return Err(CheckpointError::Continuity(format!(
1897 "batch_start_seq {} does not immediately follow predecessor batch_end_seq {}",
1898 batch_start_seq, predecessor.body.batch_end_seq
1899 )));
1900 }
1901
1902 Ok(())
1903}
1904
1905pub fn validate_checkpoint_predecessor(
1907 predecessor: &KernelCheckpoint,
1908 checkpoint: &KernelCheckpoint,
1909) -> Result<(), CheckpointError> {
1910 validate_checkpoint(predecessor)?;
1911 validate_checkpoint(checkpoint)?;
1912 let predecessor_sha256 = checkpoint_body_sha256(&predecessor.body)?;
1913 validate_checkpoint_predecessor_link(predecessor, &predecessor_sha256, checkpoint)
1914}
1915
1916fn validate_checkpoint_predecessor_link(
1917 predecessor: &KernelCheckpoint,
1918 predecessor_sha256: &str,
1919 checkpoint: &KernelCheckpoint,
1920) -> Result<(), CheckpointError> {
1921 validate_checkpoint_successor_position(
1922 predecessor,
1923 checkpoint.body.checkpoint_seq,
1924 checkpoint.body.batch_start_seq,
1925 )?;
1926
1927 let Some(previous_checkpoint_sha256) = checkpoint.body.previous_checkpoint_sha256.as_deref()
1928 else {
1929 return Err(CheckpointError::Continuity(format!(
1930 "checkpoint {} is missing predecessor digest",
1931 checkpoint.body.checkpoint_seq
1932 )));
1933 };
1934 if previous_checkpoint_sha256 != predecessor_sha256 {
1935 return Err(CheckpointError::Continuity(format!(
1936 "checkpoint {} does not match predecessor digest {}",
1937 checkpoint.body.checkpoint_seq, predecessor_sha256
1938 )));
1939 }
1940
1941 if predecessor.body.chain_root.is_some() && checkpoint.body.chain_root.is_none() {
1942 return Err(CheckpointError::Continuity(format!(
1943 "checkpoint {} drops the chain commitment its predecessor carries",
1944 checkpoint.body.checkpoint_seq
1945 )));
1946 }
1947 if checkpoint.body.schema == CHECKPOINT_SCHEMA_V2 && checkpoint.body.chain_root.is_none() {
1948 return Err(CheckpointError::Continuity(format!(
1949 "v2 checkpoint {} does not start or extend the chain commitment",
1950 checkpoint.body.checkpoint_seq
1951 )));
1952 }
1953
1954 Ok(())
1955}
1956
1957#[cfg(test)]
1958#[path = "checkpoint/tests.rs"]
1959mod tests;