From 14102b6363e5adcf1e71d6a984d8b48b92202cf6 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 8 Nov 2024 11:53:05 -0500 Subject: [PATCH 1/4] Fix childkey take and miner emission distribution --- .../subtensor/src/coinbase/run_coinbase.rs | 86 ++-- pallets/subtensor/src/lib.rs | 10 + pallets/subtensor/tests/children.rs | 274 +++++++++++++ pallets/subtensor/tests/epoch.rs | 2 +- pallets/subtensor/tests/staking.rs | 387 ++++++++++++++++++ 5 files changed, 728 insertions(+), 31 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index aa3fa17d23..08098b49db 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -199,59 +199,73 @@ 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); - - // --- 3. Track the remaining emission for accounting purposes. - let mut remaining_emission: u64 = emission_minus_take; - - // --- 4. Calculate the total stake of the hotkey, adjusted by the stakes of parents and children. + 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; + + // --- 2. Track the remaining emission for accounting purposes. + let mut remaining_emission: u64 = validating_emission; + + // --- 3. Calculate the total stake of the hotkey, adjusted by the stakes of parents and children. // Parents contribute to the stake, while children reduce it. // If this value is zero, no distribution to anyone is necessary. let total_hotkey_stake: u64 = Self::get_stake_for_hotkey_on_subnet(hotkey, netuid); if total_hotkey_stake != 0 { - // --- 5. If the total stake is not zero, iterate over each parent to determine their contribution to the hotkey's stake, + // --- 4. If the total stake is not zero, iterate over each parent to determine their contribution to the hotkey's stake, // and calculate their share of the emission accordingly. for (proportion, parent) in Self::get_parents(hotkey, netuid) { - // --- 5.1 Retrieve the parent's stake. This is the raw stake value including nominators. + // --- 4.1 Retrieve the parent's stake. This is the raw stake value including nominators. let parent_stake: u64 = Self::get_total_stake_for_hotkey(&parent); - // --- 5.2 Calculate the portion of the hotkey's total stake contributed by this parent. + // --- 4.2 Calculate the portion of the hotkey's total stake contributed by this parent. // Then, determine the parent's share of the remaining emission. let stake_from_parent: I96F32 = I96F32::from_num(parent_stake).saturating_mul( I96F32::from_num(proportion).saturating_div(I96F32::from_num(u64::MAX)), ); 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.5. Accumulate emissions for the parent hotkey. + // --- 4.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. + + // --- 4.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); + + // --- 4.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { *parent_accumulated = parent_accumulated.saturating_add(parent_emission_take) }); - // --- 5.6. Subtract the parent's share from the remaining emission for this hotkey. - remaining_emission = remaining_emission.saturating_sub(parent_emission_take); + // --- 4.6. Subtract the parent's share from the remaining emission for this hotkey. + remaining_emission = remaining_emission + .saturating_sub(parent_emission_take) + .saturating_sub(child_emission_take); } } - // --- 6. Add the remaining emission plus the hotkey's initial take to the pending emission for this hotkey. + // --- 5. Add the remaining emission plus the hotkey's initial take to the pending emission for this hotkey. 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), ) }); + + // --- 6. 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. @@ -270,8 +284,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); @@ -280,13 +300,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; @@ -327,8 +350,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 Reset the stake delta for the hotkey. diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 958ef3480e..1155076ae8 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 0182888c0c..644833345c 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3428,3 +3428,277 @@ fn test_set_weights_no_parent() { assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); }); } + +/// 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 9c4bf87cc1..df2c95d81a 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 a55db996b4..9b45e8b33d 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2409,3 +2409,390 @@ fn test_stake_delta_tracks_adds_and_removes() { ); }); } + +/// 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); + }); +} From 97f9a6e8601768cbdf143c31a81426e1f8b1e9ab Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 8 Nov 2024 12:28:05 -0500 Subject: [PATCH 2/4] Format --- pallets/subtensor/src/coinbase/run_coinbase.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 08098b49db..bbb3fa2427 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -203,7 +203,7 @@ impl Pallet { I64F64::from_num(Self::get_childkey_take(hotkey, netuid)) .saturating_div(I64F64::from_num(u16::MAX)); let mut total_childkey_take: u64 = 0; - + // --- 2. Track the remaining emission for accounting purposes. let mut remaining_emission: u64 = validating_emission; @@ -238,7 +238,7 @@ impl Pallet { // --- 4.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); - + // --- 4.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { *parent_accumulated = parent_accumulated.saturating_add(parent_emission_take) @@ -306,7 +306,7 @@ impl Pallet { .saturating_div(I64F64::from_num(u16::MAX)); let hotkey_take: u64 = (take_proportion .saturating_mul(I64F64::from_num(emission_to_distribute))) - .to_num::(); + .to_num::(); // --- 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); From 0b49118df5d3e75a65fb2458b8a8c9bc561acb2f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Tue, 12 Nov 2024 15:45:38 -0500 Subject: [PATCH 3/4] Address nit: rounding --- .../subtensor/src/coinbase/run_coinbase.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index bbb3fa2427..da7b10687c 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -199,9 +199,9 @@ impl Pallet { mining_emission: u64, ) { // --- 1. First, calculate the hotkey's share of the emission. - let childkey_take_proportion: I64F64 = - I64F64::from_num(Self::get_childkey_take(hotkey, netuid)) - .saturating_div(I64F64::from_num(u16::MAX)); + let childkey_take_proportion: I96F32 = + I96F32::from_num(Self::get_childkey_take(hotkey, netuid)) + .saturating_div(I96F32::from_num(u16::MAX)); let mut total_childkey_take: u64 = 0; // --- 2. Track the remaining emission for accounting purposes. @@ -225,19 +225,21 @@ impl Pallet { ); let proportion_from_parent: I96F32 = stake_from_parent.saturating_div(I96F32::from_num(total_hotkey_stake)); - let parent_emission: u64 = proportion_from_parent - .saturating_mul(I96F32::from_num(validating_emission)) - .to_num::(); + let parent_emission: I96F32 = + proportion_from_parent.saturating_mul(I96F32::from_num(validating_emission)); // --- 4.3 Childkey take as part of parent emission let child_emission_take: u64 = childkey_take_proportion - .saturating_mul(I64F64::from_num(parent_emission)) + .saturating_mul(parent_emission) .to_num::(); - total_childkey_take = total_childkey_take.saturating_add(child_emission_take); + total_childkey_take = + total_childkey_take.saturating_add(child_emission_take); // NOTE: Only the validation emission should be split amongst parents. // --- 4.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); + let parent_emission_take: u64 = parent_emission + .to_num::() + .saturating_sub(child_emission_take); // --- 4.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { From 81c890573dbe3b1b5436c5431b54b5b3f2846757 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Wed, 13 Nov 2024 10:12:48 -0500 Subject: [PATCH 4/4] Format --- pallets/subtensor/src/coinbase/run_coinbase.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index da7b10687c..5529fd129f 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -232,8 +232,7 @@ impl Pallet { let child_emission_take: u64 = childkey_take_proportion .saturating_mul(parent_emission) .to_num::(); - total_childkey_take = - total_childkey_take.saturating_add(child_emission_take); + total_childkey_take = total_childkey_take.saturating_add(child_emission_take); // NOTE: Only the validation emission should be split amongst parents. // --- 4.4 Compute the remaining parent emission after the childkey's share is deducted.