v0.2.9-pre.009

This commit is contained in:
2026-08-24 17:26:27 +02:00
parent 433e69272a
commit a9fb6a7ac8
12 changed files with 1498 additions and 101 deletions

View File

@@ -1,10 +1,14 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 6
// version: 7
/// 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 bounded Yellowstone gRPC request/update queue is exhausted.
pub const ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_backpressure_overflow");
/// 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 subscribe session is no longer available to the caller.
pub const ERROR_CODE_GRPC_SESSION_CLOSED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_session_closed");
/// 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.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs
// version: 3
// version: 4
/// Prepared or connected Yellowstone gRPC channel owned by KSP Transport.
///
@@ -117,6 +117,23 @@ impl YellowstoneGrpcChannel {
return crate::SolanaYellowstoneGrpcUnaryClient::new(self.channel.clone(), self.metadata.clone(), self.session.clone());
}
/// Opens one standard Solana Yellowstone bidirectional `Subscribe` session over this physical channel.
pub async fn open_standard_subscribe(
&self,
initial_request: crate::YellowstoneSubscribeRequest,
) -> ksp_core_lib::Result<crate::SolanaYellowstoneGrpcSubscribeSession> {
return crate::open_yellowstone_subscribe_session(
self.channel.clone(),
self.metadata.clone(),
self.session.clone(),
self.endpoint_name.clone(),
self.provider.clone(),
self.cluster.clone(),
initial_request,
)
.await;
}
fn from_parts(settings: &crate::YellowstoneGrpcEndpointSettings, channel: tonic::transport::Channel) -> Self {
return Self {
endpoint_name: settings.name().to_owned(),

View File

@@ -0,0 +1,584 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_stream.rs
// version: 1
use tonic_prost::prost::Message; // rust-rules: trait-import
const AUTO_SUBSCRIBE_PING_ID: i32 = 1;
const PATH_SUBSCRIBE: &str = "/geyser.Geyser/Subscribe";
/// Safe lifecycle state of one standard Yellowstone bidirectional subscribe session.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum YellowstoneGrpcSubscribeState {
/// The bidirectional stream is active and accepts request mutations.
Active,
/// KSP has started a bounded graceful half-close.
Closing,
/// The stream ended normally, including a server half-close.
Closed,
/// The stream terminated because a transport, protocol, decoding or backpressure error occurred.
Failed,
}
/// Standard Solana Yellowstone bidirectional `Subscribe` session layered on one KSP-owned physical gRPC channel.
///
/// Request mutations and decoded updates use bounded Tokio channels. The raw Tonic stream and upstream protobuf messages remain private to Transport.
pub struct SolanaYellowstoneGrpcSubscribeSession {
endpoint_name: std::string::String,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
request_tx: std::option::Option<tokio::sync::mpsc::Sender<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
update_rx: tokio::sync::mpsc::Receiver<ksp_core_lib::Result<crate::YellowstoneSubscribeUpdate>>,
shutdown_tx: tokio::sync::watch::Sender<std::option::Option<tokio::time::Instant>>,
state_rx: tokio::sync::watch::Receiver<SubscribeActorState>,
task: tokio::task::JoinHandle<()>,
close_timeout: std::time::Duration,
max_outbound_message_size_bytes: usize,
}
impl SolanaYellowstoneGrpcSubscribeSession {
/// Returns the safe logical endpoint name.
#[must_use]
pub fn endpoint_name(&self) -> &str {
return self.endpoint_name.as_str();
}
/// Returns the open provider descriptor without exposing endpoint credentials.
#[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 current safe stream lifecycle state.
#[must_use]
pub fn state(&self) -> crate::YellowstoneGrpcSubscribeState {
return self.state_rx.borrow().public_state();
}
/// Queues one complete standard Yellowstone request mutation without waiting for network dispatch.
///
/// The mutation is rejected synchronously when local validation fails, the encoded request exceeds the configured outbound bound, the bounded request
/// queue is full, or the session is no longer active. Accepted mutations preserve bounded-channel admission order.
pub fn try_update(&self, request: &crate::YellowstoneSubscribeRequest) -> ksp_core_lib::Result<()> {
if self.state() != crate::YellowstoneGrpcSubscribeState::Active {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe session is not active",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
));
}
let wire = match crate::yellowstone_subscribe_request_to_wire(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if wire.encoded_len() > self.max_outbound_message_size_bytes {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW,
"Yellowstone subscribe request exceeds the configured outbound message bound",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
));
}
let sender = match self.request_tx.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe request channel is closed",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
));
},
};
return match sender.try_send(wire) {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW,
"Yellowstone subscribe request queue is full",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe request queue is closed",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
};
}
/// Receives the next decoded standard Yellowstone update.
///
/// A normal server half-close returns `Ok(None)`. Remote statuses, malformed updates and overflow terminate the session and surface a safe KSP error.
pub async fn next_update(&mut self) -> ksp_core_lib::Result<std::option::Option<crate::YellowstoneSubscribeUpdate>> {
return match self.update_rx.recv().await {
std::option::Option::Some(std::result::Result::Ok(update)) => std::result::Result::Ok(std::option::Option::Some(update)),
std::option::Option::Some(std::result::Result::Err(error)) => {
self.request_tx.take();
std::result::Result::Err(error)
},
std::option::Option::None => {
self.request_tx.take();
match *self.state_rx.borrow() {
SubscribeActorState::Closed => std::result::Result::Ok(std::option::Option::None),
SubscribeActorState::Failed(code) => std::result::Result::Err(subscribe_session_error(
code,
terminal_message(code),
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
SubscribeActorState::Active | SubscribeActorState::Closing => std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe update channel ended before a terminal state was published",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
}
},
};
}
/// Gracefully half-closes the client request side and waits up to the configured close timeout for the server side to finish.
pub async fn close(mut self) -> ksp_core_lib::Result<()> {
let terminal_before_close = *self.state_rx.borrow();
self.request_tx.take();
if terminal_before_close == SubscribeActorState::Active {
let deadline = tokio::time::Instant::now() + self.close_timeout;
self.shutdown_tx.send_replace(std::option::Option::Some(deadline));
}
let deadline = tokio::time::Instant::now() + self.close_timeout;
match tokio::time::timeout_at(deadline, &mut self.task).await {
std::result::Result::Ok(join_result) => {
if join_result.is_err() {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe actor terminated unexpectedly",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
));
}
},
std::result::Result::Err(_) => {
self.task.abort();
let _ = (&mut self.task).await;
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_TIMEOUT,
"Yellowstone subscribe graceful shutdown exceeded the configured close timeout",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
));
},
}
return match *self.state_rx.borrow() {
SubscribeActorState::Closed => std::result::Result::Ok(()),
SubscribeActorState::Failed(code) => std::result::Result::Err(subscribe_session_error(
code,
terminal_message(code),
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
SubscribeActorState::Active | SubscribeActorState::Closing => std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_SESSION_CLOSED,
"Yellowstone subscribe actor ended without a terminal lifecycle state",
self.endpoint_name.as_str(),
self.provider.as_str(),
self.cluster.as_str(),
)),
};
}
}
impl std::fmt::Debug for SolanaYellowstoneGrpcSubscribeSession {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("SolanaYellowstoneGrpcSubscribeSession")
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("cluster", &self.cluster)
.field("state", &self.state())
.field("request_queue_capacity", &self.request_tx.as_ref().map(|sender| return sender.capacity()))
.field("update_queue_capacity", &self.update_rx.capacity())
.finish();
}
}
impl Drop for SolanaYellowstoneGrpcSubscribeSession {
fn drop(&mut self) {
self.request_tx.take();
if self.state() == crate::YellowstoneGrpcSubscribeState::Active {
self.shutdown_tx.send_replace(std::option::Option::Some(tokio::time::Instant::now() + self.close_timeout));
}
}
}
/// Opens one standard Yellowstone `Subscribe` stream from a validated KSP channel.
pub(crate) async fn open_yellowstone_subscribe_session(
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
settings: crate::YellowstoneGrpcSessionSettings,
endpoint_name: std::string::String,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
initial_request: crate::YellowstoneSubscribeRequest,
) -> ksp_core_lib::Result<crate::SolanaYellowstoneGrpcSubscribeSession> {
let initial_wire = match crate::yellowstone_subscribe_request_to_wire(&initial_request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if initial_wire.encoded_len() > settings.max_outbound_message_size_bytes() {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW,
"initial Yellowstone subscribe request exceeds the configured outbound message bound",
endpoint_name.as_str(),
provider.as_str(),
cluster.as_str(),
));
}
let (request_tx, request_rx) = tokio::sync::mpsc::channel(settings.request_channel_capacity());
if request_tx.try_send(initial_wire).is_err() {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW,
"initial Yellowstone subscribe request cannot enter the bounded request queue",
endpoint_name.as_str(),
provider.as_str(),
cluster.as_str(),
));
}
let path = match PATH_SUBSCRIBE.parse::<http::uri::PathAndQuery>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_CHANNEL_FAILED,
"internal Yellowstone Subscribe method path is invalid",
endpoint_name.as_str(),
provider.as_str(),
cluster.as_str(),
));
},
};
let mut grpc = tonic::client::Grpc::new(channel)
.max_decoding_message_size(settings.max_inbound_message_size_bytes())
.max_encoding_message_size(settings.max_outbound_message_size_bytes());
let open_future = async {
if grpc.ready().await.is_err() {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_GRPC_CHANNEL_FAILED,
"Yellowstone gRPC channel is not ready for Subscribe dispatch",
endpoint_name.as_str(),
provider.as_str(),
cluster.as_str(),
));
}
let mut request = tonic::Request::new(MpscStream::new(request_rx));
for entry in &metadata {
if let std::result::Result::Err(error) = entry.append_to(request.metadata_mut()) {
return std::result::Result::Err(error);
}
}
let response = match grpc
.streaming(
request,
path,
tonic_prost::ProstCodec::<yellowstone_grpc_proto::geyser::SubscribeRequest, yellowstone_grpc_proto::geyser::SubscribeUpdate>::default(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(status) => {
return std::result::Result::Err(stream_status_error("SubscribeOpen", status, endpoint_name.as_str(), provider.as_str(), cluster.as_str()));
},
};
return std::result::Result::Ok(response.into_inner());
};
let incoming = match tokio::time::timeout(settings.connect_timeout(), open_future).await {
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
std::result::Result::Ok(std::result::Result::Err(error)) => return std::result::Result::Err(error),
std::result::Result::Err(_) => {
return std::result::Result::Err(subscribe_session_error(
crate::ERROR_CODE_TIMEOUT,
"Yellowstone Subscribe stream opening exceeded the configured connection timeout",
endpoint_name.as_str(),
provider.as_str(),
cluster.as_str(),
));
},
};
let (update_tx, update_rx) = tokio::sync::mpsc::channel(settings.update_channel_capacity());
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(std::option::Option::None::<tokio::time::Instant>);
let (state_tx, state_rx) = tokio::sync::watch::channel(SubscribeActorState::Active);
let actor_request_tx = request_tx.clone();
let actor_endpoint_name = endpoint_name.clone();
let actor_provider = provider.clone();
let actor_cluster = cluster.clone();
let close_timeout = settings.close_timeout();
let max_outbound_message_size_bytes = settings.max_outbound_message_size_bytes();
let task = tokio::spawn(run_subscribe_actor(
actor_endpoint_name,
actor_provider,
actor_cluster,
incoming,
actor_request_tx,
update_tx,
shutdown_rx,
state_tx,
max_outbound_message_size_bytes,
));
return std::result::Result::Ok(crate::SolanaYellowstoneGrpcSubscribeSession {
endpoint_name,
provider,
cluster,
request_tx: std::option::Option::Some(request_tx),
update_rx,
shutdown_tx,
state_rx,
task,
close_timeout,
max_outbound_message_size_bytes,
});
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SubscribeActorState {
Active,
Closing,
Closed,
Failed(ksp_core_lib::ErrorCode),
}
impl SubscribeActorState {
const fn public_state(self) -> crate::YellowstoneGrpcSubscribeState {
return match self {
Self::Active => crate::YellowstoneGrpcSubscribeState::Active,
Self::Closing => crate::YellowstoneGrpcSubscribeState::Closing,
Self::Closed => crate::YellowstoneGrpcSubscribeState::Closed,
Self::Failed(_) => crate::YellowstoneGrpcSubscribeState::Failed,
};
}
}
struct MpscStream<T> {
receiver: tokio::sync::mpsc::Receiver<T>,
}
impl<T> MpscStream<T> {
const fn new(receiver: tokio::sync::mpsc::Receiver<T>) -> Self {
return Self { receiver };
}
}
impl<T> futures_util::Stream for MpscStream<T> {
type Item = T;
fn poll_next(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<std::option::Option<Self::Item>> {
return self.get_mut().receiver.poll_recv(context);
}
}
#[allow(clippy::too_many_arguments)]
async fn run_subscribe_actor(
endpoint_name: std::string::String,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
mut incoming: tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeUpdate>,
request_tx: tokio::sync::mpsc::Sender<yellowstone_grpc_proto::geyser::SubscribeRequest>,
update_tx: tokio::sync::mpsc::Sender<ksp_core_lib::Result<crate::YellowstoneSubscribeUpdate>>,
mut shutdown_rx: tokio::sync::watch::Receiver<std::option::Option<tokio::time::Instant>>,
state_tx: tokio::sync::watch::Sender<SubscribeActorState>,
max_outbound_message_size_bytes: usize,
) {
let mut request_tx = std::option::Option::Some(request_tx);
loop {
tokio::select! {
shutdown_changed = shutdown_rx.changed() => {
let deadline = shutdown_deadline(&shutdown_rx, shutdown_changed);
state_tx.send_replace(SubscribeActorState::Closing);
request_tx.take();
finish_client_half_close(&endpoint_name, &provider, &cluster, &mut incoming, deadline, &state_tx).await;
return;
}
message = incoming.message() => {
match message {
std::result::Result::Ok(std::option::Option::Some(wire)) => {
let update = match crate::yellowstone_subscribe_update_from_wire(wire) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _ = update_tx.try_send(std::result::Result::Err(error));
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_INVALID_RESPONSE));
request_tx.take();
return;
},
};
if matches!(&update, crate::YellowstoneSubscribeUpdate::Ping(_)) {
let ping_wire = ping_request_wire();
if ping_wire.encoded_len() > max_outbound_message_size_bytes {
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW));
request_tx.take();
return;
}
let sender = match request_tx.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_SESSION_CLOSED));
return;
},
};
if sender.try_send(ping_wire).is_err() {
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW));
request_tx.take();
return;
}
}
match update_tx.try_send(std::result::Result::Ok(update)) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name = endpoint_name.as_str(),
provider = provider.as_str(),
cluster = cluster.as_str(),
"Yellowstone subscribe update queue overflowed"
);
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW));
request_tx.take();
return;
},
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
state_tx.send_replace(SubscribeActorState::Closed);
request_tx.take();
return;
},
}
},
std::result::Result::Ok(std::option::Option::None) => {
state_tx.send_replace(SubscribeActorState::Closed);
request_tx.take();
return;
},
std::result::Result::Err(status) => {
let error = stream_status_error("Subscribe", status, endpoint_name.as_str(), provider.as_str(), cluster.as_str());
let _ = update_tx.try_send(std::result::Result::Err(error));
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_STATUS));
request_tx.take();
return;
},
}
}
}
}
}
async fn finish_client_half_close(
endpoint_name: &str,
provider: &crate::YellowstoneGrpcProviderName,
cluster: &crate::YellowstoneGrpcClusterName,
incoming: &mut tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeUpdate>,
deadline: tokio::time::Instant,
state_tx: &tokio::sync::watch::Sender<SubscribeActorState>,
) {
loop {
let message = tokio::time::timeout_at(deadline, incoming.message()).await;
match message {
std::result::Result::Ok(std::result::Result::Ok(std::option::Option::Some(_))) => {},
std::result::Result::Ok(std::result::Result::Ok(std::option::Option::None)) => {
state_tx.send_replace(SubscribeActorState::Closed);
return;
},
std::result::Result::Ok(std::result::Result::Err(status)) => {
let code = status.code().to_string();
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name,
provider = provider.as_str(),
cluster = cluster.as_str(),
grpc_code = code.as_str(),
"Yellowstone subscribe endpoint returned a status during graceful shutdown"
);
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_GRPC_STATUS));
return;
},
std::result::Result::Err(_) => {
state_tx.send_replace(SubscribeActorState::Failed(crate::ERROR_CODE_TIMEOUT));
return;
},
}
}
}
fn shutdown_deadline(
shutdown_rx: &tokio::sync::watch::Receiver<std::option::Option<tokio::time::Instant>>,
shutdown_changed: std::result::Result<(), tokio::sync::watch::error::RecvError>,
) -> tokio::time::Instant {
return match shutdown_changed {
std::result::Result::Ok(()) => match *shutdown_rx.borrow() {
std::option::Option::Some(deadline) => deadline,
std::option::Option::None => tokio::time::Instant::now(),
},
std::result::Result::Err(_) => tokio::time::Instant::now(),
};
}
fn ping_request_wire() -> yellowstone_grpc_proto::geyser::SubscribeRequest {
return yellowstone_grpc_proto::geyser::SubscribeRequest {
ping: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeRequestPing { id: AUTO_SUBSCRIBE_PING_ID }),
..std::default::Default::default()
};
}
fn stream_status_error(operation: &'static str, status: tonic::Status, endpoint_name: &str, provider: &str, cluster: &str) -> ksp_core_lib::Error {
let code = status.code().to_string();
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name,
provider,
cluster,
grpc_operation = operation,
grpc_code = code.as_str(),
"Yellowstone subscribe endpoint returned a gRPC status"
);
return subscribe_session_error(crate::ERROR_CODE_GRPC_STATUS, "Yellowstone subscribe endpoint returned a gRPC status", endpoint_name, provider, cluster)
.with_context("grpc_operation", operation)
.with_context("grpc_code", code);
}
fn subscribe_session_error(code: ksp_core_lib::ErrorCode, message: &'static str, endpoint_name: &str, provider: &str, cluster: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(code, message)
.with_context("endpoint_name", endpoint_name)
.with_context("provider", provider)
.with_context("cluster", cluster);
}
fn terminal_message(code: ksp_core_lib::ErrorCode) -> &'static str {
if code == crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW {
return "Yellowstone subscribe session terminated because a bounded queue overflowed";
}
if code == crate::ERROR_CODE_GRPC_STATUS {
return "Yellowstone subscribe session terminated after a remote gRPC status";
}
if code == crate::ERROR_CODE_INVALID_RESPONSE {
return "Yellowstone subscribe session terminated after an invalid update";
}
if code == crate::ERROR_CODE_TIMEOUT {
return "Yellowstone subscribe session exceeded a configured deadline";
}
return "Yellowstone subscribe session is closed";
}
#[cfg(test)]
#[path = "../unit_tests/grpc_stream.rs"]
mod tests;

View File

@@ -1,11 +1,8 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
// version: 6
// version: 7
#[cfg(test)]
const MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES: usize = 128;
#[cfg(test)]
const MAX_GRPC_BLOCK_VECTOR_COUNT: usize = 65_536;
#[cfg(test)]
const MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES: usize = 512 * 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT: usize = 256;
const MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT: usize = 50_000;
@@ -16,26 +13,16 @@ const MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT: usize = 1_024;
const MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES: usize = 128;
const MAX_GRPC_SUBSCRIBE_MEMCMP_BYTES: usize = 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_MEMCMP_TEXT_LENGTH_BYTES: usize = 2 * 1024 * 1024;
#[cfg(test)]
const MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES: usize = 16 * 1024;
const MAX_GRPC_SUBSCRIBE_TRANSACTION_SIGNATURE_TEXT_LENGTH_BYTES: usize = 128;
#[cfg(test)]
const MAX_GRPC_SUBSCRIBE_UPDATE_FILTER_COUNT: usize = 1_024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_ERROR_BYTES: usize = 64 * 1024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_INSTRUCTION_DATA_BYTES: usize = 1024 * 1024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_LOG_COUNT: usize = 16_384;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_LOG_LENGTH_BYTES: usize = 64 * 1024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_RETURN_DATA_BYTES: usize = 1024 * 1024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_TEXT_LENGTH_BYTES: usize = 64 * 1024;
#[cfg(test)]
const MAX_GRPC_TRANSACTION_VECTOR_COUNT: usize = 65_536;
#[cfg(test)]
const YELLOWSTONE_HASH_LENGTH_BYTES: usize = 32;
const YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES: usize = 64;
@@ -105,7 +92,6 @@ impl YellowstoneAccountsDataSlice {
return self.length;
}
#[cfg(test)]
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice {
return yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice { offset: self.offset, length: self.length };
}
@@ -130,7 +116,6 @@ impl YellowstoneSubscribePing {
return self.id;
}
#[cfg(test)]
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestPing {
return yellowstone_grpc_proto::geyser::SubscribeRequestPing { id: self.id };
}
@@ -144,7 +129,6 @@ pub enum YellowstoneCuckooHashAlgorithm {
}
impl YellowstoneCuckooHashAlgorithm {
#[cfg(test)]
const fn to_wire(self) -> i32 {
return match self {
Self::SipHash => yellowstone_grpc_proto::geyser::CuckooHashAlgorithm::SipHash as i32,
@@ -222,7 +206,6 @@ impl YellowstoneCuckooFilter {
return self.hash_algorithm;
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::CuckooFilter {
return yellowstone_grpc_proto::geyser::CuckooFilter {
data: self.data.clone(),
@@ -335,7 +318,6 @@ impl YellowstoneAccountMemcmp {
};
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilterMemcmp {
let data = match &self.data {
YellowstoneAccountMemcmpData::Bytes(value) => {
@@ -394,7 +376,6 @@ pub enum YellowstoneAccountFilterPredicate {
}
impl YellowstoneAccountFilterPredicate {
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilter {
let filter = match self {
Self::Memcmp(value) => yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Memcmp(value.to_wire()),
@@ -514,7 +495,6 @@ impl YellowstoneSubscribeAccountFilter {
return self.cuckoo_accounts_filter.as_ref();
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts {
account: self.accounts.iter().map(std::string::ToString::to_string).collect(),
@@ -581,7 +561,6 @@ impl YellowstoneSubscribeSlotFilter {
return self.interslot_updates;
}
#[cfg(test)]
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots {
filter_by_commitment: self.filter_by_commitment,
@@ -872,7 +851,6 @@ pub enum YellowstoneTokenAccountExpansion {
}
impl YellowstoneTokenAccountExpansion {
#[cfg(test)]
const fn to_wire(self) -> i32 {
return match self {
Self::All => yellowstone_grpc_proto::geyser::TokenAccountExpansionControlFlag::All as i32,
@@ -1033,7 +1011,6 @@ impl YellowstoneSubscribeTransactionFilter {
return self.token_accounts;
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions {
vote: self.vote,
@@ -2027,7 +2004,6 @@ impl YellowstoneSubscribeBlockFilter {
return self.cuckoo_account_include.as_ref();
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks {
account_include: self.account_include.iter().map(std::string::ToString::to_string).collect(),
@@ -2424,6 +2400,99 @@ impl std::fmt::Debug for YellowstoneEntryUpdate {
}
}
/// Server-side Yellowstone keepalive ping update.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribePingUpdate {
filters: std::vec::Vec<crate::YellowstoneSubscribeFilterName>,
created_at: std::option::Option<crate::YellowstoneUpdateTimestamp>,
}
impl YellowstoneSubscribePingUpdate {
/// Returns echoed matching filter names in server order.
#[must_use]
pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] {
return self.filters.as_slice();
}
/// Returns the optional server creation timestamp.
#[must_use]
pub const fn created_at(&self) -> std::option::Option<crate::YellowstoneUpdateTimestamp> {
return self.created_at;
}
}
impl std::fmt::Debug for YellowstoneSubscribePingUpdate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribePingUpdate")
.field("filter_count", &self.filters.len())
.field("created_at", &self.created_at)
.finish();
}
}
/// Server-side Yellowstone keepalive pong update.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribePongUpdate {
filters: std::vec::Vec<crate::YellowstoneSubscribeFilterName>,
created_at: std::option::Option<crate::YellowstoneUpdateTimestamp>,
id: i32,
}
impl YellowstoneSubscribePongUpdate {
/// Returns echoed matching filter names in server order.
#[must_use]
pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] {
return self.filters.as_slice();
}
/// Returns the optional server creation timestamp.
#[must_use]
pub const fn created_at(&self) -> std::option::Option<crate::YellowstoneUpdateTimestamp> {
return self.created_at;
}
/// Returns the exact ping identifier echoed by the server.
#[must_use]
pub const fn id(&self) -> i32 {
return self.id;
}
}
impl std::fmt::Debug for YellowstoneSubscribePongUpdate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribePongUpdate")
.field("filter_count", &self.filters.len())
.field("created_at", &self.created_at)
.field("id", &self.id)
.finish();
}
}
/// Any standard Yellowstone update delivered by the bidirectional `Subscribe` stream.
#[derive(Clone, Debug, PartialEq)]
pub enum YellowstoneSubscribeUpdate {
/// Account update.
Account(crate::YellowstoneAccountUpdate),
/// Slot lifecycle update.
Slot(crate::YellowstoneSlotUpdate),
/// Full transaction update.
Transaction(crate::YellowstoneTransactionUpdate),
/// Lightweight transaction-status update.
TransactionStatus(crate::YellowstoneTransactionStatusUpdate),
/// Full block update.
Block(crate::YellowstoneBlockUpdate),
/// Server keepalive ping. KSP replies automatically with a ping request.
Ping(crate::YellowstoneSubscribePingUpdate),
/// Server keepalive pong.
Pong(crate::YellowstoneSubscribePongUpdate),
/// Block-metadata update.
BlockMeta(crate::YellowstoneBlockMetaUpdate),
/// Entry update.
Entry(crate::YellowstoneEntryUpdate),
}
/// Named empty filter activating the standard Yellowstone `blocks_meta` family.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeBlocksMetaFilter {
@@ -2437,7 +2506,6 @@ impl YellowstoneSubscribeBlocksMetaFilter {
return Self { _private: () };
}
#[cfg(test)]
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta::default();
}
@@ -2456,7 +2524,6 @@ impl YellowstoneSubscribeEntryFilter {
return Self { _private: () };
}
#[cfg(test)]
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry::default();
}
@@ -2712,7 +2779,6 @@ impl YellowstoneSubscribeRequest {
return std::result::Result::Ok(());
}
#[cfg(test)]
fn to_wire(&self) -> ksp_core_lib::Result<yellowstone_grpc_proto::geyser::SubscribeRequest> {
let validation = self.validate();
if let std::result::Result::Err(error) = validation {
@@ -2770,7 +2836,6 @@ impl YellowstoneSubscribeRequest {
}
}
#[cfg(test)]
fn decode_account_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneAccountUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2790,7 +2855,6 @@ fn decode_account_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate)
return std::result::Result::Ok(crate::YellowstoneAccountUpdate { filters, created_at, account, slot: update.slot, is_startup: update.is_startup });
}
#[cfg(test)]
fn decode_slot_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneSlotUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2823,7 +2887,6 @@ fn decode_slot_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) ->
});
}
#[cfg(test)]
fn decode_transaction_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneTransactionUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2843,7 +2906,6 @@ fn decode_transaction_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpda
return std::result::Result::Ok(crate::YellowstoneTransactionUpdate { filters, created_at, transaction, slot: update.slot });
}
#[cfg(test)]
fn decode_transaction_status_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneTransactionStatusUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2875,7 +2937,30 @@ fn decode_transaction_status_update(wire: yellowstone_grpc_proto::geyser::Subscr
});
}
#[cfg(test)]
fn decode_ping_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneSubscribePingUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match wire.update_oneof {
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Ping(_)) => {},
_ => return invalid_subscribe_response("ping", "Yellowstone update does not contain a ping payload"),
}
return std::result::Result::Ok(crate::YellowstoneSubscribePingUpdate { filters, created_at });
}
fn decode_pong_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneSubscribePongUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pong = match wire.update_oneof {
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Pong(value)) => value,
_ => return invalid_subscribe_response("pong", "Yellowstone update does not contain a pong payload"),
};
return std::result::Result::Ok(crate::YellowstoneSubscribePongUpdate { filters, created_at, id: pong.id });
}
fn decode_block_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneBlockUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2951,7 +3036,6 @@ fn decode_block_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) ->
});
}
#[cfg(test)]
fn decode_block_meta_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneBlockMetaUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -2991,7 +3075,6 @@ fn decode_block_meta_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdat
});
}
#[cfg(test)]
fn decode_entry_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneEntryUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
@@ -3008,7 +3091,6 @@ fn decode_entry_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) ->
return std::result::Result::Ok(crate::YellowstoneEntryUpdate { filters, created_at, entry });
}
#[cfg(test)]
fn decode_block_rewards(wire: yellowstone_grpc_proto::solana::storage::confirmed_block::Rewards) -> ksp_core_lib::Result<crate::YellowstoneBlockRewards> {
if wire.rewards.len() > MAX_GRPC_BLOCK_VECTOR_COUNT {
return invalid_subscribe_response("block.rewards", "Yellowstone block reward count exceeds the KSP bound");
@@ -3024,7 +3106,6 @@ fn decode_block_rewards(wire: yellowstone_grpc_proto::solana::storage::confirmed
return std::result::Result::Ok(crate::YellowstoneBlockRewards { rewards, num_partitions: wire.num_partitions.map(|value| return value.num_partitions) });
}
#[cfg(test)]
fn decode_entry_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateEntry) -> ksp_core_lib::Result<crate::YellowstoneEntryInfo> {
let hash = match decode_entry_hash(wire.hash) {
std::result::Result::Ok(value) => value,
@@ -3040,7 +3121,6 @@ fn decode_entry_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateEntry)
});
}
#[cfg(test)]
fn decode_entry_hash(bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<crate::YellowstoneHashBytes> {
let bytes: [u8; YELLOWSTONE_HASH_LENGTH_BYTES] = match bytes.try_into() {
std::result::Result::Ok(value) => value,
@@ -3049,7 +3129,6 @@ fn decode_entry_hash(bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<crate::Ye
return std::result::Result::Ok(crate::YellowstoneHashBytes { bytes });
}
#[cfg(test)]
fn decode_blockhash_text(field: &'static str, value: std::string::String) -> ksp_core_lib::Result<std::string::String> {
if value.is_empty()
|| value.len() > MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES
@@ -3061,7 +3140,6 @@ fn decode_blockhash_text(field: &'static str, value: std::string::String) -> ksp
return std::result::Result::Ok(value);
}
#[cfg(test)]
fn decode_transaction_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo) -> ksp_core_lib::Result<crate::YellowstoneTransactionInfo> {
let signature = match decode_transaction_signature("transaction.signature", wire.signature) {
std::result::Result::Ok(value) => value,
@@ -3084,7 +3162,6 @@ fn decode_transaction_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate
return std::result::Result::Ok(crate::YellowstoneTransactionInfo { signature, is_vote: wire.is_vote, transaction, meta, index: wire.index });
}
#[cfg(test)]
fn decode_stored_transaction(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction,
) -> ksp_core_lib::Result<crate::YellowstoneStoredTransaction> {
@@ -3109,7 +3186,6 @@ fn decode_stored_transaction(
return std::result::Result::Ok(crate::YellowstoneStoredTransaction { signatures, message });
}
#[cfg(test)]
fn decode_transaction_message(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::Message,
) -> ksp_core_lib::Result<crate::YellowstoneTransactionMessage> {
@@ -3174,7 +3250,6 @@ fn decode_transaction_message(
});
}
#[cfg(test)]
fn decode_compiled_instruction(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction,
) -> ksp_core_lib::Result<crate::YellowstoneCompiledInstruction> {
@@ -3188,7 +3263,6 @@ fn decode_compiled_instruction(
});
}
#[cfg(test)]
fn decode_message_address_table_lookup(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::MessageAddressTableLookup,
) -> ksp_core_lib::Result<crate::YellowstoneMessageAddressTableLookup> {
@@ -3206,7 +3280,6 @@ fn decode_message_address_table_lookup(
});
}
#[cfg(test)]
fn decode_transaction_status_meta(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta,
) -> ksp_core_lib::Result<crate::YellowstoneTransactionStatusMeta> {
@@ -3310,7 +3383,6 @@ fn decode_transaction_status_meta(
});
}
#[cfg(test)]
fn decode_inner_instructions(
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions,
) -> ksp_core_lib::Result<crate::YellowstoneInnerInstructions> {
@@ -3332,7 +3404,6 @@ fn decode_inner_instructions(
return std::result::Result::Ok(crate::YellowstoneInnerInstructions { index: wire.index, instructions });
}
#[cfg(test)]
fn decode_token_balance(wire: yellowstone_grpc_proto::solana::storage::confirmed_block::TokenBalance) -> ksp_core_lib::Result<crate::YellowstoneTokenBalance> {
if wire.mint.len() > MAX_GRPC_TRANSACTION_TEXT_LENGTH_BYTES
|| wire.owner.len() > MAX_GRPC_TRANSACTION_TEXT_LENGTH_BYTES
@@ -3363,7 +3434,6 @@ fn decode_token_balance(wire: yellowstone_grpc_proto::solana::storage::confirmed
});
}
#[cfg(test)]
fn decode_return_data(wire: yellowstone_grpc_proto::solana::storage::confirmed_block::ReturnData) -> ksp_core_lib::Result<crate::YellowstoneReturnData> {
if wire.data.len() > MAX_GRPC_TRANSACTION_RETURN_DATA_BYTES {
return invalid_subscribe_response("transaction.meta.return_data", "Yellowstone transaction return data exceeds the KSP bound");
@@ -3375,7 +3445,6 @@ fn decode_return_data(wire: yellowstone_grpc_proto::solana::storage::confirmed_b
return std::result::Result::Ok(crate::YellowstoneReturnData { program_id, data: wire.data });
}
#[cfg(test)]
fn decode_reward(wire: yellowstone_grpc_proto::solana::storage::confirmed_block::Reward) -> ksp_core_lib::Result<crate::YellowstoneReward> {
if wire.pubkey.len() > MAX_GRPC_TRANSACTION_TEXT_LENGTH_BYTES
|| wire.commission.len() > MAX_GRPC_TRANSACTION_TEXT_LENGTH_BYTES
@@ -3410,7 +3479,6 @@ fn decode_reward(wire: yellowstone_grpc_proto::solana::storage::confirmed_block:
});
}
#[cfg(test)]
fn decode_transaction_error(
field: &'static str,
wire: yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError,
@@ -3421,7 +3489,6 @@ fn decode_transaction_error(
return std::result::Result::Ok(crate::YellowstoneTransactionError { bytes: wire.err });
}
#[cfg(test)]
fn decode_transaction_signature(field: &'static str, bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<crate::YellowstoneTransactionSignature> {
let bytes: [u8; YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES] = match bytes.try_into() {
std::result::Result::Ok(value) => value,
@@ -3430,7 +3497,6 @@ fn decode_transaction_signature(field: &'static str, bytes: std::vec::Vec<u8>) -
return std::result::Result::Ok(crate::YellowstoneTransactionSignature::new(bytes));
}
#[cfg(test)]
fn decode_hash_bytes(field: &'static str, bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<crate::YellowstoneHashBytes> {
let bytes: [u8; YELLOWSTONE_HASH_LENGTH_BYTES] = match bytes.try_into() {
std::result::Result::Ok(value) => value,
@@ -3439,7 +3505,6 @@ fn decode_hash_bytes(field: &'static str, bytes: std::vec::Vec<u8>) -> ksp_core_
return std::result::Result::Ok(crate::YellowstoneHashBytes { bytes });
}
#[cfg(test)]
fn decode_update_envelope(
filters: std::vec::Vec<std::string::String>,
created_at: std::option::Option<yellowstone_grpc_proto::prost_types::Timestamp>,
@@ -3471,7 +3536,6 @@ fn decode_update_envelope(
return std::result::Result::Ok((decoded_filters, decoded_created_at));
}
#[cfg(test)]
fn decode_account_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo) -> ksp_core_lib::Result<crate::YellowstoneAccountInfo> {
if wire.data.len() > MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES {
return invalid_subscribe_response("account.data", "Yellowstone account data exceeds the KSP bound");
@@ -3508,7 +3572,6 @@ fn decode_account_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateAcco
});
}
#[cfg(test)]
fn decode_pubkey_bytes(field: &'static str, bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
let array: [u8; 32] = match bytes.try_into() {
std::result::Result::Ok(value) => value,
@@ -3588,16 +3651,53 @@ fn validate_memcmp_text(value: &str) -> ksp_core_lib::Result<()> {
return std::result::Result::Ok(());
}
/// Converts one validated KSP subscribe request to the internal Yellowstone protobuf wire.
pub(crate) fn yellowstone_subscribe_request_to_wire(
request: &crate::YellowstoneSubscribeRequest,
) -> ksp_core_lib::Result<yellowstone_grpc_proto::geyser::SubscribeRequest> {
return request.to_wire();
}
/// Decodes one internal Yellowstone protobuf update into the complete KSP-owned update enum.
pub(crate) fn yellowstone_subscribe_update_from_wire(
wire: yellowstone_grpc_proto::geyser::SubscribeUpdate,
) -> ksp_core_lib::Result<crate::YellowstoneSubscribeUpdate> {
let kind = match wire.update_oneof.as_ref() {
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(_)) => 1_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(_)) => 2_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(_)) => 3_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(_)) => 4_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(_)) => 5_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Ping(_)) => 6_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Pong(_)) => 7_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::BlockMeta(_)) => 8_u8,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(_)) => 9_u8,
std::option::Option::None => {
return invalid_subscribe_response("update.oneof", "Yellowstone subscribe update is missing its oneof payload");
},
};
return match kind {
1 => decode_account_update(wire).map(crate::YellowstoneSubscribeUpdate::Account),
2 => decode_slot_update(wire).map(crate::YellowstoneSubscribeUpdate::Slot),
3 => decode_transaction_update(wire).map(crate::YellowstoneSubscribeUpdate::Transaction),
4 => decode_transaction_status_update(wire).map(crate::YellowstoneSubscribeUpdate::TransactionStatus),
5 => decode_block_update(wire).map(crate::YellowstoneSubscribeUpdate::Block),
6 => decode_ping_update(wire).map(crate::YellowstoneSubscribeUpdate::Ping),
7 => decode_pong_update(wire).map(crate::YellowstoneSubscribeUpdate::Pong),
8 => decode_block_meta_update(wire).map(crate::YellowstoneSubscribeUpdate::BlockMeta),
9 => decode_entry_update(wire).map(crate::YellowstoneSubscribeUpdate::Entry),
_ => invalid_subscribe_response("update.oneof", "Yellowstone subscribe update kind is unsupported"),
};
}
fn invalid_subscribe_parameter<T>(field: &'static str, message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message).with_context("field", field));
}
#[cfg(test)]
fn invalid_subscribe_response<T>(field: &'static str, message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_update", field));
}
#[cfg(test)]
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
return commitment.map(|value| {
return match value {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 41
// version: 42
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -40,11 +40,16 @@
//! staged for `pre.007008`.
//! `0.2.9-pre.006` normalizes the five unambiguously HTTP-owned private implementation modules with an `http_` prefix while preserving shared `rpc_*`,
//! JSON-RPC, error and constants modules.
//! `0.2.9-pre.007` completes the standard transaction/transaction-status filters and storage-wire projections; `pre.008` completes Blocks, block-meta and
//! entry projections. `0.2.9-pre.009` promotes those protobuf bridges into runtime and opens one KSP-owned bounded bidirectional `Subscribe` session with
//! request mutation, automatic server-Ping reply, observable Pong, normal server half-close, terminal backpressure and bounded graceful shutdown. Reconnect and
//! replay policy remain outside this tranche.
mod constants;
mod error;
mod grpc_channel;
mod grpc_settings;
mod grpc_stream;
mod grpc_subscribe;
mod grpc_unary;
mod http_client;
@@ -75,8 +80,12 @@ mod ws_transactions;
/// Error code used when no logical endpoint can satisfy a request.
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
/// Error code used when bounded Yellowstone gRPC runtime capacity is exhausted.
pub use self::error::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW;
/// 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 subscribe session is no longer available.
pub use self::error::ERROR_CODE_GRPC_SESSION_CLOSED;
/// 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.
@@ -129,6 +138,10 @@ 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 Yellowstone bidirectional Subscribe session.
pub use self::grpc_stream::SolanaYellowstoneGrpcSubscribeSession;
/// Safe Yellowstone bidirectional Subscribe lifecycle state.
pub use self::grpc_stream::YellowstoneGrpcSubscribeState;
/// One validated standard Yellowstone account predicate.
pub use self::grpc_subscribe::YellowstoneAccountFilterPredicate;
/// Typed account payload carried by one standard Yellowstone account update.
@@ -191,12 +204,18 @@ pub use self::grpc_subscribe::YellowstoneSubscribeEntryFilter;
pub use self::grpc_subscribe::YellowstoneSubscribeFilterName;
/// Optional ping mutation carried by the standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribePing;
/// Standard Yellowstone server Ping update.
pub use self::grpc_subscribe::YellowstoneSubscribePingUpdate;
/// Standard Yellowstone server Pong update.
pub use self::grpc_subscribe::YellowstoneSubscribePongUpdate;
/// Provider-neutral standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
/// Complete slot-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
/// Complete transaction-family filter shared by transactions and transaction-status maps.
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
/// Any standard Yellowstone Subscribe update.
pub use self::grpc_subscribe::YellowstoneSubscribeUpdate;
/// Optional token-account owner expansion for current Yellowstone transaction filters.
pub use self::grpc_subscribe::YellowstoneTokenAccountExpansion;
/// One pre/post token balance from Yellowstone transaction status metadata.
@@ -558,6 +577,12 @@ pub use self::ws_transactions::SolanaSignatureSubscribeConfig;
/// Owning tracing target for events emitted by the on-chain transport crate.
pub(crate) use self::constants::TRACING_TARGET;
/// Internal Yellowstone Subscribe session opener used by the physical channel.
pub(crate) use self::grpc_stream::open_yellowstone_subscribe_session;
/// Internal Yellowstone Subscribe request wire conversion shared with the stream engine.
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_request_to_wire;
/// Internal Yellowstone Subscribe update decoder shared with the stream engine.
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_update_from_wire;
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
pub(crate) use self::http_resilience::HttpConcurrencyPermit;
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.