diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index badb811fa..6689b7060 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -191,18 +191,13 @@ impl Pallet { mining_emission: u64, ) { // --- 1. First, calculate the hotkey's share of the emission. - let take_proportion: I64F64 = I64F64::from_num(Self::get_childkey_take(hotkey, netuid)) - .saturating_div(I64F64::from_num(u16::MAX)); - let hotkey_take: u64 = take_proportion - .saturating_mul(I64F64::from_num(validating_emission)) - .to_num::(); - // NOTE: Only the validation emission should be split amongst parents. - - // --- 2. Compute the remaining emission after the hotkey's share is deducted. - let emission_minus_take: u64 = validating_emission.saturating_sub(hotkey_take); + let childkey_take_proportion: I64F64 = + I64F64::from_num(Self::get_childkey_take(hotkey, netuid)) + .saturating_div(I64F64::from_num(u16::MAX)); + let mut total_childkey_take: u64 = 0; // --- 3. Track the remaining emission for accounting purposes. - let mut remaining_emission: u64 = emission_minus_take; + let mut remaining_emission: u64 = validating_emission; // --- 4. Calculate the total stake of the hotkey, adjusted by the stakes of parents and children. // Parents contribute to the stake, while children reduce it. @@ -222,9 +217,19 @@ impl Pallet { ); let proportion_from_parent: I96F32 = stake_from_parent.saturating_div(I96F32::from_num(total_hotkey_stake)); - let parent_emission_take: u64 = proportion_from_parent - .saturating_mul(I96F32::from_num(emission_minus_take)) + let parent_emission: u64 = proportion_from_parent + .saturating_mul(I96F32::from_num(validating_emission)) + .to_num::(); + + // --- 5.3 Childkey take as part of parent emission + let child_emission_take: u64 = childkey_take_proportion + .saturating_mul(I64F64::from_num(parent_emission)) .to_num::(); + total_childkey_take = total_childkey_take.saturating_add(child_emission_take); + // NOTE: Only the validation emission should be split amongst parents. + + // --- 5.4 Compute the remaining parent emission after the childkey's share is deducted. + let parent_emission_take: u64 = parent_emission.saturating_sub(child_emission_take); // --- 5.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { @@ -232,7 +237,9 @@ impl Pallet { }); // --- 5.6. Subtract the parent's share from the remaining emission for this hotkey. - remaining_emission = remaining_emission.saturating_sub(parent_emission_take); + remaining_emission = remaining_emission + .saturating_sub(parent_emission_take) + .saturating_sub(child_emission_take); } } @@ -240,10 +247,17 @@ impl Pallet { PendingdHotkeyEmission::::mutate(hotkey, |hotkey_pending| { *hotkey_pending = hotkey_pending.saturating_add( remaining_emission - .saturating_add(hotkey_take) + .saturating_add(total_childkey_take) .saturating_add(mining_emission), ) }); + + // --- 7. Update untouchable part of hotkey emission (that will not be distributed to nominators) + // This doesn't include remaining_emission, which should be distributed in drain_hotkey_emission + PendingdHotkeyEmissionUntouchable::::mutate(hotkey, |hotkey_pending| { + *hotkey_pending = + hotkey_pending.saturating_add(total_childkey_take.saturating_add(mining_emission)) + }); } //. --- 4. Drains the accumulated hotkey emission through to the nominators. The hotkey takes a proportion of the emission. @@ -262,8 +276,14 @@ impl Pallet { // --- 0. For accounting purposes record the total new added stake. let mut total_new_tao: u64 = 0; + // Get the untouchable part of pending hotkey emission, so that we don't distribute this part of + // PendingdHotkeyEmission to nominators + let untouchable_emission = PendingdHotkeyEmissionUntouchable::::get(hotkey); + let emission_to_distribute = emission.saturating_sub(untouchable_emission); + // --- 1.0 Drain the hotkey emission. PendingdHotkeyEmission::::insert(hotkey, 0); + PendingdHotkeyEmissionUntouchable::::insert(hotkey, 0); // --- 2 Update the block value to the current block number. LastHotkeyEmissionDrain::::insert(hotkey, block_number); @@ -272,13 +292,16 @@ impl Pallet { let total_hotkey_stake: u64 = Self::get_total_stake_for_hotkey(hotkey); // --- 4 Calculate the emission take for the hotkey. + // This is only the hotkey take. Childkey take was already deducted from validator emissions in + // accumulate_hotkey_emission and now it is included in untouchable_emission. let take_proportion: I64F64 = I64F64::from_num(Delegates::::get(hotkey)) .saturating_div(I64F64::from_num(u16::MAX)); - let hotkey_take: u64 = - (take_proportion.saturating_mul(I64F64::from_num(emission))).to_num::(); + let hotkey_take: u64 = (take_proportion + .saturating_mul(I64F64::from_num(emission_to_distribute))) + .to_num::(); - // --- 5 Compute the remaining emission after deducting the hotkey's take. - let emission_minus_take: u64 = emission.saturating_sub(hotkey_take); + // --- 5 Compute the remaining emission after deducting the hotkey's take and untouchable_emission. + let emission_minus_take: u64 = emission_to_distribute.saturating_sub(hotkey_take); // --- 6 Calculate the remaining emission after the hotkey's take. let mut remainder: u64 = emission_minus_take; @@ -319,8 +342,11 @@ impl Pallet { } } - // --- 13 Finally, add the stake to the hotkey itself, including its take and the remaining emission. - let hotkey_new_tao: u64 = hotkey_take.saturating_add(remainder); + // --- 13 Finally, add the stake to the hotkey itself, including its take, the remaining emission, and + // the untouchable_emission (part of pending hotkey emission that consists of mining emission and childkey take) + let hotkey_new_tao: u64 = hotkey_take + .saturating_add(remainder) + .saturating_add(untouchable_emission); Self::increase_stake_on_hotkey_account(hotkey, hotkey_new_tao); // --- 14 Record new tao creation event and return the amount created. diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index d93253cfa..3453bf4b7 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -796,6 +796,16 @@ pub mod pallet { DefaultAccumulatedEmission, >; #[pallet::storage] + /// Map ( hot ) --> emission | Part of accumulated hotkey emission that will not be distributed to nominators. + pub type PendingdHotkeyEmissionUntouchable = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + u64, + ValueQuery, + DefaultAccumulatedEmission, + >; + #[pallet::storage] /// Map ( hot, cold ) --> stake: i128 | Stake added/removed since last emission drain. pub type StakeDeltaSinceLastEmissionDrain = StorageDoubleMap< _, diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 2b99030ab..0b2aea563 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3237,3 +3237,468 @@ fn test_rank_trust_incentive_calculation_with_parent_child() { }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_childkey_set_weights_single_parent --exact --nocapture +#[test] +fn test_childkey_set_weights_single_parent() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + // Define hotkeys + let parent: U256 = U256::from(1); + let child: U256 = U256::from(2); + let weight_setter: U256 = U256::from(3); + + // Define coldkeys with more readable names + let coldkey_parent: U256 = U256::from(100); + let coldkey_child: U256 = U256::from(101); + let coldkey_weight_setter: U256 = U256::from(102); + + let stake_to_give_child = 109_999; + + // Register parent with minimal stake and child with high stake + SubtensorModule::add_balance_to_coldkey_account(&coldkey_parent, 1); + SubtensorModule::add_balance_to_coldkey_account(&coldkey_child, stake_to_give_child + 10); + SubtensorModule::add_balance_to_coldkey_account(&coldkey_weight_setter, 1_000_000); + + // Add neurons for parent, child and weight_setter + register_ok_neuron(netuid, parent, coldkey_parent, 1); + register_ok_neuron(netuid, child, coldkey_child, 1); + register_ok_neuron(netuid, weight_setter, coldkey_weight_setter, 1); + + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey_parent, + &parent, + stake_to_give_child, + ); + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey_weight_setter, + &weight_setter, + 1_000_000, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Set parent-child relationship + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey_parent), + parent, + netuid, + vec![(u64::MAX, child)] + )); + step_block(7200 + 1); + // Set weights on the child using the weight_setter account + let origin = RuntimeOrigin::signed(weight_setter); + let uids: Vec = vec![1]; // Only set weight for the child (UID 1) + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + assert_ok!(SubtensorModule::set_weights( + origin, + netuid, + uids.clone(), + values.clone(), + version_key + )); + + // Set the min stake very high + SubtensorModule::set_weights_min_stake(stake_to_give_child * 5); + + // Check the child has less stake than required + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid) + < SubtensorModule::get_weights_min_stake() + ); + + // Check the child cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&child)); + + // Set a minimum stake to set weights + SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); + + // Check if the stake for the child is above + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid) + >= SubtensorModule::get_weights_min_stake() + ); + + // Check the child can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&child)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_set_weights_no_parent --exact --nocapture +#[test] +fn test_set_weights_no_parent() { + // Verify that a regular key without a parent delegation is effected by the minimum stake requirements + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + let hotkey: U256 = U256::from(2); + let spare_hk: U256 = U256::from(3); + + let coldkey: U256 = U256::from(101); + let spare_ck = U256::from(102); + + let stake_to_give_child = 109_999; + + SubtensorModule::add_balance_to_coldkey_account(&coldkey, stake_to_give_child + 10); + + // Is registered + register_ok_neuron(netuid, hotkey, coldkey, 1); + // Register a spare key + register_ok_neuron(netuid, spare_hk, spare_ck, 1); + + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &coldkey, + &hotkey, + stake_to_give_child, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Has stake and no parent + step_block(7200 + 1); + + let uids: Vec = vec![1]; // Set weights on the other hotkey + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + + // Set the min stake very high + SubtensorModule::set_weights_min_stake(stake_to_give_child * 5); + + // Check the key has less stake than required + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid) + < SubtensorModule::get_weights_min_stake() + ); + + // Check the hotkey cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&hotkey)); + + // Set a minimum stake to set weights + SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); + + // Check if the stake for the hotkey is above + assert!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid) + >= SubtensorModule::get_weights_min_stake() + ); + + // Check the hotkey can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&hotkey)); + }); +} + +/// Test that drain_hotkey_emission sends childkey take fully to the childkey. +#[test] +fn test_childkey_take_drain() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let nominator = U256::from(4); + let netuid: u16 = 1; + let root_id: u16 = 0; + let subnet_tempo = 10; + let hotkey_tempo = 20; + let stake = 100_000_000_000; + let proportion: u64 = u64::MAX; + + // Add network, register hotkeys, and setup network parameters + add_network(root_id, subnet_tempo, 0); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, child, coldkey, 0); + register_ok_neuron(netuid, parent, coldkey, 1); + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::add_balance_to_coldkey_account( + &nominator, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_hotkey_emission_tempo(hotkey_tempo); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 2); + step_block(subnet_tempo); + pallet_subtensor::SubnetOwnerCut::::set(0); + + // Set 20% childkey take + let max_take: u16 = 0xFFFF / 5; + SubtensorModule::set_max_childkey_take(max_take); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + child, + netuid, + max_take + )); + + // Set zero hotkey take for childkey + SubtensorModule::set_min_delegate_take(0); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + child, + 0 + )); + + // Set zero hotkey take for parent + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + parent, + 0 + )); + + // Setup stakes: + // Stake from parent + // Stake from nominator to childkey + // Give 100% of parent stake to childkey + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + parent, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + child, + stake + )); + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)] + )); + // Make all stakes viable + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(parent, coldkey, -1); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(child, nominator, -1); + + // Setup YUMA so that it creates emissions: + // Parent and child both set weights + // Parent and child register on root and + // Set root weights + pallet_subtensor::Weights::::insert(netuid, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + parent, + )); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + child, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(root_id, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); + + // Run run_coinbase until PendingHotkeyEmission are populated + while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { + step_block(1); + } + + // Prevent further subnet epochs + pallet_subtensor::Tempo::::set(netuid, u16::MAX); + pallet_subtensor::Tempo::::set(root_id, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission is drained for both child and parent + step_block((hotkey_tempo * 2) as u16); + + // Verify how emission is split between keys + // - Child stake increased by its child key take only (20% * 50% = 10% of total emission) + // - Parent stake increased by 40% of total emission + // - Nominator stake increased by 50% of total emission + let child_emission = pallet_subtensor::Stake::::get(child, coldkey); + let parent_emission = pallet_subtensor::Stake::::get(parent, coldkey) - stake; + let nominator_emission = pallet_subtensor::Stake::::get(child, nominator) - stake; + let total_emission = child_emission + parent_emission + nominator_emission; + + assert!(is_within_tolerance( + child_emission, + total_emission / 10, + 500 + )); + assert!(is_within_tolerance( + parent_emission, + total_emission / 10 * 4, + 500 + )); + assert!(is_within_tolerance( + nominator_emission, + total_emission / 2, + 500 + )); + }); +} + +/// Test that drain_hotkey_emission sends childkey take fully to the childkey with validator take enabled. +#[test] +fn test_childkey_take_drain_validator_take() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let nominator = U256::from(4); + let netuid: u16 = 1; + let root_id: u16 = 0; + let subnet_tempo = 10; + let hotkey_tempo = 20; + let stake = 100_000_000_000; + let proportion: u64 = u64::MAX; + + // Add network, register hotkeys, and setup network parameters + add_network(root_id, subnet_tempo, 0); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, child, coldkey, 0); + register_ok_neuron(netuid, parent, coldkey, 1); + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::add_balance_to_coldkey_account( + &nominator, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_hotkey_emission_tempo(hotkey_tempo); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 2); + step_block(subnet_tempo); + pallet_subtensor::SubnetOwnerCut::::set(0); + + // Set 20% childkey take + let max_take: u16 = 0xFFFF / 5; + SubtensorModule::set_max_childkey_take(max_take); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + child, + netuid, + max_take + )); + + // Set 20% hotkey take for childkey + SubtensorModule::set_max_delegate_take(max_take); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + child, + max_take + )); + + // Set 20% hotkey take for parent + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + parent, + max_take + )); + + // Setup stakes: + // Stake from parent + // Stake from nominator to childkey + // Give 100% of parent stake to childkey + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + parent, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + child, + stake + )); + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)] + )); + // Make all stakes viable + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(parent, coldkey, -1); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(child, nominator, -1); + + // Setup YUMA so that it creates emissions: + // Parent and child both set weights + // Parent and child register on root and + // Set root weights + pallet_subtensor::Weights::::insert(netuid, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + parent, + )); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + child, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(root_id, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); + + // Run run_coinbase until PendingHotkeyEmission are populated + while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { + step_block(1); + } + + // Prevent further subnet epochs + pallet_subtensor::Tempo::::set(netuid, u16::MAX); + pallet_subtensor::Tempo::::set(root_id, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission is drained for both child and parent + step_block((hotkey_tempo * 2) as u16); + + // Verify how emission is split between keys + // - Child stake increased by its child key take (20% * 50% = 10% of total emission) plus childkey's delegate take (10%) + // - Parent stake increased by 40% of total emission + // - Nominator stake increased by 40% of total emission + let child_emission = pallet_subtensor::Stake::::get(child, coldkey); + let parent_emission = pallet_subtensor::Stake::::get(parent, coldkey) - stake; + let nominator_emission = pallet_subtensor::Stake::::get(child, nominator) - stake; + let total_emission = child_emission + parent_emission + nominator_emission; + + assert!(is_within_tolerance(child_emission, total_emission / 5, 500)); + assert!(is_within_tolerance( + parent_emission, + total_emission / 10 * 4, + 500 + )); + assert!(is_within_tolerance( + nominator_emission, + total_emission / 10 * 4, + 500 + )); + }); +} diff --git a/pallets/subtensor/tests/epoch.rs b/pallets/subtensor/tests/epoch.rs index 9c4bf87cc..df2c95d81 100644 --- a/pallets/subtensor/tests/epoch.rs +++ b/pallets/subtensor/tests/epoch.rs @@ -2857,7 +2857,7 @@ fn test_blocks_since_last_step() { /// * `left` - The first value to compare. /// * `right` - The second value to compare. /// * `epsilon` - The maximum allowed difference between the two values. -fn assert_approx_eq(left: I32F32, right: I32F32, epsilon: I32F32) { +pub fn assert_approx_eq(left: I32F32, right: I32F32, epsilon: I32F32) { if (left - right).abs() > epsilon { panic!( "assertion failed: `(left ≈ right)`\n left: `{:?}`,\n right: `{:?}`,\n epsilon: `{:?}`", diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index f053c7ca6..9b45e8b33 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2306,3 +2306,493 @@ fn test_get_total_delegated_stake_exclude_owner_stake() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test staking -- test_stake_delta_tracks_adds_and_removes --exact --nocapture +#[test] +fn test_stake_delta_tracks_adds_and_removes() { + new_test_ext(1).execute_with(|| { + let netuid = 1u16; + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + + let owner_stake = 1000; + let owner_added_stake = 123; + let owner_removed_stake = 456; + // Add more than removed to test that the delta is updated correctly + let owner_adds_more_stake = owner_removed_stake + 1; + + let delegator_added_stake = 999; + + // Set stake rate limit very high + TargetStakesPerInterval::::put(1e9 as u64); + + add_network(netuid, 0, 0); + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + // Give extra stake to the owner + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &delegate_coldkey, + &delegate_hotkey, + owner_stake, + ); + + // Register as a delegate + assert_ok!(SubtensorModule::become_delegate( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey + )); + + // Verify that the stake delta is empty + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + 0 + ); + + // Give the coldkey some balance; extra just in case + SubtensorModule::add_balance_to_coldkey_account( + &delegate_coldkey, + owner_added_stake + owner_adds_more_stake, + ); + + // Add some stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_added_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Add some stake from a delegator + SubtensorModule::add_balance_to_coldkey_account(&delegator, delegator_added_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + delegator_added_stake + )); + + // Verify that the stake delta is unchanged for the owner + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Remove some stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_removed_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake).saturating_sub_unsigned(owner_removed_stake.into()) + ); + + // Add more stake than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_adds_more_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + .saturating_add_unsigned((owner_adds_more_stake - owner_removed_stake).into()) + ); + }); +} + +/// Test that drain_hotkey_emission sends mining emission fully to the miner, even +/// if miner is a delegate and someone is delegating. +#[test] +fn test_mining_emission_drain() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let validator = U256::from(2); + let miner = U256::from(3); + let nominator = U256::from(4); + let netuid: u16 = 1; + let root_id: u16 = 0; + let root_tempo = 9; // neet root epoch to happen before subnet tempo + let subnet_tempo = 10; + let hotkey_tempo = 20; + let stake = 100_000_000_000; + let miner_stake = 1_000_000_000; + + // Add network, register hotkeys, and setup network parameters + add_network(root_id, root_tempo, 0); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, validator, coldkey, 0); + register_ok_neuron(netuid, miner, coldkey, 1); + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + 2 * stake + ExistentialDeposit::get(), + ); + SubtensorModule::add_balance_to_coldkey_account( + &nominator, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_hotkey_emission_tempo(hotkey_tempo); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + pallet_subtensor::SubnetOwnerCut::::set(0); + // All stake is active + pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); + // There's only one validator + pallet_subtensor::MaxAllowedUids::::set(netuid, 2); + SubtensorModule::set_max_allowed_validators(netuid, 1); + + // Set zero hotkey take for validator + SubtensorModule::set_min_delegate_take(0); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + validator, + 0 + )); + + // Set zero hotkey take for miner + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + miner, + 0 + )); + + // Setup stakes: + // Stake from validator + // Stake from miner + // Stake from nominator to miner + // Give 100% of parent stake to childkey + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + miner, + miner_stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + miner, + stake + )); + // Make all stakes viable + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(validator, coldkey, -1); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(miner, nominator, -1); + + // Setup YUMA so that it creates emissions: + // Validator sets weight for miner + // Validator registers on root and + // Sets root weights + // Last weight update is after block at registration + pallet_subtensor::Weights::::insert(netuid, 0, vec![(1, 0xFFFF)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + validator, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::BlockAtRegistration::::set(netuid, 0, 1); + pallet_subtensor::LastUpdate::::set(netuid, vec![2, 2]); + pallet_subtensor::Kappa::::set(netuid, u16::MAX / 5); + + // Run run_coinbase until root epoch is run + while pallet_subtensor::PendingEmission::::get(netuid) == 0 { + step_block(1); + } + + // Prevent further root epochs + pallet_subtensor::Tempo::::set(root_id, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission are populated + while pallet_subtensor::PendingdHotkeyEmission::::get(miner) == 0 { + step_block(1); + } + + // Prevent further subnet epochs + pallet_subtensor::Tempo::::set(netuid, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission is drained for both validator and miner + step_block((hotkey_tempo * 2) as u16); + + // Verify how emission is split between keys + // - Validator stake increased by 50% of total emission + // - Miner stake increased by 50% of total emission + // - Nominator gets nothing because he staked to miner + let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey) - miner_stake; + let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + let nominator_emission = pallet_subtensor::Stake::::get(miner, nominator) - stake; + let total_emission = validator_emission + miner_emission + nominator_emission; + + assert_eq!(validator_emission, total_emission / 2); + assert_eq!(miner_emission, total_emission / 2); + assert_eq!(nominator_emission, 0); + }); +} + +/// Test that drain_hotkey_emission sends mining emission fully to the miner, even +/// if miner is a delegate and someone is delegating, and miner gets some validation emissions +#[test] +fn test_mining_emission_drain_with_validation() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let validator_miner1 = U256::from(2); + let validator_miner2 = U256::from(3); + let nominator = U256::from(4); + let netuid: u16 = 1; + let root_id: u16 = 0; + let root_tempo = 9; // neet root epoch to happen before subnet tempo + let subnet_tempo = 10; + let hotkey_tempo = 20; + let stake = 100_000_000_000; + let half_stake = 50_000_000_000; + + // Add network, register hotkeys, and setup network parameters + add_network(root_id, root_tempo, 0); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, validator_miner1, coldkey, 0); + register_ok_neuron(netuid, validator_miner2, coldkey, 1); + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + 2 * stake + ExistentialDeposit::get(), + ); + SubtensorModule::add_balance_to_coldkey_account( + &nominator, + stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_hotkey_emission_tempo(hotkey_tempo); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + pallet_subtensor::SubnetOwnerCut::::set(0); + // All stake is active + pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); + // There are two validators + pallet_subtensor::MaxAllowedUids::::set(netuid, 2); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Set zero hotkey take for validator + SubtensorModule::set_min_delegate_take(0); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + validator_miner1, + 0 + )); + + // Set zero hotkey take for miner + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + validator_miner2, + 0 + )); + + // Setup stakes: + // Stake from validator + // Stake from miner + // Stake from nominator to miner + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_miner1, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_miner2, + half_stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + validator_miner2, + half_stake + )); + // Make all stakes viable + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set( + validator_miner1, + coldkey, + -1, + ); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set( + validator_miner2, + coldkey, + -1, + ); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set( + validator_miner2, + nominator, + -1, + ); + + // Setup YUMA so that it creates emissions: + // Validators set weights for each other + // Validator registers on root and + // Sets root weights + // Last weight update is after block at registration + pallet_subtensor::Weights::::insert(netuid, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + validator_miner1, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::BlockAtRegistration::::set(netuid, 0, 1); + pallet_subtensor::BlockAtRegistration::::set(netuid, 1, 1); + pallet_subtensor::LastUpdate::::set(netuid, vec![2, 2]); + pallet_subtensor::Kappa::::set(netuid, u16::MAX / 5); + + // Run run_coinbase until root epoch is run + while pallet_subtensor::PendingEmission::::get(netuid) == 0 { + step_block(1); + } + + // Prevent further root epochs + pallet_subtensor::Tempo::::set(root_id, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission are populated + while pallet_subtensor::PendingdHotkeyEmission::::get(validator_miner1) == 0 { + step_block(1); + } + + // Prevent further subnet epochs + pallet_subtensor::Tempo::::set(netuid, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission is drained for both validator and miner + step_block((hotkey_tempo * 2) as u16); + + // Verify how emission is split between keys + // - 50% goes to miners and 50% goes to validators + // - Miner's reward is treated as half miner and half validator + // - Neuron 1 stake is increased by 50% of total emission + // - Neuron 2 stake is increased by 37.5% of total emission (mining portion is intact, validation portion is split 50%) + // - Nominator stake is increased by 12.5% of total emission (validation portion is distributed in 50% proportion) + let validator_miner_emission1 = + pallet_subtensor::Stake::::get(validator_miner1, coldkey) - stake; + let validator_miner_emission2 = + pallet_subtensor::Stake::::get(validator_miner2, coldkey) - half_stake; + let nominator_emission = + pallet_subtensor::Stake::::get(validator_miner2, nominator) - half_stake; + let total_emission = + validator_miner_emission1 + validator_miner_emission2 + nominator_emission; + + assert_eq!(validator_miner_emission1, total_emission / 2); + assert_eq!(validator_miner_emission2, total_emission / 1000 * 375); + assert_eq!(nominator_emission, total_emission / 1000 * 125); + }); +} + +/// Test that drain_hotkey_emission sends mining emission fully to the miners, for the +/// case of one validator, one vali-miner, and one miner +#[test] +fn test_mining_emission_drain_validator_valiminer_miner() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let validator = U256::from(2); + let validator_miner = U256::from(3); + let miner = U256::from(4); + let netuid: u16 = 1; + let root_id: u16 = 0; + let root_tempo = 9; // neet root epoch to happen before subnet tempo + let subnet_tempo = 10; + let hotkey_tempo = 20; + let stake = 100_000_000_000; + + // Add network, register hotkeys, and setup network parameters + add_network(root_id, root_tempo, 0); + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, validator, coldkey, 0); + register_ok_neuron(netuid, validator_miner, coldkey, 1); + register_ok_neuron(netuid, miner, coldkey, 2); + SubtensorModule::add_balance_to_coldkey_account( + &coldkey, + 3 * stake + ExistentialDeposit::get(), + ); + SubtensorModule::set_hotkey_emission_tempo(hotkey_tempo); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + pallet_subtensor::SubnetOwnerCut::::set(0); + // All stake is active + pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); + // There are two validators and three neurons + pallet_subtensor::MaxAllowedUids::::set(netuid, 3); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Setup stakes: + // Stake from validator + // Stake from valiminer + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator, + stake + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_miner, + stake + )); + // Make all stakes viable + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(validator, coldkey, -1); + pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set( + validator_miner, + coldkey, + -1, + ); + + // Setup YUMA so that it creates emissions: + // Validator 1 sets weight for valiminer |- to achieve equal incentive for both miners + // Valiminer sets weights for the second miner | + // Validator registers on root and + // Sets root weights + // Last weight update is after block at registration + pallet_subtensor::Weights::::insert(netuid, 0, vec![(1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 1, vec![(2, 0xFFFF)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + validator, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::BlockAtRegistration::::set(netuid, 0, 1); + pallet_subtensor::BlockAtRegistration::::set(netuid, 1, 1); + pallet_subtensor::LastUpdate::::set(netuid, vec![2, 2, 2]); + pallet_subtensor::Kappa::::set(netuid, u16::MAX / 5); + + // Run run_coinbase until root epoch is run + while pallet_subtensor::PendingEmission::::get(netuid) == 0 { + step_block(1); + } + + // Prevent further root epochs + pallet_subtensor::Tempo::::set(root_id, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission are populated + while pallet_subtensor::PendingdHotkeyEmission::::get(validator) == 0 { + step_block(1); + } + + // Prevent further subnet epochs + pallet_subtensor::Tempo::::set(netuid, u16::MAX); + + // Run run_coinbase until PendingHotkeyEmission is drained for both validator and miner + step_block((hotkey_tempo * 2) as u16); + + // Verify how emission is split between keys + // - 50% goes to miners and 50% goes to validators + // - Validator gets 25% because there are two validators + // - Valiminer gets 25% as a validator and 25% as miner + // - Miner gets 25% as miner + let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + let valiminer_emission = + pallet_subtensor::Stake::::get(validator_miner, coldkey) - stake; + let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey); + let total_emission = validator_emission + valiminer_emission + miner_emission; + + assert_eq!(validator_emission, total_emission / 4); + assert_eq!(valiminer_emission, total_emission / 2); + assert_eq!(miner_emission, total_emission / 4); + }); +} diff --git a/pallets/subtensor/tests/swap_hotkey.rs b/pallets/subtensor/tests/swap_hotkey.rs index 206cc324f..59d114cf1 100644 --- a/pallets/subtensor/tests/swap_hotkey.rs +++ b/pallets/subtensor/tests/swap_hotkey.rs @@ -9,6 +9,7 @@ use mock::*; use pallet_subtensor::*; use sp_core::H256; use sp_core::U256; +use sp_runtime::SaturatedConversion; // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_owner --exact --nocapture #[test] @@ -1149,6 +1150,94 @@ fn test_swap_complex_parent_child_structure() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_hotkey_swap_stake_delta --exact --nocapture +#[test] +fn test_hotkey_swap_stake_delta() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(3); + let new_hotkey = U256::from(4); + let coldkey = U256::from(7); + + let coldkeys = [U256::from(1), U256::from(2), U256::from(5)]; + + let mut weight = Weight::zero(); + + // Set up initial state + // Add stake delta for each coldkey and the old_hotkey + for &coldkey in coldkeys.iter() { + StakeDeltaSinceLastEmissionDrain::::insert( + old_hotkey, + coldkey, + (123 + coldkey.saturated_into::()), + ); + + StakingHotkeys::::insert(coldkey, vec![old_hotkey]); + } + + // Add stake delta for one coldkey and the new_hotkey + StakeDeltaSinceLastEmissionDrain::::insert(new_hotkey, coldkeys[0], 456); + // Add corresponding StakingHotkeys + StakingHotkeys::::insert(coldkeys[0], vec![old_hotkey, new_hotkey]); + + // Perform the swap + SubtensorModule::perform_hotkey_swap(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Ensure the stake delta is correctly transferred for each coldkey + // -- coldkey[0] maintains its stake delta from the new_hotkey and the old_hotkey + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(new_hotkey, coldkeys[0]), + 123 + coldkeys[0].saturated_into::() + 456 + ); + // -- coldkey[1..] maintains its stake delta from the old_hotkey + for &coldkey in coldkeys[1..].iter() { + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(new_hotkey, coldkey), + 123 + coldkey.saturated_into::() + ); + assert!(!StakeDeltaSinceLastEmissionDrain::::contains_key( + old_hotkey, coldkey + )); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey -- test_swap_hotkey_with_pending_emissions --exact --nocapture +#[test] +fn test_swap_hotkey_with_pending_emissions() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = 0u16; + let mut weight = Weight::zero(); + + let pending_emission = 123_456_789u64; + + // Set up initial state + add_network(netuid, 0, 1); + + // Set up pending emissions + PendingdHotkeyEmission::::insert(old_hotkey, pending_emission); + // Verify the pending emissions are set + assert_eq!( + PendingdHotkeyEmission::::get(old_hotkey), + pending_emission + ); + // Verify the new hotkey does not have any pending emissions + assert!(!PendingdHotkeyEmission::::contains_key(new_hotkey)); + + // Perform the swap + SubtensorModule::perform_hotkey_swap(&old_hotkey, &new_hotkey, &coldkey, &mut weight); + + // Verify the pending emissions are transferred + assert_eq!( + PendingdHotkeyEmission::::get(new_hotkey), + pending_emission + ); + assert!(!PendingdHotkeyEmission::::contains_key(old_hotkey)); + }); +} + #[test] fn test_swap_parent_hotkey_childkey_maps() { new_test_ext(1).execute_with(|| { diff --git a/support/procedural-fork/src/pallet/parse/storage.rs b/support/procedural-fork/src/pallet/parse/storage.rs index 811832427..64a5e685b 100644 --- a/support/procedural-fork/src/pallet/parse/storage.rs +++ b/support/procedural-fork/src/pallet/parse/storage.rs @@ -718,11 +718,11 @@ fn process_generics( "CountedStorageNMap" => StorageKind::CountedNMap, found => { let msg = format!( - "Invalid pallet::storage, expected ident: `StorageValue` or \ + "Invalid pallet::storage, expected ident: `StorageValue` or \ `StorageMap` or `CountedStorageMap` or `StorageDoubleMap` or `StorageNMap` or `CountedStorageNMap` \ in order to expand metadata, found `{}`.", - found, - ); + found, + ); return Err(syn::Error::new(segment.ident.span(), msg)); } }; diff --git a/support/procedural-fork/src/runtime/parse/mod.rs b/support/procedural-fork/src/runtime/parse/mod.rs index a6a49e814..494ab2c53 100644 --- a/support/procedural-fork/src/runtime/parse/mod.rs +++ b/support/procedural-fork/src/runtime/parse/mod.rs @@ -255,19 +255,19 @@ impl Def { }; let def = Def { - input, - runtime_struct: runtime_struct.ok_or_else(|| { - syn::Error::new(item_span, + input, + runtime_struct: runtime_struct.ok_or_else(|| { + syn::Error::new(item_span, "Missing Runtime. Please add a struct inside the module and annotate it with `#[runtime::runtime]`" ) - })?, - pallets, - runtime_types: runtime_types.ok_or_else(|| { - syn::Error::new(item_span, + })?, + pallets, + runtime_types: runtime_types.ok_or_else(|| { + syn::Error::new(item_span, "Missing Runtime Types. Please annotate the runtime struct with `#[runtime::derive]`" ) - })?, - }; + })?, + }; Ok(def) }