// 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, data: std::vec::Vec, } 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, data: std::vec::Vec) -> 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, readonly_indexes: std::vec::Vec, } impl crate::RawSolanaAddressTableLookup { /// Creates one exact v0 address-table lookup. #[must_use] pub fn new(account_key: [u8; 32], writable_indexes: std::vec::Vec, readonly_indexes: std::vec::Vec) -> 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, compute_unit_limit: std::option::Option, loaded_accounts_data_size_limit: std::option::Option, heap_size: std::option::Option, } 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, compute_unit_limit: std::option::Option, loaded_accounts_data_size_limit: std::option::Option, heap_size: std::option::Option, ) -> 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 { return self.priority_fee; } /// Returns the optional compute-unit limit. #[must_use] pub const fn compute_unit_limit(self) -> std::option::Option { 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 { return self.loaded_accounts_data_size_limit; } /// Returns the optional heap-size override. #[must_use] pub const fn heap_size(self) -> std::option::Option { 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, address_table_lookups: std::vec::Vec, config: std::option::Option, } 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, address_table_lookups: std::vec::Vec, config: std::option::Option, ) -> 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 { 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> { 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 { 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) -> 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) -> 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) { 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) -> 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;