Skip to main content

canic_host/fleet_ensure/generate/capacity_import/
mod.rs

1//! Compile paired generator-input replacements for a reviewed capacity import.
2//!
3//! This boundary preserves existing estate identities and policy values. Publication,
4//! authenticated observation and Root release belong to the import workflow.
5
6#[cfg(test)]
7pub(in crate::fleet_ensure) mod tests;
8
9use crate::fleet_ensure::{
10    generate::{
11        EstateSeed, FleetGenerateError, FleetSource, MAX_GENERATOR_INPUT_BYTES,
12        canonical_recovery_controllers, validate_identity_seed,
13    },
14    model::capacity_import::{CapacityImportPlanRecord, operation::CapacityImportPublicationKind},
15    ops::capacity_import::{CapacityImportReviewError, verify_review},
16    view::capacity_import::{CapacityImportDocumentView, CapacityImportInventoryView},
17};
18use candid::Principal;
19use sha2_host::{Digest, Sha256};
20use thiserror::Error;
21
22/// Local projection failure leaves both original generator inputs untouched.
23#[derive(Debug, Error)]
24pub enum CapacityImportInventoryError {
25    #[error("capacity import generator input exceeds its byte bound")]
26    TooLarge,
27    #[error("capacity import generator input is not UTF-8")]
28    Utf8(#[from] std::str::Utf8Error),
29    #[error("capacity import generator input has an invalid current schema")]
30    Decode(#[from] toml::de::Error),
31    #[error("capacity import generator replacement cannot be encoded")]
32    Encode(#[from] toml::ser::Error),
33    #[error("capacity import generator inputs differ from the reviewed retained estate")]
34    AuthorityMismatch,
35    #[error("capacity import candidate already occurs in the destination seed")]
36    AlreadyPresent,
37    #[error(transparent)]
38    Review(#[from] CapacityImportReviewError),
39    #[error(transparent)]
40    Generation(Box<FleetGenerateError>),
41}
42
43impl From<FleetGenerateError> for CapacityImportInventoryError {
44    fn from(error: FleetGenerateError) -> Self {
45        Self::Generation(Box::new(error))
46    }
47}
48
49/// Add the exact reviewed sources to both maintained generator inputs without any writes.
50///
51/// Callers must retain and approve these byte bindings before the import's first effect.
52/// An existing retained estate is required; bootstrap owns creation of its initial identity.
53pub fn prepare_capacity_import_inventory(
54    plan: &CapacityImportPlanRecord,
55    policy_bytes: &[u8],
56    seed_bytes: &[u8],
57) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
58    prepare_inventory(
59        plan,
60        policy_bytes,
61        seed_bytes,
62        CapacityImportPublicationKind::ExtendEstate,
63    )
64}
65
66/// Publish an initial held set already declared by the explicit infrastructure seed.
67/// The bootstrap owner verifies its setup receipt before selecting this boundary.
68pub fn prepare_initial_import_inventory(
69    plan: &CapacityImportPlanRecord,
70    policy_bytes: &[u8],
71    seed_bytes: &[u8],
72) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
73    prepare_inventory(
74        plan,
75        policy_bytes,
76        seed_bytes,
77        CapacityImportPublicationKind::InitializeEstate,
78    )
79}
80
81fn prepare_inventory(
82    plan: &CapacityImportPlanRecord,
83    policy_bytes: &[u8],
84    seed_bytes: &[u8],
85    kind: CapacityImportPublicationKind,
86) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
87    verify_review(plan, plan.plan_sha256)?;
88    let policy_text = input_text(policy_bytes)?;
89    let seed_text = input_text(seed_bytes)?;
90    let mut source: FleetSource = toml::from_str(policy_text)?;
91    let mut seed: EstateSeed = toml::from_str(seed_text)?;
92    let mut policy_document: toml::Value = toml::from_str(policy_text)?;
93    let mut seed_document: toml::Value = toml::from_str(seed_text)?;
94    let authority = &plan.authority;
95    let mut recovery = authority
96        .recovery_controllers
97        .iter()
98        .map(Principal::to_text)
99        .collect::<Vec<_>>();
100    recovery.sort();
101    let configured_recovery =
102        canonical_recovery_controllers(&source.recovery_controllers, authority.operator)?;
103    let estate_matches = seed.schema_version == 1
104        && !seed.fresh_estate
105        && seed.fleet_id == authority.fleet.fleet.fleet_id
106        && seed.coordinator == authority.coordinator.to_text();
107    let policy_matches = source.schema_version == 1
108        && source.operator == authority.operator.to_text()
109        && configured_recovery == recovery;
110    if !estate_matches || !policy_matches {
111        return Err(CapacityImportInventoryError::AuthorityMismatch);
112    }
113    validate_identity_seed(&source, &seed)?;
114    let subnet = authority.subnet.into_principal().to_text();
115    let seed_index = seed
116        .roots
117        .iter()
118        .position(|root| root.root == authority.root.to_text() && root.placement_subnet == subnet)
119        .ok_or(CapacityImportInventoryError::AuthorityMismatch)?;
120    let source_index = source
121        .fleet_subnet_roots
122        .iter()
123        .position(|root| root.placement_subnet == subnet)
124        .ok_or(CapacityImportInventoryError::AuthorityMismatch)?;
125    let imports = &mut seed.roots[seed_index].pool_imports;
126    match kind {
127        CapacityImportPublicationKind::ExtendEstate => {
128            for candidate in &plan.sources {
129                let id = candidate.binding.canister_id.to_text();
130                if imports.contains(&id) {
131                    return Err(CapacityImportInventoryError::AlreadyPresent);
132                }
133                imports.push(id);
134            }
135        }
136        CapacityImportPublicationKind::InitializeEstate => {
137            let expected = plan
138                .sources
139                .iter()
140                .map(|source| source.binding.canister_id.to_text())
141                .collect::<std::collections::BTreeSet<_>>();
142            let supplied = imports
143                .iter()
144                .cloned()
145                .collect::<std::collections::BTreeSet<_>>();
146            if imports.len() != supplied.len() || supplied != expected {
147                return Err(CapacityImportInventoryError::AuthorityMismatch);
148            }
149        }
150    }
151    source.fleet_subnet_roots[source_index]
152        .canister_pool
153        .imports
154        .clone_from(imports);
155    let replacement_imports =
156        toml::Value::Array(imports.iter().cloned().map(toml::Value::String).collect());
157    // Reuse the generator's global identity and capacity rules: candidates may
158    // not alias any infrastructure, treasury, or another Root's existing pool.
159    validate_identity_seed(&source, &seed)?;
160    let source_root = array_entry(&mut policy_document, "fleet_subnet_roots", source_index)?;
161    source_root
162        .get_mut("canister_pool")
163        .and_then(toml::Value::as_table_mut)
164        .ok_or(CapacityImportInventoryError::AuthorityMismatch)?
165        .insert("imports".to_string(), replacement_imports.clone());
166    array_entry(&mut seed_document, "roots", seed_index)?
167        .as_table_mut()
168        .ok_or(CapacityImportInventoryError::AuthorityMismatch)?
169        .insert("pool_imports".to_string(), replacement_imports);
170    Ok(CapacityImportInventoryView {
171        plan_sha256: plan.plan_sha256,
172        policy: replacement(policy_bytes, &policy_document)?,
173        seed: replacement(seed_bytes, &seed_document)?,
174    })
175}
176
177fn input_text(bytes: &[u8]) -> Result<&str, CapacityImportInventoryError> {
178    if bytes.len() > MAX_GENERATOR_INPUT_BYTES {
179        return Err(CapacityImportInventoryError::TooLarge);
180    }
181    Ok(std::str::from_utf8(bytes)?)
182}
183
184fn array_entry<'a>(
185    document: &'a mut toml::Value,
186    name: &str,
187    index: usize,
188) -> Result<&'a mut toml::Value, CapacityImportInventoryError> {
189    document
190        .get_mut(name)
191        .and_then(toml::Value::as_array_mut)
192        .and_then(|entries| entries.get_mut(index))
193        .ok_or(CapacityImportInventoryError::AuthorityMismatch)
194}
195
196fn replacement(
197    original: &[u8],
198    document: &toml::Value,
199) -> Result<CapacityImportDocumentView, CapacityImportInventoryError> {
200    let replacement = if document == &toml::from_str::<toml::Value>(input_text(original)?)? {
201        original.to_vec()
202    } else {
203        toml::to_string_pretty(document)?.into_bytes()
204    };
205    if replacement.len() > MAX_GENERATOR_INPUT_BYTES {
206        return Err(CapacityImportInventoryError::TooLarge);
207    }
208    Ok(CapacityImportDocumentView {
209        before_sha256: Sha256::digest(original).into(),
210        after_sha256: Sha256::digest(&replacement).into(),
211        replacement,
212    })
213}