Files
khadhroony-solana-project/crates/ksp-raw-transaction-lib/src/signature.rs
2026-09-09 11:44:36 +02:00

320 lines
15 KiB
Rust

// 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<ksp_store_api::RawTransactionSignature> {
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<ksp_store_api::RawTransactionSignature> {
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<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),
};
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<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(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<u8> {
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;