Skip to main content

canic_core/dto/
rpc.rs

1use crate::dto::{
2    auth::{
3        InternalInvocationProofRequest, RoleAttestationRequest, SignedInternalInvocationProofV1,
4        SignedRoleAttestation,
5    },
6    prelude::*,
7};
8
9//
10// Request
11//
12// Root orchestration request.
13//
14
15#[derive(CandidType, Clone, Debug, Deserialize)]
16pub enum Request {
17    CreateCanister(CreateCanisterRequest),
18    UpgradeCanister(UpgradeCanisterRequest),
19    RecycleCanister(RecycleCanisterRequest),
20    Cycles(CyclesRequest),
21    IssueRoleAttestation(RoleAttestationRequest),
22    IssueInternalInvocationProof(InternalInvocationProofRequest),
23}
24
25impl Request {
26    // create_canister
27    //
28    // Build a root request for canister provisioning.
29    #[must_use]
30    pub const fn create_canister(request: CreateCanisterRequest) -> Self {
31        Self::CreateCanister(request)
32    }
33
34    // upgrade_canister
35    //
36    // Build a root request for upgrading an existing canister.
37    #[must_use]
38    pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
39        Self::UpgradeCanister(request)
40    }
41
42    // recycle_canister
43    //
44    // Build a root request for recycling one child canister back into the pool.
45    #[must_use]
46    pub const fn recycle_canister(request: RecycleCanisterRequest) -> Self {
47        Self::RecycleCanister(request)
48    }
49
50    // cycles
51    //
52    // Build a root request for requesting/transferring cycles.
53    #[must_use]
54    pub const fn cycles(request: CyclesRequest) -> Self {
55        Self::Cycles(request)
56    }
57
58    // issue_role_attestation
59    //
60    // Build a root request for role attestation issuance.
61    #[must_use]
62    pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
63        Self::IssueRoleAttestation(request)
64    }
65
66    // issue_internal_invocation_proof
67    //
68    // Build a root request for method-scoped internal invocation proof issuance.
69    #[must_use]
70    pub const fn issue_internal_invocation_proof(request: InternalInvocationProofRequest) -> Self {
71        Self::IssueInternalInvocationProof(request)
72    }
73
74    // family
75    //
76    // Resolve the request capability family without exposing variant matches at call sites.
77    #[must_use]
78    pub const fn family(&self) -> RequestFamily {
79        match self {
80            Self::CreateCanister(_) => RequestFamily::Provision,
81            Self::UpgradeCanister(_) => RequestFamily::Upgrade,
82            Self::RecycleCanister(_) => RequestFamily::RecycleCanister,
83            Self::Cycles(_) => RequestFamily::RequestCycles,
84            Self::IssueRoleAttestation(_) => RequestFamily::IssueRoleAttestation,
85            Self::IssueInternalInvocationProof(_) => RequestFamily::IssueInternalInvocationProof,
86        }
87    }
88
89    // metadata
90    //
91    // Return replay metadata carried by the request variant.
92    #[must_use]
93    pub const fn metadata(&self) -> Option<RootRequestMetadata> {
94        match self {
95            Self::CreateCanister(req) => req.metadata,
96            Self::UpgradeCanister(req) => req.metadata,
97            Self::RecycleCanister(req) => req.metadata,
98            Self::Cycles(req) => req.metadata,
99            Self::IssueRoleAttestation(req) => req.metadata,
100            Self::IssueInternalInvocationProof(req) => req.metadata,
101        }
102    }
103
104    // with_metadata
105    //
106    // Attach root replay metadata to the request payload.
107    #[must_use]
108    pub const fn with_metadata(mut self, metadata: RootRequestMetadata) -> Self {
109        match &mut self {
110            Self::CreateCanister(req) => req.metadata = Some(metadata),
111            Self::UpgradeCanister(req) => req.metadata = Some(metadata),
112            Self::RecycleCanister(req) => req.metadata = Some(metadata),
113            Self::Cycles(req) => req.metadata = Some(metadata),
114            Self::IssueRoleAttestation(req) => req.metadata = Some(metadata),
115            Self::IssueInternalInvocationProof(req) => req.metadata = Some(metadata),
116        }
117        self
118    }
119
120    // without_metadata
121    //
122    // Remove root replay metadata for canonical hashing and signature binding.
123    #[must_use]
124    pub const fn without_metadata(mut self) -> Self {
125        match &mut self {
126            Self::CreateCanister(req) => req.metadata = None,
127            Self::UpgradeCanister(req) => req.metadata = None,
128            Self::RecycleCanister(req) => req.metadata = None,
129            Self::Cycles(req) => req.metadata = None,
130            Self::IssueRoleAttestation(req) => req.metadata = None,
131            Self::IssueInternalInvocationProof(req) => req.metadata = None,
132        }
133        self
134    }
135
136    // canonical_capability_payload
137    //
138    // Remove fields that are not authoritative root-capability inputs before
139    // signature/replay payload hashing.
140    #[must_use]
141    pub const fn canonical_capability_payload(mut self) -> Self {
142        self = self.without_metadata();
143        if let Self::IssueRoleAttestation(req) = &mut self {
144            req.epoch = 0;
145        }
146        self
147    }
148
149    // upgrade_request
150    //
151    // Return the upgrade payload when this request belongs to the upgrade family.
152    #[must_use]
153    pub const fn upgrade_request(&self) -> Option<&UpgradeCanisterRequest> {
154        match self {
155            Self::UpgradeCanister(request) => Some(request),
156            _ => None,
157        }
158    }
159}
160
161//
162// RequestFamily
163//
164// Request family label.
165//
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum RequestFamily {
169    Provision,
170    Upgrade,
171    RecycleCanister,
172    RequestCycles,
173    IssueRoleAttestation,
174    IssueInternalInvocationProof,
175}
176
177impl RequestFamily {
178    // label
179    //
180    // Return the canonical family label used across capability checks and logs.
181    #[must_use]
182    pub const fn label(self) -> &'static str {
183        match self {
184            Self::Provision => "Provision",
185            Self::Upgrade => "Upgrade",
186            Self::RecycleCanister => "RecycleCanister",
187            Self::RequestCycles => "RequestCycles",
188            Self::IssueRoleAttestation => "IssueRoleAttestation",
189            Self::IssueInternalInvocationProof => "IssueInternalInvocationProof",
190        }
191    }
192}
193
194//
195// RootCapabilityCommand
196//
197// Internal root command.
198//
199
200#[derive(CandidType, Clone, Debug, Deserialize)]
201pub enum RootCapabilityCommand {
202    ProvisionCanister(CreateCanisterRequest),
203    UpgradeCanister(UpgradeCanisterRequest),
204    RecycleCanister(RecycleCanisterRequest),
205    RequestCycles(CyclesRequest),
206    IssueRoleAttestation(RoleAttestationRequest),
207    IssueInternalInvocationProof(InternalInvocationProofRequest),
208}
209
210impl From<Request> for RootCapabilityCommand {
211    fn from(value: Request) -> Self {
212        match value {
213            Request::CreateCanister(req) => Self::ProvisionCanister(req),
214            Request::UpgradeCanister(req) => Self::UpgradeCanister(req),
215            Request::RecycleCanister(req) => Self::RecycleCanister(req),
216            Request::Cycles(req) => Self::RequestCycles(req),
217            Request::IssueRoleAttestation(req) => Self::IssueRoleAttestation(req),
218            Request::IssueInternalInvocationProof(req) => Self::IssueInternalInvocationProof(req),
219        }
220    }
221}
222
223//
224// RootRequestMetadata
225//
226// Replay metadata.
227//
228
229#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
230pub struct RootRequestMetadata {
231    pub request_id: [u8; 32],
232    pub ttl_seconds: u64,
233}
234
235//
236// CreateCanisterRequest
237//
238// Create-canister payload.
239//
240
241#[derive(CandidType, Clone, Debug, Deserialize)]
242pub struct CreateCanisterRequest {
243    pub canister_role: CanisterRole,
244    pub parent: CreateCanisterParent,
245    pub extra_arg: Option<Vec<u8>>,
246    #[serde(default)]
247    pub metadata: Option<RootRequestMetadata>,
248}
249
250//
251// CreateCanisterParent
252//
253// Parent selection.
254//
255
256#[derive(CandidType, Clone, Debug, Deserialize)]
257pub enum CreateCanisterParent {
258    Root,
259    // Use the requesting canister.
260    ThisCanister,
261    // Use the caller's parent.
262    Parent,
263    Canister(Principal),
264    Index(CanisterRole),
265}
266
267//
268// UpgradeCanisterRequest
269//
270// Upgrade-canister payload.
271//
272
273#[derive(CandidType, Clone, Debug, Deserialize)]
274pub struct UpgradeCanisterRequest {
275    pub canister_pid: Principal,
276    #[serde(default)]
277    pub metadata: Option<RootRequestMetadata>,
278}
279
280//
281// RecycleCanisterRequest
282//
283// Recycle-one-child payload.
284//
285
286#[derive(CandidType, Clone, Debug, Deserialize)]
287pub struct RecycleCanisterRequest {
288    pub canister_pid: Principal,
289    #[serde(default)]
290    pub metadata: Option<RootRequestMetadata>,
291}
292
293//
294// CyclesRequest
295//
296// Cycles payload.
297//
298
299#[derive(CandidType, Clone, Debug, Deserialize)]
300pub struct CyclesRequest {
301    pub cycles: u128,
302    #[serde(default)]
303    pub metadata: Option<RootRequestMetadata>,
304}
305
306//
307// Response
308//
309// Root response payload.
310//
311
312#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
313pub enum Response {
314    CreateCanister(CreateCanisterResponse),
315    UpgradeCanister(UpgradeCanisterResponse),
316    RecycleCanister(RecycleCanisterResponse),
317    Cycles(CyclesResponse),
318    RoleAttestationIssued(SignedRoleAttestation),
319    InternalInvocationProofIssued(SignedInternalInvocationProofV1),
320}
321
322//
323// CreateCanisterResponse
324// Result of creating and installing a new canister.
325//
326
327#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
328pub struct CreateCanisterResponse {
329    pub new_canister_pid: Principal,
330}
331
332//
333// UpgradeCanisterResponse
334// Result of an upgrade request (currently empty, reserved for metadata)
335//
336
337#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
338pub struct UpgradeCanisterResponse {}
339
340//
341// RecycleCanisterResponse
342// Result of recycling one canister back into the pool.
343//
344
345#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
346pub struct RecycleCanisterResponse {}
347
348//
349// CyclesResponse
350// Result of transferring cycles to a child canister
351//
352
353#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
354pub struct CyclesResponse {
355    pub cycles_transferred: u128,
356}
357
358//
359// TESTS
360//
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    fn p(id: u8) -> Principal {
367        Principal::from_slice(&[id; 29])
368    }
369
370    fn metadata(id: u8) -> RootRequestMetadata {
371        RootRequestMetadata {
372            request_id: [id; 32],
373            ttl_seconds: 60,
374        }
375    }
376
377    fn requests_with_no_metadata() -> Vec<Request> {
378        vec![
379            Request::create_canister(CreateCanisterRequest {
380                canister_role: CanisterRole::new("app"),
381                parent: CreateCanisterParent::Root,
382                extra_arg: None,
383                metadata: None,
384            }),
385            Request::upgrade_canister(UpgradeCanisterRequest {
386                canister_pid: p(2),
387                metadata: None,
388            }),
389            Request::recycle_canister(RecycleCanisterRequest {
390                canister_pid: p(7),
391                metadata: None,
392            }),
393            Request::cycles(CyclesRequest {
394                cycles: 100,
395                metadata: None,
396            }),
397            Request::issue_role_attestation(RoleAttestationRequest {
398                subject: p(5),
399                role: CanisterRole::new("test"),
400                subnet_id: None,
401                audience: p(6),
402                ttl_secs: 60,
403                epoch: 0,
404                metadata: None,
405            }),
406            Request::issue_internal_invocation_proof(InternalInvocationProofRequest {
407                subject: p(5),
408                role: CanisterRole::new("test"),
409                subnet_id: None,
410                audience: p(6),
411                audience_method: "canic_internal".to_string(),
412                ttl_secs: 60,
413                metadata: None,
414            }),
415        ]
416    }
417
418    #[test]
419    fn request_family_matches_all_variants() {
420        let families: Vec<RequestFamily> = requests_with_no_metadata()
421            .iter()
422            .map(Request::family)
423            .collect();
424        assert_eq!(
425            families,
426            vec![
427                RequestFamily::Provision,
428                RequestFamily::Upgrade,
429                RequestFamily::RecycleCanister,
430                RequestFamily::RequestCycles,
431                RequestFamily::IssueRoleAttestation,
432                RequestFamily::IssueInternalInvocationProof,
433            ]
434        );
435    }
436
437    #[test]
438    fn with_metadata_and_without_metadata_cover_all_variants() {
439        let replay_meta = metadata(7);
440
441        for request in requests_with_no_metadata() {
442            let with_meta = request.clone().with_metadata(replay_meta);
443            assert_eq!(
444                with_meta.metadata(),
445                Some(replay_meta),
446                "with_metadata must set metadata for every request variant"
447            );
448
449            let without_meta = with_meta.without_metadata();
450            assert_eq!(
451                without_meta.metadata(),
452                None,
453                "without_metadata must strip metadata for every request variant"
454            );
455        }
456    }
457
458    #[test]
459    fn canonical_capability_payload_strips_metadata_and_ignored_epoch() {
460        let canonical = Request::issue_role_attestation(RoleAttestationRequest {
461            subject: p(5),
462            role: CanisterRole::new("test"),
463            subnet_id: None,
464            audience: p(6),
465            ttl_secs: 60,
466            epoch: 99,
467            metadata: Some(metadata(9)),
468        })
469        .canonical_capability_payload();
470
471        match canonical {
472            Request::IssueRoleAttestation(req) => {
473                assert_eq!(req.metadata, None);
474                assert_eq!(req.epoch, 0);
475            }
476            other => panic!("expected role-attestation request, got {other:?}"),
477        }
478    }
479
480    #[test]
481    fn upgrade_request_is_only_available_for_upgrade_variant() {
482        let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
483            canister_pid: p(9),
484            metadata: Some(metadata(9)),
485        });
486        assert!(upgrade.upgrade_request().is_some());
487
488        for request in requests_with_no_metadata() {
489            if !matches!(request, Request::UpgradeCanister(_)) {
490                assert!(request.upgrade_request().is_none());
491            }
492        }
493    }
494}