1use 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
17pub 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
32pub fn binary_encode<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, bincode::Error> {
42 bincode::serialize(value)
43}
44
45pub fn binary_decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, bincode::Error> {
55 bincode::deserialize(bytes)
56}
57
58#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct CanonicalTraceV1 {
90 #[serde(
92 default = "default_serialization_schema_version",
93 deserialize_with = "deserialize_serialization_schema_version"
94 )]
95 pub schema_version: String,
96 pub events: Vec<ObsEvent>,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct CanonicalReplayFragmentV1 {
103 #[serde(
105 default = "default_serialization_schema_version",
106 deserialize_with = "deserialize_serialization_schema_version"
107 )]
108 pub schema_version: String,
109 pub obs_trace: Vec<ObsEvent>,
111 pub effect_trace: Vec<EffectTraceEntry>,
113 pub crashed_sites: Vec<String>,
115 pub partitioned_edges: Vec<(String, String)>,
117 pub corrupted_edges: Vec<((String, String), CorruptionType)>,
119 pub timed_out_sites: Vec<(String, u64)>,
121 #[serde(default)]
123 pub effect_determinism_tier: EffectDeterminismTier,
124 #[serde(default)]
126 pub communication_replay_mode: CommunicationReplayMode,
127 #[serde(default)]
129 pub communication_replay_root: Option<Hash>,
130 #[serde(default)]
132 pub communication_consumption_artifacts: Vec<CommunicationConsumptionArtifact>,
133 #[serde(default)]
135 pub semantic_audit_log: Vec<SemanticAuditRecord>,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum SemanticAuditRecord {
142 Authority {
144 tick: Option<u64>,
146 session: Option<SessionId>,
148 artifact: AuthorityArtifact,
150 event: AuthorityAuditEvent,
152 reason: Option<String>,
154 },
155 Delegation {
157 tick: u64,
159 session: SessionId,
161 receipt: DelegationReceipt,
163 status: DelegationStatus,
165 reason: Option<String>,
167 },
168 FailureBranch {
170 tick: u64,
172 session: SessionId,
174 coro_id: usize,
176 fault: crate::coroutine::Fault,
178 },
179 TimeoutIssued {
181 tick: u64,
183 site: String,
185 until_tick: u64,
187 witness_id: AuthorityWitnessId,
189 },
190 CancellationRequested {
192 tick: u64,
194 session: SessionId,
196 witness_id: AuthorityWitnessId,
198 owner_id: FragmentOwnerId,
200 reason: OwnershipTerminalReason,
202 },
203 Cancelled {
205 tick: u64,
207 session: SessionId,
209 witness_id: AuthorityWitnessId,
211 reason: OwnershipTerminalReason,
213 },
214 SessionTerminal {
216 tick: u64,
218 session: SessionId,
220 reason: SessionTerminalReason,
222 },
223 EffectObservation {
225 effect_id: u64,
227 ordering_key: u64,
229 session: Option<SessionId>,
231 effect_kind: String,
233 effect_interface: Option<String>,
235 effect_operation: Option<String>,
237 handler_identity: String,
239 inputs: JsonValue,
241 outputs: JsonValue,
243 },
244}
245
246#[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#[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#[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#[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#[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}