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