Skip to main content

canic_core/
replay_policy.rs

1//! Replay policy inventory for Canic-owned endpoint surfaces.
2//!
3//! This is Slice A scaffolding for the 0.61 replay-safety work. It records the
4//! intended replay and cost policy for endpoints emitted by Canic macros. Later
5//! slices wire these classifications into shared replay receipts and cost
6//! guards.
7
8///
9/// EndpointKind
10///
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum EndpointKind {
13    Query,
14    Update,
15}
16
17///
18/// ReplayPolicy
19///
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum ReplayPolicy {
22    QueryOrReadOnly,
23    ResponseIdempotent {
24        command_kind: &'static str,
25    },
26    ReplayProtected {
27        command_kind: &'static str,
28        requires_operation_id: bool,
29    },
30    MonotonicTransition {
31        command_kind: &'static str,
32    },
33    SnapshotConvergent {
34        command_kind: &'static str,
35    },
36    CommandDispatch {
37        command_kind: &'static str,
38        command_manifest: &'static str,
39    },
40    IntentionallyNonIdempotent {
41        command_kind: &'static str,
42        reason: &'static str,
43    },
44}
45
46///
47/// CostClass
48///
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum CostClass {
51    None,
52    RootCanisterSignaturePrepare,
53    IssuerCanisterSignaturePrepare,
54    ManagementDeployment,
55    ValueTransfer,
56    DurablePublish,
57}
58
59///
60/// ReplayImplementationStatus
61///
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum ReplayImplementationStatus {
64    Implemented,
65    ReleaseBlocker,
66}
67
68///
69/// EndpointReplayPolicy
70///
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct EndpointReplayPolicy {
73    pub endpoint: &'static str,
74    pub endpoint_kind: EndpointKind,
75    pub replay_policy: ReplayPolicy,
76    pub implementation_status: ReplayImplementationStatus,
77    pub cost_class: CostClass,
78    pub quota_policy: Option<&'static str>,
79    pub cycle_reserve_policy: Option<&'static str>,
80}
81
82///
83/// PoolAdminCommandReplayPolicy
84///
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct PoolAdminCommandReplayPolicy {
87    pub variant: &'static str,
88    pub replay_policy: ReplayPolicy,
89    pub implementation_status: ReplayImplementationStatus,
90    pub cost_class: CostClass,
91    pub quota_policy: Option<&'static str>,
92    pub cycle_reserve_policy: Option<&'static str>,
93}
94
95///
96/// RootCapabilityCommandReplayPolicy
97///
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub struct RootCapabilityCommandReplayPolicy {
100    pub variant: &'static str,
101    pub replay_policy: ReplayPolicy,
102    pub implementation_status: ReplayImplementationStatus,
103    pub cost_class: CostClass,
104    pub quota_policy: Option<&'static str>,
105    pub cycle_reserve_policy: Option<&'static str>,
106}
107
108const ROOT_CANISTER_SIGNATURE_PREPARE_QUOTA_V1: &str = "root_canister_signature_prepare.quota.v1";
109const ISSUER_CANISTER_SIGNATURE_PREPARE_QUOTA_V1: &str =
110    "issuer_canister_signature_prepare.quota.v1";
111const DEPLOYMENT_QUOTA_V1: &str = "deployment.quota.v1";
112const DEPLOYMENT_RESERVE_V1: &str = "deployment.cycle_reserve.v1";
113const VALUE_TRANSFER_QUOTA_V1: &str = "value_transfer.quota.v1";
114const VALUE_TRANSFER_RESERVE_V1: &str = "value_transfer.cycle_reserve.v1";
115const DURABLE_PUBLISH_QUOTA_V1: &str = "durable_publish.quota.v1";
116const DURABLE_PUBLISH_RESERVE_V1: &str = "durable_publish.cycle_reserve.v1";
117
118pub const ENDPOINT_REPLAY_POLICY_MANIFEST: &[EndpointReplayPolicy] = &[
119    update_response_idempotent("canic_app", "app.command.v1"),
120    update_read_only("canic_canister_status"),
121    update_costed_response_idempotent(
122        "canic_canister_upgrade",
123        "management.canister_upgrade.v1",
124        CostClass::ManagementDeployment,
125        Some(DEPLOYMENT_QUOTA_V1),
126        Some(DEPLOYMENT_RESERVE_V1),
127    ),
128    update_replay_protected(
129        "canic_icp_refill",
130        "icp.refill.v1",
131        ReplayImplementationStatus::Implemented,
132        CostClass::ValueTransfer,
133        Some(VALUE_TRANSFER_QUOTA_V1),
134        Some(VALUE_TRANSFER_RESERVE_V1),
135    ),
136    update_command_dispatch(
137        "canic_pool_admin",
138        "pool.admin.v1",
139        "pool.admin.command_manifest.v1",
140        ReplayImplementationStatus::Implemented,
141        CostClass::ManagementDeployment,
142        Some(DEPLOYMENT_QUOTA_V1),
143        Some(DEPLOYMENT_RESERVE_V1),
144    ),
145    update_replay_protected(
146        "canic_prepare_delegation_proof",
147        "auth.prepare_delegation_proof.v1",
148        ReplayImplementationStatus::Implemented,
149        CostClass::RootCanisterSignaturePrepare,
150        Some(ROOT_CANISTER_SIGNATURE_PREPARE_QUOTA_V1),
151        None,
152    ),
153    update_replay_protected(
154        "canic_prepare_role_attestation",
155        "auth.prepare_role_attestation.v1",
156        ReplayImplementationStatus::Implemented,
157        CostClass::RootCanisterSignaturePrepare,
158        Some(ROOT_CANISTER_SIGNATURE_PREPARE_QUOTA_V1),
159        None,
160    ),
161    query_read_only("canic_get_role_attestation"),
162    update_command_dispatch(
163        "canic_response_capability_v1",
164        "root.capability_rpc.v1",
165        "root.capability.command_manifest.v1",
166        ReplayImplementationStatus::Implemented,
167        CostClass::ManagementDeployment,
168        Some(DEPLOYMENT_QUOTA_V1),
169        Some(DEPLOYMENT_RESERVE_V1),
170    ),
171    update_snapshot_convergent("canic_sync_state", "cascade.sync_state.v1"),
172    update_snapshot_convergent("canic_sync_topology", "cascade.sync_topology.v1"),
173    update_intentionally_non_idempotent(
174        "canic_install_active_delegation_proof",
175        "auth.install_active_delegation_proof.v1",
176        "controller maintenance endpoint replaces issuer-local active proof metadata",
177    ),
178    update_replay_protected(
179        "canic_prepare_delegated_token",
180        "auth.prepare_delegated_token.v1",
181        ReplayImplementationStatus::Implemented,
182        CostClass::IssuerCanisterSignaturePrepare,
183        Some(ISSUER_CANISTER_SIGNATURE_PREPARE_QUOTA_V1),
184        None,
185    ),
186    query_read_only("canic_get_delegated_token"),
187    update_monotonic_publish(
188        "canic_template_prepare_admin",
189        "wasm_store.template_prepare_admin.v1",
190    ),
191    update_monotonic_publish(
192        "canic_template_publish_chunk_admin",
193        "wasm_store.template_publish_chunk_admin.v1",
194    ),
195    update_monotonic_publish(
196        "canic_template_stage_manifest_admin",
197        "wasm_store.template_stage_manifest_admin.v1",
198    ),
199    update_response_idempotent(
200        "canic_wasm_store_bootstrap_resume_root_admin",
201        "wasm_store.bootstrap_resume.ensure_v1",
202    ),
203    update_monotonic_publish("canic_wasm_store_admin", "wasm_store.admin.v1"),
204    update_monotonic_publish("canic_wasm_store_begin_gc", "wasm_store.begin_gc.v1"),
205    update_monotonic_publish("canic_wasm_store_chunk", "wasm_store.chunk.v1"),
206    update_monotonic_publish("canic_wasm_store_complete_gc", "wasm_store.complete_gc.v1"),
207    update_monotonic_publish("canic_wasm_store_info", "wasm_store.info.v1"),
208    update_monotonic_publish("canic_wasm_store_prepare", "wasm_store.prepare.v1"),
209    update_monotonic_publish("canic_wasm_store_prepare_gc", "wasm_store.prepare_gc.v1"),
210    update_monotonic_publish(
211        "canic_wasm_store_publish_chunk",
212        "wasm_store.publish_chunk.v1",
213    ),
214    update_monotonic_publish(
215        "canic_wasm_store_stage_manifest",
216        "wasm_store.stage_manifest.v1",
217    ),
218];
219
220pub const POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST: &[PoolAdminCommandReplayPolicy] = &[
221    pool_admin_replay_protected(
222        "CreateEmpty",
223        "pool.create_empty.v1",
224        ReplayImplementationStatus::Implemented,
225        CostClass::ManagementDeployment,
226        Some(DEPLOYMENT_QUOTA_V1),
227        Some(DEPLOYMENT_RESERVE_V1),
228    ),
229    pool_admin_response_idempotent(
230        "Recycle",
231        "pool.recycle.ensure_v1",
232        ReplayImplementationStatus::Implemented,
233        CostClass::ManagementDeployment,
234        Some(DEPLOYMENT_QUOTA_V1),
235        Some(DEPLOYMENT_RESERVE_V1),
236    ),
237    pool_admin_response_idempotent(
238        "ImportImmediate",
239        "pool.import_immediate.ensure_v1",
240        ReplayImplementationStatus::Implemented,
241        CostClass::ManagementDeployment,
242        Some(DEPLOYMENT_QUOTA_V1),
243        Some(DEPLOYMENT_RESERVE_V1),
244    ),
245    pool_admin_snapshot_convergent(
246        "ImportQueued",
247        "pool.import_queued.ensure_v1",
248        ReplayImplementationStatus::Implemented,
249        CostClass::None,
250        None,
251        None,
252    ),
253];
254
255pub const ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST: &[RootCapabilityCommandReplayPolicy] = &[
256    root_capability_replay_protected(
257        "ProvisionCanister",
258        "root.provision.v1",
259        ReplayImplementationStatus::Implemented,
260        CostClass::ManagementDeployment,
261        Some(DEPLOYMENT_QUOTA_V1),
262        Some(DEPLOYMENT_RESERVE_V1),
263    ),
264    root_capability_replay_protected(
265        "UpgradeCanister",
266        "root.upgrade.v1",
267        ReplayImplementationStatus::Implemented,
268        CostClass::ManagementDeployment,
269        Some(DEPLOYMENT_QUOTA_V1),
270        Some(DEPLOYMENT_RESERVE_V1),
271    ),
272    root_capability_replay_protected(
273        "RecycleCanister",
274        "root.recycle_canister.v1",
275        ReplayImplementationStatus::Implemented,
276        CostClass::ManagementDeployment,
277        Some(DEPLOYMENT_QUOTA_V1),
278        Some(DEPLOYMENT_RESERVE_V1),
279    ),
280    root_capability_replay_protected(
281        "RequestCycles",
282        "root.request_cycles.v1",
283        ReplayImplementationStatus::Implemented,
284        CostClass::ValueTransfer,
285        Some(VALUE_TRANSFER_QUOTA_V1),
286        Some(VALUE_TRANSFER_RESERVE_V1),
287    ),
288];
289
290#[must_use]
291pub const fn endpoint_replay_policy_manifest() -> &'static [EndpointReplayPolicy] {
292    ENDPOINT_REPLAY_POLICY_MANIFEST
293}
294
295#[must_use]
296pub const fn pool_admin_command_replay_policy_manifest() -> &'static [PoolAdminCommandReplayPolicy]
297{
298    POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
299}
300
301#[must_use]
302pub const fn root_capability_command_replay_policy_manifest()
303-> &'static [RootCapabilityCommandReplayPolicy] {
304    ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST
305}
306
307const fn update_response_idempotent(
308    endpoint: &'static str,
309    command_kind: &'static str,
310) -> EndpointReplayPolicy {
311    EndpointReplayPolicy {
312        endpoint,
313        endpoint_kind: EndpointKind::Update,
314        replay_policy: ReplayPolicy::ResponseIdempotent { command_kind },
315        implementation_status: ReplayImplementationStatus::Implemented,
316        cost_class: CostClass::None,
317        quota_policy: None,
318        cycle_reserve_policy: None,
319    }
320}
321
322const fn update_costed_response_idempotent(
323    endpoint: &'static str,
324    command_kind: &'static str,
325    cost_class: CostClass,
326    quota_policy: Option<&'static str>,
327    cycle_reserve_policy: Option<&'static str>,
328) -> EndpointReplayPolicy {
329    EndpointReplayPolicy {
330        endpoint,
331        endpoint_kind: EndpointKind::Update,
332        replay_policy: ReplayPolicy::ResponseIdempotent { command_kind },
333        implementation_status: ReplayImplementationStatus::Implemented,
334        cost_class,
335        quota_policy,
336        cycle_reserve_policy,
337    }
338}
339
340const fn update_read_only(endpoint: &'static str) -> EndpointReplayPolicy {
341    EndpointReplayPolicy {
342        endpoint,
343        endpoint_kind: EndpointKind::Update,
344        replay_policy: ReplayPolicy::QueryOrReadOnly,
345        implementation_status: ReplayImplementationStatus::Implemented,
346        cost_class: CostClass::None,
347        quota_policy: None,
348        cycle_reserve_policy: None,
349    }
350}
351
352const fn query_read_only(endpoint: &'static str) -> EndpointReplayPolicy {
353    EndpointReplayPolicy {
354        endpoint,
355        endpoint_kind: EndpointKind::Query,
356        replay_policy: ReplayPolicy::QueryOrReadOnly,
357        implementation_status: ReplayImplementationStatus::Implemented,
358        cost_class: CostClass::None,
359        quota_policy: None,
360        cycle_reserve_policy: None,
361    }
362}
363
364const fn update_replay_protected(
365    endpoint: &'static str,
366    command_kind: &'static str,
367    implementation_status: ReplayImplementationStatus,
368    cost_class: CostClass,
369    quota_policy: Option<&'static str>,
370    cycle_reserve_policy: Option<&'static str>,
371) -> EndpointReplayPolicy {
372    EndpointReplayPolicy {
373        endpoint,
374        endpoint_kind: EndpointKind::Update,
375        replay_policy: ReplayPolicy::ReplayProtected {
376            command_kind,
377            requires_operation_id: true,
378        },
379        implementation_status,
380        cost_class,
381        quota_policy,
382        cycle_reserve_policy,
383    }
384}
385
386const fn update_monotonic_publish(
387    endpoint: &'static str,
388    command_kind: &'static str,
389) -> EndpointReplayPolicy {
390    EndpointReplayPolicy {
391        endpoint,
392        endpoint_kind: EndpointKind::Update,
393        replay_policy: ReplayPolicy::MonotonicTransition { command_kind },
394        implementation_status: ReplayImplementationStatus::Implemented,
395        cost_class: CostClass::DurablePublish,
396        quota_policy: Some(DURABLE_PUBLISH_QUOTA_V1),
397        cycle_reserve_policy: Some(DURABLE_PUBLISH_RESERVE_V1),
398    }
399}
400
401const fn update_snapshot_convergent(
402    endpoint: &'static str,
403    command_kind: &'static str,
404) -> EndpointReplayPolicy {
405    EndpointReplayPolicy {
406        endpoint,
407        endpoint_kind: EndpointKind::Update,
408        replay_policy: ReplayPolicy::SnapshotConvergent { command_kind },
409        implementation_status: ReplayImplementationStatus::Implemented,
410        cost_class: CostClass::None,
411        quota_policy: None,
412        cycle_reserve_policy: None,
413    }
414}
415
416const fn update_intentionally_non_idempotent(
417    endpoint: &'static str,
418    command_kind: &'static str,
419    reason: &'static str,
420) -> EndpointReplayPolicy {
421    EndpointReplayPolicy {
422        endpoint,
423        endpoint_kind: EndpointKind::Update,
424        replay_policy: ReplayPolicy::IntentionallyNonIdempotent {
425            command_kind,
426            reason,
427        },
428        implementation_status: ReplayImplementationStatus::Implemented,
429        cost_class: CostClass::None,
430        quota_policy: None,
431        cycle_reserve_policy: None,
432    }
433}
434
435const fn update_command_dispatch(
436    endpoint: &'static str,
437    command_kind: &'static str,
438    command_manifest: &'static str,
439    implementation_status: ReplayImplementationStatus,
440    cost_class: CostClass,
441    quota_policy: Option<&'static str>,
442    cycle_reserve_policy: Option<&'static str>,
443) -> EndpointReplayPolicy {
444    EndpointReplayPolicy {
445        endpoint,
446        endpoint_kind: EndpointKind::Update,
447        replay_policy: ReplayPolicy::CommandDispatch {
448            command_kind,
449            command_manifest,
450        },
451        implementation_status,
452        cost_class,
453        quota_policy,
454        cycle_reserve_policy,
455    }
456}
457
458const fn pool_admin_response_idempotent(
459    variant: &'static str,
460    command_kind: &'static str,
461    implementation_status: ReplayImplementationStatus,
462    cost_class: CostClass,
463    quota_policy: Option<&'static str>,
464    cycle_reserve_policy: Option<&'static str>,
465) -> PoolAdminCommandReplayPolicy {
466    PoolAdminCommandReplayPolicy {
467        variant,
468        replay_policy: ReplayPolicy::ResponseIdempotent { command_kind },
469        implementation_status,
470        cost_class,
471        quota_policy,
472        cycle_reserve_policy,
473    }
474}
475
476const fn pool_admin_replay_protected(
477    variant: &'static str,
478    command_kind: &'static str,
479    implementation_status: ReplayImplementationStatus,
480    cost_class: CostClass,
481    quota_policy: Option<&'static str>,
482    cycle_reserve_policy: Option<&'static str>,
483) -> PoolAdminCommandReplayPolicy {
484    PoolAdminCommandReplayPolicy {
485        variant,
486        replay_policy: ReplayPolicy::ReplayProtected {
487            command_kind,
488            requires_operation_id: true,
489        },
490        implementation_status,
491        cost_class,
492        quota_policy,
493        cycle_reserve_policy,
494    }
495}
496
497const fn pool_admin_snapshot_convergent(
498    variant: &'static str,
499    command_kind: &'static str,
500    implementation_status: ReplayImplementationStatus,
501    cost_class: CostClass,
502    quota_policy: Option<&'static str>,
503    cycle_reserve_policy: Option<&'static str>,
504) -> PoolAdminCommandReplayPolicy {
505    PoolAdminCommandReplayPolicy {
506        variant,
507        replay_policy: ReplayPolicy::SnapshotConvergent { command_kind },
508        implementation_status,
509        cost_class,
510        quota_policy,
511        cycle_reserve_policy,
512    }
513}
514
515const fn root_capability_replay_protected(
516    variant: &'static str,
517    command_kind: &'static str,
518    implementation_status: ReplayImplementationStatus,
519    cost_class: CostClass,
520    quota_policy: Option<&'static str>,
521    cycle_reserve_policy: Option<&'static str>,
522) -> RootCapabilityCommandReplayPolicy {
523    RootCapabilityCommandReplayPolicy {
524        variant,
525        replay_policy: ReplayPolicy::ReplayProtected {
526            command_kind,
527            requires_operation_id: true,
528        },
529        implementation_status,
530        cost_class,
531        quota_policy,
532        cycle_reserve_policy,
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::protocol::{
540        CANIC_TEMPLATE_PREPARE_ADMIN, CANIC_TEMPLATE_PUBLISH_CHUNK_ADMIN,
541        CANIC_TEMPLATE_STAGE_MANIFEST_ADMIN, CANIC_WASM_STORE_ROOT_UPDATE_METHODS,
542    };
543    use std::collections::BTreeSet;
544
545    #[test]
546    fn endpoint_manifest_entries_are_unique() {
547        let mut seen = BTreeSet::new();
548        for entry in ENDPOINT_REPLAY_POLICY_MANIFEST {
549            assert!(
550                seen.insert(entry.endpoint),
551                "duplicate replay policy entry for {}",
552                entry.endpoint
553            );
554        }
555    }
556
557    #[test]
558    fn emitted_canic_update_endpoints_have_replay_policy_entries() {
559        let emitted = emitted_update_endpoint_names();
560        let manifest = ENDPOINT_REPLAY_POLICY_MANIFEST
561            .iter()
562            .filter(|entry| entry.endpoint_kind == EndpointKind::Update)
563            .map(|entry| entry.endpoint)
564            .collect::<BTreeSet<_>>();
565
566        let missing = emitted.difference(&manifest).copied().collect::<Vec<_>>();
567
568        assert!(
569            missing.is_empty(),
570            "missing replay policy entries for update endpoints: {missing:?}"
571        );
572    }
573
574    #[test]
575    fn costed_manifest_entries_declare_guards() {
576        for entry in ENDPOINT_REPLAY_POLICY_MANIFEST {
577            if entry.cost_class == CostClass::None {
578                continue;
579            }
580            assert!(
581                entry.quota_policy.is_some(),
582                "costed entry {} missing quota policy",
583                entry.endpoint
584            );
585            assert!(
586                entry.cost_class == CostClass::RootCanisterSignaturePrepare
587                    || entry.cost_class == CostClass::IssuerCanisterSignaturePrepare
588                    || entry.cycle_reserve_policy.is_some(),
589                "costed entry {} missing cycle-reserve policy",
590                entry.endpoint
591            );
592        }
593    }
594
595    #[test]
596    fn costed_pool_admin_command_entries_declare_guards() {
597        for entry in POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST {
598            if entry.cost_class == CostClass::None {
599                continue;
600            }
601            assert!(
602                entry.quota_policy.is_some(),
603                "costed pool admin command {} missing quota policy",
604                entry.variant
605            );
606            assert!(
607                entry.cycle_reserve_policy.is_some(),
608                "costed pool admin command {} missing cycle-reserve policy",
609                entry.variant
610            );
611        }
612    }
613
614    #[test]
615    fn costed_root_capability_command_entries_declare_guards() {
616        for entry in ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST {
617            if entry.cost_class == CostClass::None {
618                continue;
619            }
620            assert!(
621                entry.quota_policy.is_some(),
622                "costed root capability command {} missing quota policy",
623                entry.variant
624            );
625            assert!(
626                entry.cycle_reserve_policy.is_some(),
627                "costed root capability command {} missing cycle-reserve policy",
628                entry.variant
629            );
630        }
631    }
632
633    #[test]
634    fn release_candidate_manifests_have_no_release_blockers() {
635        let blockers = release_candidate_manifest_blockers();
636
637        assert!(
638            blockers.is_empty(),
639            "release candidate manifests still contain replay blockers: {blockers:?}"
640        );
641    }
642
643    #[test]
644    fn durable_publish_entries_are_wasm_store_publication_surfaces() {
645        let expected = durable_publish_endpoint_names();
646        let actual = ENDPOINT_REPLAY_POLICY_MANIFEST
647            .iter()
648            .filter(|entry| entry.cost_class == CostClass::DurablePublish)
649            .map(|entry| entry.endpoint)
650            .collect::<BTreeSet<_>>();
651
652        assert_eq!(
653            actual, expected,
654            "durable-publish cost class must stay scoped to wasm-store publication surfaces"
655        );
656
657        for endpoint in expected {
658            let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
659                .iter()
660                .find(|entry| entry.endpoint == endpoint)
661                .expect("durable publish endpoint entry");
662
663            assert_eq!(
664                entry.implementation_status,
665                ReplayImplementationStatus::Implemented
666            );
667            assert_eq!(entry.endpoint_kind, EndpointKind::Update);
668            assert!(
669                matches!(
670                    entry.replay_policy,
671                    ReplayPolicy::MonotonicTransition { .. }
672                ),
673                "{endpoint} must stay classified as monotonic publication"
674            );
675            assert_eq!(entry.quota_policy, Some(DURABLE_PUBLISH_QUOTA_V1));
676            assert_eq!(entry.cycle_reserve_policy, Some(DURABLE_PUBLISH_RESERVE_V1));
677        }
678    }
679
680    #[test]
681    fn delegation_proof_prepare_is_manifested_as_implemented() {
682        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
683            .iter()
684            .find(|entry| entry.endpoint == "canic_prepare_delegation_proof")
685            .expect("delegation prepare endpoint policy entry");
686
687        assert_eq!(
688            entry.implementation_status,
689            ReplayImplementationStatus::Implemented
690        );
691        assert_eq!(entry.cost_class, CostClass::RootCanisterSignaturePrepare);
692        assert_eq!(
693            entry.quota_policy,
694            Some(ROOT_CANISTER_SIGNATURE_PREPARE_QUOTA_V1)
695        );
696        assert_eq!(entry.cycle_reserve_policy, None);
697        assert_eq!(
698            entry.replay_policy,
699            ReplayPolicy::ReplayProtected {
700                command_kind: "auth.prepare_delegation_proof.v1",
701                requires_operation_id: true,
702            }
703        );
704    }
705
706    #[test]
707    fn active_delegation_proof_install_is_controller_maintenance() {
708        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
709            .iter()
710            .find(|entry| entry.endpoint == "canic_install_active_delegation_proof")
711            .expect("active delegation proof install policy entry");
712
713        assert_eq!(
714            entry.implementation_status,
715            ReplayImplementationStatus::Implemented
716        );
717        assert_eq!(entry.cost_class, CostClass::None);
718        assert_eq!(entry.quota_policy, None);
719        assert_eq!(entry.cycle_reserve_policy, None);
720        assert_eq!(
721            entry.replay_policy,
722            ReplayPolicy::IntentionallyNonIdempotent {
723                command_kind: "auth.install_active_delegation_proof.v1",
724                reason: "controller maintenance endpoint replaces issuer-local active proof metadata",
725            }
726        );
727    }
728
729    #[test]
730    fn delegated_token_prepare_get_endpoints_are_manifested() {
731        let prepare = ENDPOINT_REPLAY_POLICY_MANIFEST
732            .iter()
733            .find(|entry| entry.endpoint == "canic_prepare_delegated_token")
734            .expect("delegated-token prepare policy entry");
735
736        assert_eq!(
737            prepare.implementation_status,
738            ReplayImplementationStatus::Implemented
739        );
740        assert_eq!(
741            prepare.replay_policy,
742            ReplayPolicy::ReplayProtected {
743                command_kind: "auth.prepare_delegated_token.v1",
744                requires_operation_id: true,
745            }
746        );
747        assert_eq!(
748            prepare.cost_class,
749            CostClass::IssuerCanisterSignaturePrepare
750        );
751        assert_eq!(
752            prepare.quota_policy,
753            Some(ISSUER_CANISTER_SIGNATURE_PREPARE_QUOTA_V1)
754        );
755        assert_eq!(prepare.cycle_reserve_policy, None);
756
757        let get = ENDPOINT_REPLAY_POLICY_MANIFEST
758            .iter()
759            .find(|entry| entry.endpoint == "canic_get_delegated_token")
760            .expect("delegated-token get policy entry");
761
762        assert_eq!(get.endpoint_kind, EndpointKind::Query);
763        assert_eq!(get.replay_policy, ReplayPolicy::QueryOrReadOnly);
764        assert_eq!(get.cost_class, CostClass::None);
765    }
766
767    #[test]
768    fn role_attestation_prepare_get_endpoints_are_manifested() {
769        let prepare = ENDPOINT_REPLAY_POLICY_MANIFEST
770            .iter()
771            .find(|entry| entry.endpoint == "canic_prepare_role_attestation")
772            .expect("role-attestation prepare policy entry");
773
774        assert_eq!(
775            prepare.implementation_status,
776            ReplayImplementationStatus::Implemented
777        );
778        assert_eq!(
779            prepare.replay_policy,
780            ReplayPolicy::ReplayProtected {
781                command_kind: "auth.prepare_role_attestation.v1",
782                requires_operation_id: true,
783            }
784        );
785        assert_eq!(prepare.cost_class, CostClass::RootCanisterSignaturePrepare);
786        assert_eq!(
787            prepare.quota_policy,
788            Some(ROOT_CANISTER_SIGNATURE_PREPARE_QUOTA_V1)
789        );
790        assert_eq!(prepare.cycle_reserve_policy, None);
791
792        let get = ENDPOINT_REPLAY_POLICY_MANIFEST
793            .iter()
794            .find(|entry| entry.endpoint == "canic_get_role_attestation")
795            .expect("role-attestation get policy entry");
796
797        assert_eq!(get.endpoint_kind, EndpointKind::Query);
798        assert_eq!(get.replay_policy, ReplayPolicy::QueryOrReadOnly);
799        assert_eq!(get.cost_class, CostClass::None);
800    }
801
802    #[test]
803    fn canister_status_is_manifested_as_read_only() {
804        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
805            .iter()
806            .find(|entry| entry.endpoint == "canic_canister_status")
807            .expect("canister status policy entry");
808
809        assert_eq!(
810            entry.implementation_status,
811            ReplayImplementationStatus::Implemented
812        );
813        assert_eq!(entry.replay_policy, ReplayPolicy::QueryOrReadOnly);
814        assert_eq!(entry.cost_class, CostClass::None);
815        assert_eq!(entry.quota_policy, None);
816        assert_eq!(entry.cycle_reserve_policy, None);
817    }
818
819    #[test]
820    fn canister_upgrade_is_manifested_as_implemented_response_idempotent() {
821        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
822            .iter()
823            .find(|entry| entry.endpoint == "canic_canister_upgrade")
824            .expect("canister upgrade policy entry");
825
826        assert_eq!(
827            entry.implementation_status,
828            ReplayImplementationStatus::Implemented
829        );
830        assert_eq!(
831            entry.replay_policy,
832            ReplayPolicy::ResponseIdempotent {
833                command_kind: "management.canister_upgrade.v1",
834            }
835        );
836        assert_eq!(entry.cost_class, CostClass::ManagementDeployment);
837        assert_eq!(entry.quota_policy, Some(DEPLOYMENT_QUOTA_V1));
838        assert_eq!(entry.cycle_reserve_policy, Some(DEPLOYMENT_RESERVE_V1));
839    }
840
841    #[test]
842    fn icp_refill_is_manifested_as_implemented_value_transfer() {
843        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
844            .iter()
845            .find(|entry| entry.endpoint == "canic_icp_refill")
846            .expect("ICP refill policy entry");
847
848        assert_eq!(
849            entry.implementation_status,
850            ReplayImplementationStatus::Implemented
851        );
852        assert_eq!(
853            entry.replay_policy,
854            ReplayPolicy::ReplayProtected {
855                command_kind: "icp.refill.v1",
856                requires_operation_id: true,
857            }
858        );
859        assert_eq!(entry.cost_class, CostClass::ValueTransfer);
860        assert_eq!(entry.quota_policy, Some(VALUE_TRANSFER_QUOTA_V1));
861        assert_eq!(entry.cycle_reserve_policy, Some(VALUE_TRANSFER_RESERVE_V1));
862    }
863
864    #[test]
865    fn remaining_release_blockers_are_explicit_endpoint_slices() {
866        let blockers = ENDPOINT_REPLAY_POLICY_MANIFEST
867            .iter()
868            .filter(|entry| {
869                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
870            })
871            .map(|entry| entry.endpoint)
872            .collect::<BTreeSet<_>>();
873
874        assert!(blockers.is_empty(), "unexpected blockers: {blockers:?}");
875    }
876
877    #[test]
878    fn root_capability_command_variants_have_replay_policy_entries() {
879        let variants = root_capability_command_variant_names();
880        let manifest = ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST
881            .iter()
882            .map(|entry| entry.variant)
883            .collect::<BTreeSet<_>>();
884
885        assert_eq!(manifest, variants);
886    }
887
888    #[test]
889    fn root_capability_endpoint_is_manifested_as_command_dispatch() {
890        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
891            .iter()
892            .find(|entry| entry.endpoint == "canic_response_capability_v1")
893            .expect("root capability endpoint policy entry");
894
895        assert_eq!(
896            entry.implementation_status,
897            ReplayImplementationStatus::Implemented
898        );
899        assert_eq!(
900            entry.replay_policy,
901            ReplayPolicy::CommandDispatch {
902                command_kind: "root.capability_rpc.v1",
903                command_manifest: "root.capability.command_manifest.v1",
904            }
905        );
906        assert_eq!(entry.cost_class, CostClass::ManagementDeployment);
907        assert_eq!(entry.quota_policy, Some(DEPLOYMENT_QUOTA_V1));
908        assert_eq!(entry.cycle_reserve_policy, Some(DEPLOYMENT_RESERVE_V1));
909    }
910
911    #[test]
912    fn root_capability_command_blockers_are_explicit() {
913        let blockers = ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST
914            .iter()
915            .filter(|entry| {
916                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
917            })
918            .map(|entry| entry.variant)
919            .collect::<BTreeSet<_>>();
920
921        assert!(blockers.is_empty(), "unexpected blockers: {blockers:?}");
922    }
923
924    #[test]
925    fn root_capability_implemented_commands_are_replay_protected() {
926        for (variant, command_kind, cost_class) in [
927            (
928                "ProvisionCanister",
929                "root.provision.v1",
930                CostClass::ManagementDeployment,
931            ),
932            (
933                "UpgradeCanister",
934                "root.upgrade.v1",
935                CostClass::ManagementDeployment,
936            ),
937            (
938                "RecycleCanister",
939                "root.recycle_canister.v1",
940                CostClass::ManagementDeployment,
941            ),
942            (
943                "RequestCycles",
944                "root.request_cycles.v1",
945                CostClass::ValueTransfer,
946            ),
947        ] {
948            let entry = ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST
949                .iter()
950                .find(|entry| entry.variant == variant)
951                .expect("root capability command policy entry");
952
953            assert_eq!(
954                entry.implementation_status,
955                ReplayImplementationStatus::Implemented
956            );
957            assert_eq!(
958                entry.replay_policy,
959                ReplayPolicy::ReplayProtected {
960                    command_kind,
961                    requires_operation_id: true,
962                }
963            );
964            assert_eq!(entry.cost_class, cost_class);
965        }
966    }
967
968    #[test]
969    fn pool_admin_command_variants_have_replay_policy_entries() {
970        let variants = pool_admin_command_variant_names();
971        let manifest = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
972            .iter()
973            .map(|entry| entry.variant)
974            .collect::<BTreeSet<_>>();
975
976        assert_eq!(manifest, variants);
977    }
978
979    #[test]
980    fn pool_admin_endpoint_is_manifested_as_implemented_command_dispatch() {
981        let entry = ENDPOINT_REPLAY_POLICY_MANIFEST
982            .iter()
983            .find(|entry| entry.endpoint == "canic_pool_admin")
984            .expect("pool admin endpoint policy entry");
985
986        assert_eq!(
987            entry.implementation_status,
988            ReplayImplementationStatus::Implemented
989        );
990        assert_eq!(
991            entry.replay_policy,
992            ReplayPolicy::CommandDispatch {
993                command_kind: "pool.admin.v1",
994                command_manifest: "pool.admin.command_manifest.v1",
995            }
996        );
997        assert_eq!(entry.cost_class, CostClass::ManagementDeployment);
998        assert_eq!(entry.quota_policy, Some(DEPLOYMENT_QUOTA_V1));
999        assert_eq!(entry.cycle_reserve_policy, Some(DEPLOYMENT_RESERVE_V1));
1000    }
1001
1002    #[test]
1003    fn pool_admin_endpoint_requires_all_command_variants_implemented() {
1004        let blockers = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1005            .iter()
1006            .filter(|entry| {
1007                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
1008            })
1009            .map(|entry| entry.variant)
1010            .collect::<Vec<_>>();
1011
1012        assert!(
1013            blockers.is_empty(),
1014            "pool admin endpoint cannot be implemented while command variants remain blocked: {blockers:?}"
1015        );
1016    }
1017
1018    #[test]
1019    fn pool_create_empty_command_is_manifested_as_implemented() {
1020        let entry = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1021            .iter()
1022            .find(|entry| entry.variant == "CreateEmpty")
1023            .expect("CreateEmpty command policy entry");
1024
1025        assert_eq!(
1026            entry.implementation_status,
1027            ReplayImplementationStatus::Implemented
1028        );
1029        assert_eq!(
1030            entry.replay_policy,
1031            ReplayPolicy::ReplayProtected {
1032                command_kind: "pool.create_empty.v1",
1033                requires_operation_id: true,
1034            }
1035        );
1036    }
1037
1038    #[test]
1039    fn pool_import_queued_command_is_manifested_as_implemented_convergent() {
1040        let entry = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1041            .iter()
1042            .find(|entry| entry.variant == "ImportQueued")
1043            .expect("ImportQueued command policy entry");
1044
1045        assert_eq!(
1046            entry.implementation_status,
1047            ReplayImplementationStatus::Implemented
1048        );
1049        assert_eq!(entry.cost_class, CostClass::None);
1050        assert_eq!(
1051            entry.replay_policy,
1052            ReplayPolicy::SnapshotConvergent {
1053                command_kind: "pool.import_queued.ensure_v1",
1054            }
1055        );
1056    }
1057
1058    #[test]
1059    fn pool_import_immediate_command_is_manifested_as_implemented_idempotent() {
1060        let entry = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1061            .iter()
1062            .find(|entry| entry.variant == "ImportImmediate")
1063            .expect("ImportImmediate command policy entry");
1064
1065        assert_eq!(
1066            entry.implementation_status,
1067            ReplayImplementationStatus::Implemented
1068        );
1069        assert_eq!(
1070            entry.replay_policy,
1071            ReplayPolicy::ResponseIdempotent {
1072                command_kind: "pool.import_immediate.ensure_v1",
1073            }
1074        );
1075        assert_eq!(entry.cost_class, CostClass::ManagementDeployment);
1076        assert_eq!(entry.quota_policy, Some(DEPLOYMENT_QUOTA_V1));
1077        assert_eq!(entry.cycle_reserve_policy, Some(DEPLOYMENT_RESERVE_V1));
1078    }
1079
1080    #[test]
1081    fn pool_recycle_command_is_manifested_as_implemented_idempotent() {
1082        let entry = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1083            .iter()
1084            .find(|entry| entry.variant == "Recycle")
1085            .expect("Recycle command policy entry");
1086
1087        assert_eq!(
1088            entry.implementation_status,
1089            ReplayImplementationStatus::Implemented
1090        );
1091        assert_eq!(
1092            entry.replay_policy,
1093            ReplayPolicy::ResponseIdempotent {
1094                command_kind: "pool.recycle.ensure_v1",
1095            }
1096        );
1097        assert_eq!(entry.cost_class, CostClass::ManagementDeployment);
1098        assert_eq!(entry.quota_policy, Some(DEPLOYMENT_QUOTA_V1));
1099        assert_eq!(entry.cycle_reserve_policy, Some(DEPLOYMENT_RESERVE_V1));
1100    }
1101
1102    #[test]
1103    fn intentionally_non_idempotent_entries_must_state_reason() {
1104        for entry in ENDPOINT_REPLAY_POLICY_MANIFEST {
1105            if let ReplayPolicy::IntentionallyNonIdempotent { reason, .. } = entry.replay_policy {
1106                assert!(
1107                    !reason.trim().is_empty(),
1108                    "non-idempotent entry {} must state a reason",
1109                    entry.endpoint
1110                );
1111            }
1112        }
1113    }
1114
1115    fn pool_admin_command_variant_names() -> BTreeSet<&'static str> {
1116        enum_variant_names_from_source(include_str!("dto/pool.rs"), "pub enum PoolAdminCommand")
1117    }
1118
1119    fn root_capability_command_variant_names() -> BTreeSet<&'static str> {
1120        enum_variant_names_from_source(include_str!("dto/rpc.rs"), "pub enum RootCapabilityCommand")
1121    }
1122
1123    fn durable_publish_endpoint_names() -> BTreeSet<&'static str> {
1124        [
1125            CANIC_TEMPLATE_PREPARE_ADMIN,
1126            CANIC_TEMPLATE_PUBLISH_CHUNK_ADMIN,
1127            CANIC_TEMPLATE_STAGE_MANIFEST_ADMIN,
1128            "canic_wasm_store_admin",
1129        ]
1130        .into_iter()
1131        .chain(CANIC_WASM_STORE_ROOT_UPDATE_METHODS.iter().copied())
1132        .collect()
1133    }
1134
1135    fn release_candidate_manifest_blockers() -> BTreeSet<String> {
1136        let endpoint_blockers = ENDPOINT_REPLAY_POLICY_MANIFEST
1137            .iter()
1138            .filter(|entry| {
1139                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
1140            })
1141            .map(|entry| format!("endpoint:{}", entry.endpoint));
1142
1143        let root_command_blockers = ROOT_CAPABILITY_COMMAND_REPLAY_POLICY_MANIFEST
1144            .iter()
1145            .filter(|entry| {
1146                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
1147            })
1148            .map(|entry| format!("root-capability:{}", entry.variant));
1149
1150        let pool_command_blockers = POOL_ADMIN_COMMAND_REPLAY_POLICY_MANIFEST
1151            .iter()
1152            .filter(|entry| {
1153                entry.implementation_status == ReplayImplementationStatus::ReleaseBlocker
1154            })
1155            .map(|entry| format!("pool-admin:{}", entry.variant));
1156
1157        endpoint_blockers
1158            .chain(root_command_blockers)
1159            .chain(pool_command_blockers)
1160            .collect()
1161    }
1162
1163    fn enum_variant_names_from_source(
1164        source: &'static str,
1165        marker: &'static str,
1166    ) -> BTreeSet<&'static str> {
1167        let start = source.find(marker).expect("enum exists in source");
1168        let body_start = source[start..]
1169            .find('{')
1170            .map(|offset| start + offset + 1)
1171            .expect("enum has body");
1172        let body_end = source[body_start..]
1173            .find("\n}")
1174            .map(|offset| body_start + offset)
1175            .expect("enum body closes");
1176
1177        source[body_start..body_end]
1178            .lines()
1179            .filter_map(enum_variant_name_from_line)
1180            .collect()
1181    }
1182
1183    fn enum_variant_name_from_line(line: &'static str) -> Option<&'static str> {
1184        let line = line.trim();
1185        let first = line.as_bytes().first().copied()?;
1186        if !first.is_ascii_uppercase() {
1187            return None;
1188        }
1189        let end = line
1190            .find(|ch: char| ch == '(' || ch == '{' || ch == ',' || ch.is_whitespace())
1191            .unwrap_or(line.len());
1192        Some(&line[..end])
1193    }
1194
1195    fn emitted_update_endpoint_names() -> BTreeSet<&'static str> {
1196        [
1197            include_str!("../../canic/src/macros/endpoints/root.rs"),
1198            include_str!("../../canic/src/macros/endpoints/shared.rs"),
1199            include_str!("../../canic/src/macros/endpoints/wasm_store.rs"),
1200            include_str!("../../canic/src/macros/endpoints/nonroot.rs"),
1201            include_str!("../../canic/src/macros/endpoints/icp_refill.rs"),
1202        ]
1203        .into_iter()
1204        .flat_map(update_endpoint_names_from_source)
1205        .collect()
1206    }
1207
1208    fn update_endpoint_names_from_source(source: &'static str) -> Vec<&'static str> {
1209        let lines = source.lines().collect::<Vec<_>>();
1210        let mut names = Vec::new();
1211        for (index, line) in lines.iter().enumerate() {
1212            if !line.contains("#[$crate::canic_update") {
1213                continue;
1214            }
1215            let Some(name) = lines
1216                .iter()
1217                .skip(index + 1)
1218                .take(6)
1219                .find_map(|candidate| endpoint_name_from_fn_line(candidate))
1220            else {
1221                panic!("canic_update endpoint attribute without following function");
1222            };
1223            names.push(name);
1224        }
1225        names
1226    }
1227
1228    fn endpoint_name_from_fn_line(line: &'static str) -> Option<&'static str> {
1229        let marker = "fn ";
1230        let start = line.find(marker)? + marker.len();
1231        let rest = &line[start..];
1232        let end = rest.find('(')?;
1233        Some(&rest[..end])
1234    }
1235}