v0.3.4-pre.005

This commit is contained in:
2026-08-30 22:17:47 +02:00
parent eb6dbc31e8
commit 50d4142797
10 changed files with 766 additions and 31 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 370
# version: 371
[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-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.3.4-pre.4.fix.1"
version = "0.3.4-pre.5"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 17
// version: 18
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -27,7 +27,9 @@
//! navigation index, external-schema compatibility and the bounded prerelease
//! checksum transition from the provisional `pre.002` schema. `0.3.4-pre.004`
//! adds backend-private RAW account state/observation read mapping and hostile-row
//! guards while keeping all account writes, pagination and trait implementations closed.
//! guards. `0.3.4-pre.005` adds atomic account state+observation acquisition writes
//! with exact idempotence/conflict classification while pagination, additional-observation
//! writes and all four account trait implementations remain closed.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
@@ -71,6 +73,8 @@ pub(crate) use self::migration::current_migration_version;
pub(crate) use self::raw_account::get_raw_account_observation;
/// Private RAW account state reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::get_raw_account_state;
/// Private atomic RAW account acquisition writer consumed by the physical backend runtime.
pub(crate) use self::raw_account::persist_raw_account_acquisition;
/// Private RAW transaction cursor decoder consumed by the physical RAW module.
pub(crate) use self::raw_transaction::cursor::decode_raw_transaction_cursor;
/// Private RAW transaction cursor encoder consumed by the physical RAW module.

View File

@@ -1,8 +1,12 @@
// file: crates/ksp-store-postgres-lib/src/raw_account.rs
// version: 2
// version: 3
const GET_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1";
const GET_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3";
const INSERT_ACCOUNT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_account_observations (observation_key, account_pubkey, account_slot, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version) VALUES ($1, $2, $3::TEXT::NUMERIC, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::TEXT::NUMERIC) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
const INSERT_ACCOUNT_STATE_SQL: &str = "INSERT INTO ksp_raw_account_states (pubkey, slot, state_hash, lamports, owner, executable, rent_epoch, data) VALUES ($1, $2::TEXT::NUMERIC, $3, $4::TEXT::NUMERIC, $5, $6, $7::TEXT::NUMERIC, $8) ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING pubkey";
const LOCK_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1 FOR UPDATE";
const LOCK_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR UPDATE";
struct RawAccountObservationDbRow {
account_pubkey: std::vec::Vec<u8>,
@@ -103,7 +107,10 @@ pub(crate) async fn get_raw_account_observation(
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_observation_query"));
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::ReadFailed,
"raw_account_observation_query",
));
},
};
if rows.is_empty() {
@@ -130,6 +137,58 @@ pub(crate) async fn get_raw_account_observation(
return std::result::Result::Ok(std::option::Option::Some(decoded));
}
/// Persists one canonical RAW account state and its acquisition observation atomically.
pub(crate) async fn persist_raw_account_acquisition(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
let input_result = ensure_acquisition_inputs(network, &state, &observation);
if let std::result::Result::Err(error) = input_result {
return std::result::Result::Err(error);
}
let client_result = pool.get().await;
let mut client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let sql_transaction_result = client.transaction().await;
let sql_transaction = match sql_transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_begin")),
};
let insert_result = insert_account_state(&sql_transaction, &state).await;
let inserted = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entity_outcome = if inserted {
ksp_store_api::RawEntityWriteOutcome::Inserted
} else {
let locked_result = load_locked_account_state(&sql_transaction, network, state.reference()).await;
let locked = match locked_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_acquisition_conflict_missing")),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !raw_account_states_equal(&locked, &state) {
return std::result::Result::Err(conflict("raw_account_acquisition_content_conflict"));
}
ksp_store_api::RawEntityWriteOutcome::AlreadyPresent
};
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
let observation_outcome = match observation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let commit_result = sql_transaction.commit().await;
if commit_result.is_err() {
return std::result::Result::Err(write_failed("raw_account_acquisition_commit"));
}
return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(entity_outcome, observation_outcome));
}
fn raw_account_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountObservationDbRow, crate::PostgresBackendError> {
let account_pubkey: std::vec::Vec<u8> = match row.try_get("account_pubkey") {
std::result::Result::Ok(value) => value,
@@ -432,6 +491,209 @@ fn decode_raw_account_state_row(
};
}
async fn insert_account_state(
sql_transaction: &deadpool_postgres::Transaction<'_>,
state: &ksp_store_api::RawAccountState,
) -> std::result::Result<bool, crate::PostgresBackendError> {
let reference = state.reference();
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
let slot_text = reference.slot().to_string();
let state_hash = reference.state_hash();
let state_hash_bytes: &[u8] = state_hash.as_bytes();
let lamports_text = state.lamports().to_string();
let owner_bytes: &[u8] = state.owner().as_ref();
let executable = state.executable();
let rent_epoch_text = state.rent_epoch().to_string();
let data = state.data();
let row_result = sql_transaction
.query_opt(
INSERT_ACCOUNT_STATE_SQL,
&[
&pubkey_bytes,
&slot_text.as_str(),
&state_hash_bytes,
&lamports_text.as_str(),
&owner_bytes,
&executable,
&rent_epoch_text.as_str(),
&data,
],
)
.await;
return match row_result {
std::result::Result::Ok(std::option::Option::Some(_)) => std::result::Result::Ok(true),
std::result::Result::Ok(std::option::Option::None) => std::result::Result::Ok(false),
std::result::Result::Err(_) => std::result::Result::Err(write_failed("raw_account_acquisition_insert_state")),
};
}
async fn load_locked_account_state(
sql_transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
reference: &ksp_store_api::RawAccountStateReference,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
let slot_text = reference.slot().to_string();
let state_hash = reference.state_hash();
let state_hash_bytes: &[u8] = state_hash.as_bytes();
let row_result = sql_transaction.query_opt(LOCK_ACCOUNT_STATE_SQL, &[&pubkey_bytes, &slot_text.as_str(), &state_hash_bytes]).await;
let row = match row_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Ok(std::option::Option::None),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_lock_state")),
};
let physical = match raw_account_state_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match decode_raw_account_state_row(network, physical) {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
async fn persist_account_observation_row(
sql_transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
observation: &ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
let provenance = observation.provenance();
let observation_key = observation.observation_key();
let observation_key_bytes: &[u8] = observation_key.as_bytes();
let account = observation.account();
let account_pubkey_bytes: &[u8] = account.pubkey().as_ref();
let account_slot_text = account.slot().to_string();
let account_state_hash = account.state_hash();
let account_state_hash_bytes: &[u8] = account_state_hash.as_bytes();
let origin = match encode_origin(provenance.origin()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let received_at = match i64::try_from(provenance.received_at().unix_millis()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_received_at_encode")),
};
let observed_at = match provenance.observed_at() {
std::option::Option::Some(value) => match i64::try_from(value.unix_millis()) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_observed_at_encode")),
},
std::option::Option::None => std::option::Option::None,
};
let source_payload_size = match provenance.source_payload_size_bytes() {
std::option::Option::Some(value) => match i64::try_from(value) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_source_size_encode")),
},
std::option::Option::None => std::option::Option::None,
};
let capture_session_id = provenance.capture_session_id().map(|value| return value.as_str());
let commitment = provenance.commitment().map(|value| return value.as_str());
let endpoint_id = provenance.endpoint_id().map(|value| return value.as_str());
let filter_id = provenance.filter_id().map(|value| return value.as_str());
let source_payload_hash = provenance.source_payload_hash();
let source_payload_hash_bytes: std::option::Option<&[u8]> = source_payload_hash.as_ref().map(|value| return &value.as_bytes()[..]);
let is_startup = observation.is_startup();
let transaction_signature = observation.transaction_signature();
let transaction_signature_bytes: std::option::Option<&[u8]> = transaction_signature.as_ref().map(|value| return &value.as_bytes()[..]);
let write_version_text = observation.write_version().map(|value| return value.to_string());
let row_result = sql_transaction
.query_opt(
INSERT_ACCOUNT_OBSERVATION_SQL,
&[
&observation_key_bytes,
&account_pubkey_bytes,
&account_slot_text.as_str(),
&account_state_hash_bytes,
&provenance.provider().as_str(),
&provenance.protocol().as_str(),
&provenance.acquisition_method().as_str(),
&origin,
&received_at,
&capture_session_id,
&commitment,
&endpoint_id,
&filter_id,
&observed_at,
&source_payload_hash_bytes,
&source_payload_size,
&is_startup,
&transaction_signature_bytes,
&write_version_text,
],
)
.await;
let inserted = match row_result {
std::result::Result::Ok(std::option::Option::Some(_)) => true,
std::result::Result::Ok(std::option::Option::None) => false,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_insert")),
};
if inserted {
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::Inserted);
}
let existing_result = sql_transaction.query_opt(LOCK_ACCOUNT_OBSERVATION_SQL, &[&observation_key_bytes]).await;
let existing_row = match existing_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_observation_conflict_missing")),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_conflict_query")),
};
let physical = match raw_account_observation_db_row(&existing_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let stored = match decode_raw_account_observation_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if stored.eq(observation) {
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::AlreadyPresent);
}
return std::result::Result::Err(conflict("raw_account_observation_content_conflict"));
}
fn ensure_acquisition_inputs(
network: &ksp_store_api::RawNetworkId,
state: &ksp_store_api::RawAccountState,
observation: &ksp_store_api::RawAccountObservation,
) -> std::result::Result<(), crate::PostgresBackendError> {
let state_network_result = ensure_network(network, state.reference(), "raw_account_acquisition_state_network");
if let std::result::Result::Err(error) = state_network_result {
return std::result::Result::Err(error);
}
let observation_network_result = ensure_network(network, observation.account(), "raw_account_acquisition_observation_network");
if let std::result::Result::Err(error) = observation_network_result {
return std::result::Result::Err(error);
}
if observation.account() != state.reference() {
return std::result::Result::Err(conflict("raw_account_acquisition_reference_mismatch"));
}
return std::result::Result::Ok(());
}
fn raw_account_states_equal(left: &ksp_store_api::RawAccountState, right: &ksp_store_api::RawAccountState) -> bool {
return left.reference() == right.reference()
&& left.lamports() == right.lamports()
&& left.owner() == right.owner()
&& left.executable() == right.executable()
&& left.rent_epoch() == right.rent_epoch()
&& left.data() == right.data();
}
fn encode_origin(origin: ksp_store_api::RawAcquisitionOrigin) -> std::result::Result<&'static str, crate::PostgresBackendError> {
return match origin {
ksp_store_api::RawAcquisitionOrigin::Backfill => std::result::Result::Ok("backfill"),
ksp_store_api::RawAcquisitionOrigin::Import => std::result::Result::Ok("import"),
ksp_store_api::RawAcquisitionOrigin::Live => std::result::Result::Ok("live"),
ksp_store_api::RawAcquisitionOrigin::Repair => std::result::Result::Ok("repair"),
ksp_store_api::RawAcquisitionOrigin::Replay => std::result::Result::Ok("replay"),
_ => std::result::Result::Err(data_invalid("raw_account_observation_origin_encode")),
};
}
fn conflict(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::Conflict, phase);
}
fn decode_origin(value: &str) -> std::result::Result<ksp_store_api::RawAcquisitionOrigin, crate::PostgresBackendError> {
return match value {
"backfill" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Backfill),
@@ -497,6 +759,10 @@ fn data_invalid(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::DataInvalid, phase);
}
fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
}
#[cfg(test)]
#[path = "../unit_tests/raw_account.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 11
// version: 12
const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -348,6 +348,15 @@ impl PostgresBackend {
return crate::get_raw_account_observation(&self.pool, &self.network, observation_key).await;
}
/// Persists one complete RAW account state and its acquisition observation atomically.
pub async fn persist_raw_account_acquisition(
&self,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
return crate::persist_raw_account_acquisition(&self.pool, &self.network, state, observation).await;
}
/// Reads one canonical RAW transaction without exposing physical PostgreSQL row types.
pub async fn get_raw_transaction(
&self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 18
// version: 19
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -152,7 +152,7 @@ fn pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch() {
}
#[test]
fn pre_004_raw_account_read_sql_and_mapping_remain_backend_private_and_read_only() {
fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_later_scope_closed() {
let crate_root = include_str!("../src/lib.rs");
let raw = include_str!("../src/raw_account.rs");
let runtime = include_str!("../src/runtime.rs");
@@ -161,19 +161,27 @@ fn pre_004_raw_account_read_sql_and_mapping_remain_backend_private_and_read_only
for required in [
"GET_ACCOUNT_STATE_SQL",
"GET_ACCOUNT_OBSERVATION_SQL",
"slot::text AS slot_text",
"lamports::text AS lamports_text",
"rent_epoch::text AS rent_epoch_text",
"write_version::text AS write_version_text",
"INSERT_ACCOUNT_STATE_SQL",
"INSERT_ACCOUNT_OBSERVATION_SQL",
"LOCK_ACCOUNT_STATE_SQL",
"LOCK_ACCOUNT_OBSERVATION_SQL",
"ON CONFLICT (pubkey, slot, state_hash) DO NOTHING",
"ON CONFLICT (observation_key) DO NOTHING",
"FOR UPDATE",
"persist_raw_account_acquisition",
"raw_account_states_equal",
"RawAccountState::try_new",
"RawAccountObservation::new",
"PostgresBackendErrorKind::Conflict",
"PostgresBackendErrorKind::DataInvalid",
"PostgresBackendErrorKind::WrongNetwork",
"PostgresBackendErrorKind::WriteFailed",
] {
assert!(raw.contains(required), "missing private RAW account read mapping contract: {required}");
assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}");
}
for forbidden in ["INSERT INTO", "UPDATE ", "DELETE FROM", "ON CONFLICT", "FOR UPDATE", " OFFSET ", "list_raw_account_states"] {
assert!(!raw.contains(forbidden), "pre.004 account module contains write/pagination material: {forbidden}");
assert!(runtime.contains("pub async fn persist_raw_account_acquisition"));
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET ", "list_raw_account_states", "record_raw_account_observation"] {
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
@@ -181,10 +189,10 @@ fn pre_004_raw_account_read_sql_and_mapping_remain_backend_private_and_read_only
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] {
assert!(!runtime.contains(forbidden), "pre.004 opened RawAccount trait scope prematurely: {forbidden}");
assert!(!runtime.contains(forbidden), "pre.005 opened RawAccount trait scope prematurely: {forbidden}");
}
for forbidden in ["std::env", "dotenv", "ksp_store_lib", "ksp_config_lib"] {
assert!(!raw.contains(forbidden), "RAW account module contains forbidden ownership material: {forbidden}");
for forbidden in ["std::env", "dotenv", "ksp_store_lib", "ksp_config_lib", "sqlx::", "SELECT *"] {
assert!(!raw.contains(forbidden), "RAW account module contains forbidden ownership/query material: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
// version: 10
// version: 11
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -82,6 +82,12 @@ fn pre_004_raw_account_read_bridge_uses_only_backend_independent_models() {
return;
}
#[test]
fn pre_005_raw_account_acquisition_bridge_uses_only_backend_independent_models_and_outcomes() {
let _acquisition = ksp_store_postgres_lib::PostgresBackend::persist_raw_account_acquisition;
return;
}
#[test]
fn pre_004_raw_read_bridge_uses_only_backend_independent_models() {
let _get = ksp_store_postgres_lib::PostgresBackend::get_raw_transaction;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
// version: 1
// version: 2
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
@@ -219,3 +219,106 @@ fn pre_004_account_wrong_network_guard_is_pre_io_and_static() {
assert_eq!(error.phase(), "raw_account_state_network");
return;
}
#[test]
fn pre_005_account_acquisition_input_guard_requires_network_and_exact_reference() {
let backend = network();
let valid_state = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
};
let valid_observation = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
};
assert!(super::ensure_acquisition_inputs(&backend, &valid_state, &valid_observation).is_ok());
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
};
let foreign_state = match super::decode_raw_account_state_row(&foreign, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign account state rejected: {error:?}"),
};
let foreign_observation = match super::decode_raw_account_observation_row(&foreign, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign account observation rejected: {error:?}"),
};
let error = super::ensure_acquisition_inputs(&backend, &foreign_state, &foreign_observation).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
let mut mismatched_row = observation_row();
mismatched_row.account_state_hash = vec![9; 32];
let mismatched_observation = match super::decode_raw_account_observation_row(&backend, mismatched_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid mismatched observation model rejected: {error:?}"),
};
let error = super::ensure_acquisition_inputs(&backend, &valid_state, &mismatched_observation).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
return;
}
#[test]
fn pre_005_account_state_idempotence_compares_complete_content_not_state_hash_only() {
let backend = network();
let first = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
};
let identical = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid identical account state rejected: {error:?}"),
};
assert!(super::raw_account_states_equal(&first, &identical));
let mut divergent_row = state_row();
divergent_row.lamports_text = (u64::MAX - 1).to_string();
let divergent = match super::decode_raw_account_state_row(&backend, divergent_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid divergent account state rejected: {error:?}"),
};
assert_eq!(first.reference(), divergent.reference());
assert!(!super::raw_account_states_equal(&first, &divergent));
return;
}
#[test]
fn pre_005_account_observation_idempotence_compares_optional_metadata_exactly() {
let backend = network();
let first = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
};
let identical = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid identical account observation rejected: {error:?}"),
};
assert_eq!(first, identical);
let mut divergent_row = observation_row();
divergent_row.write_version_text = std::option::Option::Some((u64::MAX - 1).to_string());
let divergent = match super::decode_raw_account_observation_row(&backend, divergent_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid divergent account observation rejected: {error:?}"),
};
assert_eq!(first.observation_key(), divergent.observation_key());
assert_eq!(first.account(), divergent.account());
assert_ne!(first, divergent);
return;
}
#[test]
fn pre_005_account_origin_encoding_is_exact_and_static() {
let cases = [
(ksp_store_api::RawAcquisitionOrigin::Backfill, "backfill"),
(ksp_store_api::RawAcquisitionOrigin::Import, "import"),
(ksp_store_api::RawAcquisitionOrigin::Live, "live"),
(ksp_store_api::RawAcquisitionOrigin::Repair, "repair"),
(ksp_store_api::RawAcquisitionOrigin::Replay, "replay"),
];
for (origin, expected) in cases {
let encoded = match super::encode_origin(origin) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account origin rejected: {error:?}"),
};
assert_eq!(encoded, expected);
}
return;
}

308
deltas/0.3.4/pre.005.md Normal file
View File

@@ -0,0 +1,308 @@
<!-- file: deltas/0.3.4/pre.005.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.005` — acquisition atomique et idempotence `RawAccountState`
## 1. Base requise
```text
0.3.4-pre.4.fix.1
```
Le gate opérateur fourni le 2026-08-30 pour `pre.004-fix.001` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 141 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 52 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
Le mapping/read account de `pre.004` est donc considéré acquis avant l'ouverture des writes d'acquisition.
## 2. Objectif
Implémenter uniquement l'écriture atomique du couple :
```text
RawAccountState
+
RawAccountObservation
```
avec les invariants figés par `pre.001` :
```text
guards réseau/référence avant I/O
state insert idempotent
collision state -> FOR UPDATE + comparaison exacte
observation insert idempotente
collision observation -> FOR UPDATE + comparaison exacte
commit unique
aucun overwrite silencieux
```
L'observation supplémentaire sans retransmettre les bytes account reste réservée à `pre.006`.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.5
```
Aucune crate ne redéfinit localement la version héritée.
## 4. Préconditions avant I/O
`persist_raw_account_acquisition` valide avant `pool.get()` :
```text
state.reference.network == backend.network
observation.account.network == backend.network
observation.account == state.reference
```
Un mauvais réseau devient `WrongNetwork`. Une référence observation/state divergente devient `Conflict`. Aucune connexion PostgreSQL n'est consommée avant ces gardes.
## 5. Insertion canonique du state
Le state utilise :
```text
INSERT INTO ksp_raw_account_states (...)
ON CONFLICT (pubkey, slot, state_hash) DO NOTHING
RETURNING pubkey
```
Les domaines `u64` restent encodés sans narrowing via chaînes décimales et cast PostgreSQL `TEXT::NUMERIC` :
```text
slot
lamports
rent_epoch
```
Les pubkeys/hash restent en bytes fixes et `data` est persistée sans base64 ni copie structurale.
Si l'INSERT gagne :
```text
RawEntityWriteOutcome::Inserted
```
Si l'identité existe déjà, le backend charge la ligne canonique sous :
```text
FOR UPDATE
```
et compare exactement :
```text
reference
lamports
owner
executable
rent_epoch
data
```
Le `state_hash` ne suffit donc jamais à déclarer l'idempotence. Contenu identique -> `AlreadyPresent`; contenu divergent -> `Conflict`.
## 6. Insertion idempotente de l'observation
L'observation est encodée avec toute la provenance et toutes les metadata optionnelles :
```text
observation_key
account pubkey/slot/state_hash
provider
protocol
acquisition_method
origin
received_at
capture_session_id
commitment
endpoint_id
filter_id
observed_at
source_payload_hash
source_payload_size_bytes
is_startup
transaction_signature
write_version
```
Le SQL utilise :
```text
ON CONFLICT (observation_key) DO NOTHING
```
En cas de collision, la ligne existante est relue `FOR UPDATE`, décodée avec le codec hostile-row de `pre.004`, puis comparée au `RawAccountObservation` entrant complet. Identique -> `AlreadyPresent`; divergent -> `Conflict`.
Aucun champ optionnel n'est ignoré dans l'idempotence.
## 7. Atomicité
State et observation vivent dans la même transaction PostgreSQL :
```text
BEGIN
state insert/compare
observation insert/compare
COMMIT
```
Si un nouveau state a été inséré mais que l'observation collide avec un contenu divergent, le chemin retourne une erreur avant commit. Le drop de la transaction annule alors l'insertion du state : aucune moitié d'acquisition durable n'est admise.
Aucun pré-check `has_*`, aucun `ON CONFLICT DO UPDATE`, aucun `UPDATE` et aucun `DELETE` account n'est introduit.
## 8. Surface backend
`PostgresBackend` expose le bridge étroit :
```text
persist_raw_account_acquisition(
RawAccountState,
RawAccountObservation,
) -> RawAcquisitionWriteOutcome
```
Le bridge reste backend-agnostic et n'expose aucun type PostgreSQL.
Cette méthode n'implémente pas encore `RawAccountStateWrite`. Les quatre traits account restent volontairement absents jusqu'à `pre.008` afin d'ouvrir la conformance physique 10/10 en une seule tranche.
## 9. Tests et canaries
Les tests unitaires ajoutés couvrent :
```text
guard réseau state
référence observation/state exacte
state identique -> comparaison vraie
même référence/hash + lamports divergent -> comparaison fausse
observation identique -> égalité exacte
même clé/référence + write_version divergent -> non-identique
encodage exact des cinq RawAcquisitionOrigin
```
Le canari `dependency_boundary` exige maintenant :
```text
INSERT_ACCOUNT_STATE_SQL
INSERT_ACCOUNT_OBSERVATION_SQL
LOCK_ACCOUNT_STATE_SQL
LOCK_ACCOUNT_OBSERVATION_SQL
ON CONFLICT ... DO NOTHING
FOR UPDATE
persist_raw_account_acquisition
```
et continue d'interdire :
```text
UPDATE / DELETE account
ON CONFLICT DO UPDATE
record_raw_account_observation
list_raw_account_states
OFFSET
les quatre impl RawAccount*
```
`public_api.rs` vérifie le nouveau bridge backend-agnostic.
## 10. Scope explicitement reporté
```text
record_raw_account_observation pre.006
races/cancellation ciblées pre.006
list_raw_account_states pre.007
cursor KSPA pre.007
4 impl RawAccount* PostgresBackend pre.008
4 dispatch Store pre.008
live PostgreSQL account pre.009
```
## 11. Migrations
Aucune ressource sous `migrations/` n'est modifiée.
Checksums conservés :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 12. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 13. Fichier ajouté
```text
deltas/0.3.4/pre.005.md
```
## 14. Fichiers supprimés
```text
aucun
```
## 15. 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/0.3.4
PASS
```
Les contrôles différentiels doivent également confirmer l'identité byte-à-byte de V000/V001/V002 et l'absence de surface `pre.006+`.
## 16. Validations non exécutées dans l'environnement d'assemblage
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo post-`pre.005` n'est revendiqué ici.
Gate opérateur à rejouer :
```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/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 17. Verdict
`pre.005` ouvre uniquement l'acquisition atomique `RawAccountState + RawAccountObservation`. L'observation supplémentaire, les races/cancellations dédiées, la pagination et les capabilities account restent fermées conformément au sizing souple de `0.3.4`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
@@ -617,17 +617,21 @@ La tranche reste strictement read-only : aucun `INSERT`, `UPDATE`, `DELETE`, `ON
#### `pre.004-fix.001` — conformité Clippy du mapping `Row`
**Statut : réalisé ; gate opérateur complet à rejouer.**
**Statut : réalisé ; gate opérateur complet PASS.**
Le gate opérateur de `pre.004` confirme `cargo check --workspace`, les suites ciblées `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib` (52 tests), `ksp-config-lib` et `ksp-store-lib --no-default-features`, tandis que `cargo clippy --workspace --all-targets` rejette les 27 extractions `Row::try_get(...).map_err(...)?` du nouveau module. Chaque occurrence produit conjointement `clippy::question-mark-used` et `clippy::implicit-return`, conformément aux interdictions `RUST-ERR-002/003`. Le même gate signale aussi un `crate_root` inutilisé dans le canari `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
Le correctif déplie les extractions physiques en `match` typés avec retour `DataInvalid` explicite et supprime uniquement la variable de test inutilisée. Aucun SELECT, phase d'erreur, type physique, invariant de mapping, signature de bridge, test fonctionnel, migration ou checksum n'est modifié. La version Cargo devient `0.3.4-pre.4.fix.1` conformément à `VER-ID-007/010`, car deux fichiers Rust sont modifiés.
Le correctif déplie les extractions physiques en `match` typés avec retour `DataInvalid` explicite et supprime uniquement la variable de test inutilisée. Aucun SELECT, phase d'erreur, type physique, invariant de mapping, signature de bridge, test fonctionnel, migration ou checksum n'est modifié. La version Cargo devient `0.3.4-pre.4.fix.1` conformément à `VER-ID-007/010`, car deux fichiers Rust sont modifiés. Le gate opérateur complet du correctif est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
### `pre.005` — Acquisition atomique et idempotence
**Statut : planifié.**
**Statut : réalisé ; gate opérateur complet à rejouer.**
Budget cible : **15-20 min**. Implémenter l'acquisition atomique state+observation, l'idempotence exacte et la classification des conflits.
Budget cible : **15-20 min**. L'acquisition account est désormais une transaction PostgreSQL unique : guard réseau et identité exacte avant I/O, `INSERT ... ON CONFLICT DO NOTHING` du state, verrouillage `FOR UPDATE` et comparaison exacte de `lamports/owner/executable/rent_epoch/data` en cas de collision, puis insertion idempotente de l'observation avec comparaison exacte de la référence, de toute la provenance et des metadata optionnelles. Une divergence retourne `Conflict` sans overwrite.
Si le state vient d'être inséré mais que l'observation collide avec un contenu divergent, le retour d'erreur abandonne la transaction et ne laisse donc aucun state orphelin durable. Le bridge étroit `PostgresBackend::persist_raw_account_acquisition` est ouvert sans implémenter `RawAccountStateWrite`; les quatre traits account restent fermés jusqu'à `pre.008`.
`record_raw_account_observation`, `list_raw_account_states`, le cursor `KSPA`, tout `UPDATE`/`DELETE` account et tout `ON CONFLICT DO UPDATE` restent absents. `pre.006` conserve la responsabilité de l'observation supplémentaire et des scénarios de concurrence/cancellation ciblés.
### `pre.006` — Observation supplémentaire et concurrence unitaire

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
@@ -274,8 +274,8 @@ Le test live account devra être opt-in/ignored, URI stdin, sans environnement n
| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | DONE |
| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PASS |
| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PASS |
| pre.004 | mapping privé state/observation + get reads + hostile rows | RECHECK |
| pre.005 | acquisition atomique state+observation + idempotence/conflict | PLANNED |
| pre.004 | mapping privé state/observation + get reads + hostile rows | PASS |
| pre.005 | acquisition atomique state+observation + idempotence/conflict | RECHECK |
| pre.006 | observation supplémentaire + races/cancellation unitaires | PLANNED |
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PLANNED |
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PLANNED |
@@ -399,7 +399,7 @@ Gate opérateur de `pre.003-fix.001` du 2026-08-30 : **PASS complet** — audits
## 21. Verdict `pre.004`
Mapping PostgreSQL privé account : **tests opérateur PASS ; Clippy `pre.004` FAIL ; `pre.004-fix.001` appliqué statiquement, gate complet à rejouer**.
Mapping PostgreSQL privé account : **PASS final via `pre.004-fix.001`**. Le gate initial `pre.004` avait échoué uniquement sur Clippy ; le gate complet du correctif est désormais vert.
Surface matérialisée :
@@ -447,5 +447,32 @@ Invariants du fix :
- aucun write, cursor, trait account ou dispatch Store ouvert ;
- `workspace.package.version = 0.3.4-pre.4.fix.1`.
Validations exécutées dans l'environnement d'assemblage du fix : audit Rust général/export/workspace **PASS**, audit Markdown **PASS**, absence de `?` dans `src/raw_account.rs` confirmée. Le gate opérateur complet doit rejouer `cargo fmt`, audits, check, Clippy et suites ciblées après application du delta.
Validations exécutées dans l'environnement d'assemblage du fix : audit Rust général/export/workspace **PASS**, audit Markdown **PASS**, absence de `?` dans `src/raw_account.rs` confirmée. Gate opérateur du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (52 tests), Config et `--no-default-features` sont verts.
## 22. Verdict `pre.005`
Acquisition atomique account : **PASS statique ; gate opérateur complet à rejouer**.
Surface ajoutée :
```text
PostgresBackend::persist_raw_account_acquisition
INSERT state ON CONFLICT DO NOTHING
SELECT state FOR UPDATE sur collision
comparaison exacte du state canonique complet
INSERT observation ON CONFLICT DO NOTHING
SELECT observation FOR UPDATE sur collision
comparaison exacte référence + provenance + metadata optionnelles
COMMIT unique state + observation
```
Les préconditions `state.reference.network == backend.network`, `observation.account.network == backend.network` et `observation.account == state.reference` sont évaluées avant `pool.get()`. Une divergence de référence ou de contenu est classée `Conflict`; un échec physique d'écriture reste `WriteFailed`, sans texte PostgreSQL conservé.
L'idempotence state ne fait aucune confiance exclusive à `state_hash` : un même `(pubkey, slot, state_hash)` n'est `AlreadyPresent` que si `lamports`, `owner`, `executable`, `rent_epoch` et `data` sont identiques. L'idempotence observation compare le modèle `RawAccountObservation` complet, y compris `is_startup`, `transaction_signature`, `write_version` et toute la provenance.
La transaction commune garantit qu'une collision divergente sur l'observation après insertion d'un nouveau state ne peut pas laisser ce state durable isolément. Aucun `has_*`, `ON CONFLICT DO UPDATE`, `UPDATE` ou `DELETE` account n'est introduit.
Canaries `pre.005` : guard réseau/référence, comparaison state complète, comparaison observation complète, encodage origin fermé, SQL `DO NOTHING + FOR UPDATE`, bridge backend-agnostic et absence des quatre implémentations `RawAccount*`.
Reste explicitement hors tranche : `record_raw_account_observation` (`pre.006`), concurrence/cancellation ciblées (`pre.006`), pagination/cursor (`pre.007`) et implémentations/dispatch 10/10 (`pre.008`). Les migrations V000/V001/V002 restent inchangées et le checksum V002 final demeure `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.