v0.2.5-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 152
|
# version: 153
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.5-pre.5.fix.1"
|
version = "0.2.5-pre.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -30,6 +30,7 @@ tracing = { version = "^0.1", default-features = false }
|
|||||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
tokio = { version = "^1.53", default-features = false }
|
tokio = { version = "^1.53", default-features = false }
|
||||||
|
tempfile = { version = "^3.27" }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
chrono = { version = "^0.4", default-features = false }
|
||||||
tauri = { version = "^2.11" }
|
tauri = { version = "^2.11" }
|
||||||
tauri-build = { version = "^2.6" }
|
tauri-build = { version = "^2.6" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 49 -->
|
<!-- version: 50 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
|
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
|
||||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||||
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
||||||
- [/] `0.2.5` — Wallet foundation : `pre.001` fixe le threat model et le format V1 autonome ; `pre.002` crée `ksp-wallet-lib` avec capabilities VIEW/OWNER et frontières ; `pre.003` fige le wire strict, key slots, transcript OWNER/AAD et la spécification externe ; `pre.004` matérialise Argon2id/XChaCha20-Poly1305/CSPRNG et le wrapping ; `pre.005` fixe le profil de création KSP issu du benchmark, les payloads owner-control/metadata/secret, l’autorité Ed25519 séparée et les flux in-memory create/open VIEW/OWNER avec vecteur complet interopérable. `Pubkey` reste consommée uniquement via `ksp-core-lib`; Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. `pre.006` porte ensuite la persistence atomique/no-clobber ; signature Solana publique, administration/rotations puis import/export restent répartis jusqu’à `pre.010` ; `WalletPolicy` reste exclu.
|
- [/] `0.2.5` — Wallet foundation : `pre.001` fixe le threat model et le format V1 autonome ; `pre.002` crée `ksp-wallet-lib` avec capabilities VIEW/OWNER et frontières ; `pre.003` fige le wire strict, key slots, transcript OWNER/AAD et la spécification externe ; `pre.004` matérialise Argon2id/XChaCha20-Poly1305/CSPRNG et le wrapping ; `pre.005` fixe le profil de création KSP issu du benchmark, les payloads owner-control/metadata/secret, l’autorité Ed25519 séparée et les flux in-memory create/open VIEW/OWNER avec vecteur complet interopérable. `Pubkey` reste consommée uniquement via `ksp-core-lib`; Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. `pre.006` ajoute maintenant la persistence async-first bornée, la création no-clobber par temp file same-directory synchronisé, les ouvertures VIEW/OWNER depuis fichier et les fault/concurrency canaries ; signature Solana publique, administration/rotations puis import/export restent répartis jusqu’à `pre.010` ; `WalletPolicy` reste exclu.
|
||||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
||||||
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
||||||
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-wallet-lib/Cargo.toml
|
# file: crates/ksp-wallet-lib/Cargo.toml
|
||||||
# version: 5
|
# version: 6
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-wallet-lib"
|
name = "ksp-wallet-lib"
|
||||||
@@ -19,6 +19,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
solana-keypair.workspace = true
|
solana-keypair.workspace = true
|
||||||
tokio = { workspace = true, features = ["rt"] }
|
tokio = { workspace = true, features = ["rt"] }
|
||||||
|
tempfile.workspace = true
|
||||||
zeroize.workspace = true
|
zeroize.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
|
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
|
||||||
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
|
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
|
||||||
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
|
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
|
||||||
//! Solana transaction signing and filesystem persistence remain outside this tranche. Public
|
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. Solana
|
||||||
|
//! transaction signing and OWNER/VIEW administration remain outside this tranche. Public
|
||||||
//! keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral observability uses only
|
//! keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral observability uses only
|
||||||
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ mod metadata;
|
|||||||
mod owner;
|
mod owner;
|
||||||
mod password;
|
mod password;
|
||||||
mod payload;
|
mod payload;
|
||||||
|
mod persistence;
|
||||||
mod transcript;
|
mod transcript;
|
||||||
mod view;
|
mod view;
|
||||||
mod wallet;
|
mod wallet;
|
||||||
@@ -149,6 +151,14 @@ pub use self::owner::WalletOwner;
|
|||||||
pub use self::password::OwnerPassword;
|
pub use self::password::OwnerPassword;
|
||||||
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
|
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
|
||||||
pub use self::password::ViewPassword;
|
pub use self::password::ViewPassword;
|
||||||
|
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
|
||||||
|
pub use self::persistence::create_wallet_file_v1;
|
||||||
|
/// Reads and verifies a locked native `.kspwallet` V1 file.
|
||||||
|
pub use self::persistence::inspect_locked_wallet_file_v1;
|
||||||
|
/// Opens a native `.kspwallet` V1 file with OWNER capability.
|
||||||
|
pub use self::persistence::open_wallet_owner_file_v1;
|
||||||
|
/// Opens a native `.kspwallet` V1 file with VIEW capability.
|
||||||
|
pub use self::persistence::open_wallet_view_file_v1;
|
||||||
/// Authorized VIEW capability handle.
|
/// Authorized VIEW capability handle.
|
||||||
pub use self::view::WalletView;
|
pub use self::view::WalletView;
|
||||||
/// Creates a new in-memory native Wallet V1.
|
/// Creates a new in-memory native Wallet V1.
|
||||||
|
|||||||
275
crates/ksp-wallet-lib/src/persistence.rs
Normal file
275
crates/ksp-wallet-lib/src/persistence.rs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// file: crates/ksp-wallet-lib/src/persistence.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Async-first native Wallet V1 filesystem persistence.
|
||||||
|
|
||||||
|
use std::io::{Read as _, Write as _};
|
||||||
|
|
||||||
|
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
|
||||||
|
///
|
||||||
|
/// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then
|
||||||
|
/// published with no-clobber semantics. The destination directory must already exist; Wallet never discovers or creates a configured default directory.
|
||||||
|
pub async fn create_wallet_file_v1(
|
||||||
|
destination: impl std::convert::AsRef<std::path::Path>,
|
||||||
|
owner_password: crate::OwnerPassword,
|
||||||
|
view_password: std::option::Option<crate::ViewPassword>,
|
||||||
|
metadata: crate::WalletCreateMetadataV1,
|
||||||
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||||
|
let destination = destination.as_ref().to_path_buf();
|
||||||
|
let owner_result = crate::create_wallet_v1(owner_password, view_password, metadata).await;
|
||||||
|
let owner = match owner_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let serialized_result = owner.to_json_bytes();
|
||||||
|
let serialized = match serialized_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let persist_result = persist_new_wallet_async(destination, serialized).await;
|
||||||
|
if let std::result::Result::Err(error) = persist_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
operation = "wallet_create_file",
|
||||||
|
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||||
|
"native wallet persisted with no-clobber semantics"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a native `.kspwallet` V1 from `source` with VIEW capability.
|
||||||
|
///
|
||||||
|
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow.
|
||||||
|
pub async fn open_wallet_view_file_v1(
|
||||||
|
source: impl std::convert::AsRef<std::path::Path>,
|
||||||
|
password: crate::ViewPassword,
|
||||||
|
) -> ksp_core_lib::Result<crate::WalletView> {
|
||||||
|
let source = source.as_ref().to_path_buf();
|
||||||
|
let bytes_result = read_wallet_file_async(source).await;
|
||||||
|
let bytes = match bytes_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return crate::open_wallet_view_v1(bytes.as_slice(), password).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a native `.kspwallet` V1 from `source` with OWNER capability.
|
||||||
|
///
|
||||||
|
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow.
|
||||||
|
pub async fn open_wallet_owner_file_v1(
|
||||||
|
source: impl std::convert::AsRef<std::path::Path>,
|
||||||
|
password: crate::OwnerPassword,
|
||||||
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||||
|
let source = source.as_ref().to_path_buf();
|
||||||
|
let bytes_result = read_wallet_file_async(source).await;
|
||||||
|
let bytes = match bytes_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return crate::open_wallet_owner_v1(bytes.as_slice(), password).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF.
|
||||||
|
pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||||
|
let source = source.as_ref().to_path_buf();
|
||||||
|
let bytes_result = read_wallet_file_async(source).await;
|
||||||
|
let bytes = match bytes_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return crate::inspect_locked_wallet_v1(bytes.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||||
|
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
||||||
|
return match task.await {
|
||||||
|
std::result::Result::Ok(result) => result,
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), content.as_slice()));
|
||||||
|
return match task.await {
|
||||||
|
std::result::Result::Ok(result) => result,
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("create_task", error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||||
|
let opened = std::fs::File::open(source);
|
||||||
|
let file = match opened {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(io_error("open", error)),
|
||||||
|
};
|
||||||
|
let metadata_result = file.metadata();
|
||||||
|
let metadata = match metadata_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(io_error("metadata", error)),
|
||||||
|
};
|
||||||
|
if metadata.len() > crate::KSPWALLET_MAX_FILE_BYTES as u64 {
|
||||||
|
return std::result::Result::Err(oversized_document_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let capacity = std::cmp::min(metadata.len(), crate::KSPWALLET_MAX_FILE_BYTES as u64) as usize;
|
||||||
|
let mut bytes = std::vec::Vec::with_capacity(capacity);
|
||||||
|
let mut bounded = file.take((crate::KSPWALLET_MAX_FILE_BYTES + 1) as u64);
|
||||||
|
let read_result = bounded.read_to_end(&mut bytes);
|
||||||
|
if let std::result::Result::Err(error) = read_result {
|
||||||
|
return std::result::Result::Err(io_error("read", error));
|
||||||
|
}
|
||||||
|
if bytes.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
||||||
|
return std::result::Result::Err(oversized_document_error());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist_new_wallet_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||||
|
return persist_new_wallet_with_hook(destination, content, || return std::result::Result::Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist_new_wallet_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
|
||||||
|
where
|
||||||
|
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
||||||
|
{
|
||||||
|
if content.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
||||||
|
return std::result::Result::Err(oversized_document_error());
|
||||||
|
}
|
||||||
|
let parent = destination_parent(destination);
|
||||||
|
let parent = match parent {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if destination.file_name().is_none() {
|
||||||
|
return std::result::Result::Err(atomic_error("destination", "Wallet destination has no file name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let temporary_result = tempfile::Builder::new().prefix(".kspwallet-write-").suffix(".tmp").tempfile_in(parent);
|
||||||
|
let mut temporary = match temporary_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(atomic_io_error("temporary_create", error)),
|
||||||
|
};
|
||||||
|
let write_result = temporary.write_all(content);
|
||||||
|
if let std::result::Result::Err(error) = write_result {
|
||||||
|
return std::result::Result::Err(atomic_io_error("temporary_write", error));
|
||||||
|
}
|
||||||
|
let sync_result = temporary.as_file().sync_all();
|
||||||
|
if let std::result::Result::Err(error) = sync_result {
|
||||||
|
return std::result::Result::Err(atomic_io_error("temporary_sync", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let hook_result = before_publish();
|
||||||
|
if let std::result::Result::Err(error) = hook_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let persist_result = temporary.persist_noclobber(destination);
|
||||||
|
let persisted = match persist_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
if error.error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||||
|
return std::result::Result::Err(destination_exists_error());
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(atomic_io_error("publish_noclobber", error.error));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let final_sync_result = persisted.sync_all();
|
||||||
|
if let std::result::Result::Err(error) = final_sync_result {
|
||||||
|
return std::result::Result::Err(atomic_io_error("published_file_sync", error));
|
||||||
|
}
|
||||||
|
sync_parent_directory_best_effort(parent);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination_parent(destination: &std::path::Path) -> ksp_core_lib::Result<&std::path::Path> {
|
||||||
|
return match destination.parent() {
|
||||||
|
std::option::Option::Some(parent) if !parent.as_os_str().is_empty() => std::result::Result::Ok(parent),
|
||||||
|
std::option::Option::Some(_) => std::result::Result::Ok(std::path::Path::new(".")),
|
||||||
|
std::option::Option::None => std::result::Result::Err(atomic_error("destination", "Wallet destination has no parent directory")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn sync_parent_directory_best_effort(parent: &std::path::Path) {
|
||||||
|
let opened = std::fs::File::open(parent);
|
||||||
|
let directory = match opened {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
operation = "wallet_parent_directory_sync",
|
||||||
|
io_kind = ?error.kind(),
|
||||||
|
"wallet publication succeeded but parent-directory durability sync could not start"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let std::result::Result::Err(error) = directory.sync_all() {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
operation = "wallet_parent_directory_sync",
|
||||||
|
io_kind = ?error.kind(),
|
||||||
|
"wallet publication succeeded but parent-directory durability sync failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn sync_parent_directory_best_effort(_parent: &std::path::Path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oversized_document_error() -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet document exceeds the V1 size limit").with_context("field", "document");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination_exists_error() -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_DESTINATION_EXISTS, "Wallet destination already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn io_error(operation: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_IO_FAILED, "Wallet filesystem I/O failed")
|
||||||
|
.with_context("operation", operation)
|
||||||
|
.with_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atomic_error(operation: &'static str, message: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, message).with_context("operation", operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atomic_io_error(operation: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, "Atomic Wallet persistence failed")
|
||||||
|
.with_context("operation", operation)
|
||||||
|
.with_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blocking_io_error(operation: &'static str, source: tokio::task::JoinError) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_IO_FAILED, "Wallet filesystem task failed")
|
||||||
|
.with_context("operation", operation)
|
||||||
|
.with_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blocking_atomic_error(operation: &'static str, source: tokio::task::JoinError) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, "Atomic Wallet persistence task failed")
|
||||||
|
.with_context("operation", operation)
|
||||||
|
.with_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn persist_new_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||||
|
return persist_new_wallet_with_hook(destination, content, || {
|
||||||
|
return std::result::Result::Err(atomic_error("fault_injection", "Injected Wallet persistence failure before publication"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn persist_new_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||||
|
return persist_new_wallet_blocking(destination, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/persistence.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Wallet-specific dependency and ownership canaries.
|
//! Wallet-specific dependency and ownership canaries.
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
|
|||||||
assert!(manifest.contains("serde_json.workspace = true"));
|
assert!(manifest.contains("serde_json.workspace = true"));
|
||||||
assert!(manifest.contains("solana-keypair.workspace = true"));
|
assert!(manifest.contains("solana-keypair.workspace = true"));
|
||||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
|
||||||
|
assert!(manifest.contains("tempfile.workspace = true"));
|
||||||
assert!(manifest.contains("zeroize.workspace = true"));
|
assert!(manifest.contains("zeroize.workspace = true"));
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
"ksp-config-lib",
|
"ksp-config-lib",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Public API canaries for the Wallet foundation.
|
//! Public API canaries for the Wallet foundation.
|
||||||
|
|
||||||
@@ -92,3 +92,23 @@ fn public_pre_005_create_open_and_calibrated_defaults_are_available_from_crate_r
|
|||||||
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM, 1);
|
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM, 1);
|
||||||
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES, 32);
|
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_pre_006_file_persistence_surface_is_available_from_crate_root() {
|
||||||
|
let path = std::path::Path::new("not-polled.kspwallet");
|
||||||
|
let owner_password = ksp_wallet_lib::OwnerPassword::new("public-pre006-owner-password").expect("public test OWNER password must be valid");
|
||||||
|
let create_future =
|
||||||
|
ksp_wallet_lib::create_wallet_file_v1(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadataV1::default());
|
||||||
|
drop(create_future);
|
||||||
|
|
||||||
|
let view_password = ksp_wallet_lib::ViewPassword::new("public-pre006-view-password").expect("public test VIEW password must be valid");
|
||||||
|
let view_future = ksp_wallet_lib::open_wallet_view_file_v1(path, view_password);
|
||||||
|
drop(view_future);
|
||||||
|
|
||||||
|
let owner_password = ksp_wallet_lib::OwnerPassword::new("public-pre006-owner-password").expect("public test OWNER password must be valid");
|
||||||
|
let owner_future = ksp_wallet_lib::open_wallet_owner_file_v1(path, owner_password);
|
||||||
|
drop(owner_future);
|
||||||
|
|
||||||
|
let inspect_future = ksp_wallet_lib::inspect_locked_wallet_file_v1(path);
|
||||||
|
drop(inspect_future);
|
||||||
|
}
|
||||||
|
|||||||
130
crates/ksp-wallet-lib/unit_tests/persistence.rs
Normal file
130
crates/ksp-wallet-lib/unit_tests/persistence.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// file: crates/ksp-wallet-lib/unit_tests/persistence.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn temp_directory() -> std::io::Result<tempfile::TempDir> {
|
||||||
|
return tempfile::tempdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_clobber_create_keeps_the_first_published_document() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("wallet.kspwallet");
|
||||||
|
let first = b"first-wallet";
|
||||||
|
let second = b"second-wallet";
|
||||||
|
|
||||||
|
crate::persistence::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
|
||||||
|
let second_result = crate::persistence::persist_new_wallet_for_test(destination.as_path(), second);
|
||||||
|
let error = second_result.expect_err("second publication must not overwrite an existing wallet");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_DESTINATION_EXISTS);
|
||||||
|
let persisted = std::fs::read(destination.as_path()).expect("published wallet must remain readable");
|
||||||
|
assert_eq!(persisted, first);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_failure_before_publish_leaves_no_destination_or_partial_wallet() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("wallet.kspwallet");
|
||||||
|
let result = crate::persistence::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
|
||||||
|
let error = result.expect_err("fault injection must abort before publication");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
|
||||||
|
assert!(!destination.exists());
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(directory.path()).expect("Wallet persistence test directory must remain readable");
|
||||||
|
let count = entries.count();
|
||||||
|
assert_eq!(count, 0, "temporary artifacts should be cleaned on ordinary error unwinding");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concurrent_no_clobber_publish_has_exactly_one_winner() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("wallet.kspwallet");
|
||||||
|
let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
|
||||||
|
let mut handles = std::vec::Vec::new();
|
||||||
|
|
||||||
|
for index in 0_u8..8 {
|
||||||
|
let destination = destination.clone();
|
||||||
|
let barrier = std::sync::Arc::clone(&barrier);
|
||||||
|
handles.push(std::thread::spawn(move || {
|
||||||
|
barrier.wait();
|
||||||
|
let content = [index; 32];
|
||||||
|
return crate::persistence::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut success_count = 0_usize;
|
||||||
|
let mut exists_count = 0_usize;
|
||||||
|
let mut unexpected_code = std::option::Option::None;
|
||||||
|
for handle in handles {
|
||||||
|
let result = handle.join().expect("Wallet persistence test worker must not panic");
|
||||||
|
match result {
|
||||||
|
std::result::Result::Ok(()) => success_count += 1,
|
||||||
|
std::result::Result::Err(error) if error.code() == crate::ERROR_CODE_DESTINATION_EXISTS => exists_count += 1,
|
||||||
|
std::result::Result::Err(error) => unexpected_code = std::option::Option::Some(error.code()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(unexpected_code, std::option::Option::None);
|
||||||
|
assert_eq!(success_count, 1);
|
||||||
|
assert_eq!(exists_count, 7);
|
||||||
|
assert_eq!(std::fs::metadata(destination.as_path()).expect("winning wallet must exist").len(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounded_reader_rejects_oversized_wallet_before_parser_allocation() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("oversized.kspwallet");
|
||||||
|
let oversized = std::vec![b'x'; crate::KSPWALLET_MAX_FILE_BYTES + 1];
|
||||||
|
std::fs::write(destination.as_path(), oversized).expect("oversized fixture must be writable");
|
||||||
|
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
||||||
|
let result = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path()));
|
||||||
|
let error = result.expect_err("oversized Wallet file must be rejected before parsing");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_file_create_and_locked_inspect_round_trip_without_revealing_identity() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("created.kspwallet");
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
||||||
|
let owner_password = crate::OwnerPassword::new("pre006-owner-password").expect("test OWNER password must be valid");
|
||||||
|
let created = runtime
|
||||||
|
.block_on(crate::create_wallet_file_v1(destination.as_path(), owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default()))
|
||||||
|
.expect("native Wallet file creation must succeed");
|
||||||
|
let locked = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path())).expect("persisted Wallet must inspect successfully");
|
||||||
|
|
||||||
|
assert!(!locked.view_enabled());
|
||||||
|
assert_eq!(created.capability(), crate::WalletCapability::Owner);
|
||||||
|
assert!(destination.exists());
|
||||||
|
let second_password = crate::OwnerPassword::new("pre006-other-owner-password").expect("second test OWNER password must be valid");
|
||||||
|
let second = runtime.block_on(crate::create_wallet_file_v1(
|
||||||
|
destination.as_path(),
|
||||||
|
second_password,
|
||||||
|
std::option::Option::None,
|
||||||
|
crate::WalletCreateMetadataV1::default(),
|
||||||
|
));
|
||||||
|
assert_eq!(second.expect_err("create must remain no-clobber").code(), crate::ERROR_CODE_DESTINATION_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
|
||||||
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
||||||
|
let destination = directory.path().join("vector.kspwallet");
|
||||||
|
let vector = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
||||||
|
crate::persistence::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
|
||||||
|
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
||||||
|
let view_password = crate::ViewPassword::new("pre005-view-password").expect("test VIEW password must be valid");
|
||||||
|
let view = runtime
|
||||||
|
.block_on(crate::open_wallet_view_file_v1(destination.as_path(), view_password))
|
||||||
|
.expect("persisted full vector must open through VIEW file API");
|
||||||
|
let owner_password = crate::OwnerPassword::new("pre005-owner-password").expect("test OWNER password must be valid");
|
||||||
|
let owner = runtime
|
||||||
|
.block_on(crate::open_wallet_owner_file_v1(destination.as_path(), owner_password))
|
||||||
|
.expect("persisted full vector must open through OWNER file API");
|
||||||
|
|
||||||
|
assert_eq!(view.capability(), crate::WalletCapability::View);
|
||||||
|
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
|
||||||
|
assert_eq!(view.pubkey(), owner.pubkey());
|
||||||
|
assert_eq!(view.alias(), std::option::Option::Some("pre005-vector-wallet"));
|
||||||
|
assert_eq!(owner.alias(), std::option::Option::Some("pre005-vector-wallet"));
|
||||||
|
}
|
||||||
155
deltas/0.2.5/pre.006.md
Normal file
155
deltas/0.2.5/pre.006.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<!-- file: deltas/0.2.5/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.5-pre.006` — persistence native async/no-clobber
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.5-pre.5.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur de cette base a validé `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-wallet-lib`, `cargo test --workspace` et l'audit Cargo tree. Une seule génération `ed25519-dalek 2.2.0` est utilisée et `solana-address 2.7.0` est unifié entre Core et `solana-keypair`.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.5-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise la persistence native `.kspwallet` sans Config, environnement, Transport ou Tauri :
|
||||||
|
|
||||||
|
```text
|
||||||
|
create_wallet_file_v1(path, ...)
|
||||||
|
open_wallet_view_file_v1(path, ...)
|
||||||
|
open_wallet_owner_file_v1(path, ...)
|
||||||
|
inspect_locked_wallet_file_v1(path)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le chemin est toujours fourni explicitement par le caller. Wallet ne possède et ne découvre aucun répertoire par défaut.
|
||||||
|
|
||||||
|
## Persistence retenue
|
||||||
|
|
||||||
|
`tempfile ^3.27` est ajouté sous `[workspace.dependencies]` puis consommé par `ksp-wallet-lib` avec `tempfile.workspace = true`.
|
||||||
|
|
||||||
|
La création native suit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
create crypto in-memory
|
||||||
|
-> serialize locked JSON
|
||||||
|
-> NamedTempFile dans le même répertoire que la destination
|
||||||
|
-> write_all
|
||||||
|
-> sync_all(temp)
|
||||||
|
-> persist_noclobber(destination)
|
||||||
|
-> sync_all(fichier publié)
|
||||||
|
-> parent-directory sync best-effort sur Unix
|
||||||
|
```
|
||||||
|
|
||||||
|
`create` ne propose aucun overwrite. Une destination existante retourne `wallet.destination_exists` même sous concurrence.
|
||||||
|
|
||||||
|
La documentation upstream de `tempfile 3.27.0` précise que `persist_noclobber` ne peut pas être présenté comme atomique sur absolument toutes les plateformes/filesystems et peut laisser un hard-link temporaire dans certains scénarios de crash. KSP conserve donc une documentation explicite de cette limite : no-clobber est le contrat, tandis que la durabilité power-loss universelle n'est pas revendiquée.
|
||||||
|
|
||||||
|
## Async / lecture bornée
|
||||||
|
|
||||||
|
Les opérations filesystem bloquantes sont regroupées dans `tokio::task::spawn_blocking`; aucune feature Tokio filesystem supplémentaire n'est requise.
|
||||||
|
|
||||||
|
La lecture :
|
||||||
|
|
||||||
|
- ouvre le fichier dans la frontière blocking ;
|
||||||
|
- vérifie d'abord la taille metadata ;
|
||||||
|
- utilise ensuite une lecture bornée à `KSPWALLET_MAX_FILE_BYTES + 1` afin de couvrir aussi un fichier qui grossirait pendant la lecture ;
|
||||||
|
- rejette un document surdimensionné avant parsing/KDF.
|
||||||
|
|
||||||
|
Les wrappers fichier délèguent ensuite aux flows `pre.005` déjà validés et n'introduisent aucune seconde implémentation crypto.
|
||||||
|
|
||||||
|
## Fault / concurrence
|
||||||
|
|
||||||
|
Nouveaux canaries déterministes :
|
||||||
|
|
||||||
|
- première publication no-clobber conservée face à une seconde tentative ;
|
||||||
|
- fault injecté après `sync_all(temp)` mais avant publication : aucune destination partielle ;
|
||||||
|
- cleanup du temp lors d'un retour d'erreur ordinaire ;
|
||||||
|
- huit créateurs concurrents : exactement un succès et sept `destination_exists` ;
|
||||||
|
- rejet pré-parser d'un fichier supérieur à 1 MiB ;
|
||||||
|
- création publique vers fichier puis projection locked ;
|
||||||
|
- le vecteur complet `pre.005` peut être publié puis ouvert par les nouvelles API VIEW et OWNER depuis fichier.
|
||||||
|
|
||||||
|
Un kill/power-loss brutal peut laisser un artifact temporaire selon l'OS/filesystem ; la tranche ne prétend pas simuler ni garantir le cleanup après arrêt non déroulé.
|
||||||
|
|
||||||
|
## Frontières conservées
|
||||||
|
|
||||||
|
```text
|
||||||
|
Wallet -> ksp-core-lib
|
||||||
|
Wallet -> ksp-logging-lib
|
||||||
|
Wallet -> primitives crypto/Solana retenues
|
||||||
|
Wallet -> tempfile
|
||||||
|
|
||||||
|
Wallet -X-> Config
|
||||||
|
Wallet -X-> Transport
|
||||||
|
Wallet -X-> ExecutionPolicy
|
||||||
|
Wallet -X-> Store
|
||||||
|
Wallet -X-> Tauri
|
||||||
|
Wallet -X-> tracing direct
|
||||||
|
Wallet -X-> solana-pubkey direct
|
||||||
|
```
|
||||||
|
|
||||||
|
`Pubkey` reste consommée exclusivement via `ksp_core_lib::Pubkey`. `solana-keypair` reste propriété de Wallet parce qu'il encapsule du matériau secret et une capacité de signature ; aucune keypair brute n'est remontée dans Core.
|
||||||
|
|
||||||
|
## Replace administratif
|
||||||
|
|
||||||
|
`pre.006` n'expose volontairement pas une primitive publique générale d'overwrite/replace. Le remplacement atomique d'un wallet existant sera introduit en `pre.007` à travers les seules opérations autorisées : administration OWNER et self-rotation du password VIEW. Cela évite de donner à un consumer de niveau inférieur une primitive arbitraire `replace(path, bytes)`.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-wallet-lib/src/persistence.rs
|
||||||
|
crates/ksp-wallet-lib/unit_tests/persistence.rs
|
||||||
|
deltas/0.2.5/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
crates/ksp-wallet-lib/Cargo.toml
|
||||||
|
crates/ksp-wallet-lib/src/lib.rs
|
||||||
|
crates/ksp-wallet-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-wallet-lib/tests/public_api.rs
|
||||||
|
docs/formats/KSPWALLET_V1.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation opérateur requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-wallet-lib
|
||||||
|
cargo test --workspace
|
||||||
|
|
||||||
|
cargo tree -p ksp-wallet-lib
|
||||||
|
cargo tree -p ksp-wallet-lib -d
|
||||||
|
cargo tree -i tempfile@3.27.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Points à contrôler dans le tree : pas de nouvelle dépendance Config/Transport/Tauri, `tempfile` uniquement par Wallet dans cette tranche et aucune duplication crypto nouvelle injustifiée.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après validation, `0.2.5-pre.007` porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
signature Solana publique sans getter secret
|
||||||
|
alias/notes OWNER
|
||||||
|
rotation password VIEW par VIEW
|
||||||
|
rotation password VIEW par OWNER sans ancien password VIEW
|
||||||
|
rotation password OWNER
|
||||||
|
suppression/recréation VIEW
|
||||||
|
révocation forte VIEW avec rekey metadata
|
||||||
|
replace atomique lié aux mutations autorisées
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/formats/KSPWALLET_V1.md -->
|
<!-- file: docs/formats/KSPWALLET_V1.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# `.kspwallet` V1 — spécification du format natif Wallet KSP
|
# `.kspwallet` V1 — spécification du format natif Wallet KSP
|
||||||
|
|
||||||
@@ -764,7 +764,7 @@ Une création KSP V1 en mémoire suit conceptuellement :
|
|||||||
10. vérifier immédiatement la signature produite avant de retourner le handle OWNER.
|
10. vérifier immédiatement la signature produite avant de retourner le handle OWNER.
|
||||||
```
|
```
|
||||||
|
|
||||||
Les opérations Argon2id sont exécutées hors du thread executor async par une frontière blocking dédiée. La création `pre.005` est **in-memory** : la persistence arrive en `pre.006`.
|
Les opérations Argon2id sont exécutées hors du thread executor async par une frontière blocking dédiée. La construction cryptographique reste entièrement en mémoire ; `pre.006` ajoute ensuite la publication filesystem no-clobber décrite en section 23.
|
||||||
|
|
||||||
### 21.2 Ouverture VIEW
|
### 21.2 Ouverture VIEW
|
||||||
|
|
||||||
@@ -830,3 +830,22 @@ wallet JSON complet
|
|||||||
|
|
||||||
Le vecteur a été généré et revérifié indépendamment du code Rust avec Argon2id, XChaCha20-Poly1305 construit via HChaCha20 + ChaCha20-Poly1305 IETF et Ed25519. Une implémentation externe conforme doit pouvoir reproduire les mêmes dérivations, déchiffrements et vérifications à partir des deux fichiers sans dépendre d'un type Rust KSP.
|
Le vecteur a été généré et revérifié indépendamment du code Rust avec Argon2id, XChaCha20-Poly1305 construit via HChaCha20 + ChaCha20-Poly1305 IETF et Ed25519. Une implémentation externe conforme doit pouvoir reproduire les mêmes dérivations, déchiffrements et vérifications à partir des deux fichiers sans dépendre d'un type Rust KSP.
|
||||||
|
|
||||||
|
## 23. Persistence native matérialisée par `pre.006`
|
||||||
|
|
||||||
|
La persistence ne modifie aucun octet du wire V1. Elle définit uniquement comment un document V1 complet est publié et relu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
caller path explicite
|
||||||
|
-> temp file unique dans le même répertoire
|
||||||
|
-> écriture complète des bytes JSON verrouillés
|
||||||
|
-> sync_all(temp)
|
||||||
|
-> persist_noclobber(destination)
|
||||||
|
-> sync_all(fichier publié)
|
||||||
|
-> sync parent directory best-effort sur Unix
|
||||||
|
```
|
||||||
|
|
||||||
|
`create_wallet_file_v1` n'écrase jamais une destination existante. Une collision ou une course concurrente retourne `wallet.destination_exists`; il n'existe aucun mode overwrite pour create/import. Les ouvertures fichier sont bornées à `KSPWALLET_MAX_FILE_BYTES` avant parser/KDF et délèguent ensuite exactement aux procédures VIEW/OWNER des sections 21.2/21.3.
|
||||||
|
|
||||||
|
La couche filesystem est async-first, mais les appels OS bloquants sont regroupés derrière `tokio::task::spawn_blocking`. KSP ne lit ni Config ni environnement pour choisir le chemin et ne journalise pas les chemins par défaut.
|
||||||
|
|
||||||
|
La durabilité est décrite sans surpromesse : `persist_noclobber` ne garantit pas une atomicité universelle sur tous les filesystems et un crash brutal peut laisser un artefact ou hard-link temporaire complet. KSP garantit le no-clobber et l'absence de publication partielle dans le flux normal et dans les fault tests avant publication ; il ne garantit pas la suppression des temporaires après kill/power-loss ni une durabilité identique de l'entrée de répertoire sur tous les OS. Ces limites concernent la persistence et ne changent pas les garanties cryptographiques du format.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 50 -->
|
<!-- version: 51 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# Séquence des releases fonctionnelles KSP
|
||||||
|
|
||||||
@@ -416,6 +416,8 @@ La release doit fournir `docs/formats/KSPWALLET_V1.md` comme spécification sép
|
|||||||
|
|
||||||
`0.2.5-pre.005` consomme ce benchmark (`64 MiB / 3 / 1 ≈ 1742 ms`, `128 MiB ≈ 3459 ms`, `256 MiB ≈ 6925 ms` sur la machine opérateur) et retient pour les créations KSP V1 `65 536 KiB / 3 / 1` avec salt 32 octets, tout en conservant les paramètres sérialisés comme autorité de lecture de chaque wallet. La tranche fixe les payloads `owner_control` (seed Ed25519 d’administration + `K_metadata` + `K_secret`), metadata strictes et secret Solana 64 octets, ajoute une autorité Ed25519 séparée de la keypair Solana et vérifie sa signature d’état avant tout KDF. Les API in-memory `create_wallet_v1`, `open_wallet_view_v1`, `open_wallet_owner_v1` et `inspect_locked_wallet_v1` matérialisent l’indépendance VIEW/OWNER : VIEW ne déchiffre jamais `owner_control`/secret, OWNER ne dépend pas de VIEW et la Pubkey reste un `ksp_core_lib::Pubkey`. Un vecteur `.kspwallet` complet test-only, revérifiable hors Rust, ferme l’interop de cette tranche. La persistence filesystem reste explicitement `pre.006`; signature Solana publique et mutations/rotations restent `pre.007+`.
|
`0.2.5-pre.005` consomme ce benchmark (`64 MiB / 3 / 1 ≈ 1742 ms`, `128 MiB ≈ 3459 ms`, `256 MiB ≈ 6925 ms` sur la machine opérateur) et retient pour les créations KSP V1 `65 536 KiB / 3 / 1` avec salt 32 octets, tout en conservant les paramètres sérialisés comme autorité de lecture de chaque wallet. La tranche fixe les payloads `owner_control` (seed Ed25519 d’administration + `K_metadata` + `K_secret`), metadata strictes et secret Solana 64 octets, ajoute une autorité Ed25519 séparée de la keypair Solana et vérifie sa signature d’état avant tout KDF. Les API in-memory `create_wallet_v1`, `open_wallet_view_v1`, `open_wallet_owner_v1` et `inspect_locked_wallet_v1` matérialisent l’indépendance VIEW/OWNER : VIEW ne déchiffre jamais `owner_control`/secret, OWNER ne dépend pas de VIEW et la Pubkey reste un `ksp_core_lib::Pubkey`. Un vecteur `.kspwallet` complet test-only, revérifiable hors Rust, ferme l’interop de cette tranche. La persistence filesystem reste explicitement `pre.006`; signature Solana publique et mutations/rotations restent `pre.007+`.
|
||||||
|
|
||||||
|
`0.2.5-pre.006` ajoute la persistence native sans Config : `create_wallet_file_v1` reçoit un chemin explicite du caller et publie uniquement en no-clobber via un fichier temporaire créé dans le même répertoire, écrit puis `sync_all` avant `persist_noclobber`; `open_wallet_view_file_v1`, `open_wallet_owner_file_v1` et `inspect_locked_wallet_file_v1` effectuent une lecture bornée à la limite V1 avant de déléguer au parser/crypto acquis. Les opérations filesystem bloquantes sont isolées par `spawn_blocking`. Les tests couvrent destination existante, concurrence avec un seul gagnant, fault injection avant publication, cleanup ordinaire des temporaires et rejet d'un fichier surdimensionné. La synchronisation du répertoire parent est best-effort sur Unix et n'est pas transformée en garantie portable de crash-durability. Les ACL/permissions OS restent hors du modèle Wallet.
|
||||||
|
|
||||||
## `0.2.6` — Wallet Desk
|
## `0.2.6` — Wallet Desk
|
||||||
|
|
||||||
Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince.
|
Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Plan `0.2.5` — Wallet foundation
|
# Plan `0.2.5` — Wallet foundation
|
||||||
|
|
||||||
@@ -432,14 +432,14 @@ L'absence d'une ligne « audited » dans ce document ne signifie donc pas « sû
|
|||||||
|
|
||||||
### 8.7 Encodage et persistence
|
### 8.7 Encodage et persistence
|
||||||
|
|
||||||
État au terme de `pre.005` :
|
État au terme de `pre.006` :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
base64 0.23.1 acquis depuis pre.003 pour Base64url sans padding des champs binaires JSON
|
base64 0.23.1 acquis depuis pre.003 pour Base64url sans padding des champs binaires JSON
|
||||||
tempfile 3.27.0 candidate pre.006 pour temp files same-directory + persist/persist_noclobber
|
tempfile 3.27.0 acquis en pre.006 pour temp files same-directory + publication no-clobber
|
||||||
```
|
```
|
||||||
|
|
||||||
`tempfile` ne sera ajouté que si `pre.006` confirme sa sémantique portable et son besoin réel pour l'atomicité/no-clobber.
|
`pre.006` retient `NamedTempFile::new_in` et `persist_noclobber` parce que la publication reste sur le même filesystem et ne peut pas écraser une destination existante. La documentation upstream précise que `persist_noclobber` n'est pas atomique sur absolument toutes les plateformes/filesystems et peut laisser un hard-link temporaire après certains crashes : KSP documente cette limite au lieu de surpromettre une durabilité universelle.
|
||||||
|
|
||||||
## 9. Format natif `.kspwallet` V1
|
## 9. Format natif `.kspwallet` V1
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ Pour créer/remplacer un `.kspwallet` :
|
|||||||
6. synchroniser le répertoire parent lorsque la plateforme le permet ;
|
6. synchroniser le répertoire parent lorsque la plateforme le permet ;
|
||||||
7. ne jamais considérer un fichier partiellement écrit comme nouveau wallet valide.
|
7. ne jamais considérer un fichier partiellement écrit comme nouveau wallet valide.
|
||||||
|
|
||||||
`tempfile::NamedTempFile`/`persist_noclobber`/`persist` est la candidate de `pre.006`, complétée par les fsync nécessaires. Sa sémantique exacte doit être testée par plateforme avant de revendiquer la durabilité crash-safe.
|
`pre.006` retient `tempfile::NamedTempFile` créé dans le même répertoire que la destination et `persist_noclobber` pour les créations. Le temp file est synchronisé avant publication puis le fichier publié est resynchronisé. Sur Unix, une synchronisation du répertoire parent est tentée en best-effort ; son échec est journalisé sans transformer une publication déjà réussie en échec ambigu. KSP ne revendique donc pas une durabilité power-loss/crash-safe universelle sur tous les OS/filesystems.
|
||||||
|
|
||||||
### 14.2 No-clobber
|
### 14.2 No-clobber
|
||||||
|
|
||||||
@@ -803,7 +803,7 @@ L'import ne fournit aucun mode overwrite/replace d'un `.kspwallet` existant et n
|
|||||||
|
|
||||||
### 14.4 Écriture interrompue
|
### 14.4 Écriture interrompue
|
||||||
|
|
||||||
Un crash peut laisser un temp artifact complet à nettoyer ; l'invariant prioritaire est de ne pas transformer l'ancien wallet valide en fichier partiellement remplacé. Les ACL/permissions du système d'exploitation ne font pas partie des garanties de `ksp-wallet-lib`.
|
`pre.006` teste un fault injecté après écriture+`sync_all` du temp file mais avant publication : aucune destination partielle n'apparaît et le temp est nettoyé lors d'un retour d'erreur normal. Un crash brutal du processus peut néanmoins laisser un temp artifact/hard-link complet selon la plateforme ; KSP ne traite jamais ces noms temporaires comme le wallet destination et ne promet pas leur suppression après kill/power-loss. L'invariant prioritaire est que `create` ne publie jamais un contenu partiel et ne remplace jamais une destination existante. Les ACL/permissions du système d'exploitation ne font pas partie des garanties de `ksp-wallet-lib`.
|
||||||
|
|
||||||
## 15. Erreurs et non-oracle
|
## 15. Erreurs et non-oracle
|
||||||
|
|
||||||
@@ -1049,7 +1049,20 @@ create/open KDF via spawn_blocking
|
|||||||
vecteur complet test-only généré et revérifié indépendamment du Rust
|
vecteur complet test-only généré et revérifié indépendamment du Rust
|
||||||
```
|
```
|
||||||
|
|
||||||
La persistence, la signature Solana publique, les mutations metadata et les rotations restent volontairement absentes de cette tranche.
|
La signature Solana publique, les mutations metadata et les rotations restent volontairement absentes de cette tranche ; la persistence est désormais acquise par `pre.006`.
|
||||||
|
|
||||||
|
### 19.8 État acquis après `pre.006`
|
||||||
|
|
||||||
|
`pre.006` ajoute la frontière filesystem sans Config ni environnement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
create_wallet_file_v1(path, ...) -> create in-memory -> JSON verrouillé -> temp same-directory -> sync_all -> persist_noclobber
|
||||||
|
open_wallet_view_file_v1(path, ...) -> bounded read -> open VIEW acquis
|
||||||
|
open_wallet_owner_file_v1(path, ...) -> bounded read -> open OWNER acquis
|
||||||
|
inspect_locked_wallet_file_v1(path) -> bounded read -> signature-state verify sans KDF
|
||||||
|
```
|
||||||
|
|
||||||
|
La lecture est bornée à `KSPWALLET_MAX_FILE_BYTES` avant parsing, y compris si le fichier grossit entre metadata et lecture. Le filesystem blocking est exécuté via `tokio::task::spawn_blocking`. La création concurrente a exactement un gagnant ; les autres reçoivent `wallet.destination_exists`. Aucun répertoire n'est créé ou choisi par Wallet. Le replace administratif reste volontairement pour `pre.007`, où il pourra être lié à une capability OWNER ou à la self-rotation VIEW au lieu d'exposer une primitive générale d'overwrite.
|
||||||
|
|
||||||
## 20. Sizing
|
## 20. Sizing
|
||||||
|
|
||||||
@@ -1110,7 +1123,7 @@ Toutes les dépendances tierces communes restent centralisées sous `[workspace.
|
|||||||
Candidates restantes, à réauditer juste avant insertion :
|
Candidates restantes, à réauditer juste avant insertion :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
tempfile ^3.27 # pre.006 si la sémantique atomic/no-clobber est confirmée
|
tempfile ^3.27 # acquis pre.006 : temp same-directory + persist_noclobber
|
||||||
solana-signer ^3.0 # seulement si un contrat public/impl l'exige réellement
|
solana-signer ^3.0 # seulement si un contrat public/impl l'exige réellement
|
||||||
solana-signature ^3.5 # seulement si le type public l'exige
|
solana-signature ^3.5 # seulement si le type public l'exige
|
||||||
```
|
```
|
||||||
@@ -1218,4 +1231,4 @@ Une future `format_version >= 2` pourra réétudier des facteurs/ancrages extern
|
|||||||
|
|
||||||
## 26. Suite immédiate
|
## 26. Suite immédiate
|
||||||
|
|
||||||
`0.2.5-pre.003` fige le codec JSON strict, les limites structurelles, `slot_id` 16 octets, le descripteur VIEW, les DTOs d’enveloppe/key slots, les TLV transcript/AAD et la première spécification `docs/formats/KSPWALLET_V1.md`. `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS et le wrapping de content keys. Le benchmark opérateur a ensuite permis à `pre.005` de retenir le profil initial `64 MiB / 3 / 1`, de figer les payloads `owner_control`/metadata/secret, d'introduire l'autorité Ed25519 OWNER distincte de la keypair Solana, de créer/ouvrir réellement VIEW et OWNER en mémoire et de publier un vecteur `.kspwallet` complet interopérable. **La suite immédiate est `pre.006` : persistence async/atomique/no-clobber**, sans déplacer de logique filesystem dans Config.
|
`0.2.5-pre.003` fige le codec JSON strict, les limites structurelles, `slot_id` 16 octets, le descripteur VIEW, les DTOs d’enveloppe/key slots, les TLV transcript/AAD et la première spécification `docs/formats/KSPWALLET_V1.md`. `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS et le wrapping de content keys. Le benchmark opérateur a ensuite permis à `pre.005` de retenir le profil initial `64 MiB / 3 / 1`, de figer les payloads `owner_control`/metadata/secret, d'introduire l'autorité Ed25519 OWNER distincte de la keypair Solana, de créer/ouvrir réellement VIEW et OWNER en mémoire et de publier un vecteur `.kspwallet` complet interopérable. **La suite immédiate est `pre.007` : signature Solana publique + administration metadata/passwords/VIEW**, sans déplacer de logique filesystem dans Config.
|
||||||
|
|||||||
Reference in New Issue
Block a user