v0.3.4-pre.006

This commit is contained in:
2026-08-30 22:31:02 +02:00
parent 50d4142797
commit fccb7d876c
10 changed files with 364 additions and 35 deletions

View File

@@ -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"

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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");

View File

@@ -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;

View File

@@ -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
View 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.

View File

@@ -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

View File

@@ -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`.