Files
khadhroony-solana-project/crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs

472 lines
22 KiB
Rust

// file: crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
// version: 3
//! Deterministic local Yellowstone gRPC parity canary for source-neutral RAW transaction v1 projection.
struct FixtureStream<T> {
receiver: tokio::sync::mpsc::Receiver<T>,
}
impl<T> FixtureStream<T> {
fn new(receiver: tokio::sync::mpsc::Receiver<T>) -> Self {
return Self { receiver };
}
}
impl<T> futures_util::Stream for FixtureStream<T> {
type Item = T;
fn poll_next(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<std::option::Option<Self::Item>> {
return self.get_mut().receiver.poll_recv(context);
}
}
#[derive(Clone)]
struct FixtureGeyser;
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
#[tonic::async_trait]
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
type SubscribeStream = FixtureStream<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>>;
async fn subscribe(
&self,
request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
let mut inbound = request.into_inner();
let (sender, receiver) = tokio::sync::mpsc::channel(4);
tokio::spawn(async move {
let initial = inbound.message().await;
if !matches!(initial, std::result::Result::Ok(std::option::Option::Some(_))) {
return;
}
if sender.send(std::result::Result::Ok(transaction_update())).await.is_err() {
return;
}
let _ = sender.send(std::result::Result::Ok(block_update())).await;
});
return std::result::Result::Ok(tonic::Response::new(FixtureStream::new(receiver)));
}
type SubscribeDeshredStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>>;
async fn subscribe_deshred(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside the pre.006 fixture"));
}
type SubscribeGossipStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateGossip, tonic::Status>>;
async fn subscribe_gossip(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeGossipRequest>,
) -> std::result::Result<tonic::Response<Self::SubscribeGossipStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("gossip is outside the pre.006 fixture"));
}
async fn subscribe_replay_info(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside the pre.006 fixture"));
}
async fn ping(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
async fn get_latest_blockhash(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
async fn get_block_height(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
async fn get_slot(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
async fn is_blockhash_valid(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
async fn get_version(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
}
}
struct FixtureServer {
endpoint_url: std::string::String,
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
task: tokio::task::JoinHandle<()>,
}
impl FixtureServer {
async fn start() -> std::result::Result<Self, &'static str> {
let bind_address = match "127.0.0.1:0".parse::<std::net::SocketAddr>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err("fixture bind address must parse"),
};
let incoming = match tonic::transport::server::TcpIncoming::bind(bind_address) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must bind"),
};
let local_address = match incoming.local_addr() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must expose local address"),
};
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
let task = tokio::spawn(async move {
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser);
let result = tonic::transport::Server::builder()
.serve_with_incoming_shutdown(service, incoming, async move {
let _ = shutdown_receiver.await;
})
.await;
assert!(result.is_ok());
});
return std::result::Result::Ok(Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task });
}
async fn stop(mut self) {
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
let result = self.task.await;
assert!(result.is_ok());
}
}
fn transaction_info() -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
signature: vec![9_u8; 64],
is_vote: false,
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
signatures: vec![vec![9_u8; 64]],
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
}),
account_keys: vec![vec![1_u8; 32], vec![2_u8; 32]],
recent_blockhash: vec![3_u8; 32],
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction {
program_id_index: 1,
accounts: vec![0_u8],
data: vec![4_u8, 5, 6],
}],
versioned: true,
address_table_lookups: vec![],
config: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionConfig {
priority_fee: std::option::Option::Some(7),
compute_unit_limit: std::option::Option::Some(8),
loaded_accounts_data_size_limit: std::option::Option::Some(9),
heap_size: std::option::Option::Some(32_768),
}),
}),
}),
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
fee: 5_000,
pre_balances: vec![100, 200],
post_balances: vec![90, 210],
compute_units_consumed: std::option::Option::Some(123),
cost_units: std::option::Option::Some(456),
..std::default::Default::default()
}),
index: 3,
};
}
fn transaction_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["transactions-pre-006".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(
yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction { transaction: std::option::Option::Some(transaction_info()), slot: 42 },
)),
created_at: std::option::Option::None,
};
}
fn block_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["blocks-pre-006".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
slot: 42,
blockhash: ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string(),
rewards: std::option::Option::None,
block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_787_104_000 }),
block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 41 }),
transactions: vec![transaction_info()],
parent_slot: 41,
parent_blockhash: ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string(),
executed_transaction_count: 1,
updated_account_count: 0,
accounts: vec![],
entries_count: 0,
entries: vec![],
},
)),
created_at: std::option::Option::None,
};
}
fn fixture_settings(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
let endpoint_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(url) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
"pre-006-fixture",
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
endpoint_url,
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
));
}
fn project_wire(
transaction: &ksp_onchain_transport_lib::YellowstoneStoredTransaction,
) -> ksp_core_lib::Result<ksp_raw_transaction_lib::RawSolanaTransactionWire> {
let message = transaction.message();
let header = message.header();
let required = match u8::try_from(header.num_required_signatures()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_required_signatures")),
};
let readonly_signed = match u8::try_from(header.num_readonly_signed_accounts()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_signed_accounts")),
};
let readonly_unsigned = match u8::try_from(header.num_readonly_unsigned_accounts()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_unsigned_accounts")),
};
let version = if message.config().is_some() {
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1
} else if message.versioned() {
ksp_raw_transaction_lib::RawSolanaMessageVersion::V0
} else {
ksp_raw_transaction_lib::RawSolanaMessageVersion::Legacy
};
let signatures = transaction.signatures().iter().map(|value| return *value.as_bytes()).collect();
let account_keys = message.account_keys().iter().map(solana_pubkey_bytes).collect();
let mut instructions = std::vec::Vec::with_capacity(message.instructions().len());
for instruction in message.instructions() {
let program_id_index = match u8::try_from(instruction.program_id_index()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("program_id_index")),
};
instructions.push(ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(
program_id_index,
instruction.accounts().to_vec(),
instruction.data().to_vec(),
));
}
let address_table_lookups = message
.address_table_lookups()
.iter()
.map(|lookup| {
return ksp_raw_transaction_lib::RawSolanaAddressTableLookup::new(
lookup.account_key().to_bytes(),
lookup.writable_indexes().to_vec(),
lookup.readonly_indexes().to_vec(),
);
})
.collect();
let config = message.config().map(|value| {
return ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
value.priority_fee(),
value.compute_unit_limit(),
value.loaded_accounts_data_size_limit(),
value.heap_size(),
);
});
let raw_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
version,
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(required, readonly_signed, readonly_unsigned),
account_keys,
*message.recent_blockhash().as_bytes(),
instructions,
address_table_lookups,
config,
);
return std::result::Result::Ok(ksp_raw_transaction_lib::RawSolanaTransactionWire::new(signatures, raw_message));
}
fn solana_pubkey_bytes(value: &ksp_core_lib::Pubkey) -> [u8; 32] {
return value.to_bytes();
}
fn projection_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "yellowstone_raw_projection"), "Yellowstone fixture field does not fit common wire")
.with_context("field", field);
}
fn error_code_text(error: &ksp_core_lib::Error) -> std::string::String {
return format!("{}.{}", error.code().domain(), error.code().code());
}
fn expected_v1_wire() -> ksp_raw_transaction_lib::RawSolanaTransactionWire {
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 1),
vec![[1_u8; 32], [2_u8; 32]],
[3_u8; 32],
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(1, vec![0_u8], vec![4_u8, 5, 6])],
vec![],
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
std::option::Option::Some(7),
std::option::Option::Some(8),
std::option::Option::Some(9),
std::option::Option::Some(32_768),
)),
);
return ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[9_u8; 64]], message);
}
fn fail_test(message: std::string::String) {
assert!(message.is_empty(), "{message}");
}
#[tokio::test]
async fn pre_006_yellowstone_transaction_and_block_preserve_exact_v1_wire_and_block_time() {
let server = match FixtureServer::start().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
assert!(error.is_empty(), "fixture server start failed: {error}");
return;
},
};
let settings = match fixture_settings(server.endpoint_url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("fixture settings failed: {}", error_code_text(&error)));
return;
},
};
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&settings).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("fixture channel connect failed: {}", error_code_text(&error)));
return;
},
};
let request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let mut session = match channel.open_standard_subscribe(request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("fixture subscribe failed: {}", error_code_text(&error)));
return;
},
};
let expected = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&expected_v1_wire()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("expected V1 wire serialization failed: {}", error_code_text(&error)));
return;
},
};
let transaction = match session.next_update().await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
fail_test("transaction update must be present".to_owned());
return;
},
std::result::Result::Err(error) => {
fail_test(format!("transaction update decode failed: {}", error_code_text(&error)));
return;
},
};
let transaction = match transaction {
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Transaction(value) => value,
_ => {
fail_test("first Yellowstone fixture update must be a transaction".to_owned());
return;
},
};
assert_eq!(transaction.slot(), 42);
assert_eq!(transaction.transaction().index(), 3);
assert_eq!(transaction.transaction().meta().fee(), 5_000);
let transaction_wire = match project_wire(transaction.transaction().transaction()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("transaction projection failed: {}", error_code_text(&error)));
return;
},
};
let transaction_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&transaction_wire) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("transaction V1 wire serialization failed: {}", error_code_text(&error)));
return;
},
};
assert_eq!(transaction_bytes, expected);
let block = match session.next_update().await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
fail_test("block update must be present".to_owned());
return;
},
std::result::Result::Err(error) => {
fail_test(format!("block update decode failed: {}", error_code_text(&error)));
return;
},
};
let block = match block {
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) => value,
_ => {
fail_test("second Yellowstone fixture update must be a block".to_owned());
return;
},
};
assert_eq!(block.slot(), 42);
assert_eq!(block.block_time(), std::option::Option::Some(1_787_104_000));
assert_eq!(block.transactions().len(), 1);
assert_eq!(block.transactions()[0].index(), 3);
let block_wire = match project_wire(block.transactions()[0].transaction()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("block transaction projection failed: {}", error_code_text(&error)));
return;
},
};
let block_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&block_wire) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
fail_test(format!("block V1 wire serialization failed: {}", error_code_text(&error)));
return;
},
};
assert_eq!(block_bytes, expected);
assert_eq!(block_bytes, transaction_bytes);
assert!(transaction.transaction().meta().error().is_none());
assert_eq!(block.transactions()[0].meta().fee(), transaction.transaction().meta().fee());
assert!(session.close().await.is_ok());
server.stop().await;
}