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