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> {
requirement.validate_plan(plan)?;
if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
return Err(FenceObligationError::FenceNotRequired);
}
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> {
requirement.validate_plans(plan, source)?;
if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
return Err(FenceObligationError::FenceNotRequired);
}
let fence = requirement
.expected_fence()
.ok_or(FenceObligationError::FenceNotRequired)?;
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);
}
if plan
.attempt_authority(self.acquisition_operation)?
.budget()
.mutations()
== 0
{
return Err(FenceObligationError::NoAcquisitionAllowance);
}
Ok(())
}
pub fn validate_capture(
&self,
plan: &OperationPlanRecord,
requirement: &ConsistencyRequirementRecord,
fence: &ApplicationFenceBinding,
) -> Result<(), FenceObligationError> {
let original = Self::for_capture(plan, requirement, self.acquisition_operation, fence)?;
self.validate_original(&original)
}
pub fn validate_restore(
&self,
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
requirement: &RestoreSafetyRequirementRecord,
) -> Result<(), FenceObligationError> {
let original = Self::for_restore(plan, source, requirement, self.acquisition_operation)?;
self.validate_original(&original)
}
fn validate_original(&self, original: &Self) -> Result<(), FenceObligationError> {
if self != original {
return Err(FenceObligationError::BindingMismatch);
}
Ok(())
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/fence-obligation/v1\0".to_vec();
bytes.extend_from_slice(self.plan_intent.hash().as_bytes());
bytes.extend_from_slice(&self.acquisition_operation.to_be_bytes());
let (tag, hashes) = match &self.scope {
FenceObligationScopeRecord::Capture {
requirement,
identity,
membership_revision,
} => (0, vec![requirement, identity, membership_revision]),
FenceObligationScopeRecord::Restore { requirement, fence } => (
1,
vec![
requirement,
&fence.identity,
&fence.membership_revision,
&fence.external_obligations_revision,
],
),
};
bytes.push(tag);
for hash in hashes {
bytes.extend_from_slice(hash.hash().as_bytes());
}
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[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;