ic_backup/model/snapshot_read/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 attempt_journal::OperationBindingRecord,
6 control_authority::ControllerSet,
7 ic_request::{IcManagementMethodRecord, IcManagementRequestRecord, IcRequestError},
8 operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
9};
10use thiserror::Error;
11
12pub const MAX_SNAPSHOT_VIEWERS: usize = 10;
14pub const MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS: u32 = 1024;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct SnapshotViewerSet {
20 principals: Vec<String>,
21}
22impl SnapshotViewerSet {
23 pub fn new(mut principals: Vec<String>) -> Result<Self, SnapshotReadObservationError> {
27 if principals.len() > MAX_SNAPSHOT_VIEWERS {
28 return Err(SnapshotReadObservationError::TooManyViewers);
29 }
30 for principal in &mut principals {
31 *principal = super::principal::canonical_text(principal)
32 .ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
33 }
34 principals.sort();
35 if principals.windows(2).any(|pair| pair[0] == pair[1]) {
36 return Err(SnapshotReadObservationError::DuplicateViewer);
37 }
38 Ok(Self { principals })
39 }
40 #[must_use]
42 pub fn principals(&self) -> &[String] {
43 &self.principals
44 }
45 #[must_use]
47 pub fn contains_caller(&self, binding: &OperationBindingRecord) -> bool {
48 self.principals
49 .binary_search_by(|principal| principal.as_str().cmp(binding.caller()))
50 .is_ok()
51 }
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
59pub enum SnapshotVisibility {
60 Controllers,
62 Public,
64 AllowedViewers(SnapshotViewerSet),
66}
67
68#[derive(Clone, Debug)]
76pub struct SnapshotReadRequest<'a> {
77 binding: OperationBindingRecord,
78 wire: &'a IcManagementRequestRecord,
79 challenge: ArtifactChecksumRecord,
80 max_remote_observations: u32,
81}
82impl<'a> SnapshotReadRequest<'a> {
83 pub fn new(
87 plan: &OperationPlanRecord,
88 sequence: u64,
89 wire: &'a IcManagementRequestRecord,
90 observation: &ArtifactChecksumRecord,
91 challenge: ArtifactChecksumRecord,
92 max_remote_observations: u32,
93 ) -> Result<Self, SnapshotReadRequestError> {
94 if max_remote_observations > MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS {
95 return Err(SnapshotReadRequestError::ObservationLimitTooLarge);
96 }
97 if wire.method() != IcManagementMethodRecord::ListCanisterSnapshots {
98 return Err(SnapshotReadRequestError::UnsupportedMethod);
99 }
100 let binding = plan.attempt_authority(sequence)?.binding().clone();
101 wire.validate_observation_binding(&binding, observation)?;
102 Ok(Self {
103 binding,
104 wire,
105 challenge,
106 max_remote_observations,
107 })
108 }
109 #[must_use]
111 pub const fn binding(&self) -> &OperationBindingRecord {
112 &self.binding
113 }
114 #[must_use]
116 pub const fn wire(&self) -> &IcManagementRequestRecord {
117 self.wire
118 }
119 #[must_use]
121 pub const fn challenge(&self) -> &ArtifactChecksumRecord {
122 &self.challenge
123 }
124 #[must_use]
126 pub const fn max_remote_observations(&self) -> u32 {
127 self.max_remote_observations
128 }
129 #[must_use]
135 pub fn digest(&self) -> ArtifactChecksumRecord {
136 let mut bytes = b"ic-backup/snapshot-read/v1\0".to_vec();
137 bytes.extend_from_slice(self.binding.intent().as_bytes());
138 bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
139 bytes.extend_from_slice(self.wire.digest().hash().as_bytes());
140 bytes.extend_from_slice(self.challenge.hash().as_bytes());
141 bytes.extend_from_slice(&self.max_remote_observations.to_be_bytes());
142 ArtifactChecksumRecord::from_bytes(&bytes)
143 }
144}
145
146#[derive(Clone, Debug)]
148pub struct SnapshotReadObservationInput {
149 pub request: ArtifactChecksumRecord,
151 pub context: PlanContextRecord,
153 pub target: String,
155 pub visibility: SnapshotVisibility,
157 pub controllers: Option<ControllerSet>,
163 pub evidence: ArtifactChecksumRecord,
165 pub remote_observations: u32,
167}
168#[derive(Clone, Debug)]
170pub struct SnapshotReadObservation {
171 input: SnapshotReadObservationInput,
172}
173impl SnapshotReadObservation {
174 pub fn new(
178 mut input: SnapshotReadObservationInput,
179 ) -> Result<Self, SnapshotReadObservationError> {
180 input.target = super::principal::canonical_text(&input.target)
181 .ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
182 Ok(Self { input })
183 }
184 #[must_use]
186 pub const fn request(&self) -> &ArtifactChecksumRecord {
187 &self.input.request
188 }
189 #[must_use]
191 pub const fn context(&self) -> &PlanContextRecord {
192 &self.input.context
193 }
194 #[must_use]
196 pub fn target(&self) -> &str {
197 &self.input.target
198 }
199 #[must_use]
201 pub const fn visibility(&self) -> &SnapshotVisibility {
202 &self.input.visibility
203 }
204 #[must_use]
206 pub const fn controllers(&self) -> Option<&ControllerSet> {
207 self.input.controllers.as_ref()
208 }
209 #[must_use]
211 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
212 &self.input.evidence
213 }
214 #[must_use]
216 pub const fn remote_observations(&self) -> u32 {
217 self.input.remote_observations
218 }
219}
220
221#[derive(Debug, Eq, Error, PartialEq)]
223pub enum SnapshotReadObservationError {
224 #[error("invalid snapshot read principal")]
226 InvalidPrincipal,
227 #[error("snapshot viewer set exceeds {MAX_SNAPSHOT_VIEWERS}")]
229 TooManyViewers,
230 #[error("duplicate snapshot viewer")]
232 DuplicateViewer,
233}
234#[derive(Debug, Error)]
236pub enum SnapshotReadRequestError {
237 #[error("snapshot read observation ceiling exceeds {MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS}")]
239 ObservationLimitTooLarge,
240 #[error("snapshot read contract requires list_canister_snapshots")]
242 UnsupportedMethod,
243 #[error(transparent)]
245 Plan(#[from] OperationPlanError),
246 #[error(transparent)]
248 Payload(#[from] IcRequestError),
249}
250
251#[cfg(test)]
252mod tests;