use crate::base::hash::{sha256_canonical, sha256_concat_ids, Hash256};
use crate::base::time::Time;
use crate::record::record::Record;
use crate::record::refs::RecordId;
use crate::state::state::State;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Checkpoint {
pub log_length: u64,
pub last_time: Time,
pub last_record_id: RecordId,
pub state_hash: Hash256,
pub log_hash: Hash256,
}
pub fn create_checkpoint(
records: &[Record],
state: &State,
) -> Result<Checkpoint, serde_json::Error> {
let ids: Vec<Hash256> = records.iter().map(|r| r.id).collect();
Ok(Checkpoint {
log_length: records.len() as u64,
last_time: records.last().map(|r| r.time).unwrap_or(0),
last_record_id: records.last().map(|r| r.id).unwrap_or([0u8; 32]),
state_hash: sha256_canonical(state)?,
log_hash: sha256_concat_ids(&ids),
})
}
pub struct TrustedCheckpoint {
checkpoint: Checkpoint,
rules_hash: Hash256,
}
impl TrustedCheckpoint {
pub fn from_verified_log(
records: &[Record],
rules: &crate::verify::rules::VerifierRules,
) -> Option<Self> {
let verdict = crate::verify::verifier::verify_log(records, rules, None);
if verdict.result != crate::record::kind::VerdictResult::Accept {
return None;
}
let state = crate::state::build::build_state_unchecked(records).ok()?;
let checkpoint = create_checkpoint(records, &state).ok()?;
Some(Self {
checkpoint,
rules_hash: sha256_canonical(rules).ok()?,
})
}
pub fn assume_verified(
checkpoint: Checkpoint,
rules: &crate::verify::rules::VerifierRules,
) -> Result<Self, serde_json::Error> {
Ok(Self {
checkpoint,
rules_hash: sha256_canonical(rules)?,
})
}
pub fn checkpoint(&self) -> &Checkpoint {
&self.checkpoint
}
pub(crate) fn rules_hash(&self) -> &Hash256 {
&self.rules_hash
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HeadAttestation {
pub head_hash: Hash256,
pub record_count: u64,
pub spec_version: String,
pub timestamp: CanonicalUtcTimestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidTimestamp;
impl std::fmt::Display for InvalidTimestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"head attestation timestamps must be canonical RFC 3339 UTC: YYYY-MM-DDTHH:MM:SSZ"
)
}
}
impl std::error::Error for InvalidTimestamp {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct CanonicalUtcTimestamp(String);
impl CanonicalUtcTimestamp {
pub fn new(value: String) -> Result<Self, InvalidTimestamp> {
Self::try_from(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for CanonicalUtcTimestamp {
type Error = InvalidTimestamp;
fn try_from(value: String) -> Result<Self, Self::Error> {
if is_canonical_rfc3339_utc(&value) {
Ok(Self(value))
} else {
Err(InvalidTimestamp)
}
}
}
impl std::str::FromStr for CanonicalUtcTimestamp {
type Err = InvalidTimestamp;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value.to_owned())
}
}
impl AsRef<str> for CanonicalUtcTimestamp {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for CanonicalUtcTimestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for CanonicalUtcTimestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::try_from(value).map_err(serde::de::Error::custom)
}
}
fn is_canonical_rfc3339_utc(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' {
return false;
}
if b[13] != b':' || b[16] != b':' || b[19] != b'Z' {
return false;
}
let digit_at = |i: usize| b[i].is_ascii_digit();
let num = |from: usize, to: usize| -> u32 {
s[from..to].parse().unwrap_or(u32::MAX) };
for i in [0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18] {
if !digit_at(i) {
return false;
}
}
let (year, month, day) = (num(0, 4), num(5, 7), num(8, 10));
let (hour, minute, second) = (num(11, 13), num(14, 16), num(17, 19));
if !(1..=12).contains(&month) || hour > 23 || minute > 59 || second > 59 {
return false;
}
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
let days_in_month = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if leap => 29,
2 => 28,
_ => 0,
};
(1..=days_in_month).contains(&day)
}
pub fn head_attestation(
records: &[Record],
timestamp: String,
) -> Result<HeadAttestation, InvalidTimestamp> {
let timestamp = CanonicalUtcTimestamp::try_from(timestamp)?;
let ids: Vec<Hash256> = records.iter().map(|r| r.id).collect();
Ok(HeadAttestation {
head_hash: sha256_concat_ids(&ids),
record_count: records.len() as u64,
spec_version: crate::base::schema::SPEC_VERSION.to_string(),
timestamp,
})
}
impl HeadAttestation {
pub fn canonical_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
crate::base::canonical::canonical_json(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_checkpoint() {
let state = State::default();
let cp = create_checkpoint(&[], &state).unwrap();
assert_eq!(cp.log_length, 0);
assert_eq!(cp.last_time, 0);
assert_eq!(cp.last_record_id, [0u8; 32]);
}
#[test]
fn test_head_attestation_canonical_form() {
let att = head_attestation(&[], "2026-08-07T12:00:00Z".into()).unwrap();
assert_eq!(att.record_count, 0);
let s = String::from_utf8(att.canonical_bytes().unwrap()).unwrap();
assert!(s.starts_with("{\"head_hash\":["));
assert!(s.contains("\"record_count\":0"));
assert!(s.contains("\"spec_version\":\"0.2\""));
assert!(s.ends_with("\"timestamp\":\"2026-08-07T12:00:00Z\"}"));
assert_eq!(att.timestamp.as_str(), "2026-08-07T12:00:00Z");
let round_trip: HeadAttestation = serde_json::from_str(&s).unwrap();
assert_eq!(round_trip, att);
}
#[test]
fn test_head_attestation_deserialization_rejects_invalid_timestamp() {
let att = head_attestation(&[], "2026-08-07T12:00:00Z".into()).unwrap();
let mut value = serde_json::to_value(att).unwrap();
value["timestamp"] = serde_json::Value::String("2026-08-07T12:00:00+00:00".into());
assert!(serde_json::from_value::<HeadAttestation>(value).is_err());
}
#[test]
fn test_checkpoint_deserialization_rejects_unknown_fields() {
let state = State::default();
let checkpoint = create_checkpoint(&[], &state).unwrap();
let mut value = serde_json::to_value(checkpoint).unwrap();
value
.as_object_mut()
.unwrap()
.insert("trusted".into(), serde_json::json!(true));
assert!(serde_json::from_value::<Checkpoint>(value).is_err());
}
#[test]
fn test_head_attestation_deserialization_rejects_unknown_fields() {
let attestation = head_attestation(&[], "2026-08-07T12:00:00Z".into()).unwrap();
let mut value = serde_json::to_value(attestation).unwrap();
value
.as_object_mut()
.unwrap()
.insert("witness".into(), serde_json::json!("untrusted"));
assert!(serde_json::from_value::<HeadAttestation>(value).is_err());
}
}