canic_host/fleet_ensure/ops/capacity_import/
mod.rs1pub 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#[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
51pub fn prepare_review(
53 mut authority: CapacityImportAuthority,
54 mut sources: Vec<CapacityImportSourceRecord>,
55 root_budget: CapacityImportRootBudget,
56) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
57 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
87pub 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
100pub 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
115pub(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
143pub 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
184pub 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}