v0.3.13-pre.002

This commit is contained in:
2026-09-10 09:50:42 +02:00
parent ee0359efd5
commit 793178b345
15 changed files with 976 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
// version: 8
// version: 9
const MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES: usize = 128;
const MAX_GRPC_BLOCK_VECTOR_COUNT: usize = 65_536;
@@ -2529,13 +2529,34 @@ impl YellowstoneSubscribeEntryFilter {
}
}
/// Opaque deterministic identity material for one complete Yellowstone Subscribe request.
///
/// The encoded bytes remain private. `Hash` feeds those canonical bytes to a caller-provided hasher while `Debug` exposes only their length.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeRequestIdentity {
bytes: std::vec::Vec<u8>,
}
impl std::hash::Hash for crate::YellowstoneSubscribeRequestIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(&(self.bytes.len() as u64).to_be_bytes());
state.write(self.bytes.as_slice());
return;
}
}
impl std::fmt::Debug for crate::YellowstoneSubscribeRequestIdentity {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("YellowstoneSubscribeRequestIdentity").field("byte_len", &self.bytes.len()).finish();
}
}
/// Provider-neutral standard Yellowstone subscribe request owned by KSP.
///
/// The seven upstream maps are represented independently and retain named empty entries. An entirely empty map is the logical KSP representation of no active
/// filter in that family; protobuf map encoding does not distinguish an omitted map from an empty map. Filter-group names are globally unique across all seven
/// maps so the names echoed by `SubscribeUpdate.filters` remain unambiguous. `Debug` exposes only counts and common scalar options, never filter names or
/// future
/// filter payloads.
/// future filter payloads.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeRequest {
accounts: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeAccountFilter>,
@@ -2551,7 +2572,7 @@ pub struct YellowstoneSubscribeRequest {
from_slot: std::option::Option<u64>,
}
impl YellowstoneSubscribeRequest {
impl crate::YellowstoneSubscribeRequest {
/// Creates an empty subscribe request. Empty requests are valid because later bidi lifecycle code uses request mutations to clear filters or carry ping
/// state.
#[must_use]
@@ -2762,6 +2783,40 @@ impl YellowstoneSubscribeRequest {
return self.from_slot;
}
/// Builds one opaque deterministic identity for the complete logical Subscribe request.
///
/// The identity preserves filter-family separation, globally sorted filter names, exact filter wire payloads and common request options. Its Debug surface
/// exposes only the encoded byte length. Callers may hash the opaque value but cannot recover the underlying identity bytes through this API.
pub fn identity(&self) -> ksp_core_lib::Result<crate::YellowstoneSubscribeRequestIdentity> {
let validation = self.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let mut bytes = std::vec::Vec::new();
append_subscribe_identity_map(&mut bytes, b"accounts", &self.accounts, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"blocks", &self.blocks, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"blocks_meta", &self.blocks_meta, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"entry", &self.entry, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"slots", &self.slots, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"transactions", &self.transactions, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"transactions_status", &self.transactions_status, |filter| return filter.to_wire());
let mut common = match self.to_wire() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
common.accounts.clear();
common.blocks.clear();
common.blocks_meta.clear();
common.entry.clear();
common.slots.clear();
common.transactions.clear();
common.transactions_status.clear();
let common = yellowstone_grpc_proto::prost::Message::encode_to_vec(&common);
append_subscribe_identity_component(&mut bytes, b"common");
append_subscribe_identity_component(&mut bytes, common.as_slice());
return std::result::Result::Ok(crate::YellowstoneSubscribeRequestIdentity { bytes });
}
/// Validates all deterministic common subscribe-request bounds before any network I/O.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
if self.total_filter_count() > MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT {
@@ -3710,13 +3765,38 @@ fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>)
});
}
impl std::default::Default for YellowstoneSubscribeRequest {
fn append_subscribe_identity_component(output: &mut std::vec::Vec<u8>, value: &[u8]) {
output.extend_from_slice(&(value.len() as u64).to_be_bytes());
output.extend_from_slice(value);
return;
}
fn append_subscribe_identity_map<V, W, F>(
output: &mut std::vec::Vec<u8>,
family: &[u8],
values: &std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, V>,
mut to_wire: F,
) where
W: yellowstone_grpc_proto::prost::Message,
F: FnMut(&V) -> W,
{
append_subscribe_identity_component(output, family);
output.extend_from_slice(&(values.len() as u64).to_be_bytes());
for (name, filter) in values {
append_subscribe_identity_component(output, name.as_str().as_bytes());
let wire = yellowstone_grpc_proto::prost::Message::encode_to_vec(&to_wire(filter));
append_subscribe_identity_component(output, wire.as_slice());
}
return;
}
impl std::default::Default for crate::YellowstoneSubscribeRequest {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for YellowstoneSubscribeRequest {
impl std::fmt::Debug for crate::YellowstoneSubscribeRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribeRequest")

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 46
// version: 47
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -215,6 +215,8 @@ pub use self::grpc_subscribe::YellowstoneSubscribePingUpdate;
pub use self::grpc_subscribe::YellowstoneSubscribePongUpdate;
/// Provider-neutral standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
/// Opaque deterministic identity for one complete Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequestIdentity;
/// Complete slot-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
/// Complete transaction-family filter shared by transactions and transaction-status maps.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 51
// version: 52
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -1032,3 +1032,15 @@ fn public_v0_3_10_pre_004_observed_get_block_surface_is_available_from_crate_roo
let _ = method;
return;
}
#[test]
fn public_v0_3_13_pre_002_yellowstone_subscribe_identity_is_opaque_and_available_from_crate_root() {
let request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let identity = request.identity().expect("empty validated request identity must build");
let _identity_type = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeRequestIdentity>();
let debug = std::format!("{identity:?}");
assert!(debug.contains("YellowstoneSubscribeRequestIdentity"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("bytes:"));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 43
// version: 44
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -1339,3 +1339,18 @@ fn release_v0_2_9_pre_010_adds_bounded_reconnect_replay_and_conservative_continu
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
}
#[test]
fn release_v0_3_13_pre_002_yellowstone_subscribe_identity_remains_opaque_and_dependency_neutral() {
let source = include_str!("../src/grpc_subscribe.rs");
let root = include_str!("../src/lib.rs");
let identity_start = source.find("pub struct YellowstoneSubscribeRequestIdentity").expect("identity struct must remain present");
let request_start = source.find("/// Provider-neutral standard Yellowstone subscribe request").expect("request contract marker must remain present");
let identity_surface = &source[identity_start..request_start];
assert!(source.contains("pub fn identity(&self)"));
assert!(root.contains("pub use self::grpc_subscribe::YellowstoneSubscribeRequestIdentity;"));
assert!(!identity_surface.contains("pub fn bytes("));
assert!(!identity_surface.contains("pub fn as_bytes("));
assert!(!root.contains("pub use yellowstone_grpc_proto"));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
// version: 5
// version: 6
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
@@ -138,6 +138,37 @@ fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() {
assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err());
}
#[test]
fn yellowstone_subscribe_request_identity_is_order_stable_exact_and_debug_redacted() {
let mut first = crate::YellowstoneSubscribeRequest::new();
let mut second = crate::YellowstoneSubscribeRequest::new();
let mut changed = crate::YellowstoneSubscribeRequest::new();
let mut filter_a = crate::YellowstoneSubscribeTransactionFilter::new();
filter_a.set_failed(std::option::Option::Some(false));
let mut filter_b = crate::YellowstoneSubscribeTransactionFilter::new();
filter_b.set_vote(std::option::Option::Some(false));
assert!(first.insert_transaction_filter(filter_name("identity-filter-beta-canary"), filter_b.clone()).is_ok());
assert!(first.insert_transaction_filter(filter_name("identity-filter-alpha-canary"), filter_a.clone()).is_ok());
assert!(second.insert_transaction_filter(filter_name("identity-filter-alpha-canary"), filter_a.clone()).is_ok());
assert!(second.insert_transaction_filter(filter_name("identity-filter-beta-canary"), filter_b.clone()).is_ok());
filter_a.set_failed(std::option::Option::Some(true));
assert!(changed.insert_transaction_filter(filter_name("identity-filter-alpha-canary"), filter_a).is_ok());
assert!(changed.insert_transaction_filter(filter_name("identity-filter-beta-canary"), filter_b).is_ok());
first.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Confirmed));
second.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Confirmed));
changed.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Confirmed));
let first_identity = first.identity().expect("first request identity must build");
let second_identity = second.identity().expect("second request identity must build");
let changed_identity = changed.identity().expect("changed request identity must build");
assert_eq!(first_identity, second_identity);
assert_ne!(first_identity, changed_identity);
let debug = std::format!("{first_identity:?}");
assert!(debug.contains("byte_len"));
assert!(!debug.contains("identity-filter-alpha-canary"));
assert!(!debug.contains("identity-filter-beta-canary"));
return;
}
#[test]
fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() {
let mut request = crate::YellowstoneSubscribeRequest::new();