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    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 current = journal.view();
92        let request = Self {
93            plan,
94            mutation,
95            payload,
96            authority,
97            mutation_attempt: current
98                .pending_mutation
99                .ok_or(IcObservationRequestError::NoPendingMutation)?,
100            observation_attempt: current
101                .pending_observation
102                .ok_or(IcObservationRequestError::NoPendingObservation)?,
103            original_chunk_checksum: chunk_checksum,
104        };
105        request.validate_journal(journal)?;
106        Ok(request)
107    }
108
109    /// Read the retained complete original plan and immutable allowances.
110    #[must_use]
111    pub const fn plan(&self) -> &OperationPlanRecord {
112        self.plan
113    }
114    /// Read exact original source-bound upload bytes, without a dispatch permit.
115    #[must_use]
116    pub const fn mutation(&self) -> &'request IcSnapshotUploadRequest<'source> {
117        self.mutation
118    }
119    /// Read exact original destination metadata/read request, without fresh access.
120    #[must_use]
121    pub const fn payload(&self) -> &'request IcSnapshotDataRequest<'metadata> {
122        self.payload
123    }
124    /// Read original operation authority and assigned limits.
125    #[must_use]
126    pub const fn authority(&self) -> &AttemptAuthorityRecord {
127        &self.authority
128    }
129    /// Read the original pending data-upload attempt.
130    #[must_use]
131    pub const fn mutation_attempt(&self) -> u32 {
132        self.mutation_attempt
133    }
134    /// Read the already consumed exact data-read observation attempt.
135    #[must_use]
136    pub const fn observation_attempt(&self) -> u32 {
137        self.observation_attempt
138    }
139    /// Read original encoded chunk identity; it establishes no write attribution.
140    #[must_use]
141    pub const fn original_chunk_checksum(&self) -> &ArtifactChecksumRecord {
142        self.original_chunk_checksum
143    }
144    /// Recheck original current authority, pending IDs and exact read digest.
145    /// # Errors
146    /// Rejects changed/settled reservations and different original read bytes.
147    pub fn validate_journal(
148        &self,
149        journal: &AttemptJournalRecord,
150    ) -> Result<(), IcObservationRequestError> {
151        ObservationReservation {
152            authority: &self.authority,
153            mutation_attempt: self.mutation_attempt,
154            observation_attempt: self.observation_attempt,
155            request: self.payload.digest(),
156        }
157        .validate(journal)
158    }
159}
160
161/// Exact original data/readback declaration denial, with no outcome transition.
162#[derive(Debug, Error)]
163pub enum IcSnapshotUploadDataObservationError {
164    /// Metadata allocation is outside this boundary.
165    #[error("data upload observation requires original data intent")]
166    DataUploadRequired,
167    /// Destination target, raw ID or exact original extent/hash differs.
168    #[error("data upload observation readback differs")]
169    ReadbackMismatch,
170    /// Destination source is unknown/not uploaded, or declared region sizes differ.
171    #[error("data upload observation destination metadata differs")]
172    DestinationMetadataMismatch,
173    /// Canonical original upload authority/source admission rejected.
174    #[error(transparent)]
175    Upload(#[from] IcSnapshotUploadAttemptError),
176    /// Canonical current original observation reservation rejected.
177    #[error(transparent)]
178    Observation(#[from] IcObservationRequestError),
179}
180
181/// Passive destination-data claims using the existing 2 MiB data-reply ceiling.
182///
183/// The existing status/list response keeps its 1 MiB ceiling. This owner reuses
184/// canonical attempt/principal admission and redaction without widening that API.
185#[derive(Clone)]
186pub struct IcSnapshotUploadDataObservationResponse {
187    input: IcObservationResponseInput,
188}
189
190impl IcSnapshotUploadDataObservationResponse {
191    /// Admit bounded raw bytes and passive chronological/canonical claims.
192    /// # Errors
193    /// Rejects invalid attempts/principals and replies over the existing data bound.
194    pub fn new(mut input: IcObservationResponseInput) -> Result<Self, IcObservationResponseError> {
195        validate_response_input(&mut input, MAX_IC_SNAPSHOT_DATA_REPLY_BYTES)?;
196        Ok(Self { input })
197    }
198    /// Read immutable existing claim fields and exact original raw data reply.
199    #[must_use]
200    pub const fn input(&self) -> &IcObservationResponseInput {
201        &self.input
202    }
203}
204impl fmt::Debug for IcSnapshotUploadDataObservationResponse {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        fmt_response_input(
207            &self.input,
208            formatter,
209            "IcSnapshotUploadDataObservationResponse",
210        )
211    }
212}
213
214#[cfg(test)]
215mod tests;