diff --git a/pallets/subtensor/src/errors.rs b/pallets/subtensor/src/errors.rs index 100be89b7..f26a1cd55 100644 --- a/pallets/subtensor/src/errors.rs +++ b/pallets/subtensor/src/errors.rs @@ -126,6 +126,8 @@ mod errors { CommitRevealEnabled, /// Attemtping to commit/reveal weights when disabled. CommitRevealDisabled, + /// Not able to join the senate. + CouldNotJoinSenate, /// Attempting to set alpha high/low while disabled LiquidAlphaDisabled, /// Alpha high is too low: alpha_high > 0.8 diff --git a/pallets/subtensor/src/events.rs b/pallets/subtensor/src/events.rs index 2ed0d621d..a5fb90e3f 100644 --- a/pallets/subtensor/src/events.rs +++ b/pallets/subtensor/src/events.rs @@ -132,6 +132,13 @@ mod events { MinDelegateTakeSet(u16), /// the target stakes per interval is set by sudo/admin transaction TargetStakesPerIntervalSet(u64), + /// a member of the senate is adjusted + SenateAdjusted { + /// the account ID of the old senate member, if any + old_member: Option, + /// the account ID of the new senate member + new_member: T::AccountId, + }, /// A coldkey has been swapped ColdkeySwapped { /// the account ID of old coldkey diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 4326fba86..b7c39fc48 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2039,6 +2039,15 @@ pub mod pallet { Self::do_root_register(origin, hotkey) } + /// Attempt to adjust the senate membership to include a hotkey + #[pallet::call_index(63)] + #[pallet::weight((Weight::from_parts(0, 0) + .saturating_add(T::DbWeight::get().reads(0)) + .saturating_add(T::DbWeight::get().writes(0)), DispatchClass::Normal, Pays::Yes))] + pub fn adjust_senate(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { + Self::do_adjust_senate(origin, hotkey) + } + /// User register a new subnetwork via burning token #[pallet::call_index(7)] #[pallet::weight((Weight::from_parts(177_000_000, 0) diff --git a/pallets/subtensor/src/root.rs b/pallets/subtensor/src/root.rs index f39ae97e0..a973b8d7d 100644 --- a/pallets/subtensor/src/root.rs +++ b/pallets/subtensor/src/root.rs @@ -571,9 +571,133 @@ impl Pallet { ); } - let current_stake = Self::get_total_stake_for_hotkey(&hotkey); + // --- 13. Join the Senate if eligible. + // Returns the replaced member, if any. + let _ = Self::join_senate_if_eligible(&hotkey)?; + + // --- 14. Force all members on root to become a delegate. + if !Self::hotkey_is_delegate(&hotkey) { + Self::delegate_hotkey(&hotkey, 11_796); // 18% cut defaulted. + } + + // --- 15. Update the registration counters for both the block and interval. + #[allow(clippy::arithmetic_side_effects)] + // note this RA + clippy false positive is a known substrate issue + RegistrationsThisInterval::::mutate(root_netuid, |val| *val += 1); + #[allow(clippy::arithmetic_side_effects)] + // note this RA + clippy false positive is a known substrate issue + RegistrationsThisBlock::::mutate(root_netuid, |val| *val += 1); + + // --- 16. Log and announce the successful registration. + log::info!( + "RootRegistered(netuid:{:?} uid:{:?} hotkey:{:?})", + root_netuid, + subnetwork_uid, + hotkey + ); + Self::deposit_event(Event::NeuronRegistered(root_netuid, subnetwork_uid, hotkey)); + + // --- 17. Finish and return success. + Ok(()) + } + + // Checks if a hotkey should be a member of the Senate, and if so, adds them. + // + // This function is responsible for adding a hotkey to the Senate if they meet the requirements. + // The root key with the least stake is pruned in the event of a filled membership. + // + // # Arguments: + // * 'origin': Represents the origin of the call. + // * 'hotkey': The hotkey that the user wants to register to the root network. + // + // # Returns: + // * 'DispatchResult': A result type indicating success or failure of the registration. + // + pub fn do_adjust_senate(origin: T::RuntimeOrigin, hotkey: T::AccountId) -> DispatchResult { + // --- 0. Get the unique identifier (UID) for the root network. + let root_netuid: u16 = Self::get_root_netuid(); + ensure!( + Self::if_subnet_exist(root_netuid), + Error::::RootNetworkDoesNotExist + ); + + // --- 1. Ensure that the call originates from a signed source and retrieve the caller's account ID (coldkey). + let coldkey = ensure_signed(origin)?; + log::info!( + "do_root_register( coldkey: {:?}, hotkey: {:?} )", + coldkey, + hotkey + ); + + // --- 2. Check if the hotkey is already registered to the root network. If not, error out. + ensure!( + Uids::::contains_key(root_netuid, &hotkey), + Error::::HotKeyNotRegisteredInSubNet + ); + + // --- 3. Create a network account for the user if it doesn't exist. + Self::create_account_if_non_existent(&coldkey, &hotkey); + + // --- 4. Join the Senate if eligible. + // Returns the replaced member, if any. + let replaced = Self::join_senate_if_eligible(&hotkey)?; + + if replaced.is_none() { + // Not eligible to join the Senate, or no replacement needed. + // Check if the hotkey is *now* a member of the Senate. + // Otherwise, error out. + ensure!( + T::SenateMembers::is_member(&hotkey), + Error::::StakeTooLowForRoot, // Had less stake than the lowest stake incumbent. + ); + } + + // --- 5. Log and announce the successful Senate adjustment. + log::info!( + "SenateAdjusted(old_hotkey:{:?} hotkey:{:?})", + replaced, + hotkey + ); + Self::deposit_event(Event::SenateAdjusted { + old_member: replaced.cloned(), + new_member: hotkey, + }); + + // --- 6. Finish and return success. + Ok(()) + } + + // Checks if a hotkey should be a member of the Senate, and if so, adds them. + // + // # Arguments: + // * 'hotkey': The hotkey that the user wants to register to the root network. + // + // # Returns: + // * 'Result, Error>': A result containing the replaced member, if any. + // + fn join_senate_if_eligible(hotkey: &T::AccountId) -> Result, Error> { + // Get the root network UID. + let root_netuid: u16 = Self::get_root_netuid(); + + // --- 1. Check the hotkey is registered in the root network. + ensure!( + Uids::::contains_key(root_netuid, hotkey), + Error::::HotKeyNotRegisteredInSubNet + ); + + // --- 2. Verify the hotkey is NOT already a member of the Senate. + ensure!( + !T::SenateMembers::is_member(hotkey), + Error::::HotKeyAlreadyRegisteredInSubNet + ); + + // --- 3. Grab the hotkey's stake. + let current_stake = Self::get_total_stake_for_hotkey(hotkey); + + // Add the hotkey to the Senate. // If we're full, we'll swap out the lowest stake member. let members = T::SenateMembers::members(); + let last: Option<&T::AccountId> = None; if (members.len() as u32) == T::SenateMembers::max_members() { let mut sorted_members = members.clone(); sorted_members.sort_by(|a, b| { @@ -587,34 +711,17 @@ impl Pallet { let last_stake = Self::get_total_stake_for_hotkey(last); if last_stake < current_stake { - T::SenateMembers::swap_member(last, &hotkey).map_err(|e| e.error)?; - T::TriumvirateInterface::remove_votes(last)?; + // Swap the member with the lowest stake. + T::SenateMembers::swap_member(last, hotkey) + .map_err(|_| Error::::CouldNotJoinSenate)?; } } } else { - T::SenateMembers::add_member(&hotkey).map_err(|e| e.error)?; + T::SenateMembers::add_member(hotkey).map_err(|_| Error::::CouldNotJoinSenate)?; } - // --- 13. Force all members on root to become a delegate. - if !Self::hotkey_is_delegate(&hotkey) { - Self::delegate_hotkey(&hotkey, 11_796); // 18% cut defaulted. - } - - // --- 14. Update the registration counters for both the block and interval. - RegistrationsThisInterval::::mutate(root_netuid, |val| val.saturating_inc()); - RegistrationsThisBlock::::mutate(root_netuid, |val| val.saturating_inc()); - - // --- 15. Log and announce the successful registration. - log::info!( - "RootRegistered(netuid:{:?} uid:{:?} hotkey:{:?})", - root_netuid, - subnetwork_uid, - hotkey - ); - Self::deposit_event(Event::NeuronRegistered(root_netuid, subnetwork_uid, hotkey)); - - // --- 16. Finish and return success. - Ok(()) + // Return the swapped out member, if any. + Ok(last) } pub fn do_set_root_weights( diff --git a/pallets/subtensor/tests/mock.rs b/pallets/subtensor/tests/mock.rs index c3a864075..f7ac00db6 100644 --- a/pallets/subtensor/tests/mock.rs +++ b/pallets/subtensor/tests/mock.rs @@ -203,23 +203,29 @@ use pallet_subtensor::{CollectiveInterface, MemberManagement}; pub struct ManageSenateMembers; impl MemberManagement for ManageSenateMembers { fn add_member(account: &AccountId) -> DispatchResultWithPostInfo { - SenateMembers::add_member(RawOrigin::Root.into(), *account) + let who = *account; + SenateMembers::add_member(RawOrigin::Root.into(), who) } fn remove_member(account: &AccountId) -> DispatchResultWithPostInfo { - SenateMembers::remove_member(RawOrigin::Root.into(), *account) + let who = *account; + SenateMembers::remove_member(RawOrigin::Root.into(), who) } - fn swap_member(remove: &AccountId, add: &AccountId) -> DispatchResultWithPostInfo { - SenateMembers::swap_member(RawOrigin::Root.into(), *remove, *add) + fn swap_member(rm: &AccountId, add: &AccountId) -> DispatchResultWithPostInfo { + let remove = *rm; + let add = *add; + + Triumvirate::remove_votes(rm)?; + SenateMembers::swap_member(RawOrigin::Root.into(), remove, add) } fn is_member(account: &AccountId) -> bool { - Senate::is_member(account) + SenateMembers::members().contains(account) } fn members() -> Vec { - Senate::members() + SenateMembers::members().into() } fn max_members() -> u32 { diff --git a/pallets/subtensor/tests/senate.rs b/pallets/subtensor/tests/senate.rs index ac020ae04..bcec1a63a 100644 --- a/pallets/subtensor/tests/senate.rs +++ b/pallets/subtensor/tests/senate.rs @@ -488,7 +488,10 @@ fn test_senate_leave_vote_removal() { assert!( SubtensorModule::get_uid_for_net_and_hotkey(root_netuid, &hotkey_account_id).is_err() ); + // No longer a member of the senate + assert!(!Senate::is_member(&hotkey_account_id)); assert_eq!( + // Vote is removed Triumvirate::has_voted(hash, 0, &hotkey_account_id), Ok(false) ); @@ -574,3 +577,234 @@ fn test_senate_not_leave_when_stake_removed() { assert!(Senate::is_member(&hotkey_account_id)); }); } + +#[test] +fn test_senate_join_current_delegate() { + // Test that a current delegate can join the senate + new_test_ext().execute_with(|| { + migration::migrate_create_root_network::(); + + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); + + //add network + SubtensorModule::set_burn(netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give some coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 10000); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid, + hotkey_account_id + )); + // Check if balance has decreased to pay for the burn. + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + (10000 - burn_cost) + ); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), + coldkey_account_id + ); + + // Register in the root network + assert_ok!(SubtensorModule::root_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id + )); + // But, remove from the senate + assert_ok!(SenateMembers::remove_member( + <::RuntimeOrigin>::root(), + hotkey_account_id + )); + + // Should *NOT* be a member of the senate now + assert!(!Senate::is_member(&hotkey_account_id)); + + System::reset_events(); + + // We can call now to adjust the senate + assert_ok!(SubtensorModule::adjust_senate( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id + )); + + // This should make the hotkey a member of the senate + assert!(Senate::is_member(&hotkey_account_id)); + + // Check the events + assert!( + System::events().contains(&record(RuntimeEvent::SubtensorModule( + SubtensorEvent::SenateAdjusted { + old_member: None, + new_member: hotkey_account_id + } + ))) + ); + }); +} + +#[test] +fn test_adjust_senate_events() { + // Test the events emitted after adjusting the senate successfully + new_test_ext().execute_with(|| { + migration::migrate_create_root_network::(); + + let netuid: u16 = 1; + let tempo: u16 = 13; + let hotkey_account_id = U256::from(6); + let burn_cost = 1000; + let coldkey_account_id = U256::from(667); + let root_netuid = SubtensorModule::get_root_netuid(); + + let max_senate_size: u16 = SenateMaxMembers::get() as u16; + let stake_threshold: u64 = 100_000; // Give this much to every senator + + // We will be registering MaxMembers hotkeys and two more to try a replace + let balance_to_add = 50_000 + (stake_threshold + burn_cost) * (max_senate_size + 2) as u64; + + let replacement_hotkey_account_id = U256::from(7); // Will be added to the senate to replace hotkey_account_id + + //add network + SubtensorModule::set_burn(netuid, burn_cost); + add_network(netuid, tempo, 0); + // Give some coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, balance_to_add); + + // Allow all registrations in netuid in same block. Same for root network. + SubtensorModule::set_max_registrations_per_block(netuid, max_senate_size + 1); + SubtensorModule::set_target_registrations_per_interval(netuid, max_senate_size + 1); + SubtensorModule::set_max_registrations_per_block(root_netuid, max_senate_size + 1); + SubtensorModule::set_target_registrations_per_interval(root_netuid, max_senate_size + 1); + + // Subscribe and check extrinsic output + assert_ok!(SubtensorModule::burned_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid, + hotkey_account_id + )); + // Check if balance has decreased to pay for the burn. + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + (balance_to_add - burn_cost) + ); // funds drained on reg. + // Check if neuron has added to the specified network(netuid) + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + // Check if hotkey is added to the Hotkeys + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_account_id), + coldkey_account_id + ); + + // Should *NOT* be a member of the senate + assert!(!Senate::is_member(&hotkey_account_id)); + + // root register + assert_ok!(SubtensorModule::root_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + hotkey_account_id + )); // Has no stake, but is now a senate member + + // Check if they are a member of the senate + assert!(Senate::is_member(&hotkey_account_id)); + + // Register MaxMembers - 1 more hotkeys, add stake and join the senate + for i in 0..(max_senate_size - 1) { + let new_hotkey_account_id = U256::from(8 + i); + + assert_ok!(SubtensorModule::burned_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid, + new_hotkey_account_id + )); + // Check if this hotkey is added to the Hotkeys + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey_account_id), + coldkey_account_id + ); + // Add/delegate enough stake to join the senate + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey_account_id), + new_hotkey_account_id, + stake_threshold + 1 + i as u64 // Increasing with i to make them ordered + )); // +1 to be above hotkey_account_id + // Join senate + assert_ok!(SubtensorModule::root_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + new_hotkey_account_id + )); + // Check if they are a member of the senate + assert!(Senate::is_member(&new_hotkey_account_id)); + } + + // Verify we are at max senate size + assert_eq!(Senate::members().len(), max_senate_size as usize); + + // Verify the replacement hotkey is not a member of the senate + assert!(!Senate::is_member(&replacement_hotkey_account_id)); + + // Register + assert_ok!(SubtensorModule::burned_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid, + replacement_hotkey_account_id + )); + + // Register in root network + assert_ok!(SubtensorModule::root_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + replacement_hotkey_account_id + )); + + // Check if they are a member of the senate, should not be, + // as they have no stake + assert!(!Senate::is_member(&replacement_hotkey_account_id)); + + // Add/delegate enough stake to join the senate + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey_account_id), + replacement_hotkey_account_id, + 1 // Will be more than the last one in the senate by stake (has 0 stake) + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey( + &coldkey_account_id, + &replacement_hotkey_account_id + ), + 1 + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&replacement_hotkey_account_id), + 1 + ); + + System::reset_events(); + + // We can call now to adjust the senate + assert_ok!(SubtensorModule::adjust_senate( + <::RuntimeOrigin>::signed(coldkey_account_id), + replacement_hotkey_account_id + )); + + // This should make the hotkey a member of the senate + assert!(Senate::is_member(&replacement_hotkey_account_id)); + + // Check the events + assert!( + System::events().contains(&record(RuntimeEvent::SubtensorModule( + SubtensorEvent::SenateAdjusted { + old_member: None, + new_member: replacement_hotkey_account_id + } + ))) + ); + }); +}