mod requirement;
pub use requirement::{
MAX_RESTORE_SAFETY_REQUIREMENT_BYTES, RestoreFenceBindingRecord, RestoreSafetyLaneRecord,
RestoreSafetyRequirementError, RestoreSafetyRequirementRecord, RestoreSafetyRequirementRequest,
};
use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::OperationBindingRecord,
consistency::ApplicationFenceState,
ic_request::{IcManagementMethodRecord, IcManagementRequestRecord, IcRequestError},
inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
};
use ic_management_canister_types::CanisterStatusType;
use thiserror::Error;
pub const MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS: u32 = 1024;
#[derive(Clone, Debug)]
pub struct RestoreSafetyRequestInput {
pub operation_sequence: u64,
pub challenge: ArtifactChecksumRecord,
pub max_remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct RestoreSafetyRequest<'a> {
plan: &'a OperationPlanRecord,
requirement: &'a RestoreSafetyRequirementRecord,
binding: OperationBindingRecord,
wire: &'a IcManagementRequestRecord,
input: RestoreSafetyRequestInput,
}
impl<'a> RestoreSafetyRequest<'a> {
pub fn new(
plan: &'a OperationPlanRecord,
source: &OperationPlanRecord,
requirement: &'a RestoreSafetyRequirementRecord,
wire: &'a IcManagementRequestRecord,
input: RestoreSafetyRequestInput,
) -> Result<Self, RestoreSafetyRequestError> {
requirement.validate_plans(plan, source)?;
if input.max_remote_observations > MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS {
return Err(RestoreSafetyRequestError::ObservationLimitTooLarge);
}
if !matches!(
wire.method(),
IcManagementMethodRecord::LoadCanisterSnapshot
| IcManagementMethodRecord::StartCanister
) {
return Err(RestoreSafetyRequestError::UnsupportedMethod);
}
let binding = plan
.attempt_authority(input.operation_sequence)?
.binding()
.clone();
wire.validate_mutation_binding(&binding)?;
Ok(Self {
plan,
requirement,
binding,
wire,
input,
})
}
#[must_use]
pub const fn binding(&self) -> &OperationBindingRecord {
&self.binding
}
#[must_use]
pub const fn inventory(&self) -> &InventoryRecord {
self.plan.inventory()
}
#[must_use]
pub fn selected_targets(&self) -> &[String] {
self.plan.selected_targets()
}
#[must_use]
pub const fn requirement(&self) -> &RestoreSafetyRequirementRecord {
self.requirement
}
#[must_use]
pub const fn wire(&self) -> &IcManagementRequestRecord {
self.wire
}
#[must_use]
pub const fn challenge(&self) -> &ArtifactChecksumRecord {
&self.input.challenge
}
#[must_use]
pub const fn max_remote_observations(&self) -> u32 {
self.input.max_remote_observations
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/restore-safety-request/v1\0".to_vec();
bytes.extend_from_slice(self.requirement.digest().hash().as_bytes());
bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
bytes.extend_from_slice(self.wire.digest().hash().as_bytes());
bytes.extend_from_slice(self.challenge().hash().as_bytes());
bytes.extend_from_slice(&self.max_remote_observations().to_be_bytes());
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[derive(Clone, Debug)]
pub struct TargetRestoreEvidence {
pub target: String,
pub state: CanisterStatusType,
pub lifecycle_evidence: ArtifactChecksumRecord,
pub restored_acceptance: Option<ArtifactChecksumRecord>,
}
#[derive(Clone, Debug)]
pub struct RestoreFenceEvidence {
pub state: ApplicationFenceState,
pub binding: RestoreFenceBindingRecord,
pub whole_selection: ArtifactChecksumRecord,
pub rewind_independent_custody: ArtifactChecksumRecord,
pub external_obligations_and_replay: ArtifactChecksumRecord,
pub controlled_execution: Option<ArtifactChecksumRecord>,
}
#[derive(Clone, Debug)]
pub enum RestoreSafetyEvidence {
NoIrreversibleEffects(ArtifactChecksumRecord),
ApplicationFenced(Box<RestoreFenceEvidence>),
Unresolved(ArtifactChecksumRecord),
}
#[derive(Clone, Debug)]
pub struct RestoreSafetyObservationInput {
pub request: ArtifactChecksumRecord,
pub context: PlanContextRecord,
pub inventory: InventoryRecord,
pub source_plan_intent: ArtifactChecksumRecord,
pub source_artifacts: ArtifactChecksumRecord,
pub targets: Vec<TargetRestoreEvidence>,
pub safety: RestoreSafetyEvidence,
pub evidence: ArtifactChecksumRecord,
pub remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct RestoreSafetyObservation {
input: RestoreSafetyObservationInput,
}
impl RestoreSafetyObservation {
pub fn new(
mut input: RestoreSafetyObservationInput,
) -> Result<Self, RestoreSafetyObservationError> {
if input.targets.is_empty() || input.targets.len() > MAX_INVENTORY_TARGETS {
return Err(RestoreSafetyObservationError::InvalidTargetCount);
}
for target in &mut input.targets {
target.target = super::principal::canonical_text(&target.target)
.ok_or(RestoreSafetyObservationError::InvalidPrincipal)?;
if input.inventory.target(&target.target).is_err() {
return Err(RestoreSafetyObservationError::TargetAbsentFromInventory);
}
}
input.targets.sort_by(|a, b| a.target.cmp(&b.target));
if input
.targets
.windows(2)
.any(|pair| pair[0].target == pair[1].target)
{
return Err(RestoreSafetyObservationError::DuplicateTarget);
}
Ok(Self { input })
}
#[must_use]
pub const fn request(&self) -> &ArtifactChecksumRecord {
&self.input.request
}
#[must_use]
pub const fn context(&self) -> &PlanContextRecord {
&self.input.context
}
#[must_use]
pub const fn inventory(&self) -> &InventoryRecord {
&self.input.inventory
}
#[must_use]
pub const fn source_plan_intent(&self) -> &ArtifactChecksumRecord {
&self.input.source_plan_intent
}
#[must_use]
pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
&self.input.source_artifacts
}
#[must_use]
pub fn targets(&self) -> &[TargetRestoreEvidence] {
&self.input.targets
}
#[must_use]
pub const fn safety(&self) -> &RestoreSafetyEvidence {
&self.input.safety
}
#[must_use]
pub const fn evidence(&self) -> &ArtifactChecksumRecord {
&self.input.evidence
}
#[must_use]
pub const fn remote_observations(&self) -> u32 {
self.input.remote_observations
}
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum RestoreSafetyObservationError {
#[error("restore safety targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
InvalidTargetCount,
#[error("invalid restore safety target principal")]
InvalidPrincipal,
#[error("duplicate restore safety target")]
DuplicateTarget,
#[error("restore safety target absent from inventory")]
TargetAbsentFromInventory,
}
#[derive(Debug, Error)]
pub enum RestoreSafetyRequestError {
#[error(transparent)]
Requirement(#[from] RestoreSafetyRequirementError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
#[error(transparent)]
Payload(#[from] IcRequestError),
#[error("restore safety requires exact load or start request")]
UnsupportedMethod,
#[error("restore safety observation ceiling exceeds {MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS}")]
ObservationLimitTooLarge,
}
#[cfg(test)]
mod tests;