use crate::model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::OperationBindingRecord,
control_authority::ControllerSet,
ic_request::{IcManagementMethodRecord, IcManagementRequestRecord, IcRequestError},
operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
};
use thiserror::Error;
pub const MAX_SNAPSHOT_VIEWERS: usize = 10;
pub const MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS: u32 = 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SnapshotViewerSet {
principals: Vec<String>,
}
impl SnapshotViewerSet {
pub fn new(mut principals: Vec<String>) -> Result<Self, SnapshotReadObservationError> {
if principals.len() > MAX_SNAPSHOT_VIEWERS {
return Err(SnapshotReadObservationError::TooManyViewers);
}
for principal in &mut principals {
*principal = super::principal::canonical_text(principal)
.ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
}
principals.sort();
if principals.windows(2).any(|pair| pair[0] == pair[1]) {
return Err(SnapshotReadObservationError::DuplicateViewer);
}
Ok(Self { principals })
}
#[must_use]
pub fn principals(&self) -> &[String] {
&self.principals
}
#[must_use]
pub fn contains_caller(&self, binding: &OperationBindingRecord) -> bool {
self.principals
.binary_search_by(|principal| principal.as_str().cmp(binding.caller()))
.is_ok()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SnapshotVisibility {
Controllers,
Public,
AllowedViewers(SnapshotViewerSet),
}
#[derive(Clone, Debug)]
pub struct SnapshotReadRequest<'a> {
binding: OperationBindingRecord,
wire: &'a IcManagementRequestRecord,
challenge: ArtifactChecksumRecord,
max_remote_observations: u32,
}
impl<'a> SnapshotReadRequest<'a> {
pub fn new(
plan: &OperationPlanRecord,
sequence: u64,
wire: &'a IcManagementRequestRecord,
observation: &ArtifactChecksumRecord,
challenge: ArtifactChecksumRecord,
max_remote_observations: u32,
) -> Result<Self, SnapshotReadRequestError> {
if max_remote_observations > MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS {
return Err(SnapshotReadRequestError::ObservationLimitTooLarge);
}
if wire.method() != IcManagementMethodRecord::ListCanisterSnapshots {
return Err(SnapshotReadRequestError::UnsupportedMethod);
}
let binding = plan.attempt_authority(sequence)?.binding().clone();
wire.validate_observation_binding(&binding, observation)?;
Ok(Self {
binding,
wire,
challenge,
max_remote_observations,
})
}
#[must_use]
pub const fn binding(&self) -> &OperationBindingRecord {
&self.binding
}
#[must_use]
pub const fn wire(&self) -> &IcManagementRequestRecord {
self.wire
}
#[must_use]
pub const fn challenge(&self) -> &ArtifactChecksumRecord {
&self.challenge
}
#[must_use]
pub const fn max_remote_observations(&self) -> u32 {
self.max_remote_observations
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/snapshot-read/v1\0".to_vec();
bytes.extend_from_slice(self.binding.intent().as_bytes());
bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
bytes.extend_from_slice(self.wire.digest().hash().as_bytes());
bytes.extend_from_slice(self.challenge.hash().as_bytes());
bytes.extend_from_slice(&self.max_remote_observations.to_be_bytes());
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[derive(Clone, Debug)]
pub struct SnapshotReadObservationInput {
pub request: ArtifactChecksumRecord,
pub context: PlanContextRecord,
pub target: String,
pub visibility: SnapshotVisibility,
pub controllers: Option<ControllerSet>,
pub evidence: ArtifactChecksumRecord,
pub remote_observations: u32,
}
#[derive(Clone, Debug)]
pub struct SnapshotReadObservation {
input: SnapshotReadObservationInput,
}
impl SnapshotReadObservation {
pub fn new(
mut input: SnapshotReadObservationInput,
) -> Result<Self, SnapshotReadObservationError> {
input.target = super::principal::canonical_text(&input.target)
.ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
Ok(Self { input })
}
#[must_use]
pub const fn request(&self) -> &ArtifactChecksumRecord {
&self.input.request
}
#[must_use]
pub const fn context(&self) -> &PlanContextRecord {
&self.input.context
}
#[must_use]
pub fn target(&self) -> &str {
&self.input.target
}
#[must_use]
pub const fn visibility(&self) -> &SnapshotVisibility {
&self.input.visibility
}
#[must_use]
pub const fn controllers(&self) -> Option<&ControllerSet> {
self.input.controllers.as_ref()
}
#[must_use]
pub const fn evidence(&self) -> &ArtifactChecksumRecord {
&self.input.evidence
}
#[must_use]
pub const fn remote_observations(&self) -> u32 {
self.input.remote_observations
}
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum SnapshotReadObservationError {
#[error("invalid snapshot read principal")]
InvalidPrincipal,
#[error("snapshot viewer set exceeds {MAX_SNAPSHOT_VIEWERS}")]
TooManyViewers,
#[error("duplicate snapshot viewer")]
DuplicateViewer,
}
#[derive(Debug, Error)]
pub enum SnapshotReadRequestError {
#[error("snapshot read observation ceiling exceeds {MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS}")]
ObservationLimitTooLarge,
#[error("snapshot read contract requires list_canister_snapshots")]
UnsupportedMethod,
#[error(transparent)]
Plan(#[from] OperationPlanError),
#[error(transparent)]
Payload(#[from] IcRequestError),
}
#[cfg(test)]
mod tests;