diff --git a/Cargo.toml b/Cargo.toml index 8e109cc..a7012ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 241 +# version: 242 [workspace] 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"] [workspace.package] -version = "0.2.9-pre.3" +version = "0.2.9-pre.3.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/grpc_unary.rs b/crates/ksp-onchain-transport-lib/src/grpc_unary.rs new file mode 100644 index 0000000..a5a6793 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/grpc_unary.rs @@ -0,0 +1,398 @@ +// file: crates/ksp-onchain-transport-lib/src/grpc_unary.rs +// version: 1 + +const MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES: usize = 256; +const MAX_YELLOWSTONE_VERSION_LENGTH_BYTES: usize = 512; +const PATH_GET_BLOCK_HEIGHT: &str = "/geyser.Geyser/GetBlockHeight"; +const PATH_GET_LATEST_BLOCKHASH: &str = "/geyser.Geyser/GetLatestBlockhash"; +const PATH_GET_SLOT: &str = "/geyser.Geyser/GetSlot"; +const PATH_GET_VERSION: &str = "/geyser.Geyser/GetVersion"; +const PATH_IS_BLOCKHASH_VALID: &str = "/geyser.Geyser/IsBlockhashValid"; +const PATH_PING: &str = "/geyser.Geyser/Ping"; +const PATH_SUBSCRIBE_REPLAY_INFO: &str = "/geyser.Geyser/SubscribeReplayInfo"; + +/// Standard Solana Yellowstone unary client layered on one KSP-owned physical gRPC channel. +/// +/// This N2 facade deliberately exposes no raw Tonic client and no upstream protobuf types. Streaming `Subscribe` is not part of `0.2.9-pre.003`. +#[derive(Clone)] +pub struct SolanaYellowstoneGrpcUnaryClient { + channel: tonic::transport::Channel, + metadata: std::vec::Vec, + unary_timeout: std::time::Duration, + max_inbound_message_size_bytes: usize, + max_outbound_message_size_bytes: usize, +} + +impl SolanaYellowstoneGrpcUnaryClient { + /// Creates the internal unary facade state from a KSP-owned physical channel and validated endpoint settings. + pub(crate) fn new( + channel: tonic::transport::Channel, + metadata: std::vec::Vec, + settings: crate::YellowstoneGrpcSessionSettings, + ) -> Self { + return Self { + channel, + metadata, + unary_timeout: settings.unary_timeout(), + max_inbound_message_size_bytes: settings.max_inbound_message_size_bytes(), + max_outbound_message_size_bytes: settings.max_outbound_message_size_bytes(), + }; + } + + /// Returns the first slot retained by the endpoint for replay when advertised. + pub async fn subscribe_replay_info(&self) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse = + match self.unary("SubscribeReplayInfo", PATH_SUBSCRIBE_REPLAY_INFO, yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest {}).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneReplayInfo::new(response.first_available)); + } + + /// Executes the standard Yellowstone unary `Ping` RPC and verifies the echoed count. + pub async fn ping(&self, count: i32) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::PongResponse = + match self.unary("Ping", PATH_PING, yellowstone_grpc_proto::geyser::PingRequest { count }).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if response.count != count { + return invalid_unary_response("Ping", "Yellowstone Ping response count does not match the request"); + } + return std::result::Result::Ok(crate::YellowstonePong::new(response.count)); + } + + /// Returns the latest blockhash advertised by the Yellowstone endpoint at an optional commitment. + pub async fn get_latest_blockhash( + &self, + commitment: std::option::Option, + ) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse = match self + .unary( + "GetLatestBlockhash", + PATH_GET_LATEST_BLOCKHASH, + yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest { commitment: commitment_to_wire(commitment) }, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if response.blockhash.is_empty() || response.blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES { + return invalid_unary_response("GetLatestBlockhash", "Yellowstone latest blockhash response violates the KSP text bound"); + } + return std::result::Result::Ok(crate::YellowstoneLatestBlockhash::new(response.slot, response.blockhash, response.last_valid_block_height)); + } + + /// Returns the current block height at an optional commitment. + pub async fn get_block_height(&self, commitment: std::option::Option) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::GetBlockHeightResponse = match self + .unary( + "GetBlockHeight", + PATH_GET_BLOCK_HEIGHT, + yellowstone_grpc_proto::geyser::GetBlockHeightRequest { commitment: commitment_to_wire(commitment) }, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneBlockHeight::new(response.block_height)); + } + + /// Returns the current slot at an optional commitment. + pub async fn get_slot(&self, commitment: std::option::Option) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::GetSlotResponse = + match self.unary("GetSlot", PATH_GET_SLOT, yellowstone_grpc_proto::geyser::GetSlotRequest { commitment: commitment_to_wire(commitment) }).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneSlot::new(response.slot)); + } + + /// Checks one opaque Solana blockhash at an optional commitment. + pub async fn is_blockhash_valid( + &self, + blockhash: impl std::convert::Into, + commitment: std::option::Option, + ) -> ksp_core_lib::Result { + let blockhash = blockhash.into(); + if blockhash.is_empty() || blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES || blockhash.trim() != blockhash { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone blockhash must be non-empty, bounded and untrimmed") + .with_context("grpc_operation", "IsBlockhashValid") + .with_context("field", "blockhash"), + ); + } + let response: yellowstone_grpc_proto::geyser::IsBlockhashValidResponse = match self + .unary( + "IsBlockhashValid", + PATH_IS_BLOCKHASH_VALID, + yellowstone_grpc_proto::geyser::IsBlockhashValidRequest { blockhash, commitment: commitment_to_wire(commitment) }, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneBlockhashValidity::new(response.slot, response.valid)); + } + + /// Returns the endpoint's Yellowstone/validator version string after applying a deterministic KSP text bound. + pub async fn get_version(&self) -> ksp_core_lib::Result { + let response: yellowstone_grpc_proto::geyser::GetVersionResponse = + match self.unary("GetVersion", PATH_GET_VERSION, yellowstone_grpc_proto::geyser::GetVersionRequest {}).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if response.version.is_empty() || response.version.len() > MAX_YELLOWSTONE_VERSION_LENGTH_BYTES { + return invalid_unary_response("GetVersion", "Yellowstone version response violates the KSP text bound"); + } + return std::result::Result::Ok(crate::YellowstoneVersionInfo::new(response.version)); + } + + async fn unary(&self, operation: &'static str, path: &'static str, message: Request) -> ksp_core_lib::Result + where + Request: tonic_prost::prost::Message + Send + Sync + 'static, + Response: tonic_prost::prost::Message + std::default::Default + Send + Sync + 'static, + { + let path = match path.parse::() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "internal Yellowstone gRPC method path is invalid") + .with_context("grpc_operation", operation), + ); + }, + }; + let mut grpc = tonic::client::Grpc::new(self.channel.clone()) + .max_decoding_message_size(self.max_inbound_message_size_bytes) + .max_encoding_message_size(self.max_outbound_message_size_bytes); + let future = async { + if grpc.ready().await.is_err() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel is not ready for unary dispatch") + .with_context("grpc_operation", operation), + ); + } + let mut request = tonic::Request::new(message); + for entry in &self.metadata { + if let std::result::Result::Err(error) = entry.append_to(request.metadata_mut()) { + return std::result::Result::Err(error); + } + } + let response = match grpc.unary(request, path, tonic_prost::ProstCodec::::default()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(status) => return std::result::Result::Err(grpc_status_error(operation, status)), + }; + return std::result::Result::Ok(response.into_inner()); + }; + return match tokio::time::timeout(self.unary_timeout, future).await { + std::result::Result::Ok(result) => result, + std::result::Result::Err(_) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, "Yellowstone gRPC unary operation exceeded the KSP deadline") + .with_context("grpc_operation", operation), + ), + }; + } +} + +impl std::fmt::Debug for SolanaYellowstoneGrpcUnaryClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("SolanaYellowstoneGrpcUnaryClient") + .field("metadata_count", &self.metadata.len()) + .field("unary_timeout", &self.unary_timeout) + .field("max_inbound_message_size_bytes", &self.max_inbound_message_size_bytes) + .field("max_outbound_message_size_bytes", &self.max_outbound_message_size_bytes) + .field("channel", &"") + .finish(); + } +} + +/// Replay availability advertised by `SubscribeReplayInfo`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneReplayInfo { + first_available: std::option::Option, +} + +impl YellowstoneReplayInfo { + /// Creates a replay-info projection. + #[must_use] + pub const fn new(first_available: std::option::Option) -> Self { + return Self { first_available }; + } + + /// Returns the first replayable slot when the endpoint advertises one. + #[must_use] + pub const fn first_available(&self) -> std::option::Option { + return self.first_available; + } +} + +/// Echo returned by the Yellowstone unary `Ping` RPC. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstonePong { + count: i32, +} + +impl YellowstonePong { + /// Creates a ping response projection. + #[must_use] + pub const fn new(count: i32) -> Self { + return Self { count }; + } + + /// Returns the echoed ping count. + #[must_use] + pub const fn count(&self) -> i32 { + return self.count; + } +} + +/// Latest blockhash response returned by Yellowstone. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct YellowstoneLatestBlockhash { + slot: u64, + blockhash: std::string::String, + last_valid_block_height: u64, +} + +impl YellowstoneLatestBlockhash { + /// Creates a latest-blockhash projection. + #[must_use] + pub fn new(slot: u64, blockhash: std::string::String, last_valid_block_height: u64) -> Self { + return Self { slot, blockhash, last_valid_block_height }; + } + + /// Returns the response slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns the opaque Solana blockhash text. + #[must_use] + pub fn blockhash(&self) -> &str { + return self.blockhash.as_str(); + } + + /// Returns the last valid block height associated with the blockhash. + #[must_use] + pub const fn last_valid_block_height(&self) -> u64 { + return self.last_valid_block_height; + } +} + +/// Current block height returned by Yellowstone. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneBlockHeight { + block_height: u64, +} + +impl YellowstoneBlockHeight { + /// Creates a block-height projection. + #[must_use] + pub const fn new(block_height: u64) -> Self { + return Self { block_height }; + } + + /// Returns the block height. + #[must_use] + pub const fn block_height(&self) -> u64 { + return self.block_height; + } +} + +/// Current slot returned by Yellowstone. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneSlot { + slot: u64, +} + +impl YellowstoneSlot { + /// Creates a slot projection. + #[must_use] + pub const fn new(slot: u64) -> Self { + return Self { slot }; + } + + /// Returns the slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } +} + +/// Result of checking one blockhash with Yellowstone. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneBlockhashValidity { + slot: u64, + valid: bool, +} + +impl YellowstoneBlockhashValidity { + /// Creates a blockhash-validity projection. + #[must_use] + pub const fn new(slot: u64, valid: bool) -> Self { + return Self { slot, valid }; + } + + /// Returns the response slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns whether the blockhash is valid at the requested commitment. + #[must_use] + pub const fn valid(&self) -> bool { + return self.valid; + } +} + +/// Bounded Yellowstone endpoint version information. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct YellowstoneVersionInfo { + version: std::string::String, +} + +impl YellowstoneVersionInfo { + /// Creates a version projection. + #[must_use] + pub fn new(version: std::string::String) -> Self { + return Self { version }; + } + + /// Returns the bounded endpoint version string. + #[must_use] + pub fn version(&self) -> &str { + return self.version.as_str(); + } +} + +fn commitment_to_wire(commitment: std::option::Option) -> std::option::Option { + return commitment.map(|value| { + return match value { + crate::SolanaCommitment::Processed => yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32, + crate::SolanaCommitment::Confirmed => yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed as i32, + crate::SolanaCommitment::Finalized => yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32, + }; + }); +} + +fn grpc_status_error(operation: &'static str, status: tonic::Status) -> ksp_core_lib::Error { + let code = status.code().to_string(); + ksp_logging_lib::warn!(target: crate::TRACING_TARGET, grpc_operation = operation, grpc_code = code.as_str(), "Yellowstone gRPC unary endpoint returned a status"); + return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_STATUS, "Yellowstone gRPC endpoint returned a gRPC status") + .with_context("grpc_operation", operation) + .with_context("grpc_code", code); +} + +fn invalid_unary_response(operation: &'static str, message: &str) -> ksp_core_lib::Result { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_operation", operation)); +} + +#[cfg(test)] +#[path = "../unit_tests/grpc_unary.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs b/crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs new file mode 100644 index 0000000..9322b96 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs @@ -0,0 +1,288 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs +// version: 2 + +#[derive(Clone, Default)] +struct FixtureGeyser; + +#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies. +#[tonic::async_trait] +impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser { + type SubscribeStream = std::pin::Pin< + std::boxed::Box> + Send + 'static>, + >; + type SubscribeDeshredStream = std::pin::Pin< + std::boxed::Box< + dyn futures_util::Stream> + Send + 'static, + >, + >; + + async fn subscribe( + &self, + _request: tonic::Request>, + ) -> std::result::Result, tonic::Status> { + return std::result::Result::Err(tonic::Status::unimplemented("streaming is outside the pre.003 fixture")); + } + + async fn subscribe_deshred( + &self, + _request: tonic::Request>, + ) -> std::result::Result, tonic::Status> { + return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside KSP 0.2.9")); + } + + async fn subscribe_replay_info( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse { + first_available: std::option::Option::Some(100), + })); + } + + async fn ping( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + let count = request.into_inner().count; + if count == -999 { + return std::result::Result::Err(tonic::Status::permission_denied("GRPC-REMOTE-SECRET-CANARY")); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::PongResponse { count })); + } + + async fn get_latest_blockhash( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse { + slot: 101, + blockhash: "fixture-blockhash".to_owned(), + last_valid_block_height: 999, + })); + } + + async fn get_block_height( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetBlockHeightResponse { block_height: 202 })); + } + + async fn get_slot( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + if request.get_ref().commitment == std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32) { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } else if let std::result::Result::Err(error) = + require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) + { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetSlotResponse { slot: 303 })); + } + + async fn is_blockhash_valid( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) { + return std::result::Result::Err(error); + } + if request.get_ref().blockhash != "fixture-blockhash" { + return std::result::Result::Err(tonic::Status::invalid_argument("unexpected fixture blockhash")); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::IsBlockhashValidResponse { slot: 404, valid: true })); + } + + async fn get_version( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetVersionResponse { + version: "fixture-yellowstone-12.6".to_owned(), + })); + } +} + +struct FixtureServer { + endpoint_url: std::string::String, + shutdown: std::option::Option>, + task: tokio::task::JoinHandle<()>, +} + +impl FixtureServer { + async fn start() -> Self { + let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse"); + let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind"); + let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address"); + let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel(); + let task = tokio::spawn(async move { + let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser); + let result = tonic::transport::Server::builder() + .serve_with_incoming_shutdown(service, incoming, async move { + let _ = shutdown_receiver.await; + }) + .await; + assert!(result.is_ok()); + }); + return Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task }; + } + + async fn stop(mut self) { + if let std::option::Option::Some(shutdown) = self.shutdown.take() { + let _ = shutdown.send(()); + } + let result = self.task.await; + assert!(result.is_ok()); + } +} + +fn verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> { + let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok()); + let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok()); + if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-SECRET-CANARY") { + return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch")); + } + return std::result::Result::Ok(()); +} + +fn require_commitment(actual: std::option::Option, expected: yellowstone_grpc_proto::geyser::CommitmentLevel) -> std::result::Result<(), tonic::Status> { + if actual != std::option::Option::Some(expected as i32) { + return std::result::Result::Err(tonic::Status::invalid_argument("fixture commitment mismatch")); + } + return std::result::Result::Ok(()); +} + +fn fixture_settings(url: &str, unary_timeout: std::time::Duration) -> crate::YellowstoneGrpcEndpointSettings { + let defaults = crate::YellowstoneGrpcSessionSettings::default(); + let session = crate::YellowstoneGrpcSessionSettings::new( + defaults.connect_timeout(), + unary_timeout, + defaults.close_timeout(), + defaults.reconnect().clone(), + defaults.request_channel_capacity(), + defaults.update_channel_capacity(), + defaults.max_inbound_message_size_bytes(), + defaults.max_outbound_message_size_bytes(), + ); + let metadata = std::vec![ + crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"), + crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("fixture secret metadata must be valid"), + ]; + return crate::YellowstoneGrpcEndpointSettings::new( + "fixture", + true, + crate::YellowstoneGrpcProviderName::new("fixture-provider"), + crate::YellowstoneGrpcClusterName::new("devnet"), + crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"), + session, + ) + .with_metadata(metadata) + .expect("fixture metadata settings must validate"); +} + +#[tokio::test(flavor = "current_thread")] +async fn yellowstone_unary_fixture_covers_all_seven_standard_methods_and_metadata() { + let server = FixtureServer::start().await; + let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1)); + let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect"); + let client = channel.standard_unary_client(); + let replay = client.subscribe_replay_info().await.expect("ReplayInfo fixture must succeed"); + assert_eq!(replay.first_available(), std::option::Option::Some(100)); + let pong = client.ping(7).await.expect("Ping fixture must succeed"); + assert_eq!(pong.count(), 7); + let latest = client + .get_latest_blockhash(std::option::Option::Some(crate::SolanaCommitment::Confirmed)) + .await + .expect("latest blockhash fixture must succeed"); + assert_eq!(latest.slot(), 101); + assert_eq!(latest.blockhash(), "fixture-blockhash"); + assert_eq!(latest.last_valid_block_height(), 999); + let height = client.get_block_height(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("block height fixture must succeed"); + assert_eq!(height.block_height(), 202); + let slot = client.get_slot(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("slot fixture must succeed"); + assert_eq!(slot.slot(), 303); + let validity = client + .is_blockhash_valid("fixture-blockhash", std::option::Option::Some(crate::SolanaCommitment::Confirmed)) + .await + .expect("blockhash validity fixture must succeed"); + assert_eq!(validity.slot(), 404); + assert!(validity.valid()); + let version = client.get_version().await.expect("version fixture must succeed"); + assert_eq!(version.version(), "fixture-yellowstone-12.6"); + let rendered = format!("{client:?} {channel:?}"); + assert!(!rendered.contains("GRPC-SECRET-CANARY")); + assert!(!rendered.contains(server.endpoint_url.as_str())); + server.stop().await; +} + +#[tokio::test(flavor = "current_thread")] +async fn yellowstone_remote_status_does_not_copy_message_details_or_metadata() { + let server = FixtureServer::start().await; + let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1)); + let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect"); + let error = channel.standard_unary_client().ping(-999).await.expect_err("fixture must return remote status"); + assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS); + let rendered = format!("{error:?}"); + assert!(!rendered.contains("GRPC-REMOTE-SECRET-CANARY")); + assert!(!rendered.contains("GRPC-SECRET-CANARY")); + assert!(!rendered.contains(server.endpoint_url.as_str())); + server.stop().await; +} + +#[tokio::test(flavor = "current_thread")] +async fn yellowstone_unary_timeout_is_bounded_and_safe() { + let server = FixtureServer::start().await; + let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_millis(5)); + let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect"); + let error = channel + .standard_unary_client() + .get_slot(std::option::Option::Some(crate::SolanaCommitment::Processed)) + .await + .expect_err("delayed fixture must exceed unary timeout"); + assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT); + let rendered = format!("{error:?}"); + assert!(!rendered.contains("GRPC-SECRET-CANARY")); + assert!(!rendered.contains(server.endpoint_url.as_str())); + server.stop().await; +} + +#[tokio::test(flavor = "current_thread")] +async fn yellowstone_invalid_blockhash_is_rejected_before_io() { + let settings = fixture_settings("http://127.0.0.1:1", std::time::Duration::from_secs(1)); + let channel = crate::YellowstoneGrpcChannel::prepare(&settings).expect("lazy fixture channel must prepare"); + let error = channel + .standard_unary_client() + .is_blockhash_valid(" ", std::option::Option::None) + .await + .expect_err("invalid blockhash must be rejected locally"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); +} diff --git a/deltas/0.2.9/pre.002-fix.002.md b/deltas/0.2.9/pre.002-fix.002.md new file mode 100644 index 0000000..0868b70 --- /dev/null +++ b/deltas/0.2.9/pre.002-fix.002.md @@ -0,0 +1,68 @@ + + + +# Delta `0.2.9-pre.002-fix.002` — canari public API sous runtime Tokio + +## 1. Déclencheur + +Le second gate opérateur, après `pre.002-fix.001`, confirme : + +```text +cargo fmt --all PASS +audit_rust_workspace_rules.py PASS / clean +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS +cargo test -p ksp-onchain-transport-lib unit PASS 346/346 +public_api FAIL 41/42 +dependency canary Core PASS 3/3 +``` + +Le seul échec restant est le canari : + +```text +public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root +``` + +Il appelle `YellowstoneGrpcChannel::prepare()` depuis un `#[test]` synchrone. Depuis `fix.001`, `prepare()` exige volontairement un runtime Tokio actif avant `tonic::transport::Endpoint::connect_lazy()` ; le canari public API n'avait pas encore été aligné sur cette précondition KSP explicite. + +## 2. Correction + +Le test d'intégration devient : + +```text +#[tokio::test(flavor = "current_thread")] +async fn public_v0_2_9_pre_002_... +``` + +Aucun code N1 de production n'est modifié. En particulier : + +```text +YellowstoneGrpcChannel::prepare() inchangé +_channel de production conservé +aucun cfg(test) ajouté au channel +aucune dépendance/feature Cargo ajoutée +aucun TLS/metadata/unary anticipé +``` + +## 3. Version + +Le correctif touche le code de test et fait partie du signal technique de la prerelease : + +```text +workspace.package.version = 0.2.9-pre.2.fix.2 +commit attendu = v0.2.9-pre.002-fix.002 +``` + +## 4. Validation requise + +```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 +``` + +Les graphes Cargo n'ont pas changé avec `fix.002`; ils ne nécessitent pas une nouvelle inspection structurelle, mais peuvent être relancés si l'opérateur veut conserver une preuve complète du gate final `pre.002`. diff --git a/deltas/0.2.9/pre.003-fix.001.md b/deltas/0.2.9/pre.003-fix.001.md new file mode 100644 index 0000000..ccbd54f --- /dev/null +++ b/deltas/0.2.9/pre.003-fix.001.md @@ -0,0 +1,82 @@ + + + +# Delta `0.2.9-pre.003-fix.001` — conformité Clippy de la fixture unary + +## 1. Déclencheur + +Le premier gate opérateur `pre.003` confirme : + +```text +cargo fmt --all PASS +python3 scripts/audit_rust_workspace_rules.py PASS / clean +cargo check --workspace PASS +cargo clippy --workspace --all-targets FAIL + 11 x clippy::implicit_return dans unit_tests/grpc_unary.rs +cargo test -p ksp-onchain-transport-lib PASS + unit 354/354 + public_api 43/43 + release_completeness 36/36 + doctests 4/4 +cargo test -p ksp-core-lib --test workspace_dependencies + PASS 3/3 +cargo test --workspace PASS +``` + +Le problème est donc limité à la conformité Clippy de la fixture locale ; aucune défaillance fonctionnelle TLS/metadata/unary n'est observée. + +## 2. Diagnostic + +Neuf diagnostics pointent les méthodes de l'implémentation `Geyser` annotée `#[tonic::async_trait]`. Les corps source ont déjà leurs retours explicites ; le lint vise les wrappers/tails async issus de l'expansion de macro et propose un `return async fn ...` non applicable au code Rust source. + +Deux diagnostics supplémentaires concernent les closures `and_then` utilisées pour lire les metadata de fixture ; ces retours sont directement sous contrôle KSP. + +## 3. Correction + +```text +impl FixtureGeyser : + allow(clippy::implicit_return) strictement local et commenté + aucune relaxation au niveau crate/workspace + +closures metadata : + return explicite conforme à la politique workspace +``` + +Aucun changement de production : + +```text +YellowstoneGrpcChannel inchangé +SolanaYellowstoneGrpcUnaryClient inchangé +TLS/metadata/settings inchangés +7 unary inchangés +aucune dépendance/feature Cargo ajoutée ou retirée +Subscribe/PublicNode/Config V3 toujours hors tranche +``` + +## 4. Version + +```text +workspace.package.version = 0.2.9-pre.3.fix.1 +commit attendu = v0.2.9-pre.003-fix.001 +``` + +## 5. Validation requise + +```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 +``` + +Les dépendances et features ne changent pas avec ce fix. Les graphes Cargo demandés par `pre.003` restent néanmoins à fournir/inspecter si ce n'est pas déjà fait : + +```bash +cargo tree -p ksp-onchain-transport-lib +cargo tree -p ksp-onchain-transport-lib -e features +cargo tree -p ksp-onchain-transport-lib --duplicates +cargo tree --duplicates +``` diff --git a/deltas/0.2.9/pre.003.md b/deltas/0.2.9/pre.003.md new file mode 100644 index 0000000..64af273 --- /dev/null +++ b/deltas/0.2.9/pre.003.md @@ -0,0 +1,294 @@ + + + +# Delta `0.2.9-pre.003` — TLS + metadata générique + fixture locale + 7 unary Yellowstone + +## 1. Objet + +Cette tranche poursuit le moteur Yellowstone après la fermeture opérateur complète de `pre.002-fix.002`. Elle matérialise exactement le forecast prévu : + +```text +N1 : connexion HTTP/2 réelle + TLS WebPKI + metadata ASCII publique/secrète redacted +N2 : sept unary RPCs Yellowstone standard typed derrière une façade KSP +tests : fixture Geyser locale couvrant le wire réel, metadata, Status hostile et timeout +``` + +Restent hors tranche : + +```text +Subscribe / streaming bidi +SubscribeDeshred +PublicNode / N3 provider +Config V3 +reconnect/resubscribe/replay lifecycle complet +``` + +Version workspace : + +```text +0.2.9-pre.3 +``` + +Commit attendu après validation opérateur : + +```text +v0.2.9-pre.003 +``` + +## 2. Preuve héritée — fermeture de `pre.002` + +Le gate opérateur final fourni pour `0.2.9-pre.2.fix.2` est intégralement vert : + +```text +cargo fmt --all PASS +python3 scripts/audit_rust_workspace_rules.py PASS / clean +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS +cargo test -p ksp-onchain-transport-lib PASS + unit 346/346 + public_api 42/42 + release_completeness 35/35 + doctests 4/4 +cargo test -p ksp-core-lib --test workspace_dependencies + PASS 3/3 +cargo test --workspace PASS +``` + +`pre.003` part donc d'une base technique fermée, sans dette reportée de `pre.002`. + +## 3. Dépendances et features + +La stratégie B reste inchangée : le runtime KSP utilise les messages publiés `yellowstone-grpc-proto` et un client KSP autour de Tonic ; `yellowstone-grpc-client` reste absent. + +Ajouts workspace : + +```text +http = ^1.5, default-features = false +tonic-prost = ^0.14, default-features = false +``` + +Features Transport runtime : + +```text +tonic = channel + tls-aws-lc + tls-webpki-roots +tonic-prost = runtime ProstCodec +yellowstone-grpc-proto = aucune feature locale +``` + +Features uniquement dev/test : + +```text +tonic = codegen + server +yellowstone-grpc-proto = tonic +``` + +La feature `tonic` de la crate proto est donc utilisée uniquement pour générer `GeyserServer` dans la fixture locale. Le runtime ne consomme ni le client généré upstream ni sa sémantique de lifecycle. + +Toujours absents comme dépendances KSP directes : + +```text +yellowstone-grpc-client +prost +prost-types +``` + +Le choix `tls-aws-lc` reste aligné avec la stack Rustls déjà présente via Reqwest ; le gate Cargo opérateur doit confirmer l'absence de duplication injustifiée. + +## 4. N1 — TLS, connexion réelle et metadata + +`YellowstoneGrpcChannel` possède maintenant deux chemins explicites : + +```text +prepare() -> connect_lazy(), aucun socket +connect() -> établissement réel du channel HTTP/2 +``` + +Pour un endpoint `https://`, le builder applique : + +```text +ClientTlsConfig +WebPKI roots +TLS handshake timeout borné +connect timeout borné +request buffer borné +``` + +Les erreurs d'établissement restent projetées vers `onchain_transport/grpc_channel_failed` sans recopier URI ou message arbitraire Tonic. + +Ajout de `YellowstoneGrpcMetadataEntry` : + +```text +ASCII uniquement +clé <= 128 bytes +valeur <= 8 KiB +<= 64 entrées par endpoint +clés grpc-* refusées +metadata binaire *-bin hors contrat pre.003 +public et secret séparés explicitement +valeurs toujours absentes de Debug +secret -> MetadataValue::set_sensitive(true) +``` + +Transport ne connaît aucun nom `KSP_SECRET_*`, ne lit pas l'environnement et ne lie aucune clé provider particulière au standard N1/N2. + +## 5. N2 — sept unary standard + +Ajout de `SolanaYellowstoneGrpcUnaryClient`, construit uniquement depuis `YellowstoneGrpcChannel`. Le Tonic `Channel`, `Grpc`, les Requests et les messages protobuf restent privés. + +Surface exacte : + +```text +SubscribeReplayInfo +Ping +GetLatestBlockhash +GetBlockHeight +GetSlot +IsBlockhashValid +GetVersion +``` + +Les chemins wire sont les chemins `geyser.Geyser` officiels. `SolanaCommitment` existant est réutilisé et mappé vers `Processed / Confirmed / Finalized`. + +DTOs publics KSP : + +```text +YellowstoneReplayInfo +YellowstonePong +YellowstoneLatestBlockhash +YellowstoneBlockHeight +YellowstoneSlot +YellowstoneBlockhashValidity +YellowstoneVersionInfo +``` + +Bornes/sécurité : + +```text +max inbound/outbound appliqués au dispatcher Tonic +une seule deadline KSP couvre readiness + metadata + unary dispatch +blockhash request localement borné/validé avant I/O +blockhash/version response textuellement bornés +Ping exige l'echo exact du count +``` + +Nouveau code d'erreur : + +```text +onchain_transport/grpc_status +``` + +Un `tonic::Status` distant n'est jamais copié dans `KspError` : seuls l'opération et le code gRPC sont conservés. Message/details/metadata provider restent hors diagnostic public. + +## 6. Fixture Geyser locale + +Les tests dev activent le serveur généré officiel et démarrent un `GeyserServer` sur `127.0.0.1:0`. Les deux RPCs streaming obligatoires du trait retournent volontairement `Unimplemented`, car ils restent hors `pre.003`. + +La fixture vérifie réellement : + +```text +les 7 paths unary +commitment wire +metadata publique reçue +metadata secrète reçue +replay optional first_available +Ping exact +latest blockhash / height / slot / validity / version +Status hostile contenant un canari secret non propagé +timeout unary local déterministe +blockhash invalide rejeté avant I/O +Debug client/channel sans URL ni secret +``` + +La fixture est locale et déterministe ; aucun provider externe n'est requis pour fermer cette tranche. + +## 7. Canaries structurels + +Les canaries vérifient en plus : + +```text +feature set runtime/dev exact +yellowstone-grpc-client absent +proto runtime sans feature tonic +server/codegen uniquement dev/test +aucun pub use tonic/yellowstone_grpc_proto +sept chemins unary exacts +aucun Subscribe standard ajouté +aucun SubscribeDeshred ajouté +aucun PublicNode ajouté +Transport -X-> Config/env/WS pour la surface gRPC +``` + +## 8. Documentation + +Mise à jour de : + +```text +docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md +docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +``` + +La matrice enregistre la fermeture réelle de `pre.002` et place les sept unary/TLS/metadata/fixture en candidate `pre.003`. Le forecast reste inchangé : `pre.004` ouvre seulement la foundation `Subscribe`. + +## 9. Validation exécutée dans l'environnement de préparation + +Exécuté réellement après les modifications source/documentaires : + +```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 fournit pas Cargo/Rust ; aucune commande Cargo n'est déclarée réussie localement. + +## 10. Gate opérateur requis + +```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 -e features +cargo tree -p ksp-onchain-transport-lib --duplicates +cargo tree --duplicates +``` + +Les graphes doivent être réinspectés car `http`, `tonic-prost` et les features TLS/server/codegen changent le graphe de `pre.002`. Contrôler notamment : + +```text +tonic / tonic-prost +prost / prost-types +http / hyper / hyper-util / tower +rustls / tokio-rustls / aws-lc +yellowstone-grpc-proto runtime vs dev feature unification +solana-pubkey +yellowstone-grpc-client absent +router/gzip/zstd non activés par la surface KSP ; server/codegen uniquement via dev/test +``` + +## 11. 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/grpc_unary.rs +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_common.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 +crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.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.003.md +``` diff --git a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md index 0225eeb..b87f703 100644 --- a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md +++ b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode -> **Statut : `0.2.9-pre.003` — `pre.002-fix.002` est fermée sur gate opérateur intégralement vert (fmt/audit/check/Clippy, Transport 346 unit + 42 public API + 35 completeness + 4 doctests, dependency canary et workspace complet). `pre.003` ajoute uniquement TLS client, metadata générique redacted, connexion HTTP/2 réelle, fixture Geyser locale et les sept unary standard ; `Subscribe`, PublicNode et Config V3 restent hors tranche. `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 15–20 minutes de travail effectif et la release complète doit rester clôturable dans une seule session de chat.** +> **Statut : `0.2.9-pre.003-fix.001` — `pre.002-fix.002` est fermée sur gate opérateur intégralement vert. Le premier gate `pre.003` confirme fmt/audit/check, les 354 unit tests Transport, 43 public API, 36 completeness, 4 doctests, le dependency canary et le workspace complet ; seul Clippy échoue sur `implicit_return` dans la fixture `#[tonic::async_trait]` et deux closures metadata. `fix.001` corrige uniquement cette conformité de test, sans changement du moteur/TLS/metadata/unary de production. `pre.003` ajoute uniquement TLS client, metadata générique redacted, connexion HTTP/2 réelle, fixture Geyser locale et les sept unary standard ; `Subscribe`, PublicNode et Config V3 restent hors tranche. `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 15–20 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 @@ -905,7 +905,7 @@ pre.001 DONE — audit upstream/service/proto + providers gratuits + licences/d pre.002 DONE — moteur Yellowstone : proto/dependencies + settings/errors + channel minimal budget : 15–20 min ; gate final fix.002 : fmt/audit/check/Clippy + Transport 346/42/35/4 + dependency canary + workspace PASS -pre.003 CANDIDATE — moteur TLS/metadata + façade N2 unary + fixture locale + 7 unary RPCs +pre.003 FIX.001 CANDIDATE — moteur TLS/metadata + façade N2 unary + fixture locale + 7 unary RPCs budget : 15–20 min ; preuve cible : connect/TLS/timeouts/Status safe + metadata redacted + wire unary exact + cargo tree pre.004 standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds @@ -1058,6 +1058,10 @@ cargo test --workspace PASS ; seuls smokes/bench diagnos ## 19.2 Gate source `pre.003` +Le premier gate opérateur de `pre.003` confirme que la surface fonctionnelle compile et que les tests sont verts, mais Clippy isole 11 diagnostics `implicit_return` exclusivement dans `unit_tests/grpc_unary.rs` : neuf sur les méthodes transformées par `#[tonic::async_trait]` malgré des retours explicites dans leurs corps, et deux sur des closures `and_then`. `pre.003-fix.001` applique une exception de lint localisée à l'implémentation fixture générée par la macro et rend explicites les deux retours de closures. Aucun code runtime N1/N2 n'est modifié. + + + La candidate matérialise : ```text diff --git a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md index 592d17f..1779d81 100644 --- a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +++ b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md @@ -1,9 +1,9 @@ - + # Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode -> **Statut : `pre.002-fix.002` est fermé sur gate opérateur intégralement vert : fmt/audit/check/Clippy, Transport 346 unit + 42 public API + 35 release completeness + 4 doctests, Core dependency canary et workspace complet. `0.2.9-pre.003` est maintenant candidate : TLS client, metadata générique redacted, connexion réelle et exactement sept unary Yellowstone standard sont matérialisés avec une fixture Geyser locale ; `Subscribe`, PublicNode et Config V3 restent hors tranche. OrbitFlare et Helius sont les seules releases provider suivantes planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.** +> **Statut : `pre.002-fix.002` est fermé. Le premier gate `pre.003` confirme fmt/audit/check, Transport 354 unit + 43 public API + 36 release completeness + 4 doctests, Core dependency canary et workspace complet ; seul Clippy échoue sur 11 diagnostics `implicit_return` dans la fixture unary. `0.2.9-pre.003-fix.001` corrige uniquement ces diagnostics de test, sans changement runtime. TLS client, metadata générique redacted, connexion réelle et exactement sept unary Yellowstone standard restent la surface `pre.003` ; `Subscribe`, PublicNode et Config V3 restent hors tranche. OrbitFlare et Helius sont les seules releases provider suivantes planifiées ; les autres providers restent en TODO/IDEAS sans numéro réservé.** ## 1. Autorités du gate @@ -286,25 +286,25 @@ DONE pre.003 source/tests channel/client Debug sans URL, metadata value ni raw ## 11. Lifecycle / backpressure / replay -| Cas | Attendu | État | -|--------------------------------------------|------------------------------------------|------| -| stream open | session bornée | TODO | -| request mutation | ordre déterministe | TODO | -| server Ping -> client request ping -> Pong | explicite | TODO | -| server half-close | terminal/reconnect selon policy | TODO | -| client close | cleanup borné | TODO | -| receiver drop | cleanup capacité | TODO | -| slow subscription | pas de queue infinie | TODO | -| inbound oversized | rejet avant allocation excessive | TODO | -| outbound oversized | rejet avant write | TODO | -| reconnect budget | borné | TODO | -| resubscribe order | déterministe | TODO | -| `from_slot` | utilisé sans promesse lossless | TODO | +| Cas | Attendu | État | +|--------------------------------------------|------------------------------------------|-----------| +| stream open | session bornée | TODO | +| request mutation | ordre déterministe | TODO | +| server Ping -> client request ping -> Pong | explicite | TODO | +| server half-close | terminal/reconnect selon policy | TODO | +| client close | cleanup borné | TODO | +| receiver drop | cleanup capacité | TODO | +| slow subscription | pas de queue infinie | TODO | +| inbound oversized | rejet avant allocation excessive | TODO | +| outbound oversized | rejet avant write | TODO | +| reconnect budget | borné | TODO | +| resubscribe order | déterministe | TODO | +| `from_slot` | utilisé sans promesse lossless | TODO | | ReplayInfo | informatif | CANDIDATE | -| duplicates | observables | TODO | -| gaps | observables | TODO | -| divergent node history | couverture documentée | TODO | -| shutdown during reconnect | aucune nouvelle connexion après shutdown | TODO | +| duplicates | observables | TODO | +| gaps | observables | TODO | +| divergent node history | couverture documentée | TODO | +| shutdown during reconnect | aucune nouvelle connexion après shutdown | TODO | Claims interdits sans nouvelle preuve : @@ -551,4 +551,35 @@ Le second échec ne révèle aucune nouvelle faiblesse du moteur N1 : il confirm La fixture active `yellowstone-grpc-proto/tonic` et Tonic `codegen+server` uniquement dans le graphe dev/test. Le runtime utilise les messages Protobuf publiés, `tonic::client::Grpc` et `tonic-prost::ProstCodec` derrière une façade KSP sans raw escape hatch. **Verdict `pre.003` : candidate source prête ; fermeture seulement après gate Cargo opérateur complet et réinspection des graphes.** +### 18.1 Premier gate opérateur `pre.003` et `pre.003-fix.001` +| Gate / observation | Résultat `pre.003` | État `fix.001` | +|------------------------------------------|-------------------------------------------------------------|----------------------------| +| workspace version | `0.2.9-pre.3` | `0.2.9-pre.3.fix.1` | +| `cargo fmt --all` | PASS | à réexécuter | +| audit Rust workspace | PASS / clean | audit local PASS / clean | +| `cargo check --workspace` | PASS | inchangé | +| `cargo clippy --workspace --all-targets` | FAIL : 11 `implicit_return` dans `unit_tests/grpc_unary.rs` | corrigé ; à réexécuter | +| Transport unit | 354/354 PASS | inchangé | +| Transport `public_api` | 43/43 PASS | inchangé | +| Transport `release_completeness` | 36/36 PASS | inchangé | +| Transport doctests | 4/4 PASS | inchangé | +| Core dependency canary | 3/3 PASS | inchangé | +| `cargo test --workspace` | PASS | inchangé fonctionnellement | + +Correction : + +```text +9 diagnostics proviennent de l'expansion #[tonic::async_trait] sur la fixture serveur, +alors que les corps source ont déjà des return explicites. +=> allow(clippy::implicit_return) strictement local à cette implémentation de test, avec justification. + +2 diagnostics concernent les closures metadata and_then. +=> return explicite dans les closures. + +aucun changement N1/N2 runtime +aucune dépendance/feature modifiée +aucun Subscribe/PublicNode/Config V3 anticipé +``` + +**Verdict `pre.003-fix.001` : correctif Clippy minimal prêt ; fermeture de `pre.003` après réexécution verte des gates habituels et inspection des graphes Cargo demandée par la tranche.**