Skip to content

Commit

Permalink
Storage migration (#1057)
Browse files Browse the repository at this point in the history
* Brainstorm

* Basic idea

* Add encrypted db

* Seems to work

* Define types in migration, refactor

* where in stronghold is the data?

* Make stronghold snapshot migration work

* Add migration to bindings, add get_chrysalis_data()

* Save chrysalis data from snapshot in db

* Address some TODOs

* Address todos

* Add chrysalis-backup-work-factor-0.stronghold

* Fix wasm

* Don't error on ClientDataNotPresent

* Clippy

* Address review comments

* Remove extra function

* Update bindings/nodejs/CHANGELOG.md

Co-authored-by: Thibault Martinez <[email protected]>

* Remove `from`

* Clippy

* Changelog

* Update sdk/src/wallet/storage/constants.rs

* Move function to storage

* Address review comments

* Address review comments

* Review suggestions

* Address review comments

* Add db_encryption_key to Rust, export migrateDbChrysalisToStardust and add nodejs example

* Single import, lint

* Address review comments

---------

Co-authored-by: Thibault Martinez <[email protected]>
Co-authored-by: Thibault Martinez <[email protected]>
  • Loading branch information
3 people authored Aug 30, 2023
1 parent 5cfc4aa commit ce77da1
Show file tree
Hide file tree
Showing 41 changed files with 982 additions and 24 deletions.
5 changes: 5 additions & 0 deletions bindings/core/src/method/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ pub enum WalletMethod {
/// Read accounts.
/// Expected response: [`Accounts`](crate::Response::Accounts)
GetAccounts,
/// Get historic chrysalis data.
/// Expected response: [`ChrysalisData`](crate::Response::ChrysalisData)
#[cfg(feature = "storage")]
#[cfg_attr(docsrs, doc(cfg(feature = "storage")))]
GetChrysalisData,
/// Consume an account method.
/// Returns [`Response`](crate::Response)
#[serde(rename_all = "camelCase")]
Expand Down
1 change: 1 addition & 0 deletions bindings/core/src/method_handler/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM
}
Response::Accounts(account_dtos)
}
WalletMethod::GetChrysalisData => Response::ChrysalisData(wallet.get_chrysalis_data().await?),
WalletMethod::CallAccountMethod { account_id, method } => {
let account = wallet.get_account(account_id).await?;
call_account_method_internal(&account, method).await?
Expand Down
5 changes: 4 additions & 1 deletion bindings/core/src/response.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;
#[cfg(not(target_family = "wasm"))]
use std::collections::HashSet;

Expand Down Expand Up @@ -46,7 +47,6 @@ use serde::Serialize;
use {
iota_sdk::types::api::plugins::participation::types::{ParticipationEventId, ParticipationEventStatus},
iota_sdk::wallet::account::{AccountParticipationOverview, ParticipationEventWithNodes},
std::collections::HashMap,
};

use crate::{error::Error, OmittedDebug};
Expand Down Expand Up @@ -308,6 +308,9 @@ pub enum Response {
/// - [`AddressesWithUnspentOutputs`](crate::method::AccountMethod::AddressesWithUnspentOutputs)
AddressesWithUnspentOutputs(Vec<AddressWithUnspentOutputs>),
/// Response for:
/// - [`GetChrysalisData`](crate::method::WalletMethod::GetChrysalisData)
ChrysalisData(Option<HashMap<String, String>>),
/// Response for:
/// - [`MinimumRequiredStorageDeposit`](crate::method::ClientMethod::MinimumRequiredStorageDeposit)
/// - [`ComputeStorageDeposit`](crate::method::UtilsMethod::ComputeStorageDeposit)
MinimumRequiredStorageDeposit(String),
Expand Down
5 changes: 5 additions & 0 deletions bindings/nodejs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 1.0.6 - 2023-08-25

### Added

- `migrateDbChrysalisToStardust` function;
- `Wallet::getChrysalisData` method;

### Fixed

- `Account::prepareBurn()` return type;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { WalletOptions, Wallet, migrateDbChrysalisToStardust } from '@iota/sdk';
require('dotenv').config({ path: '.env' });

// Run with command:
// yarn run-example wallet/migrate-db-chrysalis-to-stardust.ts

const walletDbPath = './chrysalis-db';

async function run() {
const { initLogger } = require('@iota/sdk');
initLogger({
name: './wallet.log',
levelFilter: 'debug',
targetExclusions: ['h2', 'hyper', 'rustls'],
});
if (!process.env.NODE_URL) {
throw new Error('.env NODE_URL is undefined, see .env.example');
}
if (!process.env.STRONGHOLD_PASSWORD) {
throw new Error(
'.env STRONGHOLD_PASSWORD is undefined, see .env.example',
);
}

migrateDbChrysalisToStardust(walletDbPath, 'password');

const walletOptions: WalletOptions = {
storagePath: walletDbPath,
clientOptions: {
nodes: [process.env.NODE_URL],
},
secretManager: {
stronghold: {
snapshotPath: walletDbPath + 'wallet.stronghold',
password: process.env.STRONGHOLD_PASSWORD,
},
},
};
console.log(walletOptions);
const wallet = new Wallet(walletOptions);

// Accounts migrated from the Chrysalis db
const accounts = await wallet.getAccounts();
console.log(accounts);

const historicChrysalisData = await wallet.getChrysalisData();
console.log(historicChrysalisData);
}

run().then(() => process.exit());
2 changes: 2 additions & 0 deletions bindings/nodejs/lib/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {
getClientFromWallet,
getSecretManagerFromWallet,
migrateStrongholdSnapshotV2ToV3,
migrateDbChrysalisToStardust,
} = addon;

const callClientMethodAsync = (
Expand Down Expand Up @@ -116,4 +117,5 @@ export {
getSecretManagerFromWallet,
listenMqtt,
migrateStrongholdSnapshotV2ToV3,
migrateDbChrysalisToStardust,
};
2 changes: 2 additions & 0 deletions bindings/nodejs/lib/types/wallet/bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import type {
__GetAccountMethod__,
__GetAccountIndexesMethod__,
__GetAccountsMethod__,
__GetChrysalisDataMethod__,
__GetLedgerNanoStatusMethod__,
__GenerateEd25519AddressMethod__,
__IsStrongholdPasswordAvailableMethod__,
Expand Down Expand Up @@ -153,6 +154,7 @@ export type __Method__ =
| __GetAccountMethod__
| __GetAccountIndexesMethod__
| __GetAccountsMethod__
| __GetChrysalisDataMethod__
| __GetLedgerNanoStatusMethod__
| __GenerateEd25519AddressMethod__
| __IsStrongholdPasswordAvailableMethod__
Expand Down
4 changes: 4 additions & 0 deletions bindings/nodejs/lib/types/wallet/bridge/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export type __GetAccountMethod__ = {
data: { accountId: AccountId };
};

export type __GetChrysalisDataMethod__ = {
name: 'getChrysalisData';
};

export type __GetLedgerNanoStatusMethod__ = {
name: 'getLedgerNanoStatus';
};
Expand Down
1 change: 1 addition & 0 deletions bindings/nodejs/lib/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './account';
export * from './wallet';
export * from './wallet-method-handler';
export * from '../types/wallet';
export { migrateDbChrysalisToStardust } from '../bindings';
11 changes: 11 additions & 0 deletions bindings/nodejs/lib/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ export class Wallet {
return this.methodHandler.getClient();
}

/**
* Get chrysalis data.
*/
async getChrysalisData(): Promise<Record<string, string>> {
const response = await this.methodHandler.callMethod({
name: 'getChrysalisData',
});

return JSON.parse(response).payload;
}

/**
* Get secret manager.
*/
Expand Down
1 change: 1 addition & 0 deletions bindings/nodejs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("getClientFromWallet", wallet::get_client)?;
cx.export_function("getSecretManagerFromWallet", wallet::get_secret_manager)?;
cx.export_function("listenWallet", wallet::listen_wallet)?;
cx.export_function("migrateDbChrysalisToStardust", wallet::migrate_db_chrysalis_to_stardust)?;

Ok(())
}
25 changes: 25 additions & 0 deletions bindings/nodejs/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use iota_sdk_bindings_core::{
call_wallet_method as rust_call_wallet_method,
iota_sdk::wallet::{
events::types::{Event, WalletEventType},
migration::migrate_db_chrysalis_to_stardust as rust_migrate_db_chrysalis_to_stardust,
Wallet,
},
Response, Result, WalletMethod, WalletOptions,
Expand Down Expand Up @@ -232,3 +233,27 @@ pub fn get_secret_manager(mut cx: FunctionContext) -> JsResult<JsPromise> {

Ok(promise)
}

pub fn migrate_db_chrysalis_to_stardust(mut cx: FunctionContext) -> JsResult<JsPromise> {
let storage_path = cx.argument::<JsString>(0)?.value(&mut cx);
let password = cx
.argument_opt(1)
.map(|opt| opt.downcast_or_throw::<JsString, _>(&mut cx))
.transpose()?
.map(|opt| opt.value(&mut cx))
.map(Into::into);

let channel = cx.channel();
let (deferred, promise) = cx.promise();
crate::RUNTIME.spawn(async move {
if let Err(err) = rust_migrate_db_chrysalis_to_stardust(storage_path, password, None).await {
deferred.settle_with(&channel, move |mut cx| {
cx.error(serde_json::to_string(&Response::Error(err.into())).expect("json to string error"))
});
} else {
deferred.settle_with(&channel, move |mut cx| Ok(cx.boxed(())));
}
});

Ok(promise)
}
3 changes: 2 additions & 1 deletion bindings/wasm/lib/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { __UtilsMethods__ } from './utils';
// Import needs to be in a single line, otherwise it breaks
// prettier-ignore
// @ts-ignore: path is set to match runtime transpiled js path when bundled.
import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3 } from '../wasm/iota_sdk_wasm';
import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3, migrateDbChrysalisToStardust } from '../wasm/iota_sdk_wasm';

const callUtilsMethod = (method: __UtilsMethods__): any => {
const response = JSON.parse(callUtilsMethodRust(JSON.stringify(method)));
Expand All @@ -36,4 +36,5 @@ export {
getSecretManagerFromWallet,
listenMqtt,
migrateStrongholdSnapshotV2ToV3,
migrateDbChrysalisToStardust,
};
11 changes: 11 additions & 0 deletions bindings/wasm/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,14 @@ pub async fn listen_wallet(

Ok(JsValue::UNDEFINED)
}

/// Rocksdb chrysalis migration is not supported for WebAssembly bindings.
///
/// Throws an error if called, only included for compatibility
/// with the Node.js bindings TypeScript definitions.
#[wasm_bindgen(js_name = migrateDbChrysalisToStardust)]
pub fn migrate_db_chrysalis_to_stardust(_storage_path: String, _password: Option<String>) -> Result<(), JsValue> {
let js_error = js_sys::Error::new("Rocksdb chrysalis migration is not supported for WebAssembly");

Err(JsValue::from(js_error))
}
5 changes: 5 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 1.0.3 - 2023-MM-DD

### Added

- `migrate_db_chrysalis_to_stardust()` function;
- `Wallet::get_chrysalis_data()` method;

### Fixed

- `Clients` returning the default protocol parameters when multiple `Client` instances are used;
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/client/stronghold/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub(super) const DERIVE_OUTPUT_RECORD_PATH: &[u8] = b"iota-wallet-derived";
/// The client path for the seed.
///
/// The value has been hard-coded historically.
pub(super) const PRIVATE_DATA_CLIENT_PATH: &[u8] = b"iota_seed";
pub(crate) const PRIVATE_DATA_CLIENT_PATH: &[u8] = b"iota_seed";

/// The path for the user-data encryption key for the Stronghold store.
pub(super) const USERDATA_STORE_KEY_RECORD_PATH: &[u8] = b"userdata-store-key";
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/client/stronghold/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ use tokio::{
};
use zeroize::Zeroizing;

use self::common::PRIVATE_DATA_CLIENT_PATH;
pub(crate) use self::common::PRIVATE_DATA_CLIENT_PATH;
pub use self::error::Error;
use super::{storage::StorageAdapter, utils::Password};

Expand Down
12 changes: 10 additions & 2 deletions sdk/src/wallet/core/operations/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ mod storage_stub {
},
wallet::{
core::builder::dto::WalletBuilderDto,
storage::constants::{SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY},
WalletBuilder,
storage::constants::{CHRYSALIS_STORAGE_KEY, SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY},
Wallet, WalletBuilder,
},
};

Expand Down Expand Up @@ -84,6 +84,14 @@ mod storage_stub {
Ok(res.map(Into::into))
}
}

impl Wallet {
pub async fn get_chrysalis_data(
&self,
) -> crate::wallet::Result<Option<std::collections::HashMap<String, String>>> {
self.storage_manager.read().await.get(CHRYSALIS_STORAGE_KEY).await
}
}
}
#[cfg(not(feature = "storage"))]
mod storage_stub {
Expand Down
21 changes: 18 additions & 3 deletions sdk/src/wallet/core/operations/stronghold_backup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ use crate::wallet::WalletBuilder;
use crate::{
client::{
secret::{stronghold::StrongholdSecretManager, SecretManager, SecretManagerConfig, SecretManagerDto},
storage::StorageAdapter,
utils::Password,
},
types::block::address::Hrp,
wallet::{Account, Wallet},
wallet::{storage::constants::CHRYSALIS_STORAGE_KEY, Account, Wallet},
};

impl Wallet {
Expand Down Expand Up @@ -102,7 +103,7 @@ impl Wallet {
.password(stronghold_password.clone())
.build(backup_path.clone())?;

let (read_client_options, read_coin_type, read_secret_manager, read_accounts) =
let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) =
read_data_from_stronghold_snapshot::<SecretManager>(&new_stronghold).await?;

// If the coin type is not matching the current one, then the addresses in the accounts will also not be
Expand Down Expand Up @@ -204,6 +205,13 @@ impl Wallet {
for account in accounts.iter() {
account.save(None).await?;
}
if let Some(chrysalis_data) = chrysalis_data {
self.storage_manager
.read()
.await
.set(CHRYSALIS_STORAGE_KEY, &chrysalis_data)
.await?;
}
}

Ok(())
Expand Down Expand Up @@ -272,7 +280,7 @@ impl Wallet<StrongholdSecretManager> {
.password(stronghold_password.clone())
.build(backup_path.clone())?;

let (read_client_options, read_coin_type, read_secret_manager, read_accounts) =
let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) =
read_data_from_stronghold_snapshot::<StrongholdSecretManager>(&new_stronghold).await?;

// If the coin type is not matching the current one, then the addresses in the accounts will also not be
Expand Down Expand Up @@ -367,6 +375,13 @@ impl Wallet<StrongholdSecretManager> {
for account in accounts.iter() {
account.save(None).await?;
}
if let Some(chrysalis_data) = chrysalis_data {
self.storage_manager
.read()
.await
.set(CHRYSALIS_STORAGE_KEY, &chrysalis_data)
.await?;
}
}

Ok(())
Expand Down
Loading

0 comments on commit ce77da1

Please sign in to comment.