v0.3.10-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
||||
# version: 6
|
||||
# version: 7
|
||||
|
||||
[package]
|
||||
name = "ksp-job-backfill-lib"
|
||||
@@ -22,6 +22,8 @@ tokio = { workspace = true, features = ["macros", "sync"] }
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
||||
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
||||
|
||||
#[test]
|
||||
fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_boundaries() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let production_manifest = match manifest.split_once("[dev-dependencies]") {
|
||||
std::option::Option::Some((production, _)) => production,
|
||||
std::option::Option::None => manifest,
|
||||
};
|
||||
for required in [
|
||||
"futures-util = { workspace = true, features = [\"std\"] }",
|
||||
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
||||
@@ -18,7 +22,7 @@ fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_bound
|
||||
"sha2.workspace = true",
|
||||
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
||||
] {
|
||||
assert!(manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||
assert!(production_manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"ksp-config-lib",
|
||||
@@ -33,7 +37,7 @@ fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_bound
|
||||
"serde = {",
|
||||
"tonic",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||
assert!(!production_manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||
}
|
||||
let root = include_str!("../src/lib.rs");
|
||||
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
||||
@@ -59,6 +63,30 @@ fn pre_005_websocket_parity_dependency_stays_test_only() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_yellowstone_parity_dependencies_stay_test_only() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||
_ => {
|
||||
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(!dependencies.contains("tonic"), "Yellowstone parity server must not become a production Backfill dependency");
|
||||
assert!(!dependencies.contains("yellowstone-grpc-proto"), "Yellowstone protobuf must not become a production Backfill dependency");
|
||||
assert!(
|
||||
dev_dependencies.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"),
|
||||
"Yellowstone parity server must remain an explicit test-only dependency",
|
||||
);
|
||||
assert!(
|
||||
dev_dependencies.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"),
|
||||
"Yellowstone protobuf must remain an explicit test-only dependency",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
||||
let neutral_sources = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
||||
|
||||
@@ -275,7 +275,7 @@ fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
|
||||
"tokio",
|
||||
])
|
||||
);
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite"]));
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite", "tonic", "yellowstone-grpc-proto"]));
|
||||
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
||||
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
||||
|
||||
465
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
465
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||
// version: 1
|
||||
|
||||
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-RAW-TRANSACTION-PRE-006-PROJECTION", "Yellowstone fixture field does not fit common wire")
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -16,6 +16,7 @@ mod acquisition;
|
||||
mod canonical;
|
||||
mod error;
|
||||
mod signature;
|
||||
mod wire;
|
||||
|
||||
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||
pub use self::acquisition::RawTransactionAcquisition;
|
||||
@@ -47,6 +48,24 @@ pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
pub use self::signature::extract_raw_transaction_signature_from_binary_base64;
|
||||
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||
pub use self::signature::parse_raw_transaction_signature;
|
||||
/// One source-neutral v0 address-table lookup.
|
||||
pub use self::wire::RawSolanaAddressTableLookup;
|
||||
/// One source-neutral compiled Solana instruction.
|
||||
pub use self::wire::RawSolanaCompiledInstruction;
|
||||
/// Source-neutral Solana message header.
|
||||
pub use self::wire::RawSolanaMessageHeader;
|
||||
/// Source-neutral Solana transaction message version.
|
||||
pub use self::wire::RawSolanaMessageVersion;
|
||||
/// Optional source-neutral Transaction V1 inline configuration.
|
||||
pub use self::wire::RawSolanaTransactionConfig;
|
||||
/// Complete source-neutral Solana transaction message.
|
||||
pub use self::wire::RawSolanaTransactionMessage;
|
||||
/// Complete source-neutral Solana transaction wire material.
|
||||
pub use self::wire::RawSolanaTransactionWire;
|
||||
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||
pub use self::wire::serialize_solana_transaction_wire;
|
||||
/// Serializes one source-neutral Solana transaction to canonical padded standard Base64.
|
||||
pub use self::wire::serialize_solana_transaction_wire_base64;
|
||||
|
||||
/// Creates a safe canonicalization error without copying source payload material.
|
||||
pub(crate) use self::error::canonicalization_error;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
@@ -68,7 +68,14 @@ pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_
|
||||
if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let prefix = decode_signature_count(decoded.as_slice());
|
||||
if decoded.first() == std::option::Option::Some(&0x81) {
|
||||
return extract_v1_signature(decoded.as_slice());
|
||||
}
|
||||
return extract_legacy_or_v0_signature(decoded.as_slice());
|
||||
}
|
||||
|
||||
fn extract_legacy_or_v0_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||
let prefix = decode_signature_count(decoded);
|
||||
let (signature_count, prefix_len) = match prefix {
|
||||
std::result::Result::Ok(prefix) => prefix,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -87,11 +94,151 @@ pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_
|
||||
if message_offset >= decoded.len() {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let first_end = match prefix_len.checked_add(64) {
|
||||
return copy_signature(decoded, prefix_len);
|
||||
}
|
||||
|
||||
fn extract_v1_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||
if decoded.len() > 4_096 || decoded.len() < 42 {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let required_signatures = match decoded.get(1) {
|
||||
std::option::Option::Some(value) => usize::from(*value),
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let readonly_signed = match decoded.get(2) {
|
||||
std::option::Option::Some(value) => usize::from(*value),
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let readonly_unsigned = match decoded.get(3) {
|
||||
std::option::Option::Some(value) => usize::from(*value),
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
if required_signatures == 0 || required_signatures > 12 || readonly_signed >= required_signatures {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let mask_bytes = match decoded.get(4..8) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let mask = u32::from_le_bytes([mask_bytes[0], mask_bytes[1], mask_bytes[2], mask_bytes[3]]);
|
||||
if mask & !0x1f != 0 || matches!(mask & 0b11, 1 | 2) {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let instruction_count = match decoded.get(40) {
|
||||
std::option::Option::Some(value) => usize::from(*value),
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let address_count = match decoded.get(41) {
|
||||
std::option::Option::Some(value) => usize::from(*value),
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
if instruction_count > 64 || address_count > 64 || address_count < required_signatures + readonly_unsigned {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let addresses_len = match address_count.checked_mul(32) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let mut offset = match 42_usize.checked_add(addresses_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let config_len = config_value_length(mask);
|
||||
offset = match offset.checked_add(config_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let headers_len = match instruction_count.checked_mul(4) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let headers_end = match offset.checked_add(headers_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
if headers_end > decoded.len() {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
let mut payload_len = 0_usize;
|
||||
for index in 0..instruction_count {
|
||||
let header_offset = offset + (index * 4);
|
||||
let program_index = usize::from(decoded[header_offset]);
|
||||
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||
let data_len = u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]);
|
||||
if program_index >= address_count {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
payload_len = match payload_len.checked_add(account_index_count).and_then(|value| return value.checked_add(usize::from(data_len))) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
}
|
||||
let payload_end = match headers_end.checked_add(payload_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let payload = match decoded.get(headers_end..payload_end) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let mut payload_offset = 0_usize;
|
||||
for index in 0..instruction_count {
|
||||
let header_offset = offset + (index * 4);
|
||||
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||
let data_len = usize::from(u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]));
|
||||
let account_end = match payload_offset.checked_add(account_index_count) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let account_indexes = match payload.get(payload_offset..account_end) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
if account_indexes.iter().any(|value| return usize::from(*value) >= address_count) {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
payload_offset = match account_end.checked_add(data_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
}
|
||||
let signatures_len = match required_signatures.checked_mul(64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let expected_end = match payload_end.checked_add(signatures_len) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
if expected_end != decoded.len() {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
return copy_signature(decoded, payload_end);
|
||||
}
|
||||
|
||||
fn config_value_length(mask: u32) -> usize {
|
||||
let mut length = 0_usize;
|
||||
if mask & 0b11 == 0b11 {
|
||||
length += 8;
|
||||
}
|
||||
if mask & (1_u32 << 2) != 0 {
|
||||
length += 4;
|
||||
}
|
||||
if mask & (1_u32 << 3) != 0 {
|
||||
length += 4;
|
||||
}
|
||||
if mask & (1_u32 << 4) != 0 {
|
||||
length += 4;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
fn copy_signature(decoded: &[u8], offset: usize) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||
let first_end = match offset.checked_add(64) {
|
||||
std::option::Option::Some(end) => end,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let first = match decoded.get(prefix_len..first_end) {
|
||||
let first = match decoded.get(offset..first_end) {
|
||||
std::option::Option::Some(first) => first,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
|
||||
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/wire.rs
|
||||
// version: 2
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
const MAX_LEGACY_SHORT_VECTOR_VALUE: usize = u16::MAX as usize;
|
||||
const MAX_V1_ADDRESS_COUNT: usize = 64;
|
||||
const MAX_V1_INSTRUCTION_COUNT: usize = 64;
|
||||
const MAX_V1_SIGNATURE_COUNT: usize = 12;
|
||||
const MAX_V1_TRANSACTION_BYTES: usize = 4_096;
|
||||
|
||||
/// Source-neutral Solana transaction message version used by the exact wire serializer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RawSolanaMessageVersion {
|
||||
/// Legacy Solana message without a version prefix.
|
||||
Legacy,
|
||||
/// Versioned transaction message v0, prefixed with `0x80`.
|
||||
V0,
|
||||
/// Transaction V1 / SIMD-0385 message, prefixed with `0x81`.
|
||||
V1,
|
||||
}
|
||||
|
||||
/// Exact three-byte Solana message header.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct RawSolanaMessageHeader {
|
||||
num_required_signatures: u8,
|
||||
num_readonly_signed_accounts: u8,
|
||||
num_readonly_unsigned_accounts: u8,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaMessageHeader {
|
||||
/// Creates an exact Solana message header.
|
||||
#[must_use]
|
||||
pub const fn new(num_required_signatures: u8, num_readonly_signed_accounts: u8, num_readonly_unsigned_accounts: u8) -> Self {
|
||||
return Self { num_required_signatures, num_readonly_signed_accounts, num_readonly_unsigned_accounts };
|
||||
}
|
||||
|
||||
/// Returns the required signature count.
|
||||
#[must_use]
|
||||
pub const fn num_required_signatures(self) -> u8 {
|
||||
return self.num_required_signatures;
|
||||
}
|
||||
|
||||
/// Returns the readonly signed-account count.
|
||||
#[must_use]
|
||||
pub const fn num_readonly_signed_accounts(self) -> u8 {
|
||||
return self.num_readonly_signed_accounts;
|
||||
}
|
||||
|
||||
/// Returns the readonly unsigned-account count.
|
||||
#[must_use]
|
||||
pub const fn num_readonly_unsigned_accounts(self) -> u8 {
|
||||
return self.num_readonly_unsigned_accounts;
|
||||
}
|
||||
}
|
||||
|
||||
/// One source-neutral compiled Solana instruction.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct RawSolanaCompiledInstruction {
|
||||
program_id_index: u8,
|
||||
accounts: std::vec::Vec<u8>,
|
||||
data: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaCompiledInstruction {
|
||||
/// Creates one compiled instruction while preserving exact ordered account indexes and data bytes.
|
||||
#[must_use]
|
||||
pub fn new(program_id_index: u8, accounts: std::vec::Vec<u8>, data: std::vec::Vec<u8>) -> Self {
|
||||
return Self { program_id_index, accounts, data };
|
||||
}
|
||||
|
||||
/// Returns the program account index.
|
||||
#[must_use]
|
||||
pub const fn program_id_index(&self) -> u8 {
|
||||
return self.program_id_index;
|
||||
}
|
||||
|
||||
/// Returns ordered account indexes.
|
||||
#[must_use]
|
||||
pub fn accounts(&self) -> &[u8] {
|
||||
return self.accounts.as_slice();
|
||||
}
|
||||
|
||||
/// Returns exact instruction data bytes.
|
||||
#[must_use]
|
||||
pub fn data(&self) -> &[u8] {
|
||||
return self.data.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawSolanaCompiledInstruction {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("RawSolanaCompiledInstruction")
|
||||
.field("program_id_index", &self.program_id_index)
|
||||
.field("account_index_count", &self.accounts.len())
|
||||
.field("data_length", &self.data.len())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// One source-neutral v0 address-table lookup.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct RawSolanaAddressTableLookup {
|
||||
account_key: [u8; 32],
|
||||
writable_indexes: std::vec::Vec<u8>,
|
||||
readonly_indexes: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaAddressTableLookup {
|
||||
/// Creates one exact v0 address-table lookup.
|
||||
#[must_use]
|
||||
pub fn new(account_key: [u8; 32], writable_indexes: std::vec::Vec<u8>, readonly_indexes: std::vec::Vec<u8>) -> Self {
|
||||
return Self { account_key, writable_indexes, readonly_indexes };
|
||||
}
|
||||
|
||||
/// Returns the lookup-table account key bytes.
|
||||
#[must_use]
|
||||
pub const fn account_key(&self) -> &[u8; 32] {
|
||||
return &self.account_key;
|
||||
}
|
||||
|
||||
/// Returns ordered writable lookup indexes.
|
||||
#[must_use]
|
||||
pub fn writable_indexes(&self) -> &[u8] {
|
||||
return self.writable_indexes.as_slice();
|
||||
}
|
||||
|
||||
/// Returns ordered readonly lookup indexes.
|
||||
#[must_use]
|
||||
pub fn readonly_indexes(&self) -> &[u8] {
|
||||
return self.readonly_indexes.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawSolanaAddressTableLookup {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("RawSolanaAddressTableLookup")
|
||||
.field("writable_index_count", &self.writable_indexes.len())
|
||||
.field("readonly_index_count", &self.readonly_indexes.len())
|
||||
.finish_non_exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional inline budget configuration carried by Solana Transaction V1.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct RawSolanaTransactionConfig {
|
||||
priority_fee: std::option::Option<u64>,
|
||||
compute_unit_limit: std::option::Option<u32>,
|
||||
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||
heap_size: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaTransactionConfig {
|
||||
/// Creates an explicit Transaction V1 configuration, including the meaningful all-`None` case.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
priority_fee: std::option::Option<u64>,
|
||||
compute_unit_limit: std::option::Option<u32>,
|
||||
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||
heap_size: std::option::Option<u32>,
|
||||
) -> Self {
|
||||
return Self { priority_fee, compute_unit_limit, loaded_accounts_data_size_limit, heap_size };
|
||||
}
|
||||
|
||||
/// Returns the optional priority fee.
|
||||
#[must_use]
|
||||
pub const fn priority_fee(self) -> std::option::Option<u64> {
|
||||
return self.priority_fee;
|
||||
}
|
||||
|
||||
/// Returns the optional compute-unit limit.
|
||||
#[must_use]
|
||||
pub const fn compute_unit_limit(self) -> std::option::Option<u32> {
|
||||
return self.compute_unit_limit;
|
||||
}
|
||||
|
||||
/// Returns the optional loaded-account-data-size limit.
|
||||
#[must_use]
|
||||
pub const fn loaded_accounts_data_size_limit(self) -> std::option::Option<u32> {
|
||||
return self.loaded_accounts_data_size_limit;
|
||||
}
|
||||
|
||||
/// Returns the optional heap-size override.
|
||||
#[must_use]
|
||||
pub const fn heap_size(self) -> std::option::Option<u32> {
|
||||
return self.heap_size;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete source-neutral Solana transaction message required for exact wire serialization.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct RawSolanaTransactionMessage {
|
||||
version: crate::RawSolanaMessageVersion,
|
||||
header: crate::RawSolanaMessageHeader,
|
||||
account_keys: std::vec::Vec<[u8; 32]>,
|
||||
recent_blockhash: [u8; 32],
|
||||
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaTransactionMessage {
|
||||
/// Creates one exact source-neutral message.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
version: crate::RawSolanaMessageVersion,
|
||||
header: crate::RawSolanaMessageHeader,
|
||||
account_keys: std::vec::Vec<[u8; 32]>,
|
||||
recent_blockhash: [u8; 32],
|
||||
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||
) -> Self {
|
||||
return Self { version, header, account_keys, recent_blockhash, instructions, address_table_lookups, config };
|
||||
}
|
||||
|
||||
/// Returns the message version.
|
||||
#[must_use]
|
||||
pub const fn version(&self) -> crate::RawSolanaMessageVersion {
|
||||
return self.version;
|
||||
}
|
||||
|
||||
/// Returns the message header.
|
||||
#[must_use]
|
||||
pub const fn header(&self) -> crate::RawSolanaMessageHeader {
|
||||
return self.header;
|
||||
}
|
||||
|
||||
/// Returns ordered static account key bytes.
|
||||
#[must_use]
|
||||
pub fn account_keys(&self) -> &[[u8; 32]] {
|
||||
return self.account_keys.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the recent blockhash bytes.
|
||||
#[must_use]
|
||||
pub const fn recent_blockhash(&self) -> &[u8; 32] {
|
||||
return &self.recent_blockhash;
|
||||
}
|
||||
|
||||
/// Returns ordered compiled instructions.
|
||||
#[must_use]
|
||||
pub fn instructions(&self) -> &[crate::RawSolanaCompiledInstruction] {
|
||||
return self.instructions.as_slice();
|
||||
}
|
||||
|
||||
/// Returns ordered v0 address-table lookups.
|
||||
#[must_use]
|
||||
pub fn address_table_lookups(&self) -> &[crate::RawSolanaAddressTableLookup] {
|
||||
return self.address_table_lookups.as_slice();
|
||||
}
|
||||
|
||||
/// Returns optional V1 inline configuration.
|
||||
#[must_use]
|
||||
pub const fn config(&self) -> std::option::Option<crate::RawSolanaTransactionConfig> {
|
||||
return self.config;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawSolanaTransactionMessage {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("RawSolanaTransactionMessage")
|
||||
.field("version", &self.version)
|
||||
.field("header", &self.header)
|
||||
.field("account_key_count", &self.account_keys.len())
|
||||
.field("instruction_count", &self.instructions.len())
|
||||
.field("address_table_lookup_count", &self.address_table_lookups.len())
|
||||
.field("has_config", &self.config.is_some())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete source-neutral Solana transaction wire material.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct RawSolanaTransactionWire {
|
||||
signatures: std::vec::Vec<[u8; 64]>,
|
||||
message: crate::RawSolanaTransactionMessage,
|
||||
}
|
||||
|
||||
impl crate::RawSolanaTransactionWire {
|
||||
/// Creates one complete transaction wire.
|
||||
#[must_use]
|
||||
pub fn new(signatures: std::vec::Vec<[u8; 64]>, message: crate::RawSolanaTransactionMessage) -> Self {
|
||||
return Self { signatures, message };
|
||||
}
|
||||
|
||||
/// Returns ordered signature bytes.
|
||||
#[must_use]
|
||||
pub fn signatures(&self) -> &[[u8; 64]] {
|
||||
return self.signatures.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the exact source-neutral message.
|
||||
#[must_use]
|
||||
pub const fn message(&self) -> &crate::RawSolanaTransactionMessage {
|
||||
return &self.message;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawSolanaTransactionWire {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("RawSolanaTransactionWire").field("signature_count", &self.signatures.len()).field("message", &self.message).finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||
pub fn serialize_solana_transaction_wire(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
if let std::result::Result::Err(error) = validate_transaction(transaction) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let mut output = std::vec::Vec::new();
|
||||
match transaction.message().version() {
|
||||
crate::RawSolanaMessageVersion::Legacy | crate::RawSolanaMessageVersion::V0 => {
|
||||
if let std::result::Result::Err(error) = encode_short_vec(transaction.signatures().len(), &mut output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for signature in transaction.signatures() {
|
||||
output.extend_from_slice(signature);
|
||||
}
|
||||
if transaction.message().version() == crate::RawSolanaMessageVersion::V0 {
|
||||
output.push(0x80);
|
||||
}
|
||||
if let std::result::Result::Err(error) = encode_legacy_or_v0_message(transaction.message(), &mut output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
crate::RawSolanaMessageVersion::V1 => {
|
||||
if let std::result::Result::Err(error) = encode_v1_message(transaction, &mut output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
if output.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||
}
|
||||
if transaction.message().version() == crate::RawSolanaMessageVersion::V1 && output.len() > MAX_V1_TRANSACTION_BYTES {
|
||||
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Serializes one source-neutral Solana transaction and returns canonical padded standard Base64.
|
||||
pub fn serialize_solana_transaction_wire_base64(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::string::String> {
|
||||
let bytes = match crate::serialize_solana_transaction_wire(transaction) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(base64::engine::general_purpose::STANDARD.encode(bytes));
|
||||
}
|
||||
|
||||
fn validate_transaction(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<()> {
|
||||
let message = transaction.message();
|
||||
let required_signatures = usize::from(message.header().num_required_signatures());
|
||||
if required_signatures == 0 || transaction.signatures().len() != required_signatures {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
if usize::from(message.header().num_readonly_signed_accounts()) >= required_signatures {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
if message.account_keys().len() < required_signatures + usize::from(message.header().num_readonly_unsigned_accounts()) {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
let loaded_count = message.address_table_lookups().iter().fold(0_usize, |count, lookup| {
|
||||
return count.saturating_add(lookup.writable_indexes().len()).saturating_add(lookup.readonly_indexes().len());
|
||||
});
|
||||
let account_count = message.account_keys().len().saturating_add(loaded_count);
|
||||
for instruction in message.instructions() {
|
||||
if usize::from(instruction.program_id_index()) >= account_count {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
for account_index in instruction.accounts() {
|
||||
if usize::from(*account_index) >= account_count {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
}
|
||||
}
|
||||
match message.version() {
|
||||
crate::RawSolanaMessageVersion::Legacy => {
|
||||
if !message.address_table_lookups().is_empty() || message.config().is_some() {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
},
|
||||
crate::RawSolanaMessageVersion::V0 => {
|
||||
if message.config().is_some() {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
},
|
||||
crate::RawSolanaMessageVersion::V1 => {
|
||||
if message.config().is_none() || !message.address_table_lookups().is_empty() {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
if transaction.signatures().len() > MAX_V1_SIGNATURE_COUNT
|
||||
|| message.account_keys().len() > MAX_V1_ADDRESS_COUNT
|
||||
|| message.instructions().len() > MAX_V1_INSTRUCTION_COUNT
|
||||
{
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
for (index, account) in message.account_keys().iter().enumerate() {
|
||||
if message.account_keys()[..index].contains(account) {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
}
|
||||
for instruction in message.instructions() {
|
||||
if instruction.accounts().len() > usize::from(u8::MAX) || instruction.data().len() > usize::from(u16::MAX) {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(heap_size) = message.config().and_then(crate::RawSolanaTransactionConfig::heap_size) {
|
||||
if !(32_768..=262_144).contains(&heap_size) || heap_size % 1_024 != 0 {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn encode_legacy_or_v0_message(message: &crate::RawSolanaTransactionMessage, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
encode_header(message.header(), output);
|
||||
if let std::result::Result::Err(error) = encode_short_vec(message.account_keys().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for account_key in message.account_keys() {
|
||||
output.extend_from_slice(account_key);
|
||||
}
|
||||
output.extend_from_slice(message.recent_blockhash());
|
||||
if let std::result::Result::Err(error) = encode_short_vec(message.instructions().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for instruction in message.instructions() {
|
||||
output.push(instruction.program_id_index());
|
||||
if let std::result::Result::Err(error) = encode_short_vec(instruction.accounts().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.extend_from_slice(instruction.accounts());
|
||||
if let std::result::Result::Err(error) = encode_short_vec(instruction.data().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.extend_from_slice(instruction.data());
|
||||
}
|
||||
if message.version() == crate::RawSolanaMessageVersion::V0 {
|
||||
if let std::result::Result::Err(error) = encode_short_vec(message.address_table_lookups().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for lookup in message.address_table_lookups() {
|
||||
output.extend_from_slice(lookup.account_key());
|
||||
if let std::result::Result::Err(error) = encode_short_vec(lookup.writable_indexes().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.extend_from_slice(lookup.writable_indexes());
|
||||
if let std::result::Result::Err(error) = encode_short_vec(lookup.readonly_indexes().len(), output) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.extend_from_slice(lookup.readonly_indexes());
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn encode_v1_message(transaction: &crate::RawSolanaTransactionWire, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
let message = transaction.message();
|
||||
let config = match message.config() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::material_error("wire")),
|
||||
};
|
||||
output.push(0x81);
|
||||
encode_header(message.header(), output);
|
||||
let mut mask = 0_u32;
|
||||
if config.priority_fee().is_some() {
|
||||
mask |= 0b11;
|
||||
}
|
||||
if config.compute_unit_limit().is_some() {
|
||||
mask |= 1_u32 << 2;
|
||||
}
|
||||
if config.loaded_accounts_data_size_limit().is_some() {
|
||||
mask |= 1_u32 << 3;
|
||||
}
|
||||
if config.heap_size().is_some() {
|
||||
mask |= 1_u32 << 4;
|
||||
}
|
||||
output.extend_from_slice(&mask.to_le_bytes());
|
||||
output.extend_from_slice(message.recent_blockhash());
|
||||
output.push(message.instructions().len() as u8);
|
||||
output.push(message.account_keys().len() as u8);
|
||||
for account_key in message.account_keys() {
|
||||
output.extend_from_slice(account_key);
|
||||
}
|
||||
if let std::option::Option::Some(value) = config.priority_fee() {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
if let std::option::Option::Some(value) = config.compute_unit_limit() {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
if let std::option::Option::Some(value) = config.loaded_accounts_data_size_limit() {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
if let std::option::Option::Some(value) = config.heap_size() {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
for instruction in message.instructions() {
|
||||
output.push(instruction.program_id_index());
|
||||
output.push(instruction.accounts().len() as u8);
|
||||
output.extend_from_slice(&(instruction.data().len() as u16).to_le_bytes());
|
||||
}
|
||||
for instruction in message.instructions() {
|
||||
output.extend_from_slice(instruction.accounts());
|
||||
output.extend_from_slice(instruction.data());
|
||||
}
|
||||
for signature in transaction.signatures() {
|
||||
output.extend_from_slice(signature);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn encode_header(header: crate::RawSolanaMessageHeader, output: &mut std::vec::Vec<u8>) {
|
||||
output.push(header.num_required_signatures());
|
||||
output.push(header.num_readonly_signed_accounts());
|
||||
output.push(header.num_readonly_unsigned_accounts());
|
||||
}
|
||||
|
||||
fn encode_short_vec(value: usize, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
if value > MAX_LEGACY_SHORT_VECTOR_VALUE {
|
||||
return std::result::Result::Err(crate::material_error("wire"));
|
||||
}
|
||||
let mut remaining = value;
|
||||
loop {
|
||||
let mut byte = (remaining & 0x7f) as u8;
|
||||
remaining >>= 7;
|
||||
if remaining != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
output.push(byte);
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wire.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Dependency-boundary canaries for the common RAW Transaction foundation.
|
||||
|
||||
@@ -10,7 +10,7 @@ fn manifest() -> std::string::String {
|
||||
fn production_sources() -> std::string::String {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut combined = std::string::String::new();
|
||||
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"] {
|
||||
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"] {
|
||||
let source = std::fs::read_to_string(root.join(name)).unwrap_or_default();
|
||||
combined.push_str(source.as_str());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Integration canaries for the public common RAW Transaction crate-root surface.
|
||||
|
||||
@@ -99,3 +99,24 @@ fn pre_004_embedded_signature_and_block_material_contract_are_available_from_cra
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_source_neutral_wire_contract_is_available_from_crate_root() {
|
||||
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 0),
|
||||
vec![[1_u8; 32]],
|
||||
[2_u8; 32],
|
||||
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(0, vec![0], vec![3])],
|
||||
vec![],
|
||||
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::default()),
|
||||
);
|
||||
let wire = ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[4_u8; 64]], message);
|
||||
let encoded = ksp_raw_transaction_lib::serialize_solana_transaction_wire_base64(&wire);
|
||||
assert!(encoded.is_ok());
|
||||
if let std::result::Result::Ok(encoded) = encoded {
|
||||
assert!(encoded.starts_with("gQ"));
|
||||
assert!(ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded.as_str()).is_ok());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Release-completeness canaries for the common RAW Transaction foundation.
|
||||
|
||||
@@ -22,7 +22,7 @@ fn pre_002_production_module_inventory_is_exact() {
|
||||
names.push(name.to_owned());
|
||||
}
|
||||
names.sort();
|
||||
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"];
|
||||
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"];
|
||||
assert_eq!(names.len(), expected.len());
|
||||
for (actual, expected_name) in names.iter().zip(expected.iter()) {
|
||||
assert_eq!(actual.as_str(), *expected_name);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
#[test]
|
||||
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
|
||||
@@ -60,3 +62,20 @@ fn pre_004_embedded_signature_rejects_invalid_base64_noncanonical_short_vec_and_
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_embedded_signature_extracts_terminal_v1_signature_and_rejects_trailing_bytes() {
|
||||
let encoded = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||
let signature = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||
assert!(signature.is_ok());
|
||||
if let std::result::Result::Ok(signature) = signature {
|
||||
assert_eq!(signature.as_bytes(), &[0xaa_u8; 64]);
|
||||
}
|
||||
let decoded = base64::engine::general_purpose::STANDARD.decode(encoded);
|
||||
assert!(decoded.is_ok());
|
||||
if let std::result::Result::Ok(mut decoded) = decoded {
|
||||
decoded.push(0);
|
||||
let trailing = base64::engine::general_purpose::STANDARD.encode(decoded);
|
||||
assert!(crate::extract_raw_transaction_signature_from_binary_base64(trailing.as_str()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
// file: crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||
// version: 1
|
||||
|
||||
const LEGACY_GOLDEN_BASE64: &str = "AREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREBAAECISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExAQEBAAKquw==";
|
||||
const V0_GOLDEN_BASE64: &str = "ARISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhKAAQAAASMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIBAAEBAcwBJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQBAAA=";
|
||||
const V1_EMPTY_CONFIG_GOLDEN_BASE64: &str = "gQEAAAAAAABSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgABQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0Orq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur";
|
||||
const V1_GOLDEN_BASE64: &str = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||
|
||||
fn legacy_wire() -> crate::RawSolanaTransactionWire {
|
||||
let message = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::Legacy,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||
vec![[0x21_u8; 32], [0x22_u8; 32]],
|
||||
[0x31_u8; 32],
|
||||
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![0xaa, 0xbb])],
|
||||
vec![],
|
||||
std::option::Option::None,
|
||||
);
|
||||
return crate::RawSolanaTransactionWire::new(vec![[0x11_u8; 64]], message);
|
||||
}
|
||||
|
||||
fn v0_wire() -> crate::RawSolanaTransactionWire {
|
||||
let message = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::V0,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||
vec![[0x23_u8; 32]],
|
||||
[0x32_u8; 32],
|
||||
vec![crate::RawSolanaCompiledInstruction::new(0, vec![1], vec![0xcc])],
|
||||
vec![crate::RawSolanaAddressTableLookup::new([0x24_u8; 32], vec![0], vec![])],
|
||||
std::option::Option::None,
|
||||
);
|
||||
return crate::RawSolanaTransactionWire::new(vec![[0x12_u8; 64]], message);
|
||||
}
|
||||
|
||||
fn v1_wire() -> crate::RawSolanaTransactionWire {
|
||||
let message = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::V1,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||
vec![[0x41_u8; 32], [0x42_u8; 32]],
|
||||
[0x51_u8; 32],
|
||||
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![1, 2, 3])],
|
||||
vec![],
|
||||
std::option::Option::Some(crate::RawSolanaTransactionConfig::new(
|
||||
std::option::Option::Some(7),
|
||||
std::option::Option::Some(8),
|
||||
std::option::Option::Some(9),
|
||||
std::option::Option::Some(32_768),
|
||||
)),
|
||||
);
|
||||
return crate::RawSolanaTransactionWire::new(vec![[0xaa_u8; 64]], message);
|
||||
}
|
||||
|
||||
fn v1_empty_config_wire() -> crate::RawSolanaTransactionWire {
|
||||
let message = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::V1,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||
vec![[0x43_u8; 32]],
|
||||
[0x52_u8; 32],
|
||||
vec![],
|
||||
vec![],
|
||||
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||
);
|
||||
return crate::RawSolanaTransactionWire::new(vec![[0xab_u8; 64]], message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_legacy_v0_and_v1_wire_goldens_are_exact() {
|
||||
let legacy = crate::serialize_solana_transaction_wire_base64(&legacy_wire());
|
||||
let v0 = crate::serialize_solana_transaction_wire_base64(&v0_wire());
|
||||
let v1 = crate::serialize_solana_transaction_wire_base64(&v1_wire());
|
||||
let v1_empty = crate::serialize_solana_transaction_wire_base64(&v1_empty_config_wire());
|
||||
assert_eq!(legacy.as_deref(), std::result::Result::Ok(LEGACY_GOLDEN_BASE64));
|
||||
assert_eq!(v0.as_deref(), std::result::Result::Ok(V0_GOLDEN_BASE64));
|
||||
assert_eq!(v1.as_deref(), std::result::Result::Ok(V1_GOLDEN_BASE64));
|
||||
assert_eq!(v1_empty.as_deref(), std::result::Result::Ok(V1_EMPTY_CONFIG_GOLDEN_BASE64));
|
||||
let v1_bytes = crate::serialize_solana_transaction_wire(&v1_wire());
|
||||
assert!(v1_bytes.is_ok());
|
||||
if let std::result::Result::Ok(v1_bytes) = v1_bytes {
|
||||
assert_eq!(v1_bytes.len(), 198);
|
||||
assert_eq!(v1_bytes.first(), std::option::Option::Some(&0x81));
|
||||
assert_eq!(v1_bytes.get(v1_bytes.len() - 64..), std::option::Option::Some([0xaa_u8; 64].as_slice()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_v1_structural_guards_reject_alt_duplicates_and_missing_config() {
|
||||
let mut missing_config = v1_wire();
|
||||
missing_config.message.config = std::option::Option::None;
|
||||
assert!(crate::serialize_solana_transaction_wire(&missing_config).is_err());
|
||||
let mut with_lookup = v1_wire();
|
||||
with_lookup.message.address_table_lookups.push(crate::RawSolanaAddressTableLookup::new([9_u8; 32], vec![], vec![]));
|
||||
assert!(crate::serialize_solana_transaction_wire(&with_lookup).is_err());
|
||||
let mut duplicate = v1_wire();
|
||||
duplicate.message.account_keys[1] = duplicate.message.account_keys[0];
|
||||
assert!(crate::serialize_solana_transaction_wire(&duplicate).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_v1_program_index_zero_is_valid_and_bounds_remain_enforced() {
|
||||
let message = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::V1,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||
vec![[1_u8; 32]],
|
||||
[2_u8; 32],
|
||||
vec![crate::RawSolanaCompiledInstruction::new(0, vec![0], vec![])],
|
||||
vec![],
|
||||
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||
);
|
||||
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], message);
|
||||
assert!(crate::serialize_solana_transaction_wire(&wire).is_ok());
|
||||
let too_many_addresses = crate::RawSolanaTransactionMessage::new(
|
||||
crate::RawSolanaMessageVersion::V1,
|
||||
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||
(0_u8..65_u8).map(|value| return [value; 32]).collect(),
|
||||
[2_u8; 32],
|
||||
vec![],
|
||||
vec![],
|
||||
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||
);
|
||||
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], too_many_addresses);
|
||||
assert!(crate::serialize_solana_transaction_wire(&wire).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user