use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
consistency::ApplicationFenceEvidence,
fence_obligation::{FenceObligationError, FenceObligationRecord},
inventory::{InventoryRecord, InventoryRecordError, MAX_INVENTORY_TARGETS},
operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
restore_safety::RestoreFenceEvidence,
};
use thiserror::Error;
pub const MAX_FENCE_RECONCILIATION_REMOTE_OBSERVATIONS: u32 = 1;
#[derive(Clone, Debug)]
pub struct FenceReconciliationIntent<'a> {
plan: &'a OperationPlanRecord,
obligation: &'a FenceObligationRecord,
authority: AttemptAuthorityRecord,
mutation_attempt: u32,
challenge: ArtifactChecksumRecord,
}
impl<'a> FenceReconciliationIntent<'a> {
pub fn new(
plan: &'a OperationPlanRecord,
obligation: &'a FenceObligationRecord,
journal: &AttemptJournalRecord,
challenge: ArtifactChecksumRecord,
) -> Result<Self, FenceReconciliationRequestError> {
obligation.validate_plan(plan)?;
let authority = plan.attempt_authority(obligation.acquisition_operation())?;
if journal.authority() != &authority {
return Err(FenceReconciliationRequestError::AuthorityMismatch);
}
let mutation_attempt = journal
.view()
.pending_mutation
.ok_or(FenceReconciliationRequestError::NoPendingMutation)?;
Ok(Self {
plan,
obligation,
authority,
mutation_attempt,
challenge,
})
}
#[must_use]
pub const fn obligation(&self) -> &FenceObligationRecord {
self.obligation
}
#[must_use]
pub const fn plan(&self) -> &OperationPlanRecord {
self.plan
}
#[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 challenge(&self) -> &ArtifactChecksumRecord {
&self.challenge
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
observation_digest(
&self.obligation.digest(),
&self.authority.digest(),
self.mutation_attempt,
&self.challenge,
)
}
fn validate_mutation(
&self,
journal: &AttemptJournalRecord,
) -> Result<(), FenceReconciliationRequestError> {
if journal.authority() != &self.authority {
return Err(FenceReconciliationRequestError::AuthorityMismatch);
}
if journal.view().pending_mutation != Some(self.mutation_attempt) {
return Err(FenceReconciliationRequestError::MutationMismatch);
}
Ok(())
}
}
fn observation_digest(
obligation: &ArtifactChecksumRecord,
authority: &ArtifactChecksumRecord,
mutation: u32,
challenge: &ArtifactChecksumRecord,
) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/fence-reconciliation/v1\0".to_vec();
bytes.extend_from_slice(obligation.hash().as_bytes());
bytes.extend_from_slice(authority.hash().as_bytes());
bytes.extend_from_slice(&mutation.to_be_bytes());
bytes.extend_from_slice(challenge.hash().as_bytes());
ArtifactChecksumRecord::from_bytes(&bytes)
}
#[derive(Clone, Debug)]
pub struct FenceReconciliationRequest<'a, 'plan> {
intent: &'a FenceReconciliationIntent<'plan>,
observation_attempt: u32,
}
impl<'a, 'plan> FenceReconciliationRequest<'a, 'plan> {
pub fn new(
intent: &'a FenceReconciliationIntent<'plan>,
journal: &AttemptJournalRecord,
) -> Result<Self, FenceReconciliationRequestError> {
intent.validate_mutation(journal)?;
let observation_attempt = journal
.view()
.pending_observation
.ok_or(FenceReconciliationRequestError::NoPendingObservation)?;
let request = Self {
intent,
observation_attempt,
};
request.validate_journal(journal)?;
Ok(request)
}
#[must_use]
pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
self.intent
}
#[must_use]
pub const fn observation_attempt(&self) -> u32 {
self.observation_attempt
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
self.intent.digest()
}
pub fn validate_journal(
&self,
journal: &AttemptJournalRecord,
) -> Result<(), FenceReconciliationRequestError> {
self.intent.validate_mutation(journal)?;
if journal.view().pending_observation != Some(self.observation_attempt) {
return Err(FenceReconciliationRequestError::ObservationMismatch);
}
if journal.pending_observation_request() != Some(self.digest().hash()) {
return Err(FenceReconciliationRequestError::RequestMismatch);
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub enum FenceReconciliationEvidence {
AcquiredCapture {
fence: Box<ApplicationFenceEvidence>,
attribution: ArtifactChecksumRecord,
},
AcquiredRestore {
fence: Box<RestoreFenceEvidence>,
attribution: ArtifactChecksumRecord,
},
NotAcquired {
exclusion: ArtifactChecksumRecord,
},
Unresolved {
uncertainty: ArtifactChecksumRecord,
},
}
#[derive(Clone, Debug)]
pub struct FenceReconciliationObservationInput {
pub request: ArtifactChecksumRecord,
pub mutation_attempt: u32,
pub observation_attempt: u32,
pub context: PlanContextRecord,
pub inventory: InventoryRecord,
pub selected_targets: Vec<String>,
pub settlement: FenceReconciliationEvidence,
pub evidence: ArtifactChecksumRecord,
pub remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct FenceReconciliationObservation {
input: FenceReconciliationObservationInput,
}
impl FenceReconciliationObservation {
pub fn new(
mut input: FenceReconciliationObservationInput,
) -> Result<Self, FenceReconciliationObservationError> {
if input.mutation_attempt == 0
|| input.observation_attempt <= input.mutation_attempt
|| input.observation_attempt > MAX_OPERATION_ATTEMPTS
{
return Err(FenceReconciliationObservationError::InvalidAttempts);
}
if input.selected_targets.is_empty() || input.selected_targets.len() > MAX_INVENTORY_TARGETS
{
return Err(FenceReconciliationObservationError::InvalidTargetCount);
}
for target in &mut input.selected_targets {
*target = input.inventory.target(target)?.canister_id().into();
}
input.selected_targets.sort();
if input
.selected_targets
.windows(2)
.any(|pair| pair[0] == pair[1])
{
return Err(FenceReconciliationObservationError::DuplicateTarget);
}
Ok(Self { input })
}
#[must_use]
pub const fn input(&self) -> &FenceReconciliationObservationInput {
&self.input
}
}
#[derive(Debug, Error)]
pub enum FenceReconciliationRequestError {
#[error("fence reconciliation original journal authority mismatch")]
AuthorityMismatch,
#[error("fence reconciliation requires a pending acquisition mutation")]
NoPendingMutation,
#[error("fence reconciliation pending acquisition mismatch")]
MutationMismatch,
#[error("fence reconciliation requires a pending observation reservation")]
NoPendingObservation,
#[error("fence reconciliation observation attempt mismatch")]
ObservationMismatch,
#[error("fence reconciliation reserved request mismatch")]
RequestMismatch,
#[error(transparent)]
Obligation(#[from] FenceObligationError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
}
#[derive(Debug, Error)]
pub enum FenceReconciliationObservationError {
#[error("invalid fence reconciliation attempt identities")]
InvalidAttempts,
#[error("invalid fence reconciliation target count")]
InvalidTargetCount,
#[error("duplicate fence reconciliation target")]
DuplicateTarget,
#[error(transparent)]
Inventory(#[from] InventoryRecordError),
}
#[cfg(test)]
mod tests;