Skip to main content

ic_backup/model/ic_snapshot_transfer_read/
mod.rs

1//! Exact originally reserved metadata/data reads; no dispatch or transfer attestation.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
6    ic_snapshot_data::{IcSnapshotDataRequest, MAX_IC_SNAPSHOT_DATA_REPLY_BYTES},
7    ic_snapshot_metadata::IcSnapshotMetadataRequest,
8    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
9};
10use std::fmt;
11use thiserror::Error;
12
13/// The existing two bounded replicated-update read payloads; no new encoder.
14#[derive(Clone, Copy, Debug)]
15pub enum IcSnapshotTransferReadPayload<'request, 'metadata> {
16    /// Read the exact original raw snapshot ID's metadata.
17    Metadata(&'request IcSnapshotMetadataRequest),
18    /// Read one metadata-bound range or known chunk hash.
19    Data(&'request IcSnapshotDataRequest<'metadata>),
20}
21
22impl IcSnapshotTransferReadPayload<'_, '_> {
23    /// Read the canonical effective routing target.
24    #[must_use]
25    pub fn target(&self) -> &str {
26        match self {
27            Self::Metadata(payload) => payload.target(),
28            Self::Data(payload) => payload.target(),
29        }
30    }
31    /// Read the management receiver; both methods use host replicated updates.
32    #[must_use]
33    pub const fn receiver(&self) -> &'static str {
34        match self {
35            Self::Metadata(payload) => payload.receiver(),
36            Self::Data(payload) => payload.receiver(),
37        }
38    }
39    /// Read the exact original method name.
40    #[must_use]
41    pub const fn method(&self) -> &'static str {
42        match self {
43            Self::Metadata(payload) => payload.method(),
44            Self::Data(payload) => payload.method(),
45        }
46    }
47    /// Read existing canonical bounded Candid bytes.
48    #[must_use]
49    pub fn arguments(&self) -> &[u8] {
50        match self {
51            Self::Metadata(payload) => payload.arguments(),
52            Self::Data(payload) => payload.arguments(),
53        }
54    }
55    /// Reuse the original nonrecursive wire digest; metadata evidence stays separate.
56    #[must_use]
57    pub fn digest(&self) -> ArtifactChecksumRecord {
58        match self {
59            Self::Metadata(payload) => payload.digest(),
60            Self::Data(payload) => payload.digest(),
61        }
62    }
63}
64
65/// Full original plan and already consumed update reservation for one transfer read.
66///
67/// A semantic read still uses replicated update ingress and the existing mutation
68/// reservation lane. It grants no fresh permission or proof of never-dispatched
69/// custody; reconstruction never permits repeating a lost read.
70#[derive(Debug)]
71pub struct IcSnapshotTransferReadRequest<'request, 'metadata> {
72    plan: &'request OperationPlanRecord,
73    payload: IcSnapshotTransferReadPayload<'request, 'metadata>,
74    authority: AttemptAuthorityRecord,
75    mutation_attempt: u32,
76}
77
78impl<'request, 'metadata> IcSnapshotTransferReadRequest<'request, 'metadata> {
79    /// Bind exact original target/wire bytes and immutable original allowance.
80    /// # Errors
81    /// Rejects another plan/operation/payload, absent or changed update reservation,
82    /// or already pending recovery. Creates no journal and spends nothing.
83    pub fn new(
84        plan: &'request OperationPlanRecord,
85        operation_sequence: u64,
86        journal: &AttemptJournalRecord,
87        payload: IcSnapshotTransferReadPayload<'request, 'metadata>,
88    ) -> Result<Self, IcSnapshotTransferReadError> {
89        let authority = plan.attempt_authority(operation_sequence)?;
90        if journal.authority() != &authority {
91            return Err(IcSnapshotTransferReadError::AuthorityMismatch);
92        }
93        if payload.target() != authority.binding().target()
94            || payload.digest().hash() != authority.binding().request()
95        {
96            return Err(IcSnapshotTransferReadError::PayloadMismatch);
97        }
98        let mutation_attempt = journal
99            .view()
100            .pending_mutation
101            .ok_or(IcSnapshotTransferReadError::NoPendingMutation)?;
102        let request = Self {
103            plan,
104            payload,
105            authority,
106            mutation_attempt,
107        };
108        request.validate_journal(journal)?;
109        Ok(request)
110    }
111    /// Read full original context, inventory, graph and allowances.
112    #[must_use]
113    pub const fn plan(&self) -> &'request OperationPlanRecord {
114        self.plan
115    }
116    /// Read the exact metadata/data payload without reconstructing its bytes.
117    #[must_use]
118    pub const fn payload(&self) -> IcSnapshotTransferReadPayload<'request, 'metadata> {
119        self.payload
120    }
121    /// Read the full original operation authority.
122    #[must_use]
123    pub const fn authority(&self) -> &AttemptAuthorityRecord {
124        &self.authority
125    }
126    /// Read the already consumed replicated-update attempt.
127    #[must_use]
128    pub const fn mutation_attempt(&self) -> u32 {
129        self.mutation_attempt
130    }
131    /// Recheck original reservation before passive response association.
132    /// # Errors
133    /// Rejects changed authority, settled/replaced update or pending recovery.
134    pub fn validate_journal(
135        &self,
136        journal: &AttemptJournalRecord,
137    ) -> Result<(), IcSnapshotTransferReadError> {
138        if journal.authority() != &self.authority {
139            return Err(IcSnapshotTransferReadError::AuthorityMismatch);
140        }
141        let view = journal.view();
142        if view.pending_mutation != Some(self.mutation_attempt) {
143            return Err(IcSnapshotTransferReadError::MutationMismatch);
144        }
145        if view.pending_observation.is_some() {
146            return Err(IcSnapshotTransferReadError::ObservationPending);
147        }
148        Ok(())
149    }
150}
151
152/// Passive provider association claims; integrations authenticate these independently.
153#[derive(Clone)]
154pub struct IcSnapshotTransferReadResponseInput {
155    /// Full original plan/context/operation/request/allowance authority digest.
156    pub authority: ArtifactChecksumRecord,
157    /// Already reserved replicated-update number.
158    pub mutation_attempt: u32,
159    /// Actual claimed network/caller/release, without credentials.
160    pub context: PlanContextRecord,
161    /// Actual claimed routing target; normalized on admission.
162    pub target: String,
163    /// Exact raw reply. Data is capped at 2 MiB; metadata decoding retains 1 MiB.
164    pub reply: Vec<u8>,
165    /// Opaque evidence binding the original read and actual response claims.
166    pub evidence: ArtifactChecksumRecord,
167}
168
169/// Bounded immutable raw reply, with no receipt, persisted authority or default.
170#[derive(Clone)]
171pub struct IcSnapshotTransferReadResponse {
172    input: IcSnapshotTransferReadResponseInput,
173}
174
175impl IcSnapshotTransferReadResponse {
176    /// Admit the existing finite raw-data ceiling and original attempt range.
177    /// # Errors
178    /// Rejects invalid attempt, excessive bytes and invalid target principals.
179    pub fn new(
180        mut input: IcSnapshotTransferReadResponseInput,
181    ) -> Result<Self, IcSnapshotTransferReadError> {
182        if input.mutation_attempt == 0 || input.mutation_attempt > MAX_OPERATION_ATTEMPTS {
183            return Err(IcSnapshotTransferReadError::InvalidAttempt);
184        }
185        if input.reply.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
186            return Err(IcSnapshotTransferReadError::ReplyTooLarge);
187        }
188        input.target = crate::model::principal::canonical_text(&input.target)
189            .ok_or(IcSnapshotTransferReadError::InvalidTarget)?;
190        Ok(Self { input })
191    }
192    /// Read exact retained claims and raw bytes; no mutable access is exposed.
193    #[must_use]
194    pub const fn input(&self) -> &IcSnapshotTransferReadResponseInput {
195        &self.input
196    }
197}
198
199impl fmt::Debug for IcSnapshotTransferReadResponse {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("IcSnapshotTransferReadResponse")
202            .field("authority", &self.input.authority)
203            .field("mutation_attempt", &self.input.mutation_attempt)
204            .field("target", &self.input.target)
205            .field("reply_bytes", &self.input.reply.len())
206            .finish_non_exhaustive()
207    }
208}
209
210/// Structural read rejection; every denial retains original accounting and evidence.
211#[derive(Debug, Error)]
212pub enum IcSnapshotTransferReadError {
213    /// Current journal differs from full original authority.
214    #[error("snapshot transfer read authority differs")]
215    AuthorityMismatch,
216    /// Original target or exact wire digest differs.
217    #[error("snapshot transfer read payload differs")]
218    PayloadMismatch,
219    /// No original replicated-update reservation is pending.
220    #[error("snapshot transfer read requires a pending original update")]
221    NoPendingMutation,
222    /// Original update was changed or settled.
223    #[error("snapshot transfer read original attempt differs")]
224    MutationMismatch,
225    /// Existing recovery observation remains pending.
226    #[error("snapshot transfer read recovery observation is pending")]
227    ObservationPending,
228    /// Attempt is outside the existing 1..=1,024 bound.
229    #[error("invalid snapshot transfer read attempt")]
230    InvalidAttempt,
231    /// Raw reply exceeds the existing 2 MiB data wire ceiling.
232    #[error("snapshot transfer read reply too large")]
233    ReplyTooLarge,
234    /// Claimed actual target is not a principal.
235    #[error("invalid snapshot transfer read target")]
236    InvalidTarget,
237    /// Original plan cannot derive this operation authority.
238    #[error(transparent)]
239    Plan(#[from] OperationPlanError),
240}