v0.2.9-pre.002

This commit is contained in:
2026-08-24 09:54:14 +02:00
parent 21de590aea
commit 835de48cb7
14 changed files with 1302 additions and 75 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 237 # version: 238
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.9-pre.1" version = "0.2.9-pre.2"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -33,6 +33,8 @@ tracing-subscriber = { version = "^0.3", default-features = false }
tracing-appender = { version = "^0.2", default-features = false } tracing-appender = { version = "^0.2", default-features = false }
tokio = { version = "^1.53", default-features = false } tokio = { version = "^1.53", default-features = false }
tokio-tungstenite = { version = "^0.30", default-features = false } tokio-tungstenite = { version = "^0.30", default-features = false }
tonic = { version = "^0.14", default-features = false }
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
tempfile = { version = "^3.27" } tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false } chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" } tauri = { version = "^2.11" }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs // 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. //! 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("reqwest = { workspace = true, features = [\"rustls\"] }"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }")); 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("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("[dev-dependencies]"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }")); 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); let dependency_names = manifest_dependency_names(dependencies);
assert_eq!( assert_eq!(
dependency_names, 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); let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1);
assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist"); assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist");

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-onchain-transport-lib/Cargo.toml # file: crates/ksp-onchain-transport-lib/Cargo.toml
# version: 6 # version: 7
[package] [package]
name = "ksp-onchain-transport-lib" name = "ksp-onchain-transport-lib"
@@ -16,6 +16,8 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] } tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] } tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] }
tonic = { workspace = true, features = ["channel"] }
yellowstone-grpc-proto.workspace = true
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] } tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs // file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 4 // version: 5
/// Error code used when no logical endpoint can satisfy a request. /// 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"); 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. /// 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"); 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. /// Error code used when an HTTP request fails after a connection exists.

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

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs // file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 34 // version: 35
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![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 //! `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 //! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
//! per-subscription backpressure path. //! 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 client;
mod constants; mod constants;
mod error; mod error;
mod executor; mod executor;
mod grpc_channel;
mod grpc_settings;
mod json_rpc; mod json_rpc;
mod pool; mod pool;
mod resilience; mod resilience;
@@ -70,6 +75,8 @@ pub use self::client::HttpEndpointRoleSnapshot;
pub use self::client::HttpEndpointSnapshot; pub use self::client::HttpEndpointSnapshot;
/// Error code used when no logical endpoint can satisfy a request. /// Error code used when no logical endpoint can satisfy a request.
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED; 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. /// Error code used when an HTTP connection cannot be established.
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED; pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
/// Error code used when an HTTP request fails after connection establishment. /// 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; pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR;
/// Error code used when a WebSocket session is no longer available. /// Error code used when a WebSocket session is no longer available.
pub use self::error::ERROR_CODE_WS_SESSION_CLOSED; 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. /// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
pub use self::json_rpc::JsonRpcErrorObject; pub use self::json_rpc::JsonRpcErrorObject;
/// Validated JSON-RPC 2.0 error response. /// Validated JSON-RPC 2.0 error response.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs // 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. //! 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>(); 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"); 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");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs // 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. //! 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"); 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")); 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>();
}

View 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());
}

View 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());
}

259
deltas/0.2.9/pre.002.md Normal file
View File

@@ -0,0 +1,259 @@
<!-- file: deltas/0.2.9/pre.002.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.002` — moteur Yellowstone : dépendances + settings/errors + channel minimal
## 1. Objet
Cette tranche matérialise uniquement la première fondation **N1 moteur Yellowstone gRPC** validée par `pre.001` :
```text
proto Yellowstone publié
+ dépendances Tonic minimales
+ settings runtime bornés/redacted
+ error code initial gRPC
+ channel Tonic lazy privé
```
Elle ne commence pas encore N2 fonctionnel : aucun unary Yellowstone, aucune metadata/auth, aucune fixture serveur gRPC, aucun `Subscribe`, aucun lifecycle bidi et aucune intégration PublicNode ne sont introduits ici.
Version workspace :
```text
0.2.9-pre.2
```
Commit attendu après validation opérateur :
```text
v0.2.9-pre.002
```
## 2. Dépendances matérialisées
Stratégie B conservée, mais avec le feature set le plus étroit utile à cette tranche :
```text
workspace root:
tonic = ^0.14, default-features = false
yellowstone-grpc-proto = ^12.6, default-features = false
ksp-onchain-transport-lib:
tonic workspace = true, features = ["channel"]
yellowstone-grpc-proto workspace = true, aucune feature locale
```
Explicitement absents :
```text
yellowstone-grpc-client
tonic-prost direct KSP
prost direct KSP
prost-types direct KSP
tokio-stream nouveau
feature tonic du proto
feature tonic-compression du proto
TLS Tonic
```
La crate publiée `yellowstone-grpc-proto` fournit les messages Protobuf générés utiles sans que KSP copie les `.proto`. Son build utilise son outillage `protoc` vendored ; aucun `protoc` système ni `build.rs` KSP n'est ajouté.
Le feature optionnel `tonic` de la crate proto n'est pas activé en `pre.002` : le moteur ne consomme encore aucun client RPC généré. `pre.003` devra justifier explicitement toute feature/dependency supplémentaire nécessaire aux unary, TLS et metadata.
## 3. Settings Yellowstone N1
Ajout de `grpc_settings.rs` avec les contrats publics crate-root :
```text
YellowstoneGrpcEndpointUrl
YellowstoneGrpcProviderName
YellowstoneGrpcClusterName
YellowstoneGrpcReconnectSettings
YellowstoneGrpcSessionSettings
YellowstoneGrpcEndpointSettings
YellowstoneGrpcTransportSettings
```
Principes :
```text
Transport-owned
aucun Config/env
aucun WsProtocolKind/WsEndpointSettings
URL http/https distincte de HTTP/WS et Debug redacted
provider/cluster = descripteurs ouverts, pas des enums commerciaux fermés
URL/descripteurs/nombre dendpoints bornés
connect/unary/close timeouts bornés
reconnect attempts/backoff bornés
request/update channel capacities bornées
max inbound/outbound message sizes bornées
au moins un endpoint configuré et enabled
endpoint names uniques
```
Les defaults servent uniquement de runtime defaults N1 ; ils ne deviennent pas des quotas provider.
## 4. Channel minimal
Ajout de `YellowstoneGrpcChannel` dans `grpc_channel.rs`.
`prepare()` :
```text
valide les settings avant I/O
refuse un endpoint disabled
construit tonic::transport::Endpoint depuis l'URL sensible
applique connect timeout / unary timeout / request buffer
utilise connect_lazy()
ne réalise aucune connexion réseau
ne configure encore ni TLS ni metadata
ne recopie pas les erreurs URI Tonic arbitraires dans KspError
```
Le `tonic::transport::Channel` réel est un champ privé et n'a aucun getter public. `Debug` n'expose ni URL, ni channel brut, ni credential potentiel.
Cette surface ne constitue pas encore `YellowstoneGrpcSession` : elle prépare seulement la ressource physique minimale qui sera utilisée par `pre.003+`.
## 5. Erreurs et observabilité
Nouveau code :
```text
onchain_transport/grpc_channel_failed
```
Les erreurs de préparation conservent uniquement des contexts allowlistés sûrs (`endpoint_name`) et n'injectent pas le message d'erreur Tonic/URI dans le contrat KSP.
Les logs passent exclusivement par `ksp-logging-lib` et n'émettent que :
```text
endpoint_name
provider
cluster
bornes numériques non sensibles
```
Aucune URL n'est loggée.
## 6. Canaries ajoutés
Unit tests Transport :
```text
gRPC URL http/https + Debug redacted
gRPC URL non-http ou surdimensionnée rejetée sans secret echo
descripteurs et endpoint inventory plafonnés
defaults session bornés
zero/excessive/reversed runtime settings rejetés
endpoint inventory unique/enabled
channel lazy préparé sans réseau
channel Debug sans URL/secret
endpoint disabled rejeté avant I/O
raw proto disponible uniquement côté interne/test
```
Public API :
```text
settings N1 constructibles depuis crate root
YellowstoneGrpcChannel constructible sans Config
nouvel error code public KSP
aucun raw client Tonic requis par le consumer
```
Release completeness / workspace dependency canaries :
```text
dépendances root/member exactes
yellowstone-grpc-client absent
Transport -X-> Config
aucun couplage Ws*
aucun pub use tonic/yellowstone_grpc_proto
channel = from_shared + connect_lazy
TLS/metadata explicitement absents de pre.002
```
## 7. Documentation
Les versions opérateur alignées de :
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
sont conservées comme base de mise en forme et avancées pour enregistrer :
```text
feature set réellement matérialisé
séparation stricte pre.002 / pre.003
surface N1 réellement ouverte
état des canaries
Cargo gates encore opérateur
```
Le forecast reste inchangé en nombre de tranches : `pre.003` porte toujours TLS/metadata + fixture locale + 7 unary.
## 8. Validation exécutée dans l'environnement de préparation
Exécuté réellement :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
L'environnement de préparation ne possède pas Cargo/Rust. Les commandes Cargo ci-dessous ne sont donc **pas déclarées réussies**.
## 9. Gate opérateur requis avant commit
Après application du delta :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Inspecter particulièrement :
```text
yellowstone-grpc-proto 12.6.x réellement résolu
tonic 0.14.x réellement résolu
prost / prost-types / tonic-prost transitifs
bytes / http / hyper / hyper-util / tower
rustls / tokio-rustls
solana-pubkey unifié ou doublons justifiés
aucune arrivée de yellowstone-grpc-client
features Tonic réellement activées
```
Si la compilation démontre que `yellowstone-grpc-proto` sans feature `tonic` ou Tonic `channel` ne suffit pas à la tranche actuelle, ne pas élargir silencieusement : produire un `pre.002-fix.001` avec la dépendance minimale démontrée.
## 10. Fichiers ajoutés/modifiés
```text
Cargo.toml
crates/ksp-core-lib/tests/workspace_dependencies.rs
crates/ksp-onchain-transport-lib/Cargo.toml
crates/ksp-onchain-transport-lib/src/error.rs
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.002.md
```

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md --> <!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode # Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
> **Statut : `0.2.9-pre.001-fix.002` — gate audit/sizing corrigé. `0.2.9` est bornée à un moteur client Yellowstone partagé, une façade Solana Yellowstone standard et une première intégration concrète PublicNode. Seuls OrbitFlare puis Helius LaserStream gRPC sont actuellement planifiés comme releases provider suivantes ; les autres providers restent en TODO/IDEAS sans numéro réservé. Chaque prerelease vise 1520 minutes de travail effectif et la release complète doit rester clôturable dans une seule session de chat.** > **Statut : `0.2.9-pre.002` — première matérialisation N1 préparée : dépendances Yellowstone/Tonic minimales, settings runtime bornés, error mapping initial et channel Tonic lazy privé. Les gates Cargo restent à exécuter par lopérateur avant commit. `0.2.9` reste bornée à un moteur client Yellowstone partagé, une façade Solana Yellowstone standard et une première intégration concrète PublicNode. Seuls OrbitFlare puis Helius LaserStream gRPC sont actuellement planifiés comme releases provider suivantes ; les autres providers restent en TODO/IDEAS sans numéro réservé. Chaque prerelease vise 1520 minutes de travail effectif et la release complète doit rester clôturable dans une seule session de chat.**
## 1. Objet, base et état d'ouverture ## 1. Objet, base et état d'ouverture
@@ -298,14 +298,20 @@ https://docs.rs/crate/yellowstone-grpc-proto/latest
KSP suit la **Rust stable courante de l'opérateur** et ne documente pas de numéro Rust upstream comme objectif de projet. Le seul gate utile est opérationnel : les dépendances finalement retenues doivent compiler avec la stable courante utilisée par le workspace. KSP suit la **Rust stable courante de l'opérateur** et ne documente pas de numéro Rust upstream comme objectif de projet. Le seul gate utile est opérationnel : les dépendances finalement retenues doivent compiler avec la stable courante utilisée par le workspace.
Éléments build à vérifier en `pre.002` : Résultat de matérialisation `pre.002` :
```text ```text
proto crate crate publiée/pré-générée ; pas de vendoring de .proto KSP yellowstone-grpc-proto ^12.6 workspace dependency, default-features = false, aucune feature KSP activée
client KSP autour de Tonic features client/TLS minimales tonic ^0.14 workspace dependency, default-features = false ; Transport active seulement `channel`
protoc/build.rs aucun outil système ajouté sauf besoin démontré yellowstone-grpc-client absent
prost/prost-types aucune dépendance KSP directe
tonic-prost aucune dépendance KSP directe à ce stade
proto crate publiée/générée ; aucun .proto copié dans KSP
protoc aucun outil système KSP ajouté ; la crate proto publiée utilise son build vendored
``` ```
Le feature `tonic` optionnel de `yellowstone-grpc-proto` n'est **pas** activé en `pre.002`. KSP n'a besoin à ce stade que des messages Protobuf générés bruts ; le channel physique est construit directement avec Tonic. Cette décision évite de tirer prématurément la surface client/serveur Tonic générée et laisse `pre.003` choisir explicitement les features supplémentaires réellement nécessaires aux unary/TLS/metadata.
Un minimum Rust déclaré par une dépendance n'est enregistré que s'il devient un **blocage réel** lors de la compilation ; il n'est pas suivi comme métrique de release. Un minimum Rust déclaré par une dépendance n'est enregistré que s'il devient un **blocage réel** lors de la compilation ; il n'est pas suivi comme métrique de release.
## 6. Matrice du service `Geyser` courant ## 6. Matrice du service `Geyser` courant
@@ -529,16 +535,19 @@ Avantages :
**Décision : stratégie cible retenue.** **Décision : stratégie cible retenue.**
Matérialisation attendue en `pre.002` : Matérialisation réalisée en `pre.002` :
```text ```text
yellowstone-grpc-proto ^12.6 workspace dependency, features minimales yellowstone-grpc-proto ^12.6 root : default-features = false ; Transport : .workspace = true sans feature
tonic ^0.14 workspace dependency, features client/TLS minimales tonic ^0.14 root : default-features = false ; Transport : features = ["channel"]
prost/prost-types pas de direct dependency KSP sauf besoin démontré yellowstone-grpc-client absent
tokio-stream/futures seulement si nécessaire et sans doublon gratuit prost/prost-types pas de dépendance KSP directe
tonic-prost pas de dépendance KSP directe en pre.002
tokio-stream pas ajouté
futures-util dépendance existante inchangée
``` ```
L'exacte feature set sera déterminée par compilation et `cargo tree`, pas par anticipation documentaire. Le choix est volontairement plus étroit que le forecast initial : `pre.002` matérialise les messages wire publiés et le channel HTTP/2 lazy, mais **pas encore le client RPC généré, TLS, metadata ou compression**. Les éventuelles features/dependencies supplémentaires doivent être justifiées par `pre.003` et vérifiées par `cargo tree`; elles ne sont pas activées par anticipation.
### C. proto/génération KSP minimale bornée ### C. proto/génération KSP minimale bornée
@@ -582,11 +591,17 @@ pas de façade provider vide uniquement pour renommer le même protocole
Noms cibles, affinables sans casser le principe : Noms cibles, affinables sans casser le principe :
```text ```text
# N1 — moteur Yellowstone # N1 — moteur Yellowstone matérialisé en pre.002
YellowstoneGrpcEndpointUrl YellowstoneGrpcEndpointUrl
YellowstoneGrpcProviderName
YellowstoneGrpcClusterName
YellowstoneGrpcReconnectSettings
YellowstoneGrpcEndpointSettings YellowstoneGrpcEndpointSettings
YellowstoneGrpcSessionSettings YellowstoneGrpcSessionSettings
YellowstoneGrpcTransportSettings YellowstoneGrpcTransportSettings
YellowstoneGrpcChannel # channel Tonic lazy privé, sans I/O réseau en pre.002
# N1 — étapes ultérieures
YellowstoneGrpcSession YellowstoneGrpcSession
YellowstoneSubscriptionHandle<T> YellowstoneSubscriptionHandle<T>
@@ -880,8 +895,8 @@ Prévision courante :
pre.001 DONE — audit upstream/service/proto + providers gratuits + licences/deps + architecture + threat model + sizing pre.001 DONE — audit upstream/service/proto + providers gratuits + licences/deps + architecture + threat model + sizing
budget : 1520 min nominal ; preuve : plan + matrice + stratégie B + forecast recalibré budget : 1520 min nominal ; preuve : plan + matrice + stratégie B + forecast recalibré
pre.002 moteur Yellowstone : proto/dependencies + settings/errors + channel minimal pre.002 IMPLEMENTED — moteur Yellowstone : proto/dependencies + settings/errors + channel minimal
budget : 1520 min ; preuve : features minimales + compile/tests + redaction + cargo tree budget : 1520 min ; preuve locale : audit statique KSP clean + redaction/API canaries ajoutés ; compile/tests/cargo tree = gate opérateur
pre.003 moteur Yellowstone : TLS/metadata générique + fixture locale + 7 unary RPCs pre.003 moteur Yellowstone : TLS/metadata générique + fixture locale + 7 unary RPCs
budget : 1520 min ; preuve : connect/TLS/timeouts/Status safe + wire unary exact budget : 1520 min ; preuve : connect/TLS/timeouts/Status safe + wire unary exact
@@ -936,10 +951,16 @@ et le reste est replanifié explicitement ; aucune capacité n'est abandonnée s
## 18. Fichiers attendus par tranche ## 18. Fichiers attendus par tranche
Cibles probables, sans imposer artificiellement le découpage source : Cibles réelles ouvertes par `pre.002`, puis cibles probables suivantes :
```text ```text
# pre.002
crates/ksp-onchain-transport-lib/src/grpc_settings.rs crates/ksp-onchain-transport-lib/src/grpc_settings.rs
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
# pre.003+
crates/ksp-onchain-transport-lib/src/grpc_session.rs crates/ksp-onchain-transport-lib/src/grpc_session.rs
crates/ksp-onchain-transport-lib/src/grpc_protocol.rs crates/ksp-onchain-transport-lib/src/grpc_protocol.rs
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs

View File

@@ -1,9 +1,9 @@
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md --> <!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode # Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode
> **Statut : matrice ouverte par `0.2.9-pre.001` et corrigée par `pre.001-fix.002`. Le scope distingue moteur Yellowstone, façade Solana standard et première intégration PublicNode. OrbitFlare et Helius sont les seules releases provider suivantes actuellement planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.** > **Statut : matrice ouverte par `0.2.9-pre.001`, corrigée par `pre.001-fix.002` et avancée par `pre.002`. N1 possède désormais une première matérialisation settings/error/channel lazy ; la validation Cargo de cette tranche reste un gate opérateur. Le scope distingue moteur Yellowstone, façade Solana standard et première intégration PublicNode. OrbitFlare et Helius sont les seules releases provider suivantes actuellement planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.**
## 1. Autorités du gate ## 1. Autorités du gate
@@ -207,17 +207,21 @@ Preuve cible : fixtures issues du wire Protobuf + cas old/current + malformed/ov
## 8. Dépendances / licence ## 8. Dépendances / licence
| Gate | Décision `pre.001` | Preuve suivante | | Gate | Décision / matérialisation `pre.002` | Preuve / état |
|-----------------------------|--------------------------------|-------------------------| |--------------------------------|---------------------------------------------------------------|---------------------------------|
| repository default | AGPL-3.0-only | `LICENSING.md` | | repository default | AGPL-3.0-only | `LICENSING.md` |
| client subtree | Apache-2.0 | `LICENSING.md` | | client subtree | Apache-2.0 | `LICENSING.md` |
| proto subtree | Apache-2.0 | `LICENSING.md` | | proto subtree | Apache-2.0 | `LICENSING.md` |
| strategy A client+proto | rejetée comme default | documenté | | strategy A client+proto | rejetée comme default | documenté |
| strategy B proto+Tonic KSP | **retenue** | `pre.002` compile/tree | | strategy B proto+Tonic KSP | **retenue et matérialisée** | source PASS / Cargo pending |
| strategy C vendored proto | fallback seulement | aucun vendoring pre.001 | | `yellowstone-grpc-proto` | `^12.6`, `default-features = false`, aucune feature KSP | Cargo tree pending |
| system protoc | non attendu avec crate publiée | vérifier build réel | | `tonic` | `^0.14`, `default-features = false`, feature `channel` locale | Cargo tree pending |
| raw upstream types public | interdit par défaut | public API canary | | `yellowstone-grpc-client` | absent | manifest canary ajouté |
| executable Yellowstone deps | interdit | dependency firewall | | `prost/prost-types` direct KSP | absent | manifest / Cargo tree pending |
| strategy C vendored proto | fallback seulement | aucun vendoring |
| system protoc | aucun outil système KSP ajouté | proto publié/build vendored |
| raw upstream types public | interdit ; aucun reexport Tonic/Yellowstone | public API canary ajouté |
| executable Yellowstone deps | interdit | dependency firewall à revalider |
Versions observées : Versions observées :
@@ -230,52 +234,53 @@ prost/prost-types 0.14.x (0.14.4 latest observé)
## 9. Architecture / ownership ## 9. Architecture / ownership
| Invariant | Preuve cible | État | | Invariant | Preuve cible | État `pre.002` |
|--------------------------------------------------------------------------------|--------------------------------|------| |--------------------------------------------------------------------------------|--------------------------------|---------------------------------|
| backend gRPC distinct HTTP/WS | source/API canary | TODO | | backend gRPC distinct HTTP/WS | source/API canary | IMPLEMENTED / Cargo pending |
| aucun `WsProtocolKind` gRPC | source scan | TODO | | aucun `WsProtocolKind` gRPC | source scan | SOURCE PASS |
| Transport owns gRPC | dependency graph | TODO | | Transport owns gRPC | dependency graph | IMPLEMENTED / tree pending |
| Config -> Transport seulement | ownership test | TODO | | Config -> Transport seulement | ownership test | hérité / final TODO |
| Transport -X-> Config | ownership test | TODO | | Transport -X-> Config | ownership test | SOURCE PASS / Cargo pending |
| Transport -X-> std::env KSP_* | ownership test | TODO | | Transport -X-> std::env KSP_* | ownership test | SOURCE PASS |
| Logging façade KSP only | logging ownership | TODO | | Logging façade KSP only | logging ownership | SOURCE PASS |
| no raw Tonic client escape hatch | public API canary | TODO | | no raw Tonic client escape hatch | public API canary | IMPLEMENTED / Cargo pending |
| moteur Yellowstone partagé sans duplication provider | source/API canary | TODO | | moteur Yellowstone partagé sans duplication provider | source/API canary | N1 FOUNDATION IMPLEMENTED |
| façade Solana Yellowstone standard distincte du moteur | public API canary | TODO | | façade Solana Yellowstone standard distincte du moteur | public API canary | TODO `pre.003+` |
| PublicNode représenté comme provider/capabilities, pas comme nouveau protocole | public API/config canary | TODO | | PublicNode représenté comme provider/capabilities, pas comme nouveau protocole | public API/config canary | TODO `pre.010/011` |
| provider peut réutiliser, restreindre ou étendre N2 sans dupliquer N1 | capability/completeness review | TODO | | provider peut réutiliser, restreindre ou étendre N2 sans dupliquer N1 | capability/completeness review | architecture conservée |
| aucune équivalence provider/standard présumée sans preuve | provider matrix/tests | TODO | | aucune équivalence provider/standard présumée sans preuve | provider matrix/tests | architecture conservée |
| façade provider spécialisée seulement si delta réel | completeness review | TODO | | façade provider spécialisée seulement si delta réel | completeness review | TODO provider integration |
## 10. Settings, bounds et redaction ## 10. Settings, bounds et redaction
À fermer par tests : État après `pre.002` :
```text ```text
endpoint schemes TLS/non-TLS explicitement validés DONE/source+tests ajoutés endpoint schemes http/https validés ; TLS réel reporté pre.003
connect timeout > 0 DONE/source+tests ajoutés endpoint URL, descripteurs et nombre dendpoints plafonnés
unary timeout > 0 DONE/source+tests ajoutés connect timeout > 0 et plafonné
close timeout > 0 DONE/source+tests ajoutés unary timeout > 0 et plafonné
max inbound/outbound > 0 et plafonnés DONE/source+tests ajoutés close timeout > 0 et plafonné
request/update queue capacities > 0 et plafonnées DONE/source+tests ajoutés max inbound/outbound > 0 et plafonnés
reconnect attempt/backoff bornés DONE/source+tests ajoutés request/update queue capacities > 0 et plafones
filter-group count DONE/source+tests ajoutés reconnect attempt/backoff bornés
filter-name length/uniqueness TODO pre.004 filter-group count
metadata key/value count/size TODO pre.004 filter-name length/uniqueness
account/owner/include/exclude/required counts TODO pre.003 metadata key/value count/size
memcmp/data slice bounds TODO pre.005+ account/owner/include/exclude/required counts
Cuckoo dimensions/data bounds TODO pre.004+ memcmp/data slice bounds
TODO pre.005+ Cuckoo dimensions/data bounds
``` ```
Security canaries : Security canaries :
```text ```text
endpoint URL Debug redacted DONE/source+tests ajoutés endpoint URL Debug redacted
secret metadata Debug/Display absent TODO pre.003 secret metadata Debug/Display absent
Status message/details distants non recopiés aveuglément en context TODO pre.003 Status message/details distants non recopiés aveuglément en context
TLS/connect error sans credential/URI sensible PARTIAL pre.002 URI Tonic invalide mappée sans recopier l'erreur ; TLS/connect réel pre.003
request/update Debug sans payload arbitraire sensible TODO pre.004+ request/update Debug sans payload arbitraire sensible
snapshot sans endpoint secret ni remote internals inutiles PARTIAL pre.002 channel Debug ne contient ni URL ni raw Tonic ; snapshot lifecycle ultérieur
``` ```
## 11. Lifecycle / backpressure / replay ## 11. Lifecycle / backpressure / replay
@@ -397,7 +402,7 @@ Les autres providers restent en TODO/IDEAS sans release dédiée. Aucune façade
```text ```text
pre.001 DONE audit/sizing/architecture 1520 min nominal pre.001 DONE audit/sizing/architecture 1520 min nominal
pre.002 TODO moteur: deps/settings/errors/channel 1520 min pre.002 IMPLEMENTED / CARGO GATE moteur: deps/settings/errors/channel 1520 min
pre.003 TODO moteur: TLS/metadata + fixture + 7 unary 1520 min pre.003 TODO moteur: TLS/metadata + fixture + 7 unary 1520 min
pre.004 TODO standard: Subscribe common/from_slot/bounds 1520 min pre.004 TODO standard: Subscribe common/from_slot/bounds 1520 min
pre.005 TODO standard: accounts + slots 1520 min pre.005 TODO standard: accounts + slots 1520 min
@@ -440,3 +445,26 @@ Gate de sizing : une prerelease estimée à plus de 20 minutes est scindée ; si
| aucune implémentation lourde prématurée | PASS | | aucune implémentation lourde prématurée | PASS |
**Verdict `pre.001` : gate positif ; `pre.002` peut matérialiser la stratégie B sous validation Cargo.** **Verdict `pre.001` : gate positif ; `pre.002` peut matérialiser la stratégie B sous validation Cargo.**
## 17. Gate `pre.002`
| Critère de tranche | Verdict local |
|---------------------------------------------------------|------------------------------------------------|
| workspace version `0.2.9-pre.2` | PASS source |
| stratégie B matérialisée sans `yellowstone-grpc-client` | PASS source |
| proto `12.6` sans default/features KSP | PASS manifest |
| Tonic `0.14` limité à `channel` | PASS manifest |
| aucun Config/env dans Transport gRPC | PASS source/audit |
| aucun couplage WebSocket | PASS source |
| settings runtime bornés | PASS source/tests ajoutés |
| URL/channel diagnostics redacted | PASS source/tests ajoutés |
| channel Tonic lazy privé | PASS source/API canary ajouté |
| raw Tonic/Yellowstone absent de l'API publique | PASS source/API canary ajouté |
| audit `scripts/audit_rust_workspace_rules.py` | PASS / clean |
| `cargo fmt --all` | PENDING opérateur |
| `cargo check --workspace` | PENDING opérateur |
| `cargo clippy --workspace --all-targets` | PENDING opérateur |
| tests Transport/Core/workspace | PENDING opérateur |
| `cargo tree` direct/duplicates | PENDING opérateur |
**Verdict `pre.002` local : implémentation candidate prête pour validation opérateur. La tranche n'est déclarée fermée qu'après les gates Cargo réels.**