Skip to main content

canic_testkit/pic/
mod.rs

1use candid::{CandidType, Principal, decode_one, encode_args, encode_one, utils::ArgumentEncoder};
2use canic::{
3    Error,
4    cdk::types::TC,
5    dto::{
6        abi::v1::CanisterInitPayload,
7        env::EnvBootstrapArgs,
8        subnet::SubnetIdentity,
9        topology::{AppDirectoryArgs, SubnetDirectoryArgs, SubnetRegistryResponse},
10    },
11    ids::CanisterRole,
12    protocol,
13};
14use pocket_ic::{PocketIc, PocketIcBuilder};
15use serde::de::DeserializeOwned;
16use std::{
17    collections::HashMap,
18    ops::{Deref, DerefMut},
19    panic::{AssertUnwindSafe, catch_unwind},
20    time::Duration,
21};
22
23mod baseline;
24mod process_lock;
25mod standalone;
26mod startup;
27
28pub use baseline::{
29    CachedPicBaseline, CachedPicBaselineGuard, ControllerSnapshots, acquire_cached_pic_baseline,
30};
31pub use process_lock::{
32    PicSerialGuard, PicSerialGuardError, acquire_pic_serial_guard, try_acquire_pic_serial_guard,
33};
34pub use startup::PicStartError;
35const INSTALL_CYCLES: u128 = 500 * TC;
36
37///
38/// PicInstallError
39///
40
41#[derive(Debug, Eq, PartialEq)]
42pub struct PicInstallError {
43    canister_id: Principal,
44    message: String,
45}
46
47///
48/// StandaloneCanisterFixtureError
49///
50
51#[derive(Debug)]
52pub enum StandaloneCanisterFixtureError {
53    SerialGuard(PicSerialGuardError),
54    Start(PicStartError),
55    Install(PicInstallError),
56}
57
58pub use standalone::{
59    StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
60    install_standalone_canister, try_install_prebuilt_canister,
61    try_install_prebuilt_canister_with_cycles,
62};
63
64///
65/// Create a fresh PocketIC universe.
66///
67/// IMPORTANT:
68/// - Each call creates a new IC instance
69/// - WARNING: callers must hold a `PicSerialGuard` for the full `Pic` lifetime
70/// - Required to avoid PocketIC wasm chunk store exhaustion
71///
72#[must_use]
73pub fn pic() -> Pic {
74    try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
75}
76
77/// Create a fresh PocketIC universe without panicking on startup failures.
78pub fn try_pic() -> Result<Pic, PicStartError> {
79    PicBuilder::new().with_application_subnet().try_build()
80}
81
82/// Wait until a PocketIC canister reports `canic_ready`.
83pub fn wait_until_ready(pic: &PocketIc, canister_id: Principal, tick_limit: usize) {
84    let payload = encode_args(()).expect("encode empty args");
85
86    for _ in 0..tick_limit {
87        if let Ok(bytes) = pic.query_call(
88            canister_id,
89            Principal::anonymous(),
90            protocol::CANIC_READY,
91            payload.clone(),
92        ) && let Ok(ready) = decode_one::<bool>(&bytes)
93            && ready
94        {
95            return;
96        }
97        pic.tick();
98    }
99
100    panic!("canister did not report ready in time: {canister_id}");
101}
102
103/// Resolve one role principal from root's subnet registry, polling until present.
104#[must_use]
105pub fn role_pid(
106    pic: &PocketIc,
107    root_id: Principal,
108    role: &'static str,
109    tick_limit: usize,
110) -> Principal {
111    for _ in 0..tick_limit {
112        let registry: Result<Result<SubnetRegistryResponse, Error>, Error> = {
113            let payload = encode_args(()).expect("encode empty args");
114            pic.query_call(
115                root_id,
116                Principal::anonymous(),
117                protocol::CANIC_SUBNET_REGISTRY,
118                payload,
119            )
120            .map_err(|err| {
121                Error::internal(format!(
122                    "pocket_ic query_call failed (canister={root_id}, method={}): {err}",
123                    protocol::CANIC_SUBNET_REGISTRY
124                ))
125            })
126            .and_then(|bytes| {
127                decode_one(&bytes).map_err(|err| {
128                    Error::internal(format!("decode_one failed for subnet registry: {err}"))
129                })
130            })
131        };
132
133        if let Ok(Ok(registry)) = registry
134            && let Some(pid) = registry
135                .0
136                .into_iter()
137                .find(|entry| entry.role == CanisterRole::new(role))
138                .map(|entry| entry.pid)
139        {
140            return pid;
141        }
142
143        pic.tick();
144    }
145
146    panic!("{role} canister must be registered");
147}
148
149///
150/// PicBuilder
151/// Thin wrapper around the PocketIC builder.
152///
153/// This builder is only used to configure the singleton. It does not create
154/// additional IC instances beyond the global `Pic`.
155///
156/// Note: this file is test-only infrastructure; simplicity wins over abstraction.
157///
158
159pub struct PicBuilder(PocketIcBuilder);
160
161#[expect(clippy::new_without_default)]
162impl PicBuilder {
163    /// Start a new PicBuilder with sensible defaults.
164    #[must_use]
165    pub fn new() -> Self {
166        Self(PocketIcBuilder::new())
167    }
168
169    /// Include an application subnet in the PocketIC universe.
170    #[must_use]
171    pub fn with_application_subnet(mut self) -> Self {
172        self.0 = self.0.with_application_subnet();
173        self
174    }
175
176    /// Include an II subnet so threshold keys are available in the PocketIC universe.
177    #[must_use]
178    pub fn with_ii_subnet(mut self) -> Self {
179        self.0 = self.0.with_ii_subnet();
180        self
181    }
182
183    /// Include an NNS subnet in the PocketIC universe.
184    #[must_use]
185    pub fn with_nns_subnet(mut self) -> Self {
186        self.0 = self.0.with_nns_subnet();
187        self
188    }
189
190    /// Finish building the PocketIC instance and wrap it.
191    #[must_use]
192    pub fn build(self) -> Pic {
193        self.try_build()
194            .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
195    }
196
197    /// Finish building the PocketIC instance without panicking on startup failures.
198    pub fn try_build(self) -> Result<Pic, PicStartError> {
199        startup::try_build_pic(AssertUnwindSafe(self.0).0)
200    }
201}
202
203impl PicInstallError {
204    /// Capture one install failure for a specific canister id.
205    #[must_use]
206    pub const fn new(canister_id: Principal, message: String) -> Self {
207        Self {
208            canister_id,
209            message,
210        }
211    }
212
213    /// Read the canister id that failed to install.
214    #[must_use]
215    pub const fn canister_id(&self) -> Principal {
216        self.canister_id
217    }
218
219    /// Read the captured panic message from the install attempt.
220    #[must_use]
221    pub fn message(&self) -> &str {
222        &self.message
223    }
224}
225
226impl std::fmt::Display for PicInstallError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        write!(
229            f,
230            "failed to install canister {}: {}",
231            self.canister_id, self.message
232        )
233    }
234}
235
236impl std::error::Error for PicInstallError {}
237
238impl std::fmt::Display for StandaloneCanisterFixtureError {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        match self {
241            Self::SerialGuard(err) => write!(f, "{err}"),
242            Self::Start(err) => write!(f, "{err}"),
243            Self::Install(err) => write!(f, "{err}"),
244        }
245    }
246}
247
248impl std::error::Error for StandaloneCanisterFixtureError {
249    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
250        match self {
251            Self::SerialGuard(err) => Some(err),
252            Self::Start(err) => Some(err),
253            Self::Install(err) => Some(err),
254        }
255    }
256}
257
258///
259/// Pic
260/// Thin wrapper around a PocketIC instance.
261///
262/// This type intentionally exposes only a minimal API surface; callers should
263/// use `pic()` to obtain an instance and then perform installs/calls.
264/// Callers must hold a `PicSerialGuard` for the full `Pic` lifetime.
265///
266
267pub struct Pic {
268    inner: PocketIc,
269}
270
271impl Pic {
272    /// Capture the current PocketIC wall-clock time as nanoseconds since epoch.
273    #[must_use]
274    pub fn current_time_nanos(&self) -> u64 {
275        self.inner.get_time().as_nanos_since_unix_epoch()
276    }
277
278    /// Restore PocketIC wall-clock and certified time from a captured nanosecond value.
279    pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
280        let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
281        self.inner.set_time(restored);
282        self.inner.set_certified_time(restored);
283    }
284
285    /// Install a root canister with the default root init arguments.
286    pub fn create_and_install_root_canister(&self, wasm: Vec<u8>) -> Result<Principal, Error> {
287        let init_bytes = install_root_args()?;
288
289        Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
290    }
291
292    /// Install a canister with the given type and wasm bytes.
293    ///
294    /// Install failures are treated as fatal in tests.
295    pub fn create_and_install_canister(
296        &self,
297        role: CanisterRole,
298        wasm: Vec<u8>,
299    ) -> Result<Principal, Error> {
300        let init_bytes = install_args(role)?;
301
302        Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
303    }
304
305    /// Install one arbitrary wasm module with caller-provided init bytes.
306    ///
307    /// This is the generic install path for downstreams that use `canic-testkit`
308    /// without depending on Canic canister init payload conventions.
309    #[must_use]
310    pub fn create_and_install_with_args(
311        &self,
312        wasm: Vec<u8>,
313        init_bytes: Vec<u8>,
314        install_cycles: u128,
315    ) -> Principal {
316        self.try_create_and_install_with_args(wasm, init_bytes, install_cycles)
317            .unwrap_or_else(|err| panic!("{err}"))
318    }
319
320    /// Install one arbitrary wasm module with caller-provided init bytes.
321    pub fn try_create_and_install_with_args(
322        &self,
323        wasm: Vec<u8>,
324        init_bytes: Vec<u8>,
325        install_cycles: u128,
326    ) -> Result<Principal, PicInstallError> {
327        self.try_create_funded_and_install(wasm, init_bytes, install_cycles)
328    }
329
330    /// Wait until one canister reports `canic_ready`.
331    pub fn wait_for_ready(&self, canister_id: Principal, tick_limit: usize, context: &str) {
332        for _ in 0..tick_limit {
333            self.tick();
334            if self.fetch_ready(canister_id) {
335                return;
336            }
337        }
338
339        self.dump_canister_debug(canister_id, context);
340        panic!("{context}: canister {canister_id} did not become ready after {tick_limit} ticks");
341    }
342
343    /// Wait until all provided canisters report `canic_ready`.
344    pub fn wait_for_all_ready<I>(&self, canister_ids: I, tick_limit: usize, context: &str)
345    where
346        I: IntoIterator<Item = Principal>,
347    {
348        let canister_ids = canister_ids.into_iter().collect::<Vec<_>>();
349
350        for _ in 0..tick_limit {
351            self.tick();
352            if canister_ids
353                .iter()
354                .copied()
355                .all(|canister_id| self.fetch_ready(canister_id))
356            {
357                return;
358            }
359        }
360
361        for canister_id in &canister_ids {
362            self.dump_canister_debug(*canister_id, context);
363        }
364        panic!("{context}: canisters did not become ready after {tick_limit} ticks");
365    }
366
367    /// Wait out the PocketIC `install_code` cooldown window inside the same instance.
368    pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
369        self.advance_time(cooldown);
370        self.tick_n(2);
371    }
372
373    /// Retry one install_code-like operation while PocketIC still reports rate limiting.
374    pub fn retry_install_code_ok<T, F>(
375        &self,
376        retry_limit: usize,
377        cooldown: Duration,
378        mut op: F,
379    ) -> Result<T, String>
380    where
381        F: FnMut() -> Result<T, String>,
382    {
383        let mut last_err = None;
384
385        for _ in 0..retry_limit {
386            match op() {
387                Ok(value) => return Ok(value),
388                Err(err) if is_install_code_rate_limited(&err) => {
389                    last_err = Some(err);
390                    self.wait_out_install_code_rate_limit(cooldown);
391                }
392                Err(err) => return Err(err),
393            }
394        }
395
396        Err(last_err.unwrap_or_else(|| "install_code retry loop exhausted".to_string()))
397    }
398
399    /// Retry one install_code-like failure path while PocketIC still reports rate limiting.
400    pub fn retry_install_code_err<F>(
401        &self,
402        retry_limit: usize,
403        cooldown: Duration,
404        first: Result<(), String>,
405        mut op: F,
406    ) -> Result<(), String>
407    where
408        F: FnMut() -> Result<(), String>,
409    {
410        match first {
411            Ok(()) => return Ok(()),
412            Err(err) if !is_install_code_rate_limited(&err) => return Err(err),
413            Err(_) => {}
414        }
415
416        self.wait_out_install_code_rate_limit(cooldown);
417
418        for _ in 1..retry_limit {
419            match op() {
420                Ok(()) => return Ok(()),
421                Err(err) if is_install_code_rate_limited(&err) => {
422                    self.wait_out_install_code_rate_limit(cooldown);
423                }
424                Err(err) => return Err(err),
425            }
426        }
427
428        op()
429    }
430
431    /// Dump basic PocketIC status and log context for one canister.
432    pub fn dump_canister_debug(&self, canister_id: Principal, context: &str) {
433        eprintln!("{context}: debug for canister {canister_id}");
434
435        match self.canister_status(canister_id, None) {
436            Ok(status) => eprintln!("canister_status: {status:?}"),
437            Err(err) => eprintln!("canister_status failed: {err:?}"),
438        }
439
440        match self.fetch_canister_logs(canister_id, Principal::anonymous()) {
441            Ok(records) => {
442                if records.is_empty() {
443                    eprintln!("canister logs: <empty>");
444                } else {
445                    for record in records {
446                        eprintln!("canister log: {record:?}");
447                    }
448                }
449            }
450            Err(err) => eprintln!("fetch_canister_logs failed: {err:?}"),
451        }
452    }
453
454    /// Capture one restorable snapshot per canister using a shared controller.
455    pub fn capture_controller_snapshots<I>(
456        &self,
457        controller_id: Principal,
458        canister_ids: I,
459    ) -> Option<ControllerSnapshots>
460    where
461        I: IntoIterator<Item = Principal>,
462    {
463        let mut snapshots = HashMap::new();
464
465        for canister_id in canister_ids {
466            let Some(snapshot) = self.try_take_controller_snapshot(controller_id, canister_id)
467            else {
468                eprintln!(
469                    "capture_controller_snapshots: snapshot capture unavailable for {canister_id}"
470                );
471                return None;
472            };
473            snapshots.insert(canister_id, snapshot);
474        }
475
476        Some(ControllerSnapshots::new(snapshots))
477    }
478
479    /// Restore a previously captured snapshot set using the same controller.
480    pub fn restore_controller_snapshots(
481        &self,
482        controller_id: Principal,
483        snapshots: &ControllerSnapshots,
484    ) {
485        for (canister_id, snapshot_id, sender) in snapshots.iter() {
486            self.restore_controller_snapshot(controller_id, canister_id, sender, snapshot_id);
487        }
488    }
489
490    /// Generic update call helper (serializes args + decodes result).
491    pub fn update_call<T, A>(
492        &self,
493        canister_id: Principal,
494        method: &str,
495        args: A,
496    ) -> Result<T, Error>
497    where
498        T: CandidType + DeserializeOwned,
499        A: ArgumentEncoder,
500    {
501        let bytes: Vec<u8> = encode_args(args)
502            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
503        let result = self
504            .inner
505            .update_call(canister_id, Principal::anonymous(), method, bytes)
506            .map_err(|err| {
507                Error::internal(format!(
508                    "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
509                ))
510            })?;
511
512        decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
513    }
514
515    /// Generic update call helper with an explicit caller principal.
516    pub fn update_call_as<T, A>(
517        &self,
518        canister_id: Principal,
519        caller: Principal,
520        method: &str,
521        args: A,
522    ) -> Result<T, Error>
523    where
524        T: CandidType + DeserializeOwned,
525        A: ArgumentEncoder,
526    {
527        let bytes: Vec<u8> = encode_args(args)
528            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
529        let result = self
530            .inner
531            .update_call(canister_id, caller, method, bytes)
532            .map_err(|err| {
533                Error::internal(format!(
534                    "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
535                ))
536            })?;
537
538        decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
539    }
540
541    /// Generic query call helper.
542    pub fn query_call<T, A>(
543        &self,
544        canister_id: Principal,
545        method: &str,
546        args: A,
547    ) -> Result<T, Error>
548    where
549        T: CandidType + DeserializeOwned,
550        A: ArgumentEncoder,
551    {
552        let bytes: Vec<u8> = encode_args(args)
553            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
554        let result = self
555            .inner
556            .query_call(canister_id, Principal::anonymous(), method, bytes)
557            .map_err(|err| {
558                Error::internal(format!(
559                    "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
560                ))
561            })?;
562
563        decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
564    }
565
566    /// Generic query call helper with an explicit caller principal.
567    pub fn query_call_as<T, A>(
568        &self,
569        canister_id: Principal,
570        caller: Principal,
571        method: &str,
572        args: A,
573    ) -> Result<T, Error>
574    where
575        T: CandidType + DeserializeOwned,
576        A: ArgumentEncoder,
577    {
578        let bytes: Vec<u8> = encode_args(args)
579            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
580        let result = self
581            .inner
582            .query_call(canister_id, caller, method, bytes)
583            .map_err(|err| {
584                Error::internal(format!(
585                    "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
586                ))
587            })?;
588
589        decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
590    }
591
592    /// Advance PocketIC by a fixed number of ticks.
593    pub fn tick_n(&self, times: usize) {
594        for _ in 0..times {
595            self.tick();
596        }
597    }
598
599    // Install a canister after creating it and funding it with cycles.
600    fn try_create_funded_and_install(
601        &self,
602        wasm: Vec<u8>,
603        init_bytes: Vec<u8>,
604        install_cycles: u128,
605    ) -> Result<Principal, PicInstallError> {
606        let canister_id = self.create_canister();
607        self.add_cycles(canister_id, install_cycles);
608
609        let install = catch_unwind(AssertUnwindSafe(|| {
610            self.inner
611                .install_canister(canister_id, wasm, init_bytes, None);
612        }));
613        if let Err(payload) = install {
614            eprintln!("install_canister trapped for {canister_id}");
615            if let Ok(status) = self.inner.canister_status(canister_id, None) {
616                eprintln!("canister_status for {canister_id}: {status:?}");
617            }
618            if let Ok(logs) = self
619                .inner
620                .fetch_canister_logs(canister_id, Principal::anonymous())
621            {
622                for record in logs {
623                    eprintln!("canister_log {canister_id}: {record:?}");
624                }
625            }
626            return Err(PicInstallError::new(
627                canister_id,
628                startup::panic_payload_to_string(payload.as_ref()),
629            ));
630        }
631
632        Ok(canister_id)
633    }
634
635    // Query `canic_ready` and panic with debug context on transport failures.
636    fn fetch_ready(&self, canister_id: Principal) -> bool {
637        match self.query_call(canister_id, protocol::CANIC_READY, ()) {
638            Ok(ready) => ready,
639            Err(err) => {
640                self.dump_canister_debug(canister_id, "query canic_ready failed");
641                panic!("query canic_ready failed: {err:?}");
642            }
643        }
644    }
645
646    // Capture one snapshot with sender fallbacks that match controller ownership.
647    fn try_take_controller_snapshot(
648        &self,
649        controller_id: Principal,
650        canister_id: Principal,
651    ) -> Option<(Vec<u8>, Option<Principal>)> {
652        let candidates = controller_sender_candidates(controller_id, canister_id);
653        let mut last_err = None;
654
655        for sender in candidates {
656            match self.take_canister_snapshot(canister_id, sender, None) {
657                Ok(snapshot) => return Some((snapshot.id, sender)),
658                Err(err) => last_err = Some((sender, err)),
659            }
660        }
661
662        if let Some((sender, err)) = last_err {
663            eprintln!(
664                "failed to capture canister snapshot for {canister_id} using sender {sender:?}: {err}"
665            );
666        }
667        None
668    }
669
670    // Restore one snapshot with sender fallbacks that match controller ownership.
671    fn restore_controller_snapshot(
672        &self,
673        controller_id: Principal,
674        canister_id: Principal,
675        snapshot_sender: Option<Principal>,
676        snapshot_id: &[u8],
677    ) {
678        let fallback_sender = if snapshot_sender.is_some() {
679            None
680        } else {
681            Some(controller_id)
682        };
683        let candidates = [snapshot_sender, fallback_sender];
684        let mut last_err = None;
685
686        for sender in candidates {
687            match self.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec()) {
688                Ok(()) => return,
689                Err(err) => last_err = Some((sender, err)),
690            }
691        }
692
693        let (sender, err) =
694            last_err.expect("snapshot restore must have at least one sender attempt");
695        panic!(
696            "failed to restore canister snapshot for {canister_id} using sender {sender:?}: {err}"
697        );
698    }
699}
700
701fn is_install_code_rate_limited(message: &str) -> bool {
702    message.contains("CanisterInstallCodeRateLimited")
703}
704
705impl Deref for Pic {
706    type Target = PocketIc;
707
708    fn deref(&self) -> &Self::Target {
709        &self.inner
710    }
711}
712
713impl DerefMut for Pic {
714    fn deref_mut(&mut self) -> &mut Self::Target {
715        &mut self.inner
716    }
717}
718
719/// --------------------------------------
720/// install_args helper
721/// --------------------------------------
722///
723/// Init semantics:
724/// - Root canisters receive a `SubnetIdentity` (direct root bootstrap).
725/// - Non-root canisters receive `EnvBootstrapArgs` + optional directory snapshots.
726///
727/// Directory handling:
728/// - By default, directory views are empty for standalone installs.
729/// - Directory-dependent logic is opt-in via `install_args_with_directories`.
730/// - Root-provisioned installs will populate directories via cascade.
731///
732
733fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
734    if role.is_root() {
735        install_root_args()
736    } else {
737        // Non-root standalone install.
738        // Provide only what is structurally known at install time.
739        let env = EnvBootstrapArgs {
740            prime_root_pid: None,
741            subnet_role: None,
742            subnet_pid: None,
743            root_pid: None,
744            canister_role: Some(role),
745            parent_pid: None,
746        };
747
748        // Intentional: standalone installs do not require directories unless
749        // a test explicitly exercises directory-dependent behavior.
750        let payload = CanisterInitPayload {
751            env,
752            app_directory: AppDirectoryArgs(Vec::new()),
753            subnet_directory: SubnetDirectoryArgs(Vec::new()),
754        };
755
756        encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
757            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
758    }
759}
760
761fn install_root_args() -> Result<Vec<u8>, Error> {
762    encode_one(SubnetIdentity::Manual)
763        .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
764}
765
766// Prefer the likely controller sender first to reduce noisy management-call failures.
767fn controller_sender_candidates(
768    controller_id: Principal,
769    canister_id: Principal,
770) -> [Option<Principal>; 2] {
771    if canister_id == controller_id {
772        [None, Some(controller_id)]
773    } else {
774        [Some(controller_id), None]
775    }
776}