use crate::model::evidence_digest::EvidenceDigest;
use crate::model::{
artifacts::ArtifactChecksumRecord,
consistency::{
ApplicationFenceBinding, ConsistencyGuaranteeRecord, ConsistencyRequirementError,
ConsistencyRequirementRecord,
},
operation_plan::{OperationPlanError, OperationPlanRecord},
restore_safety::{
RestoreFenceBindingRecord, RestoreSafetyLaneRecord, RestoreSafetyRequirementError,
RestoreSafetyRequirementRecord,
},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const MAX_FENCE_OBLIGATION_BYTES: u64 = 1024;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "purpose", rename_all = "snake_case", deny_unknown_fields)]
pub enum FenceObligationScopeRecord {
Capture {
requirement: ArtifactChecksumRecord,
identity: ArtifactChecksumRecord,
membership_revision: ArtifactChecksumRecord,
},
Restore {
requirement: ArtifactChecksumRecord,
fence: RestoreFenceBindingRecord,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "ObligationFields")]
pub struct FenceObligationRecord {
version: u16,
plan_intent: ArtifactChecksumRecord,
acquisition_operation: u64,
scope: FenceObligationScopeRecord,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ObligationFields {
version: u16,
plan_intent: ArtifactChecksumRecord,
acquisition_operation: u64,
scope: FenceObligationScopeRecord,
}
impl TryFrom<ObligationFields> for FenceObligationRecord {
type Error = FenceObligationError;
fn try_from(fields: ObligationFields) -> Result<Self, Self::Error> {
if fields.version != 1 {
return Err(FenceObligationError::UnsupportedVersion(fields.version));
}
Ok(Self {
version: 1,
plan_intent: fields.plan_intent,
acquisition_operation: fields.acquisition_operation,
scope: fields.scope,
})
}
}
impl FenceObligationRecord {
pub fn for_capture(
plan: &OperationPlanRecord,
requirement: &ConsistencyRequirementRecord,
acquisition_operation: u64,
fence: &ApplicationFenceBinding,
) -> Result<Self, FenceObligationError> {
require_capture(plan, requirement)?;
Self::new(
plan,
acquisition_operation,
FenceObligationScopeRecord::Capture {
requirement: requirement.digest(),
identity: fence.identity.clone(),
membership_revision: fence.membership_revision.clone(),
},
)
}
pub fn for_restore(
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
requirement: &RestoreSafetyRequirementRecord,
acquisition_operation: u64,
) -> Result<Self, FenceObligationError> {
let fence = require_restore(plan, source, requirement)?;
Self::new(
plan,
acquisition_operation,
FenceObligationScopeRecord::Restore {
requirement: requirement.digest(),
fence: fence.clone(),
},
)
}
fn new(
plan: &OperationPlanRecord,
acquisition_operation: u64,
scope: FenceObligationScopeRecord,
) -> Result<Self, FenceObligationError> {
let record = Self {
version: 1,
plan_intent: plan.digest(),
acquisition_operation,
scope,
};
record.validate_plan(plan)?;
Ok(record)
}
#[must_use]
pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
&self.plan_intent
}
#[must_use]
pub const fn acquisition_operation(&self) -> u64 {
self.acquisition_operation
}
#[must_use]
pub const fn scope(&self) -> &FenceObligationScopeRecord {
&self.scope
}
pub fn validate_plan(&self, plan: &OperationPlanRecord) -> Result<(), FenceObligationError> {
if self.plan_intent != plan.digest() {
return Err(FenceObligationError::PlanMismatch);
}
require_acquisition(plan, self.acquisition_operation)
}
pub fn validate_capture(
&self,
plan: &OperationPlanRecord,
requirement: &ConsistencyRequirementRecord,
fence: &ApplicationFenceBinding,
) -> Result<(), FenceObligationError> {
require_capture(plan, requirement)?;
require_acquisition(plan, self.acquisition_operation)?;
let scope_matches = matches!(
&self.scope,
FenceObligationScopeRecord::Capture { requirement: original, identity, membership_revision }
if original == &requirement.digest()
&& identity == &fence.identity
&& membership_revision == &fence.membership_revision
);
self.validate_binding(plan, scope_matches)
}
pub fn validate_restore(
&self,
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
requirement: &RestoreSafetyRequirementRecord,
) -> Result<(), FenceObligationError> {
let fence = require_restore(plan, source, requirement)?;
require_acquisition(plan, self.acquisition_operation)?;
let scope_matches = matches!(
&self.scope,
FenceObligationScopeRecord::Restore { requirement: original, fence: original_fence }
if original == &requirement.digest() && original_fence == fence
);
self.validate_binding(plan, scope_matches)
}
fn validate_binding(
&self,
plan: &OperationPlanRecord,
scope_matches: bool,
) -> Result<(), FenceObligationError> {
if self.plan_intent != plan.digest() || !scope_matches {
return Err(FenceObligationError::BindingMismatch);
}
Ok(())
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = EvidenceDigest::new(b"ic-backup/fence-obligation/v1\0");
bytes.checksum(&self.plan_intent);
bytes.u64_be(self.acquisition_operation);
match &self.scope {
FenceObligationScopeRecord::Capture {
requirement,
identity,
membership_revision,
} => {
bytes.byte(0);
bytes.checksum(requirement);
bytes.checksum(identity);
bytes.checksum(membership_revision);
}
FenceObligationScopeRecord::Restore { requirement, fence } => {
bytes.byte(1);
bytes.checksum(requirement);
bytes.checksum(&fence.identity);
bytes.checksum(&fence.membership_revision);
bytes.checksum(&fence.external_obligations_revision);
}
}
bytes.finish()
}
}
fn require_capture(
plan: &OperationPlanRecord,
requirement: &ConsistencyRequirementRecord,
) -> Result<(), FenceObligationError> {
requirement.validate_plan(plan)?;
if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
return Err(FenceObligationError::FenceNotRequired);
}
Ok(())
}
fn require_restore<'a>(
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
requirement: &'a RestoreSafetyRequirementRecord,
) -> Result<&'a RestoreFenceBindingRecord, FenceObligationError> {
requirement.validate_plans(plan, source)?;
if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
return Err(FenceObligationError::FenceNotRequired);
}
requirement
.expected_fence()
.ok_or(FenceObligationError::FenceNotRequired)
}
fn require_acquisition(
plan: &OperationPlanRecord,
sequence: u64,
) -> Result<(), FenceObligationError> {
if plan.operation(sequence)?.budget().mutations() == 0 {
return Err(FenceObligationError::NoAcquisitionAllowance);
}
Ok(())
}
#[derive(Debug, Error)]
pub enum FenceObligationError {
#[error("unsupported fence obligation version {0}")]
UnsupportedVersion(u16),
#[error("fence obligation original plan mismatch")]
PlanMismatch,
#[error("fence obligation original binding mismatch")]
BindingMismatch,
#[error("original requirement does not require an application fence")]
FenceNotRequired,
#[error("original fence acquisition operation has no mutation allowance")]
NoAcquisitionAllowance,
#[error(transparent)]
Plan(#[from] OperationPlanError),
#[error(transparent)]
Consistency(#[from] ConsistencyRequirementError),
#[error(transparent)]
Restore(#[from] RestoreSafetyRequirementError),
}
#[cfg(test)]
mod tests;