1pub mod compat_manifest;
16#[allow(unsafe_code)]
19mod generated;
20pub mod heal_control;
21mod runtime_sources;
22
23use proto_gen::node_service::node_service_client::NodeServiceClient;
24use rustfs_common::{cache_connection, cached_connection, evict_connection_with_log_level};
25use std::{
26 collections::HashMap,
27 error::Error,
28 sync::LazyLock,
29 sync::atomic::{AtomicU8, AtomicUsize, Ordering},
30 time::{Duration, Instant},
31};
32use tokio::sync::Mutex;
33use tonic::{
34 Request, Status,
35 service::interceptor::InterceptedService,
36 transport::{Certificate, Channel, ClientTlsConfig, Endpoint},
37};
38use tracing::{debug, info, warn};
39use uuid::Uuid;
40
41pub type NodeServiceClientType = NodeServiceClient<
43 InterceptedService<Channel, Box<dyn Fn(Request<()>) -> Result<Request<()>, Status> + Send + Sync + 'static>>,
44>;
45
46pub use generated::*;
47pub use rustfs_common::ConnectionEvictionLogLevel;
48
49pub const DEFAULT_GRPC_SERVER_MESSAGE_LEN: usize = 100 * 1024 * 1024;
51
52const RUSTFS_HTTPS_PREFIX: &str = "https://";
57const TLS_GENERATION_CACHE_MAX_SIZE: usize = 512;
58const INTERNODE_RPC_MSGPACK_ONLY_CACHE_UNSET: u8 = 0;
59const INTERNODE_RPC_MSGPACK_ONLY_CACHE_FALSE: u8 = 1;
60const INTERNODE_RPC_MSGPACK_ONLY_CACHE_TRUE: u8 = 2;
61static TLS_GENERATION_CACHE: LazyLock<Mutex<HashMap<String, u64>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
62static INTERNODE_RPC_MSGPACK_ONLY_CACHE: AtomicU8 = AtomicU8::new(INTERNODE_RPC_MSGPACK_ONLY_CACHE_UNSET);
63
64fn enforce_tls_generation_cache_bound(generation_cache: &mut HashMap<String, u64>, generation: u64, addr: &str) {
65 if generation_cache.len() < TLS_GENERATION_CACHE_MAX_SIZE || generation_cache.contains_key(addr) {
66 return;
67 }
68
69 generation_cache.retain(|_, g| *g == generation);
70 if generation_cache.len() >= TLS_GENERATION_CACHE_MAX_SIZE
71 && let Some(victim) = generation_cache.keys().next().cloned()
72 {
73 generation_cache.remove(&victim);
74 }
75}
76
77fn internode_connect_timeout() -> Duration {
78 Duration::from_secs(rustfs_utils::get_env_u64(
79 rustfs_config::ENV_INTERNODE_CONNECT_TIMEOUT_SECS,
80 rustfs_config::DEFAULT_INTERNODE_CONNECT_TIMEOUT_SECS,
81 ))
82}
83
84fn internode_tcp_keepalive() -> Duration {
85 Duration::from_secs(rustfs_utils::get_env_u64(
86 rustfs_config::ENV_INTERNODE_TCP_KEEPALIVE_SECS,
87 rustfs_config::DEFAULT_INTERNODE_TCP_KEEPALIVE_SECS,
88 ))
89}
90
91fn internode_http2_keep_alive_interval() -> Duration {
92 Duration::from_secs(rustfs_utils::get_env_u64(
93 rustfs_config::ENV_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS,
94 rustfs_config::DEFAULT_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS,
95 ))
96}
97
98fn internode_http2_keep_alive_timeout() -> Duration {
99 Duration::from_secs(rustfs_utils::get_env_u64(
100 rustfs_config::ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS,
101 rustfs_config::DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS,
102 ))
103}
104
105fn internode_rpc_timeout() -> Duration {
106 normalize_internode_rpc_timeout(Duration::from_secs(rustfs_utils::get_env_u64(
107 rustfs_config::ENV_INTERNODE_RPC_TIMEOUT_SECS,
108 rustfs_config::DEFAULT_INTERNODE_RPC_TIMEOUT_SECS,
109 )))
110}
111
112fn normalize_internode_rpc_timeout(timeout: Duration) -> Duration {
113 timeout.max(Duration::from_secs(1))
114}
115
116pub fn heal_control_execution_timeout() -> Duration {
119 heal_control_execution_timeout_for(internode_rpc_timeout())
120}
121
122fn heal_control_execution_timeout_for(transport_timeout: Duration) -> Duration {
123 const TRANSPORT_GUARD: Duration = Duration::from_secs(1);
124 let transport_timeout = transport_timeout.max(Duration::from_secs(1));
125 transport_timeout
126 .saturating_sub(TRANSPORT_GUARD.min(transport_timeout / 2))
127 .max(Duration::from_millis(1))
128 .min(Duration::from_millis(
129 u64::try_from(heal_control::MAX_LIFETIME_MS).expect("positive heal control lifetime must fit u64"),
130 ))
131}
132
133fn internode_rpc_tcp_nodelay() -> bool {
134 rustfs_utils::get_env_bool(
135 rustfs_config::ENV_INTERNODE_RPC_TCP_NODELAY,
136 rustfs_config::DEFAULT_INTERNODE_RPC_TCP_NODELAY,
137 )
138}
139
140fn internode_rpc_http2_stream_window() -> Option<u32> {
143 match rustfs_utils::get_env_u32(
144 rustfs_config::ENV_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE,
145 rustfs_config::DEFAULT_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE,
146 ) {
147 0 => None,
148 v => Some(v),
149 }
150}
151
152fn internode_rpc_http2_conn_window() -> Option<u32> {
155 match rustfs_utils::get_env_u32(
156 rustfs_config::ENV_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE,
157 rustfs_config::DEFAULT_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE,
158 ) {
159 0 => None,
160 v => Some(v),
161 }
162}
163
164pub fn internode_rpc_max_message_size() -> usize {
168 rustfs_utils::get_env_usize(rustfs_config::ENV_INTERNODE_RPC_MAX_MESSAGE_SIZE, DEFAULT_GRPC_SERVER_MESSAGE_LEN)
169}
170
171pub const HEAL_CONTROL_RPC_MAX_MESSAGE_SIZE: usize = heal_control::RESULT_MAX_SIZE + 1024;
172pub const HEAL_CONTROL_PROTOCOL_VERSION: u32 = 3;
173pub const DYNAMIC_CONFIG_PROTOCOL_VERSION: u32 = 1;
174pub const BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION: u32 = 2;
175pub const HEAL_CONTROL_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-heal-control-capability-v4\0";
180pub const REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-tier-remote-version-state-capability-v1\0";
181pub const CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-cross-pool-fence-capability-v1\0";
182pub const ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-ilm-recovery-export-capability-v1\0";
183pub const TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX: &[u8] =
184 b"rustfs-transition-transaction-compaction-capability-v1\0";
185pub const TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE: usize = 64 * 1024;
186pub const TIER_MUTATION_RPC_MAX_COMMIT_PAYLOAD_SIZE: usize = 1024;
187pub const TIER_MUTATION_RPC_MAX_ABORT_PAYLOAD_SIZE: usize = TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE;
188pub const TIER_MUTATION_RPC_MAX_MESSAGE_SIZE: usize = TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE + 4096;
189
190pub fn heal_control_coordinator_epoch(topology_fingerprint: &str) -> Result<u64, &'static str> {
191 let prefix = topology_fingerprint
192 .get(..16)
193 .ok_or("heal control topology fingerprint is too short")?;
194 let epoch = u64::from_str_radix(prefix, 16).map_err(|_| "heal control topology fingerprint is not hexadecimal")?;
195 if epoch == 0 {
196 return Err("heal control topology epoch is zero");
197 }
198 Ok(epoch)
199}
200
201pub fn heal_control_capability_probe(nonce: &[u8; 16]) -> Vec<u8> {
202 let mut probe = Vec::with_capacity(HEAL_CONTROL_CAPABILITY_PROBE_PREFIX.len() + nonce.len());
203 probe.extend_from_slice(HEAL_CONTROL_CAPABILITY_PROBE_PREFIX);
204 probe.extend_from_slice(nonce);
205 probe
206}
207
208pub fn is_heal_control_capability_probe(command: &[u8]) -> bool {
209 command.len() == HEAL_CONTROL_CAPABILITY_PROBE_PREFIX.len() + 16 && command.starts_with(HEAL_CONTROL_CAPABILITY_PROBE_PREFIX)
210}
211
212pub fn remote_version_state_capability_probe(nonce: &[u8; 16]) -> Vec<u8> {
213 let mut probe = Vec::with_capacity(REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX.len() + nonce.len());
214 probe.extend_from_slice(REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX);
215 probe.extend_from_slice(nonce);
216 probe
217}
218
219pub fn is_remote_version_state_capability_probe(command: &[u8]) -> bool {
220 command.len() == REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX.len() + 16
221 && command.starts_with(REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX)
222}
223
224pub fn is_cross_pool_fence_capability_probe(command: &[u8]) -> bool {
225 command.len() == CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX.len() + 16
226 && command.starts_with(CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX)
227}
228
229pub fn ilm_recovery_export_capability_probe(nonce: &[u8; 16]) -> Vec<u8> {
230 let mut probe = Vec::with_capacity(ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX.len() + nonce.len());
231 probe.extend_from_slice(ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX);
232 probe.extend_from_slice(nonce);
233 probe
234}
235
236pub fn is_ilm_recovery_export_capability_probe(command: &[u8]) -> bool {
237 command.len() == ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX.len() + 16
238 && command.starts_with(ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX)
239}
240
241pub fn transition_transaction_compaction_capability_probe(nonce: &[u8; 16]) -> Vec<u8> {
242 let mut probe = Vec::with_capacity(TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX.len() + nonce.len());
243 probe.extend_from_slice(TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX);
244 probe.extend_from_slice(nonce);
245 probe
246}
247
248pub fn is_transition_transaction_compaction_capability_probe(command: &[u8]) -> bool {
249 command.len() == TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX.len() + 16
250 && command.starts_with(TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX)
251}
252
253pub fn encode_remote_version_state_capability(
254 topology_member: &str,
255 process_epoch: &[u8; 16],
256) -> Result<Vec<u8>, std::num::TryFromIntError> {
257 let topology_member = topology_member.as_bytes();
258 let mut result = Vec::with_capacity(8 + topology_member.len() + process_epoch.len());
259 result.extend_from_slice(&u64::try_from(topology_member.len())?.to_be_bytes());
260 result.extend_from_slice(topology_member);
261 result.extend_from_slice(process_epoch);
262 Ok(result)
263}
264
265pub fn decode_remote_version_state_capability(result: &[u8]) -> Result<(&str, &[u8; 16]), &'static str> {
266 let member_len = result
267 .get(..8)
268 .and_then(|value| value.try_into().ok())
269 .map(u64::from_be_bytes)
270 .ok_or("remote version state capability is truncated")?;
271 let member_len = usize::try_from(member_len).map_err(|_| "remote version state member length cannot be represented")?;
272 let member_end = 8_usize
273 .checked_add(member_len)
274 .ok_or("remote version state member length overflow")?;
275 let topology_member = std::str::from_utf8(result.get(8..member_end).ok_or("remote version state member is truncated")?)
276 .map_err(|_| "remote version state member is not UTF-8")?;
277 let process_epoch = result
278 .get(member_end..)
279 .and_then(|value| value.try_into().ok())
280 .ok_or("remote version state process epoch has an invalid length")?;
281 Ok((topology_member, process_epoch))
282}
283
284pub fn encode_cross_pool_fence_capability(
285 supported_version: u32,
286 topology_member: &str,
287 process_epoch: &[u8; 16],
288) -> Result<Vec<u8>, std::num::TryFromIntError> {
289 let identity = encode_remote_version_state_capability(topology_member, process_epoch)?;
290 let mut result = Vec::with_capacity(4 + identity.len());
291 result.extend_from_slice(&supported_version.to_be_bytes());
292 result.extend_from_slice(&identity);
293 Ok(result)
294}
295
296pub fn canonical_heal_control_request_body(
301 version: u32,
302 topology_fingerprint: &str,
303 command: &[u8],
304) -> Result<Vec<u8>, std::num::TryFromIntError> {
305 const DOMAIN: &[u8] = b"rustfs-heal-control-v3\0";
306
307 let fingerprint = topology_fingerprint.as_bytes();
308 let mut body = Vec::with_capacity(DOMAIN.len() + 4 + 8 + fingerprint.len() + 8 + command.len());
309 body.extend_from_slice(DOMAIN);
310 body.extend_from_slice(&version.to_be_bytes());
311 body.extend_from_slice(&u64::try_from(fingerprint.len())?.to_be_bytes());
312 body.extend_from_slice(fingerprint);
313 body.extend_from_slice(&u64::try_from(command.len())?.to_be_bytes());
314 body.extend_from_slice(command);
315 Ok(body)
316}
317
318pub fn canonical_heal_control_capability_ack(
321 version: u32,
322 topology_fingerprint: &str,
323 probe: &[u8],
324) -> Result<Vec<u8>, std::num::TryFromIntError> {
325 const DOMAIN: &[u8] = b"rustfs-heal-control-capability-ack-v4\0";
326
327 let fingerprint = topology_fingerprint.as_bytes();
328 let mut body = Vec::with_capacity(DOMAIN.len() + 4 + 8 + fingerprint.len() + 8 + probe.len());
329 body.extend_from_slice(DOMAIN);
330 body.extend_from_slice(&version.to_be_bytes());
331 body.extend_from_slice(&u64::try_from(fingerprint.len())?.to_be_bytes());
332 body.extend_from_slice(fingerprint);
333 body.extend_from_slice(&u64::try_from(probe.len())?.to_be_bytes());
334 body.extend_from_slice(probe);
335 Ok(body)
336}
337
338pub fn canonical_heal_control_response_body(
339 version: u32,
340 topology_fingerprint: &str,
341 command: &[u8],
342 result: &[u8],
343) -> Result<Vec<u8>, std::num::TryFromIntError> {
344 const DOMAIN: &[u8] = b"rustfs-heal-control-response-v3\0";
345
346 let fingerprint = topology_fingerprint.as_bytes();
347 let mut body = Vec::with_capacity(DOMAIN.len() + 4 + 8 + fingerprint.len() + 8 + command.len() + 8 + result.len());
348 body.extend_from_slice(DOMAIN);
349 body.extend_from_slice(&version.to_be_bytes());
350 body.extend_from_slice(&u64::try_from(fingerprint.len())?.to_be_bytes());
351 body.extend_from_slice(fingerprint);
352 body.extend_from_slice(&u64::try_from(command.len())?.to_be_bytes());
353 body.extend_from_slice(command);
354 body.extend_from_slice(&u64::try_from(result.len())?.to_be_bytes());
355 body.extend_from_slice(result);
356 Ok(body)
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360#[non_exhaustive]
361pub enum TierMutationRpcPhase {
362 Prepare,
363 Commit,
364 Abort,
365}
366
367impl TierMutationRpcPhase {
368 fn as_wire_str(self) -> &'static str {
369 match self {
370 Self::Prepare => "prepare",
371 Self::Commit => "commit",
372 Self::Abort => "abort",
373 }
374 }
375}
376
377pub const TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION: u32 = 3;
384pub const TIER_MUTATION_RPC_PROTOCOL_VERSION: u32 = 4;
385pub const TIER_MUTATION_RPC_MAX_ERROR_INFO_SIZE: usize = 1024;
386pub const TIER_MUTATION_RPC_MAX_RESPONSE_PROOF_SIZE: usize = 4096;
387
388pub fn canonical_tier_mutation_rpc_body(
389 version: u32,
390 phase: TierMutationRpcPhase,
391 mutation_id: Uuid,
392 canonical_payload: &[u8],
393) -> Result<Vec<u8>, std::num::TryFromIntError> {
394 const DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-v1\0";
395
396 let phase = phase.as_wire_str().as_bytes();
397 let mutation_id = mutation_id.as_bytes();
398 let mut body = Vec::with_capacity(DOMAIN.len() + 4 + 8 + phase.len() + mutation_id.len() + 8 + canonical_payload.len());
399 body.extend_from_slice(DOMAIN);
400 body.extend_from_slice(&version.to_be_bytes());
401 body.extend_from_slice(&u64::try_from(phase.len())?.to_be_bytes());
402 body.extend_from_slice(phase);
403 body.extend_from_slice(mutation_id);
404 body.extend_from_slice(&u64::try_from(canonical_payload.len())?.to_be_bytes());
405 body.extend_from_slice(canonical_payload);
406 Ok(body)
407}
408
409pub struct TierMutationRpcResponseProofInput<'a> {
410 pub version: u32,
411 pub phase: TierMutationRpcPhase,
412 pub mutation_id: Uuid,
413 pub canonical_payload: &'a [u8],
414 pub success: bool,
415 pub state: i32,
416 pub applied: bool,
417 pub error_info: Option<&'a str>,
418 pub failure_class: i32,
419}
420
421pub fn canonical_tier_mutation_rpc_response_body(
422 input: TierMutationRpcResponseProofInput<'_>,
423) -> Result<Vec<u8>, std::num::TryFromIntError> {
424 const V3_DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-response-v1\0";
425 const V4_DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-response-v2\0";
426
427 let phase = input.phase.as_wire_str().as_bytes();
428 let mutation_id = input.mutation_id.as_bytes();
429 let error_info = input.error_info.map(str::as_bytes);
430 let error_info_len = error_info.map_or(0, <[u8]>::len);
431 let is_v4 = input.version >= TIER_MUTATION_RPC_PROTOCOL_VERSION;
432 let domain = if is_v4 { V4_DOMAIN } else { V3_DOMAIN };
433 let mut body = Vec::with_capacity(
434 domain.len()
435 + 4
436 + 8
437 + phase.len()
438 + mutation_id.len()
439 + 8
440 + input.canonical_payload.len()
441 + 1
442 + 4
443 + 1
444 + 1
445 + 8
446 + error_info_len
447 + if is_v4 { 4 } else { 0 },
448 );
449 body.extend_from_slice(domain);
450 body.extend_from_slice(&input.version.to_be_bytes());
451 body.extend_from_slice(&u64::try_from(phase.len())?.to_be_bytes());
452 body.extend_from_slice(phase);
453 body.extend_from_slice(mutation_id);
454 body.extend_from_slice(&u64::try_from(input.canonical_payload.len())?.to_be_bytes());
455 body.extend_from_slice(input.canonical_payload);
456 body.push(u8::from(input.success));
457 body.extend_from_slice(&input.state.to_be_bytes());
458 body.push(u8::from(input.applied));
459 body.push(u8::from(error_info.is_some()));
460 body.extend_from_slice(&u64::try_from(error_info_len)?.to_be_bytes());
461 if let Some(error_info) = error_info {
462 body.extend_from_slice(error_info);
463 }
464 if is_v4 {
465 body.extend_from_slice(&input.failure_class.to_be_bytes());
466 }
467 Ok(body)
468}
469
470pub fn canonical_scanner_activity_request_body(
472 request: &proto_gen::node_service::ScannerActivityRequest,
473) -> Result<Vec<u8>, std::num::TryFromIntError> {
474 const DOMAIN: &[u8] = b"rustfs-scanner-activity-request-v1\0";
475
476 let challenge = request.challenge.as_ref();
477 let acknowledge_instance_id = request.acknowledge_instance_id.as_bytes();
478 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + acknowledge_instance_id.len() + 4 + 8 * 3);
479 body.extend_from_slice(DOMAIN);
480 body.extend_from_slice(&request.protocol_version.to_be_bytes());
481 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
482 body.extend_from_slice(challenge);
483 body.extend_from_slice(&u64::try_from(acknowledge_instance_id.len())?.to_be_bytes());
484 body.extend_from_slice(acknowledge_instance_id);
485 body.extend_from_slice(&request.acknowledge_dirty_usage_generation.to_be_bytes());
486 Ok(body)
487}
488
489pub fn canonical_scanner_activity_v4_response_body(
491 challenge: &[u8],
492 response: &proto_gen::node_service::ScannerActivityResponse,
493) -> Result<Vec<u8>, std::num::TryFromIntError> {
494 const DOMAIN: &[u8] = b"rustfs-scanner-activity-response-v1\0";
495
496 let instance_id = response.instance_id.as_bytes();
497 let topology_digest = response.topology_digest.as_ref();
498 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + instance_id.len() + topology_digest.len() + 4 + 8 * 5 + 1);
499 body.extend_from_slice(DOMAIN);
500 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
501 body.extend_from_slice(challenge);
502 body.extend_from_slice(&u64::try_from(instance_id.len())?.to_be_bytes());
503 body.extend_from_slice(instance_id);
504 body.extend_from_slice(&response.namespace_generation.to_be_bytes());
505 body.extend_from_slice(&response.maintenance_generation.to_be_bytes());
506 body.extend_from_slice(&response.protocol_version.to_be_bytes());
507 body.extend_from_slice(&u64::try_from(topology_digest.len())?.to_be_bytes());
508 body.extend_from_slice(topology_digest);
509 body.push(u8::from(response.data_movement_active));
510 Ok(body)
511}
512
513pub fn canonical_scanner_activity_response_body(
515 challenge: &[u8],
516 response: &proto_gen::node_service::ScannerActivityResponse,
517) -> Result<Vec<u8>, std::num::TryFromIntError> {
518 const DOMAIN: &[u8] = b"rustfs-scanner-activity-response-v2\0";
519
520 let instance_id = response.instance_id.as_bytes();
521 let topology_digest = response.topology_digest.as_ref();
522 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + instance_id.len() + topology_digest.len() + 4 + 8 * 6 + 2);
523 body.extend_from_slice(DOMAIN);
524 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
525 body.extend_from_slice(challenge);
526 body.extend_from_slice(&u64::try_from(instance_id.len())?.to_be_bytes());
527 body.extend_from_slice(instance_id);
528 body.extend_from_slice(&response.namespace_generation.to_be_bytes());
529 body.extend_from_slice(&response.maintenance_generation.to_be_bytes());
530 body.extend_from_slice(&response.protocol_version.to_be_bytes());
531 body.extend_from_slice(&u64::try_from(topology_digest.len())?.to_be_bytes());
532 body.extend_from_slice(topology_digest);
533 body.push(u8::from(response.data_movement_active));
534 body.extend_from_slice(&response.dirty_usage_generation.to_be_bytes());
535 body.push(u8::from(response.dirty_usage_pending));
536 Ok(body)
537}
538
539pub fn canonical_scanner_activity_v7_response_body(
543 challenge: &[u8],
544 response: &proto_gen::node_service::ScannerActivityResponse,
545) -> Result<Vec<u8>, std::num::TryFromIntError> {
546 const DOMAIN: &[u8] = b"rustfs-scanner-activity-response-v3\0";
547
548 let instance_id = response.instance_id.as_bytes();
549 let topology_digest = response.topology_digest.as_ref();
550 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + instance_id.len() + topology_digest.len() + 4 + 8 * 8 + 4);
551 body.extend_from_slice(DOMAIN);
552 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
553 body.extend_from_slice(challenge);
554 body.extend_from_slice(&u64::try_from(instance_id.len())?.to_be_bytes());
555 body.extend_from_slice(instance_id);
556 body.extend_from_slice(&response.namespace_generation.to_be_bytes());
557 body.extend_from_slice(&response.maintenance_generation.to_be_bytes());
558 body.extend_from_slice(&response.protocol_version.to_be_bytes());
559 body.extend_from_slice(&u64::try_from(topology_digest.len())?.to_be_bytes());
560 body.extend_from_slice(topology_digest);
561 body.push(u8::from(response.data_movement_active));
562 body.extend_from_slice(&response.dirty_usage_generation.to_be_bytes());
563 body.push(u8::from(response.dirty_usage_pending));
564 body.push(u8::from(response.movement_generation.is_some()));
565 if let Some(generation) = response.movement_generation {
566 body.extend_from_slice(&generation.to_be_bytes());
567 }
568 body.push(u8::from(response.publication_blocked.is_some()));
569 if let Some(blocked) = response.publication_blocked {
570 body.push(u8::from(blocked));
571 }
572 Ok(body)
573}
574
575pub mod scoped_dirty_usage;
576
577pub fn canonical_scanner_dirty_usage_snapshot_request_body(
578 request: &proto_gen::node_service::ScannerDirtyUsageSnapshotRequest,
579) -> Result<Vec<u8>, std::num::TryFromIntError> {
580 let mut body = CanonicalBodyBuilder::new(b"rustfs-scanner-dirty-usage-snapshot-request-v1\0");
581 body.push_u32(request.protocol_version);
582 body.push_bytes(request.challenge.as_ref())?;
583 Ok(body.finish())
584}
585
586pub fn canonical_scanner_dirty_usage_snapshot_response_body(
587 challenge: &[u8],
588 response: &proto_gen::node_service::ScannerDirtyUsageSnapshotResponse,
589) -> Result<Vec<u8>, std::num::TryFromIntError> {
590 let mut body = CanonicalBodyBuilder::new(b"rustfs-scanner-dirty-usage-snapshot-response-v1\0");
591 body.push_bytes(challenge)?;
592 body.push_str(&response.instance_id)?;
593 body.push_u64(response.generation);
594 body.push_u64(response.pending_bucket_count);
595 body.push_u32(response.protocol_version);
596 body.push_str(&response.owner_id)?;
597 body.push_bool(response.complete);
598 body.push_count(response.buckets.len())?;
599 for bucket in &response.buckets {
600 body.push_str(&bucket.bucket)?;
601 body.push_u64(bucket.generation);
602 body.push_bytes(bucket.bucket_incarnation.as_ref())?;
603 }
604 Ok(body.finish())
605}
606
607pub fn canonical_scanner_publication_lease_request_body(
611 request: &proto_gen::node_service::ScannerPublicationLeaseRequest,
612) -> Result<Vec<u8>, std::num::TryFromIntError> {
613 const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-request-v1\0";
614 let challenge = request.challenge.as_ref();
615 let session_id = request.expected_session_id.as_bytes();
616 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + session_id.len() + 40);
617 body.extend_from_slice(DOMAIN);
618 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
619 body.extend_from_slice(challenge);
620 body.extend_from_slice(&request.expected_movement_generation.to_be_bytes());
621 body.extend_from_slice(&request.ttl_ms.to_be_bytes());
622 body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
623 body.extend_from_slice(session_id);
624 let token = request.token.as_ref();
628 if !token.is_empty() {
629 body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
630 body.extend_from_slice(token);
631 }
632 Ok(body)
633}
634
635pub fn canonical_scanner_publication_lease_release_request_body(
638 request: &proto_gen::node_service::ScannerPublicationLeaseReleaseRequest,
639) -> Result<Vec<u8>, std::num::TryFromIntError> {
640 const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-release-request-v1\0";
641 let challenge = request.challenge.as_ref();
642 let token = request.token.as_ref();
643 let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + token.len() + 16);
644 body.extend_from_slice(DOMAIN);
645 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
646 body.extend_from_slice(challenge);
647 body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
648 body.extend_from_slice(token);
649 let owner_id = request.owner_id.as_bytes();
650 let session_id = request.session_id.as_bytes();
651 body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
652 body.extend_from_slice(owner_id);
653 body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
654 body.extend_from_slice(session_id);
655 Ok(body)
656}
657
658pub fn canonical_scanner_publication_lease_response_body(
659 challenge: &[u8],
660 response: &proto_gen::node_service::ScannerPublicationLeaseResponse,
661) -> Result<Vec<u8>, std::num::TryFromIntError> {
662 const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-response-v1\0";
663 let token = response.token.as_ref();
664 let owner_id = response.owner_id.as_bytes();
665 let session_id = response.session_id.as_bytes();
666 let error_info = response.error.as_ref().map(|error| error.error_info.as_bytes());
667 let error_code = response.error.as_ref().map_or(0, |error| error.code);
668 let mut body = Vec::with_capacity(
669 DOMAIN.len() + challenge.len() + token.len() + owner_id.len() + session_id.len() + error_info.map_or(0, |v| v.len()) + 72,
670 );
671 body.extend_from_slice(DOMAIN);
672 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
673 body.extend_from_slice(challenge);
674 body.push(u8::from(response.success));
675 body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
676 body.extend_from_slice(token);
677 body.extend_from_slice(&response.movement_generation.to_be_bytes());
678 body.extend_from_slice(&response.lease_ttl_ms.to_be_bytes());
679 body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
680 body.extend_from_slice(owner_id);
681 body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
682 body.extend_from_slice(session_id);
683 body.push(u8::from(response.error.is_some()));
684 body.extend_from_slice(&error_code.to_be_bytes());
685 body.extend_from_slice(&u64::try_from(error_info.map_or(0, |value| value.len()))?.to_be_bytes());
686 if let Some(error_info) = error_info {
687 body.extend_from_slice(error_info);
688 }
689 Ok(body)
690}
691
692pub fn canonical_scanner_publication_lease_release_response_body(
693 challenge: &[u8],
694 request: &proto_gen::node_service::ScannerPublicationLeaseReleaseRequest,
695 response: &proto_gen::node_service::ScannerPublicationLeaseReleaseResponse,
696) -> Result<Vec<u8>, std::num::TryFromIntError> {
697 const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-release-response-v1\0";
698 let token = request.token.as_ref();
699 let owner_id = request.owner_id.as_bytes();
700 let session_id = request.session_id.as_bytes();
701 let error_info = response.error.as_ref().map(|error| error.error_info.as_bytes());
702 let error_code = response.error.as_ref().map_or(0, |error| error.code);
703 let mut body = Vec::with_capacity(
704 DOMAIN.len() + challenge.len() + token.len() + owner_id.len() + session_id.len() + error_info.map_or(0, |v| v.len()) + 56,
705 );
706 body.extend_from_slice(DOMAIN);
707 body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
708 body.extend_from_slice(challenge);
709 body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
710 body.extend_from_slice(token);
711 body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
712 body.extend_from_slice(owner_id);
713 body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
714 body.extend_from_slice(session_id);
715 body.push(u8::from(response.success));
716 body.push(u8::from(response.error.is_some()));
717 body.extend_from_slice(&error_code.to_be_bytes());
718 body.extend_from_slice(&u64::try_from(error_info.map_or(0, |value| value.len()))?.to_be_bytes());
719 if let Some(error_info) = error_info {
720 body.extend_from_slice(error_info);
721 }
722 Ok(body)
723}
724
725struct CanonicalBodyBuilder {
729 body: Vec<u8>,
730}
731
732impl CanonicalBodyBuilder {
733 fn new(domain: &'static [u8]) -> Self {
734 let mut body = Vec::with_capacity(domain.len() + 128);
735 body.extend_from_slice(domain);
736 Self { body }
737 }
738
739 fn push_bytes(&mut self, field: &[u8]) -> Result<(), std::num::TryFromIntError> {
740 self.body.extend_from_slice(&u64::try_from(field.len())?.to_be_bytes());
741 self.body.extend_from_slice(field);
742 Ok(())
743 }
744
745 fn push_str(&mut self, field: &str) -> Result<(), std::num::TryFromIntError> {
746 self.push_bytes(field.as_bytes())
747 }
748
749 fn push_optional_str(&mut self, field: Option<&str>) -> Result<(), std::num::TryFromIntError> {
750 self.body.push(u8::from(field.is_some()));
751 self.push_str(field.unwrap_or_default())
752 }
753
754 fn push_bool(&mut self, field: bool) {
755 self.body.push(u8::from(field));
756 }
757
758 fn push_u32(&mut self, field: u32) {
759 self.body.extend_from_slice(&field.to_be_bytes());
760 }
761
762 fn push_u64(&mut self, field: u64) {
763 self.body.extend_from_slice(&field.to_be_bytes());
764 }
765
766 fn push_count(&mut self, count: usize) -> Result<(), std::num::TryFromIntError> {
767 self.body.extend_from_slice(&u64::try_from(count)?.to_be_bytes());
768 Ok(())
769 }
770
771 fn finish(self) -> Vec<u8> {
772 self.body
773 }
774}
775
776pub const PEER_RESTSIGNAL: &str = "signal";
777pub const PEER_RESTSUB_SYS: &str = "sub-sys";
778pub const PEER_RESTDRY_RUN: &str = "dry-run";
779
780pub trait CanonicalMutationBody {
785 fn canonical_body(&self) -> Result<Vec<u8>, std::num::TryFromIntError>;
786}
787
788macro_rules! impl_canonical_mutation_body {
789 ($request:ty, $domain:expr, |$value:ident, $body:ident| $fields:block) => {
790 impl CanonicalMutationBody for $request {
791 fn canonical_body(&self) -> Result<Vec<u8>, std::num::TryFromIntError> {
792 let $value = self;
793 let mut $body = CanonicalBodyBuilder::new($domain);
794 $fields
795 Ok($body.finish())
796 }
797 }
798 };
799 ($request:ty, $domain:expr) => {
800 impl CanonicalMutationBody for $request {
801 fn canonical_body(&self) -> Result<Vec<u8>, std::num::TryFromIntError> {
802 Ok(CanonicalBodyBuilder::new($domain).finish())
803 }
804 }
805 };
806}
807
808impl_canonical_mutation_body!(
809 proto_gen::node_service::SignalServiceRequest,
810 b"rustfs-signal-service-request-v1\0",
811 |request, body| {
812 let vars = request.vars.as_ref().map(|vars| &vars.value);
813 body.push_optional_str(vars.and_then(|vars| vars.get(PEER_RESTSIGNAL).map(String::as_str)))?;
814 body.push_optional_str(vars.and_then(|vars| vars.get(PEER_RESTSUB_SYS).map(String::as_str)))?;
815 body.push_optional_str(vars.and_then(|vars| vars.get(PEER_RESTDRY_RUN).map(String::as_str)))?;
816 }
817);
818impl_canonical_mutation_body!(
819 proto_gen::node_service::HealBucketRequest,
820 b"rustfs-heal-bucket-request-v1\0",
821 |request, body| {
822 body.push_str(&request.bucket)?;
823 body.push_str(&request.options)?;
824 if !request.bucket_incarnation_id.is_empty() {
825 body.push_bytes(&request.bucket_incarnation_id)?;
826 }
827 }
828);
829impl_canonical_mutation_body!(
830 proto_gen::node_service::MakeBucketRequest,
831 b"rustfs-make-bucket-request-v1\0",
832 |request, body| {
833 body.push_str(&request.name)?;
834 body.push_str(&request.options)?;
835 }
836);
837impl_canonical_mutation_body!(
838 proto_gen::node_service::DeleteBucketRequest,
839 b"rustfs-delete-bucket-request-v1\0",
840 |request, body| {
841 body.push_str(&request.bucket)?;
842 body.push_str(&request.options)?;
843 }
844);
845impl_canonical_mutation_body!(
846 proto_gen::node_service::GenerallyLockRequest,
847 b"rustfs-lock-request-v1\0",
848 |request, body| {
849 body.push_str(&request.args)?;
850 }
851);
852impl_canonical_mutation_body!(
853 proto_gen::node_service::BatchGenerallyLockRequest,
854 b"rustfs-lock-batch-request-v1\0",
855 |request, body| {
856 body.push_count(request.args.len())?;
857 for arg in &request.args {
858 body.push_str(arg)?;
859 }
860 }
861);
862impl_canonical_mutation_body!(
863 proto_gen::node_service::LoadBucketMetadataRequest,
864 b"rustfs-load-bucket-metadata-request-v1\0",
865 |request, body| {
866 body.push_str(&request.bucket)?;
867 body.push_bool(request.scanner_maintenance_change);
868 }
869);
870impl_canonical_mutation_body!(
871 proto_gen::node_service::DeleteBucketMetadataRequest,
872 b"rustfs-delete-bucket-metadata-request-v1\0",
873 |request, body| {
874 body.push_str(&request.bucket)?;
875 }
876);
877impl_canonical_mutation_body!(
878 proto_gen::node_service::DeletePolicyRequest,
879 b"rustfs-delete-policy-request-v1\0",
880 |request, body| {
881 body.push_str(&request.policy_name)?;
882 }
883);
884impl_canonical_mutation_body!(
885 proto_gen::node_service::LoadPolicyRequest,
886 b"rustfs-load-policy-request-v1\0",
887 |request, body| {
888 body.push_str(&request.policy_name)?;
889 }
890);
891impl_canonical_mutation_body!(
892 proto_gen::node_service::LoadPolicyMappingRequest,
893 b"rustfs-load-policy-mapping-request-v1\0",
894 |request, body| {
895 body.push_str(&request.user_or_group)?;
896 body.push_u64(request.user_type);
897 body.push_bool(request.is_group);
898 }
899);
900impl_canonical_mutation_body!(
901 proto_gen::node_service::DeleteUserRequest,
902 b"rustfs-delete-user-request-v1\0",
903 |request, body| {
904 body.push_str(&request.access_key)?;
905 }
906);
907impl_canonical_mutation_body!(
908 proto_gen::node_service::DeleteServiceAccountRequest,
909 b"rustfs-delete-service-account-request-v1\0",
910 |request, body| {
911 body.push_str(&request.access_key)?;
912 }
913);
914impl_canonical_mutation_body!(
915 proto_gen::node_service::LoadUserRequest,
916 b"rustfs-load-user-request-v1\0",
917 |request, body| {
918 body.push_str(&request.access_key)?;
919 body.push_bool(request.temp);
920 }
921);
922impl_canonical_mutation_body!(
923 proto_gen::node_service::LoadServiceAccountRequest,
924 b"rustfs-load-service-account-request-v1\0",
925 |request, body| {
926 body.push_str(&request.access_key)?;
927 }
928);
929impl_canonical_mutation_body!(
930 proto_gen::node_service::LoadGroupRequest,
931 b"rustfs-load-group-request-v1\0",
932 |request, body| {
933 body.push_str(&request.group)?;
934 }
935);
936impl_canonical_mutation_body!(
937 proto_gen::node_service::ReloadSiteReplicationConfigRequest,
938 b"rustfs-reload-site-replication-config-request-v1\0"
939);
940impl_canonical_mutation_body!(proto_gen::node_service::ReloadPoolMetaRequest, b"rustfs-reload-pool-meta-request-v1\0");
941impl_canonical_mutation_body!(
942 proto_gen::node_service::StopRebalanceRequest,
943 b"rustfs-stop-rebalance-request-v1\0",
944 |request, body| {
945 body.push_str(&request.expected_rebalance_id)?;
946 }
947);
948impl_canonical_mutation_body!(
949 proto_gen::node_service::LoadRebalanceMetaRequest,
950 b"rustfs-load-rebalance-meta-request-v1\0",
951 |request, body| {
952 body.push_bool(request.start_rebalance);
953 }
954);
955impl_canonical_mutation_body!(
956 proto_gen::node_service::StartDecommissionRequest,
957 b"rustfs-start-decommission-request-v1\0",
958 |request, body| {
959 body.push_count(request.pool_indices.len())?;
960 for pool_index in &request.pool_indices {
961 body.push_u32(*pool_index);
962 }
963 }
964);
965impl_canonical_mutation_body!(
966 proto_gen::node_service::CancelDecommissionRequest,
967 b"rustfs-cancel-decommission-request-v1\0",
968 |request, body| {
969 body.push_u32(request.pool_index);
970 }
971);
972impl_canonical_mutation_body!(
973 proto_gen::node_service::ClearDecommissionRequest,
974 b"rustfs-clear-decommission-request-v1\0",
975 |request, body| {
976 body.push_u32(request.pool_index);
977 }
978);
979impl_canonical_mutation_body!(
980 proto_gen::node_service::LoadTransitionTierConfigRequest,
981 b"rustfs-load-transition-tier-config-request-v1\0"
982);
983
984pub fn canonical_rename_data_request_body(
990 request: &proto_gen::node_service::RenameDataRequest,
991) -> Result<Vec<u8>, std::num::TryFromIntError> {
992 let mut body = CanonicalBodyBuilder::new(b"rustfs-rename-data-request-v1\0");
993 body.push_str(&request.disk)?;
994 body.push_str(&request.src_volume)?;
995 body.push_str(&request.src_path)?;
996 body.push_str(&request.file_info)?;
997 body.push_str(&request.dst_volume)?;
998 body.push_str(&request.dst_path)?;
999 body.push_bytes(&request.file_info_bin)?;
1000 if !request.scanner_publication_lease_token.is_empty() {
1003 body.push_bytes(&request.scanner_publication_lease_token)?;
1004 }
1005 if !request.bucket_incarnation_id.is_empty() {
1006 body.push_str("bucket-incarnation-v1")?;
1007 body.push_bytes(&request.bucket_incarnation_id)?;
1008 }
1009 Ok(body.finish())
1010}
1011
1012pub fn canonical_delete_version_request_body(
1013 request: &proto_gen::node_service::DeleteVersionRequest,
1014) -> Result<Vec<u8>, std::num::TryFromIntError> {
1015 let mut body = CanonicalBodyBuilder::new(b"rustfs-delete-version-request-v1\0");
1016 body.push_str(&request.disk)?;
1017 body.push_str(&request.volume)?;
1018 body.push_str(&request.path)?;
1019 body.push_str(&request.file_info)?;
1020 body.push_bool(request.force_del_marker);
1021 body.push_str(&request.opts)?;
1022 body.push_bytes(&request.file_info_bin)?;
1023 body.push_bytes(&request.opts_bin)?;
1024 if !request.bucket_incarnation_id.is_empty() {
1025 body.push_bytes(&request.bucket_incarnation_id)?;
1026 }
1027 Ok(body.finish())
1028}
1029
1030pub fn canonical_delete_versions_request_body(
1031 request: &proto_gen::node_service::DeleteVersionsRequest,
1032) -> Result<Vec<u8>, std::num::TryFromIntError> {
1033 let mut body = CanonicalBodyBuilder::new(b"rustfs-delete-versions-request-v1\0");
1034 body.push_str(&request.disk)?;
1035 body.push_str(&request.volume)?;
1036 body.push_count(request.versions.len())?;
1037 for version in &request.versions {
1038 body.push_str(version)?;
1039 }
1040 body.push_str(&request.opts)?;
1041 body.push_count(request.versions_bin.len())?;
1042 for version_bin in &request.versions_bin {
1043 body.push_bytes(version_bin)?;
1044 }
1045 body.push_bytes(&request.opts_bin)?;
1046 Ok(body.finish())
1047}
1048
1049pub fn canonical_write_metadata_request_body(
1050 request: &proto_gen::node_service::WriteMetadataRequest,
1051) -> Result<Vec<u8>, std::num::TryFromIntError> {
1052 let mut body = CanonicalBodyBuilder::new(b"rustfs-write-metadata-request-v1\0");
1053 body.push_str(&request.disk)?;
1054 body.push_str(&request.volume)?;
1055 body.push_str(&request.path)?;
1056 body.push_str(&request.file_info)?;
1057 body.push_bytes(&request.file_info_bin)?;
1058 if !request.bucket_incarnation_id.is_empty() {
1059 body.push_str("bucket-incarnation-v1")?;
1060 body.push_bytes(&request.bucket_incarnation_id)?;
1061 }
1062 Ok(body.finish())
1063}
1064
1065pub fn canonical_update_metadata_request_body(
1066 request: &proto_gen::node_service::UpdateMetadataRequest,
1067) -> Result<Vec<u8>, std::num::TryFromIntError> {
1068 let mut body = CanonicalBodyBuilder::new(b"rustfs-update-metadata-request-v1\0");
1069 body.push_str(&request.disk)?;
1070 body.push_str(&request.volume)?;
1071 body.push_str(&request.path)?;
1072 body.push_str(&request.file_info)?;
1073 body.push_str(&request.opts)?;
1074 body.push_bytes(&request.file_info_bin)?;
1075 body.push_bytes(&request.opts_bin)?;
1076 Ok(body.finish())
1077}
1078
1079pub fn canonical_write_all_request_body(
1080 request: &proto_gen::node_service::WriteAllRequest,
1081) -> Result<Vec<u8>, std::num::TryFromIntError> {
1082 let mut body = CanonicalBodyBuilder::new(b"rustfs-write-all-request-v1\0");
1083 body.push_str(&request.disk)?;
1084 body.push_str(&request.volume)?;
1085 body.push_str(&request.path)?;
1086 body.push_bytes(&request.data)?;
1087 Ok(body.finish())
1088}
1089
1090pub fn canonical_compare_and_update_file_request_body(
1091 request: &proto_gen::node_service::CompareAndUpdateFileRequest,
1092) -> Result<Vec<u8>, std::num::TryFromIntError> {
1093 let mut body = CanonicalBodyBuilder::new(b"rustfs-compare-and-update-file-request-v1\0");
1094 body.push_str(&request.disk)?;
1095 body.push_str(&request.volume)?;
1096 body.push_str(&request.path)?;
1097 body.push_bool(request.expected.is_some());
1098 body.push_bytes(request.expected.as_deref().unwrap_or_default())?;
1099 body.push_bool(request.replacement.is_some());
1100 body.push_bytes(request.replacement.as_deref().unwrap_or_default())?;
1101 Ok(body.finish())
1102}
1103
1104pub fn canonical_delete_request_body(
1105 request: &proto_gen::node_service::DeleteRequest,
1106) -> Result<Vec<u8>, std::num::TryFromIntError> {
1107 let mut body = CanonicalBodyBuilder::new(b"rustfs-delete-request-v1\0");
1108 body.push_str(&request.disk)?;
1109 body.push_str(&request.volume)?;
1110 body.push_str(&request.path)?;
1111 body.push_str(&request.options)?;
1112 if !request.scanner_publication_lease_token.is_empty() {
1113 body.push_bytes(&request.scanner_publication_lease_token)?;
1114 }
1115 if !request.bucket_incarnation_id.is_empty() {
1116 body.push_str("bucket-incarnation-v1")?;
1117 body.push_bytes(&request.bucket_incarnation_id)?;
1118 }
1119 Ok(body.finish())
1120}
1121
1122pub fn canonical_delete_paths_request_body(
1123 request: &proto_gen::node_service::DeletePathsRequest,
1124) -> Result<Vec<u8>, std::num::TryFromIntError> {
1125 let mut body = CanonicalBodyBuilder::new(b"rustfs-delete-paths-request-v1\0");
1126 body.push_str(&request.disk)?;
1127 body.push_str(&request.volume)?;
1128 body.push_count(request.paths.len())?;
1129 for path in &request.paths {
1130 body.push_str(path)?;
1131 }
1132 Ok(body.finish())
1133}
1134
1135pub fn canonical_snapshot_lease_request_body(
1136 request: &proto_gen::node_service::SnapshotLeaseRequest,
1137) -> Result<Vec<u8>, std::num::TryFromIntError> {
1138 let mut body = CanonicalBodyBuilder::new(b"rustfs-snapshot-lease-request-v1\0");
1139 body.push_str(&request.disk)?;
1140 body.push_str(&request.volume)?;
1141 body.push_str(&request.path)?;
1142 body.push_u64(request.ttl_ms);
1143 Ok(body.finish())
1144}
1145
1146pub fn canonical_snapshot_lease_renew_request_body(
1147 request: &proto_gen::node_service::SnapshotLeaseRenewRequest,
1148) -> Result<Vec<u8>, std::num::TryFromIntError> {
1149 let mut body = CanonicalBodyBuilder::new(b"rustfs-snapshot-lease-renew-request-v1\0");
1150 body.push_str(&request.disk)?;
1151 body.push_str(&request.volume)?;
1152 body.push_str(&request.path)?;
1153 body.push_bytes(&request.token)?;
1154 body.push_u64(request.ttl_ms);
1155 Ok(body.finish())
1156}
1157
1158pub fn canonical_snapshot_lease_release_request_body(
1159 request: &proto_gen::node_service::SnapshotLeaseReleaseRequest,
1160) -> Result<Vec<u8>, std::num::TryFromIntError> {
1161 let mut body = CanonicalBodyBuilder::new(b"rustfs-snapshot-lease-release-request-v1\0");
1162 body.push_str(&request.disk)?;
1163 body.push_str(&request.volume)?;
1164 body.push_str(&request.path)?;
1165 body.push_bytes(&request.token)?;
1166 Ok(body.finish())
1167}
1168
1169pub fn canonical_rename_file_request_body(
1170 request: &proto_gen::node_service::RenameFileRequest,
1171) -> Result<Vec<u8>, std::num::TryFromIntError> {
1172 let mut body = CanonicalBodyBuilder::new(b"rustfs-rename-file-request-v1\0");
1173 body.push_str(&request.disk)?;
1174 body.push_str(&request.src_volume)?;
1175 body.push_str(&request.src_path)?;
1176 body.push_str(&request.dst_volume)?;
1177 body.push_str(&request.dst_path)?;
1178 if request.durable {
1181 body.push_bool(true);
1182 }
1183 Ok(body.finish())
1184}
1185
1186pub fn canonical_rename_part_request_body(
1187 request: &proto_gen::node_service::RenamePartRequest,
1188) -> Result<Vec<u8>, std::num::TryFromIntError> {
1189 let mut body = CanonicalBodyBuilder::new(b"rustfs-rename-part-request-v1\0");
1190 body.push_str(&request.disk)?;
1191 body.push_str(&request.src_volume)?;
1192 body.push_str(&request.src_path)?;
1193 body.push_str(&request.dst_volume)?;
1194 body.push_str(&request.dst_path)?;
1195 body.push_bytes(&request.meta)?;
1196 Ok(body.finish())
1197}
1198
1199pub fn canonical_prepare_part_transaction_request_body(
1200 request: &proto_gen::node_service::PreparePartTransactionRequest,
1201) -> Result<Vec<u8>, std::num::TryFromIntError> {
1202 let mut body = CanonicalBodyBuilder::new(b"rustfs-prepare-part-transaction-request-v1\0");
1203 body.push_str(&request.disk)?;
1204 body.push_str(&request.src_volume)?;
1205 body.push_str(&request.src_path)?;
1206 body.push_str(&request.dst_volume)?;
1207 body.push_str(&request.dst_path)?;
1208 body.push_bytes(&request.meta)?;
1209 Ok(body.finish())
1210}
1211
1212pub fn canonical_settle_part_transaction_request_body(
1213 request: &proto_gen::node_service::SettlePartTransactionRequest,
1214) -> Result<Vec<u8>, std::num::TryFromIntError> {
1215 let mut body = CanonicalBodyBuilder::new(b"rustfs-settle-part-transaction-request-v1\0");
1216 body.push_str(&request.disk)?;
1217 body.push_str(&request.volume)?;
1218 body.push_str(&request.path)?;
1219 body.push_bool(request.rollback);
1220 Ok(body.finish())
1221}
1222
1223pub fn canonical_delete_volume_request_body(
1224 request: &proto_gen::node_service::DeleteVolumeRequest,
1225) -> Result<Vec<u8>, std::num::TryFromIntError> {
1226 let mut body = CanonicalBodyBuilder::new(b"rustfs-delete-volume-request-v1\0");
1227 body.push_str(&request.disk)?;
1228 body.push_str(&request.volume)?;
1229 body.push_bool(request.force);
1232 Ok(body.finish())
1233}
1234
1235pub fn canonical_make_volume_request_body(
1236 request: &proto_gen::node_service::MakeVolumeRequest,
1237) -> Result<Vec<u8>, std::num::TryFromIntError> {
1238 let mut body = CanonicalBodyBuilder::new(b"rustfs-make-volume-request-v1\0");
1239 body.push_str(&request.disk)?;
1240 body.push_str(&request.volume)?;
1241 Ok(body.finish())
1242}
1243
1244pub fn canonical_make_volumes_request_body(
1245 request: &proto_gen::node_service::MakeVolumesRequest,
1246) -> Result<Vec<u8>, std::num::TryFromIntError> {
1247 let mut body = CanonicalBodyBuilder::new(b"rustfs-make-volumes-request-v1\0");
1248 body.push_str(&request.disk)?;
1249 body.push_count(request.volumes.len())?;
1250 for volume in &request.volumes {
1251 body.push_str(volume)?;
1252 }
1253 Ok(body.finish())
1254}
1255
1256#[cfg(test)]
1257mod disk_mutation_canonical_tests {
1258 use super::proto_gen::node_service::{
1259 CompareAndUpdateFileRequest, DeletePathsRequest, DeleteRequest, DeleteVersionRequest, DeleteVersionsRequest,
1260 DeleteVolumeRequest, MakeVolumeRequest, MakeVolumesRequest, PreparePartTransactionRequest, RenameDataRequest,
1261 RenameFileRequest, RenamePartRequest, SettlePartTransactionRequest, SnapshotLeaseReleaseRequest,
1262 SnapshotLeaseRenewRequest, SnapshotLeaseRequest, UpdateMetadataRequest, WriteAllRequest, WriteMetadataRequest,
1263 };
1264 use super::*;
1265
1266 fn assert_all_distinct(bodies: &[Vec<u8>]) {
1267 for (i, a) in bodies.iter().enumerate() {
1268 for (j, b) in bodies.iter().enumerate() {
1269 if i != j {
1270 assert_ne!(a, b, "canonical bodies {i} and {j} must differ");
1271 }
1272 }
1273 }
1274 }
1275
1276 #[test]
1277 fn rename_data_canonical_body_binds_every_field() {
1278 let baseline = RenameDataRequest {
1279 bucket_incarnation_id: Default::default(),
1280 disk: "disk-a".into(),
1281 src_volume: "src-vol".into(),
1282 src_path: "src-path".into(),
1283 file_info: "{\"v\":1}".into(),
1284 dst_volume: "dst-vol".into(),
1285 dst_path: "dst-path".into(),
1286 file_info_bin: vec![0x81, 0x01].into(),
1287 scanner_publication_lease_token: Vec::new().into(),
1288 };
1289 let mut bodies = vec![canonical_rename_data_request_body(&baseline).unwrap()];
1290 for mutate in [
1291 |r: &mut RenameDataRequest| r.disk = "disk-b".into(),
1292 |r: &mut RenameDataRequest| r.src_volume = "src-vol2".into(),
1293 |r: &mut RenameDataRequest| r.src_path = "src-path2".into(),
1294 |r: &mut RenameDataRequest| r.file_info = "{\"v\":2}".into(),
1295 |r: &mut RenameDataRequest| r.dst_volume = "dst-vol2".into(),
1296 |r: &mut RenameDataRequest| r.dst_path = "dst-path2".into(),
1297 |r: &mut RenameDataRequest| r.file_info_bin = vec![0x81, 0x02].into(),
1298 |r: &mut RenameDataRequest| r.file_info_bin = Vec::new().into(),
1299 |r: &mut RenameDataRequest| r.scanner_publication_lease_token = vec![0x01; 16].into(),
1300 |r: &mut RenameDataRequest| r.bucket_incarnation_id = vec![0x01; 16].into(),
1301 ] {
1302 let mut request = baseline.clone();
1303 mutate(&mut request);
1304 bodies.push(canonical_rename_data_request_body(&request).unwrap());
1305 }
1306 assert_all_distinct(&bodies);
1307 }
1308
1309 #[test]
1310 fn rename_data_canonical_body_is_injective_across_field_boundaries() {
1311 let shifted_left = RenameDataRequest {
1313 src_volume: "ab".into(),
1314 src_path: "c".into(),
1315 ..Default::default()
1316 };
1317 let shifted_right = RenameDataRequest {
1318 src_volume: "a".into(),
1319 src_path: "bc".into(),
1320 ..Default::default()
1321 };
1322 assert_ne!(
1323 canonical_rename_data_request_body(&shifted_left).unwrap(),
1324 canonical_rename_data_request_body(&shifted_right).unwrap(),
1325 );
1326 }
1327
1328 #[test]
1329 fn delete_version_canonical_body_binds_every_field() {
1330 let baseline = DeleteVersionRequest {
1331 bucket_incarnation_id: Default::default(),
1332 disk: "disk-a".into(),
1333 volume: "vol".into(),
1334 path: "path".into(),
1335 file_info: "{\"v\":1}".into(),
1336 force_del_marker: false,
1337 opts: "{}".into(),
1338 file_info_bin: vec![0x81].into(),
1339 opts_bin: vec![0x80].into(),
1340 };
1341 let mut bodies = vec![canonical_delete_version_request_body(&baseline).unwrap()];
1342 for mutate in [
1343 |r: &mut DeleteVersionRequest| r.disk = "disk-b".into(),
1344 |r: &mut DeleteVersionRequest| r.volume = "vol2".into(),
1345 |r: &mut DeleteVersionRequest| r.path = "path2".into(),
1346 |r: &mut DeleteVersionRequest| r.file_info = "{\"v\":2}".into(),
1347 |r: &mut DeleteVersionRequest| r.force_del_marker = true,
1348 |r: &mut DeleteVersionRequest| r.opts = "{\"o\":1}".into(),
1349 |r: &mut DeleteVersionRequest| r.file_info_bin = vec![0x82].into(),
1350 |r: &mut DeleteVersionRequest| r.opts_bin = Vec::new().into(),
1351 |r: &mut DeleteVersionRequest| r.bucket_incarnation_id = vec![1; 16].into(),
1352 ] {
1353 let mut request = baseline.clone();
1354 mutate(&mut request);
1355 bodies.push(canonical_delete_version_request_body(&request).unwrap());
1356 }
1357 assert_all_distinct(&bodies);
1358 }
1359
1360 #[test]
1361 fn delete_versions_canonical_body_binds_lists_and_options() {
1362 let baseline = DeleteVersionsRequest {
1363 disk: "disk-a".into(),
1364 volume: "vol".into(),
1365 versions: vec!["v1".into(), "v2".into()],
1366 opts: "{}".into(),
1367 versions_bin: vec![vec![0x01].into(), vec![0x02].into()],
1368 opts_bin: vec![0x80].into(),
1369 };
1370 let mut bodies = vec![canonical_delete_versions_request_body(&baseline).unwrap()];
1371 for mutate in [
1372 |r: &mut DeleteVersionsRequest| r.disk = "disk-b".into(),
1373 |r: &mut DeleteVersionsRequest| r.volume = "vol2".into(),
1374 |r: &mut DeleteVersionsRequest| r.versions = vec!["v1".into(), "v3".into()],
1375 |r: &mut DeleteVersionsRequest| r.versions = vec!["v1v2".into()],
1376 |r: &mut DeleteVersionsRequest| r.opts = "{\"o\":1}".into(),
1377 |r: &mut DeleteVersionsRequest| r.versions_bin = vec![vec![0x01].into(), vec![0x03].into()],
1378 |r: &mut DeleteVersionsRequest| r.versions_bin = vec![vec![0x01, 0x02].into()],
1379 |r: &mut DeleteVersionsRequest| r.versions_bin = Vec::new(),
1380 |r: &mut DeleteVersionsRequest| r.opts_bin = vec![0x81].into(),
1381 ] {
1382 let mut request = baseline.clone();
1383 mutate(&mut request);
1384 bodies.push(canonical_delete_versions_request_body(&request).unwrap());
1385 }
1386 assert_all_distinct(&bodies);
1387 }
1388
1389 #[test]
1390 fn remaining_disk_mutation_canonical_bodies_bind_every_field() {
1391 let write_metadata = WriteMetadataRequest {
1394 bucket_incarnation_id: Default::default(),
1395 disk: "d".into(),
1396 volume: "v".into(),
1397 path: "p".into(),
1398 file_info: "{\"a\":1}".into(),
1399 file_info_bin: vec![0x81].into(),
1400 };
1401 let mut bodies = vec![canonical_write_metadata_request_body(&write_metadata).unwrap()];
1402 for mutate in [
1403 |r: &mut WriteMetadataRequest| r.disk = "d2".into(),
1404 |r: &mut WriteMetadataRequest| r.volume = "v2".into(),
1405 |r: &mut WriteMetadataRequest| r.path = "p2".into(),
1406 |r: &mut WriteMetadataRequest| r.file_info = "{\"a\":2}".into(),
1407 |r: &mut WriteMetadataRequest| r.file_info_bin = vec![0x82].into(),
1408 |r: &mut WriteMetadataRequest| r.bucket_incarnation_id = vec![1; 16].into(),
1409 ] {
1410 let mut request = write_metadata.clone();
1411 mutate(&mut request);
1412 bodies.push(canonical_write_metadata_request_body(&request).unwrap());
1413 }
1414 assert_all_distinct(&bodies);
1415
1416 let update_metadata = UpdateMetadataRequest {
1417 disk: "d".into(),
1418 volume: "v".into(),
1419 path: "p".into(),
1420 file_info: "{\"a\":1}".into(),
1421 opts: "{\"o\":1}".into(),
1422 file_info_bin: vec![0x81].into(),
1423 opts_bin: vec![0x80].into(),
1424 };
1425 let mut bodies = vec![canonical_update_metadata_request_body(&update_metadata).unwrap()];
1426 for mutate in [
1427 |r: &mut UpdateMetadataRequest| r.disk = "d2".into(),
1428 |r: &mut UpdateMetadataRequest| r.volume = "v2".into(),
1429 |r: &mut UpdateMetadataRequest| r.path = "p2".into(),
1430 |r: &mut UpdateMetadataRequest| r.file_info = "{\"a\":2}".into(),
1431 |r: &mut UpdateMetadataRequest| r.opts = "{\"o\":2}".into(),
1432 |r: &mut UpdateMetadataRequest| r.file_info_bin = vec![0x82].into(),
1433 |r: &mut UpdateMetadataRequest| r.opts_bin = vec![0x81].into(),
1434 ] {
1435 let mut request = update_metadata.clone();
1436 mutate(&mut request);
1437 bodies.push(canonical_update_metadata_request_body(&request).unwrap());
1438 }
1439 assert_all_distinct(&bodies);
1440
1441 let write_all = WriteAllRequest {
1442 disk: "d".into(),
1443 volume: "v".into(),
1444 path: "p".into(),
1445 data: vec![0xAA, 0xBB].into(),
1446 };
1447 let mut bodies = vec![canonical_write_all_request_body(&write_all).unwrap()];
1448 for mutate in [
1449 |r: &mut WriteAllRequest| r.disk = "d2".into(),
1450 |r: &mut WriteAllRequest| r.volume = "v2".into(),
1451 |r: &mut WriteAllRequest| r.path = "p2".into(),
1452 |r: &mut WriteAllRequest| r.data = vec![0xAA, 0xBC].into(),
1453 ] {
1454 let mut request = write_all.clone();
1455 mutate(&mut request);
1456 bodies.push(canonical_write_all_request_body(&request).unwrap());
1457 }
1458 assert_all_distinct(&bodies);
1459
1460 let compare_and_update_file = CompareAndUpdateFileRequest {
1461 disk: "d".into(),
1462 volume: "v".into(),
1463 path: "p".into(),
1464 expected: Some(vec![0xAA, 0xBB].into()),
1465 replacement: Some(vec![0xCC, 0xDD].into()),
1466 };
1467 let mut bodies = vec![canonical_compare_and_update_file_request_body(&compare_and_update_file).unwrap()];
1468 for mutate in [
1469 |r: &mut CompareAndUpdateFileRequest| r.disk = "d2".into(),
1470 |r: &mut CompareAndUpdateFileRequest| r.volume = "v2".into(),
1471 |r: &mut CompareAndUpdateFileRequest| r.path = "p2".into(),
1472 |r: &mut CompareAndUpdateFileRequest| r.expected = None,
1473 |r: &mut CompareAndUpdateFileRequest| r.expected = Some(Vec::new().into()),
1474 |r: &mut CompareAndUpdateFileRequest| r.expected = Some(vec![0xAA, 0xBC].into()),
1475 |r: &mut CompareAndUpdateFileRequest| r.replacement = None,
1476 |r: &mut CompareAndUpdateFileRequest| r.replacement = Some(Vec::new().into()),
1477 |r: &mut CompareAndUpdateFileRequest| r.replacement = Some(vec![0xCC, 0xDE].into()),
1478 ] {
1479 let mut request = compare_and_update_file.clone();
1480 mutate(&mut request);
1481 bodies.push(canonical_compare_and_update_file_request_body(&request).unwrap());
1482 }
1483 assert_all_distinct(&bodies);
1484
1485 let delete = DeleteRequest {
1486 bucket_incarnation_id: Default::default(),
1487 disk: "d".into(),
1488 volume: "v".into(),
1489 path: "p".into(),
1490 options: "{\"o\":1}".into(),
1491 scanner_publication_lease_token: Vec::new().into(),
1492 };
1493 let mut bodies = vec![canonical_delete_request_body(&delete).unwrap()];
1494 for mutate in [
1495 |r: &mut DeleteRequest| r.disk = "d2".into(),
1496 |r: &mut DeleteRequest| r.volume = "v2".into(),
1497 |r: &mut DeleteRequest| r.path = "p2".into(),
1498 |r: &mut DeleteRequest| r.options = "{\"recursive\":true}".into(),
1499 |r: &mut DeleteRequest| r.bucket_incarnation_id = vec![1; 16].into(),
1500 |r: &mut DeleteRequest| r.scanner_publication_lease_token = vec![0x01; 16].into(),
1501 ] {
1502 let mut request = delete.clone();
1503 mutate(&mut request);
1504 bodies.push(canonical_delete_request_body(&request).unwrap());
1505 }
1506 assert_all_distinct(&bodies);
1507
1508 let delete_paths = DeletePathsRequest {
1509 disk: "d".into(),
1510 volume: "v".into(),
1511 paths: vec!["a".into(), "b".into()],
1512 };
1513 let mut bodies = vec![canonical_delete_paths_request_body(&delete_paths).unwrap()];
1514 for mutate in [
1515 |r: &mut DeletePathsRequest| r.disk = "d2".into(),
1516 |r: &mut DeletePathsRequest| r.volume = "v2".into(),
1517 |r: &mut DeletePathsRequest| r.paths = vec!["a".into(), "c".into()],
1518 |r: &mut DeletePathsRequest| r.paths = vec!["ab".into()],
1519 ] {
1520 let mut request = delete_paths.clone();
1521 mutate(&mut request);
1522 bodies.push(canonical_delete_paths_request_body(&request).unwrap());
1523 }
1524 assert_all_distinct(&bodies);
1525
1526 let rename_file = RenameFileRequest {
1527 durable: false,
1528 disk: "d".into(),
1529 src_volume: "sv".into(),
1530 src_path: "sp".into(),
1531 dst_volume: "dv".into(),
1532 dst_path: "dp".into(),
1533 };
1534 let mut bodies = vec![canonical_rename_file_request_body(&rename_file).unwrap()];
1535 for mutate in [
1536 |r: &mut RenameFileRequest| r.durable = true,
1537 |r: &mut RenameFileRequest| r.disk = "d2".into(),
1538 |r: &mut RenameFileRequest| r.src_volume = "sv2".into(),
1539 |r: &mut RenameFileRequest| r.src_path = "sp2".into(),
1540 |r: &mut RenameFileRequest| r.dst_volume = "dv2".into(),
1541 |r: &mut RenameFileRequest| r.dst_path = "dp2".into(),
1542 ] {
1543 let mut request = rename_file.clone();
1544 mutate(&mut request);
1545 bodies.push(canonical_rename_file_request_body(&request).unwrap());
1546 }
1547 assert_all_distinct(&bodies);
1548
1549 let prepare_part = PreparePartTransactionRequest {
1550 disk: "d".into(),
1551 src_volume: "sv".into(),
1552 src_path: "sp".into(),
1553 dst_volume: "dv".into(),
1554 dst_path: "dp".into(),
1555 meta: vec![0x01].into(),
1556 };
1557 let mut bodies = vec![canonical_prepare_part_transaction_request_body(&prepare_part).unwrap()];
1558 for mutate in [
1559 |r: &mut PreparePartTransactionRequest| r.disk = "d2".into(),
1560 |r: &mut PreparePartTransactionRequest| r.src_volume = "sv2".into(),
1561 |r: &mut PreparePartTransactionRequest| r.src_path = "sp2".into(),
1562 |r: &mut PreparePartTransactionRequest| r.dst_volume = "dv2".into(),
1563 |r: &mut PreparePartTransactionRequest| r.dst_path = "dp2".into(),
1564 |r: &mut PreparePartTransactionRequest| r.meta = vec![0x02].into(),
1565 ] {
1566 let mut request = prepare_part.clone();
1567 mutate(&mut request);
1568 bodies.push(canonical_prepare_part_transaction_request_body(&request).unwrap());
1569 }
1570 assert_all_distinct(&bodies);
1571
1572 let settle_part = SettlePartTransactionRequest {
1573 disk: "d".into(),
1574 volume: "v".into(),
1575 path: "p".into(),
1576 rollback: false,
1577 };
1578 let mut bodies = vec![canonical_settle_part_transaction_request_body(&settle_part).unwrap()];
1579 for mutate in [
1580 |r: &mut SettlePartTransactionRequest| r.disk = "d2".into(),
1581 |r: &mut SettlePartTransactionRequest| r.volume = "v2".into(),
1582 |r: &mut SettlePartTransactionRequest| r.path = "p2".into(),
1583 |r: &mut SettlePartTransactionRequest| r.rollback = true,
1584 ] {
1585 let mut request = settle_part.clone();
1586 mutate(&mut request);
1587 bodies.push(canonical_settle_part_transaction_request_body(&request).unwrap());
1588 }
1589 assert_all_distinct(&bodies);
1590
1591 let rename_part = RenamePartRequest {
1592 disk: "d".into(),
1593 src_volume: "sv".into(),
1594 src_path: "sp".into(),
1595 dst_volume: "dv".into(),
1596 dst_path: "dp".into(),
1597 meta: vec![0x01].into(),
1598 };
1599 let mut bodies = vec![canonical_rename_part_request_body(&rename_part).unwrap()];
1600 for mutate in [
1601 |r: &mut RenamePartRequest| r.disk = "d2".into(),
1602 |r: &mut RenamePartRequest| r.src_volume = "sv2".into(),
1603 |r: &mut RenamePartRequest| r.src_path = "sp2".into(),
1604 |r: &mut RenamePartRequest| r.dst_volume = "dv2".into(),
1605 |r: &mut RenamePartRequest| r.dst_path = "dp2".into(),
1606 |r: &mut RenamePartRequest| r.meta = vec![0x02].into(),
1607 ] {
1608 let mut request = rename_part.clone();
1609 mutate(&mut request);
1610 bodies.push(canonical_rename_part_request_body(&request).unwrap());
1611 }
1612 assert_all_distinct(&bodies);
1613 }
1614
1615 #[test]
1616 fn volume_mutation_canonical_bodies_bind_their_fields() {
1617 let delete_volume = DeleteVolumeRequest {
1618 disk: "http://node-a:9000/data/rustfs0".into(),
1619 volume: "bucket".into(),
1620 force: false,
1621 };
1622 let mut recursive = delete_volume.clone();
1624 recursive.force = true;
1625 assert_ne!(
1626 canonical_delete_volume_request_body(&delete_volume).unwrap(),
1627 canonical_delete_volume_request_body(&recursive).unwrap(),
1628 );
1629 let mut retargeted = delete_volume.clone();
1630 retargeted.volume = "victim".into();
1631 assert_ne!(
1632 canonical_delete_volume_request_body(&delete_volume).unwrap(),
1633 canonical_delete_volume_request_body(&retargeted).unwrap(),
1634 );
1635
1636 let make_volume = MakeVolumeRequest {
1637 disk: "d".into(),
1638 volume: "v".into(),
1639 };
1640 let mut tampered = make_volume.clone();
1641 tampered.volume = "v2".into();
1642 assert_ne!(
1643 canonical_make_volume_request_body(&make_volume).unwrap(),
1644 canonical_make_volume_request_body(&tampered).unwrap(),
1645 );
1646
1647 let make_volumes = MakeVolumesRequest {
1648 disk: "d".into(),
1649 volumes: vec!["a".into(), "b".into()],
1650 };
1651 let mut merged = make_volumes.clone();
1652 merged.volumes = vec!["ab".into()];
1653 assert_ne!(
1654 canonical_make_volumes_request_body(&make_volumes).unwrap(),
1655 canonical_make_volumes_request_body(&merged).unwrap(),
1656 );
1657 }
1658
1659 #[test]
1660 fn snapshot_lease_canonical_bodies_bind_every_field() {
1661 let acquire = SnapshotLeaseRequest {
1662 disk: "d".into(),
1663 volume: "v".into(),
1664 path: "p".into(),
1665 ttl_ms: 60_000,
1666 };
1667 let mut acquire_bodies = vec![canonical_snapshot_lease_request_body(&acquire).unwrap()];
1668 for mutate in [
1669 |r: &mut SnapshotLeaseRequest| r.disk = "d2".into(),
1670 |r: &mut SnapshotLeaseRequest| r.volume = "v2".into(),
1671 |r: &mut SnapshotLeaseRequest| r.path = "p2".into(),
1672 |r: &mut SnapshotLeaseRequest| r.ttl_ms = 60_001,
1673 ] {
1674 let mut request = acquire.clone();
1675 mutate(&mut request);
1676 acquire_bodies.push(canonical_snapshot_lease_request_body(&request).unwrap());
1677 }
1678 assert_all_distinct(&acquire_bodies);
1679
1680 let renew = SnapshotLeaseRenewRequest {
1681 disk: "d".into(),
1682 volume: "v".into(),
1683 path: "p".into(),
1684 token: vec![1; 16].into(),
1685 ttl_ms: 60_000,
1686 };
1687 let mut changed_token = renew.clone();
1688 changed_token.token = vec![2; 16].into();
1689 let mut changed_ttl = renew.clone();
1690 changed_ttl.ttl_ms += 1;
1691 assert_all_distinct(&[
1692 canonical_snapshot_lease_renew_request_body(&renew).unwrap(),
1693 canonical_snapshot_lease_renew_request_body(&changed_token).unwrap(),
1694 canonical_snapshot_lease_renew_request_body(&changed_ttl).unwrap(),
1695 ]);
1696
1697 let release = SnapshotLeaseReleaseRequest {
1698 disk: "d".into(),
1699 volume: "v".into(),
1700 path: "p".into(),
1701 token: vec![1; 16].into(),
1702 };
1703 let mut changed_release = release.clone();
1704 changed_release.token = vec![2; 16].into();
1705 assert_ne!(
1706 canonical_snapshot_lease_release_request_body(&release).unwrap(),
1707 canonical_snapshot_lease_release_request_body(&changed_release).unwrap()
1708 );
1709 }
1710
1711 #[test]
1712 fn durable_rename_extension_preserves_old_wire_defaults_and_signatures() {
1713 use crate::proto_gen::node_service::RenameFileResponse;
1714 use prost::Message;
1715 let mut request =
1717 RenameFileRequest::decode(b"\x0a\x01d\x12\x01s\x1a\x01p\x22\x01v\x2a\x01q".as_slice()).expect("release request");
1718 assert!(!request.durable);
1719 let old_body = b"rustfs-rename-file-request-v1\0\0\0\0\0\0\0\0\x01d\0\0\0\0\0\0\0\x01s\0\0\0\0\0\0\0\x01p\0\0\0\0\0\0\0\x01v\0\0\0\0\0\0\0\x01q";
1720 assert_eq!(canonical_rename_file_request_body(&request).expect("body"), old_body);
1721 request.durable = true;
1722 assert_ne!(canonical_rename_file_request_body(&request).expect("durable body"), old_body);
1723 let old_success = RenameFileResponse::decode(b"\x08\x01".as_slice()).expect("release response");
1724 assert!(old_success.success);
1725 assert!(!old_success.durability_applied, "old success cannot certify durable publication");
1726 }
1727
1728 #[test]
1729 fn disk_mutation_canonical_domains_are_distinct_per_message() {
1730 let rename_file = RenameFileRequest {
1732 durable: false,
1733 disk: "d".into(),
1734 src_volume: "sv".into(),
1735 src_path: "sp".into(),
1736 dst_volume: "dv".into(),
1737 dst_path: "dp".into(),
1738 };
1739 let rename_part = RenamePartRequest {
1740 disk: "d".into(),
1741 src_volume: "sv".into(),
1742 src_path: "sp".into(),
1743 dst_volume: "dv".into(),
1744 dst_path: "dp".into(),
1745 meta: Vec::new().into(),
1746 };
1747 assert_ne!(
1748 canonical_rename_file_request_body(&rename_file).unwrap(),
1749 canonical_rename_part_request_body(&rename_part).unwrap(),
1750 );
1751 }
1752}
1753
1754#[cfg(test)]
1755mod non_disk_mutation_canonical_tests {
1756 use super::*;
1757 use proto_gen::node_service::*;
1758 use std::collections::HashMap;
1759
1760 macro_rules! assert_fields_bound {
1761 ($request:ty, {$($field:ident: $value:expr),+ $(,)?}) => {{
1762 let baseline = <$request>::default();
1763 let expected = baseline.canonical_body().expect("baseline canonical body should encode");
1764 $(
1765 let mut variant = baseline.clone();
1766 variant.$field = $value;
1767 assert_ne!(
1768 expected,
1769 variant.canonical_body().expect("variant canonical body should encode"),
1770 concat!(stringify!($request), " omitted field ", stringify!($field)),
1771 );
1772 )+
1773 }};
1774 }
1775
1776 fn signal_request(signal: Option<&str>, sub_system: Option<&str>, dry_run: Option<&str>) -> SignalServiceRequest {
1777 let mut value = HashMap::new();
1778 if let Some(signal) = signal {
1779 value.insert(PEER_RESTSIGNAL.to_string(), signal.to_string());
1780 }
1781 if let Some(sub_system) = sub_system {
1782 value.insert(PEER_RESTSUB_SYS.to_string(), sub_system.to_string());
1783 }
1784 if let Some(dry_run) = dry_run {
1785 value.insert(PEER_RESTDRY_RUN.to_string(), dry_run.to_string());
1786 }
1787 SignalServiceRequest {
1788 vars: Some(Mss { value }),
1789 }
1790 }
1791
1792 #[test]
1793 fn signal_service_canonical_body_is_versioned_and_binds_every_semantic_field() {
1794 let request = signal_request(Some("2"), Some("scanner"), Some("false"));
1795 let baseline = request.canonical_body().expect("small signal request should encode");
1796 assert!(baseline.starts_with(b"rustfs-signal-service-request-v1\0"));
1797
1798 for variant in [
1799 signal_request(Some("1"), Some("scanner"), Some("false")),
1800 signal_request(Some("2"), Some("heal"), Some("false")),
1801 signal_request(Some("2"), Some("scanner"), Some("true")),
1802 signal_request(None, Some("scanner"), Some("false")),
1803 signal_request(Some("2"), None, Some("false")),
1804 signal_request(Some("2"), Some("scanner"), None),
1805 ] {
1806 assert_ne!(baseline, variant.canonical_body().expect("small signal request should encode"));
1807 }
1808
1809 let mut ignored_field = request;
1810 ignored_field
1811 .vars
1812 .as_mut()
1813 .expect("signal vars should exist")
1814 .value
1815 .insert("future-field".to_string(), "ignored".to_string());
1816 assert_eq!(
1817 baseline,
1818 ignored_field
1819 .canonical_body()
1820 .expect("unknown signal field should not affect the semantic body"),
1821 );
1822 }
1823
1824 #[test]
1825 fn signal_service_canonical_body_distinguishes_missing_and_empty_fields() {
1826 assert_ne!(
1827 signal_request(None, Some("scanner"), Some("false"))
1828 .canonical_body()
1829 .expect("missing signal should encode"),
1830 signal_request(Some(""), Some("scanner"), Some("false"))
1831 .canonical_body()
1832 .expect("empty signal should encode"),
1833 );
1834 }
1835
1836 #[test]
1837 fn bucket_and_lock_canonical_bodies_bind_every_semantic_field() {
1838 assert_fields_bound!(HealBucketRequest, { bucket: "bucket".into(), options: "opts".into(), bucket_incarnation_id: vec![1; 16].into() });
1839 assert_fields_bound!(MakeBucketRequest, { name: "bucket".into(), options: "opts".into() });
1840 assert_fields_bound!(DeleteBucketRequest, { bucket: "bucket".into(), options: "opts".into() });
1841 assert_fields_bound!(GenerallyLockRequest, { args: "lock".into() });
1842 assert_fields_bound!(BatchGenerallyLockRequest, { args: vec!["first".into(), "second".into()] });
1843
1844 let first = BatchGenerallyLockRequest {
1845 args: vec!["first".into(), "second".into()],
1846 };
1847 let reversed = BatchGenerallyLockRequest {
1848 args: vec!["second".into(), "first".into()],
1849 };
1850 assert_ne!(first.canonical_body().unwrap(), reversed.canonical_body().unwrap());
1851 }
1852
1853 #[test]
1854 fn metadata_and_iam_canonical_bodies_bind_every_semantic_field() {
1855 assert_fields_bound!(LoadBucketMetadataRequest, {
1856 bucket: "bucket".into(),
1857 scanner_maintenance_change: true,
1858 });
1859 assert_fields_bound!(DeleteBucketMetadataRequest, { bucket: "bucket".into() });
1860 assert_fields_bound!(DeletePolicyRequest, { policy_name: "policy".into() });
1861 assert_fields_bound!(LoadPolicyRequest, { policy_name: "policy".into() });
1862 assert_fields_bound!(LoadPolicyMappingRequest, {
1863 user_or_group: "user".into(),
1864 user_type: 1,
1865 is_group: true,
1866 });
1867 assert_fields_bound!(DeleteUserRequest, { access_key: "user".into() });
1868 assert_fields_bound!(DeleteServiceAccountRequest, { access_key: "service".into() });
1869 assert_fields_bound!(LoadUserRequest, { access_key: "user".into(), temp: true });
1870 assert_fields_bound!(LoadServiceAccountRequest, { access_key: "service".into() });
1871 assert_fields_bound!(LoadGroupRequest, { group: "group".into() });
1872 }
1873
1874 #[test]
1875 fn control_plane_canonical_bodies_bind_every_semantic_field() {
1876 assert_fields_bound!(StopRebalanceRequest, { expected_rebalance_id: "rebalance".into() });
1877 assert_fields_bound!(LoadRebalanceMetaRequest, { start_rebalance: true });
1878 assert_fields_bound!(StartDecommissionRequest, { pool_indices: vec![1, 2] });
1879 assert_fields_bound!(CancelDecommissionRequest, { pool_index: 1 });
1880 assert_fields_bound!(ClearDecommissionRequest, { pool_index: 1 });
1881
1882 let first = StartDecommissionRequest {
1883 pool_indices: vec![1, 2],
1884 };
1885 let reversed = StartDecommissionRequest {
1886 pool_indices: vec![2, 1],
1887 };
1888 assert_ne!(first.canonical_body().unwrap(), reversed.canonical_body().unwrap());
1889
1890 let empty_domains = [
1891 ReloadSiteReplicationConfigRequest::default().canonical_body().unwrap(),
1892 ReloadPoolMetaRequest::default().canonical_body().unwrap(),
1893 LoadTransitionTierConfigRequest::default().canonical_body().unwrap(),
1894 ];
1895 assert!(empty_domains.iter().all(|body| !body.is_empty()));
1896 assert_ne!(empty_domains[0], empty_domains[1]);
1897 assert_ne!(empty_domains[0], empty_domains[2]);
1898 assert_ne!(empty_domains[1], empty_domains[2]);
1899 }
1900}
1901
1902#[cfg(test)]
1903mod scanner_activity_tests {
1904 use super::{
1905 canonical_scanner_activity_request_body, canonical_scanner_activity_response_body,
1906 canonical_scanner_activity_v4_response_body, canonical_scanner_activity_v7_response_body,
1907 canonical_scanner_dirty_usage_snapshot_request_body, canonical_scanner_dirty_usage_snapshot_response_body,
1908 canonical_scanner_publication_lease_release_request_body, canonical_scanner_publication_lease_request_body,
1909 canonical_scanner_publication_lease_response_body,
1910 proto_gen::node_service::{
1911 ScannerActivityRequest, ScannerActivityResponse, ScannerDirtyUsageBucket, ScannerDirtyUsageSnapshotRequest,
1912 ScannerDirtyUsageSnapshotResponse, ScannerPublicationLeaseRequest, ScannerPublicationLeaseResponse,
1913 },
1914 };
1915
1916 #[test]
1917 fn canonical_scanner_dirty_usage_snapshot_request_binds_every_field() {
1918 let request = ScannerDirtyUsageSnapshotRequest {
1919 challenge: vec![1; 16].into(),
1920 protocol_version: 1,
1921 };
1922 let baseline = canonical_scanner_dirty_usage_snapshot_request_body(&request)
1923 .expect("scanner dirty usage snapshot request should encode");
1924
1925 for variant in [
1926 ScannerDirtyUsageSnapshotRequest {
1927 challenge: vec![2; 16].into(),
1928 ..request.clone()
1929 },
1930 ScannerDirtyUsageSnapshotRequest {
1931 protocol_version: 2,
1932 ..request
1933 },
1934 ] {
1935 assert_ne!(
1936 baseline,
1937 canonical_scanner_dirty_usage_snapshot_request_body(&variant)
1938 .expect("scanner dirty usage snapshot request variant should encode")
1939 );
1940 }
1941 }
1942
1943 #[test]
1944 fn canonical_scanner_dirty_usage_snapshot_response_binds_every_field() {
1945 let response = ScannerDirtyUsageSnapshotResponse {
1946 instance_id: "0123456789abcdef0123456789abcdef".to_string(),
1947 generation: 7,
1948 pending_bucket_count: 2,
1949 protocol_version: 1,
1950 complete: true,
1951 buckets: vec![
1952 ScannerDirtyUsageBucket {
1953 bucket: "archive".to_string(),
1954 generation: 3,
1955 bucket_incarnation: vec![1; 16].into(),
1956 },
1957 ScannerDirtyUsageBucket {
1958 bucket: "photos".to_string(),
1959 generation: 7,
1960 bucket_incarnation: vec![2; 16].into(),
1961 },
1962 ],
1963 response_proof: vec![9; 32].into(),
1964 owner_id: "11111111-1111-1111-1111-111111111111".to_string(),
1965 };
1966 let baseline = canonical_scanner_dirty_usage_snapshot_response_body(&[1; 16], &response)
1967 .expect("scanner dirty usage snapshot response should encode");
1968 let mut variants = Vec::new();
1969 let mut instance = response.clone();
1970 instance.instance_id = "fedcba9876543210fedcba9876543210".to_string();
1971 variants.push(instance);
1972 let mut generation = response.clone();
1973 generation.generation = 8;
1974 variants.push(generation);
1975 let mut count = response.clone();
1976 count.pending_bucket_count = 3;
1977 variants.push(count);
1978 let mut protocol = response.clone();
1979 protocol.protocol_version = 2;
1980 variants.push(protocol);
1981 let mut owner = response.clone();
1982 owner.owner_id = "22222222-2222-2222-2222-222222222222".to_string();
1983 variants.push(owner);
1984 let mut complete = response.clone();
1985 complete.complete = false;
1986 variants.push(complete);
1987 let mut bucket_name = response.clone();
1988 bucket_name.buckets[0].bucket = "backups".to_string();
1989 variants.push(bucket_name);
1990 let mut bucket_generation = response.clone();
1991 bucket_generation.buckets[0].generation = 4;
1992 variants.push(bucket_generation);
1993 let mut bucket_incarnation = response.clone();
1994 bucket_incarnation.buckets[0].bucket_incarnation = vec![3; 16].into();
1995 variants.push(bucket_incarnation);
1996 let mut bucket_order = response.clone();
1997 bucket_order.buckets.reverse();
1998 variants.push(bucket_order);
1999
2000 for variant in variants {
2001 assert_ne!(
2002 baseline,
2003 canonical_scanner_dirty_usage_snapshot_response_body(&[1; 16], &variant)
2004 .expect("scanner dirty usage snapshot response variant should encode")
2005 );
2006 }
2007 assert_ne!(
2008 baseline,
2009 canonical_scanner_dirty_usage_snapshot_response_body(&[2; 16], &response)
2010 .expect("scanner dirty usage snapshot response challenge variant should encode")
2011 );
2012
2013 let mut proof_only = response;
2014 proof_only.response_proof = vec![8; 32].into();
2015 assert_eq!(
2016 baseline,
2017 canonical_scanner_dirty_usage_snapshot_response_body(&[1; 16], &proof_only)
2018 .expect("response proof must not authenticate itself")
2019 );
2020 }
2021
2022 #[test]
2023 fn canonical_scanner_activity_request_binds_every_field() {
2024 let request = ScannerActivityRequest {
2025 challenge: vec![1; 16].into(),
2026 protocol_version: 5,
2027 acknowledge_instance_id: "0123456789abcdef0123456789abcdef".to_string(),
2028 acknowledge_dirty_usage_generation: 11,
2029 };
2030 let baseline = canonical_scanner_activity_request_body(&request).expect("scanner activity request should encode");
2031
2032 let variants = [
2033 ScannerActivityRequest {
2034 challenge: vec![2; 16].into(),
2035 ..request.clone()
2036 },
2037 ScannerActivityRequest {
2038 protocol_version: 4,
2039 ..request.clone()
2040 },
2041 ScannerActivityRequest {
2042 acknowledge_instance_id: "1123456789abcdef0123456789abcdef".to_string(),
2043 ..request.clone()
2044 },
2045 ScannerActivityRequest {
2046 acknowledge_dirty_usage_generation: 12,
2047 ..request
2048 },
2049 ];
2050 for variant in variants {
2051 assert_ne!(
2052 baseline,
2053 canonical_scanner_activity_request_body(&variant).expect("scanner activity request variant should encode")
2054 );
2055 }
2056 }
2057
2058 #[test]
2059 fn canonical_scanner_activity_response_binds_challenge_and_every_status_field() {
2060 let response = ScannerActivityResponse {
2061 instance_id: "0123456789abcdef0123456789abcdef".to_string(),
2062 namespace_generation: 7,
2063 maintenance_generation: 3,
2064 protocol_version: 4,
2065 topology_digest: vec![9; 32].into(),
2066 data_movement_active: true,
2067 response_proof: Vec::new().into(),
2068 dirty_usage_generation: 11,
2069 dirty_usage_pending: true,
2070 movement_generation: Some(19),
2071 publication_blocked: Some(false),
2072 };
2073 let baseline =
2074 canonical_scanner_activity_response_body(&[1; 16], &response).expect("scanner activity response should encode");
2075
2076 let variants = [
2077 ScannerActivityResponse {
2078 instance_id: "1123456789abcdef0123456789abcdef".to_string(),
2079 ..response.clone()
2080 },
2081 ScannerActivityResponse {
2082 namespace_generation: 8,
2083 ..response.clone()
2084 },
2085 ScannerActivityResponse {
2086 maintenance_generation: 4,
2087 ..response.clone()
2088 },
2089 ScannerActivityResponse {
2090 protocol_version: 5,
2091 ..response.clone()
2092 },
2093 ScannerActivityResponse {
2094 topology_digest: vec![8; 32].into(),
2095 ..response.clone()
2096 },
2097 ScannerActivityResponse {
2098 data_movement_active: false,
2099 ..response.clone()
2100 },
2101 ScannerActivityResponse {
2102 dirty_usage_generation: 12,
2103 ..response.clone()
2104 },
2105 ScannerActivityResponse {
2106 dirty_usage_pending: false,
2107 ..response.clone()
2108 },
2109 ];
2110 for variant in variants {
2111 assert_ne!(
2112 baseline,
2113 canonical_scanner_activity_response_body(&[1; 16], &variant)
2114 .expect("scanner activity response variant should encode")
2115 );
2116 }
2117 assert_ne!(
2118 baseline,
2119 canonical_scanner_activity_response_body(&[2; 16], &response)
2120 .expect("scanner activity response with a different challenge should encode")
2121 );
2122
2123 let v7_baseline =
2124 canonical_scanner_activity_v7_response_body(&[1; 16], &response).expect("scanner activity v7 response should encode");
2125 for variant in [
2126 ScannerActivityResponse {
2127 movement_generation: Some(20),
2128 ..response.clone()
2129 },
2130 ScannerActivityResponse {
2131 publication_blocked: Some(true),
2132 ..response
2133 },
2134 ] {
2135 assert_ne!(
2136 v7_baseline,
2137 canonical_scanner_activity_v7_response_body(&[1; 16], &variant)
2138 .expect("scanner activity v7 response variant should encode")
2139 );
2140 }
2141 }
2142
2143 #[test]
2144 fn scanner_activity_v4_response_canonicalization_ignores_v5_fields() {
2145 let response = ScannerActivityResponse {
2146 instance_id: "0123456789abcdef0123456789abcdef".to_string(),
2147 namespace_generation: 7,
2148 maintenance_generation: 3,
2149 protocol_version: 4,
2150 topology_digest: vec![9; 32].into(),
2151 data_movement_active: true,
2152 response_proof: Vec::new().into(),
2153 dirty_usage_generation: 0,
2154 dirty_usage_pending: false,
2155 movement_generation: None,
2156 publication_blocked: None,
2157 };
2158 let baseline =
2159 canonical_scanner_activity_v4_response_body(&[1; 16], &response).expect("scanner activity v4 response should encode");
2160 let expected = [
2161 b"rustfs-scanner-activity-response-v1\0".as_slice(),
2162 16u64.to_be_bytes().as_slice(),
2163 [1u8; 16].as_slice(),
2164 32u64.to_be_bytes().as_slice(),
2165 b"0123456789abcdef0123456789abcdef".as_slice(),
2166 7u64.to_be_bytes().as_slice(),
2167 3u64.to_be_bytes().as_slice(),
2168 4u32.to_be_bytes().as_slice(),
2169 32u64.to_be_bytes().as_slice(),
2170 [9u8; 32].as_slice(),
2171 [1u8].as_slice(),
2172 ]
2173 .concat();
2174 assert_eq!(
2175 baseline, expected,
2176 "protocol v4 response bytes must remain stable during rolling upgrades"
2177 );
2178 let extended = ScannerActivityResponse {
2179 dirty_usage_generation: 11,
2180 dirty_usage_pending: true,
2181 ..response
2182 };
2183
2184 assert_eq!(
2185 baseline,
2186 canonical_scanner_activity_v4_response_body(&[1; 16], &extended)
2187 .expect("scanner activity v4 response should ignore v5 fields")
2188 );
2189 }
2190
2191 #[test]
2192 fn scanner_publication_lease_canonical_bodies_bind_session_owner_and_generation() {
2193 let request = ScannerPublicationLeaseRequest {
2194 challenge: vec![1; 16].into(),
2195 expected_movement_generation: 7,
2196 ttl_ms: 60_000,
2197 expected_session_id: "session-a".to_string(),
2198 token: Vec::new().into(),
2199 };
2200 let baseline = canonical_scanner_publication_lease_request_body(&request).unwrap();
2201 for variant in [
2202 ScannerPublicationLeaseRequest {
2203 expected_movement_generation: 8,
2204 ..request.clone()
2205 },
2206 ScannerPublicationLeaseRequest {
2207 expected_session_id: "session-b".to_string(),
2208 ..request.clone()
2209 },
2210 ScannerPublicationLeaseRequest {
2211 ttl_ms: 30_000,
2212 ..request.clone()
2213 },
2214 ScannerPublicationLeaseRequest {
2215 token: vec![3; 16].into(),
2216 ..request
2217 },
2218 ] {
2219 assert_ne!(baseline, canonical_scanner_publication_lease_request_body(&variant).unwrap());
2220 }
2221
2222 let release_a = crate::proto_gen::node_service::ScannerPublicationLeaseReleaseRequest {
2223 challenge: vec![2; 16].into(),
2224 token: vec![3; 16].into(),
2225 owner_id: "owner-a".to_string(),
2226 session_id: "session-a".to_string(),
2227 };
2228 let release_b = crate::proto_gen::node_service::ScannerPublicationLeaseReleaseRequest {
2229 owner_id: "owner-b".to_string(),
2230 ..release_a.clone()
2231 };
2232 assert_ne!(
2233 canonical_scanner_publication_lease_release_request_body(&release_a).unwrap(),
2234 canonical_scanner_publication_lease_release_request_body(&release_b).unwrap()
2235 );
2236
2237 let response = ScannerPublicationLeaseResponse {
2238 success: true,
2239 token: vec![4; 16].into(),
2240 movement_generation: 7,
2241 lease_ttl_ms: 60_000,
2242 error: None,
2243 response_proof: Vec::new().into(),
2244 owner_id: "owner-a".to_string(),
2245 session_id: "session-a".to_string(),
2246 };
2247 let response_changed = ScannerPublicationLeaseResponse {
2248 owner_id: "owner-b".to_string(),
2249 ..response.clone()
2250 };
2251 assert_ne!(
2252 canonical_scanner_publication_lease_response_body(&[1; 16], &response).unwrap(),
2253 canonical_scanner_publication_lease_response_body(&[1; 16], &response_changed).unwrap()
2254 );
2255 }
2256}
2257
2258#[cfg(test)]
2259mod heal_control_tests {
2260 use super::{
2261 CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX, HEAL_CONTROL_CAPABILITY_PROBE_PREFIX, HEAL_CONTROL_PROTOCOL_VERSION,
2262 ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX, REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX,
2263 TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX, canonical_heal_control_capability_ack,
2264 canonical_heal_control_request_body, canonical_heal_control_response_body, decode_remote_version_state_capability,
2265 encode_cross_pool_fence_capability, encode_remote_version_state_capability, heal_control_capability_probe,
2266 heal_control_coordinator_epoch, heal_control_execution_timeout, heal_control_execution_timeout_for,
2267 ilm_recovery_export_capability_probe, internode_rpc_timeout, is_cross_pool_fence_capability_probe,
2268 is_heal_control_capability_probe, is_ilm_recovery_export_capability_probe, is_remote_version_state_capability_probe,
2269 is_transition_transaction_compaction_capability_probe, normalize_internode_rpc_timeout,
2270 remote_version_state_capability_probe, transition_transaction_compaction_capability_probe,
2271 };
2272 use crate::heal_control;
2273 use std::time::Duration;
2274
2275 #[test]
2276 fn canonical_heal_control_body_binds_every_field_and_boundary() {
2277 let baseline = canonical_heal_control_request_body(1, "ab", b"c").expect("small request should encode");
2278 let mut golden = b"rustfs-heal-control-v3\0".to_vec();
2279 golden.extend_from_slice(&1_u32.to_be_bytes());
2280 golden.extend_from_slice(&2_u64.to_be_bytes());
2281 golden.extend_from_slice(b"ab");
2282 golden.extend_from_slice(&1_u64.to_be_bytes());
2283 golden.extend_from_slice(b"c");
2284 assert_eq!(baseline, golden);
2285
2286 assert_ne!(
2287 baseline,
2288 canonical_heal_control_request_body(2, "ab", b"c").expect("small request should encode")
2289 );
2290 assert_ne!(
2291 baseline,
2292 canonical_heal_control_request_body(1, "ac", b"c").expect("small request should encode")
2293 );
2294 assert_ne!(
2295 baseline,
2296 canonical_heal_control_request_body(1, "ab", b"d").expect("small request should encode")
2297 );
2298 assert_ne!(
2299 canonical_heal_control_request_body(1, "ab", b"c").expect("small request should encode"),
2300 canonical_heal_control_request_body(1, "a", b"bc").expect("small request should encode")
2301 );
2302 }
2303
2304 #[test]
2305 fn canonical_capability_ack_binds_version_and_topology() {
2306 assert_eq!(HEAL_CONTROL_PROTOCOL_VERSION, 3);
2307 assert!(HEAL_CONTROL_CAPABILITY_PROBE_PREFIX.starts_with(b"rustfs-heal-control-capability-v4"));
2308 let probe = heal_control_capability_probe(&[7; 16]);
2309 let ack = canonical_heal_control_capability_ack(1, "ab", &probe).expect("small acknowledgement should encode");
2310 let mut golden = b"rustfs-heal-control-capability-ack-v4\0".to_vec();
2311 golden.extend_from_slice(&1_u32.to_be_bytes());
2312 golden.extend_from_slice(&2_u64.to_be_bytes());
2313 golden.extend_from_slice(b"ab");
2314 golden.extend_from_slice(&u64::try_from(probe.len()).unwrap().to_be_bytes());
2315 golden.extend_from_slice(&probe);
2316 assert_eq!(ack, golden);
2317 assert_ne!(ack, canonical_heal_control_capability_ack(2, "ab", &probe).unwrap());
2318 assert_ne!(ack, canonical_heal_control_capability_ack(1, "ac", &probe).unwrap());
2319 assert_ne!(
2320 ack,
2321 canonical_heal_control_capability_ack(1, "ab", &heal_control_capability_probe(&[8; 16])).unwrap()
2322 );
2323 assert!(is_heal_control_capability_probe(&probe));
2324 assert!(!is_heal_control_capability_probe(HEAL_CONTROL_CAPABILITY_PROBE_PREFIX));
2325 let legacy_probe = [b"rustfs-heal-control-capability-v3\0".as_slice(), &[7; 16]].concat();
2326 assert!(!is_heal_control_capability_probe(&legacy_probe));
2327 }
2328
2329 #[test]
2330 fn remote_version_state_capability_probe_requires_exact_nonce() {
2331 let probe = remote_version_state_capability_probe(&[7; 16]);
2332 assert!(is_remote_version_state_capability_probe(&probe));
2333 assert!(!is_remote_version_state_capability_probe(REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX));
2334 }
2335
2336 #[test]
2337 fn ilm_recovery_export_capability_probe_requires_exact_prefix_and_nonce() {
2338 let probe = ilm_recovery_export_capability_probe(&[7; 16]);
2339 assert!(is_ilm_recovery_export_capability_probe(&probe));
2340 assert!(!is_ilm_recovery_export_capability_probe(ILM_RECOVERY_EXPORT_CAPABILITY_PROBE_PREFIX));
2341 let mut wrong_prefix = probe.clone();
2342 wrong_prefix[0] ^= 1;
2343 assert!(!is_ilm_recovery_export_capability_probe(&wrong_prefix));
2344 let mut extra = probe;
2345 extra.push(0);
2346 assert!(!is_ilm_recovery_export_capability_probe(&extra));
2347 }
2348
2349 #[test]
2350 fn transition_transaction_compaction_probe_requires_exact_prefix_and_nonce() {
2351 let probe = transition_transaction_compaction_capability_probe(&[7; 16]);
2352 assert!(is_transition_transaction_compaction_capability_probe(&probe));
2353 assert!(!is_transition_transaction_compaction_capability_probe(
2354 TRANSITION_TRANSACTION_COMPACTION_CAPABILITY_PROBE_PREFIX,
2355 ));
2356 let mut wrong_prefix = probe.clone();
2357 wrong_prefix[0] ^= 1;
2358 assert!(!is_transition_transaction_compaction_capability_probe(&wrong_prefix));
2359 let mut extra = probe;
2360 extra.push(0);
2361 assert!(!is_transition_transaction_compaction_capability_probe(&extra));
2362 }
2363
2364 #[test]
2365 fn remote_version_state_capability_binds_member_and_process_epoch() {
2366 let encoded =
2367 encode_remote_version_state_capability("node-a:9000", &[7; 16]).expect("small capability response should encode");
2368 assert_eq!(
2369 decode_remote_version_state_capability(&encoded).expect("capability response should decode"),
2370 ("node-a:9000", &[7; 16])
2371 );
2372 assert!(decode_remote_version_state_capability(&encoded[..encoded.len() - 1]).is_err());
2373
2374 let mut invalid_utf8 =
2375 encode_remote_version_state_capability("node-a", &[7; 16]).expect("small capability response should encode");
2376 invalid_utf8[8] = 0xff;
2377 assert!(decode_remote_version_state_capability(&invalid_utf8).is_err());
2378 }
2379
2380 #[test]
2381 fn cross_pool_fence_capability_binds_version_member_and_epoch() {
2382 assert_eq!(CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX, b"rustfs-cross-pool-fence-capability-v1\0");
2383 let mut probe = b"rustfs-cross-pool-fence-capability-v1\0".to_vec();
2384 probe.extend_from_slice(&[7; 16]);
2385 assert!(is_cross_pool_fence_capability_probe(&probe));
2386 assert!(!is_cross_pool_fence_capability_probe(CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX));
2387 let mut wrong_prefix = probe.clone();
2388 wrong_prefix[0] ^= 1;
2389 assert!(!is_cross_pool_fence_capability_probe(&wrong_prefix));
2390
2391 let encoded = encode_cross_pool_fence_capability(0x0102_0304, "node-a:9000", &[7; 16])
2392 .expect("small capability response should encode");
2393 let mut expected_response = vec![1, 2, 3, 4];
2394 expected_response.extend_from_slice(&11_u64.to_be_bytes());
2395 expected_response.extend_from_slice(b"node-a:9000");
2396 expected_response.extend_from_slice(&[7; 16]);
2397 assert_eq!(encoded, expected_response);
2398 }
2399
2400 #[test]
2401 fn canonical_response_binds_request_and_result() {
2402 let baseline = canonical_heal_control_response_body(2, "abcdef", b"query", b"result").unwrap();
2403 assert_ne!(baseline, canonical_heal_control_response_body(1, "abcdef", b"query", b"result").unwrap());
2404 assert_ne!(baseline, canonical_heal_control_response_body(2, "bbcdef", b"query", b"result").unwrap());
2405 assert_ne!(baseline, canonical_heal_control_response_body(2, "abcdef", b"cancel", b"result").unwrap());
2406 assert_ne!(
2407 baseline,
2408 canonical_heal_control_response_body(2, "abcdef", b"query", b"tampered").unwrap()
2409 );
2410 }
2411
2412 #[test]
2413 fn coordinator_epoch_is_stable_and_rejects_invalid_fingerprints() {
2414 assert_eq!(heal_control_coordinator_epoch("0123456789abcdefextra"), Ok(0x0123_4567_89ab_cdef));
2415 assert_eq!(
2416 heal_control_coordinator_epoch("0000000000000000"),
2417 Err("heal control topology epoch is zero")
2418 );
2419 assert_eq!(
2420 heal_control_coordinator_epoch("short"),
2421 Err("heal control topology fingerprint is too short")
2422 );
2423 assert_eq!(
2424 heal_control_coordinator_epoch("not-hex-value!!!!"),
2425 Err("heal control topology fingerprint is not hexadecimal")
2426 );
2427 }
2428
2429 #[test]
2430 fn execution_budget_precedes_transport_timeout() {
2431 let execution = heal_control_execution_timeout();
2432 assert!(!execution.is_zero());
2433 assert!(execution < internode_rpc_timeout());
2434 assert!(execution <= std::time::Duration::from_millis(heal_control::MAX_LIFETIME_MS as u64));
2435 }
2436
2437 #[test]
2438 fn execution_budget_is_nonzero_for_zero_transport_configuration() {
2439 let normalized_transport = Duration::from_secs(1);
2440 assert_eq!(normalize_internode_rpc_timeout(Duration::ZERO), normalized_transport);
2441 for configured_transport in [Duration::ZERO, normalized_transport] {
2442 let execution = heal_control_execution_timeout_for(configured_transport);
2443 assert!(execution > Duration::ZERO);
2444 assert!(execution < normalized_transport);
2445 }
2446 }
2447}
2448
2449#[cfg(test)]
2450mod tier_mutation_rpc_tests {
2451 use super::{
2452 TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION, TIER_MUTATION_RPC_PROTOCOL_VERSION, TierMutationRpcPhase,
2453 TierMutationRpcResponseProofInput, canonical_tier_mutation_rpc_body, canonical_tier_mutation_rpc_response_body,
2454 };
2455 use crate::proto_gen::node_service::{TierMutationFailureClass, TierMutationPeerState};
2456 use uuid::uuid;
2457
2458 #[test]
2459 fn canonical_tier_mutation_body_binds_phase_id_and_payload() {
2460 let mutation_id = uuid!("12345678-1234-5678-9abc-def012345678");
2461 let payload = b"canonical-intent-record";
2462 let baseline = canonical_tier_mutation_rpc_body(
2463 TIER_MUTATION_RPC_PROTOCOL_VERSION,
2464 TierMutationRpcPhase::Prepare,
2465 mutation_id,
2466 payload,
2467 )
2468 .expect("small mutation body should encode");
2469 let mut golden = b"rustfs-tier-mutation-rpc-v1\0".to_vec();
2470 golden.extend_from_slice(&TIER_MUTATION_RPC_PROTOCOL_VERSION.to_be_bytes());
2471 golden.extend_from_slice(&7_u64.to_be_bytes());
2472 golden.extend_from_slice(b"prepare");
2473 golden.extend_from_slice(mutation_id.as_bytes());
2474 golden.extend_from_slice(&u64::try_from(payload.len()).expect("payload length should fit").to_be_bytes());
2475 golden.extend_from_slice(payload);
2476 assert_eq!(baseline, golden);
2477
2478 assert_ne!(
2479 baseline,
2480 canonical_tier_mutation_rpc_body(
2481 TIER_MUTATION_RPC_PROTOCOL_VERSION + 1,
2482 TierMutationRpcPhase::Prepare,
2483 mutation_id,
2484 payload,
2485 )
2486 .expect("small mutation body should encode")
2487 );
2488 assert_ne!(
2489 baseline,
2490 canonical_tier_mutation_rpc_body(
2491 TIER_MUTATION_RPC_PROTOCOL_VERSION,
2492 TierMutationRpcPhase::Commit,
2493 mutation_id,
2494 payload,
2495 )
2496 .expect("small mutation body should encode")
2497 );
2498 assert_ne!(
2499 baseline,
2500 canonical_tier_mutation_rpc_body(
2501 TIER_MUTATION_RPC_PROTOCOL_VERSION,
2502 TierMutationRpcPhase::Prepare,
2503 uuid!("22345678-1234-5678-9abc-def012345678"),
2504 payload,
2505 )
2506 .expect("small mutation body should encode")
2507 );
2508 assert_ne!(
2509 baseline,
2510 canonical_tier_mutation_rpc_body(
2511 TIER_MUTATION_RPC_PROTOCOL_VERSION,
2512 TierMutationRpcPhase::Prepare,
2513 mutation_id,
2514 b"canonical-intent-record-tampered",
2515 )
2516 .expect("small mutation body should encode")
2517 );
2518 }
2519
2520 #[test]
2521 fn tier_mutation_v3_request_and_response_golden_bytes_are_unchanged() {
2522 let mutation_id = uuid!("12345678-1234-5678-9abc-def012345678");
2523 let payload = b"canonical-intent-record";
2524 let request = canonical_tier_mutation_rpc_body(
2525 TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
2526 TierMutationRpcPhase::Prepare,
2527 mutation_id,
2528 payload,
2529 )
2530 .expect("v3 request should encode");
2531 let mut request_golden = b"rustfs-tier-mutation-rpc-v1\0".to_vec();
2532 request_golden.extend_from_slice(&TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION.to_be_bytes());
2533 request_golden.extend_from_slice(&7_u64.to_be_bytes());
2534 request_golden.extend_from_slice(b"prepare");
2535 request_golden.extend_from_slice(mutation_id.as_bytes());
2536 request_golden.extend_from_slice(&u64::try_from(payload.len()).expect("payload length should fit").to_be_bytes());
2537 request_golden.extend_from_slice(payload);
2538 assert_eq!(request, request_golden);
2539
2540 let response = canonical_tier_mutation_rpc_response_body(TierMutationRpcResponseProofInput {
2541 version: TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
2542 phase: TierMutationRpcPhase::Prepare,
2543 mutation_id,
2544 canonical_payload: payload,
2545 success: true,
2546 state: TierMutationPeerState::Prepared as i32,
2547 applied: true,
2548 error_info: None,
2549 failure_class: TierMutationFailureClass::PreDispatchRejected as i32,
2551 })
2552 .expect("v3 response should encode");
2553 let mut response_golden = b"rustfs-tier-mutation-rpc-response-v1\0".to_vec();
2554 response_golden.extend_from_slice(&TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION.to_be_bytes());
2555 response_golden.extend_from_slice(&7_u64.to_be_bytes());
2556 response_golden.extend_from_slice(b"prepare");
2557 response_golden.extend_from_slice(mutation_id.as_bytes());
2558 response_golden.extend_from_slice(&u64::try_from(payload.len()).expect("payload length should fit").to_be_bytes());
2559 response_golden.extend_from_slice(payload);
2560 response_golden.push(1);
2561 response_golden.extend_from_slice(&(TierMutationPeerState::Prepared as i32).to_be_bytes());
2562 response_golden.push(1);
2563 response_golden.push(0);
2564 response_golden.extend_from_slice(&0_u64.to_be_bytes());
2565 assert_eq!(response, response_golden);
2566 }
2567
2568 #[test]
2569 fn canonical_tier_mutation_v4_response_binds_request_result_and_failure_class() {
2570 let mutation_id = uuid!("12345678-1234-5678-9abc-def012345678");
2571 let payload = b"canonical-intent-record";
2572 let baseline = canonical_tier_mutation_rpc_response_body(TierMutationRpcResponseProofInput {
2573 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2574 phase: TierMutationRpcPhase::Prepare,
2575 mutation_id,
2576 canonical_payload: payload,
2577 success: false,
2578 state: TierMutationPeerState::Unspecified as i32,
2579 applied: false,
2580 error_info: Some("failure"),
2581 failure_class: TierMutationFailureClass::Ambiguous as i32,
2582 })
2583 .expect("small v4 mutation response should encode");
2584
2585 let cases = [
2586 TierMutationRpcResponseProofInput {
2587 version: TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
2588 phase: TierMutationRpcPhase::Prepare,
2589 mutation_id,
2590 canonical_payload: payload,
2591 success: false,
2592 state: TierMutationPeerState::Unspecified as i32,
2593 applied: false,
2594 error_info: Some("failure"),
2595 failure_class: TierMutationFailureClass::Ambiguous as i32,
2596 },
2597 TierMutationRpcResponseProofInput {
2598 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2599 phase: TierMutationRpcPhase::Commit,
2600 mutation_id,
2601 canonical_payload: payload,
2602 success: false,
2603 state: TierMutationPeerState::Unspecified as i32,
2604 applied: false,
2605 error_info: Some("failure"),
2606 failure_class: TierMutationFailureClass::Ambiguous as i32,
2607 },
2608 TierMutationRpcResponseProofInput {
2609 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2610 phase: TierMutationRpcPhase::Prepare,
2611 mutation_id: uuid!("22345678-1234-5678-9abc-def012345678"),
2612 canonical_payload: payload,
2613 success: false,
2614 state: TierMutationPeerState::Unspecified as i32,
2615 applied: false,
2616 error_info: Some("failure"),
2617 failure_class: TierMutationFailureClass::Ambiguous as i32,
2618 },
2619 TierMutationRpcResponseProofInput {
2620 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2621 phase: TierMutationRpcPhase::Prepare,
2622 mutation_id,
2623 canonical_payload: b"tampered-intent-record",
2624 success: false,
2625 state: TierMutationPeerState::Unspecified as i32,
2626 applied: false,
2627 error_info: Some("failure"),
2628 failure_class: TierMutationFailureClass::Ambiguous as i32,
2629 },
2630 TierMutationRpcResponseProofInput {
2631 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2632 phase: TierMutationRpcPhase::Prepare,
2633 mutation_id,
2634 canonical_payload: payload,
2635 success: true,
2636 state: TierMutationPeerState::Unspecified as i32,
2637 applied: false,
2638 error_info: Some("failure"),
2639 failure_class: TierMutationFailureClass::Ambiguous as i32,
2640 },
2641 TierMutationRpcResponseProofInput {
2642 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2643 phase: TierMutationRpcPhase::Prepare,
2644 mutation_id,
2645 canonical_payload: payload,
2646 success: false,
2647 state: TierMutationPeerState::Committed as i32,
2648 applied: false,
2649 error_info: Some("failure"),
2650 failure_class: TierMutationFailureClass::Ambiguous as i32,
2651 },
2652 TierMutationRpcResponseProofInput {
2653 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2654 phase: TierMutationRpcPhase::Prepare,
2655 mutation_id,
2656 canonical_payload: payload,
2657 success: false,
2658 state: TierMutationPeerState::Unspecified as i32,
2659 applied: true,
2660 error_info: Some("failure"),
2661 failure_class: TierMutationFailureClass::Ambiguous as i32,
2662 },
2663 TierMutationRpcResponseProofInput {
2664 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2665 phase: TierMutationRpcPhase::Prepare,
2666 mutation_id,
2667 canonical_payload: payload,
2668 success: false,
2669 state: TierMutationPeerState::Unspecified as i32,
2670 applied: false,
2671 error_info: Some("other failure"),
2672 failure_class: TierMutationFailureClass::Ambiguous as i32,
2673 },
2674 TierMutationRpcResponseProofInput {
2675 version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
2676 phase: TierMutationRpcPhase::Prepare,
2677 mutation_id,
2678 canonical_payload: payload,
2679 success: false,
2680 state: TierMutationPeerState::Unspecified as i32,
2681 applied: false,
2682 error_info: Some("failure"),
2683 failure_class: TierMutationFailureClass::PreDispatchRejected as i32,
2684 },
2685 ];
2686 for case in cases {
2687 assert_ne!(
2688 baseline,
2689 canonical_tier_mutation_rpc_response_body(case).expect("small mutation response should encode")
2690 );
2691 }
2692 }
2693}
2694
2695pub fn internode_rpc_msgpack_only() -> bool {
2703 match INTERNODE_RPC_MSGPACK_ONLY_CACHE.load(Ordering::Acquire) {
2704 INTERNODE_RPC_MSGPACK_ONLY_CACHE_FALSE => return false,
2705 INTERNODE_RPC_MSGPACK_ONLY_CACHE_TRUE => return true,
2706 _ => {}
2707 }
2708
2709 let enabled = rustfs_utils::get_env_bool(
2710 rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY,
2711 rustfs_config::DEFAULT_INTERNODE_RPC_MSGPACK_ONLY,
2712 ) && rustfs_utils::get_env_bool(
2713 rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED,
2714 rustfs_config::DEFAULT_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED,
2715 );
2716 INTERNODE_RPC_MSGPACK_ONLY_CACHE.store(
2717 if enabled {
2718 INTERNODE_RPC_MSGPACK_ONLY_CACHE_TRUE
2719 } else {
2720 INTERNODE_RPC_MSGPACK_ONLY_CACHE_FALSE
2721 },
2722 Ordering::Release,
2723 );
2724 enabled
2725}
2726
2727#[doc(hidden)]
2728pub fn reset_internode_rpc_msgpack_only_cache() {
2729 INTERNODE_RPC_MSGPACK_ONLY_CACHE.store(INTERNODE_RPC_MSGPACK_ONLY_CACHE_UNSET, Ordering::Release);
2730}
2731
2732fn internode_offline_failure_threshold() -> u32 {
2735 rustfs_utils::get_env_u32(
2736 rustfs_config::ENV_INTERNODE_OFFLINE_FAILURE_THRESHOLD,
2737 rustfs_config::DEFAULT_INTERNODE_OFFLINE_FAILURE_THRESHOLD,
2738 )
2739}
2740
2741#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2750pub enum ChannelClass {
2751 Control,
2752 Bulk,
2753}
2754
2755const INTERNODE_RPC_CONCURRENCY_LIMIT: usize = 64;
2757
2758fn channel_isolation_enabled() -> bool {
2760 rustfs_utils::get_env_bool(
2761 rustfs_config::ENV_INTERNODE_CHANNEL_ISOLATION,
2762 rustfs_config::DEFAULT_INTERNODE_CHANNEL_ISOLATION,
2763 )
2764}
2765
2766fn bulk_channel_pool_size() -> usize {
2768 rustfs_utils::get_env_usize(rustfs_config::ENV_INTERNODE_BULK_CHANNELS, rustfs_config::DEFAULT_INTERNODE_BULK_CHANNELS).max(1)
2769}
2770
2771static BULK_CHANNEL_CURSOR: AtomicUsize = AtomicUsize::new(0);
2775
2776fn bulk_cache_key(addr: &str, idx: usize) -> String {
2779 format!("{addr}\u{0}bulk\u{0}{idx}")
2780}
2781
2782pub async fn get_channel_for_class(addr: &str, class: ChannelClass) -> Result<Channel, Box<dyn Error>> {
2790 if class == ChannelClass::Control || !channel_isolation_enabled() {
2791 if let Some(channel) = cached_connection(addr).await {
2792 debug!("Using cached control gRPC channel for: {}", addr);
2793 return Ok(channel);
2794 }
2795 return create_new_channel(addr).await;
2796 }
2797
2798 let pool_size = bulk_channel_pool_size();
2799 let idx = BULK_CHANNEL_CURSOR.fetch_add(1, Ordering::Relaxed) % pool_size;
2800 let cache_key = bulk_cache_key(addr, idx);
2801 if let Some(channel) = cached_connection(&cache_key).await {
2802 debug!("Using cached bulk gRPC channel {} for: {}", idx, addr);
2803 return Ok(channel);
2804 }
2805 build_channel(addr, &cache_key).await
2806}
2807
2808pub async fn create_new_channel(addr: &str) -> Result<Channel, Box<dyn Error>> {
2818 build_channel(addr, addr).await
2820}
2821
2822async fn build_channel(dial_addr: &str, cache_key: &str) -> Result<Channel, Box<dyn Error>> {
2829 debug!("Creating new gRPC channel to: {} (cache key: {})", dial_addr, cache_key);
2830 let dial_started_at = Instant::now();
2831 let connect_timeout = internode_connect_timeout();
2832 let tcp_keepalive = internode_tcp_keepalive();
2833 let http2_keepalive_interval = internode_http2_keep_alive_interval();
2834 let http2_keepalive_timeout = internode_http2_keep_alive_timeout();
2835 let rpc_timeout = internode_rpc_timeout();
2836
2837 let mut connector = Endpoint::from_shared(dial_addr.to_string())?
2838 .connect_timeout(connect_timeout)
2840 .concurrency_limit(INTERNODE_RPC_CONCURRENCY_LIMIT)
2841 .tcp_keepalive(Some(tcp_keepalive))
2843 .tcp_nodelay(internode_rpc_tcp_nodelay())
2845 .http2_keep_alive_interval(http2_keepalive_interval)
2847 .keep_alive_timeout(http2_keepalive_timeout)
2849 .keep_alive_while_idle(true)
2851 .timeout(rpc_timeout);
2853
2854 if let Some(stream_window) = internode_rpc_http2_stream_window() {
2858 connector = connector.initial_stream_window_size(stream_window);
2859 }
2860 if let Some(conn_window) = internode_rpc_http2_conn_window() {
2861 connector = connector.initial_connection_window_size(conn_window);
2862 }
2863
2864 let outbound_tls = runtime_sources::outbound_tls_state().await;
2865 let generation = outbound_tls.generation.0;
2866 let mut stale_generation = false;
2867 {
2868 let generation_cache = TLS_GENERATION_CACHE.lock().await;
2869 if let Some(cached_generation) = generation_cache.get(cache_key)
2870 && *cached_generation != generation
2871 {
2872 stale_generation = true;
2873 }
2874 }
2875 if dial_addr.starts_with(RUSTFS_HTTPS_PREFIX) {
2876 if let Some(cert_pem) = outbound_tls.root_ca_pem.as_ref() {
2877 let ca = Certificate::from_pem(cert_pem);
2878 let domain = dial_addr
2880 .trim_start_matches(RUSTFS_HTTPS_PREFIX)
2881 .split('/')
2882 .next()
2883 .unwrap_or("")
2884 .split(':')
2885 .next()
2886 .unwrap_or("");
2887 let tls = if !domain.is_empty() {
2888 let mut cfg = ClientTlsConfig::new().ca_certificate(ca).domain_name(domain);
2889 if let Some(id) = outbound_tls.mtls_identity.as_ref() {
2890 let identity = tonic::transport::Identity::from_pem(id.cert_pem.clone(), id.key_pem.clone());
2891 cfg = cfg.identity(identity);
2892 }
2893 cfg
2894 } else {
2895 ClientTlsConfig::new().ca_certificate(ca)
2897 };
2898 connector = connector.tls_config(tls)?;
2899 debug!("Configured TLS with custom root certificate for: {}", dial_addr);
2900 } else {
2901 debug!("No custom root certificate configured; using system roots for TLS: {}", dial_addr);
2904 connector = connector.tls_config(ClientTlsConfig::new())?;
2905 }
2906 }
2907
2908 let channel = match connector.connect().await {
2909 Ok(channel) => {
2910 runtime_sources::record_grpc_dial_result(dial_started_at.elapsed(), true);
2911 runtime_sources::record_peer_reachable(dial_addr);
2914 channel
2915 }
2916 Err(err) => {
2917 runtime_sources::record_grpc_dial_result(dial_started_at.elapsed(), false);
2918 runtime_sources::record_peer_unreachable(dial_addr, internode_offline_failure_threshold());
2919 return Err(err.into());
2920 }
2921 };
2922
2923 cache_connection(cache_key.to_string(), channel.clone()).await;
2924 {
2925 let mut generation_cache = TLS_GENERATION_CACHE.lock().await;
2926 enforce_tls_generation_cache_bound(&mut generation_cache, generation, cache_key);
2927 generation_cache.insert(cache_key.to_string(), generation);
2928 }
2929 if stale_generation {
2930 runtime_sources::record_stale_grpc_channel_tls_generation();
2931 }
2932
2933 debug!(
2934 "Successfully created and cached gRPC channel to: {} (cache key: {})",
2935 dial_addr, cache_key
2936 );
2937 Ok(channel)
2938}
2939
2940pub async fn evict_failed_connection(addr: &str) {
2943 evict_failed_connection_with_log_level(addr, ConnectionEvictionLogLevel::Warn).await;
2944}
2945
2946pub async fn evict_failed_connection_with_log_level(addr: &str, log_level: ConnectionEvictionLogLevel) {
2948 match log_level {
2949 ConnectionEvictionLogLevel::Warn => {
2950 warn!(
2951 addr = %addr,
2952 "Evicting cached gRPC connection after RPC failure; the next request will attempt to reconnect automatically"
2953 );
2954 }
2955 ConnectionEvictionLogLevel::Info => {
2956 info!(
2957 addr = %addr,
2958 "Evicting cached gRPC connection after RPC failure; the next request will attempt to reconnect automatically"
2959 );
2960 }
2961 ConnectionEvictionLogLevel::Debug => {
2962 debug!(
2963 addr = %addr,
2964 "Evicting cached gRPC connection after RPC failure; the next request will attempt to reconnect automatically"
2965 );
2966 }
2967 }
2968
2969 let cache_log_level = match log_level {
2970 ConnectionEvictionLogLevel::Warn => ConnectionEvictionLogLevel::Info,
2971 level => level,
2972 };
2973 evict_connection_with_log_level(addr, cache_log_level).await;
2974 TLS_GENERATION_CACHE.lock().await.remove(addr);
2975
2976 runtime_sources::record_peer_unreachable(addr, internode_offline_failure_threshold());
2979
2980 if channel_isolation_enabled() {
2984 for idx in 0..bulk_channel_pool_size() {
2985 let cache_key = bulk_cache_key(addr, idx);
2986 evict_connection_with_log_level(&cache_key, cache_log_level).await;
2987 TLS_GENERATION_CACHE.lock().await.remove(&cache_key);
2988 }
2989 }
2990}
2991
2992#[cfg(test)]
2993mod tests {
2994 use super::*;
2995 use prost::Message as _;
2996 use std::sync::Mutex;
2997
2998 static INTERNODE_RPC_MSGPACK_ONLY_ENV_LOCK: Mutex<()> = Mutex::new(());
2999
3000 #[derive(Clone, PartialEq, prost::Message)]
3001 struct BackgroundHealStatusRequestV1 {}
3002
3003 #[test]
3004 fn background_heal_status_request_remains_rolling_upgrade_compatible() {
3005 let current = proto_gen::node_service::BackgroundHealStatusRequest {
3006 protocol_version: BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION,
3007 };
3008 let encoded = current.encode_to_vec();
3009 BackgroundHealStatusRequestV1::decode(encoded.as_slice()).expect("v1 server should ignore the version field");
3010
3011 let encoded = BackgroundHealStatusRequestV1 {}.encode_to_vec();
3012 let decoded = proto_gen::node_service::BackgroundHealStatusRequest::decode(encoded.as_slice())
3013 .expect("v2 server should accept a v1 request");
3014 assert_eq!(decoded.protocol_version, 0);
3015 }
3016
3017 use crate::compat_manifest::{CompatPayloadField, REQUEST_COMPAT_SEND_SITES, RESPONSE_COMPAT_SEND_SITES, RequestJsonPolicy};
3018
3019 fn proto_bin_json_fields(message_suffix: &str) -> Vec<CompatPayloadField> {
3020 let proto = include_str!("node.proto");
3021 let mut fields = Vec::new();
3022 let mut current_message = None;
3023
3024 for line in proto.lines() {
3025 let line = line.trim();
3026 if let Some(rest) = line.strip_prefix("message ") {
3027 current_message = rest.split_whitespace().next();
3028 continue;
3029 }
3030 if line == "}" {
3031 current_message = None;
3032 continue;
3033 }
3034 let Some(message) = current_message else {
3035 continue;
3036 };
3037 if line.starts_with("//") || !message.ends_with(message_suffix) || !line.contains("_bin") {
3038 continue;
3039 }
3040 let Some(bin_field) = line.split_whitespace().find(|part| part.ends_with("_bin")) else {
3041 continue;
3042 };
3043 let Some(json_field) = bin_field.strip_suffix("_bin") else {
3044 continue;
3045 };
3046 fields.push(CompatPayloadField {
3047 message,
3048 json_field,
3049 bin_field,
3050 });
3051 }
3052
3053 fields.sort();
3054 fields
3055 }
3056
3057 #[test]
3058 fn request_compat_send_site_manifest_covers_node_proto_bin_fields() {
3059 let mut manifest_fields = REQUEST_COMPAT_SEND_SITES
3060 .iter()
3061 .map(|send_site| send_site.field)
3062 .collect::<Vec<_>>();
3063 manifest_fields.sort();
3064 manifest_fields.dedup();
3065
3066 assert_eq!(
3067 manifest_fields.len(),
3068 REQUEST_COMPAT_SEND_SITES.len(),
3069 "duplicate request send-site manifest entry"
3070 );
3071 assert_eq!(manifest_fields, proto_bin_json_fields("Request"));
3072 }
3073
3074 #[test]
3075 fn request_compat_send_site_manifest_pins_exact_json_policies() {
3076 let mut policies = REQUEST_COMPAT_SEND_SITES
3077 .iter()
3078 .map(|send_site| (send_site.field.message, send_site.field.json_field, send_site.policy))
3079 .collect::<Vec<_>>();
3080 policies.sort_by_key(|(message, json_field, _)| (*message, *json_field));
3081
3082 assert_eq!(
3083 policies,
3084 [
3085 (
3086 "BatchReadVersionRequest",
3087 "batch_read_version_req",
3088 RequestJsonPolicy::MsgpackOnlyEligible
3089 ),
3090 ("DeleteVersionRequest", "file_info", RequestJsonPolicy::AlwaysDualWriteUntilFallbackZero,),
3091 ("DeleteVersionRequest", "opts", RequestJsonPolicy::AlwaysDualWriteUntilFallbackZero),
3092 ("DeleteVersionsRequest", "opts", RequestJsonPolicy::AlwaysDualWriteUntilFallbackZero,),
3093 ("DeleteVersionsRequest", "versions", RequestJsonPolicy::AlwaysDualWriteUntilFallbackZero,),
3094 ("ReadMultipleRequest", "read_multiple_req", RequestJsonPolicy::MsgpackOnlyEligible),
3095 ("ReadVersionRequest", "opts", RequestJsonPolicy::MsgpackOnlyEligible),
3096 ("RenameDataRequest", "file_info", RequestJsonPolicy::MsgpackOnlyEligible),
3097 ("UpdateMetadataRequest", "file_info", RequestJsonPolicy::MsgpackOnlyEligible),
3098 ("UpdateMetadataRequest", "opts", RequestJsonPolicy::MsgpackOnlyEligible),
3099 ("WriteMetadataRequest", "file_info", RequestJsonPolicy::MsgpackOnlyEligible),
3100 ]
3101 );
3102 }
3103
3104 #[test]
3105 fn response_compat_send_site_manifest_covers_node_proto_bin_fields() {
3106 let mut manifest_fields = RESPONSE_COMPAT_SEND_SITES
3107 .iter()
3108 .map(|send_site| send_site.field)
3109 .collect::<Vec<_>>();
3110 manifest_fields.sort();
3111 manifest_fields.dedup();
3112
3113 assert_eq!(
3114 manifest_fields.len(),
3115 RESPONSE_COMPAT_SEND_SITES.len(),
3116 "duplicate response send-site manifest entry"
3117 );
3118 assert_eq!(manifest_fields, proto_bin_json_fields("Response"));
3119 }
3120
3121 #[test]
3122 fn enforce_tls_generation_cache_bound_evicts_when_retained_entries_still_full() {
3123 let mut cache = HashMap::new();
3124 for i in 0..TLS_GENERATION_CACHE_MAX_SIZE {
3125 cache.insert(format!("node-{i}"), 42);
3126 }
3127
3128 enforce_tls_generation_cache_bound(&mut cache, 42, "new-node");
3129 assert_eq!(cache.len(), TLS_GENERATION_CACHE_MAX_SIZE - 1);
3130 }
3131
3132 #[tokio::test]
3133 async fn evict_failed_connection_with_log_level_removes_cached_connection() {
3134 let addr = "http://evict-failed-connection-debug-test";
3135 let channel = Endpoint::from_static("http://127.0.0.1:1").connect_lazy();
3136 cache_connection(addr.to_string(), channel).await;
3137
3138 evict_failed_connection_with_log_level(addr, ConnectionEvictionLogLevel::Debug).await;
3139
3140 assert!(!rustfs_common::has_cached_connection(addr).await);
3141 }
3142
3143 #[test]
3144 fn bulk_cache_key_is_distinct_and_cannot_collide_with_control_key() {
3145 let addr = "https://node-a:9000";
3146 let k0 = bulk_cache_key(addr, 0);
3147 let k1 = bulk_cache_key(addr, 1);
3148 assert_ne!(k0, k1);
3149 assert_ne!(k0, addr);
3150 assert!(k0.starts_with(addr));
3151 assert!(k0.contains('\u{0}'));
3153 }
3154
3155 #[test]
3156 fn bulk_channel_pool_size_is_at_least_one() {
3157 assert!(bulk_channel_pool_size() >= 1);
3159 }
3160
3161 #[test]
3162 fn internode_rpc_msgpack_only_reuses_cached_env_until_reset() {
3163 let _guard = INTERNODE_RPC_MSGPACK_ONLY_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3164
3165 reset_internode_rpc_msgpack_only_cache();
3166
3167 temp_env::with_vars(
3168 [
3169 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
3170 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
3171 ],
3172 || {
3173 assert!(internode_rpc_msgpack_only());
3174 },
3175 );
3176
3177 temp_env::with_vars(
3178 [
3179 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, None::<&str>),
3180 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, None::<&str>),
3181 ],
3182 || {
3183 assert!(internode_rpc_msgpack_only(), "cached value should avoid repeated env reads");
3184 reset_internode_rpc_msgpack_only_cache();
3185 assert!(!internode_rpc_msgpack_only(), "reset must reload current env values");
3186 },
3187 );
3188
3189 reset_internode_rpc_msgpack_only_cache();
3190 }
3191
3192 #[test]
3193 fn internode_rpc_msgpack_only_requires_request_and_fleet_confirmation() {
3194 let _guard = INTERNODE_RPC_MSGPACK_ONLY_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3195
3196 for (requested, fleet_confirmed, expected) in [
3197 (None, None, false),
3198 (Some("true"), None, false),
3199 (None, Some("true"), false),
3200 (Some("true"), Some("false"), false),
3201 (Some("false"), Some("true"), false),
3202 (Some("true"), Some("true"), true),
3203 ] {
3204 reset_internode_rpc_msgpack_only_cache();
3205 temp_env::with_vars(
3206 [
3207 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, requested),
3208 (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, fleet_confirmed),
3209 ],
3210 || {
3211 assert_eq!(internode_rpc_msgpack_only(), expected);
3212 },
3213 );
3214 }
3215
3216 reset_internode_rpc_msgpack_only_cache();
3217 }
3218
3219 #[tokio::test]
3220 async fn get_channel_for_class_bulk_reuses_control_cache_when_isolation_disabled() {
3221 let addr = "http://get-channel-isolation-off-test";
3224 let channel = Endpoint::from_static("http://127.0.0.1:1").connect_lazy();
3225 cache_connection(addr.to_string(), channel).await;
3226
3227 let acquired = get_channel_for_class(addr, ChannelClass::Bulk).await;
3228 assert!(acquired.is_ok());
3229 assert!(!rustfs_common::has_cached_connection(&bulk_cache_key(addr, 0)).await);
3230
3231 evict_failed_connection_with_log_level(addr, ConnectionEvictionLogLevel::Debug).await;
3232 }
3233}