pmetal_distributed/
namespace.rs1use sha3::{Digest, Sha3_256};
19use std::env;
20use tracing::info;
21
22const DEFAULT_VERSION: &str = env!("CARGO_PKG_VERSION");
24
25const NAMESPACE_ENV_VAR: &str = "PMETAL_NAMESPACE";
27
28#[derive(Debug, Clone)]
30pub struct NetworkNamespace {
31 version: String,
33 cluster: Option<String>,
35 psk: [u8; 32],
37 namespace_str: String,
39}
40
41impl NetworkNamespace {
42 pub fn new(version: &str, cluster: Option<&str>) -> Self {
49 let namespace_override = env::var(NAMESPACE_ENV_VAR).ok();
51
52 let namespace_str = match &namespace_override {
53 Some(override_ns) => override_ns.clone(),
54 None => match cluster {
55 Some(c) => format!("{}/{}", version, c),
56 None => version.to_string(),
57 },
58 };
59
60 let psk = Self::compute_psk(&namespace_str);
61
62 if namespace_override.is_some() {
63 info!(
64 "Using namespace override from {}: {}",
65 NAMESPACE_ENV_VAR, namespace_str
66 );
67 } else {
68 info!("Network namespace: {}", namespace_str);
69 }
70
71 Self {
72 version: version.to_string(),
73 cluster: cluster.map(|s| s.to_string()),
74 psk,
75 namespace_str,
76 }
77 }
78
79 pub fn default_version(cluster: Option<&str>) -> Self {
81 Self::new(&format!("pmetal/{}", DEFAULT_VERSION), cluster)
82 }
83
84 fn compute_psk(namespace: &str) -> [u8; 32] {
86 let mut hasher = Sha3_256::new();
87 hasher.update(namespace.as_bytes());
88 let result = hasher.finalize();
89
90 let mut psk = [0u8; 32];
91 psk.copy_from_slice(&result);
92 psk
93 }
94
95 pub fn psk(&self) -> &[u8; 32] {
97 &self.psk
98 }
99
100 pub fn namespace_str(&self) -> &str {
102 &self.namespace_str
103 }
104
105 pub fn version(&self) -> &str {
107 &self.version
108 }
109
110 pub fn cluster(&self) -> Option<&str> {
112 self.cluster.as_deref()
113 }
114
115 pub fn is_compatible(&self, other: &NetworkNamespace) -> bool {
117 self.psk == other.psk
118 }
119
120 pub fn verify_psk(&self, received: &[u8; 32]) -> bool {
122 let mut result = 0u8;
124 for (a, b) in self.psk.iter().zip(received.iter()) {
125 result |= a ^ b;
126 }
127 result == 0
128 }
129
130 pub fn gossipsub_topic(&self, suffix: &str) -> String {
132 format!("{}/{}", self.namespace_str, suffix)
133 }
134
135 pub fn protocol_id(&self) -> String {
137 format!("/{}", self.namespace_str.replace('/', "-"))
138 }
139}
140
141impl Default for NetworkNamespace {
142 fn default() -> Self {
143 Self::default_version(None)
144 }
145}
146
147pub fn validate_peer_namespace(local: &NetworkNamespace, remote_protocol: &str) -> bool {
151 let expected = local.protocol_id();
152 remote_protocol.starts_with(&expected)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_psk_computation() {
161 let ns1 = NetworkNamespace::new("pmetal/0.1.0", None);
162 let ns2 = NetworkNamespace::new("pmetal/0.1.0", None);
163
164 assert_eq!(ns1.psk(), ns2.psk());
165 }
166
167 #[test]
168 fn test_different_versions() {
169 let ns1 = NetworkNamespace::new("pmetal/0.1.0", None);
170 let ns2 = NetworkNamespace::new("pmetal/0.2.0", None);
171
172 assert_ne!(ns1.psk(), ns2.psk());
173 assert!(!ns1.is_compatible(&ns2));
174 }
175
176 #[test]
177 fn test_cluster_isolation() {
178 let ns1 = NetworkNamespace::new("pmetal/0.1.0", Some("cluster-a"));
179 let ns2 = NetworkNamespace::new("pmetal/0.1.0", Some("cluster-b"));
180
181 assert_ne!(ns1.psk(), ns2.psk());
182 assert!(!ns1.is_compatible(&ns2));
183 }
184
185 #[test]
186 fn test_psk_verification() {
187 let ns = NetworkNamespace::new("test", None);
188 let valid_psk = *ns.psk();
189 let mut invalid_psk = valid_psk;
190 invalid_psk[0] ^= 0xFF;
191
192 assert!(ns.verify_psk(&valid_psk));
193 assert!(!ns.verify_psk(&invalid_psk));
194 }
195
196 #[test]
197 fn test_gossipsub_topic() {
198 let ns = NetworkNamespace::new("pmetal/0.1.0", Some("test"));
199 let topic = ns.gossipsub_topic("gradients");
200
201 assert_eq!(topic, "pmetal/0.1.0/test/gradients");
202 }
203
204 #[test]
205 fn test_protocol_id() {
206 let ns = NetworkNamespace::new("pmetal/0.1.0", None);
207 let protocol = ns.protocol_id();
208
209 assert!(protocol.starts_with("/pmetal-"));
210 }
211}