v0.2.9-pre.003
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||
// version: 1
|
||||
// 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);
|
||||
@@ -16,6 +16,9 @@ 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);
|
||||
|
||||
@@ -25,6 +28,7 @@ const MAX_GRPC_RUNTIME_DURATION: std::time::Duration = std::time::Duration::from
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct YellowstoneGrpcEndpointUrl {
|
||||
value: std::string::String,
|
||||
uses_tls: bool,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcEndpointUrl {
|
||||
@@ -70,7 +74,7 @@ impl YellowstoneGrpcEndpointUrl {
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, scheme = parsed.scheme(), "validated Yellowstone gRPC endpoint URL syntax");
|
||||
return std::result::Result::Ok(Self { value });
|
||||
return std::result::Result::Ok(Self { value, uses_tls: parsed.scheme() == "https" });
|
||||
}
|
||||
|
||||
/// Returns the sensitive runtime URL text.
|
||||
@@ -80,6 +84,12 @@ impl YellowstoneGrpcEndpointUrl {
|
||||
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 {
|
||||
@@ -88,6 +98,107 @@ impl std::fmt::Debug for 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<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
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<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
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::<tonic::metadata::Ascii>::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<Self> {
|
||||
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::<tonic::metadata::Ascii>::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", &"<redacted>")
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct YellowstoneGrpcProviderName {
|
||||
@@ -330,6 +441,7 @@ pub struct YellowstoneGrpcEndpointSettings {
|
||||
cluster: crate::YellowstoneGrpcClusterName,
|
||||
url: crate::YellowstoneGrpcEndpointUrl,
|
||||
session: crate::YellowstoneGrpcSessionSettings,
|
||||
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcEndpointSettings {
|
||||
@@ -343,7 +455,7 @@ impl YellowstoneGrpcEndpointSettings {
|
||||
url: crate::YellowstoneGrpcEndpointUrl,
|
||||
session: crate::YellowstoneGrpcSessionSettings,
|
||||
) -> Self {
|
||||
return Self { name: name.into(), enabled, provider, cluster, url, session };
|
||||
return Self { name: name.into(), enabled, provider, cluster, url, session, metadata: std::vec::Vec::new() };
|
||||
}
|
||||
|
||||
/// Returns the logical endpoint name.
|
||||
@@ -382,6 +494,24 @@ impl YellowstoneGrpcEndpointSettings {
|
||||
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<crate::YellowstoneGrpcMetadataEntry>) -> ksp_core_lib::Result<Self> {
|
||||
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");
|
||||
@@ -464,6 +594,9 @@ fn validate_grpc_endpoint(endpoint: &crate::YellowstoneGrpcEndpointSettings, fie
|
||||
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(),
|
||||
@@ -523,6 +656,11 @@ fn grpc_invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()>
|
||||
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<T>(message: &str, field: &str) -> ksp_core_lib::Result<T> {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user