mod requirement;
pub use requirement::{
ConsistencyGuaranteeRecord, ConsistencyRequirementError, ConsistencyRequirementRecord,
MAX_CONSISTENCY_REQUIREMENT_BYTES,
};
use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::OperationBindingRecord,
inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
};
use thiserror::Error;
pub const MAX_CONSISTENCY_REMOTE_OBSERVATIONS: u32 = 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConsistencyBoundary {
BeforeCapture,
AfterCapture,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ApplicationFenceBinding {
pub identity: ArtifactChecksumRecord,
pub membership_revision: ArtifactChecksumRecord,
}
#[derive(Clone, Debug)]
pub struct ConsistencyRequestInput {
pub operation_sequence: u64,
pub challenge: ArtifactChecksumRecord,
pub boundary: ConsistencyBoundary,
pub expected_fence: Option<ApplicationFenceBinding>,
pub max_remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct ConsistencyRequest<'a> {
plan: &'a OperationPlanRecord,
requirement: &'a ConsistencyRequirementRecord,
binding: OperationBindingRecord,
input: ConsistencyRequestInput,
}
impl<'a> ConsistencyRequest<'a> {
pub fn new(
plan: &'a OperationPlanRecord,
requirement: &'a ConsistencyRequirementRecord,
input: ConsistencyRequestInput,
) -> Result<Self, ConsistencyRequestError> {
requirement.validate_plan(plan)?;
if input.max_remote_observations > MAX_CONSISTENCY_REMOTE_OBSERVATIONS {
return Err(ConsistencyRequestError::ObservationLimitTooLarge);
}
match (requirement.guarantee(), input.expected_fence.as_ref()) {
(ConsistencyGuaranteeRecord::ApplicationCoordinated, None) => {
return Err(ConsistencyRequestError::FenceRequired);
}
(ConsistencyGuaranteeRecord::PerCanister, Some(_)) => {
return Err(ConsistencyRequestError::UnexpectedFence);
}
_ => {}
}
let binding = plan
.attempt_authority(input.operation_sequence)?
.binding()
.clone();
Ok(Self {
plan,
requirement,
binding,
input,
})
}
#[must_use]
pub const fn binding(&self) -> &OperationBindingRecord {
&self.binding
}
#[must_use]
pub const fn inventory(&self) -> &InventoryRecord {
self.plan.inventory()
}
#[must_use]
pub fn selected_targets(&self) -> &[String] {
self.plan.selected_targets()
}
#[must_use]
pub const fn requirement(&self) -> &ConsistencyRequirementRecord {
self.requirement
}
#[must_use]
pub const fn challenge(&self) -> &ArtifactChecksumRecord {
&self.input.challenge
}
#[must_use]
pub const fn boundary(&self) -> ConsistencyBoundary {
self.input.boundary
}
#[must_use]
pub const fn expected_fence(&self) -> Option<&ApplicationFenceBinding> {
self.input.expected_fence.as_ref()
}
#[must_use]
pub const fn max_remote_observations(&self) -> u32 {
self.input.max_remote_observations
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/consistency-request/v1\0".to_vec();
bytes.extend_from_slice(self.requirement.digest().hash().as_bytes());
bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
bytes.extend_from_slice(self.challenge().hash().as_bytes());
bytes.push(match self.boundary() {
ConsistencyBoundary::BeforeCapture => 0,
ConsistencyBoundary::AfterCapture => 1,
});
match self.expected_fence() {
None => bytes.push(0),
Some(fence) => {
bytes.push(1);
bytes.extend_from_slice(fence.identity.hash().as_bytes());
bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
}
}
bytes.extend_from_slice(&self.max_remote_observations().to_be_bytes());
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CaptureState {
Running,
Stopping,
Stopped,
}
#[derive(Clone, Debug)]
pub struct TargetCaptureEvidence {
pub target: String,
pub state: CaptureState,
pub stopped_and_drained: ArtifactChecksumRecord,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ApplicationFenceState {
Active,
Inactive,
}
#[derive(Clone, Debug)]
pub struct ApplicationFenceEvidence {
pub state: ApplicationFenceState,
pub identity: ArtifactChecksumRecord,
pub membership_revision: ArtifactChecksumRecord,
pub writes: ArtifactChecksumRecord,
pub membership: ArtifactChecksumRecord,
pub timers: ArtifactChecksumRecord,
pub external_work: ArtifactChecksumRecord,
pub drained_work: ArtifactChecksumRecord,
}
#[derive(Clone, Debug)]
pub enum ConsistencyEvidence {
PerCanister,
ApplicationCoordinated(Box<ApplicationFenceEvidence>),
}
#[derive(Clone, Debug)]
pub struct ConsistencyObservationInput {
pub request: ArtifactChecksumRecord,
pub context: PlanContextRecord,
pub inventory: InventoryRecord,
pub targets: Vec<TargetCaptureEvidence>,
pub membership_revision: Option<ArtifactChecksumRecord>,
pub consistency: ConsistencyEvidence,
pub evidence: ArtifactChecksumRecord,
pub remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct ConsistencyObservation {
input: ConsistencyObservationInput,
}
impl ConsistencyObservation {
pub fn new(
mut input: ConsistencyObservationInput,
) -> Result<Self, ConsistencyObservationError> {
if input.targets.is_empty() || input.targets.len() > MAX_INVENTORY_TARGETS {
return Err(ConsistencyObservationError::InvalidTargetCount);
}
for target in &mut input.targets {
target.target = super::principal::canonical_text(&target.target)
.ok_or(ConsistencyObservationError::InvalidPrincipal)?;
if input.inventory.target(&target.target).is_err() {
return Err(ConsistencyObservationError::TargetAbsentFromInventory);
}
}
input.targets.sort_by(|a, b| a.target.cmp(&b.target));
if input
.targets
.windows(2)
.any(|pair| pair[0].target == pair[1].target)
{
return Err(ConsistencyObservationError::DuplicateTarget);
}
Ok(Self { input })
}
#[must_use]
pub const fn request(&self) -> &ArtifactChecksumRecord {
&self.input.request
}
#[must_use]
pub const fn context(&self) -> &PlanContextRecord {
&self.input.context
}
#[must_use]
pub const fn inventory(&self) -> &InventoryRecord {
&self.input.inventory
}
#[must_use]
pub fn targets(&self) -> &[TargetCaptureEvidence] {
&self.input.targets
}
#[must_use]
pub const fn membership_revision(&self) -> Option<&ArtifactChecksumRecord> {
self.input.membership_revision.as_ref()
}
#[must_use]
pub const fn consistency(&self) -> &ConsistencyEvidence {
&self.input.consistency
}
#[must_use]
pub const fn evidence(&self) -> &ArtifactChecksumRecord {
&self.input.evidence
}
#[must_use]
pub const fn remote_observations(&self) -> u32 {
self.input.remote_observations
}
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum ConsistencyObservationError {
#[error("consistency targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
InvalidTargetCount,
#[error("invalid consistency target principal")]
InvalidPrincipal,
#[error("duplicate consistency target")]
DuplicateTarget,
#[error("consistency target absent from actual inventory")]
TargetAbsentFromInventory,
}
#[derive(Debug, Error)]
pub enum ConsistencyRequestError {
#[error(transparent)]
Requirement(#[from] ConsistencyRequirementError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
#[error("coordinated consistency requires retained fence identity")]
FenceRequired,
#[error("per-canister consistency does not admit expected fence identity")]
UnexpectedFence,
#[error("consistency observation ceiling exceeds {MAX_CONSISTENCY_REMOTE_OBSERVATIONS}")]
ObservationLimitTooLarge,
}
#[cfg(test)]
mod tests;