canic_host/fleet_ensure/ops/release/reservation/
mod.rs1#[cfg(test)]
7pub(in crate::fleet_ensure) mod tests;
8
9use crate::fleet_ensure::{
10 model::{
11 FleetEnsureCompletion, FleetEnsureJournalRecord,
12 release::{FleetReleaseAuthority, FleetReleaseExecutionRecord, FleetReleaseReviewRecord},
13 },
14 ops::{EnsurePaths, EnsureStateError, read_journal, write_journal},
15 policy::release::{FleetReleaseError, reserve_observation_calls},
16 view::release::FleetReleaseObservation,
17};
18use candid::Principal;
19use canic_contracts::ids::SubnetId;
20use canic_core::cdk::utils::hash::hex_bytes;
21use std::{collections::BTreeMap, fs::File};
22use thiserror::Error;
23
24#[derive(Debug, Error)]
26pub enum ReleaseReservationError {
27 #[error(transparent)]
28 State(#[from] EnsureStateError),
29 #[error(transparent)]
30 Evidence(#[from] FleetReleaseError),
31 #[error("release journal, review or operation authority differs")]
32 Integrity,
33 #[error("release reservation persistence was uncertain; reopen the retained journal")]
34 PersistenceUncertain,
35}
36
37pub struct ReleaseObservationJournal {
39 _lock: File,
40 paths: EnsurePaths,
41 journal: FleetEnsureJournalRecord,
42 poisoned: bool,
43}
44
45pub struct ReleaseObservationReservation<'a> {
47 owner: &'a ReleaseObservationJournal,
48 canister: Principal,
49 subnet: SubnetId,
50}
51
52impl ReleaseObservationReservation<'_> {
53 pub(super) fn authorizes(
54 &self,
55 authority: &FleetReleaseAuthority,
56 canister: Principal,
57 subnet: SubnetId,
58 ) -> bool {
59 self.canister == canister
60 && self.subnet == subnet
61 && self
62 .owner
63 .journal
64 .release
65 .as_ref()
66 .is_some_and(|record| &record.review.authority == authority)
67 }
68}
69
70impl ReleaseObservationJournal {
71 pub fn attach(
74 paths: &EnsurePaths,
75 review: &FleetReleaseReviewRecord,
76 observed: &FleetReleaseObservation,
77 ) -> Result<Self, ReleaseReservationError> {
78 super::verify_review(review, observed)?;
79 let mut owner = Self::read_locked(paths)?;
80 validate_envelope(&owner.journal, review)?;
81 if owner.journal.release.is_none() {
82 if !owner.journal.effects.is_empty() {
83 return Err(ReleaseReservationError::Integrity);
84 }
85 owner.journal.release = Some(FleetReleaseExecutionRecord {
86 plan_sha256: owner.journal.plan_sha256.clone(),
87 review: review.clone(),
88 reserved_paid_calls: review
89 .sources
90 .iter()
91 .map(|source| (source.binding.canister_id.to_text(), 0))
92 .collect(),
93 });
94 owner.save()?;
95 }
96 owner.validate(review)?;
97 Ok(owner)
98 }
99
100 pub fn resume(
102 paths: &EnsurePaths,
103 review: &FleetReleaseReviewRecord,
104 ) -> Result<Self, ReleaseReservationError> {
105 let owner = Self::read_locked(paths)?;
106 owner.validate(review)?;
107 Ok(owner)
108 }
109
110 pub fn reserve(
112 &mut self,
113 canister: Principal,
114 ) -> Result<ReleaseObservationReservation<'_>, ReleaseReservationError> {
115 if self.poisoned {
116 return Err(ReleaseReservationError::PersistenceUncertain);
117 }
118 if read_journal(&self.paths)?.as_ref() != Some(&self.journal) {
119 return Err(ReleaseReservationError::Integrity);
120 }
121 let record = self
122 .journal
123 .release
124 .as_mut()
125 .ok_or(ReleaseReservationError::Integrity)?;
126 let source = record
127 .review
128 .sources
129 .iter()
130 .find(|source| source.binding.canister_id == canister)
131 .ok_or(ReleaseReservationError::Integrity)?;
132 let calls = record
133 .reserved_paid_calls
134 .get_mut(&canister.to_text())
135 .ok_or(ReleaseReservationError::Integrity)?;
136 *calls = reserve_observation_calls(
137 source,
138 *calls,
139 super::observation::PHYSICAL_SAMPLE_PAID_CALLS,
140 )?;
141 let subnet = source.binding.subnet;
142 self.save()?;
143 Ok(ReleaseObservationReservation {
144 owner: self,
145 canister,
146 subnet,
147 })
148 }
149
150 fn read_locked(paths: &EnsurePaths) -> Result<Self, ReleaseReservationError> {
151 let lock = crate::fleet_ensure::ops::lock_fleet_file(paths)?;
152 crate::fleet_ensure::ops::capacity_import::journal::require_no_approved_import(paths)?;
153 let journal = read_journal(paths)?.ok_or(ReleaseReservationError::Integrity)?;
154 Ok(Self {
155 _lock: lock,
156 paths: paths.clone(),
157 journal,
158 poisoned: false,
159 })
160 }
161
162 fn validate(&self, review: &FleetReleaseReviewRecord) -> Result<(), ReleaseReservationError> {
163 super::verify_digest(review)?;
164 validate_envelope(&self.journal, review)?;
165 let record = self
166 .journal
167 .release
168 .as_ref()
169 .ok_or(ReleaseReservationError::Integrity)?;
170 let sources = review
171 .sources
172 .iter()
173 .map(|source| (source.binding.canister_id.to_text(), source))
174 .collect::<BTreeMap<_, _>>();
175 if &record.review != review
176 || record.plan_sha256 != self.journal.plan_sha256
177 || sources.len() != review.sources.len()
178 || sources.len() != record.reserved_paid_calls.len()
179 {
180 return Err(ReleaseReservationError::Integrity);
181 }
182 for (id, calls) in &record.reserved_paid_calls {
183 let source = sources.get(id).ok_or(ReleaseReservationError::Integrity)?;
184 reserve_observation_calls(source, *calls, 0)?;
185 }
186 Ok(())
187 }
188
189 fn save(&mut self) -> Result<(), ReleaseReservationError> {
190 if let Err(error) = write_journal(&self.paths, &self.journal) {
191 self.poisoned = true;
192 return Err(error.into());
193 }
194 Ok(())
195 }
196}
197
198fn validate_envelope(
199 journal: &FleetEnsureJournalRecord,
200 review: &FleetReleaseReviewRecord,
201) -> Result<(), ReleaseReservationError> {
202 let operation_matches = journal.operation_id == hex_bytes(review.authority.operation_id)
203 && crate::fleet_ensure::ops::is_sha256(&journal.plan_sha256)
204 && journal.completion == FleetEnsureCompletion::InProgress;
205 let independent_authority = journal.bootstrap_registration_recovery.is_some()
206 || journal.estate_funding_required.is_some();
207 let ordinary_history_empty = journal.funding_reviews.is_empty()
208 && journal.funding_observations.is_empty()
209 && journal.successor_phases.is_empty();
210 if !operation_matches || independent_authority || !ordinary_history_empty {
211 return Err(ReleaseReservationError::Integrity);
212 }
213 Ok(())
214}
215
216pub(in crate::fleet_ensure::ops) fn require_no_active_release(
218 paths: &EnsurePaths,
219) -> Result<(), EnsureStateError> {
220 if let Some(journal) = crate::fleet_ensure::ops::operation_selection::read(&paths.journal)?
221 && journal
222 .get("release")
223 .is_some_and(|release| !release.is_null())
224 && journal
225 .get("completion")
226 .and_then(serde_json::Value::as_str)
227 != Some("converged")
228 {
229 return Err(EnsureStateError::ReleaseInProgress {
230 path: paths.journal.clone(),
231 });
232 }
233 Ok(())
234}