v0.3.10-pre.002
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
17
crates/ksp-raw-transaction-lib/Cargo.toml
Normal file
17
crates/ksp-raw-transaction-lib/Cargo.toml
Normal 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
|
||||||
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal file
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal 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;
|
||||||
305
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal file
305
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal 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;
|
||||||
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal file
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal 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");
|
||||||
|
}
|
||||||
54
crates/ksp-raw-transaction-lib/src/lib.rs
Normal file
54
crates/ksp-raw-transaction-lib/src/lib.rs
Normal 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;
|
||||||
67
crates/ksp-raw-transaction-lib/src/signature.rs
Normal file
67
crates/ksp-raw-transaction-lib/src/signature.rs
Normal 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;
|
||||||
52
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
Normal file
52
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
Normal 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;
|
||||||
|
}
|
||||||
77
crates/ksp-raw-transaction-lib/tests/public_api.rs
Normal file
77
crates/ksp-raw-transaction-lib/tests/public_api.rs
Normal 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;
|
||||||
|
}
|
||||||
37
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
Normal file
37
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
Normal 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;
|
||||||
|
}
|
||||||
26
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
Normal file
26
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
Normal 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;
|
||||||
|
}
|
||||||
73
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
Normal file
73
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
Normal 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;
|
||||||
|
}
|
||||||
219
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
Normal file
219
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
Normal 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;
|
||||||
|
}
|
||||||
33
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
Normal file
33
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
Normal 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
261
deltas/0.3.10/pre.002.md
Normal 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é n’est 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 n’accepte que les représentations décodant exactement vers `64` octets. Aucun Solana SDK ou codec externe n’est 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 n’entre dans la common crate. La représentation interne peut être étendue ultérieurement pour Yellowstone uniquement lorsqu’une 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 d’objets 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 l’observation sur la référence exacte de la transaction. Aucun domaine de clé Backfill ou Worker n’est 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 d’inté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 l’environnement d’assemblage
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
L’environnement d’assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo de `pre.002` n’est 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 jusqu’aux tranches de parité prévues ; elles ne doivent pas modifier RAW v1 par anticipation.
|
||||||
@@ -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 d’ingestion multi-source
|
# Plan v0.3.10 — RAW Transaction commune + Worker d’ingestion 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 l’identité KSP canonique ; mainnet-beta reste alias externe/legacy.
|
mainnet reste l’identité KSP canonique ; mainnet-beta reste alias externe/legacy.
|
||||||
ksp-raw-transaction-lib devient l’unique owner de la canonicalisation RAW v1.
|
ksp-raw-transaction-lib devient l’unique 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 l’indique déjà.
|
RawObservationKey reste producer-owned comme le contrat Store l’indique 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 d’un consumer. Le dessin ne crée pas d’edge `ksp-store-lib -> common` : c’est 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 d’un 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 l’assemblage transaction + observation producer-owned. Le golden `112` bytes / `220792...a7c3` est verrouillé par test. Aucun Worker ni migration Backfill n’est 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
|
||||||
|
|
||||||
|
|||||||
@@ -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 d’ingestion
|
# Validation v0.3.10 — RAW Transaction commune + Worker d’ingestion
|
||||||
|
|
||||||
@@ -80,6 +80,31 @@ Tauri build : NON EXÉCUTÉ ici
|
|||||||
|
|
||||||
Aucun de ces gates n’est 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 n’est 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, l’opé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` n’a été fourni dans ce message et il n’est 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 l’entité
|
RawTransactionObservation est séparée de l’entité
|
||||||
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 d’assemblage
|
||||||
|
|
||||||
|
Exécuté dans l’environnement d’assemblage :
|
||||||
|
|
||||||
|
```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 qu’après ce gate vert.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user