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