// file: crates/ksp-onchain-transport-lib/src/grpc_settings.rs // version: 2 const DEFAULT_GRPC_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); const DEFAULT_GRPC_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024; const DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024; const DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(250); const DEFAULT_GRPC_RECONNECT_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5); const DEFAULT_GRPC_RECONNECT_MAX_RETRIES: u32 = 5; const DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY: usize = 128; const DEFAULT_GRPC_UNARY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY: usize = 256; const MAX_GRPC_CHANNEL_CAPACITY: usize = 65_536; const MAX_GRPC_DESCRIPTOR_LENGTH_BYTES: usize = 128; const MAX_GRPC_ENDPOINT_COUNT: usize = 128; const MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES: usize = 8 * 1024; const MAX_GRPC_MESSAGE_SIZE_BYTES: usize = 512 * 1024 * 1024; const MAX_GRPC_METADATA_ENTRY_COUNT: usize = 64; const MAX_GRPC_METADATA_KEY_LENGTH_BYTES: usize = 128; const MAX_GRPC_METADATA_VALUE_LENGTH_BYTES: usize = 8 * 1024; const MAX_GRPC_RECONNECT_RETRIES: u32 = 100; const MAX_GRPC_RUNTIME_DURATION: std::time::Duration = std::time::Duration::from_secs(300); /// Runtime Yellowstone gRPC endpoint URL owned by Transport. /// /// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted. #[derive(Clone, Eq, PartialEq)] pub struct YellowstoneGrpcEndpointUrl { value: std::string::String, uses_tls: bool, } impl YellowstoneGrpcEndpointUrl { /// Parses and validates one Yellowstone gRPC endpoint URL. pub fn parse(value: impl std::convert::Into) -> ksp_core_lib::Result { ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating Yellowstone gRPC endpoint URL"); let value = value.into(); if value.len() > MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL exceeds the KSP length bound") .with_context("field", "grpc_endpoints.url"), ); } let parsed = match reqwest::Url::parse(value.as_str()) { std::result::Result::Ok(parsed) => parsed, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected invalid Yellowstone gRPC endpoint URL"); return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL is invalid") .with_context("field", "grpc_endpoints.url") .with_source(error), ); }, }; if parsed.scheme() != "http" && parsed.scheme() != "https" { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, field = "grpc_endpoints.url", scheme = parsed.scheme(), "rejected Yellowstone gRPC endpoint URL with unsupported scheme" ); return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must use http or https") .with_context("field", "grpc_endpoints.url") .with_context("scheme", parsed.scheme()), ); } if parsed.host_str().is_none() { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected Yellowstone gRPC endpoint URL without host"); return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must contain a host") .with_context("field", "grpc_endpoints.url"), ); } ksp_logging_lib::trace!(target: crate::TRACING_TARGET, scheme = parsed.scheme(), "validated Yellowstone gRPC endpoint URL syntax"); return std::result::Result::Ok(Self { value, uses_tls: parsed.scheme() == "https" }); } /// Returns the sensitive runtime URL text. /// /// Callers must not write this value to logs, generic diagnostics or snapshots. #[must_use] pub fn as_str(&self) -> &str { return self.value.as_str(); } /// Returns whether this endpoint URL requires TLS. #[must_use] pub const fn uses_tls(&self) -> bool { return self.uses_tls; } } impl std::fmt::Debug for YellowstoneGrpcEndpointUrl { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str("YellowstoneGrpcEndpointUrl()"); } } /// One validated ASCII metadata entry attached to Yellowstone gRPC requests. /// /// Metadata values are intentionally omitted from [`std::fmt::Debug`] for both public and secret entries. Secret entries are additionally marked sensitive on /// the Tonic metadata value before transmission so the HTTP/2 stack avoids indexing them where supported. Binary `*-bin` metadata is not part of the /// `0.2.9-pre.003` contract. #[derive(Clone, Eq, PartialEq)] pub struct YellowstoneGrpcMetadataEntry { key: std::string::String, value: std::string::String, secret: bool, } impl YellowstoneGrpcMetadataEntry { /// Creates one non-secret ASCII metadata entry. pub fn public(key: impl std::convert::Into, value: impl std::convert::Into) -> ksp_core_lib::Result { return Self::new(key.into(), value.into(), false); } /// Creates one secret ASCII metadata entry with redacted diagnostics. pub fn secret(key: impl std::convert::Into, value: impl std::convert::Into) -> ksp_core_lib::Result { return Self::new(key.into(), value.into(), true); } /// Returns the validated metadata key. #[must_use] pub fn key(&self) -> &str { return self.key.as_str(); } /// Returns whether the value must be treated as secret by Transport. #[must_use] pub const fn is_secret(&self) -> bool { return self.secret; } /// Appends this validated value to an internal Tonic metadata map while preserving its sensitivity marker. pub(crate) fn append_to(&self, metadata: &mut tonic::metadata::MetadataMap) -> ksp_core_lib::Result<()> { let key = match tonic::metadata::MetadataKey::::from_bytes(self.key.as_bytes()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid") .with_context("field", "grpc_endpoint.metadata.key"), ); }, }; let mut value = match tonic::metadata::AsciiMetadataValue::try_from(self.value.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value is invalid") .with_context("field", "grpc_endpoint.metadata.value") .with_context("metadata_key", self.key.as_str()), ); }, }; value.set_sensitive(self.secret); metadata.append(key, value); return std::result::Result::Ok(()); } fn new(key: std::string::String, value: std::string::String, secret: bool) -> ksp_core_lib::Result { if key.is_empty() || key.len() > MAX_GRPC_METADATA_KEY_LENGTH_BYTES || key != key.to_ascii_lowercase() || key.starts_with("grpc-") || key.ends_with("-bin") { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key violates the KSP ASCII metadata contract") .with_context("field", "grpc_endpoint.metadata.key"), ); } if tonic::metadata::MetadataKey::::from_bytes(key.as_bytes()).is_err() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid") .with_context("field", "grpc_endpoint.metadata.key"), ); } if value.len() > MAX_GRPC_METADATA_VALUE_LENGTH_BYTES || tonic::metadata::AsciiMetadataValue::try_from(value.as_str()).is_err() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value violates the KSP ASCII metadata contract") .with_context("field", "grpc_endpoint.metadata.value") .with_context("metadata_key", key.as_str()), ); } return std::result::Result::Ok(Self { key, value, secret }); } } impl std::fmt::Debug for YellowstoneGrpcMetadataEntry { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("YellowstoneGrpcMetadataEntry") .field("key", &self.key) .field("secret", &self.secret) .field("value", &"") .finish(); } } /// Open provider descriptor used by Yellowstone gRPC endpoint settings. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct YellowstoneGrpcProviderName { value: std::string::String, } impl YellowstoneGrpcProviderName { /// Creates an open provider descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`]. #[must_use] pub fn new(value: impl std::convert::Into) -> Self { return Self { value: value.into() }; } /// Returns the provider descriptor text. #[must_use] pub fn as_str(&self) -> &str { return self.value.as_str(); } } /// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct YellowstoneGrpcClusterName { value: std::string::String, } impl YellowstoneGrpcClusterName { /// Creates an open cluster descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`]. #[must_use] pub fn new(value: impl std::convert::Into) -> Self { return Self { value: value.into() }; } /// Returns the cluster descriptor text. #[must_use] pub fn as_str(&self) -> &str { return self.value.as_str(); } } /// Bounded reconnect settings owned by the Yellowstone gRPC runtime. #[derive(Clone, Debug, Eq, PartialEq)] pub struct YellowstoneGrpcReconnectSettings { max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration, } impl YellowstoneGrpcReconnectSettings { /// Creates bounded reconnect settings. #[must_use] pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self { return Self { max_retries, initial_backoff, max_backoff }; } /// Returns the number of reconnect attempts allowed after one live channel or stream is lost. #[must_use] pub const fn max_retries(&self) -> u32 { return self.max_retries; } /// Returns the initial reconnect backoff. #[must_use] pub const fn initial_backoff(&self) -> std::time::Duration { return self.initial_backoff; } /// Returns the maximum reconnect backoff. #[must_use] pub const fn max_backoff(&self) -> std::time::Duration { return self.max_backoff; } } impl std::default::Default for YellowstoneGrpcReconnectSettings { fn default() -> Self { return Self::new(DEFAULT_GRPC_RECONNECT_MAX_RETRIES, DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF, DEFAULT_GRPC_RECONNECT_MAX_BACKOFF); } } /// Runtime limits and lifecycle settings for one Yellowstone gRPC physical channel/session path. #[derive(Clone, Debug, Eq, PartialEq)] pub struct YellowstoneGrpcSessionSettings { connect_timeout: std::time::Duration, unary_timeout: std::time::Duration, close_timeout: std::time::Duration, reconnect: crate::YellowstoneGrpcReconnectSettings, request_channel_capacity: usize, update_channel_capacity: usize, max_inbound_message_size_bytes: usize, max_outbound_message_size_bytes: usize, } impl YellowstoneGrpcSessionSettings { /// Creates complete runtime settings for one physical Yellowstone gRPC channel/session path. #[must_use] #[allow(clippy::too_many_arguments)] pub const fn new( connect_timeout: std::time::Duration, unary_timeout: std::time::Duration, close_timeout: std::time::Duration, reconnect: crate::YellowstoneGrpcReconnectSettings, request_channel_capacity: usize, update_channel_capacity: usize, max_inbound_message_size_bytes: usize, max_outbound_message_size_bytes: usize, ) -> Self { return Self { connect_timeout, unary_timeout, close_timeout, reconnect, request_channel_capacity, update_channel_capacity, max_inbound_message_size_bytes, max_outbound_message_size_bytes, }; } /// Returns the connection establishment timeout. #[must_use] pub const fn connect_timeout(&self) -> std::time::Duration { return self.connect_timeout; } /// Returns the default unary request timeout. #[must_use] pub const fn unary_timeout(&self) -> std::time::Duration { return self.unary_timeout; } /// Returns the bounded graceful-close timeout. #[must_use] pub const fn close_timeout(&self) -> std::time::Duration { return self.close_timeout; } /// Returns the reconnect policy. #[must_use] pub const fn reconnect(&self) -> &crate::YellowstoneGrpcReconnectSettings { return &self.reconnect; } /// Returns the bounded outgoing Subscribe request channel capacity. #[must_use] pub const fn request_channel_capacity(&self) -> usize { return self.request_channel_capacity; } /// Returns the bounded incoming update delivery channel capacity. #[must_use] pub const fn update_channel_capacity(&self) -> usize { return self.update_channel_capacity; } /// Returns the maximum inbound protobuf message size accepted by KSP. #[must_use] pub const fn max_inbound_message_size_bytes(&self) -> usize { return self.max_inbound_message_size_bytes; } /// Returns the maximum outbound protobuf message size accepted by KSP. #[must_use] pub const fn max_outbound_message_size_bytes(&self) -> usize { return self.max_outbound_message_size_bytes; } /// Validates KSP-owned runtime bounds without reading Config or environment state. pub fn validate(&self) -> ksp_core_lib::Result<()> { if let std::result::Result::Err(error) = validate_grpc_duration(self.connect_timeout, "grpc_session.connect_timeout") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_duration(self.unary_timeout, "grpc_session.unary_timeout") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_duration(self.close_timeout, "grpc_session.close_timeout") { return std::result::Result::Err(error); } if self.reconnect.max_retries() > MAX_GRPC_RECONNECT_RETRIES { return grpc_invalid_settings("Yellowstone gRPC reconnect retry count exceeds the KSP runtime bound", "grpc_session.reconnect.max_retries"); } if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.initial_backoff(), "grpc_session.reconnect.initial_backoff") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.max_backoff(), "grpc_session.reconnect.max_backoff") { return std::result::Result::Err(error); } if self.reconnect.initial_backoff() > self.reconnect.max_backoff() { return grpc_invalid_settings("Yellowstone gRPC reconnect initial backoff must not exceed maximum backoff", "grpc_session.reconnect"); } if let std::result::Result::Err(error) = validate_grpc_capacity(self.request_channel_capacity, "grpc_session.request_channel_capacity") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_capacity(self.update_channel_capacity, "grpc_session.update_channel_capacity") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_message_size(self.max_inbound_message_size_bytes, "grpc_session.max_inbound_message_size_bytes") { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_grpc_message_size(self.max_outbound_message_size_bytes, "grpc_session.max_outbound_message_size_bytes") { return std::result::Result::Err(error); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, request_channel_capacity = self.request_channel_capacity, update_channel_capacity = self.update_channel_capacity, max_inbound_message_size_bytes = self.max_inbound_message_size_bytes, max_outbound_message_size_bytes = self.max_outbound_message_size_bytes, reconnect_max_retries = self.reconnect.max_retries(), "validated Yellowstone gRPC session settings" ); return std::result::Result::Ok(()); } } impl std::default::Default for YellowstoneGrpcSessionSettings { fn default() -> Self { return Self::new( DEFAULT_GRPC_CONNECT_TIMEOUT, DEFAULT_GRPC_UNARY_TIMEOUT, DEFAULT_GRPC_CLOSE_TIMEOUT, crate::YellowstoneGrpcReconnectSettings::default(), DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY, DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY, DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES, DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES, ); } } /// Runtime settings for one named Yellowstone gRPC endpoint. #[derive(Clone, Debug, Eq, PartialEq)] pub struct YellowstoneGrpcEndpointSettings { name: std::string::String, enabled: bool, provider: crate::YellowstoneGrpcProviderName, cluster: crate::YellowstoneGrpcClusterName, url: crate::YellowstoneGrpcEndpointUrl, session: crate::YellowstoneGrpcSessionSettings, metadata: std::vec::Vec, } impl YellowstoneGrpcEndpointSettings { /// Creates explicit settings for one logical Yellowstone gRPC endpoint. #[must_use] pub fn new( name: impl std::convert::Into, enabled: bool, provider: crate::YellowstoneGrpcProviderName, cluster: crate::YellowstoneGrpcClusterName, url: crate::YellowstoneGrpcEndpointUrl, session: crate::YellowstoneGrpcSessionSettings, ) -> Self { return Self { name: name.into(), enabled, provider, cluster, url, session, metadata: std::vec::Vec::new() }; } /// Returns the logical endpoint name. #[must_use] pub fn name(&self) -> &str { return self.name.as_str(); } /// Returns whether this endpoint can be used to prepare a physical channel. #[must_use] pub const fn enabled(&self) -> bool { return self.enabled; } /// Returns the open provider descriptor. #[must_use] pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName { return &self.provider; } /// Returns the open cluster descriptor. #[must_use] pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName { return &self.cluster; } /// Returns the sensitive Yellowstone gRPC endpoint URL wrapper. #[must_use] pub const fn url(&self) -> &crate::YellowstoneGrpcEndpointUrl { return &self.url; } /// Returns the effective runtime settings for the channel/session path. #[must_use] pub const fn session(&self) -> &crate::YellowstoneGrpcSessionSettings { return &self.session; } /// Returns metadata entries in declaration order without exposing their values. #[must_use] pub fn metadata(&self) -> &[crate::YellowstoneGrpcMetadataEntry] { return self.metadata.as_slice(); } /// Replaces request metadata after validating KSP bounds and ASCII metadata rules. pub fn with_metadata(mut self, metadata: std::vec::Vec) -> ksp_core_lib::Result { if metadata.len() > MAX_GRPC_METADATA_ENTRY_COUNT { return grpc_invalid_settings_value("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata"); } self.metadata = metadata; if let std::result::Result::Err(error) = self.validate() { return std::result::Result::Err(error); } return std::result::Result::Ok(self); } /// Validates this endpoint without performing network I/O. pub fn validate(&self) -> ksp_core_lib::Result<()> { return validate_grpc_endpoint(self, "grpc_endpoint"); } } /// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine. #[derive(Clone, Debug, Eq, PartialEq)] pub struct YellowstoneGrpcTransportSettings { endpoints: std::vec::Vec, } impl YellowstoneGrpcTransportSettings { /// Creates complete Yellowstone gRPC runtime settings. #[must_use] pub fn new(endpoints: std::vec::Vec) -> Self { return Self { endpoints }; } /// Returns configured Yellowstone gRPC endpoints in declaration order. #[must_use] pub fn endpoints(&self) -> &[crate::YellowstoneGrpcEndpointSettings] { return self.endpoints.as_slice(); } /// Validates structural runtime invariants without reading Config or environment state. pub fn validate(&self) -> ksp_core_lib::Result<()> { ksp_logging_lib::trace!(target: crate::TRACING_TARGET, endpoint_count = self.endpoints.len(), "validating Yellowstone gRPC transport settings"); if self.endpoints.is_empty() { return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be configured", "grpc_endpoints"); } if self.endpoints.len() > MAX_GRPC_ENDPOINT_COUNT { return grpc_invalid_settings("Yellowstone gRPC endpoint count exceeds the KSP bound", "grpc_endpoints"); } let mut enabled_endpoint_count = 0_usize; for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() { let field_prefix = format!("grpc_endpoints[{endpoint_index}]"); if let std::result::Result::Err(error) = validate_grpc_endpoint(endpoint, field_prefix.as_str()) { return std::result::Result::Err(error); } if endpoint.enabled() { enabled_endpoint_count += 1; } for previous in &self.endpoints[..endpoint_index] { if previous.name() == endpoint.name() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint names must be unique") .with_context("field", format!("grpc_endpoints[{endpoint_index}].name")) .with_context("endpoint_name", endpoint.name()), ); } } } if enabled_endpoint_count == 0 { return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be enabled", "grpc_endpoints.enabled"); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, endpoint_count = self.endpoints.len(), enabled_endpoint_count, "validated Yellowstone gRPC transport settings" ); return std::result::Result::Ok(()); } } fn validate_grpc_endpoint(endpoint: &crate::YellowstoneGrpcEndpointSettings, field_prefix: &str) -> ksp_core_lib::Result<()> { let name_field = format!("{field_prefix}.name"); if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.name(), name_field.as_str()) { return std::result::Result::Err(error); } let provider_field = format!("{field_prefix}.provider"); if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.provider().as_str(), provider_field.as_str()) { return std::result::Result::Err(error); } let cluster_field = format!("{field_prefix}.cluster"); if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = endpoint.session().validate() { return std::result::Result::Err(error); } if endpoint.metadata().len() > MAX_GRPC_METADATA_ENTRY_COUNT { return grpc_invalid_settings("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata"); } ksp_logging_lib::trace!( target: crate::TRACING_TARGET, endpoint_name = endpoint.name(), provider = endpoint.provider().as_str(), cluster = endpoint.cluster().as_str(), enabled = endpoint.enabled(), "validated Yellowstone gRPC endpoint settings" ); return std::result::Result::Ok(()); } fn validate_grpc_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> { if value.trim().is_empty() { return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not be empty", field); } if value.trim() != value { return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not contain leading or trailing whitespace", field); } if value.len() > MAX_GRPC_DESCRIPTOR_LENGTH_BYTES { return grpc_invalid_settings("Yellowstone gRPC transport descriptor exceeds the KSP length bound", field); } return std::result::Result::Ok(()); } fn validate_grpc_duration(value: std::time::Duration, field: &str) -> ksp_core_lib::Result<()> { if value.is_zero() { return grpc_invalid_settings("Yellowstone gRPC runtime duration must be greater than zero", field); } if value > MAX_GRPC_RUNTIME_DURATION { return grpc_invalid_settings("Yellowstone gRPC runtime duration exceeds the KSP bound", field); } return std::result::Result::Ok(()); } fn validate_grpc_capacity(value: usize, field: &str) -> ksp_core_lib::Result<()> { if value == 0 { return grpc_invalid_settings("Yellowstone gRPC channel capacity must be greater than zero", field); } if value > MAX_GRPC_CHANNEL_CAPACITY { return grpc_invalid_settings("Yellowstone gRPC channel capacity exceeds the KSP bound", field); } return std::result::Result::Ok(()); } fn validate_grpc_message_size(value: usize, field: &str) -> ksp_core_lib::Result<()> { if value == 0 { return grpc_invalid_settings("Yellowstone gRPC message size must be greater than zero", field); } if value > MAX_GRPC_MESSAGE_SIZE_BYTES { return grpc_invalid_settings("Yellowstone gRPC message size exceeds the KSP bound", field); } return std::result::Result::Ok(()); } fn grpc_invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings"); return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field)); } fn grpc_invalid_settings_value(message: &str, field: &str) -> ksp_core_lib::Result { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings"); return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field)); } #[cfg(test)] #[path = "../unit_tests/grpc_settings.rs"] mod tests;