From e4bbc78a41b42c9ce125ed06de72aca9c425d49a Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 24 Aug 2026 11:55:17 +0200 Subject: [PATCH] v0.2.9-pre.004 --- Cargo.toml | 4 +- .../src/grpc_subscribe.rs | 562 ++++++++++++++++++ crates/ksp-onchain-transport-lib/src/lib.rs | 27 +- .../tests/public_api.rs | 43 +- .../tests/release_completeness.rs | 47 +- .../unit_tests/grpc_subscribe.rs | 125 ++++ deltas/0.2.9/pre.004.md | 250 ++++++++ .../plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md | 37 +- .../validation/012-V0_2_9_YELLOWSTONE_GRPC.md | 124 ++-- 9 files changed, 1163 insertions(+), 56 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs create mode 100644 crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs create mode 100644 deltas/0.2.9/pre.004.md diff --git a/Cargo.toml b/Cargo.toml index a7012ae..aa0031e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 242 +# version: 243 [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.fix.1" +version = "0.2.9-pre.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs b/crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs new file mode 100644 index 0000000..5c890bd --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs @@ -0,0 +1,562 @@ +// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs +// version: 1 + +const MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT: usize = 128; +const MAX_GRPC_SUBSCRIBE_DATA_SLICE_LENGTH_BYTES: u64 = 64 * 1024 * 1024; +const MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT: usize = 1_024; +const MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES: usize = 128; + +/// Validated logical filter name used by the standard Yellowstone `SubscribeRequest` maps. +/// +/// Filter names are returned by Yellowstone in update envelopes. KSP therefore keeps them as typed public data, but omits their text from `Debug` so routine +/// diagnostics cannot accidentally disclose caller-defined labels. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct YellowstoneSubscribeFilterName { + value: std::string::String, +} + +impl YellowstoneSubscribeFilterName { + /// Validates one provider-neutral Yellowstone filter-group name. + pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { + let value = value.into(); + if value.is_empty() || value.len() > MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES || value.trim() != value || value.chars().any(char::is_control) { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter name is invalid") + .with_context("field", "grpc_subscribe.filter_name"), + ); + } + return std::result::Result::Ok(Self { value }); + } + + /// Returns the validated filter name text. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } +} + +impl std::fmt::Debug for YellowstoneSubscribeFilterName { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("YellowstoneSubscribeFilterName()"); + } +} + +/// One standard Yellowstone account-data slice applied to subscribed account payloads. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneAccountsDataSlice { + offset: u64, + length: u64, +} + +impl YellowstoneAccountsDataSlice { + /// Creates and validates one account-data slice. + pub fn new(offset: u64, length: u64) -> ksp_core_lib::Result { + if length > MAX_GRPC_SUBSCRIBE_DATA_SLICE_LENGTH_BYTES || offset.checked_add(length).is_none() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice exceeds KSP bounds") + .with_context("field", "grpc_subscribe.accounts_data_slice"), + ); + } + return std::result::Result::Ok(Self { offset, length }); + } + + /// Returns the byte offset. + #[must_use] + pub const fn offset(self) -> u64 { + return self.offset; + } + + /// Returns the requested byte length. + #[must_use] + pub const fn length(self) -> u64 { + return self.length; + } + + fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice { + return yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice { offset: self.offset, length: self.length }; + } +} + +/// Optional ping mutation carried inside the standard Yellowstone subscribe stream. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct YellowstoneSubscribePing { + id: i32, +} + +impl YellowstoneSubscribePing { + /// Creates one subscribe-stream ping request. + #[must_use] + pub const fn new(id: i32) -> Self { + return Self { id }; + } + + /// Returns the exact ping identifier carried on the wire. + #[must_use] + pub const fn id(self) -> i32 { + return self.id; + } + + fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestPing { + return yellowstone_grpc_proto::geyser::SubscribeRequestPing { id: self.id }; + } +} + +/// Account-family filter-group shell for the standard Yellowstone subscribe surface. +/// +/// `0.2.9-pre.004` intentionally materializes only the top-level map contract. Account selectors and account-specific filter oneofs are added by `pre.005`. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeAccountFilter { + _private: (), +} + +impl YellowstoneSubscribeAccountFilter { + /// Creates an empty account filter group. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default(); + } +} + +/// Slot-family filter-group shell for the standard Yellowstone subscribe surface. +/// +/// Slot optional flags and update decoding are added by `0.2.9-pre.005`. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeSlotFilter { + _private: (), +} + +impl YellowstoneSubscribeSlotFilter { + /// Creates an empty slot filter group. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default(); + } +} + +/// Transaction-family filter-group shell shared by `transactions` and `transactions_status`. +/// +/// Transaction selectors, Cuckoo filters and token-account expansion are added by `0.2.9-pre.006`. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeTransactionFilter { + _private: (), +} + +impl YellowstoneSubscribeTransactionFilter { + /// Creates an empty transaction filter group. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default(); + } +} + +/// Block-family filter-group shell for the standard Yellowstone subscribe surface. +/// +/// Block selectors, include flags and Cuckoo filters are added by `0.2.9-pre.007`. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeBlockFilter { + _private: (), +} + +impl YellowstoneSubscribeBlockFilter { + /// Creates an empty block filter group. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default(); + } +} + +/// Named empty filter activating the standard Yellowstone `blocks_meta` family. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeBlocksMetaFilter { + _private: (), +} + +impl YellowstoneSubscribeBlocksMetaFilter { + /// Creates the empty marker filter. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta::default(); + } +} + +/// Named empty filter activating the standard Yellowstone `entry` family. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct YellowstoneSubscribeEntryFilter { + _private: (), +} + +impl YellowstoneSubscribeEntryFilter { + /// Creates the empty marker filter. + #[must_use] + pub const fn new() -> Self { + return Self { _private: () }; + } + + fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry { + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry::default(); + } +} + +/// Provider-neutral standard Yellowstone subscribe request owned by KSP. +/// +/// The seven upstream maps are represented independently and retain named empty entries. An entirely empty map is the logical KSP representation of no active +/// filter in that family; protobuf map encoding does not distinguish an omitted map from an empty map. Filter-group names are globally unique across all seven +/// maps so the names echoed by `SubscribeUpdate.filters` remain unambiguous. `Debug` exposes only counts and common scalar options, never filter names or future +/// filter payloads. +#[derive(Clone, Eq, PartialEq)] +pub struct YellowstoneSubscribeRequest { + accounts: std::collections::BTreeMap, + slots: std::collections::BTreeMap, + transactions: std::collections::BTreeMap, + transactions_status: std::collections::BTreeMap, + blocks: std::collections::BTreeMap, + blocks_meta: std::collections::BTreeMap, + entry: std::collections::BTreeMap, + commitment: std::option::Option, + accounts_data_slice: std::vec::Vec, + ping: std::option::Option, + from_slot: std::option::Option, +} + +impl YellowstoneSubscribeRequest { + /// Creates an empty subscribe request. Empty requests are valid because later bidi lifecycle code uses request mutations to clear filters or carry ping state. + #[must_use] + pub fn new() -> Self { + return Self { + accounts: std::collections::BTreeMap::new(), + slots: std::collections::BTreeMap::new(), + transactions: std::collections::BTreeMap::new(), + transactions_status: std::collections::BTreeMap::new(), + blocks: std::collections::BTreeMap::new(), + blocks_meta: std::collections::BTreeMap::new(), + entry: std::collections::BTreeMap::new(), + commitment: std::option::Option::None, + accounts_data_slice: std::vec::Vec::new(), + ping: std::option::Option::None, + from_slot: std::option::Option::None, + }; + } + + /// Inserts one named account filter group. + pub fn insert_account_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeAccountFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.accounts.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named slot filter group. + pub fn insert_slot_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeSlotFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.slots.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named transaction filter group. + pub fn insert_transaction_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeTransactionFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.transactions.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named transaction-status filter group. + pub fn insert_transaction_status_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeTransactionFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.transactions_status.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named block filter group. + pub fn insert_block_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeBlockFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.blocks.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named empty `blocks_meta` filter group. + pub fn insert_blocks_meta_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeBlocksMetaFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.blocks_meta.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Inserts one named empty `entry` filter group. + pub fn insert_entry_filter( + &mut self, + name: crate::YellowstoneSubscribeFilterName, + filter: crate::YellowstoneSubscribeEntryFilter, + ) -> ksp_core_lib::Result<()> { + let validation = self.validate_new_filter_name(&name); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + self.entry.insert(name, filter); + return std::result::Result::Ok(()); + } + + /// Sets or clears the optional standard Yellowstone commitment. + pub fn set_commitment(&mut self, commitment: std::option::Option) { + self.commitment = commitment; + } + + /// Adds one validated account-data slice while preserving insertion order. + pub fn push_accounts_data_slice(&mut self, slice: crate::YellowstoneAccountsDataSlice) -> ksp_core_lib::Result<()> { + if self.accounts_data_slice.len() >= MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice count exceeds KSP bounds") + .with_context("field", "grpc_subscribe.accounts_data_slice"), + ); + } + self.accounts_data_slice.push(slice); + return std::result::Result::Ok(()); + } + + /// Sets or clears the optional subscribe-stream ping mutation. + pub fn set_ping(&mut self, ping: std::option::Option) { + self.ping = ping; + } + + /// Sets or clears the optional replay starting slot. + pub fn set_from_slot(&mut self, from_slot: std::option::Option) { + self.from_slot = from_slot; + } + + /// Returns the number of active account filter groups. + #[must_use] + pub fn account_filter_count(&self) -> usize { + return self.accounts.len(); + } + + /// Returns the number of active slot filter groups. + #[must_use] + pub fn slot_filter_count(&self) -> usize { + return self.slots.len(); + } + + /// Returns the number of active transaction filter groups. + #[must_use] + pub fn transaction_filter_count(&self) -> usize { + return self.transactions.len(); + } + + /// Returns the number of active transaction-status filter groups. + #[must_use] + pub fn transaction_status_filter_count(&self) -> usize { + return self.transactions_status.len(); + } + + /// Returns the number of active block filter groups. + #[must_use] + pub fn block_filter_count(&self) -> usize { + return self.blocks.len(); + } + + /// Returns the number of active block-meta filter groups. + #[must_use] + pub fn blocks_meta_filter_count(&self) -> usize { + return self.blocks_meta.len(); + } + + /// Returns the number of active entry filter groups. + #[must_use] + pub fn entry_filter_count(&self) -> usize { + return self.entry.len(); + } + + /// Returns the optional request commitment. + #[must_use] + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + + /// Returns account-data slices in exact wire order. + #[must_use] + pub fn accounts_data_slices(&self) -> &[crate::YellowstoneAccountsDataSlice] { + return self.accounts_data_slice.as_slice(); + } + + /// Returns the optional subscribe-stream ping mutation. + #[must_use] + pub const fn ping(&self) -> std::option::Option { + return self.ping; + } + + /// Returns the optional replay starting slot. + #[must_use] + pub const fn from_slot(&self) -> std::option::Option { + return self.from_slot; + } + + /// Validates all deterministic common subscribe-request bounds before any network I/O. + pub fn validate(&self) -> ksp_core_lib::Result<()> { + if self.total_filter_count() > MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter-group count exceeds KSP bounds") + .with_context("field", "grpc_subscribe.filter_groups"), + ); + } + if self.accounts_data_slice.len() > MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice count exceeds KSP bounds") + .with_context("field", "grpc_subscribe.accounts_data_slice"), + ); + } + return std::result::Result::Ok(()); + } + + fn to_wire(&self) -> ksp_core_lib::Result { + let validation = self.validate(); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(yellowstone_grpc_proto::geyser::SubscribeRequest { + accounts: self.accounts.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(), + slots: self.slots.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(), + transactions: self.transactions.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(), + transactions_status: self.transactions_status.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(), + blocks: self.blocks.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(), + blocks_meta: self.blocks_meta.iter().map(|(name, filter)| return (name.as_str().to_owned(), (*filter).to_wire())).collect(), + entry: self.entry.iter().map(|(name, filter)| return (name.as_str().to_owned(), (*filter).to_wire())).collect(), + commitment: commitment_to_wire(self.commitment), + accounts_data_slice: self.accounts_data_slice.iter().copied().map(crate::YellowstoneAccountsDataSlice::to_wire).collect(), + ping: self.ping.map(crate::YellowstoneSubscribePing::to_wire), + from_slot: self.from_slot, + }); + } + + fn total_filter_count(&self) -> usize { + return self.accounts.len() + + self.slots.len() + + self.transactions.len() + + self.transactions_status.len() + + self.blocks.len() + + self.blocks_meta.len() + + self.entry.len(); + } + + fn contains_filter_name(&self, name: &crate::YellowstoneSubscribeFilterName) -> bool { + return self.accounts.contains_key(name) + || self.slots.contains_key(name) + || self.transactions.contains_key(name) + || self.transactions_status.contains_key(name) + || self.blocks.contains_key(name) + || self.blocks_meta.contains_key(name) + || self.entry.contains_key(name); + } + + fn validate_new_filter_name(&self, name: &crate::YellowstoneSubscribeFilterName) -> ksp_core_lib::Result<()> { + if self.contains_filter_name(name) { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter name must be globally unique") + .with_context("field", "grpc_subscribe.filter_name"), + ); + } + if self.total_filter_count() >= MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter-group count exceeds KSP bounds") + .with_context("field", "grpc_subscribe.filter_groups"), + ); + } + return std::result::Result::Ok(()); + } +} + +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, + }; + }); +} + +impl std::default::Default for YellowstoneSubscribeRequest { + fn default() -> Self { + return Self::new(); + } +} + +impl std::fmt::Debug for YellowstoneSubscribeRequest { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneSubscribeRequest") + .field("account_filter_count", &self.accounts.len()) + .field("slot_filter_count", &self.slots.len()) + .field("transaction_filter_count", &self.transactions.len()) + .field("transaction_status_filter_count", &self.transactions_status.len()) + .field("block_filter_count", &self.blocks.len()) + .field("blocks_meta_filter_count", &self.blocks_meta.len()) + .field("entry_filter_count", &self.entry.len()) + .field("commitment", &self.commitment) + .field("accounts_data_slice_count", &self.accounts_data_slice.len()) + .field("ping", &self.ping) + .field("from_slot", &self.from_slot) + .finish(); + } +} + +#[cfg(test)] +#[path = "../unit_tests/grpc_subscribe.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index 38e9025..e03be21 100644 --- a/crates/ksp-onchain-transport-lib/src/lib.rs +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/lib.rs -// version: 36 +// version: 37 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -34,7 +34,9 @@ //! `0.2.9-pre.002` opens the Yellowstone gRPC N1 engine foundation with Transport-owned redacted settings, bounded reconnect/channel/message policies, the //! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types. //! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs -//! through KSP-owned DTOs. Streaming `Subscribe` remains deliberately deferred to `pre.004`. +//! through KSP-owned DTOs. +//! `0.2.9-pre.004` materializes the provider-neutral standard `SubscribeRequest` foundation: all seven named filter maps, global filter-name bounds/uniqueness, +//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot/transaction/block filter fields remain staged for `pre.005–007`. mod client; mod constants; @@ -42,6 +44,7 @@ mod error; mod executor; mod grpc_channel; mod grpc_settings; +mod grpc_subscribe; mod grpc_unary; mod json_rpc; mod pool; @@ -131,6 +134,26 @@ pub use self::grpc_settings::YellowstoneGrpcReconnectSettings; pub use self::grpc_settings::YellowstoneGrpcSessionSettings; /// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine. pub use self::grpc_settings::YellowstoneGrpcTransportSettings; +/// One standard Yellowstone account-data slice. +pub use self::grpc_subscribe::YellowstoneAccountsDataSlice; +/// Account-family filter-group shell for standard Yellowstone Subscribe. +pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter; +/// Block-family filter-group shell for standard Yellowstone Subscribe. +pub use self::grpc_subscribe::YellowstoneSubscribeBlockFilter; +/// Empty filter marker activating the standard Yellowstone blocks-meta family. +pub use self::grpc_subscribe::YellowstoneSubscribeBlocksMetaFilter; +/// Empty filter marker activating the standard Yellowstone entry family. +pub use self::grpc_subscribe::YellowstoneSubscribeEntryFilter; +/// Validated globally unique logical filter name for standard Yellowstone Subscribe maps. +pub use self::grpc_subscribe::YellowstoneSubscribeFilterName; +/// Optional ping mutation carried by the standard Yellowstone Subscribe request. +pub use self::grpc_subscribe::YellowstoneSubscribePing; +/// Provider-neutral standard Yellowstone Subscribe request. +pub use self::grpc_subscribe::YellowstoneSubscribeRequest; +/// Slot-family filter-group shell for standard Yellowstone Subscribe. +pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter; +/// Transaction-family filter-group shell shared by transactions and transaction-status maps. +pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter; /// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel. pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient; /// Block height returned by the standard Yellowstone unary surface. diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 6192613..db5beeb 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 41 +// version: 42 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -825,3 +825,44 @@ fn public_v0_2_9_pre_003_yellowstone_metadata_and_seven_unary_contracts_are_avai assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.domain(), "onchain_transport"); assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.code(), "grpc_status"); } + +#[test] +fn public_v0_2_9_pre_004_yellowstone_subscribe_common_contract_is_available_from_crate_root() { + let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new(); + let account_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("accounts").expect("account filter name must validate"); + let slot_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots").expect("slot filter name must validate"); + let transaction_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transactions").expect("transaction filter name must validate"); + let transaction_status_name = + ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transaction-status").expect("transaction-status filter name must validate"); + let block_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("block filter name must validate"); + let block_meta_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks-meta").expect("block-meta filter name must validate"); + let entry_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("entry filter name must validate"); + assert!(request.insert_account_filter(account_name, ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new()).is_ok()); + assert!(request.insert_slot_filter(slot_name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()).is_ok()); + assert!(request.insert_transaction_filter(transaction_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()).is_ok()); + assert!( + request + .insert_transaction_status_filter(transaction_status_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()) + .is_ok() + ); + assert!(request.insert_block_filter(block_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new()).is_ok()); + assert!(request.insert_blocks_meta_filter(block_meta_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok()); + assert!(request.insert_entry_filter(entry_name, ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new()).is_ok()); + request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)); + let slice = ksp_onchain_transport_lib::YellowstoneAccountsDataSlice::new(4, 32).expect("account-data slice must validate"); + assert!(request.push_accounts_data_slice(slice).is_ok()); + request.set_ping(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7))); + request.set_from_slot(std::option::Option::Some(99)); + assert!(request.validate().is_ok()); + assert_eq!(request.account_filter_count(), 1); + assert_eq!(request.slot_filter_count(), 1); + assert_eq!(request.transaction_filter_count(), 1); + assert_eq!(request.transaction_status_filter_count(), 1); + assert_eq!(request.block_filter_count(), 1); + assert_eq!(request.blocks_meta_filter_count(), 1); + assert_eq!(request.entry_filter_count(), 1); + assert_eq!(request.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)); + assert_eq!(request.accounts_data_slices(), &[slice]); + assert_eq!(request.ping(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7))); + assert_eq!(request.from_slot(), std::option::Option::Some(99)); +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 64732b9..9cf7b66 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 34 +// version: 35 //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. @@ -1066,3 +1066,48 @@ fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_met assert!(!crate_root.contains("pub use yellowstone_grpc_proto")); let _client = std::any::type_name::(); } + +#[test] +fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() { + let source = include_str!("../src/grpc_subscribe.rs"); + let crate_root = include_str!("../src/lib.rs"); + for wire_field in [ + "accounts:", + "slots:", + "transactions:", + "transactions_status:", + "blocks:", + "blocks_meta:", + "entry:", + "commitment:", + "accounts_data_slice:", + "ping:", + "from_slot:", + ] { + assert!(source.contains(wire_field), "missing standard Yellowstone SubscribeRequest field: {wire_field}"); + } + assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT")); + assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES")); + assert!(source.contains("MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT")); + assert!(source.contains("offset.checked_add(length)")); + assert!(source.contains("globally unique")); + assert!(source.contains("SubscribeRequestFilterBlocksMeta")); + assert!(source.contains("SubscribeRequestFilterEntry")); + assert!(source.contains("YellowstoneSubscribeAccountFilter")); + assert!(source.contains("YellowstoneSubscribeSlotFilter")); + assert!(source.contains("YellowstoneSubscribeTransactionFilter")); + assert!(source.contains("YellowstoneSubscribeBlockFilter")); + assert!(!source.contains("SubscribeDeshred")); + assert!(!source.contains("PublicNode")); + assert!(!source.contains("OrbitFlare")); + assert!(!source.contains("Helius")); + assert!(!source.contains("pub fn to_wire")); + assert!(!source.contains("pub(crate) fn to_wire")); + assert!(!crate_root.contains("pub use tonic")); + assert!(!crate_root.contains("pub use yellowstone_grpc_proto")); + let _request = std::any::type_name::(); + let _account = std::any::type_name::(); + let _slot = std::any::type_name::(); + let _transaction = std::any::type_name::(); + let _block = std::any::type_name::(); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs b/crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs new file mode 100644 index 0000000..6af4ce4 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs @@ -0,0 +1,125 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs +// version: 1 + +fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName { + return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate"); +} + +#[test] +fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() { + assert!(crate::YellowstoneSubscribeFilterName::new("").is_err()); + assert!(crate::YellowstoneSubscribeFilterName::new(" leading").is_err()); + assert!(crate::YellowstoneSubscribeFilterName::new("trailing ").is_err()); + assert!(crate::YellowstoneSubscribeFilterName::new("line\nbreak").is_err()); + assert!(crate::YellowstoneSubscribeFilterName::new("x".repeat(129)).is_err()); + let name = filter_name("account-primary"); + assert_eq!(name.as_str(), "account-primary"); + assert!(!format!("{name:?}").contains("account-primary")); + let mut request = crate::YellowstoneSubscribeRequest::new(); + assert!(request.insert_account_filter(name.clone(), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); + let duplicate = request.insert_slot_filter(name, crate::YellowstoneSubscribeSlotFilter::new()); + assert!(duplicate.is_err()); +} + +#[test] +fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() { + let empty = crate::YellowstoneSubscribeRequest::new().to_wire().expect("empty request must encode"); + assert!(empty.accounts.is_empty()); + assert!(empty.slots.is_empty()); + assert!(empty.transactions.is_empty()); + assert!(empty.transactions_status.is_empty()); + assert!(empty.blocks.is_empty()); + assert!(empty.blocks_meta.is_empty()); + assert!(empty.entry.is_empty()); + assert_eq!(empty.commitment, std::option::Option::None); + assert!(empty.accounts_data_slice.is_empty()); + assert_eq!(empty.ping, std::option::Option::None); + assert_eq!(empty.from_slot, std::option::Option::None); + let mut request = crate::YellowstoneSubscribeRequest::new(); + assert!(request.insert_account_filter(filter_name("accounts"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); + assert!(request.insert_slot_filter(filter_name("slots"), crate::YellowstoneSubscribeSlotFilter::new()).is_ok()); + assert!(request.insert_transaction_filter(filter_name("transactions"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok()); + assert!(request.insert_transaction_status_filter(filter_name("transaction-status"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok()); + assert!(request.insert_block_filter(filter_name("blocks"), crate::YellowstoneSubscribeBlockFilter::new()).is_ok()); + assert!(request.insert_blocks_meta_filter(filter_name("blocks-meta"), crate::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok()); + assert!(request.insert_entry_filter(filter_name("entry"), crate::YellowstoneSubscribeEntryFilter::new()).is_ok()); + let wire = request.to_wire().expect("named empty request must encode"); + assert_eq!(wire.accounts.len(), 1); + assert_eq!(wire.slots.len(), 1); + assert_eq!(wire.transactions.len(), 1); + assert_eq!(wire.transactions_status.len(), 1); + assert_eq!(wire.blocks.len(), 1); + assert_eq!(wire.blocks_meta.len(), 1); + assert_eq!(wire.entry.len(), 1); + assert_eq!(wire.accounts.get("accounts"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default())); + assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default())); + assert_eq!( + wire.transactions.get("transactions"), + std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()) + ); + assert_eq!( + wire.transactions_status.get("transaction-status"), + std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()) + ); + assert_eq!(wire.blocks.get("blocks"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default())); + assert!(wire.blocks_meta.contains_key("blocks-meta")); + assert!(wire.entry.contains_key("entry")); +} + +#[test] +fn yellowstone_subscribe_common_fields_preserve_optional_and_ordered_wire_semantics() { + let mut request = crate::YellowstoneSubscribeRequest::new(); + request.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Finalized)); + let first = crate::YellowstoneAccountsDataSlice::new(8, 16).expect("first slice must validate"); + let second = crate::YellowstoneAccountsDataSlice::new(64, 0).expect("zero-length slice remains representable"); + assert!(request.push_accounts_data_slice(first).is_ok()); + assert!(request.push_accounts_data_slice(second).is_ok()); + request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7))); + request.set_from_slot(std::option::Option::Some(42)); + assert_eq!(request.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized)); + assert_eq!(request.accounts_data_slices(), &[first, second]); + assert_eq!(request.ping(), std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7))); + assert_eq!(request.from_slot(), std::option::Option::Some(42)); + let wire = request.to_wire().expect("common fields must encode"); + assert_eq!(wire.commitment, std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32)); + assert_eq!(wire.accounts_data_slice.len(), 2); + assert_eq!(wire.accounts_data_slice[0].offset, 8); + assert_eq!(wire.accounts_data_slice[0].length, 16); + assert_eq!(wire.accounts_data_slice[1].offset, 64); + assert_eq!(wire.accounts_data_slice[1].length, 0); + assert_eq!(wire.ping.map(|ping| return ping.id), std::option::Option::Some(-7)); + assert_eq!(wire.from_slot, std::option::Option::Some(42)); +} + +#[test] +fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() { + assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024).is_ok()); + assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024 + 1).is_err()); + assert!(crate::YellowstoneAccountsDataSlice::new(u64::MAX, 1).is_err()); + let mut slices = crate::YellowstoneSubscribeRequest::new(); + for index in 0..128_u64 { + let slice = crate::YellowstoneAccountsDataSlice::new(index, 1).expect("bounded fixture slice must validate"); + assert!(slices.push_accounts_data_slice(slice).is_ok()); + } + let excess = crate::YellowstoneAccountsDataSlice::new(129, 1).expect("excess fixture slice itself must validate"); + assert!(slices.push_accounts_data_slice(excess).is_err()); + let mut filters = crate::YellowstoneSubscribeRequest::new(); + for index in 0..1_024_u32 { + let name = crate::YellowstoneSubscribeFilterName::new(format!("f{index}")).expect("bounded filter name must validate"); + assert!(filters.insert_account_filter(name, crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); + } + let excess_name = crate::YellowstoneSubscribeFilterName::new("excess").expect("excess filter name must validate independently"); + assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err()); +} + +#[test] +fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() { + let mut request = crate::YellowstoneSubscribeRequest::new(); + assert!(request.insert_account_filter(filter_name("sensitive-label"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); + request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(9))); + request.set_from_slot(std::option::Option::Some(77)); + let debug = format!("{request:?}"); + assert!(!debug.contains("sensitive-label")); + assert!(debug.contains("account_filter_count")); + assert!(debug.contains("from_slot")); +} diff --git a/deltas/0.2.9/pre.004.md b/deltas/0.2.9/pre.004.md new file mode 100644 index 0000000..d097e26 --- /dev/null +++ b/deltas/0.2.9/pre.004.md @@ -0,0 +1,250 @@ + + + +# Delta `0.2.9-pre.004` — fondation `SubscribeRequest` Yellowstone standard + +## 1. Base et signal de version + +Base obligatoire : + +```text +0.2.9-pre.3.fix.1 +``` + +Le gate opérateur fourni ferme `pre.003-fix.001` : + +```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 --workspace PASS +Transport unit 354/354 +Transport public_api 43/43 +Transport release_completeness 36/36 +Transport doctests 4/4 +cargo tree -p ... fourni/relu +cargo tree -p ... -e features fourni/relu +cargo tree -p ... --duplicates fourni/relu +cargo tree --duplicates fourni/relu +``` + +Le graphe pertinent reste unifié sur `tonic 0.14.6`, `tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, `yellowstone-grpc-proto 12.6.0` et `solana-pubkey 4.3.0`. + +Cette tranche non-fix synchronise : + +```text +workspace.package.version = 0.2.9-pre.4 +commit attendu = v0.2.9-pre.004 +aucun tag prerelease +``` + +## 2. Scope exact + +`pre.004` matérialise uniquement la fondation N2 du `SubscribeRequest` standard publié : + +```text +accounts +slots +transactions +transactions_status +blocks +blocks_meta +entry +commitment +accounts_data_slice +ping +from_slot +``` + +Le proto `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant implémentation et conserve exactement ces champs top-level. + +Sont explicitement hors tranche : + +```text +champs détaillés Accounts / Slots -> pre.005 +champs détaillés Transactions / transaction_status -> pre.006 +champs détaillés Blocks / block_meta / entry updates -> pre.007 +stream bidi / mutation / Ping-Pong runtime -> pre.008 +reconnect / replay / gaps / duplicates -> pre.009 +Config V3 / PublicNode -> pre.010/011 +SubscribeDeshred -> OUT 0.2.9 standard +``` + +## 3. Contrat KSP ajouté + +Nouveaux types publics : + +```text +YellowstoneSubscribeFilterName +YellowstoneAccountsDataSlice +YellowstoneSubscribePing +YellowstoneSubscribeAccountFilter +YellowstoneSubscribeSlotFilter +YellowstoneSubscribeTransactionFilter +YellowstoneSubscribeBlockFilter +YellowstoneSubscribeBlocksMetaFilter +YellowstoneSubscribeEntryFilter +YellowstoneSubscribeRequest +``` + +Les quatre filtres de famille riches sont volontairement des shells typés en `pre.004`. Ils permettent de matérialiser les sept maps et les entrées nommées vides sans anticiper les champs propres aux tranches suivantes. + +Le wire `yellowstone_grpc_proto::geyser::SubscribeRequest` reste privé : aucun type Tonic/Prost/Yellowstone n'est réexporté au crate root. + +## 4. Sémantique maps / noms + +KSP conserve sept maps logiques indépendantes. + +```text +map vide = aucun filtre actif dans la famille +entrée nommée + message vide = activation explicite d'un groupe vide supporté par le proto +``` + +Le Protobuf ne distingue pas une map top-level omise d'une map vide ; KSP ne prétend donc pas préserver une différence wire inexistante. + +Les noms sont : + +```text +non vides +trim exact +sans caractère de contrôle +<= 128 octets UTF-8 +uniques globalement entre les sept maps +``` + +L'unicité globale évite l'ambiguïté lorsque Yellowstone renvoie seulement les noms correspondants dans `SubscribeUpdate.filters[]`. + +Le nombre total de groupes nommés est plafonné à `1024` sur l'ensemble des sept maps. + +## 5. Champs communs et bounds + +`commitment` réutilise `SolanaCommitment` et mappe exactement : + +```text +Processed -> PROCESSED +Confirmed -> CONFIRMED +Finalized -> FINALIZED +``` + +`accounts_data_slice` : + +```text +ordre d'insertion conservé +nombre <= 128 +length <= 64 MiB +offset + length doit rester représentable en u64 +length = 0 reste représentable +``` + +`ping` conserve l'identifiant `i32` exact. + +`from_slot` conserve l'optional `u64` sans lui attribuer encore une promesse de replay/lossless ; sa sémantique lifecycle reste en `pre.009`. + +## 6. Diagnostics et sécurité + +`Debug` de `YellowstoneSubscribeFilterName` est redacted. + +`Debug` de `YellowstoneSubscribeRequest` expose uniquement : + +```text +compteurs par famille +commitment +nombre de data slices +ping +from_slot +``` + +Il n'expose aucun nom de filtre ni futur payload account/transaction/block. + +Les erreurs déterministes utilisent `ERROR_CODE_INVALID_RPC_PARAMETERS` et n'incluent pas les valeurs de noms/payloads rejetés. + +## 7. Dépendances + +Aucune dépendance ni feature Cargo n'est modifiée dans `pre.004`. + +Les graphes fournis pour `pre.003-fix.001` restent l'autorité courante ; aucun nouveau `cargo tree` n'est requis spécifiquement par ce delta sauf anomalie de compilation. + +## 8. Tests/canaries ajoutés + +Unit tests : + +```text +nom vide/whitespace/control/oversized rejeté +Debug nom redacted +unicité globale cross-family +request vide -> sept maps vides + common absent +sept entrées nommées vides -> wire exact +commitment/ping/from_slot exacts +ordre data slices conservé +zero-length slice conservée +slice length/overflow/count bornés +filter-group count borné +request Debug sans filter name +``` + +Public API : construction des sept maps et champs communs depuis le crate root. + +Release completeness : + +```text +onze champs top-level présents dans l'adapter +bounds communs matérialisés +aucun SubscribeDeshred +aucun PublicNode/OrbitFlare/Helius runtime +aucun raw Tonic/Yellowstone reexport +to_wire protobuf non public +``` + +## 9. Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs +crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs +deltas/0.2.9/pre.004.md +``` + +## 10. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md +docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +``` + +## 11. Validation de préparation + +```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 compilation n'est déclarée réussie localement. + +## 12. Gate opérateur attendu + +```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 +``` + +Attendu si aucun fix n'est nécessaire : + +```text +Transport unit 359 +Transport public_api 44 +Transport release_completeness 37 +Transport doctests 4 +``` + +Les live smokes restent ignored/opt-in. 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 b87f703..44031ff 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-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.** +> **Statut : `0.2.9-pre.004` candidate. `pre.003-fix.001` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 354 unit + 43 public API + 36 completeness + 4 doctests, et graphes Cargo inspectés sans seconde version Tonic/Prost/Solana. `pre.004` matérialise uniquement la fondation `SubscribeRequest` standard : sept maps nommées, commitment, account-data slices ordonnées, ping, `from_slot`, bornes communes, unicité globale des noms et Debug sans payload arbitraire. Les champs détaillés Accounts/Slots/Transactions/Blocks restent réservés à `pre.005–007` et aucun stream bidi n'est encore ouvert. 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 @@ -431,7 +431,18 @@ ping? id from_slot? u64 ``` -KSP appliquera des bornes déterministes avant I/O sur les noms, cardinalités, listes de comptes/owners, memcmp, data slices et tailles de payload. Les valeurs exactes sont un contrat KSP et non une copie aveugle des quotas d'un provider ; elles seront matérialisées avec tests en `pre.004`. +KSP applique des bornes déterministes avant I/O sur les noms, cardinalités, listes de comptes/owners, memcmp, data slices et tailles de payload. Les valeurs exactes sont un contrat KSP et non une copie aveugle des quotas d'un provider. `pre.004` matérialise les bornes communes suivantes : + +```text +filter groups nommés, total <= 1024 sur les sept maps +filter name non vide, trim exact, sans caractère de contrôle, <= 128 octets +filter names uniques globalement entre les sept maps +accounts_data_slice count <= 128 +accounts_data_slice length <= 64 MiB +offset + length aucun overflow u64 +``` + +Les bornes account/owner/include/exclude/required, memcmp et Cuckoo restent volontairement dans les tranches de famille `pre.005–007`. ## 8. Matrice `SubscribeUpdate` @@ -905,11 +916,11 @@ 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 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.003 DONE — moteur TLS/metadata + façade N2 unary + fixture locale + 7 unary RPCs + budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS + graphes Cargo inspectés -pre.004 standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds - budget : 15–20 min ; preuve : omitted/empty/oneof exact + rejets avant I/O +pre.004 CANDIDATE — standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds + budget : 15–20 min ; preuve : map vide/entrée nommée vide/common wire exact + rejets déterministes avant I/O pre.005 standard Solana : Accounts + Slots filters/updates budget : 15–20 min ; preuve : fixtures exactes + enum/optional/malformed/adversarial @@ -973,14 +984,16 @@ crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs crates/ksp-onchain-transport-lib/src/grpc_channel.rs crates/ksp-onchain-transport-lib/src/grpc_settings.rs -# pre.004+ -crates/ksp-onchain-transport-lib/src/grpc_session.rs -crates/ksp-onchain-transport-lib/src/grpc_protocol.rs +# pre.004 crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs -crates/ksp-onchain-transport-lib/src/grpc_updates.rs -unit_tests/ correspondants +crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs tests/public_api.rs tests/release_completeness.rs + +# pre.005+ +crates/ksp-onchain-transport-lib/src/grpc_session.rs +crates/ksp-onchain-transport-lib/src/grpc_updates.rs +unit_tests/ correspondants tests/yellowstone_grpc_*_smoke.rs crates/ksp-config-lib/src/transport.rs config/std.transport.json diff --git a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md index 1779d81..e67cf00 100644 --- a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +++ b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md @@ -3,7 +3,7 @@ # Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode -> **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é.** +> **Statut : `pre.003-fix.001` est fermé sur gate opérateur intégralement vert et graphes Cargo inspectés. `0.2.9-pre.004` est candidate et ajoute uniquement la fondation standard `SubscribeRequest` : sept maps, commitment, account-data slices, ping, `from_slot`, bornes communes et redaction Debug. Aucun stream bidi n'est encore ouvert ; les filtres détaillés Accounts/Slots/Transactions/Blocks restent réservés à `pre.005–007`. 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 @@ -55,17 +55,17 @@ crate proto : 12.6.0 ## 3. Matrice service `Geyser` -| RPC | Forme | Classification | Scope | Preuve cible | État | -|-----------------------|-------|------------------------------------------------------|-------|-----------------------|------| -| `Subscribe` | bidi | standard | IN | fixture locale + live | TODO | -| `SubscribeDeshred` | bidi | Triton extension/pré-exécution malgré présence proto | OUT | canari d'absence/API | TODO | -| `SubscribeReplayInfo` | unary | standard | IN | fixture unary | TODO | -| `Ping` | unary | standard | IN | fixture unary | TODO | -| `GetLatestBlockhash` | unary | standard | IN | fixture unary | TODO | -| `GetBlockHeight` | unary | standard | IN | fixture unary | TODO | -| `GetSlot` | unary | standard | IN | fixture unary | TODO | -| `IsBlockhashValid` | unary | standard | IN | fixture unary | TODO | -| `GetVersion` | unary | standard | IN | fixture unary | TODO | +| RPC | Forme | Classification | Scope | Preuve cible | État | +|-----------------------|-------|------------------------------------------------------|-------|-----------------------|-----------------------------------------------| +| `Subscribe` | bidi | standard | IN | fixture locale + live | PARTIAL pre.004 request / stream TODO pre.008 | +| `SubscribeDeshred` | bidi | Triton extension/pré-exécution malgré présence proto | OUT | canari d'absence/API | OUT | +| `SubscribeReplayInfo` | unary | standard | IN | fixture unary | DONE pre.003 | +| `Ping` | unary | standard | IN | fixture unary | DONE pre.003 | +| `GetLatestBlockhash` | unary | standard | IN | fixture unary | DONE pre.003 | +| `GetBlockHeight` | unary | standard | IN | fixture unary | DONE pre.003 | +| `GetSlot` | unary | standard | IN | fixture unary | DONE pre.003 | +| `IsBlockhashValid` | unary | standard | IN | fixture unary | DONE pre.003 | +| `GetVersion` | unary | standard | IN | fixture unary | DONE pre.003 | ## 4. `SubscribeRequest` — coverage normative @@ -265,11 +265,11 @@ DONE/source+tests ajoutés close timeout > 0 et plafonné DONE/source+tests ajoutés max inbound/outbound > 0 et plafonnés DONE/source+tests ajoutés request/update queue capacities > 0 et plafonnées DONE/source+tests ajoutés reconnect attempt/backoff bornés -TODO pre.004 filter-group count -TODO pre.004 filter-name length/uniqueness +CANDIDATE pre.004 filter-group count <= 1024 total sur les sept maps +CANDIDATE pre.004 filter-name non vide/trim/control-free <= 128 octets + unicité globale DONE/source+tests ajoutés metadata ASCII key/value count/size + reserved/bin bounds TODO pre.005+ account/owner/include/exclude/required counts -TODO pre.004+ memcmp/data slice bounds +CANDIDATE pre.004 data slice count <= 128, length <= 64 MiB, offset+length sans overflow ; memcmp TODO pre.005 TODO pre.005+ Cuckoo dimensions/data bounds ``` @@ -279,32 +279,32 @@ Security canaries : DONE/source+tests ajoutés endpoint URL Debug redacted DONE/source+tests ajoutés metadata publique/secrète redacted ; secret marqué sensitive DONE/source+fixture Status message/details/metadata distants exclus des KspError -CANDIDATE pre.003 connect réel + TLS WebPKI configuré ; erreur connect/TLS safe, Cargo pending -TODO pre.004+ request/update Debug sans payload arbitraire sensible +DONE pre.003/fix.001 connect réel + TLS WebPKI configuré ; erreur connect/TLS safe, gate Cargo PASS +CANDIDATE pre.004 SubscribeRequest Debug sans filter names/payload arbitraire ; update Debug TODO pre.005+ DONE pre.003 source/tests channel/client Debug sans URL, metadata value ni raw Tonic ; lifecycle stream ultérieur ``` ## 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 | -| 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 | +| 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 | DONE unary ; usage reconnect 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 : @@ -404,8 +404,8 @@ Les autres providers restent en TODO/IDEAS sans release dédiée. Aucune façade ```text pre.001 DONE audit/sizing/architecture 15–20 min nominal pre.002 DONE moteur: deps/settings/errors/channel 15–20 min ; gate final fix.002 PASS -pre.003 CANDIDATE TLS/metadata + fixture + 7 unary standard 15–20 min -pre.004 TODO standard: Subscribe common/from_slot/bounds 15–20 min +pre.003 DONE TLS/metadata + fixture + 7 unary standard 15–20 min ; gate final fix.001 PASS +pre.004 CANDIDATE standard: Subscribe common/from_slot/bounds 15–20 min pre.005 TODO standard: accounts + slots 15–20 min pre.006 TODO standard: transactions + transaction_status 15–20 min pre.007 TODO standard: blocks + block_meta + entry 15–20 min @@ -583,3 +583,51 @@ 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.** + +### 18.2 Gate final opérateur `pre.003-fix.001` + +| Gate | Résultat final | +|-------------------------------------------|----------------| +| `cargo fmt --all` | PASS | +| audit Rust workspace | PASS / clean | +| `cargo check --workspace` | PASS | +| `cargo clippy --workspace --all-targets` | PASS | +| Transport unit | 354/354 PASS | +| Transport `public_api` | 43/43 PASS | +| Transport `release_completeness` | 36/36 PASS | +| Transport doctests | 4/4 PASS | +| `cargo test --workspace` | PASS | +| `cargo tree -p ...` | fourni/relu | +| `cargo tree -p ... -e features` | fourni/relu | +| `cargo tree -p ... --duplicates` | fourni/relu | +| `cargo tree --duplicates` | fourni/relu | + +Graphe pertinent confirmé : `tonic 0.14.6`, `tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, `yellowstone-grpc-proto 12.6.0`, `solana-pubkey 4.3.0`. Les features serveur/codegen et `yellowstone-grpc-proto/tonic` restent liées à la fixture dev/test ; aucune seconde version Tonic/Prost/Solana n'est introduite par la tranche. + +**Verdict : `pre.003` fermée.** + +## 19. Gate `pre.004` — candidate + +| Surface | État candidate | +|-----------------------------------------------------|----------------| +| workspace version | `0.2.9-pre.4` | +| sept maps `SubscribeRequest` | SOURCE PASS | +| map vide / entrée nommée vide | SOURCE+TEST | +| commitment optional exact | SOURCE+TEST | +| account-data slices ordonnées | SOURCE+TEST | +| ping optional exact | SOURCE+TEST | +| `from_slot` optional exact | SOURCE+TEST | +| filter names <=128 octets / unicité globale | SOURCE+TEST | +| filter groups <=1024 total | SOURCE+TEST | +| slices <=128 / length <=64 MiB / overflow rejeté | SOURCE+TEST | +| request Debug sans noms/payloads de filtres | SOURCE+TEST | +| filtres Accounts/Slots détaillés | OUT pre.004 | +| filtres Transactions/Blocks détaillés | OUT pre.004 | +| stream bidi / lifecycle / updates | OUT pre.004 | +| PublicNode / Config V3 | OUT pre.004 | +| audit Rust workspace local | PASS / clean | +| fmt/check/Clippy/tests | opérateur TODO | + +Le proto publié `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant implémentation : les onze champs top-level retenus restent `accounts`, `slots`, `transactions`, `transactions_status`, `blocks`, `blocks_meta`, `entry`, `commitment`, `accounts_data_slice`, `ping`, `from_slot`. Aucun `SubscribeDeshred` n'entre dans la surface KSP. + +**Verdict `pre.004` : candidate source prête ; fermeture après gate Cargo opérateur.**