From a19b7c7ec2372645ab83d8ad4371a54547bc7c81 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 11 Oct 2024 19:50:23 -0400 Subject: [PATCH 01/19] Childkey fixes, tests are incomplete --- .../subtensor/src/coinbase/run_coinbase.rs | 52 +++++--- pallets/subtensor/src/lib.rs | 10 ++ pallets/subtensor/tests/children.rs | 116 ++++++++++++++++++ 3 files changed, 161 insertions(+), 17 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 4486356a4..455a9011a 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -194,18 +194,11 @@ 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)) + let childkey_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; + 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. @@ -225,9 +218,18 @@ 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 childkey_take: u64 = childkey_take_proportion + .saturating_mul(I64F64::from_num(parent_emission)) .to_num::(); + // 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(childkey_take); // --- 5.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { @@ -243,10 +245,15 @@ impl Pallet { PendingdHotkeyEmission::::mutate(hotkey, |hotkey_pending| { *hotkey_pending = hotkey_pending.saturating_add( remaining_emission - .saturating_add(hotkey_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(mining_emission) + }); } /// Calculates the nonviable stake for a nominator. @@ -279,8 +286,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); @@ -289,13 +302,15 @@ 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::(); + (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; @@ -336,8 +351,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 6c8da7a69..c11671269 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -763,6 +763,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 ) --> block_number | Last add stake increase. pub type LastAddStakeIncrease = StorageDoubleMap< _, diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 0182888c0..9ff7ddd8f 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3428,3 +3428,119 @@ 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); + + // Set 50% childkey take + let max_take: u16 = 0xFFFF / 2; + 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 is populated for child + while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { + step_block(1); + } + + // Run run_coinbase until PendingHotkeyEmission is drained for child + let mut pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); + while pallet_subtensor::PendingdHotkeyEmission::::get(child) != 0 { + println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); + + pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); + step_block(1); + } + + // Verify that child key stake increased by its child key take only + println!("pending_child_emission = {:?}", pending_child_emission); + println!("Child stake: {:?}", pallet_subtensor::Stake::::get(child, coldkey)); + println!("Parent stake: {:?}", pallet_subtensor::Stake::::get(parent, coldkey) - stake); + println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); + + }); +} \ No newline at end of file From db01f5546eb0cc40795ab72cdb0c36cbee2ebae6 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 11:36:31 -0400 Subject: [PATCH 02/19] Test draining childkey emission: childkey take --- .../subtensor/src/coinbase/run_coinbase.rs | 7 +++- pallets/subtensor/tests/children.rs | 42 +++++++++++-------- pallets/subtensor/tests/epoch.rs | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 455a9011a..c73e5d109 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -196,6 +196,7 @@ impl Pallet { // --- 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 mut total_childkey_take: u64 = 0; // --- 3. Track the remaining emission for accounting purposes. let mut remaining_emission: u64 = validating_emission; @@ -226,6 +227,7 @@ impl Pallet { let childkey_take: u64 = childkey_take_proportion .saturating_mul(I64F64::from_num(parent_emission)) .to_num::(); + total_childkey_take = total_childkey_take.saturating_add(childkey_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. @@ -252,7 +254,10 @@ impl Pallet { // --- 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(mining_emission) + *hotkey_pending = hotkey_pending.saturating_add( + total_childkey_take + .saturating_add(mining_emission), + ) }); } diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 9ff7ddd8f..f2788ae0f 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3455,9 +3455,10 @@ fn test_childkey_take_drain() { 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 50% childkey take - let max_take: u16 = 0xFFFF / 2; + // 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), @@ -3522,25 +3523,30 @@ fn test_childkey_take_drain() { 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 is populated for child + // Run run_coinbase until PendingHotkeyEmission are populated while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { step_block(1); } - // Run run_coinbase until PendingHotkeyEmission is drained for child - let mut pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); - while pallet_subtensor::PendingdHotkeyEmission::::get(child) != 0 { - println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); - - pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); - step_block(1); - } - - // Verify that child key stake increased by its child key take only - println!("pending_child_emission = {:?}", pending_child_emission); - println!("Child stake: {:?}", pallet_subtensor::Stake::::get(child, coldkey)); - println!("Parent stake: {:?}", pallet_subtensor::Stake::::get(parent, coldkey) - stake); - println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); - + // 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 + // println!("pending_child_emission = {:?}", pending_child_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)); }); } \ No newline at end of file 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: `{:?}`", From adaca6211a9ad0052e247b7c226e318e0b9b4174 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 14:12:15 -0400 Subject: [PATCH 03/19] Test draining emission: miner emission. Improve readability of childkey take in accumulate_hotkey_emission. --- .../subtensor/src/coinbase/run_coinbase.rs | 11 +- pallets/subtensor/tests/children.rs | 1 - pallets/subtensor/tests/staking.rs | 125 ++++++++++++++++++ 3 files changed, 132 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index c73e5d109..064bddbe1 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -224,14 +224,14 @@ impl Pallet { .to_num::(); // --- 5.3 Childkey take as part of parent emission - let childkey_take: u64 = childkey_take_proportion + 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(childkey_take); + 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(childkey_take); + 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| { @@ -239,7 +239,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); } } @@ -247,6 +249,7 @@ impl Pallet { PendingdHotkeyEmission::::mutate(hotkey, |hotkey_pending| { *hotkey_pending = hotkey_pending.saturating_add( remaining_emission + .saturating_add(total_childkey_take) .saturating_add(mining_emission), ) }); diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index f2788ae0f..5699a3751 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3539,7 +3539,6 @@ fn test_childkey_take_drain() { // - 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 - // println!("pending_child_emission = {:?}", pending_child_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; diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index a55db996b..95c65f380 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2409,3 +2409,128 @@ 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); + SubtensorModule::set_max_allowed_validators(netuid, 2); + 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); + pallet_subtensor::MaxAllowedValidators::::set(netuid, 1); + // pallet_subtensor::ValidatorPermit::::set(netuid, vec![true, false]); + + // 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 weights + // 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)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + validator, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + // pallet_subtensor::Weights::::insert(root_id, 1, 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); + }); +} \ No newline at end of file From 68c16b3a069ea3e6a525892fb564b992d593b1fc Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 14:12:33 -0400 Subject: [PATCH 04/19] Format --- .../subtensor/src/coinbase/run_coinbase.rs | 20 ++++++------- pallets/subtensor/tests/children.rs | 30 +++++++++++++++---- pallets/subtensor/tests/staking.rs | 14 ++++++--- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 064bddbe1..f26758164 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -194,8 +194,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: 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. @@ -257,10 +258,8 @@ impl Pallet { // --- 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), - ) + *hotkey_pending = + hotkey_pending.saturating_add(total_childkey_take.saturating_add(mining_emission)) }); } @@ -294,7 +293,7 @@ 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 + // 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); @@ -310,12 +309,13 @@ 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 + // 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_distribute))).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 and untouchable_emission. let emission_minus_take: u64 = emission_to_distribute.saturating_sub(hotkey_take); diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 5699a3751..5b288afe9 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3449,8 +3449,14 @@ fn test_childkey_take_drain() { 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::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); @@ -3544,8 +3550,20 @@ fn test_childkey_take_drain() { 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)); + 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 + )); }); -} \ No newline at end of file +} diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 95c65f380..7f357efb9 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2432,8 +2432,14 @@ fn test_mining_emission_drain() { 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::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); SubtensorModule::set_max_allowed_validators(netuid, 2); @@ -2499,7 +2505,7 @@ fn test_mining_emission_drain() { // pallet_subtensor::Weights::::insert(root_id, 1, 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); + pallet_subtensor::Kappa::::set(netuid, u16::MAX / 5); // Run run_coinbase until root epoch is run while pallet_subtensor::PendingEmission::::get(netuid) == 0 { @@ -2533,4 +2539,4 @@ fn test_mining_emission_drain() { assert_eq!(miner_emission, total_emission / 2); assert_eq!(nominator_emission, 0); }); -} \ No newline at end of file +} From 0a341470ada541573f2796c3c31729f25f4ec156 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Wed, 16 Oct 2024 16:22:23 -0400 Subject: [PATCH 05/19] Add swapping child's parents to hotkey swap --- pallets/subtensor/src/swap/swap_hotkey.rs | 15 +++- pallets/subtensor/tests/swap_hotkey.rs | 97 +++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 54095d5fb..d7f62cf53 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -325,7 +325,20 @@ impl Pallet { // Remove the old hotkey's child entries ChildKeys::::remove(old_hotkey, netuid); // Insert the same child entries for the new hotkey - ChildKeys::::insert(new_hotkey, netuid, my_children); + ChildKeys::::insert(new_hotkey, netuid, my_children.clone()); + for (_, child_key_i) in my_children { + // For each child, update their parent list + let mut child_parents: Vec<(u64, T::AccountId)> = + ParentKeys::::get(child_key_i.clone(), netuid); + for parent in child_parents.iter_mut() { + // If the parent is the old hotkey, replace it with the new hotkey + if parent.1 == *old_hotkey { + parent.1 = new_hotkey.clone(); + } + } + // Update the child's parent list + ParentKeys::::insert(child_key_i, netuid, child_parents); + } } // 13. Swap ParentKeys. diff --git a/pallets/subtensor/tests/swap_hotkey.rs b/pallets/subtensor/tests/swap_hotkey.rs index 57f206452..efac0eb9d 100644 --- a/pallets/subtensor/tests/swap_hotkey.rs +++ b/pallets/subtensor/tests/swap_hotkey.rs @@ -1204,3 +1204,100 @@ fn test_swap_hotkey_with_pending_emissions() { assert!(!PendingdHotkeyEmission::::contains_key(old_hotkey)); }); } + +#[test] +fn test_swap_parent_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let parent_old = U256::from(1); + let coldkey = U256::from(2); + let child = U256::from(3); + let parent_new = U256::from(4); + add_network(netuid, 0, 0); + SubtensorModule::create_account_if_non_existent(&coldkey, &parent_old); + + // Set child and verify state maps + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent_old, + netuid, + vec![(u64::MAX, child)] + )); + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_old)] + ); + assert_eq!( + ChildKeys::::get(parent_old, netuid), + vec![(u64::MAX, child)] + ); + + // Swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap( + &parent_old, + &parent_new, + &coldkey, + &mut weight + )); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_new)] + ); + assert_eq!( + ChildKeys::::get(parent_new, netuid), + vec![(u64::MAX, child)] + ); + }) +} + +#[test] +fn test_swap_child_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let parent = U256::from(1); + let coldkey = U256::from(2); + let child_old = U256::from(3); + let child_new = U256::from(4); + add_network(netuid, 0, 0); + SubtensorModule::create_account_if_non_existent(&coldkey, &child_old); + SubtensorModule::create_account_if_non_existent(&coldkey, &parent); + + // Set child and verify state maps + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(u64::MAX, child_old)] + )); + assert_eq!( + ParentKeys::::get(child_old, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_old)] + ); + + // Swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap( + &child_old, + &child_new, + &coldkey, + &mut weight + )); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child_new, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_new)] + ); + }) +} From 34e0e61801ca1a07561915a0a8d02f149d7b2758 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 12:47:09 -0400 Subject: [PATCH 06/19] Add more tests for miner and childkey emission drain --- pallets/subtensor/tests/children.rs | 135 ++++++++++++++++++++++++++ pallets/subtensor/tests/staking.rs | 143 +++++++++++++++++++++++++++- 2 files changed, 274 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 5b288afe9..644833345 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3567,3 +3567,138 @@ fn test_childkey_take_drain() { )); }); } + +/// 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/staking.rs b/pallets/subtensor/tests/staking.rs index 7f357efb9..6cc2bdfec 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2442,15 +2442,13 @@ fn test_mining_emission_drain() { ); 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); // All stake is active pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); // There's only one validator pallet_subtensor::MaxAllowedUids::::set(netuid, 2); - pallet_subtensor::MaxAllowedValidators::::set(netuid, 1); - // pallet_subtensor::ValidatorPermit::::set(netuid, vec![true, false]); + SubtensorModule::set_max_allowed_validators(netuid, 1); // Set zero hotkey take for validator SubtensorModule::set_min_delegate_take(0); @@ -2502,7 +2500,6 @@ fn test_mining_emission_drain() { validator, )); pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); - // pallet_subtensor::Weights::::insert(root_id, 1, 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); @@ -2540,3 +2537,141 @@ fn test_mining_emission_drain() { 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 = 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 nominator_stake = 99_999_999_999; // 1 rao less than validator so that validator makes it to top_k + + // 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, 2); + + // 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, + nominator_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 weights + // Miner sets weights + // 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, + )); + 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(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 + // - 50% goes to miners and 50% goes to validators + // - Miner's reward is treated as half miner and half validator because he set weights + // - Validator stake increased by 50% of total emission + // - Miner stake increased by 25% of total emission + // - Nominator gets increased by 25% of total emission + let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey); + let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + let nominator_emission = + pallet_subtensor::Stake::::get(miner, nominator) - nominator_stake; + let total_emission = validator_emission + miner_emission + nominator_emission; + + assert_eq!(validator_emission, total_emission / 2); + assert!(is_within_tolerance(miner_emission, total_emission / 4, 5)); + assert!(is_within_tolerance( + nominator_emission, + total_emission / 4, + 5 + )); + }); +} From 37f80520e9666fb2d7e10cfe55fac445f414903f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 13:54:12 -0400 Subject: [PATCH 07/19] Update test for two validator-miner neurons --- pallets/subtensor/tests/staking.rs | 87 +++++++++++++++++------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 6cc2bdfec..ef6f08deb 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2490,11 +2490,11 @@ fn test_mining_emission_drain() { pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(miner, nominator, -1); // Setup YUMA so that it creates emissions: - // Validator sets weights + // 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![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 0, vec![(1, 0xFFFF)]); assert_ok!(SubtensorModule::do_root_register( RuntimeOrigin::signed(coldkey), validator, @@ -2544,8 +2544,8 @@ fn test_mining_emission_drain() { fn test_mining_emission_drain_with_validation() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); - let validator = U256::from(2); - let miner = U256::from(3); + 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; @@ -2553,13 +2553,13 @@ fn test_mining_emission_drain_with_validation() { let subnet_tempo = 10; let hotkey_tempo = 20; let stake = 100_000_000_000; - let nominator_stake = 99_999_999_999; // 1 rao less than validator so that validator makes it to top_k + 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, coldkey, 0); - register_ok_neuron(netuid, miner, coldkey, 1); + 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(), @@ -2582,14 +2582,14 @@ fn test_mining_emission_drain_with_validation() { SubtensorModule::set_min_delegate_take(0); assert_ok!(SubtensorModule::do_become_delegate( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, 0 )); // Set zero hotkey take for miner assert_ok!(SubtensorModule::do_become_delegate( RuntimeOrigin::signed(coldkey), - miner, + validator_miner2, 0 )); @@ -2600,26 +2600,38 @@ fn test_mining_emission_drain_with_validation() { // Give 100% of parent stake to childkey assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, stake )); - // assert_ok!(SubtensorModule::add_stake( - // RuntimeOrigin::signed(coldkey), - // miner, - // miner_stake - // )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_miner2, + half_stake + )); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(nominator), - miner, - nominator_stake + validator_miner2, + half_stake )); // Make all stakes viable - pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(validator, coldkey, -1); - pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(miner, nominator, -1); + 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: - // Validator sets weights - // Miner sets weights + // Validators set weights for each other // Validator registers on root and // Sets root weights // Last weight update is after block at registration @@ -2627,7 +2639,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::Weights::::insert(netuid, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); assert_ok!(SubtensorModule::do_root_register( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, )); pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); pallet_subtensor::BlockAtRegistration::::set(netuid, 0, 1); @@ -2644,7 +2656,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::Tempo::::set(root_id, u16::MAX); // Run run_coinbase until PendingHotkeyEmission are populated - while pallet_subtensor::PendingdHotkeyEmission::::get(miner) == 0 { + while pallet_subtensor::PendingdHotkeyEmission::::get(validator_miner1) == 0 { step_block(1); } @@ -2656,22 +2668,21 @@ fn test_mining_emission_drain_with_validation() { // 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 because he set weights - // - Validator stake increased by 50% of total emission - // - Miner stake increased by 25% of total emission - // - Nominator gets increased by 25% of total emission - let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey); - let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + // - 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(miner, nominator) - nominator_stake; - let total_emission = validator_emission + miner_emission + 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_emission, total_emission / 2); - assert!(is_within_tolerance(miner_emission, total_emission / 4, 5)); - assert!(is_within_tolerance( - nominator_emission, - total_emission / 4, - 5 - )); + 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); }); } From 6ceaa72d97221fe0f9330b7d903a1d6c5f180006 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 15:29:26 -0400 Subject: [PATCH 08/19] Add test case for draining emissions with three neurons: validator, valiminer, and miner --- pallets/subtensor/tests/staking.rs | 114 ++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index ef6f08deb..9b45e8b33 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2574,7 +2574,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::SubnetOwnerCut::::set(0); // All stake is active pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); - // There's only one validator + // There are two validators pallet_subtensor::MaxAllowedUids::::set(netuid, 2); SubtensorModule::set_max_allowed_validators(netuid, 2); @@ -2597,7 +2597,6 @@ fn test_mining_emission_drain_with_validation() { // 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_miner1, @@ -2686,3 +2685,114 @@ fn test_mining_emission_drain_with_validation() { 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 e9d8bf4323dc7ace9cf49822e6b2ad8a67890359 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Mon, 23 Sep 2024 15:34:17 -0400 Subject: [PATCH 09/19] bump spec above mainnet (#829) --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6990cb907..ec5120e9a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -146,7 +146,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 197, + spec_version: 206, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 8c2e5e300a6f1a69779e684c60fc18283553eb0e Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 11 Oct 2024 19:50:23 -0400 Subject: [PATCH 10/19] Childkey fixes, tests are incomplete --- .../subtensor/src/coinbase/run_coinbase.rs | 54 ++- pallets/subtensor/src/lib.rs | 10 + pallets/subtensor/tests/children.rs | 307 ++++++++++++++++++ 3 files changed, 353 insertions(+), 18 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 723edc423..00ba4b7c1 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -191,18 +191,11 @@ 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)) + let childkey_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; + 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 +215,18 @@ 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 childkey_take: u64 = childkey_take_proportion + .saturating_mul(I64F64::from_num(parent_emission)) .to_num::(); + // 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(childkey_take); // --- 5.5. Accumulate emissions for the parent hotkey. PendingdHotkeyEmission::::mutate(parent, |parent_accumulated| { @@ -240,10 +242,15 @@ impl Pallet { PendingdHotkeyEmission::::mutate(hotkey, |hotkey_pending| { *hotkey_pending = hotkey_pending.saturating_add( remaining_emission - .saturating_add(hotkey_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(mining_emission) + }); } //. --- 4. Drains the accumulated hotkey emission through to the nominators. The hotkey takes a proportion of the emission. @@ -262,8 +269,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 Retrieve the last time this hotkey's emissions were drained. let last_emission_drain: u64 = LastHotkeyEmissionDrain::::get(hotkey); @@ -274,14 +287,16 @@ impl Pallet { // --- 4 Retrieve the total stake for the hotkey from all nominations. let total_hotkey_stake: u64 = Self::get_total_stake_for_hotkey(hotkey); - // --- 5 Calculate the emission take for the 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::(); + (take_proportion.saturating_mul(I64F64::from_num(emission_to_distribute))).to_num::(); - // --- 6 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); // --- 7 Calculate the remaining emission after the hotkey's take. let mut remainder: u64 = emission_minus_take; @@ -323,8 +338,11 @@ impl Pallet { } } - // --- 14 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); // --- 15 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 2985736c8..bccf2c1f7 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -758,6 +758,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 ) --> block_number | Last add stake increase. pub type LastAddStakeIncrease = StorageDoubleMap< _, diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 2b99030ab..3739069cb 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3237,3 +3237,310 @@ 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, netuid)); + + // 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, netuid)); + }); +} + +// 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, netuid)); + + // 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, 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); + + // Set 50% childkey take + let max_take: u16 = 0xFFFF / 2; + 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 is populated for child + while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { + step_block(1); + } + + // Run run_coinbase until PendingHotkeyEmission is drained for child + let mut pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); + while pallet_subtensor::PendingdHotkeyEmission::::get(child) != 0 { + println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); + + pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); + step_block(1); + } + + // Verify that child key stake increased by its child key take only + println!("pending_child_emission = {:?}", pending_child_emission); + println!("Child stake: {:?}", pallet_subtensor::Stake::::get(child, coldkey)); + println!("Parent stake: {:?}", pallet_subtensor::Stake::::get(parent, coldkey) - stake); + println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); + + }); +} From 20f288b21a6f306c2908132ffd6f934abd090e87 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 11:36:31 -0400 Subject: [PATCH 11/19] Test draining childkey emission: childkey take --- .../subtensor/src/coinbase/run_coinbase.rs | 7 +++- pallets/subtensor/tests/children.rs | 42 +++++++++++-------- pallets/subtensor/tests/epoch.rs | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 00ba4b7c1..e853f2744 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -193,6 +193,7 @@ impl Pallet { // --- 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 mut total_childkey_take: u64 = 0; // --- 3. Track the remaining emission for accounting purposes. let mut remaining_emission: u64 = validating_emission; @@ -223,6 +224,7 @@ impl Pallet { let childkey_take: u64 = childkey_take_proportion .saturating_mul(I64F64::from_num(parent_emission)) .to_num::(); + total_childkey_take = total_childkey_take.saturating_add(childkey_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. @@ -249,7 +251,10 @@ impl Pallet { // --- 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(mining_emission) + *hotkey_pending = hotkey_pending.saturating_add( + total_childkey_take + .saturating_add(mining_emission), + ) }); } diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 3739069cb..60c2709d4 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3455,9 +3455,10 @@ fn test_childkey_take_drain() { 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 50% childkey take - let max_take: u16 = 0xFFFF / 2; + // 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), @@ -3522,25 +3523,30 @@ fn test_childkey_take_drain() { 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 is populated for child + // Run run_coinbase until PendingHotkeyEmission are populated while pallet_subtensor::PendingdHotkeyEmission::::get(child) == 0 { step_block(1); } - // Run run_coinbase until PendingHotkeyEmission is drained for child - let mut pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); - while pallet_subtensor::PendingdHotkeyEmission::::get(child) != 0 { - println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); - - pending_child_emission = pallet_subtensor::PendingdHotkeyEmission::::get(child); - step_block(1); - } - - // Verify that child key stake increased by its child key take only - println!("pending_child_emission = {:?}", pending_child_emission); - println!("Child stake: {:?}", pallet_subtensor::Stake::::get(child, coldkey)); - println!("Parent stake: {:?}", pallet_subtensor::Stake::::get(parent, coldkey) - stake); - println!("Nom stake: {:?}", pallet_subtensor::Stake::::get(child, nominator) - stake); - + // 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 + // println!("pending_child_emission = {:?}", pending_child_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)); }); } 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: `{:?}`", From 8405d2ed43a608f8918b3c0ec3c904bf1b29c7df Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 14:12:15 -0400 Subject: [PATCH 12/19] Test draining emission: miner emission. Improve readability of childkey take in accumulate_hotkey_emission. --- .../subtensor/src/coinbase/run_coinbase.rs | 11 +- pallets/subtensor/tests/children.rs | 1 - pallets/subtensor/tests/staking.rs | 228 ++++++++++++++++++ 3 files changed, 235 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index e853f2744..1276d60b3 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -221,14 +221,14 @@ impl Pallet { .to_num::(); // --- 5.3 Childkey take as part of parent emission - let childkey_take: u64 = childkey_take_proportion + 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(childkey_take); + 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(childkey_take); + 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| { @@ -236,7 +236,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); } } @@ -244,6 +246,7 @@ impl Pallet { PendingdHotkeyEmission::::mutate(hotkey, |hotkey_pending| { *hotkey_pending = hotkey_pending.saturating_add( remaining_emission + .saturating_add(total_childkey_take) .saturating_add(mining_emission), ) }); diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 60c2709d4..d8b0259e9 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3539,7 +3539,6 @@ fn test_childkey_take_drain() { // - 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 - // println!("pending_child_emission = {:?}", pending_child_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; diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index f053c7ca6..408a15b79 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2306,3 +2306,231 @@ 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); + SubtensorModule::set_max_allowed_validators(netuid, 2); + 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); + pallet_subtensor::MaxAllowedValidators::::set(netuid, 1); + // pallet_subtensor::ValidatorPermit::::set(netuid, vec![true, false]); + + // 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 weights + // 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)]); + assert_ok!(SubtensorModule::do_root_register( + RuntimeOrigin::signed(coldkey), + validator, + )); + pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); + // pallet_subtensor::Weights::::insert(root_id, 1, 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); + }); +} From 05948f56def28c3259e24a06d8f49763929a95b3 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 14 Oct 2024 14:12:33 -0400 Subject: [PATCH 13/19] Format --- .../subtensor/src/coinbase/run_coinbase.rs | 20 ++++++------- pallets/subtensor/tests/children.rs | 28 +++++++++++++++---- pallets/subtensor/tests/staking.rs | 12 ++++++-- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 1276d60b3..9974087b0 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -191,8 +191,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: 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. @@ -254,10 +255,8 @@ impl Pallet { // --- 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), - ) + *hotkey_pending = + hotkey_pending.saturating_add(total_childkey_take.saturating_add(mining_emission)) }); } @@ -277,7 +276,7 @@ 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 + // 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); @@ -296,12 +295,13 @@ 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 + // 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_distribute))).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 and untouchable_emission. let emission_minus_take: u64 = emission_to_distribute.saturating_sub(hotkey_take); diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index d8b0259e9..5b288afe9 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3449,8 +3449,14 @@ fn test_childkey_take_drain() { 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::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); @@ -3544,8 +3550,20 @@ fn test_childkey_take_drain() { 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)); + 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 + )); }); } diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 408a15b79..7f357efb9 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2432,8 +2432,14 @@ fn test_mining_emission_drain() { 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::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); SubtensorModule::set_max_allowed_validators(netuid, 2); @@ -2499,7 +2505,7 @@ fn test_mining_emission_drain() { // pallet_subtensor::Weights::::insert(root_id, 1, 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); + pallet_subtensor::Kappa::::set(netuid, u16::MAX / 5); // Run run_coinbase until root epoch is run while pallet_subtensor::PendingEmission::::get(netuid) == 0 { From a28c55c2225511db76c3a2044163475fc3a83ea1 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 12:47:09 -0400 Subject: [PATCH 14/19] Add more tests for miner and childkey emission drain --- pallets/subtensor/tests/children.rs | 135 ++++++++++++++++++++++++++ pallets/subtensor/tests/staking.rs | 143 +++++++++++++++++++++++++++- 2 files changed, 274 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 5b288afe9..644833345 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3567,3 +3567,138 @@ fn test_childkey_take_drain() { )); }); } + +/// 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/staking.rs b/pallets/subtensor/tests/staking.rs index 7f357efb9..6cc2bdfec 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2442,15 +2442,13 @@ fn test_mining_emission_drain() { ); 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); // All stake is active pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); // There's only one validator pallet_subtensor::MaxAllowedUids::::set(netuid, 2); - pallet_subtensor::MaxAllowedValidators::::set(netuid, 1); - // pallet_subtensor::ValidatorPermit::::set(netuid, vec![true, false]); + SubtensorModule::set_max_allowed_validators(netuid, 1); // Set zero hotkey take for validator SubtensorModule::set_min_delegate_take(0); @@ -2502,7 +2500,6 @@ fn test_mining_emission_drain() { validator, )); pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); - // pallet_subtensor::Weights::::insert(root_id, 1, 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); @@ -2540,3 +2537,141 @@ fn test_mining_emission_drain() { 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 = 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 nominator_stake = 99_999_999_999; // 1 rao less than validator so that validator makes it to top_k + + // 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, 2); + + // 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, + nominator_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 weights + // Miner sets weights + // 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, + )); + 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(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 + // - 50% goes to miners and 50% goes to validators + // - Miner's reward is treated as half miner and half validator because he set weights + // - Validator stake increased by 50% of total emission + // - Miner stake increased by 25% of total emission + // - Nominator gets increased by 25% of total emission + let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey); + let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + let nominator_emission = + pallet_subtensor::Stake::::get(miner, nominator) - nominator_stake; + let total_emission = validator_emission + miner_emission + nominator_emission; + + assert_eq!(validator_emission, total_emission / 2); + assert!(is_within_tolerance(miner_emission, total_emission / 4, 5)); + assert!(is_within_tolerance( + nominator_emission, + total_emission / 4, + 5 + )); + }); +} From de289c3f953b57d19af26953865931e7d75c50f9 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 13:54:12 -0400 Subject: [PATCH 15/19] Update test for two validator-miner neurons --- pallets/subtensor/tests/staking.rs | 87 +++++++++++++++++------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 6cc2bdfec..ef6f08deb 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2490,11 +2490,11 @@ fn test_mining_emission_drain() { pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(miner, nominator, -1); // Setup YUMA so that it creates emissions: - // Validator sets weights + // 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![(0, 0xFFFF), (1, 0xFFFF)]); + pallet_subtensor::Weights::::insert(netuid, 0, vec![(1, 0xFFFF)]); assert_ok!(SubtensorModule::do_root_register( RuntimeOrigin::signed(coldkey), validator, @@ -2544,8 +2544,8 @@ fn test_mining_emission_drain() { fn test_mining_emission_drain_with_validation() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); - let validator = U256::from(2); - let miner = U256::from(3); + 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; @@ -2553,13 +2553,13 @@ fn test_mining_emission_drain_with_validation() { let subnet_tempo = 10; let hotkey_tempo = 20; let stake = 100_000_000_000; - let nominator_stake = 99_999_999_999; // 1 rao less than validator so that validator makes it to top_k + 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, coldkey, 0); - register_ok_neuron(netuid, miner, coldkey, 1); + 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(), @@ -2582,14 +2582,14 @@ fn test_mining_emission_drain_with_validation() { SubtensorModule::set_min_delegate_take(0); assert_ok!(SubtensorModule::do_become_delegate( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, 0 )); // Set zero hotkey take for miner assert_ok!(SubtensorModule::do_become_delegate( RuntimeOrigin::signed(coldkey), - miner, + validator_miner2, 0 )); @@ -2600,26 +2600,38 @@ fn test_mining_emission_drain_with_validation() { // Give 100% of parent stake to childkey assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, stake )); - // assert_ok!(SubtensorModule::add_stake( - // RuntimeOrigin::signed(coldkey), - // miner, - // miner_stake - // )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_miner2, + half_stake + )); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(nominator), - miner, - nominator_stake + validator_miner2, + half_stake )); // Make all stakes viable - pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(validator, coldkey, -1); - pallet_subtensor::StakeDeltaSinceLastEmissionDrain::::set(miner, nominator, -1); + 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: - // Validator sets weights - // Miner sets weights + // Validators set weights for each other // Validator registers on root and // Sets root weights // Last weight update is after block at registration @@ -2627,7 +2639,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::Weights::::insert(netuid, 1, vec![(0, 0xFFFF), (1, 0xFFFF)]); assert_ok!(SubtensorModule::do_root_register( RuntimeOrigin::signed(coldkey), - validator, + validator_miner1, )); pallet_subtensor::Weights::::insert(root_id, 0, vec![(0, 0xFFFF), (1, 0xFFFF)]); pallet_subtensor::BlockAtRegistration::::set(netuid, 0, 1); @@ -2644,7 +2656,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::Tempo::::set(root_id, u16::MAX); // Run run_coinbase until PendingHotkeyEmission are populated - while pallet_subtensor::PendingdHotkeyEmission::::get(miner) == 0 { + while pallet_subtensor::PendingdHotkeyEmission::::get(validator_miner1) == 0 { step_block(1); } @@ -2656,22 +2668,21 @@ fn test_mining_emission_drain_with_validation() { // 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 because he set weights - // - Validator stake increased by 50% of total emission - // - Miner stake increased by 25% of total emission - // - Nominator gets increased by 25% of total emission - let miner_emission = pallet_subtensor::Stake::::get(miner, coldkey); - let validator_emission = pallet_subtensor::Stake::::get(validator, coldkey) - stake; + // - 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(miner, nominator) - nominator_stake; - let total_emission = validator_emission + miner_emission + 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_emission, total_emission / 2); - assert!(is_within_tolerance(miner_emission, total_emission / 4, 5)); - assert!(is_within_tolerance( - nominator_emission, - total_emission / 4, - 5 - )); + 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); }); } From 07eb20704dd1a15cb428a0b0b0009043efa98563 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Oct 2024 15:29:26 -0400 Subject: [PATCH 16/19] Add test case for draining emissions with three neurons: validator, valiminer, and miner --- pallets/subtensor/tests/staking.rs | 114 ++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index ef6f08deb..9b45e8b33 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2574,7 +2574,7 @@ fn test_mining_emission_drain_with_validation() { pallet_subtensor::SubnetOwnerCut::::set(0); // All stake is active pallet_subtensor::ActivityCutoff::::set(netuid, u16::MAX); - // There's only one validator + // There are two validators pallet_subtensor::MaxAllowedUids::::set(netuid, 2); SubtensorModule::set_max_allowed_validators(netuid, 2); @@ -2597,7 +2597,6 @@ fn test_mining_emission_drain_with_validation() { // 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_miner1, @@ -2686,3 +2685,114 @@ fn test_mining_emission_drain_with_validation() { 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 f040a1c9bff675dab84467d77d39221d106f6438 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Wed, 16 Oct 2024 16:22:23 -0400 Subject: [PATCH 17/19] Add swapping child's parents to hotkey swap --- pallets/subtensor/src/swap/swap_hotkey.rs | 15 +- pallets/subtensor/tests/swap_hotkey.rs | 185 ++++++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 793e34bff..e872b038e 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -316,7 +316,20 @@ impl Pallet { // Remove the old hotkey's child entries ChildKeys::::remove(old_hotkey, netuid); // Insert the same child entries for the new hotkey - ChildKeys::::insert(new_hotkey, netuid, my_children); + ChildKeys::::insert(new_hotkey, netuid, my_children.clone()); + for (_, child_key_i) in my_children { + // For each child, update their parent list + let mut child_parents: Vec<(u64, T::AccountId)> = + ParentKeys::::get(child_key_i.clone(), netuid); + for parent in child_parents.iter_mut() { + // If the parent is the old hotkey, replace it with the new hotkey + if parent.1 == *old_hotkey { + parent.1 = new_hotkey.clone(); + } + } + // Update the child's parent list + ParentKeys::::insert(child_key_i, netuid, child_parents); + } } // 12. Swap ParentKeys. diff --git a/pallets/subtensor/tests/swap_hotkey.rs b/pallets/subtensor/tests/swap_hotkey.rs index bff738b86..c51a8c550 100644 --- a/pallets/subtensor/tests/swap_hotkey.rs +++ b/pallets/subtensor/tests/swap_hotkey.rs @@ -1115,3 +1115,188 @@ 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(|| { + let netuid: u16 = 1; + let parent_old = U256::from(1); + let coldkey = U256::from(2); + let child = U256::from(3); + let parent_new = U256::from(4); + add_network(netuid, 0, 0); + SubtensorModule::create_account_if_non_existent(&coldkey, &parent_old); + + // Set child and verify state maps + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent_old, + netuid, + vec![(u64::MAX, child)] + )); + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_old)] + ); + assert_eq!( + ChildKeys::::get(parent_old, netuid), + vec![(u64::MAX, child)] + ); + + // Swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap( + &parent_old, + &parent_new, + &coldkey, + &mut weight + )); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_new)] + ); + assert_eq!( + ChildKeys::::get(parent_new, netuid), + vec![(u64::MAX, child)] + ); + }) +} + +#[test] +fn test_swap_child_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + let parent = U256::from(1); + let coldkey = U256::from(2); + let child_old = U256::from(3); + let child_new = U256::from(4); + add_network(netuid, 0, 0); + SubtensorModule::create_account_if_non_existent(&coldkey, &child_old); + SubtensorModule::create_account_if_non_existent(&coldkey, &parent); + + // Set child and verify state maps + assert_ok!(SubtensorModule::do_set_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(u64::MAX, child_old)] + )); + assert_eq!( + ParentKeys::::get(child_old, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_old)] + ); + + // Swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap( + &child_old, + &child_new, + &coldkey, + &mut weight + )); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child_new, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_new)] + ); + }) +} From a47276265b7b398aa39e75f040b8874b00358f7d Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 4 Nov 2024 16:26:40 -0500 Subject: [PATCH 18/19] Format --- .../src/pallet/parse/storage.rs | 6 +++--- .../procedural-fork/src/runtime/parse/mod.rs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) 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) } From 0d1f992aa2e67c82bbd851e3b73428abdf2a035c Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 4 Nov 2024 16:37:19 -0500 Subject: [PATCH 19/19] Make tests build --- pallets/subtensor/tests/children.rs | 8 ++++---- pallets/subtensor/tests/swap_hotkey.rs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/tests/children.rs b/pallets/subtensor/tests/children.rs index 644833345..0b2aea563 100644 --- a/pallets/subtensor/tests/children.rs +++ b/pallets/subtensor/tests/children.rs @@ -3322,7 +3322,7 @@ fn test_childkey_set_weights_single_parent() { Error::::NotEnoughStakeToSetWeights ); - assert!(!SubtensorModule::check_weights_min_stake(&child, netuid)); + assert!(!SubtensorModule::check_weights_min_stake(&child)); // Set a minimum stake to set weights SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); @@ -3342,7 +3342,7 @@ fn test_childkey_set_weights_single_parent() { version_key )); - assert!(SubtensorModule::check_weights_min_stake(&child, netuid)); + assert!(SubtensorModule::check_weights_min_stake(&child)); }); } @@ -3405,7 +3405,7 @@ fn test_set_weights_no_parent() { Error::::NotEnoughStakeToSetWeights ); - assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + assert!(!SubtensorModule::check_weights_min_stake(&hotkey)); // Set a minimum stake to set weights SubtensorModule::set_weights_min_stake(stake_to_give_child - 5); @@ -3425,7 +3425,7 @@ fn test_set_weights_no_parent() { version_key )); - assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + assert!(SubtensorModule::check_weights_min_stake(&hotkey)); }); } diff --git a/pallets/subtensor/tests/swap_hotkey.rs b/pallets/subtensor/tests/swap_hotkey.rs index c3b81629e..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]