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