mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
## 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>
146 lines
5.3 KiB
Rust
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"
|
|
);
|
|
});
|
|
}
|