use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
ic_lifecycle_reply::MAX_IC_LIFECYCLE_REPLY_BYTES,
ic_request::{IcManagementRequestRecord, IcRequestError},
ic_snapshot_reply::MAX_IC_SNAPSHOT_REPLY_BYTES,
operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
};
use std::fmt;
use thiserror::Error;
pub const MAX_IC_OBSERVATION_REPLY_BYTES: usize =
if MAX_IC_SNAPSHOT_REPLY_BYTES < MAX_IC_LIFECYCLE_REPLY_BYTES {
MAX_IC_SNAPSHOT_REPLY_BYTES
} else {
MAX_IC_LIFECYCLE_REPLY_BYTES
};
#[derive(Debug)]
pub struct IcObservationRequest<'a> {
plan: &'a OperationPlanRecord,
mutation: &'a IcManagementRequestRecord,
payload: &'a IcManagementRequestRecord,
authority: AttemptAuthorityRecord,
mutation_attempt: u32,
observation_attempt: u32,
}
impl<'a> IcObservationRequest<'a> {
pub fn new(
plan: &'a OperationPlanRecord,
operation_sequence: u64,
journal: &AttemptJournalRecord,
mutation: &'a IcManagementRequestRecord,
payload: &'a IcManagementRequestRecord,
) -> Result<Self, IcObservationRequestError> {
let authority = plan.attempt_authority(operation_sequence)?;
if journal.authority() != &authority {
return Err(IcObservationRequestError::AuthorityMismatch);
}
mutation.validate_mutation_binding(authority.binding())?;
payload.validate_observation_binding(authority.binding(), &payload.digest())?;
let current = journal.view();
let request = Self {
plan,
mutation,
payload,
authority,
mutation_attempt: current
.pending_mutation
.ok_or(IcObservationRequestError::NoPendingMutation)?,
observation_attempt: current
.pending_observation
.ok_or(IcObservationRequestError::NoPendingObservation)?,
};
request.validate_journal(journal)?;
Ok(request)
}
#[must_use]
pub const fn plan(&self) -> &OperationPlanRecord {
self.plan
}
#[must_use]
pub const fn mutation(&self) -> &'a IcManagementRequestRecord {
self.mutation
}
#[must_use]
pub const fn payload(&self) -> &'a IcManagementRequestRecord {
self.payload
}
#[must_use]
pub const fn authority(&self) -> &AttemptAuthorityRecord {
&self.authority
}
#[must_use]
pub const fn mutation_attempt(&self) -> u32 {
self.mutation_attempt
}
#[must_use]
pub const fn observation_attempt(&self) -> u32 {
self.observation_attempt
}
pub fn validate_journal(
&self,
journal: &AttemptJournalRecord,
) -> Result<(), IcObservationRequestError> {
ObservationReservation {
authority: &self.authority,
mutation_attempt: self.mutation_attempt,
observation_attempt: self.observation_attempt,
request: self.payload.digest(),
}
.validate(journal)
}
}
pub(crate) struct ObservationReservation<'a> {
pub authority: &'a AttemptAuthorityRecord,
pub mutation_attempt: u32,
pub observation_attempt: u32,
pub request: ArtifactChecksumRecord,
}
impl ObservationReservation<'_> {
pub(crate) fn validate(
&self,
journal: &AttemptJournalRecord,
) -> Result<(), IcObservationRequestError> {
if journal.authority() != self.authority {
return Err(IcObservationRequestError::AuthorityMismatch);
}
let current = journal.view();
if current.pending_mutation != Some(self.mutation_attempt) {
return Err(IcObservationRequestError::MutationMismatch);
}
if current.pending_observation != Some(self.observation_attempt) {
return Err(IcObservationRequestError::ObservationMismatch);
}
if journal.pending_observation_request() != Some(self.request.hash()) {
return Err(IcObservationRequestError::RequestMismatch);
}
Ok(())
}
}
#[derive(Clone)]
pub struct IcObservationResponseInput {
pub authority: ArtifactChecksumRecord,
pub mutation_attempt: u32,
pub observation_attempt: u32,
pub request: ArtifactChecksumRecord,
pub context: PlanContextRecord,
pub target: String,
pub reply: Vec<u8>,
pub evidence: ArtifactChecksumRecord,
}
#[derive(Clone)]
pub struct IcObservationResponse {
input: IcObservationResponseInput,
}
impl IcObservationResponse {
pub fn new(mut input: IcObservationResponseInput) -> Result<Self, IcObservationResponseError> {
validate_response_input(&mut input, MAX_IC_OBSERVATION_REPLY_BYTES)?;
Ok(Self { input })
}
#[must_use]
pub const fn input(&self) -> &IcObservationResponseInput {
&self.input
}
}
impl fmt::Debug for IcObservationResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_response_input(&self.input, formatter, "IcObservationResponse")
}
}
pub(crate) fn validate_response_input(
input: &mut IcObservationResponseInput,
max_reply_bytes: usize,
) -> Result<(), IcObservationResponseError> {
if input.mutation_attempt == 0
|| input.observation_attempt <= input.mutation_attempt
|| input.observation_attempt > MAX_OPERATION_ATTEMPTS
{
return Err(IcObservationResponseError::InvalidAttempts);
}
if input.reply.len() > max_reply_bytes {
return Err(IcObservationResponseError::ReplyTooLarge);
}
input.target = crate::model::principal::canonical_text(&input.target)
.ok_or(IcObservationResponseError::InvalidTarget)?;
Ok(())
}
pub(crate) fn fmt_response_input(
input: &IcObservationResponseInput,
formatter: &mut fmt::Formatter<'_>,
name: &str,
) -> fmt::Result {
formatter
.debug_struct(name)
.field("authority", &input.authority)
.field("mutation_attempt", &input.mutation_attempt)
.field("observation_attempt", &input.observation_attempt)
.field("request", &input.request)
.field("target", &input.target)
.field("reply_bytes", &input.reply.len())
.finish_non_exhaustive()
}
#[derive(Debug, Error)]
pub enum IcObservationRequestError {
#[error("IC observation original authority mismatch")]
AuthorityMismatch,
#[error("IC observation requires a pending original mutation")]
NoPendingMutation,
#[error("IC observation requires a pending original observation")]
NoPendingObservation,
#[error("IC observation original mutation mismatch")]
MutationMismatch,
#[error("IC observation original attempt mismatch")]
ObservationMismatch,
#[error("IC observation reserved request mismatch")]
RequestMismatch,
#[error(transparent)]
Payload(#[from] IcRequestError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum IcObservationResponseError {
#[error("invalid IC observation response attempts")]
InvalidAttempts,
#[error("IC observation response reply too large")]
ReplyTooLarge,
#[error("invalid IC observation response target")]
InvalidTarget,
}
mod capture_settlement;
pub use capture_settlement::{IcCaptureAttribution, IcCaptureSettlement};
mod settlement;
pub use settlement::{IcLifecycleAttribution, IcLifecycleSettlement};
#[cfg(test)]
mod tests;