diff --git a/Cargo.lock b/Cargo.lock index b49277401c..5841f6a89f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18538,6 +18538,7 @@ dependencies = [ "approx", "environmental", "frame-support", + "impl-trait-for-tuples", "num-traits", "parity-scale-codec", "polkadot-runtime-common", diff --git a/common/Cargo.toml b/common/Cargo.toml index 9fa9bd1856..b7facc8279 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -24,6 +24,7 @@ sp-rpc = { workspace = true, optional = true } substrate-fixed.workspace = true subtensor-macros.workspace = true runtime-common.workspace = true +impl-trait-for-tuples.workspace = true approx = { workspace = true, optional = true } [lints] diff --git a/common/src/lib.rs b/common/src/lib.rs index ad29f123b2..135830e1cc 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,10 +16,12 @@ use subtensor_macros::freeze_struct; pub use currency::*; pub use evm_context::*; +pub use proxy::*; pub use transaction_error::*; mod currency; mod evm_context; +mod proxy; mod transaction_error; /// Balance of an account. @@ -132,181 +134,6 @@ impl TypeInfo for NetUid { } } -#[derive( - Copy, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - Encode, - Decode, - DecodeWithMemTracking, - Debug, - MaxEncodedLen, - TypeInfo, -)] -pub enum ProxyType { - Any, - Owner, // Subnet owner Calls - NonCritical, - NonTransfer, - Senate, - NonFungible, // Nothing involving moving TAO - Triumvirate, - Governance, // Both above governance - Staking, - Registration, - Transfer, - SmallTransfer, - RootWeights, // deprecated - ChildKeys, - SudoUncheckedSetCode, - SwapHotkey, - SubnetLeaseBeneficiary, // Used to operate the leased subnet - RootClaim, -} - -impl TryFrom for ProxyType { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Any), - 1 => Ok(Self::Owner), - 2 => Ok(Self::NonCritical), - 3 => Ok(Self::NonTransfer), - 4 => Ok(Self::Senate), - 5 => Ok(Self::NonFungible), - 6 => Ok(Self::Triumvirate), - 7 => Ok(Self::Governance), - 8 => Ok(Self::Staking), - 9 => Ok(Self::Registration), - 10 => Ok(Self::Transfer), - 11 => Ok(Self::SmallTransfer), - 12 => Ok(Self::RootWeights), - 13 => Ok(Self::ChildKeys), - 14 => Ok(Self::SudoUncheckedSetCode), - 15 => Ok(Self::SwapHotkey), - 16 => Ok(Self::SubnetLeaseBeneficiary), - 17 => Ok(Self::RootClaim), - _ => Err(()), - } - } -} - -impl From for u8 { - fn from(proxy_type: ProxyType) -> Self { - match proxy_type { - ProxyType::Any => 0, - ProxyType::Owner => 1, - ProxyType::NonCritical => 2, - ProxyType::NonTransfer => 3, - ProxyType::Senate => 4, - ProxyType::NonFungible => 5, - ProxyType::Triumvirate => 6, - ProxyType::Governance => 7, - ProxyType::Staking => 8, - ProxyType::Registration => 9, - ProxyType::Transfer => 10, - ProxyType::SmallTransfer => 11, - ProxyType::RootWeights => 12, - ProxyType::ChildKeys => 13, - ProxyType::SudoUncheckedSetCode => 14, - ProxyType::SwapHotkey => 15, - ProxyType::SubnetLeaseBeneficiary => 16, - ProxyType::RootClaim => 17, - } - } -} - -impl ProxyType { - pub fn is_deprecated(&self) -> bool { - matches!( - self, - Self::Triumvirate | Self::Senate | Self::Governance | Self::RootWeights - ) - } -} - -impl Default for ProxyType { - // allow all Calls; required to be most permissive - fn default() -> Self { - Self::Any - } -} - -/// Conditions that must be met beyond matching the call itself. -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] -pub enum CallCondition { - /// A numeric parameter must be less than this limit - ParamLessThan { param_name: Vec, limit: u128 }, - /// The nested call inside must match this pallet/call - NestedCallMustBe { - pallet_name: Vec, - call_name: Vec, - }, -} - -/// Describes which call(s) a proxy filter rule applies to. -/// -/// When `call_name` and `call_index` are `None`, the rule applies to ALL calls in the pallet. -/// When they are `Some`, the rule applies to that specific call only. -#[freeze_struct("57f984617f6084cc")] -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] -pub struct CallInfo { - /// Pallet name (always present) - pub pallet_name: Vec, - /// Pallet index in runtime (always present) - pub pallet_index: u8, - /// Call name within pallet. None means ALL calls in this pallet. - pub call_name: Option>, - /// Call index within pallet. None means ALL calls in this pallet. - pub call_index: Option, - /// Additional condition that must be met (value limits, nested call requirements) - pub condition: Option, -} - -/// Describes how a ProxyType filters incoming calls. -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] -pub enum FilterMode { - /// All calls are permitted regardless of the calls list (e.g. ProxyType::Any) - AllowAll, - /// No calls are permitted (e.g. deprecated proxy types) - DenyAll, - /// Only calls listed in the `calls` field are permitted - Allow, - /// All calls are permitted EXCEPT those listed in the `calls` field - Deny, -} - -/// Complete filter description for a ProxyType, returned by the Runtime API. -/// -/// Interpretation: -/// - `filter_mode: AllowAll` — everything permitted, `calls` is empty -/// - `filter_mode: DenyAll` — nothing permitted, `calls` is empty -/// - `filter_mode: Allow` — only `calls` are permitted (minus `exceptions`) -/// - `filter_mode: Deny` — everything EXCEPT `calls` is permitted -/// - `call_name: None` in a CallInfo — rule applies to ALL calls in the pallet -#[freeze_struct("4453d44869f8a188")] -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] -pub struct ProxyFilterInfo { - pub proxy_type: u8, - pub name: Vec, - pub deprecated: bool, - pub filter_mode: FilterMode, - pub calls: Vec, - pub exceptions: Vec, -} - -#[freeze_struct("b0cce66ed9b2451b")] -#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] -pub struct ProxyTypeInfo { - pub name: Vec, - pub index: u8, - pub deprecated: bool, -} - pub trait SubnetInfo { fn exists(netuid: NetUid) -> bool; fn mechanism(netuid: NetUid) -> u16; diff --git a/common/src/proxy.rs b/common/src/proxy.rs new file mode 100644 index 0000000000..f40b3f2076 --- /dev/null +++ b/common/src/proxy.rs @@ -0,0 +1,219 @@ +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::traits::{Contains, GetCallIndex, GetCallName, PalletInfoAccess}; +use scale_info::TypeInfo; +use sp_runtime::Vec; +use subtensor_macros::freeze_struct; + +/// Shared proxy filter model exposed by the runtime API. +/// +/// Runtime filtering remains the source of truth. This metadata is the client-facing +/// allowlist view of the same rules. +/// Stable proxy type identifiers used on-chain and by RPC clients. +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + DecodeWithMemTracking, + Debug, + MaxEncodedLen, + TypeInfo, +)] +pub enum ProxyType { + Any, + Owner, + NonCritical, + NonTransfer, + Senate, + NonFungible, // Nothing involving moving TAO + Triumvirate, + Governance, + Staking, + Registration, + Transfer, + SmallTransfer, + RootWeights, // Deprecated + ChildKeys, + SudoUncheckedSetCode, + SwapHotkey, + SubnetLeaseBeneficiary, + RootClaim, +} + +impl TryFrom for ProxyType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Any), + 1 => Ok(Self::Owner), + 2 => Ok(Self::NonCritical), + 3 => Ok(Self::NonTransfer), + 4 => Ok(Self::Senate), + 5 => Ok(Self::NonFungible), + 6 => Ok(Self::Triumvirate), + 7 => Ok(Self::Governance), + 8 => Ok(Self::Staking), + 9 => Ok(Self::Registration), + 10 => Ok(Self::Transfer), + 11 => Ok(Self::SmallTransfer), + 12 => Ok(Self::RootWeights), + 13 => Ok(Self::ChildKeys), + 14 => Ok(Self::SudoUncheckedSetCode), + 15 => Ok(Self::SwapHotkey), + 16 => Ok(Self::SubnetLeaseBeneficiary), + 17 => Ok(Self::RootClaim), + _ => Err(()), + } + } +} + +impl From for u8 { + fn from(proxy_type: ProxyType) -> Self { + match proxy_type { + ProxyType::Any => 0, + ProxyType::Owner => 1, + ProxyType::NonCritical => 2, + ProxyType::NonTransfer => 3, + ProxyType::Senate => 4, + ProxyType::NonFungible => 5, + ProxyType::Triumvirate => 6, + ProxyType::Governance => 7, + ProxyType::Staking => 8, + ProxyType::Registration => 9, + ProxyType::Transfer => 10, + ProxyType::SmallTransfer => 11, + ProxyType::RootWeights => 12, + ProxyType::ChildKeys => 13, + ProxyType::SudoUncheckedSetCode => 14, + ProxyType::SwapHotkey => 15, + ProxyType::SubnetLeaseBeneficiary => 16, + ProxyType::RootClaim => 17, + } + } +} + +impl ProxyType { + pub fn is_deprecated(&self) -> bool { + matches!( + self, + Self::Triumvirate | Self::Senate | Self::Governance | Self::RootWeights + ) + } +} + +impl Default for ProxyType { + fn default() -> Self { + Self::Any + } +} + +/// Extra constraint attached to an allowed call. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] +pub enum CallConstraint { + /// The named numeric parameter must be lower than `limit`. + ParamLessThan { param_name: Vec, limit: u128 }, + /// The named boxed call parameter must target this pallet/call pair. + NestedCallMustBe { + param_name: Vec, + pallet_name: Vec, + call_name: Vec, + }, +} + +/// Runtime call identity exposed in proxy filter metadata. +#[freeze_struct("85f86877d3d9b870")] +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] +pub struct CallInfo { + /// Runtime pallet name. + pub pallet_name: Vec, + /// Runtime pallet index. + pub pallet_index: u8, + /// Pallet call name. + pub call_name: Vec, + /// Pallet call index. + pub call_index: u8, + /// Optional value or nested-call constraint. + pub constraint: Option, +} + +pub fn call_info_by_name( + name: &str, +) -> CallInfo { + let names = C::get_call_names(); + let indices = C::get_call_indices(); + let pos = names + .iter() + .position(|n| *n == name) + .unwrap_or_else(|| panic!("Call '{}' not found in pallet '{}'", name, P::name())); + + CallInfo { + pallet_name: P::name().as_bytes().to_vec(), + pallet_index: P::index() as u8, + call_name: name.as_bytes().to_vec(), + call_index: indices + .get(pos) + .copied() + .unwrap_or_else(|| panic!("Call '{}' index out of bounds in '{}'", name, P::name())), + constraint: None, + } +} + +/// Metadata view for a call filter group. +/// +/// Implementations should be generated from the same rules as the executable +/// filter so clients and runtime behavior cannot drift. +pub trait CallFilterMetadata { + fn call_infos() -> Vec; +} + +/// A reusable filter group: executable predicate plus metadata for clients. +pub trait CallFilterGroup: Contains + CallFilterMetadata {} + +impl CallFilterGroup for T where T: Contains + CallFilterMetadata {} + +impl CallFilterMetadata for () { + fn call_infos() -> Vec { + Vec::new() + } +} + +#[impl_trait_for_tuples::impl_for_tuples(1, 32)] +impl CallFilterMetadata for Tuple { + fn call_infos() -> Vec { + let mut infos = Vec::new(); + for_tuples!( #( infos.extend(Tuple::call_infos()); )* ); + infos + } +} + +/// Public metadata model for a proxy filter. +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] +pub enum FilterMode { + /// All runtime calls are allowed. + AllowAll, + /// Only listed calls are allowed. An empty list means deny all. + Allow(Vec), +} + +/// Runtime API response for one proxy type. +#[freeze_struct("288413f4da5ab4ee")] +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] +pub struct ProxyFilterInfo { + pub proxy_type: u8, + pub name: Vec, + pub deprecated: bool, + pub filter_mode: FilterMode, +} + +#[freeze_struct("b0cce66ed9b2451b")] +#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] +pub struct ProxyTypeInfo { + pub name: Vec, + pub index: u8, + pub deprecated: bool, +} diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 657bd9e3eb..174cd7b553 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -81,6 +81,6 @@ sp_api::decl_runtime_apis! { pub trait ProxyFilterRuntimeApi { fn get_proxy_types() -> Vec; - fn get_proxy_filter(proxy_type: Option) -> Vec; + fn get_proxy_filters(proxy_types: Option>) -> Vec; } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b4fb481f7f..74ce76a228 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,6 +13,7 @@ use core::num::NonZeroU64; pub mod check_mortality; pub mod check_nonce; mod migrations; +mod proxy_filters; pub mod sudo_wrapper; pub mod transaction_payment_wrapper; @@ -584,194 +585,6 @@ parameter_types! { pub const AnnouncementDepositFactor: Balance = deposit(0, 68); } -// Proxy filter definitions. This macro is the single source of truth for both -// on-chain InstanceFilter::filter() logic and the ProxyFilterRuntimeApi response. -// -// Syntax: -// pallets { Alias => (RuntimeVariant, module_path), ... } -// ProxyType => allow { Pallet::call, ... } — allowlist -// ProxyType => deny { Pallet::call, ... } — denylist -// ProxyType => allow_all; — permit everything -// ProxyType => deny_all; — permit nothing -// ProxyType => allow { ... } except { ... } — allowlist with exceptions -// ProxyType => allow_conditional { Pallet::call where (param) < LIMIT, ... } -// ProxyType => allow_nested { Pallet::call where nested(arg) == Target::method, ... } -// Pallet::* in a list means all calls in that pallet. -// -// To add a new extrinsic to an existing proxy type, append Pallet::call_name -// to the relevant block. To add a new pallet, register it in the pallets {} section first. -// -// Human-readable descriptions of each extrinsic are available to clients via -// runtime metadata (v14/v15) which includes doc comments from pallet call definitions. -subtensor_macros::define_proxy_filters! { - pallets { - Balances => (Balances, pallet_balances), - SubtensorModule => (SubtensorModule, pallet_subtensor), - AdminUtils => (AdminUtils, pallet_admin_utils), - Sudo => (Sudo, pallet_sudo), - System => (System, frame_system), - } - - Any => allow_all; - - NonTransfer => deny { - Balances::*, - SubtensorModule::transfer_stake, - SubtensorModule::schedule_swap_coldkey, - SubtensorModule::swap_coldkey, - SubtensorModule::announce_coldkey_swap, - SubtensorModule::swap_coldkey_announced, - SubtensorModule::clear_coldkey_swap_announcement, - SubtensorModule::dispute_coldkey_swap, - } - - NonFungible => deny { - Balances::*, - SubtensorModule::add_stake, - SubtensorModule::add_stake_limit, - SubtensorModule::remove_stake, - SubtensorModule::remove_stake_limit, - SubtensorModule::remove_stake_full_limit, - SubtensorModule::unstake_all, - SubtensorModule::unstake_all_alpha, - SubtensorModule::swap_stake, - SubtensorModule::swap_stake_limit, - SubtensorModule::move_stake, - SubtensorModule::transfer_stake, - SubtensorModule::burned_register, - SubtensorModule::root_register, - SubtensorModule::schedule_swap_coldkey, - SubtensorModule::swap_coldkey, - SubtensorModule::announce_coldkey_swap, - SubtensorModule::swap_coldkey_announced, - SubtensorModule::clear_coldkey_swap_announcement, - SubtensorModule::dispute_coldkey_swap, - SubtensorModule::swap_hotkey, - SubtensorModule::swap_hotkey_v2, - } - - Transfer => allow { - Balances::transfer_keep_alive, - Balances::transfer_allow_death, - Balances::transfer_all, - SubtensorModule::transfer_stake, - } - - SmallTransfer => allow_conditional { - Balances::transfer_keep_alive where (value) < SMALL_TRANSFER_LIMIT, - Balances::transfer_allow_death where (value) < SMALL_TRANSFER_LIMIT, - SubtensorModule::transfer_stake where (alpha_amount) < SMALL_ALPHA_TRANSFER_LIMIT, - } - - Owner => allow { - AdminUtils::*, - SubtensorModule::set_subnet_identity, - SubtensorModule::update_symbol, - } except { - AdminUtils::sudo_set_sn_owner_hotkey, - } - - NonCritical => deny { - SubtensorModule::dissolve_network, - SubtensorModule::root_register, - SubtensorModule::burned_register, - Sudo::*, - SubtensorModule::announce_coldkey_swap, - SubtensorModule::swap_coldkey_announced, - SubtensorModule::clear_coldkey_swap_announcement, - SubtensorModule::dispute_coldkey_swap, - } - - Triumvirate => deny_all; - Senate => deny_all; - Governance => deny_all; - - Staking => allow { - SubtensorModule::add_stake, - SubtensorModule::remove_stake, - SubtensorModule::unstake_all, - SubtensorModule::unstake_all_alpha, - SubtensorModule::swap_stake, - SubtensorModule::swap_stake_limit, - SubtensorModule::move_stake, - SubtensorModule::add_stake_limit, - SubtensorModule::remove_stake_limit, - SubtensorModule::remove_stake_full_limit, - SubtensorModule::set_root_claim_type, - } - - Registration => allow { - SubtensorModule::burned_register, - SubtensorModule::register, - SubtensorModule::register_limit, - } - - RootWeights => deny_all; - - ChildKeys => allow { - SubtensorModule::set_children, - SubtensorModule::set_childkey_take, - } - - SudoUncheckedSetCode => allow_nested { - Sudo::sudo_unchecked_weight where nested(call) == System::set_code, - } - - SwapHotkey => allow { - SubtensorModule::swap_hotkey, - SubtensorModule::swap_hotkey_v2, - } - - SubnetLeaseBeneficiary => allow { - SubtensorModule::start_call, - AdminUtils::sudo_set_serving_rate_limit, - AdminUtils::sudo_set_min_difficulty, - AdminUtils::sudo_set_max_difficulty, - AdminUtils::sudo_set_weights_version_key, - AdminUtils::sudo_set_adjustment_alpha, - AdminUtils::sudo_set_immunity_period, - AdminUtils::sudo_set_min_allowed_weights, - AdminUtils::sudo_set_kappa, - AdminUtils::sudo_set_rho, - AdminUtils::sudo_set_activity_cutoff, - AdminUtils::sudo_set_network_registration_allowed, - AdminUtils::sudo_set_network_pow_registration_allowed, - AdminUtils::sudo_set_max_burn, - AdminUtils::sudo_set_bonds_moving_average, - AdminUtils::sudo_set_bonds_penalty, - AdminUtils::sudo_set_commit_reveal_weights_enabled, - AdminUtils::sudo_set_liquid_alpha_enabled, - AdminUtils::sudo_set_alpha_values, - AdminUtils::sudo_set_commit_reveal_weights_interval, - AdminUtils::sudo_set_toggle_transfer, - AdminUtils::sudo_set_subnet_emission_enabled, - AdminUtils::sudo_set_min_childkey_take_per_subnet, - } - - RootClaim => allow { - SubtensorModule::claim_root, - } -} - -impl InstanceFilter for ProxyType { - fn filter(&self, c: &RuntimeCall) -> bool { - proxy_type_filter(self, c) - } - fn is_superset(&self, o: &Self) -> bool { - match (self, o) { - (x, y) if x == y => true, - (ProxyType::Any, _) => true, - (_, ProxyType::Any) => false, - (ProxyType::NonTransfer, _) => { - // NonTransfer is NOT a superset of Transfer or SmallTransfer - !matches!(o, ProxyType::Transfer | ProxyType::SmallTransfer) - } - (ProxyType::Transfer, ProxyType::SmallTransfer) => true, - _ => false, - } - } -} - impl pallet_proxy::Config for Runtime { type RuntimeCall = RuntimeCall; type Currency = Balances; @@ -1766,59 +1579,6 @@ fn generate_genesis_json() -> Vec { type EventRecord = frame_system::EventRecord; -fn call_info_by_name(name: &str) -> CallInfo { - let names = C::get_call_names(); - let indices = C::get_call_indices(); - let pos = names - .iter() - .position(|n| *n == name) - .unwrap_or_else(|| panic!("Call '{}' not found in pallet '{}'", name, P::name())); - CallInfo { - pallet_name: P::name().as_bytes().to_vec(), - pallet_index: P::index() as u8, - call_name: Some(name.as_bytes().to_vec()), - call_index: Some( - indices.get(pos).copied().unwrap_or_else(|| { - panic!("Call '{}' index out of bounds in '{}'", name, P::name()) - }), - ), - condition: None, - } -} - -fn call_info_by_name_conditional( - name: &str, - condition: CallCondition, -) -> CallInfo { - let mut info = call_info_by_name::(name); - info.condition = Some(condition); - info -} - -fn pallet_wildcard() -> CallInfo { - CallInfo { - pallet_name: P::name().as_bytes().to_vec(), - pallet_index: P::index() as u8, - call_name: None, - call_index: None, - condition: None, - } -} - -pub fn get_all_proxy_type_infos() -> Vec { - (0u8..=u8::MAX) - .filter_map(|i: u8| { - ProxyType::try_from(i) - .ok() - .map(|pt: ProxyType| ProxyTypeInfo { - name: alloc::format!("{:?}", pt).into_bytes(), - index: i, - deprecated: pt.is_deprecated(), - }) - }) - .collect() -} - impl_runtime_apis! { impl sp_api::Core for Runtime { fn version() -> RuntimeVersion { @@ -2587,15 +2347,11 @@ impl_runtime_apis! { impl subtensor_custom_rpc_runtime_api::ProxyFilterRuntimeApi for Runtime { fn get_proxy_types() -> Vec { - get_all_proxy_type_infos() + proxy_filters::get_all_proxy_type_infos() } - fn get_proxy_filter(proxy_type: Option) -> Vec { - let all = get_all_proxy_filters(); - match proxy_type { - None => all, - Some(idx) => all.into_iter().filter(|f| f.proxy_type == idx).collect(), - } + fn get_proxy_filters(proxy_types: Option>) -> Vec { + proxy_filters::get_proxy_filters(proxy_types) } } diff --git a/runtime/src/proxy_filters/call_groups.rs b/runtime/src/proxy_filters/call_groups.rs new file mode 100644 index 0000000000..8044a285b1 --- /dev/null +++ b/runtime/src/proxy_filters/call_groups.rs @@ -0,0 +1,753 @@ +//! Runtime call inventory for proxy filters. +//! +//! Keep this file boring: one generated group per call-bearing runtime pallet. +//! Proxy-specific policy belongs in `mod.rs`. + +use frame_system::Call as SystemCall; +use pallet_admin_utils::Call as AdminUtilsCall; +use pallet_balances::Call as BalancesCall; +use pallet_base_fee::Call as BaseFeeCall; +use pallet_commitments::Call as CommitmentsCall; +use pallet_contracts::Call as ContractsCall; +use pallet_crowdloan::Call as CrowdloanCall; +use pallet_drand::Call as DrandCall; +use pallet_ethereum::Call as EthereumCall; +use pallet_evm::Call as EvmCall; +use pallet_grandpa::Call as GrandpaCall; +use pallet_limit_orders::Call as LimitOrdersCall; +use pallet_multisig::Call as MultisigCall; +use pallet_preimage::Call as PreimageCall; +use pallet_safe_mode::Call as SafeModeCall; +use pallet_scheduler::Call as SchedulerCall; +use pallet_shield::Call as MevShieldCall; +use pallet_subtensor::Call as SubtensorCall; +use pallet_subtensor_proxy::Call as ProxyCall; +use pallet_subtensor_swap::Call as SwapCall; +use pallet_subtensor_utility::Call as UtilityCall; +use pallet_sudo::Call as SudoCall; +use pallet_timestamp::Call as TimestampCall; +use subtensor_macros::call_filter_group; +use subtensor_runtime_common::{SMALL_ALPHA_TRANSFER_LIMIT, SMALL_TRANSFER_LIMIT}; + +call_filter_group!( + SystemCalls, + [ + RuntimeCall::System(SystemCall::remark), + RuntimeCall::System(SystemCall::remark_with_event), + RuntimeCall::System(SystemCall::set_heap_pages), + RuntimeCall::System(SystemCall::set_code), + RuntimeCall::System(SystemCall::set_code_without_checks), + RuntimeCall::System(SystemCall::set_storage), + RuntimeCall::System(SystemCall::kill_storage), + RuntimeCall::System(SystemCall::kill_prefix), + RuntimeCall::System(SystemCall::authorize_upgrade), + RuntimeCall::System(SystemCall::authorize_upgrade_without_checks), + RuntimeCall::System(SystemCall::apply_authorized_upgrade), + ] +); + +call_filter_group!( + TimestampCalls, + [RuntimeCall::Timestamp(TimestampCall::set),] +); + +call_filter_group!( + GrandpaCalls, + [ + RuntimeCall::Grandpa(GrandpaCall::report_equivocation), + RuntimeCall::Grandpa(GrandpaCall::report_equivocation_unsigned), + RuntimeCall::Grandpa(GrandpaCall::note_stalled), + ] +); + +call_filter_group!( + SafeModeCalls, + [ + RuntimeCall::SafeMode(SafeModeCall::enter), + RuntimeCall::SafeMode(SafeModeCall::force_enter), + RuntimeCall::SafeMode(SafeModeCall::extend), + RuntimeCall::SafeMode(SafeModeCall::force_extend), + RuntimeCall::SafeMode(SafeModeCall::force_exit), + RuntimeCall::SafeMode(SafeModeCall::force_slash_deposit), + RuntimeCall::SafeMode(SafeModeCall::release_deposit), + RuntimeCall::SafeMode(SafeModeCall::force_release_deposit), + ] +); + +call_filter_group!( + EthereumCalls, + [RuntimeCall::Ethereum(EthereumCall::transact),] +); + +call_filter_group!( + EvmCalls, + [ + RuntimeCall::EVM(EvmCall::withdraw), + RuntimeCall::EVM(EvmCall::call), + RuntimeCall::EVM(EvmCall::create), + RuntimeCall::EVM(EvmCall::create2), + RuntimeCall::EVM(EvmCall::set_whitelist), + RuntimeCall::EVM(EvmCall::disable_whitelist), + ] +); + +call_filter_group!( + BaseFeeCalls, + [ + RuntimeCall::BaseFee(BaseFeeCall::set_base_fee_per_gas), + RuntimeCall::BaseFee(BaseFeeCall::set_elasticity), + ] +); + +call_filter_group!( + DrandCalls, + [ + RuntimeCall::Drand(DrandCall::write_pulse), + RuntimeCall::Drand(DrandCall::set_beacon_config), + RuntimeCall::Drand(DrandCall::set_oldest_stored_round), + ] +); + +call_filter_group!( + CrowdloanCalls, + [ + RuntimeCall::Crowdloan(CrowdloanCall::create), + RuntimeCall::Crowdloan(CrowdloanCall::contribute), + RuntimeCall::Crowdloan(CrowdloanCall::withdraw), + RuntimeCall::Crowdloan(CrowdloanCall::finalize), + RuntimeCall::Crowdloan(CrowdloanCall::refund), + RuntimeCall::Crowdloan(CrowdloanCall::dissolve), + RuntimeCall::Crowdloan(CrowdloanCall::update_min_contribution), + RuntimeCall::Crowdloan(CrowdloanCall::update_end), + RuntimeCall::Crowdloan(CrowdloanCall::update_cap), + RuntimeCall::Crowdloan(CrowdloanCall::set_max_contribution), + ] +); + +call_filter_group!( + SwapCalls, + [ + RuntimeCall::Swap(SwapCall::toggle_user_liquidity), + RuntimeCall::Swap(SwapCall::add_liquidity), + RuntimeCall::Swap(SwapCall::remove_liquidity), + RuntimeCall::Swap(SwapCall::modify_position), + RuntimeCall::Swap(SwapCall::disable_lp), + RuntimeCall::Swap(SwapCall::set_fee_rate), + ] +); + +call_filter_group!( + ContractsCalls, + [ + RuntimeCall::Contracts(ContractsCall::call_old_weight), + RuntimeCall::Contracts(ContractsCall::instantiate_with_code_old_weight), + RuntimeCall::Contracts(ContractsCall::instantiate_old_weight), + RuntimeCall::Contracts(ContractsCall::upload_code), + RuntimeCall::Contracts(ContractsCall::remove_code), + RuntimeCall::Contracts(ContractsCall::set_code), + RuntimeCall::Contracts(ContractsCall::call), + RuntimeCall::Contracts(ContractsCall::instantiate_with_code), + RuntimeCall::Contracts(ContractsCall::instantiate), + RuntimeCall::Contracts(ContractsCall::migrate), + ] +); + +call_filter_group!( + MevShieldCalls, + [ + RuntimeCall::MevShield(MevShieldCall::announce_next_key), + RuntimeCall::MevShield(MevShieldCall::submit_encrypted), + RuntimeCall::MevShield(MevShieldCall::store_encrypted), + RuntimeCall::MevShield(MevShieldCall::set_max_pending_extrinsics_number), + RuntimeCall::MevShield(MevShieldCall::set_on_initialize_weight), + RuntimeCall::MevShield(MevShieldCall::set_stored_extrinsic_lifetime), + RuntimeCall::MevShield(MevShieldCall::set_max_extrinsic_weight), + ] +); + +call_filter_group!( + LimitOrdersCalls, + [ + RuntimeCall::LimitOrders(LimitOrdersCall::execute_orders), + RuntimeCall::LimitOrders(LimitOrdersCall::execute_batched_orders), + RuntimeCall::LimitOrders(LimitOrdersCall::cancel_order), + RuntimeCall::LimitOrders(LimitOrdersCall::set_pallet_status), + ] +); + +call_filter_group!( + UtilityCalls, + [ + RuntimeCall::Utility(UtilityCall::batch), + RuntimeCall::Utility(UtilityCall::as_derivative), + RuntimeCall::Utility(UtilityCall::batch_all), + RuntimeCall::Utility(UtilityCall::dispatch_as), + RuntimeCall::Utility(UtilityCall::force_batch), + RuntimeCall::Utility(UtilityCall::with_weight), + RuntimeCall::Utility(UtilityCall::if_else), + RuntimeCall::Utility(UtilityCall::dispatch_as_fallible), + ] +); + +call_filter_group!( + SudoCalls, + [ + RuntimeCall::Sudo(SudoCall::sudo), + RuntimeCall::Sudo(SudoCall::sudo_unchecked_weight), + RuntimeCall::Sudo(SudoCall::set_key), + RuntimeCall::Sudo(SudoCall::sudo_as), + RuntimeCall::Sudo(SudoCall::remove_key), + ] +); + +call_filter_group!( + MultisigCalls, + [ + RuntimeCall::Multisig(MultisigCall::as_multi_threshold_1), + RuntimeCall::Multisig(MultisigCall::as_multi), + RuntimeCall::Multisig(MultisigCall::approve_as_multi), + RuntimeCall::Multisig(MultisigCall::cancel_as_multi), + RuntimeCall::Multisig(MultisigCall::poke_deposit), + ] +); + +call_filter_group!( + PreimageCalls, + [ + RuntimeCall::Preimage(PreimageCall::note_preimage), + RuntimeCall::Preimage(PreimageCall::unnote_preimage), + RuntimeCall::Preimage(PreimageCall::request_preimage), + RuntimeCall::Preimage(PreimageCall::unrequest_preimage), + RuntimeCall::Preimage(PreimageCall::ensure_updated), + ] +); + +call_filter_group!( + SchedulerCalls, + [ + RuntimeCall::Scheduler(SchedulerCall::schedule), + RuntimeCall::Scheduler(SchedulerCall::cancel), + RuntimeCall::Scheduler(SchedulerCall::schedule_named), + RuntimeCall::Scheduler(SchedulerCall::cancel_named), + RuntimeCall::Scheduler(SchedulerCall::schedule_after), + RuntimeCall::Scheduler(SchedulerCall::schedule_named_after), + RuntimeCall::Scheduler(SchedulerCall::set_retry), + RuntimeCall::Scheduler(SchedulerCall::set_retry_named), + RuntimeCall::Scheduler(SchedulerCall::cancel_retry), + RuntimeCall::Scheduler(SchedulerCall::cancel_retry_named), + ] +); + +call_filter_group!( + ProxyCalls, + [ + RuntimeCall::Proxy(ProxyCall::proxy), + RuntimeCall::Proxy(ProxyCall::add_proxy), + RuntimeCall::Proxy(ProxyCall::remove_proxy), + RuntimeCall::Proxy(ProxyCall::remove_proxies), + RuntimeCall::Proxy(ProxyCall::create_pure), + RuntimeCall::Proxy(ProxyCall::kill_pure), + RuntimeCall::Proxy(ProxyCall::announce), + RuntimeCall::Proxy(ProxyCall::remove_announcement), + RuntimeCall::Proxy(ProxyCall::reject_announcement), + RuntimeCall::Proxy(ProxyCall::proxy_announced), + RuntimeCall::Proxy(ProxyCall::poke_deposit), + RuntimeCall::Proxy(ProxyCall::set_real_pays_fee), + ] +); + +call_filter_group!( + CommitmentsCalls, + [ + RuntimeCall::Commitments(CommitmentsCall::set_commitment), + RuntimeCall::Commitments(CommitmentsCall::set_max_space), + ] +); + +// Ordinary balance transfers — moving your own free balance. +call_filter_group!( + BalanceTransferCalls, + [ + RuntimeCall::Balances(BalancesCall::transfer_keep_alive), + RuntimeCall::Balances(BalancesCall::transfer_allow_death), + RuntimeCall::Balances(BalancesCall::transfer_all), + ] +); + +// Privileged, root-only balance operations (force transfer/unreserve, burn, +// issuance adjustment). +call_filter_group!( + BalanceMaintenanceCalls, + [ + RuntimeCall::Balances(BalancesCall::force_transfer), + RuntimeCall::Balances(BalancesCall::force_unreserve), + RuntimeCall::Balances(BalancesCall::upgrade_accounts), + RuntimeCall::Balances(BalancesCall::force_set_balance), + RuntimeCall::Balances(BalancesCall::force_adjust_total_issuance), + RuntimeCall::Balances(BalancesCall::burn), + ] +); + +// Managing your own stake: add, remove, and move between hotkeys/subnets. +call_filter_group!( + StakeManagementCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::add_stake), + RuntimeCall::SubtensorModule(SubtensorCall::add_stake_limit), + RuntimeCall::SubtensorModule(SubtensorCall::remove_stake), + RuntimeCall::SubtensorModule(SubtensorCall::remove_stake_limit), + RuntimeCall::SubtensorModule(SubtensorCall::remove_stake_full_limit), + RuntimeCall::SubtensorModule(SubtensorCall::unstake_all), + RuntimeCall::SubtensorModule(SubtensorCall::unstake_all_alpha), + RuntimeCall::SubtensorModule(SubtensorCall::move_stake), + RuntimeCall::SubtensorModule(SubtensorCall::swap_stake), + RuntimeCall::SubtensorModule(SubtensorCall::swap_stake_limit), + ] +); + +// Moving staked value to another coldkey — the stake analogue of a transfer. +call_filter_group!( + StakeTransferCalls, + [RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake),] +); + +// Permissionless proof-of-work registration (costs no TAO). +call_filter_group!( + PowRegistrationCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::register), + RuntimeCall::SubtensorModule(SubtensorCall::register_limit), + ] +); + +// Registration paid by burning TAO (spends value, unlike POW registration). +call_filter_group!( + BurnedRegistrationCalls, + [RuntimeCall::SubtensorModule(SubtensorCall::burned_register),] +); + +// Registration into the root subnet. +call_filter_group!( + RootRegistrationCalls, + [RuntimeCall::SubtensorModule(SubtensorCall::root_register),] +); + +// Rotating a neuron's hotkey. +call_filter_group!( + HotkeySwapCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey), + RuntimeCall::SubtensorModule(SubtensorCall::swap_hotkey_v2), + ] +); + +// Rotating a coldkey — the full account-takeover surface. +call_filter_group!( + ColdkeySwapCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::schedule_swap_coldkey), + RuntimeCall::SubtensorModule(SubtensorCall::swap_coldkey), + RuntimeCall::SubtensorModule(SubtensorCall::announce_coldkey_swap), + RuntimeCall::SubtensorModule(SubtensorCall::swap_coldkey_announced), + RuntimeCall::SubtensorModule(SubtensorCall::clear_coldkey_swap_announcement), + RuntimeCall::SubtensorModule(SubtensorCall::dispute_coldkey_swap), + RuntimeCall::SubtensorModule(SubtensorCall::reset_coldkey_swap), + ] +); + +// Dissolving a subnet — irreversible network destruction. +call_filter_group!( + CriticalNetworkCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::dissolve_network), + RuntimeCall::SubtensorModule(SubtensorCall::root_dissolve_network), + ] +); + +// Delegating a hotkey's work to child keys. +call_filter_group!( + ChildKeyCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::set_children), + RuntimeCall::SubtensorModule(SubtensorCall::set_childkey_take), + ] +); + +// Claiming accumulated root dividends. +call_filter_group!( + RootClaimCalls, + [RuntimeCall::SubtensorModule(SubtensorCall::claim_root),] +); + +// Selecting how root dividends are claimed (a staking-side setting). +call_filter_group!( + RootClaimTypeCalls, + [RuntimeCall::SubtensorModule( + SubtensorCall::set_root_claim_type + ),] +); + +// A subnet's public identity and token symbol. +call_filter_group!( + SubnetIdentityCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::set_subnet_identity), + RuntimeCall::SubtensorModule(SubtensorCall::update_symbol), + ] +); + +// Starting a subnet's emission schedule (start_call). +call_filter_group!( + SubnetActivationCalls, + [RuntimeCall::SubtensorModule(SubtensorCall::start_call),] +); + +// Residual pallet-subtensor calls that no proxy needs to grant on their own: +// weights, serving, delegate-take, alpha lock/burn, network registration, +// childkey admin, account association, tempo control, voting power, root-claim +// admin, and lease teardown. +call_filter_group!( + SubtensorCommonCalls, + [ + RuntimeCall::SubtensorModule(SubtensorCall::set_weights), + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights), + RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights), + RuntimeCall::SubtensorModule(SubtensorCall::commit_weights), + RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights), + RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights), + RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights), + RuntimeCall::SubtensorModule(SubtensorCall::reveal_weights), + RuntimeCall::SubtensorModule(SubtensorCall::reveal_mechanism_weights), + RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights), + RuntimeCall::SubtensorModule(SubtensorCall::add_stake_burn), + RuntimeCall::SubtensorModule(SubtensorCall::lock_stake), + RuntimeCall::SubtensorModule(SubtensorCall::move_lock), + RuntimeCall::SubtensorModule(SubtensorCall::set_perpetual_lock), + RuntimeCall::SubtensorModule(SubtensorCall::recycle_alpha), + RuntimeCall::SubtensorModule(SubtensorCall::burn_alpha), + RuntimeCall::SubtensorModule(SubtensorCall::register_network), + RuntimeCall::SubtensorModule(SubtensorCall::register_network_with_identity), + RuntimeCall::SubtensorModule(SubtensorCall::register_leased_network), + RuntimeCall::SubtensorModule(SubtensorCall::decrease_take), + RuntimeCall::SubtensorModule(SubtensorCall::increase_take), + RuntimeCall::SubtensorModule(SubtensorCall::serve_axon), + RuntimeCall::SubtensorModule(SubtensorCall::serve_axon_tls), + RuntimeCall::SubtensorModule(SubtensorCall::serve_prometheus), + RuntimeCall::SubtensorModule(SubtensorCall::set_identity), + RuntimeCall::SubtensorModule(SubtensorCall::try_associate_hotkey), + RuntimeCall::SubtensorModule(SubtensorCall::associate_evm_key), + RuntimeCall::SubtensorModule(SubtensorCall::set_coldkey_auto_stake_hotkey), + RuntimeCall::SubtensorModule(SubtensorCall::set_pending_childkey_cooldown), + RuntimeCall::SubtensorModule(SubtensorCall::set_auto_parent_delegation_enabled), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_tx_childkey_take_rate_limit), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_min_childkey_take), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_max_childkey_take), + RuntimeCall::SubtensorModule(SubtensorCall::terminate_lease), + RuntimeCall::SubtensorModule(SubtensorCall::set_tempo), + RuntimeCall::SubtensorModule(SubtensorCall::set_activity_cutoff_factor), + RuntimeCall::SubtensorModule(SubtensorCall::trigger_epoch), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_num_root_claims), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_root_claim_threshold), + RuntimeCall::SubtensorModule(SubtensorCall::enable_voting_power_tracking), + RuntimeCall::SubtensorModule(SubtensorCall::disable_voting_power_tracking), + RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_voting_power_ema_alpha), + ] +); + +// Subnet parameters a subnet owner may set directly (the admin-utils calls +// guarded by `ensure_sn_owner_or_root`). These are the genuine owner/lease +// management surface, as opposed to the root-only `RootConfigCalls`. +call_filter_group!( + SubnetManagementCalls, + [ + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_serving_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_difficulty), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_weights_version_key), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_adjustment_alpha), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_immunity_period), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_allowed_weights), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_rho), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_activity_cutoff), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_burn), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_burn), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_bonds_moving_average), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_bonds_penalty), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_childkey_take_per_subnet), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_commit_reveal_weights_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_liquid_alpha_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_alpha_values), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_commit_reveal_weights_interval), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_toggle_transfer), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_recycle_or_burn), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_alpha_sigmoid_steepness), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_yuma3_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_bonds_reset_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_owner_immune_neuron_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_mechanism_count), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_mechanism_emission_split), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_trim_to_max_allowed_uids), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_allowed_uids), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_burn_half_life), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_burn_increase_mult), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_owner_cut_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_owner_cut_auto_lock_enabled), + ] +); + +// Admin parameters that require root (set via sudo / governance). A subnet +// owner cannot call these; they reach the broad proxies only as inert grants +// (the dispatch's `ensure_root` rejects a proxy's signed origin). Includes the +// two deprecated extrinsics that always error. +call_filter_group!( + RootConfigCalls, + [ + RuntimeCall::AdminUtils(AdminUtilsCall::swap_authorities), + RuntimeCall::AdminUtils(AdminUtilsCall::schedule_grandpa_change), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_default_take), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tx_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_difficulty), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_weights_set_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_adjustment_interval), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_kappa), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_network_registration_allowed), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_network_pow_registration_allowed), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_target_registrations_per_interval), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_difficulty), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_allowed_validators), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_registrations_per_block), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subnet_owner_cut), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_network_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tempo), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_total_issuance), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_network_immunity_period), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_network_min_lock_cost), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subnet_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_lock_reduction_interval), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_rao_recycled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_stake_threshold), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_nominator_min_required_stake), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tx_delegate_take_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_delegate_take), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_dissolve_network_schedule_duration), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_evm_chain_id), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_toggle_evm_precompile), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subnet_moving_alpha), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_ema_price_halving_period), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subtoken_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_commit_reveal_version), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_ck_burn), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_admin_freeze_window), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_owner_hparam_rate_limit), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_allowed_uids), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_non_immune_uids), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_cutoff), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_normalization_exponent), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_tao_flow_smoothing_factor), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_net_tao_flow_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_mechanism_count), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_start_call_delay), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_coldkey_swap_announcement_delay), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_coldkey_swap_reannouncement_delay), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subnet_emission_enabled), + RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_epochs_per_block), + ] +); + +// Rotating a subnet's owner key — an ownership-takeover vector, deliberately +// kept out of the Owner proxy. +call_filter_group!( + OwnerKeyCalls, + [RuntimeCall::AdminUtils( + AdminUtilsCall::sudo_set_sn_owner_hotkey + ),] +); + +// ============================================================================ +// Conditional, proxy-specific groups +// +// These carry amount / nested-call constraints, so they overlap the inventory +// groups above (e.g. `transfer_keep_alive` is also in `BalanceTransferCalls`). +// They are deliberately excluded from `AllCalls` to keep that a non-overlapping +// partition. Generating them with the same macro keeps the executable filter +// and the client-facing metadata in lockstep. +// ============================================================================ + +// `SmallTransfer`: amount-bounded balance and stake transfers. +call_filter_group!(SmallTransferCalls, [ + RuntimeCall::Balances(BalancesCall::transfer_keep_alive) + where value < SMALL_TRANSFER_LIMIT, + RuntimeCall::Balances(BalancesCall::transfer_allow_death) + where value < SMALL_TRANSFER_LIMIT, + RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake) + where alpha_amount < SMALL_ALPHA_TRANSFER_LIMIT, +]); + +// `SudoUncheckedSetCode`: a single sudo call, only when it wraps +// `System::set_code`. +call_filter_group!(SudoSetCodeCalls, [ + RuntimeCall::Sudo(SudoCall::sudo_unchecked_weight) + where nested(call) == RuntimeCall::System(SystemCall::set_code), +]); + +// Full inventory of every runtime call, used only by the coverage test that +// checks it against `RuntimeCall` metadata. Nested in three blocks so the +// flattened tuple stays within the `CallFilterMetadata` tuple-impl arity; +// `call_infos()` recurses regardless. +// Infrastructure pallets granted wholesale to the broad proxies, excluding +// `SudoCalls` (which `NonCritical` denies). Shared by the proxy policy in +// `mod.rs` and by the `WholesalePalletCalls` inventory below so the list lives +// in one place. +pub(super) type InfraCommonCalls = ( + SystemCalls, + TimestampCalls, + GrandpaCalls, + UtilityCalls, + MultisigCalls, + PreimageCalls, + SchedulerCalls, + ProxyCalls, + CommitmentsCalls, + SafeModeCalls, + EthereumCalls, + EvmCalls, + BaseFeeCalls, + DrandCalls, + CrowdloanCalls, + SwapCalls, + ContractsCalls, + MevShieldCalls, + LimitOrdersCalls, +); + +#[cfg(test)] +pub(super) type AllCalls = ( + WholesalePalletCalls, + SubtensorSplitCalls, + AdminUtilsSplitCalls, +); + +// Pallets every granting proxy grants in full. +#[cfg(test)] +type WholesalePalletCalls = (InfraCommonCalls, SudoCalls); + +// Balances + pallet-subtensor, split by proxy membership. +#[cfg(test)] +type SubtensorSplitCalls = ( + BalanceTransferCalls, + BalanceMaintenanceCalls, + StakeManagementCalls, + StakeTransferCalls, + PowRegistrationCalls, + BurnedRegistrationCalls, + RootRegistrationCalls, + HotkeySwapCalls, + ColdkeySwapCalls, + CriticalNetworkCalls, + ChildKeyCalls, + RootClaimCalls, + RootClaimTypeCalls, + SubnetIdentityCalls, + SubnetActivationCalls, + SubtensorCommonCalls, +); + +// admin-utils, split for the Owner and SubnetLeaseBeneficiary proxies. +#[cfg(test)] +type AdminUtilsSplitCalls = (SubnetManagementCalls, RootConfigCalls, OwnerKeyCalls); + +#[cfg(test)] +mod tests { + use super::*; + use crate::RuntimeCall; + use alloc::{collections::BTreeSet, format, string::String, vec::Vec}; + use frame_support::traits::GetCallMetadata; + use subtensor_runtime_common::{CallFilterMetadata, CallInfo}; + + #[test] + fn all_call_groups_cover_runtime_call_metadata() { + let groups_call_infos = AllCalls::call_infos(); + let groups_call_names = call_names_from_group_infos(&groups_call_infos); + let runtime_call_names = call_names_from_runtime_metadata(); + let duplicate_group_calls = duplicate_call_names(&groups_call_infos); + + let missing_from_groups = runtime_call_names + .difference(&groups_call_names) + .cloned() + .collect::>(); + let extra_in_groups = groups_call_names + .difference(&runtime_call_names) + .cloned() + .collect::>(); + + assert!( + missing_from_groups.is_empty() + && extra_in_groups.is_empty() + && duplicate_group_calls.is_empty(), + "AllCalls inventory does not match RuntimeCall metadata.\n\ + call_infos: {}\n\ + runtime calls: {}\n\ + missing from AllCalls ({}):\n{}\n\ + extra in AllCalls ({}):\n{}\n\ + duplicates in AllCalls ({}):\n{}", + groups_call_names.len(), + runtime_call_names.len(), + missing_from_groups.len(), + format_call_list(&missing_from_groups), + extra_in_groups.len(), + format_call_list(&extra_in_groups), + duplicate_group_calls.len(), + format_call_list(&duplicate_group_calls), + ); + } + + fn call_names_from_group_infos(call_infos: &[CallInfo]) -> BTreeSet { + call_infos.iter().map(format_call_info).collect() + } + + fn call_names_from_runtime_metadata() -> BTreeSet { + RuntimeCall::get_module_names() + .iter() + .flat_map(|module| { + RuntimeCall::get_call_names(module) + .iter() + .map(move |call| format!("{}::{}", module, call)) + }) + .collect() + } + + fn duplicate_call_names(call_infos: &[CallInfo]) -> Vec { + let mut seen = BTreeSet::new(); + let mut duplicates = Vec::new(); + + for call_info in call_infos { + let call_name = format_call_info(call_info); + if !seen.insert(call_name.clone()) { + duplicates.push(call_name); + } + } + + duplicates + } + + fn format_call_info(call_info: &CallInfo) -> String { + format!( + "{}::{}", + String::from_utf8_lossy(&call_info.pallet_name), + String::from_utf8_lossy(&call_info.call_name) + ) + } + + fn format_call_list(calls: &[String]) -> String { + if calls.is_empty() { + return " ".into(); + } + + calls + .iter() + .map(|call| format!(" {}", call)) + .collect::>() + .join("\n") + } +} diff --git a/runtime/src/proxy_filters/mod.rs b/runtime/src/proxy_filters/mod.rs new file mode 100644 index 0000000000..1e1438ec88 --- /dev/null +++ b/runtime/src/proxy_filters/mod.rs @@ -0,0 +1,561 @@ +mod call_groups; + +use alloc::{format, vec::Vec}; + +use call_groups::*; +use frame_support::traits::{Contains, InstanceFilter}; +use subtensor_runtime_common::{ + CallFilterMetadata, FilterMode, ProxyFilterInfo, ProxyType, ProxyTypeInfo, +}; + +use crate::RuntimeCall; + +// ============================================================================ +// Per-proxy allow-lists +// +// Each proxy type's permission set is an *additive* union of whole call groups +// from `call_groups`. A call a proxy does not list is denied. `Any` allows +// everything; the deprecated proxies allow nothing. +// +// `Contains` for a tuple is logical OR (any member matches), so these aliases +// read as "allow if the call is in any of these groups". +// ============================================================================ + +/// All admin-utils configuration. Every broad proxy historically allowed every +/// admin call; the root-only (`RootConfigCalls`) and owner-key (`OwnerKeyCalls`) +/// calls are gated by the dispatch's own origin check, so granting them to a +/// signed proxy is inert. +type AdminAll = (SubnetManagementCalls, RootConfigCalls, OwnerKeyCalls); + +/// `Transfer`: liquid value movement. +type TransferAllowed = (BalanceTransferCalls, StakeTransferCalls); + +/// `Staking`: stake position management plus root-claim mode selection. +type StakingAllowed = (StakeManagementCalls, RootClaimTypeCalls); + +/// `Registration`: acquire a slot (POW or by burn). +type RegistrationAllowed = (PowRegistrationCalls, BurnedRegistrationCalls); + +/// `Owner`: run a subnet you own — subnet identity plus the owner-settable +/// admin config. Excludes root-only admin (it can't pass `ensure_root`) and +/// owner-key rotation. +type OwnerAllowed = (SubnetIdentityCalls, SubnetManagementCalls); + +/// `SubnetLeaseBeneficiary`: operate a leased subnet (activation, identity, and +/// the owner-settable subnet management config). +type SubnetLeaseAllowed = ( + SubnetActivationCalls, + SubnetIdentityCalls, + SubnetManagementCalls, +); + +/// `NonTransfer`: everything except liquid value movement and coldkey swaps. +type NonTransferAllowed = ( + InfraCommonCalls, + AdminAll, + SudoCalls, + StakeManagementCalls, + PowRegistrationCalls, + BurnedRegistrationCalls, + RootRegistrationCalls, + HotkeySwapCalls, + CriticalNetworkCalls, + ChildKeyCalls, + RootClaimCalls, + RootClaimTypeCalls, + SubnetIdentityCalls, + SubnetActivationCalls, + SubtensorCommonCalls, +); + +/// `NonFungible`: nothing that moves TAO/alpha and no key swaps. +type NonFungibleAllowed = ( + InfraCommonCalls, + AdminAll, + SudoCalls, + PowRegistrationCalls, + CriticalNetworkCalls, + ChildKeyCalls, + RootClaimCalls, + RootClaimTypeCalls, + SubnetIdentityCalls, + SubnetActivationCalls, + SubtensorCommonCalls, +); + +/// `NonCritical`: day-to-day operations including value movement, but no sudo, +/// network dissolution, root/burned registration, or coldkey swaps. +type NonCriticalAllowed = ( + InfraCommonCalls, + AdminAll, + BalanceTransferCalls, + BalanceMaintenanceCalls, + StakeManagementCalls, + StakeTransferCalls, + PowRegistrationCalls, + HotkeySwapCalls, + ChildKeyCalls, + RootClaimCalls, + RootClaimTypeCalls, + SubnetIdentityCalls, + SubnetActivationCalls, + SubtensorCommonCalls, +); + +pub(crate) fn proxy_type_filter(proxy_type: &ProxyType, call: &RuntimeCall) -> bool { + match proxy_type { + ProxyType::Any => true, + ProxyType::Owner => OwnerAllowed::contains(call), + ProxyType::NonCritical => NonCriticalAllowed::contains(call), + ProxyType::NonTransfer => NonTransferAllowed::contains(call), + ProxyType::NonFungible => NonFungibleAllowed::contains(call), + ProxyType::Staking => StakingAllowed::contains(call), + ProxyType::Registration => RegistrationAllowed::contains(call), + ProxyType::Transfer => TransferAllowed::contains(call), + ProxyType::SmallTransfer => SmallTransferCalls::contains(call), + ProxyType::ChildKeys => ChildKeyCalls::contains(call), + ProxyType::SwapHotkey => HotkeySwapCalls::contains(call), + ProxyType::SubnetLeaseBeneficiary => SubnetLeaseAllowed::contains(call), + ProxyType::RootClaim => RootClaimCalls::contains(call), + ProxyType::SudoUncheckedSetCode => SudoSetCodeCalls::contains(call), + ProxyType::Triumvirate + | ProxyType::Senate + | ProxyType::Governance + | ProxyType::RootWeights => false, + } +} + +impl InstanceFilter for ProxyType { + fn filter(&self, call: &RuntimeCall) -> bool { + proxy_type_filter(self, call) + } + + fn is_superset(&self, other: &Self) -> bool { + match (self, other) { + (x, y) if x == y => true, + (ProxyType::Any, _) => true, + (_, ProxyType::Any) => false, + (ProxyType::NonTransfer, _) => { + !matches!(other, ProxyType::Transfer | ProxyType::SmallTransfer) + } + (ProxyType::Transfer, ProxyType::SmallTransfer) => true, + _ => false, + } + } +} + +// ============================================================================ +// Runtime API metadata +// +// The client-facing allowlist view is derived from the same call groups the +// filter uses, so the two cannot drift. +// ============================================================================ + +/// The filter mode (allow-all or an explicit allowlist) for one proxy type. +fn proxy_filter_mode(proxy_type: ProxyType) -> FilterMode { + match proxy_type { + ProxyType::Any => FilterMode::AllowAll, + ProxyType::Owner => FilterMode::Allow(OwnerAllowed::call_infos()), + ProxyType::NonCritical => FilterMode::Allow(NonCriticalAllowed::call_infos()), + ProxyType::NonTransfer => FilterMode::Allow(NonTransferAllowed::call_infos()), + ProxyType::NonFungible => FilterMode::Allow(NonFungibleAllowed::call_infos()), + ProxyType::Staking => FilterMode::Allow(StakingAllowed::call_infos()), + ProxyType::Registration => FilterMode::Allow(RegistrationAllowed::call_infos()), + ProxyType::Transfer => FilterMode::Allow(TransferAllowed::call_infos()), + ProxyType::SmallTransfer => FilterMode::Allow(SmallTransferCalls::call_infos()), + ProxyType::ChildKeys => FilterMode::Allow(ChildKeyCalls::call_infos()), + ProxyType::SwapHotkey => FilterMode::Allow(HotkeySwapCalls::call_infos()), + ProxyType::SubnetLeaseBeneficiary => FilterMode::Allow(SubnetLeaseAllowed::call_infos()), + ProxyType::RootClaim => FilterMode::Allow(RootClaimCalls::call_infos()), + ProxyType::SudoUncheckedSetCode => FilterMode::Allow(SudoSetCodeCalls::call_infos()), + ProxyType::Triumvirate + | ProxyType::Senate + | ProxyType::Governance + | ProxyType::RootWeights => FilterMode::Allow(Vec::new()), + } +} + +/// Every proxy type with its on-chain index and deprecation flag. +pub fn get_all_proxy_type_infos() -> Vec { + (0u8..=u8::MAX) + .filter_map(|index| { + ProxyType::try_from(index) + .ok() + .map(|proxy_type| ProxyTypeInfo { + name: format!("{:?}", proxy_type).into_bytes(), + index, + deprecated: proxy_type.is_deprecated(), + }) + }) + .collect() +} + +/// Filter metadata for the requested proxy types (all of them when `None`). +pub fn get_proxy_filters(proxy_types: Option>) -> Vec { + (0u8..=u8::MAX) + .filter_map(|index| { + ProxyType::try_from(index) + .ok() + .map(|proxy_type| (index, proxy_type)) + }) + .filter(|(index, _)| { + proxy_types + .as_ref() + .is_none_or(|selected| selected.contains(index)) + }) + .map(|(index, proxy_type)| ProxyFilterInfo { + proxy_type: index, + name: format!("{:?}", proxy_type).into_bytes(), + deprecated: proxy_type.is_deprecated(), + filter_mode: proxy_filter_mode(proxy_type), + }) + .collect() +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use super::*; + use alloc::{ + collections::BTreeSet, + string::{String, ToString}, + vec, + }; + use frame_support::traits::GetCallMetadata; + use subtensor_runtime_common::CallInfo; + + fn call_name(info: &CallInfo) -> String { + format!( + "{}::{}", + String::from_utf8_lossy(&info.pallet_name), + String::from_utf8_lossy(&info.call_name) + ) + } + + /// All `pallet::call` names in the runtime, straight from `RuntimeCall` + /// metadata. + fn all_runtime_calls() -> BTreeSet { + RuntimeCall::get_module_names() + .iter() + .flat_map(|module| { + RuntimeCall::get_call_names(module) + .iter() + .map(move |call| format!("{}::{}", module, call)) + }) + .collect() + } + + fn group_calls() -> BTreeSet { + G::call_infos().iter().map(call_name).collect() + } + + /// The set of calls a proxy type allows, taken from its metadata view. + fn allowed_calls(proxy_type: ProxyType) -> BTreeSet { + match proxy_filter_mode(proxy_type) { + FilterMode::AllowAll => all_runtime_calls(), + FilterMode::Allow(infos) => infos.iter().map(call_name).collect(), + } + } + + fn expected(calls: &[&str]) -> BTreeSet { + calls.iter().map(|c| c.to_string()).collect() + } + + #[test] + fn any_allows_everything_and_deprecated_allow_nothing() { + assert_eq!(allowed_calls(ProxyType::Any), all_runtime_calls()); + for deprecated in [ + ProxyType::Triumvirate, + ProxyType::Senate, + ProxyType::Governance, + ProxyType::RootWeights, + ] { + assert!(allowed_calls(deprecated).is_empty()); + } + } + + // Broad proxies are specified subtractively here (all calls minus a few + // denied groups) and checked against the additive composition in the filter. + // Because the inventory groups partition every runtime call, the two must + // agree exactly; a missing or extra group in the filter shows up as a diff. + #[test] + fn non_transfer_is_everything_but_transfers_and_coldkey_swaps() { + let denied = &(&group_calls::() + | &group_calls::()) + | &(&group_calls::() | &group_calls::()); + assert_eq!( + allowed_calls(ProxyType::NonTransfer), + &all_runtime_calls() - &denied + ); + } + + #[test] + fn non_fungible_is_everything_but_value_movement_and_key_swaps() { + let denied = &(&(&group_calls::() + | &group_calls::()) + | &(&group_calls::() | &group_calls::())) + | &(&(&group_calls::() + | &group_calls::()) + | &(&group_calls::() | &group_calls::())); + assert_eq!( + allowed_calls(ProxyType::NonFungible), + &all_runtime_calls() - &denied + ); + } + + #[test] + fn non_critical_is_everything_but_sudo_and_critical_ops() { + let denied = &(&(&group_calls::() | &group_calls::()) + | &(&group_calls::() | &group_calls::())) + | &group_calls::(); + assert_eq!( + allowed_calls(ProxyType::NonCritical), + &all_runtime_calls() - &denied + ); + } + + #[test] + fn owner_allows_only_owner_settable_config() { + let owner = allowed_calls(ProxyType::Owner); + // Owner-settable subnet params + subnet identity. + assert!(owner.contains("AdminUtils::sudo_set_serving_rate_limit")); + assert!(owner.contains("AdminUtils::sudo_set_max_difficulty")); + assert!(owner.contains("SubtensorModule::set_subnet_identity")); + // Root-only admin is not owner-settable (gated by `ensure_root`). + assert!(!owner.contains("AdminUtils::sudo_set_tempo")); + assert!(!owner.contains("AdminUtils::sudo_set_kappa")); + assert!(!owner.contains("AdminUtils::sudo_set_total_issuance")); + assert!(!owner.contains("AdminUtils::swap_authorities")); + // Never owner-key rotation. + assert!(!owner.contains("AdminUtils::sudo_set_sn_owner_hotkey")); + // Exactly subnet identity plus the owner-settable management config. + let expected = + &group_calls::() | &group_calls::(); + assert_eq!(owner, expected); + } + + #[test] + fn subnet_lease_boundaries() { + let lease = allowed_calls(ProxyType::SubnetLeaseBeneficiary); + // Can activate and tune the subnet's owner-settable params... + assert!(lease.contains("SubtensorModule::start_call")); + assert!(lease.contains("SubtensorModule::set_subnet_identity")); + assert!(lease.contains("AdminUtils::sudo_set_serving_rate_limit")); + // ...but not root-only params, owner keys, authorities, or lease teardown. + assert!(!lease.contains("AdminUtils::sudo_set_kappa")); + assert!(!lease.contains("AdminUtils::sudo_set_total_issuance")); + assert!(!lease.contains("AdminUtils::sudo_set_sn_owner_hotkey")); + assert!(!lease.contains("AdminUtils::swap_authorities")); + assert!(!lease.contains("SubtensorModule::terminate_lease")); + } + + #[test] + fn narrow_proxies_have_exact_allow_lists() { + assert_eq!( + allowed_calls(ProxyType::Transfer), + expected(&[ + "Balances::transfer_keep_alive", + "Balances::transfer_allow_death", + "Balances::transfer_all", + "SubtensorModule::transfer_stake", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::SmallTransfer), + expected(&[ + "Balances::transfer_keep_alive", + "Balances::transfer_allow_death", + "SubtensorModule::transfer_stake", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::Staking), + expected(&[ + "SubtensorModule::add_stake", + "SubtensorModule::add_stake_limit", + "SubtensorModule::remove_stake", + "SubtensorModule::remove_stake_limit", + "SubtensorModule::remove_stake_full_limit", + "SubtensorModule::unstake_all", + "SubtensorModule::unstake_all_alpha", + "SubtensorModule::move_stake", + "SubtensorModule::swap_stake", + "SubtensorModule::swap_stake_limit", + "SubtensorModule::set_root_claim_type", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::Registration), + expected(&[ + "SubtensorModule::register", + "SubtensorModule::register_limit", + "SubtensorModule::burned_register", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::ChildKeys), + expected(&[ + "SubtensorModule::set_children", + "SubtensorModule::set_childkey_take", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::SwapHotkey), + expected(&[ + "SubtensorModule::swap_hotkey", + "SubtensorModule::swap_hotkey_v2", + ]) + ); + assert_eq!( + allowed_calls(ProxyType::RootClaim), + expected(&["SubtensorModule::claim_root"]) + ); + assert_eq!( + allowed_calls(ProxyType::SudoUncheckedSetCode), + expected(&["Sudo::sudo_unchecked_weight"]) + ); + } + + // The newer calls that leaked through `main`'s denylists must stay denied + // for every broad proxy. + #[test] + fn tightened_denylist_leaks_stay_denied() { + for proxy_type in [ + ProxyType::NonTransfer, + ProxyType::NonFungible, + ProxyType::NonCritical, + ] { + let allowed = allowed_calls(proxy_type); + assert!(!allowed.contains("SubtensorModule::reset_coldkey_swap")); + assert!(!allowed.contains("SubtensorModule::swap_coldkey")); + assert!(!allowed.contains("SubtensorModule::schedule_swap_coldkey")); + } + // `root_dissolve_network` leaked into NonCritical specifically. + assert!( + !allowed_calls(ProxyType::NonCritical) + .contains("SubtensorModule::root_dissolve_network") + ); + } + + // The SmallTransfer / SudoUncheckedSetCode metadata must carry their + // amount / nested-call constraints. + #[test] + fn conditional_proxies_expose_constraints() { + use subtensor_runtime_common::CallConstraint; + + let small = match proxy_filter_mode(ProxyType::SmallTransfer) { + FilterMode::Allow(infos) => infos, + FilterMode::AllowAll => vec![], + }; + assert!( + small + .iter() + .all(|info| matches!(info.constraint, Some(CallConstraint::ParamLessThan { .. }))) + ); + + let set_code = match proxy_filter_mode(ProxyType::SudoUncheckedSetCode) { + FilterMode::Allow(infos) => infos, + FilterMode::AllowAll => vec![], + }; + assert!(set_code.iter().any(|info| matches!( + &info.constraint, + Some(CallConstraint::NestedCallMustBe { pallet_name, call_name, .. }) + if pallet_name == b"System" && call_name == b"set_code" + ))); + } + + // The name-based golden tests above don't exercise the amount / nested-call + // predicates, so check them directly through the filter. + #[test] + fn small_transfer_enforces_amount_limits() { + use frame_system::Call as SystemCall; + use pallet_balances::Call as BalancesCall; + use pallet_subtensor::Call as SubtensorCall; + use subtensor_runtime_common::{ + AccountId, AlphaBalance, NetUid, SMALL_ALPHA_TRANSFER_LIMIT, SMALL_TRANSFER_LIMIT, + TaoBalance, + }; + + let dest = AccountId::new([2u8; 32]); + + let balance_transfer = |value: TaoBalance| { + RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: dest.clone().into(), + value, + }) + }; + let stake_transfer = |alpha_amount: AlphaBalance| { + RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake { + destination_coldkey: dest.clone(), + hotkey: dest.clone(), + origin_netuid: NetUid::from(1), + destination_netuid: NetUid::from(1), + alpha_amount, + }) + }; + + // Strictly-below the limit is allowed; at the limit is denied. + assert!(proxy_type_filter( + &ProxyType::SmallTransfer, + &balance_transfer(TaoBalance::from(1)) + )); + assert!(!proxy_type_filter( + &ProxyType::SmallTransfer, + &balance_transfer(SMALL_TRANSFER_LIMIT) + )); + assert!(proxy_type_filter( + &ProxyType::SmallTransfer, + &stake_transfer(AlphaBalance::from(1)) + )); + assert!(!proxy_type_filter( + &ProxyType::SmallTransfer, + &stake_transfer(SMALL_ALPHA_TRANSFER_LIMIT) + )); + + // A non-transfer call is never a small transfer. + let remark = RuntimeCall::System(SystemCall::remark { remark: vec![] }); + assert!(!proxy_type_filter(&ProxyType::SmallTransfer, &remark)); + + // `Transfer` is unconditional: the at-limit amount still passes. + assert!(proxy_type_filter( + &ProxyType::Transfer, + &balance_transfer(SMALL_TRANSFER_LIMIT) + )); + } + + #[test] + fn sudo_unchecked_set_code_only_matches_set_code() { + use alloc::boxed::Box; + use frame_support::weights::Weight; + use frame_system::Call as SystemCall; + use pallet_sudo::Call as SudoCall; + + let unchecked = |inner: RuntimeCall| { + RuntimeCall::Sudo(SudoCall::sudo_unchecked_weight { + call: Box::new(inner), + weight: Weight::zero(), + }) + }; + let set_code = RuntimeCall::System(SystemCall::set_code { code: vec![] }); + let remark = RuntimeCall::System(SystemCall::remark { remark: vec![] }); + + // Allowed only when wrapping `System::set_code`. + assert!(proxy_type_filter( + &ProxyType::SudoUncheckedSetCode, + &unchecked(set_code.clone()) + )); + assert!(!proxy_type_filter( + &ProxyType::SudoUncheckedSetCode, + &unchecked(remark) + )); + // `Sudo::sudo` (checked) never matches, even wrapping set_code. + let checked = RuntimeCall::Sudo(SudoCall::sudo { + call: Box::new(set_code), + }); + assert!(!proxy_type_filter( + &ProxyType::SudoUncheckedSetCode, + &checked + )); + } +} diff --git a/runtime/tests/pallet_proxy.rs b/runtime/tests/pallet_proxy.rs deleted file mode 100644 index 481c17b53d..0000000000 --- a/runtime/tests/pallet_proxy.rs +++ /dev/null @@ -1,715 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use frame_support::{assert_ok, traits::InstanceFilter}; -use node_subtensor_runtime::{ - BalancesCall, BuildStorage, Proxy, Runtime, RuntimeCall, RuntimeEvent, RuntimeGenesisConfig, - RuntimeOrigin, SubtensorModule, System, SystemCall, get_all_proxy_filters, - get_all_proxy_type_infos, -}; -use pallet_subtensor_proxy as pallet_proxy; -use subtensor_runtime_common::{ - AccountId, CallCondition, FilterMode, NetUid, ProxyType, SMALL_ALPHA_TRANSFER_LIMIT, - SMALL_TRANSFER_LIMIT, TaoBalance, -}; - -const ACCOUNT: [u8; 32] = [1_u8; 32]; -const DELEGATE: [u8; 32] = [2_u8; 32]; -const OTHER_ACCOUNT: [u8; 32] = [3_u8; 32]; - -type SystemError = frame_system::Error; - -pub fn new_test_ext() -> sp_io::TestExternalities { - sp_tracing::try_init_simple(); - let amount = TaoBalance::from(1_000_000_000_000_u64); - let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { - balances: pallet_balances::GenesisConfig { - balances: vec![ - (AccountId::from(ACCOUNT), amount), - (AccountId::from(DELEGATE), amount), - (AccountId::from(OTHER_ACCOUNT), amount), - ], - dev_accounts: None, - }, - ..Default::default() - } - .build_storage() - .unwrap() - .into(); - ext.execute_with(|| System::set_block_number(1)); - ext -} - -// transfer call -fn call_transfer() -> RuntimeCall { - let value = TaoBalance::from(100); - RuntimeCall::Balances(BalancesCall::transfer_allow_death { - dest: AccountId::from(OTHER_ACCOUNT).into(), - value, - }) -} - -// remark call -fn call_remark() -> RuntimeCall { - let remark = vec![1, 2, 3]; - RuntimeCall::System(SystemCall::remark { remark }) -} - -// owner call -fn call_owner_util() -> RuntimeCall { - let netuid = NetUid::from(1); - let serving_rate_limit = 2; - RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_serving_rate_limit { - netuid, - serving_rate_limit, - }) -} - -// sn owner hotkey call -fn call_sn_owner_hotkey() -> RuntimeCall { - let netuid = NetUid::from(1); - RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_sn_owner_hotkey { - netuid, - hotkey: AccountId::from(ACCOUNT).into(), - }) -} - -// set subnet identity call -fn call_set_subnet_identity() -> RuntimeCall { - let netuid = NetUid::from(1); - RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_subnet_identity { - netuid, - subnet_name: vec![], - github_repo: vec![], - subnet_contact: vec![], - subnet_url: vec![], - discord: vec![], - description: vec![], - logo_url: vec![], - additional: vec![], - }) -} - -// update symbol call -fn call_update_symbol() -> RuntimeCall { - let netuid = NetUid::from(1); - RuntimeCall::SubtensorModule(pallet_subtensor::Call::update_symbol { - netuid, - symbol: vec![], - }) -} - -// critical call for Subtensor -fn call_root_register() -> RuntimeCall { - RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: AccountId::from(ACCOUNT), - }) -} - -// staking call -fn call_add_stake() -> RuntimeCall { - let netuid = NetUid::from(1); - let amount_staked = 100; - RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { - hotkey: AccountId::from(DELEGATE), - netuid, - amount_staked: amount_staked.into(), - }) -} - -// register call, account as hotkey, delegate as coldkey -fn call_register() -> RuntimeCall { - let block_number: u64 = 1; - let netuid = NetUid::from(2); - - // lower diff first - SubtensorModule::set_difficulty(netuid, 100); - - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - 0, - &AccountId::from(ACCOUNT), - ); - - RuntimeCall::SubtensorModule(pallet_subtensor::Call::register { - netuid, - block_number, - nonce, - work: work.clone(), - hotkey: AccountId::from(ACCOUNT), - coldkey: AccountId::from(DELEGATE), - }) -} - -fn verify_call_with_proxy_type(proxy_type: &ProxyType, call: &RuntimeCall) { - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(AccountId::from(DELEGATE)), - AccountId::from(ACCOUNT).into(), - None, - Box::new(call.clone()), - )); - - let filtered_event: RuntimeEvent = pallet_proxy::Event::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(); - - // check if the filter works by checking the last event - // filtered if the last event is SystemError::CallFiltered - // not filtered if the last event is proxy executed done or any error from proxy call - if proxy_type.filter(call) { - let last_event = System::events().last().unwrap().event.clone(); - assert_ne!(last_event, filtered_event); - } else { - System::assert_last_event(filtered_event); - } -} - -#[test] -fn test_proxy_pallet() { - let proxy_types = [ - ProxyType::Any, - ProxyType::Owner, - ProxyType::NonCritical, - ProxyType::NonTransfer, - ProxyType::NonFungible, - ProxyType::Staking, - ProxyType::Registration, - ]; - - let calls = [ - call_transfer, - call_remark, - call_owner_util, - call_root_register, - call_add_stake, - call_register, - ]; - - for call in calls.iter() { - for proxy_type in proxy_types.iter() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(AccountId::from(ACCOUNT)), - AccountId::from(DELEGATE).into(), - *proxy_type, - 0 - )); - - verify_call_with_proxy_type(proxy_type, &call()); - }); - } - } -} - -#[test] -fn test_non_transfer_cannot_transfer() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(AccountId::from(ACCOUNT)), - AccountId::from(DELEGATE).into(), - ProxyType::NonTransfer, - 0 - )); - - let call = call_transfer(); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(AccountId::from(DELEGATE)), - AccountId::from(ACCOUNT).into(), - None, - Box::new(call.clone()), - )); - - System::assert_last_event( - pallet_proxy::Event::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - }); -} - -#[test] -fn test_owner_type_cannot_set_sn_owner_hotkey() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(AccountId::from(ACCOUNT)), - AccountId::from(DELEGATE).into(), - ProxyType::Owner, - 0 - )); - - let call = call_sn_owner_hotkey(); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(AccountId::from(DELEGATE)), - AccountId::from(ACCOUNT).into(), - None, - Box::new(call.clone()), - )); - - System::assert_last_event( - pallet_proxy::Event::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - }); -} - -#[test] -fn test_owner_type_can_set_subnet_identity_and_update_symbol() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(AccountId::from(ACCOUNT)), - AccountId::from(DELEGATE).into(), - ProxyType::Owner, - 0 - )); - - verify_call_with_proxy_type(&ProxyType::Owner, &call_set_subnet_identity()); - verify_call_with_proxy_type(&ProxyType::Owner, &call_update_symbol()); - }); -} - -// --- ProxyFilter RuntimeAPI sync tests --- - -fn call_small_transfer() -> RuntimeCall { - let value = TaoBalance::from(100u64); - RuntimeCall::Balances(BalancesCall::transfer_allow_death { - dest: AccountId::new([2u8; 32]).into(), - value, - }) -} - -fn call_large_transfer() -> RuntimeCall { - let value = TaoBalance::from(1_000_000_000u64); - RuntimeCall::Balances(BalancesCall::transfer_allow_death { - dest: AccountId::new([2u8; 32]).into(), - value, - }) -} - -fn call_sudo_remark() -> RuntimeCall { - RuntimeCall::Sudo(pallet_sudo::Call::sudo { - call: Box::new(RuntimeCall::System(SystemCall::remark { - remark: vec![1, 2, 3], - })), - }) -} - -#[test] -fn proxy_filter_api_behavior_matches_instance_filter() { - new_test_ext().execute_with(|| { - // Part 1: Ground truth — verify InstanceFilter::filter() returns expected results - // based on reading the InstanceFilter source code - - // Any allows everything - assert!(ProxyType::Any.filter(&call_transfer())); - assert!(ProxyType::Any.filter(&call_remark())); - assert!(ProxyType::Any.filter(&call_add_stake())); - - // NonTransfer denies Balances and specific transfer calls - assert!(!ProxyType::NonTransfer.filter(&call_transfer())); - assert!(ProxyType::NonTransfer.filter(&call_remark())); - assert!(ProxyType::NonTransfer.filter(&call_add_stake())); - - // NonFungible denies Balances and staking-related calls - assert!(!ProxyType::NonFungible.filter(&call_transfer())); - assert!(!ProxyType::NonFungible.filter(&call_add_stake())); - assert!(ProxyType::NonFungible.filter(&call_remark())); - - // Transfer allows only specific transfer calls - assert!(ProxyType::Transfer.filter(&call_transfer())); - assert!(!ProxyType::Transfer.filter(&call_remark())); - assert!(!ProxyType::Transfer.filter(&call_add_stake())); - - // SmallTransfer allows transfers below limit - assert!(ProxyType::SmallTransfer.filter(&call_small_transfer())); - assert!(!ProxyType::SmallTransfer.filter(&call_large_transfer())); - assert!(!ProxyType::SmallTransfer.filter(&call_remark())); - - // Owner allows AdminUtils and specific SubtensorModule calls - assert!(ProxyType::Owner.filter(&call_owner_util())); - assert!(ProxyType::Owner.filter(&call_set_subnet_identity())); - assert!(ProxyType::Owner.filter(&call_update_symbol())); - assert!(!ProxyType::Owner.filter(&call_sn_owner_hotkey())); - assert!(!ProxyType::Owner.filter(&call_remark())); - - // NonCritical denies critical calls - assert!(!ProxyType::NonCritical.filter(&call_root_register())); - assert!(ProxyType::NonCritical.filter(&call_remark())); - assert!(ProxyType::NonCritical.filter(&call_transfer())); - assert!(!ProxyType::NonCritical.filter(&call_sudo_remark())); - - // Staking allows staking calls only - assert!(ProxyType::Staking.filter(&call_add_stake())); - assert!(!ProxyType::Staking.filter(&call_transfer())); - assert!(!ProxyType::Staking.filter(&call_remark())); - - // Registration allows registration calls only - assert!(ProxyType::Registration.filter(&call_register())); - assert!(!ProxyType::Registration.filter(&call_remark())); - - // Deprecated types deny everything - assert!(!ProxyType::Triumvirate.filter(&call_remark())); - assert!(!ProxyType::Senate.filter(&call_remark())); - assert!(!ProxyType::Governance.filter(&call_remark())); - assert!(!ProxyType::RootWeights.filter(&call_remark())); - }); -} - -#[test] -fn proxy_filter_api_structural_validation() { - new_test_ext().execute_with(|| { - let type_infos = get_all_proxy_type_infos(); - let filters = get_all_proxy_filters(); - - // Part 2: Structural validation of API output - - // Verify total count equals number of ProxyType variants (18) - assert_eq!(type_infos.len(), 18); - assert_eq!(filters.len(), 18); - - // Verify ProxyTypeInfo correctness - let any_info = type_infos.iter().find(|t| t.index == 0).unwrap(); - assert_eq!(any_info.name, b"Any"); - assert!(!any_info.deprecated); - - let triumvirate_info = type_infos.iter().find(|t| t.index == 6).unwrap(); - assert_eq!(triumvirate_info.name, b"Triumvirate"); - assert!(triumvirate_info.deprecated); - - let senate_info = type_infos.iter().find(|t| t.index == 4).unwrap(); - assert_eq!(senate_info.name, b"Senate"); - assert!(senate_info.deprecated); - - // Verify deprecated ProxyTypes have DenyAll filter mode - for filter in &filters { - let pt = ProxyType::try_from(filter.proxy_type).unwrap(); - if pt.is_deprecated() { - assert_eq!( - filter.filter_mode, - FilterMode::DenyAll, - "Deprecated ProxyType {:?} should have DenyAll filter mode", - pt - ); - assert!(filter.calls.is_empty()); - } - } - - // Verify Any has AllowAll - let any_filter = filters.iter().find(|f| f.proxy_type == 0).unwrap(); - assert_eq!(any_filter.filter_mode, FilterMode::AllowAll); - assert!(any_filter.calls.is_empty()); - - // Verify NonTransfer has Deny mode with Balances wildcard - let non_transfer = filters.iter().find(|f| f.proxy_type == 3).unwrap(); - assert_eq!(non_transfer.filter_mode, FilterMode::Deny); - assert!( - non_transfer - .calls - .iter() - .any(|c| c.call_name.is_none() && c.pallet_name == b"Balances") - ); - - // Verify Owner has Allow mode with exceptions - let owner = filters.iter().find(|f| f.proxy_type == 1).unwrap(); - assert_eq!(owner.filter_mode, FilterMode::Allow); - assert!( - owner - .calls - .iter() - .any(|c| c.call_name.is_none() && c.pallet_name == b"AdminUtils") - ); - assert!(!owner.exceptions.is_empty()); - assert!( - owner - .exceptions - .iter() - .any(|c| c.call_name.as_deref() == Some(b"sudo_set_sn_owner_hotkey".as_slice())) - ); - - // Verify SmallTransfer has conditions with correct limits - let small_transfer = filters.iter().find(|f| f.proxy_type == 11).unwrap(); - assert_eq!(small_transfer.filter_mode, FilterMode::Allow); - let has_tao_limit = small_transfer.calls.iter().any(|c| { - matches!( - &c.condition, - Some(CallCondition::ParamLessThan { limit, .. }) - if *limit == Into::::into(SMALL_TRANSFER_LIMIT) as u128 - ) - }); - assert!( - has_tao_limit, - "SmallTransfer should have TAO limit condition" - ); - - let has_alpha_limit = small_transfer.calls.iter().any(|c| { - matches!( - &c.condition, - Some(CallCondition::ParamLessThan { param_name, limit, .. }) - if param_name == b"alpha_amount" - && *limit == Into::::into(SMALL_ALPHA_TRANSFER_LIMIT) as u128 - ) - }); - assert!( - has_alpha_limit, - "SmallTransfer should have Alpha limit condition" - ); - - // Verify NonCritical has Deny mode with Sudo wildcard - let non_critical = filters.iter().find(|f| f.proxy_type == 2).unwrap(); - assert_eq!(non_critical.filter_mode, FilterMode::Deny); - assert!( - non_critical - .calls - .iter() - .any(|c| c.call_name.is_none() && c.pallet_name == b"Sudo") - ); - - // Verify SudoUncheckedSetCode has NestedCallMustBe condition - let sudo_set_code = filters.iter().find(|f| f.proxy_type == 14).unwrap(); - assert_eq!(sudo_set_code.filter_mode, FilterMode::Allow); - let has_nested_condition = sudo_set_code.calls.iter().any(|c| { - matches!( - &c.condition, - Some(CallCondition::NestedCallMustBe { - pallet_name, - call_name, - }) if pallet_name == b"System" && call_name == b"set_code" - ) - }); - assert!( - has_nested_condition, - "SudoUncheckedSetCode should have NestedCallMustBe condition" - ); - - // Verify pallet indices are correct (from construct_runtime!) - // Balances = 5, SubtensorModule = 7, Sudo = 12, AdminUtils = 19 - let balances_wildcard = non_transfer - .calls - .iter() - .find(|c| c.call_name.is_none() && c.pallet_name == b"Balances") - .unwrap(); - assert_eq!(balances_wildcard.pallet_index, 5); - - let sudo_wildcard = non_critical - .calls - .iter() - .find(|c| c.call_name.is_none() && c.pallet_name == b"Sudo") - .unwrap(); - assert_eq!(sudo_wildcard.pallet_index, 12); - - let admin_wildcard = owner - .calls - .iter() - .find(|c| c.call_name.is_none() && c.pallet_name == b"AdminUtils") - .unwrap(); - assert_eq!(admin_wildcard.pallet_index, 19); - }); -} - -#[test] -#[allow(clippy::indexing_slicing)] -fn proxy_filter_api_cross_check_filter_behavior() { - new_test_ext().execute_with(|| { - let filters = get_all_proxy_filters(); - - // Part 3: For each non-wildcard, non-conditional call in the API output, - // verify that InstanceFilter::filter() agrees with the declared filter_mode - - // Build a set of test calls indexed by (pallet_index, call_index) - let test_calls: Vec<(u8, u8, RuntimeCall)> = vec![ - // Balances calls - { - let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { - dest: AccountId::new([0u8; 32]).into(), - value: Default::default(), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - // SubtensorModule calls - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { - hotkey: AccountId::new([0u8; 32]), - netuid: Default::default(), - amount_staked: Default::default(), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: AccountId::new([0u8; 32]), - netuid: Default::default(), - amount_unstaked: Default::default(), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::burned_register { - netuid: Default::default(), - hotkey: AccountId::new([0u8; 32]), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { - hotkey: AccountId::new([0u8; 32]), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: AccountId::new([0u8; 32]), - new_hotkey: AccountId::new([0u8; 32]), - netuid: Default::default(), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_children { - hotkey: AccountId::new([0u8; 32]), - netuid: Default::default(), - children: Default::default(), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - // AdminUtils calls - { - let call = RuntimeCall::AdminUtils( - pallet_admin_utils::Call::sudo_set_serving_rate_limit { - netuid: Default::default(), - serving_rate_limit: Default::default(), - }, - ); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - { - let call = - RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_sn_owner_hotkey { - netuid: Default::default(), - hotkey: AccountId::new([0u8; 32]), - }); - let encoded = codec::Encode::encode(&call); - (encoded[0], encoded[1], call) - }, - ]; - - for filter_info in &filters { - let proxy_type = ProxyType::try_from(filter_info.proxy_type).unwrap(); - - match filter_info.filter_mode { - FilterMode::AllowAll => { - for (_, _, call) in &test_calls { - assert!( - proxy_type.filter(call), - "AllowAll ProxyType {:?} should allow all calls", - proxy_type - ); - } - } - FilterMode::DenyAll => { - for (_, _, call) in &test_calls { - assert!( - !proxy_type.filter(call), - "DenyAll ProxyType {:?} should deny all calls", - proxy_type - ); - } - } - FilterMode::Allow => { - for call_info in &filter_info.calls { - if call_info.call_name.is_none() || call_info.condition.is_some() { - continue; - } - if let Some((_, _, call)) = test_calls.iter().find(|(pi, ci, _)| { - *pi == call_info.pallet_index && Some(*ci) == call_info.call_index - }) { - assert!( - proxy_type.filter(call), - "Allow-mode ProxyType {:?} should allow call {:?}", - proxy_type, - call_info - .call_name - .as_ref() - .map(|n| core::str::from_utf8(n).unwrap_or("?")) - .unwrap_or("*") - ); - } - } - // Verify exceptions are denied - for exc_info in &filter_info.exceptions { - if let Some((_, _, call)) = test_calls.iter().find(|(pi, ci, _)| { - *pi == exc_info.pallet_index && Some(*ci) == exc_info.call_index - }) { - assert!( - !proxy_type.filter(call), - "ProxyType {:?} should deny exception {:?}", - proxy_type, - exc_info - .call_name - .as_ref() - .map(|n| core::str::from_utf8(n).unwrap_or("?")) - .unwrap_or("*") - ); - } - } - } - FilterMode::Deny => { - for call_info in &filter_info.calls { - if call_info.call_name.is_none() || call_info.condition.is_some() { - continue; - } - if let Some((_, _, call)) = test_calls.iter().find(|(pi, ci, _)| { - *pi == call_info.pallet_index && Some(*ci) == call_info.call_index - }) { - assert!( - !proxy_type.filter(call), - "Deny-mode ProxyType {:?} should deny call {:?}", - proxy_type, - call_info - .call_name - .as_ref() - .map(|n| core::str::from_utf8(n).unwrap_or("?")) - .unwrap_or("*") - ); - } - } - } - } - } - }); -} - -#[test] -fn proxy_filter_api_deprecated_consistency() { - new_test_ext().execute_with(|| { - let type_infos = get_all_proxy_type_infos(); - - for pt_info in &type_infos { - let pt = ProxyType::try_from(pt_info.index).unwrap(); - if pt_info.deprecated { - assert!( - pt.is_deprecated(), - "ProxyTypeInfo reports {:?} as deprecated but is_deprecated() disagrees", - pt - ); - assert!( - !pt.filter(&call_remark()), - "Deprecated ProxyType {:?} should deny all calls", - pt - ); - assert!(!pt.filter(&call_transfer())); - assert!(!pt.filter(&call_add_stake())); - } - } - }); -} diff --git a/support/macros/Cargo.toml b/support/macros/Cargo.toml index 4ba891a6ea..7d8f3dad55 100644 --- a/support/macros/Cargo.toml +++ b/support/macros/Cargo.toml @@ -16,6 +16,7 @@ syn = { workspace = true, features = [ "full", "visit-mut", "visit", + "clone-impls", "extra-traits", "parsing", "printing", diff --git a/support/macros/src/call_filter_group.rs b/support/macros/src/call_filter_group.rs new file mode 100644 index 0000000000..37bbf994cc --- /dev/null +++ b/support/macros/src/call_filter_group.rs @@ -0,0 +1,398 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + Error, Expr, Ident, Path, Result, Token, bracketed, parenthesized, + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +/// Parsed input for one call filter group. +pub struct CallFilterGroupInput { + group: Ident, + rules: Punctuated, +} + +/// One allowed call, optionally guarded by a simple condition. +struct CallRule { + target: RuntimeCallRef, + condition: Option, +} + +/// Reference to `RuntimeCall::Variant(pallet::Call::method)`. +#[derive(Clone)] +struct RuntimeCallRef { + variant: Ident, + call_enum: Path, + call: Ident, +} + +/// Constraints supported by the generated filter and metadata. +enum CallConstraint { + ParamLessThan { + field: Ident, + limit: Expr, + }, + NestedCallMustBe { + field: Ident, + target: RuntimeCallRef, + }, +} + +impl Parse for CallFilterGroupInput { + fn parse(input: ParseStream) -> Result { + let group = input.parse()?; + input.parse::()?; + + let content; + bracketed!(content in input); + let rules = content.parse_terminated(CallRule::parse, Token![,])?; + + Ok(Self { group, rules }) + } +} + +impl Parse for CallRule { + fn parse(input: ParseStream) -> Result { + let target = input.parse()?; + let condition = if input.peek(Token![where]) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }; + + Ok(Self { target, condition }) + } +} + +impl Parse for RuntimeCallRef { + fn parse(input: ParseStream) -> Result { + let runtime_call = input.parse::()?; + if runtime_call != "RuntimeCall" { + return Err(Error::new_spanned( + runtime_call, + "expected RuntimeCall::Variant(call_enum::Call::method)", + )); + } + + input.parse::()?; + let variant = input.parse()?; + + let content; + parenthesized!(content in input); + let call_path = content.parse()?; + if !content.is_empty() { + return Err(content.error("expected a single call path")); + } + + let (call_enum, call) = split_call_path(call_path)?; + Ok(Self { + variant, + call_enum, + call, + }) + } +} + +impl Parse for CallConstraint { + fn parse(input: ParseStream) -> Result { + let field = input.parse::()?; + + if field == "nested" { + let content; + parenthesized!(content in input); + let nested_field = content.parse()?; + if !content.is_empty() { + return Err(content.error("expected a single nested call field")); + } + + input.parse::()?; + let target = input.parse()?; + + return Ok(Self::NestedCallMustBe { + field: nested_field, + target, + }); + } + + input.parse::()?; + let limit = input.parse()?; + Ok(Self::ParamLessThan { field, limit }) + } +} + +/// Generate both impls for a filter group: +/// - `Contains` for execution-time filtering +/// - `CallFilterMetadata` for runtime API discovery +pub fn generate(input: CallFilterGroupInput) -> Result { + let group = input.group; + let contains_rules = input.rules.iter().map(generate_contains_rule); + let call_infos = input.rules.iter().map(generate_call_info); + + Ok(quote! { + pub(super) struct #group; + + impl frame_support::traits::Contains for #group { + fn contains(call: &crate::RuntimeCall) -> bool { + false #( || #contains_rules )* + } + } + + impl subtensor_runtime_common::CallFilterMetadata for #group { + fn call_infos() -> ::alloc::vec::Vec { + ::alloc::vec![#(#call_infos),*] + } + } + }) +} + +/// Build the executable predicate for one allowed call. +fn generate_contains_rule(rule: &CallRule) -> TokenStream2 { + match &rule.condition { + None => { + let pattern = call_pattern(&rule.target, None); + quote! { matches!(call, #pattern) } + } + Some(CallConstraint::ParamLessThan { field, limit }) => { + let pattern = call_pattern(&rule.target, Some(field)); + quote! { + match call { + #pattern => *#field < #limit, + _ => false, + } + } + } + Some(CallConstraint::NestedCallMustBe { field, target }) => { + let source_pattern = call_pattern(&rule.target, Some(field)); + let target_pattern = call_pattern(target, None); + quote! { + match call { + #source_pattern => matches!(#field.as_ref(), #target_pattern), + _ => false, + } + } + } + } +} + +/// Build the metadata entry for one allowed call. +fn generate_call_info(rule: &CallRule) -> TokenStream2 { + let base = call_info_expr(&rule.target); + + match &rule.condition { + None => base, + Some(CallConstraint::ParamLessThan { field, limit }) => quote! {{ + let mut info = #base; + info.constraint = Some(subtensor_runtime_common::CallConstraint::ParamLessThan { + param_name: stringify!(#field).as_bytes().to_vec(), + limit: Into::::into(#limit) as u128, + }); + info + }}, + Some(CallConstraint::NestedCallMustBe { field, target }) => { + let nested = call_info_expr(target); + quote! {{ + let mut info = #base; + let nested = #nested; + info.constraint = Some(subtensor_runtime_common::CallConstraint::NestedCallMustBe { + param_name: stringify!(#field).as_bytes().to_vec(), + pallet_name: nested.pallet_name, + call_name: nested.call_name, + }); + info + }} + } + } +} + +/// Convert a call reference into a `RuntimeCall` match pattern. +fn call_pattern(target: &RuntimeCallRef, field: Option<&Ident>) -> TokenStream2 { + let variant = &target.variant; + let call_enum = &target.call_enum; + let call = &target.call; + + match field { + Some(field) => quote! { + crate::RuntimeCall::#variant(#call_enum::#call { #field, .. }) + }, + None => quote! { + crate::RuntimeCall::#variant(#call_enum::#call { .. }) + }, + } +} + +/// Convert a call reference into a runtime lookup for pallet/call indexes. +fn call_info_expr(target: &RuntimeCallRef) -> TokenStream2 { + let variant = &target.variant; + let call_enum = &target.call_enum; + let call = &target.call; + + quote! { + subtensor_runtime_common::call_info_by_name::< + crate::#variant, + #call_enum, + >(stringify!(#call)) + } +} + +/// Split `pallet::Call::method` into `pallet::Call` and `method`. +fn split_call_path(call_path: Path) -> Result<(Path, Ident)> { + if call_path.segments.len() < 2 { + return Err(Error::new_spanned( + call_path, + "expected a call path like pallet_name::Call::method", + )); + } + + let mut call_enum_segments = Punctuated::new(); + for segment in call_path + .segments + .iter() + .take(call_path.segments.len().saturating_sub(1)) + { + call_enum_segments.push((*segment).clone()); + } + + #[allow(clippy::expect_used)] + let call = call_path + .segments + .last() + .expect("length checked above") + .ident + .clone(); + + Ok(( + Path { + leading_colon: call_path.leading_colon, + segments: call_enum_segments, + }, + call, + )) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::indexing_slicing)] + + use quote::quote; + + use super::*; + + fn generate_tokens(input: TokenStream2) -> String { + let input = syn::parse2::(input).unwrap(); + generate(input).unwrap().to_string() + } + + #[test] + fn parses_group_name_and_call_targets() { + let input = syn::parse2::(quote! { + StakingOperations, [ + RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake), + RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake), + ] + }) + .unwrap(); + + assert_eq!(input.group, "StakingOperations"); + assert_eq!(input.rules.len(), 2); + assert_eq!(input.rules[0].target.variant, "SubtensorModule"); + assert_eq!(input.rules[0].target.call, "add_stake"); + assert!(input.rules[0].condition.is_none()); + } + + #[test] + fn generated_group_implements_contains_and_shared_metadata() { + let generated = generate_tokens(quote! { + StakingOperations, [ + RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake), + ] + }); + + assert!(generated.contains( + "impl frame_support :: traits :: Contains < crate :: RuntimeCall > for StakingOperations" + )); + assert!( + generated.contains( + "impl subtensor_runtime_common :: CallFilterMetadata for StakingOperations" + ) + ); + assert!(generated.contains( + "matches ! (call , crate :: RuntimeCall :: SubtensorModule (pallet_subtensor :: Call :: add_stake" + )); + assert!(generated.contains("subtensor_runtime_common :: call_info_by_name")); + assert!(generated.contains("crate :: SubtensorModule")); + assert!(generated.contains("pallet_subtensor :: Call < crate :: Runtime >")); + assert!(generated.contains("stringify ! (add_stake)")); + } + + #[test] + fn generated_group_supports_param_less_than_condition() { + let generated = generate_tokens(quote! { + SmallTransfers, [ + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death) + where value < SMALL_TRANSFER_LIMIT, + ] + }); + + assert!(generated.contains("* value < SMALL_TRANSFER_LIMIT")); + assert!(generated.contains("subtensor_runtime_common :: CallConstraint :: ParamLessThan")); + assert!(generated.contains("param_name : stringify ! (value)")); + assert!( + generated.contains("limit : Into :: < u64 > :: into (SMALL_TRANSFER_LIMIT) as u128") + ); + } + + #[test] + fn generated_group_supports_nested_call_condition() { + let generated = generate_tokens(quote! { + SudoUncheckedSetCodeOperation, [ + RuntimeCall::Sudo(pallet_sudo::Call::sudo_unchecked_weight) + where nested(call) == RuntimeCall::System(frame_system::Call::set_code), + ] + }); + + assert!(generated.contains( + "matches ! (call . as_ref () , crate :: RuntimeCall :: System (frame_system :: Call :: set_code" + )); + assert!( + generated.contains("subtensor_runtime_common :: CallConstraint :: NestedCallMustBe") + ); + assert!(generated.contains("param_name : stringify ! (call)")); + assert!(generated.contains("pallet_name : nested . pallet_name")); + assert!(generated.contains("call_name : nested . call_name")); + } + + #[test] + fn rejects_non_runtime_call_target() { + let err = match syn::parse2::(quote! { + BadGroup, [ + Call::SubtensorModule(pallet_subtensor::Call::add_stake), + ] + }) { + Ok(_) => panic!("invalid runtime call target should fail to parse"), + Err(err) => err, + }; + + assert!( + err.to_string() + .contains("expected RuntimeCall::Variant(call_enum::Call::method)") + ); + } + + #[test] + fn rejects_call_paths_without_call_name() { + let err = match syn::parse2::(quote! { + BadGroup, [ + RuntimeCall::SubtensorModule(add_stake), + ] + }) { + Ok(_) => panic!("call path without call enum should fail to parse"), + Err(err) => err, + }; + + assert!( + err.to_string() + .contains("expected a call path like pallet_name::Call::method") + ); + } +} diff --git a/support/macros/src/lib.rs b/support/macros/src/lib.rs index 2e19a9fbce..0580e1fd62 100644 --- a/support/macros/src/lib.rs +++ b/support/macros/src/lib.rs @@ -3,12 +3,10 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::{Error, ItemStruct, LitStr, Result, parse2, visit_mut::visit_item_struct_mut}; +mod call_filter_group; mod visitor; use visitor::*; -mod proxy_filter; -use proxy_filter::ProxyFilterInput; - /// Freezes the layout of a struct to the current hash of its fields, ensuring that future /// changes require updating the hash. /// @@ -30,6 +28,18 @@ pub fn freeze_struct(attr: TokenStream, tokens: TokenStream) -> TokenStream { } } +/// Defines a reusable runtime call filter group and derives its metadata from the same allowlist. +/// +/// Example: `call_filter_group!(GroupName, [RuntimeCall::Foobar(pallet_foobar::Call::some_call)]);` +#[proc_macro] +pub fn call_filter_group(input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as call_filter_group::CallFilterGroupInput); + match call_filter_group::generate(parsed) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + fn freeze_struct_impl( attr: impl Into, tokens: impl Into, @@ -72,15 +82,3 @@ fn freeze_struct_impl( } Ok(item) } - -/// Defines proxy filter rules as a single source of truth, generating both the -/// `proxy_type_filter()` function (used by `InstanceFilter::filter()`) and the -/// `get_all_proxy_filters()` function (used by the Runtime API). -/// -/// This ensures the on-chain filtering logic and the off-chain API metadata -/// can never drift apart. -#[proc_macro] -pub fn define_proxy_filters(input: TokenStream) -> TokenStream { - let parsed = syn::parse_macro_input!(input as ProxyFilterInput); - parsed.generate().into() -} diff --git a/support/macros/src/proxy_filter.rs b/support/macros/src/proxy_filter.rs deleted file mode 100644 index dc03db780d..0000000000 --- a/support/macros/src/proxy_filter.rs +++ /dev/null @@ -1,540 +0,0 @@ -//! Proc-macro implementation for `define_proxy_filters!`. -//! -//! This module provides parsing and code generation for a declarative DSL that -//! defines proxy filter rules as a single source of truth. From one definition, -//! it generates: -//! - `proxy_type_filter()` — the runtime filtering function used by `InstanceFilter::filter()` -//! - `get_all_proxy_filters()` — the Runtime API data function returning filter metadata -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{ - Expr, Ident, Result, Token, - parse::{Parse, ParseStream}, -}; - -// ============================================================================ -// DSL AST types -// ============================================================================ - -/// Top-level input: pallets { ... } followed by filter rules -pub struct ProxyFilterInput { - pub pallets: Vec, - pub rules: Vec, -} - -/// Maps a short pallet name to its RuntimeCall variant and module path -/// e.g. `Balances => (Balances, pallet_balances)` -pub struct PalletDef { - pub name: Ident, - pub runtime_variant: Ident, - pub module: Ident, -} - -/// A single filter rule for one ProxyType variant -pub struct FilterRule { - pub proxy_type: Ident, - pub kind: FilterKind, -} - -pub enum FilterKind { - AllowAll, - DenyAll, - Allow { - calls: Vec, - exceptions: Vec, - }, - Deny { - calls: Vec, - }, - AllowConditional { - calls: Vec, - }, - AllowNested { - calls: Vec, - }, -} - -/// Reference to a specific call or wildcard pallet -pub enum CallRef { - Wildcard(Ident), - Specific(Ident, Ident), -} - -/// Conditional call: `Pallet::call where (field) < LIMIT` -pub struct ConditionalCallRef { - pub pallet: Ident, - pub call: Ident, - pub field: Ident, - pub limit: Expr, -} - -/// Nested call: `Pallet::call where nested(field) == TargetPallet::target_call` -pub struct NestedCallRef { - pub pallet: Ident, - pub call: Ident, - pub field: Ident, - pub target_pallet: Ident, - pub target_call: Ident, -} - -// ============================================================================ -// Parsing -// ============================================================================ - -mod kw { - syn::custom_keyword!(pallets); - syn::custom_keyword!(allow_all); - syn::custom_keyword!(deny_all); - syn::custom_keyword!(allow); - syn::custom_keyword!(deny); - syn::custom_keyword!(allow_conditional); - syn::custom_keyword!(allow_nested); - syn::custom_keyword!(except); - syn::custom_keyword!(nested); -} - -impl Parse for ProxyFilterInput { - fn parse(input: ParseStream) -> Result { - input.parse::()?; - let pallet_content; - syn::braced!(pallet_content in input); - let mut pallets = Vec::new(); - while !pallet_content.is_empty() { - pallets.push(pallet_content.parse::()?); - if pallet_content.peek(Token![,]) { - pallet_content.parse::()?; - } - } - - let mut rules = Vec::new(); - while !input.is_empty() { - rules.push(input.parse::()?); - } - - Ok(ProxyFilterInput { pallets, rules }) - } -} - -impl Parse for PalletDef { - fn parse(input: ParseStream) -> Result { - let name: Ident = input.parse()?; - input.parse::]>()?; - let content; - syn::parenthesized!(content in input); - let runtime_variant: Ident = content.parse()?; - content.parse::()?; - let module: Ident = content.parse()?; - Ok(PalletDef { - name, - runtime_variant, - module, - }) - } -} - -impl Parse for FilterRule { - fn parse(input: ParseStream) -> Result { - let proxy_type: Ident = input.parse()?; - input.parse::]>()?; - - let kind = if input.peek(kw::allow_all) { - input.parse::()?; - input.parse::()?; - FilterKind::AllowAll - } else if input.peek(kw::deny_all) { - input.parse::()?; - input.parse::()?; - FilterKind::DenyAll - } else if input.peek(kw::allow_conditional) { - input.parse::()?; - let content; - syn::braced!(content in input); - let calls = parse_conditional_calls(&content)?; - FilterKind::AllowConditional { calls } - } else if input.peek(kw::allow_nested) { - input.parse::()?; - let content; - syn::braced!(content in input); - let calls = parse_nested_calls(&content)?; - FilterKind::AllowNested { calls } - } else if input.peek(kw::allow) { - input.parse::()?; - let content; - syn::braced!(content in input); - let calls = parse_call_refs(&content)?; - - let exceptions = if input.peek(kw::except) { - input.parse::()?; - let exc_content; - syn::braced!(exc_content in input); - parse_call_refs(&exc_content)? - } else { - Vec::new() - }; - - FilterKind::Allow { calls, exceptions } - } else if input.peek(kw::deny) { - input.parse::()?; - let content; - syn::braced!(content in input); - let calls = parse_call_refs(&content)?; - FilterKind::Deny { calls } - } else { - return Err(input.error( - "expected allow_all, deny_all, allow, deny, allow_conditional, or allow_nested", - )); - }; - - // Consume optional trailing semicolon after braced rules - if input.peek(Token![;]) { - input.parse::()?; - } - - Ok(FilterRule { proxy_type, kind }) - } -} - -fn parse_call_refs(input: ParseStream) -> Result> { - let mut calls = Vec::new(); - while !input.is_empty() { - let pallet: Ident = input.parse()?; - input.parse::()?; - if input.peek(Token![*]) { - input.parse::()?; - calls.push(CallRef::Wildcard(pallet)); - } else { - let call: Ident = input.parse()?; - calls.push(CallRef::Specific(pallet, call)); - } - if input.peek(Token![,]) { - input.parse::()?; - } - } - Ok(calls) -} - -fn parse_conditional_calls(input: ParseStream) -> Result> { - let mut calls = Vec::new(); - while !input.is_empty() { - let pallet: Ident = input.parse()?; - input.parse::()?; - let call: Ident = input.parse()?; - // parse: where (field) < LIMIT - input.parse::()?; - let field_content; - syn::parenthesized!(field_content in input); - let field: Ident = field_content.parse()?; - input.parse::()?; - let limit: Expr = input.parse()?; - calls.push(ConditionalCallRef { - pallet, - call, - field, - limit, - }); - if input.peek(Token![,]) { - input.parse::()?; - } - } - Ok(calls) -} - -fn parse_nested_calls(input: ParseStream) -> Result> { - let mut calls = Vec::new(); - while !input.is_empty() { - let pallet: Ident = input.parse()?; - input.parse::()?; - let call: Ident = input.parse()?; - // parse: where nested(field) == TargetPallet::target_call - input.parse::()?; - input.parse::()?; - let field_content; - syn::parenthesized!(field_content in input); - let field: Ident = field_content.parse()?; - input.parse::()?; - let target_pallet: Ident = input.parse()?; - input.parse::()?; - let target_call: Ident = input.parse()?; - calls.push(NestedCallRef { - pallet, - call, - field, - target_pallet, - target_call, - }); - if input.peek(Token![,]) { - input.parse::()?; - } - } - Ok(calls) -} - -// ============================================================================ -// Code generation -// ============================================================================ - -impl ProxyFilterInput { - pub fn generate(self) -> TokenStream2 { - let filter_fn = self.generate_filter_fn(); - let data_fn = self.generate_data_fn(); - - quote! { - #filter_fn - #data_fn - } - } - - fn find_pallet(&self, name: &Ident) -> &PalletDef { - self.pallets - .iter() - .find(|p| p.name == *name) - .unwrap_or_else(|| panic!("Pallet '{}' not found in pallets block", name)) - } - - // ======================================================================== - // filter function generation - // ======================================================================== - - fn generate_filter_fn(&self) -> TokenStream2 { - let arms: Vec = self.rules.iter().map(|rule| { - let pt = &rule.proxy_type; - match &rule.kind { - FilterKind::AllowAll => quote! { - ProxyType::#pt => true, - }, - FilterKind::DenyAll => quote! { - ProxyType::#pt => false, - }, - FilterKind::Allow { calls, exceptions } => { - let patterns = self.call_refs_to_patterns(calls); - if exceptions.is_empty() { - quote! { - ProxyType::#pt => matches!(c, #(#patterns)|*), - } - } else { - let exc_patterns = self.call_refs_to_patterns(exceptions); - quote! { - ProxyType::#pt => { - matches!(c, #(#patterns)|*) && !matches!(c, #(#exc_patterns)|*) - } - } - } - } - FilterKind::Deny { calls } => { - let patterns = self.call_refs_to_patterns(calls); - quote! { - ProxyType::#pt => !matches!(c, #(#patterns)|*), - } - } - FilterKind::AllowConditional { calls } => { - let arms = calls.iter().map(|cond| { - let pallet_def = self.find_pallet(&cond.pallet); - let variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - let call_name = &cond.call; - let field = &cond.field; - let limit = &cond.limit; - quote! { - RuntimeCall::#variant(#module::Call::#call_name { #field, .. }) => { - *#field < #limit - } - } - }); - quote! { - ProxyType::#pt => match c { - #(#arms)* - _ => false, - }, - } - } - FilterKind::AllowNested { calls } => { - let arms = calls.iter().map(|nested| { - let pallet_def = self.find_pallet(&nested.pallet); - let variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - let call_name = &nested.call; - let field = &nested.field; - let target_pallet_def = self.find_pallet(&nested.target_pallet); - let target_variant = &target_pallet_def.runtime_variant; - let target_module = &target_pallet_def.module; - let target_call_name = &nested.target_call; - quote! { - RuntimeCall::#variant(#module::Call::#call_name { #field, .. }) => { - let inner_call: RuntimeCall = *#field.clone(); - matches!( - inner_call, - RuntimeCall::#target_variant(#target_module::Call::#target_call_name { .. }) - ) - } - } - }); - quote! { - ProxyType::#pt => match c { - #(#arms)* - _ => false, - }, - } - } - } - }).collect(); - - quote! { - fn proxy_type_filter(pt: &ProxyType, c: &RuntimeCall) -> bool { - match pt { - #(#arms)* - } - } - } - } - - fn call_refs_to_patterns(&self, calls: &[CallRef]) -> Vec { - calls - .iter() - .map(|call_ref| match call_ref { - CallRef::Wildcard(pallet) => { - let pallet_def = self.find_pallet(pallet); - let variant = &pallet_def.runtime_variant; - quote! { RuntimeCall::#variant(..) } - } - CallRef::Specific(pallet, call) => { - let pallet_def = self.find_pallet(pallet); - let variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - quote! { RuntimeCall::#variant(#module::Call::#call { .. }) } - } - }) - .collect() - } - - // ======================================================================== - // data function generation - // ======================================================================== - - fn generate_data_fn(&self) -> TokenStream2 { - let entries: Vec = self - .rules - .iter() - .map(|rule| { - let pt = &rule.proxy_type; - let (mode, calls_expr, exceptions_expr) = match &rule.kind { - FilterKind::AllowAll => ( - quote! { FilterMode::AllowAll }, - quote! { Vec::new() }, - quote! { Vec::new() }, - ), - FilterKind::DenyAll => ( - quote! { FilterMode::DenyAll }, - quote! { Vec::new() }, - quote! { Vec::new() }, - ), - FilterKind::Allow { calls, exceptions } => ( - quote! { FilterMode::Allow }, - self.call_refs_to_data(calls), - self.call_refs_to_data(exceptions), - ), - FilterKind::Deny { calls } => ( - quote! { FilterMode::Deny }, - self.call_refs_to_data(calls), - quote! { Vec::new() }, - ), - FilterKind::AllowConditional { calls } => { - let data = self.conditional_calls_to_data(calls); - (quote! { FilterMode::Allow }, data, quote! { Vec::new() }) - } - FilterKind::AllowNested { calls } => { - let data = self.nested_calls_to_data(calls); - (quote! { FilterMode::Allow }, data, quote! { Vec::new() }) - } - }; - - quote! { - ProxyFilterInfo { - proxy_type: ProxyType::#pt.into(), - name: alloc::format!("{:?}", ProxyType::#pt).into_bytes(), - deprecated: ProxyType::#pt.is_deprecated(), - filter_mode: #mode, - calls: #calls_expr, - exceptions: #exceptions_expr, - } - } - }) - .collect(); - - quote! { - pub fn get_all_proxy_filters() -> Vec { - vec![ - #(#entries),* - ] - } - } - } - - fn call_refs_to_data(&self, calls: &[CallRef]) -> TokenStream2 { - let items: Vec = calls - .iter() - .map(|call_ref| match call_ref { - CallRef::Wildcard(pallet) => { - let pallet_def = self.find_pallet(pallet); - let runtime_variant = &pallet_def.runtime_variant; - quote! { pallet_wildcard::<#runtime_variant>() } - } - CallRef::Specific(pallet, call) => { - let pallet_def = self.find_pallet(pallet); - let runtime_variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - let call_str = call.to_string(); - quote! { - call_info_by_name::<#runtime_variant, #module::Call>(#call_str) - } - } - }) - .collect(); - quote! { vec![#(#items),*] } - } - - fn conditional_calls_to_data(&self, calls: &[ConditionalCallRef]) -> TokenStream2 { - let items: Vec = calls - .iter() - .map(|cond| { - let pallet_def = self.find_pallet(&cond.pallet); - let runtime_variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - let call_str = cond.call.to_string(); - let field_str = cond.field.to_string(); - let limit = &cond.limit; - quote! { - call_info_by_name_conditional::<#runtime_variant, #module::Call>( - #call_str, - CallCondition::ParamLessThan { - param_name: #field_str.as_bytes().to_vec(), - limit: Into::::into(#limit) as u128, - }, - ) - } - }) - .collect(); - quote! { vec![#(#items),*] } - } - - fn nested_calls_to_data(&self, calls: &[NestedCallRef]) -> TokenStream2 { - let items: Vec = calls.iter().map(|nested| { - let pallet_def = self.find_pallet(&nested.pallet); - let runtime_variant = &pallet_def.runtime_variant; - let module = &pallet_def.module; - let call_str = nested.call.to_string(); - let target_pallet_def = self.find_pallet(&nested.target_pallet); - let target_runtime_variant = &target_pallet_def.runtime_variant; - let target_call_str = nested.target_call.to_string(); - quote! { - call_info_by_name_conditional::<#runtime_variant, #module::Call>( - #call_str, - CallCondition::NestedCallMustBe { - pallet_name: <#target_runtime_variant as PalletInfoAccess>::name().as_bytes().to_vec(), - call_name: #target_call_str.as_bytes().to_vec(), - }, - ) - } - }).collect(); - quote! { vec![#(#items),*] } - } -}