diff --git a/Cargo.lock b/Cargo.lock index 06004704..421654d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5524,6 +5524,21 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-maintenance-mode" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-membership" version = "4.0.0-dev" @@ -10921,6 +10936,7 @@ dependencies = [ "pallet-collator-selection", "pallet-inv4", "pallet-ipf", + "pallet-maintenance-mode", "pallet-preimage", "pallet-randomness-collective-flip", "pallet-rmrk-core", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 4d2aeb75..4d9e0887 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -338,5 +338,6 @@ fn testnet_genesis( }, treasury: Default::default(), vesting: Default::default(), + maintenance_mode: Default::default(), } } diff --git a/pallets/maintenance-mode/Cargo.toml b/pallets/maintenance-mode/Cargo.toml new file mode 100644 index 00000000..6577db58 --- /dev/null +++ b/pallets/maintenance-mode/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pallet-maintenance-mode" +authors = [ "PureStake" ] +description = "Puts a FRAME-based runtime into maintenance mode where restricted interactions are allowed." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = "0.4" + +# Substrate +frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.26" } +frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.26" } +parity-scale-codec = { version = "3.0.0", default-features = false } +scale-info = { version = "2.0", default-features = false, features = [ "derive" ] } +sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.26" } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.26" } + + +[dev-dependencies] +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26" } + +[features] +default = [ "std" ] +std = [ + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = [ "frame-support/try-runtime" ] diff --git a/pallets/maintenance-mode/src/lib.rs b/pallets/maintenance-mode/src/lib.rs new file mode 100644 index 00000000..15202499 --- /dev/null +++ b/pallets/maintenance-mode/src/lib.rs @@ -0,0 +1,228 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! A pallet to put your runtime into a restricted maintenance or safe mode. This is useful when +//! performing site maintenance, running data migrations, or protecting the chain during an attack. +//! +//! This introduces one storage read to fetch the base filter for each extrinsic. However, it should +//! be that the state cache eliminates this cost almost entirely. I wonder if that can or should be +//! reflected in the weight calculation. +//! +//! Possible future improvements +//! 1. This could be more configurable by letting the runtime developer specify a type (probably an +//! enum) that can be converted into a filter. Similar end result (but different implementation) as +//! Acala has it +//! github.com/AcalaNetwork/Acala/blob/pause-transaction/modules/transaction-pause/src/lib.rs#L71 +//! +//! 2. Automatically enable maintenance mode after a long timeout is detected between blocks. +//! To implement this we would couple to the timestamp pallet and store the timestamp of the +//! previous block. +//! +//! 3. Different origins for entering and leaving maintenance mode. +//! +//! 4. Maintenance mode timeout. To avoid getting stuck in maintenance mode. It could automatically +//! switch back to normal mode after a pre-decided number of blocks. Maybe there could be an +//! extrinsic to extend the maintenance time. + +#![allow(non_camel_case_types)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +mod types; +pub use types::*; + +use frame_support::pallet; + +pub use pallet::*; + +#[pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_support::traits::{ + Contains, EnsureOrigin, OffchainWorker, OnFinalize, OnIdle, OnInitialize, OnRuntimeUpgrade, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::DispatchResult; + /// Pallet for migrations + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + /// Pause and resume execution of XCM + pub trait PauseXcmExecution { + fn suspend_xcm_execution() -> DispatchResult; + fn resume_xcm_execution() -> DispatchResult; + } + + impl PauseXcmExecution for () { + fn suspend_xcm_execution() -> DispatchResult { + Ok(()) + } + fn resume_xcm_execution() -> DispatchResult { + Ok(()) + } + } + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type + type Event: From + IsType<::Event>; + /// The base call filter to be used in normal operating mode + /// (When we aren't in the middle of a migration) + type NormalCallFilter: Contains; + /// The base call filter to be used when we are in the middle of migrations + /// This should be very restrictive. Probably not allowing anything except possibly + /// something like sudo or other emergency processes + type MaintenanceCallFilter: Contains; + /// The origin from which the call to enter or exit maintenance mode must come + /// Take care when choosing your maintenance call filter to ensure that you'll still be + /// able to return to normal mode. For example, if your MaintenanceOrigin is a council, make + /// sure that your councilors can still cast votes. + type MaintenanceOrigin: EnsureOrigin; + /// The executive hooks that will be used in normal operating mode + /// Important: Use AllPalletsReversedWithSystemFirst here if you dont want to modify the + /// hooks behaviour + type NormalExecutiveHooks: OnRuntimeUpgrade + + OnInitialize + + OnIdle + + OnFinalize + + OffchainWorker; + /// The executive hooks that will be used in maintenance mode + /// Important: Use AllPalletsReversedWithSystemFirst here if you dont want to modify the + /// hooks behaviour + type MaintenanceExecutiveHooks: OnRuntimeUpgrade + + OnInitialize + + OnIdle + + OnFinalize + + OffchainWorker; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// The chain was put into Maintenance Mode + EnteredMaintenanceMode, + /// The chain returned to its normal operating state + NormalOperationResumed, + /// The call to suspend on_idle XCM execution failed with inner error + FailedToSuspendIdleXcmExecution { error: DispatchError }, + /// The call to resume on_idle XCM execution failed with inner error + FailedToResumeIdleXcmExecution { error: DispatchError }, + } + + /// An error that can occur while executing this pallet's extrinsics. + #[pallet::error] + pub enum Error { + /// The chain cannot enter maintenance mode because it is already in maintenance mode + AlreadyInMaintenanceMode, + /// The chain cannot resume normal operation because it is not in maintenance mode + NotInMaintenanceMode, + } + + #[pallet::storage] + #[pallet::getter(fn maintenance_mode)] + /// Whether the site is in maintenance mode + type MaintenanceMode = StorageValue<_, bool, ValueQuery>; + + #[pallet::call] + impl Pallet { + /// Place the chain in maintenance mode + /// + /// Weight cost is: + /// * One DB read to ensure we're not already in maintenance mode + /// * Three DB writes - 1 for the mode, 1 for suspending xcm execution, 1 for the event + #[pallet::weight(T::DbWeight::get().read + 3 * T::DbWeight::get().write)] + pub fn enter_maintenance_mode(origin: OriginFor) -> DispatchResultWithPostInfo { + // Ensure Origin + T::MaintenanceOrigin::ensure_origin(origin)?; + + // Ensure we're not aleady in maintenance mode. + // This test is not strictly necessary, but seeing the error may help a confused chain + // operator during an emergency + ensure!( + !MaintenanceMode::::get(), + Error::::AlreadyInMaintenanceMode + ); + + // Write to storage + MaintenanceMode::::put(true); + + // Event + >::deposit_event(Event::EnteredMaintenanceMode); + + Ok(().into()) + } + + /// Return the chain to normal operating mode + /// + /// Weight cost is: + /// * One DB read to ensure we're in maintenance mode + /// * Three DB writes - 1 for the mode, 1 for resuming xcm execution, 1 for the event + #[pallet::weight(T::DbWeight::get().read + 3 * T::DbWeight::get().write)] + pub fn resume_normal_operation(origin: OriginFor) -> DispatchResultWithPostInfo { + // Ensure Origin + T::MaintenanceOrigin::ensure_origin(origin)?; + + // Ensure we're actually in maintenance mode. + // This test is not strictly necessary, but seeing the error may help a confused chain + // operator during an emergency + ensure!( + MaintenanceMode::::get(), + Error::::NotInMaintenanceMode + ); + + // Write to storage + MaintenanceMode::::put(false); + + // Event + >::deposit_event(Event::NormalOperationResumed); + + Ok(().into()) + } + } + + #[derive(Default)] + #[pallet::genesis_config] + /// Genesis config for maintenance mode pallet + pub struct GenesisConfig { + /// Whether to launch in maintenance mode + pub start_in_maintenance_mode: bool, + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + if self.start_in_maintenance_mode { + MaintenanceMode::::put(true); + } + } + } + + impl Contains for Pallet { + fn contains(call: &T::Call) -> bool { + if MaintenanceMode::::get() { + T::MaintenanceCallFilter::contains(call) + } else { + T::NormalCallFilter::contains(call) + } + } + } +} diff --git a/pallets/maintenance-mode/src/mock.rs b/pallets/maintenance-mode/src/mock.rs new file mode 100644 index 00000000..29a8d366 --- /dev/null +++ b/pallets/maintenance-mode/src/mock.rs @@ -0,0 +1,332 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! A minimal runtime including the maintenance-mode pallet +use super::*; +use crate as pallet_maintenance_mode; +use cumulus_primitives_core::{relay_chain::BlockNumber as RelayBlockNumber, DmpMessageHandler}; +use frame_support::{ + construct_runtime, parameter_types, + traits::{ + Contains, Everything, GenesisBuild, OffchainWorker, OnFinalize, OnIdle, OnInitialize, + OnRuntimeUpgrade, + }, + weights::Weight, +}; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +//TODO use TestAccount once it is in a common place (currently it lives with democracy precompiles) +pub type AccountId = u64; +pub type BlockNumber = u64; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + MaintenanceMode: pallet_maintenance_mode::{Pallet, Call, Storage, Event, Config}, + MockPalletMaintenanceHooks: mock_pallet_maintenance_hooks::{Pallet, Call, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const SS58Prefix: u8 = 42; +} +impl frame_system::Config for Test { + type BaseCallFilter = MaintenanceMode; + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +/// During maintenance mode we will not allow any calls. +pub struct MaintenanceCallFilter; +impl Contains for MaintenanceCallFilter { + fn contains(_: &Call) -> bool { + false + } +} + +pub struct MaintenanceDmpHandler; +#[cfg(feature = "xcm-support")] +impl DmpMessageHandler for MaintenanceDmpHandler { + // This implementation makes messages be queued + // Since the limit is 0, messages are queued for next iteration + fn handle_dmp_messages( + _iter: impl Iterator)>, + _limit: Weight, + ) -> Weight { + return 1; + } +} + +pub struct NormalDmpHandler; +#[cfg(feature = "xcm-support")] +impl DmpMessageHandler for NormalDmpHandler { + // This implementation makes messages be queued + // Since the limit is 0, messages are queued for next iteration + fn handle_dmp_messages( + _iter: impl Iterator)>, + _limit: Weight, + ) -> Weight { + return 0; + } +} + +impl mock_pallet_maintenance_hooks::Config for Test { + type Event = Event; +} + +// Pallet to throw events, used to test maintenance mode hooks +#[frame_support::pallet] +pub mod mock_pallet_maintenance_hooks { + use frame_support::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From + IsType<::Event>; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet {} + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + MaintenanceOnIdle, + MaintenanceOnInitialize, + MaintenanceOffchainWorker, + MaintenanceOnFinalize, + MaintenanceOnRuntimeUpgrade, + NormalOnIdle, + NormalOnInitialize, + NormalOffchainWorker, + NormalOnFinalize, + NormalOnRuntimeUpgrade, + } +} + +pub struct MaintenanceHooks; + +impl OnInitialize for MaintenanceHooks { + fn on_initialize(_n: BlockNumber) -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::MaintenanceOnInitialize, + ); + return 1; + } +} + +impl OnIdle for MaintenanceHooks { + fn on_idle(_n: BlockNumber, _max_weight: Weight) -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::MaintenanceOnIdle, + ); + return 1; + } +} + +impl OnRuntimeUpgrade for MaintenanceHooks { + fn on_runtime_upgrade() -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::MaintenanceOnRuntimeUpgrade, + ); + return 1; + } +} + +impl OnFinalize for MaintenanceHooks { + fn on_finalize(_n: BlockNumber) { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::MaintenanceOnFinalize, + ); + } +} + +impl OffchainWorker for MaintenanceHooks { + fn offchain_worker(_n: BlockNumber) { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::MaintenanceOffchainWorker, + ); + } +} + +pub struct NormalHooks; + +impl OnInitialize for NormalHooks { + fn on_initialize(_n: BlockNumber) -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::NormalOnInitialize, + ); + return 0; + } +} + +impl OnIdle for NormalHooks { + fn on_idle(_n: BlockNumber, _max_weight: Weight) -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::NormalOnIdle, + ); + return 0; + } +} + +impl OnRuntimeUpgrade for NormalHooks { + fn on_runtime_upgrade() -> Weight { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::NormalOnRuntimeUpgrade, + ); + + return 0; + } +} + +impl OnFinalize for NormalHooks { + fn on_finalize(_n: BlockNumber) { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::NormalOnFinalize, + ); + } +} + +impl OffchainWorker for NormalHooks { + fn offchain_worker(_n: BlockNumber) { + MockPalletMaintenanceHooks::deposit_event( + mock_pallet_maintenance_hooks::Event::NormalOffchainWorker, + ); + } +} + +impl Config for Test { + type Event = Event; + type NormalCallFilter = Everything; + type MaintenanceCallFilter = MaintenanceCallFilter; + type MaintenanceOrigin = EnsureRoot; + #[cfg(feature = "xcm-support")] + type XcmExecutionManager = (); + #[cfg(feature = "xcm-support")] + type NormalDmpHandler = NormalDmpHandler; + #[cfg(feature = "xcm-support")] + type MaintenanceDmpHandler = MaintenanceDmpHandler; + type NormalExecutiveHooks = NormalHooks; + type MaintenanceExecutiveHooks = MaintenanceHooks; +} + +/// Externality builder for pallet maintenance mode's mock runtime +pub(crate) struct ExtBuilder { + maintenance_mode: bool, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { + maintenance_mode: false, + } + } +} + +impl ExtBuilder { + pub(crate) fn with_maintenance_mode(mut self, m: bool) -> Self { + self.maintenance_mode = m; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .expect("Frame system builds valid default genesis config"); + + GenesisBuild::::assimilate_storage( + &pallet_maintenance_mode::GenesisConfig { + start_in_maintenance_mode: self.maintenance_mode, + }, + &mut t, + ) + .expect("Pallet maintenance mode storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let Event::MaintenanceMode(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>() +} + +pub(crate) fn mock_events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let Event::MockPalletMaintenanceHooks(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>() +} diff --git a/pallets/maintenance-mode/src/tests.rs b/pallets/maintenance-mode/src/tests.rs new file mode 100644 index 00000000..557ad503 --- /dev/null +++ b/pallets/maintenance-mode/src/tests.rs @@ -0,0 +1,203 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! Unit testing +use crate::mock::{ + events, mock_events, Call as OuterCall, ExtBuilder, MaintenanceMode, Origin, Test, +}; +use crate::{Call, Error, Event, ExecutiveHooks}; +use cumulus_primitives_core::DmpMessageHandler; +use frame_support::{ + assert_noop, assert_ok, + dispatch::Dispatchable, + traits::{OffchainWorker, OnFinalize, OnIdle, OnInitialize, OnRuntimeUpgrade}, +}; + +#[test] +fn can_remark_during_normal_operation() { + ExtBuilder::default().build().execute_with(|| { + let call: OuterCall = frame_system::Call::remark { remark: vec![] }.into(); + assert_ok!(call.dispatch(Origin::signed(1))); + }) +} + +#[test] +fn cannot_remark_during_maintenance_mode() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + let call: OuterCall = frame_system::Call::remark { remark: vec![] }.into(); + assert_noop!( + call.dispatch(Origin::signed(1)), + frame_system::Error::::CallFiltered + ); + }) +} + +#[test] +fn can_enter_maintenance_mode() { + ExtBuilder::default().build().execute_with(|| { + let call: OuterCall = Call::enter_maintenance_mode {}.into(); + assert_ok!(call.dispatch(Origin::root())); + + assert_eq!(events(), vec![Event::EnteredMaintenanceMode,]); + }) +} + +#[test] +fn cannot_enter_maintenance_mode_from_wrong_origin() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + let call: OuterCall = Call::enter_maintenance_mode {}.into(); + assert_noop!( + call.dispatch(Origin::signed(1)), + frame_system::Error::::CallFiltered + ); + }) +} + +#[test] +fn cannot_enter_maintenance_mode_when_already_in_it() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + let call: OuterCall = Call::enter_maintenance_mode {}.into(); + assert_noop!( + call.dispatch(Origin::root()), + Error::::AlreadyInMaintenanceMode + ); + }) +} + +#[test] +fn can_resume_normal_operation() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + let call: OuterCall = Call::resume_normal_operation {}.into(); + assert_ok!(call.dispatch(Origin::root())); + + assert_eq!(events(), vec![Event::NormalOperationResumed,]); + }) +} + +#[test] +fn cannot_resume_normal_operation_from_wrong_origin() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + let call: OuterCall = Call::resume_normal_operation {}.into(); + assert_noop!( + call.dispatch(Origin::signed(1)), + frame_system::Error::::CallFiltered + ); + }) +} + +#[test] +fn cannot_resume_normal_operation_while_already_operating_normally() { + ExtBuilder::default().build().execute_with(|| { + let call: OuterCall = Call::resume_normal_operation {}.into(); + assert_noop!( + call.dispatch(Origin::root()), + Error::::NotInMaintenanceMode + ); + }) +} + +#[cfg(feature = "xcm-support")] +#[test] +fn normal_dmp_in_non_maintenance() { + ExtBuilder::default() + .with_maintenance_mode(false) + .build() + .execute_with(|| { + assert_eq!( + MaintenanceMode::handle_dmp_messages(vec![].into_iter(), 1), + 0 + ); + }) +} + +#[cfg(feature = "xcm-support")] +#[test] +fn maintenance_dmp_in_maintenance() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + assert_eq!( + MaintenanceMode::handle_dmp_messages(vec![].into_iter(), 1), + 1 + ); + }) +} + +#[test] +fn normal_hooks_in_non_maintenance() { + ExtBuilder::default() + .with_maintenance_mode(false) + .build() + .execute_with(|| { + assert_eq!(ExecutiveHooks::::on_idle(0, 0), 0); + assert_eq!(ExecutiveHooks::::on_initialize(0), 0); + assert_eq!(ExecutiveHooks::::on_runtime_upgrade(), 0); + ExecutiveHooks::::on_finalize(0); + ExecutiveHooks::::offchain_worker(0); + + assert_eq!( + mock_events(), + [ + crate::mock::mock_pallet_maintenance_hooks::Event::NormalOnIdle, + crate::mock::mock_pallet_maintenance_hooks::Event::NormalOnInitialize, + crate::mock::mock_pallet_maintenance_hooks::Event::NormalOnRuntimeUpgrade, + crate::mock::mock_pallet_maintenance_hooks::Event::NormalOnFinalize, + crate::mock::mock_pallet_maintenance_hooks::Event::NormalOffchainWorker + ] + ); + }) +} + +#[test] +fn maintenance_hooks_in_maintenance() { + ExtBuilder::default() + .with_maintenance_mode(true) + .build() + .execute_with(|| { + assert_eq!(ExecutiveHooks::::on_idle(0, 0), 1); + assert_eq!(ExecutiveHooks::::on_initialize(0), 1); + assert_eq!(ExecutiveHooks::::on_runtime_upgrade(), 1); + + ExecutiveHooks::::on_finalize(0); + ExecutiveHooks::::offchain_worker(0); + assert_eq!( + mock_events(), + [ + crate::mock::mock_pallet_maintenance_hooks::Event::MaintenanceOnIdle, + crate::mock::mock_pallet_maintenance_hooks::Event::MaintenanceOnInitialize, + crate::mock::mock_pallet_maintenance_hooks::Event::MaintenanceOnRuntimeUpgrade, + crate::mock::mock_pallet_maintenance_hooks::Event::MaintenanceOnFinalize, + crate::mock::mock_pallet_maintenance_hooks::Event::MaintenanceOffchainWorker + ] + ); + }) +} diff --git a/pallets/maintenance-mode/src/types.rs b/pallets/maintenance-mode/src/types.rs new file mode 100644 index 00000000..ca87cadb --- /dev/null +++ b/pallets/maintenance-mode/src/types.rs @@ -0,0 +1,111 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +// This file contains the ExecutiveHooks type which is intended to be used +// with frame_executive::Executive. This instructs which pallets execute +// hooks in each of the normal and maintenance modes. +use super::*; +use frame_support::{ + traits::{OffchainWorker, OnFinalize, OnIdle, OnInitialize, OnRuntimeUpgrade}, + weights::Weight, +}; +use sp_std::marker::PhantomData; + +pub struct ExecutiveHooks(PhantomData); +type BlockNumberOf = ::BlockNumber; + +impl OnIdle> for ExecutiveHooks +where + T: Config, +{ + fn on_idle(n: BlockNumberOf, remaining_weight: Weight) -> Weight { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::on_idle(n, remaining_weight) + } else { + T::NormalExecutiveHooks::on_idle(n, remaining_weight) + } + } +} + +impl OnInitialize> for ExecutiveHooks +where + T: Config, +{ + fn on_initialize(n: BlockNumberOf) -> Weight { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::on_initialize(n) + } else { + T::NormalExecutiveHooks::on_initialize(n) + } + } +} + +impl OnFinalize> for ExecutiveHooks +where + T: Config, +{ + fn on_finalize(n: BlockNumberOf) { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::on_finalize(n) + } else { + T::NormalExecutiveHooks::on_finalize(n) + } + } +} + +impl OffchainWorker> for ExecutiveHooks +where + T: Config, +{ + fn offchain_worker(n: BlockNumberOf) { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::offchain_worker(n) + } else { + T::NormalExecutiveHooks::offchain_worker(n) + } + } +} + +impl OnRuntimeUpgrade for ExecutiveHooks +where + T: Config, +{ + fn on_runtime_upgrade() -> Weight { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::on_runtime_upgrade() + } else { + T::NormalExecutiveHooks::on_runtime_upgrade() + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::pre_upgrade() + } else { + T::NormalExecutiveHooks::pre_upgrade() + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + if Pallet::::maintenance_mode() { + T::MaintenanceExecutiveHooks::post_upgrade() + } else { + T::NormalExecutiveHooks::post_upgrade() + } + } +} diff --git a/runtime/tinkernet/Cargo.toml b/runtime/tinkernet/Cargo.toml index 59a7c7dc..80e1dfa3 100644 --- a/runtime/tinkernet/Cargo.toml +++ b/runtime/tinkernet/Cargo.toml @@ -30,6 +30,8 @@ pallet-inv4 = { git = "https://github.com/InvArch/InvArch-Frames", branch = "mai #pallet-inv4 = { path = "../../../InvArch-Pallet-Library/INV4/pallet-inv4", default-features = false } #pallet-ipf = { path = "../../../InvArch-Pallet-Library/INV4/pallet-ipf", default-features = false } +pallet-maintenance-mode = { path = "../../pallets/maintenance-mode", default-features = false } + ## Parity's Unique + RMRK Pallets pallet-rmrk-core = { git = "https://github.com/rmrk-team/rmrk-substrate", default-features = false, rev = "405af2499ca2a9b3f4717eec238fc80dd6abc3f7" } pallet-rmrk-equip = { git = "https://github.com/rmrk-team/rmrk-substrate", default-features = false, rev = "405af2499ca2a9b3f4717eec238fc80dd6abc3f7" } @@ -170,6 +172,7 @@ std = [ "orml-vesting/std", "pallet-ipf/std", "pallet-inv4/std", + "pallet-maintenance-mode/std" ] runtime-benchmarks = [ diff --git a/runtime/tinkernet/src/lib.rs b/runtime/tinkernet/src/lib.rs index 49f254c9..d068016c 100644 --- a/runtime/tinkernet/src/lib.rs +++ b/runtime/tinkernet/src/lib.rs @@ -187,7 +187,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("tinkernet_node"), impl_name: create_runtime_str!("tinkernet_node"), authoring_version: 1, - spec_version: 3, + spec_version: 4, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -281,6 +281,70 @@ impl Contains for BaseFilter { } } +pub struct MaintenanceFilter; +impl Contains for MaintenanceFilter { + fn contains(c: &Call) -> bool { + match c { + Call::Balances(_) => false, + Call::Vesting(_) => false, + _ => true, + } + } +} + +/// The hooks we want to run in Maintenance Mode +pub struct MaintenanceHooks; + +impl frame_support::traits::OnInitialize for MaintenanceHooks { + fn on_initialize(n: BlockNumber) -> Weight { + AllPalletsReversedWithSystemFirst::on_initialize(n) + } +} + +impl frame_support::traits::OnRuntimeUpgrade for MaintenanceHooks { + fn on_runtime_upgrade() -> Weight { + AllPalletsReversedWithSystemFirst::on_runtime_upgrade() + } + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + AllPalletsReversedWithSystemFirst::pre_upgrade() + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + AllPalletsReversedWithSystemFirst::post_upgrade() + } +} + +impl frame_support::traits::OnFinalize for MaintenanceHooks { + fn on_finalize(n: BlockNumber) { + AllPalletsReversedWithSystemFirst::on_finalize(n) + } +} + +impl frame_support::traits::OnIdle for MaintenanceHooks { + fn on_idle(_n: BlockNumber, _max_weight: Weight) -> Weight { + 0 + } +} + +impl frame_support::traits::OffchainWorker for MaintenanceHooks { + fn offchain_worker(n: BlockNumber) { + AllPalletsReversedWithSystemFirst::offchain_worker(n) + } +} + +impl pallet_maintenance_mode::Config for Runtime { + type Event = Event; + type NormalCallFilter = BaseFilter; + type MaintenanceCallFilter = MaintenanceFilter; + type MaintenanceOrigin = EnsureRoot; + // We use AllPalletsReversedWithSystemFirst because we dont want to change the hooks in normal + // operation + type NormalExecutiveHooks = AllPalletsReversedWithSystemFirst; + type MaintenanceExecutiveHooks = MaintenanceHooks; +} + // Configure FRAME pallets to include in runtime. impl frame_system::Config for Runtime { @@ -321,7 +385,7 @@ impl frame_system::Config for Runtime { /// The weight of database operations that the runtime can invoke. type DbWeight = RocksDbWeight; /// The basic call filter to use in dispatchable. - type BaseCallFilter = Everything; + type BaseCallFilter = MaintenanceMode; /// Weight information for the extrinsics of this pallet. type SystemWeightInfo = frame_system::weights::SubstrateWeight; /// Block & extrinsics weights: base values and limits. @@ -1102,6 +1166,7 @@ construct_runtime!( ParachainInfo: parachain_info::{Pallet, Storage, Config} = 4, Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 5, Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 6, + MaintenanceMode: pallet_maintenance_mode::{Pallet, Call, Config, Storage, Event} = 7, // Monetary stuff Balances: pallet_balances::{Pallet, Call, Storage, Config, Event} = 10,