use super::{
IcObservationAssociationError, IcObservationReplyView, IcObservationResponseView,
validate_response,
};
use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::{AttemptJournalRecord, ObservationOutcomeRecord},
ic_observation::{
IcLifecycleAttribution, IcLifecycleSettlement, IcObservationRequest, IcObservationResponse,
},
ic_request::IcManagementMethodRecord as Method,
};
use crate::policy::settlement_identity::{SettlementIdentity, SettlementIdentityMismatch};
use thiserror::Error;
#[derive(Debug)]
pub struct IcLifecycleSettlementView<'a> {
observation: IcObservationResponseView<'a>,
settlement: &'a IcLifecycleSettlement,
outcome: ObservationOutcomeRecord,
}
impl<'a> IcLifecycleSettlementView<'a> {
#[must_use]
pub const fn outcome(&self) -> ObservationOutcomeRecord {
self.outcome
}
#[must_use]
pub const fn observation(&self) -> &IcObservationResponseView<'a> {
&self.observation
}
#[must_use]
pub const fn settlement(&self) -> &'a IcLifecycleSettlement {
self.settlement
}
}
pub fn validate_lifecycle_settlement<'a>(
request: &IcObservationRequest<'a>,
journal: &AttemptJournalRecord,
response: &'a IcObservationResponse,
challenge: &ArtifactChecksumRecord,
settlement: &'a IcLifecycleSettlement,
) -> Result<IcLifecycleSettlementView<'a>, IcLifecycleSettlementError> {
if !matches!(
request.mutation().method(),
Method::StopCanister | Method::StartCanister | Method::LoadCanisterSnapshot
) || request.payload().method() != Method::CanisterStatus
{
return Err(IcLifecycleSettlementError::UnsupportedMethod);
}
let observation = validate_response(request, journal, response)?;
SettlementIdentity {
authority: &settlement.authority,
mutation_attempt: settlement.mutation_attempt,
observation_attempt: settlement.observation_attempt,
challenge: &settlement.challenge,
}
.compare(&SettlementIdentity {
authority: &request.authority().digest(),
mutation_attempt: request.mutation_attempt(),
observation_attempt: request.observation_attempt(),
challenge,
})?;
let IcObservationReplyView::Status(status) = observation.reply() else {
return Err(IcLifecycleSettlementError::UnsupportedMethod);
};
if settlement.status != status.digest() {
return Err(IcLifecycleSettlementError::StatusMismatch);
}
if settlement.observation_evidence != response.input().evidence {
return Err(IcLifecycleSettlementError::ObservationEvidenceMismatch);
}
let outcome = match &settlement.attribution {
IcLifecycleAttribution::Applied { .. } => ObservationOutcomeRecord::Applied,
IcLifecycleAttribution::NotApplied { .. } => ObservationOutcomeRecord::NotApplied,
IcLifecycleAttribution::Unresolved { .. } => ObservationOutcomeRecord::Uncertain,
};
Ok(IcLifecycleSettlementView {
observation,
settlement,
outcome,
})
}
#[derive(Debug, Error)]
pub enum IcLifecycleSettlementError {
#[error("lifecycle settlement requires original stop/start/load and status observation")]
UnsupportedMethod,
#[error(transparent)]
Observation(#[from] IcObservationAssociationError),
#[error("lifecycle settlement original authority differs")]
AuthorityMismatch,
#[error("lifecycle settlement attempt identities differ")]
AttemptMismatch,
#[error("lifecycle settlement challenge differs")]
ChallengeMismatch,
#[error("lifecycle settlement status evidence differs")]
StatusMismatch,
#[error("lifecycle settlement observation evidence differs")]
ObservationEvidenceMismatch,
}
impl From<SettlementIdentityMismatch> for IcLifecycleSettlementError {
fn from(value: SettlementIdentityMismatch) -> Self {
match value {
SettlementIdentityMismatch::Authority => Self::AuthorityMismatch,
SettlementIdentityMismatch::Attempt => Self::AttemptMismatch,
SettlementIdentityMismatch::Challenge => Self::ChallengeMismatch,
}
}
}
#[cfg(test)]
mod tests;