From fbac1e09affd8b79ae3e7b2a3921f2775a5b32b4 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 21 Apr 2025 16:55:24 +0400 Subject: [PATCH] Add execution delay until the next block. --- pallets/subtensor/src/lib.rs | 11 +- pallets/subtensor/src/macros/dispatches.rs | 12 +- pallets/subtensor/src/macros/hooks.rs | 4 +- pallets/subtensor/src/staking/add_stake.rs | 6 +- pallets/subtensor/src/staking/remove_stake.rs | 12 +- pallets/subtensor/src/staking/stake_utils.rs | 478 +++++++++------- pallets/subtensor/src/tests/staking.rs | 533 +++++++++++++++++- 7 files changed, 810 insertions(+), 246 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 9976e61415..2905375d9b 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -891,8 +891,15 @@ pub mod pallet { StorageValue<_, u64, ValueQuery, DefaultSenateRequiredStakePercentage>; #[pallet::storage] - pub type StakeJobs = - StorageMap<_, Blake2_128Concat, u64, StakeJob, OptionQuery>; + pub type StakeJobs = StorageDoubleMap< + _, + Blake2_128Concat, + BlockNumberFor, // first key: current block number + Twox64Concat, + u64, // second key: unique job ID + StakeJob, + OptionQuery, + >; #[pallet::storage] /// Ensures unique IDs for StakeJobs storage map diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index e3464cc776..25dda3d202 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2032,7 +2032,7 @@ mod dispatches { /// price, the staking order may execute only partially or not execute /// at all. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * 'origin': (Origin): @@ -2086,7 +2086,7 @@ mod dispatches { /// price, the staking order may execute only partially or not execute /// at all. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * 'origin': (Origin): @@ -2140,7 +2140,7 @@ mod dispatches { /// price, the staking order may execute only partially or not execute /// at all. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * 'origin': (Origin): @@ -2203,7 +2203,7 @@ mod dispatches { /// price, the staking order may execute only partially or not execute /// at all. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * 'origin': (Origin): @@ -2260,7 +2260,7 @@ mod dispatches { /// ---- The implementation for the extrinsic unstake_all_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * `origin` - (::Origin): @@ -2293,7 +2293,7 @@ mod dispatches { /// ---- The implementation for the extrinsic unstake_all_alpha_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. /// - /// The operation will be delayed until the end of the block. + /// The operation will be delayed. /// /// # Args: /// * `origin` - (::Origin): diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 6cbbc1b0a5..47d8fdc715 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -39,8 +39,8 @@ mod hooks { // # Args: // * 'n': (BlockNumberFor): // - The number of the block we are finalizing. - fn on_finalize(_block_number: BlockNumberFor) { - Self::do_on_finalize(); + fn on_finalize(block_number: BlockNumberFor) { + Self::do_on_finalize(block_number); } fn on_runtime_upgrade() -> frame_support::weights::Weight { diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 6a3eaca60f..9ec9c7022f 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -135,8 +135,9 @@ impl Pallet { }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); Ok(()) @@ -211,8 +212,9 @@ impl Pallet { }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); Ok(()) diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index e7b678c769..104c47d9fa 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -144,8 +144,9 @@ impl Pallet { }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); Ok(()) @@ -270,8 +271,9 @@ impl Pallet { let stake_job = StakeJob::UnstakeAll { hotkey, coldkey }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); Ok(()) @@ -409,8 +411,9 @@ impl Pallet { let stake_job = StakeJob::UnstakeAllAlpha { hotkey, coldkey }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); Ok(()) @@ -589,8 +592,9 @@ impl Pallet { }; let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); - StakeJobs::::insert(stake_job_id, stake_job); + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); NextStakeJobId::::set(stake_job_id.saturating_add(1)); // Done and ok. diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 4b91e92ccb..11e040db1d 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1,6 +1,8 @@ use super::*; +use frame_system::pallet_prelude::BlockNumberFor; use safe_math::*; use share_pool::{SharePool, SharePoolDataOperations}; +use sp_runtime::Saturating; use sp_std::ops::Neg; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32, U110F18}; @@ -1158,8 +1160,12 @@ impl Pallet { } // Process staking job for on_finalize() hook. - pub(crate) fn do_on_finalize() { - let stake_jobs = StakeJobs::::drain().collect::>(); + pub(crate) fn do_on_finalize(current_block_number: BlockNumberFor) { + // We delay job execution + const DELAY_IN_BLOCKS: u32 = 1u32; + let actual_block_with_delay = current_block_number.saturating_sub(DELAY_IN_BLOCKS.into()); + + let stake_jobs = StakeJobs::::drain_prefix(actual_block_with_delay).collect::>(); // Sort jobs by job type let mut add_stake = vec![]; @@ -1179,6 +1185,12 @@ impl Pallet { StakeJob::UnstakeAllAlpha { .. } => unstake_all_aplha.push(job), } } + // Reorder jobs based on the previous block hash + let previous_block_hash = >::parent_hash(); + let hash_bytes = previous_block_hash.as_ref(); + let first_byte = hash_bytes.first().expect("hash operation is infallible"); + // Extract the first bit + let altered_order = (first_byte & 0b10000000) != 0; // Ascending sort by coldkey remove_stake_limit.sort_by(|a, b| match (a, b) { @@ -1186,7 +1198,13 @@ impl Pallet { StakeJob::RemoveStakeLimit { coldkey: a_key, .. }, StakeJob::RemoveStakeLimit { coldkey: b_key, .. }, ) => { - a_key.cmp(b_key) // ascending + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } } _ => sp_std::cmp::Ordering::Equal, // unreachable }); @@ -1196,7 +1214,13 @@ impl Pallet { StakeJob::RemoveStake { coldkey: a_key, .. }, StakeJob::RemoveStake { coldkey: b_key, .. }, ) => { - a_key.cmp(b_key) // ascending + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } } _ => sp_std::cmp::Ordering::Equal, // unreachable }); @@ -1206,7 +1230,13 @@ impl Pallet { StakeJob::UnstakeAll { coldkey: a_key, .. }, StakeJob::UnstakeAll { coldkey: b_key, .. }, ) => { - a_key.cmp(b_key) // ascending + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } } _ => sp_std::cmp::Ordering::Equal, // unreachable }); @@ -1216,7 +1246,13 @@ impl Pallet { StakeJob::UnstakeAllAlpha { coldkey: a_key, .. }, StakeJob::UnstakeAllAlpha { coldkey: b_key, .. }, ) => { - a_key.cmp(b_key) // ascending + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } } _ => sp_std::cmp::Ordering::Equal, // unreachable }); @@ -1227,7 +1263,13 @@ impl Pallet { StakeJob::AddStakeLimit { coldkey: a_key, .. }, StakeJob::AddStakeLimit { coldkey: b_key, .. }, ) => { - b_key.cmp(a_key) // descending + let direct_order = b_key.cmp(a_key); // descending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } } _ => sp_std::cmp::Ordering::Equal, // unreachable }); @@ -1237,239 +1279,243 @@ impl Pallet { StakeJob::AddStake { coldkey: a_key, .. }, StakeJob::AddStake { coldkey: b_key, .. }, ) => { - b_key.cmp(a_key) // descending - } - _ => sp_std::cmp::Ordering::Equal, // unreachable - }); + let direct_order = b_key.cmp(a_key); // descending - // Process RemoveStakeLimit job (priority 1) - for job in remove_stake_limit.into_iter() { - if let StakeJob::RemoveStakeLimit { - hotkey, - coldkey, - netuid, - alpha_unstaked, - limit_price, - allow_partial, - } = job - { - let result = Self::do_remove_stake_limit( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - netuid, - alpha_unstaked, - limit_price, - allow_partial, - ); - - if let Err(err) = result { - log::debug!( - "Failed to remove aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", - coldkey, - hotkey, - netuid, - alpha_unstaked, - limit_price, - allow_partial, - err - ); - Self::deposit_event(Event::FailedToRemoveAggregatedLimitedStake( - coldkey, - hotkey, - netuid, - alpha_unstaked, - limit_price, - allow_partial, - )); + if altered_order { + direct_order.reverse() } else { - Self::deposit_event(Event::AggregatedLimitedStakeRemoved( - coldkey, - hotkey, - netuid, - alpha_unstaked, - limit_price, - allow_partial, - )); + direct_order } } - } - - // Process RemoveStake job (priority 2) - for job in remove_stake.into_iter() { - if let StakeJob::RemoveStake { - coldkey, - hotkey, - netuid, - alpha_unstaked, - } = job - { - let result = Self::do_remove_stake( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - netuid, - alpha_unstaked, - ); - - if let Err(err) = result { - log::debug!( - "Failed to remove aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", - coldkey, - hotkey, - netuid, - alpha_unstaked, - err - ); - Self::deposit_event(Event::FailedToRemoveAggregatedStake( - coldkey, - hotkey, - netuid, - alpha_unstaked, - )); - } else { - Self::deposit_event(Event::AggregatedStakeRemoved( - coldkey, - hotkey, - netuid, - alpha_unstaked, - )); - } - } - } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); - // Process UnstakeAll job (priority 3) - for job in unstake_all.into_iter() { - if let StakeJob::UnstakeAll { hotkey, coldkey } = job { - let result = Self::do_unstake_all( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - ); - - if let Err(err) = result { - log::debug!( - "Failed to unstake all: {:?}, {:?}, {:?}", - coldkey, - hotkey, - err - ); - Self::deposit_event(Event::AggregatedUnstakeAllFailed(coldkey, hotkey)); - } else { - Self::deposit_event(Event::AggregatedUnstakeAllSucceeded(coldkey, hotkey)); - } - } + // direct job order + let mut job_batches = vec![ + remove_stake_limit, + remove_stake, + unstake_all, + unstake_all_aplha, + add_stake_limit, + add_stake, + ]; + if altered_order { + job_batches.reverse(); } - // Process UnstakeAll job (priority 4) - for job in unstake_all_aplha.into_iter() { - if let StakeJob::UnstakeAllAlpha { hotkey, coldkey } = job { - let result = Self::do_unstake_all_alpha( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - ); - - if let Err(err) = result { - log::debug!( - "Failed to unstake all alpha: {:?}, {:?}, {:?}", - coldkey, + for jobs in job_batches.into_iter() { + for job in jobs.into_iter() { + match job { + StakeJob::RemoveStakeLimit { hotkey, - err - ); - Self::deposit_event(Event::AggregatedUnstakeAllAlphaFailed(coldkey, hotkey)); - } else { - Self::deposit_event(Event::AggregatedUnstakeAllAlphaSucceeded(coldkey, hotkey)); - } - } - } - - // Process AddStakeLimit job (priority 5) - for job in add_stake_limit.into_iter() { - if let StakeJob::AddStakeLimit { - hotkey, - coldkey, - netuid, - stake_to_be_added, - limit_price, - allow_partial, - } = job - { - let result = Self::do_add_stake_limit( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - netuid, - stake_to_be_added, - limit_price, - allow_partial, - ); - - if let Err(err) = result { - log::debug!( - "Failed to add aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", coldkey, - hotkey, netuid, - stake_to_be_added, + alpha_unstaked, limit_price, allow_partial, - err - ); - Self::deposit_event(Event::FailedToAddAggregatedLimitedStake( + } => { + let result = Self::do_remove_stake_limit( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + limit_price, + allow_partial, + ); + + if let Err(err) = result { + log::debug!( + "Failed to remove aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + err + ); + Self::deposit_event(Event::FailedToRemoveAggregatedLimitedStake( + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + )); + } else { + Self::deposit_event(Event::AggregatedLimitedStakeRemoved( + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + )); + } + } + StakeJob::RemoveStake { coldkey, hotkey, netuid, - stake_to_be_added, - limit_price, - allow_partial, - )); - } else { - Self::deposit_event(Event::AggregatedLimitedStakeAdded( - coldkey, + alpha_unstaked, + } => { + let result = Self::do_remove_stake( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + ); + + if let Err(err) = result { + log::debug!( + "Failed to remove aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + alpha_unstaked, + err + ); + Self::deposit_event(Event::FailedToRemoveAggregatedStake( + coldkey, + hotkey, + netuid, + alpha_unstaked, + )); + } else { + Self::deposit_event(Event::AggregatedStakeRemoved( + coldkey, + hotkey, + netuid, + alpha_unstaked, + )); + } + } + StakeJob::UnstakeAll { hotkey, coldkey } => { + let result = Self::do_unstake_all( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + ); + + if let Err(err) = result { + log::debug!( + "Failed to unstake all: {:?}, {:?}, {:?}", + coldkey, + hotkey, + err + ); + Self::deposit_event(Event::AggregatedUnstakeAllFailed(coldkey, hotkey)); + } else { + Self::deposit_event(Event::AggregatedUnstakeAllSucceeded( + coldkey, hotkey, + )); + } + } + StakeJob::UnstakeAllAlpha { hotkey, coldkey } => { + let result = Self::do_unstake_all_alpha( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + ); + + if let Err(err) = result { + log::debug!( + "Failed to unstake all alpha: {:?}, {:?}, {:?}", + coldkey, + hotkey, + err + ); + Self::deposit_event(Event::AggregatedUnstakeAllAlphaFailed( + coldkey, hotkey, + )); + } else { + Self::deposit_event(Event::AggregatedUnstakeAllAlphaSucceeded( + coldkey, hotkey, + )); + } + } + StakeJob::AddStakeLimit { hotkey, + coldkey, netuid, stake_to_be_added, limit_price, allow_partial, - )); - } - } - } - - // Process AddStake job (priority 6) - for job in add_stake.into_iter() { - if let StakeJob::AddStake { - hotkey, - coldkey, - netuid, - stake_to_be_added, - } = job - { - let result = Self::do_add_stake( - dispatch::RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - netuid, - stake_to_be_added, - ); - - if let Err(err) = result { - log::debug!( - "Failed to add aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", - coldkey, - hotkey, - netuid, - stake_to_be_added, - err - ); - Self::deposit_event(Event::FailedToAddAggregatedStake( - coldkey, + } => { + let result = Self::do_add_stake_limit( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + limit_price, + allow_partial, + ); + + if let Err(err) = result { + log::debug!( + "Failed to add aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + err + ); + Self::deposit_event(Event::FailedToAddAggregatedLimitedStake( + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + )); + } else { + Self::deposit_event(Event::AggregatedLimitedStakeAdded( + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + )); + } + } + StakeJob::AddStake { hotkey, - netuid, - stake_to_be_added, - )); - } else { - Self::deposit_event(Event::AggregatedStakeAdded( coldkey, - hotkey, netuid, stake_to_be_added, - )); + } => { + let result = Self::do_add_stake( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + ); + + if let Err(err) = result { + log::debug!( + "Failed to add aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + stake_to_be_added, + err + ); + Self::deposit_event(Event::FailedToAddAggregatedStake( + coldkey, + hotkey, + netuid, + stake_to_be_added, + )); + } else { + Self::deposit_event(Event::AggregatedStakeAdded( + coldkey, + hotkey, + netuid, + stake_to_be_added, + )); + } + } } } } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 91573fa488..9ab34c608f 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -129,9 +129,20 @@ fn test_add_stake_aggregate_ok_no_emission() { SubtensorModule::get_network_min_lock() ); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check if stake has increased assert_abs_diff_eq!( SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), @@ -183,9 +194,20 @@ fn test_add_stake_aggregate_failed() { amount )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!( @@ -281,7 +303,7 @@ fn test_verify_aggregated_stake_order() { )); // Enable on_finalize code to run - run_to_block_ext(2, true); + run_to_block_ext(3, true); let add_stake_position = System::events() .iter() @@ -375,6 +397,194 @@ fn test_verify_aggregated_stake_order() { }); } +#[test] +#[allow(clippy::indexing_slicing)] +fn test_verify_aggregated_stake_order_reversed() { + new_test_ext(1).execute_with(|| { + let amount = 900_000_000_000; // over the maximum + let limit_price = 6_000_000_000u64; + + // Coldkeys and hotkeys + let coldkeys = vec![ + U256::from(100), // add_stake + U256::from(200), // add_stake_limit + U256::from(300), // remove_stake + U256::from(400), // remove_stake_limit + U256::from(500), // unstake_all + U256::from(600), // unstake_all_alpha + ]; + + let hotkeys = (1..=6).map(U256::from).collect::>(); + + let netuids: Vec<_> = hotkeys + .iter() + .zip(coldkeys.iter()) + .map(|(h, c)| add_dynamic_network(h, c)) + .collect(); + + let tao_reserve = U96F32::from_num(150_000_000_000u64); + let alpha_in = U96F32::from_num(100_000_000_000u64); + + for netuid in &netuids { + SubnetTAO::::insert(*netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(*netuid, alpha_in.to_num::()); + } + + for coldkey in &coldkeys { + SubtensorModule::add_balance_to_coldkey_account(coldkey, amount); + } + + for ((hotkey, coldkey), netuid) in hotkeys.iter().zip(coldkeys.iter()).zip(netuids.iter()) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, *netuid, amount, + ); + } + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[2]), + hotkeys[2], + netuids[2], + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[3]), + hotkeys[3], + netuids[3], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[0]), + hotkeys[0], + netuids[0], + amount, + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[1]), + hotkeys[1], + netuids[1], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[4]), + hotkeys[4], + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[5]), + hotkeys[5], + )); + + // Enable on_finalize code to run + run_to_block_ext(2, false); + // Reorder jobs based on the previous block hash + let mut parent_hash = >::parent_hash(); + parent_hash.as_mut()[0] = 0b10000000; + >::set_parent_hash(parent_hash); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let add_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(.., netuid, _)) = + e.event + { + netuid == netuids[0] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let add_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded( + _, + _, + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuids[1] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(.., netuid, _)) = + e.event + { + netuid == netuids[2] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + .., + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuids[3] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_alpha_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + // Check events order + assert!(add_stake_limit_position > add_stake_position); + assert!(add_stake_position < unstake_all_alpha_position); + assert!(unstake_all_position > unstake_all_alpha_position); + assert!(remove_stake_position > unstake_all_position); + assert!(remove_stake_limit_position > remove_stake_position); + }); +} + #[test] #[allow(clippy::indexing_slicing)] fn test_verify_all_job_type_sort_by_coldkey() { @@ -505,7 +715,7 @@ fn test_verify_all_job_type_sort_by_coldkey() { )); // Finalize block - run_to_block_ext(2, true); + run_to_block_ext(3, true); // === Collect coldkeys by event type === let mut add_coldkeys = vec![]; @@ -555,6 +765,191 @@ fn test_verify_all_job_type_sort_by_coldkey() { }); } +#[test] +#[allow(clippy::indexing_slicing)] +fn test_verify_all_job_type_sort_by_coldkey_reverse_order() { + new_test_ext(1).execute_with(|| { + let amount = 1_000_000_000_000; + let limit_price = 6_000_000_000u64; + + // Coldkeys and hotkeys + let coldkeys = vec![ + U256::from(100), // add_stake + U256::from(200), // add_stake + U256::from(300), // add_stake_limit + U256::from(400), // add_stake_limit + U256::from(500), // remove_stake + U256::from(600), // remove_stake + U256::from(700), // remove_stake_limit + U256::from(800), // remove_stake_limit + U256::from(900), // unstake_all + U256::from(1000), // unstake_all + U256::from(1100), // unstake_all_alpha + U256::from(1200), // unstake_all_alpha + ]; + + let hotkeys = (1..=12).map(U256::from).collect::>(); + + let netuids: Vec<_> = hotkeys + .iter() + .zip(coldkeys.iter()) + .map(|(h, c)| add_dynamic_network(h, c)) + .collect(); + + let tao_reserve = U96F32::from_num(150_000_000_000u64); + let alpha_in = U96F32::from_num(100_000_000_000u64); + + for netuid in &netuids { + SubnetTAO::::insert(*netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(*netuid, alpha_in.to_num::()); + } + + for coldkey in &coldkeys { + SubtensorModule::add_balance_to_coldkey_account(coldkey, amount); + } + + for ((hotkey, coldkey), netuid) in hotkeys.iter().zip(coldkeys.iter()).zip(netuids.iter()) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, *netuid, amount, + ); + } + + // === Submit all job types === + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[0]), + hotkeys[0], + netuids[0], + amount + )); + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[1]), + hotkeys[1], + netuids[1], + amount + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[2]), + hotkeys[2], + netuids[2], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[3]), + hotkeys[3], + netuids[3], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[4]), + hotkeys[4], + netuids[4], + amount + )); + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[5]), + hotkeys[5], + netuids[5], + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[6]), + hotkeys[6], + netuids[6], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[7]), + hotkeys[7], + netuids[7], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[8]), + hotkeys[8], + )); + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[9]), + hotkeys[9], + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[10]), + hotkeys[10], + )); + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[11]), + hotkeys[11], + )); + + // Reorder jobs based on the previous block hash + let mut parent_hash = >::parent_hash(); + parent_hash.as_mut()[0] = 0b10000000; + >::set_parent_hash(parent_hash); + + // Finalize block + run_to_block_ext(3, true); + + // === Collect coldkeys by event type === + let mut add_coldkeys = vec![]; + let mut add_limit_coldkeys = vec![]; + let mut remove_coldkeys = vec![]; + let mut remove_limit_coldkeys = vec![]; + let mut unstake_all_coldkeys = vec![]; + let mut unstake_all_alpha_coldkeys = vec![]; + + for event in System::events().iter().map(|e| &e.event) { + match event { + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(coldkey, ..)) => { + add_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(coldkey, ..)) => { + add_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(coldkey, ..)) => { + remove_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + coldkey, + .., + )) => { + remove_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(coldkey, _)) => { + unstake_all_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded( + coldkey, + _, + )) => { + unstake_all_alpha_coldkeys.push(*coldkey); + } + _ => {} + } + } + + // === Assertions === + assert_eq!(add_coldkeys, vec![coldkeys[0], coldkeys[1]]); // ascending (reversed) + assert_eq!(add_limit_coldkeys, vec![coldkeys[2], coldkeys[3]]); // ascending (reversed) + assert_eq!(remove_coldkeys, vec![coldkeys[5], coldkeys[4]]); // descending (reversed) + assert_eq!(remove_limit_coldkeys, vec![coldkeys[7], coldkeys[6]]); // descending (reversed) + assert_eq!(unstake_all_coldkeys, vec![coldkeys[9], coldkeys[8]]); // descending (reversed) + assert_eq!(unstake_all_alpha_coldkeys, vec![coldkeys[11], coldkeys[10]]); // descending (reversed) + }); +} + #[test] fn test_dividends_with_run_to_block() { new_test_ext(1).execute_with(|| { @@ -934,9 +1329,20 @@ fn test_remove_stake_aggregate_ok_no_emission() { amount )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + let fee = SubtensorModule::calculate_staking_fee( Some((&hotkey_account_id, netuid)), &coldkey_account_id, @@ -990,9 +1396,20 @@ fn test_remove_stake_aggregate_fail() { amount )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!( @@ -4320,9 +4737,20 @@ fn test_add_stake_limit_aggregate_ok() { true )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check if stake has increased only by 75 Alpha assert_abs_diff_eq!( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -4386,9 +4814,20 @@ fn test_add_stake_limit_aggregate_fail() { true )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedLimitedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!( @@ -4562,9 +5001,20 @@ fn test_remove_stake_limit_aggregate_ok() { true )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check if stake has decreased only by assert_abs_diff_eq!( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -4621,9 +5071,20 @@ fn test_remove_stake_limit_aggregate_fail() { true )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedLimitedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!( @@ -5242,9 +5703,20 @@ fn test_unstake_all_alpha_aggregate_works() { hotkey, )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); @@ -5273,9 +5745,20 @@ fn test_unstake_all_alpha_aggregate_fails() { hotkey, )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaFailed(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!( @@ -5372,9 +5855,20 @@ fn test_unstake_all_aggregate_works() { hotkey, )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); @@ -5403,9 +5897,20 @@ fn test_unstake_all_aggregate_fails() { hotkey, )); - // Enable on_finalize code to run + // Check for the block delay run_to_block_ext(2, true); + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllFailed(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check that event was emitted. assert!(System::events().iter().any(|e| { matches!(