Skip to main content

ic_backup/model/ic_snapshot_upload_data_observation/
mod.rs

1//! Exact already reserved destination-data observations of unresolved uploads.
2
3use crate::model::attempt_journal::{ObservationReservation, ObservationReservationError};
4use crate::model::{
5    artifacts::ArtifactChecksumRecord,
6    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord},
7    ic_observation::{
8        IcObservationResponseError, IcObservationResponseInput, fmt_response_input,
9        validate_response_input,
10    },
11    ic_snapshot_data::{IcSnapshotDataRequest, MAX_IC_SNAPSHOT_DATA_REPLY_BYTES},
12    ic_snapshot_upload::{
13        IcSnapshotUploadAttemptError, IcSnapshotUploadKind, IcSnapshotUploadRequest,
14        original_authority,
15    },
16    operation_plan::OperationPlanRecord,
17};
18use std::fmt;
19use thiserror::Error;
20
21mod settlement;
22pub use settlement::IcSnapshotUploadDataSettlement;
23
24/// Original data-upload intent and its independently spent exact readback request.
25///
26/// Retain destination metadata and original read bytes before reservation. Their
27/// authenticity, allocation attribution, fresh access, chronology and proof of no
28/// prior observation dispatch remain integration-owned. Reconstruction is no retry.
29#[derive(Debug)]
30pub struct IcSnapshotUploadDataObservationRequest<'request, 'source, 'metadata> {
31    plan: &'request OperationPlanRecord,
32    mutation: &'request IcSnapshotUploadRequest<'source>,
33    payload: &'request IcSnapshotDataRequest<'metadata>,
34    reservation: ObservationReservation,
35    original_chunk_checksum: &'request ArtifactChecksumRecord,
36}
37
38impl<'request, 'source, 'metadata>
39    IcSnapshotUploadDataObservationRequest<'request, 'source, 'metadata>
40{
41    /// Bind one original data upload to a reserved read of the same destination/extent.
42    ///
43    /// Metadata allocation, other IDs, targets, regions, offsets, lengths or chunk
44    /// hashes reject. Destination metadata must declare uploaded source and original
45    /// region sizes. This neither authenticates that metadata nor spends.
46    /// # Errors
47    /// Rejects changed originals, mismatching readback and missing reservations.
48    pub fn new(
49        plan: &'request OperationPlanRecord,
50        operation_sequence: u64,
51        journal: &AttemptJournalRecord,
52        mutation: &'request IcSnapshotUploadRequest<'source>,
53        payload: &'request IcSnapshotDataRequest<'metadata>,
54    ) -> Result<Self, IcSnapshotUploadDataObservationError> {
55        let IcSnapshotUploadKind::Data {
56            snapshot_id,
57            source_kind,
58            chunk_checksum,
59            ..
60        } = mutation.kind()
61        else {
62            return Err(IcSnapshotUploadDataObservationError::DataUploadRequired);
63        };
64        let authority = original_authority(plan, operation_sequence, journal, mutation)?;
65        if payload.target() != mutation.target()
66            || payload.snapshot_id() != snapshot_id
67            || !payload.matches_kind(source_kind)
68        {
69            return Err(IcSnapshotUploadDataObservationError::ReadbackMismatch);
70        }
71        let actual = payload.metadata().metadata();
72        if !matches!(
73            actual.source,
74            Some(ic_platform::management_canister_types::SnapshotSource::MetadataUpload(_))
75        ) || payload.metadata().region_sizes() != mutation.source().region_sizes()
76        {
77            return Err(IcSnapshotUploadDataObservationError::DestinationMetadataMismatch);
78        }
79        let reservation = ObservationReservation::new(authority, journal, payload.digest())?;
80        let request = Self {
81            plan,
82            mutation,
83            payload,
84            reservation,
85            original_chunk_checksum: chunk_checksum,
86        };
87        Ok(request)
88    }
89
90    /// Read the retained complete original plan and immutable allowances.
91    #[must_use]
92    pub const fn plan(&self) -> &OperationPlanRecord {
93        self.plan
94    }
95    /// Read exact original source-bound upload bytes, without a dispatch permit.
96    #[must_use]
97    pub const fn mutation(&self) -> &'request IcSnapshotUploadRequest<'source> {
98        self.mutation
99    }
100    /// Read exact original destination metadata/read request, without fresh access.
101    #[must_use]
102    pub const fn payload(&self) -> &'request IcSnapshotDataRequest<'metadata> {
103        self.payload
104    }
105    /// Read original operation authority and assigned limits.
106    #[must_use]
107    pub const fn authority(&self) -> &AttemptAuthorityRecord {
108        &self.reservation.authority
109    }
110    /// Read the original pending data-upload attempt.
111    #[must_use]
112    pub const fn mutation_attempt(&self) -> u32 {
113        self.reservation.mutation_attempt
114    }
115    /// Read the already consumed exact data-read observation attempt.
116    #[must_use]
117    pub const fn observation_attempt(&self) -> u32 {
118        self.reservation.observation_attempt
119    }
120    /// Read original encoded chunk identity; it establishes no write attribution.
121    #[must_use]
122    pub const fn original_chunk_checksum(&self) -> &ArtifactChecksumRecord {
123        self.original_chunk_checksum
124    }
125    /// Recheck original current authority, pending IDs and exact read digest.
126    /// # Errors
127    /// Rejects changed/settled reservations and different original read bytes.
128    pub fn validate_journal(
129        &self,
130        journal: &AttemptJournalRecord,
131    ) -> Result<(), ObservationReservationError> {
132        self.reservation.validate(journal)
133    }
134}
135
136/// Exact original data/readback declaration denial, with no outcome transition.
137#[derive(Debug, Error)]
138pub enum IcSnapshotUploadDataObservationError {
139    /// Metadata allocation is outside this boundary.
140    #[error("data upload observation requires original data intent")]
141    DataUploadRequired,
142    /// Destination target, raw ID or exact original extent/hash differs.
143    #[error("data upload observation readback differs")]
144    ReadbackMismatch,
145    /// Destination source is unknown/not uploaded, or declared region sizes differ.
146    #[error("data upload observation destination metadata differs")]
147    DestinationMetadataMismatch,
148    /// Canonical original upload authority/source admission rejected.
149    #[error(transparent)]
150    Upload(#[from] IcSnapshotUploadAttemptError),
151    /// Canonical current original observation reservation rejected.
152    #[error(transparent)]
153    Reservation(#[from] ObservationReservationError),
154}
155
156/// Passive destination-data claims using the existing 2 MiB data-reply ceiling.
157///
158/// The existing status/list response keeps its 1 MiB ceiling. This owner reuses
159/// canonical attempt/principal admission and redaction without widening that API.
160#[derive(Clone)]
161pub struct IcSnapshotUploadDataObservationResponse {
162    input: IcObservationResponseInput,
163}
164
165impl IcSnapshotUploadDataObservationResponse {
166    /// Admit bounded raw bytes and passive chronological/canonical claims.
167    /// # Errors
168    /// Rejects invalid attempts/principals and replies over the existing data bound.
169    pub fn new(mut input: IcObservationResponseInput) -> Result<Self, IcObservationResponseError> {
170        validate_response_input(&mut input, MAX_IC_SNAPSHOT_DATA_REPLY_BYTES)?;
171        Ok(Self { input })
172    }
173    /// Read immutable existing claim fields and exact original raw data reply.
174    #[must_use]
175    pub const fn input(&self) -> &IcObservationResponseInput {
176        &self.input
177    }
178}
179impl fmt::Debug for IcSnapshotUploadDataObservationResponse {
180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        fmt_response_input(
182            &self.input,
183            formatter,
184            "IcSnapshotUploadDataObservationResponse",
185        )
186    }
187}
188
189#[cfg(test)]
190mod tests;