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        journal.validate_pending_mutation(
147            &self.authority,
148            self.mutation_attempt,
149            IcSnapshotTransferReadError::AuthorityMismatch,
150            IcSnapshotTransferReadError::MutationMismatch,
151            IcSnapshotTransferReadError::ObservationPending,
152        )
153    }
154}
155
156/// Bounded immutable raw reply, with no receipt, persisted authority or default.
157#[derive(Clone)]
158pub struct IcSnapshotTransferReadResponse {
159    input: IcUpdateResponseInput,
160}
161
162impl IcSnapshotTransferReadResponse {
163    /// Admit the existing finite raw-data ceiling and original attempt range.
164    /// # Errors
165    /// Rejects invalid attempt, excessive bytes and invalid target principals.
166    pub fn new(mut input: IcUpdateResponseInput) -> Result<Self, IcSnapshotTransferReadError> {
167        input.admit(
168            MAX_IC_SNAPSHOT_DATA_REPLY_BYTES,
169            IcSnapshotTransferReadError::InvalidAttempt,
170            IcSnapshotTransferReadError::ReplyTooLarge,
171            IcSnapshotTransferReadError::InvalidTarget,
172        )?;
173        Ok(Self { input })
174    }
175    /// Read exact retained claims and raw bytes; no mutable access is exposed.
176    #[must_use]
177    pub const fn input(&self) -> &IcUpdateResponseInput {
178        &self.input
179    }
180}
181
182impl fmt::Debug for IcSnapshotTransferReadResponse {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        self.input
185            .fmt_redacted(formatter, "IcSnapshotTransferReadResponse")
186    }
187}
188
189/// Structural read rejection; every denial retains original accounting and evidence.
190#[derive(Debug, Error)]
191pub enum IcSnapshotTransferReadError {
192    /// Current journal differs from full original authority.
193    #[error("snapshot transfer read authority differs")]
194    AuthorityMismatch,
195    /// Original target or exact wire digest differs.
196    #[error("snapshot transfer read payload differs")]
197    PayloadMismatch,
198    /// No original replicated-update reservation is pending.
199    #[error("snapshot transfer read requires a pending original update")]
200    NoPendingMutation,
201    /// Original update was changed or settled.
202    #[error("snapshot transfer read original attempt differs")]
203    MutationMismatch,
204    /// Existing recovery observation remains pending.
205    #[error("snapshot transfer read recovery observation is pending")]
206    ObservationPending,
207    /// Attempt is outside the existing 1..=1,024 bound.
208    #[error("invalid snapshot transfer read attempt")]
209    InvalidAttempt,
210    /// Raw reply exceeds the existing 2 MiB data wire ceiling.
211    #[error("snapshot transfer read reply too large")]
212    ReplyTooLarge,
213    /// Claimed actual target is not a principal.
214    #[error("invalid snapshot transfer read target")]
215    InvalidTarget,
216    /// Original plan cannot derive this operation authority.
217    #[error(transparent)]
218    Plan(#[from] OperationPlanError),
219}