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