code/pallets/subtensor/src/migrations/migrate_subnet_state.rs

migrate_subnet_state.rs

232 lines · 8,963 bytes · 973b2a9565RawGitHub
use super::*;
use frame_support::{traits::Get, weights::Weight};
use sp_std::collections::btree_map::BTreeMap;

pub(crate) const MIGRATION_NAME: &[u8] = b"backfill_subnet_lifecycle_state_v1";

/// Backfills the reporting-only subnet lifecycle map from the existing operational state.
pub fn migrate_subnet_state<T: Config>() -> Weight {
    let migration_name = MIGRATION_NAME.to_vec();
    let mut weight = T::DbWeight::get().reads(1);

    if HasMigrationRun::<T>::get(&migration_name) {
        return weight;
    }

    let mut expected = BTreeMap::<NetUid, SubnetLifecycleState>::new();
    for (netuid, added) in NetworksAdded::<T>::iter() {
        weight.saturating_accrue(T::DbWeight::get().reads(1));
        if !added {
            continue;
        }

        let state = if netuid == NetUid::ROOT {
            SubnetLifecycleState::Started
        } else {
            let started = FirstEmissionBlockNumber::<T>::contains_key(netuid)
                || SubtokenEnabled::<T>::get(netuid);
            weight.saturating_accrue(T::DbWeight::get().reads(2));
            if started {
                SubnetLifecycleState::Started
            } else {
                SubnetLifecycleState::Registered
            }
        };

        expected.insert(netuid, state);
    }

    let queued = DissolveCleanupQueue::<T>::get();
    weight.saturating_accrue(T::DbWeight::get().reads(1));
    for netuid in queued {
        expected.insert(netuid, SubnetLifecycleState::PendingDissolution);
    }

    if let Some(status) = CurrentDissolveCleanupStatus::<T>::get() {
        expected.insert(status.netuid, SubnetLifecycleState::Dissolving);
    }
    weight.saturating_accrue(T::DbWeight::get().reads(1));

    for (netuid, state) in &expected {
        SubnetState::<T>::insert(netuid, state);
        weight.saturating_accrue(T::DbWeight::get().writes(1));
    }

    HasMigrationRun::<T>::insert(migration_name, true);
    weight.saturating_accrue(T::DbWeight::get().writes(1));
    weight
}

/// [`OnRuntimeUpgrade`](frame_support::traits::OnRuntimeUpgrade) wrapper for
/// [`migrate_subnet_state`]. Its try-runtime hooks snapshot the lifecycle implied by the
/// legacy operational storage before the upgrade, then require the complete reporting map
/// and migration marker afterward.
pub mod subnet_state {
    use super::*;
    use frame_support::traits::OnRuntimeUpgrade;
    use sp_std::marker::PhantomData;

    #[cfg(feature = "try-runtime")]
    use codec::{Decode, Encode};
    #[cfg(feature = "try-runtime")]
    use frame_support::ensure;
    #[cfg(feature = "try-runtime")]
    use sp_runtime::TryRuntimeError;
    #[cfg(feature = "try-runtime")]
    use sp_std::vec::Vec;

    #[cfg(feature = "try-runtime")]
    #[derive(Encode, Decode)]
    struct PreUpgradeState {
        expected: Vec<(NetUid, SubnetLifecycleState)>,
    }

    #[cfg(feature = "try-runtime")]
    fn expected_lifecycle_state<T: Config>() -> BTreeMap<NetUid, SubnetLifecycleState> {
        let mut expected = BTreeMap::new();
        for (netuid, added) in NetworksAdded::<T>::iter() {
            if !added {
                continue;
            }
            let state = if netuid == NetUid::ROOT
                || FirstEmissionBlockNumber::<T>::contains_key(netuid)
                || SubtokenEnabled::<T>::get(netuid)
            {
                SubnetLifecycleState::Started
            } else {
                SubnetLifecycleState::Registered
            };
            expected.insert(netuid, state);
        }
        for netuid in DissolveCleanupQueue::<T>::get() {
            expected.insert(netuid, SubnetLifecycleState::PendingDissolution);
        }
        if let Some(status) = CurrentDissolveCleanupStatus::<T>::get() {
            expected.insert(status.netuid, SubnetLifecycleState::Dissolving);
        }
        expected
    }

    #[cfg(feature = "try-runtime")]
    fn validate_lifecycle_state<T: Config>(
        expected: BTreeMap<NetUid, SubnetLifecycleState>,
    ) -> Result<(), TryRuntimeError> {
        let actual: BTreeMap<NetUid, SubnetLifecycleState> = SubnetState::<T>::iter().collect();
        ensure!(
            actual == expected,
            "SubnetState must exactly match the expected lifecycle state"
        );
        Ok(())
    }

    /// Validate the live lifecycle invariant after every migration in the upgrade has run.
    #[cfg(feature = "try-runtime")]
    pub(crate) fn try_state<T: Config>() -> Result<(), TryRuntimeError> {
        validate_lifecycle_state::<T>(expected_lifecycle_state::<T>())
    }

    pub struct Migration<T: Config>(PhantomData<T>);

    impl<T: Config> OnRuntimeUpgrade for Migration<T> {
        fn on_runtime_upgrade() -> Weight {
            migrate_subnet_state::<T>()
        }

        #[cfg(feature = "try-runtime")]
        fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
            // Deliberately derive this independently of the migration's `expected` map: the
            // check must compare the old and new representations rather than reuse migration
            // output and merely read it back.
            Ok(PreUpgradeState {
                expected: expected_lifecycle_state::<T>().into_iter().collect(),
            }
            .encode())
        }

        #[cfg(feature = "try-runtime")]
        fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
            let before: PreUpgradeState =
                Decode::decode(&mut &state[..]).map_err(|_| "pre_upgrade state must decode")?;
            ensure!(
                HasMigrationRun::<T>::get(MIGRATION_NAME.to_vec()),
                "subnet lifecycle migration marker must be set"
            );
            let expected: BTreeMap<NetUid, SubnetLifecycleState> =
                before.expected.into_iter().collect();
            validate_lifecycle_state::<T>(expected)
        }
    }
}

#[cfg(all(test, feature = "try-runtime"))]
mod try_runtime_tests {
    use super::*;
    use crate::{
        subnets::dissolution::DissolveCleanupStatus,
        tests::mock::{Test, new_test_ext},
    };
    use frame_support::traits::OnRuntimeUpgrade;

    type Migration = subnet_state::Migration<Test>;

    #[test]
    fn post_upgrade_rejects_missing_incorrect_and_extra_lifecycle_entries()
    -> Result<(), sp_runtime::TryRuntimeError> {
        new_test_ext(1).execute_with(|| {
            let registered = NetUid::from(100);
            let queued = NetUid::from(101);
            let dissolving = NetUid::from(102);
            let extra = NetUid::from(103);
            let started = NetUid::from(104);

            NetworksAdded::<Test>::insert(registered, true);
            NetworksAdded::<Test>::insert(started, true);
            FirstEmissionBlockNumber::<Test>::insert(started, 10);
            DissolveCleanupQueue::<Test>::set(vec![queued, dissolving]);
            CurrentDissolveCleanupStatus::<Test>::set(Some(DissolveCleanupStatus::new(dissolving)));

            // Simulate the old runtime, which did not populate the reporting map.
            let existing: Vec<_> = SubnetState::<Test>::iter_keys().collect();
            for netuid in existing {
                SubnetState::<Test>::remove(netuid);
            }
            HasMigrationRun::<Test>::remove(MIGRATION_NAME.to_vec());

            let pre_upgrade = Migration::pre_upgrade()?;
            Migration::on_runtime_upgrade();
            assert!(Migration::post_upgrade(pre_upgrade.clone()).is_ok());
            assert!(subnet_state::try_state::<Test>().is_ok());

            SubnetState::<Test>::remove(registered);
            assert!(
                Migration::post_upgrade(pre_upgrade.clone()).is_err(),
                "a missing lifecycle entry must fail validation"
            );
            assert!(subnet_state::try_state::<Test>().is_err());

            SubnetState::<Test>::insert(registered, SubnetLifecycleState::PendingDissolution);
            assert!(
                Migration::post_upgrade(pre_upgrade.clone()).is_err(),
                "an incorrectly classified lifecycle entry must fail validation"
            );
            assert!(subnet_state::try_state::<Test>().is_err());

            SubnetState::<Test>::insert(registered, SubnetLifecycleState::Registered);
            SubnetState::<Test>::insert(extra, SubnetLifecycleState::Registered);
            assert!(
                Migration::post_upgrade(pre_upgrade.clone()).is_err(),
                "an extra lifecycle entry must fail validation"
            );
            assert!(subnet_state::try_state::<Test>().is_err());

            SubnetState::<Test>::remove(extra);
            assert!(subnet_state::try_state::<Test>().is_ok());
            HasMigrationRun::<Test>::remove(MIGRATION_NAME.to_vec());
            assert!(
                Migration::post_upgrade(pre_upgrade).is_err(),
                "a missing completion marker must fail validation"
            );
            Ok(())
        })
    }
}