mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
feat: Add SafeMode and TxPause Pallets (#192)
### Overview This PR integrates the `pallet-safe-mode` and `pallet-tx-pause` from Polkadot SDK to provide comprehensive emergency governance controls across all DataHaven runtime networks (mainnet, stagenet, testnet). ### Key Changes #### 🔧 **Core Integration** - **Dependencies**: Added `pallet-safe-mode` and `pallet-tx-pause` from `polkadot-stable2412-6` - **Runtime Integration**: Integrated both pallets across all three runtime networks with pallet indices 103 and 104 - **Call Filtering**: Implemented unified `RuntimeCallFilter` that combines Normal, SafeMode, and TxPause restrictions #### 🛡️ **SafeMode Pallet Configuration** - **Duration**: 1 day activation period (`DAYS` constant) - **Deposits**: Disabled permissionless entry/extension (all `None`) - **Origins**: Root-only for all force operations (`force_enter`, `force_exit`, `force_extend`, etc.) - **Whitelisting**: SafeMode and Sudo calls are immune to restrictions #### ⏸️ **TxPause Pallet Configuration** - **Origins**: Root-only pause/unpause control - **Whitelisting**: SafeMode and Sudo calls cannot be paused - **Max Call Name Length**: 256 characters #### 🏗️ **Architecture** - **Shared Types**: Created `operator/runtime/common/src/safe_mode.rs` with reusable configurations - **Combined Filtering**: `RuntimeCallFilter` applies all three filter layers (Normal + SafeMode + TxPause) - **Consistent Config**: Identical configuration across mainnet, stagenet, and testnet #### 📊 **Infrastructure Updates** - **Benchmarking**: Added both pallets to benchmark suites across all networks - **Weight Mappings**: Placeholder weights using Substrate defaults (ready for chain-specific benchmarking) - **Metadata**: Updated runtime metadata for new pallet exposure #### 🧪 **Testing Framework** - **Coverage**: Tests for individual pallet behavior, combined restrictions, whitelisting, and edge cases ### Emergency Control Capabilities **SafeMode Pallet** (8 calls): - User calls: `enter`, `extend`, `release_deposit` - Force calls: `force_enter`, `force_exit`, `force_extend`, `force_slash_deposit`, `force_release_deposit` **TxPause Pallet** (2 calls): - `pause_call` / `unpause_call` - Granular transaction type pausing --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
This commit is contained in:
parent
b4f697f954
commit
ac09a4f2bb
34 changed files with 1834 additions and 13 deletions
43
operator/Cargo.lock
generated
43
operator/Cargo.lock
generated
|
|
@ -2892,6 +2892,7 @@ dependencies = [
|
|||
"pallet-proxy",
|
||||
"pallet-randomness",
|
||||
"pallet-referenda",
|
||||
"pallet-safe-mode",
|
||||
"pallet-scheduler",
|
||||
"pallet-session",
|
||||
"pallet-storage-providers",
|
||||
|
|
@ -2901,6 +2902,7 @@ dependencies = [
|
|||
"pallet-transaction-payment",
|
||||
"pallet-transaction-payment-rpc-runtime-api",
|
||||
"pallet-treasury",
|
||||
"pallet-tx-pause",
|
||||
"pallet-utility",
|
||||
"pallet-whitelist",
|
||||
"parity-scale-codec",
|
||||
|
|
@ -3081,7 +3083,9 @@ dependencies = [
|
|||
"pallet-evm",
|
||||
"pallet-evm-precompile-proxy",
|
||||
"pallet-migrations",
|
||||
"pallet-safe-mode",
|
||||
"pallet-treasury",
|
||||
"pallet-tx-pause",
|
||||
"parity-scale-codec",
|
||||
"polkadot-primitives",
|
||||
"polkadot-runtime-common",
|
||||
|
|
@ -3166,6 +3170,7 @@ dependencies = [
|
|||
"pallet-proxy",
|
||||
"pallet-randomness",
|
||||
"pallet-referenda",
|
||||
"pallet-safe-mode",
|
||||
"pallet-scheduler",
|
||||
"pallet-session",
|
||||
"pallet-storage-providers",
|
||||
|
|
@ -3175,6 +3180,7 @@ dependencies = [
|
|||
"pallet-transaction-payment",
|
||||
"pallet-transaction-payment-rpc-runtime-api",
|
||||
"pallet-treasury",
|
||||
"pallet-tx-pause",
|
||||
"pallet-utility",
|
||||
"pallet-whitelist",
|
||||
"parity-scale-codec",
|
||||
|
|
@ -3306,6 +3312,7 @@ dependencies = [
|
|||
"pallet-proxy",
|
||||
"pallet-randomness",
|
||||
"pallet-referenda",
|
||||
"pallet-safe-mode",
|
||||
"pallet-scheduler",
|
||||
"pallet-session",
|
||||
"pallet-storage-providers",
|
||||
|
|
@ -3315,6 +3322,7 @@ dependencies = [
|
|||
"pallet-transaction-payment",
|
||||
"pallet-transaction-payment-rpc-runtime-api",
|
||||
"pallet-treasury",
|
||||
"pallet-tx-pause",
|
||||
"pallet-utility",
|
||||
"pallet-whitelist",
|
||||
"parity-scale-codec",
|
||||
|
|
@ -9589,6 +9597,24 @@ dependencies = [
|
|||
"sp-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-safe-mode"
|
||||
version = "20.0.0"
|
||||
source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753"
|
||||
dependencies = [
|
||||
"docify",
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-balances",
|
||||
"pallet-proxy",
|
||||
"pallet-utility",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-arithmetic",
|
||||
"sp-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-scheduler"
|
||||
version = "40.2.0"
|
||||
|
|
@ -9789,6 +9815,23 @@ dependencies = [
|
|||
"sp-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-tx-pause"
|
||||
version = "20.0.0"
|
||||
source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753"
|
||||
dependencies = [
|
||||
"docify",
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-balances",
|
||||
"pallet-proxy",
|
||||
"pallet-utility",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-utility"
|
||||
version = "39.1.0"
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ pallet-multisig = { git = "https://github.com/paritytech/polkadot-sdk", tag = "p
|
|||
pallet-offences = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-parameters = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-preimage = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-safe-mode = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-tx-pause = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-collective = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
pallet-conviction-voting = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pallet-balances = { workspace = true }
|
|||
pallet-evm = { workspace = true }
|
||||
pallet-evm-precompile-proxy = { workspace = true }
|
||||
pallet-migrations = { workspace = true }
|
||||
pallet-safe-mode = { workspace = true }
|
||||
pallet-tx-pause = { workspace = true }
|
||||
pallet-treasury = { workspace = true }
|
||||
polkadot-primitives = { workspace = true }
|
||||
polkadot-runtime-common = { workspace = true }
|
||||
|
|
@ -34,6 +36,8 @@ std = [
|
|||
"pallet-evm/std",
|
||||
"pallet-evm-precompile-proxy/std",
|
||||
"pallet-migrations/std",
|
||||
"pallet-safe-mode/std",
|
||||
"pallet-tx-pause/std",
|
||||
"pallet-treasury/std",
|
||||
"polkadot-primitives/std",
|
||||
"polkadot-runtime-common/std",
|
||||
|
|
@ -48,6 +52,8 @@ std = [
|
|||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"pallet-migrations/runtime-benchmarks",
|
||||
"pallet-safe-mode/runtime-benchmarks",
|
||||
"pallet-tx-pause/runtime-benchmarks",
|
||||
"polkadot-primitives/runtime-benchmarks",
|
||||
"polkadot-runtime-common/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ pub mod deal_with_fees;
|
|||
pub mod impl_on_charge_evm_transaction;
|
||||
pub mod migrations;
|
||||
pub use migrations::*;
|
||||
pub mod safe_mode;
|
||||
pub use safe_mode::*;
|
||||
|
||||
use fp_account::EthereumSignature;
|
||||
pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;
|
||||
|
|
|
|||
73
operator/runtime/common/src/safe_mode.rs
Normal file
73
operator/runtime/common/src/safe_mode.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2019-2025 DataHaven Inc.
|
||||
// This file is part of DataHaven.
|
||||
|
||||
// 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.
|
||||
|
||||
// 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 Moonbeam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Safe Mode and Tx Pause shared types, constants, and utilities
|
||||
|
||||
use crate::time::DAYS;
|
||||
use crate::Balance;
|
||||
use frame_support::{parameter_types, traits::Contains};
|
||||
use pallet_tx_pause::RuntimeCallNameOf;
|
||||
use polkadot_primitives::BlockNumber;
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
// Safe Mode Constants
|
||||
parameter_types! {
|
||||
/// Default duration for safe mode activation (1 day)
|
||||
pub const SafeModeDuration: BlockNumber = DAYS;
|
||||
pub const SafeModeEnterDeposit: Option<Balance> = None;
|
||||
/// Safe mode extend deposit - None disables permissionless extend
|
||||
pub const SafeModeExtendDeposit: Option<Balance> = None;
|
||||
/// Release delay - None disables permissionless release
|
||||
pub const ReleaseDelayNone: Option<BlockNumber> = None;
|
||||
}
|
||||
|
||||
/// Calls that cannot be paused by the tx-pause pallet.
|
||||
pub struct TxPauseWhitelistedCalls<R>(PhantomData<R>);
|
||||
/// Whitelist `Balances::transfer_keep_alive`, all others are pauseable.
|
||||
impl<R> Contains<RuntimeCallNameOf<R>> for TxPauseWhitelistedCalls<R>
|
||||
where
|
||||
R: pallet_tx_pause::Config,
|
||||
{
|
||||
fn contains(full_name: &RuntimeCallNameOf<R>) -> bool {
|
||||
match (full_name.0.as_slice(), full_name.1.as_slice()) {
|
||||
// sudo calls
|
||||
(b"Sudo", _) => true,
|
||||
// SafeMode calls
|
||||
(b"SafeMode", _) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined Call Filter that applies Normal, SafeMode, and TxPause filters
|
||||
/// This filter is generic over the runtime call type and identical across all runtimes
|
||||
pub struct RuntimeCallFilter<Call, NormalFilter, SafeModeFilter, TxPauseFilter>(
|
||||
PhantomData<(Call, NormalFilter, SafeModeFilter, TxPauseFilter)>,
|
||||
);
|
||||
|
||||
impl<Call, NormalFilter, SafeModeFilter, TxPauseFilter> Contains<Call>
|
||||
for RuntimeCallFilter<Call, NormalFilter, SafeModeFilter, TxPauseFilter>
|
||||
where
|
||||
NormalFilter: Contains<Call>,
|
||||
SafeModeFilter: Contains<Call>,
|
||||
TxPauseFilter: Contains<Call>,
|
||||
{
|
||||
fn contains(call: &Call) -> bool {
|
||||
NormalFilter::contains(call)
|
||||
&& SafeModeFilter::contains(call)
|
||||
&& TxPauseFilter::contains(call)
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true }
|
|||
pallet-datahaven-native-transfer = { workspace = true }
|
||||
pallet-parameters = { workspace = true }
|
||||
pallet-preimage = { workspace = true }
|
||||
pallet-safe-mode = { workspace = true }
|
||||
pallet-tx-pause = { workspace = true }
|
||||
pallet-proxy = { workspace = true }
|
||||
pallet-referenda = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
|
|
@ -211,6 +213,8 @@ std = [
|
|||
"pallet-offences/std",
|
||||
"pallet-parameters/std",
|
||||
"pallet-preimage/std",
|
||||
"pallet-safe-mode/std",
|
||||
"pallet-tx-pause/std",
|
||||
"pallet-referenda/std",
|
||||
"pallet-proxy/std",
|
||||
"pallet-scheduler/std",
|
||||
|
|
@ -310,6 +314,8 @@ runtime-benchmarks = [
|
|||
"pallet-offences/runtime-benchmarks",
|
||||
"pallet-parameters/runtime-benchmarks",
|
||||
"pallet-preimage/runtime-benchmarks",
|
||||
"pallet-safe-mode/runtime-benchmarks",
|
||||
"pallet-tx-pause/runtime-benchmarks",
|
||||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
|
|
@ -358,6 +364,8 @@ try-runtime = [
|
|||
"pallet-offences/try-runtime",
|
||||
"pallet-parameters/try-runtime",
|
||||
"pallet-preimage/try-runtime",
|
||||
"pallet-safe-mode/try-runtime",
|
||||
"pallet-tx-pause/try-runtime",
|
||||
"pallet-referenda/try-runtime",
|
||||
"pallet-proxy/try-runtime",
|
||||
"pallet-scheduler/try-runtime",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_transaction_payment, TransactionPayment]
|
||||
[pallet_parameters, Parameters]
|
||||
[pallet_message_queue, MessageQueue]
|
||||
[pallet_safe_mode, SafeMode]
|
||||
[pallet_tx_pause, TxPause]
|
||||
|
||||
// EVM pallets
|
||||
[pallet_evm, Evm]
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ use super::{
|
|||
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
|
||||
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
|
||||
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
|
||||
};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
|
|
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
|
|||
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
|
||||
MigrationIdentifierMaxLen, MigrationStatusHandler,
|
||||
},
|
||||
safe_mode::{
|
||||
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
|
||||
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
|
||||
},
|
||||
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
|
||||
};
|
||||
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
|
||||
|
|
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calls that can bypass the safe-mode pallet.
|
||||
/// These calls are essential for emergency governance and system maintenance.
|
||||
pub struct SafeModeWhitelistedCalls;
|
||||
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
|
||||
fn contains(call: &RuntimeCall) -> bool {
|
||||
match call {
|
||||
// Core system calls
|
||||
RuntimeCall::System(_) => true,
|
||||
// Safe mode management
|
||||
RuntimeCall::SafeMode(_) => true,
|
||||
// Transaction pause management
|
||||
RuntimeCall::TxPause(_) => true,
|
||||
// Emergency admin access (testnet/dev only)
|
||||
RuntimeCall::Sudo(_) => true,
|
||||
// Governance infrastructure - critical for emergency responses
|
||||
RuntimeCall::Whitelist(_) => true,
|
||||
RuntimeCall::Preimage(_) => true,
|
||||
RuntimeCall::Scheduler(_) => true,
|
||||
RuntimeCall::ConvictionVoting(_) => true,
|
||||
RuntimeCall::Referenda(_) => true,
|
||||
RuntimeCall::TechnicalCommittee(_) => true,
|
||||
RuntimeCall::TreasuryCouncil(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type MainnetRuntimeCallFilter =
|
||||
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
|
||||
|
||||
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
|
||||
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
|
||||
/// but overridden as needed.
|
||||
|
|
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
|
|||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type SystemWeightInfo = mainnet_weights::frame_system::WeightInfo<Runtime>;
|
||||
type MultiBlockMigrator = MultiBlockMigrations;
|
||||
/// Use the NormalCallFilter to restrict certain runtime calls
|
||||
type BaseCallFilter = NormalCallFilter;
|
||||
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
|
||||
type BaseCallFilter = MainnetRuntimeCallFilter;
|
||||
}
|
||||
|
||||
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
|
||||
|
|
@ -1480,6 +1514,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
|
|||
type WeightInfo = mainnet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
|
||||
//║ SAFE MODE & TX PAUSE PALLETS ║
|
||||
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
impl pallet_safe_mode::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type RuntimeHoldReason = RuntimeHoldReason;
|
||||
type WhitelistedCalls = SafeModeWhitelistedCalls;
|
||||
type EnterDuration = SafeModeDuration;
|
||||
type ExtendDuration = SafeModeDuration;
|
||||
type EnterDepositAmount = SafeModeEnterDeposit;
|
||||
type ExtendDepositAmount = SafeModeExtendDeposit;
|
||||
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExitOrigin = EnsureRoot<AccountId>;
|
||||
type ForceDepositOrigin = EnsureRoot<AccountId>;
|
||||
type ReleaseDelay = ReleaseDelayNone;
|
||||
type Notify = ();
|
||||
type WeightInfo = mainnet_weights::pallet_safe_mode::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
impl pallet_tx_pause::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type PauseOrigin = EnsureRoot<AccountId>;
|
||||
type UnpauseOrigin = EnsureRoot<AccountId>;
|
||||
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
|
||||
type MaxNameLen = ConstU32<256>;
|
||||
type WeightInfo = mainnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -378,6 +378,12 @@ mod runtime {
|
|||
|
||||
#[runtime::pallet_index(39)]
|
||||
pub type MultiBlockMigrations = pallet_migrations;
|
||||
|
||||
#[runtime::pallet_index(103)]
|
||||
pub type SafeMode = pallet_safe_mode;
|
||||
|
||||
#[runtime::pallet_index(104)]
|
||||
pub type TxPause = pallet_tx_pause;
|
||||
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
|
||||
|
||||
// ╔═════════════════════════ Governance Pallets ════════════════════╗
|
||||
|
|
|
|||
|
|
@ -41,11 +41,13 @@ pub mod pallet_multisig;
|
|||
pub mod pallet_parameters;
|
||||
pub mod pallet_preimage;
|
||||
pub mod pallet_proxy;
|
||||
pub mod pallet_safe_mode;
|
||||
pub mod pallet_scheduler;
|
||||
pub mod pallet_sudo;
|
||||
pub mod pallet_timestamp;
|
||||
pub mod pallet_transaction_payment;
|
||||
pub mod pallet_treasury;
|
||||
pub mod pallet_tx_pause;
|
||||
pub mod pallet_utility;
|
||||
|
||||
// Governance pallets
|
||||
|
|
|
|||
7
operator/runtime/mainnet/src/weights/pallet_safe_mode.rs
Normal file
7
operator/runtime/mainnet/src/weights/pallet_safe_mode.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
|
||||
// weights in this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;
|
||||
7
operator/runtime/mainnet/src/weights/pallet_tx_pause.rs
Normal file
7
operator/runtime/mainnet/src/weights/pallet_tx_pause.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
|
||||
// this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;
|
||||
|
|
@ -5,6 +5,7 @@ pub mod governance;
|
|||
mod migrations;
|
||||
mod native_token_transfer;
|
||||
mod proxy;
|
||||
mod safe_mode_tx_pause;
|
||||
|
||||
use common::*;
|
||||
use datahaven_mainnet_runtime::{
|
||||
|
|
|
|||
431
operator/runtime/mainnet/tests/safe_mode_tx_pause.rs
Normal file
431
operator/runtime/mainnet/tests/safe_mode_tx_pause.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
#[path = "common.rs"]
|
||||
mod common;
|
||||
|
||||
use common::{account_id, ExtBuilder, ALICE, BOB};
|
||||
use datahaven_mainnet_runtime::{
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, System, UncheckedExtrinsic,
|
||||
};
|
||||
use frame_support::{assert_noop, assert_ok, BoundedVec};
|
||||
use pallet_safe_mode::EnteredUntil;
|
||||
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
|
||||
use sp_runtime::{
|
||||
traits::Dispatchable,
|
||||
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
|
||||
};
|
||||
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
|
||||
|
||||
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
|
||||
use frame_support::traits::GetCallMetadata;
|
||||
let metadata = call.get_call_metadata();
|
||||
(
|
||||
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
|
||||
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn transfer_call(amount: u128) -> RuntimeCall {
|
||||
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
|
||||
dest: account_id(BOB),
|
||||
value: amount,
|
||||
})
|
||||
}
|
||||
|
||||
mod safe_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn force_enter_requires_root() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
|
||||
Runtime,
|
||||
>::Entered {
|
||||
until: EnteredUntil::<Runtime>::get().unwrap(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_safe_mode_blocks_non_whitelisted_calls() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let xt = transfer_call(1u128);
|
||||
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
unchecked_xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_calls_dispatch_in_safe_mode() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod tx_pause {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pause_requires_root() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paused_call_is_blocked() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
call.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// After unpause, the call should be dispatchable
|
||||
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_call_cannot_be_paused() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()),
|
||||
TxPauseError::<Runtime>::Unpausable
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod combined_behaviour {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dual_restrictions_require_both_to_clear() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let still_blocked = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
still_blocked,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
// After exiting safe mode and unpausing, call should be dispatchable
|
||||
assert_ok!(call
|
||||
.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default()
|
||||
),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_plane_calls_work_under_restrictions() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn governance_whitelisted_calls_work_during_safe_mode() {
|
||||
use sp_core::H256;
|
||||
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Enter safe mode
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Verify safe mode is active
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
|
||||
// Verify normal calls are blocked during safe mode
|
||||
let normal_call = transfer_call(100);
|
||||
assert_noop!(
|
||||
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
// Test Whitelist pallet - critical for emergency runtime upgrades
|
||||
let call_hash = H256::random();
|
||||
assert_ok!(
|
||||
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Test Preimage pallet - required for storing governance call data
|
||||
let dummy_preimage = vec![1u8; 32];
|
||||
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
|
||||
bytes: dummy_preimage,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match preimage_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Preimage calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Scheduler pallet - needed for time-delayed governance actions
|
||||
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
|
||||
when: 100,
|
||||
index: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match scheduler_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Scheduler calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Referenda pallet - core OpenGov proposal system
|
||||
let referenda_result =
|
||||
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match referenda_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Referenda calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test ConvictionVoting - allows token holders to vote during emergencies
|
||||
let voting_result = RuntimeCall::ConvictionVoting(
|
||||
pallet_conviction_voting::Call::remove_other_vote {
|
||||
target: account_id(BOB),
|
||||
class: 0,
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match voting_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"ConvictionVoting calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test TechnicalCommittee - expert oversight for emergency actions
|
||||
let tech_committee_result =
|
||||
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
|
||||
new_members: vec![account_id(ALICE)],
|
||||
prime: None,
|
||||
old_count: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match tech_committee_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"TechnicalCommittee calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true }
|
|||
pallet-datahaven-native-transfer = { workspace = true }
|
||||
pallet-parameters = { workspace = true }
|
||||
pallet-preimage = { workspace = true }
|
||||
pallet-safe-mode = { workspace = true }
|
||||
pallet-tx-pause = { workspace = true }
|
||||
pallet-proxy = { workspace = true }
|
||||
pallet-referenda = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
|
|
@ -212,6 +214,8 @@ std = [
|
|||
"pallet-offences/std",
|
||||
"pallet-parameters/std",
|
||||
"pallet-preimage/std",
|
||||
"pallet-safe-mode/std",
|
||||
"pallet-tx-pause/std",
|
||||
"pallet-referenda/std",
|
||||
"pallet-proxy/std",
|
||||
"pallet-scheduler/std",
|
||||
|
|
@ -311,6 +315,8 @@ runtime-benchmarks = [
|
|||
"pallet-offences/runtime-benchmarks",
|
||||
"pallet-parameters/runtime-benchmarks",
|
||||
"pallet-preimage/runtime-benchmarks",
|
||||
"pallet-safe-mode/runtime-benchmarks",
|
||||
"pallet-tx-pause/runtime-benchmarks",
|
||||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
|
|
@ -359,6 +365,8 @@ try-runtime = [
|
|||
"pallet-offences/try-runtime",
|
||||
"pallet-parameters/try-runtime",
|
||||
"pallet-preimage/try-runtime",
|
||||
"pallet-safe-mode/try-runtime",
|
||||
"pallet-tx-pause/try-runtime",
|
||||
"pallet-referenda/try-runtime",
|
||||
"pallet-proxy/try-runtime",
|
||||
"pallet-scheduler/try-runtime",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_transaction_payment, TransactionPayment]
|
||||
[pallet_parameters, Parameters]
|
||||
[pallet_message_queue, MessageQueue]
|
||||
[pallet_safe_mode, SafeMode]
|
||||
[pallet_tx_pause, TxPause]
|
||||
|
||||
// Governance pallets
|
||||
[pallet_collective_technical_committee, TechnicalCommittee]
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ use super::{
|
|||
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
|
||||
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
|
||||
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
|
||||
};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
|
|
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
|
|||
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
|
||||
MigrationIdentifierMaxLen, MigrationStatusHandler,
|
||||
},
|
||||
safe_mode::{
|
||||
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
|
||||
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
|
||||
},
|
||||
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
|
||||
};
|
||||
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
|
||||
|
|
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calls that can bypass the safe-mode pallet.
|
||||
/// These calls are essential for emergency governance and system maintenance.
|
||||
pub struct SafeModeWhitelistedCalls;
|
||||
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
|
||||
fn contains(call: &RuntimeCall) -> bool {
|
||||
match call {
|
||||
// Core system calls
|
||||
RuntimeCall::System(_) => true,
|
||||
// Safe mode management
|
||||
RuntimeCall::SafeMode(_) => true,
|
||||
// Transaction pause management
|
||||
RuntimeCall::TxPause(_) => true,
|
||||
// Emergency admin access (testnet/dev only)
|
||||
RuntimeCall::Sudo(_) => true,
|
||||
// Governance infrastructure - critical for emergency responses
|
||||
RuntimeCall::Whitelist(_) => true,
|
||||
RuntimeCall::Preimage(_) => true,
|
||||
RuntimeCall::Scheduler(_) => true,
|
||||
RuntimeCall::ConvictionVoting(_) => true,
|
||||
RuntimeCall::Referenda(_) => true,
|
||||
RuntimeCall::TechnicalCommittee(_) => true,
|
||||
RuntimeCall::TreasuryCouncil(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type StagenetRuntimeCallFilter =
|
||||
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
|
||||
|
||||
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
|
||||
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
|
||||
/// but overridden as needed.
|
||||
|
|
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
|
|||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type SystemWeightInfo = stagenet_weights::frame_system::WeightInfo<Runtime>;
|
||||
type MultiBlockMigrator = MultiBlockMigrations;
|
||||
/// Use the NormalCallFilter to restrict certain runtime calls
|
||||
type BaseCallFilter = NormalCallFilter;
|
||||
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
|
||||
type BaseCallFilter = StagenetRuntimeCallFilter;
|
||||
}
|
||||
|
||||
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
|
||||
|
|
@ -1481,6 +1515,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
|
|||
type WeightInfo = stagenet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
|
||||
//║ SAFE MODE & TX PAUSE PALLETS ║
|
||||
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
impl pallet_safe_mode::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type RuntimeHoldReason = RuntimeHoldReason;
|
||||
type WhitelistedCalls = SafeModeWhitelistedCalls;
|
||||
type EnterDuration = SafeModeDuration;
|
||||
type ExtendDuration = SafeModeDuration;
|
||||
type EnterDepositAmount = SafeModeEnterDeposit;
|
||||
type ExtendDepositAmount = SafeModeExtendDeposit;
|
||||
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExitOrigin = EnsureRoot<AccountId>;
|
||||
type ForceDepositOrigin = EnsureRoot<AccountId>;
|
||||
type ReleaseDelay = ReleaseDelayNone;
|
||||
type Notify = ();
|
||||
type WeightInfo = stagenet_weights::pallet_safe_mode::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
impl pallet_tx_pause::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type PauseOrigin = EnsureRoot<AccountId>;
|
||||
type UnpauseOrigin = EnsureRoot<AccountId>;
|
||||
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
|
||||
type MaxNameLen = ConstU32<256>;
|
||||
type WeightInfo = stagenet_weights::pallet_tx_pause::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -380,6 +380,12 @@ mod runtime {
|
|||
|
||||
#[runtime::pallet_index(39)]
|
||||
pub type MultiBlockMigrations = pallet_migrations;
|
||||
|
||||
#[runtime::pallet_index(103)]
|
||||
pub type SafeMode = pallet_safe_mode;
|
||||
|
||||
#[runtime::pallet_index(104)]
|
||||
pub type TxPause = pallet_tx_pause;
|
||||
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
|
||||
|
||||
// ╔═════════════════════════ Governance Pallets ════════════════════╗
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ pub mod pallet_multisig;
|
|||
pub mod pallet_parameters;
|
||||
pub mod pallet_preimage;
|
||||
pub mod pallet_proxy;
|
||||
pub mod pallet_safe_mode;
|
||||
pub mod pallet_scheduler;
|
||||
pub mod pallet_sudo;
|
||||
pub mod pallet_timestamp;
|
||||
pub mod pallet_transaction_payment;
|
||||
pub mod pallet_treasury;
|
||||
pub mod pallet_tx_pause;
|
||||
pub mod pallet_utility;
|
||||
|
||||
// Governance pallets
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
|
||||
// weights in this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;
|
||||
7
operator/runtime/stagenet/src/weights/pallet_tx_pause.rs
Normal file
7
operator/runtime/stagenet/src/weights/pallet_tx_pause.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
|
||||
// this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;
|
||||
|
|
@ -4,6 +4,7 @@ pub mod common;
|
|||
pub mod governance;
|
||||
mod native_token_transfer;
|
||||
mod proxy;
|
||||
mod safe_mode_tx_pause;
|
||||
|
||||
use common::*;
|
||||
use datahaven_stagenet_runtime::{
|
||||
|
|
|
|||
432
operator/runtime/stagenet/tests/safe_mode_tx_pause.rs
Normal file
432
operator/runtime/stagenet/tests/safe_mode_tx_pause.rs
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
#[path = "common.rs"]
|
||||
mod common;
|
||||
|
||||
use common::{account_id, ExtBuilder, ALICE, BOB};
|
||||
use datahaven_stagenet_runtime::{
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause,
|
||||
UncheckedExtrinsic,
|
||||
};
|
||||
use frame_support::{assert_noop, assert_ok, BoundedVec};
|
||||
use pallet_safe_mode::EnteredUntil;
|
||||
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
|
||||
use sp_runtime::{
|
||||
traits::Dispatchable,
|
||||
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
|
||||
};
|
||||
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
|
||||
|
||||
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
|
||||
use frame_support::traits::GetCallMetadata;
|
||||
let metadata = call.get_call_metadata();
|
||||
(
|
||||
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
|
||||
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn transfer_call(amount: u128) -> RuntimeCall {
|
||||
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
|
||||
dest: account_id(BOB),
|
||||
value: amount,
|
||||
})
|
||||
}
|
||||
|
||||
mod safe_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn force_enter_requires_root() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
|
||||
Runtime,
|
||||
>::Entered {
|
||||
until: EnteredUntil::<Runtime>::get().unwrap(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_safe_mode_blocks_non_whitelisted_calls() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let xt = transfer_call(1u128);
|
||||
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
unchecked_xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_calls_dispatch_in_safe_mode() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod tx_pause {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pause_requires_root() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paused_call_is_blocked() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
call.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// After unpause, the call should be dispatchable
|
||||
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_call_cannot_be_paused() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()),
|
||||
TxPauseError::<Runtime>::Unpausable
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod combined_behaviour {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dual_restrictions_require_both_to_clear() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let still_blocked = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
still_blocked,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
// After exiting safe mode and unpausing, call should be dispatchable
|
||||
assert_ok!(call
|
||||
.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default()
|
||||
),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_plane_calls_work_under_restrictions() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn governance_whitelisted_calls_work_during_safe_mode() {
|
||||
use sp_core::H256;
|
||||
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Enter safe mode
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Verify safe mode is active
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
|
||||
// Verify normal calls are blocked during safe mode
|
||||
let normal_call = transfer_call(100);
|
||||
assert_noop!(
|
||||
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
// Test Whitelist pallet - critical for emergency runtime upgrades
|
||||
let call_hash = H256::random();
|
||||
assert_ok!(
|
||||
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Test Preimage pallet - required for storing governance call data
|
||||
let dummy_preimage = vec![1u8; 32];
|
||||
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
|
||||
bytes: dummy_preimage,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match preimage_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Preimage calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Scheduler pallet - needed for time-delayed governance actions
|
||||
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
|
||||
when: 100,
|
||||
index: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match scheduler_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Scheduler calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Referenda pallet - core OpenGov proposal system
|
||||
let referenda_result =
|
||||
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match referenda_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Referenda calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test ConvictionVoting - allows token holders to vote during emergencies
|
||||
let voting_result = RuntimeCall::ConvictionVoting(
|
||||
pallet_conviction_voting::Call::remove_other_vote {
|
||||
target: account_id(BOB),
|
||||
class: 0,
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match voting_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"ConvictionVoting calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test TechnicalCommittee - expert oversight for emergency actions
|
||||
let tech_committee_result =
|
||||
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
|
||||
new_members: vec![account_id(ALICE)],
|
||||
prime: None,
|
||||
old_count: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match tech_committee_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"TechnicalCommittee calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,8 @@ pallet-offences = { workspace = true }
|
|||
pallet-outbound-commitment-store = { workspace = true }
|
||||
pallet-parameters = { workspace = true }
|
||||
pallet-preimage = { workspace = true }
|
||||
pallet-safe-mode = { workspace = true }
|
||||
pallet-tx-pause = { workspace = true }
|
||||
pallet-proxy = { workspace = true }
|
||||
pallet-referenda = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
|
|
@ -209,6 +211,8 @@ std = [
|
|||
"pallet-offences/std",
|
||||
"pallet-parameters/std",
|
||||
"pallet-preimage/std",
|
||||
"pallet-safe-mode/std",
|
||||
"pallet-tx-pause/std",
|
||||
"pallet-referenda/std",
|
||||
"pallet-proxy/std",
|
||||
"pallet-scheduler/std",
|
||||
|
|
@ -310,6 +314,8 @@ runtime-benchmarks = [
|
|||
"pallet-offences/runtime-benchmarks",
|
||||
"pallet-parameters/runtime-benchmarks",
|
||||
"pallet-preimage/runtime-benchmarks",
|
||||
"pallet-safe-mode/runtime-benchmarks",
|
||||
"pallet-tx-pause/runtime-benchmarks",
|
||||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
|
|
@ -358,6 +364,8 @@ try-runtime = [
|
|||
"pallet-offences/try-runtime",
|
||||
"pallet-parameters/try-runtime",
|
||||
"pallet-preimage/try-runtime",
|
||||
"pallet-safe-mode/try-runtime",
|
||||
"pallet-tx-pause/try-runtime",
|
||||
"pallet-referenda/try-runtime",
|
||||
"pallet-proxy/try-runtime",
|
||||
"pallet-scheduler/try-runtime",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_transaction_payment, TransactionPayment]
|
||||
[pallet_parameters, Parameters]
|
||||
[pallet_message_queue, MessageQueue]
|
||||
[pallet_safe_mode, SafeMode]
|
||||
[pallet_tx_pause, TxPause]
|
||||
|
||||
// EVM pallets
|
||||
[pallet_evm, Evm]
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ use super::{
|
|||
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
|
||||
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
|
||||
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
|
||||
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
|
||||
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
|
||||
};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
|
|
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
|
|||
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
|
||||
MigrationIdentifierMaxLen, MigrationStatusHandler,
|
||||
},
|
||||
safe_mode::{
|
||||
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
|
||||
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
|
||||
},
|
||||
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
|
||||
};
|
||||
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
|
||||
|
|
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calls that can bypass the safe-mode pallet.
|
||||
/// These calls are essential for emergency governance and system maintenance.
|
||||
pub struct SafeModeWhitelistedCalls;
|
||||
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
|
||||
fn contains(call: &RuntimeCall) -> bool {
|
||||
match call {
|
||||
// Core system calls
|
||||
RuntimeCall::System(_) => true,
|
||||
// Safe mode management
|
||||
RuntimeCall::SafeMode(_) => true,
|
||||
// Transaction pause management
|
||||
RuntimeCall::TxPause(_) => true,
|
||||
// Emergency admin access (testnet/dev only)
|
||||
RuntimeCall::Sudo(_) => true,
|
||||
// Governance infrastructure - critical for emergency responses
|
||||
RuntimeCall::Whitelist(_) => true,
|
||||
RuntimeCall::Preimage(_) => true,
|
||||
RuntimeCall::Scheduler(_) => true,
|
||||
RuntimeCall::ConvictionVoting(_) => true,
|
||||
RuntimeCall::Referenda(_) => true,
|
||||
RuntimeCall::TechnicalCommittee(_) => true,
|
||||
RuntimeCall::TreasuryCouncil(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type TestnetRuntimeCallFilter =
|
||||
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
|
||||
|
||||
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
|
||||
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
|
||||
/// but overridden as needed.
|
||||
|
|
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
|
|||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type SystemWeightInfo = testnet_weights::frame_system::WeightInfo<Runtime>;
|
||||
type MultiBlockMigrator = MultiBlockMigrations;
|
||||
/// Use the NormalCallFilter to restrict certain runtime calls
|
||||
type BaseCallFilter = NormalCallFilter;
|
||||
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
|
||||
type BaseCallFilter = TestnetRuntimeCallFilter;
|
||||
}
|
||||
|
||||
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
|
||||
|
|
@ -1479,6 +1513,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
|
|||
type WeightInfo = testnet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
|
||||
//║ SAFE MODE & TX PAUSE PALLETS ║
|
||||
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
impl pallet_safe_mode::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type RuntimeHoldReason = RuntimeHoldReason;
|
||||
type WhitelistedCalls = SafeModeWhitelistedCalls;
|
||||
type EnterDuration = SafeModeDuration;
|
||||
type ExtendDuration = SafeModeDuration;
|
||||
type EnterDepositAmount = SafeModeEnterDeposit;
|
||||
type ExtendDepositAmount = SafeModeExtendDeposit;
|
||||
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
|
||||
type ForceExitOrigin = EnsureRoot<AccountId>;
|
||||
type ForceDepositOrigin = EnsureRoot<AccountId>;
|
||||
type ReleaseDelay = ReleaseDelayNone;
|
||||
type Notify = ();
|
||||
type WeightInfo = testnet_weights::pallet_safe_mode::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
impl pallet_tx_pause::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type PauseOrigin = EnsureRoot<AccountId>;
|
||||
type UnpauseOrigin = EnsureRoot<AccountId>;
|
||||
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
|
||||
type MaxNameLen = ConstU32<256>;
|
||||
type WeightInfo = testnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -377,6 +377,12 @@ mod runtime {
|
|||
|
||||
#[runtime::pallet_index(39)]
|
||||
pub type MultiBlockMigrations = pallet_migrations;
|
||||
|
||||
#[runtime::pallet_index(103)]
|
||||
pub type SafeMode = pallet_safe_mode;
|
||||
|
||||
#[runtime::pallet_index(104)]
|
||||
pub type TxPause = pallet_tx_pause;
|
||||
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
|
||||
|
||||
// ╔═════════════════════════ Governance Pallets ════════════════════╗
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ pub mod pallet_multisig;
|
|||
pub mod pallet_parameters;
|
||||
pub mod pallet_preimage;
|
||||
pub mod pallet_proxy;
|
||||
pub mod pallet_safe_mode;
|
||||
pub mod pallet_scheduler;
|
||||
pub mod pallet_sudo;
|
||||
pub mod pallet_timestamp;
|
||||
pub mod pallet_transaction_payment;
|
||||
pub mod pallet_treasury;
|
||||
pub mod pallet_tx_pause;
|
||||
pub mod pallet_utility;
|
||||
|
||||
// Governance pallets
|
||||
|
|
|
|||
7
operator/runtime/testnet/src/weights/pallet_safe_mode.rs
Normal file
7
operator/runtime/testnet/src/weights/pallet_safe_mode.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
|
||||
// weights in this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;
|
||||
7
operator/runtime/testnet/src/weights/pallet_tx_pause.rs
Normal file
7
operator/runtime/testnet/src/weights/pallet_tx_pause.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
|
||||
//
|
||||
// We reuse the upstream Substrate weight assumptions which are conservative enough for
|
||||
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
|
||||
// this module via the runtime benchmarking CLI.
|
||||
|
||||
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;
|
||||
|
|
@ -4,6 +4,7 @@ pub mod common;
|
|||
pub mod governance;
|
||||
mod native_token_transfer;
|
||||
mod proxy;
|
||||
mod safe_mode_tx_pause;
|
||||
|
||||
use common::*;
|
||||
use datahaven_testnet_runtime::{
|
||||
|
|
|
|||
535
operator/runtime/testnet/tests/safe_mode_tx_pause.rs
Normal file
535
operator/runtime/testnet/tests/safe_mode_tx_pause.rs
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
#[path = "common.rs"]
|
||||
mod common;
|
||||
|
||||
use common::{account_id, ExtBuilder, ALICE, BOB};
|
||||
use datahaven_testnet_runtime::{
|
||||
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause,
|
||||
UncheckedExtrinsic,
|
||||
};
|
||||
use frame_support::{assert_noop, assert_ok, BoundedVec};
|
||||
use pallet_safe_mode::EnteredUntil;
|
||||
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
|
||||
use sp_runtime::{
|
||||
traits::Dispatchable,
|
||||
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
|
||||
};
|
||||
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
|
||||
|
||||
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
|
||||
use frame_support::traits::GetCallMetadata;
|
||||
let metadata = call.get_call_metadata();
|
||||
(
|
||||
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
|
||||
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn transfer_call(amount: u128) -> RuntimeCall {
|
||||
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
|
||||
dest: account_id(BOB),
|
||||
value: amount,
|
||||
})
|
||||
}
|
||||
|
||||
mod safe_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn force_enter_requires_root() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
|
||||
Runtime,
|
||||
>::Entered {
|
||||
until: EnteredUntil::<Runtime>::get().unwrap(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_safe_mode_blocks_non_whitelisted_calls() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let xt = transfer_call(1u128);
|
||||
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
unchecked_xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_calls_dispatch_in_safe_mode() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert!(EnteredUntil::<Runtime>::get().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_restores_normal_flow() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Enter safe mode
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(100);
|
||||
|
||||
// Verify call is blocked in safe mode
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default()
|
||||
),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
// Exit safe mode
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
// Verify call now works
|
||||
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sudo_bypass() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Enter safe mode
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let transfer = transfer_call(100);
|
||||
|
||||
// Wrap in sudo call
|
||||
let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo {
|
||||
call: Box::new(transfer),
|
||||
});
|
||||
|
||||
// Sudo should bypass safe mode filter
|
||||
assert_ok!(sudo_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod tx_pause {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pause_requires_root() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paused_call_is_blocked() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
call.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
assert_ok!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// After unpause, the call should be dispatchable
|
||||
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_call_cannot_be_paused() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_noop!(
|
||||
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()),
|
||||
TxPauseError::<Runtime>::Unpausable
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod combined_behaviour {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dual_restrictions_require_both_to_clear() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let validity = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
validity,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let still_blocked = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
still_blocked,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
// After exiting safe mode and unpausing, call should be dispatchable
|
||||
assert_ok!(call
|
||||
.clone()
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
let xt = UncheckedExtrinsic::new_bare(call.into());
|
||||
assert_eq!(
|
||||
Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default()
|
||||
),
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_plane_calls_work_under_restrictions() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(1u128);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name.clone(),
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
|
||||
ident: call_name.clone()
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn governance_whitelisted_calls_work_during_safe_mode() {
|
||||
use sp_core::H256;
|
||||
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Enter safe mode
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Verify safe mode is active
|
||||
assert!(EnteredUntil::<Runtime>::get().is_some());
|
||||
|
||||
// Verify normal calls are blocked during safe mode
|
||||
let normal_call = transfer_call(100);
|
||||
assert_noop!(
|
||||
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
|
||||
// Test Whitelist pallet - critical for emergency runtime upgrades
|
||||
let call_hash = H256::random();
|
||||
assert_ok!(
|
||||
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
// Test Preimage pallet - required for storing governance call data
|
||||
let dummy_preimage = vec![1u8; 32];
|
||||
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
|
||||
bytes: dummy_preimage,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match preimage_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Preimage calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Scheduler pallet - needed for time-delayed governance actions
|
||||
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
|
||||
when: 100,
|
||||
index: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match scheduler_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Scheduler calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Referenda pallet - core OpenGov proposal system
|
||||
let referenda_result =
|
||||
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match referenda_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"Referenda calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test ConvictionVoting - allows token holders to vote during emergencies
|
||||
let voting_result = RuntimeCall::ConvictionVoting(
|
||||
pallet_conviction_voting::Call::remove_other_vote {
|
||||
target: account_id(BOB),
|
||||
class: 0,
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
|
||||
|
||||
match voting_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"ConvictionVoting calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test TechnicalCommittee - expert oversight for emergency actions
|
||||
let tech_committee_result =
|
||||
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
|
||||
new_members: vec![account_id(ALICE)],
|
||||
prime: None,
|
||||
old_count: 0,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root());
|
||||
|
||||
match tech_committee_result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let call_filtered_error: sp_runtime::DispatchError =
|
||||
frame_system::Error::<Runtime>::CallFiltered.into();
|
||||
assert_ne!(
|
||||
format!("{:?}", e.error),
|
||||
format!("{:?}", call_filtered_error),
|
||||
"TechnicalCommittee calls should not be filtered by safe mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_surface_consistency() {
|
||||
ExtBuilder::default()
|
||||
.with_sudo(account_id(ALICE))
|
||||
.with_balances(vec![(account_id(ALICE), 1_000_000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// Activate both restrictions
|
||||
assert_ok!(
|
||||
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
|
||||
.dispatch(RuntimeOrigin::root())
|
||||
);
|
||||
|
||||
let call = transfer_call(100);
|
||||
let call_name = call_name(&call);
|
||||
|
||||
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
|
||||
full_name: call_name,
|
||||
})
|
||||
.dispatch(RuntimeOrigin::root()));
|
||||
|
||||
// Validate the blocked call - should return consistent error
|
||||
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
|
||||
let validation_result = Runtime::validate_transaction(
|
||||
TransactionSource::External,
|
||||
xt,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
// Should return InvalidTransaction::Call
|
||||
assert_eq!(
|
||||
validation_result,
|
||||
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
|
||||
);
|
||||
|
||||
// Dispatch should also fail with consistent error
|
||||
assert_noop!(
|
||||
call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
|
||||
frame_system::Error::<Runtime>::CallFiltered
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.1.0-autogenerated.10311212470204876148",
|
||||
"version": "0.1.0-autogenerated.11494808361211823293",
|
||||
"name": "@polkadot-api/descriptors",
|
||||
"files": [
|
||||
"dist"
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in a new issue