v0.2.9-pre.002
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
||||
|
||||
@@ -65,6 +65,8 @@ fn transport_manifest_preserves_ksp_dependency_firewall() {
|
||||
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }"));
|
||||
assert!(manifest.contains("tokio-tungstenite = { workspace = true, features = [\"connect\", \"rustls-tls-webpki-roots\"] }"));
|
||||
assert!(manifest.contains("tonic = { workspace = true, features = [\"channel\"] }"));
|
||||
assert!(manifest.contains("yellowstone-grpc-proto.workspace = true"));
|
||||
assert!(manifest.contains("[dev-dependencies]"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }"));
|
||||
}
|
||||
@@ -86,7 +88,18 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
|
||||
let dependency_names = manifest_dependency_names(dependencies);
|
||||
assert_eq!(
|
||||
dependency_names,
|
||||
std::vec!["futures-util", "ksp-core-lib", "ksp-logging-lib", "reqwest", "serde", "serde_json", "tokio", "tokio-tungstenite"]
|
||||
std::vec![
|
||||
"futures-util",
|
||||
"ksp-core-lib",
|
||||
"ksp-logging-lib",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tonic",
|
||||
"yellowstone-grpc-proto"
|
||||
]
|
||||
);
|
||||
let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1);
|
||||
assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
# version: 6
|
||||
# version: 7
|
||||
|
||||
[package]
|
||||
name = "ksp-onchain-transport-lib"
|
||||
@@ -16,6 +16,8 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
|
||||
tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] }
|
||||
tonic = { workspace = true, features = ["channel"] }
|
||||
yellowstone-grpc-proto.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Error code used when no logical endpoint can satisfy a request.
|
||||
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
|
||||
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
|
||||
pub const ERROR_CODE_GRPC_CHANNEL_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_channel_failed");
|
||||
/// Error code used when an HTTP connection cannot be established.
|
||||
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
|
||||
/// Error code used when an HTTP request fails after a connection exists.
|
||||
|
||||
99
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal file
99
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||
// version: 1
|
||||
|
||||
/// Prepared Yellowstone gRPC channel owned by KSP Transport.
|
||||
///
|
||||
/// `pre.002` intentionally prepares a lazy HTTP/2 channel without performing network I/O. TLS configuration, provider-neutral metadata injection and live
|
||||
/// connection/error semantics are added by `pre.003`. The underlying Tonic channel and upstream Yellowstone protobuf types are never exposed publicly.
|
||||
pub struct YellowstoneGrpcChannel {
|
||||
endpoint_name: std::string::String,
|
||||
provider: crate::YellowstoneGrpcProviderName,
|
||||
cluster: crate::YellowstoneGrpcClusterName,
|
||||
channel: tonic::transport::Channel,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcChannel {
|
||||
/// Prepares one lazy Yellowstone gRPC channel from validated Transport-owned endpoint settings.
|
||||
///
|
||||
/// This operation performs no network connection. HTTPS endpoints remain syntactically accepted here; TLS is deliberately completed in `pre.003`
|
||||
/// before any live call is allowed.
|
||||
pub fn prepare(endpoint: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||
if let std::result::Result::Err(error) = endpoint.validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if !endpoint.enabled() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "disabled Yellowstone gRPC endpoint cannot prepare a channel")
|
||||
.with_context("field", "grpc_endpoint.enabled")
|
||||
.with_context("endpoint_name", endpoint.name()),
|
||||
);
|
||||
}
|
||||
let tonic_endpoint = match tonic::transport::Endpoint::from_shared(endpoint.url().as_str().to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
endpoint_name = endpoint.name(),
|
||||
provider = endpoint.provider().as_str(),
|
||||
cluster = endpoint.cluster().as_str(),
|
||||
"failed to prepare Yellowstone gRPC channel URI"
|
||||
);
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel could not be prepared")
|
||||
.with_context("endpoint_name", endpoint.name()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let tonic_endpoint = tonic_endpoint
|
||||
.connect_timeout(endpoint.session().connect_timeout())
|
||||
.timeout(endpoint.session().unary_timeout())
|
||||
.buffer_size(endpoint.session().request_channel_capacity());
|
||||
let channel = tonic_endpoint.connect_lazy();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
endpoint_name = endpoint.name(),
|
||||
provider = endpoint.provider().as_str(),
|
||||
cluster = endpoint.cluster().as_str(),
|
||||
"prepared lazy Yellowstone gRPC channel"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
endpoint_name: endpoint.name().to_owned(),
|
||||
provider: endpoint.provider().clone(),
|
||||
cluster: endpoint.cluster().clone(),
|
||||
channel,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the safe logical endpoint name.
|
||||
#[must_use]
|
||||
pub fn endpoint_name(&self) -> &str {
|
||||
return self.endpoint_name.as_str();
|
||||
}
|
||||
|
||||
/// Returns the safe open provider descriptor.
|
||||
#[must_use]
|
||||
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
|
||||
return &self.provider;
|
||||
}
|
||||
|
||||
/// Returns the safe open cluster descriptor.
|
||||
#[must_use]
|
||||
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
|
||||
return &self.cluster;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for YellowstoneGrpcChannel {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("YellowstoneGrpcChannel")
|
||||
.field("endpoint_name", &self.endpoint_name)
|
||||
.field("provider", &self.provider)
|
||||
.field("cluster", &self.cluster)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/grpc_channel.rs"]
|
||||
mod tests;
|
||||
528
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal file
528
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||
// version: 1
|
||||
|
||||
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_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,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcEndpointUrl {
|
||||
/// Parses and validates one Yellowstone gRPC endpoint URL.
|
||||
pub fn parse(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
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 });
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for YellowstoneGrpcEndpointUrl {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("YellowstoneGrpcEndpointUrl(<redacted>)");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<std::string::String>) -> 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<std::string::String>) -> 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,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcEndpointSettings {
|
||||
/// Creates explicit settings for one logical Yellowstone gRPC endpoint.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
name: impl std::convert::Into<std::string::String>,
|
||||
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 };
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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<crate::YellowstoneGrpcEndpointSettings>,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpcTransportSettings {
|
||||
/// Creates complete Yellowstone gRPC runtime settings.
|
||||
#[must_use]
|
||||
pub fn new(endpoints: std::vec::Vec<crate::YellowstoneGrpcEndpointSettings>) -> 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);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/grpc_settings.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 34
|
||||
// version: 35
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -31,11 +31,16 @@
|
||||
//! `0.2.8-pre.005` adds the typed Helius `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live
|
||||
//! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
|
||||
//! per-subscription backpressure path.
|
||||
//! `0.2.9-pre.002` opens the Yellowstone gRPC N1 engine foundation with Transport-owned redacted settings, bounded reconnect/channel/message policies, the
|
||||
//! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types. TLS, metadata and
|
||||
//! live unary calls remain deliberately deferred to `pre.003`.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod executor;
|
||||
mod grpc_channel;
|
||||
mod grpc_settings;
|
||||
mod json_rpc;
|
||||
mod pool;
|
||||
mod resilience;
|
||||
@@ -70,6 +75,8 @@ pub use self::client::HttpEndpointRoleSnapshot;
|
||||
pub use self::client::HttpEndpointSnapshot;
|
||||
/// Error code used when no logical endpoint can satisfy a request.
|
||||
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
||||
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
|
||||
pub use self::error::ERROR_CODE_GRPC_CHANNEL_FAILED;
|
||||
/// Error code used when an HTTP connection cannot be established.
|
||||
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
||||
/// Error code used when an HTTP request fails after connection establishment.
|
||||
@@ -102,6 +109,22 @@ pub use self::error::ERROR_CODE_WS_CONNECTION_FAILED;
|
||||
pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR;
|
||||
/// Error code used when a WebSocket session is no longer available.
|
||||
pub use self::error::ERROR_CODE_WS_SESSION_CLOSED;
|
||||
/// Prepared lazy Yellowstone gRPC channel owned by KSP Transport.
|
||||
pub use self::grpc_channel::YellowstoneGrpcChannel;
|
||||
/// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings.
|
||||
pub use self::grpc_settings::YellowstoneGrpcClusterName;
|
||||
/// Runtime settings for one named Yellowstone gRPC endpoint.
|
||||
pub use self::grpc_settings::YellowstoneGrpcEndpointSettings;
|
||||
/// Runtime Yellowstone gRPC endpoint URL with redacted diagnostics.
|
||||
pub use self::grpc_settings::YellowstoneGrpcEndpointUrl;
|
||||
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
|
||||
pub use self::grpc_settings::YellowstoneGrpcProviderName;
|
||||
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
|
||||
pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
|
||||
/// Runtime limits and lifecycle settings for one Yellowstone gRPC channel/session path.
|
||||
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
|
||||
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
|
||||
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
|
||||
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
|
||||
pub use self::json_rpc::JsonRpcErrorObject;
|
||||
/// Validated JSON-RPC 2.0 error response.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 38
|
||||
// version: 39
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -760,3 +760,32 @@ fn public_v0_2_8_pre_009_helius_slots_updates_surface_reuses_shared_typed_contra
|
||||
let _shared_slot_update = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotUpdate>();
|
||||
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root() {
|
||||
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000")
|
||||
.expect("public Yellowstone gRPC URL parser must accept HTTP fixture endpoint");
|
||||
let session = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default();
|
||||
assert!(session.validate().is_ok());
|
||||
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||
"fixture",
|
||||
true,
|
||||
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||
url,
|
||||
session,
|
||||
);
|
||||
let settings = ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings::new(std::vec![endpoint.clone()]);
|
||||
assert!(settings.validate().is_ok());
|
||||
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||
assert!(channel.is_ok());
|
||||
let channel = match channel {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(channel.endpoint_name(), "fixture");
|
||||
assert_eq!(channel.provider().as_str(), "fixture-provider");
|
||||
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.domain(), "onchain_transport");
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.code(), "grpc_channel_failed");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 32
|
||||
// version: 33
|
||||
|
||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||
|
||||
@@ -996,3 +996,33 @@ fn release_v0_2_8_pre_010_live_smoke_policy_preserves_secret_and_dependency_owne
|
||||
let env_example = std::fs::read_to_string(workspace.join(".env.example")).expect(".env.example must be readable");
|
||||
assert!(env_example.contains("KSP_SECRET_HELIUS_API_KEY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_9_pre_002_materializes_minimal_yellowstone_engine_without_provider_or_ws_coupling() {
|
||||
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport test must resolve workspace root");
|
||||
let root_manifest = std::fs::read_to_string(workspace.join("Cargo.toml")).expect("workspace manifest must be readable");
|
||||
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
|
||||
let settings_source = include_str!("../src/grpc_settings.rs");
|
||||
let channel_source = include_str!("../src/grpc_channel.rs");
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(root_manifest.contains("tonic = { version = \"^0.14\", default-features = false }"));
|
||||
assert!(root_manifest.contains("yellowstone-grpc-proto = { version = \"^12.6\", default-features = false }"));
|
||||
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\"] }"));
|
||||
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
|
||||
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
|
||||
assert!(!transport_manifest.contains("ksp-config-lib"));
|
||||
assert!(settings_source.contains("YellowstoneGrpcEndpointUrl"));
|
||||
assert!(settings_source.contains("YellowstoneGrpcSessionSettings"));
|
||||
assert!(settings_source.contains("YellowstoneGrpcTransportSettings"));
|
||||
assert!(!settings_source.contains("WsProtocolKind"));
|
||||
assert!(!settings_source.contains("KSP_SECRET_"));
|
||||
assert!(channel_source.contains("tonic::transport::Endpoint::from_shared"));
|
||||
assert!(channel_source.contains("connect_lazy"));
|
||||
assert!(!channel_source.contains("tls_config"));
|
||||
assert!(!channel_source.contains("MetadataMap"));
|
||||
assert!(!channel_source.contains("WsSession"));
|
||||
assert!(!crate_root.contains("pub use tonic"));
|
||||
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||
let _published_wire = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
|
||||
}
|
||||
|
||||
40
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal file
40
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
|
||||
// version: 1
|
||||
|
||||
fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSettings {
|
||||
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value).expect("fixture Yellowstone gRPC URL must parse");
|
||||
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||
"fixture",
|
||||
enabled,
|
||||
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||
parsed,
|
||||
crate::YellowstoneGrpcSessionSettings::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() {
|
||||
let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY");
|
||||
let channel = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||
assert!(channel.is_ok());
|
||||
let channel = match channel {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(channel.endpoint_name(), "fixture");
|
||||
assert_eq!(channel.provider().as_str(), "fixture-provider");
|
||||
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||
let rendered = format!("{channel:?}");
|
||||
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||
assert!(!rendered.contains("127.0.0.1"));
|
||||
let _channel_type = std::any::type_name_of_val(&channel.channel);
|
||||
let _wire_type = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_channel_prepare_rejects_disabled_endpoint_before_network_io() {
|
||||
let endpoint = endpoint(false, "http://127.0.0.1:10000");
|
||||
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
151
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal file
151
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
|
||||
// version: 1
|
||||
|
||||
fn endpoint(name: &str, enabled: bool, url: &str, session: crate::YellowstoneGrpcSessionSettings) -> crate::YellowstoneGrpcEndpointSettings {
|
||||
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture Yellowstone gRPC URL must parse");
|
||||
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||
name,
|
||||
enabled,
|
||||
crate::YellowstoneGrpcProviderName::new("fixture"),
|
||||
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||
parsed,
|
||||
session,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_endpoint_url_accepts_http_and_https_and_redacts_debug() {
|
||||
for value in ["http://127.0.0.1:10000", "https://yellowstone.example.invalid:443/path?token=GRPC-SECRET-CANARY"] {
|
||||
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value);
|
||||
assert!(parsed.is_ok());
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert_eq!(parsed.as_str(), value);
|
||||
let rendered = format!("{parsed:?}");
|
||||
assert_eq!(rendered, "YellowstoneGrpcEndpointUrl(<redacted>)");
|
||||
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_endpoint_url_rejects_non_http_schemes_without_echoing_secret() {
|
||||
let result = crate::YellowstoneGrpcEndpointUrl::parse("wss://GRPC-SECRET-CANARY@example.invalid/socket");
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
let rendered = error.to_string();
|
||||
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||
assert!(!format!("{error:?}").contains("GRPC-SECRET-CANARY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_endpoint_url_rejects_excessive_length_without_echoing_payload() {
|
||||
let value = format!("https://example.invalid/{}", "S".repeat(super::MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES + 1));
|
||||
let result = crate::YellowstoneGrpcEndpointUrl::parse(value);
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert!(!format!("{error:?}").contains(&"S".repeat(64)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_session_defaults_are_bounded_and_validate() {
|
||||
let settings = crate::YellowstoneGrpcSessionSettings::default();
|
||||
assert!(settings.validate().is_ok());
|
||||
assert!(!settings.connect_timeout().is_zero());
|
||||
assert!(!settings.unary_timeout().is_zero());
|
||||
assert!(!settings.close_timeout().is_zero());
|
||||
assert!(settings.request_channel_capacity() > 0);
|
||||
assert!(settings.update_channel_capacity() > 0);
|
||||
assert!(settings.max_inbound_message_size_bytes() > 0);
|
||||
assert!(settings.max_outbound_message_size_bytes() > 0);
|
||||
assert!(settings.reconnect().max_retries() <= super::MAX_GRPC_RECONNECT_RETRIES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_session_rejects_zero_excessive_and_reversed_runtime_bounds() {
|
||||
let zero = crate::YellowstoneGrpcSessionSettings::new(
|
||||
std::time::Duration::ZERO,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
crate::YellowstoneGrpcReconnectSettings::default(),
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
assert!(zero.validate().is_err());
|
||||
let excessive = crate::YellowstoneGrpcSessionSettings::new(
|
||||
super::MAX_GRPC_RUNTIME_DURATION + std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(
|
||||
super::MAX_GRPC_RECONNECT_RETRIES + 1,
|
||||
std::time::Duration::from_millis(1),
|
||||
std::time::Duration::from_millis(2),
|
||||
),
|
||||
super::MAX_GRPC_CHANNEL_CAPACITY + 1,
|
||||
1,
|
||||
super::MAX_GRPC_MESSAGE_SIZE_BYTES + 1,
|
||||
1,
|
||||
);
|
||||
assert!(excessive.validate().is_err());
|
||||
let reversed = crate::YellowstoneGrpcSessionSettings::new(
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
crate::YellowstoneGrpcReconnectSettings::new(5, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
assert!(reversed.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_transport_settings_validate_unique_enabled_endpoints_and_redact_urls() {
|
||||
let first = endpoint("first", true, "https://GRPC-SECRET-CANARY@example.invalid:443", crate::YellowstoneGrpcSessionSettings::default());
|
||||
let second = endpoint("second", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||
let settings = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), second]);
|
||||
assert!(settings.validate().is_ok());
|
||||
let rendered = format!("{settings:?}");
|
||||
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||
assert!(!rendered.contains("example.invalid"));
|
||||
let duplicate = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), first]);
|
||||
assert!(duplicate.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_transport_settings_reject_excessive_descriptor_and_endpoint_count() {
|
||||
let oversized_name = "n".repeat(super::MAX_GRPC_DESCRIPTOR_LENGTH_BYTES + 1);
|
||||
let oversized = endpoint(oversized_name.as_str(), true, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![oversized]).validate().is_err());
|
||||
let parsed = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("fixture Yellowstone gRPC URL must parse");
|
||||
let endpoints = (0..=super::MAX_GRPC_ENDPOINT_COUNT)
|
||||
.map(|index| {
|
||||
crate::YellowstoneGrpcEndpointSettings::new(
|
||||
format!("endpoint-{index}"),
|
||||
true,
|
||||
crate::YellowstoneGrpcProviderName::new("fixture"),
|
||||
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||
parsed.clone(),
|
||||
crate::YellowstoneGrpcSessionSettings::default(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert!(crate::YellowstoneGrpcTransportSettings::new(endpoints).validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_transport_settings_require_one_enabled_endpoint() {
|
||||
let disabled = endpoint("disabled", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![disabled]).validate().is_err());
|
||||
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![]).validate().is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user