feat: tiki v2 migration - populate TikiHolder from UserTikis

Storage migration v1->v2 scans all UserTikis entries and populates
TikiHolder for unique roles (Serok, SerokiMeclise, Xezinedar, Balyoz).
Fixes empty TikiHolder on live chain despite roles being assigned.

Includes try-runtime pre/post upgrade checks and unit tests.
This commit is contained in:
2026-02-11 04:37:55 +03:00
parent 0be7357f90
commit f1b671ad65
@@ -8,7 +8,7 @@ use pezframe_support::{
use pezsp_std::marker::PhantomData;
/// Current storage version
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
/// Migration from v0 to v1
/// This is a template migration that can be customized based on actual storage changes
@@ -26,20 +26,11 @@ pub mod v1 {
);
if current == StorageVersion::new(0) {
// Perform migration logic here
// Example: Iterate over storage and transform data
let migrated = 0u64;
let mut weight = Weight::zero();
// Example: Migrate CitizenNft storage if format changed
// for (account, nft_id) in CitizenNft::<T>::iter() {
// // Transform data if needed
// migrated += 1;
// }
// Update storage version
STORAGE_VERSION.put::<Pezpallet<T>>();
// Update storage version to v1
StorageVersion::new(1).put::<Pezpallet<T>>();
log::info!("✅ Migrated {migrated} entries in pezpallet-tiki");
@@ -125,12 +116,12 @@ pub mod v1 {
}
}
/// Example migration for future version changes
/// This demonstrates how to handle storage item renames or format changes
/// Migration v1 -> v2: Populate TikiHolder from UserTikis
/// Fixes: TikiHolder was empty despite unique roles (Serok) being assigned.
/// This scans all UserTikis entries and populates TikiHolder for unique roles.
pub mod v2 {
use super::*;
/// Example: Migration when storage format changes
pub struct MigrateToV2<T>(PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV2<T> {
@@ -138,62 +129,189 @@ pub mod v2 {
let current = Pezpallet::<T>::on_chain_storage_version();
if current < StorageVersion::new(2) {
log::info!("🔄 Running migration for pezpallet-tiki to v2");
log::info!("Running migration for pezpallet-tiki v1 -> v2");
// Example migration logic
// 1. Create new storage with modified format
// 2. Migrate data from old storage to new
// 3. Remove old storage
// 4. Update version
let mut reads = 1u64; // version read
let mut writes = 1u64; // version write
let mut holders_fixed = 0u32;
// Scan all UserTikis to find unique role holders
for (account, tikis) in UserTikis::<T>::iter() {
reads += 1;
for tiki in tikis.iter() {
if Pezpallet::<T>::is_unique_role(tiki) {
// Only set if not already populated
if TikiHolder::<T>::get(tiki).is_none() {
TikiHolder::<T>::insert(tiki, account.clone());
holders_fixed += 1;
writes += 1;
}
reads += 1;
}
}
}
// For now, this is just a template
STORAGE_VERSION.put::<Pezpallet<T>>();
log::info!("✅ Completed migration to pezpallet-tiki v2");
log::info!(
"Completed pezpallet-tiki v2 migration: fixed {holders_fixed} TikiHolder entries"
);
T::DbWeight::get().reads_writes(1, 1)
T::DbWeight::get().reads_writes(reads, writes)
} else {
log::info!("👌 pezpallet-tiki v2 migration not needed");
log::info!("pezpallet-tiki v2 migration not needed, current: {current:?}");
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
use codec::Encode;
let tiki_holder_count = TikiHolder::<T>::iter().count() as u32;
log::info!("Pre-upgrade: TikiHolder entries = {tiki_holder_count}");
Ok(tiki_holder_count.encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(
state: pezsp_std::vec::Vec<u8>,
) -> Result<(), pezsp_runtime::TryRuntimeError> {
use codec::Decode;
let pre_count: u32 =
Decode::decode(&mut &state[..]).map_err(|_| "Failed to decode pre-upgrade state")?;
let post_count = TikiHolder::<T>::iter().count() as u32;
log::info!("Post-upgrade: TikiHolder {pre_count} -> {post_count}");
// Should have at least as many entries as before
assert!(
post_count >= pre_count,
"TikiHolder entries decreased during migration"
);
// Verify consistency: every unique role in UserTikis has a TikiHolder entry
for (account, tikis) in UserTikis::<T>::iter() {
for tiki in tikis.iter() {
if Pezpallet::<T>::is_unique_role(tiki) {
let holder = TikiHolder::<T>::get(tiki);
assert!(
holder.is_some(),
"Unique role missing from TikiHolder after migration"
);
assert_eq!(
holder.unwrap(),
account,
"TikiHolder mismatch for unique role"
);
}
}
}
assert_eq!(
Pezpallet::<T>::on_chain_storage_version(),
STORAGE_VERSION,
"Storage version not updated"
);
log::info!("Post-upgrade checks passed for pezpallet-tiki v2");
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{new_test_ext, Test};
use crate::{
mock::{new_test_ext, Test},
pezpallet::{CitizenNft, Tiki, TikiHolder, UserTikis},
};
use pezframe_support::traits::OnRuntimeUpgrade;
#[test]
fn test_migration_v1() {
new_test_ext().execute_with(|| {
// Set initial storage version to 0
StorageVersion::new(0).put::<Pezpallet<Test>>();
// Run migration
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
// Verify version was updated
assert_eq!(Pezpallet::<Test>::on_chain_storage_version(), STORAGE_VERSION);
// Verify weight is non-zero
assert_eq!(Pezpallet::<Test>::on_chain_storage_version(), StorageVersion::new(1));
assert!(weight != Weight::zero());
});
}
#[test]
fn test_migration_idempotent() {
fn test_migration_v2_populates_tiki_holder() {
new_test_ext().execute_with(|| {
StorageVersion::new(1).put::<Pezpallet<Test>>();
// Simulate on-chain state: account 1 has Serok in UserTikis but TikiHolder is empty
let account: u64 = 1;
CitizenNft::<Test>::insert(&account, 0u32);
UserTikis::<Test>::mutate(&account, |tikis| {
let _ = tikis.try_push(Tiki::Welati);
let _ = tikis.try_push(Tiki::Serok);
});
// TikiHolder should be empty before migration
assert!(TikiHolder::<Test>::get(Tiki::Serok).is_none());
// Run migration
let weight = v2::MigrateToV2::<Test>::on_runtime_upgrade();
// TikiHolder should now have Serok -> account 1
assert_eq!(TikiHolder::<Test>::get(Tiki::Serok), Some(account));
assert_eq!(Pezpallet::<Test>::on_chain_storage_version(), STORAGE_VERSION);
assert!(weight != Weight::zero());
});
}
#[test]
fn test_migration_v2_idempotent() {
new_test_ext().execute_with(|| {
// Set current version
STORAGE_VERSION.put::<Pezpallet<Test>>();
// Run migration again
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
let weight = v2::MigrateToV2::<Test>::on_runtime_upgrade();
// Should be a no-op
assert_eq!(weight, pezframe_support::weights::constants::RocksDbWeight::get().reads(1));
});
}
#[test]
fn test_migration_v2_multiple_unique_roles() {
new_test_ext().execute_with(|| {
StorageVersion::new(1).put::<Pezpallet<Test>>();
// Account 1: Serok
CitizenNft::<Test>::insert(&1u64, 0u32);
UserTikis::<Test>::mutate(&1u64, |tikis| {
let _ = tikis.try_push(Tiki::Welati);
let _ = tikis.try_push(Tiki::Serok);
});
// Account 2: SerokiMeclise
CitizenNft::<Test>::insert(&2u64, 1u32);
UserTikis::<Test>::mutate(&2u64, |tikis| {
let _ = tikis.try_push(Tiki::Welati);
let _ = tikis.try_push(Tiki::SerokiMeclise);
});
// Account 3: just Welati (no unique role)
CitizenNft::<Test>::insert(&3u64, 2u32);
UserTikis::<Test>::mutate(&3u64, |tikis| {
let _ = tikis.try_push(Tiki::Welati);
});
v2::MigrateToV2::<Test>::on_runtime_upgrade();
assert_eq!(TikiHolder::<Test>::get(Tiki::Serok), Some(1u64));
assert_eq!(TikiHolder::<Test>::get(Tiki::SerokiMeclise), Some(2u64));
assert!(TikiHolder::<Test>::get(Tiki::Xezinedar).is_none());
assert!(TikiHolder::<Test>::get(Tiki::Balyoz).is_none());
});
}
}