Skip to main content

canic_host/fleet_ensure/ops/infrastructure_bootstrap/
mod.rs

1//! Seal supplied custody and compile initialization through the existing Ensure effect contract.
2//!
3//! Workflow owns observation budgets, approval and persistence before effects.
4
5pub(in crate::fleet_ensure) mod automatic;
6pub(in crate::fleet_ensure) mod convergence;
7pub(in crate::fleet_ensure) mod declarations;
8pub(in crate::fleet_ensure) mod inspection;
9pub(in crate::fleet_ensure) mod publication;
10pub(in crate::fleet_ensure) mod registration;
11pub(in crate::fleet_ensure) mod registration_recovery;
12pub(in crate::fleet_ensure) mod survey;
13pub(in crate::fleet_ensure) mod terminal;
14#[cfg(test)]
15pub(in crate::fleet_ensure::ops) mod tests;
16
17use crate::{
18    fleet_ensure::{
19        model::{
20            CanisterRuntimeStatus, DesiredFleet, FleetEnsurePlan, FleetEnsurePlanScope,
21            FleetEnsureStateRecord, FleetObservation, LiveCanister,
22            infrastructure_bootstrap::{
23                BootstrapCoordinatorSelection, InfrastructureBootstrapRecord,
24            },
25        },
26        ops::{
27            EnsureStateError, canic_init,
28            capacity_import::{
29                admission::observer::{inventory, management},
30                journal::CapacityImportJournalError,
31            },
32            resolve_desired_artifacts,
33        },
34        policy::{EnsurePolicyError, infrastructure_bootstrap},
35        view::infrastructure_bootstrap::InfrastructureBootstrapObservation,
36    },
37    icp::IcpCli,
38};
39use candid::{CandidType, Nat, Principal};
40use canic_contracts::{dto::fleet_registry::FleetRegistry, ids::CanonicalNetworkId};
41use canic_core::{
42    cdk::utils::hash::{decode_hex, hex_bytes},
43    control_plane_support::ops::fleet_registry::FleetRegistryOps,
44    shared_support::fleet_admission_policy::bind_initial_fleet_admission_policy,
45};
46use sha2_host::{Digest, Sha256};
47use std::{
48    collections::{BTreeMap, BTreeSet},
49    path::Path,
50};
51use thiserror::Error;
52
53/// A bootstrap failure preserves supplied identities and all retained effect receipts.
54#[derive(Debug, Error)]
55pub enum InfrastructureBootstrapError {
56    #[error(
57        "bootstrap registration needs {required_cycles} cycles of execution allowance; controlled balance {available_cycles}, funding shortfall {funding_shortfall_cycles}; remaining approved execution allowance {remaining_execution_cycles}, execution-budget shortfall {execution_shortfall_cycles}; preserve the retained operation and review it with fleet bootstrap --recover <plan-sha256>"
58    )]
59    RegistrationBudget {
60        required_cycles: u128,
61        available_cycles: u128,
62        funding_shortfall_cycles: u128,
63        remaining_execution_cycles: u128,
64        execution_shortfall_cycles: u128,
65    },
66    #[error(
67        "bootstrap registration recovery requires approval of review {review_sha256}; the original operation is retained"
68    )]
69    RegistrationApproval { review_sha256: String },
70    #[error(
71        "bootstrap inspection allowance exhausted; retain original effects and balances and review continuation with canic --environment <environment> fleet recover-attempts <fleet>"
72    )]
73    InspectionBudget,
74    #[error("infrastructure bootstrap source, plan or current authority differs")]
75    Integrity,
76    #[error("bootstrap observation is unavailable: {0}")]
77    Observation(String),
78    #[error(transparent)]
79    Policy(#[from] EnsurePolicyError),
80    #[error(transparent)]
81    State(#[from] EnsureStateError),
82    #[error(transparent)]
83    Inspection(#[from] CapacityImportJournalError),
84    #[error(transparent)]
85    Encoding(#[from] serde_json::Error),
86    #[error(transparent)]
87    Io(#[from] std::io::Error),
88}
89
90/// Bind original observations without rebasing them during apply or restart.
91pub fn seal_sources(
92    mut record: InfrastructureBootstrapRecord,
93) -> Result<InfrastructureBootstrapRecord, InfrastructureBootstrapError> {
94    verify_declarations(&record)?;
95    record.source_sha256 = source_digest(&record)?;
96    Ok(record)
97}
98
99fn verify_declarations(
100    record: &InfrastructureBootstrapRecord,
101) -> Result<(), InfrastructureBootstrapError> {
102    let invalid = || InfrastructureBootstrapError::Integrity;
103    let digest: [u8; 32] = Sha256::digest(record.declarations_toml.as_bytes()).into();
104    if record.declarations_toml.len() > 256 * 1024 || digest != record.declarations_sha256 {
105        return Err(invalid());
106    }
107    let declarations: declarations::BootstrapDeclarations =
108        toml::from_str(&record.declarations_toml).map_err(|_| invalid())?;
109    if declarations.schema_version != 1
110        || Principal::from_text(&declarations.operator).ok() != Some(record.operator)
111        || declarations.network_root_key_sha256 != hex_bytes(record.network_root_key_sha256)
112        || declarations.canisters.len() != record.sources.len()
113        || declarations.root_owned.len() != record.held_sources.len()
114    {
115        return Err(invalid());
116    }
117    let mut seen = BTreeSet::new();
118    for declaration in &declarations.canisters {
119        let binding = declaration.binding().map_err(|_| invalid())?;
120        if !seen.insert(binding.canister_id)
121            || !declaration.no_external_obligations
122            || !declaration.no_other_fleet_ownership
123            || declaration.evidence.trim().is_empty()
124        {
125            return Err(invalid());
126        }
127        let source = record
128            .sources
129            .values()
130            .find(|source| source.sample.binding.canister_id == binding.canister_id)
131            .ok_or_else(invalid)?;
132        let mut expected = source.sample.binding.clone();
133        expected.stopped = true;
134        expected.snapshots_size_bytes = 0;
135        if expected != binding || source.disposition != declaration.disposition(digest) {
136            return Err(invalid());
137        }
138    }
139    for declaration in &declarations.root_owned {
140        let binding = declaration.binding()?;
141        if !seen.insert(binding.canister) {
142            return Err(invalid());
143        }
144        let source = record
145            .held_sources
146            .values()
147            .find(|source| source.custody.canister == binding.canister)
148            .ok_or_else(invalid)?;
149        if source.custody != binding
150            || source.disposition != declaration.disposition(digest)
151            || !binding.controllers.contains(&source.root)
152        {
153            return Err(invalid());
154        }
155    }
156    Ok(())
157}
158
159fn source_digest(
160    record: &InfrastructureBootstrapRecord,
161) -> Result<[u8; 32], InfrastructureBootstrapError> {
162    let mut projection = record.clone();
163    projection.source_sha256 = [0; 32];
164    let mut hash = Sha256::new();
165    hash.update(b"canic.infrastructure-bootstrap.sources.v1\0");
166    hash.update(serde_json::to_vec(&projection)?);
167    Ok(hash.finalize().into())
168}
169
170/// Quote initialization and registration from current artifacts and sealed source custody.
171/// Workflow must admit the complete budget before retaining this as a fresh review.
172pub fn prepare(
173    root: &Path,
174    desired: &DesiredFleet,
175    record: &InfrastructureBootstrapRecord,
176    desired_sha256: &str,
177    time: u64,
178) -> Result<FleetEnsurePlan, InfrastructureBootstrapError> {
179    prepare_with_funding(
180        root,
181        desired,
182        record,
183        desired_sha256,
184        time,
185        &BTreeMap::new(),
186    )
187}
188
189fn prepare_with_funding(
190    root: &Path,
191    desired: &DesiredFleet,
192    record: &InfrastructureBootstrapRecord,
193    desired_sha256: &str,
194    time: u64,
195    funding_minima: &BTreeMap<String, u128>,
196) -> Result<FleetEnsurePlan, InfrastructureBootstrapError> {
197    if source_digest(record)? != record.source_sha256 {
198        return Err(InfrastructureBootstrapError::Integrity);
199    }
200    verify_declarations(record)?;
201    publication::validate_seed(desired, record)?;
202    verify_ready_coordinator(root, desired, record)?;
203    let observation = original_observation(desired, record);
204    let artifacts = resolve_desired_artifacts(root, desired)?;
205    let mut plan = infrastructure_bootstrap::compile(
206        desired,
207        &artifacts,
208        record,
209        &observation,
210        desired_sha256,
211        time,
212        funding_minima,
213    )?;
214    let registration = registration::quote(root, &plan, &artifacts)?;
215    plan.conservation.maximum_execution_burn_cycles = plan
216        .conservation
217        .maximum_execution_burn_cycles
218        .checked_add(registration)
219        .ok_or(EnsurePolicyError::ArithmeticOverflow {
220            field: "bootstrap initialization and registration",
221        })?;
222    let available = plan
223        .conservation
224        .observed_controlled_cycles
225        .checked_add(plan.conservation.maximum_new_funding_cycles)
226        .ok_or(EnsurePolicyError::ArithmeticOverflow {
227            field: "bootstrap available cycles",
228        })?;
229    plan.conservation.expected_post_operation_cycles =
230        available.saturating_sub(plan.conservation.maximum_execution_burn_cycles);
231    plan.plan_sha256 = crate::fleet_ensure::policy::expected_plan_sha256(&plan);
232    publication::validate_capacity(&plan)?;
233    Ok(plan)
234}
235
236/// Put an affordable shortfall into the same explicit funding approval before effects.
237pub(in crate::fleet_ensure) fn prepare_review(
238    root: &Path,
239    desired: &DesiredFleet,
240    record: &InfrastructureBootstrapRecord,
241    desired_sha256: &str,
242    time: u64,
243) -> Result<FleetEnsurePlan, InfrastructureBootstrapError> {
244    let plan = prepare(root, desired, record, desired_sha256, time)?;
245    let shortfall = match infrastructure_bootstrap::admit_review(&plan) {
246        Ok(()) => return Ok(plan),
247        Err(EnsurePolicyError::InfrastructureBootstrap(
248            infrastructure_bootstrap::InfrastructureBootstrapError::Budget { shortfall, .. },
249        )) => shortfall,
250        Err(error) => return Err(error.into()),
251    };
252    // Store owns artifact publication. Keep this exact credit in the ordinary
253    // funding journal; it adds shared native surplus, never an untracked top-up.
254    let target = plan
255        .canisters
256        .iter()
257        .find(|target| {
258            desired.canisters.iter().any(|configured| {
259                configured.name == target.name
260                    && configured.kind == crate::fleet_ensure::model::DesiredCanisterKind::Store
261            })
262        })
263        .ok_or(InfrastructureBootstrapError::Integrity)?;
264    let sample = &record
265        .sources
266        .get(&target.name)
267        .ok_or(InfrastructureBootstrapError::Integrity)?
268        .sample;
269    let deficit = target
270        .actions
271        .iter()
272        .find_map(|action| {
273            if let crate::fleet_ensure::model::EnsureAction::Fund {
274                funding_deficit_cycles,
275                ..
276            } = action
277            {
278                Some(*funding_deficit_cycles)
279            } else {
280                None
281            }
282        })
283        .unwrap_or(0);
284    let minimum = sample
285        .cycles
286        .checked_add(deficit)
287        .and_then(|amount| amount.checked_add(shortfall))
288        .ok_or(EnsurePolicyError::ArithmeticOverflow {
289            field: "bootstrap registration funding",
290        })?;
291    let funded = prepare_with_funding(
292        root,
293        desired,
294        record,
295        desired_sha256,
296        time,
297        &BTreeMap::from([(target.name.clone(), minimum)]),
298    )?;
299    infrastructure_bootstrap::admit_review(&funded)?;
300    Ok(funded)
301}
302
303/// Reject altered phase shape before the generic Ensure driver can issue an effect.
304pub(in crate::fleet_ensure) fn verify_plan(
305    root: &Path,
306    plan: &FleetEnsurePlan,
307) -> Result<(), InfrastructureBootstrapError> {
308    let record = plan
309        .infrastructure_bootstrap
310        .as_ref()
311        .ok_or(InfrastructureBootstrapError::Integrity)?;
312    let desired = plan
313        .reviewed_desired
314        .as_ref()
315        .ok_or(InfrastructureBootstrapError::Integrity)?
316        .desired();
317    let mut funding_minima = BTreeMap::new();
318    for target in &plan.canisters {
319        for action in &target.actions {
320            if let crate::fleet_ensure::model::EnsureAction::Fund {
321                funding_deficit_cycles,
322                ..
323            } = action
324            {
325                let sample = &record
326                    .sources
327                    .get(&target.name)
328                    .ok_or(InfrastructureBootstrapError::Integrity)?
329                    .sample;
330                let minimum = sample
331                    .cycles
332                    .checked_add(*funding_deficit_cycles)
333                    .ok_or(InfrastructureBootstrapError::Integrity)?;
334                if funding_minima
335                    .insert(target.name.clone(), minimum)
336                    .is_some()
337                {
338                    return Err(InfrastructureBootstrapError::Integrity);
339                }
340            }
341        }
342    }
343    let mut expected = prepare_with_funding(
344        root,
345        desired,
346        record,
347        &plan.desired_sha256,
348        plan.planned_at_time,
349        &funding_minima,
350    )?;
351    // A retained approval owns its numeric ceiling. Re-estimating current work
352    // must not rewrite that authority or strand its already issued effects.
353    // All effects, funding, source bindings and conservation arithmetic still match.
354    let available = expected
355        .conservation
356        .observed_controlled_cycles
357        .checked_add(expected.conservation.maximum_new_funding_cycles)
358        .ok_or(InfrastructureBootstrapError::Integrity)?;
359    expected.conservation.maximum_execution_burn_cycles =
360        plan.conservation.maximum_execution_burn_cycles;
361    expected.conservation.expected_post_operation_cycles = available
362        .checked_sub(plan.conservation.maximum_execution_burn_cycles)
363        .ok_or(InfrastructureBootstrapError::Integrity)?;
364    expected.plan_sha256 = crate::fleet_ensure::policy::expected_plan_sha256(&expected);
365    if plan.scope != FleetEnsurePlanScope::InfrastructureBootstrap || expected != *plan {
366        return Err(InfrastructureBootstrapError::Integrity);
367    }
368    Ok(())
369}
370
371/// Original samples become the reviewed numerical input; no management call is issued here.
372pub(in crate::fleet_ensure) fn original_observation(
373    desired: &DesiredFleet,
374    record: &InfrastructureBootstrapRecord,
375) -> FleetObservation {
376    let view = InfrastructureBootstrapObservation {
377        held_sources: record
378            .held_sources
379            .iter()
380            .map(|(name, source)| (name.clone(), source.custody.clone()))
381            .collect(),
382        canisters: desired
383            .canisters
384            .iter()
385            .map(|configured| {
386                (
387                    configured.name.clone(),
388                    record
389                        .sources
390                        .get(&configured.name)
391                        .map(|source| source.sample.clone()),
392                )
393            })
394            .collect(),
395        coordinator_registry: None,
396        operator_cycles: record.operator_cycles,
397        ledger_fee_cycles: record.ledger_fee_cycles,
398    };
399    fleet_observation(&view)
400}
401
402pub(in crate::fleet_ensure) fn fleet_observation(
403    view: &InfrastructureBootstrapObservation,
404) -> FleetObservation {
405    FleetObservation {
406        additional_controlled_cycles: BTreeMap::new(),
407        estate_funding_domains: BTreeMap::new(),
408        protocol_ready: BTreeMap::new(),
409        operator_cycles: view.operator_cycles,
410        ledger_fee_cycles: view.ledger_fee_cycles,
411        canisters: view
412            .canisters
413            .iter()
414            .map(|(name, sample)| {
415                (
416                    name.clone(),
417                    sample.as_ref().map(|sample| LiveCanister {
418                        canister_version: Some(sample.binding.canister_version),
419                        controllers: sample
420                            .binding
421                            .controllers
422                            .iter()
423                            .map(Principal::to_text)
424                            .collect(),
425                        cycles: sample.cycles,
426                        module_sha256: sample.binding.module_sha256.map(hex_bytes),
427                        principal: sample.binding.canister_id.to_text(),
428                        reinstall_required: false,
429                        root_owned_lifecycle: None,
430                        status: if sample.binding.stopped {
431                            CanisterRuntimeStatus::Stopped
432                        } else {
433                            CanisterRuntimeStatus::Running
434                        },
435                    }),
436                )
437            })
438            .collect(),
439    }
440}
441
442fn verify_ready_coordinator(
443    root: &Path,
444    desired: &DesiredFleet,
445    record: &InfrastructureBootstrapRecord,
446) -> Result<(), InfrastructureBootstrapError> {
447    if record.coordinator != BootstrapCoordinatorSelection::Ready {
448        return Ok(());
449    }
450    let encoded = record
451        .coordinator_registry_candid_hex
452        .as_ref()
453        .ok_or(InfrastructureBootstrapError::Integrity)?;
454    let bytes = decode_hex(encoded).map_err(|_| InfrastructureBootstrapError::Integrity)?;
455    let observed: FleetRegistry =
456        candid::decode_one(&bytes).map_err(|_| InfrastructureBootstrapError::Integrity)?;
457    let principals = desired
458        .canisters
459        .iter()
460        .filter_map(|configured| {
461            configured
462                .principal
463                .as_ref()
464                .map(|id| (configured.name.clone(), id.clone()))
465        })
466        .collect();
467    if observed != expected_registry(root, desired, &principals)? {
468        return Err(InfrastructureBootstrapError::Integrity);
469    }
470    Ok(())
471}
472
473fn expected_registry(
474    root: &Path,
475    desired: &DesiredFleet,
476    principals: &BTreeMap<String, String>,
477) -> Result<FleetRegistry, InfrastructureBootstrapError> {
478    let bootstrap = desired
479        .bootstrap
480        .as_ref()
481        .ok_or(InfrastructureBootstrapError::Integrity)?;
482    let authorities = canic_init::compile_root_authorities(root, desired, principals)
483        .map_err(|_| InfrastructureBootstrapError::Integrity)?;
484    let authority = authorities
485        .first()
486        .ok_or(InfrastructureBootstrapError::Integrity)?
487        .1
488        .binding
489        .authority
490        .clone();
491    let admission =
492        bind_initial_fleet_admission_policy(authority.binding.fleet.clone(), &bootstrap.admission)
493            .map_err(|_| InfrastructureBootstrapError::Integrity)?;
494    FleetRegistryOps::compile_genesis(
495        &bootstrap.app,
496        authority,
497        &bootstrap
498            .component_deployment_configuration
499            .component_topology,
500        admission,
501    )
502    .map_err(|_| InfrastructureBootstrapError::Integrity)
503}
504
505/// Require exact current Coordinator genesis before dependent Root or Store effects.
506pub(in crate::fleet_ensure::ops) fn verify_coordinator(
507    icp: &IcpCli,
508    root: &Path,
509    desired: &DesiredFleet,
510    source: &InfrastructureBootstrapRecord,
511    state: &FleetEnsureStateRecord,
512) -> Result<(), InfrastructureBootstrapError> {
513    let principals = desired
514        .canisters
515        .iter()
516        .map(|configured| {
517            configured
518                .principal
519                .as_ref()
520                .or_else(|| state.pending_principals.get(&configured.name))
521                .or_else(|| state.principals.get(&configured.name))
522                .map(|id| (configured.name.clone(), id.clone()))
523                .ok_or(InfrastructureBootstrapError::Integrity)
524        })
525        .collect::<Result<BTreeMap<_, _>, _>>()?;
526    verify_registry(
527        icp,
528        desired,
529        source,
530        &expected_registry(root, desired, &principals)?,
531    )
532}
533
534/// Check the exact current Registry against authenticated operator/network selection.
535pub(in crate::fleet_ensure::ops) fn verify_registry(
536    icp: &IcpCli,
537    desired: &DesiredFleet,
538    source: &InfrastructureBootstrapRecord,
539    expected: &FleetRegistry,
540) -> Result<(), InfrastructureBootstrapError> {
541    let agent = authenticated_agent(icp, desired, source)?;
542    let runtime = tokio::runtime::Builder::new_current_thread()
543        .enable_all()
544        .build()?;
545    let observed = runtime.block_on(inventory::registry(
546        &agent,
547        expected.authority.binding.coordinator,
548    ))?;
549    if observed != *expected {
550        return Err(InfrastructureBootstrapError::Integrity);
551    }
552    Ok(())
553}
554
555#[derive(CandidType)]
556struct LedgerAccount {
557    owner: Principal,
558    subaccount: Option<Vec<u8>>,
559}
560
561/// Read exact management custody; the caller has reserved the phase's observation allowance.
562pub(in crate::fleet_ensure::ops) fn observe(
563    icp: &IcpCli,
564    desired: &DesiredFleet,
565    record: &InfrastructureBootstrapRecord,
566    state: &FleetEnsureStateRecord,
567) -> Result<InfrastructureBootstrapObservation, InfrastructureBootstrapError> {
568    let agent = authenticated_agent(icp, desired, record)?;
569    let bootstrap = desired
570        .bootstrap
571        .as_ref()
572        .ok_or(InfrastructureBootstrapError::Integrity)?;
573    let runtime = tokio::runtime::Builder::new_current_thread()
574        .enable_all()
575        .build()
576        .map_err(|error| InfrastructureBootstrapError::Observation(error.to_string()))?;
577    let mut canisters = BTreeMap::new();
578    let mut held_sources = BTreeMap::new();
579    for configured in &desired.canisters {
580        if let Some(source) = record.held_sources.get(&configured.name) {
581            let custody = runtime.block_on(declarations::observe_held(&agent, source))?;
582            held_sources.insert(configured.name.clone(), custody);
583            canisters.insert(configured.name.clone(), None);
584            continue;
585        }
586        let id = configured
587            .principal
588            .as_ref()
589            .or_else(|| state.pending_principals.get(&configured.name))
590            .or_else(|| state.principals.get(&configured.name));
591        let sample = match id {
592            Some(id) => Some(runtime.block_on(management::observe(
593                &agent,
594                Principal::from_text(id).map_err(|_| InfrastructureBootstrapError::Integrity)?,
595            ))?),
596            None if configured.name == bootstrap.coordinator
597                && record.coordinator == BootstrapCoordinatorSelection::Create =>
598            {
599                None
600            }
601            None => return Err(InfrastructureBootstrapError::Integrity),
602        };
603        canisters.insert(configured.name.clone(), sample);
604    }
605    let coordinator_registry = if record.coordinator == BootstrapCoordinatorSelection::Ready {
606        let id = canisters
607            .get(&bootstrap.coordinator)
608            .and_then(Option::as_ref)
609            .ok_or(InfrastructureBootstrapError::Integrity)?
610            .binding
611            .canister_id;
612        Some(runtime.block_on(inventory::registry(&agent, id))?)
613    } else {
614        None
615    };
616    let fee: Nat = icp
617        .canister_query_candid(&desired.cycles_ledger, "icrc1_fee", &(), None)
618        .map_err(|error| InfrastructureBootstrapError::Observation(error.to_string()))?;
619    let balance: Nat = icp
620        .canister_query_candid(
621            &desired.cycles_ledger,
622            "icrc1_balance_of",
623            &LedgerAccount {
624                owner: record.operator,
625                subaccount: None,
626            },
627            None,
628        )
629        .map_err(|error| InfrastructureBootstrapError::Observation(error.to_string()))?;
630    Ok(InfrastructureBootstrapObservation {
631        held_sources,
632        canisters,
633        coordinator_registry,
634        operator_cycles: balance
635            .0
636            .try_into()
637            .map_err(|_| InfrastructureBootstrapError::Integrity)?,
638        ledger_fee_cycles: fee
639            .0
640            .try_into()
641            .map_err(|_| InfrastructureBootstrapError::Integrity)?,
642    })
643}
644
645fn authenticated_agent(
646    icp: &IcpCli,
647    desired: &DesiredFleet,
648    record: &InfrastructureBootstrapRecord,
649) -> Result<ic_agent::Agent, InfrastructureBootstrapError> {
650    let agent = icp
651        .authenticated_agent_with_response_limit(2 * 1024 * 1024)
652        .map_err(|error| InfrastructureBootstrapError::Observation(error.to_string()))?;
653    let root_key = agent.read_root_key();
654    let key_hash: [u8; 32] = Sha256::digest(&root_key).into();
655    let bootstrap = desired
656        .bootstrap
657        .as_ref()
658        .ok_or(InfrastructureBootstrapError::Integrity)?;
659    if agent.get_principal().ok() != Some(record.operator)
660        || key_hash != record.network_root_key_sha256
661        || CanonicalNetworkId::from_der_root_trust_anchor(&root_key).ok()
662            != Some(bootstrap.canonical_network_id)
663    {
664        return Err(InfrastructureBootstrapError::Integrity);
665    }
666    Ok(agent)
667}
668
669/// Fresh pre-effect evidence must preserve every original binding and bounded cycle baseline.
670pub(in crate::fleet_ensure) fn verify_initial(
671    plan: &FleetEnsurePlan,
672    observed: &InfrastructureBootstrapObservation,
673) -> Result<(), InfrastructureBootstrapError> {
674    let source = plan
675        .infrastructure_bootstrap
676        .as_ref()
677        .ok_or(InfrastructureBootstrapError::Integrity)?;
678    let desired = plan
679        .reviewed_desired
680        .as_ref()
681        .ok_or(InfrastructureBootstrapError::Integrity)?
682        .desired();
683    let allowance = desired
684        .maximum_observation_burn_cycles
685        .parse::<canic_contracts::cycles::Cycles>()
686        .map_err(|_| InfrastructureBootstrapError::Integrity)?
687        .to_u128();
688    let held = source
689        .held_sources
690        .iter()
691        .map(|(name, source)| (name.clone(), source.custody.clone()))
692        .collect::<BTreeMap<_, _>>();
693    if observed.held_sources != held
694        || observed.canisters.len() != desired.canisters.len()
695        || observed.operator_cycles < source.operator_cycles
696        || observed.ledger_fee_cycles != source.ledger_fee_cycles
697    {
698        return Err(InfrastructureBootstrapError::Integrity);
699    }
700    for configured in &desired.canisters {
701        let actual = observed
702            .canisters
703            .get(&configured.name)
704            .ok_or(InfrastructureBootstrapError::Integrity)?;
705        match (source.sources.get(&configured.name), actual) {
706            (None, None)
707                if configured.kind == crate::fleet_ensure::model::DesiredCanisterKind::Pool
708                    && source.held_sources.contains_key(&configured.name) => {}
709            (None, None)
710                if configured.kind
711                    == crate::fleet_ensure::model::DesiredCanisterKind::Coordinator
712                    && source.coordinator == BootstrapCoordinatorSelection::Create => {}
713            (Some(original), Some(actual)) => {
714                let before = original
715                    .sample
716                    .cycles
717                    .checked_add(original.sample.reserved_cycles)
718                    .ok_or(InfrastructureBootstrapError::Integrity)?;
719                let after = actual
720                    .cycles
721                    .checked_add(actual.reserved_cycles)
722                    .ok_or(InfrastructureBootstrapError::Integrity)?;
723                let mut expected_binding = original.sample.binding.clone();
724                if !expected_binding.stopped
725                    && actual.binding.canister_version >= expected_binding.canister_version
726                {
727                    expected_binding.canister_version = actual.binding.canister_version;
728                }
729                if expected_binding != actual.binding || before.saturating_sub(after) > allowance {
730                    return Err(InfrastructureBootstrapError::Integrity);
731                }
732            }
733            _ => return Err(InfrastructureBootstrapError::Integrity),
734        }
735    }
736    let actual_registry = observed
737        .coordinator_registry
738        .as_ref()
739        .map(candid::encode_one)
740        .transpose()
741        .map_err(|_| InfrastructureBootstrapError::Integrity)?
742        .map(hex_bytes);
743    if source.coordinator_registry_candid_hex != actual_registry {
744        return Err(InfrastructureBootstrapError::Integrity);
745    }
746    Ok(())
747}
748
749/// Project only initialized infrastructure into terminal accounting after exact custody checks.
750pub(in crate::fleet_ensure) fn terminal_observation(
751    workspace: &Path,
752    plan: &FleetEnsurePlan,
753    state: &FleetEnsureStateRecord,
754    observed: &InfrastructureBootstrapObservation,
755) -> Result<(FleetObservation, u128), InfrastructureBootstrapError> {
756    let source = plan
757        .infrastructure_bootstrap
758        .as_ref()
759        .ok_or(InfrastructureBootstrapError::Integrity)?;
760    let held = source
761        .held_sources
762        .iter()
763        .map(|(name, source)| (name.clone(), source.custody.clone()))
764        .collect::<BTreeMap<_, _>>();
765    if observed.held_sources != held {
766        return Err(InfrastructureBootstrapError::Integrity);
767    }
768    let desired = plan
769        .reviewed_desired
770        .as_ref()
771        .ok_or(InfrastructureBootstrapError::Integrity)?
772        .desired();
773    let artifacts = resolve_desired_artifacts(workspace, desired)?;
774    let principals = desired
775        .canisters
776        .iter()
777        .filter_map(|configured| {
778            configured
779                .principal
780                .as_ref()
781                .or_else(|| state.pending_principals.get(&configured.name))
782                .or_else(|| state.principals.get(&configured.name))
783                .map(|id| (configured.name.clone(), id.clone()))
784        })
785        .collect::<BTreeMap<_, _>>();
786    let mut terminal = fleet_observation(observed);
787    terminal
788        .canisters
789        .retain(|name, _| plan.canisters.iter().any(|canister| canister.name == *name));
790    if terminal.canisters.len() != plan.canisters.len() {
791        return Err(InfrastructureBootstrapError::Integrity);
792    }
793    let mut total = 0_u128;
794    for target in &plan.canisters {
795        let configured = desired
796            .canisters
797            .iter()
798            .find(|candidate| candidate.name == target.name)
799            .ok_or(InfrastructureBootstrapError::Integrity)?;
800        let actual = observed
801            .canisters
802            .get(&target.name)
803            .and_then(Option::as_ref)
804            .ok_or(InfrastructureBootstrapError::Integrity)?;
805        let named_controllers = configured
806            .controller_canisters
807            .iter()
808            .map(|name| {
809                principals
810                    .get(name)
811                    .cloned()
812                    .ok_or(InfrastructureBootstrapError::Integrity)
813            })
814            .collect::<Result<Vec<_>, _>>()?;
815        let mut controllers = configured
816            .controllers
817            .iter()
818            .cloned()
819            .chain(named_controllers)
820            .map(|id| Principal::from_text(id).map_err(|_| InfrastructureBootstrapError::Integrity))
821            .collect::<Result<Vec<_>, _>>()?;
822        controllers.sort_unstable();
823        controllers.dedup();
824        let minimum = configured
825            .minimum_cycles
826            .parse::<canic_contracts::cycles::Cycles>()
827            .map_err(|_| InfrastructureBootstrapError::Integrity)?
828            .to_u128();
829        if principals.get(&target.name) != Some(&actual.binding.canister_id.to_text())
830            || configured.subnet != actual.binding.subnet.to_string()
831            || actual.binding.controllers != controllers
832            || actual.binding.stopped
833            || actual.binding.snapshots_size_bytes != 0
834            || actual.cycles < minimum
835            || artifacts.wasm_sha256_by_canister.get(&target.name)
836                != actual.binding.module_sha256.map(hex_bytes).as_ref()
837        {
838            return Err(InfrastructureBootstrapError::Integrity);
839        }
840        total = total
841            .checked_add(actual.cycles)
842            .and_then(|n| n.checked_add(actual.reserved_cycles))
843            .ok_or(InfrastructureBootstrapError::Integrity)?;
844    }
845    Ok((terminal, total))
846}