v0.3.10-pre.006
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user