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 ThresholdEcdsaSign,
53 ManagementDeployment,
54 ValueTransfer,
55 DurablePublish,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum ReplayImplementationStatus {
63 Implemented,
64 ReleaseBlocker,
65}
66
67#[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#[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#[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}