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