1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use crate::{
9 validate_production_chunk_graph, OptimizationPolicyId, ProductionChunkGraph, ProductionChunkId,
10 ProductionChunkKind, ResumeBuildId, ResumeManifest, RuntimeTableId,
11 RESUME_RUNTIME_PROTOCOL_VERSION,
12};
13
14pub const PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION: u32 = 1;
15pub const PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION: u32 = 1;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum ProductionOrdinalWidth {
20 U8,
21 U16,
22 U32,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct ProductionOrdinalMapping {
28 pub canonical_id: String,
29 pub ordinal: u32,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct ProductionRuntimeTable {
35 pub table_id: RuntimeTableId,
36 pub table_kind: String,
37 pub schema_version: u32,
38 pub count: u32,
39 pub ordinal_width: ProductionOrdinalWidth,
40 pub checksum: String,
41 pub mappings: Vec<ProductionOrdinalMapping>,
42 pub referenced_table_ids: Vec<RuntimeTableId>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct ProductionRuntimeTableRegistry {
48 pub tables: Vec<ProductionRuntimeTable>,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct ProductionArtifactChunkRecord {
54 pub chunk_id: ProductionChunkId,
55 pub kind: String,
56 pub module_filename: String,
57 pub dependency_chunk_ids: Vec<ProductionChunkId>,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct ProductionArtifactActivationEntry {
63 pub activation_root_id: String,
64 pub root_chunk_id: ProductionChunkId,
65 pub shared_chunk_ids: Vec<ProductionChunkId>,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ProductionArtifactEntry {
71 pub eager_chunk_id: ProductionChunkId,
72 pub activations: Vec<ProductionArtifactActivationEntry>,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct ProductionArtifactIntegrity {
78 pub artifact_checksum: String,
79 pub table_checksums: Vec<String>,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ProductionRuntimeArtifactV1 {
85 pub schema_version: u32,
86 pub build_id: ResumeBuildId,
87 pub runtime_protocol_version: u32,
88 pub optimization_policy: OptimizationPolicyId,
89 pub tables: ProductionRuntimeTableRegistry,
90 pub programs: Vec<String>,
91 pub chunks: Vec<ProductionArtifactChunkRecord>,
92 pub entry: ProductionArtifactEntry,
93 pub integrity: ProductionArtifactIntegrity,
94}
95
96#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
97pub enum ProductionArtifactIntegrityViolation {
98 ArtifactChecksumMismatch,
99 BuildIdMismatch,
100 ChunkReferenceMismatch,
101 GraphTopologyMismatch,
102 InvalidOrdinalWidth,
103 RuntimeProtocolMismatch,
104 SchemaVersionMismatch,
105 TableChecksumMismatch(RuntimeTableId),
106 TableMappingMismatch(RuntimeTableId),
107 UnknownTableReference(RuntimeTableId),
108}
109
110#[must_use]
112pub fn build_production_runtime_table(
113 table_kind: &str,
114 canonical_ids: &[String],
115 referenced_table_ids: &[RuntimeTableId],
116) -> Option<ProductionRuntimeTable> {
117 let table_id = RuntimeTableId::for_artifact_table("production_runtime", table_kind)?;
118 let mut ids = canonical_ids.to_vec();
119 ids.sort();
120 if ids.windows(2).any(|pair| pair[0] == pair[1]) {
121 return None;
122 }
123 let mut references = referenced_table_ids.to_vec();
124 references.sort();
125 if references.windows(2).any(|pair| pair[0] == pair[1]) {
126 return None;
127 }
128 let mut mappings = Vec::with_capacity(ids.len());
129 for canonical_id in ids {
130 mappings.push(ProductionOrdinalMapping {
131 canonical_id,
132 ordinal: u32::try_from(mappings.len()).ok()?,
133 });
134 }
135 let mut table = ProductionRuntimeTable {
136 table_id,
137 table_kind: table_kind.to_string(),
138 schema_version: PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION,
139 count: u32::try_from(mappings.len()).ok()?,
140 ordinal_width: ordinal_width(mappings.len()),
141 checksum: String::new(),
142 mappings,
143 referenced_table_ids: references,
144 };
145 table.checksum = table_checksum(&table);
146 Some(table)
147}
148
149#[allow(clippy::too_many_lines)]
160pub fn build_production_runtime_artifact(
161 resume: &ResumeManifest,
162 chunk_graph: &ProductionChunkGraph,
163) -> Result<ProductionRuntimeArtifactV1, Vec<ProductionArtifactIntegrityViolation>> {
164 if validate_production_chunk_graph(chunk_graph).is_err() {
165 return Err(vec![
166 ProductionArtifactIntegrityViolation::GraphTopologyMismatch,
167 ]);
168 }
169 let programs = production_programs(resume);
170 let activation_roots = chunk_graph
171 .activation_plans
172 .iter()
173 .map(|plan| plan.activation_root_id.clone())
174 .collect::<Vec<_>>();
175 let anchor_ids = resume
176 .anchors
177 .iter()
178 .map(|anchor| anchor.anchor_id.to_string())
179 .collect::<Vec<_>>();
180 let event_ids = resume
181 .events
182 .iter()
183 .map(|event| event.resume_event_id.to_string())
184 .collect::<Vec<_>>();
185 let activation_ids = resume
186 .activations
187 .iter()
188 .map(|activation| activation.activation_id.to_string())
189 .collect::<Vec<_>>();
190 let chunk_ids = chunk_graph
191 .chunks
192 .iter()
193 .map(|chunk| chunk.id.to_string())
194 .collect::<Vec<_>>();
195 let program_table = build_production_runtime_table("programs", &programs, &[])
196 .expect("compiler-derived program IDs are canonical and unique");
197 let chunk_table = build_production_runtime_table("chunks", &chunk_ids, &[])
198 .expect("validated graph chunk IDs are canonical and unique");
199 let activation_table = build_production_runtime_table(
200 "activation_roots",
201 &activation_roots,
202 std::slice::from_ref(&chunk_table.table_id),
203 )
204 .expect("graph activation roots are canonical and unique");
205 let anchor_table = build_production_runtime_table("anchors", &anchor_ids, &[])
206 .expect("manifest anchor IDs are canonical and unique");
207 let event_table = build_production_runtime_table(
208 "events",
209 &event_ids,
210 std::slice::from_ref(&anchor_table.table_id),
211 )
212 .expect("manifest event IDs are canonical and unique");
213 let activation_id_table = build_production_runtime_table(
214 "activations",
215 &activation_ids,
216 std::slice::from_ref(&chunk_table.table_id),
217 )
218 .expect("manifest activation IDs are canonical and unique");
219 let tables = ProductionRuntimeTableRegistry {
220 tables: vec![
221 activation_id_table,
222 activation_table,
223 anchor_table,
224 chunk_table,
225 event_table,
226 program_table,
227 ],
228 };
229 let dependencies = chunk_graph.dependencies.iter().fold(
230 BTreeMap::<ProductionChunkId, Vec<ProductionChunkId>>::new(),
231 |mut map, edge| {
232 map.entry(edge.dependent_chunk_id.clone())
233 .or_default()
234 .push(edge.dependency_chunk_id.clone());
235 map
236 },
237 );
238 let mut chunks = chunk_graph
239 .chunks
240 .iter()
241 .map(|chunk| ProductionArtifactChunkRecord {
242 chunk_id: chunk.id.clone(),
243 kind: chunk_kind_name(chunk.kind).to_string(),
244 module_filename: chunk.provisional_module_filename.clone(),
245 dependency_chunk_ids: dependencies.get(&chunk.id).cloned().unwrap_or_default(),
246 })
247 .collect::<Vec<_>>();
248 chunks.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
249 for chunk in &mut chunks {
250 chunk.dependency_chunk_ids.sort();
251 chunk.dependency_chunk_ids.dedup();
252 }
253 let mut activations = chunk_graph
254 .activation_plans
255 .iter()
256 .map(|plan| ProductionArtifactActivationEntry {
257 activation_root_id: plan.activation_root_id.clone(),
258 root_chunk_id: plan.root_chunk_id.clone(),
259 shared_chunk_ids: plan.shared_chunk_ids.clone(),
260 })
261 .collect::<Vec<_>>();
262 activations.sort_by(|left, right| left.activation_root_id.cmp(&right.activation_root_id));
263 let mut artifact = ProductionRuntimeArtifactV1 {
264 schema_version: PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION,
265 build_id: resume.build_id.clone(),
266 runtime_protocol_version: resume.runtime_protocol_version,
267 optimization_policy: OptimizationPolicyId::production_v1(),
268 tables,
269 programs,
270 chunks,
271 entry: ProductionArtifactEntry {
272 eager_chunk_id: chunk_graph.eager_chunk_id.clone(),
273 activations,
274 },
275 integrity: ProductionArtifactIntegrity {
276 artifact_checksum: String::new(),
277 table_checksums: Vec::new(),
278 },
279 };
280 artifact.integrity.table_checksums = artifact
281 .tables
282 .tables
283 .iter()
284 .map(|table| table.checksum.clone())
285 .collect();
286 artifact.integrity.artifact_checksum = artifact_checksum(&artifact);
287 Ok(artifact)
288}
289
290#[must_use]
291pub fn production_runtime_artifact_json(artifact: &ProductionRuntimeArtifactV1) -> String {
296 serde_json::to_string(artifact).expect("production runtime artifact should serialize") + "\n"
297}
298
299pub fn parse_production_runtime_artifact_v1(
306 json: &str,
307 expected_build_id: &ResumeBuildId,
308) -> Result<ProductionRuntimeArtifactV1, Vec<ProductionArtifactIntegrityViolation>> {
309 let artifact = serde_json::from_str::<ProductionRuntimeArtifactV1>(json)
310 .map_err(|_| vec![ProductionArtifactIntegrityViolation::SchemaVersionMismatch])?;
311 let errors = validate_production_runtime_artifact(&artifact, expected_build_id);
312 errors.is_empty().then_some(artifact).ok_or(errors)
313}
314
315#[must_use]
316pub fn validate_production_runtime_artifact(
317 artifact: &ProductionRuntimeArtifactV1,
318 expected_build_id: &ResumeBuildId,
319) -> Vec<ProductionArtifactIntegrityViolation> {
320 let mut violations = Vec::new();
321 if artifact.schema_version != PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION {
322 violations.push(ProductionArtifactIntegrityViolation::SchemaVersionMismatch);
323 }
324 if artifact.runtime_protocol_version != RESUME_RUNTIME_PROTOCOL_VERSION {
325 violations.push(ProductionArtifactIntegrityViolation::RuntimeProtocolMismatch);
326 }
327 if &artifact.build_id != expected_build_id {
328 violations.push(ProductionArtifactIntegrityViolation::BuildIdMismatch);
329 }
330 let table_ids = artifact
331 .tables
332 .tables
333 .iter()
334 .map(|table| table.table_id.clone())
335 .collect::<BTreeSet<_>>();
336 for table in &artifact.tables.tables {
337 if table.checksum != table_checksum(table) {
338 violations.push(ProductionArtifactIntegrityViolation::TableChecksumMismatch(
339 table.table_id.clone(),
340 ));
341 }
342 let mappings_are_canonical = u32::try_from(table.mappings.len()) == Ok(table.count)
343 && table.ordinal_width == ordinal_width(table.mappings.len())
344 && table
345 .mappings
346 .iter()
347 .enumerate()
348 .all(|(ordinal, mapping)| u32::try_from(ordinal) == Ok(mapping.ordinal))
349 && table
350 .mappings
351 .windows(2)
352 .all(|pair| pair[0].canonical_id < pair[1].canonical_id);
353 if !mappings_are_canonical
354 || table.schema_version != PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION
355 {
356 violations.push(ProductionArtifactIntegrityViolation::TableMappingMismatch(
357 table.table_id.clone(),
358 ));
359 }
360 if table.ordinal_width != ordinal_width(table.count as usize) {
361 violations.push(ProductionArtifactIntegrityViolation::InvalidOrdinalWidth);
362 }
363 for reference in &table.referenced_table_ids {
364 if !table_ids.contains(reference) {
365 violations.push(ProductionArtifactIntegrityViolation::UnknownTableReference(
366 reference.clone(),
367 ));
368 }
369 }
370 }
371 let chunk_ids = artifact
372 .chunks
373 .iter()
374 .map(|chunk| chunk.chunk_id.clone())
375 .collect::<BTreeSet<_>>();
376 let chunk_references_valid = chunk_ids.contains(&artifact.entry.eager_chunk_id)
377 && artifact.chunks.iter().all(|chunk| {
378 chunk
379 .dependency_chunk_ids
380 .iter()
381 .all(|dependency| chunk_ids.contains(dependency))
382 })
383 && artifact.entry.activations.iter().all(|activation| {
384 chunk_ids.contains(&activation.root_chunk_id)
385 && activation
386 .shared_chunk_ids
387 .iter()
388 .all(|shared| chunk_ids.contains(shared))
389 });
390 if !chunk_references_valid {
391 violations.push(ProductionArtifactIntegrityViolation::ChunkReferenceMismatch);
392 }
393 if artifact.integrity.artifact_checksum != artifact_checksum(artifact) {
394 violations.push(ProductionArtifactIntegrityViolation::ArtifactChecksumMismatch);
395 }
396 violations.sort();
397 violations.dedup();
398 violations
399}
400
401fn production_programs(resume: &ResumeManifest) -> Vec<String> {
402 let mut programs = BTreeSet::new();
403 for chunk in &resume.chunks {
404 programs.extend(chunk.provided_program_ids.iter().cloned());
405 }
406 programs.extend(
407 resume
408 .capture_programs
409 .iter()
410 .map(|program| program.program_id.to_string()),
411 );
412 programs.extend(
413 resume
414 .restore_programs
415 .iter()
416 .map(|program| program.program_id.to_string()),
417 );
418 programs.extend(
419 resume
420 .events
421 .iter()
422 .map(|event| event.action_or_submit_program_id.clone()),
423 );
424 programs.into_iter().collect()
425}
426
427fn ordinal_width(count: usize) -> ProductionOrdinalWidth {
428 if count <= 255 {
429 ProductionOrdinalWidth::U8
430 } else if count <= 65_535 {
431 ProductionOrdinalWidth::U16
432 } else {
433 ProductionOrdinalWidth::U32
434 }
435}
436
437fn table_checksum(table: &ProductionRuntimeTable) -> String {
438 let mut bytes = format!(
439 "table:{}\nschema:{}\ncount:{}\nwidth:{:?}\n",
440 table.table_id, table.schema_version, table.count, table.ordinal_width
441 )
442 .into_bytes();
443 for mapping in &table.mappings {
444 bytes.extend_from_slice(mapping.canonical_id.as_bytes());
445 bytes.extend_from_slice(format!("\0{}\n", mapping.ordinal).as_bytes());
446 }
447 for reference in &table.referenced_table_ids {
448 bytes.extend_from_slice(reference.to_string().as_bytes());
449 bytes.push(b'\n');
450 }
451 format!("{:x}", Sha256::digest(bytes))
452}
453
454fn artifact_checksum(artifact: &ProductionRuntimeArtifactV1) -> String {
455 let mut canonical = artifact.clone();
456 canonical.integrity.artifact_checksum.clear();
457 format!(
458 "{:x}",
459 Sha256::digest(
460 serde_json::to_vec(&canonical).expect("production artifact should serialize")
461 )
462 )
463}
464
465const fn chunk_kind_name(kind: ProductionChunkKind) -> &'static str {
466 match kind {
467 ProductionChunkKind::Eager => "eager",
468 ProductionChunkKind::Root => "root",
469 ProductionChunkKind::Shared => "shared",
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::{
477 extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionRootChunkInput,
478 ResumeBoundaryId, ResumeManifest, SharedChunkCandidatePlan,
479 };
480 use std::str::FromStr;
481
482 fn manifest() -> ResumeManifest {
483 ResumeManifest {
484 schema_version: 6,
485 build_id: ResumeBuildId::zero_sentinel(),
486 snapshot_schema_version: 1,
487 runtime_protocol_version: 1,
488 application_root_boundary_id: ResumeBoundaryId::from_str("resume-boundary:root")
489 .expect("boundary"),
490 boundaries: Vec::new(),
491 slot_schemas: Vec::new(),
492 capture_programs: Vec::new(),
493 restore_programs: Vec::new(),
494 chunks: Vec::new(),
495 activations: Vec::new(),
496 anchors: Vec::new(),
497 events: Vec::new(),
498 phase_i_component_resume_records: Vec::new(),
499 phase_i_form_resume_records: Vec::new(),
500 }
501 }
502
503 fn graph() -> ProductionChunkGraph {
504 extract_production_chunk_graph(
505 &SharedChunkCandidatePlan {
506 candidates: Vec::new(),
507 rejections: Vec::new(),
508 },
509 &[ProductionRootChunkInput {
510 activation_root_id: "root-a".to_string(),
511 root_kind: "interaction".to_string(),
512 programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
513 b"a",
514 )],
515 }],
516 )
517 .expect("graph")
518 .0
519 }
520
521 #[test]
522 fn k8_assigns_canonical_dense_ordinals_and_width_boundaries() {
523 let ids = (0..256)
524 .map(|index| format!("id-{index:03}"))
525 .collect::<Vec<_>>();
526 let table = build_production_runtime_table("programs", &ids, &[]).expect("table");
527 assert_eq!(table.mappings[0].canonical_id, "id-000");
528 assert_eq!(table.mappings[255].ordinal, 255);
529 assert_eq!(table.ordinal_width, ProductionOrdinalWidth::U16);
530 assert!(build_production_runtime_table(
531 "programs",
532 &["same".to_string(), "same".to_string()],
533 &[]
534 )
535 .is_none());
536 }
537
538 #[test]
539 fn k8_round_trips_canonical_artifact_and_rejects_integrity_drift() {
540 let manifest = manifest();
541 let artifact = build_production_runtime_artifact(&manifest, &graph()).expect("artifact");
542 let json = production_runtime_artifact_json(&artifact);
543 assert!(json.ends_with('\n'));
544 assert!(!json.contains("src/"));
545 assert_eq!(
546 parse_production_runtime_artifact_v1(&json, &manifest.build_id),
547 Ok(artifact.clone())
548 );
549 let mut checksum_drift = artifact.clone();
550 checksum_drift.tables.tables[0].checksum = "bad".to_string();
551 assert!(
552 validate_production_runtime_artifact(&checksum_drift, &manifest.build_id)
553 .iter()
554 .any(|violation| matches!(
555 violation,
556 ProductionArtifactIntegrityViolation::TableChecksumMismatch(_)
557 ))
558 );
559 }
560
561 #[test]
562 fn k8_rejects_unknown_chunk_reference_and_build_mismatch() {
563 let manifest = manifest();
564 let mut artifact =
565 build_production_runtime_artifact(&manifest, &graph()).expect("artifact");
566 artifact.chunks[0]
567 .dependency_chunk_ids
568 .push(ProductionChunkId::from_str("production-chunk:unknown").expect("chunk ID"));
569 let errors = validate_production_runtime_artifact(
570 &artifact,
571 &ResumeBuildId::from_str("resume-build:other").expect("build ID"),
572 );
573 assert!(errors.contains(&ProductionArtifactIntegrityViolation::BuildIdMismatch));
574 assert!(errors.contains(&ProductionArtifactIntegrityViolation::ChunkReferenceMismatch));
575 assert!(errors.contains(&ProductionArtifactIntegrityViolation::ArtifactChecksumMismatch));
576 }
577}