Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: account migration #338

Merged
merged 3 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 3 additions & 115 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));

mod migrations;

use codec::{Decode, Encode, MaxEncodedLen};

use migrations::account_data_migration;
use pallet_commitments::CanCommit;
use pallet_grandpa::{
fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList,
Expand Down Expand Up @@ -1147,121 +1150,6 @@ pub type SignedExtra = (
pallet_commitments::CommitmentsSignedExtension<Runtime>,
);

mod account_data_migration {
use super::*;
use frame_support::log;
use pallet_balances::ExtraFlags;

mod prev {
use super::*;
use frame_support::{pallet_prelude::ValueQuery, storage_alias, Blake2_128Concat};

#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, Debug)]
pub struct AccountData<Balance> {
pub free: Balance,
pub reserved: Balance,
pub misc_frozen: Balance,
pub fee_frozen: Balance,
}

#[storage_alias]
pub type Account<T: frame_system::pallet::Config> = StorageMap<
frame_system::pallet::Pallet<T>,
Blake2_128Concat,
AccountId,
AccountData<Balance>,
ValueQuery,
>;
}

const TARGET: &'static str = "runtime::account_data_migration";
pub struct Migration;
impl OnRuntimeUpgrade for Migration {
/// Save pre-upgrade account ids to check are decodable post-upgrade.
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
let account_ids = prev::Account::<Runtime>::iter_keys().collect::<Vec<_>>();
log::info!(target: TARGET, "pre-upgrade");

Ok(account_ids.encode())
}

/// Ensures post-upgrade that
/// 1. Number of accounts has not changed.
/// 2. Each account exists in storage and decodes.
/// 3. Each account has a provider val >0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this something you wrote? I was thinking how it was even possible to get an old lifecycle runtime upgrade hook from upstream to execute in our chain, but if you wrote a brand new one, it does the job just fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I wrote this from scratch.

#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
use frame_support::ensure;
log::info!(target: TARGET, "Running post-upgrade...");

let pre_upgrade_account_ids: Vec<<Runtime as frame_system::Config>::AccountId> =
Decode::decode(&mut &state[..]).expect("pre_upgrade provides a valid state; qed");

// Ensure number of accounts has not changed.
let account_ids = prev::Account::<Runtime>::iter_keys().collect::<Vec<_>>();
ensure!(
pre_upgrade_account_ids.len() == account_ids.len(),
"number of accounts has changed"
);

for acc in account_ids {
// Ensure account exists in storage and decodes.
match frame_system::pallet::Account::<Runtime>::try_get(&acc) {
Ok(d) => {
ensure!(d.data.free > 0 || d.data.reserved > 0, "account has 0 bal");
}
_ => {
panic!("account not found")
}
};

// Ensure account provider is >0.
ensure!(
frame_system::Pallet::<Runtime>::providers(&acc) > 0,
"provider == 0"
);
}

log::info!(target: TARGET, "post-upgrade success ✅");
Ok(())
}

/// Migrates AccountData storage to the new format, bumping providers where required.
fn on_runtime_upgrade() -> Weight {
// Pull the storage in the previous format into memory
let accounts = prev::Account::<Runtime>::iter().collect::<Vec<_>>();
log::info!(target: TARGET, "Migrating {} accounts...", accounts.len());

for (acc, data) in accounts.clone().into_iter() {
// Move account to new data format
let new_data = pallet_balances::AccountData {
free: data.free,
reserved: data.reserved,
frozen: data.misc_frozen.saturating_add(data.fee_frozen),
flags: ExtraFlags::old_logic(),
};
frame_system::pallet::Account::<Runtime>::mutate(acc.clone(), |a| {
a.data = new_data;
});

// Ensure provider
if frame_system::Pallet::<Runtime>::providers(&acc) == 0 {
frame_system::Pallet::<Runtime>::inc_providers(&acc);
}

// Ensure upgraded
pallet_balances::Pallet::<Runtime, ()>::ensure_upgraded(&acc);
}

log::info!(target: TARGET, "Migrated {} accounts ✅", accounts.len());

// R/W not important for solo chain.
return <Runtime as frame_system::Config>::DbWeight::get().reads_writes(0u64, 0u64);
}
}
}

type Migrations = account_data_migration::Migration;

// Unchecked extrinsic type as expected by this runtime.
Expand Down
162 changes: 162 additions & 0 deletions runtime/src/migrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
pub(crate) mod account_data_migration {
orriin marked this conversation as resolved.
Show resolved Hide resolved
use crate::*;
use frame_support::log;
use pallet_balances::ExtraFlags;

mod prev {
use super::*;
use frame_support::{pallet_prelude::ValueQuery, storage_alias, Blake2_128Concat};

#[derive(
Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen,
)]
pub struct AccountDataStruct<Balance> {
pub free: Balance,
pub reserved: Balance,
pub misc_frozen: Balance,
pub fee_frozen: Balance,
}

#[derive(
Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen,
)]
pub struct AccountStruct<Balance> {
pub nonce: u32,
pub consumers: u32,
pub providers: u32,
pub sufficients: u32,
pub data: AccountDataStruct<Balance>,
}

#[storage_alias]
pub type Account<T: frame_system::pallet::Config> = StorageMap<
frame_system::pallet::Pallet<T>,
Blake2_128Concat,
AccountId,
AccountStruct<Balance>,
ValueQuery,
>;
}

const TARGET: &'static str = "runtime::account_data_migration";
pub struct Migration;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might wanna give this a more descriptive name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it is the only Migration in the account_data_migration module :P

impl OnRuntimeUpgrade for Migration {
/// Save pre-upgrade account ids to check are decodable post-upgrade.
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
use sp_std::collections::btree_map::BTreeMap;
log::info!(target: TARGET, "pre-upgrade");

// Save the expected post-migration account state.
let mut expected_account: BTreeMap<
AccountId,
frame_system::AccountInfo<u32, pallet_balances::AccountData<Balance>>,
> = BTreeMap::new();

for (acc_id, acc) in prev::Account::<Runtime>::iter() {
let expected_data = pallet_balances::AccountData {
free: acc.data.free,
reserved: acc.data.reserved,
frozen: acc.data.misc_frozen.saturating_add(acc.data.fee_frozen),
flags: ExtraFlags::default(),
};

// `ensure_upgraded` bumps the consumers if there is a non zero reserved balance and no frozen balance.
// https://github.com/paritytech/polkadot-sdk/blob/305d311d5c732fcc4629f3295768f1ed44ef434c/substrate/frame/balances/src/lib.rs#L785
let expected_consumers = if acc.data.reserved > 0 && expected_data.frozen == 0 {
acc.consumers + 1
} else {
acc.consumers
};
let expected_acc = frame_system::AccountInfo {
nonce: acc.nonce,
consumers: expected_consumers,
providers: acc.providers,
sufficients: acc.sufficients,
data: expected_data,
};
expected_account.insert(acc_id, expected_acc);
}

Ok(expected_account.encode())
}

/// Migrates Account storage to the new format, and calls `ensure_upgraded` for them.
fn on_runtime_upgrade() -> Weight {
// Pull the storage in the previous format into memory
let accounts = prev::Account::<Runtime>::iter().collect::<Vec<_>>();
log::info!(target: TARGET, "Migrating {} accounts...", accounts.len());

for (acc_id, acc_info) in accounts.clone().into_iter() {
let prev_data = acc_info.clone().data;

// Move account to new data format
let new_data = pallet_balances::AccountData {
free: prev_data.free,
reserved: prev_data.reserved,
frozen: prev_data.misc_frozen.saturating_add(prev_data.fee_frozen),
flags: ExtraFlags::old_logic(),
};
let new_account = frame_system::AccountInfo {
nonce: acc_info.nonce,
consumers: acc_info.consumers,
providers: acc_info.providers,
sufficients: acc_info.sufficients,
data: new_data,
};
frame_system::pallet::Account::<Runtime>::insert(acc_id.clone(), new_account);

// Ensure upgraded
pallet_balances::Pallet::<Runtime, ()>::ensure_upgraded(&acc_id);
}

log::info!(target: TARGET, "Migrated {} accounts ✅", accounts.len());

// R/W not important for solo chain.
return <Runtime as frame_system::Config>::DbWeight::get().reads_writes(0u64, 0u64);
orriin marked this conversation as resolved.
Show resolved Hide resolved
}

/// Ensures post-upgrade that every Account entry matches what is expected.
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
use frame_support::ensure;
use sp_std::collections::btree_map::BTreeMap;

log::info!(target: TARGET, "Running post-upgrade...");

let expected_accounts: BTreeMap<
AccountId,
frame_system::AccountInfo<u32, pallet_balances::AccountData<Balance>>,
> = Decode::decode(&mut &state[..]).expect("decoding state failed");

// Ensure the actual post-migration state matches the expected
for (acc_id, acc) in frame_system::pallet::Account::<Runtime>::iter() {
let expected = expected_accounts.get(&acc_id).expect("account not found");

// New system logic nukes the account if no providers or sufficients.
if acc.providers > 0 || acc.sufficients > 0 {
ensure!(acc.nonce == expected.nonce, "nonce mismatch");
ensure!(acc.consumers == expected.consumers, "consumers mismatch");
ensure!(acc.providers == expected.providers, "providers mismatch");
ensure!(
acc.sufficients == expected.sufficients,
"sufficients mismatch"
);
ensure!(acc.data.free == expected.data.free, "data.free mismatch");
ensure!(
acc.data.reserved == expected.data.reserved,
"data.reserved mismatch"
);
ensure!(
acc.data.frozen == expected.data.frozen,
"data.frozen mismatch"
);
ensure!(acc.data.flags == expected.data.flags, "data.flags mismatch");
}
}

log::info!(target: TARGET, "post-upgrade success ✅");
Ok(())
}
}
}
Loading