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