use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
fence_obligation::{FenceObligationError, FenceObligationRecord},
operation_plan::{OperationPlanError, OperationPlanRecord},
};
use ic_principal::Principal;
use std::fmt;
use thiserror::Error;
pub const MAX_FENCE_ACQUISITION_ARGUMENT_BYTES: usize = 1024 * 1024;
pub const MAX_FENCE_ACQUISITION_METHOD_BYTES: usize = 128;
#[derive(Clone)]
pub struct FenceAcquisitionPayload {
target: String,
target_bytes: Vec<u8>,
method: String,
arguments: Vec<u8>,
}
impl FenceAcquisitionPayload {
pub fn new(
target: &str,
method: &str,
arguments: &[u8],
) -> Result<Self, FenceAcquisitionError> {
if arguments.len() > MAX_FENCE_ACQUISITION_ARGUMENT_BYTES {
return Err(FenceAcquisitionError::ArgumentsTooLarge);
}
if method.is_empty()
|| method.len() > MAX_FENCE_ACQUISITION_METHOD_BYTES
|| !method.bytes().all(|byte| byte.is_ascii_graphic())
{
return Err(FenceAcquisitionError::InvalidMethod);
}
let target = crate::model::principal::canonical_text(target)
.ok_or(FenceAcquisitionError::InvalidTarget)?;
let principal =
Principal::from_text(&target).map_err(|_| FenceAcquisitionError::InvalidTarget)?;
if principal.as_slice().is_empty() {
return Err(FenceAcquisitionError::ManagementReceiver);
}
Ok(Self {
target,
target_bytes: principal.as_slice().to_vec(),
method: method.into(),
arguments: arguments.into(),
})
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
#[must_use]
pub fn method(&self) -> &str {
&self.method
}
#[must_use]
pub fn arguments(&self) -> &[u8] {
&self.arguments
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/application-fence-acquisition/v1\0".to_vec();
bytes.push(self.target_bytes.len().to_le_bytes()[0]);
bytes.extend_from_slice(&self.target_bytes);
bytes.push(1); bytes.extend_from_slice(&u32::from(self.method.len().to_le_bytes()[0]).to_be_bytes());
bytes.extend_from_slice(self.method.as_bytes());
bytes.extend_from_slice(&(self.arguments.len() as u64).to_be_bytes());
bytes.extend_from_slice(&self.arguments);
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
impl fmt::Debug for FenceAcquisitionPayload {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("FenceAcquisitionPayload")
.field("target", &self.target)
.field("method", &self.method)
.field("argument_bytes", &self.arguments.len())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct FenceAcquisitionRequest<'a> {
plan: &'a OperationPlanRecord,
obligation: &'a FenceObligationRecord,
payload: &'a FenceAcquisitionPayload,
authority: AttemptAuthorityRecord,
mutation_attempt: u32,
}
impl<'a> FenceAcquisitionRequest<'a> {
pub fn new(
plan: &'a OperationPlanRecord,
obligation: &'a FenceObligationRecord,
journal: &AttemptJournalRecord,
payload: &'a FenceAcquisitionPayload,
) -> Result<Self, FenceAcquisitionError> {
obligation.validate_plan(plan)?;
let authority = plan.attempt_authority(obligation.acquisition_operation())?;
if journal.authority() != &authority {
return Err(FenceAcquisitionError::AuthorityMismatch);
}
if payload.target() != authority.binding().target() {
return Err(FenceAcquisitionError::TargetMismatch);
}
if payload.digest().hash() != authority.binding().request() {
return Err(FenceAcquisitionError::PayloadMismatch);
}
let mutation_attempt = journal
.view()
.pending_mutation
.ok_or(FenceAcquisitionError::NoPendingMutation)?;
let request = Self {
plan,
obligation,
payload,
authority,
mutation_attempt,
};
request.validate_journal(journal)?;
Ok(request)
}
#[must_use]
pub const fn plan(&self) -> &OperationPlanRecord {
self.plan
}
#[must_use]
pub const fn obligation(&self) -> &FenceObligationRecord {
self.obligation
}
#[must_use]
pub const fn payload(&self) -> &FenceAcquisitionPayload {
self.payload
}
#[must_use]
pub const fn authority(&self) -> &AttemptAuthorityRecord {
&self.authority
}
#[must_use]
pub const fn mutation_attempt(&self) -> u32 {
self.mutation_attempt
}
pub fn validate_journal(
&self,
journal: &AttemptJournalRecord,
) -> Result<(), FenceAcquisitionError> {
if journal.authority() != &self.authority {
return Err(FenceAcquisitionError::AuthorityMismatch);
}
let current = journal.view();
if current.pending_mutation != Some(self.mutation_attempt) {
return Err(FenceAcquisitionError::MutationMismatch);
}
if current.pending_observation.is_some() {
return Err(FenceAcquisitionError::ObservationPending);
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct FenceAcquisitionAcknowledgement {
pub authority: ArtifactChecksumRecord,
pub mutation_attempt: u32,
pub evidence: ArtifactChecksumRecord,
}
impl FenceAcquisitionAcknowledgement {
pub fn new(
authority: ArtifactChecksumRecord,
mutation_attempt: u32,
evidence: ArtifactChecksumRecord,
) -> Result<Self, FenceAcquisitionError> {
if mutation_attempt == 0 || mutation_attempt > MAX_OPERATION_ATTEMPTS {
return Err(FenceAcquisitionError::InvalidAttempt);
}
Ok(Self {
authority,
mutation_attempt,
evidence,
})
}
}
#[derive(Debug, Error)]
pub enum FenceAcquisitionError {
#[error("invalid fence acquisition receiver")]
InvalidTarget,
#[error("fence acquisition requires an application receiver")]
ManagementReceiver,
#[error("invalid fence acquisition method")]
InvalidMethod,
#[error("fence acquisition arguments exceed the input bound")]
ArgumentsTooLarge,
#[error("fence acquisition original journal authority mismatch")]
AuthorityMismatch,
#[error("fence acquisition original target mismatch")]
TargetMismatch,
#[error("fence acquisition original payload mismatch")]
PayloadMismatch,
#[error("fence acquisition requires a pending mutation reservation")]
NoPendingMutation,
#[error("fence acquisition pending mutation mismatch")]
MutationMismatch,
#[error("fence acquisition observation recovery is pending")]
ObservationPending,
#[error("invalid fence acquisition acknowledgement attempt")]
InvalidAttempt,
#[error(transparent)]
Obligation(#[from] FenceObligationError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
}
#[cfg(test)]
mod tests;