Skip to main content

canwu_sim/runtime/
hashing.rs

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)]
20/// Canonical roots for independent authoritative state and evidence domains.
21pub 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
923/// Computes the engine's canonical JSON commitment for plugin-owned data.
924///
925/// The caller supplies a stable, versioned domain string. This is the same
926/// deterministic encoding and domain separation used by the runtime's own
927/// commitments, allowing extension operation identifiers to remain replay
928/// compatible with kernel validation.
929pub 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/// Computes a domain-separated BLAKE3 commitment over an already canonical
947/// byte payload.
948///
949/// Extension contracts should prefer [`canonical_hash`] for serde values. This
950/// raw form exists for frozen binary contracts, such as Merkle interior nodes,
951/// whose byte encoding is defined independently of JSON.
952#[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}