Skip to main content

telltale_vm/
serialization.rs

1//! Canonical serialization helpers for deterministic replay/testing artifacts.
2
3use crate::communication_replay::{CommunicationConsumptionArtifact, CommunicationReplayMode};
4use crate::determinism::EffectDeterminismTier;
5use crate::effect::{CorruptionType, EffectTraceEntry};
6use crate::session::{
7    AuthorityArtifact, AuthorityAuditEvent, AuthorityAuditRecord, AuthorityWitnessId,
8    FragmentOwnerId, OwnershipTerminalReason, SessionId,
9};
10use crate::trace::normalize_trace;
11use crate::transfer_semantics::{DelegationAuditRecord, DelegationReceipt, DelegationStatus};
12use crate::verification::Hash;
13use crate::vm::{ObsEvent, SessionTerminalReason};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use serde_json::Value as JsonValue;
16
17/// Canonical schema version identifier for VM replay/trace payloads.
18pub const SERIALIZATION_SCHEMA_VERSION: &str = "vm.serialization.v1";
19
20fn default_serialization_schema_version() -> String {
21    SERIALIZATION_SCHEMA_VERSION.to_string()
22}
23
24fn normalize_serialization_schema_version(raw: &str) -> String {
25    if raw == "1" {
26        SERIALIZATION_SCHEMA_VERSION.to_string()
27    } else {
28        raw.to_string()
29    }
30}
31
32/// Serialize one value through the canonical VM binary codec.
33///
34/// This wrapper keeps binary-serialization policy centralized inside the VM
35/// crate instead of scattering direct `bincode` calls through runtime code.
36///
37/// # Errors
38///
39/// Returns a `bincode::Error` if the value cannot be serialized by the
40/// canonical binary codec.
41pub fn binary_encode<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, bincode::Error> {
42    bincode::serialize(value)
43}
44
45/// Deserialize one value through the canonical VM binary codec.
46///
47/// This wrapper keeps binary-serialization policy centralized inside the VM
48/// crate instead of scattering direct `bincode` calls through runtime code.
49///
50/// # Errors
51///
52/// Returns a `bincode::Error` if the bytes do not decode as the requested type
53/// under the canonical binary codec.
54pub fn binary_decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, bincode::Error> {
55    bincode::deserialize(bytes)
56}
57
58/// Return the binary-encoded size for one value, saturating to `usize`.
59#[must_use]
60pub fn binary_size<T: Serialize + ?Sized>(value: &T) -> usize {
61    bincode::serialized_size(value)
62        .ok()
63        .and_then(|bytes| usize::try_from(bytes).ok())
64        .unwrap_or(0)
65}
66
67fn deserialize_serialization_schema_version<'de, D>(deserializer: D) -> Result<String, D::Error>
68where
69    D: serde::Deserializer<'de>,
70{
71    #[derive(Deserialize)]
72    #[serde(untagged)]
73    enum SchemaVersionValue {
74        String(String),
75        Integer(u64),
76    }
77
78    let parsed = SchemaVersionValue::deserialize(deserializer)?;
79    Ok(match parsed {
80        SchemaVersionValue::String(version) => normalize_serialization_schema_version(&version),
81        SchemaVersionValue::Integer(version) => {
82            normalize_serialization_schema_version(&version.to_string())
83        }
84    })
85}
86
87/// Versioned canonical trace payload used for cross-target normalization.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct CanonicalTraceV1 {
90    /// Schema version for canonical trace serialization.
91    #[serde(
92        default = "default_serialization_schema_version",
93        deserialize_with = "deserialize_serialization_schema_version"
94    )]
95    pub schema_version: String,
96    /// Canonically normalized observable events.
97    pub events: Vec<ObsEvent>,
98}
99
100/// Versioned canonical replay-state fragment used by tests and replay checks.
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct CanonicalReplayFragmentV1 {
103    /// Schema version for canonical replay serialization.
104    #[serde(
105        default = "default_serialization_schema_version",
106        deserialize_with = "deserialize_serialization_schema_version"
107    )]
108    pub schema_version: String,
109    /// Canonically normalized observable trace.
110    pub obs_trace: Vec<ObsEvent>,
111    /// Canonically sorted effect trace.
112    pub effect_trace: Vec<EffectTraceEntry>,
113    /// Sorted crashed sites.
114    pub crashed_sites: Vec<String>,
115    /// Sorted directed partition edges.
116    pub partitioned_edges: Vec<(String, String)>,
117    /// Sorted directed corruption edges with policies.
118    pub corrupted_edges: Vec<((String, String), CorruptionType)>,
119    /// Sorted timeout horizons keyed by site.
120    pub timed_out_sites: Vec<(String, u64)>,
121    /// Declared effect determinism tier for this run.
122    #[serde(default)]
123    pub effect_determinism_tier: EffectDeterminismTier,
124    /// Active communication replay mode.
125    #[serde(default)]
126    pub communication_replay_mode: CommunicationReplayMode,
127    /// Deterministic communication replay-state root.
128    #[serde(default)]
129    pub communication_replay_root: Option<Hash>,
130    /// Proof-friendly receive consumption artifacts.
131    #[serde(default)]
132    pub communication_consumption_artifacts: Vec<CommunicationConsumptionArtifact>,
133    /// Canonical semantic audit records derived from authority/failure/effect surfaces.
134    #[serde(default)]
135    pub semantic_audit_log: Vec<SemanticAuditRecord>,
136}
137
138/// Replay-stable semantic record derived from authority, delegation, effect, and
139/// failure-visible runtime artifacts.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum SemanticAuditRecord {
142    /// Authority witness issuance/consumption/rejection.
143    Authority {
144        /// Scheduler tick associated with the authority artifact, when present.
145        tick: Option<u64>,
146        /// Session referenced by the authority artifact, when session-scoped.
147        session: Option<SessionId>,
148        /// Authority witness or receipt artifact carried by the audit record.
149        artifact: AuthorityArtifact,
150        /// Audit event kind recorded for the authority artifact.
151        event: AuthorityAuditEvent,
152        /// Optional rejection or failure reason associated with the audit record.
153        reason: Option<String>,
154    },
155    /// Delegation/transfer completion or rollback.
156    Delegation {
157        /// Scheduler tick at which the delegation audit record was emitted.
158        tick: u64,
159        /// Session being delegated.
160        session: SessionId,
161        /// Delegation receipt proving the sanctioned transfer path.
162        receipt: DelegationReceipt,
163        /// Final delegation status for the receipt.
164        status: DelegationStatus,
165        /// Optional rollback or rejection reason for the transfer.
166        reason: Option<String>,
167    },
168    /// Explicit typed failure branch entry.
169    FailureBranch {
170        /// Scheduler tick at which the failure branch became visible.
171        tick: u64,
172        /// Session containing the failing coroutine.
173        session: SessionId,
174        /// Coroutine entering the failure branch.
175        coro_id: usize,
176        /// Typed fault surfaced by the branch.
177        fault: crate::coroutine::Fault,
178    },
179    /// Explicit timeout activation and timeout witness issuance.
180    TimeoutIssued {
181        /// Scheduler tick at which the timeout became active.
182        tick: u64,
183        /// Site for which timeout was issued.
184        site: String,
185        /// Tick horizon until which the timeout remains active.
186        until_tick: u64,
187        /// Issued timeout witness identifier.
188        witness_id: AuthorityWitnessId,
189    },
190    /// Explicit cancellation request.
191    CancellationRequested {
192        /// Scheduler tick at which cancellation was requested.
193        tick: u64,
194        /// Session being cancelled.
195        session: SessionId,
196        /// Cancellation witness authorizing the request.
197        witness_id: AuthorityWitnessId,
198        /// Owner capability active when cancellation was requested.
199        owner_id: FragmentOwnerId,
200        /// Terminal ownership reason causing the cancellation request.
201        reason: OwnershipTerminalReason,
202    },
203    /// Explicit cancellation completion.
204    Cancelled {
205        /// Scheduler tick at which cancellation completed.
206        tick: u64,
207        /// Session that was cancelled.
208        session: SessionId,
209        /// Cancellation witness consumed by completion.
210        witness_id: AuthorityWitnessId,
211        /// Terminal ownership reason recorded for the cancellation.
212        reason: OwnershipTerminalReason,
213    },
214    /// Explicit session terminal reason.
215    SessionTerminal {
216        /// Scheduler tick at which terminal state became visible.
217        tick: u64,
218        /// Session that reached terminal state.
219        session: SessionId,
220        /// Deterministic terminal reason recorded by the runtime.
221        reason: SessionTerminalReason,
222    },
223    /// Structured effect/interface observation.
224    EffectObservation {
225        /// Stable effect identifier assigned by the runtime.
226        effect_id: u64,
227        /// Deterministic ordering key used for canonical replay comparison.
228        ordering_key: u64,
229        /// Session referenced by the effect observation, when derivable.
230        session: Option<SessionId>,
231        /// Raw runtime effect kind tag.
232        effect_kind: String,
233        /// Nominal effect interface classification, when known.
234        effect_interface: Option<String>,
235        /// Nominal effect operation classification, when known.
236        effect_operation: Option<String>,
237        /// Stable handler identity attached to the observation.
238        handler_identity: String,
239        /// Serialized effect inputs.
240        inputs: JsonValue,
241        /// Serialized effect outputs.
242        outputs: JsonValue,
243    },
244}
245
246/// Normalize an observable trace into the canonical versioned format.
247#[must_use]
248pub fn canonical_trace_v1(trace: &[ObsEvent]) -> CanonicalTraceV1 {
249    CanonicalTraceV1 {
250        schema_version: default_serialization_schema_version(),
251        events: normalize_trace(trace),
252    }
253}
254
255/// Canonicalize effect-trace ordering for deterministic replay diffs.
256#[must_use]
257pub fn canonical_effect_trace(trace: &[EffectTraceEntry]) -> Vec<EffectTraceEntry> {
258    let mut out = trace.to_vec();
259    out.sort_by(|lhs, rhs| {
260        (lhs.ordering_key, lhs.effect_id, &lhs.effect_kind).cmp(&(
261            rhs.ordering_key,
262            rhs.effect_id,
263            &rhs.effect_kind,
264        ))
265    });
266    out
267}
268
269fn authority_artifact_session(artifact: &AuthorityArtifact) -> Option<SessionId> {
270    match artifact {
271        AuthorityArtifact::Readiness(witness) => Some(witness.session_id),
272        AuthorityArtifact::Cancellation(witness) => Some(witness.session_id),
273        AuthorityArtifact::Timeout(_) => None,
274    }
275}
276
277fn effect_entry_session(entry: &EffectTraceEntry) -> Option<SessionId> {
278    entry
279        .inputs
280        .get("session")
281        .and_then(JsonValue::as_u64)
282        .and_then(|sid| usize::try_from(sid).ok())
283        .or_else(|| {
284            entry
285                .inputs
286                .get("sid")
287                .and_then(JsonValue::as_u64)
288                .and_then(|sid| usize::try_from(sid).ok())
289        })
290}
291
292fn semantic_rank(record: &SemanticAuditRecord) -> u8 {
293    match record {
294        SemanticAuditRecord::Authority { .. } => 0,
295        SemanticAuditRecord::Delegation { .. } => 1,
296        SemanticAuditRecord::FailureBranch { .. } => 2,
297        SemanticAuditRecord::TimeoutIssued { .. } => 3,
298        SemanticAuditRecord::CancellationRequested { .. } => 4,
299        SemanticAuditRecord::Cancelled { .. } => 5,
300        SemanticAuditRecord::SessionTerminal { .. } => 6,
301        SemanticAuditRecord::EffectObservation { .. } => 7,
302    }
303}
304
305fn semantic_tick(record: &SemanticAuditRecord) -> u64 {
306    match record {
307        SemanticAuditRecord::Authority { tick, .. } => tick.unwrap_or(0),
308        SemanticAuditRecord::Delegation { tick, .. }
309        | SemanticAuditRecord::FailureBranch { tick, .. }
310        | SemanticAuditRecord::TimeoutIssued { tick, .. }
311        | SemanticAuditRecord::CancellationRequested { tick, .. }
312        | SemanticAuditRecord::Cancelled { tick, .. }
313        | SemanticAuditRecord::SessionTerminal { tick, .. } => *tick,
314        SemanticAuditRecord::EffectObservation { ordering_key, .. } => *ordering_key,
315    }
316}
317
318/// Canonicalize semantic audit ordering for deterministic replay diffs.
319#[must_use]
320pub fn canonical_semantic_audit_log(records: &[SemanticAuditRecord]) -> Vec<SemanticAuditRecord> {
321    let mut out = records.to_vec();
322    out.sort_by(|lhs, rhs| {
323        let lhs_key = (
324            semantic_tick(lhs),
325            semantic_rank(lhs),
326            serde_json::to_string(lhs).unwrap_or_default(),
327        );
328        let rhs_key = (
329            semantic_tick(rhs),
330            semantic_rank(rhs),
331            serde_json::to_string(rhs).unwrap_or_default(),
332        );
333        lhs_key.cmp(&rhs_key)
334    });
335    out
336}
337
338/// Build canonical semantic audit records from authority, delegation,
339/// failure-visible observable events, and effect/interface observations.
340#[must_use]
341pub fn semantic_audit_log_v1(
342    authority_audit_log: &[AuthorityAuditRecord],
343    delegation_audit_log: &[DelegationAuditRecord],
344    obs_trace: &[ObsEvent],
345    effect_trace: &[EffectTraceEntry],
346) -> Vec<SemanticAuditRecord> {
347    let mut records = Vec::new();
348
349    records.extend(authority_audit_log.iter().cloned().map(|record| {
350        SemanticAuditRecord::Authority {
351            tick: record.tick,
352            session: authority_artifact_session(&record.artifact),
353            artifact: record.artifact,
354            event: record.event,
355            reason: record.reason,
356        }
357    }));
358
359    records.extend(delegation_audit_log.iter().cloned().map(|record| {
360        SemanticAuditRecord::Delegation {
361            tick: record.tick,
362            session: record.receipt.session,
363            receipt: record.receipt,
364            status: record.status,
365            reason: record.reason,
366        }
367    }));
368
369    records.extend(obs_trace.iter().filter_map(|event| match event {
370        ObsEvent::FailureBranchEntered {
371            tick,
372            session,
373            coro_id,
374            fault,
375        } => Some(SemanticAuditRecord::FailureBranch {
376            tick: *tick,
377            session: *session,
378            coro_id: *coro_id,
379            fault: fault.clone(),
380        }),
381        ObsEvent::TimeoutIssued {
382            tick,
383            site,
384            until_tick,
385            witness_id,
386        } => Some(SemanticAuditRecord::TimeoutIssued {
387            tick: *tick,
388            site: site.clone(),
389            until_tick: *until_tick,
390            witness_id: *witness_id,
391        }),
392        ObsEvent::CancellationRequested {
393            tick,
394            session,
395            witness_id,
396            owner_id,
397            reason,
398        } => Some(SemanticAuditRecord::CancellationRequested {
399            tick: *tick,
400            session: *session,
401            witness_id: *witness_id,
402            owner_id: owner_id.clone(),
403            reason: reason.clone(),
404        }),
405        ObsEvent::Cancelled {
406            tick,
407            session,
408            witness_id,
409            reason,
410        } => Some(SemanticAuditRecord::Cancelled {
411            tick: *tick,
412            session: *session,
413            witness_id: *witness_id,
414            reason: reason.clone(),
415        }),
416        ObsEvent::SessionTerminal {
417            tick,
418            session,
419            reason,
420        } => Some(SemanticAuditRecord::SessionTerminal {
421            tick: *tick,
422            session: *session,
423            reason: reason.clone(),
424        }),
425        _ => None,
426    }));
427
428    records.extend(effect_trace.iter().cloned().map(|entry| {
429        SemanticAuditRecord::EffectObservation {
430            effect_id: entry.effect_id,
431            ordering_key: entry.ordering_key,
432            session: effect_entry_session(&entry),
433            effect_kind: entry.effect_kind,
434            effect_interface: entry.effect_interface,
435            effect_operation: entry.effect_operation,
436            handler_identity: entry.handler_identity,
437            inputs: entry.inputs,
438            outputs: entry.outputs,
439        }
440    }));
441
442    canonical_semantic_audit_log(&records)
443}
444
445/// Build a canonical replay-state fragment from runtime snapshots.
446#[must_use]
447#[allow(clippy::too_many_arguments)]
448pub fn canonical_replay_fragment_v1(
449    obs_trace: &[ObsEvent],
450    effect_trace: &[EffectTraceEntry],
451    authority_audit_log: &[AuthorityAuditRecord],
452    delegation_audit_log: &[DelegationAuditRecord],
453    mut crashed_sites: Vec<String>,
454    mut partitioned_edges: Vec<(String, String)>,
455    mut corrupted_edges: Vec<((String, String), CorruptionType)>,
456    mut timed_out_sites: Vec<(String, u64)>,
457    effect_determinism_tier: EffectDeterminismTier,
458    communication_replay_mode: CommunicationReplayMode,
459    communication_replay_root: Option<Hash>,
460    communication_consumption_artifacts: Vec<CommunicationConsumptionArtifact>,
461) -> CanonicalReplayFragmentV1 {
462    crashed_sites.sort_unstable();
463    crashed_sites.dedup();
464
465    partitioned_edges.sort_unstable();
466    partitioned_edges.dedup();
467
468    corrupted_edges.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
469    corrupted_edges.dedup();
470
471    timed_out_sites.sort_unstable();
472
473    CanonicalReplayFragmentV1 {
474        schema_version: default_serialization_schema_version(),
475        obs_trace: canonical_trace_v1(obs_trace).events,
476        effect_trace: canonical_effect_trace(effect_trace),
477        crashed_sites,
478        partitioned_edges,
479        corrupted_edges,
480        timed_out_sites,
481        effect_determinism_tier,
482        communication_replay_mode,
483        communication_replay_root,
484        communication_consumption_artifacts,
485        semantic_audit_log: semantic_audit_log_v1(
486            authority_audit_log,
487            delegation_audit_log,
488            obs_trace,
489            effect_trace,
490        ),
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::session::Edge;
498
499    #[test]
500    fn canonical_effect_trace_is_stably_sorted() {
501        let trace = vec![
502            EffectTraceEntry {
503                effect_id: 2,
504                effect_kind: "b".to_string(),
505                inputs: serde_json::json!({}),
506                outputs: serde_json::json!({}),
507                handler_identity: "h".to_string(),
508                effect_interface: None,
509                effect_operation: None,
510                ordering_key: 3,
511                topology: None,
512            },
513            EffectTraceEntry {
514                effect_id: 1,
515                effect_kind: "a".to_string(),
516                inputs: serde_json::json!({}),
517                outputs: serde_json::json!({}),
518                handler_identity: "h".to_string(),
519                effect_interface: None,
520                effect_operation: None,
521                ordering_key: 2,
522                topology: None,
523            },
524        ];
525
526        let sorted = canonical_effect_trace(&trace);
527        assert_eq!(sorted[0].effect_id, 1);
528        assert_eq!(sorted[1].effect_id, 2);
529    }
530
531    #[test]
532    fn canonical_trace_payload_has_version() {
533        let trace = vec![ObsEvent::Sent {
534            tick: 1,
535            edge: Edge::new(1, "A", "B"),
536            session: 1,
537            from: "A".to_string(),
538            to: "B".to_string(),
539            label: "m".to_string(),
540        }];
541        let payload = canonical_trace_v1(&trace);
542        assert_eq!(payload.schema_version, SERIALIZATION_SCHEMA_VERSION);
543        assert_eq!(payload.events.len(), 1);
544    }
545
546    #[test]
547    fn legacy_numeric_schema_version_deserializes_to_string_identifier() {
548        let payload = serde_json::json!({
549            "schema_version": 1,
550            "events": []
551        });
552        let decoded: CanonicalTraceV1 =
553            serde_json::from_value(payload).expect("legacy schema version should deserialize");
554        assert_eq!(decoded.schema_version, SERIALIZATION_SCHEMA_VERSION);
555    }
556}