v0.2.5-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-wallet-lib/Cargo.toml
|
||||
# version: 5
|
||||
# version: 6
|
||||
|
||||
[package]
|
||||
name = "ksp-wallet-lib"
|
||||
@@ -19,6 +19,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
solana-keypair.workspace = true
|
||||
tokio = { workspace = true, features = ["rt"] }
|
||||
tempfile.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![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
|
||||
//! 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.
|
||||
//! 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
|
||||
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
|
||||
@@ -22,6 +23,7 @@ mod metadata;
|
||||
mod owner;
|
||||
mod password;
|
||||
mod payload;
|
||||
mod persistence;
|
||||
mod transcript;
|
||||
mod view;
|
||||
mod wallet;
|
||||
@@ -149,6 +151,14 @@ pub use self::owner::WalletOwner;
|
||||
pub use self::password::OwnerPassword;
|
||||
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
|
||||
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.
|
||||
pub use self::view::WalletView;
|
||||
/// 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
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! 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("solana-keypair.workspace = true"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
|
||||
assert!(manifest.contains("tempfile.workspace = true"));
|
||||
assert!(manifest.contains("zeroize.workspace = true"));
|
||||
for forbidden in [
|
||||
"ksp-config-lib",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! 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_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"));
|
||||
}
|
||||
Reference in New Issue
Block a user