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, manifest,
14 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 canonical_sorted_hash_by("canwu.commitment.domain-records.v1", records, |record| {
282 record.reference.clone()
283 })
284}
285
286pub(super) fn decision_commitment_root(decisions: &DecisionState) -> Result<String, CanwuError> {
287 if decisions.is_empty() {
288 return Ok(String::new());
289 }
290 canonical_hash("canwu.commitment.decisions.v1", decisions)
291}
292
293pub(super) fn scheduler_commitment_root(
294 now: SimTime,
295 scheduled: &[ScheduledRecord],
296) -> Result<String, CanwuError> {
297 canonical_hash(
298 "canwu.commitment.scheduler.v1",
299 &SchedulerCommitmentMaterial {
300 now,
301 scheduled: canonical_sorted_hash_by(
302 "canwu.commitment.scheduler.entries.v1",
303 scheduled,
304 |record| record.key.clone(),
305 )?,
306 },
307 )
308}
309
310pub(super) fn random_stream_commitment_root(
311 streams: &[RandomStreamState],
312) -> Result<String, CanwuError> {
313 canonical_sorted_hash_by("canwu.commitment.random.streams.v1", streams, |stream| {
314 stream.key.clone()
315 })
316}
317
318#[allow(clippy::too_many_arguments)]
319pub(super) fn identity_commitment_root(
320 engine_version: &str,
321 snapshot_format_version: u32,
322 run_manifest: &RunManifest,
323 run_manifest_hash: &str,
324 initial_time: SimTime,
325 initial_scenario: Option<&Scenario>,
326 authority_root_seed: u64,
327 plugin_descriptors: &[PluginDescriptor],
328 schema: &SchemaRegistry,
329) -> Result<String, CanwuError> {
330 canonical_hash(
331 "canwu.commitment.identity.v1",
332 &IdentityCommitmentMaterial {
333 engine_version,
334 snapshot_format_version,
335 run_manifest,
336 run_manifest_hash,
337 initial_time,
338 initial_scenario,
339 authority_root_seed,
340 plugin_descriptors: canonical_sorted_hash_by(
341 "canwu.commitment.identity.plugins.v1",
342 plugin_descriptors,
343 |descriptor| descriptor.name.clone(),
344 )?,
345 schema,
346 },
347 )
348}
349
350fn commitment_roots(
351 material: &StateHashMaterial<'_>,
352 boundary_head: Option<&str>,
353 journal_roots: Option<&JournalCommitmentRoots>,
354) -> Result<CommitmentRoots, CanwuError> {
355 let world = world_commitment_root(material.world, material.entities.unwrap_or_default())?;
356 let knowledge = knowledge_commitment_root(material.knowledge)?;
357 let plugin_components = plugin_component_commitment_root(material.plugin_components)?;
358 let domain_records = domain_record_commitment_root(material.domain_records)?;
359 let decisions = decision_commitment_root(material.decisions)?;
360 let scheduler = scheduler_commitment_root(material.now, material.scheduled)?;
361 let command_root = match journal_roots {
362 Some(roots) => roots.commands.clone(),
363 None => canonical_sorted_hash_by(
364 "canwu.commitment.commands.accepted.v1",
365 material.commands,
366 |record| record.id,
367 )?,
368 };
369 let attempt_root = match journal_roots {
370 Some(roots) => roots.attempts.clone(),
371 None => canonical_sorted_hash_by(
372 "canwu.commitment.commands.attempts.v1",
373 material.command_attempts,
374 |record| record.id,
375 )?,
376 };
377 let commands = canonical_hash(
378 "canwu.commitment.commands.v1",
379 &CommandCommitmentMaterial {
380 commands: command_root,
381 attempts: attempt_root,
382 },
383 )?;
384 let events = match journal_roots {
385 Some(roots) => roots.events.clone(),
386 None => canonical_sorted_hash_by("canwu.commitment.events.v1", material.events, |event| {
387 event.id
388 })?,
389 };
390 let ingress = match journal_roots {
391 Some(roots) => roots.ingress.clone(),
392 None => {
393 canonical_sorted_hash_by("canwu.commitment.ingress.v1", material.ingress, |record| {
394 record.id
395 })?
396 }
397 };
398 let random = canonical_hash(
399 "canwu.commitment.random.v1",
400 &RandomCommitmentMaterial {
401 root_seed: material.root_seed,
402 streams: random_stream_commitment_root(material.random_streams)?,
403 draws: match journal_roots {
404 Some(roots) => roots.random_draws.clone(),
405 None => canonical_sorted_hash_by(
406 "canwu.commitment.random.draws.v1",
407 material.random_draws,
408 |draw| draw.id,
409 )?,
410 },
411 },
412 )?;
413 let boundary_chain = canonical_hash(
414 "canwu.commitment.boundary-chain.v1",
415 boundary_head.unwrap_or(GENESIS_BOUNDARY_HASH),
416 )?;
417 let identity = identity_commitment_root(
418 material.engine_version,
419 material.snapshot_format_version,
420 material.run_manifest,
421 material.run_manifest_hash,
422 material.initial_time,
423 material.initial_scenario,
424 material.authority_root_seed,
425 material.plugin_descriptors,
426 material.schema,
427 )?;
428 let control = canonical_hash(
429 "canwu.commitment.control.v1",
430 &ControlCommitmentMaterial {
431 plugin_registration_closed: material.plugin_registration_closed,
432 next_event_id: material.next_event_id,
433 next_command_id: material.next_command_id,
434 next_command_attempt_id: material.next_command_attempt_id,
435 next_ingress_id: material.next_ingress_id,
436 next_boundary_id: material.next_boundary_id,
437 next_random_draw_id: material.next_random_draw_id,
438 next_knowledge_record_id: material.next_knowledge_record_id,
439 next_schedule_sequence: material.next_schedule_sequence,
440 next_correlation_id: material.next_correlation_id,
441 next_decision_trace_id: material.next_decision_trace_id,
442 },
443 )?;
444 Ok(CommitmentRoots {
445 world,
446 knowledge,
447 plugin_components,
448 domain_records,
449 decisions,
450 scheduler,
451 commands,
452 events,
453 ingress,
454 random,
455 boundary_chain,
456 identity,
457 control,
458 })
459}
460
461pub(super) fn runtime_commitment_roots(
462 domain: &RuntimeDomainCommitmentRoots,
463 journal: &JournalCommitmentRoots,
464 root_seed: u64,
465 boundary_head: Option<&str>,
466 control: &ControlCommitmentMaterial,
467) -> Result<CommitmentRoots, CanwuError> {
468 let commands = canonical_hash(
469 "canwu.commitment.commands.v1",
470 &CommandCommitmentMaterial {
471 commands: journal.commands.clone(),
472 attempts: journal.attempts.clone(),
473 },
474 )?;
475 let random = canonical_hash(
476 "canwu.commitment.random.v1",
477 &RandomCommitmentMaterial {
478 root_seed,
479 streams: domain.random_streams.clone(),
480 draws: journal.random_draws.clone(),
481 },
482 )?;
483 Ok(CommitmentRoots {
484 world: domain.world.clone(),
485 knowledge: domain.knowledge.clone(),
486 plugin_components: domain.plugin_components.clone(),
487 domain_records: domain.domain_records.clone(),
488 decisions: domain.decisions.clone(),
489 scheduler: domain.scheduler.clone(),
490 commands,
491 events: journal.events.clone(),
492 ingress: journal.ingress.clone(),
493 random,
494 boundary_chain: canonical_hash(
495 "canwu.commitment.boundary-chain.v1",
496 boundary_head.unwrap_or(GENESIS_BOUNDARY_HASH),
497 )?,
498 identity: domain.identity.clone(),
499 control: canonical_hash("canwu.commitment.control.v1", control)?,
500 })
501}
502
503pub(super) fn boundary_state_hash_for_commitments(
504 commitments: &CommitmentRoots,
505) -> Result<String, CanwuError> {
506 Ok(format!(
507 "{BOUNDARY_STATE_HASH_V1_PREFIX}{}",
508 canonical_hash("canwu.boundary-state.v1", commitments)?
509 ))
510}
511
512pub(super) fn authoritative_run_identity(
513 run_manifest: &RunManifest,
514 run_manifest_hash: &str,
515 run_configuration: &RunConfigurationSnapshot,
516) -> Result<(RunManifest, String), CanwuError> {
517 if !matches!(run_configuration, RunConfigurationSnapshot::Declared(_)) {
518 return Ok((run_manifest.clone(), run_manifest_hash.to_owned()));
519 }
520 let mut authoritative_manifest = run_manifest.clone();
521 let RunManifest::Declared {
522 run_configuration, ..
523 } = &mut authoritative_manifest;
524 **run_configuration = ArtifactManifest::new(
525 "canwu.core",
526 "authoritative-policy-excluded",
527 "1",
528 policy::authoritative_configuration_hash()?,
529 )?;
530 let authoritative_manifest_hash = manifest::hash(&authoritative_manifest)?;
531 Ok((authoritative_manifest, authoritative_manifest_hash))
532}
533
534pub(super) fn snapshot_state_hash(snapshot: &SimulationSnapshot) -> Result<String, CanwuError> {
535 let Some(run_manifest) = &snapshot.run_manifest else {
536 return Err(CanwuError::new(
537 ErrorCode::InvalidRunManifest,
538 "snapshot is missing its run manifest",
539 ));
540 };
541 let run_configuration = snapshot.run_configuration.as_ref().ok_or_else(|| {
542 CanwuError::new(
543 ErrorCode::InvalidRunConfiguration,
544 "snapshot is missing its run configuration",
545 )
546 })?;
547 let (authoritative_manifest, authoritative_manifest_hash) =
548 authoritative_run_identity(run_manifest, &snapshot.run_manifest_hash, run_configuration)?;
549 let initial_scenario = committed_initial_scenario(snapshot.initial_scenario.as_ref());
550 state_hash(&StateHashMaterial {
551 engine_version: &snapshot.engine_version,
552 snapshot_format_version: snapshot.snapshot_format_version,
553 run_manifest: &authoritative_manifest,
554 run_manifest_hash: &authoritative_manifest_hash,
555 initial_time: snapshot.initial_time,
556 initial_scenario: initial_scenario.as_ref(),
557 now: snapshot.now,
558 plugin_registration_closed: snapshot.plugin_registration_closed,
559 entities: committed_entities(&snapshot.entities, &snapshot.world),
560 world: &snapshot.world,
561 knowledge: &snapshot.knowledge,
562 events: &snapshot.events,
563 commands: &snapshot.commands,
564 command_attempts: &snapshot.command_attempts,
565 ingress: &snapshot.ingress,
566 plugin_components: &snapshot.plugin_components,
567 domain_records: &snapshot.domain_records,
568 decisions: &snapshot.decisions,
569 plugin_descriptors: &snapshot.plugin_descriptors,
570 schema: &snapshot.schema,
571 scheduled: &snapshot.scheduled,
572 root_seed: snapshot.root_seed,
573 authority_root_seed: snapshot.authority_root_seed,
574 random_streams: &snapshot.random_streams,
575 random_draws: &snapshot.random_draws,
576 next_event_id: snapshot.next_event_id,
577 next_command_id: snapshot.next_command_id,
578 next_command_attempt_id: snapshot.next_command_attempt_id,
579 next_ingress_id: snapshot.next_ingress_id,
580 next_boundary_id: snapshot.next_boundary_id,
581 next_random_draw_id: snapshot.next_random_draw_id,
582 next_knowledge_record_id: snapshot.next_knowledge_record_id,
583 next_schedule_sequence: snapshot.next_schedule_sequence,
584 next_correlation_id: snapshot.next_correlation_id,
585 next_decision_trace_id: snapshot.next_decision_trace_id,
586 })
587}
588
589pub(super) fn snapshot_boundary_head_state_hash(
590 snapshot: &SimulationSnapshot,
591) -> Result<String, CanwuError> {
592 let boundary = snapshot
593 .boundaries
594 .last()
595 .ok_or_else(|| invalid_snapshot_error("snapshot has no boundary head commitment"))?;
596 match boundary_state_hash_format(boundary.state_hash.as_deref())? {
597 BoundaryStateHashFormat::LegacyV0 => snapshot_state_hash(snapshot),
598 BoundaryStateHashFormat::CommitmentsV1 => {
599 if snapshot.commitment_format_version != COMMITMENT_FORMAT_VERSION {
600 return invalid_snapshot(
601 "boundary state commitment v1 requires current domain commitments",
602 );
603 }
604 let mut roots = snapshot.commitment_roots.clone().ok_or_else(|| {
605 invalid_snapshot_error(
606 "boundary state commitment v1 is missing current domain commitments",
607 )
608 })?;
609 roots.boundary_chain = canonical_hash(
610 "canwu.commitment.boundary-chain.v1",
611 boundary.previous_hash.as_str(),
612 )?;
613 boundary_state_hash_for_commitments(&roots)
614 }
615 }
616}
617
618pub(super) fn snapshot_commitment_roots(
619 snapshot: &SimulationSnapshot,
620) -> Result<CommitmentRoots, CanwuError> {
621 let Some(run_manifest) = &snapshot.run_manifest else {
622 return Err(CanwuError::new(
623 ErrorCode::InvalidRunManifest,
624 "snapshot is missing its run manifest",
625 ));
626 };
627 let run_configuration = snapshot.run_configuration.as_ref().ok_or_else(|| {
628 CanwuError::new(
629 ErrorCode::InvalidRunConfiguration,
630 "snapshot is missing its run configuration",
631 )
632 })?;
633 let (authoritative_manifest, authoritative_manifest_hash) =
634 authoritative_run_identity(run_manifest, &snapshot.run_manifest_hash, run_configuration)?;
635 let initial_scenario = committed_initial_scenario(snapshot.initial_scenario.as_ref());
636 commitment_roots(
637 &StateHashMaterial {
638 engine_version: &snapshot.engine_version,
639 snapshot_format_version: snapshot.snapshot_format_version,
640 run_manifest: &authoritative_manifest,
641 run_manifest_hash: &authoritative_manifest_hash,
642 initial_time: snapshot.initial_time,
643 initial_scenario: initial_scenario.as_ref(),
644 now: snapshot.now,
645 plugin_registration_closed: snapshot.plugin_registration_closed,
646 entities: committed_entities(&snapshot.entities, &snapshot.world),
647 world: &snapshot.world,
648 knowledge: &snapshot.knowledge,
649 events: &snapshot.events,
650 commands: &snapshot.commands,
651 command_attempts: &snapshot.command_attempts,
652 ingress: &snapshot.ingress,
653 plugin_components: &snapshot.plugin_components,
654 domain_records: &snapshot.domain_records,
655 decisions: &snapshot.decisions,
656 plugin_descriptors: &snapshot.plugin_descriptors,
657 schema: &snapshot.schema,
658 scheduled: &snapshot.scheduled,
659 root_seed: snapshot.root_seed,
660 authority_root_seed: snapshot.authority_root_seed,
661 random_streams: &snapshot.random_streams,
662 random_draws: &snapshot.random_draws,
663 next_event_id: snapshot.next_event_id,
664 next_command_id: snapshot.next_command_id,
665 next_command_attempt_id: snapshot.next_command_attempt_id,
666 next_ingress_id: snapshot.next_ingress_id,
667 next_boundary_id: snapshot.next_boundary_id,
668 next_random_draw_id: snapshot.next_random_draw_id,
669 next_knowledge_record_id: snapshot.next_knowledge_record_id,
670 next_schedule_sequence: snapshot.next_schedule_sequence,
671 next_correlation_id: snapshot.next_correlation_id,
672 next_decision_trace_id: snapshot.next_decision_trace_id,
673 },
674 snapshot
675 .boundaries
676 .last()
677 .map(|record| record.hash.as_str()),
678 None,
679 )
680}
681
682fn checkpoint_hash(state_hash: &str, boundary_head: Option<&str>) -> Result<String, CanwuError> {
683 canonical_hash(
684 "canwu.checkpoint.v1",
685 &CheckpointHashMaterialV1 {
686 state_hash,
687 boundary_head,
688 },
689 )
690}
691
692pub(super) fn checkpoint_hash_for_configuration(
693 state_hash: &str,
694 boundary_head: Option<&str>,
695 run_manifest_hash: &str,
696 run_configuration: &RunConfigurationSnapshot,
697 revision_format_version: u32,
698 state_revision: u64,
699 replay_revision_format_version: u32,
700) -> Result<String, CanwuError> {
701 if revision_format_version == STATE_REVISION_FORMAT_VERSION {
702 return canonical_hash(
703 "canwu.checkpoint.v3",
704 &CheckpointHashMaterialV3 {
705 state_hash,
706 boundary_head,
707 run_manifest_hash: matches!(
708 run_configuration,
709 RunConfigurationSnapshot::Declared(_)
710 )
711 .then_some(run_manifest_hash),
712 revision_format_version,
713 state_revision,
714 replay_revision_format_version,
715 },
716 );
717 }
718 if revision_format_version != 0 || state_revision != 0 || replay_revision_format_version != 0 {
719 return Err(CanwuError::new(
720 ErrorCode::UnsupportedSnapshotVersion,
721 format!(
722 "state revision format {revision_format_version} is unsupported; this engine writes format {STATE_REVISION_FORMAT_VERSION}"
723 ),
724 ));
725 }
726 if !matches!(run_configuration, RunConfigurationSnapshot::Declared(_)) {
727 return checkpoint_hash(state_hash, boundary_head);
728 }
729 canonical_hash(
730 "canwu.checkpoint.v2",
731 &CheckpointHashMaterialV2 {
732 state_hash,
733 boundary_head,
734 run_manifest_hash,
735 },
736 )
737}
738
739pub(super) fn checkpoint_hash_for_commitments(
740 commitments: &CommitmentRoots,
741 run_manifest_hash: &str,
742 commitment_format_version: u32,
743 revision_format_version: u32,
744 state_revision: u64,
745 replay_revision_format_version: u32,
746) -> Result<String, CanwuError> {
747 if commitment_format_version != COMMITMENT_FORMAT_VERSION {
748 return Err(CanwuError::new(
749 ErrorCode::UnsupportedSnapshotVersion,
750 format!(
751 "commitment format {commitment_format_version} is unsupported; this engine writes format {COMMITMENT_FORMAT_VERSION}"
752 ),
753 ));
754 }
755 if revision_format_version != STATE_REVISION_FORMAT_VERSION {
756 return Err(CanwuError::new(
757 ErrorCode::UnsupportedSnapshotVersion,
758 format!(
759 "commitment format {commitment_format_version} requires state revision format {STATE_REVISION_FORMAT_VERSION}"
760 ),
761 ));
762 }
763 canonical_hash(
764 "canwu.checkpoint.v4",
765 &CheckpointHashMaterialV4 {
766 commitments,
767 run_manifest_hash,
768 commitment_format_version,
769 revision_format_version,
770 state_revision,
771 replay_revision_format_version,
772 },
773 )
774}
775
776pub(super) fn snapshot_checkpoint_hash(
777 snapshot: &SimulationSnapshot,
778) -> Result<String, CanwuError> {
779 match snapshot.commitment_format_version {
780 COMMITMENT_FORMAT_VERSION => checkpoint_hash_for_commitments(
781 snapshot.commitment_roots.as_ref().ok_or_else(|| {
782 invalid_snapshot_error("current commitment snapshot is missing its domain roots")
783 })?,
784 &snapshot.run_manifest_hash,
785 snapshot.commitment_format_version,
786 snapshot.revision_format_version,
787 snapshot.state_revision,
788 snapshot.replay_revision_format_version,
789 ),
790 0 => {
791 if snapshot.commitment_roots.is_some() {
792 return invalid_snapshot(
793 "legacy commitment snapshot cannot contain current domain roots",
794 );
795 }
796 let state_hash = snapshot_state_hash(snapshot)?;
797 checkpoint_hash_for_configuration(
798 &state_hash,
799 snapshot
800 .boundaries
801 .last()
802 .map(|record| record.hash.as_str()),
803 &snapshot.run_manifest_hash,
804 snapshot.run_configuration.as_ref().ok_or_else(|| {
805 CanwuError::new(
806 ErrorCode::InvalidRunConfiguration,
807 "snapshot is missing its run configuration",
808 )
809 })?,
810 snapshot.revision_format_version,
811 snapshot.state_revision,
812 snapshot.replay_revision_format_version,
813 )
814 }
815 version => Err(CanwuError::new(
816 ErrorCode::UnsupportedSnapshotVersion,
817 format!(
818 "commitment format {version} is unsupported; this engine reads legacy format 0 and current format {COMMITMENT_FORMAT_VERSION}"
819 ),
820 )),
821 }
822}
823
824pub(super) fn snapshot_is_at_boundary_head(snapshot: &SimulationSnapshot) -> bool {
825 let Some(last) = snapshot.boundaries.last() else {
826 return false;
827 };
828 if last.at != snapshot.now {
829 return false;
830 }
831 let admitted_attempts: BTreeSet<_> = snapshot
832 .boundaries
833 .iter()
834 .flat_map(|record| record.admitted_attempts.iter().copied())
835 .collect();
836 if admitted_attempts.len() != snapshot.command_attempts.len() {
837 return false;
838 }
839 let admitted_commands: BTreeSet<_> = snapshot
840 .boundaries
841 .iter()
842 .flat_map(|record| record.admitted_commands.iter().copied())
843 .collect();
844 if admitted_commands.len() != snapshot.commands.len() {
845 return false;
846 }
847 let admitted_ingress: BTreeSet<_> = snapshot
848 .boundaries
849 .iter()
850 .flat_map(|record| record.admitted_ingress.iter().copied())
851 .collect();
852 if admitted_ingress.len() != snapshot.ingress.len() {
853 return false;
854 }
855 let accounted_events: BTreeSet<_> = snapshot
856 .boundaries
857 .iter()
858 .flat_map(|record| {
859 record
860 .admitted_events
861 .iter()
862 .copied()
863 .chain(record.emissions.iter().map(|emission| emission.event))
864 })
865 .collect();
866 accounted_events.len() == snapshot.events.len()
867}
868
869pub(super) fn compute_boundary_hash(record: &BoundaryRecord) -> Result<String, CanwuError> {
870 #[derive(Serialize)]
871 struct BoundaryHashMaterial<'a> {
872 id: BoundaryId,
873 at: SimTime,
874 correlation_id: u64,
875 cadences: &'a [SystemCadence],
876 #[serde(skip_serializing_if = "command_attempt_id_slice_is_empty")]
877 admitted_attempts: &'a [CommandAttemptId],
878 admitted_commands: &'a [CommandId],
879 #[serde(skip_serializing_if = "Option::is_none")]
880 admitted_ingress: Option<&'a [IngressId]>,
881 #[serde(skip_serializing_if = "Option::is_none")]
882 generated_ingress: Option<&'a [BoundaryIngressGeneration]>,
883 admitted_events: &'a [EventId],
884 reservation_offers: &'a [ReservationOfferRecord],
885 reservation_requests: &'a [ReservationRequestRecord],
886 allocations: &'a [ReservationAllocation],
887 random_draws: &'a [RandomDrawId],
888 changes: &'a [BoundaryChange],
889 #[serde(skip_serializing_if = "domain_record_change_slice_is_empty")]
890 record_changes: &'a [DomainRecordChange],
891 emissions: &'a [BoundaryEmission],
892 state_hash: &'a Option<String>,
893 previous_hash: &'a str,
894 }
895
896 canonical_hash(
897 "canwu.boundary-record.v1",
898 &BoundaryHashMaterial {
899 id: record.id,
900 at: record.at,
901 correlation_id: record.correlation_id,
902 cadences: &record.cadences,
903 admitted_attempts: &record.admitted_attempts,
904 admitted_commands: &record.admitted_commands,
905 admitted_ingress: (!record.admitted_ingress.is_empty())
906 .then_some(record.admitted_ingress.as_slice()),
907 generated_ingress: (!record.generated_ingress.is_empty())
908 .then_some(record.generated_ingress.as_slice()),
909 admitted_events: &record.admitted_events,
910 reservation_offers: &record.reservation_offers,
911 reservation_requests: &record.reservation_requests,
912 allocations: &record.allocations,
913 random_draws: &record.random_draws,
914 changes: &record.changes,
915 record_changes: &record.record_changes,
916 emissions: &record.emissions,
917 state_hash: &record.state_hash,
918 previous_hash: &record.previous_hash,
919 },
920 )
921}
922
923pub fn canonical_hash<T: Serialize + ?Sized>(
930 domain: &str,
931 value: &T,
932) -> Result<String, CanwuError> {
933 let encoded = serde_json::to_vec(value).map_err(|error| {
934 CanwuError::new(
935 ErrorCode::InvalidSnapshot,
936 format!("could not encode deterministic hash material: {error}"),
937 )
938 })?;
939 let mut hasher = blake3::Hasher::new();
940 hasher.update(domain.as_bytes());
941 hasher.update(&[0]);
942 hasher.update(&encoded);
943 Ok(hasher.finalize().to_hex().to_string())
944}
945
946#[must_use]
953pub fn canonical_byte_hash(domain: &str, payload: &[u8]) -> String {
954 let mut hasher = blake3::Hasher::new();
955 hasher.update(domain.as_bytes());
956 hasher.update(&[0]);
957 hasher.update(payload);
958 hasher.finalize().to_hex().to_string()
959}
960
961pub(super) fn is_canonical_hash(value: &str) -> bool {
962 value.len() == 64
963 && value
964 .bytes()
965 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
966}
967
968pub(super) fn commitment_roots_are_canonical(roots: &CommitmentRoots) -> bool {
969 [
970 &roots.world,
971 &roots.knowledge,
972 &roots.plugin_components,
973 &roots.domain_records,
974 &roots.scheduler,
975 &roots.commands,
976 &roots.events,
977 &roots.ingress,
978 &roots.random,
979 &roots.boundary_chain,
980 &roots.identity,
981 &roots.control,
982 ]
983 .into_iter()
984 .all(|root| is_canonical_hash(root))
985 && (roots.decisions.is_empty() || is_canonical_hash(&roots.decisions))
986}