datahaven/operator/runtime/testnet/tests/migrations.rs
Steve Degosserie 72c3457183
fix: 🔨 Replace FreezeChainOnFailedMigration with SafeMode handler (#308)
## Summary

Replaced the `DefaultFailedMigrationHandler` (which completely froze the
chain on migration failures) with `EnterSafeModeOnFailedMigration`
across all three runtimes (mainnet, stagenet, testnet). When a migration
fails, the chain now automatically enters SafeMode instead of freezing,
allowing governance to intervene and fix issues while preventing regular
user transactions.

## Problem

Previously, when a runtime migration failed, the chain would use
`FreezeChainOnFailedMigration`, which completely halted all operations
including governance functions. This made it impossible to recover from
migration failures without manual intervention at the node level.

## Solution

Implemented `EnterSafeModeOnFailedMigration` which:
- **Enters SafeMode** when a migration fails: the chain remains
_indefinitely_ under safe mode until it is disabled, either with Sudo or
Governance.
- **Allows governance operations** to continue (Sudo, SafeMode, TxPause,
Preimage, Scheduler, etc.)
- **Blocks regular user transactions** to prevent interaction with
potentially inconsistent storage
- **Falls back to freezing** if SafeMode cannot be entered

## Changes

### Core Implementation
- **`runtime/common/src/migrations.rs`**: Added
`FailedMigrationHandler<SafeMode>` type alias that wraps
`EnterSafeModeOnFailedMigration` with comprehensive documentation
- **All three runtimes** (`mainnet`, `stagenet`, `testnet`):
- Updated `pallet_migrations::Config::FailedMigrationHandler` to use
`FailedMigrationHandler<SafeMode>`
  - Removed obsolete TODO comments

### Tests
Added comprehensive migration failure tests to all three runtimes:
- **`failed_migration_enters_safe_mode`**: Verifies SafeMode is
activated, expiry is set, and event is emitted
- **`safe_mode_allows_governance_during_migration_failure`**: Confirms
governance can exit SafeMode after migration failure
- **`migrations_force_calls_are_root_only`**: Existing test for
migration management permissions

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
2025-11-25 16:09:19 +01:00

146 lines
5.3 KiB
Rust

// Copyright 2025 DataHaven
// This file is part of DataHaven.
// DataHaven is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// DataHaven is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
#[path = "common.rs"]
mod common;
use common::*;
use datahaven_testnet_runtime::{
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System,
};
use frame_support::{assert_noop, assert_ok};
use pallet_migrations::{Call as MigrationsCall, HistoricCleanupSelector};
use sp_runtime::{traits::Dispatchable, DispatchError};
#[test]
fn migrations_force_calls_are_root_only() {
ExtBuilder::default().build().execute_with(|| {
let signed_origin = RuntimeOrigin::signed(account_id(ALICE));
let force_onboard =
RuntimeCall::MultiBlockMigrations(MigrationsCall::<Runtime>::force_onboard_mbms {});
assert_noop!(
force_onboard.clone().dispatch(signed_origin.clone()),
DispatchError::BadOrigin
);
assert_ok!(force_onboard.dispatch(RuntimeOrigin::root()));
let force_set_cursor =
RuntimeCall::MultiBlockMigrations(MigrationsCall::<Runtime>::force_set_cursor {
cursor: None,
});
assert_noop!(
force_set_cursor.clone().dispatch(signed_origin.clone()),
DispatchError::BadOrigin
);
assert_ok!(force_set_cursor.dispatch(RuntimeOrigin::root()));
let force_set_active =
RuntimeCall::MultiBlockMigrations(MigrationsCall::<Runtime>::force_set_active_cursor {
index: 0,
inner_cursor: None,
started_at: None,
});
assert_noop!(
force_set_active.clone().dispatch(signed_origin.clone()),
DispatchError::BadOrigin
);
assert_ok!(force_set_active.dispatch(RuntimeOrigin::root()));
let clear_historic =
RuntimeCall::MultiBlockMigrations(MigrationsCall::<Runtime>::clear_historic {
selector: HistoricCleanupSelector::Specific(Vec::new()),
});
assert_noop!(
clear_historic.clone().dispatch(signed_origin),
DispatchError::BadOrigin
);
assert_ok!(clear_historic.dispatch(RuntimeOrigin::root()));
});
}
#[test]
fn failed_migration_enters_safe_mode() {
ExtBuilder::default().build().execute_with(|| {
// Verify SafeMode is not active initially
assert!(
!SafeMode::is_entered(),
"SafeMode should not be active initially"
);
// Simulate a failed migration by directly calling the FailedMigrationHandler
// This tests that when migrations fail, the system enters SafeMode
use frame_support::migrations::FailedMigrationHandler;
type Handler = <Runtime as pallet_migrations::Config>::FailedMigrationHandler;
// Call the failed handler (simulating a migration failure)
let result = Handler::failed(Some(0));
// The handler should indicate that SafeMode was entered
assert_eq!(
result,
frame_support::migrations::FailedMigrationHandling::KeepStuck,
"Handler should keep the chain stuck in SafeMode"
);
// Verify that SafeMode is now active
assert!(
SafeMode::is_entered(),
"SafeMode should be active after migration failure"
);
// Get the block number when SafeMode should expire
let entered_until = pallet_safe_mode::EnteredUntil::<Runtime>::get();
assert!(
entered_until.is_some(),
"SafeMode should have an expiry block"
);
// Verify that the SafeMode event was emitted
let events = System::events();
assert!(
events.iter().any(|e| matches!(
e.event,
RuntimeEvent::SafeMode(pallet_safe_mode::Event::Entered { .. })
)),
"SafeMode::Entered event should be emitted"
);
});
}
#[test]
fn safe_mode_allows_governance_during_migration_failure() {
ExtBuilder::default().build().execute_with(|| {
// Simulate a failed migration
use frame_support::migrations::FailedMigrationHandler;
type Handler = <Runtime as pallet_migrations::Config>::FailedMigrationHandler;
Handler::failed(Some(0));
// Verify SafeMode is active
assert!(SafeMode::is_entered(), "SafeMode should be active");
// Test that SafeMode management calls are still allowed
let force_exit_call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
let result = force_exit_call.dispatch(RuntimeOrigin::root());
assert_ok!(result);
// Verify SafeMode is now inactive
assert!(
!SafeMode::is_entered(),
"SafeMode should be inactive after force exit"
);
});
}