Skip to main content

canic_host/fleet_ensure/generate/
mod.rs

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