1use super::{
2 ArtifactManifest, BOUNDARY_STATE_HASH_V1_PREFIX, BoundaryChange, BoundaryEmission, BoundaryId,
3 BoundaryIngressGeneration, BoundaryRecord, BoundaryStateHashFormat, COMMITMENT_FORMAT_VERSION,
4 CanwuError, CommandAttemptId, CommandAttemptRecord, CommandId, CommandRecord, DecisionState,
5 DomainRecord, DomainRecordChange, EntityRef, ErrorCode, EventId, GENESIS_BOUNDARY_HASH,
6 IngressId, IngressRecord, JournalCommitmentRoots, KnowledgeSnapshot, PluginComponentRecord,
7 PluginDescriptor, RandomDrawId, RandomDrawRecord, RandomStreamState, ReservationAllocation,
8 ReservationOfferRecord, ReservationRequestRecord, RunConfigurationSnapshot, RunManifest,
9 RuntimeDomainCommitmentRoots, STATE_REVISION_FORMAT_VERSION, Scenario, ScheduledRecord,
10 SchemaRegistry, SimEvent, SimTime, SimulationSnapshot, SystemCadence, WorldSnapshot,
11 boundary_state_hash_format, command_attempt_id_slice_is_empty, command_attempt_slice_is_empty,
12 component_key, domain_record_change_slice_is_empty, domain_record_slice_is_empty,
13 ingress_record_slice_is_empty, invalid_snapshot, invalid_snapshot_error, is_one_u64,
14 maintenance_change_slice_is_empty, manifest, policy,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeSet;
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
20pub struct CommitmentRoots {
22 pub world: String,
23 pub knowledge: String,
24 pub plugin_components: String,
25 pub domain_records: String,
26 #[serde(default, skip_serializing_if = "String::is_empty")]
27 pub decisions: String,
28 pub scheduler: String,
29 pub commands: String,
30 pub events: String,
31 pub ingress: String,
32 pub random: String,
33 pub boundary_chain: String,
34 pub identity: String,
35 pub control: String,
36}
37
38#[derive(Serialize)]
39pub(super) struct StateHashMaterial<'a> {
40 pub(super) engine_version: &'a str,
41 pub(super) snapshot_format_version: u32,
42 pub(super) run_manifest: &'a RunManifest,
43 pub(super) run_manifest_hash: &'a str,
44 pub(super) initial_time: SimTime,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub(super) initial_scenario: Option<&'a Scenario>,
47 pub(super) now: SimTime,
48 pub(super) plugin_registration_closed: bool,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub(super) entities: Option<&'a [EntityRef]>,
51 pub(super) world: &'a WorldSnapshot,
52 pub(super) knowledge: &'a KnowledgeSnapshot,
53 pub(super) events: &'a [SimEvent],
54 pub(super) commands: &'a [CommandRecord],
55 #[serde(skip_serializing_if = "command_attempt_slice_is_empty")]
56 pub(super) command_attempts: &'a [CommandAttemptRecord],
57 #[serde(skip_serializing_if = "ingress_record_slice_is_empty")]
58 pub(super) ingress: &'a [IngressRecord],
59 pub(super) plugin_components: &'a [PluginComponentRecord],
60 #[serde(skip_serializing_if = "domain_record_slice_is_empty")]
61 pub(super) domain_records: &'a [DomainRecord],
62 #[serde(skip_serializing_if = "DecisionState::is_empty")]
63 pub(super) decisions: &'a DecisionState,
64 pub(super) plugin_descriptors: &'a [PluginDescriptor],
65 pub(super) schema: &'a SchemaRegistry,
66 pub(super) scheduled: &'a [ScheduledRecord],
67 pub(super) root_seed: u64,
68 pub(super) authority_root_seed: u64,
69 pub(super) random_streams: &'a [RandomStreamState],
70 pub(super) random_draws: &'a [RandomDrawRecord],
71 pub(super) next_event_id: u64,
72 pub(super) next_command_id: u64,
73 #[serde(skip_serializing_if = "is_one_u64")]
74 pub(super) next_command_attempt_id: u64,
75 #[serde(skip_serializing_if = "is_one_u64")]
76 pub(super) next_ingress_id: u64,
77 pub(super) next_boundary_id: u64,
78 pub(super) next_random_draw_id: u64,
79 #[serde(skip_serializing_if = "is_one_u64")]
80 pub(super) next_knowledge_record_id: u64,
81 pub(super) next_schedule_sequence: u64,
82 pub(super) next_correlation_id: u64,
83 #[serde(skip_serializing_if = "is_one_u64")]
84 pub(super) next_decision_trace_id: u64,
85}
86
87#[derive(Serialize)]
88struct WorldCommitmentMaterial<'a> {
89 people: String,
90 governments: String,
91 territories: String,
92 routes: String,
93 armies: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 entities: Option<&'a [EntityRef]>,
96}
97
98#[derive(Serialize)]
99struct SchedulerCommitmentMaterial {
100 now: SimTime,
101 scheduled: String,
102}
103
104#[derive(Serialize)]
105struct CommandCommitmentMaterial {
106 commands: String,
107 attempts: String,
108}
109
110#[derive(Serialize)]
111struct RandomCommitmentMaterial {
112 root_seed: u64,
113 streams: String,
114 draws: String,
115}
116
117#[derive(Serialize)]
118struct IdentityCommitmentMaterial<'a> {
119 engine_version: &'a str,
120 snapshot_format_version: u32,
121 run_manifest: &'a RunManifest,
122 run_manifest_hash: &'a str,
123 initial_time: SimTime,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 initial_scenario: Option<&'a Scenario>,
126 authority_root_seed: u64,
127 plugin_descriptors: String,
128 schema: &'a SchemaRegistry,
129}
130
131#[derive(Serialize)]
132pub(super) struct ControlCommitmentMaterial {
133 pub(super) plugin_registration_closed: bool,
134 pub(super) next_event_id: u64,
135 pub(super) next_command_id: u64,
136 pub(super) next_command_attempt_id: u64,
137 pub(super) next_ingress_id: u64,
138 pub(super) next_boundary_id: u64,
139 pub(super) next_random_draw_id: u64,
140 #[serde(skip_serializing_if = "is_one_u64")]
141 pub(super) next_knowledge_record_id: u64,
142 pub(super) next_schedule_sequence: u64,
143 pub(super) next_correlation_id: u64,
144 #[serde(skip_serializing_if = "is_one_u64")]
145 pub(super) next_decision_trace_id: u64,
146}
147
148#[derive(Serialize)]
149struct CheckpointHashMaterialV1<'a> {
150 state_hash: &'a str,
151 boundary_head: Option<&'a str>,
152}
153
154#[derive(Serialize)]
155struct CheckpointHashMaterialV2<'a> {
156 state_hash: &'a str,
157 boundary_head: Option<&'a str>,
158 run_manifest_hash: &'a str,
159}
160
161#[derive(Serialize)]
162struct CheckpointHashMaterialV3<'a> {
163 state_hash: &'a str,
164 boundary_head: Option<&'a str>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 run_manifest_hash: Option<&'a str>,
167 revision_format_version: u32,
168 state_revision: u64,
169 replay_revision_format_version: u32,
170}
171
172#[derive(Serialize)]
173struct CheckpointHashMaterialV4<'a> {
174 commitments: &'a CommitmentRoots,
175 run_manifest_hash: &'a str,
176 commitment_format_version: u32,
177 revision_format_version: u32,
178 state_revision: u64,
179 replay_revision_format_version: u32,
180}
181
182pub(super) fn state_hash(material: &StateHashMaterial<'_>) -> Result<String, CanwuError> {
183 canonical_hash("canwu.boundary-state.v1", material)
184}
185
186fn canonical_sorted_hash_by<T, K, F>(
187 domain: &str,
188 values: &[T],
189 mut key: F,
190) -> Result<String, CanwuError>
191where
192 T: Serialize,
193 K: Ord,
194 F: FnMut(&T) -> K,
195{
196 let mut ordered: Vec<_> = values.iter().collect();
197 ordered.sort_by_key(|value| key(value));
198 canonical_hash(domain, &ordered)
199}
200
201pub(super) fn committed_entities<'a>(
202 entities: &'a [EntityRef],
203 world: &WorldSnapshot,
204) -> Option<&'a [EntityRef]> {
205 (!entities.is_empty() && entities != super::scenario::legacy_entities(world))
206 .then_some(entities)
207}
208
209pub(super) fn committed_initial_scenario(scenario: Option<&Scenario>) -> Option<Scenario> {
210 scenario.cloned().map(|mut scenario| {
211 if scenario.entities == super::scenario::legacy_entities(&scenario.world) {
212 scenario.entities.clear();
213 }
214 scenario
215 })
216}
217
218pub(super) fn world_commitment_root(
219 world: &WorldSnapshot,
220 entities: &[EntityRef],
221) -> Result<String, CanwuError> {
222 canonical_hash(
223 "canwu.commitment.world.v1",
224 &WorldCommitmentMaterial {
225 people: canonical_sorted_hash_by(
226 "canwu.commitment.world.people.v1",
227 &world.people,
228 |value| value.id,
229 )?,
230 governments: canonical_sorted_hash_by(
231 "canwu.commitment.world.governments.v1",
232 &world.governments,
233 |value| value.id,
234 )?,
235 territories: canonical_sorted_hash_by(
236 "canwu.commitment.world.territories.v1",
237 &world.territories,
238 |value| value.id,
239 )?,
240 routes: canonical_sorted_hash_by(
241 "canwu.commitment.world.routes.v1",
242 &world.routes,
243 |value| value.id,
244 )?,
245 armies: canonical_sorted_hash_by(
246 "canwu.commitment.world.armies.v1",
247 &world.armies,
248 |value| value.id,
249 )?,
250 entities: committed_entities(entities, world),
251 },
252 )
253}
254
255pub(super) fn knowledge_commitment_root(
256 knowledge: &KnowledgeSnapshot,
257) -> Result<String, CanwuError> {
258 canonical_hash("canwu.commitment.knowledge.v1", knowledge)
259}
260
261pub(super) fn plugin_component_commitment_root(
262 components: &[PluginComponentRecord],
263) -> Result<String, CanwuError> {
264 canonical_sorted_hash_by(
265 "canwu.commitment.plugin-components.v1",
266 components,
267 |record| {
268 component_key(
269 &record.plugin,
270 &record.state,
271 &record.entity,
272 &record.component,
273 )
274 },
275 )
276}
277
278pub(super) fn domain_record_commitment_root(
279 records: &[DomainRecord],
280) -> Result<String, CanwuError> {
281 let records = records
282 .iter()
283 .cloned()
284 .map(|record| (record.reference.clone(), record))
285 .collect();
286 super::PersistentDomainRecordStore::from_records(records)?.commitment_root()
287}
288
289pub(super) fn decision_commitment_root(decisions: &DecisionState) -> Result<String, CanwuError> {
290 if decisions.is_empty() {
291 return Ok(String::new());
292 }
293 decisions
294 .authoritative_commitment()
295 .map_err(super::decision::decision_error)
296}
297
298pub(super) fn scheduler_commitment_root(
299 now: SimTime,
300 scheduled: &[ScheduledRecord],
301) -> Result<String, CanwuError> {
302 canonical_hash(
303 "canwu.commitment.scheduler.v1",
304 &SchedulerCommitmentMaterial {
305 now,
306 scheduled: canonical_sorted_hash_by(
307 "canwu.commitment.scheduler.entries.v1",
308 scheduled,
309 |record| record.key.clone(),
310 )?,
311 },
312 )
313}
314
315pub(super) fn random_stream_commitment_root(
316 streams: &[RandomStreamState],
317) -> Result<String, CanwuError> {
318 canonical_sorted_hash_by("canwu.commitment.random.streams.v1", streams, |stream| {
319 stream.key.clone()
320 })
321}
322
323#[allow(clippy::too_many_arguments)]
324pub(super) fn identity_commitment_root(
325 engine_version: &str,
326 snapshot_format_version: u32,
327 run_manifest: &RunManifest,
328 run_manifest_hash: &str,
329 initial_time: SimTime,
330 initial_scenario: Option<&Scenario>,
331 authority_root_seed: u64,
332 plugin_descriptors: &[PluginDescriptor],
333 schema: &SchemaRegistry,
334) -> Result<String, CanwuError> {
335 canonical_hash(
336 "canwu.commitment.identity.v1",
337 &IdentityCommitmentMaterial {
338 engine_version,
339 snapshot_format_version,
340 run_manifest,
341 run_manifest_hash,
342 initial_time,
343 initial_scenario,
344 authority_root_seed,
345 plugin_descriptors: canonical_sorted_hash_by(
346 "canwu.commitment.identity.plugins.v1",
347 plugin_descriptors,
348 |descriptor| descriptor.name.clone(),
349 )?,
350 schema,
351 },
352 )
353}
354
355fn commitment_roots(
356 material: &StateHashMaterial<'_>,
357 boundary_head: Option<&str>,
358 journal_roots: Option<&JournalCommitmentRoots>,
359) -> Result<CommitmentRoots, CanwuError> {
360 let world = world_commitment_root(material.world, material.entities.unwrap_or_default())?;
361 let knowledge = knowledge_commitment_root(material.knowledge)?;
362 let plugin_components = plugin_component_commitment_root(material.plugin_components)?;
363 let domain_records = domain_record_commitment_root(material.domain_records)?;
364 let decisions = decision_commitment_root(material.decisions)?;
365 let scheduler = scheduler_commitment_root(material.now, material.scheduled)?;
366 let command_root = match journal_roots {
367 Some(roots) => roots.commands.clone(),
368 None => canonical_sorted_hash_by(
369 "canwu.commitment.commands.accepted.v1",
370 material.commands,
371 |record| record.id,
372 )?,
373 };
374 let attempt_root = match journal_roots {
375 Some(roots) => roots.attempts.clone(),
376 None => canonical_sorted_hash_by(
377 "canwu.commitment.commands.attempts.v1",
378 material.command_attempts,
379 |record| record.id,
380 )?,
381 };
382 let commands = canonical_hash(
383 "canwu.commitment.commands.v1",
384 &CommandCommitmentMaterial {
385 commands: command_root,
386 attempts: attempt_root,
387 },
388 )?;
389 let events = match journal_roots {
390 Some(roots) => roots.events.clone(),
391 None => canonical_sorted_hash_by("canwu.commitment.events.v1", material.events, |event| {
392 event.id
393 })?,
394 };
395 let ingress = match journal_roots {
396 Some(roots) => roots.ingress.clone(),
397 None => {
398 canonical_sorted_hash_by("canwu.commitment.ingress.v1", material.ingress, |record| {
399 record.id
400 })?
401 }
402 };
403 let random = canonical_hash(
404 "canwu.commitment.random.v1",
405 &RandomCommitmentMaterial {
406 root_seed: material.root_seed,
407 streams: random_stream_commitment_root(material.random_streams)?,
408 draws: match journal_roots {
409 Some(roots) => roots.random_draws.clone(),
410 None => canonical_sorted_hash_by(
411 "canwu.commitment.random.draws.v1",
412 material.random_draws,
413 |draw| draw.id,
414 )?,
415 },
416 },
417 )?;
418 let boundary_chain = canonical_hash(
419 "canwu.commitment.boundary-chain.v1",
420 boundary_head.unwrap_or(GENESIS_BOUNDARY_HASH),
421 )?;
422 let identity = identity_commitment_root(
423 material.engine_version,
424 material.snapshot_format_version,
425 material.run_manifest,
426 material.run_manifest_hash,
427 material.initial_time,
428 material.initial_scenario,
429 material.authority_root_seed,
430 material.plugin_descriptors,
431 material.schema,
432 )?;
433 let control = canonical_hash(
434 "canwu.commitment.control.v1",
435 &ControlCommitmentMaterial {
436 plugin_registration_closed: material.plugin_registration_closed,
437 next_event_id: material.next_event_id,
438 next_command_id: material.next_command_id,
439 next_command_attempt_id: material.next_command_attempt_id,
440 next_ingress_id: material.next_ingress_id,
441 next_boundary_id: material.next_boundary_id,
442 next_random_draw_id: material.next_random_draw_id,
443 next_knowledge_record_id: material.next_knowledge_record_id,
444 next_schedule_sequence: material.next_schedule_sequence,
445 next_correlation_id: material.next_correlation_id,
446 next_decision_trace_id: material.next_decision_trace_id,
447 },
448 )?;
449 Ok(CommitmentRoots {
450 world,
451 knowledge,
452 plugin_components,
453 domain_records,
454 decisions,
455 scheduler,
456 commands,
457 events,
458 ingress,
459 random,
460 boundary_chain,
461 identity,
462 control,
463 })
464}
465
466pub(super) fn runtime_commitment_roots(
467 domain: &RuntimeDomainCommitmentRoots,
468 journal: &JournalCommitmentRoots,
469 root_seed: u64,
470 boundary_head: Option<&str>,
471 control: &ControlCommitmentMaterial,
472) -> Result<CommitmentRoots, CanwuError> {
473 let commands = canonical_hash(
474 "canwu.commitment.commands.v1",
475 &CommandCommitmentMaterial {
476 commands: journal.commands.clone(),
477 attempts: journal.attempts.clone(),
478 },
479 )?;
480 let random = canonical_hash(
481 "canwu.commitment.random.v1",
482 &RandomCommitmentMaterial {
483 root_seed,
484 streams: domain.random_streams.clone(),
485 draws: journal.random_draws.clone(),
486 },
487 )?;
488 Ok(CommitmentRoots {
489 world: domain.world.clone(),
490 knowledge: domain.knowledge.clone(),
491 plugin_components: domain.plugin_components.clone(),
492 domain_records: domain.domain_records.clone(),
493 decisions: domain.decisions.clone(),
494 scheduler: domain.scheduler.clone(),
495 commands,
496 events: journal.events.clone(),
497 ingress: journal.ingress.clone(),
498 random,
499 boundary_chain: canonical_hash(
500 "canwu.commitment.boundary-chain.v1",
501 boundary_head.unwrap_or(GENESIS_BOUNDARY_HASH),
502 )?,
503 identity: domain.identity.clone(),
504 control: canonical_hash("canwu.commitment.control.v1", control)?,
505 })
506}
507
508pub(super) fn boundary_state_hash_for_commitments(
509 commitments: &CommitmentRoots,
510) -> Result<String, CanwuError> {
511 Ok(format!(
512 "{BOUNDARY_STATE_HASH_V1_PREFIX}{}",
513 canonical_hash("canwu.boundary-state.v1", commitments)?
514 ))
515}
516
517pub(super) fn authoritative_run_identity(
518 run_manifest: &RunManifest,
519 run_manifest_hash: &str,
520 run_configuration: &RunConfigurationSnapshot,
521) -> Result<(RunManifest, String), CanwuError> {
522 if !matches!(run_configuration, RunConfigurationSnapshot::Declared(_)) {
523 return Ok((run_manifest.clone(), run_manifest_hash.to_owned()));
524 }
525 let mut authoritative_manifest = run_manifest.clone();
526 let RunManifest::Declared {
527 run_configuration, ..
528 } = &mut authoritative_manifest;
529 **run_configuration = ArtifactManifest::new(
530 "canwu.core",
531 "authoritative-policy-excluded",
532 "1",
533 policy::authoritative_configuration_hash()?,
534 )?;
535 let authoritative_manifest_hash = manifest::hash(&authoritative_manifest)?;
536 Ok((authoritative_manifest, authoritative_manifest_hash))
537}
538
539pub(super) fn snapshot_state_hash(snapshot: &SimulationSnapshot) -> Result<String, CanwuError> {
540 let Some(run_manifest) = &snapshot.run_manifest else {
541 return Err(CanwuError::new(
542 ErrorCode::InvalidRunManifest,
543 "snapshot is missing its run manifest",
544 ));
545 };
546 let run_configuration = snapshot.run_configuration.as_ref().ok_or_else(|| {
547 CanwuError::new(
548 ErrorCode::InvalidRunConfiguration,
549 "snapshot is missing its run configuration",
550 )
551 })?;
552 let (authoritative_manifest, authoritative_manifest_hash) =
553 authoritative_run_identity(run_manifest, &snapshot.run_manifest_hash, run_configuration)?;
554 let initial_scenario = committed_initial_scenario(snapshot.initial_scenario.as_ref());
555 state_hash(&StateHashMaterial {
556 engine_version: &snapshot.engine_version,
557 snapshot_format_version: snapshot.snapshot_format_version,
558 run_manifest: &authoritative_manifest,
559 run_manifest_hash: &authoritative_manifest_hash,
560 initial_time: snapshot.initial_time,
561 initial_scenario: initial_scenario.as_ref(),
562 now: snapshot.now,
563 plugin_registration_closed: snapshot.plugin_registration_closed,
564 entities: committed_entities(&snapshot.entities, &snapshot.world),
565 world: &snapshot.world,
566 knowledge: &snapshot.knowledge,
567 events: &snapshot.events,
568 commands: &snapshot.commands,
569 command_attempts: &snapshot.command_attempts,
570 ingress: &snapshot.ingress,
571 plugin_components: &snapshot.plugin_components,
572 domain_records: &snapshot.domain_records,
573 decisions: &snapshot.decisions,
574 plugin_descriptors: &snapshot.plugin_descriptors,
575 schema: &snapshot.schema,
576 scheduled: &snapshot.scheduled,
577 root_seed: snapshot.root_seed,
578 authority_root_seed: snapshot.authority_root_seed,
579 random_streams: &snapshot.random_streams,
580 random_draws: &snapshot.random_draws,
581 next_event_id: snapshot.next_event_id,
582 next_command_id: snapshot.next_command_id,
583 next_command_attempt_id: snapshot.next_command_attempt_id,
584 next_ingress_id: snapshot.next_ingress_id,
585 next_boundary_id: snapshot.next_boundary_id,
586 next_random_draw_id: snapshot.next_random_draw_id,
587 next_knowledge_record_id: snapshot.next_knowledge_record_id,
588 next_schedule_sequence: snapshot.next_schedule_sequence,
589 next_correlation_id: snapshot.next_correlation_id,
590 next_decision_trace_id: snapshot.next_decision_trace_id,
591 })
592}
593
594pub(super) fn snapshot_boundary_head_state_hash(
595 snapshot: &SimulationSnapshot,
596) -> Result<String, CanwuError> {
597 let boundary = snapshot
598 .boundaries
599 .last()
600 .ok_or_else(|| invalid_snapshot_error("snapshot has no boundary head commitment"))?;
601 match boundary_state_hash_format(boundary.state_hash.as_deref())? {
602 BoundaryStateHashFormat::LegacyV0 => snapshot_state_hash(snapshot),
603 BoundaryStateHashFormat::CommitmentsV1 => {
604 if snapshot.commitment_format_version != COMMITMENT_FORMAT_VERSION {
605 return invalid_snapshot(
606 "boundary state commitment v1 requires current domain commitments",
607 );
608 }
609 let mut roots = snapshot.commitment_roots.clone().ok_or_else(|| {
610 invalid_snapshot_error(
611 "boundary state commitment v1 is missing current domain commitments",
612 )
613 })?;
614 roots.boundary_chain = canonical_hash(
615 "canwu.commitment.boundary-chain.v1",
616 boundary.previous_hash.as_str(),
617 )?;
618 boundary_state_hash_for_commitments(&roots)
619 }
620 }
621}
622
623pub(super) fn snapshot_commitment_roots(
624 snapshot: &SimulationSnapshot,
625) -> Result<CommitmentRoots, CanwuError> {
626 let Some(run_manifest) = &snapshot.run_manifest else {
627 return Err(CanwuError::new(
628 ErrorCode::InvalidRunManifest,
629 "snapshot is missing its run manifest",
630 ));
631 };
632 let run_configuration = snapshot.run_configuration.as_ref().ok_or_else(|| {
633 CanwuError::new(
634 ErrorCode::InvalidRunConfiguration,
635 "snapshot is missing its run configuration",
636 )
637 })?;
638 let (authoritative_manifest, authoritative_manifest_hash) =
639 authoritative_run_identity(run_manifest, &snapshot.run_manifest_hash, run_configuration)?;
640 let initial_scenario = committed_initial_scenario(snapshot.initial_scenario.as_ref());
641 commitment_roots(
642 &StateHashMaterial {
643 engine_version: &snapshot.engine_version,
644 snapshot_format_version: snapshot.snapshot_format_version,
645 run_manifest: &authoritative_manifest,
646 run_manifest_hash: &authoritative_manifest_hash,
647 initial_time: snapshot.initial_time,
648 initial_scenario: initial_scenario.as_ref(),
649 now: snapshot.now,
650 plugin_registration_closed: snapshot.plugin_registration_closed,
651 entities: committed_entities(&snapshot.entities, &snapshot.world),
652 world: &snapshot.world,
653 knowledge: &snapshot.knowledge,
654 events: &snapshot.events,
655 commands: &snapshot.commands,
656 command_attempts: &snapshot.command_attempts,
657 ingress: &snapshot.ingress,
658 plugin_components: &snapshot.plugin_components,
659 domain_records: &snapshot.domain_records,
660 decisions: &snapshot.decisions,
661 plugin_descriptors: &snapshot.plugin_descriptors,
662 schema: &snapshot.schema,
663 scheduled: &snapshot.scheduled,
664 root_seed: snapshot.root_seed,
665 authority_root_seed: snapshot.authority_root_seed,
666 random_streams: &snapshot.random_streams,
667 random_draws: &snapshot.random_draws,
668 next_event_id: snapshot.next_event_id,
669 next_command_id: snapshot.next_command_id,
670 next_command_attempt_id: snapshot.next_command_attempt_id,
671 next_ingress_id: snapshot.next_ingress_id,
672 next_boundary_id: snapshot.next_boundary_id,
673 next_random_draw_id: snapshot.next_random_draw_id,
674 next_knowledge_record_id: snapshot.next_knowledge_record_id,
675 next_schedule_sequence: snapshot.next_schedule_sequence,
676 next_correlation_id: snapshot.next_correlation_id,
677 next_decision_trace_id: snapshot.next_decision_trace_id,
678 },
679 snapshot
680 .boundaries
681 .last()
682 .map(|record| record.hash.as_str()),
683 None,
684 )
685}
686
687fn checkpoint_hash(state_hash: &str, boundary_head: Option<&str>) -> Result<String, CanwuError> {
688 canonical_hash(
689 "canwu.checkpoint.v1",
690 &CheckpointHashMaterialV1 {
691 state_hash,
692 boundary_head,
693 },
694 )
695}
696
697pub(super) fn checkpoint_hash_for_configuration(
698 state_hash: &str,
699 boundary_head: Option<&str>,
700 run_manifest_hash: &str,
701 run_configuration: &RunConfigurationSnapshot,
702 revision_format_version: u32,
703 state_revision: u64,
704 replay_revision_format_version: u32,
705) -> Result<String, CanwuError> {
706 if revision_format_version == STATE_REVISION_FORMAT_VERSION {
707 return canonical_hash(
708 "canwu.checkpoint.v3",
709 &CheckpointHashMaterialV3 {
710 state_hash,
711 boundary_head,
712 run_manifest_hash: matches!(
713 run_configuration,
714 RunConfigurationSnapshot::Declared(_)
715 )
716 .then_some(run_manifest_hash),
717 revision_format_version,
718 state_revision,
719 replay_revision_format_version,
720 },
721 );
722 }
723 if revision_format_version != 0 || state_revision != 0 || replay_revision_format_version != 0 {
724 return Err(CanwuError::new(
725 ErrorCode::UnsupportedSnapshotVersion,
726 format!(
727 "state revision format {revision_format_version} is unsupported; this engine writes format {STATE_REVISION_FORMAT_VERSION}"
728 ),
729 ));
730 }
731 if !matches!(run_configuration, RunConfigurationSnapshot::Declared(_)) {
732 return checkpoint_hash(state_hash, boundary_head);
733 }
734 canonical_hash(
735 "canwu.checkpoint.v2",
736 &CheckpointHashMaterialV2 {
737 state_hash,
738 boundary_head,
739 run_manifest_hash,
740 },
741 )
742}
743
744pub(super) fn checkpoint_hash_for_commitments(
745 commitments: &CommitmentRoots,
746 run_manifest_hash: &str,
747 commitment_format_version: u32,
748 revision_format_version: u32,
749 state_revision: u64,
750 replay_revision_format_version: u32,
751) -> Result<String, CanwuError> {
752 if commitment_format_version != COMMITMENT_FORMAT_VERSION {
753 return Err(CanwuError::new(
754 ErrorCode::UnsupportedSnapshotVersion,
755 format!(
756 "commitment format {commitment_format_version} is unsupported; this engine writes format {COMMITMENT_FORMAT_VERSION}"
757 ),
758 ));
759 }
760 if revision_format_version != STATE_REVISION_FORMAT_VERSION {
761 return Err(CanwuError::new(
762 ErrorCode::UnsupportedSnapshotVersion,
763 format!(
764 "commitment format {commitment_format_version} requires state revision format {STATE_REVISION_FORMAT_VERSION}"
765 ),
766 ));
767 }
768 canonical_hash(
769 "canwu.checkpoint.v4",
770 &CheckpointHashMaterialV4 {
771 commitments,
772 run_manifest_hash,
773 commitment_format_version,
774 revision_format_version,
775 state_revision,
776 replay_revision_format_version,
777 },
778 )
779}
780
781pub(super) fn snapshot_checkpoint_hash(
782 snapshot: &SimulationSnapshot,
783) -> Result<String, CanwuError> {
784 match snapshot.commitment_format_version {
785 COMMITMENT_FORMAT_VERSION => checkpoint_hash_for_commitments(
786 snapshot.commitment_roots.as_ref().ok_or_else(|| {
787 invalid_snapshot_error("current commitment snapshot is missing its domain roots")
788 })?,
789 &snapshot.run_manifest_hash,
790 snapshot.commitment_format_version,
791 snapshot.revision_format_version,
792 snapshot.state_revision,
793 snapshot.replay_revision_format_version,
794 ),
795 0 => {
796 if snapshot.commitment_roots.is_some() {
797 return invalid_snapshot(
798 "legacy commitment snapshot cannot contain current domain roots",
799 );
800 }
801 let state_hash = snapshot_state_hash(snapshot)?;
802 checkpoint_hash_for_configuration(
803 &state_hash,
804 snapshot
805 .boundaries
806 .last()
807 .map(|record| record.hash.as_str()),
808 &snapshot.run_manifest_hash,
809 snapshot.run_configuration.as_ref().ok_or_else(|| {
810 CanwuError::new(
811 ErrorCode::InvalidRunConfiguration,
812 "snapshot is missing its run configuration",
813 )
814 })?,
815 snapshot.revision_format_version,
816 snapshot.state_revision,
817 snapshot.replay_revision_format_version,
818 )
819 }
820 version => Err(CanwuError::new(
821 ErrorCode::UnsupportedSnapshotVersion,
822 format!(
823 "commitment format {version} is unsupported; this engine reads legacy format 0 and current format {COMMITMENT_FORMAT_VERSION}"
824 ),
825 )),
826 }
827}
828
829pub(super) fn snapshot_is_at_boundary_head(snapshot: &SimulationSnapshot) -> bool {
830 let Some(last) = snapshot.boundaries.last() else {
831 return false;
832 };
833 if last.at != snapshot.now {
834 return false;
835 }
836 let admitted_attempts: BTreeSet<_> = snapshot
837 .boundaries
838 .iter()
839 .flat_map(|record| record.admitted_attempts.iter().copied())
840 .collect();
841 if admitted_attempts.len() != snapshot.command_attempts.len() {
842 return false;
843 }
844 let admitted_commands: BTreeSet<_> = snapshot
845 .boundaries
846 .iter()
847 .flat_map(|record| record.admitted_commands.iter().copied())
848 .collect();
849 if admitted_commands.len() != snapshot.commands.len() {
850 return false;
851 }
852 let admitted_ingress: BTreeSet<_> = snapshot
853 .boundaries
854 .iter()
855 .flat_map(|record| record.admitted_ingress.iter().copied())
856 .collect();
857 if admitted_ingress.len() != snapshot.ingress.len() {
858 return false;
859 }
860 let accounted_events: BTreeSet<_> = snapshot
861 .boundaries
862 .iter()
863 .flat_map(|record| {
864 record
865 .admitted_events
866 .iter()
867 .copied()
868 .chain(record.emissions.iter().map(|emission| emission.event))
869 })
870 .collect();
871 accounted_events.len() == snapshot.events.len()
872}
873
874pub(super) fn compute_boundary_hash(record: &BoundaryRecord) -> Result<String, CanwuError> {
875 #[derive(Serialize)]
876 struct BoundaryHashMaterial<'a> {
877 id: BoundaryId,
878 at: SimTime,
879 correlation_id: u64,
880 cadences: &'a [SystemCadence],
881 #[serde(skip_serializing_if = "command_attempt_id_slice_is_empty")]
882 admitted_attempts: &'a [CommandAttemptId],
883 admitted_commands: &'a [CommandId],
884 #[serde(skip_serializing_if = "Option::is_none")]
885 admitted_ingress: Option<&'a [IngressId]>,
886 #[serde(skip_serializing_if = "Option::is_none")]
887 generated_ingress: Option<&'a [BoundaryIngressGeneration]>,
888 admitted_events: &'a [EventId],
889 reservation_offers: &'a [ReservationOfferRecord],
890 reservation_requests: &'a [ReservationRequestRecord],
891 allocations: &'a [ReservationAllocation],
892 random_draws: &'a [RandomDrawId],
893 changes: &'a [BoundaryChange],
894 #[serde(skip_serializing_if = "domain_record_change_slice_is_empty")]
895 record_changes: &'a [DomainRecordChange],
896 #[serde(skip_serializing_if = "maintenance_change_slice_is_empty")]
897 maintenance_changes: &'a [super::MaintenanceChangeRecord],
898 #[serde(skip_serializing_if = "Option::is_none")]
899 maintenance_terminal_root: &'a Option<String>,
900 emissions: &'a [BoundaryEmission],
901 state_hash: &'a Option<String>,
902 previous_hash: &'a str,
903 }
904
905 canonical_hash(
906 "canwu.boundary-record.v1",
907 &BoundaryHashMaterial {
908 id: record.id,
909 at: record.at,
910 correlation_id: record.correlation_id,
911 cadences: &record.cadences,
912 admitted_attempts: &record.admitted_attempts,
913 admitted_commands: &record.admitted_commands,
914 admitted_ingress: (!record.admitted_ingress.is_empty())
915 .then_some(record.admitted_ingress.as_slice()),
916 generated_ingress: (!record.generated_ingress.is_empty())
917 .then_some(record.generated_ingress.as_slice()),
918 admitted_events: &record.admitted_events,
919 reservation_offers: &record.reservation_offers,
920 reservation_requests: &record.reservation_requests,
921 allocations: &record.allocations,
922 random_draws: &record.random_draws,
923 changes: &record.changes,
924 record_changes: &record.record_changes,
925 maintenance_changes: &record.maintenance_changes,
926 maintenance_terminal_root: &record.maintenance_terminal_root,
927 emissions: &record.emissions,
928 state_hash: &record.state_hash,
929 previous_hash: &record.previous_hash,
930 },
931 )
932}
933
934pub fn canonical_hash<T: Serialize + ?Sized>(
941 domain: &str,
942 value: &T,
943) -> Result<String, CanwuError> {
944 let encoded = serde_json::to_vec(value).map_err(|error| {
945 CanwuError::new(
946 ErrorCode::InvalidSnapshot,
947 format!("could not encode deterministic hash material: {error}"),
948 )
949 })?;
950 let mut hasher = blake3::Hasher::new();
951 hasher.update(domain.as_bytes());
952 hasher.update(&[0]);
953 hasher.update(&encoded);
954 Ok(hasher.finalize().to_hex().to_string())
955}
956
957#[must_use]
964pub fn canonical_byte_hash(domain: &str, payload: &[u8]) -> String {
965 let mut hasher = blake3::Hasher::new();
966 hasher.update(domain.as_bytes());
967 hasher.update(&[0]);
968 hasher.update(payload);
969 hasher.finalize().to_hex().to_string()
970}
971
972pub(super) fn is_canonical_hash(value: &str) -> bool {
973 value.len() == 64
974 && value
975 .bytes()
976 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
977}
978
979pub(super) fn commitment_roots_are_canonical(roots: &CommitmentRoots) -> bool {
980 [
981 &roots.world,
982 &roots.knowledge,
983 &roots.plugin_components,
984 &roots.domain_records,
985 &roots.scheduler,
986 &roots.commands,
987 &roots.events,
988 &roots.ingress,
989 &roots.random,
990 &roots.boundary_chain,
991 &roots.identity,
992 &roots.control,
993 ]
994 .into_iter()
995 .all(|root| is_canonical_hash(root))
996 && (roots.decisions.is_empty() || is_canonical_hash(&roots.decisions))
997}