v0.3.10-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-raw-transaction-lib/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-raw-transaction-lib"
|
||||
@@ -8,6 +8,7 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-store-api = { path = "../ksp-store-api" }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/canonical.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
@@ -75,6 +75,26 @@ impl crate::RawTransactionMaterial {
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates complete source-neutral RAW v1 material from a Base64 transaction wire that embeds its own canonical signature array.
|
||||
pub fn binary_base64_with_embedded_signature(
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
slot: u64,
|
||||
block_time: std::option::Option<i64>,
|
||||
transaction_data: impl std::convert::Into<std::string::String>,
|
||||
meta: crate::RawTransactionWireField<serde_json::Value>,
|
||||
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
|
||||
transaction_index: crate::RawTransactionWireField<u32>,
|
||||
) -> ksp_core_lib::Result<Self> {
|
||||
let transaction_data = transaction_data.into();
|
||||
let signature = crate::extract_raw_transaction_signature_from_binary_base64(transaction_data.as_str());
|
||||
return match signature {
|
||||
std::result::Result::Ok(signature) => {
|
||||
std::result::Result::Ok(Self { block_time, meta, network, signature, slot, transaction_data, transaction_index, version })
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawTransactionMaterial {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/error.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction", "canonicalization_invalid");
|
||||
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "material_invalid");
|
||||
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
|
||||
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "signature_invalid");
|
||||
|
||||
/// Creates a safe canonicalization error without copying source payload material.
|
||||
@@ -21,7 +21,7 @@ pub(crate) fn material_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
/// Creates a safe signature error without copying textual signature material.
|
||||
/// Creates a safe signature error without copying source signature or transaction material.
|
||||
pub(crate) fn signature_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID, "invalid canonical Solana transaction signature");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -37,12 +37,14 @@ pub use self::canonical::canonicalize_raw_transaction;
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID;
|
||||
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID;
|
||||
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
|
||||
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID;
|
||||
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub use self::signature::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
/// Extracts the first canonical 64-byte Solana signature from one complete Base64 transaction wire.
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
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;
|
||||
@@ -50,6 +52,80 @@ pub fn parse_raw_transaction_signature(value: &str) -> ksp_core_lib::Result<ksp_
|
||||
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());
|
||||
}
|
||||
let prefix = decode_signature_count(decoded.as_slice());
|
||||
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());
|
||||
}
|
||||
let first_end = match prefix_len.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) {
|
||||
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'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Dependency-boundary canaries for the common RAW Transaction foundation.
|
||||
|
||||
@@ -18,8 +18,9 @@ fn production_sources() -> std::string::String {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_manifest_uses_api_models_and_exact_common_dependencies() {
|
||||
fn pre_004_manifest_adds_only_base64_to_the_exact_common_dependency_surface() {
|
||||
let manifest = manifest();
|
||||
assert!(manifest.contains("base64.workspace = true"));
|
||||
assert!(manifest.contains("ksp-core-lib = { path = \"../ksp-core-lib\" }"));
|
||||
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
||||
assert!(manifest.contains("serde_json = { workspace = true }"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Integration canaries for the public common RAW Transaction crate-root surface.
|
||||
|
||||
@@ -77,3 +77,25 @@ fn pre_002_acquisition_assembly_accepts_store_api_producer_owned_metadata() {
|
||||
assert_eq!(acquisition.transaction().reference(), acquisition.observation().transaction());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_embedded_signature_and_block_material_contract_are_available_from_crate_root() {
|
||||
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
let signature = ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||
assert!(signature.is_ok());
|
||||
let network = ksp_store_api::RawNetworkId::new("devnet");
|
||||
assert!(network.is_ok());
|
||||
if let std::result::Result::Ok(network) = network {
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
1,
|
||||
std::option::Option::None,
|
||||
encoded,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(0),
|
||||
);
|
||||
assert!(material.is_ok());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn network() -> std::option::Option<ksp_store_api::RawNetworkId> {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
@@ -217,3 +217,31 @@ fn pre_002_material_and_wire_debug_do_not_render_transaction_or_meta_material()
|
||||
assert_eq!(std::format!("{:?}", crate::RawTransactionWireField::Value(secret_meta)), "Value(..)");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_embedded_signature_material_preserves_block_fields_and_canonicalizes() {
|
||||
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(network) => network,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let material = crate::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
430_000_123,
|
||||
std::option::Option::Some(1_787_072_400),
|
||||
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
crate::RawTransactionWireField::Value(serde_json::json!({"err":null,"fee":5000})),
|
||||
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
|
||||
crate::RawTransactionWireField::Value(0_u32),
|
||||
);
|
||||
assert!(material.is_ok());
|
||||
let transaction = match material.and_then(crate::canonicalize_raw_transaction) {
|
||||
std::result::Result::Ok(transaction) => transaction,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(transaction.reference().signature().as_bytes(), &[0_u8; 64]);
|
||||
assert_eq!(transaction.slot(), 430_000_123);
|
||||
assert_eq!(transaction.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
|
||||
assert!(transaction.payload().bytes().starts_with(b"{\"transaction\":[\"AQAAAA"));
|
||||
assert!(transaction.payload().bytes().ends_with(b"\"version\":\"legacy\",\"transactionIndex\":0}"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
|
||||
@@ -31,3 +31,32 @@ fn pre_002_signature_parser_rejects_text_bounds_invalid_base58_and_noncanonical_
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_embedded_signature_extracts_first_signature_from_canonical_transaction_base64() {
|
||||
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
let signature = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||
assert!(signature.is_ok());
|
||||
if let std::result::Result::Ok(signature) = signature {
|
||||
assert_eq!(signature.as_bytes(), &[0_u8; 64]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_embedded_signature_rejects_invalid_base64_noncanonical_short_vec_and_truncation() {
|
||||
for encoded in [
|
||||
"not-base64",
|
||||
"gQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||
] {
|
||||
let result = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||
assert!(result.is_err());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
|
||||
assert!(!error.to_string().contains(encoded));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user