canic_host/protocol_binding/
mod.rs1use crate::{
8 icp::existing_local_canister_candid_path, registry::RegistryEntry,
9 release_set::CanicInfrastructureArtifactEntry,
10};
11use canic_core::{
12 ids::CanisterRole,
13 role_contract::{ProtocolProfileDigest, RoleCapabilityKey, derive_protocol_profile_hashes},
14};
15use std::{collections::BTreeSet, fs, io, path::PathBuf};
16use thiserror::Error as ThisError;
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct RegistryProtocolBinding {
21 pub release_identity: String,
22 pub role: CanisterRole,
23 pub capabilities: BTreeSet<RoleCapabilityKey>,
24 pub candid_sha256: [u8; 32],
25 pub protocol_profile_digest: ProtocolProfileDigest,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct ResolvedProtocolBinding {
31 pub(crate) binding: RegistryProtocolBinding,
32 pub(crate) candid_path: PathBuf,
33}
34
35impl ResolvedProtocolBinding {
36 #[must_use]
38 pub const fn binding(&self) -> &RegistryProtocolBinding {
39 &self.binding
40 }
41
42 #[must_use]
44 pub fn candid_path(&self) -> &std::path::Path {
45 &self.candid_path
46 }
47}
48
49#[derive(Debug, ThisError)]
51pub enum ProtocolBindingError {
52 #[error("Canister {canister} has no exact protocol binding in protected registry metadata")]
53 MissingBinding { canister: String },
54
55 #[error("Canister {canister} has no exact role in protected registry metadata")]
56 MissingRole { canister: String },
57
58 #[error(
59 "Canister {canister} registry role {registry_role} conflicts with protocol role {protocol_role}"
60 )]
61 RoleMismatch {
62 canister: String,
63 registry_role: String,
64 protocol_role: String,
65 },
66
67 #[error("Canister {canister} protocol release identity is empty")]
68 MissingReleaseIdentity { canister: String },
69
70 #[error("Canister {canister} has no local Candid sidecar for exact role {role}")]
71 MissingCandid { canister: String, role: String },
72
73 #[error("failed to read exact Candid sidecar {path}: {source}")]
74 ReadCandid {
75 path: PathBuf,
76 #[source]
77 source: io::Error,
78 },
79
80 #[error("Canister {canister} local Candid hash conflicts with protected registry metadata")]
81 CandidHashMismatch { canister: String },
82
83 #[error(
84 "Canister {canister} protocol-profile digest conflicts with protected registry metadata"
85 )]
86 ProfileDigestMismatch { canister: String },
87
88 #[error(
89 "infrastructure artifact role {artifact_role} conflicts with protocol role {protocol_role}"
90 )]
91 InfrastructureRoleMismatch {
92 artifact_role: String,
93 protocol_role: String,
94 },
95}
96
97pub fn resolve_registry_protocol_binding(
99 icp_root: &std::path::Path,
100 artifact_environment: &str,
101 entry: &RegistryEntry,
102) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
103 let binding =
104 entry
105 .protocol_binding
106 .clone()
107 .ok_or_else(|| ProtocolBindingError::MissingBinding {
108 canister: entry.pid.clone(),
109 })?;
110 let registry_role = entry
111 .role
112 .as_deref()
113 .ok_or_else(|| ProtocolBindingError::MissingRole {
114 canister: entry.pid.clone(),
115 })?;
116 if binding.release_identity.trim().is_empty() {
117 return Err(ProtocolBindingError::MissingReleaseIdentity {
118 canister: entry.pid.clone(),
119 });
120 }
121 if registry_role != binding.role.as_str() {
122 return Err(ProtocolBindingError::RoleMismatch {
123 canister: entry.pid.clone(),
124 registry_role: registry_role.to_string(),
125 protocol_role: binding.role.to_string(),
126 });
127 }
128 resolve_local_protocol_binding(icp_root, artifact_environment, &entry.pid, binding)
129}
130
131pub fn resolve_infrastructure_protocol_binding(
133 icp_root: &std::path::Path,
134 artifact_environment: &str,
135 artifact: &CanicInfrastructureArtifactEntry,
136) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
137 if artifact.protocol_role.as_str() != artifact.role.protocol_role_name() {
138 return Err(ProtocolBindingError::InfrastructureRoleMismatch {
139 artifact_role: artifact.role.as_str().to_string(),
140 protocol_role: artifact.protocol_role.to_string(),
141 });
142 }
143 resolve_local_protocol_binding(
144 icp_root,
145 artifact_environment,
146 artifact.role.as_str(),
147 RegistryProtocolBinding {
148 release_identity: artifact.protocol_release_identity.clone(),
149 role: artifact.protocol_role.clone(),
150 capabilities: artifact.protocol_capabilities.clone(),
151 candid_sha256: artifact.candid_sha256,
152 protocol_profile_digest: artifact.protocol_profile_digest,
153 },
154 )
155}
156
157fn resolve_local_protocol_binding(
158 icp_root: &std::path::Path,
159 artifact_environment: &str,
160 target: &str,
161 binding: RegistryProtocolBinding,
162) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
163 if binding.release_identity.trim().is_empty() {
164 return Err(ProtocolBindingError::MissingReleaseIdentity {
165 canister: target.to_string(),
166 });
167 }
168 let candid_path =
169 existing_local_canister_candid_path(icp_root, artifact_environment, binding.role.as_str())
170 .ok_or_else(|| ProtocolBindingError::MissingCandid {
171 canister: target.to_string(),
172 role: binding.role.to_string(),
173 })?;
174 let candid = fs::read(&candid_path).map_err(|source| ProtocolBindingError::ReadCandid {
175 path: candid_path.clone(),
176 source,
177 })?;
178 let observed = derive_protocol_profile_hashes(
179 &binding.release_identity,
180 &binding.role,
181 &binding.capabilities,
182 &candid,
183 );
184 if observed.candid_sha256 != binding.candid_sha256 {
185 return Err(ProtocolBindingError::CandidHashMismatch {
186 canister: target.to_string(),
187 });
188 }
189 if observed.protocol_profile_digest != binding.protocol_profile_digest {
190 return Err(ProtocolBindingError::ProfileDigestMismatch {
191 canister: target.to_string(),
192 });
193 }
194 Ok(ResolvedProtocolBinding {
195 binding,
196 candid_path,
197 })
198}
199
200#[cfg(test)]
201mod tests;