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