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