From affa18a6e9090b99630b3ea5cdcf4fc0ff5c6a33 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 24 Aug 2026 16:16:48 +0200 Subject: [PATCH] v0.2.9-pre.008 --- Cargo.toml | 4 +- .../src/grpc_subscribe.rs | 673 +++++++++++++++++- crates/ksp-onchain-transport-lib/src/lib.rs | 14 +- .../tests/public_api.rs | 46 +- .../tests/release_completeness.rs | 53 +- .../unit_tests/grpc_subscribe.rs | 225 +++++- deltas/0.2.9/pre.008.md | 73 ++ .../plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md | 70 +- .../validation/012-V0_2_9_YELLOWSTONE_GRPC.md | 80 ++- 9 files changed, 1198 insertions(+), 40 deletions(-) create mode 100644 deltas/0.2.9/pre.008.md diff --git a/Cargo.toml b/Cargo.toml index 6c38022..29179d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 246 +# version: 247 [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.7" +version = "0.2.9-pre.8" 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 index be38a58..1bdee46 100644 --- a/crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs +++ b/crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs @@ -1,6 +1,10 @@ // file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs -// version: 5 +// version: 6 +#[cfg(test)] +const MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES: usize = 128; +#[cfg(test)] +const MAX_GRPC_BLOCK_VECTOR_COUNT: usize = 65_536; #[cfg(test)] const MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES: usize = 512 * 1024 * 1024; const MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT: usize = 256; @@ -1945,24 +1949,478 @@ impl std::fmt::Debug for YellowstoneTransactionStatusUpdate { } } -/// 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.008`. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +/// Complete current block-family filter for the standard Yellowstone subscribe surface. +#[derive(Clone, Eq, PartialEq)] pub struct YellowstoneSubscribeBlockFilter { - _private: (), + account_include: std::vec::Vec, + include_transactions: std::option::Option, + include_accounts: std::option::Option, + include_entries: std::option::Option, + cuckoo_account_include: std::option::Option, } impl YellowstoneSubscribeBlockFilter { /// Creates an empty block filter group. #[must_use] - pub const fn new() -> Self { - return Self { _private: () }; + pub fn new() -> Self { + return Self { + account_include: std::vec::Vec::new(), + include_transactions: std::option::Option::None, + include_accounts: std::option::Option::None, + include_entries: std::option::Option::None, + cuckoo_account_include: std::option::Option::None, + }; + } + + /// Adds one account include selector while preserving insertion order. + pub fn push_account_include(&mut self, value: ksp_core_lib::Pubkey) -> ksp_core_lib::Result<()> { + return push_block_selector(&mut self.account_include, value); + } + + /// Sets or clears whether full transaction payloads are included in matching block updates. + pub fn set_include_transactions(&mut self, value: std::option::Option) { + self.include_transactions = value; + } + + /// Sets or clears whether account payloads are included in matching block updates. + pub fn set_include_accounts(&mut self, value: std::option::Option) { + self.include_accounts = value; + } + + /// Sets or clears whether entry payloads are included in matching block updates. + pub fn set_include_entries(&mut self, value: std::option::Option) { + self.include_entries = value; + } + + /// Sets or clears the optional compressed account-include filter. + pub fn set_cuckoo_account_include(&mut self, value: std::option::Option) { + self.cuckoo_account_include = value; + } + + /// Returns ordered account include selectors. + #[must_use] + pub fn account_include(&self) -> &[ksp_core_lib::Pubkey] { + return self.account_include.as_slice(); + } + + /// Returns the optional transaction-payload inclusion flag. + #[must_use] + pub const fn include_transactions(&self) -> std::option::Option { + return self.include_transactions; + } + + /// Returns the optional account-payload inclusion flag. + #[must_use] + pub const fn include_accounts(&self) -> std::option::Option { + return self.include_accounts; + } + + /// Returns the optional entry-payload inclusion flag. + #[must_use] + pub const fn include_entries(&self) -> std::option::Option { + return self.include_entries; + } + + /// Returns the optional compressed account-include filter. + #[must_use] + pub const fn cuckoo_account_include(&self) -> std::option::Option<&crate::YellowstoneCuckooFilter> { + return self.cuckoo_account_include.as_ref(); } #[cfg(test)] fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks { - return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default(); + return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks { + account_include: self.account_include.iter().map(std::string::ToString::to_string).collect(), + include_transactions: self.include_transactions, + include_accounts: self.include_accounts, + include_entries: self.include_entries, + cuckoo_account_include: self.cuckoo_account_include.as_ref().map(crate::YellowstoneCuckooFilter::to_wire), + }; + } +} + +impl std::default::Default for YellowstoneSubscribeBlockFilter { + fn default() -> Self { + return Self::new(); + } +} + +impl std::fmt::Debug for YellowstoneSubscribeBlockFilter { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneSubscribeBlockFilter") + .field("account_include_count", &self.account_include.len()) + .field("include_transactions", &self.include_transactions) + .field("include_accounts", &self.include_accounts) + .field("include_entries", &self.include_entries) + .field("has_cuckoo_account_include", &self.cuckoo_account_include.is_some()) + .finish(); + } +} + +/// Rewards container carried by Yellowstone block and block-meta updates. +#[derive(Clone, Eq, PartialEq)] +pub struct YellowstoneBlockRewards { + rewards: std::vec::Vec, + num_partitions: std::option::Option, +} + +impl YellowstoneBlockRewards { + /// Returns ordered reward entries. + #[must_use] + pub fn rewards(&self) -> &[crate::YellowstoneReward] { + return self.rewards.as_slice(); + } + + /// Returns the optional number of reward partitions represented by the block. + #[must_use] + pub const fn num_partitions(&self) -> std::option::Option { + return self.num_partitions; + } +} + +impl std::fmt::Debug for YellowstoneBlockRewards { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneBlockRewards") + .field("reward_count", &self.rewards.len()) + .field("num_partitions", &self.num_partitions) + .finish(); + } +} + +/// One Yellowstone block-entry payload. The same wire message is used standalone and inside full block updates. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct YellowstoneEntryInfo { + slot: u64, + index: u64, + num_hashes: u64, + hash: crate::YellowstoneHashBytes, + executed_transaction_count: u64, + starting_transaction_index: u64, +} + +impl YellowstoneEntryInfo { + /// Returns the containing slot. + #[must_use] + pub const fn slot(self) -> u64 { + return self.slot; + } + + /// Returns the entry index within the slot. + #[must_use] + pub const fn index(self) -> u64 { + return self.index; + } + + /// Returns the number of hashes represented by this entry. + #[must_use] + pub const fn num_hashes(self) -> u64 { + return self.num_hashes; + } + + /// Returns the exact fixed-width entry hash. + #[must_use] + pub const fn hash(self) -> crate::YellowstoneHashBytes { + return self.hash; + } + + /// Returns the number of transactions executed by this entry. + #[must_use] + pub const fn executed_transaction_count(self) -> u64 { + return self.executed_transaction_count; + } + + /// Returns the starting transaction index, including the legacy zero value emitted by older validators. + #[must_use] + pub const fn starting_transaction_index(self) -> u64 { + return self.starting_transaction_index; + } +} + +impl std::fmt::Debug for YellowstoneEntryInfo { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneEntryInfo") + .field("slot", &self.slot) + .field("index", &self.index) + .field("num_hashes", &self.num_hashes) + .field("executed_transaction_count", &self.executed_transaction_count) + .field("starting_transaction_index", &self.starting_transaction_index) + .finish_non_exhaustive(); + } +} + +/// Full standard Yellowstone block update projected into KSP-owned types. +#[derive(Clone, PartialEq)] +pub struct YellowstoneBlockUpdate { + filters: std::vec::Vec, + created_at: std::option::Option, + slot: u64, + blockhash: std::string::String, + rewards: std::option::Option, + block_time: std::option::Option, + block_height: std::option::Option, + parent_slot: u64, + parent_blockhash: std::string::String, + executed_transaction_count: u64, + transactions: std::vec::Vec, + updated_account_count: u64, + accounts: std::vec::Vec, + entries_count: u64, + entries: std::vec::Vec, +} + +impl YellowstoneBlockUpdate { + /// Returns echoed matching filter names in server order. + #[must_use] + pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] { + return self.filters.as_slice(); + } + + /// Returns optional server creation timestamp. + #[must_use] + pub const fn created_at(&self) -> std::option::Option { + return self.created_at; + } + + /// Returns the block slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns the exact blockhash text validated as a 32-byte Base58 Solana hash. + #[must_use] + pub fn blockhash(&self) -> &str { + return self.blockhash.as_str(); + } + + /// Returns optional block rewards. + #[must_use] + pub const fn rewards(&self) -> std::option::Option<&crate::YellowstoneBlockRewards> { + return self.rewards.as_ref(); + } + + /// Returns optional Unix block time. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns optional block height. + #[must_use] + pub const fn block_height(&self) -> std::option::Option { + return self.block_height; + } + + /// Returns the parent slot. + #[must_use] + pub const fn parent_slot(&self) -> u64 { + return self.parent_slot; + } + + /// Returns the exact parent blockhash text. + #[must_use] + pub fn parent_blockhash(&self) -> &str { + return self.parent_blockhash.as_str(); + } + + /// Returns the server-reported executed transaction count independently of whether transaction payloads were requested. + #[must_use] + pub const fn executed_transaction_count(&self) -> u64 { + return self.executed_transaction_count; + } + + /// Returns transaction payloads included by the server. + #[must_use] + pub fn transactions(&self) -> &[crate::YellowstoneTransactionInfo] { + return self.transactions.as_slice(); + } + + /// Returns the server-reported updated account count independently of whether account payloads were requested. + #[must_use] + pub const fn updated_account_count(&self) -> u64 { + return self.updated_account_count; + } + + /// Returns account payloads included by the server. + #[must_use] + pub fn accounts(&self) -> &[crate::YellowstoneAccountInfo] { + return self.accounts.as_slice(); + } + + /// Returns the server-reported entry count independently of whether entry payloads were requested. + #[must_use] + pub const fn entries_count(&self) -> u64 { + return self.entries_count; + } + + /// Returns entry payloads included by the server. + #[must_use] + pub fn entries(&self) -> &[crate::YellowstoneEntryInfo] { + return self.entries.as_slice(); + } +} + +impl std::fmt::Debug for YellowstoneBlockUpdate { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneBlockUpdate") + .field("filter_count", &self.filters.len()) + .field("created_at", &self.created_at) + .field("slot", &self.slot) + .field("has_rewards", &self.rewards.is_some()) + .field("block_time", &self.block_time) + .field("block_height", &self.block_height) + .field("parent_slot", &self.parent_slot) + .field("executed_transaction_count", &self.executed_transaction_count) + .field("transaction_payload_count", &self.transactions.len()) + .field("updated_account_count", &self.updated_account_count) + .field("account_payload_count", &self.accounts.len()) + .field("entries_count", &self.entries_count) + .field("entry_payload_count", &self.entries.len()) + .finish_non_exhaustive(); + } +} + +/// Metadata-only standard Yellowstone block update. +#[derive(Clone, Eq, PartialEq)] +pub struct YellowstoneBlockMetaUpdate { + filters: std::vec::Vec, + created_at: std::option::Option, + slot: u64, + blockhash: std::string::String, + rewards: std::option::Option, + block_time: std::option::Option, + block_height: std::option::Option, + parent_slot: u64, + parent_blockhash: std::string::String, + executed_transaction_count: u64, + entries_count: u64, +} + +impl YellowstoneBlockMetaUpdate { + /// Returns echoed matching filter names in server order. + #[must_use] + pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] { + return self.filters.as_slice(); + } + + /// Returns optional server creation timestamp. + #[must_use] + pub const fn created_at(&self) -> std::option::Option { + return self.created_at; + } + + /// Returns the block slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns the exact blockhash text. + #[must_use] + pub fn blockhash(&self) -> &str { + return self.blockhash.as_str(); + } + + /// Returns optional block rewards. + #[must_use] + pub const fn rewards(&self) -> std::option::Option<&crate::YellowstoneBlockRewards> { + return self.rewards.as_ref(); + } + + /// Returns optional Unix block time. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns optional block height. + #[must_use] + pub const fn block_height(&self) -> std::option::Option { + return self.block_height; + } + + /// Returns the parent slot. + #[must_use] + pub const fn parent_slot(&self) -> u64 { + return self.parent_slot; + } + + /// Returns the exact parent blockhash text. + #[must_use] + pub fn parent_blockhash(&self) -> &str { + return self.parent_blockhash.as_str(); + } + + /// Returns the server-reported executed transaction count. + #[must_use] + pub const fn executed_transaction_count(&self) -> u64 { + return self.executed_transaction_count; + } + + /// Returns the server-reported entry count. + #[must_use] + pub const fn entries_count(&self) -> u64 { + return self.entries_count; + } +} + +impl std::fmt::Debug for YellowstoneBlockMetaUpdate { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneBlockMetaUpdate") + .field("filter_count", &self.filters.len()) + .field("created_at", &self.created_at) + .field("slot", &self.slot) + .field("has_rewards", &self.rewards.is_some()) + .field("block_time", &self.block_time) + .field("block_height", &self.block_height) + .field("parent_slot", &self.parent_slot) + .field("executed_transaction_count", &self.executed_transaction_count) + .field("entries_count", &self.entries_count) + .finish_non_exhaustive(); + } +} + +/// Standalone standard Yellowstone entry update with its update envelope. +#[derive(Clone, Eq, PartialEq)] +pub struct YellowstoneEntryUpdate { + filters: std::vec::Vec, + created_at: std::option::Option, + entry: crate::YellowstoneEntryInfo, +} + +impl YellowstoneEntryUpdate { + /// Returns echoed matching filter names in server order. + #[must_use] + pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] { + return self.filters.as_slice(); + } + + /// Returns optional server creation timestamp. + #[must_use] + pub const fn created_at(&self) -> std::option::Option { + return self.created_at; + } + + /// Returns the complete entry payload. + #[must_use] + pub const fn entry(&self) -> crate::YellowstoneEntryInfo { + return self.entry; + } +} + +impl std::fmt::Debug for YellowstoneEntryUpdate { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("YellowstoneEntryUpdate") + .field("filter_count", &self.filters.len()) + .field("created_at", &self.created_at) + .field("entry", &self.entry) + .finish(); } } @@ -2417,6 +2875,192 @@ fn decode_transaction_status_update(wire: yellowstone_grpc_proto::geyser::Subscr }); } +#[cfg(test)] +fn decode_block_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result { + let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let update = match wire.update_oneof { + std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(value)) => value, + _ => return invalid_subscribe_response("block", "Yellowstone update does not contain a block payload"), + }; + if update.transactions.len() > MAX_GRPC_BLOCK_VECTOR_COUNT + || update.accounts.len() > MAX_GRPC_BLOCK_VECTOR_COUNT + || update.entries.len() > MAX_GRPC_BLOCK_VECTOR_COUNT + { + return invalid_subscribe_response("block", "Yellowstone block payload collection exceeds the KSP bound"); + } + let blockhash = match decode_blockhash_text("block.blockhash", update.blockhash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let parent_blockhash = match decode_blockhash_text("block.parent_blockhash", update.parent_blockhash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let rewards = match update.rewards { + std::option::Option::Some(value) => match decode_block_rewards(value) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }, + std::option::Option::None => std::option::Option::None, + }; + let block_time = update.block_time.map(|value| return value.timestamp); + let block_height = update.block_height.map(|value| return value.block_height); + let mut transactions = std::vec::Vec::with_capacity(update.transactions.len()); + for value in update.transactions { + let value = match decode_transaction_info(value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + transactions.push(value); + } + let mut accounts = std::vec::Vec::with_capacity(update.accounts.len()); + for value in update.accounts { + let value = match decode_account_info(value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + accounts.push(value); + } + let mut entries = std::vec::Vec::with_capacity(update.entries.len()); + for value in update.entries { + let value = match decode_entry_info(value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + entries.push(value); + } + return std::result::Result::Ok(crate::YellowstoneBlockUpdate { + filters, + created_at, + slot: update.slot, + blockhash, + rewards, + block_time, + block_height, + parent_slot: update.parent_slot, + parent_blockhash, + executed_transaction_count: update.executed_transaction_count, + transactions, + updated_account_count: update.updated_account_count, + accounts, + entries_count: update.entries_count, + entries, + }); +} + +#[cfg(test)] +fn decode_block_meta_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result { + let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let update = match wire.update_oneof { + std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::BlockMeta(value)) => value, + _ => return invalid_subscribe_response("block_meta", "Yellowstone update does not contain a block-meta payload"), + }; + let blockhash = match decode_blockhash_text("block_meta.blockhash", update.blockhash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let parent_blockhash = match decode_blockhash_text("block_meta.parent_blockhash", update.parent_blockhash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let rewards = match update.rewards { + std::option::Option::Some(value) => match decode_block_rewards(value) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }, + std::option::Option::None => std::option::Option::None, + }; + return std::result::Result::Ok(crate::YellowstoneBlockMetaUpdate { + filters, + created_at, + slot: update.slot, + blockhash, + rewards, + block_time: update.block_time.map(|value| return value.timestamp), + block_height: update.block_height.map(|value| return value.block_height), + parent_slot: update.parent_slot, + parent_blockhash, + executed_transaction_count: update.executed_transaction_count, + entries_count: update.entries_count, + }); +} + +#[cfg(test)] +fn decode_entry_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result { + let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let update = match wire.update_oneof { + std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(value)) => value, + _ => return invalid_subscribe_response("entry", "Yellowstone update does not contain an entry payload"), + }; + let entry = match decode_entry_info(update) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneEntryUpdate { filters, created_at, entry }); +} + +#[cfg(test)] +fn decode_block_rewards(wire: yellowstone_grpc_proto::solana::storage::confirmed_block::Rewards) -> ksp_core_lib::Result { + if wire.rewards.len() > MAX_GRPC_BLOCK_VECTOR_COUNT { + return invalid_subscribe_response("block.rewards", "Yellowstone block reward count exceeds the KSP bound"); + } + let mut rewards = std::vec::Vec::with_capacity(wire.rewards.len()); + for value in wire.rewards { + let value = match decode_reward(value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + rewards.push(value); + } + return std::result::Result::Ok(crate::YellowstoneBlockRewards { rewards, num_partitions: wire.num_partitions.map(|value| return value.num_partitions) }); +} + +#[cfg(test)] +fn decode_entry_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateEntry) -> ksp_core_lib::Result { + let hash = match decode_entry_hash(wire.hash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::YellowstoneEntryInfo { + slot: wire.slot, + index: wire.index, + num_hashes: wire.num_hashes, + hash, + executed_transaction_count: wire.executed_transaction_count, + starting_transaction_index: wire.starting_transaction_index, + }); +} + +#[cfg(test)] +fn decode_entry_hash(bytes: std::vec::Vec) -> ksp_core_lib::Result { + let bytes: [u8; YELLOWSTONE_HASH_LENGTH_BYTES] = match bytes.try_into() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return invalid_subscribe_response("entry.hash", "Yellowstone entry contains an invalid hash length"), + }; + return std::result::Result::Ok(crate::YellowstoneHashBytes { bytes }); +} + +#[cfg(test)] +fn decode_blockhash_text(field: &'static str, value: std::string::String) -> ksp_core_lib::Result { + if value.is_empty() + || value.len() > MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES + || value.trim() != value + || base58_decoded_length(value.as_str()) != std::option::Option::Some(YELLOWSTONE_HASH_LENGTH_BYTES) + { + return invalid_subscribe_response(field, "Yellowstone block update contains an invalid Solana blockhash"); + } + return std::result::Result::Ok(value); +} + #[cfg(test)] fn decode_transaction_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo) -> ksp_core_lib::Result { let signature = match decode_transaction_signature("transaction.signature", wire.signature) { @@ -2873,6 +3517,17 @@ fn decode_pubkey_bytes(field: &'static str, bytes: std::vec::Vec) -> ksp_cor return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(array)); } +fn push_block_selector(target: &mut std::vec::Vec, value: ksp_core_lib::Pubkey) -> ksp_core_lib::Result<()> { + if target.len() >= MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone block account selector count exceeds KSP bounds") + .with_context("field", "grpc_subscribe.block.account_include"), + ); + } + target.push(value); + return std::result::Result::Ok(()); +} + fn push_transaction_selector(target: &mut std::vec::Vec, value: ksp_core_lib::Pubkey, field: &'static str) -> ksp_core_lib::Result<()> { if target.len() >= MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT { return std::result::Result::Err( diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index f7faa08..5fed480 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: 40 +// version: 41 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -143,12 +143,22 @@ pub use self::grpc_subscribe::YellowstoneAccountMemcmpEncoding; pub use self::grpc_subscribe::YellowstoneAccountUpdate; /// One standard Yellowstone account-data slice. pub use self::grpc_subscribe::YellowstoneAccountsDataSlice; +/// Metadata-only standard Yellowstone block update. +pub use self::grpc_subscribe::YellowstoneBlockMetaUpdate; +/// Rewards container carried by Yellowstone block and block-meta updates. +pub use self::grpc_subscribe::YellowstoneBlockRewards; +/// Full standard Yellowstone block update. +pub use self::grpc_subscribe::YellowstoneBlockUpdate; /// One compiled instruction from the Yellowstone Solana-storage transaction wire. pub use self::grpc_subscribe::YellowstoneCompiledInstruction; /// Wire-preserving KSP representation of a standard Yellowstone Cuckoo filter. pub use self::grpc_subscribe::YellowstoneCuckooFilter; /// Hash algorithm carried by a standard Yellowstone Cuckoo filter. pub use self::grpc_subscribe::YellowstoneCuckooHashAlgorithm; +/// One Yellowstone block-entry payload reused by block and standalone entry updates. +pub use self::grpc_subscribe::YellowstoneEntryInfo; +/// Standalone standard Yellowstone entry update. +pub use self::grpc_subscribe::YellowstoneEntryUpdate; /// Fixed-width 32-byte hash from the Yellowstone Solana-storage transaction wire. pub use self::grpc_subscribe::YellowstoneHashBytes; /// One inner instruction from Yellowstone transaction status metadata. @@ -171,7 +181,7 @@ pub use self::grpc_subscribe::YellowstoneSlotUpdate; pub use self::grpc_subscribe::YellowstoneStoredTransaction; /// Complete account-family filter group for standard Yellowstone Subscribe. pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter; -/// Block-family filter-group shell for standard Yellowstone Subscribe. +/// Complete block-family filter group 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; diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 6bf0ca1..26df56d 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: 44 +// version: 45 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -925,3 +925,47 @@ fn public_v0_2_9_pre_007_yellowstone_transactions_contract_is_available_from_cra let _return_data = std::any::type_name::(); let _reward = std::any::type_name::(); } + +#[test] +fn public_v0_2_9_pre_008_yellowstone_blocks_contract_is_available_from_crate_root() { + let account = ksp_core_lib::Pubkey::new_from_array([71_u8; 32]); + let mut block = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new(); + assert!(block.push_account_include(account).is_ok()); + block.set_include_transactions(std::option::Option::Some(true)); + block.set_include_accounts(std::option::Option::Some(false)); + block.set_include_entries(std::option::Option::Some(true)); + assert_eq!(block.account_include(), &[account]); + assert_eq!(block.include_transactions(), std::option::Option::Some(true)); + assert_eq!(block.include_accounts(), std::option::Option::Some(false)); + assert_eq!(block.include_entries(), std::option::Option::Some(true)); + let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new(); + assert!( + request + .insert_block_filter(ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("filter name must validate"), block,) + .is_ok() + ); + assert!( + request + .insert_blocks_meta_filter( + ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("block-meta").expect("filter name must validate"), + ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new(), + ) + .is_ok() + ); + assert!( + request + .insert_entry_filter( + ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("filter name must validate"), + ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new(), + ) + .is_ok() + ); + assert_eq!(request.block_filter_count(), 1); + assert_eq!(request.blocks_meta_filter_count(), 1); + assert_eq!(request.entry_filter_count(), 1); + let _block_update = std::any::type_name::(); + let _block_meta = std::any::type_name::(); + let _block_rewards = std::any::type_name::(); + let _entry_info = std::any::type_name::(); + let _entry_update = std::any::type_name::(); +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 2088792..256ae3e 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: 38 +// version: 39 //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. @@ -1113,7 +1113,7 @@ fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() } #[test] -fn release_v0_2_9_pre_005_completes_standard_accounts_and_slots_without_advancing_transactions_or_blocks() { +fn release_v0_2_9_pre_005_accounts_and_slots_contract_remains_complete() { let source = include_str!("../src/grpc_subscribe.rs"); let crate_root = include_str!("../src/lib.rs"); for required in [ @@ -1146,7 +1146,6 @@ fn release_v0_2_9_pre_005_completes_standard_accounts_and_slots_without_advancin assert!(source.contains("MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES")); assert!(source.contains("#[cfg(test)]\nfn decode_account_update")); assert!(source.contains("#[cfg(test)]\nfn decode_slot_update")); - assert!(!source.contains("YellowstoneBlockUpdate")); assert!(!source.contains("SubscribeDeshred")); assert!(!source.contains("PublicNode")); assert!(!crate_root.contains("pub use tonic")); @@ -1181,7 +1180,7 @@ fn release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules() } #[test] -fn release_v0_2_9_pre_007_completes_standard_transactions_without_advancing_blocks_or_bidi() { +fn release_v0_2_9_pre_007_transactions_contract_remains_complete_without_bidi() { let source = include_str!("../src/grpc_subscribe.rs"); let crate_root = include_str!("../src/lib.rs"); for required in [ @@ -1211,8 +1210,7 @@ fn release_v0_2_9_pre_007_completes_standard_transactions_without_advancing_bloc } assert!(source.contains("base58_decoded_length")); assert!(source.contains("YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES")); - assert!(!source.contains("YellowstoneBlockUpdate")); - assert!(!source.contains("fn decode_block_update")); + assert!(!source.contains("pub async fn subscribe(")); assert!(!source.contains("SubscribeDeshred")); assert!(!source.contains("PublicNode")); assert!(!crate_root.contains("pub use tonic")); @@ -1221,3 +1219,46 @@ fn release_v0_2_9_pre_007_completes_standard_transactions_without_advancing_bloc let _update = std::any::type_name::(); let _status = std::any::type_name::(); } + +#[test] +fn release_v0_2_9_pre_008_completes_standard_blocks_without_advancing_bidi() { + let source = include_str!("../src/grpc_subscribe.rs"); + let crate_root = include_str!("../src/lib.rs"); + for required in [ + "YellowstoneSubscribeBlockFilter", + "account_include", + "include_transactions", + "include_accounts", + "include_entries", + "cuckoo_account_include", + "YellowstoneBlockRewards", + "YellowstoneBlockUpdate", + "YellowstoneBlockMetaUpdate", + "YellowstoneEntryInfo", + "YellowstoneEntryUpdate", + "executed_transaction_count", + "updated_account_count", + "entries_count", + "starting_transaction_index", + "decode_block_update", + "decode_block_meta_update", + "decode_entry_update", + "decode_block_rewards", + ] { + assert!(source.contains(required), "missing pre.008 Blocks contract token: {required}"); + } + assert!(source.contains("MAX_GRPC_BLOCK_VECTOR_COUNT")); + assert!(source.contains("base58_decoded_length(value.as_str())")); + assert!(source.contains("std::vec::Vec")); + assert!(source.contains("std::vec::Vec")); + assert!(source.contains("std::vec::Vec")); + assert!(source.contains("#[cfg(test)]\nfn decode_block_update")); + assert!(!source.contains("pub async fn subscribe(")); + assert!(!source.contains("SubscribeDeshred")); + assert!(!source.contains("PublicNode")); + assert!(!crate_root.contains("pub use tonic")); + assert!(!crate_root.contains("pub use yellowstone_grpc_proto")); + let _block = std::any::type_name::(); + let _meta = std::any::type_name::(); + let _entry = 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 index 6c6157b..2cfd0e2 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs @@ -1,10 +1,37 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs -// version: 3 +// version: 4 fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName { return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate"); } +fn minimal_transaction_info(signature_byte: u8, index: u64) -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo { + let mut meta = yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta::default(); + meta.fee = 5_000; + return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo { + signature: vec![signature_byte; 64], + is_vote: false, + transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction { + signatures: vec![vec![signature_byte; 64]], + message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message { + header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }), + account_keys: vec![vec![1_u8; 32]], + recent_blockhash: vec![2_u8; 32], + instructions: vec![], + versioned: false, + address_table_lookups: vec![], + config: std::option::Option::None, + }), + }), + meta: std::option::Option::Some(meta), + index, + }; +} + #[test] fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() { assert!(crate::YellowstoneSubscribeFilterName::new("").is_err()); @@ -553,3 +580,199 @@ fn yellowstone_transaction_status_update_preserves_error_and_rejects_malformed_s }; assert!(super::decode_transaction_status_update(malformed).is_err()); } + +#[test] +fn yellowstone_block_filter_encodes_complete_current_wire_and_redacts_selectors() { + let mut filter = crate::YellowstoneSubscribeBlockFilter::new(); + let account = ksp_core_lib::Pubkey::new_from_array([31_u8; 32]); + assert!(filter.push_account_include(account).is_ok()); + filter.set_include_transactions(std::option::Option::Some(true)); + filter.set_include_accounts(std::option::Option::Some(false)); + filter.set_include_entries(std::option::Option::Some(true)); + let cuckoo = crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 9, crate::YellowstoneCuckooHashAlgorithm::SipHash) + .expect("block Cuckoo filter must validate"); + filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo)); + let wire = filter.to_wire(); + assert_eq!(wire.account_include, vec![account.to_string()]); + assert_eq!(wire.include_transactions, std::option::Option::Some(true)); + assert_eq!(wire.include_accounts, std::option::Option::Some(false)); + assert_eq!(wire.include_entries, std::option::Option::Some(true)); + assert!(wire.cuckoo_account_include.is_some()); + let debug = format!("{filter:?}"); + assert!(debug.contains("account_include_count")); + assert!(!debug.contains(&account.to_string())); +} + +#[test] +fn yellowstone_block_update_reuses_transaction_account_entry_dtos_and_preserves_server_counts() { + let blockhash = ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string(); + let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string(); + let reward_pubkey = ksp_core_lib::Pubkey::new_from_array([23_u8; 32]); + let account_pubkey = ksp_core_lib::Pubkey::new_from_array([24_u8; 32]); + let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate { + filters: vec!["blocks-main".to_owned()], + update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block( + yellowstone_grpc_proto::geyser::SubscribeUpdateBlock { + slot: 500, + blockhash: blockhash.clone(), + rewards: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Rewards { + rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward { + pubkey: reward_pubkey.to_string(), + lamports: 10, + post_balance: 11, + reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Fee as i32, + commission: "".to_owned(), + commission_bps: "".to_owned(), + }], + num_partitions: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::NumPartitions { num_partitions: 3 }), + }), + block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_700_000_000 }), + block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 499 }), + transactions: vec![minimal_transaction_info(7, 2)], + parent_slot: 499, + parent_blockhash: parent_blockhash.clone(), + executed_transaction_count: 12, + updated_account_count: 34, + accounts: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo { + pubkey: vec![24_u8; 32], + lamports: 77, + owner: vec![25_u8; 32], + executable: false, + rent_epoch: 4, + data: vec![1_u8, 2, 3], + write_version: 8, + txn_signature: std::option::Option::Some(vec![7_u8; 64]), + }], + entries_count: 56, + entries: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateEntry { + slot: 500, + index: 4, + num_hashes: 5, + hash: vec![26_u8; 32], + executed_transaction_count: 6, + starting_transaction_index: 7, + }], + }, + )), + created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 12, nanos: 34 }), + }; + let update = super::decode_block_update(wire).expect("block update fixture must decode"); + assert_eq!(update.filters()[0].as_str(), "blocks-main"); + assert_eq!(update.slot(), 500); + assert_eq!(update.blockhash(), blockhash); + assert_eq!(update.parent_slot(), 499); + assert_eq!(update.parent_blockhash(), parent_blockhash); + assert_eq!(update.block_time(), std::option::Option::Some(1_700_000_000)); + assert_eq!(update.block_height(), std::option::Option::Some(499)); + assert_eq!(update.executed_transaction_count(), 12); + assert_eq!(update.transactions().len(), 1); + assert_eq!(update.transactions()[0].index(), 2); + assert_eq!(update.updated_account_count(), 34); + assert_eq!(update.accounts().len(), 1); + assert_eq!(update.accounts()[0].pubkey(), &account_pubkey); + assert_eq!(update.entries_count(), 56); + assert_eq!(update.entries().len(), 1); + assert_eq!(update.entries()[0].starting_transaction_index(), 7); + let rewards = update.rewards().expect("block rewards must be present"); + assert_eq!(rewards.rewards().len(), 1); + assert_eq!(rewards.num_partitions(), std::option::Option::Some(3)); + assert_eq!(rewards.rewards()[0].pubkey(), &reward_pubkey); + let debug = format!("{update:?}"); + assert!(!debug.contains(&blockhash)); + assert!(!debug.contains(&parent_blockhash)); + assert!(!debug.contains(&reward_pubkey.to_string())); + assert!(!debug.contains(&account_pubkey.to_string())); + let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate { + filters: vec!["blocks-main".to_owned()], + update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block( + yellowstone_grpc_proto::geyser::SubscribeUpdateBlock { + slot: 1, + blockhash: "not-a-solana-hash".to_owned(), + rewards: std::option::Option::None, + block_time: std::option::Option::None, + block_height: std::option::Option::None, + transactions: vec![], + parent_slot: 0, + parent_blockhash: ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string(), + executed_transaction_count: 0, + updated_account_count: 0, + accounts: vec![], + entries_count: 0, + entries: vec![], + }, + )), + created_at: std::option::Option::None, + }; + assert!(super::decode_block_update(malformed).is_err()); +} + +#[test] +fn yellowstone_block_meta_and_entry_updates_preserve_optional_and_legacy_entry_fields() { + let blockhash = ksp_core_lib::Pubkey::new_from_array([41_u8; 32]).to_string(); + let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([42_u8; 32]).to_string(); + let meta_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate { + filters: vec!["meta".to_owned()], + update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::BlockMeta( + yellowstone_grpc_proto::geyser::SubscribeUpdateBlockMeta { + slot: 900, + blockhash: blockhash.clone(), + rewards: std::option::Option::None, + block_time: std::option::Option::None, + block_height: std::option::Option::None, + parent_slot: 899, + parent_blockhash: parent_blockhash.clone(), + executed_transaction_count: 17, + entries_count: 18, + }, + )), + created_at: std::option::Option::None, + }; + let meta = super::decode_block_meta_update(meta_wire).expect("block-meta update must decode"); + assert_eq!(meta.slot(), 900); + assert_eq!(meta.blockhash(), blockhash); + assert_eq!(meta.parent_blockhash(), parent_blockhash); + assert_eq!(meta.rewards(), std::option::Option::None); + assert_eq!(meta.block_time(), std::option::Option::None); + assert_eq!(meta.block_height(), std::option::Option::None); + assert_eq!(meta.executed_transaction_count(), 17); + assert_eq!(meta.entries_count(), 18); + assert!(!format!("{meta:?}").contains(&blockhash)); + let entry_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate { + filters: vec!["entries".to_owned()], + update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry( + yellowstone_grpc_proto::geyser::SubscribeUpdateEntry { + slot: 900, + index: 3, + num_hashes: 4, + hash: vec![43_u8; 32], + executed_transaction_count: 5, + starting_transaction_index: 0, + }, + )), + created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 6, nanos: 7 }), + }; + let entry = super::decode_entry_update(entry_wire).expect("entry update must decode"); + assert_eq!(entry.filters()[0].as_str(), "entries"); + assert_eq!(entry.entry().slot(), 900); + assert_eq!(entry.entry().index(), 3); + assert_eq!(entry.entry().num_hashes(), 4); + assert_eq!(entry.entry().hash().as_bytes(), &[43_u8; 32]); + assert_eq!(entry.entry().executed_transaction_count(), 5); + assert_eq!(entry.entry().starting_transaction_index(), 0); + assert!(!format!("{entry:?}").contains("43, 43")); + let malformed_entry = yellowstone_grpc_proto::geyser::SubscribeUpdate { + filters: vec!["entries".to_owned()], + update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry( + yellowstone_grpc_proto::geyser::SubscribeUpdateEntry { + slot: 1, + index: 0, + num_hashes: 0, + hash: vec![0_u8; 31], + executed_transaction_count: 0, + starting_transaction_index: 0, + }, + )), + created_at: std::option::Option::None, + }; + assert!(super::decode_entry_update(malformed_entry).is_err()); +} diff --git a/deltas/0.2.9/pre.008.md b/deltas/0.2.9/pre.008.md new file mode 100644 index 0000000..247aa13 --- /dev/null +++ b/deltas/0.2.9/pre.008.md @@ -0,0 +1,73 @@ + + + +# Delta `0.2.9-pre.008` — Yellowstone Blocks + block_meta + entry + +## 1. Base et signal technique + +Base : `0.2.9-pre.007`, fermée sur gate opérateur intégralement vert. + +```text +workspace.package.version = 0.2.9-pre.8 +``` + +## 2. Scope + +La tranche complète uniquement la dernière famille de filtres/updates standard avant l’ouverture bidi : Blocks, `block_meta` et `entry`. + +| Surface | Matérialisation | +|----------------------------|-----------------| +| filtre Blocks | complet | +| `blocks_meta` | marker inchangé | +| `entry` | marker inchangé | +| `SubscribeUpdateBlock` | DTO + decode | +| `SubscribeUpdateBlockMeta` | DTO + decode | +| `SubscribeUpdateEntry` | DTO + decode | +| stream bidi / lifecycle | OUT | +| reconnect / replay | OUT | +| PublicNode / Config V3 | OUT | +| `SubscribeDeshred` | OUT `0.2.9` | + +## 3. Filtre Blocks + +`YellowstoneSubscribeBlockFilter` matérialise exactement le wire courant : `account_include[]`, `include_transactions?`, `include_accounts?`, `include_entries?` et `cuckoo_account_include?`. Les account selectors utilisent `ksp_core_lib::Pubkey`, restent ordonnés et bornés ; `Debug` ne rend aucune adresse. + +## 4. Updates Blocks + +`YellowstoneBlockUpdate` conserve la metadata de bloc, rewards, temps/hauteur optionnels, parent, compteurs et payloads optionnels. Les transactions réutilisent `YellowstoneTransactionInfo`, les comptes réutilisent `YellowstoneAccountInfo`, et les entries utilisent `YellowstoneEntryInfo`. + +`YellowstoneBlockMetaUpdate` conserve la variante metadata-only. `YellowstoneEntryUpdate` enveloppe la même `YellowstoneEntryInfo` réutilisée dans les blocs. `starting_transaction_index` reste explicite, y compris sa valeur legacy `0`. + +Les compteurs serveur sont indépendants des tailles de vecteurs : aucune égalité artificielle n’est imposée quand les flags `include_*` demandent l’omission d’un payload. + +## 5. Validation et sécurité + +Les blockhash textuels sont bornés, trim-exacts et Base58-décodés vers exactement 32 octets ; les hash d’entrée sont exactement 32 octets. Les collections Block/Rewards sont bornées. `Debug` ne copie aucun blockhash, parent blockhash, reward pubkey, account selector, données account, transaction, instruction ou entry hash. + +Les conversions protobuf et décodeurs restent sous `#[cfg(test)]` jusqu’à l’ouverture du stream runtime en `pre.009`. + +## 6. Tests/canaries ajoutés + +```text +block filter exact wire + redaction +block update complet avec Transaction/Account/Entry réutilisés +server counts distincts des payload-vector lengths +block-meta optional states +entry starting_transaction_index + malformed hash +public API canary pre.008 +release completeness canary pre.008 +``` + +## 7. Gate opérateur + +```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 +``` + +Aucune dépendance ni feature Cargo n’est modifiée ; aucun `cargo tree` supplémentaire n’est requis. 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 f85ea4f..2af4a12 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.006` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 completeness + 4 doctests. `0.2.9-pre.007` est candidate et matérialise uniquement Transactions + `transaction_status` du `Subscribe` standard : filtres complets, signature Base58 décodée exactement sur 64 octets, Cuckoo/token-account expansion, DTOs transaction/meta complets et Transaction V1 `TransactionConfig`. Blocks restent `pre.008`; aucun stream bidi n’est encore ouvert.** +> **Statut : `0.2.9-pre.007` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 367 unit + 46 public API + 40 completeness + 4 doctests. `0.2.9-pre.008` est candidate et complète uniquement Blocks + `block_meta` + `entry` du `Subscribe` standard : filtre Blocks courant complet, rewards/time/height, réutilisation des DTOs Transaction/Account, compteurs serveur indépendants des payloads optionnels et `starting_transaction_index`. Aucun stream bidi n’est encore ouvert ; lifecycle reste `pre.009`.** ## 1. Objet, base et état d'ouverture @@ -928,10 +928,10 @@ pre.005 DONE — standard Solana : Accounts + Slots filters/updates pre.006 DONE — structure Transport : namespace privé HTTP explicite budget : 15–20 min ; gate opérateur final PASS 364/45/39/4 + workspace -pre.007 CANDIDATE — standard Solana : Transactions + transaction_status +pre.007 DONE — standard Solana : Transactions + transaction_status budget : 15–20 min ; preuve : include/exclude/required/Cuckoo/token expansion + tx/meta + TransactionConfig V1 -pre.008 standard Solana : Blocks + block_meta + entry +pre.008 CANDIDATE — standard Solana : Blocks + block_meta + entry budget : 15–20 min ; preuve : counts/arrays/optional/oneof/payload bounds pre.009 moteur partagé : bidi mutation + Ping/Pong + half-close + backpressure + shutdown @@ -1273,3 +1273,65 @@ Les marqueurs `inner_instructions_none`, `log_messages_none` et `return_data_non OUT de `pre.007` : Blocks/block_meta/entry, stream bidi/Ping-Pong, reconnect/replay, Config V3, PublicNode et `SubscribeDeshred`. **Gate candidat :** audit statique clean ; compilation/Clippy/tests opérateur requis avant fermeture. + + +## 23. Gate final `pre.007` — Transactions + `transaction_status` + +Preuve opérateur du `2026-08-24` : + +| 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 | 367/367 PASS | +| Transport `public_api` | 46/46 PASS | +| Transport `release_completeness` | 40/40 PASS | +| Transport doctests | 4/4 PASS | +| Core dependency canary | 3/3 PASS | +| `cargo test --workspace` | PASS | + +Le gate confirme la projection Transactions complète, incluant `TransactionConfig` V1 et les marqueurs legacy de `TransactionStatusMeta`, sans régression HTTP/WS et sans avancer Blocks ou bidi. + +**Verdict : `pre.007` fermée.** + +## 24. `pre.008` — Blocks + `block_meta` + `entry` candidate + +Le proto `yellowstone-grpc-proto 12.6.0` est réaudité avant implémentation. `SubscribeRequestFilterBlocks` contient exactement `account_include[]`, `include_transactions?`, `include_accounts?`, `include_entries?` et `cuckoo_account_include?`. `blocks_meta` et `entry` conservent leurs filtres marqueurs vides. + +La candidate matérialise : + +```text +request filter Blocks + account_include[] ordonné et borné + include_transactions? + include_accounts? + include_entries? + cuckoo_account_include? + +SubscribeUpdateBlock + slot / blockhash / parent_slot / parent_blockhash + rewards? + num_partitions? + block_time? / block_height? + executed_transaction_count + transactions[] -> YellowstoneTransactionInfo réutilisé + updated_account_count + accounts[] -> YellowstoneAccountInfo réutilisé + entries_count + entries[] -> YellowstoneEntryInfo + +SubscribeUpdateBlockMeta + même metadata sans payloads transaction/account/entry + +SubscribeUpdateEntry + slot / index / num_hashes / hash[32] + executed_transaction_count + starting_transaction_index +``` + +Les compteurs serveur ne sont volontairement pas comparés aux longueurs des vecteurs : les options `include_transactions`, `include_accounts` et `include_entries` permettent au serveur de rapporter les totaux tout en omettant les payloads correspondants. Les `blockhash`/`parent_blockhash` sont validés comme Base58 représentant 32 octets, et les hash d’entrée restent des 32 octets exacts. `Debug` n’expose aucun hash, account selector, reward pubkey ou payload imbriqué. + +Les conversions request protobuf et décodeurs Block/BlockMeta/Entry restent `#[cfg(test)]` jusqu’à l’ouverture du stream runtime en `pre.009`. Aucun `SubscribeDeshred`, provider PublicNode, Config V3, Ping/Pong lifecycle ou reconnect n’entre dans cette tranche. + +**Gate candidat :** audit statique clean ; compilation/Clippy/tests opérateur requis avant fermeture. diff --git a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md index f848284..94b0678 100644 --- a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +++ b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md @@ -1,9 +1,9 @@ - + # Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode -> **Statut : `pre.006` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 completeness + 4 doctests. `0.2.9-pre.007` est candidate Transactions + `transaction_status` : filtre current complet, signature Base58 -> 64 octets, Cuckoo/token expansion, updates transaction/status et projection typée complète du `solana-storage.proto` courant incluant Transaction V1 `TransactionConfig`. Blocks restent `pre.008`; bidi reste `pre.009`.** +> **Statut : `pre.007` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 367 unit + 46 public API + 40 completeness + 4 doctests. `0.2.9-pre.008` est candidate Blocks + `block_meta` + `entry` : filtre Blocks courant complet, metadata/rewards/time/height, payloads Transaction/Account réutilisés, Entry complète et compteurs serveur conservés indépendamment des payloads optionnels. Bidi reste `pre.009`.** ## 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 | PARTIAL pre.005 Accounts/Slots / stream TODO pre.009 | -| `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 | +| RPC | Forme | Classification | Scope | Preuve cible | État | +|-----------------------|-------|------------------------------------------------------|-------|-----------------------|-------------------------------------------------------| +| `Subscribe` | bidi | standard | IN | fixture locale + live | PARTIAL pre.008 filters/updates / stream TODO pre.009 | +| `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 @@ -409,8 +409,8 @@ pre.003 DONE TLS/metadata + fixture + 7 unary standard 15 pre.004 DONE standard: Subscribe common/from_slot/bounds 15–20 min ; gate final fix.001 PASS pre.005 DONE standard: accounts + slots 15–20 min ; gate final fix.001 PASS pre.006 DONE structure: namespace privé HTTP `http_*` 15–20 min ; gate PASS -pre.007 CANDIDATE standard: transactions + transaction_status 15–20 min -pre.008 TODO standard: blocks + block_meta + entry 15–20 min +pre.007 DONE standard: transactions + transaction_status 15–20 min ; gate PASS +pre.008 CANDIDATE standard: blocks + block_meta + entry 15–20 min pre.009 TODO moteur: bidi/backpressure/half-close/shutdown 15–20 min pre.010 TODO moteur: reconnect/replay/gap/duplicate 15–20 min pre.011 TODO Config V3 + protocol/provider + profils PublicNode 15–20 min @@ -813,3 +813,53 @@ Le renommage est limité aux cinq modules dont l'ownership HTTP est sans ambigu Le proto 12.6.0 ajoute à la représentation de transaction le `Message.config` optionnel pour Transaction V1 / SIMD-0385 ; la candidate le conserve explicitement. Les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none` ne sont pas fusionnés avec leurs collections/messages. **Verdict `pre.007` : candidate source prête ; fermeture après gate Cargo opérateur.** + + +## 24. Gate final `pre.007` + +| 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 | 367/367 PASS | +| Transport `public_api` | 46/46 PASS | +| Transport `release_completeness` | 40/40 PASS | +| Transport doctests | 4/4 PASS | +| Core dependency canary | 3/3 PASS | +| `cargo test --workspace` | PASS | + +**Verdict : `pre.007` fermée.** + +## 25. Gate `pre.008` — Blocks + `block_meta` + `entry` candidate + +| Surface / invariant | État candidate | +|-------------------------------------------------------|----------------| +| workspace version | `0.2.9-pre.8` | +| Blocks `account_include[]` | SOURCE+TEST | +| `include_transactions?` / `include_accounts?` | SOURCE+TEST | +| `include_entries?` | SOURCE+TEST | +| Cuckoo block account include | SOURCE+TEST | +| `SubscribeUpdateBlock` | SOURCE+TEST | +| rewards + `num_partitions?` | SOURCE+TEST | +| block time / block height optionnels | SOURCE+TEST | +| transactions réutilisent `YellowstoneTransactionInfo` | SOURCE+TEST | +| accounts réutilisent `YellowstoneAccountInfo` | SOURCE+TEST | +| compteurs serveur indépendants des payload vectors | SOURCE+TEST | +| `SubscribeUpdateBlockMeta` | SOURCE+TEST | +| `SubscribeUpdateEntry` + `starting_transaction_index` | SOURCE+TEST | +| blockhash/parent blockhash Base58 -> 32 octets | SOURCE+TEST | +| entry hash exactement 32 octets | SOURCE+TEST | +| Debug sans hashes/sélecteurs/payloads imbriqués | SOURCE+TEST | +| stream bidi / Ping-Pong / lifecycle | OUT pre.008 | +| reconnect / replay | OUT pre.008 | +| PublicNode / Config V3 | OUT pre.008 | +| audit Rust workspace local | PASS / clean | +| fmt/check/Clippy/tests | opérateur TODO | + +Les `executed_transaction_count`, `updated_account_count` et `entries_count` sont conservés tels que fournis par le serveur ; ils ne sont pas forcés à égaler les tailles de `transactions[]`, `accounts[]` ou `entries[]`, car les trois flags `include_*` peuvent omettre ces payloads. Les messages imbriqués Transaction et Account réutilisent strictement les DTOs déjà introduits ; aucune seconde projection n’est créée. + +Les helpers wire/decode restent test-only jusqu’au premier consommateur runtime en `pre.009`. + +**Verdict `pre.008` : candidate source prête ; fermeture après gate Cargo opérateur.**