Files
khadhroony-solana-project/crates/ksp-raw-transaction-lib/src/wire.rs

546 lines
21 KiB
Rust

// file: crates/ksp-raw-transaction-lib/src/wire.rs
// version: 3
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)
&& (!(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;