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