From fccb7d876c1536768abb4195ebeb375b17ca32f6 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 30 Aug 2026 22:31:02 +0200 Subject: [PATCH] v0.3.4-pre.006 --- Cargo.toml | 4 +- crates/ksp-store-postgres-lib/src/lib.rs | 9 +- .../ksp-store-postgres-lib/src/raw_account.rs | 78 +++++++-- crates/ksp-store-postgres-lib/src/runtime.rs | 10 +- .../tests/dependency_boundary.rs | 34 +++- .../tests/public_api.rs | 8 +- .../unit_tests/raw_account.rs | 56 ++++++- deltas/0.3.4/pre.006.md | 148 ++++++++++++++++++ ...-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md | 16 +- .../021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md | 36 ++++- 10 files changed, 364 insertions(+), 35 deletions(-) create mode 100644 deltas/0.3.4/pre.006.md diff --git a/Cargo.toml b/Cargo.toml index f706936..7e27285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/ksp-store-postgres-lib/src/lib.rs b/crates/ksp-store-postgres-lib/src/lib.rs index de0117e..3f2662a 100644 --- a/crates/ksp-store-postgres-lib/src/lib.rs +++ b/crates/ksp-store-postgres-lib/src/lib.rs @@ -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. diff --git a/crates/ksp-store-postgres-lib/src/raw_account.rs b/crates/ksp-store-postgres-lib/src/raw_account.rs index 66709f2..130665c 100644 --- a/crates/ksp-store-postgres-lib/src/raw_account.rs +++ b/crates/ksp-store-postgres-lib/src/raw_account.rs @@ -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 { + 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 { let account_pubkey: std::vec::Vec = 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); } diff --git a/crates/ksp-store-postgres-lib/src/runtime.rs b/crates/ksp-store-postgres-lib/src/runtime.rs index af78617..622d890 100644 --- a/crates/ksp-store-postgres-lib/src/runtime.rs +++ b/crates/ksp-store-postgres-lib/src/runtime.rs @@ -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 { + 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, diff --git a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs index ca2b8c6..aae0597 100644 --- a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs @@ -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"); diff --git a/crates/ksp-store-postgres-lib/tests/public_api.rs b/crates/ksp-store-postgres-lib/tests/public_api.rs index 9d14032..9ef19f1 100644 --- a/crates/ksp-store-postgres-lib/tests/public_api.rs +++ b/crates/ksp-store-postgres-lib/tests/public_api.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; diff --git a/crates/ksp-store-postgres-lib/unit_tests/raw_account.rs b/crates/ksp-store-postgres-lib/unit_tests/raw_account.rs index b4e4885..50fcd0f 100644 --- a/crates/ksp-store-postgres-lib/unit_tests/raw_account.rs +++ b/crates/ksp-store-postgres-lib/unit_tests/raw_account.rs @@ -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; +} diff --git a/deltas/0.3.4/pre.006.md b/deltas/0.3.4/pre.006.md new file mode 100644 index 0000000..c51764d --- /dev/null +++ b/deltas/0.3.4/pre.006.md @@ -0,0 +1,148 @@ + + + +# 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. diff --git a/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md index bb25118..b7f262d 100644 --- a/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md +++ b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md @@ -1,5 +1,5 @@ - + # 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 diff --git a/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md index 983ac5e..ade440f 100644 --- a/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md +++ b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md @@ -1,5 +1,5 @@ - + # 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`.