Skip to main content

ic_backup/policy/ic_snapshot_upload_observation/settlement/
mod.rs

1//! Pure original metadata-allocation settlement admission.
2use super::validate_response;
3use crate::{
4    model::{
5        artifacts::ArtifactChecksumRecord,
6        attempt_journal::{AttemptJournalRecord, ObservationOutcomeRecord},
7        ic_observation::IcObservationResponse,
8        ic_snapshot_reply::{IcSnapshotInfo, IcSnapshotReply},
9        ic_snapshot_upload::IcSnapshotUploadError,
10        ic_snapshot_upload_observation::{
11            IcSnapshotUploadAttribution, IcSnapshotUploadObservationRequest,
12            IcSnapshotUploadSettlement,
13        },
14    },
15    policy::{
16        ic_observation::{
17            IcObservationAssociationError, IcObservationReplyView, IcObservationResponseView,
18        },
19        inventory_settlement::{
20            InventorySettlementIdentity, InventorySettlementMismatch, new_candidate_index,
21        },
22        snapshot_inventory_delta::{SnapshotInventoryDeltaError, compare_inventories},
23    },
24};
25use thiserror::Error;
26
27/// Exact local evidence and passive attribution; never an authenticated receipt.
28#[derive(Debug)]
29pub struct IcSnapshotUploadSettlementView<'a> {
30    observation: IcObservationResponseView<'a>,
31    baseline: &'a IcSnapshotReply<'a>,
32    settlement: &'a IcSnapshotUploadSettlement,
33    allocated_snapshot: Option<usize>,
34    outcome: ObservationOutcomeRecord,
35}
36impl<'a> IcSnapshotUploadSettlementView<'a> {
37    /// Read the passive claim; only a qualified integration may record it.
38    #[must_use]
39    pub const fn outcome(&self) -> ObservationOutcomeRecord {
40        self.outcome
41    }
42    /// Read the original independently retained baseline.
43    #[must_use]
44    pub const fn baseline(&self) -> &'a IcSnapshotReply<'a> {
45        self.baseline
46    }
47    /// Read the exact bounded current list association.
48    #[must_use]
49    pub const fn observation(&self) -> &IcObservationResponseView<'a> {
50        &self.observation
51    }
52    /// Read the complete retained integration claims.
53    #[must_use]
54    pub const fn settlement(&self) -> &'a IcSnapshotUploadSettlement {
55        self.settlement
56    }
57    /// Read only the explicitly attributed new descriptor, with no singleton inference.
58    #[must_use]
59    pub fn allocated_snapshot(&self) -> Option<&IcSnapshotInfo> {
60        match self.observation.reply() {
61            IcObservationReplyView::Inventory(reply) => self
62                .allocated_snapshot
63                .map(|index| &reply.snapshots()[index]),
64            IcObservationReplyView::Status(_) => None,
65        }
66    }
67}
68
69/// Admit separately qualified allocation claims against exact original and current lists.
70///
71/// Every baseline ID and its metadata must remain unchanged. Applied requires an
72/// explicitly attributed new ID distinct from the retained source; several candidates
73/// may exist. Zero/one/many candidates imply no outcome. Negative evidence must exclude
74/// transient allocation/deletion. Unresolved requires a settled authenticated list,
75/// never a lost response. Integrations own original baseline chronology, actual
76/// authentication, freshness, attribution and stable custody; matching digests prove
77/// none of these. No calls, IO, receipts, retries, refunds or release admission occur.
78/// # Errors
79/// Rejects changed reservations/claims/wire, baseline drift, settlement identity drift
80/// and an Applied ID outside the exact new candidate set or original destination bounds.
81pub fn validate_settlement<'a>(
82    request: &IcSnapshotUploadObservationRequest<'a, '_>,
83    journal: &AttemptJournalRecord,
84    baseline: &'a IcSnapshotReply<'a>,
85    response: &'a IcObservationResponse,
86    challenge: &ArtifactChecksumRecord,
87    settlement: &'a IcSnapshotUploadSettlement,
88) -> Result<IcSnapshotUploadSettlementView<'a>, IcSnapshotUploadSettlementError> {
89    let observation = validate_response(request, journal, response)?;
90    let IcObservationReplyView::Inventory(inventory) = observation.reply() else {
91        return Err(IcSnapshotUploadSettlementError::WrongInventory);
92    };
93    let candidates = compare_inventories(request.payload().target(), baseline, inventory)?;
94    let authority = request.authority().digest();
95    let baseline_digest = baseline.digest();
96    let inventory_digest = inventory.digest();
97    InventorySettlementIdentity {
98        authority: &settlement.authority,
99        mutation_attempt: settlement.mutation_attempt,
100        observation_attempt: settlement.observation_attempt,
101        challenge: &settlement.challenge,
102        baseline: &settlement.baseline,
103        inventory: &settlement.inventory,
104        observation_evidence: &settlement.observation_evidence,
105    }
106    .compare(&InventorySettlementIdentity {
107        authority: &authority,
108        mutation_attempt: request.mutation_attempt(),
109        observation_attempt: request.observation_attempt(),
110        challenge,
111        baseline: &baseline_digest,
112        inventory: &inventory_digest,
113        observation_evidence: &response.input().evidence,
114    })?;
115    let (outcome, allocated_snapshot) = match &settlement.attribution {
116        IcSnapshotUploadAttribution::Applied { snapshot_id, .. } => {
117            request.mutation().validate_data_destination(snapshot_id)?;
118            let index = new_candidate_index(inventory, &candidates, snapshot_id)
119                .ok_or(IcSnapshotUploadSettlementError::NotNewCandidate)?;
120            (ObservationOutcomeRecord::Applied, Some(index))
121        }
122        IcSnapshotUploadAttribution::NotApplied { .. } => {
123            (ObservationOutcomeRecord::NotApplied, None)
124        }
125        IcSnapshotUploadAttribution::Unresolved { .. } => {
126            (ObservationOutcomeRecord::Uncertain, None)
127        }
128    };
129    Ok(IcSnapshotUploadSettlementView {
130        observation,
131        baseline,
132        settlement,
133        allocated_snapshot,
134        outcome,
135    })
136}
137
138/// Local admission denial; original spending and obligations remain retained.
139#[derive(Debug, Error)]
140pub enum IcSnapshotUploadSettlementError {
141    /// Existing exact reservation, claim or bounded wire admission failed.
142    #[error(transparent)]
143    Observation(#[from] IcObservationAssociationError),
144    /// Existing canonical closed-baseline comparison failed.
145    #[error(transparent)]
146    Baseline(#[from] SnapshotInventoryDeltaError),
147    /// Existing raw destination bounds or source-ID exclusion failed.
148    #[error(transparent)]
149    Destination(#[from] IcSnapshotUploadError),
150    /// A status projection cannot substitute for the original list.
151    #[error("metadata settlement requires snapshot inventory")]
152    WrongInventory,
153    /// Full original authority differs.
154    #[error("metadata settlement authority differs")]
155    AuthorityMismatch,
156    /// Original attempt identities differ.
157    #[error("metadata settlement attempts differ")]
158    AttemptMismatch,
159    /// Current challenge differs.
160    #[error("metadata settlement challenge differs")]
161    ChallengeMismatch,
162    /// Exact original baseline evidence differs.
163    #[error("metadata settlement baseline differs")]
164    BaselineMismatch,
165    /// Exact current inventory evidence differs.
166    #[error("metadata settlement inventory differs")]
167    InventoryMismatch,
168    /// Opaque observation evidence differs.
169    #[error("metadata settlement observation evidence differs")]
170    ObservationEvidenceMismatch,
171    /// The attributed ID is not a new candidate in the exact current inventory.
172    #[error("metadata settlement ID is not a new candidate")]
173    NotNewCandidate,
174}
175
176impl From<InventorySettlementMismatch> for IcSnapshotUploadSettlementError {
177    fn from(value: InventorySettlementMismatch) -> Self {
178        match value {
179            InventorySettlementMismatch::Authority => Self::AuthorityMismatch,
180            InventorySettlementMismatch::Attempt => Self::AttemptMismatch,
181            InventorySettlementMismatch::Challenge => Self::ChallengeMismatch,
182            InventorySettlementMismatch::Baseline => Self::BaselineMismatch,
183            InventorySettlementMismatch::Inventory => Self::InventoryMismatch,
184            InventorySettlementMismatch::ObservationEvidence => Self::ObservationEvidenceMismatch,
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests;