v0.3.10-pre.002

This commit is contained in:
2026-09-06 09:58:31 +02:00
parent 6b434750c5
commit d0da907c00
17 changed files with 1453 additions and 29 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 490 # version: 491
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"] members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
[workspace.package] [workspace.package]
version = "0.3.10-pre.1" version = "0.3.10-pre.2"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,17 @@
# file: crates/ksp-raw-transaction-lib/Cargo.toml
# version: 1
[package]
name = "ksp-raw-transaction-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-store-api = { path = "../ksp-store-api" }
serde_json = { workspace = true }
sha2 = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,49 @@
// file: crates/ksp-raw-transaction-lib/src/acquisition.rs
// version: 1
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
pub struct RawTransactionAcquisition {
observation: ksp_store_api::RawTransactionObservation,
transaction: ksp_store_api::RawTransaction,
}
impl crate::RawTransactionAcquisition {
/// Returns the producer-owned acquisition observation linked to the canonical transaction.
#[must_use]
pub const fn observation(&self) -> &ksp_store_api::RawTransactionObservation {
return &self.observation;
}
/// Returns the canonical source-independent RAW transaction.
#[must_use]
pub const fn transaction(&self) -> &ksp_store_api::RawTransaction {
return &self.transaction;
}
/// Consumes this acquisition into its canonical transaction and observation.
#[must_use]
pub fn into_parts(self) -> (ksp_store_api::RawTransaction, ksp_store_api::RawTransactionObservation) {
return (self.transaction, self.observation);
}
}
impl std::fmt::Debug for crate::RawTransactionAcquisition {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("RawTransactionAcquisition(..)");
}
}
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
#[must_use]
pub fn assemble_raw_transaction_acquisition(
transaction: ksp_store_api::RawTransaction,
observation_key: ksp_store_api::RawObservationKey,
provenance: ksp_store_api::RawAcquisitionProvenance,
) -> crate::RawTransactionAcquisition {
let observation = ksp_store_api::RawTransactionObservation::new(observation_key, transaction.reference().clone(), provenance);
return crate::RawTransactionAcquisition { observation, transaction };
}
#[cfg(test)]
#[path = "../unit_tests/acquisition.rs"]
mod tests;

View File

@@ -0,0 +1,305 @@
// file: crates/ksp-raw-transaction-lib/src/canonical.rs
// version: 1
use sha2::Digest; // rust-rules: trait-import
/// KSP-owned source-independent RAW transaction format identifier.
pub const RAW_TRANSACTION_FORMAT_ID: &str = "ksp.solana.raw_transaction";
/// Frozen KSP-owned RAW transaction format version.
pub const RAW_TRANSACTION_FORMAT_VERSION: u32 = 1;
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
#[non_exhaustive]
pub enum RawTransactionWireField<T> {
/// The source field was omitted entirely.
Omitted,
/// The source field was explicitly present as JSON/protocol null.
Null,
/// The source field carried one concrete value.
Value(T),
}
impl<T> std::fmt::Debug for crate::RawTransactionWireField<T> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::Omitted => formatter.write_str("Omitted"),
Self::Null => formatter.write_str("Null"),
Self::Value(_) => formatter.write_str("Value(..)"),
};
}
}
/// Canonical Solana transaction version admitted by RAW transaction format v1.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RawTransactionVersion {
/// Legacy unversioned transaction representation.
Legacy,
/// Explicit versioned transaction number admitted by the current Solana wire contract.
Number(u8),
}
/// Complete source-neutral material required to construct one canonical RAW transaction.
pub struct RawTransactionMaterial {
block_time: std::option::Option<i64>,
meta: crate::RawTransactionWireField<serde_json::Value>,
network: ksp_store_api::RawNetworkId,
signature: ksp_store_api::RawTransactionSignature,
slot: u64,
transaction_data: std::string::String,
transaction_index: crate::RawTransactionWireField<u32>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
}
impl crate::RawTransactionMaterial {
/// Creates complete source-neutral material for the frozen RAW v1 binary Base64 body path.
#[must_use]
pub fn binary_base64(
network: ksp_store_api::RawNetworkId,
signature: ksp_store_api::RawTransactionSignature,
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>,
) -> Self {
return Self {
block_time,
meta,
network,
signature,
slot,
transaction_data: transaction_data.into(),
transaction_index,
version,
};
}
}
impl std::fmt::Debug for crate::RawTransactionMaterial {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("RawTransactionMaterial")
.field("network", &self.network)
.field("slot", &self.slot)
.field("block_time", &self.block_time)
.field("transaction_data_len", &self.transaction_data.len())
.field("meta", &wire_field_state(&self.meta))
.field("version", &wire_field_state(&self.version))
.field("transaction_index", &wire_field_state(&self.transaction_index))
.finish_non_exhaustive();
}
}
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
pub fn canonicalize_raw_transaction(material: crate::RawTransactionMaterial) -> ksp_core_lib::Result<ksp_store_api::RawTransaction> {
let block_time = convert_block_time(material.block_time);
let block_time = match block_time {
std::result::Result::Ok(block_time) => block_time,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let bytes = canonical_payload_bytes(&material);
let bytes = match bytes {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if bytes.is_empty() || bytes.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
return std::result::Result::Err(crate::canonicalization_error("payload.bytes"));
}
let hash: [u8; 32] = sha2::Sha256::digest(bytes.as_slice()).into();
let format_id = ksp_store_api::RawFormatId::new(crate::RAW_TRANSACTION_FORMAT_ID);
let format_id = match format_id {
std::result::Result::Ok(format_id) => format_id,
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload.format_id")),
};
let payload = ksp_store_api::RawPayload::try_new(
format_id,
crate::RAW_TRANSACTION_FORMAT_VERSION,
bytes.into_boxed_slice(),
ksp_store_api::RawContentHash::new(hash),
);
let payload = match payload {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload")),
};
let reference = ksp_store_api::RawTransactionReference::new(material.network, material.signature);
return std::result::Result::Ok(ksp_store_api::RawTransaction::new(reference, material.slot, block_time, payload));
}
fn append_canonical_json(output: &mut std::vec::Vec<u8>, value: &serde_json::Value) -> ksp_core_lib::Result<()> {
return match value {
serde_json::Value::Null => {
output.extend_from_slice(b"null");
std::result::Result::Ok(())
},
serde_json::Value::Bool(value) => {
if *value {
output.extend_from_slice(b"true");
} else {
output.extend_from_slice(b"false");
}
std::result::Result::Ok(())
},
serde_json::Value::Number(value) => {
output.extend_from_slice(value.to_string().as_bytes());
std::result::Result::Ok(())
},
serde_json::Value::String(value) => append_json_string(output, value.as_str()),
serde_json::Value::Array(values) => {
output.push(b'[');
for (index, item) in values.iter().enumerate() {
if index != 0 {
output.push(b',');
}
let item_result = append_canonical_json(output, item);
if let std::result::Result::Err(error) = item_result {
return std::result::Result::Err(error);
}
}
output.push(b']');
std::result::Result::Ok(())
},
serde_json::Value::Object(values) => {
output.push(b'{');
let mut keys = values.keys().collect::<std::vec::Vec<_>>();
keys.sort_unstable();
for (index, key) in keys.iter().enumerate() {
if index != 0 {
output.push(b',');
}
let key_result = append_json_string(output, key.as_str());
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.push(b':');
let item = values.get(key.as_str());
let item = match item {
std::option::Option::Some(item) => item,
std::option::Option::None => return std::result::Result::Err(crate::canonicalization_error("payload.meta")),
};
let item_result = append_canonical_json(output, item);
if let std::result::Result::Err(error) = item_result {
return std::result::Result::Err(error);
}
}
output.push(b'}');
std::result::Result::Ok(())
},
};
}
fn append_json_string(output: &mut std::vec::Vec<u8>, value: &str) -> ksp_core_lib::Result<()> {
let encoded = serde_json::to_vec(value);
return match encoded {
std::result::Result::Ok(encoded) => {
output.extend_from_slice(encoded.as_slice());
std::result::Result::Ok(())
},
std::result::Result::Err(_) => std::result::Result::Err(crate::canonicalization_error("payload.json")),
};
}
fn append_wire_value<T, F>(
output: &mut std::vec::Vec<u8>,
key: &str,
field: &crate::RawTransactionWireField<T>,
mut append_value: F,
) -> ksp_core_lib::Result<()>
where
F: FnMut(&mut std::vec::Vec<u8>, &T) -> ksp_core_lib::Result<()>,
{
return match field {
crate::RawTransactionWireField::Omitted => std::result::Result::Ok(()),
crate::RawTransactionWireField::Null => {
output.push(b',');
let key_result = append_json_string(output, key);
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.extend_from_slice(b":null");
std::result::Result::Ok(())
},
crate::RawTransactionWireField::Value(value) => {
output.push(b',');
let key_result = append_json_string(output, key);
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.push(b':');
append_value(output, value)
},
};
}
fn canonical_payload_bytes(material: &crate::RawTransactionMaterial) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let mut bytes = std::vec::Vec::new();
bytes.extend_from_slice(b"{\"transaction\":[");
let transaction_result = append_json_string(&mut bytes, material.transaction_data.as_str());
if let std::result::Result::Err(error) = transaction_result {
return std::result::Result::Err(error);
}
bytes.push(b',');
let encoding_result = append_json_string(&mut bytes, "base64");
if let std::result::Result::Err(error) = encoding_result {
return std::result::Result::Err(error);
}
bytes.push(b']');
let meta_result = append_wire_value(&mut bytes, "meta", &material.meta, |output, value| return append_canonical_json(output, value));
if let std::result::Result::Err(error) = meta_result {
return std::result::Result::Err(error);
}
let version_result = append_wire_value(&mut bytes, "version", &material.version, |output, value| {
return match value {
crate::RawTransactionVersion::Legacy => append_json_string(output, "legacy"),
crate::RawTransactionVersion::Number(number) => {
output.extend_from_slice(number.to_string().as_bytes());
std::result::Result::Ok(())
},
};
});
if let std::result::Result::Err(error) = version_result {
return std::result::Result::Err(error);
}
let transaction_index_result = append_wire_value(&mut bytes, "transactionIndex", &material.transaction_index, |output, value| {
output.extend_from_slice(value.to_string().as_bytes());
return std::result::Result::Ok(());
});
if let std::result::Result::Err(error) = transaction_index_result {
return std::result::Result::Err(error);
}
bytes.push(b'}');
return std::result::Result::Ok(bytes);
}
fn convert_block_time(value: std::option::Option<i64>) -> ksp_core_lib::Result<std::option::Option<ksp_store_api::RawTimestamp>> {
let seconds = match value {
std::option::Option::Some(seconds) => seconds,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let seconds = match u64::try_from(seconds) {
std::result::Result::Ok(seconds) => seconds,
std::result::Result::Err(_) => return std::result::Result::Err(crate::material_error("block_time")),
};
let millis = match seconds.checked_mul(1_000) {
std::option::Option::Some(millis) => millis,
std::option::Option::None => return std::result::Result::Err(crate::material_error("block_time")),
};
let timestamp = ksp_store_api::RawTimestamp::from_unix_millis(millis);
return match timestamp {
std::result::Result::Ok(timestamp) => std::result::Result::Ok(std::option::Option::Some(timestamp)),
std::result::Result::Err(_) => std::result::Result::Err(crate::material_error("block_time")),
};
}
fn wire_field_state<T>(field: &crate::RawTransactionWireField<T>) -> &'static str {
return match field {
crate::RawTransactionWireField::Omitted => "omitted",
crate::RawTransactionWireField::Null => "null",
crate::RawTransactionWireField::Value(_) => "value",
};
}
#[cfg(test)]
#[path = "../unit_tests/canonical.rs"]
mod tests;

View File

@@ -0,0 +1,27 @@
// file: crates/ksp-raw-transaction-lib/src/error.rs
// version: 1
/// 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.
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.
pub(crate) fn canonicalization_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID, "invalid canonical RAW transaction representation")
.with_context("field", field);
}
/// Creates a safe source-material error without copying source payload material.
pub(crate) fn material_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID, "invalid source-neutral RAW transaction material")
.with_context("field", field);
}
/// Creates a safe signature error without copying textual signature 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");
}

View File

@@ -0,0 +1,54 @@
// file: crates/ksp-raw-transaction-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Source-neutral canonicalization of complete Solana transactions into KSP RAW transaction v1.
//!
//! This lower layer owns the frozen RAW transaction format, textual signature parsing, canonical
//! JSON construction, content hashing and assembly of one canonical transaction with one
//! producer-owned observation. It owns no Transport, Config, Job, Worker, async runtime or Store
//! backend behavior.
mod acquisition;
mod canonical;
mod error;
mod signature;
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
pub use self::acquisition::RawTransactionAcquisition;
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
pub use self::acquisition::assemble_raw_transaction_acquisition;
/// KSP-owned source-independent RAW transaction format identifier.
pub use self::canonical::RAW_TRANSACTION_FORMAT_ID;
/// Frozen KSP-owned RAW transaction format version.
pub use self::canonical::RAW_TRANSACTION_FORMAT_VERSION;
/// Complete source-neutral material required to construct one canonical RAW transaction.
pub use self::canonical::RawTransactionMaterial;
/// Canonical Solana transaction version admitted by RAW transaction format v1.
pub use self::canonical::RawTransactionVersion;
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
pub use self::canonical::RawTransactionWireField;
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
pub use self::canonical::canonicalize_raw_transaction;
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
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.
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;
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
pub use self::signature::parse_raw_transaction_signature;
/// Creates a safe canonicalization error without copying source payload material.
pub(crate) use self::error::canonicalization_error;
/// Creates a safe source-material error without copying source payload material.
pub(crate) use self::error::material_error;
/// Creates a safe signature error without copying textual signature material.
pub(crate) use self::error::signature_error;

View File

@@ -0,0 +1,67 @@
// file: crates/ksp-raw-transaction-lib/src/signature.rs
// version: 1
/// 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;
/// 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));
}
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;

View File

@@ -0,0 +1,52 @@
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
// version: 1
fn manifest() -> std::string::String {
return std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")).unwrap_or_default();
}
fn production_sources() -> std::string::String {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut combined = std::string::String::new();
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"] {
let source = std::fs::read_to_string(root.join(name)).unwrap_or_default();
combined.push_str(source.as_str());
}
return combined;
}
#[test]
fn pre_002_manifest_uses_api_models_and_exact_common_dependencies() {
let manifest = manifest();
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 }"));
assert!(manifest.contains("sha2 = { workspace = true }"));
for forbidden in
["ksp-store-lib", "ksp-onchain-transport-lib", "ksp-config-lib", "ksp-job-api", "ksp-job-backfill-lib", "ksp-worker-api", "tokio", "futures"]
{
assert!(!manifest.contains(forbidden));
}
return;
}
#[test]
fn pre_002_production_sources_keep_transport_runtime_job_worker_and_backend_out() {
let sources = production_sources();
for forbidden in [
"ksp_store_lib",
"ksp_onchain_transport_lib",
"ksp_config_lib",
"ksp_job_api",
"ksp_job_backfill_lib",
"ksp_worker_api",
"ksp_store_postgres_lib",
"tokio::",
"futures::",
"reqwest::",
"tonic::",
] {
assert!(!sources.contains(forbidden));
}
return;
}

View File

@@ -0,0 +1,77 @@
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
// version: 1
#[test]
fn pre_002_common_raw_contract_is_consumable_from_crate_root() {
assert_eq!(ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_ID, "ksp.solana.raw_transaction");
assert_eq!(ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_VERSION, 1);
assert_eq!(ksp_raw_transaction_lib::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES, 64);
assert_eq!(ksp_raw_transaction_lib::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES, 88);
let network = match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature("1".repeat(64).as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
network,
signature,
5,
std::option::Option::None,
"AQID",
ksp_raw_transaction_lib::RawTransactionWireField::Null,
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy),
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
);
let transaction = ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
assert!(transaction.is_ok());
return;
}
#[test]
fn pre_002_acquisition_assembly_accepts_store_api_producer_owned_metadata() {
let network = match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature("1".repeat(64).as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
network,
signature,
5,
std::option::Option::None,
"AQID",
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
);
let transaction = match ksp_raw_transaction_lib::canonicalize_raw_transaction(material) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let provider = match ksp_store_api::RawProvenanceCode::new("fixture") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let protocol = match ksp_store_api::RawProvenanceCode::new("fixture.protocol") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let method = match ksp_store_api::RawProvenanceCode::new("fixture.method") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_api::RawAcquisitionOrigin::Import, received_at);
let acquisition = ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(transaction, ksp_store_api::RawObservationKey::new([9_u8; 32]), provenance);
assert_eq!(acquisition.transaction().reference(), acquisition.observation().transaction());
return;
}

View File

@@ -0,0 +1,37 @@
// file: crates/ksp-raw-transaction-lib/tests/release_completeness.rs
// version: 1
#[test]
fn pre_002_production_module_inventory_is_exact() {
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let entries = match std::fs::read_dir(source_root) {
std::result::Result::Ok(entries) => entries,
std::result::Result::Err(_) => return,
};
let mut names = std::vec::Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
if let std::option::Option::Some(name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
names.push(name.to_owned());
}
}
}
names.sort();
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"];
assert_eq!(names.len(), expected.len());
for (actual, expected_name) in names.iter().zip(expected.iter()) {
assert_eq!(actual.as_str(), *expected_name);
}
return;
}
#[test]
fn pre_002_common_surface_contains_no_worker_job_or_transport_dependency_or_public_module() {
let lib = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/lib.rs")).unwrap_or_default();
assert!(!lib.contains("pub mod "));
for forbidden in ["ksp_worker", "ksp_job", "ksp_onchain_transport", "ksp_config", "ksp_store_lib", "ksp_store_postgres"] {
assert!(!lib.contains(forbidden));
}
return;
}

View File

@@ -0,0 +1,26 @@
// file: crates/ksp-raw-transaction-lib/tests/security_hardening.rs
// version: 1
#[test]
fn pre_002_signature_errors_never_echo_hostile_input() {
let hostile = format!("{}0", "A".repeat(63));
let result = ksp_raw_transaction_lib::parse_raw_transaction_signature(hostile.as_str());
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
assert!(!std::format!("{error:?}").contains(hostile.as_str()));
assert!(!error.to_string().contains(hostile.as_str()));
}
return;
}
#[test]
fn pre_002_public_error_codes_are_stable_and_domain_scoped() {
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID.domain(), "raw_transaction");
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID.code(), "canonicalization_invalid");
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID.domain(), "raw_transaction");
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID.code(), "material_invalid");
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID.domain(), "raw_transaction");
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID.code(), "signature_invalid");
return;
}

View File

@@ -0,0 +1,73 @@
// file: crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
// version: 1
fn canonical_transaction() -> std::option::Option<ksp_store_api::RawTransaction> {
let network = match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let signature = match crate::parse_raw_transaction_signature("1".repeat(64).as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let material = crate::RawTransactionMaterial::binary_base64(
network,
signature,
42,
std::option::Option::None,
"AQID",
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
);
return match crate::canonicalize_raw_transaction(material) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
fn provenance() -> std::option::Option<ksp_store_api::RawAcquisitionProvenance> {
let provider = match ksp_store_api::RawProvenanceCode::new("fixture") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let protocol = match ksp_store_api::RawProvenanceCode::new("fixture.protocol") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let method = match ksp_store_api::RawProvenanceCode::new("fixture.method") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_700_000_000_000) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
return std::option::Option::Some(ksp_store_api::RawAcquisitionProvenance::new(
provider,
protocol,
method,
ksp_store_api::RawAcquisitionOrigin::Import,
received_at,
));
}
#[test]
fn pre_002_acquisition_preserves_producer_key_and_exact_transaction_reference() {
let transaction = match canonical_transaction() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let provenance = match provenance() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let key = ksp_store_api::RawObservationKey::new([7_u8; 32]);
let acquisition = crate::assemble_raw_transaction_acquisition(transaction, key, provenance);
assert_eq!(acquisition.observation().observation_key(), key);
assert_eq!(acquisition.observation().transaction(), acquisition.transaction().reference());
assert_eq!(std::format!("{acquisition:?}"), "RawTransactionAcquisition(..)");
let (transaction, observation) = acquisition.into_parts();
assert_eq!(observation.transaction(), transaction.reference());
return;
}

View File

@@ -0,0 +1,219 @@
// file: crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
// version: 1
fn network() -> std::option::Option<ksp_store_api::RawNetworkId> {
return match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
fn signature() -> std::option::Option<ksp_store_api::RawTransactionSignature> {
let result = crate::parse_raw_transaction_signature("1".repeat(64).as_str());
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
fn material(
block_time: std::option::Option<i64>,
transaction_data: std::string::String,
meta: crate::RawTransactionWireField<serde_json::Value>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
transaction_index: crate::RawTransactionWireField<u32>,
) -> std::option::Option<crate::RawTransactionMaterial> {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let signature = match signature() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
return std::option::Option::Some(crate::RawTransactionMaterial::binary_base64(
network,
signature,
123,
block_time,
transaction_data,
meta,
version,
transaction_index,
));
}
#[test]
fn pre_002_canonical_raw_v1_golden_bytes_and_hash_are_exact() {
let material = match material(
std::option::Option::Some(1_700_000_000),
"AQID".to_owned(),
crate::RawTransactionWireField::Value(serde_json::json!({"z": 1, "a": {"y": true, "x": null}})),
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
crate::RawTransactionWireField::Value(7_u32),
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let transaction = crate::canonicalize_raw_transaction(material);
assert!(transaction.is_ok());
let transaction = match transaction {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let expected = b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":{\"a\":{\"x\":null,\"y\":true},\"z\":1},\"version\":\"legacy\",\"transactionIndex\":7}";
assert_eq!(transaction.payload().bytes(), expected);
assert_eq!(transaction.payload().byte_len(), 112);
assert_eq!(transaction.payload().format_id().as_str(), crate::RAW_TRANSACTION_FORMAT_ID);
assert_eq!(transaction.payload().format_version(), crate::RAW_TRANSACTION_FORMAT_VERSION);
assert_eq!(
transaction.payload().content_hash().as_bytes(),
&[
34, 7, 146, 210, 177, 93, 38, 47, 218, 36, 44, 178, 32, 119, 78, 233, 221, 239, 254, 191, 4, 220, 250, 218, 188, 248, 239, 118, 169, 177, 167, 195
]
);
assert_eq!(transaction.slot(), 123);
assert_eq!(transaction.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_700_000_000_000));
assert_eq!(transaction.reference().network().as_str(), "devnet");
assert_eq!(transaction.reference().signature().as_bytes(), &[0_u8; 64]);
return;
}
#[test]
fn pre_002_wire_omitted_null_and_value_states_remain_distinct() {
let omitted = match material(
std::option::Option::None,
"AQID".to_owned(),
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let omitted = super::canonical_payload_bytes(&omitted);
assert!(omitted.is_ok());
if let std::result::Result::Ok(omitted) = omitted {
assert_eq!(omitted, b"{\"transaction\":[\"AQID\",\"base64\"]}");
}
let nulls = match material(
std::option::Option::None,
"AQID".to_owned(),
crate::RawTransactionWireField::Null,
crate::RawTransactionWireField::Null,
crate::RawTransactionWireField::Null,
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let nulls = super::canonical_payload_bytes(&nulls);
assert!(nulls.is_ok());
if let std::result::Result::Ok(nulls) = nulls {
assert_eq!(nulls, b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":null,\"version\":null,\"transactionIndex\":null}");
}
return;
}
#[test]
fn pre_002_canonical_json_sorts_objects_preserves_arrays_and_escapes_strings() {
let meta = serde_json::json!({
"z": [3, 2, 1],
"quote": "a\"b\\c\n",
"a": {"n": -12.5, "b": false, "a": true}
});
let material = match material(
std::option::Option::None,
"A\"B".to_owned(),
crate::RawTransactionWireField::Value(meta),
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Number(0)),
crate::RawTransactionWireField::Value(u32::MAX),
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let bytes = super::canonical_payload_bytes(&material);
assert!(bytes.is_ok());
if let std::result::Result::Ok(bytes) = bytes {
let expected = b"{\"transaction\":[\"A\\\"B\",\"base64\"],\"meta\":{\"a\":{\"a\":true,\"b\":false,\"n\":-12.5},\"quote\":\"a\\\"b\\\\c\\n\",\"z\":[3,2,1]},\"version\":0,\"transactionIndex\":4294967295}";
assert_eq!(bytes, expected);
}
return;
}
#[test]
fn pre_002_block_time_rejects_negative_overflow_and_store_maximum_overrun() {
for block_time in [-1_i64, i64::MAX, 253_402_300_800_i64] {
let material = match material(
std::option::Option::Some(block_time),
"AQID".to_owned(),
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let result = crate::canonicalize_raw_transaction(material);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID);
}
}
let maximum = match material(
std::option::Option::Some(253_402_300_799_i64),
"AQID".to_owned(),
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let maximum = crate::canonicalize_raw_transaction(maximum);
assert!(maximum.is_ok());
if let std::result::Result::Ok(maximum) = maximum {
assert_eq!(maximum.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(253_402_300_799_000));
}
return;
}
#[test]
fn pre_002_payload_bound_is_enforced_by_common_before_store_construction() {
let material = match material(
std::option::Option::None,
"A".repeat(ksp_store_api::MAX_RAW_PAYLOAD_BYTES),
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
crate::RawTransactionWireField::Omitted,
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let result = crate::canonicalize_raw_transaction(material);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID);
}
return;
}
#[test]
fn pre_002_material_and_wire_debug_do_not_render_transaction_or_meta_material() {
let secret_transaction = "SECRET_TRANSACTION_BODY";
let secret_meta = "SECRET_META_VALUE";
let material = match material(
std::option::Option::None,
secret_transaction.to_owned(),
crate::RawTransactionWireField::Value(serde_json::json!({"secret": secret_meta})),
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
crate::RawTransactionWireField::Value(1_u32),
) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let debug = std::format!("{material:?}");
assert!(!debug.contains(secret_transaction));
assert!(!debug.contains(secret_meta));
assert_eq!(std::format!("{:?}", crate::RawTransactionWireField::Value(secret_meta)), "Value(..)");
return;
}

View File

@@ -0,0 +1,33 @@
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
// version: 1
#[test]
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
let text = "1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES);
let signature = crate::parse_raw_transaction_signature(text.as_str());
assert!(signature.is_ok());
if let std::result::Result::Ok(signature) = signature {
assert_eq!(signature.as_bytes(), &[0_u8; 64]);
}
return;
}
#[test]
fn pre_002_signature_parser_rejects_text_bounds_invalid_base58_and_noncanonical_lengths() {
for text in [
"1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES - 1),
"1".repeat(crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES + 1),
format!("{}0", "1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES - 1)),
"1".repeat(65),
"z".repeat(crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES),
] {
let result = crate::parse_raw_transaction_signature(text.as_str());
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
let debug = std::format!("{error:?}");
assert!(!debug.contains(text.as_str()));
}
}
return;
}

261
deltas/0.3.10/pre.002.md Normal file
View File

@@ -0,0 +1,261 @@
<!-- file: deltas/0.3.10/pre.002.md -->
<!-- version: 1 -->
# Delta `0.3.10-pre.002` — fondation RAW Transaction commune
## Base requise
```text
0.3.10-pre.001
workspace.package.version = 0.3.10-pre.1
```
Le gate opérateur communiqué pour `pre.001` passe `cargo fmt --all`, les audits Rust/Markdown, `cargo check --workspace` et Clippy workspace `--all-targets --all-features -- -D warnings`. Le message ne fournit pas de `cargo test` supplémentaire ; aucun test non communiqué nest déclaré exécuté.
## Objectif
Créer uniquement `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW Transaction v1 : matériau source-neutral, parser de signature, canonical JSON, SHA-256 et assemblage transaction/observation. Aucun Worker et aucune migration du Backfill dans cette tranche.
## Version
Cette tranche est une prerelease non-fix :
```text
workspace.package.version = 0.3.10-pre.2
```
Le `Cargo.toml` racine passe du header `490` au header `491` et ajoute `crates/ksp-raw-transaction-lib` aux membres du workspace.
## Correction normative du plan `pre.001`
Le sizing `pre.001` avait retenu `ksp-store-lib(default-features=false)` pour la common crate. La relecture au moment de matérialiser la frontière révèle une règle plus spécifique :
```text
DEP-PIPE-006
Le pipeline raw ingestion peut dépendre des modèles homogènes de
ksp-onchain-transport-lib et de ksp-store-api, mais pas de ksp-store-lib.
```
La dépendance est donc corrigée vers `ksp-store-api`. Cette correction ne change aucun invariant RAW ; elle retire au contraire la façade/runtime Store de la lower-layer réutilisable.
## Dépendances exactes
```text
ksp-raw-transaction-lib
-> ksp-core-lib
-> ksp-store-api
-> serde_json
-> sha2
```
Absents de la crate :
```text
ksp-store-lib
ksp-store-postgres-lib
ksp-onchain-transport-lib
ksp-config-lib
ksp-job-api
ksp-job-backfill-lib
ksp-worker-api
Tokio / futures
provider SDK
```
## Surface publique
```text
RAW_TRANSACTION_FORMAT_ID = ksp.solana.raw_transaction
RAW_TRANSACTION_FORMAT_VERSION = 1
MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 64
MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 88
RawTransactionMaterial
RawTransactionWireField<T>
RawTransactionVersion
RawTransactionAcquisition
parse_raw_transaction_signature(...)
canonicalize_raw_transaction(...)
assemble_raw_transaction_acquisition(...)
ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID
ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID
ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID
```
Tous les modules restent privés et les éléments consommables sont réexportés au crate-root.
## Contrat matérialisé
### Signature
Le parser reçoit un texte Base58 borné de `64..=88` octets et naccepte que les représentations décodant exactement vers `64` octets. Aucun Solana SDK ou codec externe nest ajouté.
Les erreurs ne recopient jamais la signature hostile ; seules les longueurs numériques sûres peuvent apparaître en contexte.
### Matériau source-neutral
`RawTransactionMaterial::binary_base64(...)` reçoit :
```text
network
signature canonique 64 bytes
slot u64
block_time Option<i64>
transaction data binaire Base64 déjà obtenu de la source
meta omitted/null/value
version omitted/null/value (legacy ou u8)
transactionIndex omitted/null/value u32
```
Aucun type Transport/provider nentre dans la common crate. La représentation interne peut être étendue ultérieurement pour Yellowstone uniquement lorsquune parité RAW v1 sera démontrée.
### Canonicalisation RAW v1
La sortie conserve exactement :
```text
ordre top-level : transaction -> meta -> version -> transactionIndex
transaction = [data, "base64"]
tri récursif des clés dobjets JSON
ordre des arrays conservé
omitted != null != value
block_time hors payload, converti en RawTimestamp
SHA-256 sur les bytes canoniques exacts
format id/version gelés
```
Golden verrouillé :
```text
bytes = {"transaction":["AQID","base64"],"meta":{"a":{"x":null,"y":true},"z":1},"version":"legacy","transactionIndex":7}
byte_len = 112
sha256 = 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
```
### Observation producer-owned
`assemble_raw_transaction_acquisition(...)` reçoit la `RawObservationKey` déjà choisie par le producteur et construit lobservation sur la référence exacte de la transaction. Aucun domaine de clé Backfill ou Worker nest introduit dans common.
## Tests ajoutés
Tests unitaires :
```text
signature exacte 64 bytes
signature bornes / caractère Base58 invalide / overflow / longueur décodée non canonique
golden bytes + hash RAW v1
omitted/null/value
golden JSON récursif / arrays / escaping / nombres
block_time négatif / overflow / maximum
borne payload
Debug material/wire redacted
assemblage acquisition + observation key producer-owned
```
Tests dintégration :
```text
dependency boundary exacte
absence runtime/Transport/Config/Job/Worker/backend
consommation crate-root externe
assemblage avec modèles Store API
inventaire modules production
absence de contrat Worker/Backfill/Transport dans la façade
error codes stables
signature hostile non recopiée
```
## Fichiers ajoutés
```text
crates/ksp-raw-transaction-lib/Cargo.toml
crates/ksp-raw-transaction-lib/src/acquisition.rs
crates/ksp-raw-transaction-lib/src/canonical.rs
crates/ksp-raw-transaction-lib/src/error.rs
crates/ksp-raw-transaction-lib/src/lib.rs
crates/ksp-raw-transaction-lib/src/signature.rs
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
crates/ksp-raw-transaction-lib/tests/public_api.rs
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
deltas/0.3.10/pre.002.md
```
## Fichiers modifiés
```text
Cargo.toml
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
```
## Fichiers supprimés
Aucun.
## Documentation volontairement différée
`README.md` et `USAGE.md` de la nouvelle crate ne sont pas finalisés dans cette tranche de fondation. La réconciliation version-neutral reste réservée au couloir documentaire `pre.015`, après parité cross-source, migration Backfill et freeze fonctionnelle.
## Validations exécutées dans lenvironnement dassemblage
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
Markdown table audit: clean (339 table(s), 754 file(s))
contrôle TOML : 20 membres workspace, nouvelle crate présente
contrôle manifest common : dépendances exactes core/store-api/serde_json/sha2, aucune feature/dev/build dependency
```
Lenvironnement dassemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo de `pre.002` nest donc déclaré PASS localement.
## Gate opérateur demandé
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-raw-transaction-lib
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-raw-transaction-lib -e features
```
## Validations non exécutées localement
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-raw-transaction-lib
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-raw-transaction-lib -e features
smokes live : non pertinents pour cette tranche purement déterministe
```
## Décisions prises
```text
RAW v1 reste unique et inchangé
ksp-store-api remplace ksp-store-lib dans common pour conformité DEP-PIPE-006
RawObservationKey reste producer-owned
aucun type Transport dans common
aucun async/runtime dans common
aucune migration Backfill avant pre.003
aucun Worker avant pre.007 selon le plan recalibré
```
## Questions ouvertes
Aucune question ne bloque `pre.003` après un gate opérateur vert. Les projections structurées WS/Helius/Yellowstone restent volontairement différées jusquaux tranches de parité prévues ; elles ne doivent pas modifier RAW v1 par anticipation.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md --> <!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Plan v0.3.10 — RAW Transaction commune + Worker dingestion multi-source # Plan v0.3.10 — RAW Transaction commune + Worker dingestion multi-source
@@ -19,7 +19,7 @@ sources live
-> matériau transaction complet ou discovery/hydration -> matériau transaction complet ou discovery/hydration
-> canonicalisation RAW v1 unique -> canonicalisation RAW v1 unique
-> RawTransaction + RawTransactionObservation -> RawTransaction + RawTransactionObservation
-> ksp-store-lib -> persistence par le producteur concret via ksp-store-lib
``` ```
Le Worker et le Job Backfill sont deux producteurs autonomes du même modèle durable. Aucun appel, lifecycle, orchestration, checkpoint ou contrat fonctionnel ne doit relier les deux. Le Worker et le Job Backfill sont deux producteurs autonomes du même modèle durable. Aucun appel, lifecycle, orchestration, checkpoint ou contrat fonctionnel ne doit relier les deux.
@@ -161,7 +161,7 @@ Les décisions de fermeture du gate sont :
RAW v1 reste unique ; aucun RAW v2. RAW v1 reste unique ; aucun RAW v2.
mainnet reste lidentité KSP canonique ; mainnet-beta reste alias externe/legacy. mainnet reste lidentité KSP canonique ; mainnet-beta reste alias externe/legacy.
ksp-raw-transaction-lib devient lunique owner de la canonicalisation RAW v1. ksp-raw-transaction-lib devient lunique owner de la canonicalisation RAW v1.
La common crate dépend de Store façade, jamais de Transport/Config/Job/Worker. La common crate dépend des modèles `ksp-store-api` conformément à `DEP-PIPE-006`, jamais de la façade runtime Store ni de Transport/Config/Job/Worker.
RawObservationKey reste producer-owned comme le contrat Store lindique déjà. RawObservationKey reste producer-owned comme le contrat Store lindique déjà.
Le Backfill conserve son observation-key Job-owned et ses métadonnées de campagne. Le Backfill conserve son observation-key Job-owned et ses métadonnées de campagne.
Le Worker possède son runtime concret, ses source IDs, ses observation keys et sa continuité de run. Le Worker possède son runtime concret, ses source IDs, ses observation keys et sa continuité de run.
@@ -290,7 +290,7 @@ Cible retenue :
```text ```text
ksp-raw-transaction-lib ksp-raw-transaction-lib
-> ksp-core-lib -> ksp-core-lib
-> ksp-store-lib default-features=false -> ksp-store-api
-> serde_json -> serde_json
-> sha2 -> sha2
``` ```
@@ -298,13 +298,14 @@ ksp-raw-transaction-lib
Justification : Justification :
- `ksp-core-lib` est nécessaire aux codes/erreurs KSP propres à la canonicalisation ; - `ksp-core-lib` est nécessaire aux codes/erreurs KSP propres à la canonicalisation ;
- `ksp-store-lib` fournit les reexports approuvés de `RawTransaction`, `RawPayload`, provenance et primitives ; - `ksp-store-api` fournit directement les modèles canoniques `RawTransaction`, `RawPayload`, provenance et primitives, conformément à `DEP-PIPE-006` ;
- `serde_json` reste requis par RAW v1 pour canonicaliser `meta` et préserver les états wire ; - `serde_json` reste requis par RAW v1 pour canonicaliser `meta` et préserver les états wire ;
- `sha2` reste requis pour le digest canonique exact. - `sha2` reste requis pour le digest canonique exact.
Interdits : Interdits :
```text ```text
ksp-store-lib
ksp-onchain-transport-lib ksp-onchain-transport-lib
ksp-config-lib ksp-config-lib
ksp-job-api ksp-job-api
@@ -979,33 +980,29 @@ Une preuve sur `transactions` ne valide pas automatiquement `blocks`.
## 16. Graphe de dépendances cible ## 16. Graphe de dépendances cible
```text ```text
ksp-core-lib ksp-raw-transaction-lib -> ksp-core-lib
^ ksp-raw-transaction-lib -> ksp-store-api -> ksp-core-lib
|
ksp-store-api <- ksp-store-lib <- ksp-raw-transaction-lib
^ ^
| |
| +-------+------------------+
| | |
| ksp-job-backfill-lib ksp-worker-raw-transaction-ingest-lib
| ^ ^
| | |
+----------+--------------------------+
|
ksp-onchain-transport-lib
ksp-worker-api --------------------------> ksp-worker-raw-transaction-ingest-lib ksp-job-backfill-lib -> ksp-raw-transaction-lib
ksp-logging-lib -------------------------> ksp-worker-raw-transaction-ingest-lib ksp-job-backfill-lib -> ksp-store-lib -> ksp-store-api
ksp-job-backfill-lib -> ksp-onchain-transport-lib
ksp-job-backfill-lib -> ksp-job-api
ksp-worker-raw-transaction-ingest-lib -> ksp-raw-transaction-lib
ksp-worker-raw-transaction-ingest-lib -> ksp-store-lib -> ksp-store-api
ksp-worker-raw-transaction-ingest-lib -> ksp-onchain-transport-lib
ksp-worker-raw-transaction-ingest-lib -> ksp-worker-api
ksp-worker-raw-transaction-ingest-lib -> ksp-logging-lib
``` ```
Lecture des flèches du schéma : les consumers dépendent des lower layers ; aucun lower layer ne dépend dun consumer. Le dessin ne crée pas dedge `ksp-store-lib -> common` : cest bien `common -> ksp-store-lib`. Lecture des flèches du schéma : les consumers dépendent des lower layers ; aucun lower layer ne dépend dun consumer. `ksp-raw-transaction-lib` dépend directement de `ksp-store-api` comme pipeline RAW réutilisable ; `ksp-store-lib` reste la façade runtime utilisée séparément par les producteurs concrets pour la persistence.
Edges exacts attendus : Edges exacts attendus :
```text ```text
ksp-raw-transaction-lib ksp-raw-transaction-lib
-> ksp-core-lib -> ksp-core-lib
-> ksp-store-lib default-features=false -> ksp-store-api
-> serde_json -> serde_json
-> sha2 -> sha2
@@ -1037,7 +1034,11 @@ Présente tranche : archive/règles, common extraction, capability matrix, fraî
### `pre.002` — common RAW foundation ### `pre.002` — common RAW foundation
Créer `ksp-raw-transaction-lib`, types source-neutral minimaux, signature parser, canonical JSON, SHA-256 et golden RAW v1 exact. Aucun Worker. **Statut : réalisé.**
`ksp-raw-transaction-lib` matérialise les types source-neutral minimaux, le parser Base58 strict vers 64 octets, la canonicalisation JSON RAW v1, SHA-256 et lassemblage transaction + observation producer-owned. Le golden `112` bytes / `220792...a7c3` est verrouillé par test. Aucun Worker ni migration Backfill nest ouvert.
La matérialisation a révélé une correction normative du sizing `pre.001` : `DEP-PIPE-006` impose `ksp-store-api` au pipeline RAW et interdit `ksp-store-lib`. Le graphe et la section 5.4 sont donc corrigés dans cette tranche sans changer le contrat fonctionnel RAW.
### `pre.003` — migration Backfill vers common ### `pre.003` — migration Backfill vers common

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md --> <!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Validation v0.3.10 — RAW Transaction commune + Worker dingestion # Validation v0.3.10 — RAW Transaction commune + Worker dingestion
@@ -80,6 +80,31 @@ Tauri build : NON EXÉCUTÉ ici
Aucun de ces gates nest déclaré PASS par cette session. Le journal opérateur fourni pour la stable `0.3.9` montre un gate complet antérieur ; il reste une preuve externe de la base stable, pas une exécution `pre.001` locale. Aucun de ces gates nest déclaré PASS par cette session. Le journal opérateur fourni pour la stable `0.3.9` montre un gate complet antérieur ; il reste une preuve externe de la base stable, pas une exécution `pre.001` locale.
### 2.4 Gate opérateur après livraison de `pre.001`
Le 6 septembre 2026, lopérateur a exécuté sur `workspace.package.version = 0.3.10-pre.1` :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
```
Résultats communiqués :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (339 table(s), 753 file(s))
cargo check --workspace : terminé sans erreur
cargo clippy --workspace --all-targets --all-features -- -D warnings : terminé sans erreur
```
Ce gate opérateur ferme les contrôles demandés pour passer à `pre.002`. Aucun `cargo test` de `pre.001` na été fourni dans ce message et il nest pas inventé.
## 3. Preuves de conception fermées en `pre.001` ## 3. Preuves de conception fermées en `pre.001`
### 3.1 Common RAW ### 3.1 Common RAW
@@ -92,7 +117,7 @@ format_version courant = 1
RawObservationKey est explicitement producer-owned dans Store RawObservationKey est explicitement producer-owned dans Store
RawTransaction identity = network + signature RawTransaction identity = network + signature
RawTransactionObservation est séparée de lentité RawTransactionObservation est séparée de lentité
ksp-store-lib reexporte les primitives nécessaires `DEP-PIPE-006` impose les modèles `ksp-store-api` à la lower-layer RAW réutilisable ; la décision `ksp-store-lib` de `pre.001` est corrigée en `pre.002`
Backfill conversion mélange actuellement canonicalisation commune et metadata Job-owned Backfill conversion mélange actuellement canonicalisation commune et metadata Job-owned
``` ```
@@ -352,3 +377,104 @@ pre.015 final docs reconciliation
pre.016 publication preparation pre.016 publication preparation
rel.001 stable publication rel.001 stable publication
``` ```
## 11. Gate `pre.002` — common RAW foundation
État : **matérialisé ; gate Cargo opérateur requis**.
### 11.1 Crate et frontière
```text
crate = ksp-raw-transaction-lib
workspace.package.version = 0.3.10-pre.2
runtime async = aucun
Transport / Config / Job / Worker = absents
backend Store = absent
```
La dépendance exacte retenue après contrôle normatif est :
```text
ksp-raw-transaction-lib
-> ksp-core-lib
-> ksp-store-api
-> serde_json
-> sha2
```
Cette correction remplace la proposition `ksp-store-lib(default-features=false)` de `pre.001` car `DEP-PIPE-006` est plus spécifique : le pipeline RAW partagé peut dépendre de `ksp-store-api`, pas de `ksp-store-lib`.
### 11.2 Surface matérialisée
```text
RAW_TRANSACTION_FORMAT_ID = ksp.solana.raw_transaction
RAW_TRANSACTION_FORMAT_VERSION = 1
MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 64
MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 88
RawTransactionMaterial
RawTransactionWireField<T>
RawTransactionVersion
RawTransactionAcquisition
parse_raw_transaction_signature(...)
canonicalize_raw_transaction(...)
assemble_raw_transaction_acquisition(...)
ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID
ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID
ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID
```
`RawObservationKey` reste fournie par le producteur. La common crate ne contient aucun domaine de clé Backfill/Worker.
### 11.3 Preuves déterministes ajoutées
```text
signature Base58 : bornes texte + alphabet + overflow + exactement 64 octets
RAW v1 golden : 112 bytes exacts
RAW v1 SHA-256 : 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
meta/version/index : omitted != null != value
JSON : clés objet triées récursivement, arrays conservés, strings échappées, nombres conservés
block_time : négatif/overflow/max Store
payload : borne MAX_RAW_PAYLOAD_BYTES
Debug/error : aucune signature, transaction body ou meta hostile recopiée
acquisition : observation key producer-owned et référence exactement égale à la transaction
public API : consommation par crate-root
firewall : absence Store runtime/Transport/Config/Job/Worker/backend
```
### 11.4 Validation locale dassemblage
Exécuté dans lenvironnement dassemblage :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
Markdown table audit: clean (339 table(s), 754 file(s))
manifest TOML : 20 membres workspace, ksp-raw-transaction-lib présent
manifest common : 4 dépendances exactes, aucune feature/dev/build dependency
```
`cargo`, `rustc` et `rustfmt` ne sont pas présents dans cet environnement. Les gates Cargo de la nouvelle crate restent donc **NON EXÉCUTÉS LOCALEMENT**.
### 11.5 Gate opérateur demandé
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-raw-transaction-lib
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-raw-transaction-lib -e features
```
`pre.003` ne doit migrer le Backfill quaprès ce gate vert.