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(obligation.acquisition_operation())?;
109    if journal.authority() != &authority {
110        return Err(FenceAcquisitionError::AuthorityMismatch);
111    }
112    if payload.target() != authority.binding().target() {
113        return Err(FenceAcquisitionError::TargetMismatch);
114    }
115    if payload.digest().hash() != authority.binding().request() {
116        return Err(FenceAcquisitionError::PayloadMismatch);
117    }
118    Ok(authority)
119}
120
121/// Structural binding to an already reserved acquisition; never a fresh dispatch permit.
122///
123/// Integration admission additionally requires durable originals, exact request semantics,
124/// actual permissions/context/unit, dependency/custody qualification and proof that this
125/// reservation has never been dispatched. Reconstructing a pending request proves none
126/// of those conditions. After interruption without that proof, reconcile instead.
127#[derive(Debug)]
128pub struct FenceAcquisitionRequest<'a> {
129    plan: &'a OperationPlanRecord,
130    obligation: &'a FenceObligationRecord,
131    payload: &'a FenceAcquisitionPayload,
132    authority: AttemptAuthorityRecord,
133    mutation_attempt: u32,
134}
135impl<'a> FenceAcquisitionRequest<'a> {
136    /// Bind original exact application bytes only after the canonical journal reserves mutation.
137    /// # Errors
138    /// Rejects changed originals, target/bytes, missing mutation or an acquisition already in observation recovery.
139    pub fn new(
140        plan: &'a OperationPlanRecord,
141        obligation: &'a FenceObligationRecord,
142        journal: &AttemptJournalRecord,
143        payload: &'a FenceAcquisitionPayload,
144    ) -> Result<Self, FenceAcquisitionError> {
145        let authority = original_authority(plan, obligation, journal, payload)?;
146        let mutation_attempt = journal
147            .pending_mutation()
148            .ok_or(FenceAcquisitionError::NoPendingMutation)?;
149        let request = Self {
150            plan,
151            obligation,
152            payload,
153            authority,
154            mutation_attempt,
155        };
156        request.validate_journal(journal)?;
157        Ok(request)
158    }
159    /// Read original full context/inventory/selected unit; target routing does not narrow coverage.
160    #[must_use]
161    pub const fn plan(&self) -> &OperationPlanRecord {
162        self.plan
163    }
164    /// Read original purpose/fence/revisions under the retained requirement.
165    #[must_use]
166    pub const fn obligation(&self) -> &FenceObligationRecord {
167        self.obligation
168    }
169    /// Read exact immutable application update bytes.
170    #[must_use]
171    pub const fn payload(&self) -> &FenceAcquisitionPayload {
172        self.payload
173    }
174    /// Read full original operation identity and spending limits.
175    #[must_use]
176    pub const fn authority(&self) -> &AttemptAuthorityRecord {
177        &self.authority
178    }
179    /// Read the already consumed original mutation number.
180    #[must_use]
181    pub const fn mutation_attempt(&self) -> u32 {
182        self.mutation_attempt
183    }
184    /// Recheck current reservation before associating an acknowledgement; performs no IO or effects.
185    /// # Errors
186    /// Rejects changed authority, settled/replaced mutation or pending observation recovery.
187    pub fn validate_journal(
188        &self,
189        journal: &AttemptJournalRecord,
190    ) -> Result<(), FenceAcquisitionError> {
191        journal
192            .validate_pending_mutation(&self.authority, self.mutation_attempt)
193            .map_err(Into::into)
194    }
195}
196
197/// Passive exact update reply association, without acquisition outcome or fence custody proof.
198#[derive(Clone, Debug)]
199pub struct FenceAcquisitionAcknowledgement {
200    /// Full original authority digest, including original plan/context/operation/limits.
201    pub authority: ArtifactChecksumRecord,
202    /// Original already reserved mutation attempt, not an independently numbered call.
203    pub mutation_attempt: u32,
204    /// Qualified provider's retained exact reply association evidence; not an authenticated signature.
205    pub evidence: ArtifactChecksumRecord,
206}
207impl FenceAcquisitionAcknowledgement {
208    /// Admit a passive acknowledgement with a finite original mutation identity.
209    /// # Errors
210    /// Rejects zero or attempt numbers above the existing combined attempt ceiling.
211    pub fn new(
212        authority: ArtifactChecksumRecord,
213        mutation_attempt: u32,
214        evidence: ArtifactChecksumRecord,
215    ) -> Result<Self, FenceAcquisitionError> {
216        if mutation_attempt == 0 || mutation_attempt > MAX_OPERATION_ATTEMPTS {
217            return Err(FenceAcquisitionError::InvalidAttempt);
218        }
219        Ok(Self {
220            authority,
221            mutation_attempt,
222            evidence,
223        })
224    }
225}
226
227/// Typed payload/original reservation rejection; no error refunds or releases anything.
228#[derive(Debug, Error)]
229pub enum FenceAcquisitionError {
230    /// Receiver is not a bounded canonical principal.
231    #[error("invalid fence acquisition receiver")]
232    InvalidTarget,
233    /// This port cannot call the management canister.
234    #[error("fence acquisition requires an application receiver")]
235    ManagementReceiver,
236    /// Method exceeds the exact visible ASCII envelope bound.
237    #[error("invalid fence acquisition method")]
238    InvalidMethod,
239    /// Raw arguments exceed the finite input bound.
240    #[error("fence acquisition arguments exceed the input bound")]
241    ArgumentsTooLarge,
242    /// Journal is not the exact original acquisition authority.
243    #[error("fence acquisition original journal authority mismatch")]
244    AuthorityMismatch,
245    /// Receiver differs from the original operation target.
246    #[error("fence acquisition original target mismatch")]
247    TargetMismatch,
248    /// Method/arguments differ from the original declared payload digest.
249    #[error("fence acquisition original payload mismatch")]
250    PayloadMismatch,
251    /// Original mutation was not already reserved.
252    #[error("fence acquisition requires a pending mutation reservation")]
253    NoPendingMutation,
254    /// Current journal no longer retains this exact unresolved mutation.
255    #[error("fence acquisition pending mutation mismatch")]
256    MutationMismatch,
257    /// An observation is pending; original mutation must not be dispatched again.
258    #[error("fence acquisition observation recovery is pending")]
259    ObservationPending,
260    /// Acknowledgement attempt is outside the finite original journal range.
261    #[error("invalid fence acquisition acknowledgement attempt")]
262    InvalidAttempt,
263    /// Original obligation admission failed.
264    #[error(transparent)]
265    Obligation(#[from] FenceObligationError),
266    /// Original plan authority cannot be derived.
267    #[error(transparent)]
268    Plan(#[from] OperationPlanError),
269}
270
271#[cfg(test)]
272mod tests;
273
274impl From<crate::model::attempt_journal::MutationReservationError> for FenceAcquisitionError {
275    fn from(error: crate::model::attempt_journal::MutationReservationError) -> Self {
276        match error {
277            Reservation::NoPendingMutation => Self::NoPendingMutation,
278            Reservation::AuthorityMismatch => Self::AuthorityMismatch,
279            Reservation::MutationMismatch => Self::MutationMismatch,
280            Reservation::ObservationPending => Self::ObservationPending,
281        }
282    }
283}
284
285impl From<crate::model::application_payload::ApplicationPayloadError> for FenceAcquisitionError {
286    fn from(error: crate::model::application_payload::ApplicationPayloadError) -> Self {
287        match error {
288            Payload::ArgumentsTooLarge => Self::ArgumentsTooLarge,
289            Payload::InvalidMethod => Self::InvalidMethod,
290            Payload::InvalidPrincipal => Self::InvalidTarget,
291            Payload::ManagementPrincipal => Self::ManagementReceiver,
292        }
293    }
294}