pub mod admission;
mod destination;
mod evidence;
pub(in crate::fleet_ensure) mod funding;
pub mod journal;
pub mod observation;
pub mod publication;
#[cfg(test)]
mod tests;
pub mod transport;
use crate::fleet_ensure::{
model::{
FLEET_ENSURE_SCHEMA_VERSION,
capacity_import::{
CapacityImportAuthority, CapacityImportPlanRecord, CapacityImportRootBudget,
CapacityImportSourceRecord,
},
},
policy::capacity_import::{CapacityImportPolicyError, validate_plan},
};
use sha2_host::{Digest, Sha256};
use thiserror::Error;
pub use destination::{CapacityImportPrerequisiteError, validate_destination_authority};
pub use evidence::validate_root_status;
#[derive(Debug, Error)]
pub enum CapacityImportReviewError {
#[error(
"capacity import admission evidence is incomplete or differs from its reviewed authority"
)]
AdmissionInvalid,
#[error(transparent)]
Policy(#[from] CapacityImportPolicyError),
#[error("capacity import plan cannot be encoded")]
Encode(#[from] serde_json::Error),
#[error("capacity import plan digest differs from its retained content")]
DigestMismatch,
#[error("Root reservation differs from the exact reviewed import")]
ReservationMismatch,
#[error("Root import progress or cycle receipts differ from the reviewed operation")]
RootEvidenceMismatch,
}
pub fn prepare_review(
mut authority: CapacityImportAuthority,
mut sources: Vec<CapacityImportSourceRecord>,
root_budget: CapacityImportRootBudget,
) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
authority.recovery_controllers.sort_unstable();
sources.sort_by_key(|source| source.binding.canister_id);
for source in &mut sources {
source.binding.controllers.sort_unstable();
}
let mut final_controllers = authority.recovery_controllers.clone();
final_controllers.push(authority.root);
final_controllers.sort_unstable();
final_controllers.dedup();
let mut transitional_controllers = final_controllers.clone();
transitional_controllers.push(authority.operator);
transitional_controllers.sort_unstable();
transitional_controllers.dedup();
let mut plan = CapacityImportPlanRecord {
funding_credits: Vec::new(),
schema_version: FLEET_ENSURE_SCHEMA_VERSION,
admission: None,
authority,
sources,
transitional_controllers,
final_controllers,
root_budget,
plan_sha256: [0; 32],
};
validate_plan(&plan)?;
plan.plan_sha256 = review_digest(&plan)?;
Ok(plan)
}
pub fn verify_review(
plan: &CapacityImportPlanRecord,
approved_sha256: [u8; 32],
) -> Result<(), CapacityImportReviewError> {
if plan.plan_sha256 != approved_sha256 || review_digest(plan)? != approved_sha256 {
return Err(CapacityImportReviewError::DigestMismatch);
}
validate_plan(plan)?;
admission::validate(plan)?;
Ok(())
}
pub fn with_admission(
mut plan: CapacityImportPlanRecord,
admission: crate::fleet_ensure::model::capacity_import::admission::CapacityImportAdmissionRecord,
) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
verify_review(&plan, plan.plan_sha256)?;
if plan.admission.is_some() {
return Err(CapacityImportReviewError::DigestMismatch);
}
plan.admission = Some(admission);
admission::validate(&plan)?;
plan.plan_sha256 = review_digest(&plan)?;
Ok(plan)
}
pub(in crate::fleet_ensure) fn with_funding(
mut plan: CapacityImportPlanRecord,
credits: Vec<
crate::fleet_ensure::model::capacity_import::funding::CapacityImportFundingCreditRecord,
>,
) -> Result<CapacityImportPlanRecord, CapacityImportReviewError> {
verify_review(&plan, plan.plan_sha256)?;
if !plan.funding_credits.is_empty() {
return Err(CapacityImportReviewError::DigestMismatch);
}
plan.funding_credits = credits;
plan.funding_credits
.sort_by_key(|credit| credit.before.binding.canister_id);
validate_plan(&plan)?;
plan.plan_sha256 = review_digest(&plan)?;
Ok(plan)
}
fn review_digest(plan: &CapacityImportPlanRecord) -> Result<[u8; 32], serde_json::Error> {
let mut content = plan.clone();
content.plan_sha256 = [0; 32];
let mut hash = Sha256::new();
hash.update(b"canic:fleet-capacity-import:review:v1\0");
hash.update(serde_json::to_vec(&content)?);
Ok(hash.finalize().into())
}
pub fn root_reservation(
plan: &CapacityImportPlanRecord,
) -> Result<canic_core::dto::pool_import::PoolImportReservation, CapacityImportReviewError> {
verify_review(plan, plan.plan_sha256)?;
Ok(canic_core::dto::pool_import::PoolImportReservation {
sequence: plan.authority.import_sequence,
plan_sha256: plan.plan_sha256,
root_authority_sha256: plan.authority.root_authority_sha256,
root: plan.authority.root,
operator: plan.authority.operator,
subnet: plan.authority.subnet.into_principal(),
transitional_controllers: plan.transitional_controllers.clone(),
final_controllers: plan.final_controllers.clone(),
sources: plan
.sources
.iter()
.map(|source| canic_core::dto::pool_import::PoolImportSource {
canister_id: source.binding.canister_id,
controllers: source.binding.controllers.clone(),
module_sha256: source.binding.module_sha256,
canister_version: source.binding.canister_version,
stopped: source.binding.stopped,
disposition_sha256:
crate::fleet_ensure::policy::capacity_import::disposition_digest(source),
observed_cycles: source.observed_cycles,
observed_reserved_cycles: source.observed_reserved_cycles,
minimum_ready_cycles: source.minimum_ready_cycles,
maximum_debit_cycles: source.maximum_debit_cycles,
})
.collect(),
observed_root_cycles: plan.root_budget.observed_cycles,
observed_root_reserved_cycles: plan.root_budget.observed_reserved_cycles,
minimum_root_cycles: plan.root_budget.minimum_retained_cycles,
maximum_root_debit_cycles: plan.root_budget.maximum_debit_cycles,
maximum_paid_calls: plan.root_budget.maximum_paid_calls,
})
}
pub fn reservation_evidence(
plan: &CapacityImportPlanRecord,
response: &canic_core::dto::pool_import::PoolImportStatus,
) -> Result<
crate::fleet_ensure::model::capacity_import::CapacityImportReservationRecord,
CapacityImportReviewError,
> {
validate_root_status(plan, response)?;
Ok(
crate::fleet_ensure::model::capacity_import::CapacityImportReservationRecord {
plan_sha256: plan.plan_sha256,
authority: plan.authority.clone(),
sources: plan
.sources
.iter()
.map(|source| source.binding.canister_id)
.collect(),
},
)
}