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