320 lines
15 KiB
Rust
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;
|