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,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());
}