v0.3.4-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 371
|
||||
# version: 372
|
||||
|
||||
[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.5"
|
||||
version = "0.3.4-pre.6"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||
// version: 18
|
||||
// version: 19
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -28,8 +28,9 @@
|
||||
//! 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. `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.
|
||||
//! with exact idempotence/conflict classification. `0.3.4-pre.006` adds additional
|
||||
//! account observations guarded by the existing state reference while pagination 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
|
||||
@@ -75,6 +76,8 @@ pub(crate) use self::raw_account::get_raw_account_observation;
|
||||
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 additional RAW account observation writer consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::record_raw_account_observation;
|
||||
/// 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.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
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_REFERENCE_SQL: &str =
|
||||
"SELECT 1 FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR KEY SHARE";
|
||||
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 {
|
||||
@@ -107,10 +109,7 @@ 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() {
|
||||
@@ -189,6 +188,53 @@ pub(crate) async fn persist_raw_account_acquisition(
|
||||
return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(entity_outcome, observation_outcome));
|
||||
}
|
||||
|
||||
/// Persists one additional acquisition observation for an already durable RAW account state.
|
||||
pub(crate) async fn record_raw_account_observation(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
|
||||
let input_result = ensure_observation_write_input(network, &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_observation_begin")),
|
||||
};
|
||||
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 reference_result = sql_transaction
|
||||
.query_opt(LOCK_ACCOUNT_REFERENCE_SQL, &[&account_pubkey_bytes, &account_slot_text.as_str(), &account_state_hash_bytes])
|
||||
.await;
|
||||
match reference_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(_)) => {},
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(reference_not_found("raw_account_observation_reference"));
|
||||
},
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_lock_reference")),
|
||||
}
|
||||
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
|
||||
let 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_observation_commit"));
|
||||
}
|
||||
return std::result::Result::Ok(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,
|
||||
@@ -508,16 +554,7 @@ async fn insert_account_state(
|
||||
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,
|
||||
],
|
||||
&[&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 {
|
||||
@@ -670,6 +707,13 @@ fn ensure_acquisition_inputs(
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn ensure_observation_write_input(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation: &ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
return ensure_network(network, observation.account(), "raw_account_observation_write_network");
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -759,6 +803,10 @@ fn data_invalid(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::DataInvalid, phase);
|
||||
}
|
||||
|
||||
fn reference_not_found(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReferenceNotFound, phase);
|
||||
}
|
||||
|
||||
fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
const APPLICATION_NAME: &str = "ksp-store";
|
||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||
@@ -357,6 +357,14 @@ impl PostgresBackend {
|
||||
return crate::persist_raw_account_acquisition(&self.pool, &self.network, state, observation).await;
|
||||
}
|
||||
|
||||
/// Persists one additional RAW account observation for an already durable account state.
|
||||
pub async fn record_raw_account_observation(
|
||||
&self,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
|
||||
return crate::record_raw_account_observation(&self.pool, &self.network, observation).await;
|
||||
}
|
||||
|
||||
/// Reads one canonical RAW transaction without exposing physical PostgreSQL row types.
|
||||
pub async fn get_raw_transaction(
|
||||
&self,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
// version: 19
|
||||
// version: 20
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -180,7 +180,7 @@ fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_later_scope_cl
|
||||
assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}");
|
||||
}
|
||||
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"] {
|
||||
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET ", "list_raw_account_states"] {
|
||||
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
|
||||
}
|
||||
for forbidden in [
|
||||
@@ -197,6 +197,36 @@ fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_later_scope_cl
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_account_additional_observation_is_reference_guarded_and_cancellation_safe() {
|
||||
let raw = include_str!("../src/raw_account.rs");
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for required in [
|
||||
"LOCK_ACCOUNT_REFERENCE_SQL",
|
||||
"FOR KEY SHARE",
|
||||
"record_raw_account_observation",
|
||||
"reference_not_found(\"raw_account_observation_reference\")",
|
||||
"persist_account_observation_row",
|
||||
"RawObservationWriteOutcome",
|
||||
"raw_account_observation_commit",
|
||||
] {
|
||||
assert!(raw.contains(required), "missing pre.006 additional-observation contract: {required}");
|
||||
}
|
||||
assert!(runtime.contains("pub async fn record_raw_account_observation"));
|
||||
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET ", "list_raw_account_states"] {
|
||||
assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}");
|
||||
}
|
||||
for forbidden in [
|
||||
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
|
||||
] {
|
||||
assert!(!runtime.contains(forbidden), "pre.006 opened RawAccount trait scope prematurely: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_raw_read_sql_and_mapping_remain_backend_private() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -88,6 +88,12 @@ fn pre_005_raw_account_acquisition_bridge_uses_only_backend_independent_models_a
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_account_observation_write_bridge_uses_only_backend_independent_models_and_outcomes() {
|
||||
let _observation = ksp_store_postgres_lib::PostgresBackend::record_raw_account_observation;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_raw_read_bridge_uses_only_backend_independent_models() {
|
||||
let _get = ksp_store_postgres_lib::PostgresBackend::get_raw_transaction;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
@@ -322,3 +322,57 @@ fn pre_005_account_origin_encoding_is_exact_and_static() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_additional_observation_input_guard_rejects_wrong_network_before_io() {
|
||||
let backend = network();
|
||||
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 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_observation_write_input(&backend, &observation).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_additional_observation_race_and_cancellation_contract_is_transaction_bounded() {
|
||||
let source = include_str!("../src/raw_account.rs");
|
||||
let start = match source.find("pub(crate) async fn record_raw_account_observation") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("record_raw_account_observation function missing"),
|
||||
};
|
||||
let tail = &source[start..];
|
||||
let end = match tail.find("fn raw_account_observation_db_row") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("record_raw_account_observation boundary missing"),
|
||||
};
|
||||
let function = &tail[..end];
|
||||
let begin = match function.find("client.transaction().await") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation transaction begin missing"),
|
||||
};
|
||||
let key_share = match function.find("LOCK_ACCOUNT_REFERENCE_SQL") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation reference lock missing"),
|
||||
};
|
||||
let persist = match function.find("persist_account_observation_row") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation insert/compare missing"),
|
||||
};
|
||||
let commit = match function.find("sql_transaction.commit().await") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation commit missing"),
|
||||
};
|
||||
assert!(begin < key_share);
|
||||
assert!(key_share < persist);
|
||||
assert!(persist < commit);
|
||||
assert!(!function.contains("tokio::spawn"));
|
||||
assert!(!function.contains("std::mem::forget"));
|
||||
assert!(!function.contains("ON CONFLICT DO UPDATE"));
|
||||
return;
|
||||
}
|
||||
|
||||
148
deltas/0.3.4/pre.006.md
Normal file
148
deltas/0.3.4/pre.006.md
Normal file
@@ -0,0 +1,148 @@
|
||||
<!-- file: deltas/0.3.4/pre.006.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.006` — observation account supplémentaire + contrat race/cancellation
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.5
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.005` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 142 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 — 56 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
|
||||
```
|
||||
|
||||
L'acquisition atomique/idempotente de `pre.005` est donc considérée acquise.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Implémenter uniquement l'observation supplémentaire d'un `RawAccountState` déjà durable :
|
||||
|
||||
```text
|
||||
network guard avant I/O
|
||||
state reference obligatoire
|
||||
FOR KEY SHARE
|
||||
observation idempotente existante
|
||||
aucun state implicite
|
||||
aucun overwrite
|
||||
transaction unique
|
||||
```
|
||||
|
||||
La pagination account et les quatre implémentations de capabilities restent hors tranche.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.6
|
||||
```
|
||||
|
||||
## 4. Observation supplémentaire
|
||||
|
||||
`record_raw_account_observation` valide d'abord :
|
||||
|
||||
```text
|
||||
observation.account.network == backend.network
|
||||
```
|
||||
|
||||
avant `pool.get()`.
|
||||
|
||||
La transaction exécute ensuite :
|
||||
|
||||
```text
|
||||
SELECT 1
|
||||
FROM ksp_raw_account_states
|
||||
WHERE pubkey = $1
|
||||
AND slot = $2::TEXT::NUMERIC
|
||||
AND state_hash = $3
|
||||
FOR KEY SHARE
|
||||
```
|
||||
|
||||
Une référence absente retourne `ReferenceNotFound` avec phase statique `raw_account_observation_reference`. Aucun state n'est créé ou modifié.
|
||||
|
||||
## 5. Idempotence / conflit
|
||||
|
||||
Après le lock de référence, la tranche réutilise `persist_account_observation_row` de `pre.005` :
|
||||
|
||||
```text
|
||||
INSERT ... ON CONFLICT (observation_key) DO NOTHING
|
||||
collision -> SELECT ... FOR UPDATE
|
||||
identique -> AlreadyPresent
|
||||
divergent -> Conflict
|
||||
```
|
||||
|
||||
La comparaison porte sur le `RawAccountObservation` complet : référence, provenance et metadata optionnelles incluses.
|
||||
|
||||
## 6. Concurrence et cancellation unitaires
|
||||
|
||||
Les canaris de `pre.006` figent l'ordre :
|
||||
|
||||
```text
|
||||
client.transaction()
|
||||
FOR KEY SHARE référence
|
||||
persist_account_observation_row
|
||||
sql_transaction.commit()
|
||||
```
|
||||
|
||||
Le chemin ne détache aucune tâche (`tokio::spawn` absent), ne fuit pas le handle (`std::mem::forget` absent) et ne commit pas avant l'insertion/comparaison observation. Si la future est annulée avant le commit, le `Transaction` local est droppé et la transaction PostgreSQL non committée est rollback.
|
||||
|
||||
La preuve concurrente réelle avec tâches simultanées et `JoinHandle::abort()` reste explicitement réservée au live `pre.009`.
|
||||
|
||||
## 7. Bridge backend
|
||||
|
||||
`PostgresBackend` expose :
|
||||
|
||||
```text
|
||||
record_raw_account_observation(
|
||||
RawAccountObservation,
|
||||
) -> RawObservationWriteOutcome
|
||||
```
|
||||
|
||||
Le bridge reste construit uniquement avec des types `ksp-store-api`. `RawAccountObservationWrite` n'est pas encore implémenté ; l'ouverture des quatre traits account reste groupée en `pre.008`.
|
||||
|
||||
## 8. Scope négatif
|
||||
|
||||
Restent absents :
|
||||
|
||||
```text
|
||||
list_raw_account_states
|
||||
cursor KSPA
|
||||
OFFSET
|
||||
UPDATE / DELETE account
|
||||
ON CONFLICT DO UPDATE
|
||||
impl RawAccountStateRead for PostgresBackend
|
||||
impl RawAccountStateWrite for PostgresBackend
|
||||
impl RawAccountObservationRead for PostgresBackend
|
||||
impl RawAccountObservationWrite for PostgresBackend
|
||||
Store dispatch account
|
||||
```
|
||||
|
||||
Toutes les migrations V000/V001/V002 restent byte-inchangées. Le checksum V002 final reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 9. Validations d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
|
||||
migrations V000/V001/V002 byte-identiques à pre.005 PASS
|
||||
scope pre.007+ absent PASS
|
||||
```
|
||||
|
||||
Le gate Cargo/Clippy/tests de `pre.006` doit être rejoué par l'opérateur.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
|
||||
|
||||
@@ -625,19 +625,25 @@ Le correctif déplie les extractions physiques en `match` typés avec retour `Da
|
||||
|
||||
### `pre.005` — Acquisition atomique et idempotence
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet à rejouer.**
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
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.
|
||||
À la clôture de `pre.005`, `record_raw_account_observation`, `list_raw_account_states`, le cursor `KSPA`, tout `UPDATE`/`DELETE` account et tout `ON CONFLICT DO UPDATE` restent absents. Le gate opérateur complet du 2026-08-30 est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
|
||||
|
||||
### `pre.006` — Observation supplémentaire et concurrence unitaire
|
||||
|
||||
**Statut : planifié.**
|
||||
**Statut : réalisé ; gate opérateur complet à rejouer.**
|
||||
|
||||
Budget cible : **15-20 min**. Ajouter l'observation supplémentaire et isoler les scénarios de race/cancellation unitaires sans élargir la surface de navigation.
|
||||
Budget cible : **15-20 min**. `PostgresBackend::record_raw_account_observation` ouvre le bridge étroit d'observation supplémentaire sans implémenter encore `RawAccountObservationWrite`. Le guard `observation.account.network == backend.network` est évalué avant `pool.get()`. La transaction verrouille ensuite la référence `(pubkey, slot, state_hash)` existante avec `SELECT 1 ... FOR KEY SHARE`; l'absence retourne `ReferenceNotFound` et aucun state n'est créé implicitement.
|
||||
|
||||
L'insertion réutilise exactement `persist_account_observation_row` de `pre.005` : `ON CONFLICT (observation_key) DO NOTHING`, puis `SELECT ... FOR UPDATE` et comparaison du `RawAccountObservation` complet sur collision. Une observation identique retourne `AlreadyPresent`; une collision divergente retourne `Conflict`, sans overwrite.
|
||||
|
||||
Les canaris unitaires figent l'ordre transactionnel `BEGIN -> FOR KEY SHARE -> insert/compare observation -> COMMIT`. Aucun `spawn`, `mem::forget` ou commit anticipé n'est admis dans ce chemin : si la future est annulée avant le commit, le `Transaction` local est droppé et PostgreSQL rollback la mutation non committée. Cette tranche prouve donc la structure de cancellation/race ; la preuve avec vraies tâches concurrentes et `JoinHandle::abort()` reste volontairement au live `pre.009`.
|
||||
|
||||
`list_raw_account_states`, le cursor `KSPA`, `OFFSET`, tout `UPDATE`/`DELETE` account, `ON CONFLICT DO UPDATE` et les quatre `impl RawAccount* for PostgresBackend` restent absents.
|
||||
|
||||
### `pre.007` — Liste account et cursor keyset V1
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
|
||||
|
||||
@@ -275,8 +275,8 @@ Le test live account devra être opt-in/ignored, URI stdin, sans environnement n
|
||||
| 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 | PASS |
|
||||
| pre.005 | acquisition atomique state+observation + idempotence/conflict | RECHECK |
|
||||
| pre.006 | observation supplémentaire + races/cancellation unitaires | PLANNED |
|
||||
| pre.005 | acquisition atomique state+observation + idempotence/conflict | PASS |
|
||||
| pre.006 | observation supplémentaire + races/cancellation unitaires | RECHECK |
|
||||
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PLANNED |
|
||||
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PLANNED |
|
||||
| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | PLANNED |
|
||||
@@ -451,7 +451,7 @@ Validations exécutées dans l'environnement d'assemblage du fix : audit Rust g
|
||||
|
||||
## 22. Verdict `pre.005`
|
||||
|
||||
Acquisition atomique account : **PASS statique ; gate opérateur complet à rejouer**.
|
||||
Acquisition atomique account : **PASS complet opérateur**.
|
||||
|
||||
Surface ajoutée :
|
||||
|
||||
@@ -474,5 +474,31 @@ La transaction commune garantit qu'une collision divergente sur l'observation ap
|
||||
|
||||
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`.
|
||||
Le gate opérateur du 2026-08-30 est **PASS complet** : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (56 tests unitaires), Config et `ksp-store-lib --no-default-features` sont verts.
|
||||
|
||||
Reste après `pre.005` : observation supplémentaire/concurrence ciblée (`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`.
|
||||
|
||||
## 23. Verdict `pre.006`
|
||||
|
||||
Observation supplémentaire account : **PASS statique ; gate opérateur complet à rejouer**.
|
||||
|
||||
Surface ajoutée :
|
||||
|
||||
```text
|
||||
PostgresBackend::record_raw_account_observation
|
||||
network guard avant pool.get()
|
||||
SELECT state reference FOR KEY SHARE
|
||||
state absent -> ReferenceNotFound
|
||||
INSERT observation ON CONFLICT DO NOTHING
|
||||
collision -> SELECT observation FOR UPDATE + comparaison exacte
|
||||
COMMIT unique
|
||||
```
|
||||
|
||||
Le state référencé doit être déjà durable : `record_raw_account_observation` ne crée jamais de `RawAccountState` et ne retransmet pas ses bytes. Le lock `FOR KEY SHARE` protège la référence pendant l'insertion observation/FK sans prendre un lock de contenu plus fort que nécessaire.
|
||||
|
||||
L'idempotence et le conflit réutilisent le chemin `persist_account_observation_row` déjà validé en `pre.005`. Une même clé avec observation identique produit `AlreadyPresent`; une même clé avec contenu/provenance/metadata divergents produit `Conflict`. Le réseau de l'observation est rejeté avant toute acquisition de connexion lorsqu'il diffère du backend.
|
||||
|
||||
La preuve de race/cancellation de cette tranche est volontairement unitaire/structurelle : les canaris imposent l'ordre `transaction -> FOR KEY SHARE -> persist/compare -> commit`, l'absence de commit anticipé et l'absence de tâche détachée ou de fuite volontaire du handle transactionnel. Une cancellation avant commit droppe donc le `Transaction` local et laisse le rollback à PostgreSQL. Les scénarios de vraies tâches concurrentes et `JoinHandle::abort()` restent au test live `pre.009`.
|
||||
|
||||
Restent fermés : `list_raw_account_states`, cursor `KSPA`, `OFFSET`, `UPDATE`/`DELETE`, `ON CONFLICT DO UPDATE`, les quatre implémentations `RawAccount*` sur `PostgresBackend` et tout dispatch account dans `Store`. Les migrations V000/V001/V002 sont inchangées ; le checksum V002 final reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user