Files
khadhroony-solana-project/crates/ksp-wallet-lib/src/migration.rs
2026-08-22 09:39:13 +02:00

167 lines
8.4 KiB
Rust

// file: crates/ksp-wallet-lib/src/migration.rs
// version: 1
//! Explicit authenticated native Wallet migration operations.
/// Converts one authenticated V1 document snapshot into a fresh V2 binary Wallet in memory.
///
/// The OWNER password authenticates the V1 source and is reused for the V2 OWNER credential. Because V1 and V2 slot AAD are domain-separated, an enabled
/// V1 VIEW slot cannot be copied byte-for-byte: the caller must provide the target VIEW password used to rewrap the migrated V2 metadata key. That value
/// may be the current VIEW password or a replacement chosen under OWNER authority. A disabled source VIEW must remain disabled during pure format migration.
/// No source bytes are modified by this function.
pub async fn migrate_wallet_v1_to_v2(
source: &[u8],
owner_password: crate::OwnerPassword,
target_view_password: std::option::Option<crate::ViewPassword>,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let source_envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_owner_password = owner_password.duplicate_for_internal_use();
let source_owner = match crate::open_wallet_owner_v1(source, source_owner_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = validate_view_migration_shape(source_envelope.view_descriptor().enabled(), target_view_password.is_some()) {
return std::result::Result::Err(error);
}
let metadata_payload = crate::MetadataPayloadV1::from_info(source_owner.info());
let solana_keypair = match source_owner.clone_v1_solana_keypair_for_migration() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut migrated =
match crate::create_wallet_v2_from_keypair(owner_password, target_view_password, crate::WalletCreateMetadata::default(), solana_keypair).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = migrated.replace_metadata_for_migration(metadata_payload) {
return std::result::Result::Err(error);
}
if migrated.pubkey() != source_owner.pubkey() {
return std::result::Result::Err(migration_error("Migrated Wallet public identity changed unexpectedly"));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_migrate_v1_to_v2",
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
view_enabled = source_envelope.view_descriptor().enabled(),
"authenticated native wallet migrated in memory"
);
return std::result::Result::Ok(migrated);
}
/// Migrates one V1 file snapshot into a new no-clobber V2 destination while preserving the V1 source file.
///
/// The source is read through the bounded Wallet persistence boundary. Existing destinations are never overwritten. This operation never performs an
/// implicit migration when a Wallet is merely opened.
pub async fn migrate_wallet_file_v1_to_v2(
source: impl std::convert::AsRef<std::path::Path>,
destination: impl std::convert::AsRef<std::path::Path>,
owner_password: crate::OwnerPassword,
target_view_password: std::option::Option<crate::ViewPassword>,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let source_path = source.as_ref().to_path_buf();
let destination_path = destination.as_ref().to_path_buf();
let source_bytes = match crate::read_wallet_file_async(source_path).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrated = match migrate_wallet_v1_to_v2(source_bytes.as_slice(), owner_password, target_view_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match migrated.to_native_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::persist_new_wallet_content(destination_path, serialized).await {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_migrate_v1_to_v2_file",
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
publication = "no_clobber",
"authenticated native wallet migration published to a new destination"
);
return std::result::Result::Ok(migrated);
}
/// Atomically replaces one authenticated V1 file with its V2 migration when the source state is still current.
///
/// The V1 envelope observed before OWNER authentication is retained as the expected state. Publication uses the existing state-conflict protected atomic
/// replacement boundary, so concurrent or stale changes abort rather than overwriting a newer Wallet. Failure before publication leaves the V1 bytes intact.
pub async fn migrate_wallet_file_v1_to_v2_in_place(
source: impl std::convert::AsRef<std::path::Path>,
owner_password: crate::OwnerPassword,
target_view_password: std::option::Option<crate::ViewPassword>,
) -> ksp_core_lib::Result<crate::WalletOwner> {
return migrate_wallet_file_v1_to_v2_in_place_with_hook(source.as_ref().to_path_buf(), owner_password, target_view_password, || {
return std::result::Result::Ok(());
})
.await;
}
async fn migrate_wallet_file_v1_to_v2_in_place_with_hook<F>(
source_path: std::path::PathBuf,
owner_password: crate::OwnerPassword,
target_view_password: std::option::Option<crate::ViewPassword>,
before_publish: F,
) -> ksp_core_lib::Result<crate::WalletOwner>
where
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
{
let source_bytes = match crate::read_wallet_file_async(source_path.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_v1 = match crate::KspWalletEnvelopeV1::parse_json(source_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrated = match migrate_wallet_v1_to_v2(source_bytes.as_slice(), owner_password, target_view_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match migrated.to_native_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = before_publish() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = crate::replace_wallet_file_v1(source_path, expected_v1, serialized).await {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_migrate_v1_to_v2_file",
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
publication = "atomic_replace",
"authenticated native wallet migration replaced the current V1 source"
);
return std::result::Result::Ok(migrated);
}
fn validate_view_migration_shape(source_view_enabled: bool, target_view_present: bool) -> ksp_core_lib::Result<()> {
if source_view_enabled != target_view_present {
return std::result::Result::Err(migration_error(
"V1 -> V2 migration must preserve whether VIEW capability is enabled; provide a target VIEW password exactly when the source VIEW is enabled",
));
}
return std::result::Result::Ok(());
}
fn migration_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_MIGRATION_INVALID, message);
}
#[cfg(test)]
#[path = "../unit_tests/migration.rs"]
mod tests;