Skip to main content

canic_host/fleet_ensure/generate/
mod.rs

1//! Module: fleet_ensure::generate
2//!
3//! Responsibility: compile high-level Fleet policy plus explicit live identities into desired state.
4//! Does not own: artifact builds, identity invention, paid effects, or convergence.
5//! Boundary: release artifacts are local authority; every retained Principal is explicit and live-verified.
6
7#[cfg(test)]
8mod tests;
9
10use crate::{
11    canister_protocol::query_with_candid,
12    component_topology::{
13        PlannedFleetSubnetRootTopology, PlannedFleetSubnetRootTopologyInput, PlannedFleetTopology,
14        RootComponentAdmissionInput, RootPoolCapacityError, RootPoolCapacityInput,
15        RootPoolImportCapacityError, RootPoolImportCapacityInput, plan_initial_fleet_topology,
16        validate_root_pool_capacity, validate_root_pool_import_capacity,
17    },
18    durable_io::{
19        RegularFileReadError, create_new_bytes_with_parents, read_optional_regular_bytes,
20    },
21    fleet_ensure::model::{
22        CanisterRuntimeStatus, DesiredCanister, DesiredCanisterInit, DesiredCanisterKind,
23        DesiredComponentGroupPlacement, DesiredFleet, DesiredFleetBootstrap,
24        DesiredFleetBootstrapRoot, DesiredFleetProtocol, DesiredPresence,
25        FLEET_ENSURE_SCHEMA_VERSION, MAX_FLEET_ENSURE_CANISTERS, RetainedRootStartAuthorityRecord,
26        RootManagementBinding,
27    },
28    fleet_ensure::ops::{EnsurePaths, root_owned_lifecycle, write_root_start_authority},
29    icp::IcpCli,
30    icp_config::resolve_icp_build_network_from_root,
31    network::resolve_canonical_network_id_from_root,
32    release_build::validate_finalized_release_build_manifest,
33    release_set::{
34        AppConfigSnapshot, CanicInfrastructureArtifactEntry, CanicInfrastructureRole,
35        load_persisted_application_artifact_union,
36        load_persisted_canic_infrastructure_artifact_manifest,
37        load_persisted_current_release_set_manifest,
38    },
39    subnet_catalog::load_mainnet_subnet_catalog,
40};
41use candid::{Nat, Principal};
42use canic_core::{
43    cdk::types::Cycles,
44    control_plane_support::config::ComponentDeploymentConfiguration,
45    dto::fleet_subnet_root::FleetSubnetRootAuthority,
46    dto::pool::{CanisterPoolResponse, CanisterPoolStatusRequest},
47    ids::{
48        BuildNetwork, ComponentGroupDeploymentId, ComponentSpecId, CyclesFundingBudget,
49        FleetAdmissionPolicyTemplate, FleetBinding, FleetCoordinatorBinding,
50        FleetCoordinatorRootFundingPolicy, FleetFundingProfile, FleetKey, FleetRegistryAuthority,
51        FleetSubnetCanisterPoolConfig, FleetSubnetRootBinding, FleetSubnetRootFundingAuthority,
52        FleetSubnetRootFundingPolicy, FleetSubnetRootLimits, FleetSubnetWasmStoreAuthority,
53        ReleaseBuildId, SubnetId,
54    },
55    protocol,
56    shared_support::fleet_admission_policy::compile_fleet_admission_policy_template,
57};
58use ic_query::subnet_catalog::SubnetSpecialization;
59use serde::{Deserialize, Serialize};
60use sha2::{Digest, Sha256};
61use std::{
62    collections::{BTreeMap, BTreeSet},
63    path::{Path, PathBuf},
64    time::{SystemTime, UNIX_EPOCH},
65};
66use thiserror::Error as ThisError;
67
68const MAX_GENERATOR_INPUT_BYTES: usize = 1024 * 1024;
69const MAINNET_CYCLES_LEDGER: &str = "um5iw-rqaaa-aaaaq-qaaba-cai";
70const GENERATED_RETAINED_MATERIAL_CYCLE_THRESHOLD: u128 = 1_000_000;
71// Create reserves one update plus one observation; ordinary observations keep
72// their own bound and do not inherit the additional Create execution interval.
73const GENERATED_RETAINED_MAXIMUM_OBSERVATION_BURN_CYCLES: u128 = 1_000_000_000_000;
74const GENERATED_POOL_CREATION_EXECUTION_MARGIN_CYCLES: u128 = 1_000_000_000_000;
75const GENERATED_RETAINED_MAXIMUM_UPDATE_BURN_CYCLES: u128 = 1_000_000_000_000;
76
77/// Return the generated fresh-pool funding that preserves one readiness floor
78/// across the bounded first observation and controller-finalization effects.
79#[doc(hidden)]
80pub fn fresh_pool_creation_funding(readiness_floor: u128) -> Result<u128, FleetGenerateError> {
81    crate::fleet_ensure::model::creation_observation_burn(
82        GENERATED_RETAINED_MAXIMUM_OBSERVATION_BURN_CYCLES,
83        GENERATED_RETAINED_MAXIMUM_UPDATE_BURN_CYCLES,
84    )
85    .and_then(|burn| readiness_floor.checked_add(burn))
86    .and_then(|funding| funding.checked_add(GENERATED_RETAINED_MAXIMUM_UPDATE_BURN_CYCLES))
87    .ok_or_else(|| {
88        FleetGenerateError::Authority(
89            "fresh pool creation funding plus its bounded pre-import execution margin overflowed"
90                .to_string(),
91        )
92    })
93}
94
95/// Exact local and live inputs for one no-effect desired-state generation.
96pub struct FleetGenerateRequest<'a> {
97    pub app_config: &'a Path,
98    pub environment: &'a str,
99    pub fleet: &'a str,
100    pub icp_executable: &'a str,
101    pub release_build_id: ReleaseBuildId,
102    pub root: &'a Path,
103    pub seed: &'a Path,
104    pub source: &'a Path,
105}
106
107/// Exact authority used to create one durable empty-estate identity seed.
108pub struct FreshEstateSeedRequest<'a> {
109    pub cycles_ledger: &'a str,
110    pub management_creation_fee_cycles: u128,
111    pub seed: &'a Path,
112    pub source: &'a Path,
113}
114
115/// Generated desired state plus its explicit observation summary.
116pub struct GeneratedDesiredFleet {
117    pub desired: DesiredFleet,
118    pub observed_canisters: usize,
119    pub observed_controlled_cycles: u128,
120    pub release_build_id: ReleaseBuildId,
121}
122
123/// Typed no-effect Fleet generation failure.
124#[derive(Debug, ThisError)]
125pub enum FleetGenerateError {
126    #[error("invalid desired Fleet policy: {0}")]
127    Policy(#[from] crate::fleet_ensure::policy::EnsurePolicyError),
128    #[error("invalid protected funding policy: {0}")]
129    FundingPolicy(#[from] canic_core::control_plane_support::model::fleet_funding_policy::FleetFundingPolicyValidationError),
130
131    #[error("failed to read Fleet generator input {path}: {source}")]
132    Read {
133        path: PathBuf,
134        #[source]
135        source: std::io::Error,
136    },
137
138    #[error("failed to write Fleet generator input {path}: {source}")]
139    Write {
140        path: PathBuf,
141        #[source]
142        source: std::io::Error,
143    },
144
145    #[error("Fleet generator input is not a safe regular file: {path} ({reason})")]
146    UnsafeInput { path: PathBuf, reason: &'static str },
147
148    #[error("Fleet generator input is too large: {path} ({actual} bytes; maximum {maximum})")]
149    TooLarge {
150        actual: usize,
151        maximum: usize,
152        path: PathBuf,
153    },
154
155    #[error("failed to decode Fleet generator input {path}: {source}")]
156    Decode {
157        path: PathBuf,
158        #[source]
159        source: toml::de::Error,
160    },
161
162    #[error("unsupported Fleet generator {kind} schema {actual}; expected 1")]
163    Schema { actual: u32, kind: &'static str },
164
165    #[error("invalid Fleet generator authority: {0}")]
166    Authority(String),
167
168    #[error("existing Fleet identity seed conflicts with requested fresh-estate authority: {0}")]
169    FreshSeedConflict(String),
170
171    #[error("Fleet identity seed and policy topology differ: {0}")]
172    SeedTopology(String),
173
174    #[error("retained canister {canister} is unavailable: {reason}")]
175    CanisterUnavailable { canister: String, reason: String },
176
177
178
179    #[error(transparent)]
180    StoppedRootStartRequired(Box<StoppedRootStartPrerequisite>),
181
182    #[error(
183        "retained canister {canister} controller set differs: actual {actual:?}, expected {expected:?}"
184    )]
185    ControllerMismatch {
186        actual: Vec<String>,
187        canister: String,
188        expected: Vec<String>,
189    },
190
191    #[error("retained canister {canister} is on Subnet {actual}, expected {expected}")]
192    SubnetMismatch {
193        actual: String,
194        canister: String,
195        expected: String,
196    },
197
198    #[error("release authority is invalid: {0}")]
199    Release(String),
200
201    #[error(
202        "release build {release_build_id} targets {actual}, but environment {environment} requires {expected}"
203    )]
204    ReleaseBuildNetworkMismatch {
205        release_build_id: ReleaseBuildId,
206        actual: BuildNetwork,
207        environment: String,
208        expected: BuildNetwork,
209    },
210
211    #[error("Fleet admission must not contain the anonymous Principal")]
212    FleetAdmissionAnonymous,
213
214    #[error("duplicate Fleet admission Principal {principal}")]
215    FleetAdmissionDuplicate { principal: Principal },
216
217    #[error("artifact Candid sidecar is missing: {}", path.display())]
218    CandidMissing { path: PathBuf },
219
220    #[error("artifact Candid sidecar is not a regular no-follow file: {}", path.display())]
221    CandidNotRegular { path: PathBuf },
222
223    #[error("failed to read artifact Candid sidecar {}: {source}", path.display())]
224    CandidRead {
225        path: PathBuf,
226        #[source]
227        source: std::io::Error,
228    },
229
230    #[cfg(not(unix))]
231    #[error(
232        "artifact Candid sidecar cannot be read without following links on this platform: {}",
233        path.display()
234    )]
235    CandidUnsupportedPlatform { path: PathBuf },
236
237    #[error(
238        "artifact Candid sidecar digest differs from release authority at {}: expected {expected:?}, observed {observed:?}",
239        path.display()
240    )]
241    CandidDigestMismatch {
242        path: PathBuf,
243        expected: [u8; 32],
244        observed: [u8; 32],
245    },
246
247    #[error(transparent)]
248    ComponentPoolCapacity(#[from] RootPoolCapacityError),
249
250    #[error(transparent)]
251    PoolImportCapacity(#[from] RootPoolImportCapacityError),
252
253    #[error("system clock is before the Unix epoch")]
254    Clock,
255}
256
257/// Exact management evidence for the reviewed same-Root Start prerequisite.
258#[derive(Debug, Eq, PartialEq, ThisError)]
259#[error(
260    "retained Root {root} is stopped after exact management verification \
261     (Subnet {subnet}, controller {controller}, module SHA-256 {module_sha256}); \
262     no protected Root query or desired-output mutation was attempted. Generator authority \
263     {authority_sha256} binds only the observed Root identities and installed modules. \
264     The authority covers {root_count} stopped Root(s). Review and apply only the same-ID Start \
265     through the current retained \
266     `canic fleet ensure {fleet}` authority, \
267     then rerun `canic fleet generate`"
268)]
269pub struct StoppedRootStartPrerequisite {
270    pub authority_sha256: String,
271    pub controller: String,
272    pub fleet: String,
273    pub module_sha256: String,
274    pub root: String,
275    pub root_count: usize,
276    pub subnet: String,
277}
278
279#[derive(Clone, Debug, Deserialize)]
280#[serde(deny_unknown_fields)]
281struct FleetSource {
282    schema_version: u32,
283    funding_profile: FleetFundingProfile,
284    operator: String,
285    admission: AdmissionSource,
286    coordinator: CoordinatorSource,
287    fleet_subnet_roots: Vec<RootSource>,
288}
289
290#[derive(Clone, Debug, Deserialize)]
291#[serde(deny_unknown_fields)]
292struct AdmissionSource {
293    principals: Vec<String>,
294}
295
296#[derive(Clone, Debug, Deserialize)]
297#[serde(deny_unknown_fields)]
298struct CoordinatorSource {
299    subnet: ExplicitSubnetSource,
300    creation_funding: CyclesCreationSource,
301    root_funding: CoordinatorFundingSource,
302}
303
304#[derive(Clone, Debug, Deserialize)]
305#[serde(deny_unknown_fields)]
306struct ExplicitSubnetSource {
307    kind: String,
308    subnet: String,
309    acknowledge_fiduciary_cost: bool,
310}
311
312#[derive(Clone, Debug, Deserialize)]
313#[serde(deny_unknown_fields)]
314struct CyclesCreationSource {
315    kind: String,
316    #[serde(deserialize_with = "Cycles::from_human_config")]
317    cycles: Cycles,
318}
319
320#[derive(Clone, Debug, Deserialize)]
321#[serde(deny_unknown_fields)]
322struct CoordinatorFundingSource {
323    #[serde(deserialize_with = "Cycles::from_human_config")]
324    minimum_reserve_cycles: Cycles,
325    window_secs: u64,
326    #[serde(deserialize_with = "Cycles::from_human_config")]
327    maximum_cycles: Cycles,
328    maximum_automatic_grants: u32,
329    #[serde(deserialize_with = "Cycles::from_human_config")]
330    maximum_automatic_cycles: Cycles,
331}
332
333#[derive(Clone, Debug, Deserialize)]
334#[serde(deny_unknown_fields)]
335struct RootSource {
336    placement_subnet: String,
337    #[serde(default)]
338    acknowledge_fiduciary_cost: bool,
339    #[serde(default)]
340    component_group_placements: BTreeMap<ComponentGroupDeploymentId, Vec<u32>>,
341    component_admissions: BTreeMap<ComponentSpecId, u32>,
342    canister_pool: PoolSource,
343    root_funding: RootFundingSource,
344    limits: LimitsSource,
345    root_creation_funding: CyclesCreationSource,
346    wasm_store_creation_funding: CyclesCreationSource,
347}
348
349#[derive(Clone, Debug, Deserialize)]
350#[serde(deny_unknown_fields)]
351struct PoolSource {
352    minimum_size: u32,
353    maximum_size: u32,
354    #[serde(deserialize_with = "Cycles::from_human_config")]
355    canister_cycles: Cycles,
356    #[serde(default)]
357    imports: Vec<String>,
358}
359
360#[derive(Clone, Debug, Deserialize)]
361#[serde(deny_unknown_fields)]
362struct RootFundingSource {
363    #[serde(deserialize_with = "Cycles::from_human_config")]
364    request_threshold: Cycles,
365    #[serde(deserialize_with = "Cycles::from_human_config")]
366    target_balance: Cycles,
367    cooldown_secs: u64,
368    window_secs: u64,
369    #[serde(deserialize_with = "Cycles::from_human_config")]
370    maximum_cycles: Cycles,
371    maximum_automatic_grants: u32,
372    #[serde(deserialize_with = "Cycles::from_human_config")]
373    maximum_automatic_cycles: Cycles,
374}
375
376#[derive(Clone, Debug, Deserialize)]
377#[serde(deny_unknown_fields)]
378struct LimitsSource {
379    maximum_component_instances: u32,
380    maximum_registry_bytes: u64,
381    maximum_wasm_store_bytes: u64,
382    maximum_group_placements: u32,
383    cycles_funding: CyclesFundingSource,
384}
385
386#[derive(Clone, Debug, Deserialize)]
387#[serde(deny_unknown_fields)]
388struct CyclesFundingSource {
389    window_secs: u64,
390    #[serde(deserialize_with = "Cycles::from_human_config")]
391    maximum_cycles: Cycles,
392}
393
394#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
395#[serde(deny_unknown_fields)]
396struct EstateSeed {
397    schema_version: u32,
398    fleet_id: canic_core::ids::FleetId,
399    #[serde(default)]
400    fresh_estate: bool,
401    coordinator: String,
402    #[serde(default)]
403    treasury: Option<TreasurySeed>,
404    #[serde(default = "mainnet_cycles_ledger")]
405    cycles_ledger: String,
406    management_creation_fee_cycles: String,
407    roots: Vec<RootSeed>,
408}
409
410#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
411#[serde(deny_unknown_fields)]
412struct TreasurySeed {
413    principal: String,
414    subnet: String,
415}
416
417#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419struct RootSeed {
420    placement_subnet: String,
421    root: String,
422    store: String,
423    #[serde(default)]
424    pool_imports: Vec<String>,
425}
426
427/// Create or replay one durable no-effect seed for a literally empty estate.
428pub fn initialize_fresh_estate_seed(
429    request: &FreshEstateSeedRequest<'_>,
430) -> Result<canic_core::ids::FleetId, FleetGenerateError> {
431    let source: FleetSource = load_toml(request.source, "source")?;
432    require_schema(source.schema_version, "source")?;
433    parse_principal("Cycles Ledger", request.cycles_ledger)?;
434    if let Some(existing) = read_seed(request.seed)? {
435        require_fresh_seed_authority(&existing, &source, request)?;
436        return Ok(existing.fleet_id);
437    }
438    let mut bytes = [0_u8; 32];
439    getrandom::fill(&mut bytes).map_err(|error| {
440        FleetGenerateError::Authority(format!("Fleet ID generation failed: {error}"))
441    })?;
442    let seed = fresh_seed(
443        &source,
444        canic_core::ids::FleetId::from_generated_bytes(bytes),
445        request.cycles_ledger,
446        request.management_creation_fee_cycles,
447    )?;
448    let encoded = toml::to_string_pretty(&seed).map_err(|error| {
449        FleetGenerateError::Authority(format!("fresh seed encoding failed: {error}"))
450    })?;
451    match create_new_bytes_with_parents(request.seed, encoded.as_bytes()) {
452        Ok(()) => Ok(seed.fleet_id),
453        Err(io_error) if io_error.kind() == std::io::ErrorKind::AlreadyExists => {
454            let existing = read_seed(request.seed)?.ok_or_else(|| FleetGenerateError::Read {
455                path: request.seed.to_path_buf(),
456                source: std::io::Error::new(
457                    std::io::ErrorKind::NotFound,
458                    "concurrent fresh seed publication disappeared",
459                ),
460            })?;
461            require_fresh_seed_authority(&existing, &source, request)?;
462            Ok(existing.fleet_id)
463        }
464        Err(source) => Err(FleetGenerateError::Write {
465            path: request.seed.to_path_buf(),
466            source,
467        }),
468    }
469}
470
471fn read_seed(path: &Path) -> Result<Option<EstateSeed>, FleetGenerateError> {
472    let bytes = match read_optional_regular_bytes(path) {
473        Ok(bytes) => bytes,
474        Err(RegularFileReadError::NotRegular) => {
475            return Err(FleetGenerateError::UnsafeInput {
476                path: path.to_path_buf(),
477                reason: "not a regular no-follow file",
478            });
479        }
480        Err(RegularFileReadError::Io(source)) => {
481            return Err(FleetGenerateError::Read {
482                path: path.to_path_buf(),
483                source,
484            });
485        }
486        #[cfg(not(unix))]
487        Err(RegularFileReadError::UnsupportedPlatform) => {
488            return Err(FleetGenerateError::UnsafeInput {
489                path: path.to_path_buf(),
490                reason: "safe no-follow reads are unsupported on this platform",
491            });
492        }
493    };
494    let Some(bytes) = bytes else {
495        return Ok(None);
496    };
497    if bytes.len() > MAX_GENERATOR_INPUT_BYTES {
498        return Err(FleetGenerateError::TooLarge {
499            actual: bytes.len(),
500            maximum: MAX_GENERATOR_INPUT_BYTES,
501            path: path.to_path_buf(),
502        });
503    }
504    toml::from_slice(&bytes)
505        .map(Some)
506        .map_err(|source| FleetGenerateError::Decode {
507            path: path.to_path_buf(),
508            source,
509        })
510}
511
512fn fresh_seed(
513    source: &FleetSource,
514    fleet_id: canic_core::ids::FleetId,
515    cycles_ledger: &str,
516    management_creation_fee_cycles: u128,
517) -> Result<EstateSeed, FleetGenerateError> {
518    if source
519        .fleet_subnet_roots
520        .iter()
521        .any(|root| !root.canister_pool.imports.is_empty())
522    {
523        return Err(FleetGenerateError::FreshSeedConflict(
524            "fresh estate policy must not import retained pool identities".to_string(),
525        ));
526    }
527    let controlled = source
528        .fleet_subnet_roots
529        .iter()
530        .try_fold(1_usize, |total, root| {
531            let pools = usize::try_from(root.canister_pool.minimum_size).map_err(|_| {
532                FleetGenerateError::FreshSeedConflict(
533                    "fresh pool minimum cannot be represented on this host".to_string(),
534                )
535            })?;
536            total
537                .checked_add(2)
538                .and_then(|value| value.checked_add(pools))
539                .ok_or_else(|| {
540                    FleetGenerateError::FreshSeedConflict(
541                        "fresh estate role count overflowed".to_string(),
542                    )
543                })
544        })?;
545    if controlled > MAX_FLEET_ENSURE_CANISTERS {
546        return Err(FleetGenerateError::FreshSeedConflict(format!(
547            "fresh estate declares {controlled} controlled roles, above limit {MAX_FLEET_ENSURE_CANISTERS}"
548        )));
549    }
550    let roots = source
551        .fleet_subnet_roots
552        .iter()
553        .enumerate()
554        .map(|(index, root)| RootSeed {
555            placement_subnet: root.placement_subnet.clone(),
556            root: format!("root-{index}"),
557            store: format!("store-{index}"),
558            pool_imports: (0..root.canister_pool.minimum_size)
559                .map(|pool| format!("root-{index}-pool-{pool}"))
560                .collect(),
561        })
562        .collect();
563    Ok(EstateSeed {
564        schema_version: 1,
565        fleet_id,
566        fresh_estate: true,
567        coordinator: "coordinator".to_string(),
568        treasury: None,
569        cycles_ledger: cycles_ledger.to_string(),
570        management_creation_fee_cycles: config_cycles(management_creation_fee_cycles),
571        roots,
572    })
573}
574
575fn require_fresh_seed_authority(
576    actual: &EstateSeed,
577    source: &FleetSource,
578    request: &FreshEstateSeedRequest<'_>,
579) -> Result<(), FleetGenerateError> {
580    let expected = fresh_seed(
581        source,
582        actual.fleet_id,
583        request.cycles_ledger,
584        request.management_creation_fee_cycles,
585    )?;
586    if actual == &expected {
587        Ok(())
588    } else {
589        Err(FleetGenerateError::FreshSeedConflict(
590            "existing seed is retained or differs from the requested ledger, fee, or topology"
591                .to_string(),
592        ))
593    }
594}
595
596#[derive(Clone)]
597struct ObservedCanister {
598    cycles: u128,
599    module_sha256: Option<String>,
600    subnet: String,
601}
602
603#[derive(Clone, Debug, Eq, PartialEq)]
604struct WasmStoreIdentityRelationship {
605    authority: canic_core::ids::FleetRegistryAuthority,
606    placement_subnet: SubnetId,
607    fleet_subnet_root: Principal,
608    wasm_store: Principal,
609}
610
611impl From<&FleetSubnetWasmStoreAuthority> for WasmStoreIdentityRelationship {
612    fn from(value: &FleetSubnetWasmStoreAuthority) -> Self {
613        Self {
614            authority: value.authority.clone(),
615            placement_subnet: value.placement_subnet,
616            fleet_subnet_root: value.fleet_subnet_root,
617            wasm_store: value.wasm_store,
618        }
619    }
620}
621
622#[derive(Clone, Debug, Eq, PartialEq)]
623struct RootIdentityRelationship {
624    authority: canic_core::ids::FleetRegistryAuthority,
625    placement_subnet: SubnetId,
626    fleet_subnet_root: Principal,
627}
628
629impl From<&FleetSubnetRootBinding> for RootIdentityRelationship {
630    fn from(value: &FleetSubnetRootBinding) -> Self {
631        Self {
632            authority: value.authority.clone(),
633            placement_subnet: value.placement_subnet,
634            fleet_subnet_root: value.fleet_subnet_root,
635        }
636    }
637}
638
639#[derive(Clone, Debug, Eq, PartialEq)]
640struct RootDesiredPolicy {
641    component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
642    component_topology_digest: canic_core::ids::ComponentTopologyDigest,
643    funding: FleetSubnetRootFundingAuthority,
644    installation_controller: Principal,
645    limits: FleetSubnetRootLimits,
646}
647
648impl From<&FleetSubnetRootAuthority> for RootDesiredPolicy {
649    fn from(value: &FleetSubnetRootAuthority) -> Self {
650        Self {
651            component_admissions: value.binding.component_admissions.clone(),
652            component_topology_digest: value.binding.component_topology_digest,
653            funding: value.binding.funding.clone(),
654            installation_controller: value.wasm_store_authority.installation_controller,
655            limits: value.binding.limits.clone(),
656        }
657    }
658}
659
660struct ExpectedRootEstateAuthority {
661    coordinator: FleetCoordinatorBinding,
662    root: RootIdentityRelationship,
663    store: WasmStoreIdentityRelationship,
664}
665
666struct RootEstateAuthorityRequest<'a, 'request> {
667    app: &'a canic_core::ids::AppId,
668    generation: &'a FleetGenerateRequest<'request>,
669    root_candid: &'a Path,
670    seed: &'a EstateSeed,
671    source: &'a FleetSource,
672    topology: &'a crate::component_topology::PlannedFleetTopology,
673}
674
675struct EstateObservationRequest<'a, 'request> {
676    app: &'a canic_core::ids::AppId,
677    generation: &'a FleetGenerateRequest<'request>,
678    operator: Principal,
679    root_candid: &'a Path,
680    root_wasm_sha256: &'a str,
681    seed: &'a EstateSeed,
682    source: &'a FleetSource,
683    topology: &'a crate::component_topology::PlannedFleetTopology,
684}
685
686#[derive(candid::CandidType)]
687enum RootEstateStatusRequest {
688    FleetAuthority,
689    Pool(CanisterPoolStatusRequest),
690}
691
692#[derive(candid::CandidType, Deserialize)]
693enum RootEstateStatusResponse {
694    FleetAuthority(Box<FleetSubnetRootAuthority>),
695    Pool(Box<CanisterPoolResponse>),
696}
697
698/// Generate one exact low-level desired document without issuing an IC update.
699#[expect(
700    clippy::too_many_lines,
701    reason = "generation keeps one visible fail-closed authority-validation sequence"
702)]
703pub fn generate_desired_fleet(
704    request: &FleetGenerateRequest<'_>,
705) -> Result<GeneratedDesiredFleet, FleetGenerateError> {
706    let source: FleetSource = load_toml(request.source, "source")?;
707    let seed: EstateSeed = load_toml(request.seed, "seed")?;
708    require_schema(source.schema_version, "source")?;
709    require_schema(seed.schema_version, "seed")?;
710    validate_identity_seed(&source, &seed)?;
711    require_cycles_creation(&source.coordinator.creation_funding, "Coordinator")?;
712    if source.coordinator.subnet.kind != "explicit" {
713        return Err(FleetGenerateError::Authority(
714            "Coordinator Subnet must be an exact explicit protected Subnet".to_string(),
715        ));
716    }
717    let operator = parse_principal("operator", &source.operator)?;
718    parse_subnet("Coordinator", &source.coordinator.subnet.subnet)?;
719    let config = AppConfigSnapshot::load(request.app_config)
720        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
721    let deployment_configuration = ComponentDeploymentConfiguration::compile(config.model())
722        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
723    let admission = compile_source_admission_policy(&source.admission.principals)?;
724    let root_inputs = source
725        .fleet_subnet_roots
726        .iter()
727        .map(|root| {
728            Ok(PlannedFleetSubnetRootTopologyInput {
729                placement_subnet: parse_subnet("Fleet Subnet Root", &root.placement_subnet)?,
730                component_admissions: root
731                    .component_admissions
732                    .iter()
733                    .map(
734                        |(component_spec, maximum_root_instances)| RootComponentAdmissionInput {
735                            component_spec: component_spec.clone(),
736                            maximum_root_instances: *maximum_root_instances,
737                        },
738                    )
739                    .collect(),
740                limits: root_limits(root),
741            })
742        })
743        .collect::<Result<Vec<_>, FleetGenerateError>>()?;
744    let topology = plan_initial_fleet_topology(config.model(), root_inputs)
745        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
746    let capacity_roots = bind_root_generation_inputs(
747        &source.fleet_subnet_roots,
748        &seed.roots,
749        &topology.fleet_subnet_roots,
750    )?
751    .into_iter()
752    .map(|binding| RootPoolCapacityInput {
753        component_admissions: binding.planned.component_admissions.clone(),
754        pool_target_cycles: binding
755            .planned
756            .limits
757            .canister_pool
758            .canister_cycles
759            .to_u128(),
760        root: binding.seed.root.clone(),
761    })
762    .collect::<Vec<_>>();
763    validate_root_pool_capacity(config.model(), &capacity_roots)?;
764    for root in &source.fleet_subnet_roots {
765        fresh_root_pool_count(root, &deployment_configuration)?;
766    }
767    validate_source_funding(
768        &source,
769        canonical_network_id(request, &source)?
770            == canic_core::ids::CanonicalNetworkId::ic_mainnet(),
771    )?;
772
773    let (infrastructure, complete) = release_authority(request, config.component_topology())?;
774    if complete.manifest.infrastructure_artifact_manifest_sha256 != infrastructure.digest {
775        return Err(FleetGenerateError::Release(
776            "complete release set does not bind its infrastructure manifest".to_string(),
777        ));
778    }
779    let coordinator_artifact = artifact(
780        &infrastructure.manifest.entries,
781        CanicInfrastructureRole::FleetCoordinator,
782    )?;
783    let root_artifact = artifact(
784        &infrastructure.manifest.entries,
785        CanicInfrastructureRole::FleetSubnetRoot,
786    )?;
787    let store_artifact = artifact(
788        &infrastructure.manifest.entries,
789        CanicInfrastructureRole::WasmStore,
790    )?;
791    let coordinator_candid = candid_sidecar(request.root, coordinator_artifact)?;
792    let root_candid = candid_sidecar(request.root, root_artifact)?;
793    let store_candid = candid_sidecar(request.root, store_artifact)?;
794    let root_candid_path = request.root.join(&root_candid);
795    let observed = if seed.fresh_estate {
796        validate_fresh_generation_authority(request, &source, operator)?;
797        BTreeMap::new()
798    } else {
799        observe_estate(&EstateObservationRequest {
800            app: config.model().app_id(),
801            generation: request,
802            operator,
803            root_candid: &root_candid_path,
804            root_wasm_sha256: &root_artifact.wasm_sha256_hex,
805            seed: &seed,
806            source: &source,
807            topology: &topology,
808        })?
809    };
810    let treasury = seed
811        .treasury
812        .as_ref()
813        .map_or(seed.coordinator.as_str(), |treasury| {
814            treasury.principal.as_str()
815        });
816    if !seed.fresh_estate && !observed.contains_key(treasury) {
817        return Err(FleetGenerateError::Authority(
818            "treasury must be the seeded Coordinator or another explicitly seeded controlled canister"
819                .to_string(),
820        ));
821    }
822    let ledger_fee_cycles = observe_ledger_fee(request, &seed.cycles_ledger)?;
823    let desired = compile_desired(CompileDesiredRequest {
824        request,
825        source: &source,
826        seed: &seed,
827        admission,
828        app: config.model().app_id().clone(),
829        deployment_configuration,
830        topology,
831        coordinator_artifact,
832        root_artifact,
833        store_artifact,
834        coordinator_candid: &coordinator_candid,
835        root_candid: &root_candid,
836        store_candid: &store_candid,
837        observed: &observed,
838        treasury,
839        ledger_fee_cycles,
840    })?;
841    crate::fleet_ensure::policy::validate_terminal_pool_capacity(&desired)?;
842    let observed_controlled_cycles = observed.values().try_fold(0_u128, |total, canister| {
843        total.checked_add(canister.cycles).ok_or_else(|| {
844            FleetGenerateError::Authority("observed controlled cycle total overflowed".to_string())
845        })
846    })?;
847    Ok(GeneratedDesiredFleet {
848        desired,
849        observed_canisters: observed.len(),
850        observed_controlled_cycles,
851        release_build_id: request.release_build_id,
852    })
853}
854
855fn validate_fresh_generation_authority(
856    request: &FleetGenerateRequest<'_>,
857    source: &FleetSource,
858    operator: Principal,
859) -> Result<(), FleetGenerateError> {
860    let icp = IcpCli::new(
861        request.icp_executable,
862        Some(request.environment.to_string()),
863    )
864    .with_cwd(request.root.to_path_buf());
865    let active = icp
866        .identity_principal_text()
867        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
868    if active != operator.to_text() {
869        return Err(FleetGenerateError::Authority(format!(
870            "active identity {active} differs from protected operator {operator}"
871        )));
872    }
873    let network = resolve_icp_build_network_from_root(request.root, request.environment)
874        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
875    if network != BuildNetwork::Ic {
876        return Ok(());
877    }
878    let now = SystemTime::now()
879        .duration_since(UNIX_EPOCH)
880        .map_err(|_| FleetGenerateError::Clock)?
881        .as_secs();
882    let catalog = load_mainnet_subnet_catalog(request.root, now)
883        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
884    require_fiduciary_acknowledgement(
885        &catalog.catalog,
886        &source.coordinator.subnet.subnet,
887        source.coordinator.subnet.acknowledge_fiduciary_cost,
888        "Coordinator",
889    )?;
890    for root in &source.fleet_subnet_roots {
891        require_fiduciary_acknowledgement(
892            &catalog.catalog,
893            &root.placement_subnet,
894            root.acknowledge_fiduciary_cost,
895            "Fleet Subnet Root",
896        )?;
897    }
898    Ok(())
899}
900
901fn fresh_root_pool_count(
902    source: &RootSource,
903    configuration: &ComponentDeploymentConfiguration,
904) -> Result<usize, FleetGenerateError> {
905    let workloads = crate::fleet_ensure::policy::initial_workload_count(
906        configuration,
907        source
908            .component_group_placements
909            .iter()
910            .flat_map(|(deployment, ordinals)| ordinals.iter().map(move |_| deployment.as_str())),
911        &source.placement_subnet,
912    )?;
913    let count = crate::fleet_ensure::policy::terminal_pool_supply(
914        &source.placement_subnet,
915        workloads,
916        source.canister_pool.minimum_size,
917        source.canister_pool.maximum_size,
918    )?;
919    usize::try_from(count).map_err(|_| {
920        FleetGenerateError::FreshSeedConflict(
921            "complete initial pool supply cannot be represented on this host".to_string(),
922        )
923    })
924}
925
926struct CompileDesiredRequest<'a> {
927    request: &'a FleetGenerateRequest<'a>,
928    source: &'a FleetSource,
929    seed: &'a EstateSeed,
930    admission: FleetAdmissionPolicyTemplate,
931    app: canic_core::ids::AppId,
932    deployment_configuration: ComponentDeploymentConfiguration,
933    topology: crate::component_topology::PlannedFleetTopology,
934    coordinator_artifact: &'a CanicInfrastructureArtifactEntry,
935    root_artifact: &'a CanicInfrastructureArtifactEntry,
936    store_artifact: &'a CanicInfrastructureArtifactEntry,
937    coordinator_candid: &'a str,
938    root_candid: &'a str,
939    store_candid: &'a str,
940    observed: &'a BTreeMap<String, ObservedCanister>,
941    treasury: &'a str,
942    ledger_fee_cycles: u128,
943}
944
945struct RootGenerationBinding<'a> {
946    source: &'a RootSource,
947    seed: &'a RootSeed,
948    planned: &'a PlannedFleetSubnetRootTopology,
949}
950
951fn bind_root_generation_inputs<'a>(
952    sources: &'a [RootSource],
953    seeds: &'a [RootSeed],
954    planned_roots: &'a [PlannedFleetSubnetRootTopology],
955) -> Result<Vec<RootGenerationBinding<'a>>, FleetGenerateError> {
956    let mut sources_by_subnet = BTreeMap::new();
957    for source in sources {
958        let placement = parse_subnet("Fleet policy Root", &source.placement_subnet)?;
959        if sources_by_subnet.insert(placement, source).is_some() {
960            return Err(FleetGenerateError::SeedTopology(format!(
961                "Fleet policy repeats Root placement Subnet {placement}"
962            )));
963        }
964    }
965    let mut seeds_by_subnet = BTreeMap::new();
966    for seed in seeds {
967        let placement = parse_subnet("Fleet identity-seed Root", &seed.placement_subnet)?;
968        if seeds_by_subnet.insert(placement, seed).is_some() {
969            return Err(FleetGenerateError::SeedTopology(format!(
970                "Fleet identity seed repeats Root placement Subnet {placement}"
971            )));
972        }
973    }
974    let mut planned_by_subnet = BTreeMap::new();
975    for planned in planned_roots {
976        if planned_by_subnet
977            .insert(planned.placement_subnet, planned)
978            .is_some()
979        {
980            return Err(FleetGenerateError::SeedTopology(format!(
981                "compiled topology repeats Root placement Subnet {}",
982                planned.placement_subnet
983            )));
984        }
985    }
986    let source_subnets = sources_by_subnet.keys().copied().collect::<BTreeSet<_>>();
987    let seed_subnets = seeds_by_subnet.keys().copied().collect::<BTreeSet<_>>();
988    let planned_subnets = planned_by_subnet.keys().copied().collect::<BTreeSet<_>>();
989    if source_subnets != seed_subnets || source_subnets != planned_subnets {
990        return Err(FleetGenerateError::SeedTopology(
991            "Fleet policy, identity-seed and compiled Root placement Subnet sets differ"
992                .to_string(),
993        ));
994    }
995    Ok(planned_by_subnet
996        .into_iter()
997        .map(|(placement, planned)| RootGenerationBinding {
998            source: sources_by_subnet[&placement],
999            seed: seeds_by_subnet[&placement],
1000            planned,
1001        })
1002        .collect())
1003}
1004
1005#[expect(
1006    clippy::too_many_lines,
1007    reason = "one compiler constructs the complete canonical desired document"
1008)]
1009fn compile_desired(input: CompileDesiredRequest<'_>) -> Result<DesiredFleet, FleetGenerateError> {
1010    let mut canisters = Vec::new();
1011    canisters.push(infrastructure_canister(
1012        "coordinator",
1013        DesiredCanisterKind::Coordinator,
1014        Some(DesiredCanisterInit::Coordinator),
1015        (!input.seed.fresh_estate).then_some(input.seed.coordinator.as_str()),
1016        None,
1017        &input.source.operator,
1018        &input.source.coordinator.subnet.subnet,
1019        input.source.coordinator.creation_funding.cycles.to_u128(),
1020        input
1021            .source
1022            .coordinator
1023            .root_funding
1024            .minimum_reserve_cycles
1025            .to_u128(),
1026        input.coordinator_artifact,
1027    ));
1028    let mut bootstrap_roots = Vec::new();
1029    let mut placements = Vec::new();
1030    let root_bindings = bind_root_generation_inputs(
1031        &input.source.fleet_subnet_roots,
1032        &input.seed.roots,
1033        &input.topology.fleet_subnet_roots,
1034    )?;
1035    for (index, binding) in root_bindings.into_iter().enumerate() {
1036        let RootGenerationBinding {
1037            source,
1038            seed,
1039            planned,
1040        } = binding;
1041        require_cycles_creation(&source.root_creation_funding, "Fleet Subnet Root")?;
1042        require_cycles_creation(&source.wasm_store_creation_funding, "Wasm Store")?;
1043        let root_name = format!("root-{index}");
1044        let store_name = format!("store-{index}");
1045        canisters.push(infrastructure_canister(
1046            &root_name,
1047            DesiredCanisterKind::Root,
1048            Some(DesiredCanisterInit::Root {
1049                root: root_name.clone(),
1050            }),
1051            (!input.seed.fresh_estate).then_some(seed.root.as_str()),
1052            Some("coordinator"),
1053            &input.source.operator,
1054            &source.placement_subnet,
1055            source.root_creation_funding.cycles.to_u128(),
1056            source.root_funding.request_threshold.to_u128(),
1057            input.root_artifact,
1058        ));
1059        let mut store = infrastructure_canister(
1060            &store_name,
1061            DesiredCanisterKind::Store,
1062            Some(DesiredCanisterInit::Store {
1063                root: root_name.clone(),
1064            }),
1065            (!input.seed.fresh_estate).then_some(seed.store.as_str()),
1066            Some(&root_name),
1067            &input.source.operator,
1068            &source.placement_subnet,
1069            source.wasm_store_creation_funding.cycles.to_u128(),
1070            0,
1071            input.store_artifact,
1072        );
1073        if input.seed.fresh_estate {
1074            store.controller_canisters.push(root_name.clone());
1075        } else {
1076            store.controllers.push(seed.root.clone());
1077            store.controllers.sort();
1078        }
1079        canisters.push(store);
1080        let mut pool_names = Vec::new();
1081        let pool_count = if input.seed.fresh_estate {
1082            fresh_root_pool_count(source, &input.deployment_configuration)?
1083        } else {
1084            seed.pool_imports.len()
1085        };
1086        if canisters.len().saturating_add(pool_count) > MAX_FLEET_ENSURE_CANISTERS {
1087            return Err(FleetGenerateError::Policy(
1088                crate::fleet_ensure::policy::EnsurePolicyError::TooManyCanisters {
1089                    actual: canisters.len().saturating_add(pool_count),
1090                    maximum: MAX_FLEET_ENSURE_CANISTERS,
1091                },
1092            ));
1093        }
1094        for pool_index in 0..pool_count {
1095            let name = format!("root-{index}-pool-{pool_index}");
1096            pool_names.push(name.clone());
1097            let readiness_floor = source.canister_pool.canister_cycles.to_u128();
1098            let creation_funding = if input.seed.fresh_estate {
1099                fresh_pool_creation_funding(readiness_floor)?
1100            } else {
1101                readiness_floor
1102            };
1103            canisters.push(DesiredCanister {
1104                canic_init: None,
1105                controller_canisters: if input.seed.fresh_estate {
1106                    vec![root_name.clone()]
1107                } else {
1108                    Vec::new()
1109                },
1110                controllers: if input.seed.fresh_estate {
1111                    Vec::new()
1112                } else {
1113                    vec![seed.root.clone()]
1114                },
1115                drain: None,
1116                initial_cycles: config_cycles(creation_funding),
1117                init_arg: None,
1118                init_candid: None,
1119                kind: DesiredCanisterKind::Pool,
1120                minimum_cycles: source.canister_pool.canister_cycles.to_config_string(),
1121                name,
1122                parent: Some(root_name.clone()),
1123                presence: DesiredPresence::Present,
1124                principal: (!input.seed.fresh_estate)
1125                    .then(|| seed.pool_imports[pool_index].clone()),
1126                protocol_binding: None,
1127                replace: false,
1128                subnet: source.placement_subnet.clone(),
1129                wasm: None,
1130            });
1131        }
1132        if seed.pool_imports != source.canister_pool.imports
1133            && !source.canister_pool.imports.is_empty()
1134        {
1135            return Err(FleetGenerateError::SeedTopology(format!(
1136                "Root {} pool imports differ between policy source and identity seed",
1137                source.placement_subnet
1138            )));
1139        }
1140        bootstrap_roots.push(DesiredFleetBootstrapRoot {
1141            canister_pool_imports: pool_names,
1142            component_admissions: planned.component_admissions.clone(),
1143            component_topology_digest: planned.component_topology_digest,
1144            funding: root_funding(input.source.funding_profile, &source.root_funding),
1145            limits: root_limits(source),
1146            placement_subnet: planned.placement_subnet,
1147            root: root_name.clone(),
1148            store: store_name,
1149        });
1150        for (deployment, ordinals) in &source.component_group_placements {
1151            for ordinal in ordinals {
1152                placements.push(DesiredComponentGroupPlacement {
1153                    deployment: deployment.to_string(),
1154                    ordinal: *ordinal,
1155                    root: root_name.clone(),
1156                });
1157            }
1158        }
1159    }
1160    if !input.seed.fresh_estate && input.treasury != input.seed.coordinator {
1161        let treasury_observation = input
1162            .observed
1163            .get(input.treasury)
1164            .expect("verified treasury observation");
1165        canisters.push(DesiredCanister {
1166            canic_init: None,
1167            controller_canisters: Vec::new(),
1168            controllers: vec![input.source.operator.clone()],
1169            drain: None,
1170            initial_cycles: config_cycles(0),
1171            init_arg: None,
1172            init_candid: None,
1173            kind: DesiredCanisterKind::Auxiliary,
1174            minimum_cycles: config_cycles(0),
1175            name: "treasury".to_string(),
1176            parent: None,
1177            presence: DesiredPresence::Present,
1178            principal: Some(input.treasury.to_string()),
1179            protocol_binding: None,
1180            replace: false,
1181            subnet: treasury_observation.subnet.clone(),
1182            wasm: None,
1183        });
1184    }
1185    let app_config = relative_path(input.request.root, input.request.app_config)?;
1186    let app = input.app.clone();
1187    Ok(DesiredFleet {
1188        bootstrap: Some(DesiredFleetBootstrap {
1189            admission: input.admission,
1190            app,
1191            canonical_network_id: canonical_network_id(input.request, input.source)?,
1192            component_deployment_configuration: input.deployment_configuration,
1193            coordinator: "coordinator".to_string(),
1194            coordinator_subnet: parse_subnet(
1195                "Coordinator",
1196                &input.source.coordinator.subnet.subnet,
1197            )?,
1198            fleet_id: input.seed.fleet_id,
1199            fresh_estate: input.seed.fresh_estate,
1200            release_build_id: input.request.release_build_id,
1201            root_funding: Some(coordinator_funding(
1202                input.source.funding_profile,
1203                &input.source.coordinator.root_funding,
1204            )),
1205            roots: bootstrap_roots,
1206        }),
1207        canisters,
1208        cycles_ledger: input.seed.cycles_ledger.clone(),
1209        environment: input.request.environment.to_string(),
1210        fleet: input.request.fleet.to_string(),
1211        ledger_fee_cycles: config_cycles(input.ledger_fee_cycles),
1212        // This fee binds future creation, including growth after retaining paid identities.
1213        management_creation_fee_cycles: input.seed.management_creation_fee_cycles.clone(),
1214        material_cycle_threshold: config_cycles(GENERATED_RETAINED_MATERIAL_CYCLE_THRESHOLD),
1215        maximum_observation_burn_cycles: config_cycles(
1216            GENERATED_RETAINED_MAXIMUM_OBSERVATION_BURN_CYCLES,
1217        ),
1218        maximum_stalled_observations: 8,
1219        maximum_update_burn_cycles: config_cycles(GENERATED_RETAINED_MAXIMUM_UPDATE_BURN_CYCLES),
1220        operator: input.source.operator.clone(),
1221        protocol: Some(DesiredFleetProtocol {
1222            app_config,
1223            component_group_placements: placements,
1224            coordinator_candid: input.coordinator_candid.to_string(),
1225            root_candid: input.root_candid.to_string(),
1226            store_candid: input.store_candid.to_string(),
1227        }),
1228        protocol_steps: Vec::new(),
1229        schema_version: FLEET_ENSURE_SCHEMA_VERSION,
1230        treasury: if input.treasury == input.seed.coordinator {
1231            "coordinator".to_string()
1232        } else {
1233            "treasury".to_string()
1234        },
1235    })
1236}
1237
1238fn observe_ledger_fee(
1239    request: &FleetGenerateRequest<'_>,
1240    cycles_ledger: &str,
1241) -> Result<u128, FleetGenerateError> {
1242    parse_principal("Cycles Ledger", cycles_ledger)?;
1243    let icp = IcpCli::new(
1244        request.icp_executable,
1245        Some(request.environment.to_string()),
1246    )
1247    .with_cwd(request.root.to_path_buf());
1248    let value: Nat = icp
1249        .canister_query_candid(cycles_ledger, "icrc1_fee", &(), None)
1250        .map_err(|error| {
1251            FleetGenerateError::Authority(format!(
1252                "failed to observe the exact Cycles Ledger fee: {error}"
1253            ))
1254        })?;
1255    let rendered = value.to_string();
1256    u128::try_from(value.0).map_err(|_| {
1257        FleetGenerateError::Authority(format!(
1258            "live Cycles Ledger fee exceeds the supported cycle range: {rendered}"
1259        ))
1260    })
1261}
1262
1263#[expect(
1264    clippy::too_many_lines,
1265    reason = "one observation boundary validates identity, controllers, placement, and cycles together"
1266)]
1267fn observe_estate(
1268    input: &EstateObservationRequest<'_, '_>,
1269) -> Result<BTreeMap<String, ObservedCanister>, FleetGenerateError> {
1270    let EstateObservationRequest {
1271        app,
1272        generation: request,
1273        operator,
1274        root_candid,
1275        root_wasm_sha256,
1276        seed,
1277        source,
1278        topology,
1279    } = input;
1280    let icp = IcpCli::new(
1281        request.icp_executable,
1282        Some(request.environment.to_string()),
1283    )
1284    .with_cwd(request.root.to_path_buf());
1285    let active = icp
1286        .identity_principal_text()
1287        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
1288    if active != operator.to_text() {
1289        return Err(FleetGenerateError::Authority(format!(
1290            "active identity {active} differs from protected operator {operator}"
1291        )));
1292    }
1293    let network = resolve_icp_build_network_from_root(request.root, request.environment)
1294        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
1295    let catalog = if network == BuildNetwork::Ic {
1296        let now = SystemTime::now()
1297            .duration_since(UNIX_EPOCH)
1298            .map_err(|_| FleetGenerateError::Clock)?
1299            .as_secs();
1300        Some(
1301            load_mainnet_subnet_catalog(request.root, now)
1302                .map_err(|error| FleetGenerateError::Authority(error.to_string()))?,
1303        )
1304    } else {
1305        None
1306    };
1307    if let Some(catalog) = &catalog {
1308        require_fiduciary_acknowledgement(
1309            &catalog.catalog,
1310            &source.coordinator.subnet.subnet,
1311            source.coordinator.subnet.acknowledge_fiduciary_cost,
1312            "Coordinator",
1313        )?;
1314        for root in &source.fleet_subnet_roots {
1315            require_fiduciary_acknowledgement(
1316                &catalog.catalog,
1317                &root.placement_subnet,
1318                root.acknowledge_fiduciary_cost,
1319                "Fleet Subnet Root",
1320            )?;
1321        }
1322    }
1323    let mut expected = BTreeMap::new();
1324    insert_expected_canister(
1325        &mut expected,
1326        seed.coordinator.clone(),
1327        (
1328            source.coordinator.subnet.subnet.clone(),
1329            vec![source.operator.clone()],
1330        ),
1331        "Coordinator",
1332    )?;
1333    if let Some(treasury) = &seed.treasury
1334        && treasury.principal != seed.coordinator
1335    {
1336        insert_expected_canister(
1337            &mut expected,
1338            treasury.principal.clone(),
1339            (treasury.subnet.clone(), vec![source.operator.clone()]),
1340            "treasury",
1341        )?;
1342    }
1343    let mut stopped_roots = Vec::new();
1344    for root in &seed.roots {
1345        insert_expected_canister(
1346            &mut expected,
1347            root.root.clone(),
1348            (root.placement_subnet.clone(), vec![source.operator.clone()]),
1349            "Fleet Subnet Root",
1350        )?;
1351    }
1352    let mut observed = BTreeMap::new();
1353    let root_principals = seed
1354        .roots
1355        .iter()
1356        .map(|root| root.root.as_str())
1357        .collect::<BTreeSet<_>>();
1358    let mut root_statuses = BTreeMap::new();
1359    for (canister, (expected_subnet, required_controllers)) in expected {
1360        parse_principal("retained canister", &canister)?;
1361        let status = icp.canister_status_report(&canister).map_err(|error| {
1362            FleetGenerateError::CanisterUnavailable {
1363                canister: canister.clone(),
1364                reason: error.to_string(),
1365            }
1366        })?;
1367        if status.id != canister {
1368            return Err(FleetGenerateError::SeedTopology(format!(
1369                "retained canister query for {canister} returned identity {}",
1370                status.id
1371            )));
1372        }
1373        let controllers = status
1374            .settings
1375            .map(|settings| settings.controllers)
1376            .unwrap_or_default();
1377        require_exact_controllers(&canister, controllers, required_controllers)?;
1378        let subnet = if let Some(catalog) = &catalog {
1379            catalog
1380                .catalog
1381                .resolve_canister_route(&status.id)
1382                .map_err(|error| FleetGenerateError::Authority(error.to_string()))?
1383                .subnet
1384                .to_text()
1385        } else {
1386            expected_subnet.clone()
1387        };
1388        if subnet != expected_subnet {
1389            return Err(FleetGenerateError::SubnetMismatch {
1390                actual: subnet,
1391                canister,
1392                expected: expected_subnet,
1393            });
1394        }
1395        let cycles = status
1396            .cycles
1397            .as_deref()
1398            .ok_or_else(|| FleetGenerateError::CanisterUnavailable {
1399                canister: canister.clone(),
1400                reason: "controller-authenticated native cycle balance is unavailable".to_string(),
1401            })?
1402            .replace('_', "")
1403            .parse::<u128>()
1404            .map_err(|_| FleetGenerateError::Authority("invalid live cycle balance".to_string()))?;
1405        let module_sha256 = status
1406            .module_hash
1407            .as_deref()
1408            .map(|hash| normalize_observed_module_sha256(&canister, hash))
1409            .transpose()?;
1410        if root_principals.contains(canister.as_str()) {
1411            let runtime_status = parse_observed_runtime_status(
1412                &canister,
1413                status.status.as_deref().ok_or_else(|| {
1414                    FleetGenerateError::CanisterUnavailable {
1415                        canister: canister.clone(),
1416                        reason: "management runtime status is unavailable to the selected identity"
1417                            .to_string(),
1418                    }
1419                })?,
1420            )?;
1421            if module_sha256.is_none() {
1422                return Err(FleetGenerateError::CanisterUnavailable {
1423                    canister,
1424                    reason: "retained Root has no installed module SHA-256".to_string(),
1425                });
1426            }
1427            root_statuses.insert(status.id.clone(), runtime_status);
1428        }
1429        if observed
1430            .insert(
1431                status.id.clone(),
1432                ObservedCanister {
1433                    cycles,
1434                    module_sha256,
1435                    subnet,
1436                },
1437            )
1438            .is_some()
1439        {
1440            return Err(FleetGenerateError::SeedTopology(format!(
1441                "retained identity {} is assigned to more than one role",
1442                status.id
1443            )));
1444        }
1445    }
1446    for root in &seed.roots {
1447        match root_statuses.get(&root.root) {
1448            Some(CanisterRuntimeStatus::Running)
1449                if observed
1450                    .get(&root.root)
1451                    .and_then(|value| value.module_sha256.as_deref())
1452                    == Some(*root_wasm_sha256) => {}
1453            Some(CanisterRuntimeStatus::Running) => {
1454                // Generation describes the replacement without querying a different runtime.
1455                // The reviewed Ensure prerequisite owns the management reinstall.
1456            }
1457            Some(CanisterRuntimeStatus::Stopped) => {
1458                let observation = observed.get(&root.root).ok_or_else(|| {
1459                    FleetGenerateError::SeedTopology(format!(
1460                        "retained Root {} has no exact management observation",
1461                        root.root
1462                    ))
1463                })?;
1464                let module_sha256 = observation.module_sha256.clone().ok_or_else(|| {
1465                    FleetGenerateError::CanisterUnavailable {
1466                        canister: root.root.clone(),
1467                        reason: "retained Root has no installed module SHA-256".to_string(),
1468                    }
1469                })?;
1470                let root_name = retained_root_name(source, seed, topology, &root.root)?;
1471                stopped_roots.push(RootManagementBinding {
1472                    controllers: vec![operator.to_text()],
1473                    name: root_name,
1474                    module_sha256,
1475                    principal: root.root.clone(),
1476                    subnet: observation.subnet.clone(),
1477                });
1478            }
1479            Some(CanisterRuntimeStatus::Stopping) => {
1480                return Err(FleetGenerateError::CanisterUnavailable {
1481                    canister: root.root.clone(),
1482                    reason: "retained Root is stopping; wait for terminal Stopped state and rerun generation"
1483                        .to_string(),
1484                });
1485            }
1486            None => {
1487                return Err(FleetGenerateError::SeedTopology(format!(
1488                    "retained Root {} has no runtime-status observation",
1489                    root.root
1490                )));
1491            }
1492        }
1493    }
1494    if let Some(first) = stopped_roots.first() {
1495        let authority = write_root_start_authority(
1496            &EnsurePaths::under(request.root, request.environment, request.fleet),
1497            RetainedRootStartAuthorityRecord {
1498                authority_sha256: String::new(),
1499                environment: request.environment.to_string(),
1500                fleet: request.fleet.to_string(),
1501                fleet_id: seed.fleet_id,
1502                roots: stopped_roots.clone(),
1503                schema_version: FLEET_ENSURE_SCHEMA_VERSION,
1504            },
1505        )
1506        .map_err(|error| FleetGenerateError::Authority(error.to_string()))?;
1507        return Err(FleetGenerateError::StoppedRootStartRequired(Box::new(
1508            StoppedRootStartPrerequisite {
1509                authority_sha256: authority.authority_sha256,
1510                controller: operator.to_text(),
1511                fleet: request.fleet.to_string(),
1512                module_sha256: first.module_sha256.clone(),
1513                root: first.principal.clone(),
1514                root_count: stopped_roots.len(),
1515                subnet: first.subnet.clone(),
1516            },
1517        )));
1518    }
1519    if seed.roots.iter().any(|root| {
1520        observed
1521            .get(&root.root)
1522            .and_then(|live| live.module_sha256.as_deref())
1523            != Some(*root_wasm_sha256)
1524    }) {
1525        return Ok(observed);
1526    }
1527    observe_root_owned_pool_assets(
1528        &icp,
1529        &RootEstateAuthorityRequest {
1530            app,
1531            generation: request,
1532            root_candid,
1533            seed,
1534            source,
1535            topology,
1536        },
1537        &mut observed,
1538    )?;
1539    Ok(observed)
1540}
1541
1542fn retained_root_name(
1543    source: &FleetSource,
1544    seed: &EstateSeed,
1545    topology: &PlannedFleetTopology,
1546    principal: &str,
1547) -> Result<String, FleetGenerateError> {
1548    bind_root_generation_inputs(
1549        &source.fleet_subnet_roots,
1550        &seed.roots,
1551        &topology.fleet_subnet_roots,
1552    )?
1553    .into_iter()
1554    .enumerate()
1555    .find_map(|(index, binding)| (binding.seed.root == principal).then(|| format!("root-{index}")))
1556    .ok_or_else(|| {
1557        FleetGenerateError::SeedTopology(format!(
1558            "retained Root {principal} is absent from the compiled topology"
1559        ))
1560    })
1561}
1562
1563fn parse_observed_runtime_status(
1564    canister: &str,
1565    value: &str,
1566) -> Result<CanisterRuntimeStatus, FleetGenerateError> {
1567    match value.to_ascii_lowercase().as_str() {
1568        "running" => Ok(CanisterRuntimeStatus::Running),
1569        "stopped" => Ok(CanisterRuntimeStatus::Stopped),
1570        "stopping" => Ok(CanisterRuntimeStatus::Stopping),
1571        _ => Err(FleetGenerateError::CanisterUnavailable {
1572            canister: canister.to_string(),
1573            reason: format!("unknown management runtime status {value:?}"),
1574        }),
1575    }
1576}
1577
1578fn normalize_observed_module_sha256(
1579    canister: &str,
1580    value: &str,
1581) -> Result<String, FleetGenerateError> {
1582    let normalized = value.trim_start_matches("0x").to_ascii_lowercase();
1583    if normalized.len() != 64 || !normalized.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1584        return Err(FleetGenerateError::CanisterUnavailable {
1585            canister: canister.to_string(),
1586            reason: format!("invalid installed module SHA-256 {value:?}"),
1587        });
1588    }
1589    Ok(normalized)
1590}
1591
1592fn require_root_estate_authority(
1593    root: &str,
1594    actual: &FleetSubnetRootAuthority,
1595    expected: &ExpectedRootEstateAuthority,
1596) -> Result<(), FleetGenerateError> {
1597    if actual.binding.authority.binding != expected.coordinator {
1598        return Err(FleetGenerateError::SeedTopology(format!(
1599            "Root {root} live Coordinator authority differs from protected policy"
1600        )));
1601    }
1602    if RootIdentityRelationship::from(&actual.binding) != expected.root {
1603        return Err(FleetGenerateError::SeedTopology(format!(
1604            "Root {root} live identity or placement differs from the identity seed"
1605        )));
1606    }
1607    if WasmStoreIdentityRelationship::from(&actual.wasm_store_authority) != expected.store {
1608        return Err(FleetGenerateError::SeedTopology(format!(
1609            "Root {root} live Store relationship differs from the identity seed"
1610        )));
1611    }
1612    Ok(())
1613}
1614
1615fn require_root_policy_matches(
1616    root: &str,
1617    actual: &RootDesiredPolicy,
1618    expected: &RootDesiredPolicy,
1619) -> Result<(), FleetGenerateError> {
1620    if actual != expected {
1621        return Err(FleetGenerateError::SeedTopology(format!(
1622            "Root {root} installed policy differs from the current configuration"
1623        )));
1624    }
1625    Ok(())
1626}
1627
1628fn require_fiduciary_acknowledgement(
1629    catalog: &ic_query::subnet_catalog::ValidatedSubnetCatalog,
1630    subnet: &str,
1631    acknowledged: bool,
1632    role: &str,
1633) -> Result<(), FleetGenerateError> {
1634    let info = catalog.subnet_by_principal(subnet).ok_or_else(|| {
1635        FleetGenerateError::Authority(format!(
1636            "{role} Subnet {subnet} is absent from validated Registry evidence"
1637        ))
1638    })?;
1639    let fiduciary = info.subnet_specialization == SubnetSpecialization::Fiduciary;
1640    if fiduciary != acknowledged {
1641        return Err(FleetGenerateError::Authority(format!(
1642            "{role} Subnet {subnet} Fiduciary cost acknowledgement differs from validated Registry classification"
1643        )));
1644    }
1645    Ok(())
1646}
1647
1648#[expect(
1649    clippy::too_many_lines,
1650    reason = "bounded Root inventory observation validates one complete authority tuple and page stream"
1651)]
1652fn observe_root_owned_pool_assets(
1653    icp: &IcpCli,
1654    request: &RootEstateAuthorityRequest<'_, '_>,
1655    observed: &mut BTreeMap<String, ObservedCanister>,
1656) -> Result<(), FleetGenerateError> {
1657    let expected_network = canonical_network_id(request.generation, request.source)?;
1658    let expected_fleet = request.seed.fleet_id;
1659    let expected_coordinator = parse_principal("Fleet Coordinator", &request.seed.coordinator)?;
1660    let expected_coordinator_subnet = parse_subnet(
1661        "Fleet Coordinator",
1662        &request.source.coordinator.subnet.subnet,
1663    )?;
1664    for root in &request.seed.roots {
1665        let source_root = request
1666            .source
1667            .fleet_subnet_roots
1668            .iter()
1669            .find(|candidate| candidate.placement_subnet == root.placement_subnet)
1670            .ok_or_else(|| {
1671                FleetGenerateError::SeedTopology(format!(
1672                    "Root {} has no policy source",
1673                    root.placement_subnet
1674                ))
1675            })?;
1676        let planned = request
1677            .topology
1678            .fleet_subnet_roots
1679            .iter()
1680            .find(|candidate| candidate.placement_subnet.to_string() == root.placement_subnet)
1681            .ok_or_else(|| {
1682                FleetGenerateError::SeedTopology(format!(
1683                    "Root {} has no compiled topology authority",
1684                    root.placement_subnet
1685                ))
1686            })?;
1687        let expected = std::iter::once(root.store.as_str())
1688            .chain(root.pool_imports.iter().map(String::as_str))
1689            .collect::<std::collections::BTreeSet<_>>();
1690        let root_principal = parse_principal("Fleet Subnet Root", &root.root)?;
1691        let response: RootEstateStatusResponse = query_with_candid(
1692            icp,
1693            request.root_candid,
1694            root_principal,
1695            protocol::CANIC_ROOT_STATUS,
1696            &RootEstateStatusRequest::FleetAuthority,
1697        )
1698        .map_err(|error| FleetGenerateError::CanisterUnavailable {
1699            canister: root.root.clone(),
1700            reason: format!("protected Root authority observation failed: {error}"),
1701        })?;
1702        let RootEstateStatusResponse::FleetAuthority(authority) = response else {
1703            return Err(FleetGenerateError::SeedTopology(format!(
1704                "Root {} returned the wrong authority projection",
1705                root.root
1706            )));
1707        };
1708        let expected_coordinator = FleetCoordinatorBinding {
1709            fleet: FleetBinding {
1710                fleet: FleetKey {
1711                    canonical_network_id: expected_network,
1712                    fleet_id: expected_fleet,
1713                },
1714                app: request.app.clone(),
1715            },
1716            coordinator_subnet: expected_coordinator_subnet,
1717            coordinator: expected_coordinator,
1718        };
1719        let expected_registry_authority = FleetRegistryAuthority {
1720            binding: expected_coordinator.clone(),
1721            epoch: authority.binding.authority.epoch,
1722        };
1723        let expected_authority = ExpectedRootEstateAuthority {
1724            coordinator: expected_coordinator,
1725            root: RootIdentityRelationship {
1726                authority: expected_registry_authority.clone(),
1727                placement_subnet: planned.placement_subnet,
1728                fleet_subnet_root: root_principal,
1729            },
1730            store: WasmStoreIdentityRelationship {
1731                authority: expected_registry_authority,
1732                placement_subnet: planned.placement_subnet,
1733                fleet_subnet_root: root_principal,
1734                wasm_store: parse_principal("Wasm Store", &root.store)?,
1735            },
1736        };
1737        require_root_estate_authority(&root.root, &authority, &expected_authority)?;
1738        let expected_policy = RootDesiredPolicy {
1739            component_admissions: planned.component_admissions.clone(),
1740            component_topology_digest: planned.component_topology_digest,
1741            funding: root_funding(request.source.funding_profile, &source_root.root_funding),
1742            installation_controller: parse_principal("operator", &request.source.operator)?,
1743            limits: planned.limits.clone(),
1744        };
1745        require_root_policy_matches(
1746            &root.root,
1747            &RootDesiredPolicy::from(authority.as_ref()),
1748            &expected_policy,
1749        )?;
1750        let mut found = BTreeMap::new();
1751        let mut start_after = None;
1752        loop {
1753            let status_request = RootEstateStatusRequest::Pool(CanisterPoolStatusRequest {
1754                start_after,
1755                limit: 256,
1756            });
1757            let page = {
1758                let response: RootEstateStatusResponse = query_with_candid(
1759                    icp,
1760                    request.root_candid,
1761                    root_principal,
1762                    protocol::CANIC_ROOT_STATUS,
1763                    &status_request,
1764                )
1765                .map_err(|error| FleetGenerateError::CanisterUnavailable {
1766                    canister: root.root.clone(),
1767                    reason: format!("protected Root pool observation failed: {error}"),
1768                })?;
1769                let RootEstateStatusResponse::Pool(page) = response else {
1770                    return Err(FleetGenerateError::SeedTopology(format!(
1771                        "Root {} returned the wrong pool projection",
1772                        root.root
1773                    )));
1774                };
1775                *page
1776            };
1777            if page.config != authority.binding.limits.canister_pool {
1778                return Err(FleetGenerateError::SeedTopology(format!(
1779                    "Root {} live pool policy conflicts with its retained authority",
1780                    root.root
1781                )));
1782            }
1783            for asset in page.entries {
1784                let principal = asset.canister_id.to_text();
1785                if !expected.contains(principal.as_str()) {
1786                    return Err(FleetGenerateError::SeedTopology(format!(
1787                        "Root {} retains unseeded controlled pool asset {principal}",
1788                        root.root
1789                    )));
1790                }
1791                if principal == root.store
1792                    && root_owned_lifecycle(DesiredCanisterKind::Store, &asset.status).is_none()
1793                {
1794                    return Err(FleetGenerateError::SeedTopology(format!(
1795                        "Root {} does not retain {} as its exact Store",
1796                        root.root, root.store
1797                    )));
1798                }
1799                if principal != root.store
1800                    && root_owned_lifecycle(DesiredCanisterKind::Pool, &asset.status).is_none()
1801                {
1802                    return Err(FleetGenerateError::SeedTopology(format!(
1803                        "Root {} pool identity {principal} is outside the retained bootstrap/workload lifecycle",
1804                        root.root
1805                    )));
1806                }
1807                if found.insert(principal, asset.cycles.to_u128()).is_some() {
1808                    return Err(FleetGenerateError::SeedTopology(format!(
1809                        "Root {} repeats a pool identity",
1810                        root.root
1811                    )));
1812                }
1813            }
1814            let next = page.next_start_after;
1815            if next.is_none() {
1816                break;
1817            }
1818            if next == start_after {
1819                return Err(FleetGenerateError::SeedTopology(format!(
1820                    "Root {} pool cursor did not advance",
1821                    root.root
1822                )));
1823            }
1824            start_after = next;
1825        }
1826        for principal in expected {
1827            let cycles = found.get(principal).copied().ok_or_else(|| {
1828                FleetGenerateError::SeedTopology(format!(
1829                    "Root {} does not retain seeded identity {principal}",
1830                    root.root
1831                ))
1832            })?;
1833            if let Some(entry) = observed.get(principal) {
1834                if entry.cycles != cycles {
1835                    return Err(FleetGenerateError::Authority(format!(
1836                        "Root-owned and direct cycle observations differ for {principal}"
1837                    )));
1838                }
1839            } else {
1840                observed.insert(
1841                    principal.to_string(),
1842                    ObservedCanister {
1843                        cycles,
1844                        module_sha256: None,
1845                        subnet: root.placement_subnet.clone(),
1846                    },
1847                );
1848            }
1849        }
1850    }
1851    Ok(())
1852}
1853
1854fn validate_identity_seed(
1855    source: &FleetSource,
1856    seed: &EstateSeed,
1857) -> Result<(), FleetGenerateError> {
1858    let fee = Cycles::from_human_config_str(&seed.management_creation_fee_cycles)
1859        .map_err(|_| {
1860            FleetGenerateError::SeedTopology(
1861                "estate seed management creation fee must use B, T, or Q units".to_string(),
1862            )
1863        })?
1864        .to_u128();
1865    if seed.fresh_estate {
1866        let expected = fresh_seed(source, seed.fleet_id, &seed.cycles_ledger, fee)?;
1867        if seed == &expected {
1868            return Ok(());
1869        }
1870        return Err(FleetGenerateError::FreshSeedConflict(
1871            "fresh estate seed differs from current protected topology".to_string(),
1872        ));
1873    }
1874    let mut identities = BTreeSet::new();
1875    insert_seed_identity(&mut identities, "Coordinator", &seed.coordinator)?;
1876    if let Some(treasury) = &seed.treasury {
1877        parse_subnet("treasury", &treasury.subnet)?;
1878        if treasury.principal != seed.coordinator {
1879            insert_seed_identity(&mut identities, "treasury", &treasury.principal)?;
1880        } else if treasury.subnet != source.coordinator.subnet.subnet {
1881            return Err(FleetGenerateError::SeedTopology(
1882                "Coordinator treasury Subnet differs from Coordinator placement".to_string(),
1883            ));
1884        }
1885    }
1886    let source_roots = source
1887        .fleet_subnet_roots
1888        .iter()
1889        .map(|root| root.placement_subnet.as_str())
1890        .collect::<BTreeSet<_>>();
1891    let seed_roots = seed
1892        .roots
1893        .iter()
1894        .map(|root| root.placement_subnet.as_str())
1895        .collect::<BTreeSet<_>>();
1896    if source_roots.len() != source.fleet_subnet_roots.len()
1897        || seed_roots.len() != seed.roots.len()
1898        || source_roots != seed_roots
1899    {
1900        return Err(FleetGenerateError::SeedTopology(
1901            "Root placement Subnet sets differ or contain duplicates".to_string(),
1902        ));
1903    }
1904    for root in &seed.roots {
1905        let source_root = source
1906            .fleet_subnet_roots
1907            .iter()
1908            .find(|candidate| candidate.placement_subnet == root.placement_subnet)
1909            .expect("validated Root placement");
1910        validate_root_pool_import_capacity(&RootPoolImportCapacityInput {
1911            import_count: root.pool_imports.len(),
1912            maximum_size: source_root.canister_pool.maximum_size,
1913            root: root.root.clone(),
1914        })?;
1915        insert_seed_identity(&mut identities, "Fleet Subnet Root", &root.root)?;
1916        insert_seed_identity(&mut identities, "Wasm Store", &root.store)?;
1917        for pool in &root.pool_imports {
1918            insert_seed_identity(&mut identities, "pool import", pool)?;
1919        }
1920        if !source_root.canister_pool.imports.is_empty()
1921            && source_root.canister_pool.imports != root.pool_imports
1922        {
1923            return Err(FleetGenerateError::SeedTopology(format!(
1924                "Root {} pool imports differ between policy source and identity seed",
1925                root.placement_subnet
1926            )));
1927        }
1928    }
1929    Ok(())
1930}
1931
1932fn insert_seed_identity(
1933    identities: &mut BTreeSet<String>,
1934    role: &str,
1935    principal: &str,
1936) -> Result<(), FleetGenerateError> {
1937    parse_principal(role, principal)?;
1938    if !identities.insert(principal.to_string()) {
1939        return Err(FleetGenerateError::SeedTopology(format!(
1940            "Principal {principal} is assigned to more than one retained role"
1941        )));
1942    }
1943    Ok(())
1944}
1945
1946fn insert_expected_canister(
1947    expected: &mut BTreeMap<String, (String, Vec<String>)>,
1948    principal: String,
1949    authority: (String, Vec<String>),
1950    role: &str,
1951) -> Result<(), FleetGenerateError> {
1952    if expected.insert(principal.clone(), authority).is_some() {
1953        return Err(FleetGenerateError::SeedTopology(format!(
1954            "{role} Principal {principal} is assigned more than once"
1955        )));
1956    }
1957    Ok(())
1958}
1959
1960fn require_exact_controllers(
1961    canister: &str,
1962    mut actual: Vec<String>,
1963    mut expected: Vec<String>,
1964) -> Result<(), FleetGenerateError> {
1965    actual.sort();
1966    expected.sort();
1967    if actual != expected {
1968        return Err(FleetGenerateError::ControllerMismatch {
1969            actual,
1970            canister: canister.to_string(),
1971            expected,
1972        });
1973    }
1974    Ok(())
1975}
1976
1977fn release_authority(
1978    request: &FleetGenerateRequest<'_>,
1979    component_topology: &canic_core::control_plane_support::config::ComponentTopology,
1980) -> Result<
1981    (
1982        crate::release_set::PersistedCanicInfrastructureArtifactManifest,
1983        crate::release_set::PersistedCurrentReleaseSetManifest,
1984    ),
1985    FleetGenerateError,
1986> {
1987    let complete =
1988        load_persisted_current_release_set_manifest(request.root, request.release_build_id)
1989            .map_err(|error| FleetGenerateError::Release(error.to_string()))?;
1990    let expected_network = resolve_icp_build_network_from_root(request.root, request.environment)
1991        .map_err(|error| FleetGenerateError::Release(error.to_string()))?;
1992    require_release_build_network(
1993        request.release_build_id,
1994        complete.manifest.build_network,
1995        request.environment,
1996        expected_network,
1997    )?;
1998    validate_finalized_release_build_manifest(
1999        request.root,
2000        request.release_build_id,
2001        &complete.path,
2002    )
2003    .map_err(|error| FleetGenerateError::Release(error.to_string()))?;
2004    let infrastructure = load_persisted_canic_infrastructure_artifact_manifest(
2005        request.root,
2006        request.release_build_id,
2007    )
2008    .map_err(|error| FleetGenerateError::Release(error.to_string()))?;
2009    let application = load_persisted_application_artifact_union(
2010        request.root,
2011        component_topology,
2012        request.release_build_id,
2013    )
2014    .map_err(|error| FleetGenerateError::Release(error.to_string()))?;
2015    if complete.manifest.application_artifact_union_sha256 != application.digest {
2016        return Err(FleetGenerateError::Release(
2017            "complete release set does not bind its application artifact union".to_string(),
2018        ));
2019    }
2020    Ok((infrastructure, complete))
2021}
2022
2023fn require_release_build_network(
2024    release_build_id: ReleaseBuildId,
2025    actual: BuildNetwork,
2026    environment: &str,
2027    expected: BuildNetwork,
2028) -> Result<(), FleetGenerateError> {
2029    if actual == expected {
2030        return Ok(());
2031    }
2032    Err(FleetGenerateError::ReleaseBuildNetworkMismatch {
2033        release_build_id,
2034        actual,
2035        environment: environment.to_string(),
2036        expected,
2037    })
2038}
2039
2040#[expect(
2041    clippy::too_many_arguments,
2042    reason = "single constructor makes every authority-bearing desired canister field explicit"
2043)]
2044fn infrastructure_canister(
2045    name: &str,
2046    kind: DesiredCanisterKind,
2047    canic_init: Option<DesiredCanisterInit>,
2048    principal: Option<&str>,
2049    parent: Option<&str>,
2050    operator: &str,
2051    subnet: &str,
2052    initial_cycles: u128,
2053    minimum_cycles: u128,
2054    artifact: &CanicInfrastructureArtifactEntry,
2055) -> DesiredCanister {
2056    DesiredCanister {
2057        canic_init,
2058        controller_canisters: Vec::new(),
2059        controllers: vec![operator.to_string()],
2060        drain: None,
2061        initial_cycles: config_cycles(initial_cycles),
2062        init_arg: None,
2063        init_candid: None,
2064        kind,
2065        minimum_cycles: config_cycles(minimum_cycles),
2066        name: name.to_string(),
2067        parent: parent.map(str::to_string),
2068        presence: DesiredPresence::Present,
2069        principal: principal.map(str::to_string),
2070        protocol_binding: None,
2071        replace: false,
2072        subnet: subnet.to_string(),
2073        wasm: Some(artifact.wasm_relative_path.clone()),
2074    }
2075}
2076
2077fn config_cycles(cycles: u128) -> String {
2078    Cycles::new(cycles).to_config_string()
2079}
2080
2081fn validate_source_funding(
2082    source: &FleetSource,
2083    ic_mainnet: bool,
2084) -> Result<(), FleetGenerateError> {
2085    use canic_core::control_plane_support::model::fleet_funding_policy::{
2086        validate_coordinator_root_funding_policy, validate_fleet_root_funding_capacity,
2087        validate_fleet_subnet_root_funding_authority,
2088    };
2089    let coordinator = coordinator_funding(source.funding_profile, &source.coordinator.root_funding);
2090    let roots = source
2091        .fleet_subnet_roots
2092        .iter()
2093        .map(|root| root_funding(source.funding_profile, &root.root_funding))
2094        .collect::<Vec<_>>();
2095    validate_coordinator_root_funding_policy(&coordinator)?;
2096    for root in &roots {
2097        validate_fleet_subnet_root_funding_authority(root, ic_mainnet)?;
2098    }
2099    validate_fleet_root_funding_capacity(&coordinator, &roots)?;
2100    Ok(())
2101}
2102
2103fn coordinator_funding(
2104    profile: FleetFundingProfile,
2105    source: &CoordinatorFundingSource,
2106) -> FleetCoordinatorRootFundingPolicy {
2107    FleetCoordinatorRootFundingPolicy {
2108        funding_profile: profile,
2109        minimum_reserve_cycles: source.minimum_reserve_cycles.clone(),
2110        budget: CyclesFundingBudget {
2111            window_secs: source.window_secs,
2112            maximum_cycles: source.maximum_cycles.clone(),
2113        },
2114        maximum_automatic_grants: source.maximum_automatic_grants,
2115        maximum_automatic_cycles: source.maximum_automatic_cycles.clone(),
2116    }
2117}
2118
2119fn root_funding(
2120    profile: FleetFundingProfile,
2121    source: &RootFundingSource,
2122) -> FleetSubnetRootFundingAuthority {
2123    FleetSubnetRootFundingAuthority {
2124        root_funding: FleetSubnetRootFundingPolicy {
2125            funding_profile: profile,
2126            request_threshold: source.request_threshold.clone(),
2127            target_balance: source.target_balance.clone(),
2128            cooldown_secs: source.cooldown_secs,
2129            budget: CyclesFundingBudget {
2130                window_secs: source.window_secs,
2131                maximum_cycles: source.maximum_cycles.clone(),
2132            },
2133            maximum_automatic_grants: source.maximum_automatic_grants,
2134            maximum_automatic_cycles: source.maximum_automatic_cycles.clone(),
2135        },
2136        icp_refill: None,
2137    }
2138}
2139
2140fn root_limits(source: &RootSource) -> FleetSubnetRootLimits {
2141    FleetSubnetRootLimits {
2142        maximum_component_instances: source.limits.maximum_component_instances,
2143        maximum_registry_bytes: source.limits.maximum_registry_bytes,
2144        maximum_wasm_store_bytes: source.limits.maximum_wasm_store_bytes,
2145        canister_pool: FleetSubnetCanisterPoolConfig {
2146            minimum_size: source.canister_pool.minimum_size,
2147            maximum_size: source.canister_pool.maximum_size,
2148            canister_cycles: source.canister_pool.canister_cycles.clone(),
2149            creation_execution_margin: Cycles::new(GENERATED_POOL_CREATION_EXECUTION_MARGIN_CYCLES),
2150        },
2151        cycles_funding: CyclesFundingBudget {
2152            window_secs: source.limits.cycles_funding.window_secs,
2153            maximum_cycles: source.limits.cycles_funding.maximum_cycles.clone(),
2154        },
2155        maximum_group_placements: source.limits.maximum_group_placements,
2156    }
2157}
2158
2159fn artifact(
2160    entries: &[CanicInfrastructureArtifactEntry],
2161    role: CanicInfrastructureRole,
2162) -> Result<&CanicInfrastructureArtifactEntry, FleetGenerateError> {
2163    entries
2164        .iter()
2165        .find(|entry| entry.role == role)
2166        .ok_or_else(|| FleetGenerateError::Release(format!("missing {role:?} artifact")))
2167}
2168
2169fn candid_sidecar(
2170    root: &Path,
2171    artifact: &CanicInfrastructureArtifactEntry,
2172) -> Result<String, FleetGenerateError> {
2173    let wasm = root.join(&artifact.wasm_relative_path);
2174    let path = wasm.with_extension("did");
2175    let bytes = match read_optional_regular_bytes(&path) {
2176        Ok(Some(bytes)) => bytes,
2177        Ok(None) => {
2178            return Err(FleetGenerateError::CandidMissing { path });
2179        }
2180        Err(RegularFileReadError::NotRegular) => {
2181            return Err(FleetGenerateError::CandidNotRegular { path });
2182        }
2183        Err(RegularFileReadError::Io(error)) => {
2184            return Err(FleetGenerateError::CandidRead {
2185                path,
2186                source: error,
2187            });
2188        }
2189        #[cfg(not(unix))]
2190        Err(RegularFileReadError::UnsupportedPlatform) => {
2191            return Err(FleetGenerateError::CandidUnsupportedPlatform { path });
2192        }
2193    };
2194    let actual: [u8; 32] = Sha256::digest(bytes).into();
2195    if actual != artifact.candid_sha256 {
2196        return Err(FleetGenerateError::CandidDigestMismatch {
2197            path,
2198            expected: artifact.candid_sha256,
2199            observed: actual,
2200        });
2201    }
2202    relative_path(root, &path)
2203}
2204
2205fn relative_path(root: &Path, path: &Path) -> Result<String, FleetGenerateError> {
2206    path.strip_prefix(root)
2207        .map_err(|_| {
2208            FleetGenerateError::Authority(format!("path is outside workspace: {}", path.display()))
2209        })?
2210        .to_str()
2211        .map(str::to_string)
2212        .ok_or_else(|| {
2213            FleetGenerateError::Authority(format!("path is not UTF-8: {}", path.display()))
2214        })
2215}
2216
2217fn canonical_network_id(
2218    request: &FleetGenerateRequest<'_>,
2219    _source: &FleetSource,
2220) -> Result<canic_core::ids::CanonicalNetworkId, FleetGenerateError> {
2221    resolve_canonical_network_id_from_root(request.root, request.environment)
2222        .map_err(|error| FleetGenerateError::Authority(error.to_string()))
2223}
2224
2225fn load_toml<T: for<'de> Deserialize<'de>>(
2226    path: &Path,
2227    _kind: &'static str,
2228) -> Result<T, FleetGenerateError> {
2229    let bytes = match read_optional_regular_bytes(path) {
2230        Ok(Some(bytes)) => bytes,
2231        Ok(None) => {
2232            return Err(FleetGenerateError::Read {
2233                path: path.to_path_buf(),
2234                source: std::io::Error::new(std::io::ErrorKind::NotFound, "input is missing"),
2235            });
2236        }
2237        Err(RegularFileReadError::Io(source)) => {
2238            return Err(FleetGenerateError::Read {
2239                path: path.to_path_buf(),
2240                source,
2241            });
2242        }
2243        Err(RegularFileReadError::NotRegular) => {
2244            return Err(FleetGenerateError::UnsafeInput {
2245                path: path.to_path_buf(),
2246                reason: "path is not a regular no-follow file",
2247            });
2248        }
2249        #[cfg(not(unix))]
2250        Err(RegularFileReadError::UnsupportedPlatform) => {
2251            return Err(FleetGenerateError::UnsafeInput {
2252                path: path.to_path_buf(),
2253                reason: "no-follow reads are unsupported on this platform",
2254            });
2255        }
2256    };
2257    if bytes.len() > MAX_GENERATOR_INPUT_BYTES {
2258        return Err(FleetGenerateError::TooLarge {
2259            actual: bytes.len(),
2260            maximum: MAX_GENERATOR_INPUT_BYTES,
2261            path: path.to_path_buf(),
2262        });
2263    }
2264    toml::from_slice(&bytes).map_err(|source| FleetGenerateError::Decode {
2265        path: path.to_path_buf(),
2266        source,
2267    })
2268}
2269
2270const fn require_schema(actual: u32, kind: &'static str) -> Result<(), FleetGenerateError> {
2271    if actual == 1 {
2272        Ok(())
2273    } else {
2274        Err(FleetGenerateError::Schema { actual, kind })
2275    }
2276}
2277
2278fn require_cycles_creation(
2279    source: &CyclesCreationSource,
2280    owner: &str,
2281) -> Result<(), FleetGenerateError> {
2282    if source.kind == "cycles" && source.cycles.to_u128() > 0 {
2283        Ok(())
2284    } else {
2285        Err(FleetGenerateError::Authority(format!(
2286            "{owner} fresh creation must use a positive exact cycles amount"
2287        )))
2288    }
2289}
2290
2291fn parse_principal(field: &str, value: &str) -> Result<Principal, FleetGenerateError> {
2292    Principal::from_text(value).map_err(|error| {
2293        FleetGenerateError::Authority(format!("invalid {field} Principal: {error}"))
2294    })
2295}
2296
2297fn compile_source_admission_policy(
2298    values: &[String],
2299) -> Result<FleetAdmissionPolicyTemplate, FleetGenerateError> {
2300    let mut principals = values
2301        .iter()
2302        .map(|value| parse_principal("Fleet admission", value))
2303        .collect::<Result<Vec<_>, _>>()?;
2304    if principals
2305        .iter()
2306        .any(|principal| principal == &Principal::anonymous())
2307    {
2308        return Err(FleetGenerateError::FleetAdmissionAnonymous);
2309    }
2310    principals.sort_unstable();
2311    if let Some(duplicate) = principals
2312        .windows(2)
2313        .find(|items| items[0] == items[1])
2314        .map(|items| items[0])
2315    {
2316        return Err(FleetGenerateError::FleetAdmissionDuplicate {
2317            principal: duplicate,
2318        });
2319    }
2320    compile_fleet_admission_policy_template(principals, Vec::new())
2321        .map_err(|error| FleetGenerateError::Authority(error.to_string()))
2322}
2323
2324fn parse_subnet(field: &str, value: &str) -> Result<SubnetId, FleetGenerateError> {
2325    parse_principal(field, value).map(SubnetId::from_principal)
2326}
2327
2328fn mainnet_cycles_ledger() -> String {
2329    MAINNET_CYCLES_LEDGER.to_string()
2330}