v0.2.9-pre.005

This commit is contained in:
2026-08-24 12:31:19 +02:00
parent 20320dba7b
commit dca0a4b809
9 changed files with 1420 additions and 43 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 244
# version: 245
[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.4.fix.1"
version = "0.2.9-pre.5"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,10 +1,19 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
// version: 2
// version: 3
const MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES: usize = 512 * 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT: usize = 256;
const MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT: usize = 50_000;
const MAX_GRPC_SUBSCRIBE_CUCKOO_DATA_LENGTH_BYTES: usize = 512 * 1024 * 1024;
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;
const MAX_GRPC_SUBSCRIBE_MEMCMP_BYTES: usize = 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_MEMCMP_TEXT_LENGTH_BYTES: usize = 2 * 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES: usize = 16 * 1024;
const MAX_GRPC_SUBSCRIBE_UPDATE_FILTER_COUNT: usize = 1_024;
const YELLOWSTONE_TRANSACTION_SIGNATURE_LENGTH_BYTES: usize = 64;
/// Validated logical filter name used by the standard Yellowstone `SubscribeRequest` maps.
///
@@ -103,45 +112,723 @@ impl YellowstoneSubscribePing {
}
}
/// Account-family filter-group shell for the standard Yellowstone subscribe surface.
/// Hash algorithm carried by a standard Yellowstone compressed Cuckoo account filter.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum YellowstoneCuckooHashAlgorithm {
/// Stable SipHash algorithm currently published by Yellowstone.
SipHash,
}
impl YellowstoneCuckooHashAlgorithm {
#[cfg(test)]
const fn to_wire(self) -> i32 {
return match self {
Self::SipHash => yellowstone_grpc_proto::geyser::CuckooHashAlgorithm::SipHash as i32,
};
}
}
/// Wire-preserving compressed Cuckoo filter used by current Yellowstone account subscriptions.
///
/// `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)]
/// KSP owns the public representation and bounds the serialized bucket data. The current upstream wire documents fingerprint sizes of 8, 12 or 16 bits and
/// publishes SipHash as its only hash algorithm. `Debug` never renders the bucket bytes.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneCuckooFilter {
data: std::vec::Vec<u8>,
bucket_count: u32,
entries_per_bucket: u32,
fingerprint_bits: u32,
hash_seed: u64,
hash_algorithm: crate::YellowstoneCuckooHashAlgorithm,
}
impl YellowstoneCuckooFilter {
/// Creates one bounded standard Yellowstone Cuckoo filter.
pub fn new(
data: std::vec::Vec<u8>,
bucket_count: u32,
entries_per_bucket: u32,
fingerprint_bits: u32,
hash_seed: u64,
hash_algorithm: crate::YellowstoneCuckooHashAlgorithm,
) -> ksp_core_lib::Result<Self> {
if data.len() > MAX_GRPC_SUBSCRIBE_CUCKOO_DATA_LENGTH_BYTES || bucket_count == 0 || entries_per_bucket == 0 || !matches!(fingerprint_bits, 8 | 12 | 16)
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone Cuckoo filter violates KSP structural bounds")
.with_context("field", "grpc_subscribe.cuckoo_filter"),
);
}
return std::result::Result::Ok(Self { data, bucket_count, entries_per_bucket, fingerprint_bits, hash_seed, hash_algorithm });
}
/// Returns the serialized bucket data without copying it.
#[must_use]
pub fn data(&self) -> &[u8] {
return self.data.as_slice();
}
/// Returns the number of Cuckoo buckets.
#[must_use]
pub const fn bucket_count(&self) -> u32 {
return self.bucket_count;
}
/// Returns the number of entries carried by one bucket.
#[must_use]
pub const fn entries_per_bucket(&self) -> u32 {
return self.entries_per_bucket;
}
/// Returns the fingerprint width in bits.
#[must_use]
pub const fn fingerprint_bits(&self) -> u32 {
return self.fingerprint_bits;
}
/// Returns the deterministic hash seed carried on the wire.
#[must_use]
pub const fn hash_seed(&self) -> u64 {
return self.hash_seed;
}
/// Returns the hash algorithm carried on the wire.
#[must_use]
pub const fn hash_algorithm(&self) -> crate::YellowstoneCuckooHashAlgorithm {
return self.hash_algorithm;
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::CuckooFilter {
return yellowstone_grpc_proto::geyser::CuckooFilter {
data: self.data.clone(),
bucket_count: self.bucket_count,
entries_per_bucket: self.entries_per_bucket,
fingerprint_bits: self.fingerprint_bits,
hash_seed: self.hash_seed,
hash_algorithm: self.hash_algorithm.to_wire(),
};
}
}
impl std::fmt::Debug for YellowstoneCuckooFilter {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneCuckooFilter")
.field("data_length", &self.data.len())
.field("bucket_count", &self.bucket_count)
.field("entries_per_bucket", &self.entries_per_bucket)
.field("fingerprint_bits", &self.fingerprint_bits)
.field("hash_seed", &self.hash_seed)
.field("hash_algorithm", &self.hash_algorithm)
.finish();
}
}
#[derive(Clone, Eq, PartialEq)]
enum YellowstoneAccountMemcmpData {
Bytes(std::vec::Vec<u8>),
Base58(std::string::String),
Base64(std::string::String),
}
/// Encoding selected by one standard Yellowstone account `memcmp` predicate.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum YellowstoneAccountMemcmpEncoding {
/// Raw bytes.
Bytes,
/// Base58 text.
Base58,
/// Base64 text.
Base64,
}
/// Validated standard Yellowstone `memcmp` account predicate.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneAccountMemcmp {
offset: u64,
data: YellowstoneAccountMemcmpData,
}
impl YellowstoneAccountMemcmp {
/// Creates a raw-byte `memcmp` predicate.
pub fn bytes(offset: u64, data: std::vec::Vec<u8>) -> ksp_core_lib::Result<Self> {
if data.len() > MAX_GRPC_SUBSCRIBE_MEMCMP_BYTES {
return invalid_subscribe_parameter("grpc_subscribe.accounts.filters.memcmp.bytes", "Yellowstone memcmp byte payload exceeds the KSP bound");
}
return std::result::Result::Ok(Self { offset, data: YellowstoneAccountMemcmpData::Bytes(data) });
}
/// Creates a bounded base58-text `memcmp` predicate.
pub fn base58(offset: u64, data: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let data = data.into();
if let std::result::Result::Err(error) = validate_memcmp_text(&data) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self { offset, data: YellowstoneAccountMemcmpData::Base58(data) });
}
/// Creates a bounded base64-text `memcmp` predicate.
pub fn base64(offset: u64, data: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let data = data.into();
if let std::result::Result::Err(error) = validate_memcmp_text(&data) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self { offset, data: YellowstoneAccountMemcmpData::Base64(data) });
}
/// Returns the byte offset.
#[must_use]
pub const fn offset(&self) -> u64 {
return self.offset;
}
/// Returns the selected wire encoding.
#[must_use]
pub fn encoding(&self) -> crate::YellowstoneAccountMemcmpEncoding {
return match &self.data {
YellowstoneAccountMemcmpData::Bytes(_) => crate::YellowstoneAccountMemcmpEncoding::Bytes,
YellowstoneAccountMemcmpData::Base58(_) => crate::YellowstoneAccountMemcmpEncoding::Base58,
YellowstoneAccountMemcmpData::Base64(_) => crate::YellowstoneAccountMemcmpEncoding::Base64,
};
}
/// Returns the raw-byte payload when this predicate uses bytes.
#[must_use]
pub fn bytes_value(&self) -> std::option::Option<&[u8]> {
return match &self.data {
YellowstoneAccountMemcmpData::Bytes(value) => std::option::Option::Some(value.as_slice()),
YellowstoneAccountMemcmpData::Base58(_) | YellowstoneAccountMemcmpData::Base64(_) => std::option::Option::None,
};
}
/// Returns the base58/base64 text when this predicate uses a textual encoding.
#[must_use]
pub fn text_value(&self) -> std::option::Option<&str> {
return match &self.data {
YellowstoneAccountMemcmpData::Base58(value) | YellowstoneAccountMemcmpData::Base64(value) => std::option::Option::Some(value.as_str()),
YellowstoneAccountMemcmpData::Bytes(_) => std::option::Option::None,
};
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilterMemcmp {
let data = match &self.data {
YellowstoneAccountMemcmpData::Bytes(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Bytes(value.clone())
},
YellowstoneAccountMemcmpData::Base58(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Base58(value.clone())
},
YellowstoneAccountMemcmpData::Base64(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Base64(value.clone())
},
};
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilterMemcmp { offset: self.offset, data: std::option::Option::Some(data) };
}
}
impl std::fmt::Debug for YellowstoneAccountMemcmp {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let payload_length = match &self.data {
YellowstoneAccountMemcmpData::Bytes(value) => value.len(),
YellowstoneAccountMemcmpData::Base58(value) | YellowstoneAccountMemcmpData::Base64(value) => value.len(),
};
return formatter
.debug_struct("YellowstoneAccountMemcmp")
.field("offset", &self.offset)
.field("encoding", &self.encoding())
.field("payload_length", &payload_length)
.finish();
}
}
/// Lamport comparison used by a standard Yellowstone account filter.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum YellowstoneAccountLamportsFilter {
/// Lamports equal to the value.
Eq(u64),
/// Lamports not equal to the value.
Ne(u64),
/// Lamports lower than the value.
Lt(u64),
/// Lamports greater than the value.
Gt(u64),
}
/// One standard Yellowstone account predicate.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum YellowstoneAccountFilterPredicate {
/// Compare account data at one offset.
Memcmp(crate::YellowstoneAccountMemcmp),
/// Require an exact account-data size.
DataSize(u64),
/// Require the SPL Token account-state predicate to evaluate to the supplied boolean.
TokenAccountState(bool),
/// Compare the account lamport balance.
Lamports(crate::YellowstoneAccountLamportsFilter),
}
impl YellowstoneAccountFilterPredicate {
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilter {
let filter = match self {
Self::Memcmp(value) => yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Memcmp(value.to_wire()),
Self::DataSize(value) => yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Datasize(*value),
Self::TokenAccountState(value) => yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::TokenAccountState(*value),
Self::Lamports(value) => {
let cmp = match value {
crate::YellowstoneAccountLamportsFilter::Eq(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Eq(*value)
},
crate::YellowstoneAccountLamportsFilter::Ne(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Ne(*value)
},
crate::YellowstoneAccountLamportsFilter::Lt(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Lt(*value)
},
crate::YellowstoneAccountLamportsFilter::Gt(value) => {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Gt(*value)
},
};
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Lamports(
yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilterLamports { cmp: std::option::Option::Some(cmp) },
)
},
};
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccountsFilter { filter: std::option::Option::Some(filter) };
}
}
/// Complete account-family filter group for the standard Yellowstone subscribe surface.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeAccountFilter {
_private: (),
accounts: std::vec::Vec<ksp_core_lib::Pubkey>,
owners: std::vec::Vec<ksp_core_lib::Pubkey>,
filters: std::vec::Vec<crate::YellowstoneAccountFilterPredicate>,
nonempty_txn_signature: std::option::Option<bool>,
cuckoo_accounts_filter: std::option::Option<crate::YellowstoneCuckooFilter>,
}
impl YellowstoneSubscribeAccountFilter {
/// Creates an empty account filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
pub fn new() -> Self {
return Self {
accounts: std::vec::Vec::new(),
owners: std::vec::Vec::new(),
filters: std::vec::Vec::new(),
nonempty_txn_signature: std::option::Option::None,
cuckoo_accounts_filter: std::option::Option::None,
};
}
/// Adds one exact account selector.
pub fn push_account(&mut self, account: ksp_core_lib::Pubkey) -> ksp_core_lib::Result<()> {
if self.accounts.len() >= MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT {
return invalid_subscribe_parameter("grpc_subscribe.accounts.account", "Yellowstone account selector count exceeds the KSP bound");
}
self.accounts.push(account);
return std::result::Result::Ok(());
}
/// Adds one exact account-owner selector.
pub fn push_owner(&mut self, owner: ksp_core_lib::Pubkey) -> ksp_core_lib::Result<()> {
if self.owners.len() >= MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT {
return invalid_subscribe_parameter("grpc_subscribe.accounts.owner", "Yellowstone account owner count exceeds the KSP bound");
}
self.owners.push(owner);
return std::result::Result::Ok(());
}
/// Adds one account predicate while preserving wire order.
pub fn push_filter(&mut self, filter: crate::YellowstoneAccountFilterPredicate) -> ksp_core_lib::Result<()> {
if self.filters.len() >= MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT {
return invalid_subscribe_parameter("grpc_subscribe.accounts.filters", "Yellowstone account predicate count exceeds the KSP bound");
}
self.filters.push(filter);
return std::result::Result::Ok(());
}
/// Sets or clears the `nonempty_txn_signature` selector.
pub fn set_nonempty_txn_signature(&mut self, value: std::option::Option<bool>) {
self.nonempty_txn_signature = value;
}
/// Sets or clears the compressed account selector.
pub fn set_cuckoo_accounts_filter(&mut self, value: std::option::Option<crate::YellowstoneCuckooFilter>) {
self.cuckoo_accounts_filter = value;
}
/// Returns exact account selectors in wire order.
#[must_use]
pub fn accounts(&self) -> &[ksp_core_lib::Pubkey] {
return self.accounts.as_slice();
}
/// Returns exact owner selectors in wire order.
#[must_use]
pub fn owners(&self) -> &[ksp_core_lib::Pubkey] {
return self.owners.as_slice();
}
/// Returns account predicates in wire order.
#[must_use]
pub fn filters(&self) -> &[crate::YellowstoneAccountFilterPredicate] {
return self.filters.as_slice();
}
/// Returns the optional transaction-signature presence selector.
#[must_use]
pub const fn nonempty_txn_signature(&self) -> std::option::Option<bool> {
return self.nonempty_txn_signature;
}
/// Returns the optional compressed account selector.
#[must_use]
pub fn cuckoo_accounts_filter(&self) -> std::option::Option<&crate::YellowstoneCuckooFilter> {
return self.cuckoo_accounts_filter.as_ref();
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default();
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts {
account: self.accounts.iter().map(std::string::ToString::to_string).collect(),
owner: self.owners.iter().map(std::string::ToString::to_string).collect(),
filters: self.filters.iter().map(crate::grpc_subscribe::YellowstoneAccountFilterPredicate::to_wire).collect(),
nonempty_txn_signature: self.nonempty_txn_signature,
cuckoo_accounts_filter: self.cuckoo_accounts_filter.as_ref().map(crate::grpc_subscribe::YellowstoneCuckooFilter::to_wire),
};
}
}
/// 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)]
impl std::default::Default for YellowstoneSubscribeAccountFilter {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for YellowstoneSubscribeAccountFilter {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribeAccountFilter")
.field("account_count", &self.accounts.len())
.field("owner_count", &self.owners.len())
.field("predicate_count", &self.filters.len())
.field("nonempty_txn_signature", &self.nonempty_txn_signature)
.field("has_cuckoo_accounts_filter", &self.cuckoo_accounts_filter.is_some())
.finish();
}
}
/// Complete slot-family filter group for the standard Yellowstone subscribe surface.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeSlotFilter {
_private: (),
filter_by_commitment: std::option::Option<bool>,
interslot_updates: std::option::Option<bool>,
}
impl YellowstoneSubscribeSlotFilter {
/// Creates an empty slot filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
return Self { filter_by_commitment: std::option::Option::None, interslot_updates: std::option::Option::None };
}
/// Sets or clears the commitment-filtering flag.
pub fn set_filter_by_commitment(&mut self, value: std::option::Option<bool>) {
self.filter_by_commitment = value;
}
/// Sets or clears interslot status updates.
pub fn set_interslot_updates(&mut self, value: std::option::Option<bool>) {
self.interslot_updates = value;
}
/// Returns the optional commitment-filtering flag.
#[must_use]
pub const fn filter_by_commitment(self) -> std::option::Option<bool> {
return self.filter_by_commitment;
}
/// Returns the optional interslot-update flag.
#[must_use]
pub const fn interslot_updates(self) -> std::option::Option<bool> {
return self.interslot_updates;
}
#[cfg(test)]
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default();
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots {
filter_by_commitment: self.filter_by_commitment,
interslot_updates: self.interslot_updates,
};
}
}
/// Timestamp attached to one standard Yellowstone update envelope.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneUpdateTimestamp {
seconds: i64,
nanos: u32,
}
impl YellowstoneUpdateTimestamp {
/// Creates one validated protobuf-compatible timestamp.
pub fn new(seconds: i64, nanos: u32) -> ksp_core_lib::Result<Self> {
if nanos >= 1_000_000_000 {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "Yellowstone update timestamp has invalid nanoseconds")
.with_context("grpc_update", "timestamp"),
);
}
return std::result::Result::Ok(Self { seconds, nanos });
}
/// Returns Unix seconds.
#[must_use]
pub const fn seconds(self) -> i64 {
return self.seconds;
}
/// Returns sub-second nanoseconds.
#[must_use]
pub const fn nanos(self) -> u32 {
return self.nanos;
}
}
/// Fixed-width transaction signature attached to a Yellowstone account update when available.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct YellowstoneTransactionSignature {
bytes: [u8; 64],
}
impl YellowstoneTransactionSignature {
/// Creates one fixed-width signature from exact bytes.
#[must_use]
pub const fn new(bytes: [u8; 64]) -> Self {
return Self { bytes };
}
/// Returns the exact 64 signature bytes.
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 64] {
return &self.bytes;
}
}
/// Typed account payload carried by one standard Yellowstone account update.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneAccountInfo {
pubkey: ksp_core_lib::Pubkey,
lamports: u64,
owner: ksp_core_lib::Pubkey,
executable: bool,
rent_epoch: u64,
data: std::vec::Vec<u8>,
write_version: u64,
transaction_signature: std::option::Option<crate::YellowstoneTransactionSignature>,
}
impl YellowstoneAccountInfo {
/// Returns the account public key.
#[must_use]
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
/// Returns the lamport balance.
#[must_use]
pub const fn lamports(&self) -> u64 {
return self.lamports;
}
/// Returns the owner public key.
#[must_use]
pub const fn owner(&self) -> &ksp_core_lib::Pubkey {
return &self.owner;
}
/// Returns whether the account is executable.
#[must_use]
pub const fn executable(&self) -> bool {
return self.executable;
}
/// Returns the rent epoch.
#[must_use]
pub const fn rent_epoch(&self) -> u64 {
return self.rent_epoch;
}
/// Returns the account bytes after any request-side `accounts_data_slice` has been applied by the server.
#[must_use]
pub fn data(&self) -> &[u8] {
return self.data.as_slice();
}
/// Returns the account write version.
#[must_use]
pub const fn write_version(&self) -> u64 {
return self.write_version;
}
/// Returns the optional transaction signature associated with this update.
#[must_use]
pub const fn transaction_signature(&self) -> std::option::Option<crate::YellowstoneTransactionSignature> {
return self.transaction_signature;
}
}
impl std::fmt::Debug for YellowstoneAccountInfo {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneAccountInfo")
.field("lamports", &self.lamports)
.field("executable", &self.executable)
.field("rent_epoch", &self.rent_epoch)
.field("data_length", &self.data.len())
.field("write_version", &self.write_version)
.field("has_transaction_signature", &self.transaction_signature.is_some())
.finish_non_exhaustive();
}
}
/// Standard Yellowstone account update projected into KSP-owned types.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneAccountUpdate {
filters: std::vec::Vec<crate::YellowstoneSubscribeFilterName>,
created_at: std::option::Option<crate::YellowstoneUpdateTimestamp>,
account: crate::YellowstoneAccountInfo,
slot: u64,
is_startup: bool,
}
impl YellowstoneAccountUpdate {
/// Returns echoed matching filter names in server order.
#[must_use]
pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] {
return self.filters.as_slice();
}
/// Returns the optional server creation timestamp.
#[must_use]
pub const fn created_at(&self) -> std::option::Option<crate::YellowstoneUpdateTimestamp> {
return self.created_at;
}
/// Returns the typed account payload.
#[must_use]
pub const fn account(&self) -> &crate::YellowstoneAccountInfo {
return &self.account;
}
/// Returns the slot carrying the account update.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns whether the update belongs to startup/replay state reported by the server.
#[must_use]
pub const fn is_startup(&self) -> bool {
return self.is_startup;
}
}
impl std::fmt::Debug for YellowstoneAccountUpdate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneAccountUpdate")
.field("filter_count", &self.filters.len())
.field("created_at", &self.created_at)
.field("account", &self.account)
.field("slot", &self.slot)
.field("is_startup", &self.is_startup)
.finish();
}
}
/// Slot status published by current standard Yellowstone updates.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum YellowstoneSlotStatus {
/// Slot reached processed commitment.
Processed,
/// Slot reached confirmed commitment.
Confirmed,
/// Slot reached finalized commitment.
Finalized,
/// First shred for the slot was received.
FirstShredReceived,
/// Slot ingestion completed.
Completed,
/// A bank was created for the slot.
CreatedBank,
/// Slot was marked dead.
Dead,
}
/// Standard Yellowstone slot update projected into KSP-owned types.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSlotUpdate {
filters: std::vec::Vec<crate::YellowstoneSubscribeFilterName>,
created_at: std::option::Option<crate::YellowstoneUpdateTimestamp>,
slot: u64,
parent: std::option::Option<u64>,
status: crate::YellowstoneSlotStatus,
dead_error: std::option::Option<std::string::String>,
}
impl YellowstoneSlotUpdate {
/// Returns echoed matching filter names in server order.
#[must_use]
pub fn filters(&self) -> &[crate::YellowstoneSubscribeFilterName] {
return self.filters.as_slice();
}
/// Returns the optional server creation timestamp.
#[must_use]
pub const fn created_at(&self) -> std::option::Option<crate::YellowstoneUpdateTimestamp> {
return self.created_at;
}
/// Returns the slot number.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the optional parent slot exactly as published.
#[must_use]
pub const fn parent(&self) -> std::option::Option<u64> {
return self.parent;
}
/// Returns the current Yellowstone slot status.
#[must_use]
pub const fn status(&self) -> crate::YellowstoneSlotStatus {
return self.status;
}
/// Returns the bounded server dead-slot diagnostic when present.
#[must_use]
pub fn dead_error(&self) -> std::option::Option<&str> {
return self.dead_error.as_deref();
}
}
impl std::fmt::Debug for YellowstoneSlotUpdate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSlotUpdate")
.field("filter_count", &self.filters.len())
.field("created_at", &self.created_at)
.field("slot", &self.slot)
.field("parent", &self.parent)
.field("status", &self.status)
.field("has_dead_error", &self.dead_error.is_some())
.finish();
}
}
@@ -531,6 +1218,153 @@ impl YellowstoneSubscribeRequest {
}
}
#[cfg(test)]
fn decode_account_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneAccountUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
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::Account(value)) => value,
_ => return invalid_subscribe_response("account", "Yellowstone update does not contain an account payload"),
};
let account = match update.account {
std::option::Option::Some(value) => match decode_account_info(value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => return invalid_subscribe_response("account", "Yellowstone account update is missing account info"),
};
return std::result::Result::Ok(crate::YellowstoneAccountUpdate { filters, created_at, account, slot: update.slot, is_startup: update.is_startup });
}
#[cfg(test)]
fn decode_slot_update(wire: yellowstone_grpc_proto::geyser::SubscribeUpdate) -> ksp_core_lib::Result<crate::YellowstoneSlotUpdate> {
let (filters, created_at) = match decode_update_envelope(wire.filters, wire.created_at) {
std::result::Result::Ok(value) => value,
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::Slot(value)) => value,
_ => return invalid_subscribe_response("slot", "Yellowstone update does not contain a slot payload"),
};
let status = match update.status {
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed as i32 => crate::YellowstoneSlotStatus::Processed,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotConfirmed as i32 => crate::YellowstoneSlotStatus::Confirmed,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotFinalized as i32 => crate::YellowstoneSlotStatus::Finalized,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotFirstShredReceived as i32 => crate::YellowstoneSlotStatus::FirstShredReceived,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotCompleted as i32 => crate::YellowstoneSlotStatus::Completed,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotCreatedBank as i32 => crate::YellowstoneSlotStatus::CreatedBank,
value if value == yellowstone_grpc_proto::geyser::SlotStatus::SlotDead as i32 => crate::YellowstoneSlotStatus::Dead,
_ => return invalid_subscribe_response("slot.status", "Yellowstone slot update contains an unknown status"),
};
if update.dead_error.as_ref().is_some_and(|value| value.len() > MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES) {
return invalid_subscribe_response("slot.dead_error", "Yellowstone dead-slot diagnostic exceeds the KSP bound");
}
return std::result::Result::Ok(crate::YellowstoneSlotUpdate {
filters,
created_at,
slot: update.slot,
parent: update.parent,
status,
dead_error: update.dead_error,
});
}
#[cfg(test)]
fn decode_update_envelope(
filters: std::vec::Vec<std::string::String>,
created_at: std::option::Option<yellowstone_grpc_proto::prost_types::Timestamp>,
) -> ksp_core_lib::Result<(std::vec::Vec<crate::YellowstoneSubscribeFilterName>, std::option::Option<crate::YellowstoneUpdateTimestamp>)> {
if filters.len() > MAX_GRPC_SUBSCRIBE_UPDATE_FILTER_COUNT {
return invalid_subscribe_response("filters", "Yellowstone update filter-name count exceeds the KSP bound");
}
let mut decoded_filters = std::vec::Vec::with_capacity(filters.len());
for filter in filters {
let decoded = match crate::YellowstoneSubscribeFilterName::new(filter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return invalid_subscribe_response("filters", "Yellowstone update contains an invalid filter name"),
};
decoded_filters.push(decoded);
}
let decoded_created_at = match created_at {
std::option::Option::Some(value) => {
let nanos = match u32::try_from(value.nanos) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return invalid_subscribe_response("created_at", "Yellowstone update timestamp contains negative nanoseconds"),
};
match crate::YellowstoneUpdateTimestamp::new(value.seconds, nanos) {
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((decoded_filters, decoded_created_at));
}
#[cfg(test)]
fn decode_account_info(wire: yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo) -> ksp_core_lib::Result<crate::YellowstoneAccountInfo> {
if wire.data.len() > MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES {
return invalid_subscribe_response("account.data", "Yellowstone account data exceeds the KSP bound");
}
let pubkey = match decode_pubkey_bytes("account.pubkey", wire.pubkey) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match decode_pubkey_bytes("account.owner", wire.owner) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transaction_signature = match wire.txn_signature {
std::option::Option::Some(value) => {
let bytes: [u8; YELLOWSTONE_TRANSACTION_SIGNATURE_LENGTH_BYTES] = match value.try_into() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return invalid_subscribe_response("account.txn_signature", "Yellowstone account update contains an invalid transaction signature length");
},
};
std::option::Option::Some(crate::YellowstoneTransactionSignature::new(bytes))
},
std::option::Option::None => std::option::Option::None,
};
return std::result::Result::Ok(crate::YellowstoneAccountInfo {
pubkey,
lamports: wire.lamports,
owner,
executable: wire.executable,
rent_epoch: wire.rent_epoch,
data: wire.data,
write_version: wire.write_version,
transaction_signature,
});
}
#[cfg(test)]
fn decode_pubkey_bytes(field: &'static str, bytes: std::vec::Vec<u8>) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
let array: [u8; 32] = match bytes.try_into() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return invalid_subscribe_response(field, "Yellowstone account update contains an invalid public-key length"),
};
return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(array));
}
fn validate_memcmp_text(value: &str) -> ksp_core_lib::Result<()> {
if value.len() > MAX_GRPC_SUBSCRIBE_MEMCMP_TEXT_LENGTH_BYTES || !value.is_ascii() || value.chars().any(char::is_whitespace) {
return invalid_subscribe_parameter("grpc_subscribe.accounts.filters.memcmp.text", "Yellowstone memcmp text violates KSP bounds");
}
return std::result::Result::Ok(());
}
fn invalid_subscribe_parameter<T>(field: &'static str, message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message).with_context("field", field));
}
#[cfg(test)]
fn invalid_subscribe_response<T>(field: &'static str, message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_update", field));
}
#[cfg(test)]
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
return commitment.map(|value| {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 37
// version: 38
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -134,9 +134,29 @@ 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 validated standard Yellowstone account predicate.
pub use self::grpc_subscribe::YellowstoneAccountFilterPredicate;
/// Typed account payload carried by one standard Yellowstone account update.
pub use self::grpc_subscribe::YellowstoneAccountInfo;
/// Lamport comparison used by standard Yellowstone account filters.
pub use self::grpc_subscribe::YellowstoneAccountLamportsFilter;
/// Validated standard Yellowstone account memcmp predicate.
pub use self::grpc_subscribe::YellowstoneAccountMemcmp;
/// Encoding selected by one Yellowstone account memcmp predicate.
pub use self::grpc_subscribe::YellowstoneAccountMemcmpEncoding;
/// Standard Yellowstone account-update projection owned by KSP.
pub use self::grpc_subscribe::YellowstoneAccountUpdate;
/// One standard Yellowstone account-data slice.
pub use self::grpc_subscribe::YellowstoneAccountsDataSlice;
/// Account-family filter-group shell for standard Yellowstone Subscribe.
/// 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;
/// Current standard Yellowstone slot status.
pub use self::grpc_subscribe::YellowstoneSlotStatus;
/// Standard Yellowstone slot-update projection owned by KSP.
pub use self::grpc_subscribe::YellowstoneSlotUpdate;
/// Complete account-family filter group 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;
@@ -150,10 +170,14 @@ pub use self::grpc_subscribe::YellowstoneSubscribeFilterName;
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.
/// Complete slot-family filter group 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;
/// Fixed-width transaction signature attached to Yellowstone account updates when available.
pub use self::grpc_subscribe::YellowstoneTransactionSignature;
/// Timestamp attached to standard Yellowstone update envelopes.
pub use self::grpc_subscribe::YellowstoneUpdateTimestamp;
/// 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.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 42
// version: 43
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -866,3 +866,33 @@ fn public_v0_2_9_pre_004_yellowstone_subscribe_common_contract_is_available_from
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));
}
#[test]
fn public_v0_2_9_pre_005_yellowstone_accounts_slots_contract_is_available_from_crate_root() {
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
let mut accounts = ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new();
assert!(accounts.push_account(account).is_ok());
assert!(accounts.push_owner(owner).is_ok());
let memcmp = ksp_onchain_transport_lib::YellowstoneAccountMemcmp::bytes(0, vec![1_u8, 2, 3]).expect("public memcmp must validate");
assert!(accounts.push_filter(ksp_onchain_transport_lib::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
accounts.set_nonempty_txn_signature(std::option::Option::Some(true));
let cuckoo =
ksp_onchain_transport_lib::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 1, ksp_onchain_transport_lib::YellowstoneCuckooHashAlgorithm::SipHash)
.expect("public cuckoo filter must validate");
accounts.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
let mut slots = ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new();
slots.set_filter_by_commitment(std::option::Option::Some(true));
slots.set_interslot_updates(std::option::Option::Some(true));
let timestamp = ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1, 2).expect("public timestamp must validate");
let signature = ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([3_u8; 64]);
assert_eq!(timestamp.nanos(), 2);
assert_eq!(signature.as_bytes(), &[3_u8; 64]);
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(true));
let _account_info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountInfo>();
let _account_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
let _slot_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
let _slot_status = ksp_onchain_transport_lib::YellowstoneSlotStatus::FirstShredReceived;
let _lamports = ksp_onchain_transport_lib::YellowstoneAccountLamportsFilter::Gt(10);
let _encoding = ksp_onchain_transport_lib::YellowstoneAccountMemcmpEncoding::Bytes;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 35
// version: 36
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -1111,3 +1111,47 @@ fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract()
let _transaction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter>();
}
#[test]
fn release_v0_2_9_pre_005_completes_standard_accounts_and_slots_without_advancing_transactions_or_blocks() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"account: self.accounts",
"owner: self.owners",
"filters: self.filters",
"nonempty_txn_signature",
"cuckoo_accounts_filter",
"SubscribeRequestFilterAccountsFilterMemcmp",
"DataSize",
"TokenAccountState",
"YellowstoneAccountLamportsFilter",
"filter_by_commitment",
"interslot_updates",
"YellowstoneAccountUpdate",
"YellowstoneSlotUpdate",
"YellowstoneTransactionSignature",
"SlotFirstShredReceived",
"SlotCompleted",
"SlotCreatedBank",
"SlotDead",
"dead_error",
] {
assert!(source.contains(required), "missing pre.005 Accounts/Slots contract token: {required}");
}
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_CUCKOO_DATA_LENGTH_BYTES"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES"));
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("YellowstoneTransactionUpdate"));
assert!(!source.contains("YellowstoneBlockUpdate"));
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 _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
// version: 1
// version: 2
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
@@ -123,3 +123,245 @@ fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() {
assert!(debug.contains("account_filter_count"));
assert!(debug.contains("from_slot"));
}
#[test]
fn yellowstone_account_and_slot_filters_encode_complete_current_wire() {
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
assert!(filter.push_account(account).is_ok());
assert!(filter.push_owner(owner).is_ok());
let raw = crate::YellowstoneAccountMemcmp::bytes(4, vec![1_u8, 2, 3]).expect("raw memcmp must validate");
let base58 = crate::YellowstoneAccountMemcmp::base58(8, "1234").expect("base58 memcmp must validate");
let base64 = crate::YellowstoneAccountMemcmp::base64(12, "AQID==").expect("base64 memcmp must validate");
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(raw)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base58)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base64)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::DataSize(165)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::TokenAccountState(true)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Eq(1))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Ne(2))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Lt(3))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Gt(4))).is_ok());
filter.set_nonempty_txn_signature(std::option::Option::Some(true));
let cuckoo =
crate::YellowstoneCuckooFilter::new(vec![9_u8; 16], 4, 4, 8, 77, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo fixture must validate");
filter.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
let wire = filter.to_wire();
assert_eq!(wire.account, vec![account.to_string()]);
assert_eq!(wire.owner, vec![owner.to_string()]);
assert_eq!(wire.filters.len(), 9);
match wire.filters[0].filter.as_ref().expect("raw memcmp oneof must be present") {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Memcmp(value) => assert_eq!(
value.data,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Bytes(vec![1_u8, 2, 3]))
),
_ => panic!("first predicate must stay memcmp"),
}
match wire.filters[5].filter.as_ref().expect("lamports oneof must be present") {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Lamports(value) => {
assert_eq!(value.cmp, std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Eq(1)))
},
_ => panic!("sixth predicate must stay lamports"),
}
assert_eq!(wire.nonempty_txn_signature, std::option::Option::Some(true));
let cuckoo = wire.cuckoo_accounts_filter.expect("cuckoo filter must be present");
assert_eq!(cuckoo.data, vec![9_u8; 16]);
assert_eq!(cuckoo.bucket_count, 4);
assert_eq!(cuckoo.entries_per_bucket, 4);
assert_eq!(cuckoo.fingerprint_bits, 8);
assert_eq!(cuckoo.hash_seed, 77);
assert_eq!(cuckoo.hash_algorithm, yellowstone_grpc_proto::geyser::CuckooHashAlgorithm::SipHash as i32);
let mut slots = crate::YellowstoneSubscribeSlotFilter::new();
slots.set_filter_by_commitment(std::option::Option::Some(false));
slots.set_interslot_updates(std::option::Option::Some(true));
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(false));
assert_eq!(slots.interslot_updates(), std::option::Option::Some(true));
let slots_wire = slots.to_wire();
assert_eq!(slots_wire.filter_by_commitment, std::option::Option::Some(false));
assert_eq!(slots_wire.interslot_updates, std::option::Option::Some(true));
}
#[test]
fn yellowstone_account_filter_bounds_and_debug_are_provider_neutral() {
assert!(crate::YellowstoneCuckooFilter::new(vec![], 0, 4, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 0, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 7, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 12, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_ok());
assert!(crate::YellowstoneAccountMemcmp::base58(0, "contains whitespace").is_err());
assert!(crate::YellowstoneAccountMemcmp::base64(0, "line\nbreak").is_err());
let memcmp = crate::YellowstoneAccountMemcmp::bytes(5, vec![7_u8, 8, 9]).expect("memcmp must validate");
let debug = format!("{memcmp:?}");
assert!(debug.contains("payload_length"));
assert!(!debug.contains("7, 8, 9"));
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
assert!(filter.push_account(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
let filter_debug = format!("{filter:?}");
assert!(filter_debug.contains("account_count"));
assert!(!filter_debug.contains(&ksp_core_lib::Pubkey::new_from_array([3_u8; 32]).to_string()));
}
#[test]
fn yellowstone_account_update_decodes_complete_wire_and_redacts_payload_debug() {
let signature = vec![6_u8; 64];
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts-main".to_owned(), "accounts-owner".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 32],
lamports: 42,
owner: vec![2_u8; 32],
executable: true,
rent_epoch: 9,
data: vec![0xAA_u8, 0xBB, 0xCC],
write_version: 7,
txn_signature: std::option::Option::Some(signature),
}),
slot: 123,
is_startup: true,
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 1_700_000_000, nanos: 123_456_789 }),
};
let update = super::decode_account_update(wire).expect("account update fixture must decode");
assert_eq!(update.filters()[0].as_str(), "accounts-main");
assert_eq!(update.filters()[1].as_str(), "accounts-owner");
assert_eq!(update.created_at().expect("timestamp must be present").seconds(), 1_700_000_000);
assert_eq!(update.created_at().expect("timestamp must be present").nanos(), 123_456_789);
assert_eq!(update.slot(), 123);
assert!(update.is_startup());
assert_eq!(update.account().pubkey(), &ksp_core_lib::Pubkey::new_from_array([1_u8; 32]));
assert_eq!(update.account().owner(), &ksp_core_lib::Pubkey::new_from_array([2_u8; 32]));
assert_eq!(update.account().lamports(), 42);
assert!(update.account().executable());
assert_eq!(update.account().rent_epoch(), 9);
assert_eq!(update.account().data(), &[0xAA_u8, 0xBB, 0xCC]);
assert_eq!(update.account().write_version(), 7);
assert_eq!(update.account().transaction_signature().expect("signature must be present").as_bytes(), &[6_u8; 64]);
let debug = format!("{update:?}");
assert!(!debug.contains("accounts-main"));
assert!(!debug.contains("170, 187, 204"));
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
}
#[test]
fn yellowstone_account_update_rejects_malformed_fixed_width_and_envelope_fields() {
let malformed_pubkey = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 31],
lamports: 0,
owner: vec![2_u8; 32],
executable: false,
rent_epoch: 0,
data: vec![],
write_version: 0,
txn_signature: std::option::Option::None,
}),
slot: 0,
is_startup: false,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(malformed_pubkey).is_err());
let malformed_signature = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 32],
lamports: 0,
owner: vec![2_u8; 32],
executable: false,
rent_epoch: 0,
data: vec![],
write_version: 0,
txn_signature: std::option::Option::Some(vec![9_u8; 63]),
}),
slot: 0,
is_startup: false,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(malformed_signature).is_err());
let missing_info = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount { account: std::option::Option::None, slot: 0, is_startup: false },
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(missing_info).is_err());
}
#[test]
fn yellowstone_slot_update_preserves_all_current_statuses_and_bounds_dead_error() {
let statuses = [
(yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed, crate::YellowstoneSlotStatus::Processed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotConfirmed, crate::YellowstoneSlotStatus::Confirmed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFinalized, crate::YellowstoneSlotStatus::Finalized),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFirstShredReceived, crate::YellowstoneSlotStatus::FirstShredReceived),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCompleted, crate::YellowstoneSlotStatus::Completed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCreatedBank, crate::YellowstoneSlotStatus::CreatedBank),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotDead, crate::YellowstoneSlotStatus::Dead),
];
for (wire_status, expected) in statuses {
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 88,
parent: std::option::Option::Some(87),
status: wire_status as i32,
dead_error: if expected == crate::YellowstoneSlotStatus::Dead {
std::option::Option::Some("fork rejected".to_owned())
} else {
std::option::Option::None
},
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 5, nanos: 6 }),
};
let update = super::decode_slot_update(wire).expect("slot update fixture must decode");
assert_eq!(update.status(), expected);
assert_eq!(update.slot(), 88);
assert_eq!(update.parent(), std::option::Option::Some(87));
assert_eq!(update.filters()[0].as_str(), "slots");
if expected == crate::YellowstoneSlotStatus::Dead {
assert_eq!(update.dead_error(), std::option::Option::Some("fork rejected"));
assert!(!format!("{update:?}").contains("fork rejected"));
}
}
let unknown = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 1,
parent: std::option::Option::None,
status: 99,
dead_error: std::option::Option::None,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_slot_update(unknown).is_err());
let oversized = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 1,
parent: std::option::Option::None,
status: yellowstone_grpc_proto::geyser::SlotStatus::SlotDead as i32,
dead_error: std::option::Option::Some("x".repeat(16 * 1024 + 1)),
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_slot_update(oversized).is_err());
}

121
deltas/0.2.9/pre.005.md Normal file
View File

@@ -0,0 +1,121 @@
<!-- file: deltas/0.2.9/pre.005.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.005` — Yellowstone standard Accounts + Slots
## Base et signal de version
Base autoritaire :
```text
0.2.9-pre.4.fix.1
```
Signal technique de cette tranche :
```text
workspace.package.version = 0.2.9-pre.5
commit attendu = v0.2.9-pre.005
aucun tag prerelease
```
Le gate opérateur de `pre.004-fix.001` est vert sans les warnings `dead_code` précédents ; `pre.004` est donc fermée.
## Périmètre réalisé
Cette tranche complète **uniquement Accounts + Slots** du contrat Yellowstone `Subscribe` standard actuel.
Accounts request :
```text
account[]
owner[]
filters[] :
memcmp bytes/base58/base64
datasize
token_account_state
lamports eq/ne/lt/gt
nonempty_txn_signature?
cuckoo_accounts_filter?
```
Slots request :
```text
filter_by_commitment?
interslot_updates?
```
Updates KSP :
```text
Account : filters/created_at + account info + slot + is_startup
Slot : filters/created_at + slot + parent? + status + dead_error?
```
Les sept statuts Slot actuels sont couverts : processed, confirmed, finalized, first-shred-received, completed, created-bank et dead.
## Cuckoo standard
Le Cuckoo account filter est traité comme capacité du standard Yellowstone courant, pas comme extension provider. KSP expose son propre DTO avec données, dimensions, fingerprint bits, seed et algorithme ; l'algorithme courant matérialisé est SipHash. Aucun type upstream n'est réexporté.
## Validation et sécurité
Bornes KSP provider-neutral ajoutées sur :
```text
account/owner selectors
account predicates
memcmp binary/text
Cuckoo payload/dimensions/fingerprint
account-data update
update filter names/count
timestamp nanos
transaction signature fixe 64 octets
dead_error
```
Les pubkeys/owners de wire doivent faire exactement 32 octets et les signatures 64 octets. Un enum Slot inconnu, un oneof ou champ obligatoire absent et les payloads hors bornes deviennent des erreurs KSP déterministes ; aucun payload provider arbitraire n'est recopié dans les diagnostics. Les Debug KSP n'exposent pas les sélecteurs, données memcmp/Cuckoo/account-data ni le texte `dead_error`.
## Frontière runtime
Le stream bidi reste hors tranche. Les conversions protobuf request et les décodeurs Account/Slot restent donc compilés seulement pour les tests jusqu'à `pre.008`, où ils acquerront leur premier consommateur runtime. Cela évite les warnings `dead_code` sans dupliquer l'implémentation.
## Explicitement hors `pre.005`
```text
Transactions + transaction_status
Blocks + block_meta + entry
Ping/Pong lifecycle et mutation bidi
backpressure/shutdown stream
reconnect/resubscribe/replay
Config V3
PublicNode provider integration
SubscribeDeshred
```
## Dépendances
Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. Les graphes inspectés en `pre.003` restent applicables.
## Validation attendue
```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
```
Aucun `cargo tree` supplémentaire n'est requis pour cette tranche si le manifest reste inchangé.
## Suite
```text
pre.006 Transactions + transaction_status
pre.007 Blocks + block_meta + entry
pre.008 stream bidi + mutation + Ping/Pong + half-close + backpressure + shutdown
```

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
> **Statut : `0.2.9-pre.004-fix.001` candidate. Le premier gate opérateur de `pre.004` est fonctionnellement vert (359 unit + 44 public API + 37 completeness + 4 doctests, workspace PASS) mais `cargo check`/Clippy/workspace émettent 10 warnings `dead_code` sur les conversions protobuf encore utilisées uniquement par les tests. Le fix compile ces helpers sous `#[cfg(test)]` jusqu'à leur consommation runtime lors de l'ouverture du stream. Aucun contrat public, filtre ou wire n'est modifié. `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.005007` 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 1520 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-fix.001` est fermée sur gate opérateur sans warning : fmt/audit/check/Clippy/workspace PASS, Transport 359 unit + 44 public API + 37 completeness + 4 doctests. `0.2.9-pre.005` est candidate et complète uniquement les familles Yellowstone standard Accounts + Slots : filtres request complets, Cuckoo standard, memcmp bytes/base58/base64, predicates datasize/token-state/lamports, options Slots et DTOs/decode Account/Slot. Les conversions wire/decode restent test-only tant que le stream bidi n'est pas ouvert en `pre.008`. Transactions/Blocks, 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 1520 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
@@ -919,11 +919,11 @@ pre.002 DONE — moteur Yellowstone : proto/dependencies + settings/errors + ch
pre.003 DONE — moteur TLS/metadata + façade N2 unary + fixture locale + 7 unary RPCs
budget : 1520 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS + graphes Cargo inspectés
pre.004 FIX.001 CANDIDATE — standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds
budget : 1520 min ; premier gate fonctionnel PASS, fix warning-only sur helpers wire test-only
pre.004 DONE — standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds
budget : 1520 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 359/44/37/4
pre.005 standard Solana : Accounts + Slots filters/updates
budget : 1520 min ; preuve : fixtures exactes + enum/optional/malformed/adversarial
pre.005 CANDIDATE — standard Solana : Accounts + Slots filters/updates
budget : 1520 min ; preuve : request wire complet + Cuckoo/memcmp/predicates + Account/Slot updates exacts/malformed/adversarial
pre.006 standard Solana : Transactions + transaction_status
budget : 1520 min ; preuve : include/exclude/required/Cuckoo/token expansion + tx/meta
@@ -1086,6 +1086,44 @@ OUT : Subscribe, SubscribeDeshred, PublicNode, Config V3, reconnect/stream lifec
Le runtime n'active pas la feature `tonic` de `yellowstone-grpc-proto`; cette feature et `tonic codegen/server` sont réservées au graphe dev/test de la fixture. Le gate Cargo opérateur reste requis avant commit.
## 19.3 Gate opérateur final `pre.004-fix.001`
Preuve opérateur du `2026-08-24` :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS sans warning gRPC pre.004
cargo clippy --workspace --all-targets PASS sans warning gRPC pre.004
Transport unit PASS 359/359
Transport public_api PASS 44/44
Transport release_completeness PASS 37/37
Transport doctests PASS 4/4
cargo test --workspace PASS ; seuls smokes/bench diagnostics explicitement ignored
```
`pre.004` est donc **fermée**. Aucun changement de dépendance n'ayant eu lieu depuis `pre.003`, les graphes Cargo déjà inspectés restent l'autorité du dependency set.
## 19.4 Gate source `pre.005` — Accounts + Slots
La candidate matérialise exactement :
```text
Accounts request : account[]/owner[]/filters[]/nonempty_txn_signature?/cuckoo_accounts_filter?
Account predicates : memcmp bytes/base58/base64, datasize, token_account_state, lamports eq/ne/lt/gt
Slots request : filter_by_commitment? + interslot_updates?
Account update : filters/created_at + account info + slot + is_startup
Slot update : filters/created_at + slot + parent? + 7 SlotStatus + dead_error?
```
Les bornes provider-neutral KSP de cette tranche couvrent notamment les sélecteurs Accounts, predicates, memcmp, Cuckoo, taille account-data, noms de filtres d'update, timestamp nanos, signature fixe 64 octets et `dead_error`. Les Debug KSP n'exposent ni pubkeys sélectionnées, ni payload memcmp/Cuckoo/account-data, ni texte `dead_error`.
Les conversions protobuf et décodeurs update restent sous `#[cfg(test)]` jusqu'à `pre.008`, car aucun stream runtime ne les consomme encore. Cette décision évite du faux `dead_code` sans créer une seconde implémentation : les mêmes helpers seront remis en runtime au moment de l'ouverture bidi.
OUT de `pre.005` : Transactions/transaction_status, Blocks/block_meta/entry, stream bidi, provider PublicNode, Config V3 et `SubscribeDeshred`.
**Verdict `pre.005` : candidate source prête ; fermeture après gate Cargo opérateur.**
## 20. Conditions de clôture
`0.2.9` ne devient stable que si :

View File

@@ -1,9 +1,9 @@
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
<!-- version: 12 -->
<!-- version: 14 -->
# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode
> **Statut : `pre.003-fix.001` est fermé sur gate opérateur intégralement vert et graphes Cargo inspectés. `0.2.9-pre.004-fix.001` est candidate après un premier gate `pre.004` fonctionnellement vert mais non silencieux : 10 warnings `dead_code` concernent uniquement les conversions protobuf privées encore utilisées seulement par les unit tests. Le fix les place sous `#[cfg(test)]` sans changement de runtime/public API. `0.2.9-pre.004` 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.005007`. 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.004-fix.001` est fermé sur gate opérateur intégralement vert et sans les 10 warnings `dead_code` précédents : fmt/audit/check/Clippy/workspace PASS, Transport 359 unit + 44 public API + 37 completeness + 4 doctests. `0.2.9-pre.005` est candidate et complète uniquement Accounts + Slots du `Subscribe` standard : filtres request courants complets, Cuckoo standard, memcmp/predicates, options Slots, DTOs Account/Slot et décodage exact/malformed sous fixture. Aucun stream bidi n'est encore ouvert ; Transactions/Blocks restent `pre.006007`, puis lifecycle en `pre.008`. 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 | 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 |
| RPC | Forme | Classification | Scope | Preuve cible | État |
|-----------------------|-------|------------------------------------------------------|-------|-----------------------|------------------------------------------------------|
| `Subscribe` | bidi | standard | IN | fixture locale + live | PARTIAL pre.005 Accounts/Slots / 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
@@ -651,4 +651,48 @@ Le proto publié `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant impl
Les dix symboles concernés (`to_wire` et `commitment_to_wire`) servent uniquement aux tests de projection protobuf de `pre.004`. Le stream bidi n'étant pas ouvert avant `pre.008`, ils ne font pas encore partie du runtime. `#[cfg(test)]` évite donc un faux code mort de production sans créer une seconde implémentation ni modifier le contrat public. Lors de l'intégration runtime du stream, ces helpers seront naturellement retirés du `cfg(test)` au moment où ils auront un consommateur de production.
**Verdict `pre.004-fix.001` : correctif warning-only prêt ; fermeture de `pre.004` après gate opérateur sans warning.**
**Verdict `pre.004-fix.001` : fermé.**
### 19.2 Gate opérateur final `pre.004-fix.001`
| Gate / observation | Résultat final |
|------------------------------------------|----------------------------|
| workspace version | `0.2.9-pre.4.fix.1` |
| `cargo fmt --all` | PASS |
| audit Rust workspace | PASS / clean |
| `cargo check --workspace` | PASS sans warnings pre.004 |
| `cargo clippy --workspace --all-targets` | PASS sans warnings pre.004 |
| Transport unit | 359/359 PASS |
| Transport `public_api` | 44/44 PASS |
| Transport `release_completeness` | 37/37 PASS |
| Transport doctests | 4/4 PASS |
| `cargo test --workspace` | PASS |
`pre.004` est donc fermée.
## 20. Gate `pre.005` — Accounts + Slots candidate
| Surface | État candidate |
|------------------------------------------------------|----------------|
| workspace version | `0.2.9-pre.5` |
| account selectors `account[]` / `owner[]` | SOURCE+TEST |
| account predicates memcmp bytes/base58/base64 | SOURCE+TEST |
| account predicates datasize/token-state/lamports | SOURCE+TEST |
| `nonempty_txn_signature?` | SOURCE+TEST |
| `cuckoo_accounts_filter?` + SipHash | SOURCE+TEST |
| Slots `filter_by_commitment?` / `interslot_updates?` | SOURCE+TEST |
| Account update DTO/decode | SOURCE+TEST |
| Slot update DTO/decode + 7 statuts + `dead_error?` | SOURCE+TEST |
| fixed-width pubkey/signature malformed | SOURCE+TEST |
| payload/timestamp/update-filter/dead-error bounds | SOURCE+TEST |
| Debug sans sélecteurs/payloads arbitraires | SOURCE+TEST |
| Transactions / transaction_status | OUT pre.005 |
| Blocks / block_meta / entry | OUT pre.005 |
| stream bidi / lifecycle | OUT pre.005 |
| PublicNode / Config V3 | OUT pre.005 |
| audit Rust workspace local | PASS / clean |
| fmt/check/Clippy/tests | opérateur TODO |
Les conversions request protobuf et les décodeurs Account/Slot sont test-only jusqu'à `pre.008`, faute de consommateur runtime avant l'ouverture du stream. Le contrat public reste entièrement KSP-owned.
**Verdict `pre.005` : candidate source prête ; fermeture après gate Cargo opérateur.**