use super::{
IcObservationAssociationError, IcObservationReplyView, IcObservationResponseView,
validate_response,
};
use crate::{
model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::{AttemptJournalRecord, ObservationOutcomeRecord},
ic_observation::{
IcCaptureAttribution, IcCaptureSettlement, IcObservationRequest, IcObservationResponse,
},
ic_request::IcManagementMethodRecord as Method,
ic_snapshot_reply::{IcSnapshotInfo, IcSnapshotReply},
},
policy::snapshot_inventory_delta::{SnapshotInventoryDeltaError, compare_inventories},
};
use thiserror::Error;
#[derive(Debug)]
pub struct IcCaptureSettlementView<'a> {
observation: IcObservationResponseView<'a>,
baseline: &'a IcSnapshotReply<'a>,
settlement: &'a IcCaptureSettlement,
captured_snapshot: Option<usize>,
outcome: ObservationOutcomeRecord,
}
impl<'a> IcCaptureSettlementView<'a> {
#[must_use]
pub const fn outcome(&self) -> ObservationOutcomeRecord {
self.outcome
}
#[must_use]
pub const fn baseline(&self) -> &'a IcSnapshotReply<'a> {
self.baseline
}
#[must_use]
pub const fn observation(&self) -> &IcObservationResponseView<'a> {
&self.observation
}
#[must_use]
pub const fn settlement(&self) -> &'a IcCaptureSettlement {
self.settlement
}
#[must_use]
pub fn captured_snapshot(&self) -> Option<&IcSnapshotInfo> {
match self.observation.reply() {
IcObservationReplyView::Inventory(reply) => self
.captured_snapshot
.map(|index| &reply.snapshots()[index]),
IcObservationReplyView::Status(_) => None,
}
}
}
pub fn validate_capture_settlement<'a>(
request: &IcObservationRequest<'a>,
journal: &AttemptJournalRecord,
baseline: &'a IcSnapshotReply<'a>,
response: &'a IcObservationResponse,
challenge: &ArtifactChecksumRecord,
settlement: &'a IcCaptureSettlement,
) -> Result<IcCaptureSettlementView<'a>, IcCaptureSettlementError> {
if request.mutation().method() != Method::TakeCanisterSnapshot
|| request.payload().method() != Method::ListCanisterSnapshots
{
return Err(IcCaptureSettlementError::UnsupportedMethod);
}
let observation = validate_response(request, journal, response)?;
let IcObservationReplyView::Inventory(inventory) = observation.reply() else {
return Err(IcCaptureSettlementError::UnsupportedMethod);
};
let candidates = compare_inventories(request.mutation().target(), baseline, inventory)?;
if settlement.authority != request.authority().digest() {
return Err(IcCaptureSettlementError::AuthorityMismatch);
}
if settlement.mutation_attempt != request.mutation_attempt()
|| settlement.observation_attempt != request.observation_attempt()
{
return Err(IcCaptureSettlementError::AttemptMismatch);
}
if &settlement.challenge != challenge {
return Err(IcCaptureSettlementError::ChallengeMismatch);
}
if settlement.baseline != baseline.digest() {
return Err(IcCaptureSettlementError::BaselineMismatch);
}
if settlement.inventory != inventory.digest() {
return Err(IcCaptureSettlementError::InventoryMismatch);
}
if settlement.observation_evidence != response.input().evidence {
return Err(IcCaptureSettlementError::ObservationEvidenceMismatch);
}
let (outcome, captured_snapshot) = match &settlement.attribution {
IcCaptureAttribution::Applied { snapshot_id, .. } => {
if !candidates
.iter()
.any(|snapshot| snapshot.id() == snapshot_id)
{
return Err(IcCaptureSettlementError::NotNewCandidate);
}
let index = inventory
.snapshots()
.binary_search_by(|snapshot| snapshot.id().cmp(snapshot_id.as_slice()))
.map_err(|_| IcCaptureSettlementError::NotNewCandidate)?;
(ObservationOutcomeRecord::Applied, Some(index))
}
IcCaptureAttribution::NotApplied { .. } => (ObservationOutcomeRecord::NotApplied, None),
IcCaptureAttribution::Unresolved { .. } => (ObservationOutcomeRecord::Uncertain, None),
};
Ok(IcCaptureSettlementView {
observation,
baseline,
settlement,
captured_snapshot,
outcome,
})
}
#[derive(Debug, Error)]
pub enum IcCaptureSettlementError {
#[error("capture settlement requires original capture and list observation")]
UnsupportedMethod,
#[error(transparent)]
Observation(#[from] IcObservationAssociationError),
#[error(transparent)]
Baseline(#[from] SnapshotInventoryDeltaError),
#[error("capture settlement original authority differs")]
AuthorityMismatch,
#[error("capture settlement attempt identities differ")]
AttemptMismatch,
#[error("capture settlement challenge differs")]
ChallengeMismatch,
#[error("capture settlement original baseline differs")]
BaselineMismatch,
#[error("capture settlement current inventory differs")]
InventoryMismatch,
#[error("capture settlement observation evidence differs")]
ObservationEvidenceMismatch,
#[error("capture settlement ID is not a new candidate")]
NotNewCandidate,
}
#[cfg(test)]
mod tests;