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