// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs // version: 2 fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName { return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate"); } #[test] fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() { assert!(crate::YellowstoneSubscribeFilterName::new("").is_err()); assert!(crate::YellowstoneSubscribeFilterName::new(" leading").is_err()); assert!(crate::YellowstoneSubscribeFilterName::new("trailing ").is_err()); assert!(crate::YellowstoneSubscribeFilterName::new("line\nbreak").is_err()); assert!(crate::YellowstoneSubscribeFilterName::new("x".repeat(129)).is_err()); let name = filter_name("account-primary"); assert_eq!(name.as_str(), "account-primary"); assert!(!format!("{name:?}").contains("account-primary")); let mut request = crate::YellowstoneSubscribeRequest::new(); assert!(request.insert_account_filter(name.clone(), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); let duplicate = request.insert_slot_filter(name, crate::YellowstoneSubscribeSlotFilter::new()); assert!(duplicate.is_err()); } #[test] fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() { let empty = crate::YellowstoneSubscribeRequest::new().to_wire().expect("empty request must encode"); assert!(empty.accounts.is_empty()); assert!(empty.slots.is_empty()); assert!(empty.transactions.is_empty()); assert!(empty.transactions_status.is_empty()); assert!(empty.blocks.is_empty()); assert!(empty.blocks_meta.is_empty()); assert!(empty.entry.is_empty()); assert_eq!(empty.commitment, std::option::Option::None); assert!(empty.accounts_data_slice.is_empty()); assert_eq!(empty.ping, std::option::Option::None); assert_eq!(empty.from_slot, std::option::Option::None); let mut request = crate::YellowstoneSubscribeRequest::new(); assert!(request.insert_account_filter(filter_name("accounts"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); assert!(request.insert_slot_filter(filter_name("slots"), crate::YellowstoneSubscribeSlotFilter::new()).is_ok()); assert!(request.insert_transaction_filter(filter_name("transactions"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok()); assert!(request.insert_transaction_status_filter(filter_name("transaction-status"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok()); assert!(request.insert_block_filter(filter_name("blocks"), crate::YellowstoneSubscribeBlockFilter::new()).is_ok()); assert!(request.insert_blocks_meta_filter(filter_name("blocks-meta"), crate::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok()); assert!(request.insert_entry_filter(filter_name("entry"), crate::YellowstoneSubscribeEntryFilter::new()).is_ok()); let wire = request.to_wire().expect("named empty request must encode"); assert_eq!(wire.accounts.len(), 1); assert_eq!(wire.slots.len(), 1); assert_eq!(wire.transactions.len(), 1); assert_eq!(wire.transactions_status.len(), 1); assert_eq!(wire.blocks.len(), 1); assert_eq!(wire.blocks_meta.len(), 1); assert_eq!(wire.entry.len(), 1); assert_eq!(wire.accounts.get("accounts"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default())); assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default())); assert_eq!( wire.transactions.get("transactions"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()) ); assert_eq!( wire.transactions_status.get("transaction-status"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()) ); assert_eq!(wire.blocks.get("blocks"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default())); assert!(wire.blocks_meta.contains_key("blocks-meta")); assert!(wire.entry.contains_key("entry")); } #[test] fn yellowstone_subscribe_common_fields_preserve_optional_and_ordered_wire_semantics() { let mut request = crate::YellowstoneSubscribeRequest::new(); request.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Finalized)); let first = crate::YellowstoneAccountsDataSlice::new(8, 16).expect("first slice must validate"); let second = crate::YellowstoneAccountsDataSlice::new(64, 0).expect("zero-length slice remains representable"); assert!(request.push_accounts_data_slice(first).is_ok()); assert!(request.push_accounts_data_slice(second).is_ok()); request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7))); request.set_from_slot(std::option::Option::Some(42)); assert_eq!(request.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized)); assert_eq!(request.accounts_data_slices(), &[first, second]); assert_eq!(request.ping(), std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7))); assert_eq!(request.from_slot(), std::option::Option::Some(42)); let wire = request.to_wire().expect("common fields must encode"); assert_eq!(wire.commitment, std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32)); assert_eq!(wire.accounts_data_slice.len(), 2); assert_eq!(wire.accounts_data_slice[0].offset, 8); assert_eq!(wire.accounts_data_slice[0].length, 16); assert_eq!(wire.accounts_data_slice[1].offset, 64); assert_eq!(wire.accounts_data_slice[1].length, 0); assert_eq!(wire.ping.map(|ping| return ping.id), std::option::Option::Some(-7)); assert_eq!(wire.from_slot, std::option::Option::Some(42)); } #[test] fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() { assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024).is_ok()); assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024 + 1).is_err()); assert!(crate::YellowstoneAccountsDataSlice::new(u64::MAX, 1).is_err()); let mut slices = crate::YellowstoneSubscribeRequest::new(); for index in 0..128_u64 { let slice = crate::YellowstoneAccountsDataSlice::new(index, 1).expect("bounded fixture slice must validate"); assert!(slices.push_accounts_data_slice(slice).is_ok()); } let excess = crate::YellowstoneAccountsDataSlice::new(129, 1).expect("excess fixture slice itself must validate"); assert!(slices.push_accounts_data_slice(excess).is_err()); let mut filters = crate::YellowstoneSubscribeRequest::new(); for index in 0..1_024_u32 { let name = crate::YellowstoneSubscribeFilterName::new(format!("f{index}")).expect("bounded filter name must validate"); assert!(filters.insert_account_filter(name, crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); } let excess_name = crate::YellowstoneSubscribeFilterName::new("excess").expect("excess filter name must validate independently"); assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err()); } #[test] fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() { let mut request = crate::YellowstoneSubscribeRequest::new(); assert!(request.insert_account_filter(filter_name("sensitive-label"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok()); request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(9))); request.set_from_slot(std::option::Option::Some(77)); let debug = format!("{request:?}"); assert!(!debug.contains("sensitive-label")); assert!(debug.contains("account_filter_count")); assert!(debug.contains("from_slot")); } #[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()); }