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