Skip to main content

ic_backup/policy/inventory_settlement/
mod.rs

1//! Common exact identity and explicit new-candidate checks for inventory settlements.
2
3use super::{
4    settlement_identity::{SettlementIdentity, SettlementIdentityMismatch},
5    snapshot_inventory_delta::{SnapshotInventoryDeltaError, compare_inventories},
6};
7use crate::model::{
8    artifacts::ArtifactChecksumRecord,
9    attempt_journal::ObservationOutcomeRecord,
10    ic_observation::IcCaptureAttribution,
11    ic_snapshot_reply::{IcSnapshotInfo, IcSnapshotReply},
12    ic_snapshot_upload_observation::IcSnapshotUploadAttribution,
13};
14
15pub(crate) struct InventorySettlementIdentity<'a> {
16    pub operation: SettlementIdentity<'a>,
17    pub baseline: &'a ArtifactChecksumRecord,
18    pub inventory: &'a ArtifactChecksumRecord,
19    pub observation_evidence: &'a ArtifactChecksumRecord,
20}
21
22impl InventorySettlementIdentity<'_> {
23    pub fn compare(&self, expected: &Self) -> Result<(), InventorySettlementMismatch> {
24        self.operation.compare(&expected.operation)?;
25        if self.baseline != expected.baseline {
26            return Err(InventorySettlementMismatch::Baseline);
27        }
28        if self.inventory != expected.inventory {
29            return Err(InventorySettlementMismatch::Inventory);
30        }
31        if self.observation_evidence != expected.observation_evidence {
32            return Err(InventorySettlementMismatch::ObservationEvidence);
33        }
34        Ok(())
35    }
36}
37
38/// Passive settlement claims differ from their independently qualified original evidence.
39#[derive(Debug, thiserror::Error, Eq, PartialEq)]
40pub enum InventorySettlementMismatch {
41    /// Full original operation authority differs.
42    #[error("inventory settlement authority differs")]
43    Authority,
44    /// Original mutation or observation attempt differs.
45    #[error("inventory settlement attempts differ")]
46    Attempt,
47    /// Caller-owned qualification challenge differs.
48    #[error("inventory settlement challenge differs")]
49    Challenge,
50    /// Exact retained baseline request and raw evidence differ.
51    #[error("inventory settlement baseline differs")]
52    Baseline,
53    /// Exact current inventory request and raw evidence differ.
54    #[error("inventory settlement inventory differs")]
55    Inventory,
56    /// Retained opaque observation evidence differs.
57    #[error("inventory settlement observation evidence differs")]
58    ObservationEvidence,
59}
60
61impl From<SettlementIdentityMismatch> for InventorySettlementMismatch {
62    fn from(value: SettlementIdentityMismatch) -> Self {
63        match value {
64            SettlementIdentityMismatch::Authority => Self::Authority,
65            SettlementIdentityMismatch::Attempt => Self::Attempt,
66            SettlementIdentityMismatch::Challenge => Self::Challenge,
67        }
68    }
69}
70
71pub(crate) fn validate_pair<'a, E>(
72    target: &str,
73    baseline: &IcSnapshotReply<'_>,
74    inventory: &'a IcSnapshotReply<'_>,
75    claimed: &InventorySettlementIdentity<'_>,
76    operation: &SettlementIdentity<'_>,
77    observation_evidence: &ArtifactChecksumRecord,
78) -> Result<Vec<&'a IcSnapshotInfo>, E>
79where
80    E: From<SnapshotInventoryDeltaError> + From<InventorySettlementMismatch>,
81{
82    let candidates = compare_inventories(target, baseline, inventory)?;
83    claimed.compare(&InventorySettlementIdentity {
84        operation: SettlementIdentity {
85            authority: operation.authority,
86            mutation_attempt: operation.mutation_attempt,
87            observation_attempt: operation.observation_attempt,
88            challenge: operation.challenge,
89        },
90        baseline: &baseline.digest(),
91        inventory: &inventory.digest(),
92        observation_evidence,
93    })?;
94    Ok(candidates)
95}
96
97#[derive(Clone, Copy)]
98pub(crate) enum InventoryAttribution<'a> {
99    Applied(&'a [u8]),
100    NotApplied,
101    Unresolved,
102}
103
104impl<'a> From<&'a IcCaptureAttribution> for InventoryAttribution<'a> {
105    fn from(value: &'a IcCaptureAttribution) -> Self {
106        match value {
107            IcCaptureAttribution::Applied { snapshot_id, .. } => Self::Applied(snapshot_id),
108            IcCaptureAttribution::NotApplied { .. } => Self::NotApplied,
109            IcCaptureAttribution::Unresolved { .. } => Self::Unresolved,
110        }
111    }
112}
113
114impl<'a> From<&'a IcSnapshotUploadAttribution> for InventoryAttribution<'a> {
115    fn from(value: &'a IcSnapshotUploadAttribution) -> Self {
116        match value {
117            IcSnapshotUploadAttribution::Applied { snapshot_id, .. } => Self::Applied(snapshot_id),
118            IcSnapshotUploadAttribution::NotApplied { .. } => Self::NotApplied,
119            IcSnapshotUploadAttribution::Unresolved { .. } => Self::Unresolved,
120        }
121    }
122}
123
124// A snapshot index exists only for an explicitly attributed Applied claim.
125#[derive(Debug)]
126pub(crate) enum InventorySettlementOutcome {
127    Applied(usize),
128    NotApplied,
129    Uncertain,
130}
131
132impl InventorySettlementOutcome {
133    pub const fn outcome(&self) -> ObservationOutcomeRecord {
134        match self {
135            Self::Applied(_) => ObservationOutcomeRecord::Applied,
136            Self::NotApplied => ObservationOutcomeRecord::NotApplied,
137            Self::Uncertain => ObservationOutcomeRecord::Uncertain,
138        }
139    }
140
141    pub const fn snapshot_index(&self) -> Option<usize> {
142        match self {
143            Self::Applied(index) => Some(*index),
144            Self::NotApplied | Self::Uncertain => None,
145        }
146    }
147}
148
149pub(crate) fn admitted_outcome(
150    inventory: &IcSnapshotReply<'_>,
151    candidates: &[&IcSnapshotInfo],
152    attribution: InventoryAttribution<'_>,
153) -> Result<InventorySettlementOutcome, ()> {
154    match attribution {
155        InventoryAttribution::Applied(snapshot_id) => {
156            new_candidate_index(inventory, candidates, snapshot_id)
157                .map(InventorySettlementOutcome::Applied)
158                .ok_or(())
159        }
160        InventoryAttribution::NotApplied => Ok(InventorySettlementOutcome::NotApplied),
161        InventoryAttribution::Unresolved => Ok(InventorySettlementOutcome::Uncertain),
162    }
163}
164
165pub(crate) fn new_candidate_index(
166    inventory: &IcSnapshotReply<'_>,
167    candidates: &[&IcSnapshotInfo],
168    snapshot_id: &[u8],
169) -> Option<usize> {
170    if !candidates
171        .iter()
172        .any(|snapshot| snapshot.id() == snapshot_id)
173    {
174        return None;
175    }
176    inventory
177        .snapshots()
178        .binary_search_by(|snapshot| snapshot.id().cmp(snapshot_id))
179        .ok()
180}