From f1b671ad65f5ad47ee6fb59b7a66c097964c0454 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Wed, 11 Feb 2026 04:37:55 +0300 Subject: [PATCH] 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. --- .../pezpallets/tiki/src/migrations.rs | 190 ++++++++++++++---- 1 file changed, 154 insertions(+), 36 deletions(-) diff --git a/pezcumulus/teyrchains/pezpallets/tiki/src/migrations.rs b/pezcumulus/teyrchains/pezpallets/tiki/src/migrations.rs index 4b599dec..9745a421 100644 --- a/pezcumulus/teyrchains/pezpallets/tiki/src/migrations.rs +++ b/pezcumulus/teyrchains/pezpallets/tiki/src/migrations.rs @@ -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::::iter() { - // // Transform data if needed - // migrated += 1; - // } - - // Update storage version - STORAGE_VERSION.put::>(); + // Update storage version to v1 + StorageVersion::new(1).put::>(); 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(PhantomData); impl OnRuntimeUpgrade for MigrateToV2 { @@ -138,62 +129,189 @@ pub mod v2 { let current = Pezpallet::::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::::iter() { + reads += 1; + for tiki in tikis.iter() { + if Pezpallet::::is_unique_role(tiki) { + // Only set if not already populated + if TikiHolder::::get(tiki).is_none() { + TikiHolder::::insert(tiki, account.clone()); + holders_fixed += 1; + writes += 1; + } + reads += 1; + } + } + } - // For now, this is just a template STORAGE_VERSION.put::>(); - 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_runtime::TryRuntimeError> { + use codec::Encode; + + let tiki_holder_count = TikiHolder::::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, + ) -> 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::::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::::iter() { + for tiki in tikis.iter() { + if Pezpallet::::is_unique_role(tiki) { + let holder = TikiHolder::::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::::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::>(); - // Run migration let weight = v1::MigrateToV1::::on_runtime_upgrade(); - // Verify version was updated - assert_eq!(Pezpallet::::on_chain_storage_version(), STORAGE_VERSION); - - // Verify weight is non-zero + assert_eq!(Pezpallet::::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::>(); + + // Simulate on-chain state: account 1 has Serok in UserTikis but TikiHolder is empty + let account: u64 = 1; + CitizenNft::::insert(&account, 0u32); + UserTikis::::mutate(&account, |tikis| { + let _ = tikis.try_push(Tiki::Welati); + let _ = tikis.try_push(Tiki::Serok); + }); + + // TikiHolder should be empty before migration + assert!(TikiHolder::::get(Tiki::Serok).is_none()); + + // Run migration + let weight = v2::MigrateToV2::::on_runtime_upgrade(); + + // TikiHolder should now have Serok -> account 1 + assert_eq!(TikiHolder::::get(Tiki::Serok), Some(account)); + assert_eq!(Pezpallet::::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::>(); - // Run migration again - let weight = v1::MigrateToV1::::on_runtime_upgrade(); + let weight = v2::MigrateToV2::::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::>(); + + // Account 1: Serok + CitizenNft::::insert(&1u64, 0u32); + UserTikis::::mutate(&1u64, |tikis| { + let _ = tikis.try_push(Tiki::Welati); + let _ = tikis.try_push(Tiki::Serok); + }); + + // Account 2: SerokiMeclise + CitizenNft::::insert(&2u64, 1u32); + UserTikis::::mutate(&2u64, |tikis| { + let _ = tikis.try_push(Tiki::Welati); + let _ = tikis.try_push(Tiki::SerokiMeclise); + }); + + // Account 3: just Welati (no unique role) + CitizenNft::::insert(&3u64, 2u32); + UserTikis::::mutate(&3u64, |tikis| { + let _ = tikis.try_push(Tiki::Welati); + }); + + v2::MigrateToV2::::on_runtime_upgrade(); + + assert_eq!(TikiHolder::::get(Tiki::Serok), Some(1u64)); + assert_eq!(TikiHolder::::get(Tiki::SerokiMeclise), Some(2u64)); + assert!(TikiHolder::::get(Tiki::Xezinedar).is_none()); + assert!(TikiHolder::::get(Tiki::Balyoz).is_none()); + }); + } }