// file: crates/ksp-raw-transaction-lib/src/signature.rs // version: 4 use base64::Engine; // rust-rules: trait-import /// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature. pub const MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 88; /// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature. pub const MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 64; /// Encodes one canonical 64-byte Solana transaction signature as bounded Base58 text. #[must_use] pub fn format_raw_transaction_signature(signature: &ksp_store_api::RawTransactionSignature) -> std::string::String { const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; let bytes = signature.as_bytes(); let leading_zeroes = bytes.iter().take_while(|byte| return **byte == 0).count(); let mut digits = std::vec::Vec::with_capacity(crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES); for byte in bytes { let mut carry = u32::from(*byte); for digit in &mut digits { let expanded = (u32::from(*digit) * 256) + carry; *digit = (expanded % 58) as u8; carry = expanded / 58; } while carry != 0 { digits.push((carry % 58) as u8); carry /= 58; } } let mut output = std::string::String::with_capacity(leading_zeroes + digits.len()); for _ in 0..leading_zeroes { output.push('1'); } for digit in digits.iter().rev() { output.push(char::from(ALPHABET[usize::from(*digit)])); } return output; } /// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes. pub fn parse_raw_transaction_signature(value: &str) -> ksp_core_lib::Result { let text = value.as_bytes(); if text.len() < crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES || text.len() > crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES { return std::result::Result::Err( crate::signature_error() .with_context("actual_len", text.len().to_string()) .with_context("minimum_len", crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string()) .with_context("maximum_len", crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string()), ); } let mut decoded = [0_u8; 64]; let mut leading_zeroes = 0_usize; for byte in text { if *byte != b'1' { break; } leading_zeroes += 1; } for byte in text { let digit = match base58_digit(*byte) { std::option::Option::Some(digit) => digit, std::option::Option::None => return std::result::Result::Err(crate::signature_error()), }; let mut carry = u32::from(digit); for output in decoded.iter_mut().rev() { let expanded = (u32::from(*output) * 58) + carry; *output = (expanded & 0xff) as u8; carry = expanded >> 8; } if carry != 0 { return std::result::Result::Err(crate::signature_error()); } } let significant_len = match decoded.iter().position(|byte| return *byte != 0) { std::option::Option::Some(index) => decoded.len() - index, std::option::Option::None => 0, }; if leading_zeroes + significant_len != decoded.len() { return std::result::Result::Err(crate::signature_error()); } return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(decoded)); } /// Extracts the first canonical 64-byte Solana signature from one complete Base64-encoded transaction wire. /// /// The compact signature-count prefix must use its canonical short-vector representation, contain at least one signature, /// and the decoded transaction must retain message bytes after the declared signature array. pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_core_lib::Result { if value.is_empty() || value.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES { return std::result::Result::Err(crate::signature_error()); } let decoded = base64::engine::general_purpose::STANDARD.decode(value); let decoded = match decoded { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(_) => return std::result::Result::Err(crate::signature_error()), }; if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value { return std::result::Result::Err(crate::signature_error()); } 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 { 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), }; if signature_count == 0 { return std::result::Result::Err(crate::signature_error()); } let signatures_len = match signature_count.checked_mul(64) { std::option::Option::Some(length) => length, std::option::Option::None => return std::result::Result::Err(crate::signature_error()), }; let message_offset = match prefix_len.checked_add(signatures_len) { std::option::Option::Some(offset) => offset, std::option::Option::None => return std::result::Result::Err(crate::signature_error()), }; if message_offset >= decoded.len() { return std::result::Result::Err(crate::signature_error()); } return copy_signature(decoded, prefix_len); } fn extract_v1_signature(decoded: &[u8]) -> ksp_core_lib::Result { 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 { 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(offset..first_end) { std::option::Option::Some(first) => first, std::option::Option::None => return std::result::Result::Err(crate::signature_error()), }; let mut signature = [0_u8; 64]; signature.copy_from_slice(first); return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(signature)); } fn decode_signature_count(bytes: &[u8]) -> ksp_core_lib::Result<(usize, usize)> { let mut value = 0_usize; for index in 0..3_usize { let byte = match bytes.get(index) { std::option::Option::Some(byte) => *byte, std::option::Option::None => return std::result::Result::Err(crate::signature_error()), }; let payload = usize::from(byte & 0x7f); let shift = index * 7; value |= payload << shift; if byte & 0x80 == 0 { if index != 0 { let minimum = 1_usize << shift; if value < minimum { return std::result::Result::Err(crate::signature_error()); } } if value > usize::from(u16::MAX) { return std::result::Result::Err(crate::signature_error()); } return std::result::Result::Ok((value, index + 1)); } } return std::result::Result::Err(crate::signature_error()); } fn base58_digit(byte: u8) -> std::option::Option { return match byte { b'1'..=b'9' => std::option::Option::Some(byte - b'1'), b'A'..=b'H' => std::option::Option::Some((byte - b'A') + 9), b'J'..=b'N' => std::option::Option::Some((byte - b'J') + 17), b'P'..=b'Z' => std::option::Option::Some((byte - b'P') + 22), b'a'..=b'k' => std::option::Option::Some((byte - b'a') + 33), b'm'..=b'z' => std::option::Option::Some((byte - b'm') + 44), _ => std::option::Option::None, }; } #[cfg(test)] #[path = "../unit_tests/signature.rs"] mod tests;