diff --git a/.github/workflows/require-clean-merges.yml b/.github/workflows/require-clean-merges.yml index 532a411549..c00a4b47e2 100644 --- a/.github/workflows/require-clean-merges.yml +++ b/.github/workflows/require-clean-merges.yml @@ -46,6 +46,10 @@ jobs: git checkout $PR_BRANCH git reset --hard origin/$PR_BRANCH + # Configure a temporary Git identity to allow merging + git config --local user.email "github-actions@github.com" + git config --local user.name "GitHub Actions" + for branch in $MERGE_BRANCHES; do echo "Checking merge from $branch into $PR_BRANCH..." diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 9cb51e1b27..28d1c9a7de 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -23,9 +23,9 @@ mod tests; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::dispatch::DispatchResult; use frame_support::pallet_prelude::*; use frame_support::traits::tokens::Balance; + use frame_support::{dispatch::DispatchResult, pallet_prelude::StorageMap}; use frame_system::pallet_prelude::*; use pallet_evm_chain_id::{self, ChainId}; use sp_runtime::BoundedVec; @@ -69,7 +69,16 @@ pub mod pallet { } #[pallet::event] - pub enum Event {} + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Event emitted when a precompile operation is updated. + PrecompileUpdated { + /// The type of precompile operation being updated. + precompile_id: PrecompileEnum, + /// Indicates if the precompile operation is enabled or not. + enabled: bool, + }, + } // Errors inform users that something went wrong. #[pallet::error] @@ -81,6 +90,37 @@ pub mod pallet { /// The maximum number of subnet validators must be more than the current number of UIDs already in the subnet. MaxAllowedUIdsLessThanCurrentUIds, } + /// Enum for specifying the type of precompile operation. + #[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug, Copy)] + pub enum PrecompileEnum { + /// Enum for balance transfer precompile + BalanceTransfer, + /// Enum for staking precompile + Staking, + /// Enum for subnet precompile + Subnet, + /// Enum for metagraph precompile + Metagraph, + /// Enum for neuron precompile + Neuron, + } + + #[pallet::type_value] + /// Default value for precompile enable + pub fn DefaultPrecompileEnabled() -> bool { + true + } + + #[pallet::storage] + /// Map PrecompileEnum --> enabled + pub type PrecompileEnable = StorageMap< + _, + Blake2_128Concat, + PrecompileEnum, + bool, + ValueQuery, + DefaultPrecompileEnabled, + >; /// Dispatchable functions allows users to interact with the pallet and invoke state changes. #[pallet::call] @@ -1307,6 +1347,36 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_subnet_owner_or_root(origin, netuid)?; pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle) } + + /// Toggles the enablement of an EVM precompile. + /// + /// # Arguments + /// * `origin` - The origin of the call, which must be the root account. + /// * `precompile_id` - The identifier of the EVM precompile to toggle. + /// * `enabled` - The new enablement state of the precompile. + /// + /// # Errors + /// * `BadOrigin` - If the caller is not the root account. + /// + /// # Weight + /// Weight is handled by the `#[pallet::weight]` attribute. + #[pallet::call_index(62)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_toggle_evm_precompile( + origin: OriginFor, + precompile_id: PrecompileEnum, + enabled: bool, + ) -> DispatchResult { + ensure_root(origin)?; + if PrecompileEnable::::get(precompile_id) != enabled { + PrecompileEnable::::insert(precompile_id, enabled); + Self::deposit_event(Event::PrecompileUpdated { + precompile_id, + enabled, + }); + } + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 23f7768969..048bf9960c 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -392,6 +392,7 @@ pub(crate) fn run_to_block(n: u64) { while System::block_number() < n { SubtensorModule::on_finalize(System::block_number()); System::on_finalize(System::block_number()); + System::reset_events(); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); SubtensorModule::on_initialize(System::block_number()); diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index a3b771444e..03fb1063e7 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -11,6 +11,7 @@ use pallet_subtensor::Event; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_core::{ed25519, Pair, U256}; +use crate::pallet::PrecompileEnable; use crate::Error; use mock::*; @@ -1365,3 +1366,74 @@ fn test_schedule_grandpa_change() { assert_eq!(Grandpa::grandpa_authorities(), vec![(bob, 1)]); }); } + +#[test] +fn test_sudo_toggle_evm_precompile() { + new_test_ext().execute_with(|| { + let precompile_id = crate::PrecompileEnum::BalanceTransfer; + let initial_enabled = PrecompileEnable::::get(precompile_id); + assert!(initial_enabled); // Assuming the default is true + + run_to_block(1); + + assert_eq!( + AdminUtils::sudo_toggle_evm_precompile( + <::RuntimeOrigin>::signed(U256::from(0)), + precompile_id, + false + ), + Err(DispatchError::BadOrigin) + ); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + false + )); + + assert_eq!( + System::events() + .iter() + .filter(|r| r.event + == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { + precompile_id, + enabled: false + })) + .count(), + 1 + ); + + let updated_enabled = PrecompileEnable::::get(precompile_id); + assert!(!updated_enabled); + + run_to_block(2); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + false + )); + + // no event without status change + assert_eq!( + System::events() + .iter() + .filter(|r| r.event + == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { + precompile_id, + enabled: false + })) + .count(), + 0 + ); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + true + )); + + let final_enabled = PrecompileEnable::::get(precompile_id); + assert!(final_enabled); + }); +} diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 0ad75ea200..7242575ef8 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1674,6 +1674,10 @@ pub enum CustomTransactionError { InsufficientLiquidity, SlippageTooHigh, TransferDisallowed, + HotKeyNotRegisteredInNetwork, + InvalidIpAddress, + ServingRateLimitExceeded, + InvalidPort, BadRequest, } @@ -1690,6 +1694,10 @@ impl From for u8 { CustomTransactionError::InsufficientLiquidity => 7, CustomTransactionError::SlippageTooHigh => 8, CustomTransactionError::TransferDisallowed => 9, + CustomTransactionError::HotKeyNotRegisteredInNetwork => 10, + CustomTransactionError::InvalidIpAddress => 11, + CustomTransactionError::ServingRateLimitExceeded => 12, + CustomTransactionError::InvalidPort => 13, CustomTransactionError::BadRequest => 255, } } @@ -1773,6 +1781,22 @@ where CustomTransactionError::TransferDisallowed.into(), ) .into()), + Error::::HotKeyNotRegisteredInNetwork => Err(InvalidTransaction::Custom( + CustomTransactionError::HotKeyNotRegisteredInNetwork.into(), + ) + .into()), + Error::::InvalidIpAddress => Err(InvalidTransaction::Custom( + CustomTransactionError::InvalidIpAddress.into(), + ) + .into()), + Error::::ServingRateLimitExceeded => Err(InvalidTransaction::Custom( + CustomTransactionError::ServingRateLimitExceeded.into(), + ) + .into()), + Error::::InvalidPort => Err(InvalidTransaction::Custom( + CustomTransactionError::InvalidPort.into(), + ) + .into()), _ => Err( InvalidTransaction::Custom(CustomTransactionError::BadRequest.into()).into(), ), @@ -2175,6 +2199,32 @@ where }) } } + Some(Call::serve_axon { + netuid, + version, + ip, + port, + ip_type, + protocol, + placeholder1, + placeholder2, + }) => { + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_serve_axon( + who, + *netuid, + *version, + *ip, + *port, + *ip_type, + *protocol, + *placeholder1, + *placeholder2, + ), + Self::get_priority_vanilla(), + ) + } _ => { if let Some( BalancesCall::transfer_keep_alive { .. } diff --git a/pallets/subtensor/src/subnets/serving.rs b/pallets/subtensor/src/subnets/serving.rs index 647b328cce..c8c89bc011 100644 --- a/pallets/subtensor/src/subnets/serving.rs +++ b/pallets/subtensor/src/subnets/serving.rs @@ -69,28 +69,20 @@ impl Pallet { // We check the callers (hotkey) signature. let hotkey_id = ensure_signed(origin)?; - // Ensure the hotkey is registered somewhere. - ensure!( - Self::is_hotkey_registered_on_any_network(&hotkey_id), - Error::::HotKeyNotRegisteredInNetwork - ); - - // Check the ip signature validity. - ensure!(Self::is_valid_ip_type(ip_type), Error::::InvalidIpType); - ensure!( - Self::is_valid_ip_address(ip_type, ip), - Error::::InvalidIpAddress - ); - - // Get the previous axon information. - let mut prev_axon = Self::get_axon_info(netuid, &hotkey_id); - let current_block: u64 = Self::get_current_block_as_u64(); - ensure!( - Self::axon_passes_rate_limit(netuid, &prev_axon, current_block), - Error::::ServingRateLimitExceeded - ); + // Validate user input + Self::validate_serve_axon( + &hotkey_id, + netuid, + version, + ip, + port, + ip_type, + protocol, + placeholder1, + placeholder2, + )?; - // Check certificate + // Check+insert certificate if let Some(certificate) = certificate { if let Ok(certificate) = NeuronCertificateOf::try_from(certificate) { NeuronCertificates::::insert(netuid, hotkey_id.clone(), certificate) @@ -98,6 +90,7 @@ impl Pallet { } // We insert the axon meta. + let mut prev_axon = Self::get_axon_info(netuid, &hotkey_id); prev_axon.block = Self::get_current_block_as_u64(); prev_axon.version = version; prev_axon.ip = ip; @@ -176,12 +169,6 @@ impl Pallet { // We check the callers (hotkey) signature. let hotkey_id = ensure_signed(origin)?; - // Ensure the hotkey is registered somewhere. - ensure!( - Self::is_hotkey_registered_on_any_network(&hotkey_id), - Error::::HotKeyNotRegisteredInNetwork - ); - // Check the ip signature validity. ensure!(Self::is_valid_ip_type(ip_type), Error::::InvalidIpType); ensure!( @@ -189,6 +176,12 @@ impl Pallet { Error::::InvalidIpAddress ); + // Ensure the hotkey is registered somewhere. + ensure!( + Self::is_hotkey_registered_on_any_network(&hotkey_id), + Error::::HotKeyNotRegisteredInNetwork + ); + // We get the previous axon info assoicated with this ( netuid, uid ) let mut prev_prometheus = Self::get_prometheus_info(netuid, &hotkey_id); let current_block: u64 = Self::get_current_block_as_u64(); @@ -332,4 +325,55 @@ impl Pallet { Ok(true) } + + pub fn validate_serve_axon( + hotkey_id: &T::AccountId, + netuid: u16, + version: u32, + ip: u128, + port: u16, + ip_type: u8, + protocol: u8, + placeholder1: u8, + placeholder2: u8, + ) -> Result<(), Error> { + // Ensure the hotkey is registered somewhere. + ensure!( + Self::is_hotkey_registered_on_any_network(hotkey_id), + Error::::HotKeyNotRegisteredInNetwork + ); + + // Check the ip signature validity. + ensure!(Self::is_valid_ip_type(ip_type), Error::::InvalidIpType); + ensure!( + Self::is_valid_ip_address(ip_type, ip), + Error::::InvalidIpAddress + ); + + // Get the previous axon information. + let mut prev_axon = Self::get_axon_info(netuid, hotkey_id); + let current_block: u64 = Self::get_current_block_as_u64(); + ensure!( + Self::axon_passes_rate_limit(netuid, &prev_axon, current_block), + Error::::ServingRateLimitExceeded + ); + + // Validate axon data with delegate func + prev_axon.block = Self::get_current_block_as_u64(); + prev_axon.version = version; + prev_axon.ip = ip; + prev_axon.port = port; + prev_axon.ip_type = ip_type; + prev_axon.protocol = protocol; + prev_axon.placeholder1 = placeholder1; + prev_axon.placeholder2 = placeholder2; + + let axon_validated = Self::validate_axon_data(&prev_axon); + ensure!( + axon_validated.is_ok(), + axon_validated.err().unwrap_or(Error::::InvalidPort) + ); + + Ok(()) + } } diff --git a/pallets/subtensor/src/tests/serving.rs b/pallets/subtensor/src/tests/serving.rs index 52b8f8b08f..5d5858a313 100644 --- a/pallets/subtensor/src/tests/serving.rs +++ b/pallets/subtensor/src/tests/serving.rs @@ -2,8 +2,8 @@ use super::mock::*; use crate::Error; use crate::*; -use frame_support::assert_noop; use frame_support::pallet_prelude::Weight; +use frame_support::{assert_err, assert_noop}; use frame_support::{ assert_ok, dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}, @@ -1257,3 +1257,60 @@ fn test_set_subnet_identity_dispatch_info_ok() { assert_eq!(dispatch_info.pays_fee, Pays::Yes); }); } + +// cargo test --package pallet-subtensor --lib -- tests::serving::test_serve_axon_validate --exact --show-output +#[test] +fn test_serve_axon_validate() { + // Testing the signed extension validate function + // correctly filters the `serve_axon` transaction. + + new_test_ext(0).execute_with(|| { + let hotkey = U256::from(1); + let netuid: u16 = 1; + let version: u32 = 2; + let ip: u128 = 1676056785; + let port: u16 = 128; + let ip_type: u8 = 4; + let protocol: u8 = 0; + let placeholder1: u8 = 0; + let placeholder2: u8 = 0; + + // Serve axon bad call + let call = RuntimeCall::SubtensorModule(SubtensorCall::serve_axon { + netuid, + version, + ip, + port, + ip_type, + protocol, + placeholder1, + placeholder2, + }); + + let info: crate::DispatchInfo = + crate::DispatchInfoOf::<::RuntimeCall>::default(); + + let extension = crate::SubtensorSignedExtension::::new(); + // Submit to the signed extension validate function + let result_bad = extension.validate(&hotkey, &call.clone(), &info, 10); + + // Should fail due to insufficient stake + assert_err!( + result_bad, + crate::TransactionValidityError::Invalid(crate::InvalidTransaction::Custom( + CustomTransactionError::HotKeyNotRegisteredInNetwork.into() + )) + ); + + // Register the hotkey in the subnet and try again + let coldkey = U256::from(1); + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Submit to the signed extension validate function + let result_ok = extension.validate(&hotkey, &call.clone(), &info, 10); + + // Now the call passes + assert_ok!(result_ok); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f0c0715132..28f4fabc54 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -229,7 +229,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 235, + spec_version: 236, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/src/precompiles/mod.rs b/runtime/src/precompiles/mod.rs index 933f476539..6bed2913cd 100644 --- a/runtime/src/precompiles/mod.rs +++ b/runtime/src/precompiles/mod.rs @@ -17,6 +17,8 @@ use frame_support::dispatch::{GetDispatchInfo, Pays}; use frame_system::RawOrigin; use sp_core::{hashing::keccak_256, H160}; use sp_runtime::{traits::Dispatchable, AccountId32}; + +use pallet_admin_utils::{PrecompileEnable, PrecompileEnum}; use sp_std::vec; // Include custom precompiles @@ -83,17 +85,59 @@ where // Non-Frontier specific nor Ethereum precompiles : a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)), a if a == hash(1025) => Some(ECRecoverPublicKey::execute(handle)), + a if a == hash(EDVERIFY_PRECOMPILE_INDEX) => Some(Ed25519Verify::execute(handle)), // Subtensor specific precompiles : a if a == hash(BALANCE_TRANSFER_INDEX) => { - Some(BalanceTransferPrecompile::execute(handle)) + if PrecompileEnable::::get(PrecompileEnum::BalanceTransfer) { + Some(BalanceTransferPrecompile::execute(handle)) + } else { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other( + "Precompile Balance Transfer is disabled".into(), + ), + })) + } + } + a if a == hash(STAKING_PRECOMPILE_INDEX) => { + if PrecompileEnable::::get(PrecompileEnum::Staking) { + Some(StakingPrecompile::execute(handle)) + } else { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other( + "Precompile Balance Transfer is disabled".into(), + ), + })) + } + } + + a if a == hash(SUBNET_PRECOMPILE_INDEX) => { + if PrecompileEnable::::get(PrecompileEnum::Subnet) { + Some(SubnetPrecompile::execute(handle)) + } else { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other("Precompile Subnet is disabled".into()), + })) + } } - a if a == hash(STAKING_PRECOMPILE_INDEX) => Some(StakingPrecompile::execute(handle)), - a if a == hash(SUBNET_PRECOMPILE_INDEX) => Some(SubnetPrecompile::execute(handle)), a if a == hash(METAGRAPH_PRECOMPILE_INDEX) => { - Some(MetagraphPrecompile::execute(handle)) + if PrecompileEnable::::get(PrecompileEnum::Metagraph) { + Some(MetagraphPrecompile::execute(handle)) + } else { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other("Precompile Metagrah is disabled".into()), + })) + } + } + a if a == hash(NEURON_PRECOMPILE_INDEX) => { + if PrecompileEnable::::get(PrecompileEnum::Neuron) { + Some(NeuronPrecompile::execute(handle)) + } else { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other("Precompile Neuron is disabled".into()), + })) + } } - a if a == hash(NEURON_PRECOMPILE_INDEX) => Some(NeuronPrecompile::execute(handle)), _ => None, }