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