Skip to main content

canic_core/dto/
pool.rs

1//! Passive boundary contracts for the Fleet Subnet Root physical-Canister inventory.
2
3use crate::{
4    cdk::types::Cycles,
5    ids::{ComponentInstanceId, FleetSubnetCanisterPoolConfig, ReleaseBuildId},
6};
7use candid::{CandidType, Principal};
8use serde::{Deserialize, Serialize};
9
10/// Identifies the durable Component allocation that has claimed one empty Canister.
11#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct CanisterPoolClaim {
13    pub component: ComponentInstanceId,
14    pub operation_id: [u8; 32],
15}
16
17/// Reset outcome retained while a stopped workload is still Registry-owned.
18#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19pub enum CanisterPoolRecycleReset {
20    Pending,
21    Ready,
22    Failed { reason: String },
23}
24
25/// How one physical Canister entered the root-owned inventory.
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
27pub enum CanisterPoolAssetOrigin {
28    InfrastructureStore,
29    Created,
30    Imported,
31    Recycled,
32}
33
34/// Current durable state of one root-owned physical Canister.
35#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub enum CanisterPoolAssetStatus {
37    Store,
38    StoreDeletionPending {
39        operation_id: [u8; 32],
40    },
41    PendingReset,
42    Ready,
43    Claimed {
44        claim: CanisterPoolClaim,
45    },
46    Workload {
47        claim: CanisterPoolClaim,
48    },
49    Recycling {
50        claim: CanisterPoolClaim,
51        reset: CanisterPoolRecycleReset,
52    },
53    RecoveringLedger {
54        operation_id: [u8; 32],
55    },
56    HandingOff {
57        recipient: Principal,
58    },
59    Failed {
60        reason: String,
61    },
62}
63
64/// Controller-visible inventory row for one root-owned physical Canister.
65#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
66pub struct CanisterPoolAsset {
67    pub canister_id: Principal,
68    pub cycles: Cycles,
69    pub origin: CanisterPoolAssetOrigin,
70    pub status: CanisterPoolAssetStatus,
71    pub added_at_ns: u64,
72    pub updated_at_ns: u64,
73}
74
75/// Durable transfer of one paid asset to replacement authority during root draining.
76#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77pub struct CanisterPoolHandoff {
78    pub canister_id: Principal,
79    pub recipient: Principal,
80    pub prepared_at_ns: u64,
81}
82
83/// Why one autonomous refill stopped without creating another Canister.
84#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
85pub enum CanisterPoolCreationFailure {
86    UnresolvedAfterLedgerWindow,
87    LedgerCreationFailed,
88    LedgerRejected,
89}
90
91/// Durable controller-visible progress of one autonomous refill.
92#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub enum CanisterPoolCreationProgress {
94    Intent {
95        uncertain_result: bool,
96    },
97    Created {
98        block_index: u64,
99        canister_id: Principal,
100    },
101    Blocked {
102        failure: CanisterPoolCreationFailure,
103    },
104}
105
106/// Exact Cycles Ledger request retained until its principal is in inventory.
107#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108pub struct CanisterPoolCreation {
109    pub operation_id: [u8; 32],
110    pub cycles_ledger: Principal,
111    pub placement_subnet: Principal,
112    pub root: Principal,
113    pub ledger_amount: Cycles,
114    pub created_at_time_ns: u64,
115    pub progress: CanisterPoolCreationProgress,
116}
117
118/// Bounded controller query for one canonical inventory page.
119#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
120pub struct CanisterPoolStatusRequest {
121    pub start_after: Option<Principal>,
122    pub limit: u16,
123}
124
125/// Selects one pool Canister for an import or reset retry command.
126#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
127pub struct PoolCanisterRequest {
128    pub canister_id: Principal,
129}
130
131/// Selects one pool Canister and its exact handoff recipient.
132#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
133pub struct PoolHandoffRequest {
134    pub canister_id: Principal,
135    pub recipient: Principal,
136}
137
138/// Exact release-bound helper artifact used for one empty-asset Ledger recovery.
139#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct PoolLedgerRecoveryArtifact {
141    pub candid_sha256: [u8; 32],
142    pub payload_hash: [u8; 32],
143    pub payload_size_bytes: u64,
144    pub raw_module_hash: [u8; 32],
145    pub release_build_id: ReleaseBuildId,
146}
147
148/// Reviewed authority for converting one empty pool asset's Ledger balance to native cycles.
149#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150pub struct PoolLedgerRecoveryRequest {
151    pub artifact: PoolLedgerRecoveryArtifact,
152    pub canister_id: Principal,
153    pub created_at_time_ns: u64,
154    pub cycles_ledger: Principal,
155    pub ledger_balance: Cycles,
156    pub ledger_fee: Cycles,
157    pub maximum_execution_burn_cycles: Cycles,
158    pub operation_id: [u8; 32],
159    pub withdrawal_amount: Cycles,
160}
161
162/// Durable phase of one Root-owned empty-pool Ledger recovery.
163#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
164pub enum PoolLedgerRecoveryPhase {
165    Prepared,
166    HelperInstallIssued,
167    HelperInstalled,
168    WithdrawalIssued,
169    WithdrawalVerified,
170    HelperUninstallIssued,
171    Complete,
172}
173
174/// Terminal proof that the Ledger debit became native cycles on the same pool Principal.
175#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
176pub struct PoolLedgerRecoveryReceipt {
177    pub block_index: u64,
178    pub completed_at_ns: u64,
179    pub final_native_cycles: Cycles,
180    pub operation_id: [u8; 32],
181    pub request: PoolLedgerRecoveryRequest,
182}
183
184/// Protected current-or-last result for one exact pool Ledger recovery identity.
185#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
186pub struct PoolLedgerRecoveryStatusResponse {
187    pub block_index: Option<u64>,
188    pub initial_native_cycles: Cycles,
189    pub phase: PoolLedgerRecoveryPhase,
190    pub receipt: Option<PoolLedgerRecoveryReceipt>,
191    pub request: PoolLedgerRecoveryRequest,
192}
193
194/// Exact pool policy and current exclusive root-owned physical inventory.
195#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196pub struct CanisterPoolResponse {
197    pub config: FleetSubnetCanisterPoolConfig,
198    pub tracked: u32,
199    pub store: u32,
200    pub store_deletion_pending: u32,
201    pub pooled: u32,
202    pub workload: u32,
203    pub surplus: u32,
204    pub ready: u32,
205    pub pending_reset: u32,
206    pub claimed: u32,
207    pub recycling: u32,
208    pub recovering_ledger: u32,
209    pub handing_off: u32,
210    pub failed: u32,
211    pub completed_handoffs: u64,
212    pub pending_creation: Option<CanisterPoolCreation>,
213    pub pending_handoff: Option<CanisterPoolHandoff>,
214    pub entries: Vec<CanisterPoolAsset>,
215    pub next_start_after: Option<Principal>,
216}
217
218/// Controller command for explicit pool maintenance.
219#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
220pub enum PoolAdminCommand {
221    Maintain,
222    RetryRefill,
223    Import {
224        canister_id: Principal,
225    },
226    RetryReset {
227        canister_id: Principal,
228    },
229    Handoff {
230        canister_id: Principal,
231        recipient: Principal,
232    },
233    RecoverLedger(Box<PoolLedgerRecoveryRequest>),
234}
235
236/// Result of one explicit pool maintenance command.
237#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
238pub enum PoolAdminResponse {
239    Maintained,
240    MaintenancePaused {
241        reason: String,
242    },
243    Created {
244        canister_id: Principal,
245    },
246    RefillWaitingForCycles {
247        available: Cycles,
248        creation_amount: Cycles,
249    },
250    RefillPending {
251        operation_id: [u8; 32],
252        uncertain_result: bool,
253    },
254    RefillBlocked {
255        operation_id: [u8; 32],
256        failure: CanisterPoolCreationFailure,
257    },
258    RefillRetryScheduled {
259        previous_operation_id: [u8; 32],
260    },
261    Imported {
262        canister_id: Principal,
263    },
264    ResetQueued {
265        canister_id: Principal,
266    },
267    ResetReady {
268        canister_id: Principal,
269    },
270    HandedOff {
271        canister_id: Principal,
272        recipient: Principal,
273    },
274    LedgerRecovered(Box<PoolLedgerRecoveryReceipt>),
275    ResetFailed {
276        canister_id: Principal,
277        reason: String,
278    },
279}
280
281/// Narrow result of one explicit maintenance pass.
282#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
283pub enum PoolMaintenanceResponse {
284    Maintained,
285    MaintenancePaused {
286        reason: String,
287    },
288    Created {
289        canister_id: Principal,
290    },
291    RefillWaitingForCycles {
292        available: Cycles,
293        creation_amount: Cycles,
294    },
295    RefillPending {
296        operation_id: [u8; 32],
297        uncertain_result: bool,
298    },
299    RefillBlocked {
300        operation_id: [u8; 32],
301        failure: CanisterPoolCreationFailure,
302    },
303    ResetReady {
304        canister_id: Principal,
305    },
306    ResetFailed {
307        canister_id: Principal,
308        reason: String,
309    },
310}
311
312/// Narrow result of importing one existing physical Canister.
313#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
314pub enum PoolImportResponse {
315    Imported {
316        canister_id: Principal,
317    },
318    ResetFailed {
319        canister_id: Principal,
320        reason: String,
321    },
322}
323
324/// Exact result of scheduling another blocked refill attempt.
325#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
326pub struct PoolRefillRetryResponse {
327    pub previous_operation_id: [u8; 32],
328}
329
330/// Exact result of scheduling another reset attempt.
331#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
332pub struct PoolResetRetryResponse {
333    pub canister_id: Principal,
334}
335
336/// Exact result of handing one physical Canister to replacement authority.
337#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
338pub struct PoolHandoffResponse {
339    pub canister_id: Principal,
340    pub recipient: Principal,
341}
342
343// -----------------------------------------------------------------------------
344// Tests
345// -----------------------------------------------------------------------------
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn pool_status_and_admin_contracts_round_trip_through_candid() {
353        let canister_id = Principal::from_slice(&[7; 29]);
354        let response = CanisterPoolResponse {
355            config: FleetSubnetCanisterPoolConfig {
356                minimum_size: 3,
357                maximum_size: 10,
358                canister_cycles: Cycles::new(5_000_000_000_000),
359            },
360            tracked: 1,
361            store: 0,
362            store_deletion_pending: 0,
363            pooled: 1,
364            workload: 0,
365            surplus: 0,
366            ready: 0,
367            pending_reset: 0,
368            claimed: 0,
369            recycling: 0,
370            recovering_ledger: 0,
371            handing_off: 0,
372            failed: 1,
373            completed_handoffs: 0,
374            pending_creation: Some(CanisterPoolCreation {
375                operation_id: [8; 32],
376                cycles_ledger: Principal::from_slice(&[6; 29]),
377                placement_subnet: Principal::from_slice(&[5; 29]),
378                root: Principal::from_slice(&[4; 29]),
379                ledger_amount: Cycles::new(5_500_000_000_000),
380                created_at_time_ns: 12,
381                progress: CanisterPoolCreationProgress::Blocked {
382                    failure: CanisterPoolCreationFailure::LedgerCreationFailed,
383                },
384            }),
385            pending_handoff: None,
386            entries: vec![CanisterPoolAsset {
387                canister_id,
388                cycles: Cycles::new(4_000_000_000_000),
389                origin: CanisterPoolAssetOrigin::Recycled,
390                status: CanisterPoolAssetStatus::Failed {
391                    reason: "below configured cycles".to_string(),
392                },
393                added_at_ns: 10,
394                updated_at_ns: 11,
395            }],
396            next_start_after: None,
397        };
398        let bytes = candid::encode_one(&response).expect("encode pool status");
399        assert_eq!(
400            candid::decode_one::<CanisterPoolResponse>(&bytes).expect("decode pool status"),
401            response,
402        );
403
404        let command = PoolAdminCommand::Import { canister_id };
405        let bytes = candid::encode_one(&command).expect("encode pool command");
406        assert_eq!(
407            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode pool command"),
408            command,
409        );
410
411        let command = PoolAdminCommand::RetryRefill;
412        let bytes = candid::encode_one(&command).expect("encode refill retry command");
413        assert_eq!(
414            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode refill retry command"),
415            command,
416        );
417    }
418}