use crate::session::{
SESSION_CHECKPOINT_STAMP_KEY, SESSION_RUNTIME_CHECKPOINT_PROVENANCE_KEY,
SESSION_TRANSCRIPT_HISTORY_CHECKPOINT_DIGEST_KEY, SESSION_TRANSCRIPT_HISTORY_STATE_KEY,
Session,
};
use crate::types::SessionId;
use async_trait::async_trait;
use serde::{Deserialize, Deserializer, Serialize};
use sha2::{Digest, Sha256};
use std::cell::Cell;
use std::fmt;
pub const SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION: u32 = 1;
pub const SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_RECOVERED: u32 = 2;
pub const SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_WITNESS_V3: u32 = 3;
fn required_stamp_schema_version(provenance: SessionCheckpointProvenance) -> u32 {
match provenance {
SessionCheckpointProvenance::SessionCreated
| SessionCheckpointProvenance::Forked
| SessionCheckpointProvenance::IntraTurnCheckpoint
| SessionCheckpointProvenance::RunBoundaryCommit
| SessionCheckpointProvenance::TranscriptRewrite
| SessionCheckpointProvenance::RecoveryMigration => SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION,
SessionCheckpointProvenance::RecoveredRunBoundaryCommit
| SessionCheckpointProvenance::RecoveredInterruptedBoundary => {
SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_RECOVERED
}
}
}
fn required_stamp_schema_version_for_witness(witness_format: Option<u32>) -> u32 {
match witness_format {
Some(format)
if format
>= crate::generated::session_persistence_version_authority::TRANSCRIPT_HISTORY_WITNESS_FORMAT =>
{
SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_WITNESS_V3
}
_ => SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION,
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct SessionLineageId(String);
impl SessionLineageId {
pub fn new(value: impl Into<String>) -> Result<Self, SessionCheckpointError> {
let value = value.into();
if value.trim().is_empty() {
return Err(SessionCheckpointError::EmptyLineage);
}
Ok(Self(value))
}
#[must_use]
pub fn for_session(session_id: &SessionId) -> Self {
Self(format!("session:{session_id}"))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SessionLineageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<'de> Deserialize<'de> for SessionLineageId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct SessionGeneration(u64);
impl SessionGeneration {
pub const INITIAL: Self = Self(0);
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct SessionCheckpointRevision(u64);
impl SessionCheckpointRevision {
pub const INITIAL: Self = Self(0);
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
pub fn checked_next(self) -> Result<Self, SessionCheckpointError> {
self.0
.checked_add(1)
.map(Self)
.ok_or(SessionCheckpointError::RevisionOverflow)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct SessionCheckpointDigest(String);
impl SessionCheckpointDigest {
pub fn parse(value: impl Into<String>) -> Result<Self, SessionCheckpointError> {
let value = value.into();
let Some(hex) = value.strip_prefix("sha256:") else {
return Err(SessionCheckpointError::InvalidDigest(value));
};
if hex.len() != 64
|| !hex
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(SessionCheckpointError::InvalidDigest(value));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SessionCheckpointDigest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<'de> Deserialize<'de> for SessionCheckpointDigest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::parse(value).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionCheckpointProvenance {
SessionCreated,
Forked,
IntraTurnCheckpoint,
RunBoundaryCommit,
TranscriptRewrite,
RecoveryMigration,
RecoveredRunBoundaryCommit,
RecoveredInterruptedBoundary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct SessionCheckpointAnchor {
pub session_id: SessionId,
pub lineage_id: SessionLineageId,
pub generation: SessionGeneration,
pub checkpoint_revision: SessionCheckpointRevision,
pub digest: SessionCheckpointDigest,
pub provenance: SessionCheckpointProvenance,
}
impl SessionCheckpointAnchor {
#[must_use]
pub fn from_stamp(stamp: &SessionCheckpointStamp) -> Self {
Self {
session_id: stamp.session_id.clone(),
lineage_id: stamp.lineage_id.clone(),
generation: stamp.generation,
checkpoint_revision: stamp.checkpoint_revision,
digest: stamp.digest.clone(),
provenance: stamp.provenance,
}
}
pub fn validate_for_session(
&self,
session_id: &SessionId,
lineage_id: &SessionLineageId,
) -> Result<(), SessionCheckpointError> {
if &self.session_id != session_id {
return Err(SessionCheckpointError::SessionIdMismatch {
expected: session_id.clone(),
actual: self.session_id.clone(),
});
}
if &self.lineage_id != lineage_id {
return Err(SessionCheckpointError::AuthorityBaseConflict(format!(
"checkpoint authority-base lineage {} differs from outer lineage {}",
self.lineage_id, lineage_id
)));
}
SessionCheckpointDigest::parse(self.digest.as_str())?;
if self.provenance == SessionCheckpointProvenance::IntraTurnCheckpoint {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"an intra-turn projection cannot be a checkpoint authority base".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SessionCheckpointAuthorityBase {
Absent,
Legacy {
source_blob_digest: SessionCheckpointDigest,
observed_generation: SessionGeneration,
observed_checkpoint_revision: SessionCheckpointRevision,
},
Typed { anchor: SessionCheckpointAnchor },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct SessionCheckpointStamp {
schema_version: u32,
session_id: SessionId,
lineage_id: SessionLineageId,
generation: SessionGeneration,
checkpoint_revision: SessionCheckpointRevision,
authority_base: SessionCheckpointAuthorityBase,
digest: SessionCheckpointDigest,
provenance: SessionCheckpointProvenance,
}
impl SessionCheckpointStamp {
fn from_parts(
session_id: SessionId,
lineage_id: SessionLineageId,
generation: SessionGeneration,
checkpoint_revision: SessionCheckpointRevision,
authority_base: SessionCheckpointAuthorityBase,
digest: MintedCheckpointDigest,
provenance: SessionCheckpointProvenance,
) -> Self {
Self {
schema_version: required_stamp_schema_version(provenance).max(
required_stamp_schema_version_for_witness(digest.witness_format),
),
session_id,
lineage_id,
generation,
checkpoint_revision,
authority_base,
digest: digest.digest,
provenance,
}
}
pub fn root(
session: &Session,
provenance: SessionCheckpointProvenance,
) -> Result<Self, SessionCheckpointError> {
if !matches!(
provenance,
SessionCheckpointProvenance::SessionCreated | SessionCheckpointProvenance::Forked
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"checkpoint root provenance must be session_created or forked".to_string(),
));
}
let stamp = Self::from_parts(
session.id().clone(),
SessionLineageId::for_session(session.id()),
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
SessionCheckpointAuthorityBase::Absent,
session_checkpoint_digest_for_mint(session)?,
provenance,
);
stamp.validate_for_session(session.id())?;
Ok(stamp)
}
pub fn recovery_migration(
session: &Session,
source_blob: &[u8],
observed_generation: SessionGeneration,
observed_checkpoint_revision: SessionCheckpointRevision,
) -> Result<Self, SessionCheckpointError> {
if !matches!(
session.try_checkpoint_state()?,
SessionCheckpointState::LegacyUnverified { .. }
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"recovery migration requires an untyped legacy session".to_string(),
));
}
let source_session: Session = serde_json::from_slice(source_blob)?;
if !matches!(
source_session.try_checkpoint_state()?,
SessionCheckpointState::LegacyUnverified { .. }
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"recovery migration source BLOB must be an untyped legacy session".to_string(),
));
}
if source_session.id() != session.id() {
return Err(SessionCheckpointError::SessionIdMismatch {
expected: session.id().clone(),
actual: source_session.id().clone(),
});
}
let digest = session_checkpoint_digest_selected(session, WitnessSelection::Evidence)?;
let source_digest = session_checkpoint_digest(&source_session)?;
if source_digest != digest.digest {
return Err(SessionCheckpointError::LegacySourceBlobMismatch {
expected: digest.digest,
actual: source_digest,
});
}
let source_blob_digest = legacy_session_source_blob_digest(source_blob);
let stamp = Self::from_parts(
session.id().clone(),
SessionLineageId::for_session(session.id()),
observed_generation,
observed_checkpoint_revision,
SessionCheckpointAuthorityBase::Legacy {
source_blob_digest,
observed_generation,
observed_checkpoint_revision,
},
digest,
SessionCheckpointProvenance::RecoveryMigration,
);
stamp.validate_for_session(session.id())?;
Ok(stamp)
}
pub fn successor(
session: &Session,
authority: &Self,
provenance: SessionCheckpointProvenance,
) -> Result<Self, SessionCheckpointError> {
authority.validate_for_session(session.id())?;
if !matches!(
provenance,
SessionCheckpointProvenance::IntraTurnCheckpoint
| SessionCheckpointProvenance::RunBoundaryCommit
| SessionCheckpointProvenance::TranscriptRewrite
| SessionCheckpointProvenance::RecoveredRunBoundaryCommit
| SessionCheckpointProvenance::RecoveredInterruptedBoundary
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"checkpoint successor provenance must be checkpoint, boundary, rewrite, \
or a recovered boundary"
.to_string(),
));
}
let stamp = Self::from_parts(
session.id().clone(),
authority.lineage_id().clone(),
authority.generation(),
authority.checkpoint_revision().checked_next()?,
SessionCheckpointAuthorityBase::Typed {
anchor: SessionCheckpointAnchor::from_stamp(authority),
},
session_checkpoint_digest_for_mint(session)?,
provenance,
);
stamp.validate_for_session(session.id())?;
Ok(stamp)
}
pub fn intra_turn_projection(
session: &Session,
observed: &Self,
) -> Result<Self, SessionCheckpointError> {
observed.validate_for_session(session.id())?;
let anchor = match (&observed.authority_base, observed.provenance) {
(
SessionCheckpointAuthorityBase::Typed { anchor },
SessionCheckpointProvenance::IntraTurnCheckpoint,
) => anchor.clone(),
_ => SessionCheckpointAnchor::from_stamp(observed),
};
anchor.validate_for_session(session.id(), &observed.lineage_id)?;
let stamp = Self::from_parts(
session.id().clone(),
observed.lineage_id.clone(),
anchor.generation,
anchor.checkpoint_revision.checked_next()?,
SessionCheckpointAuthorityBase::Typed { anchor },
session_checkpoint_digest_for_mint(session)?,
SessionCheckpointProvenance::IntraTurnCheckpoint,
);
stamp.validate_for_session(session.id())?;
Ok(stamp)
}
#[cfg(test)]
fn new(
session_id: SessionId,
lineage_id: SessionLineageId,
generation: SessionGeneration,
checkpoint_revision: SessionCheckpointRevision,
authority_base: SessionCheckpointAuthorityBase,
digest: SessionCheckpointDigest,
provenance: SessionCheckpointProvenance,
) -> Self {
let digest = MintedCheckpointDigest {
digest,
witness_format: None,
};
Self::from_parts(
session_id,
lineage_id,
generation,
checkpoint_revision,
authority_base,
digest,
provenance,
)
}
#[must_use]
pub const fn schema_version(&self) -> u32 {
self.schema_version
}
#[must_use]
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
#[must_use]
pub fn lineage_id(&self) -> &SessionLineageId {
&self.lineage_id
}
#[must_use]
pub const fn generation(&self) -> SessionGeneration {
self.generation
}
#[must_use]
pub const fn checkpoint_revision(&self) -> SessionCheckpointRevision {
self.checkpoint_revision
}
#[must_use]
pub fn authority_base(&self) -> &SessionCheckpointAuthorityBase {
&self.authority_base
}
#[must_use]
pub fn digest(&self) -> &SessionCheckpointDigest {
&self.digest
}
#[must_use]
pub const fn provenance(&self) -> SessionCheckpointProvenance {
self.provenance
}
pub fn validate_for_session(
&self,
session_id: &SessionId,
) -> Result<(), SessionCheckpointError> {
let provenance_floor = required_stamp_schema_version(self.provenance);
let witness_v3 = provenance_floor.max(SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_WITNESS_V3);
if self.schema_version != provenance_floor && self.schema_version != witness_v3 {
return Err(SessionCheckpointError::UnsupportedSchemaVersion(
self.schema_version,
));
}
if &self.session_id != session_id {
return Err(SessionCheckpointError::SessionIdMismatch {
expected: session_id.clone(),
actual: self.session_id.clone(),
});
}
SessionLineageId::new(self.lineage_id.as_str())?;
SessionCheckpointDigest::parse(self.digest.as_str())?;
match &self.authority_base {
SessionCheckpointAuthorityBase::Absent => {
if self.generation != SessionGeneration::INITIAL
|| self.checkpoint_revision != SessionCheckpointRevision::INITIAL
|| !matches!(
self.provenance,
SessionCheckpointProvenance::SessionCreated
| SessionCheckpointProvenance::Forked
)
{
return Err(SessionCheckpointError::AuthorityBaseConflict(
"absent authority base is legal only for a generation-zero create or fork root"
.to_string(),
));
}
}
SessionCheckpointAuthorityBase::Legacy {
source_blob_digest,
observed_generation,
observed_checkpoint_revision,
} => {
SessionCheckpointDigest::parse(source_blob_digest.as_str())?;
if self.lineage_id != SessionLineageId::for_session(session_id)
|| self.generation != *observed_generation
|| self.checkpoint_revision != *observed_checkpoint_revision
|| self.provenance != SessionCheckpointProvenance::RecoveryMigration
{
return Err(SessionCheckpointError::AuthorityBaseConflict(
"legacy migration must retain its exact observed cursor under the deterministic session lineage"
.to_string(),
));
}
}
SessionCheckpointAuthorityBase::Typed { anchor } => {
anchor.validate_for_session(session_id, &self.lineage_id)?;
if !matches!(
self.provenance,
SessionCheckpointProvenance::IntraTurnCheckpoint
| SessionCheckpointProvenance::RunBoundaryCommit
| SessionCheckpointProvenance::TranscriptRewrite
| SessionCheckpointProvenance::RecoveredRunBoundaryCommit
| SessionCheckpointProvenance::RecoveredInterruptedBoundary
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"typed authority base requires checkpoint, boundary, rewrite, or \
recovered-boundary provenance"
.to_string(),
));
}
if self.generation != anchor.generation
|| self.checkpoint_revision != anchor.checkpoint_revision.checked_next()?
{
return Err(SessionCheckpointError::AuthorityBaseConflict(format!(
"checkpoint must be the exact successor of authority generation {} revision {}",
anchor.generation.get(),
anchor.checkpoint_revision.get()
)));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionCheckpointAncestryProof {
ancestor: SessionCheckpointStamp,
descendant: SessionCheckpointStamp,
edge_count: u64,
path_digest: SessionCheckpointDigest,
}
impl SessionCheckpointAncestryProof {
pub fn try_from_stamps(
chain: impl IntoIterator<Item = SessionCheckpointStamp>,
) -> Result<Self, SessionCheckpointError> {
let mut chain = chain.into_iter();
let Some(first) = chain.next() else {
return Err(SessionCheckpointError::EmptyAncestryProof);
};
first.validate_for_session(first.session_id())?;
let ancestor = first.clone();
let mut previous = first;
let mut edge_count = 0_u64;
let mut path_hasher = Sha256::new();
path_hasher.update(b"meerkat:session-checkpoint-ancestry-proof:v1\0");
update_ancestry_path_digest(&mut path_hasher, &previous)?;
for child in chain {
edge_count = edge_count
.checked_add(1)
.ok_or(SessionCheckpointError::AncestryEdgeCountOverflow)?;
child.validate_for_session(child.session_id())?;
if child.session_id() != ancestor.session_id() {
return Err(SessionCheckpointError::AncestrySessionMismatch {
index: edge_count,
expected: ancestor.session_id().clone(),
actual: child.session_id().clone(),
});
}
if child.lineage_id() != ancestor.lineage_id() {
return Err(SessionCheckpointError::AncestryLineageMismatch {
index: edge_count,
expected: ancestor.lineage_id().clone(),
actual: child.lineage_id().clone(),
});
}
if child.generation() != ancestor.generation() {
return Err(SessionCheckpointError::AncestryGenerationMismatch {
index: edge_count,
expected: ancestor.generation().get(),
actual: child.generation().get(),
});
}
if child.checkpoint_revision() <= previous.checkpoint_revision() {
return Err(SessionCheckpointError::AncestryRevisionNotIncreasing {
index: edge_count,
previous: previous.checkpoint_revision().get(),
actual: child.checkpoint_revision().get(),
});
}
if !matches!(
child.authority_base(),
SessionCheckpointAuthorityBase::Typed { anchor }
if anchor == &SessionCheckpointAnchor::from_stamp(&previous)
) {
return Err(SessionCheckpointError::AncestryAuthorityBaseMismatch {
index: edge_count,
});
}
update_ancestry_path_digest(&mut path_hasher, &child)?;
previous = child;
}
Ok(Self {
ancestor,
descendant: previous,
edge_count,
path_digest: SessionCheckpointDigest(format!("sha256:{:x}", path_hasher.finalize())),
})
}
pub fn from_chain(chain: Vec<SessionCheckpointStamp>) -> Result<Self, SessionCheckpointError> {
Self::try_from_stamps(chain)
}
#[must_use]
pub fn ancestor(&self) -> &SessionCheckpointStamp {
&self.ancestor
}
#[must_use]
pub fn descendant(&self) -> &SessionCheckpointStamp {
&self.descendant
}
#[must_use]
pub const fn edge_count(&self) -> u64 {
self.edge_count
}
#[must_use]
pub fn path_digest(&self) -> &SessionCheckpointDigest {
&self.path_digest
}
#[must_use]
pub fn proves(
&self,
ancestor: &SessionCheckpointStamp,
descendant: &SessionCheckpointStamp,
) -> bool {
self.ancestor() == ancestor && self.descendant() == descendant
}
}
impl TryFrom<Vec<SessionCheckpointStamp>> for SessionCheckpointAncestryProof {
type Error = SessionCheckpointError;
fn try_from(value: Vec<SessionCheckpointStamp>) -> Result<Self, Self::Error> {
Self::from_chain(value)
}
}
fn update_ancestry_path_digest(
hasher: &mut Sha256,
stamp: &SessionCheckpointStamp,
) -> Result<(), SessionCheckpointError> {
let value = serde_json::to_value(stamp)?;
let mut canonical = Vec::new();
write_canonical_json(&value, &mut canonical)?;
let length = u64::try_from(canonical.len())
.map_err(|_| SessionCheckpointError::AncestryPathElementTooLarge)?;
hasher.update(length.to_be_bytes());
hasher.update(canonical);
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionCheckpointState {
Verified(SessionCheckpointStamp),
LegacyUnverified {
legacy_runtime_checkpoint: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionCheckpointMetadataState {
Stamped(SessionCheckpointStamp),
LegacyUnverified { legacy_runtime_checkpoint: bool },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionCheckpointRelation {
Exact,
LeftRevisionOlder,
LeftRevisionNewer,
RevisionConflict,
LeftGenerationOlder,
LeftGenerationNewer,
DifferentSessionIdentity,
DifferentLineage,
BothLegacyUnverified,
LeftLegacyUnverified,
RightLegacyUnverified,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionCheckpointError {
#[error("session checkpoint lineage must not be empty")]
EmptyLineage,
#[error("unsupported session checkpoint stamp schema version {0}")]
UnsupportedSchemaVersion(u32),
#[error("session checkpoint revision overflow")]
RevisionOverflow,
#[error("invalid session checkpoint digest `{0}`")]
InvalidDigest(String),
#[error("checkpoint stamp session id mismatch: expected {expected}, got {actual}")]
SessionIdMismatch {
expected: SessionId,
actual: SessionId,
},
#[error("checkpoint stamp digest mismatch: expected {expected}, got {actual}")]
DigestMismatch {
expected: SessionCheckpointDigest,
actual: SessionCheckpointDigest,
},
#[error(
"transcript-history checkpoint witness mismatch: carried {carried}, computed {computed}"
)]
TranscriptHistoryWitnessMismatch {
carried: SessionCheckpointDigest,
computed: SessionCheckpointDigest,
},
#[error(
"unsupported transcript-history witness format {0}: this binary predates the format; \
refusing before any normalization or healing of the row"
)]
UnsupportedTranscriptHistoryWitnessFormat(u32),
#[error("unsupported transcript-history revision digest format {0}")]
UnsupportedTranscriptHistoryRevisionDigestFormat(u32),
#[error("malformed transcript-history witness carrier: {0}")]
MalformedTranscriptHistoryWitness(String),
#[error(
"legacy migration source BLOB semantic digest mismatch: expected {expected}, got {actual}"
)]
LegacySourceBlobMismatch {
expected: SessionCheckpointDigest,
actual: SessionCheckpointDigest,
},
#[error("malformed legacy checkpoint provenance: expected boolean")]
MalformedLegacyProvenance,
#[error("legacy checkpoint provenance is unverified; explicit migration is required")]
LegacyCheckpointUnverified,
#[error("legacy checkpoint provenance cannot mutate a typed checkpoint")]
LegacyProvenanceMutationOnTypedCheckpoint,
#[error("session checkpoint ancestry proof must contain at least one stamp")]
EmptyAncestryProof,
#[error("session checkpoint ancestry edge count overflow")]
AncestryEdgeCountOverflow,
#[error("session checkpoint ancestry path element is too large")]
AncestryPathElementTooLarge,
#[error(
"checkpoint ancestry stamp {index} has session {actual}, expected exact session {expected}"
)]
AncestrySessionMismatch {
index: u64,
expected: SessionId,
actual: SessionId,
},
#[error(
"checkpoint ancestry stamp {index} has lineage {actual}, expected exact lineage {expected}"
)]
AncestryLineageMismatch {
index: u64,
expected: SessionLineageId,
actual: SessionLineageId,
},
#[error(
"checkpoint ancestry stamp {index} has generation {actual}, expected generation {expected}"
)]
AncestryGenerationMismatch {
index: u64,
expected: u64,
actual: u64,
},
#[error(
"checkpoint ancestry stamp {index} revision {actual} is not newer than previous revision {previous}"
)]
AncestryRevisionNotIncreasing {
index: u64,
previous: u64,
actual: u64,
},
#[error("checkpoint ancestry stamp {index} does not name the exact previous authority base")]
AncestryAuthorityBaseMismatch { index: u64 },
#[error("checkpoint authority-base conflict: {0}")]
AuthorityBaseConflict(String),
#[error("session checkpoint serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
}
pub fn session_checkpoint_metadata_state(
session_id: &SessionId,
metadata: &serde_json::Map<String, serde_json::Value>,
) -> Result<SessionCheckpointMetadataState, SessionCheckpointError> {
let legacy_runtime_checkpoint = match metadata.get(SESSION_RUNTIME_CHECKPOINT_PROVENANCE_KEY) {
Some(value) => value
.as_bool()
.ok_or(SessionCheckpointError::MalformedLegacyProvenance)?,
None => false,
};
let Some(value) = metadata.get(SESSION_CHECKPOINT_STAMP_KEY) else {
return Ok(SessionCheckpointMetadataState::LegacyUnverified {
legacy_runtime_checkpoint,
});
};
let stamp = serde_json::from_value::<SessionCheckpointStamp>(value.clone())?;
stamp.validate_for_session(session_id)?;
Ok(SessionCheckpointMetadataState::Stamped(stamp))
}
thread_local! {
static CONTENT_DIGEST_COMPUTATIONS: Cell<u64> = const { Cell::new(0) };
}
#[doc(hidden)]
#[must_use]
pub fn session_content_digest_computations() -> u64 {
CONTENT_DIGEST_COMPUTATIONS.with(Cell::get)
}
pub(crate) fn record_content_digest_computation() {
#[cfg(any(test, debug_assertions))]
if DIGEST_ACCOUNTING_SUPPRESSED.with(Cell::get) {
return;
}
CONTENT_DIGEST_COMPUTATIONS.with(|count| count.set(count.get().saturating_add(1)));
}
#[cfg(any(test, debug_assertions))]
thread_local! {
static DIGEST_ACCOUNTING_SUPPRESSED: Cell<bool> = const { Cell::new(false) };
}
#[cfg(any(test, debug_assertions))]
pub(crate) struct DigestAccountingSuppressionScope(bool);
#[cfg(any(test, debug_assertions))]
impl Drop for DigestAccountingSuppressionScope {
fn drop(&mut self) {
DIGEST_ACCOUNTING_SUPPRESSED.with(|flag| flag.set(self.0));
}
}
#[cfg(any(test, debug_assertions))]
pub(crate) fn suppress_digest_accounting() -> DigestAccountingSuppressionScope {
DIGEST_ACCOUNTING_SUPPRESSED.with(|flag| {
let previous = flag.get();
flag.set(true);
DigestAccountingSuppressionScope(previous)
})
}
thread_local! {
static CONTENT_DIGEST_BYTES: Cell<u64> = const { Cell::new(0) };
}
#[doc(hidden)]
#[must_use]
pub fn session_content_digest_bytes() -> u64 {
CONTENT_DIGEST_BYTES.with(Cell::get)
}
pub(crate) fn record_content_digest_bytes(bytes: u64) {
#[cfg(any(test, debug_assertions))]
if DIGEST_ACCOUNTING_SUPPRESSED.with(Cell::get) {
return;
}
CONTENT_DIGEST_BYTES.with(|count| count.set(count.get().saturating_add(bytes)));
GLOBAL_CONTENT_DIGEST_BYTES.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
DIGEST_SITE_BYTES[CURRENT_DIGEST_SITE.with(Cell::get)]
.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
static GLOBAL_CONTENT_DIGEST_BYTES: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[doc(hidden)]
#[must_use]
pub fn global_session_content_digest_bytes() -> u64 {
GLOBAL_CONTENT_DIGEST_BYTES.load(std::sync::atomic::Ordering::Relaxed)
}
static GLOBAL_SESSION_ENCODE_BYTES: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[doc(hidden)]
pub fn record_session_encode_bytes(bytes: u64) {
GLOBAL_SESSION_ENCODE_BYTES.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
#[doc(hidden)]
#[must_use]
pub fn global_session_encode_bytes() -> u64 {
GLOBAL_SESSION_ENCODE_BYTES.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) const DIGEST_SITE_COUNT: usize = 8;
pub(crate) const DIGEST_SITE_OTHER: usize = 0;
pub(crate) const DIGEST_SITE_DECODE: usize = 1;
pub(crate) const DIGEST_SITE_ENCODE: usize = 2;
pub(crate) const DIGEST_SITE_CHECKPOINT_DIGEST: usize = 3;
pub(crate) const DIGEST_SITE_WITNESS: usize = 4;
pub(crate) const DIGEST_SITE_REWRITE_CHAIN_WALK: usize = 5;
pub(crate) const DIGEST_SITE_APPEND_GUARD: usize = 6;
pub(crate) const DIGEST_SITE_BOUNDARY_GUARD: usize = 7;
#[doc(hidden)]
pub const DIGEST_SITE_LABELS: [&str; DIGEST_SITE_COUNT] = [
"other",
"decode",
"encode",
"checkpoint-digest",
"witness",
"rewrite-chain-walk",
"append-guard",
"boundary-guard",
];
static DIGEST_SITE_BYTES: [std::sync::atomic::AtomicU64; DIGEST_SITE_COUNT] =
[const { std::sync::atomic::AtomicU64::new(0) }; DIGEST_SITE_COUNT];
thread_local! {
static CURRENT_DIGEST_SITE: Cell<usize> = const { Cell::new(DIGEST_SITE_OTHER) };
}
#[doc(hidden)]
#[must_use]
pub fn digest_site_bytes() -> [u64; DIGEST_SITE_COUNT] {
std::array::from_fn(|site| DIGEST_SITE_BYTES[site].load(std::sync::atomic::Ordering::Relaxed))
}
pub(crate) struct DigestSiteScope(usize);
impl Drop for DigestSiteScope {
fn drop(&mut self) {
CURRENT_DIGEST_SITE.with(|site| site.set(self.0));
}
}
pub(crate) fn enter_digest_site(site: usize) -> DigestSiteScope {
CURRENT_DIGEST_SITE.with(|current| {
let enclosing = current.get();
current.set(site);
DigestSiteScope(enclosing)
})
}
pub fn session_checkpoint_digest(
session: &Session,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
Ok(session_checkpoint_digest_selected(session, WitnessSelection::Evidence)?.digest)
}
pub(crate) fn session_checkpoint_digest_for_mint(
session: &Session,
) -> Result<MintedCheckpointDigest, SessionCheckpointError> {
session_checkpoint_digest_selected(session, WitnessSelection::MintCurrent)
}
pub(crate) struct MintedCheckpointDigest {
pub(crate) digest: SessionCheckpointDigest,
pub(crate) witness_format: Option<u32>,
}
fn session_checkpoint_digest_selected(
session: &Session,
selection: WitnessSelection,
) -> Result<MintedCheckpointDigest, SessionCheckpointError> {
let _digest_site = enter_digest_site(DIGEST_SITE_CHECKPOINT_DIGEST);
let witness = resolve_transcript_history_witness(session, selection)?;
let history_digest = witness.as_ref().map(TranscriptHistoryWitness::digest);
let digest = match framed_session_checkpoint_digest(session, history_digest) {
Some(digest) => digest,
None => {
let document = checkpoint_digest_document_for_hash(session, history_digest)?;
canonical_value_digest(&document)?
}
};
session.seal_verified_checkpoint_digest(&digest);
Ok(MintedCheckpointDigest {
digest,
witness_format: witness.map(|witness| witness.witness_format()),
})
}
fn checkpoint_digest_document_for_hash(
session: &Session,
history_digest: Option<&SessionCheckpointDigest>,
) -> Result<serde_json::Value, SessionCheckpointError> {
let mut document = session.checkpoint_digest_document()?;
strip_checkpoint_digest_metadata(&mut document, history_digest);
Ok(document)
}
fn strip_checkpoint_digest_metadata(
document: &mut serde_json::Value,
history_digest: Option<&SessionCheckpointDigest>,
) {
if let Some(metadata) = document
.as_object_mut()
.and_then(|session| session.get_mut("metadata"))
.and_then(serde_json::Value::as_object_mut)
{
metadata.remove(SESSION_CHECKPOINT_STAMP_KEY);
metadata.remove(SESSION_RUNTIME_CHECKPOINT_PROVENANCE_KEY);
metadata.remove(SESSION_TRANSCRIPT_HISTORY_STATE_KEY);
metadata.remove(SESSION_TRANSCRIPT_HISTORY_CHECKPOINT_DIGEST_KEY);
if let Some(digest) = history_digest {
metadata.insert(
SESSION_TRANSCRIPT_HISTORY_STATE_KEY.to_string(),
checkpoint_history_digest_marker(digest),
);
}
}
}
fn framed_session_checkpoint_digest(
session: &Session,
history_digest: Option<&SessionCheckpointDigest>,
) -> Option<SessionCheckpointDigest> {
let (mut document, marker) = session.checkpoint_digest_framed_document().ok()?;
strip_checkpoint_digest_metadata(&mut document, history_digest);
let mut framed = Vec::new();
write_canonical_json(&document, &mut framed).ok()?;
let needle = serde_json::to_string(&marker).ok()?;
let (prefix, suffix) = split_exactly_once(&framed, needle.as_bytes())?;
let mut hasher = session.framed_document_hasher(prefix)?;
hasher.update(b"]");
hasher.update(suffix);
record_content_digest_computation();
record_content_digest_bytes(prefix.len() as u64 + 1 + suffix.len() as u64);
let digest = SessionCheckpointDigest(format!("sha256:{:x}", hasher.finalize()));
if crate::session::digest_accumulator_take_verification_sample() {
#[cfg(any(test, debug_assertions))]
let _suppress = suppress_digest_accounting();
if let Ok(document) = checkpoint_digest_document_for_hash(session, history_digest)
&& let Ok(reference) = canonical_value_digest_uncounted(&document)
{
assert_eq!(
digest, reference,
"framed checkpoint digest diverged from the canonical document digest: a \
framing seam changed the canonical byte stream without invalidating the midstate"
);
}
}
Some(digest)
}
pub fn warm_framed_checkpoint_midstate(session: &Session) {
let _digest_site = enter_digest_site(DIGEST_SITE_CHECKPOINT_DIGEST);
let Ok((mut document, marker)) = session.checkpoint_digest_framed_document() else {
return;
};
strip_checkpoint_digest_metadata(&mut document, None);
let mut framed = Vec::new();
if write_canonical_json(&document, &mut framed).is_err() {
return;
}
let Ok(needle) = serde_json::to_string(&marker) else {
return;
};
let Some((prefix, _)) = split_exactly_once(&framed, needle.as_bytes()) else {
return;
};
let _ = session.framed_document_hasher(prefix);
}
fn split_exactly_once<'a>(haystack: &'a [u8], needle: &[u8]) -> Option<(&'a [u8], &'a [u8])> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
let first = haystack
.windows(needle.len())
.position(|window| window == needle)?;
let rest = &haystack[first + 1..];
if rest.len() >= needle.len() && rest.windows(needle.len()).any(|window| window == needle) {
return None;
}
Some((&haystack[..first], &haystack[first + needle.len()..]))
}
pub fn session_transcript_history_checkpoint_digest(
session: &Session,
) -> Result<Option<SessionCheckpointDigest>, SessionCheckpointError> {
Ok(session_transcript_history_witness(session)?.map(TranscriptHistoryWitness::into_digest))
}
pub fn session_transcript_history_witness(
session: &Session,
) -> Result<Option<TranscriptHistoryWitness>, SessionCheckpointError> {
resolve_transcript_history_witness(session, WitnessSelection::Evidence)
}
#[derive(Debug, Clone, Copy)]
enum WitnessSelection {
Evidence,
DeclaredBySchema(u32),
MintCurrent,
}
pub(crate) fn session_checkpoint_digest_for_stamp(
session: &Session,
stamp: &SessionCheckpointStamp,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
Ok(session_checkpoint_digest_selected(
session,
WitnessSelection::DeclaredBySchema(stamp.schema_version()),
)?
.digest)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptHistoryWitness {
witness_format: u32,
revision_digest_format: u32,
digest: SessionCheckpointDigest,
}
impl TranscriptHistoryWitness {
#[must_use]
pub const fn witness_format(&self) -> u32 {
self.witness_format
}
#[must_use]
pub const fn revision_digest_format(&self) -> u32 {
self.revision_digest_format
}
#[must_use]
pub fn digest(&self) -> &SessionCheckpointDigest {
&self.digest
}
#[must_use]
pub fn into_digest(self) -> SessionCheckpointDigest {
self.digest
}
#[must_use]
pub fn to_carried_value(&self) -> serde_json::Value {
if self.witness_format <= 2 {
serde_json::Value::String(self.digest.as_str().to_string())
} else {
serde_json::json!({
"witness_format": self.witness_format,
"revision_digest_format": self.revision_digest_format,
"digest": self.digest.as_str(),
})
}
}
pub fn from_carried_value(value: &serde_json::Value) -> Result<Self, SessionCheckpointError> {
match value {
serde_json::Value::String(digest) => Ok(Self {
witness_format: 2,
revision_digest_format: crate::session::TRANSCRIPT_DIGEST_FORMAT_CURRENT,
digest: SessionCheckpointDigest(digest.clone()),
}),
serde_json::Value::Object(fields) => {
let witness_format = fields
.get("witness_format")
.and_then(serde_json::Value::as_u64)
.and_then(|format| u32::try_from(format).ok())
.ok_or_else(|| {
SessionCheckpointError::MalformedTranscriptHistoryWitness(
"carried witness object is missing a numeric witness_format"
.to_string(),
)
})?;
crate::generated::session_persistence_version_authority::
restore_transcript_history_witness_format(witness_format)
.map_err(|_| {
SessionCheckpointError::UnsupportedTranscriptHistoryWitnessFormat(
witness_format,
)
})?;
let revision_digest_format = fields
.get("revision_digest_format")
.and_then(serde_json::Value::as_u64)
.and_then(|format| u32::try_from(format).ok())
.ok_or_else(|| {
SessionCheckpointError::MalformedTranscriptHistoryWitness(
"carried witness object is missing a numeric revision_digest_format"
.to_string(),
)
})?;
if revision_digest_format != crate::session::TRANSCRIPT_DIGEST_FORMAT_CURRENT {
return Err(
SessionCheckpointError::UnsupportedTranscriptHistoryRevisionDigestFormat(
revision_digest_format,
),
);
}
let digest = fields
.get("digest")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
SessionCheckpointError::MalformedTranscriptHistoryWitness(
"carried witness object is missing a digest string".to_string(),
)
})?;
Ok(Self {
witness_format,
revision_digest_format,
digest: SessionCheckpointDigest(digest.to_string()),
})
}
other => Err(SessionCheckpointError::MalformedTranscriptHistoryWitness(
format!("carried witness must be a digest string or a typed object, got {other}"),
)),
}
}
}
fn resolve_transcript_history_witness(
session: &Session,
selection: WitnessSelection,
) -> Result<Option<TranscriptHistoryWitness>, SessionCheckpointError> {
let carried = session
.metadata()
.get(SESSION_TRANSCRIPT_HISTORY_CHECKPOINT_DIGEST_KEY)
.map(TranscriptHistoryWitness::from_carried_value)
.transpose()?;
let Some(history) = session.metadata().get(SESSION_TRANSCRIPT_HISTORY_STATE_KEY) else {
return Ok(carried);
};
let format = match selection {
WitnessSelection::MintCurrent => {
crate::generated::session_persistence_version_authority::TRANSCRIPT_HISTORY_WITNESS_FORMAT
}
WitnessSelection::DeclaredBySchema(schema) => witness_format_for_stamp_schema(schema),
WitnessSelection::Evidence => match stamped_witness_format(session) {
Some(format) => format,
None => carried
.as_ref()
.map_or(2, TranscriptHistoryWitness::witness_format),
},
};
let computed = computed_transcript_history_witness(session, history, format)?;
if let Some(carrier) = &carried {
let cross = if carrier.witness_format == format {
computed.clone()
} else {
computed_transcript_history_witness(session, history, carrier.witness_format)?
};
if carrier.digest != cross {
return Err(SessionCheckpointError::TranscriptHistoryWitnessMismatch {
carried: carrier.digest.clone(),
computed: cross,
});
}
}
Ok(Some(TranscriptHistoryWitness {
witness_format: format,
revision_digest_format: crate::session::TRANSCRIPT_DIGEST_FORMAT_CURRENT,
digest: computed,
}))
}
fn stamped_witness_format(session: &Session) -> Option<u32> {
session
.metadata()
.get(SESSION_CHECKPOINT_STAMP_KEY)
.and_then(|stamp| stamp.get("schema_version"))
.and_then(serde_json::Value::as_u64)
.and_then(|schema| u32::try_from(schema).ok())
.map(witness_format_for_stamp_schema)
}
fn witness_format_for_stamp_schema(schema: u32) -> u32 {
if schema >= SESSION_CHECKPOINT_STAMP_SCHEMA_VERSION_WITNESS_V3 {
crate::generated::session_persistence_version_authority::TRANSCRIPT_HISTORY_WITNESS_FORMAT
} else {
2
}
}
fn computed_transcript_history_witness(
session: &Session,
history: &serde_json::Value,
format: u32,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
if let Some(cached) = session.cached_transcript_history_witness(format) {
return Ok(SessionCheckpointDigest(cached.to_string()));
}
let computed = match format {
2 => {
match session.assemble_transcript_history_witness(history) {
Some(assembled) => assembled,
None => session_checkpoint_history_digest(history)?,
}
}
3 => session_checkpoint_history_digest_v3(history)?,
other => {
return Err(SessionCheckpointError::UnsupportedTranscriptHistoryWitnessFormat(other));
}
};
session.record_transcript_history_witness(format, computed.as_str());
Ok(computed)
}
pub(crate) const TRANSCRIPT_HISTORY_WITNESS_DOMAIN_V3: &str =
"meerkat/transcript-history-witness/v3";
fn session_checkpoint_history_digest_v3(
history: &serde_json::Value,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let _digest_site = enter_digest_site(DIGEST_SITE_WITNESS);
let malformed = |what: &str| {
SessionCheckpointError::MalformedTranscriptHistoryWitness(format!(
"transcript history graph value is missing {what}"
))
};
let object = history
.as_object()
.ok_or_else(|| malformed("an object form"))?;
let head = object
.get("head")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| malformed("a head revision string"))?;
let commits: Vec<crate::TranscriptRewriteCommit> = match object.get("commits") {
Some(commits) => serde_json::from_value(commits.clone())?,
None => Vec::new(),
};
let commits_value = serde_json::to_value(&commits)?;
let mut commits_bytes = Vec::new();
write_canonical_json(&commits_value, &mut commits_bytes)?;
let mut revision_ids: Vec<&str> = match object.get("revisions") {
Some(revisions) => revisions
.as_array()
.ok_or_else(|| malformed("a revisions array"))?
.iter()
.map(|body| {
body.get("revision")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| malformed("a revision id on every retained body"))
})
.collect::<Result<_, _>>()?,
None => Vec::new(),
};
revision_ids.sort_unstable();
revision_ids.dedup();
let ids_value = serde_json::Value::Array(
revision_ids
.into_iter()
.map(|id| serde_json::Value::String(id.to_string()))
.collect(),
);
let mut ids_bytes = Vec::new();
write_canonical_json(&ids_value, &mut ids_bytes)?;
record_content_digest_bytes((commits_bytes.len() + ids_bytes.len()) as u64);
let commits_digest = format!("sha256:{:x}", Sha256::digest(&commits_bytes));
let retained_revisions_digest = format!("sha256:{:x}", Sha256::digest(&ids_bytes));
let preimage = serde_json::json!({
"domain": TRANSCRIPT_HISTORY_WITNESS_DOMAIN_V3,
"revision_digest_format": crate::session::TRANSCRIPT_DIGEST_FORMAT_CURRENT,
"head_revision": head,
"commits_digest": commits_digest,
"retained_revisions_digest": retained_revisions_digest,
});
canonical_value_digest(&preimage)
}
#[must_use]
pub fn legacy_session_source_blob_digest(source_blob: &[u8]) -> SessionCheckpointDigest {
record_content_digest_computation();
record_content_digest_bytes(source_blob.len() as u64);
SessionCheckpointDigest(format!("sha256:{:x}", Sha256::digest(source_blob)))
}
fn session_checkpoint_history_digest(
history: &serde_json::Value,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let history = crate::session::canonicalize_checkpoint_history_value(history)?;
canonical_value_digest(&history)
}
pub(crate) fn session_checkpoint_history_digest_uncounted(
history: &serde_json::Value,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let history = crate::session::canonicalize_checkpoint_history_value(history)?;
let mut canonical = Vec::new();
write_canonical_json(&history, &mut canonical)?;
Ok(SessionCheckpointDigest(format!(
"sha256:{:x}",
Sha256::digest(canonical)
)))
}
impl SessionCheckpointDigest {
pub(crate) fn from_assembled(digest: String) -> Self {
Self(digest)
}
}
pub fn transcript_history_checkpoint_digest(
history: &crate::TranscriptHistoryState,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let value = serde_json::to_value(history)?;
session_checkpoint_history_digest(&value)
}
fn canonical_value_digest(
value: &serde_json::Value,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let mut canonical = Vec::new();
write_canonical_json(value, &mut canonical)?;
record_content_digest_computation();
record_content_digest_bytes(canonical.len() as u64);
Ok(SessionCheckpointDigest(format!(
"sha256:{:x}",
Sha256::digest(canonical)
)))
}
fn canonical_value_digest_uncounted(
value: &serde_json::Value,
) -> Result<SessionCheckpointDigest, SessionCheckpointError> {
let mut canonical = Vec::new();
write_canonical_json(value, &mut canonical)?;
Ok(SessionCheckpointDigest(format!(
"sha256:{:x}",
Sha256::digest(canonical)
)))
}
fn checkpoint_history_digest_marker(digest: &SessionCheckpointDigest) -> serde_json::Value {
serde_json::json!({
"semantic_checkpoint_history_digest_v1": digest.as_str(),
})
}
pub(crate) fn write_canonical_json(
value: &serde_json::Value,
output: &mut Vec<u8>,
) -> Result<(), serde_json::Error> {
match value {
serde_json::Value::Null => output.extend_from_slice(b"null"),
serde_json::Value::Bool(value) => {
output.extend_from_slice(if *value { b"true" } else { b"false" });
}
serde_json::Value::Number(value) => output.extend_from_slice(value.to_string().as_bytes()),
serde_json::Value::String(value) => {
output.extend_from_slice(serde_json::to_string(value)?.as_bytes());
}
serde_json::Value::Array(values) => {
output.push(b'[');
for (index, value) in values.iter().enumerate() {
if index != 0 {
output.push(b',');
}
write_canonical_json(value, output)?;
}
output.push(b']');
}
serde_json::Value::Object(values) => {
output.push(b'{');
let mut entries = values.iter().collect::<Vec<_>>();
entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
for (index, (key, value)) in entries.into_iter().enumerate() {
if index != 0 {
output.push(b',');
}
output.extend_from_slice(serde_json::to_string(key)?.as_bytes());
output.push(b':');
write_canonical_json(value, output)?;
}
output.push(b'}');
}
}
Ok(())
}
pub fn session_checkpoint_relation(
left: &Session,
right: &Session,
) -> Result<SessionCheckpointRelation, SessionCheckpointError> {
let left = left.try_checkpoint_state()?;
let right = right.try_checkpoint_state()?;
let (left, right) = match (left, right) {
(SessionCheckpointState::Verified(left), SessionCheckpointState::Verified(right)) => {
(left, right)
}
(
SessionCheckpointState::LegacyUnverified { .. },
SessionCheckpointState::LegacyUnverified { .. },
) => return Ok(SessionCheckpointRelation::BothLegacyUnverified),
(SessionCheckpointState::LegacyUnverified { .. }, SessionCheckpointState::Verified(_)) => {
return Ok(SessionCheckpointRelation::LeftLegacyUnverified);
}
(SessionCheckpointState::Verified(_), SessionCheckpointState::LegacyUnverified { .. }) => {
return Ok(SessionCheckpointRelation::RightLegacyUnverified);
}
};
Ok(verified_checkpoint_stamp_relation(&left, &right))
}
#[must_use]
pub fn verified_checkpoint_stamp_relation(
left: &SessionCheckpointStamp,
right: &SessionCheckpointStamp,
) -> SessionCheckpointRelation {
if left.session_id != right.session_id {
return SessionCheckpointRelation::DifferentSessionIdentity;
}
if left.lineage_id != right.lineage_id {
return SessionCheckpointRelation::DifferentLineage;
}
if left.generation < right.generation {
return SessionCheckpointRelation::LeftGenerationOlder;
}
if left.generation > right.generation {
return SessionCheckpointRelation::LeftGenerationNewer;
}
if left.checkpoint_revision < right.checkpoint_revision {
return SessionCheckpointRelation::LeftRevisionOlder;
}
if left.checkpoint_revision > right.checkpoint_revision {
return SessionCheckpointRelation::LeftRevisionNewer;
}
if left == right {
SessionCheckpointRelation::Exact
} else {
SessionCheckpointRelation::RevisionConflict
}
}
pub fn session_checkpoints_are_exact(
left: &Session,
right: &Session,
) -> Result<bool, SessionCheckpointError> {
Ok(session_checkpoint_relation(left, right)? == SessionCheckpointRelation::Exact)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegacySessionTranscriptRelation {
Identical,
ProjectionExtendsSnapshot,
SnapshotExtendsProjection,
Divergent,
}
pub fn legacy_session_transcript_relation(
snapshot: &Session,
projection: &Session,
) -> Result<LegacySessionTranscriptRelation, SessionCheckpointError> {
for (side, session) in [("snapshot", snapshot), ("projection", projection)] {
if !matches!(
session.try_checkpoint_state()?,
SessionCheckpointState::LegacyUnverified { .. }
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(format!(
"legacy transcript relation requires an untyped legacy {side} document"
)));
}
}
transcript_prefix_relation(snapshot, projection)
}
pub fn legacy_snapshot_vs_typed_projection_transcript_relation(
snapshot: &Session,
projection: &Session,
) -> Result<LegacySessionTranscriptRelation, SessionCheckpointError> {
if !matches!(
snapshot.try_checkpoint_state()?,
SessionCheckpointState::LegacyUnverified { .. }
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"legacy-snapshot-vs-typed-projection transcript relation requires an \
untyped legacy snapshot document"
.to_string(),
));
}
if !matches!(
projection.try_checkpoint_state()?,
SessionCheckpointState::Verified(_)
) {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"legacy-snapshot-vs-typed-projection transcript relation requires a \
verified typed projection document"
.to_string(),
));
}
transcript_prefix_relation(snapshot, projection)
}
fn transcript_prefix_relation(
snapshot: &Session,
projection: &Session,
) -> Result<LegacySessionTranscriptRelation, SessionCheckpointError> {
if snapshot.id() != projection.id() {
return Err(SessionCheckpointError::SessionIdMismatch {
expected: snapshot.id().clone(),
actual: projection.id().clone(),
});
}
let snapshot_messages = snapshot.messages();
let projection_messages = projection.messages();
let shared = snapshot_messages.len().min(projection_messages.len());
for (snapshot_message, projection_message) in snapshot_messages
.iter()
.take(shared)
.zip(projection_messages.iter().take(shared))
{
let snapshot_value = serde_json::to_value(snapshot_message)?;
let projection_value = serde_json::to_value(projection_message)?;
if snapshot_value != projection_value {
return Ok(LegacySessionTranscriptRelation::Divergent);
}
}
Ok(
match snapshot_messages.len().cmp(&projection_messages.len()) {
std::cmp::Ordering::Equal => LegacySessionTranscriptRelation::Identical,
std::cmp::Ordering::Less => LegacySessionTranscriptRelation::ProjectionExtendsSnapshot,
std::cmp::Ordering::Greater => {
LegacySessionTranscriptRelation::SnapshotExtendsProjection
}
},
)
}
pub struct AdoptedLegacySession {
pub session: Session,
pub stamp: SessionCheckpointStamp,
pub serialized: Vec<u8>,
}
pub fn adopt_legacy_session(
source_blob: &[u8],
observed_generation: SessionGeneration,
observed_checkpoint_revision: SessionCheckpointRevision,
) -> Result<AdoptedLegacySession, SessionCheckpointError> {
let mut session: Session = serde_json::from_slice(source_blob)?;
let stamp = SessionCheckpointStamp::recovery_migration(
&session,
source_blob,
observed_generation,
observed_checkpoint_revision,
)?;
session.install_checkpoint_stamp(stamp.clone())?;
let serialized = serde_json::to_vec(&session)?;
match session.try_checkpoint_state()? {
SessionCheckpointState::Verified(verified) if verified == stamp => {}
_ => {
return Err(SessionCheckpointError::AuthorityBaseConflict(
"adopted legacy session failed post-install verification".to_string(),
));
}
}
Ok(AdoptedLegacySession {
session,
stamp,
serialized,
})
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionCheckpointer: Send + Sync {
async fn checkpoint(&self, session: &Session);
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::{Message, UserMessage};
fn session_with_text(text: &str) -> Session {
let mut session = Session::new();
session.push(Message::User(UserMessage::text(text.to_string())));
session
}
fn install_stamp(session: &Session, stamp: &SessionCheckpointStamp) -> Session {
let mut document = serde_json::to_value(session).expect("serialize session");
document["metadata"][SESSION_CHECKPOINT_STAMP_KEY] =
serde_json::to_value(stamp).expect("serialize stamp");
serde_json::from_value(document).expect("deserialize stamped session")
}
fn root_stamp(session: &Session) -> SessionCheckpointStamp {
SessionCheckpointStamp::new(
session.id().clone(),
SessionLineageId::for_session(session.id()),
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
SessionCheckpointAuthorityBase::Absent,
session_checkpoint_digest(session).expect("digest"),
SessionCheckpointProvenance::SessionCreated,
)
}
fn stamped_root(session: &Session) -> Session {
let stamp = root_stamp(session);
stamp
.validate_for_session(session.id())
.expect("valid root");
install_stamp(session, &stamp)
}
fn verified_stamp(session: &Session) -> SessionCheckpointStamp {
match session.try_checkpoint_state().expect("checkpoint state") {
SessionCheckpointState::Verified(stamp) => stamp,
SessionCheckpointState::LegacyUnverified { .. } => {
panic!("expected verified checkpoint")
}
}
}
fn successor_stamp(
session: &Session,
prior: &SessionCheckpointStamp,
provenance: SessionCheckpointProvenance,
) -> SessionCheckpointStamp {
SessionCheckpointStamp::new(
session.id().clone(),
prior.lineage_id().clone(),
prior.generation(),
prior
.checkpoint_revision()
.checked_next()
.expect("next revision"),
SessionCheckpointAuthorityBase::Typed {
anchor: SessionCheckpointAnchor::from_stamp(prior),
},
session_checkpoint_digest(session).expect("digest"),
provenance,
)
}
fn advance_checkpoint(
session: &Session,
prior: &SessionCheckpointStamp,
text: &str,
) -> (Session, SessionCheckpointStamp) {
let mut candidate = session.clone();
candidate.push(Message::User(UserMessage::text(text.to_string())));
let stamp = successor_stamp(
&candidate,
prior,
SessionCheckpointProvenance::RunBoundaryCommit,
);
stamp
.validate_for_session(candidate.id())
.expect("valid successor");
(install_stamp(&candidate, &stamp), stamp)
}
#[test]
fn checkpoint_stamp_round_trips_without_ownership_atoms() {
let session = stamped_root(&session_with_text("hello"));
let stamp = verified_stamp(&session);
let encoded = serde_json::to_vec(&session).expect("serialize");
let decoded: Session = serde_json::from_slice(&encoded).expect("deserialize");
assert_eq!(verified_stamp(&decoded), stamp);
assert_eq!(
session_checkpoint_relation(&session, &decoded).expect("relation"),
SessionCheckpointRelation::Exact
);
let encoded_stamp = serde_json::to_string(&stamp).expect("stamp json");
for forbidden in ["epoch", "lease", "fence", "runtime_id", "incarnation"] {
assert!(
!encoded_stamp.contains(forbidden),
"checkpoint content identity must exclude {forbidden}: {encoded_stamp}"
);
}
}
#[test]
fn canonical_digest_is_ordered_and_excludes_only_checkpoint_metadata() {
let left = serde_json::json!({"outer": {"b": 2, "a": 1}});
let right = serde_json::json!({"outer": {"a": 1, "b": 2}});
assert_eq!(
canonical_value_digest(&left).expect("left"),
canonical_value_digest(&right).expect("right")
);
let legacy = session_with_text("digest");
let before = session_checkpoint_digest(&legacy).expect("digest");
let stamped = stamped_root(&legacy);
assert_eq!(session_checkpoint_digest(&stamped).expect("digest"), before);
let mut document = serde_json::to_value(&stamped).expect("serialize");
document["metadata"][SESSION_RUNTIME_CHECKPOINT_PROVENANCE_KEY] =
serde_json::Value::Bool(true);
let with_legacy_fact: Session = serde_json::from_value(document).expect("deserialize");
assert_eq!(
session_checkpoint_digest(&with_legacy_fact).expect("digest"),
before
);
let mut changed = stamped;
changed.set_metadata("caller_fact", serde_json::json!({"b": 2, "a": 1}));
assert_ne!(session_checkpoint_digest(&changed).expect("digest"), before);
}
#[test]
fn malformed_present_stamp_and_legacy_fact_are_errors_not_absence() {
let legacy = session_with_text("legacy");
assert_eq!(
legacy.try_checkpoint_state().expect("legacy state"),
SessionCheckpointState::LegacyUnverified {
legacy_runtime_checkpoint: false
}
);
let mut malformed = serde_json::to_value(&legacy).expect("serialize");
malformed["metadata"][SESSION_CHECKPOINT_STAMP_KEY] =
serde_json::json!({"schema_version": 1});
let malformed: Session = serde_json::from_value(malformed).expect("session envelope");
assert!(matches!(
malformed.try_checkpoint_state(),
Err(SessionCheckpointError::Serialization(_))
));
let mut malformed_legacy = serde_json::to_value(&legacy).expect("serialize");
malformed_legacy["metadata"][SESSION_RUNTIME_CHECKPOINT_PROVENANCE_KEY] =
serde_json::json!("yes");
let malformed_legacy: Session =
serde_json::from_value(malformed_legacy).expect("session envelope");
assert!(matches!(
malformed_legacy.try_checkpoint_state(),
Err(SessionCheckpointError::MalformedLegacyProvenance)
));
let mut mutated = stamped_root(&session_with_text("before"));
mutated.push(Message::User(UserMessage::text("after".to_string())));
assert!(matches!(
mutated.try_checkpoint_state(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
}
#[test]
fn cached_checkpoint_state_verifies_first_sight_then_skips_recomputation() {
let session = stamped_root(&session_with_text("cached read"));
let before = session_content_digest_computations();
assert!(matches!(
session.try_checkpoint_state_cached().expect("first read"),
SessionCheckpointState::Verified(_)
));
let after_first = session_content_digest_computations();
assert!(
after_first > before,
"first sight of a digest key in this process must fully verify"
);
for _ in 0..4 {
assert!(matches!(
session
.try_checkpoint_state_cached()
.expect("memoized read"),
SessionCheckpointState::Verified(_)
));
}
assert_eq!(
session_content_digest_computations(),
after_first,
"steady-state cached reads of an unchanged document must not recompute digests"
);
assert!(matches!(
session.try_checkpoint_state().expect("exact read"),
SessionCheckpointState::Verified(_)
));
assert!(
session_content_digest_computations() > after_first,
"try_checkpoint_state must keep re-verifying content"
);
}
#[test]
fn cached_checkpoint_state_fails_closed_on_unproved_digest_key() {
let session = session_with_text("verify me");
let mut other = session.clone();
other.push(Message::User(UserMessage::text("diverged".to_string())));
let wrong = SessionCheckpointStamp::new(
session.id().clone(),
SessionLineageId::for_session(session.id()),
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
SessionCheckpointAuthorityBase::Absent,
session_checkpoint_digest(&other).expect("digest"),
SessionCheckpointProvenance::SessionCreated,
);
let mismatched = install_stamp(&session, &wrong);
assert!(matches!(
mismatched.try_checkpoint_state_cached(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
}
#[test]
fn cached_checkpoint_state_reverifies_after_in_process_content_mutation() {
let session = stamped_root(&session_with_text("shape keyed"));
assert!(matches!(
session.try_checkpoint_state_cached().expect("seed memo"),
SessionCheckpointState::Verified(_)
));
let mut mutated = session;
mutated.set_metadata("caller_fact", serde_json::json!("drift"));
assert!(matches!(
mutated.try_checkpoint_state_cached(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
}
#[derive(Default)]
struct StubBlobStore {
blobs: std::sync::Mutex<
std::collections::HashMap<crate::blob::BlobId, crate::blob::BlobPayload>,
>,
}
impl StubBlobStore {
fn with_payload(payload: crate::blob::BlobPayload) -> Self {
Self {
blobs: std::sync::Mutex::new(std::collections::HashMap::from([(
payload.blob_id.clone(),
payload,
)])),
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl crate::blob::BlobStore for StubBlobStore {
async fn put_image(
&self,
media_type: &str,
data: &str,
) -> Result<crate::blob::BlobRef, crate::blob::BlobStoreError> {
let blob_id = crate::blob::BlobId::new(format!("sha256:stub-{}", data.len()));
self.blobs.lock().expect("stub blob store lock").insert(
blob_id.clone(),
crate::blob::BlobPayload {
blob_id: blob_id.clone(),
media_type: media_type.to_string(),
data: data.to_string(),
},
);
Ok(crate::blob::BlobRef {
blob_id,
media_type: media_type.to_string(),
})
}
async fn get(
&self,
blob_id: &crate::blob::BlobId,
) -> Result<crate::blob::BlobPayload, crate::blob::BlobStoreError> {
self.blobs
.lock()
.expect("stub blob store lock")
.get(blob_id)
.cloned()
.ok_or_else(|| crate::blob::BlobStoreError::NotFound(blob_id.clone()))
}
async fn delete(
&self,
_blob_id: &crate::blob::BlobId,
) -> Result<(), crate::blob::BlobStoreError> {
Ok(())
}
fn is_persistent(&self) -> bool {
false
}
}
fn stamped_session_with_image(data: crate::types::ImageData) -> Session {
let mut session = Session::new();
session.push(Message::User(crate::types::UserMessage::with_blocks(vec![
crate::types::ContentBlock::Image {
media_type: "image/png".to_string(),
data,
},
])));
stamped_root(&session)
}
#[tokio::test]
async fn cached_checkpoint_state_fails_closed_after_media_externalization() {
let mut session = stamped_session_with_image(crate::types::ImageData::Inline {
data: "iVBORw0KGgo=".to_string(),
});
assert!(matches!(
session.try_checkpoint_state_cached().expect("seed seal"),
SessionCheckpointState::Verified(_)
));
let store = StubBlobStore::default();
session
.externalize_media(&store, 0)
.await
.expect("externalize inline image");
assert!(matches!(
session.try_checkpoint_state_cached(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
}
#[tokio::test]
async fn cached_checkpoint_state_reproves_after_realtime_image_hydration() {
let blob_id = crate::blob::content_blob_id("image/png", "iVBORw0KGgo=");
let store = StubBlobStore::with_payload(crate::blob::BlobPayload {
blob_id: blob_id.clone(),
media_type: "image/png".to_string(),
data: "iVBORw0KGgo=".to_string(),
});
let mut session = stamped_session_with_image(crate::types::ImageData::Blob { blob_id });
assert!(matches!(
session.try_checkpoint_state_cached().expect("seed seal"),
SessionCheckpointState::Verified(_)
));
let sealed_reads = session_content_digest_computations();
assert!(matches!(
session.try_checkpoint_state_cached().expect("sealed read"),
SessionCheckpointState::Verified(_)
));
assert_eq!(
session_content_digest_computations(),
sealed_reads,
"steady-state sealed reads must not recompute digests"
);
session
.hydrate_realtime_user_images_with_usage(&store, 1024 * 1024)
.await
.expect("hydrate blob-backed image");
let before_reproof = session_content_digest_computations();
assert!(matches!(
session.try_checkpoint_state_cached().expect("re-proof"),
SessionCheckpointState::Verified(_)
));
assert!(
session_content_digest_computations() > before_reproof,
"hydration exposed the buffer for in-place mutation, so the \
cached seam must re-prove the stamp, not trust the stale seal"
);
}
#[test]
fn cached_checkpoint_state_fails_closed_after_metadata_backfill() {
let mut session = stamped_root(&session_with_text("seal backfill"));
assert!(matches!(
session.try_checkpoint_state_cached().expect("seed seal"),
SessionCheckpointState::Verified(_)
));
assert!(session.backfill_metadata_if_absent("compat_projection", serde_json::json!("v1")));
assert!(matches!(
session.try_checkpoint_state_cached(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
}
#[test]
#[allow(deprecated)]
fn typed_provenance_is_authoritative_and_legacy_mutators_refuse_it() {
let legacy = session_with_text("legacy provenance");
assert!(matches!(
legacy.try_has_runtime_checkpoint_provenance(),
Err(SessionCheckpointError::LegacyCheckpointUnverified)
));
let root = stamped_root(&session_with_text("typed provenance"));
assert!(
!root
.try_has_runtime_checkpoint_provenance()
.expect("typed root provenance")
);
let root_stamp = verified_stamp(&root);
let mut checkpoint = root;
checkpoint.push(Message::User(UserMessage::text("intra-turn".to_string())));
let checkpoint_stamp = SessionCheckpointStamp::successor(
&checkpoint,
&root_stamp,
SessionCheckpointProvenance::IntraTurnCheckpoint,
)
.expect("intra-turn stamp");
checkpoint
.install_checkpoint_stamp(checkpoint_stamp.clone())
.expect("install intra-turn stamp");
assert!(
checkpoint
.try_has_runtime_checkpoint_provenance()
.expect("typed intra-turn provenance")
);
assert!(matches!(
checkpoint.clear_runtime_checkpoint_provenance(),
Err(SessionCheckpointError::LegacyProvenanceMutationOnTypedCheckpoint)
));
assert!(matches!(
checkpoint.set_runtime_checkpoint_provenance(),
Err(SessionCheckpointError::LegacyProvenanceMutationOnTypedCheckpoint)
));
assert_eq!(verified_stamp(&checkpoint), checkpoint_stamp);
}
#[test]
fn intra_turn_projection_replacement_remains_a_sibling_of_committed_authority() {
let root = stamped_root(&session_with_text("committed"));
let root_stamp = verified_stamp(&root);
let mut first = root.clone();
first.push(Message::User(UserMessage::text(
"first projection".to_string(),
)));
let first_stamp = SessionCheckpointStamp::intra_turn_projection(&first, &root_stamp)
.expect("first projection stamp");
first
.install_checkpoint_stamp(first_stamp.clone())
.expect("install first projection stamp");
let mut replacement = root;
replacement.push(Message::User(UserMessage::text(
"replacement projection".to_string(),
)));
let replacement_stamp =
SessionCheckpointStamp::intra_turn_projection(&replacement, &first_stamp)
.expect("replacement projection stamp");
replacement
.install_checkpoint_stamp(replacement_stamp.clone())
.expect("install replacement projection stamp");
assert_eq!(
first_stamp.checkpoint_revision(),
replacement_stamp.checkpoint_revision()
);
assert_eq!(
first_stamp.authority_base(),
replacement_stamp.authority_base()
);
assert!(matches!(
replacement.try_checkpoint_state(),
Ok(SessionCheckpointState::Verified(stamp)) if stamp == replacement_stamp
));
}
#[test]
fn relation_classifies_lineage_revision_and_conflict() {
let root = stamped_root(&session_with_text("base"));
let root_stamp = verified_stamp(&root);
let mut advanced_document = root.clone();
advanced_document.push(Message::User(UserMessage::text("next".to_string())));
let advanced_stamp = successor_stamp(
&advanced_document,
&root_stamp,
SessionCheckpointProvenance::RunBoundaryCommit,
);
advanced_stamp
.validate_for_session(advanced_document.id())
.expect("valid successor");
let advanced = install_stamp(&advanced_document, &advanced_stamp);
assert_eq!(
session_checkpoint_relation(&root, &advanced).expect("relation"),
SessionCheckpointRelation::LeftRevisionOlder
);
let conflict_stamp = SessionCheckpointStamp::new(
advanced_stamp.session_id().clone(),
advanced_stamp.lineage_id().clone(),
advanced_stamp.generation(),
advanced_stamp.checkpoint_revision(),
advanced_stamp.authority_base().clone(),
advanced_stamp.digest().clone(),
SessionCheckpointProvenance::TranscriptRewrite,
);
conflict_stamp
.validate_for_session(advanced.id())
.expect("valid sibling");
let conflict = install_stamp(&advanced, &conflict_stamp);
assert_eq!(
session_checkpoint_relation(&advanced, &conflict).expect("relation"),
SessionCheckpointRelation::RevisionConflict
);
let different_lineage_stamp = SessionCheckpointStamp::new(
root_stamp.session_id().clone(),
SessionLineageId::new("session:other").expect("lineage"),
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
SessionCheckpointAuthorityBase::Absent,
root_stamp.digest().clone(),
SessionCheckpointProvenance::Forked,
);
let different_lineage = install_stamp(&root, &different_lineage_stamp);
assert_eq!(
session_checkpoint_relation(&root, &different_lineage).expect("relation"),
SessionCheckpointRelation::DifferentLineage
);
}
#[test]
fn ancestry_proof_requires_every_exact_authority_link() {
let root = stamped_root(&session_with_text("r0"));
let r0 = verified_stamp(&root);
let (session_r1, r1) = advance_checkpoint(&root, &r0, "r1");
let (session_r2, r2) = advance_checkpoint(&session_r1, &r1, "r2");
let (_session_r3, r3) = advance_checkpoint(&session_r2, &r2, "r3");
let proof = SessionCheckpointAncestryProof::from_chain(vec![
r0.clone(),
r1.clone(),
r2.clone(),
r3.clone(),
])
.expect("complete exact chain");
assert!(proof.proves(&r0, &r3));
assert_eq!(proof.edge_count(), 3);
assert!(matches!(
SessionCheckpointAncestryProof::from_chain(vec![r0.clone(), r2]),
Err(SessionCheckpointError::AncestryAuthorityBaseMismatch { index: 1 })
));
let mut sibling_document = session_r1;
sibling_document.push(Message::User(UserMessage::text("sibling-r2".to_string())));
let sibling_r2 = successor_stamp(
&sibling_document,
&r1,
SessionCheckpointProvenance::TranscriptRewrite,
);
sibling_r2
.validate_for_session(sibling_document.id())
.expect("valid sibling");
assert!(matches!(
SessionCheckpointAncestryProof::from_chain(vec![r0, r1, sibling_r2, r3]),
Err(SessionCheckpointError::AncestryAuthorityBaseMismatch { index: 3 })
));
}
#[test]
fn ancestry_proof_streams_more_than_1024_exact_links() {
let session = stamped_root(&session_with_text("long ancestry"));
let root = verified_stamp(&session);
let chain = std::iter::successors(Some(root.clone()), |prior| {
Some(
SessionCheckpointStamp::successor(
&session,
prior,
SessionCheckpointProvenance::RunBoundaryCommit,
)
.expect("exact successor"),
)
})
.take(1_501);
let proof =
SessionCheckpointAncestryProof::try_from_stamps(chain).expect("streaming proof");
assert_eq!(proof.ancestor(), &root);
assert_eq!(proof.edge_count(), 1_500);
assert_eq!(
proof.descendant().checkpoint_revision().get(),
root.checkpoint_revision().get() + 1_500
);
assert!(proof.path_digest().as_str().starts_with("sha256:"));
}
#[test]
fn metadata_only_decode_validates_identity_without_claiming_digest_verification() {
let session = stamped_root(&session_with_text("metadata"));
let encoded = serde_json::to_vec(&session).expect("serialize");
let metadata = crate::session_metadata_document_from_slice(&encoded).expect("metadata");
assert_eq!(
metadata
.try_checkpoint_metadata_state()
.expect("metadata checkpoint"),
SessionCheckpointMetadataState::Stamped(verified_stamp(&session))
);
let mut document = serde_json::to_value(&session).expect("serialize");
document["metadata"][SESSION_CHECKPOINT_STAMP_KEY]["session_id"] =
serde_json::to_value(SessionId::new()).expect("session id");
let encoded = serde_json::to_vec(&document).expect("encode document");
let metadata = crate::session_metadata_document_from_slice(&encoded).expect("metadata");
assert!(matches!(
metadata.try_checkpoint_metadata_state(),
Err(SessionCheckpointError::SessionIdMismatch { .. })
));
}
#[test]
fn checked_revision_never_wraps() {
assert!(
SessionCheckpointRevision::new(u64::MAX)
.checked_next()
.is_err()
);
}
#[test]
fn coherent_nonzero_legacy_cursor_migrates_and_missing_stays_unverified() {
let legacy = session_with_text("legacy nonzero");
let source_blob = serde_json::to_vec(&legacy).expect("legacy source BLOB");
let stamp = SessionCheckpointStamp::recovery_migration(
&legacy,
&source_blob,
SessionGeneration::new(3),
SessionCheckpointRevision::new(17),
)
.expect("coherent nonzero migration");
assert_eq!(stamp.generation(), SessionGeneration::new(3));
assert_eq!(
stamp.checkpoint_revision(),
SessionCheckpointRevision::new(17)
);
assert!(matches!(
stamp.authority_base(),
SessionCheckpointAuthorityBase::Legacy {
observed_generation,
observed_checkpoint_revision,
..
} if *observed_generation == SessionGeneration::new(3)
&& *observed_checkpoint_revision == SessionCheckpointRevision::new(17)
));
let mut migrated = legacy;
migrated
.install_checkpoint_stamp(stamp.clone())
.expect("install migration");
assert_eq!(
migrated.try_checkpoint_state().expect("verified migration"),
SessionCheckpointState::Verified(stamp)
);
let missing = Session::new();
assert_eq!(
missing.try_checkpoint_state().expect("missing state"),
SessionCheckpointState::LegacyUnverified {
legacy_runtime_checkpoint: false,
}
);
}
#[test]
fn legacy_migration_custody_distinguishes_byte_different_equal_documents() {
let legacy = session_with_text("legacy custody");
let compact = serde_json::to_vec(&legacy).expect("compact legacy source");
let pretty = serde_json::to_vec_pretty(&legacy).expect("pretty legacy source");
assert_ne!(compact, pretty);
let compact_stamp = SessionCheckpointStamp::recovery_migration(
&legacy,
&compact,
SessionGeneration::new(4),
SessionCheckpointRevision::new(19),
)
.expect("compact migration");
let pretty_stamp = SessionCheckpointStamp::recovery_migration(
&legacy,
&pretty,
SessionGeneration::new(4),
SessionCheckpointRevision::new(19),
)
.expect("pretty migration");
assert_eq!(compact_stamp.digest(), pretty_stamp.digest());
let SessionCheckpointAuthorityBase::Legacy {
source_blob_digest: compact_source_digest,
..
} = compact_stamp.authority_base()
else {
panic!("expected compact legacy authority base");
};
let SessionCheckpointAuthorityBase::Legacy {
source_blob_digest: pretty_source_digest,
..
} = pretty_stamp.authority_base()
else {
panic!("expected pretty legacy authority base");
};
assert_ne!(compact_source_digest, pretty_source_digest);
assert_eq!(
compact_source_digest,
&legacy_session_source_blob_digest(&compact)
);
assert_eq!(
pretty_source_digest,
&legacy_session_source_blob_digest(&pretty)
);
}
#[test]
fn production_stamp_constructors_require_exact_successors_and_refresh_after_mutation() {
let mut session = session_with_text("root");
let root =
SessionCheckpointStamp::root(&session, SessionCheckpointProvenance::SessionCreated)
.expect("root");
session
.install_checkpoint_stamp(root.clone())
.expect("install root");
assert_eq!(verified_stamp(&session), root);
session.push(Message::User(UserMessage::text("next".to_string())));
assert!(matches!(
session.try_checkpoint_state(),
Err(SessionCheckpointError::DigestMismatch { .. })
));
let successor = SessionCheckpointStamp::successor(
&session,
&root,
SessionCheckpointProvenance::RunBoundaryCommit,
)
.expect("successor");
assert_eq!(
successor.checkpoint_revision(),
root.checkpoint_revision().checked_next().expect("next")
);
session
.install_checkpoint_stamp(successor.clone())
.expect("install successor");
assert_eq!(verified_stamp(&session), successor);
let gap = SessionCheckpointStamp::new(
successor.session_id().clone(),
successor.lineage_id().clone(),
successor.generation(),
SessionCheckpointRevision::new(successor.checkpoint_revision().get() + 2),
SessionCheckpointAuthorityBase::Typed {
anchor: SessionCheckpointAnchor::from_stamp(&successor),
},
successor.digest().clone(),
SessionCheckpointProvenance::RunBoundaryCommit,
);
assert!(matches!(
gap.validate_for_session(session.id()),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
}
#[test]
fn checkpoint_digest_erases_transcript_construction_timestamps() {
let session = session_with_text("same semantic message");
let mut reconstructed = session.clone();
let messages = reconstructed.messages.mutate_in_place();
let Some(Message::User(user)) = messages.first_mut() else {
panic!("expected user message");
};
user.created_at = chrono::DateTime::<chrono::Utc>::UNIX_EPOCH;
user.identity.run_id = Some(crate::RunId::new());
assert_eq!(
session_checkpoint_digest(&session).expect("original digest"),
session_checkpoint_digest(&reconstructed).expect("reconstructed digest")
);
}
#[test]
fn legacy_transcript_relation_classifies_prefix_extension_and_divergence() {
let snapshot = session_with_text("turn one");
let identical = snapshot.clone();
assert_eq!(
legacy_session_transcript_relation(&snapshot, &identical).expect("identical relation"),
LegacySessionTranscriptRelation::Identical
);
let mut extended = snapshot.clone();
extended.push(Message::User(UserMessage::text("turn two".to_string())));
assert_eq!(
legacy_session_transcript_relation(&snapshot, &extended).expect("extension relation"),
LegacySessionTranscriptRelation::ProjectionExtendsSnapshot
);
assert_eq!(
legacy_session_transcript_relation(&extended, &snapshot)
.expect("stale projection relation"),
LegacySessionTranscriptRelation::SnapshotExtendsProjection
);
let mut divergent = snapshot.clone();
divergent.messages.mutate_in_place().clear();
divergent.push(Message::User(UserMessage::text(
"a different turn one".to_string(),
)));
assert_eq!(
legacy_session_transcript_relation(&snapshot, &divergent).expect("divergent relation"),
LegacySessionTranscriptRelation::Divergent
);
}
#[test]
fn legacy_snapshot_vs_typed_projection_relation_classifies_prefix_extension_and_divergence() {
let snapshot = session_with_text("turn one");
let identical = stamped_root(&snapshot);
assert_eq!(
legacy_snapshot_vs_typed_projection_transcript_relation(&snapshot, &identical)
.expect("identical relation"),
LegacySessionTranscriptRelation::Identical
);
let mut extended = snapshot.clone();
extended.push(Message::User(UserMessage::text("turn two".to_string())));
let typed_extended = stamped_root(&extended);
assert_eq!(
legacy_snapshot_vs_typed_projection_transcript_relation(&snapshot, &typed_extended)
.expect("extension relation"),
LegacySessionTranscriptRelation::ProjectionExtendsSnapshot
);
let typed_prefix = stamped_root(&snapshot);
assert_eq!(
legacy_snapshot_vs_typed_projection_transcript_relation(&extended, &typed_prefix)
.expect("stale typed projection relation"),
LegacySessionTranscriptRelation::SnapshotExtendsProjection
);
let mut divergent = snapshot.clone();
divergent.messages.mutate_in_place().clear();
divergent.push(Message::User(UserMessage::text(
"a different turn one".to_string(),
)));
let typed_divergent = stamped_root(&divergent);
assert_eq!(
legacy_snapshot_vs_typed_projection_transcript_relation(&snapshot, &typed_divergent)
.expect("divergent relation"),
LegacySessionTranscriptRelation::Divergent
);
}
#[test]
fn legacy_snapshot_vs_typed_projection_relation_refuses_wrong_checkpoint_states() {
let legacy = session_with_text("legacy copy");
let typed = stamped_root(&legacy);
assert!(matches!(
legacy_snapshot_vs_typed_projection_transcript_relation(&typed, &typed),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
assert!(matches!(
legacy_snapshot_vs_typed_projection_transcript_relation(&legacy, &legacy),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
let foreign = stamped_root(&session_with_text("legacy copy"));
assert!(matches!(
legacy_snapshot_vs_typed_projection_transcript_relation(&legacy, &foreign),
Err(SessionCheckpointError::SessionIdMismatch { .. })
));
}
#[test]
fn adopt_legacy_session_stamps_blob_and_refuses_typed_documents() {
let legacy = session_with_text("legacy blob");
let blob = serde_json::to_vec(&legacy).expect("legacy session should serialize");
let adopted = adopt_legacy_session(
&blob,
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
)
.expect("legacy blob should adopt");
assert_eq!(
adopted.stamp.provenance(),
SessionCheckpointProvenance::RecoveryMigration
);
assert_eq!(adopted.stamp.generation(), SessionGeneration::INITIAL);
let reloaded: Session =
serde_json::from_slice(&adopted.serialized).expect("adopted bytes should decode");
assert!(matches!(
reloaded
.try_checkpoint_state()
.expect("adopted checkpoint state should decode"),
SessionCheckpointState::Verified(stamp) if stamp == adopted.stamp
));
let typed_blob =
serde_json::to_vec(&stamped_root(&legacy)).expect("typed session should serialize");
assert!(matches!(
adopt_legacy_session(
&typed_blob,
SessionGeneration::INITIAL,
SessionCheckpointRevision::INITIAL,
),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
}
#[test]
fn legacy_transcript_relation_refuses_typed_documents_and_foreign_sessions() {
let legacy = session_with_text("legacy copy");
let typed = stamped_root(&session_with_text("typed copy"));
assert!(matches!(
legacy_session_transcript_relation(&typed, &legacy),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
assert!(matches!(
legacy_session_transcript_relation(&legacy, &typed),
Err(SessionCheckpointError::AuthorityBaseConflict(_))
));
let foreign = session_with_text("legacy copy");
assert!(matches!(
legacy_session_transcript_relation(&legacy, &foreign),
Err(SessionCheckpointError::SessionIdMismatch { .. })
));
}
}