v0.2.9-pre.003-fix.001
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 241
|
# version: 242
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.9-pre.3"
|
version = "0.2.9-pre.3.fix.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal file
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal file
@@ -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<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
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<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
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<crate::YellowstoneReplayInfo> {
|
||||||
|
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<crate::YellowstonePong> {
|
||||||
|
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<crate::SolanaCommitment>,
|
||||||
|
) -> ksp_core_lib::Result<crate::YellowstoneLatestBlockhash> {
|
||||||
|
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<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneBlockHeight> {
|
||||||
|
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<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneSlot> {
|
||||||
|
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<std::string::String>,
|
||||||
|
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||||
|
) -> ksp_core_lib::Result<crate::YellowstoneBlockhashValidity> {
|
||||||
|
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<crate::YellowstoneVersionInfo> {
|
||||||
|
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<Request, Response>(&self, operation: &'static str, path: &'static str, message: Request) -> ksp_core_lib::Result<Response>
|
||||||
|
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::<http::uri::PathAndQuery>() {
|
||||||
|
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::<Request, Response>::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", &"<private>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replay availability advertised by `SubscribeReplayInfo`.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneReplayInfo {
|
||||||
|
first_available: std::option::Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneReplayInfo {
|
||||||
|
/// Creates a replay-info projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(first_available: std::option::Option<u64>) -> 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<u64> {
|
||||||
|
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<crate::SolanaCommitment>) -> std::option::Option<i32> {
|
||||||
|
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<T>(operation: &'static str, message: &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_operation", operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/grpc_unary.rs"]
|
||||||
|
mod tests;
|
||||||
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal file
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal file
@@ -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<dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>> + Send + 'static>,
|
||||||
|
>;
|
||||||
|
type SubscribeDeshredStream = std::pin::Pin<
|
||||||
|
std::boxed::Box<
|
||||||
|
dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>> + Send + 'static,
|
||||||
|
>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
async fn subscribe(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, 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<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, 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<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, 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<yellowstone_grpc_proto::geyser::PingRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, 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<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, 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<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, 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<yellowstone_grpc_proto::geyser::GetSlotRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, 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<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, 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<yellowstone_grpc_proto::geyser::GetVersionRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, 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<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
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<i32>, 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);
|
||||||
|
}
|
||||||
68
deltas/0.2.9/pre.002-fix.002.md
Normal file
68
deltas/0.2.9/pre.002-fix.002.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.002-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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`.
|
||||||
82
deltas/0.2.9/pre.003-fix.001.md
Normal file
82
deltas/0.2.9/pre.003-fix.001.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.003-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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
|
||||||
|
```
|
||||||
294
deltas/0.2.9/pre.003.md
Normal file
294
deltas/0.2.9/pre.003.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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
|
||||||
|
```
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
|
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
|
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
|
||||||
|
|
||||||
> **Statut : `0.2.9-pre.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
|
## 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
|
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
|
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
|
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
|
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`
|
## 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 :
|
La candidate matérialise :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
|
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode
|
# 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
|
## 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
|
## 11. Lifecycle / backpressure / replay
|
||||||
|
|
||||||
| Cas | Attendu | État |
|
| Cas | Attendu | État |
|
||||||
|--------------------------------------------|------------------------------------------|------|
|
|--------------------------------------------|------------------------------------------|-----------|
|
||||||
| stream open | session bornée | TODO |
|
| stream open | session bornée | TODO |
|
||||||
| request mutation | ordre déterministe | TODO |
|
| request mutation | ordre déterministe | TODO |
|
||||||
| server Ping -> client request ping -> Pong | explicite | TODO |
|
| server Ping -> client request ping -> Pong | explicite | TODO |
|
||||||
| server half-close | terminal/reconnect selon policy | TODO |
|
| server half-close | terminal/reconnect selon policy | TODO |
|
||||||
| client close | cleanup borné | TODO |
|
| client close | cleanup borné | TODO |
|
||||||
| receiver drop | cleanup capacité | TODO |
|
| receiver drop | cleanup capacité | TODO |
|
||||||
| slow subscription | pas de queue infinie | TODO |
|
| slow subscription | pas de queue infinie | TODO |
|
||||||
| inbound oversized | rejet avant allocation excessive | TODO |
|
| inbound oversized | rejet avant allocation excessive | TODO |
|
||||||
| outbound oversized | rejet avant write | TODO |
|
| outbound oversized | rejet avant write | TODO |
|
||||||
| reconnect budget | borné | TODO |
|
| reconnect budget | borné | TODO |
|
||||||
| resubscribe order | déterministe | TODO |
|
| resubscribe order | déterministe | TODO |
|
||||||
| `from_slot` | utilisé sans promesse lossless | TODO |
|
| `from_slot` | utilisé sans promesse lossless | TODO |
|
||||||
| ReplayInfo | informatif | CANDIDATE |
|
| ReplayInfo | informatif | CANDIDATE |
|
||||||
| duplicates | observables | TODO |
|
| duplicates | observables | TODO |
|
||||||
| gaps | observables | TODO |
|
| gaps | observables | TODO |
|
||||||
| divergent node history | couverture documentée | TODO |
|
| divergent node history | couverture documentée | TODO |
|
||||||
| shutdown during reconnect | aucune nouvelle connexion après shutdown | TODO |
|
| shutdown during reconnect | aucune nouvelle connexion après shutdown | TODO |
|
||||||
|
|
||||||
Claims interdits sans nouvelle preuve :
|
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.
|
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.**
|
**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.**
|
||||||
|
|||||||
Reference in New Issue
Block a user