Skip to main content

canic_host/fleet_ensure/ops/capacity_import/
mod.rs

1//! Construct and verify immutable capacity reviews before workflow persists or applies them.
2//! Hashes bind local review content; authenticated observations remain a transport responsibility.
3
4pub mod admission;
5mod destination;
6mod evidence;
7
8pub(in crate::fleet_ensure) mod funding;
9pub mod journal;
10pub mod observation;
11pub mod publication;
12#[cfg(test)]
13mod tests;
14pub mod transport;
15
16use crate::fleet_ensure::{
17    model::{
18        FLEET_ENSURE_SCHEMA_VERSION,
19        capacity_import::{
20            CapacityImportAuthority, CapacityImportPlanRecord, CapacityImportRootBudget,
21            CapacityImportSourceRecord,
22        },
23    },
24    policy::capacity_import::{CapacityImportPolicyError, validate_plan},
25};
26use sha2_host::{Digest, Sha256};
27use thiserror::Error;
28
29pub use destination::{CapacityImportPrerequisiteError, validate_destination_authority};
30pub use evidence::validate_root_status;
31
32/// Retained content or policy failed before granting import authority.
33#[derive(Debug, Error)]
34pub enum CapacityImportReviewError {
35    #[error(
36        "capacity import admission evidence is incomplete or differs from its reviewed authority"
37    )]
38    AdmissionInvalid,
39    #[error(transparent)]
40    Policy(#[from] CapacityImportPolicyError),
41    #[error("capacity import plan cannot be encoded")]
42    Encode(#[from] serde_json::Error),
43    #[error("capacity import plan digest differs from its retained content")]
44    DigestMismatch,
45    #[error("Root reservation differs from the exact reviewed import")]
46    ReservationMismatch,
47    #[error("Root import progress or cycle receipts differ from the reviewed operation")]
48    RootEvidenceMismatch,
49}
50
51/// Build deterministic controller transitions and bind every reviewed source field.
52pub fn prepare_review(
53    mut authority: CapacityImportAuthority,
54    mut sources: Vec<CapacityImportSourceRecord>,
55    root_budget: CapacityImportRootBudget,
56) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
57    // Sort representations only. Duplicate input remains visible to policy.
58    authority.recovery_controllers.sort_unstable();
59    sources.sort_by_key(|source| source.binding.canister_id);
60    for source in &mut sources {
61        source.binding.controllers.sort_unstable();
62    }
63    let mut final_controllers = authority.recovery_controllers.clone();
64    final_controllers.push(authority.root);
65    final_controllers.sort_unstable();
66    final_controllers.dedup();
67    let mut transitional_controllers = final_controllers.clone();
68    transitional_controllers.push(authority.operator);
69    transitional_controllers.sort_unstable();
70    transitional_controllers.dedup();
71    let mut plan = CapacityImportPlanRecord {
72        funding_credits: Vec::new(),
73        schema_version: FLEET_ENSURE_SCHEMA_VERSION,
74        admission: None,
75        authority,
76        sources,
77        transitional_controllers,
78        final_controllers,
79        root_budget,
80        plan_sha256: [0; 32],
81    };
82    validate_plan(&plan)?;
83    plan.plan_sha256 = review_digest(&plan)?;
84    Ok(plan)
85}
86
87/// Verify both retained content and explicit approval; a digest never bypasses policy.
88pub fn verify_review(
89    plan: &CapacityImportPlanRecord,
90    approved_sha256: [u8; 32],
91) -> Result<(), CapacityImportReviewError> {
92    if plan.plan_sha256 != approved_sha256 || review_digest(plan)? != approved_sha256 {
93        return Err(CapacityImportReviewError::DigestMismatch);
94    }
95    validate_plan(plan)?;
96    admission::validate(plan)?;
97    Ok(())
98}
99
100/// Bind verified host admission before publishing any review or signed request.
101pub fn with_admission(
102    mut plan: CapacityImportPlanRecord,
103    admission: crate::fleet_ensure::model::capacity_import::admission::CapacityImportAdmissionRecord,
104) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
105    verify_review(&plan, plan.plan_sha256)?;
106    if plan.admission.is_some() {
107        return Err(CapacityImportReviewError::DigestMismatch);
108    }
109    plan.admission = Some(admission);
110    admission::validate(&plan)?;
111    plan.plan_sha256 = review_digest(&plan)?;
112    Ok(plan)
113}
114
115/// Seal the separately observed credits into the exact approval digest.
116pub(in crate::fleet_ensure) fn with_funding(
117    mut plan: CapacityImportPlanRecord,
118    credits: Vec<
119        crate::fleet_ensure::model::capacity_import::funding::CapacityImportFundingCreditRecord,
120    >,
121) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
122    verify_review(&plan, plan.plan_sha256)?;
123    if !plan.funding_credits.is_empty() {
124        return Err(CapacityImportReviewError::DigestMismatch);
125    }
126    plan.funding_credits = credits;
127    plan.funding_credits
128        .sort_by_key(|credit| credit.before.binding.canister_id);
129    validate_plan(&plan)?;
130    plan.plan_sha256 = review_digest(&plan)?;
131    Ok(plan)
132}
133
134fn review_digest(plan: &CapacityImportPlanRecord) -> Result<[u8; 32], serde_json::Error> {
135    let mut content = plan.clone();
136    content.plan_sha256 = [0; 32];
137    let mut hash = Sha256::new();
138    hash.update(b"canic:fleet-capacity-import:review:v1\0");
139    hash.update(serde_json::to_vec(&content)?);
140    Ok(hash.finalize().into())
141}
142
143/// Construct the exact Root reservation from verified retained review content.
144pub fn root_reservation(
145    plan: &CapacityImportPlanRecord,
146) -> Result<canic_contracts::dto::pool_import::PoolImportReservation, CapacityImportReviewError> {
147    verify_review(plan, plan.plan_sha256)?;
148    Ok(canic_contracts::dto::pool_import::PoolImportReservation {
149        sequence: plan.authority.import_sequence,
150        plan_sha256: plan.plan_sha256,
151        root_authority_sha256: plan.authority.root_authority_sha256,
152        root: plan.authority.root,
153        operator: plan.authority.operator,
154        subnet: plan.authority.subnet.into_principal(),
155        transitional_controllers: plan.transitional_controllers.clone(),
156        final_controllers: plan.final_controllers.clone(),
157        sources: plan
158            .sources
159            .iter()
160            .map(
161                |source| canic_contracts::dto::pool_import::PoolImportSource {
162                    canister_id: source.binding.canister_id,
163                    controllers: source.binding.controllers.clone(),
164                    module_sha256: source.binding.module_sha256,
165                    canister_version: source.binding.canister_version,
166                    stopped: source.binding.stopped,
167                    disposition_sha256:
168                        crate::fleet_ensure::policy::capacity_import::disposition_digest(source),
169                    observed_cycles: source.observed_cycles,
170                    observed_reserved_cycles: source.observed_reserved_cycles,
171                    minimum_ready_cycles: source.minimum_ready_cycles,
172                    maximum_debit_cycles: source.maximum_debit_cycles,
173                },
174            )
175            .collect(),
176        observed_root_cycles: plan.root_budget.observed_cycles,
177        observed_root_reserved_cycles: plan.root_budget.observed_reserved_cycles,
178        minimum_root_cycles: plan.root_budget.minimum_retained_cycles,
179        maximum_root_debit_cycles: plan.root_budget.maximum_debit_cycles,
180        maximum_paid_calls: plan.root_budget.maximum_paid_calls,
181    })
182}
183
184/// Admit authenticated Root reservation evidence only when every reviewed bound matches.
185/// Authentication of this protected response remains the transport owner's responsibility.
186pub fn reservation_evidence(
187    plan: &CapacityImportPlanRecord,
188    response: &canic_contracts::dto::pool_import::PoolImportStatus,
189) -> Result<
190    crate::fleet_ensure::model::capacity_import::CapacityImportReservationRecord,
191    CapacityImportReviewError,
192> {
193    validate_root_status(plan, response)?;
194    Ok(
195        crate::fleet_ensure::model::capacity_import::CapacityImportReservationRecord {
196            plan_sha256: plan.plan_sha256,
197            authority: plan.authority.clone(),
198            sources: plan
199                .sources
200                .iter()
201                .map(|source| source.binding.canister_id)
202                .collect(),
203        },
204    )
205}