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