v0.3.1-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 324
|
||||
# version: 325
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-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-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1-pre.2"
|
||||
version = "0.3.1-pre.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
9
crates/ksp-store-api/src/error.rs
Normal file
9
crates/ksp-store-api/src/error.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// file: crates/ksp-store-api/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
|
||||
pub const ERROR_CODE_RAW_MODEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_model_invalid");
|
||||
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
|
||||
pub const ERROR_CODE_RAW_PAYLOAD_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_payload_invalid");
|
||||
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
|
||||
pub const ERROR_CODE_RAW_PROVENANCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_provenance_invalid");
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-api/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -17,8 +17,49 @@
|
||||
//! dispatch remain outside this crate.
|
||||
|
||||
mod capability;
|
||||
mod error;
|
||||
mod model;
|
||||
|
||||
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
|
||||
pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;
|
||||
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
|
||||
pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID;
|
||||
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
|
||||
pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID;
|
||||
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
|
||||
pub use self::model::raw_primitives::MAX_RAW_CODE_BYTES;
|
||||
/// Maximum KSP-owned canonical RAW payload admitted by the Store API.
|
||||
pub use self::model::raw_primitives::MAX_RAW_PAYLOAD_BYTES;
|
||||
/// Maximum source-wire payload size recorded as acquisition metadata.
|
||||
pub use self::model::raw_primitives::MAX_RAW_SOURCE_PAYLOAD_BYTES;
|
||||
/// Maximum supported Unix millisecond timestamp.
|
||||
pub use self::model::raw_primitives::MAX_RAW_UNIX_MILLIS;
|
||||
/// Origin category describing why one acquisition was performed.
|
||||
pub use self::model::raw_primitives::RawAcquisitionOrigin;
|
||||
/// Safe source-independent acquisition provenance attached to one persisted observation.
|
||||
pub use self::model::raw_primitives::RawAcquisitionProvenance;
|
||||
/// Fixed-size digest identifying canonical or source bytes without retaining them.
|
||||
pub use self::model::raw_primitives::RawContentHash;
|
||||
/// Bounded identifier of one KSP-owned source-independent RAW persistence format.
|
||||
pub use self::model::raw_primitives::RawFormatId;
|
||||
/// Bounded logical network/cluster identifier used in backend-independent Store identities.
|
||||
pub use self::model::raw_primitives::RawNetworkId;
|
||||
/// Stable deterministic idempotence key for one persisted acquisition observation.
|
||||
pub use self::model::raw_primitives::RawObservationKey;
|
||||
/// Bounded source-independent KSP RAW persistence payload.
|
||||
pub use self::model::raw_primitives::RawPayload;
|
||||
/// Bounded logical code used by acquisition provenance fields.
|
||||
pub use self::model::raw_primitives::RawProvenanceCode;
|
||||
/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch.
|
||||
pub use self::model::raw_primitives::RawTimestamp;
|
||||
/// Canonical 64-byte Solana transaction signature used by Store identities.
|
||||
pub use self::model::raw_primitives::RawTransactionSignature;
|
||||
/// Canonical source-independent N1 RAW transaction persisted by Store backends.
|
||||
pub use self::model::raw_transaction::RawTransaction;
|
||||
/// Persistable acquisition observation linked to one canonical RAW transaction.
|
||||
pub use self::model::raw_transaction::RawTransactionObservation;
|
||||
/// Durable backend-independent identity of one canonical RAW transaction.
|
||||
pub use self::model::raw_transaction::RawTransactionReference;
|
||||
/// Common KSP error type used by Store-facing contracts.
|
||||
pub use ksp_core_lib::Error;
|
||||
/// Stable structured code identifying a KSP error category and condition.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// file: crates/ksp-store-api/src/model.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Private home for persistent Store models.
|
||||
//!
|
||||
//! The current release is N1 RAW-only. Future N2 STRUCTURAL models are a
|
||||
//! separate data layer and are not introduced by this scaffold. Persistent
|
||||
//! models and backend capabilities remain separate even when one capability
|
||||
//! operates on one or more models.
|
||||
//! separate data layer and are not introduced here. Persistent models and
|
||||
//! backend capabilities remain separate even when one capability operates on
|
||||
//! one or more models.
|
||||
|
||||
pub(crate) mod raw_primitives;
|
||||
pub(crate) mod raw_transaction;
|
||||
|
||||
449
crates/ksp-store-api/src/model/raw_primitives.rs
Normal file
449
crates/ksp-store-api/src/model/raw_primitives.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
// file: crates/ksp-store-api/src/model/raw_primitives.rs
|
||||
// version: 1
|
||||
|
||||
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
|
||||
pub const MAX_RAW_CODE_BYTES: usize = 128;
|
||||
/// Maximum KSP-owned canonical RAW payload admitted by the Store API.
|
||||
///
|
||||
/// This is a Store admission guard, not a Solana protocol-size claim.
|
||||
pub const MAX_RAW_PAYLOAD_BYTES: usize = 16 * 1024 * 1024;
|
||||
/// Maximum source-wire payload size recorded as acquisition metadata.
|
||||
///
|
||||
/// The source payload itself is never retained by this metadata field.
|
||||
pub const MAX_RAW_SOURCE_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024;
|
||||
/// Maximum supported Unix millisecond timestamp (`9999-12-31T23:59:59.999Z`).
|
||||
pub const MAX_RAW_UNIX_MILLIS: u64 = 253_402_300_799_999;
|
||||
|
||||
/// Fixed-size digest identifying canonical or source bytes without retaining them.
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct RawContentHash([u8; 32]);
|
||||
|
||||
impl RawContentHash {
|
||||
/// Creates one opaque 32-byte KSP content digest.
|
||||
#[must_use]
|
||||
pub const fn new(bytes: [u8; 32]) -> Self {
|
||||
return Self(bytes);
|
||||
}
|
||||
|
||||
/// Returns the exact digest bytes.
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; 32] {
|
||||
return &self.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RawContentHash {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("RawContentHash(..)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable deterministic idempotence key for one persisted acquisition observation.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RawObservationKey([u8; 32]);
|
||||
|
||||
impl RawObservationKey {
|
||||
/// Creates one producer-owned deterministic observation key.
|
||||
#[must_use]
|
||||
pub const fn new(bytes: [u8; 32]) -> Self {
|
||||
return Self(bytes);
|
||||
}
|
||||
|
||||
/// Returns the exact observation-key bytes.
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; 32] {
|
||||
return &self.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical 64-byte Solana transaction signature used by Store identities.
|
||||
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RawTransactionSignature([u8; 64]);
|
||||
|
||||
impl RawTransactionSignature {
|
||||
/// Creates one canonical signature from already-decoded Solana signature bytes.
|
||||
#[must_use]
|
||||
pub const fn new(bytes: [u8; 64]) -> Self {
|
||||
return Self(bytes);
|
||||
}
|
||||
|
||||
/// Returns the exact signature bytes.
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; 64] {
|
||||
return &self.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RawTransactionSignature {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("RawTransactionSignature(..)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded logical network/cluster identifier used in backend-independent Store identities.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RawNetworkId(std::string::String);
|
||||
|
||||
impl RawNetworkId {
|
||||
/// Creates one safe non-empty network/cluster identifier.
|
||||
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let value = value.into();
|
||||
if !valid_raw_code(value.as_str()) {
|
||||
return std::result::Result::Err(raw_model_error("network"));
|
||||
}
|
||||
return std::result::Result::Ok(Self(value));
|
||||
}
|
||||
|
||||
/// Returns the network/cluster identifier.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded logical code used by acquisition provenance fields.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RawProvenanceCode(std::string::String);
|
||||
|
||||
impl RawProvenanceCode {
|
||||
/// Creates one safe non-empty provenance code.
|
||||
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let value = value.into();
|
||||
if !valid_raw_code(value.as_str()) {
|
||||
return std::result::Result::Err(raw_provenance_error("code"));
|
||||
}
|
||||
return std::result::Result::Ok(Self(value));
|
||||
}
|
||||
|
||||
/// Returns the validated provenance code.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded identifier of one KSP-owned source-independent RAW persistence format.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RawFormatId(std::string::String);
|
||||
|
||||
impl RawFormatId {
|
||||
/// Creates one safe non-empty RAW format identifier.
|
||||
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let value = value.into();
|
||||
if !valid_raw_code(value.as_str()) {
|
||||
return std::result::Result::Err(raw_payload_error("format_id"));
|
||||
}
|
||||
return std::result::Result::Ok(Self(value));
|
||||
}
|
||||
|
||||
/// Returns the KSP-owned RAW format identifier.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RawTimestamp {
|
||||
unix_millis: u64,
|
||||
}
|
||||
|
||||
impl RawTimestamp {
|
||||
/// Creates a bounded UTC timestamp from Unix milliseconds.
|
||||
pub fn from_unix_millis(unix_millis: u64) -> ksp_core_lib::Result<Self> {
|
||||
if unix_millis > crate::MAX_RAW_UNIX_MILLIS {
|
||||
return std::result::Result::Err(raw_model_error("timestamp"));
|
||||
}
|
||||
return std::result::Result::Ok(Self { unix_millis });
|
||||
}
|
||||
|
||||
/// Returns whole milliseconds since Unix epoch.
|
||||
#[must_use]
|
||||
pub const fn unix_millis(&self) -> u64 {
|
||||
return self.unix_millis;
|
||||
}
|
||||
}
|
||||
|
||||
/// Origin category describing why one acquisition was performed.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RawAcquisitionOrigin {
|
||||
/// Historical acquisition intended to fill missing durable data.
|
||||
Backfill,
|
||||
/// Explicit import from a non-live source controlled by the caller.
|
||||
Import,
|
||||
/// Live acquisition from a currently active transport/session.
|
||||
Live,
|
||||
/// Explicit repair or reconciliation of previously known data.
|
||||
Repair,
|
||||
/// Explicit replay of an already-known source or archived acquisition.
|
||||
Replay,
|
||||
}
|
||||
|
||||
/// Safe source-independent acquisition provenance attached to one persisted observation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RawAcquisitionProvenance {
|
||||
acquisition_method: crate::RawProvenanceCode,
|
||||
capture_session_id: std::option::Option<crate::RawProvenanceCode>,
|
||||
commitment: std::option::Option<crate::RawProvenanceCode>,
|
||||
endpoint_id: std::option::Option<crate::RawProvenanceCode>,
|
||||
filter_id: std::option::Option<crate::RawProvenanceCode>,
|
||||
observed_at: std::option::Option<crate::RawTimestamp>,
|
||||
origin: crate::RawAcquisitionOrigin,
|
||||
protocol: crate::RawProvenanceCode,
|
||||
provider: crate::RawProvenanceCode,
|
||||
received_at: crate::RawTimestamp,
|
||||
source_payload_hash: std::option::Option<crate::RawContentHash>,
|
||||
source_payload_size_bytes: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl RawAcquisitionProvenance {
|
||||
/// Creates one successful acquisition provenance record with only mandatory safe metadata.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
provider: crate::RawProvenanceCode,
|
||||
protocol: crate::RawProvenanceCode,
|
||||
acquisition_method: crate::RawProvenanceCode,
|
||||
origin: crate::RawAcquisitionOrigin,
|
||||
received_at: crate::RawTimestamp,
|
||||
) -> Self {
|
||||
return Self {
|
||||
acquisition_method,
|
||||
capture_session_id: std::option::Option::None,
|
||||
commitment: std::option::Option::None,
|
||||
endpoint_id: std::option::Option::None,
|
||||
filter_id: std::option::Option::None,
|
||||
observed_at: std::option::Option::None,
|
||||
origin,
|
||||
protocol,
|
||||
provider,
|
||||
received_at,
|
||||
source_payload_hash: std::option::Option::None,
|
||||
source_payload_size_bytes: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Attaches one safe logical capture/session identifier.
|
||||
#[must_use]
|
||||
pub fn with_capture_session_id(mut self, value: crate::RawProvenanceCode) -> Self {
|
||||
self.capture_session_id = std::option::Option::Some(value);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Attaches one safe commitment code captured at acquisition.
|
||||
#[must_use]
|
||||
pub fn with_commitment(mut self, value: crate::RawProvenanceCode) -> Self {
|
||||
self.commitment = std::option::Option::Some(value);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Attaches one Config-owned logical endpoint identifier.
|
||||
#[must_use]
|
||||
pub fn with_endpoint_id(mut self, value: crate::RawProvenanceCode) -> Self {
|
||||
self.endpoint_id = std::option::Option::Some(value);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Attaches one safe logical filter identifier.
|
||||
#[must_use]
|
||||
pub fn with_filter_id(mut self, value: crate::RawProvenanceCode) -> Self {
|
||||
self.filter_id = std::option::Option::Some(value);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Attaches the source observation timestamp when it does not follow local receipt.
|
||||
pub fn try_with_observed_at(mut self, value: crate::RawTimestamp) -> ksp_core_lib::Result<Self> {
|
||||
if value > self.received_at {
|
||||
return std::result::Result::Err(raw_provenance_error("observed_at"));
|
||||
}
|
||||
self.observed_at = std::option::Option::Some(value);
|
||||
return std::result::Result::Ok(self);
|
||||
}
|
||||
|
||||
/// Attaches the digest of source-specific bytes without retaining those bytes.
|
||||
#[must_use]
|
||||
pub fn with_source_payload_hash(mut self, value: crate::RawContentHash) -> Self {
|
||||
self.source_payload_hash = std::option::Option::Some(value);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Attaches the bounded source-wire payload size.
|
||||
pub fn try_with_source_payload_size_bytes(mut self, value: u64) -> ksp_core_lib::Result<Self> {
|
||||
if value > crate::MAX_RAW_SOURCE_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(raw_provenance_error("source_payload_size_bytes"));
|
||||
}
|
||||
self.source_payload_size_bytes = std::option::Option::Some(value);
|
||||
return std::result::Result::Ok(self);
|
||||
}
|
||||
|
||||
/// Returns the logical acquisition method code.
|
||||
#[must_use]
|
||||
pub const fn acquisition_method(&self) -> &crate::RawProvenanceCode {
|
||||
return &self.acquisition_method;
|
||||
}
|
||||
|
||||
/// Returns the optional logical capture/session identifier.
|
||||
#[must_use]
|
||||
pub fn capture_session_id(&self) -> std::option::Option<&crate::RawProvenanceCode> {
|
||||
return self.capture_session_id.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the optional commitment code captured at acquisition.
|
||||
#[must_use]
|
||||
pub fn commitment(&self) -> std::option::Option<&crate::RawProvenanceCode> {
|
||||
return self.commitment.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the optional Config-owned logical endpoint identifier.
|
||||
#[must_use]
|
||||
pub fn endpoint_id(&self) -> std::option::Option<&crate::RawProvenanceCode> {
|
||||
return self.endpoint_id.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the optional logical filter identifier.
|
||||
#[must_use]
|
||||
pub fn filter_id(&self) -> std::option::Option<&crate::RawProvenanceCode> {
|
||||
return self.filter_id.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the optional source observation timestamp when the source supplies one.
|
||||
#[must_use]
|
||||
pub const fn observed_at(&self) -> std::option::Option<crate::RawTimestamp> {
|
||||
return self.observed_at;
|
||||
}
|
||||
|
||||
/// Returns the acquisition origin category.
|
||||
#[must_use]
|
||||
pub const fn origin(&self) -> crate::RawAcquisitionOrigin {
|
||||
return self.origin;
|
||||
}
|
||||
|
||||
/// Returns the logical transport/protocol code.
|
||||
#[must_use]
|
||||
pub fn protocol(&self) -> &crate::RawProvenanceCode {
|
||||
return &self.protocol;
|
||||
}
|
||||
|
||||
/// Returns the safe provider code.
|
||||
#[must_use]
|
||||
pub fn provider(&self) -> &crate::RawProvenanceCode {
|
||||
return &self.provider;
|
||||
}
|
||||
|
||||
/// Returns the local receipt timestamp.
|
||||
#[must_use]
|
||||
pub const fn received_at(&self) -> crate::RawTimestamp {
|
||||
return self.received_at;
|
||||
}
|
||||
|
||||
/// Returns the optional digest of source-specific bytes without retaining those bytes.
|
||||
#[must_use]
|
||||
pub const fn source_payload_hash(&self) -> std::option::Option<crate::RawContentHash> {
|
||||
return self.source_payload_hash;
|
||||
}
|
||||
|
||||
/// Returns the optional source-wire payload size.
|
||||
#[must_use]
|
||||
pub const fn source_payload_size_bytes(&self) -> std::option::Option<u64> {
|
||||
return self.source_payload_size_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded source-independent KSP RAW persistence payload.
|
||||
pub struct RawPayload {
|
||||
bytes: std::boxed::Box<[u8]>,
|
||||
content_hash: crate::RawContentHash,
|
||||
format_id: crate::RawFormatId,
|
||||
format_version: u32,
|
||||
}
|
||||
|
||||
impl RawPayload {
|
||||
/// Creates one canonical RAW payload after enforcing Store-owned admission invariants.
|
||||
///
|
||||
/// The supplied bytes must already use the KSP-owned source-independent format identified
|
||||
/// by `format_id` and `format_version`; this constructor performs no transport conversion.
|
||||
pub fn try_new(
|
||||
format_id: crate::RawFormatId,
|
||||
format_version: u32,
|
||||
bytes: std::boxed::Box<[u8]>,
|
||||
content_hash: crate::RawContentHash,
|
||||
) -> ksp_core_lib::Result<Self> {
|
||||
if format_version == 0 {
|
||||
return std::result::Result::Err(raw_payload_error("format_version"));
|
||||
}
|
||||
if bytes.is_empty() || bytes.len() > crate::MAX_RAW_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(
|
||||
raw_payload_error("bytes")
|
||||
.with_context("actual_len", bytes.len().to_string())
|
||||
.with_context("maximum_len", crate::MAX_RAW_PAYLOAD_BYTES.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(Self { bytes, content_hash, format_id, format_version });
|
||||
}
|
||||
|
||||
/// Returns the canonical RAW bytes without transport/provider interpretation.
|
||||
#[must_use]
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
return self.bytes.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the deterministic content digest supplied for these exact canonical bytes.
|
||||
#[must_use]
|
||||
pub const fn content_hash(&self) -> crate::RawContentHash {
|
||||
return self.content_hash;
|
||||
}
|
||||
|
||||
/// Returns the KSP-owned source-independent format identifier.
|
||||
#[must_use]
|
||||
pub fn format_id(&self) -> &crate::RawFormatId {
|
||||
return &self.format_id;
|
||||
}
|
||||
|
||||
/// Returns the KSP-owned format version.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
return self.format_version;
|
||||
}
|
||||
|
||||
/// Returns the canonical payload length in bytes.
|
||||
#[must_use]
|
||||
pub fn byte_len(&self) -> usize {
|
||||
return self.bytes.len();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RawPayload {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("RawPayload")
|
||||
.field("format_id", &self.format_id)
|
||||
.field("format_version", &self.format_version)
|
||||
.field("len", &self.bytes.len())
|
||||
.field("content_hash", &self.content_hash)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_model_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW Store model").with_context("field", field);
|
||||
}
|
||||
|
||||
fn raw_payload_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_PAYLOAD_INVALID, "invalid KSP RAW persistence payload").with_context("field", field);
|
||||
}
|
||||
|
||||
fn raw_provenance_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_PROVENANCE_INVALID, "invalid RAW acquisition provenance").with_context("field", field);
|
||||
}
|
||||
|
||||
fn valid_raw_code(value: &str) -> bool {
|
||||
if value.is_empty() || value.len() > crate::MAX_RAW_CODE_BYTES {
|
||||
return false;
|
||||
}
|
||||
return value.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':'));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../unit_tests/model/raw_primitives.rs"]
|
||||
mod tests;
|
||||
113
crates/ksp-store-api/src/model/raw_transaction.rs
Normal file
113
crates/ksp-store-api/src/model/raw_transaction.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// file: crates/ksp-store-api/src/model/raw_transaction.rs
|
||||
// version: 1
|
||||
|
||||
/// Durable backend-independent identity of one canonical RAW transaction.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RawTransactionReference {
|
||||
network: crate::RawNetworkId,
|
||||
signature: crate::RawTransactionSignature,
|
||||
}
|
||||
|
||||
impl RawTransactionReference {
|
||||
/// Creates one durable transaction identity from network and canonical Solana signature.
|
||||
#[must_use]
|
||||
pub fn new(network: crate::RawNetworkId, signature: crate::RawTransactionSignature) -> Self {
|
||||
return Self { network, signature };
|
||||
}
|
||||
|
||||
/// Returns the logical Solana network/cluster identifier.
|
||||
#[must_use]
|
||||
pub fn network(&self) -> &crate::RawNetworkId {
|
||||
return &self.network;
|
||||
}
|
||||
|
||||
/// Returns the canonical transaction signature.
|
||||
#[must_use]
|
||||
pub const fn signature(&self) -> crate::RawTransactionSignature {
|
||||
return self.signature;
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical source-independent N1 RAW transaction persisted by Store backends.
|
||||
///
|
||||
/// The payload must contain the complete KSP canonical transaction representation required
|
||||
/// for future STRUCTURAL replay, including transaction execution metadata and transaction
|
||||
/// log messages when the canonical format defines them. Provider/transport provenance is
|
||||
/// deliberately kept in [`crate::RawTransactionObservation`] instead.
|
||||
#[derive(Debug)]
|
||||
pub struct RawTransaction {
|
||||
block_time: std::option::Option<crate::RawTimestamp>,
|
||||
payload: crate::RawPayload,
|
||||
reference: crate::RawTransactionReference,
|
||||
slot: u64,
|
||||
}
|
||||
|
||||
impl RawTransaction {
|
||||
/// Creates one complete canonical RAW transaction.
|
||||
#[must_use]
|
||||
pub fn new(reference: crate::RawTransactionReference, slot: u64, block_time: std::option::Option<crate::RawTimestamp>, payload: crate::RawPayload) -> Self {
|
||||
return Self { block_time, payload, reference, slot };
|
||||
}
|
||||
|
||||
/// Returns the optional canonical block timestamp.
|
||||
#[must_use]
|
||||
pub const fn block_time(&self) -> std::option::Option<crate::RawTimestamp> {
|
||||
return self.block_time;
|
||||
}
|
||||
|
||||
/// Returns the complete KSP-owned canonical RAW payload.
|
||||
#[must_use]
|
||||
pub fn payload(&self) -> &crate::RawPayload {
|
||||
return &self.payload;
|
||||
}
|
||||
|
||||
/// Returns the durable backend-independent transaction identity.
|
||||
#[must_use]
|
||||
pub fn reference(&self) -> &crate::RawTransactionReference {
|
||||
return &self.reference;
|
||||
}
|
||||
|
||||
/// Returns the Solana slot containing the transaction.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistable acquisition observation linked to one canonical RAW transaction.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RawTransactionObservation {
|
||||
observation_key: crate::RawObservationKey,
|
||||
provenance: crate::RawAcquisitionProvenance,
|
||||
transaction: crate::RawTransactionReference,
|
||||
}
|
||||
|
||||
impl RawTransactionObservation {
|
||||
/// Creates one successful observation of a complete canonical RAW transaction.
|
||||
#[must_use]
|
||||
pub fn new(observation_key: crate::RawObservationKey, transaction: crate::RawTransactionReference, provenance: crate::RawAcquisitionProvenance) -> Self {
|
||||
return Self { observation_key, provenance, transaction };
|
||||
}
|
||||
|
||||
/// Returns the deterministic producer-owned observation idempotence key.
|
||||
#[must_use]
|
||||
pub const fn observation_key(&self) -> crate::RawObservationKey {
|
||||
return self.observation_key;
|
||||
}
|
||||
|
||||
/// Returns safe source-independent acquisition provenance.
|
||||
#[must_use]
|
||||
pub fn provenance(&self) -> &crate::RawAcquisitionProvenance {
|
||||
return &self.provenance;
|
||||
}
|
||||
|
||||
/// Returns the durable transaction identity observed by this acquisition.
|
||||
#[must_use]
|
||||
pub fn transaction(&self) -> &crate::RawTransactionReference {
|
||||
return &self.transaction;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../unit_tests/model/raw_transaction.rs"]
|
||||
mod tests;
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: crates/ksp-store-api/tests/dependency_boundary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Dependency canaries for the Store API scaffold.
|
||||
//! Dependency canaries for the Store API RAW foundation.
|
||||
|
||||
#[test]
|
||||
fn pre_002_manifest_has_exact_core_only_runtime_dependency() {
|
||||
fn pre_003_manifest_keeps_exact_core_only_runtime_dependency() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let dependencies_tail = manifest.split("[dependencies]").nth(1);
|
||||
assert!(dependencies_tail.is_some(), "Store API dependencies section must exist");
|
||||
@@ -49,26 +49,34 @@ fn pre_002_manifest_has_exact_core_only_runtime_dependency() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_source_boundary_separates_models_capabilities_and_backend_runtime() {
|
||||
fn pre_003_source_boundary_keeps_raw_models_passive_and_backend_free() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let model_home = include_str!("../src/model.rs");
|
||||
let raw_primitives = include_str!("../src/model/raw_primitives.rs");
|
||||
let raw_transaction = include_str!("../src/model/raw_transaction.rs");
|
||||
assert!(crate_root.contains("mod capability;"));
|
||||
assert!(crate_root.contains("mod error;"));
|
||||
assert!(crate_root.contains("mod model;"));
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
for forbidden in [
|
||||
"ksp_store_lib",
|
||||
"ksp_store_postgres_lib",
|
||||
"ksp_onchain_transport_lib",
|
||||
"ksp_program_api",
|
||||
"serde::",
|
||||
"sqlx::",
|
||||
"tokio::",
|
||||
"tokio_postgres::",
|
||||
"std::env::",
|
||||
"std::fs::",
|
||||
"std::net::",
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden Store API crate-root dependency/surface detected: {forbidden}");
|
||||
assert!(model_home.contains("raw_primitives"));
|
||||
assert!(model_home.contains("raw_transaction"));
|
||||
for source in [crate_root, model_home, raw_primitives, raw_transaction] {
|
||||
for forbidden in [
|
||||
"ksp_store_lib",
|
||||
"ksp_store_postgres_lib",
|
||||
"ksp_onchain_transport_lib",
|
||||
"ksp_program_api",
|
||||
"serde::",
|
||||
"sqlx::",
|
||||
"tokio::",
|
||||
"tokio_postgres::",
|
||||
"std::env::",
|
||||
"std::fs::",
|
||||
"std::net::",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "forbidden Store API dependency/runtime path detected: {forbidden}");
|
||||
}
|
||||
}
|
||||
assert!(!raw_transaction.contains("RawLog"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
// file: crates/ksp-store-api/tests/public_api.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Integration canaries for the public `ksp-store-api` scaffold.
|
||||
//! Integration canaries for the public `ksp-store-api` surface.
|
||||
|
||||
fn consume_result(value: ksp_store_api::Result<ksp_store_api::Pubkey>) -> ksp_store_api::Result<ksp_store_api::Pubkey> {
|
||||
return value;
|
||||
}
|
||||
|
||||
fn code(value: &str) -> std::option::Option<ksp_store_api::RawProvenanceCode> {
|
||||
return match ksp_store_api::RawProvenanceCode::new(value.to_owned()) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_002_core_facade_is_available_from_crate_root() {
|
||||
fn public_pre_002_core_facade_remains_available_from_crate_root() {
|
||||
let pubkey = ksp_store_api::Pubkey::new_from_array([0x31_u8; 32]);
|
||||
let forwarded = consume_result(std::result::Result::Ok(pubkey));
|
||||
assert!(forwarded.is_ok());
|
||||
@@ -22,11 +29,54 @@ fn public_pre_002_core_facade_is_available_from_crate_root() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_002_scaffold_exposes_no_private_module_paths_or_backend_types() {
|
||||
fn public_pre_003_raw_transaction_and_observation_are_constructible_from_crate_root() {
|
||||
let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let reference = ksp_store_api::RawTransactionReference::new(network, ksp_store_api::RawTransactionSignature::new([1_u8; 64]));
|
||||
let format = match ksp_store_api::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let payload = match ksp_store_api::RawPayload::try_new(format, 1, vec![1_u8, 2_u8, 3_u8].into_boxed_slice(), ksp_store_api::RawContentHash::new([2_u8; 32]))
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let transaction = ksp_store_api::RawTransaction::new(reference.clone(), 123, std::option::Option::None, payload);
|
||||
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_000) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let provider = match code("provider") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let protocol = match code("solana_http") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let method = match code("getTransaction") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_api::RawAcquisitionOrigin::Backfill, received_at);
|
||||
let observation = ksp_store_api::RawTransactionObservation::new(ksp_store_api::RawObservationKey::new([3_u8; 32]), reference, provenance);
|
||||
assert_eq!(transaction.slot(), 123);
|
||||
assert_eq!(transaction.payload().bytes(), &[1_u8, 2_u8, 3_u8]);
|
||||
assert_eq!(observation.transaction().network().as_str(), "mainnet-beta");
|
||||
assert_eq!(observation.provenance().acquisition_method().as_str(), "getTransaction");
|
||||
assert_eq!(ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID.domain(), "store_api");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_003_surface_keeps_backend_and_structural_types_out() {
|
||||
let source = include_str!("../src/lib.rs");
|
||||
assert!(!source.contains("pub mod "));
|
||||
for forbidden in ["Postgres", "Sql", "Migration", "ProgramInstruction", "RawTransaction"] {
|
||||
assert!(!source.contains(forbidden), "forbidden pre.002 Store API public concept detected: {forbidden}");
|
||||
for forbidden in ["Postgres", "Sql", "Migration", "StructuralTransaction", "RawLog", "ProgramInstruction"] {
|
||||
assert!(!source.contains(forbidden), "forbidden pre.003 Store API public concept detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
197
crates/ksp-store-api/unit_tests/model/raw_primitives.rs
Normal file
197
crates/ksp-store-api/unit_tests/model/raw_primitives.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
// file: crates/ksp-store-api/unit_tests/model/raw_primitives.rs
|
||||
// version: 1
|
||||
|
||||
fn code(value: &str) -> std::option::Option<crate::RawProvenanceCode> {
|
||||
return match crate::RawProvenanceCode::new(value.to_owned()) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn timestamp(unix_millis: u64) -> std::option::Option<crate::RawTimestamp> {
|
||||
return match crate::RawTimestamp::from_unix_millis(unix_millis) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_codes_are_bounded_nonempty_and_reject_url_like_or_control_values() {
|
||||
let network = crate::RawNetworkId::new("mainnet-beta".to_owned());
|
||||
assert!(network.is_ok());
|
||||
let format = crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned());
|
||||
assert!(format.is_ok());
|
||||
let maximum = "a".repeat(crate::MAX_RAW_CODE_BYTES);
|
||||
assert!(crate::RawProvenanceCode::new(maximum).is_ok());
|
||||
assert!(crate::RawProvenanceCode::new(std::string::String::new()).is_err());
|
||||
assert!(crate::RawProvenanceCode::new("bad value".to_owned()).is_err());
|
||||
assert!(crate::RawProvenanceCode::new("https://secret.example".to_owned()).is_err());
|
||||
assert!(crate::RawProvenanceCode::new("bad\nvalue".to_owned()).is_err());
|
||||
assert!(crate::RawProvenanceCode::new("a".repeat(crate::MAX_RAW_CODE_BYTES + 1)).is_err());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_timestamp_accepts_documented_maximum_and_rejects_larger_values() {
|
||||
let maximum = crate::RawTimestamp::from_unix_millis(crate::MAX_RAW_UNIX_MILLIS);
|
||||
assert!(maximum.is_ok());
|
||||
let too_large = crate::RawTimestamp::from_unix_millis(crate::MAX_RAW_UNIX_MILLIS + 1);
|
||||
assert!(too_large.is_err());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_payload_is_nonempty_versioned_bounded_and_debug_omits_bytes() {
|
||||
let format_result = crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned());
|
||||
assert!(format_result.is_ok());
|
||||
let format = match format_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let sentinel = b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER".to_vec().into_boxed_slice();
|
||||
let payload_result = crate::RawPayload::try_new(format, 1, sentinel, crate::RawContentHash::new([7_u8; 32]));
|
||||
assert!(payload_result.is_ok());
|
||||
let payload = match payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(payload.bytes(), b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER");
|
||||
assert_eq!(payload.byte_len(), b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER".len());
|
||||
assert_eq!(payload.format_version(), 1);
|
||||
let debug = format!("{payload:?}");
|
||||
assert!(!debug.contains("RAW_PAYLOAD_SENTINEL_NEVER_RENDER"));
|
||||
assert!(debug.contains("len"));
|
||||
let empty_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(crate::RawPayload::try_new(empty_format, 1, std::vec::Vec::new().into_boxed_slice(), crate::RawContentHash::new([0_u8; 32])).is_err());
|
||||
let zero_version_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(crate::RawPayload::try_new(zero_version_format, 0, vec![1_u8].into_boxed_slice(), crate::RawContentHash::new([0_u8; 32])).is_err());
|
||||
let oversized_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let oversized = vec![0_u8; crate::MAX_RAW_PAYLOAD_BYTES + 1].into_boxed_slice();
|
||||
let error = crate::RawPayload::try_new(oversized_format, 1, oversized, crate::RawContentHash::new([0_u8; 32]));
|
||||
assert!(error.is_err());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquisition_provenance_preserves_safe_metadata_and_validates_time_and_size() {
|
||||
let received_at = match timestamp(2_000) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let observed_at = match timestamp(1_000) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let provider = match code("publicnode") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let protocol = match code("yellowstone_grpc") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let method = match code("transactions") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let endpoint = match code("main") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let commitment = match code("confirmed") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let session = match code("session_1") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let filter = match code("all_transactions") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let provenance = crate::RawAcquisitionProvenance::new(provider, protocol, method, crate::RawAcquisitionOrigin::Live, received_at)
|
||||
.with_endpoint_id(endpoint)
|
||||
.with_commitment(commitment)
|
||||
.with_capture_session_id(session)
|
||||
.with_filter_id(filter)
|
||||
.with_source_payload_hash(crate::RawContentHash::new([3_u8; 32]));
|
||||
let provenance_result = provenance.try_with_observed_at(observed_at);
|
||||
assert!(provenance_result.is_ok());
|
||||
let provenance = match provenance_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let provenance_result = provenance.try_with_source_payload_size_bytes(4_096);
|
||||
assert!(provenance_result.is_ok());
|
||||
let provenance = match provenance_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(provenance.provider().as_str(), "publicnode");
|
||||
assert_eq!(provenance.protocol().as_str(), "yellowstone_grpc");
|
||||
assert_eq!(provenance.acquisition_method().as_str(), "transactions");
|
||||
assert_eq!(provenance.origin(), crate::RawAcquisitionOrigin::Live);
|
||||
assert_eq!(provenance.observed_at(), std::option::Option::Some(observed_at));
|
||||
assert_eq!(provenance.received_at(), received_at);
|
||||
assert_eq!(provenance.source_payload_size_bytes(), std::option::Option::Some(4_096));
|
||||
let reversed_provider = match code("provider") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let reversed_protocol = match code("solana_websocket") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let reversed_method = match code("transactionSubscribe") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let reversed_received = match timestamp(1_000) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let reversed_observed = match timestamp(2_000) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let reversed =
|
||||
crate::RawAcquisitionProvenance::new(reversed_provider, reversed_protocol, reversed_method, crate::RawAcquisitionOrigin::Live, reversed_received)
|
||||
.try_with_observed_at(reversed_observed);
|
||||
assert!(reversed.is_err());
|
||||
let oversized_provider = match code("provider") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let oversized_protocol = match code("solana_http") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let oversized_method = match code("getTransaction") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let oversized_received = match timestamp(2_000) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let oversized_source = crate::RawAcquisitionProvenance::new(
|
||||
oversized_provider,
|
||||
oversized_protocol,
|
||||
oversized_method,
|
||||
crate::RawAcquisitionOrigin::Backfill,
|
||||
oversized_received,
|
||||
)
|
||||
.try_with_source_payload_size_bytes(crate::MAX_RAW_SOURCE_PAYLOAD_BYTES + 1);
|
||||
assert!(oversized_source.is_err());
|
||||
return;
|
||||
}
|
||||
85
crates/ksp-store-api/unit_tests/model/raw_transaction.rs
Normal file
85
crates/ksp-store-api/unit_tests/model/raw_transaction.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// file: crates/ksp-store-api/unit_tests/model/raw_transaction.rs
|
||||
// version: 1
|
||||
|
||||
fn network() -> std::option::Option<crate::RawNetworkId> {
|
||||
return match crate::RawNetworkId::new("mainnet-beta".to_owned()) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn payload() -> std::option::Option<crate::RawPayload> {
|
||||
let format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return match crate::RawPayload::try_new(
|
||||
format,
|
||||
1,
|
||||
b"canonical transaction including logs".to_vec().into_boxed_slice(),
|
||||
crate::RawContentHash::new([5_u8; 32]),
|
||||
) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn provenance() -> std::option::Option<crate::RawAcquisitionProvenance> {
|
||||
let provider = match crate::RawProvenanceCode::new("publicnode".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let protocol = match crate::RawProvenanceCode::new("yellowstone_grpc".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let method = match crate::RawProvenanceCode::new("transactions".to_owned()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let received_at = match crate::RawTimestamp::from_unix_millis(1_000) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return std::option::Option::Some(crate::RawAcquisitionProvenance::new(provider, protocol, method, crate::RawAcquisitionOrigin::Backfill, received_at));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_transaction_identity_is_network_plus_signature_and_payload_remains_whole() {
|
||||
let network = match network() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let payload = match payload() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let signature = crate::RawTransactionSignature::new([9_u8; 64]);
|
||||
let reference = crate::RawTransactionReference::new(network, signature);
|
||||
let transaction = crate::RawTransaction::new(reference, 42, std::option::Option::None, payload);
|
||||
assert_eq!(transaction.reference().network().as_str(), "mainnet-beta");
|
||||
assert_eq!(transaction.reference().signature(), signature);
|
||||
assert_eq!(transaction.slot(), 42);
|
||||
assert!(transaction.block_time().is_none());
|
||||
assert_eq!(transaction.payload().bytes(), b"canonical transaction including logs");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_observation_is_separate_from_canonical_raw_payload() {
|
||||
let network = match network() {
|
||||
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 signature = crate::RawTransactionSignature::new([11_u8; 64]);
|
||||
let reference = crate::RawTransactionReference::new(network, signature);
|
||||
let observation = crate::RawTransactionObservation::new(crate::RawObservationKey::new([12_u8; 32]), reference, provenance);
|
||||
assert_eq!(observation.transaction().signature(), signature);
|
||||
assert_eq!(observation.observation_key().as_bytes(), &[12_u8; 32]);
|
||||
assert_eq!(observation.provenance().provider().as_str(), "publicnode");
|
||||
return;
|
||||
}
|
||||
204
deltas/0.3.1/pre.003.md
Normal file
204
deltas/0.3.1/pre.003.md
Normal file
@@ -0,0 +1,204 @@
|
||||
<!-- file: deltas/0.3.1/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.1-pre.003`
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
0.3.1-pre.002
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.002` est fourni vert : audits Rust/Markdown, `cargo check --workspace`, Clippy workspace et `cargo test -p ksp-store-api` passent.
|
||||
|
||||
## Objectif
|
||||
|
||||
Matérialiser les primitives N1 RAW backend-agnostic puis le premier modèle persistant réel `RawTransaction` avec observation d'acquisition séparée, sans introduire encore les matrices HTTP/WS/gRPC de `pre.004`, les capabilities de `pre.005` ni aucun backend/runtime Store.
|
||||
|
||||
## Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.1-pre.3
|
||||
```
|
||||
|
||||
## Surface ajoutée
|
||||
|
||||
Primitives communes :
|
||||
|
||||
```text
|
||||
RawNetworkId
|
||||
RawTransactionSignature
|
||||
RawTransactionReference
|
||||
RawFormatId
|
||||
RawContentHash
|
||||
RawObservationKey
|
||||
RawTimestamp
|
||||
RawAcquisitionOrigin
|
||||
RawProvenanceCode
|
||||
RawAcquisitionProvenance
|
||||
RawPayload
|
||||
```
|
||||
|
||||
Première famille N1 :
|
||||
|
||||
```text
|
||||
RawTransaction
|
||||
RawTransactionObservation
|
||||
```
|
||||
|
||||
Codes d'erreur :
|
||||
|
||||
```text
|
||||
store_api.raw_model_invalid
|
||||
store_api.raw_payload_invalid
|
||||
store_api.raw_provenance_invalid
|
||||
```
|
||||
|
||||
## Invariants RAW
|
||||
|
||||
`RawPayload` contient uniquement un format de persistence KSP déjà source-independent :
|
||||
|
||||
```text
|
||||
format_id
|
||||
format_version > 0
|
||||
bytes non vides
|
||||
content_hash [u8; 32]
|
||||
```
|
||||
|
||||
Le Store API ne choisit ni codec ni algorithme de conversion Transport. Le producer/converter du futur format canonique doit fournir des bytes complets et leur digest déterministe.
|
||||
|
||||
Bornes Store-owned :
|
||||
|
||||
```text
|
||||
code logique <= 128 bytes
|
||||
payload RAW canonique <= 16 MiB
|
||||
source payload size meta <= 64 MiB
|
||||
Unix timestamp <= 9999-12-31T23:59:59.999Z
|
||||
```
|
||||
|
||||
Ces valeurs sont des admission guards internes et ne prétendent pas définir des maxima Solana.
|
||||
|
||||
`RawPayload` et `RawTransaction` ne sont volontairement pas `Clone`, afin d'éviter de rendre triviale la copie de gros documents RAW.
|
||||
|
||||
## Identité transactionnelle
|
||||
|
||||
La référence durable est :
|
||||
|
||||
```text
|
||||
RawTransactionReference
|
||||
network
|
||||
signature [u8; 64]
|
||||
```
|
||||
|
||||
Aucune PK SQL/backend ne traverse l'API. `RawTransaction` ajoute :
|
||||
|
||||
```text
|
||||
slot: u64
|
||||
block_time: Option<RawTimestamp>
|
||||
RawPayload
|
||||
```
|
||||
|
||||
Les logs contenus dans la transaction restent à l'intérieur du payload canonique N1. Aucun modèle `RawLog` persistant ni type STRUCTURAL n'est créé.
|
||||
|
||||
## Observation et provenance
|
||||
|
||||
Une observation réussie reste distincte du RAW :
|
||||
|
||||
```text
|
||||
RawTransactionObservation
|
||||
observation_key [u8; 32]
|
||||
transaction reference
|
||||
provenance
|
||||
```
|
||||
|
||||
La provenance peut représenter avec des logical codes sûrs :
|
||||
|
||||
```text
|
||||
provider
|
||||
protocol
|
||||
acquisition method
|
||||
origin live/backfill/import/replay/repair
|
||||
endpoint id optionnel
|
||||
commitment optionnel
|
||||
capture/session id optionnel
|
||||
filter id optionnel
|
||||
observed_at optionnel
|
||||
received_at
|
||||
source payload size/hash optionnels
|
||||
```
|
||||
|
||||
Elle n'accepte aucun payload source et ses logical codes refusent notamment les espaces, contrôles et formes URL contenant `/`. Le contrat reste explicitement non-secret : un caller ne doit jamais placer une credential dans un logical code.
|
||||
|
||||
`observed_at`, lorsqu'il existe, ne peut pas être postérieur à `received_at`.
|
||||
|
||||
## Tests
|
||||
|
||||
Tests unitaires ajoutés :
|
||||
|
||||
- bornes/validation des codes ;
|
||||
- borne de timestamp ;
|
||||
- payload non vide/versionné/borné ;
|
||||
- `Debug` du payload sans bytes ;
|
||||
- provenance et ordre temporel ;
|
||||
- identité `network + signature` ;
|
||||
- séparation `RawTransaction` / `RawTransactionObservation`.
|
||||
|
||||
Canaris d'intégration mis à jour :
|
||||
|
||||
- surface crate-root des nouveaux modèles ;
|
||||
- dépendance runtime exacte `ksp-core-lib` ;
|
||||
- absence de backend, SQL, serde, codec, async runtime, Transport et Program ;
|
||||
- absence de `RawLog` et de type STRUCTURAL dans la production `pre.003`.
|
||||
|
||||
## Documentation mise à jour
|
||||
|
||||
```text
|
||||
docs/plans/022-V0_3_1_STORE_RAW_PLAN.md
|
||||
docs/validation/018-V0_3_1_STORE_RAW.md
|
||||
```
|
||||
|
||||
Ils figent les primitives et bornes réellement matérialisées par cette tranche sans avancer les matrices cross-source de `pre.004`.
|
||||
|
||||
## Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
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/0.3.1
|
||||
```
|
||||
|
||||
## Validations non exécutées dans l'environnement de génération
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas installés dans l'environnement de génération. L'opérateur doit donc exécuter :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
```
|
||||
|
||||
Une commande non exécutée n'est pas déclarée PASS.
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
```text
|
||||
conversion HTTP/WS/gRPC -> RawTransaction canonique
|
||||
RawAccountState
|
||||
TransactionStatusObservation
|
||||
events logs/slot/vote
|
||||
capabilities read/write
|
||||
queries/outcomes
|
||||
retention/tombstone concret
|
||||
ksp-store-lib
|
||||
ksp-store-postgres-lib
|
||||
PostgreSQL/tokio-postgres
|
||||
Config std.store
|
||||
N2 STRUCTURAL
|
||||
N3/N4
|
||||
```
|
||||
|
||||
## Suite
|
||||
|
||||
`0.3.1-pre.004` audite les formes HTTP/WS/gRPC réelles afin de figer la matrice d'admission cross-source et d'introduire seulement les familles N1 supplémentaires dont la sémantique commune est effectivement démontrée, en priorité `RawAccountState`/observation et `TransactionStatusObservation`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/022-V0_3_1_STORE_RAW_PLAN.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Plan `0.3.1` — Store API RAW foundation
|
||||
|
||||
@@ -553,6 +553,46 @@ hashable/idempotent
|
||||
|
||||
Le Store ne prétend pas que des bytes arbitraires sans format connu sont replayables.
|
||||
|
||||
### 7.3 Contrat matérialisé par `pre.003`
|
||||
|
||||
La première surface concrète reste indépendante de tout codec et de tout backend :
|
||||
|
||||
```text
|
||||
RawPayload
|
||||
RawFormatId
|
||||
format_version: u32 non nul
|
||||
bytes: Box<[u8]> non vide
|
||||
RawContentHash: [u8; 32]
|
||||
|
||||
RawTransactionReference
|
||||
RawNetworkId
|
||||
RawTransactionSignature: [u8; 64]
|
||||
|
||||
RawTransaction
|
||||
reference
|
||||
slot: u64
|
||||
block_time: Option<RawTimestamp>
|
||||
payload
|
||||
|
||||
RawTransactionObservation
|
||||
RawObservationKey: [u8; 32]
|
||||
transaction reference
|
||||
RawAcquisitionProvenance
|
||||
```
|
||||
|
||||
Les bornes initiales sont des **admission guards Store**, jamais des affirmations sur les maxima du protocole Solana :
|
||||
|
||||
```text
|
||||
logical code UTF-8 <= 128 bytes
|
||||
canonical RAW payload <= 16 MiB
|
||||
source payload size meta <= 64 MiB
|
||||
Unix timestamp <= 9999-12-31T23:59:59.999Z
|
||||
```
|
||||
|
||||
`RawPayload` ne calcule pas lui-même le digest et ne vérifie pas le contenu du format. Le producer/converter propriétaire du format KSP doit fournir des bytes déjà canoniques et leur digest déterministe. Cette séparation permet de conserver `ksp-store-api -> ksp-core-lib` uniquement.
|
||||
|
||||
Le payload et `RawTransaction` ne sont volontairement pas `Clone` dans cette foundation afin de ne pas encourager des copies implicites d'un document RAW potentiellement volumineux. Les références, signatures, digests, timestamps et observations compactes restent clonables/copiables lorsqu'approprié.
|
||||
|
||||
## 8. Future décomposition N1 -> niveau STRUCTURAL
|
||||
|
||||
Le nom de travail de N2 devient **STRUCTURAL**. `CORE` est abandonné dans le nouveau plan parce qu'il décrivait mal une opération qui consiste principalement à décomposer des données Solana brutes en sous-composants génériques.
|
||||
@@ -627,6 +667,10 @@ payload source complet par simple diagnostic
|
||||
|
||||
Les codes provider/protocol/method restent ouverts et bornés ; aucun enum provider fermé n'est introduit dans Store API.
|
||||
|
||||
`pre.003` matérialise cette politique avec `RawProvenanceCode`, réutilisé pour les codes logiques provider/protocol/method/endpoint/commitment/session/filter. Les valeurs sont non vides, bornées et limitées à un alphabet logique sûr ; une URL contenant `/`, des contrôles ou des espaces ne peut donc pas être stockée accidentellement dans ces champs. Cette validation n'autorise pas le caller à y placer un secret alphanumérique : le contrat reste explicitement « logical code only ».
|
||||
|
||||
`RawAcquisitionProvenance::new(...)` ne prend que les champs obligatoires ; les informations optionnelles sont ajoutées par builders dédiés. `observed_at`, lorsqu'il existe, ne peut pas être postérieur à `received_at`, et le payload source n'est jamais conservé, seulement sa taille bornée et/ou un digest optionnel.
|
||||
|
||||
## 10. Identité et idempotence
|
||||
|
||||
### 10.1 Principes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/018-V0_3_1_STORE_RAW.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Validation `0.3.1` — Store API RAW foundation
|
||||
|
||||
@@ -190,6 +190,43 @@ crate/test backend externe
|
||||
| force rehydrate implicite | interdit | `pre.006` |
|
||||
| N2/N3/N4 creep | aucune surface | `pre.007` |
|
||||
|
||||
### 8.1 Matérialisation `pre.003`
|
||||
|
||||
La tranche implémente et couvre localement par canaris source/audit :
|
||||
|
||||
```text
|
||||
RawNetworkId
|
||||
RawTransactionSignature [u8; 64]
|
||||
RawTransactionReference
|
||||
RawFormatId
|
||||
RawContentHash [u8; 32]
|
||||
RawObservationKey [u8; 32]
|
||||
RawTimestamp
|
||||
RawAcquisitionOrigin
|
||||
RawProvenanceCode
|
||||
RawAcquisitionProvenance
|
||||
RawPayload
|
||||
RawTransaction
|
||||
RawTransactionObservation
|
||||
```
|
||||
|
||||
Gates matérialisés :
|
||||
|
||||
```text
|
||||
Core-only dependency firewall conservé
|
||||
payload KSP source-independent, non vide, version > 0
|
||||
payload maximum Store = 16 MiB
|
||||
source payload metadata maximum = 64 MiB
|
||||
Debug RawPayload ne rend jamais les bytes
|
||||
signature/observation key/hash ne dépendent d'aucune PK backend
|
||||
provenance sans URL/source payload
|
||||
observed_at <= received_at
|
||||
logs transactionnels restent dans le payload RawTransaction
|
||||
aucun RawLog/N2 STRUCTURAL/backend/runtime ajouté
|
||||
```
|
||||
|
||||
La complétude sémantique d'une source HTTP/WS/gRPC vers le format canonique n'est pas simulée dans Store API : elle reste le gate d'admission/conversion de `pre.004`. `pre.003` exige seulement qu'un `RawTransaction` reçoive un `RawPayload` déjà canonique complet selon son format KSP déclaré.
|
||||
|
||||
## 9. Gates de fermeture prévus
|
||||
|
||||
### Gate technique final `pre.008`
|
||||
|
||||
Reference in New Issue
Block a user