v0.2.9-pre.003

This commit is contained in:
2026-08-24 11:10:59 +02:00
parent 3172cda241
commit a038194679
14 changed files with 603 additions and 147 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-onchain-transport-lib/Cargo.toml
# version: 7
# version: 8
[package]
name = "ksp-onchain-transport-lib"
@@ -11,16 +11,20 @@ repository.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
futures-util = { workspace = true, features = ["sink", "std"] }
http.workspace = true
reqwest = { workspace = true, features = ["rustls"] }
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"] }
tonic = { workspace = true, features = ["channel", "tls-aws-lc", "tls-webpki-roots"] }
tonic-prost.workspace = true
yellowstone-grpc-proto.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
tonic = { workspace = true, features = ["codegen", "server"] }
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
[lints]
workspace = true

View File

@@ -1,10 +1,12 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 5
// version: 6
/// 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 a Yellowstone gRPC endpoint returns a remote gRPC status.
pub const ERROR_CODE_GRPC_STATUS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_status");
/// 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.

View File

@@ -1,80 +1,96 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs
// version: 2
// version: 3
/// Prepared Yellowstone gRPC channel owned by KSP Transport.
/// Prepared or connected 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.
/// The underlying Tonic channel, endpoint URL and request metadata remain private. Callers use the KSP-owned typed Yellowstone surfaces layered on this
/// physical channel instead of receiving a raw Tonic escape hatch.
#[derive(Clone)]
pub struct YellowstoneGrpcChannel {
endpoint_name: std::string::String,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
_channel: tonic::transport::Channel,
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
session: crate::YellowstoneGrpcSessionSettings,
}
impl YellowstoneGrpcChannel {
/// Prepares one lazy Yellowstone gRPC channel from validated Transport-owned endpoint settings.
/// Prepares one lazy HTTP/2 channel without establishing a network connection.
///
/// 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() {
/// HTTPS endpoints receive the KSP TLS configuration immediately, so invalid local TLS setup is rejected before a typed client is created. Tonic lazy
/// channels require an active Tokio runtime even though no socket is opened yet.
pub fn prepare(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(error);
}
if !endpoint.enabled() {
if !settings.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()),
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot prepare a channel")
.with_context("endpoint_name", settings.name()),
);
}
if tokio::runtime::Handle::try_current().is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name = endpoint.name(),
provider = endpoint.provider().as_str(),
cluster = endpoint.cluster().as_str(),
"Yellowstone gRPC channel preparation requires an active Tokio runtime"
);
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel preparation requires an active Tokio runtime"));
}
let endpoint = match build_tonic_endpoint(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = endpoint.connect_lazy();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
metadata_count = settings.metadata().len(),
"prepared lazy Yellowstone gRPC channel"
);
return std::result::Result::Ok(Self::from_parts(settings, channel));
}
/// Establishes one Yellowstone gRPC HTTP/2 channel with bounded connect timeout and configured TLS roots.
pub async fn connect(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(error);
}
if !settings.enabled() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel requires an active Tokio runtime")
.with_context("endpoint_name", endpoint.name()),
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot connect")
.with_context("endpoint_name", settings.name()),
);
}
let tonic_endpoint = match tonic::transport::Endpoint::from_shared(endpoint.url().as_str().to_owned()) {
if tokio::runtime::Handle::try_current().is_err() {
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC connection requires an active Tokio runtime"));
}
let endpoint = match build_tonic_endpoint(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = match endpoint.connect().await {
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()),
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
"Yellowstone gRPC channel connection failed"
);
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel connection failed"));
},
};
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"
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
"connected Yellowstone gRPC channel"
);
return std::result::Result::Ok(Self {
endpoint_name: endpoint.name().to_owned(),
provider: endpoint.provider().clone(),
cluster: endpoint.cluster().clone(),
_channel: channel,
});
return std::result::Result::Ok(Self::from_parts(settings, channel));
}
/// Returns the safe logical endpoint name.
@@ -83,17 +99,34 @@ impl YellowstoneGrpcChannel {
return self.endpoint_name.as_str();
}
/// Returns the safe open provider descriptor.
/// Returns the open provider descriptor.
#[must_use]
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
return &self.provider;
}
/// Returns the safe open cluster descriptor.
/// Returns the open cluster descriptor.
#[must_use]
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
return &self.cluster;
}
/// Creates the standard Solana Yellowstone unary facade over this physical channel.
#[must_use]
pub fn standard_unary_client(&self) -> crate::SolanaYellowstoneGrpcUnaryClient {
return crate::SolanaYellowstoneGrpcUnaryClient::new(self.channel.clone(), self.metadata.clone(), self.session.clone());
}
fn from_parts(settings: &crate::YellowstoneGrpcEndpointSettings, channel: tonic::transport::Channel) -> Self {
return Self {
endpoint_name: settings.name().to_owned(),
provider: settings.provider().clone(),
cluster: settings.cluster().clone(),
channel,
metadata: settings.metadata().to_vec(),
session: settings.session().clone(),
};
}
}
impl std::fmt::Debug for YellowstoneGrpcChannel {
@@ -103,10 +136,39 @@ impl std::fmt::Debug for YellowstoneGrpcChannel {
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("cluster", &self.cluster)
.field("metadata_count", &self.metadata.len())
.field("channel", &"<private>")
.finish();
}
}
fn build_tonic_endpoint(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<tonic::transport::Endpoint> {
let endpoint = match tonic::transport::Endpoint::from_shared(settings.url().as_str().to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC endpoint URI cannot be prepared by the HTTP/2 transport"));
},
};
let endpoint = endpoint.connect_timeout(settings.session().connect_timeout()).buffer_size(settings.session().request_channel_capacity());
if settings.url().uses_tls() {
let tls = tonic::transport::ClientTlsConfig::new().with_webpki_roots().timeout(settings.session().connect_timeout());
return match endpoint.tls_config(tls) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC TLS configuration failed before connection"))
},
};
}
return std::result::Result::Ok(endpoint);
}
fn grpc_channel_error(settings: &crate::YellowstoneGrpcEndpointSettings, message: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, message)
.with_context("endpoint_name", settings.name())
.with_context("provider", settings.provider().as_str())
.with_context("cluster", settings.cluster().as_str());
}
#[cfg(test)]
#[path = "../unit_tests/grpc_channel.rs"]
mod tests;

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 35
// version: 36
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -32,8 +32,9 @@
//! 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`.
//! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types.
//! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs
//! through KSP-owned DTOs. Streaming `Subscribe` remains deliberately deferred to `pre.004`.
mod client;
mod constants;
@@ -41,6 +42,7 @@ mod error;
mod executor;
mod grpc_channel;
mod grpc_settings;
mod grpc_unary;
mod json_rpc;
mod pool;
mod resilience;
@@ -77,6 +79,8 @@ pub use self::client::HttpEndpointSnapshot;
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 a Yellowstone gRPC endpoint returns a remote status.
pub use self::error::ERROR_CODE_GRPC_STATUS;
/// 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.
@@ -109,7 +113,7 @@ 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.
/// 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;
@@ -117,6 +121,8 @@ pub use self::grpc_settings::YellowstoneGrpcClusterName;
pub use self::grpc_settings::YellowstoneGrpcEndpointSettings;
/// Runtime Yellowstone gRPC endpoint URL with redacted diagnostics.
pub use self::grpc_settings::YellowstoneGrpcEndpointUrl;
/// Validated public or secret ASCII metadata attached to Yellowstone gRPC requests.
pub use self::grpc_settings::YellowstoneGrpcMetadataEntry;
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
pub use self::grpc_settings::YellowstoneGrpcProviderName;
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
@@ -125,6 +131,22 @@ pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
/// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel.
pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient;
/// Block height returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneBlockHeight;
/// Result of a standard Yellowstone blockhash-validity check.
pub use self::grpc_unary::YellowstoneBlockhashValidity;
/// Latest blockhash returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneLatestBlockhash;
/// Echo returned by the standard Yellowstone unary Ping RPC.
pub use self::grpc_unary::YellowstonePong;
/// Replay availability advertised by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneReplayInfo;
/// Current slot returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneSlot;
/// Bounded endpoint version returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneVersionInfo;
/// 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.

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
// version: 6
// version: 7
/// Commitment level accepted by typed Solana HTTP and WebSocket adapters.
/// Commitment level accepted by typed Solana HTTP, WebSocket and Yellowstone gRPC adapters.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SolanaCommitment {
/// Query the most recent processed bank.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 40
// version: 41
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -789,3 +789,39 @@ async fn public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_
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");
}
#[test]
fn public_v0_2_9_pre_003_yellowstone_metadata_and_seven_unary_contracts_are_available_from_crate_root() {
let public = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture");
assert!(public.is_ok());
let secret = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "secret");
assert!(secret.is_ok());
let replay = ksp_onchain_transport_lib::YellowstoneReplayInfo::new(std::option::Option::Some(1));
assert_eq!(replay.first_available(), std::option::Option::Some(1));
let pong = ksp_onchain_transport_lib::YellowstonePong::new(2);
assert_eq!(pong.count(), 2);
let latest = ksp_onchain_transport_lib::YellowstoneLatestBlockhash::new(3, "hash".to_owned(), 4);
assert_eq!(latest.slot(), 3);
assert_eq!(latest.blockhash(), "hash");
assert_eq!(latest.last_valid_block_height(), 4);
assert_eq!(ksp_onchain_transport_lib::YellowstoneBlockHeight::new(5).block_height(), 5);
assert_eq!(ksp_onchain_transport_lib::YellowstoneSlot::new(6).slot(), 6);
let validity = ksp_onchain_transport_lib::YellowstoneBlockhashValidity::new(7, true);
assert_eq!(validity.slot(), 7);
assert!(validity.valid());
assert_eq!(ksp_onchain_transport_lib::YellowstoneVersionInfo::new("v".to_owned()).version(), "v");
let _replay_info = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::subscribe_replay_info;
let _ping = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::ping;
let _latest_blockhash = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_latest_blockhash;
let _block_height = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_block_height;
let _slot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_slot;
async fn call_is_blockhash_valid(
client: &ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneBlockhashValidity> {
return client.is_blockhash_valid("fixture-blockhash", std::option::Option::None).await;
}
let _is_blockhash_valid = call_is_blockhash_valid;
let _version = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_version;
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.code(), "grpc_status");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 33
// version: 34
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -1008,7 +1008,7 @@ fn release_v0_2_9_pre_002_materializes_minimal_yellowstone_engine_without_provid
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("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"));
@@ -1019,10 +1019,50 @@ fn release_v0_2_9_pre_002_materializes_minimal_yellowstone_engine_without_provid
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>();
}
#[test]
fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_methods_without_subscribe() {
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 unary_source = include_str!("../src/grpc_unary.rs");
let crate_root = include_str!("../src/lib.rs");
assert!(root_manifest.contains("http = { version = \"^1.5\", default-features = false }"));
assert!(root_manifest.contains("tonic-prost = { version = \"^0.14\", default-features = false }"));
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\", \"tls-aws-lc\", \"tls-webpki-roots\"] }"));
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"));
assert!(transport_manifest.contains("tonic-prost.workspace = true"));
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
assert!(transport_manifest.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"));
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
assert!(settings_source.contains("YellowstoneGrpcMetadataEntry"));
assert!(settings_source.contains("set_sensitive"));
assert!(channel_source.contains("ClientTlsConfig"));
assert!(channel_source.contains("with_webpki_roots"));
assert!(channel_source.contains("pub async fn connect"));
for path in [
"/geyser.Geyser/SubscribeReplayInfo",
"/geyser.Geyser/Ping",
"/geyser.Geyser/GetLatestBlockhash",
"/geyser.Geyser/GetBlockHeight",
"/geyser.Geyser/GetSlot",
"/geyser.Geyser/IsBlockhashValid",
"/geyser.Geyser/GetVersion",
] {
assert!(unary_source.contains(path), "missing standard Yellowstone unary path: {path}");
}
assert!(!unary_source.contains("const PATH_SUBSCRIBE: "));
assert!(!unary_source.contains("SubscribeDeshred"));
assert!(!unary_source.contains("PublicNode"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _client = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient>();
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
// version: 2
// version: 3
fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSettings {
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value).expect("fixture Yellowstone gRPC URL must parse");
@@ -28,7 +28,7 @@ async fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() {
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 _client = channel.standard_unary_client();
let _wire_type = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
}
@@ -48,3 +48,13 @@ fn grpc_channel_prepare_rejects_disabled_endpoint_before_network_io() {
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(result.is_err());
}
#[tokio::test(flavor = "current_thread")]
async fn grpc_channel_prepare_configures_https_without_exposing_url() {
let endpoint = endpoint(true, "https://example.invalid:443/GRPC-SECRET-CANARY");
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(result.is_ok());
let rendered = format!("{result:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains("example.invalid"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
// version: 2
// version: 3
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");
@@ -149,3 +149,50 @@ fn grpc_transport_settings_require_one_enabled_endpoint() {
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![disabled]).validate().is_err());
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![]).validate().is_err());
}
#[test]
fn grpc_metadata_validates_ascii_bounds_sensitivity_and_redacted_debug() {
let public = crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "VISIBLE-CANARY").expect("public metadata must validate");
let secret = crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("secret metadata must validate");
assert_eq!(public.key(), "x-ksp-public");
assert!(!public.is_secret());
assert!(secret.is_secret());
let rendered = format!("{public:?} {secret:?}");
assert!(!rendered.contains("VISIBLE-CANARY"));
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
let mut metadata = tonic::metadata::MetadataMap::new();
assert!(secret.append_to(&mut metadata).is_ok());
let appended = metadata.get("x-ksp-token").expect("secret metadata must be appended");
assert!(appended.is_sensitive());
}
#[test]
fn grpc_metadata_rejects_reserved_binary_uppercase_malformed_and_oversized_values() {
assert!(crate::YellowstoneGrpcMetadataEntry::public("grpc-timeout", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bin", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("X-KSP-UPPER", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bad", "line\nfeed").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-large", "S".repeat(super::MAX_GRPC_METADATA_VALUE_LENGTH_BYTES + 1)).is_err());
}
#[test]
fn grpc_endpoint_metadata_count_is_bounded_and_url_tracks_tls_scheme() {
let http = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("HTTP URL must parse");
let https = crate::YellowstoneGrpcEndpointUrl::parse("https://example.invalid:443").expect("HTTPS URL must parse");
assert!(!http.uses_tls());
assert!(https.uses_tls());
let metadata = (0..=super::MAX_GRPC_METADATA_ENTRY_COUNT)
.map(|index| {
return crate::YellowstoneGrpcMetadataEntry::public(format!("x-ksp-{index}"), "value").expect("generated metadata must be valid");
})
.collect();
let endpoint = crate::YellowstoneGrpcEndpointSettings::new(
"metadata-bound",
true,
crate::YellowstoneGrpcProviderName::new("fixture"),
crate::YellowstoneGrpcClusterName::new("devnet"),
http,
crate::YellowstoneGrpcSessionSettings::default(),
);
assert!(endpoint.with_metadata(metadata).is_err());
}