Skip to main content

ic_backup/model/fence_acquisition/
mod.rs

1//! Exact application update bytes and original fence acquisition reservation binding.
2
3use crate::model::application_payload::ApplicationPayloadError as Payload;
4use crate::model::attempt_journal::MutationReservationError as Reservation;
5use crate::model::evidence_digest::EvidenceDigest;
6use crate::model::{
7    artifacts::ArtifactChecksumRecord,
8    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
9    fence_obligation::{FenceObligationError, FenceObligationRecord},
10    operation_plan::{OperationPlanError, OperationPlanRecord},
11};
12use std::fmt;
13use thiserror::Error;
14
15/// Maximum opaque application arguments for one acquisition update, checked before copying.
16pub const MAX_FENCE_ACQUISITION_ARGUMENT_BYTES: usize = 1024 * 1024;
17/// Maximum exact visible ASCII method bytes; method spelling is never normalized.
18pub const MAX_FENCE_ACQUISITION_METHOD_BYTES: usize = 128;
19
20/// Immutable bounded host update envelope; application codecs own argument semantics.
21///
22/// Receiver and routing target are the same exact application canister. No management,
23/// proxy, query, signing or default method is supplied. Retain these original bytes
24/// before publishing their plan and obligation; hashes cannot recover missing bytes.
25/// The nonrecursive payload digest excludes plan/requirement/obligation hashes,
26/// which bind the payload subsequently through the original operation authority.
27#[derive(Clone)]
28pub struct FenceAcquisitionPayload {
29    target: String,
30    target_bytes: Vec<u8>,
31    method: String,
32    arguments: Vec<u8>,
33}
34impl FenceAcquisitionPayload {
35    /// Admit exact application receiver, method and bounded opaque argument bytes.
36    /// # Errors
37    /// Rejects malformed/management receivers, empty/nonvisible/oversized methods and excessive bytes.
38    pub fn new(
39        target: &str,
40        method: &str,
41        arguments: &[u8],
42    ) -> Result<Self, FenceAcquisitionError> {
43        crate::model::application_payload::validate_bytes(
44            arguments,
45            MAX_FENCE_ACQUISITION_ARGUMENT_BYTES,
46            method,
47            MAX_FENCE_ACQUISITION_METHOD_BYTES,
48        )?;
49        let (target, target_bytes) = crate::model::application_payload::principal(target)?;
50        Ok(Self {
51            target,
52            target_bytes,
53            method: method.into(),
54            arguments: arguments.into(),
55        })
56    }
57    /// Read the exact canonical application receiver and routing target.
58    #[must_use]
59    pub fn target(&self) -> &str {
60        &self.target
61    }
62    /// Read exact original method spelling.
63    #[must_use]
64    pub fn method(&self) -> &str {
65        &self.method
66    }
67    /// Read exact opaque original bytes; the application must qualify whole-unit acquisition semantics.
68    #[must_use]
69    pub fn arguments(&self) -> &[u8] {
70        &self.arguments
71    }
72    /// Hash NUL-terminated domain, u8 principal length/raw bytes, fixed update byte 1,
73    /// big-endian u32 method length/exact bytes and u64 argument length/exact bytes.
74    #[must_use]
75    pub fn digest(&self) -> ArtifactChecksumRecord {
76        let mut bytes = EvidenceDigest::new(b"ic-backup/application-fence-acquisition/v1\0");
77        bytes.bytes_u8(&self.target_bytes);
78        bytes.byte(1); // Exactly one host replicated update, never a query.
79        bytes.bytes_u32(self.method.as_bytes());
80        bytes.bytes_u64(&self.arguments);
81        bytes.finish()
82    }
83}
84impl fmt::Debug for FenceAcquisitionPayload {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        formatter
87            .debug_struct("FenceAcquisitionPayload")
88            .field("target", &self.target)
89            .field("method", &self.method)
90            .field("argument_bytes", &self.arguments.len())
91            .finish_non_exhaustive()
92    }
93}
94
95/// Admit exact original acquisition authority and payload before reserving spending.
96///
97/// This checks declarations only: no pending reservation, fresh permission,
98/// never-dispatched custody or executable application contract is established.
99/// # Errors
100/// Rejects changed obligation/plan, journal authority, receiver or exact wire bytes.
101pub fn original_authority(
102    plan: &OperationPlanRecord,
103    obligation: &FenceObligationRecord,
104    journal: &AttemptJournalRecord,
105    payload: &FenceAcquisitionPayload,
106) -> Result<AttemptAuthorityRecord, FenceAcquisitionError> {
107    obligation.validate_plan(plan)?;
108    let authority = plan.attempt_authority_for_journal(
109        obligation.acquisition_operation(),
110        journal,
111        FenceAcquisitionError::AuthorityMismatch,
112    )?;
113    if payload.target() != authority.binding().target() {
114        return Err(FenceAcquisitionError::TargetMismatch);
115    }
116    if payload.digest().hash() != authority.binding().request() {
117        return Err(FenceAcquisitionError::PayloadMismatch);
118    }
119    Ok(authority)
120}
121
122/// Structural binding to an already reserved acquisition; never a fresh dispatch permit.
123///
124/// Integration admission additionally requires durable originals, exact request semantics,
125/// actual permissions/context/unit, dependency/custody qualification and proof that this
126/// reservation has never been dispatched. Reconstructing a pending request proves none
127/// of those conditions. After interruption without that proof, reconcile instead.
128#[derive(Debug)]
129pub struct FenceAcquisitionRequest<'a> {
130    plan: &'a OperationPlanRecord,
131    obligation: &'a FenceObligationRecord,
132    payload: &'a FenceAcquisitionPayload,
133    authority: AttemptAuthorityRecord,
134    mutation_attempt: u32,
135}
136impl<'a> FenceAcquisitionRequest<'a> {
137    /// Bind original exact application bytes only after the canonical journal reserves mutation.
138    /// # Errors
139    /// Rejects changed originals, target/bytes, missing mutation or an acquisition already in observation recovery.
140    pub fn new(
141        plan: &'a OperationPlanRecord,
142        obligation: &'a FenceObligationRecord,
143        journal: &AttemptJournalRecord,
144        payload: &'a FenceAcquisitionPayload,
145    ) -> Result<Self, FenceAcquisitionError> {
146        let authority = original_authority(plan, obligation, journal, payload)?;
147        let mutation_attempt = journal
148            .pending_mutation()
149            .ok_or(FenceAcquisitionError::NoPendingMutation)?;
150        let request = Self {
151            plan,
152            obligation,
153            payload,
154            authority,
155            mutation_attempt,
156        };
157        request.validate_journal(journal)?;
158        Ok(request)
159    }
160    /// Read original full context/inventory/selected unit; target routing does not narrow coverage.
161    #[must_use]
162    pub const fn plan(&self) -> &OperationPlanRecord {
163        self.plan
164    }
165    /// Read original purpose/fence/revisions under the retained requirement.
166    #[must_use]
167    pub const fn obligation(&self) -> &FenceObligationRecord {
168        self.obligation
169    }
170    /// Read exact immutable application update bytes.
171    #[must_use]
172    pub const fn payload(&self) -> &FenceAcquisitionPayload {
173        self.payload
174    }
175    /// Read full original operation identity and spending limits.
176    #[must_use]
177    pub const fn authority(&self) -> &AttemptAuthorityRecord {
178        &self.authority
179    }
180    /// Read the already consumed original mutation number.
181    #[must_use]
182    pub const fn mutation_attempt(&self) -> u32 {
183        self.mutation_attempt
184    }
185    /// Recheck current reservation before associating an acknowledgement; performs no IO or effects.
186    /// # Errors
187    /// Rejects changed authority, settled/replaced mutation or pending observation recovery.
188    pub fn validate_journal(
189        &self,
190        journal: &AttemptJournalRecord,
191    ) -> Result<(), FenceAcquisitionError> {
192        journal
193            .validate_pending_mutation(&self.authority, self.mutation_attempt)
194            .map_err(Into::into)
195    }
196}
197
198/// Passive exact update reply association, without acquisition outcome or fence custody proof.
199#[derive(Clone, Debug)]
200pub struct FenceAcquisitionAcknowledgement {
201    /// Full original authority digest, including original plan/context/operation/limits.
202    pub authority: ArtifactChecksumRecord,
203    /// Original already reserved mutation attempt, not an independently numbered call.
204    pub mutation_attempt: u32,
205    /// Qualified provider's retained exact reply association evidence; not an authenticated signature.
206    pub evidence: ArtifactChecksumRecord,
207}
208impl FenceAcquisitionAcknowledgement {
209    /// Admit a passive acknowledgement with a finite original mutation identity.
210    /// # Errors
211    /// Rejects zero or attempt numbers above the existing combined attempt ceiling.
212    pub fn new(
213        authority: ArtifactChecksumRecord,
214        mutation_attempt: u32,
215        evidence: ArtifactChecksumRecord,
216    ) -> Result<Self, FenceAcquisitionError> {
217        if mutation_attempt == 0 || mutation_attempt > MAX_OPERATION_ATTEMPTS {
218            return Err(FenceAcquisitionError::InvalidAttempt);
219        }
220        Ok(Self {
221            authority,
222            mutation_attempt,
223            evidence,
224        })
225    }
226}
227
228/// Typed payload/original reservation rejection; no error refunds or releases anything.
229#[derive(Debug, Error)]
230pub enum FenceAcquisitionError {
231    /// Receiver is not a bounded canonical principal.
232    #[error("invalid fence acquisition receiver")]
233    InvalidTarget,
234    /// This port cannot call the management canister.
235    #[error("fence acquisition requires an application receiver")]
236    ManagementReceiver,
237    /// Method exceeds the exact visible ASCII envelope bound.
238    #[error("invalid fence acquisition method")]
239    InvalidMethod,
240    /// Raw arguments exceed the finite input bound.
241    #[error("fence acquisition arguments exceed the input bound")]
242    ArgumentsTooLarge,
243    /// Journal is not the exact original acquisition authority.
244    #[error("fence acquisition original journal authority mismatch")]
245    AuthorityMismatch,
246    /// Receiver differs from the original operation target.
247    #[error("fence acquisition original target mismatch")]
248    TargetMismatch,
249    /// Method/arguments differ from the original declared payload digest.
250    #[error("fence acquisition original payload mismatch")]
251    PayloadMismatch,
252    /// Original mutation was not already reserved.
253    #[error("fence acquisition requires a pending mutation reservation")]
254    NoPendingMutation,
255    /// Current journal no longer retains this exact unresolved mutation.
256    #[error("fence acquisition pending mutation mismatch")]
257    MutationMismatch,
258    /// An observation is pending; original mutation must not be dispatched again.
259    #[error("fence acquisition observation recovery is pending")]
260    ObservationPending,
261    /// Acknowledgement attempt is outside the finite original journal range.
262    #[error("invalid fence acquisition acknowledgement attempt")]
263    InvalidAttempt,
264    /// Original obligation admission failed.
265    #[error(transparent)]
266    Obligation(#[from] FenceObligationError),
267    /// Original plan authority cannot be derived.
268    #[error(transparent)]
269    Plan(#[from] OperationPlanError),
270}
271
272#[cfg(test)]
273mod tests;
274
275impl From<crate::model::attempt_journal::MutationReservationError> for FenceAcquisitionError {
276    fn from(error: crate::model::attempt_journal::MutationReservationError) -> Self {
277        match error {
278            Reservation::NoPendingMutation => Self::NoPendingMutation,
279            Reservation::AuthorityMismatch => Self::AuthorityMismatch,
280            Reservation::MutationMismatch => Self::MutationMismatch,
281            Reservation::ObservationPending => Self::ObservationPending,
282        }
283    }
284}
285
286impl From<crate::model::application_payload::ApplicationPayloadError> for FenceAcquisitionError {
287    fn from(error: crate::model::application_payload::ApplicationPayloadError) -> Self {
288        match error {
289            Payload::ArgumentsTooLarge => Self::ArgumentsTooLarge,
290            Payload::InvalidMethod => Self::InvalidMethod,
291            Payload::InvalidPrincipal => Self::InvalidTarget,
292            Payload::ManagementPrincipal => Self::ManagementReceiver,
293        }
294    }
295}