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