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