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, 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)]
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    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
893/// Computes the engine's canonical JSON commitment for plugin-owned data.
894///
895/// The caller supplies a stable, versioned domain string. This is the same
896/// deterministic encoding and domain separation used by the runtime's own
897/// commitments, allowing extension operation identifiers to remain replay
898/// compatible with kernel validation.
899pub 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/// Computes a domain-separated BLAKE3 commitment over an already canonical
917/// byte payload.
918///
919/// Extension contracts should prefer [`canonical_hash`] for serde values. This
920/// raw form exists for frozen binary contracts, such as Merkle interior nodes,
921/// whose byte encoding is defined independently of JSON.
922#[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}