From eb731260349a47140e6ce8b2eaf098654098f35b Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Wed, 29 Jul 2026 13:14:21 -0400 Subject: [PATCH] Credit childkey take to owning coldkey instead of going through dividend pool --- .../subtensor/src/coinbase/run_coinbase.rs | 185 +++++++++++++++--- pallets/subtensor/src/subnets/collateral.rs | 34 +++- pallets/subtensor/src/tests/children.rs | 65 +++--- pallets/subtensor/src/tests/coinbase.rs | 148 +++++++++++++- 4 files changed, 365 insertions(+), 67 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index e3425addba..d3af64ac5a 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -510,10 +510,24 @@ impl Pallet { ) -> ( BTreeMap, BTreeMap, + ) { + let (incentives, dividends, _) = + Self::calculate_dividends_incentives_and_child_takes(netuid, hotkey_emission); + (incentives, dividends) + } + + fn calculate_dividends_incentives_and_child_takes( + netuid: NetUid, + hotkey_emission: Vec<(T::AccountId, AlphaBalance, AlphaBalance)>, + ) -> ( + BTreeMap, + BTreeMap, + BTreeMap, ) { // Accumulate emission of dividends and incentive per hotkey. let mut incentives: BTreeMap = BTreeMap::new(); let mut dividends: BTreeMap = BTreeMap::new(); + let mut child_takes: BTreeMap = BTreeMap::new(); for (hotkey, incentive, dividend) in hotkey_emission { // Accumulate incentives to miners. incentives @@ -521,8 +535,14 @@ impl Pallet { .and_modify(|e| *e = e.saturating_add(incentive)) .or_insert(incentive); // Accumulate dividends to parents. - let div_tuples: Vec<(T::AccountId, AlphaBalance)> = - Self::get_parent_child_dividends_distribution(&hotkey, netuid, dividend); + let (div_tuples, child_take) = + Self::get_parent_child_dividends_distribution_with_child_take( + &hotkey, netuid, dividend, + ); + child_takes + .entry(hotkey.clone()) + .and_modify(|e| *e = e.saturating_add(asfloat!(child_take))) + .or_insert(asfloat!(child_take)); // Accumulate dividends per hotkey. for (parent, parent_div) in div_tuples { dividends @@ -533,8 +553,9 @@ impl Pallet { } log::debug!("incentives: {incentives:?}"); log::debug!("dividends: {dividends:?}"); + log::debug!("child_takes: {child_takes:?}"); - (incentives, dividends) + (incentives, dividends, child_takes) } pub fn calculate_dividend_distribution( @@ -679,6 +700,24 @@ impl Pallet { incentives: BTreeMap, alpha_dividends: BTreeMap, root_alpha_dividends: BTreeMap, + ) { + Self::distribute_dividends_and_incentives_with_child_takes( + netuid, + owner_cut, + incentives, + alpha_dividends, + root_alpha_dividends, + BTreeMap::new(), + ); + } + + fn distribute_dividends_and_incentives_with_child_takes( + netuid: NetUid, + owner_cut: AlphaBalance, + incentives: BTreeMap, + alpha_dividends: BTreeMap, + root_alpha_dividends: BTreeMap, + child_take_proportions: BTreeMap, ) { // Distribute the owner cut. if let Ok(owner_coldkey) = SubnetOwner::::try_get(netuid) @@ -793,19 +832,29 @@ impl Pallet { for (hotkey, alpha_divs) in alpha_dividends { let owner: T::AccountId = Owner::::get(&hotkey); let total: AlphaBalance = tou64!(alpha_divs).into(); - let alpha_take: U96F32 = - Self::get_hotkey_take_float(&hotkey).saturating_mul(alpha_divs); - let nominator_divs: U96F32 = alpha_divs.saturating_sub(alpha_take); - let take: AlphaBalance = tou64!(alpha_take).into(); - let captured = Self::settle_miner_collateral(netuid, &hotkey, &owner, total, take); - let liquid_take = take.saturating_sub(captured); - if !liquid_take.is_zero() { - log::debug!("hotkey: {hotkey:?} alpha_take: {liquid_take:?}"); + let child_take = child_take_proportions + .get(&hotkey) + .copied() + .unwrap_or_else(|| asfloat!(0)) + .saturating_mul(alpha_divs); + let shared_dividends = alpha_divs.saturating_sub(child_take); + let delegate_take = + Self::get_hotkey_take_float(&hotkey).saturating_mul(shared_dividends); + let nominator_divs = shared_dividends.saturating_sub(delegate_take); + let shared_total: AlphaBalance = tou64!(shared_dividends).into(); + let delegate_take: AlphaBalance = tou64!(delegate_take).into(); + let child_take = total.saturating_sub(shared_total); + let owner_take = delegate_take.saturating_add(child_take); + Self::settle_miner_collateral_without_stake_credit( + netuid, &hotkey, &owner, total, owner_take, + ); + if !delegate_take.is_zero() { + log::debug!("hotkey: {hotkey:?} delegate_take: {delegate_take:?}"); Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &owner, netuid, - liquid_take, + delegate_take, ); } let nominator_alpha: AlphaBalance = tou64!(nominator_divs).into(); @@ -816,6 +865,14 @@ impl Pallet { *divs = divs.saturating_add(nominator_alpha); }); } + // Credit childkey take after shared dividends so the newly created + // owner stake cannot participate in the same dividend distribution. + if !child_take.is_zero() { + log::debug!("hotkey: {hotkey:?} child_take: {child_take:?}"); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &owner, netuid, child_take, + ); + } let total_hotkey_alpha = TotalHotkeyAlpha::::get(&hotkey, netuid); TotalHotkeyAlphaLastEpoch::::insert(hotkey, netuid, total_hotkey_alpha); } @@ -826,19 +883,29 @@ impl Pallet { for (hotkey, root_alpha) in root_alpha_dividends { let owner: T::AccountId = Owner::::get(&hotkey); let total: AlphaBalance = tou64!(root_alpha).into(); - let alpha_take: U96F32 = - Self::get_hotkey_take_float(&hotkey).saturating_mul(root_alpha); - let root_claimable: U96F32 = root_alpha.saturating_sub(alpha_take); - let take: AlphaBalance = tou64!(alpha_take).into(); - let captured = Self::settle_miner_collateral(netuid, &hotkey, &owner, total, take); - let liquid_take = take.saturating_sub(captured); - if !liquid_take.is_zero() { - log::debug!("hotkey: {hotkey:?} alpha_take: {liquid_take:?}"); + let child_take = child_take_proportions + .get(&hotkey) + .copied() + .unwrap_or_else(|| asfloat!(0)) + .saturating_mul(root_alpha); + let shared_dividends = root_alpha.saturating_sub(child_take); + let delegate_take = + Self::get_hotkey_take_float(&hotkey).saturating_mul(shared_dividends); + let root_claimable = shared_dividends.saturating_sub(delegate_take); + let shared_total: AlphaBalance = tou64!(shared_dividends).into(); + let delegate_take: AlphaBalance = tou64!(delegate_take).into(); + let child_take = total.saturating_sub(shared_total); + let owner_take = delegate_take.saturating_add(child_take); + Self::settle_miner_collateral_without_stake_credit( + netuid, &hotkey, &owner, total, owner_take, + ); + if !delegate_take.is_zero() { + log::debug!("hotkey: {hotkey:?} delegate_take: {delegate_take:?}"); Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &owner, netuid, - liquid_take, + delegate_take, ); } @@ -854,6 +921,14 @@ impl Pallet { *divs = divs.saturating_add(root_claimable_alpha); }); } + // Root-derived childkey take is owner-only as well; unlike the + // shared remainder it must not become root-claimable. + if !child_take.is_zero() { + log::debug!("hotkey: {hotkey:?} child_take: {child_take:?}"); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &owner, netuid, child_take, + ); + } } } @@ -885,8 +960,46 @@ impl Pallet { BTreeMap, ), ) { - let (incentives, dividends) = - Self::calculate_dividends_and_incentives(netuid, hotkey_emission); + let (incentives, dividends, _) = + Self::calculate_dividend_and_incentive_distribution_with_child_takes( + netuid, + pending_root_alpha, + pending_validator_alpha, + hotkey_emission, + tao_weight, + ); + (incentives, dividends) + } + + fn calculate_dividend_and_incentive_distribution_with_child_takes( + netuid: NetUid, + pending_root_alpha: AlphaBalance, + pending_validator_alpha: AlphaBalance, + hotkey_emission: Vec<(T::AccountId, AlphaBalance, AlphaBalance)>, + tao_weight: U96F32, + ) -> ( + BTreeMap, + ( + BTreeMap, + BTreeMap, + ), + BTreeMap, + ) { + let (incentives, dividends, child_takes) = + Self::calculate_dividends_incentives_and_child_takes(netuid, hotkey_emission); + + // Preserve the child take's share of each hotkey's dividend score through + // normalization into the subnet and root dividend pools. + let zero = asfloat!(0); + let one = asfloat!(1); + let child_take_proportions = child_takes + .into_iter() + .filter_map(|(hotkey, child_take)| { + let total_dividends = dividends.get(&hotkey).copied()?; + let proportion = child_take.checked_div(total_dividends).unwrap_or(zero); + Some((hotkey, if proportion > one { one } else { proportion })) + }) + .collect(); let stake_map = Self::get_stake_map(netuid, dividends.keys().collect::>()); @@ -898,7 +1011,11 @@ impl Pallet { dividends, ); - (incentives, (alpha_dividends, root_alpha_dividends)) + ( + incentives, + (alpha_dividends, root_alpha_dividends), + child_take_proportions, + ) } pub fn distribute_emission( @@ -943,8 +1060,8 @@ impl Pallet { let root_alpha = pending_root_alpha; let owner_cut = pending_owner_cut; - let (incentives, (alpha_dividends, root_alpha_dividends)) = - Self::calculate_dividend_and_incentive_distribution( + let (incentives, (alpha_dividends, root_alpha_dividends), child_take_proportions) = + Self::calculate_dividend_and_incentive_distribution_with_child_takes( netuid, root_alpha, validator_alpha, @@ -952,12 +1069,13 @@ impl Pallet { tao_weight, ); - Self::distribute_dividends_and_incentives( + Self::distribute_dividends_and_incentives_with_child_takes( netuid, owner_cut, incentives, alpha_dividends, root_alpha_dividends, + child_take_proportions, ); } @@ -1012,6 +1130,14 @@ impl Pallet { netuid: NetUid, dividends: AlphaBalance, ) -> Vec<(T::AccountId, AlphaBalance)> { + Self::get_parent_child_dividends_distribution_with_child_take(hotkey, netuid, dividends).0 + } + + fn get_parent_child_dividends_distribution_with_child_take( + hotkey: &T::AccountId, + netuid: NetUid, + dividends: AlphaBalance, + ) -> (Vec<(T::AccountId, AlphaBalance)>, AlphaBalance) { // hotkey dividends. let mut dividend_tuples: Vec<(T::AccountId, AlphaBalance)> = vec![]; @@ -1138,7 +1264,10 @@ impl Pallet { // Add the hotkey's own emission to the distribution list dividend_tuples.push((hotkey.clone(), child_emission)); - dividend_tuples + ( + dividend_tuples, + total_child_take.saturating_to_num::().into(), + ) } /// Checks if the epoch should run for a given subnet based on the current block. diff --git a/pallets/subtensor/src/subnets/collateral.rs b/pallets/subtensor/src/subnets/collateral.rs index 93d60d9913..86830d28e1 100644 --- a/pallets/subtensor/src/subnets/collateral.rs +++ b/pallets/subtensor/src/subnets/collateral.rs @@ -489,6 +489,32 @@ impl Pallet { owner: &T::AccountId, emission: AlphaBalance, capturable: AlphaBalance, + ) -> AlphaBalance { + Self::settle_miner_collateral_inner(netuid, hotkey, owner, emission, capturable, true) + } + + /// Settle collateral without crediting captured stake. The caller must + /// credit the full `capturable` amount to the owner after this returns. + /// + /// This allows emission distribution to control payout ordering while + /// preserving the same collateral accounting as [`Self::settle_miner_collateral`]. + pub(crate) fn settle_miner_collateral_without_stake_credit( + netuid: NetUid, + hotkey: &T::AccountId, + owner: &T::AccountId, + emission: AlphaBalance, + capturable: AlphaBalance, + ) -> AlphaBalance { + Self::settle_miner_collateral_inner(netuid, hotkey, owner, emission, capturable, false) + } + + fn settle_miner_collateral_inner( + netuid: NetUid, + hotkey: &T::AccountId, + owner: &T::AccountId, + emission: AlphaBalance, + capturable: AlphaBalance, + credit_captured_stake: bool, ) -> AlphaBalance { if emission.is_zero() { return AlphaBalance::ZERO; @@ -508,9 +534,11 @@ impl Pallet { if captured.is_zero() { return AlphaBalance::ZERO; } - Self::increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, owner, netuid, captured, - ); + if credit_captured_stake { + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, owner, netuid, captured, + ); + } state.locked = state.locked.saturating_add(captured); return captured; } diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index 703ecb4807..94ddb654dc 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2886,15 +2886,17 @@ fn test_set_weights_no_parent() { }); } -/// Test that distribute_emission sends childkey take fully to the nominators if childkey -/// doesn't have its own stake, independently of parent hotkey take. +/// Test that distribute_emission sends childkey take exclusively to the childkey owner, +/// independently of the childkey's own stake and the parent hotkey take. /// cargo test --package pallet-subtensor --lib -- tests::children::test_childkey_take_drain --exact --show-output #[allow(clippy::assertions_on_constants)] #[test] fn test_childkey_take_drain() { - // Test cases: parent_hotkey_take - [0_u16, u16::MAX / 5].iter().for_each(|parent_hotkey_take| { - new_test_ext(1).execute_with(|| { + fn run_case( + parent_hotkey_take: u16, + childkey_take: u16, + ) -> (TaoBalance, TaoBalance, TaoBalance) { + new_test_ext(1).execute_with(move || { let parent_coldkey = U256::from(1); let parent_hotkey = U256::from(3); let child_coldkey = U256::from(2); @@ -2930,19 +2932,19 @@ fn test_childkey_take_drain() { // Set children mock_set_children_no_epochs(netuid, &parent_hotkey, &[(proportion, child_hotkey)]); - // Set 20% childkey take + // Set the requested childkey take, up to 20%. let max_take: u16 = 0xFFFF / 5; SubtensorModule::set_max_childkey_take(PerU16::from_parts(max_take)); assert_ok!(SubtensorModule::set_childkey_take( RuntimeOrigin::signed(child_coldkey), child_hotkey, netuid, - PerU16::from_parts(max_take) + PerU16::from_parts(childkey_take) )); // Set hotkey take for parent - SubtensorModule::set_max_delegate_take(PerU16::from_parts(*parent_hotkey_take)); - Delegates::::insert(parent_hotkey, PerU16::from_parts(*parent_hotkey_take)); + SubtensorModule::set_max_delegate_take(PerU16::from_parts(parent_hotkey_take)); + Delegates::::insert(parent_hotkey, PerU16::from_parts(parent_hotkey_take)); // Set 0% for childkey-as-a-delegate take Delegates::::insert(child_hotkey, PerU16::zero()); @@ -2982,36 +2984,35 @@ fn test_childkey_take_drain() { step_block(subnet_tempo); - // Verify how emission is split between keys - // - Child stake remains 0 - // - Childkey take is 20% of its total emission that rewards both inherited from - // parent stake and nominated stake, which all goes to nominators. Because child - // validator emission is 50% of total emission, 20% of it is 10% of total emission - // and it all goes to nominator. If childkey take was 0%, then only 5% would go to - // the nominator, so the final solit is: - // - Parent stake increases by 45% of total emission - // - Nominator stake increases by 55% of total emission + // Measure each participant's emission so the caller can compare this + // scenario with an otherwise identical zero-childkey-take baseline. let child_emission = SubtensorModule::get_total_stake_for_coldkey(&child_coldkey) - child_stake_before; let parent_emission = SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey) - parent_stake_before; let nominator_emission = SubtensorModule::get_total_stake_for_coldkey(&nominator) - nominator_stake_before; - let total_emission = child_emission + parent_emission + nominator_emission; - assert_abs_diff_eq!(child_emission, TaoBalance::ZERO, epsilon = 10.into()); - assert_abs_diff_eq!( - parent_emission, - total_emission * 9.into() / 20.into(), - epsilon = 10.into() - ); - assert_abs_diff_eq!( - nominator_emission, - total_emission * 11.into() / 20.into(), - epsilon = 10.into() - ); - }); - }); + (child_emission, parent_emission, nominator_emission) + }) + } + + // Parent delegate take must not affect where childkey take is paid. + for parent_hotkey_take in [0_u16, u16::MAX / 5] { + let without_child_take = run_case(parent_hotkey_take, 0); + let with_child_take = run_case(parent_hotkey_take, u16::MAX / 5); + + let child_increase = with_child_take.0.saturating_sub(without_child_take.0); + let parent_reduction = without_child_take.1.saturating_sub(with_child_take.1); + let total_without = without_child_take.0 + without_child_take.1 + without_child_take.2; + let total_with = with_child_take.0 + with_child_take.1 + with_child_take.2; + + assert_abs_diff_eq!(without_child_take.0, TaoBalance::ZERO, epsilon = 10.into()); + assert!(child_increase > TaoBalance::ZERO); + assert_abs_diff_eq!(child_increase, parent_reduction, epsilon = 10.into()); + assert_abs_diff_eq!(with_child_take.2, without_child_take.2, epsilon = 10.into()); + assert_abs_diff_eq!(total_with, total_without, epsilon = 10.into()); + } } // 44: Test with a chain of parent-child relationships (e.g., A -> B -> C) diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 4a54157b63..0c47943fb2 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -1605,6 +1605,10 @@ fn test_get_root_children_drain() { // Lets change the take value. (Bob is greedy.) ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX)); + let alice_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&alice, &cold_alice, alpha); + let bob_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha); // Lets drain let pending_alpha = AlphaBalance::from(1_000_000_000); @@ -1622,9 +1626,26 @@ fn test_get_root_children_drain() { AlphaDividendsPerSubnet::::get(alpha, alice), AlphaBalance::ZERO ); - // Bob makes it all. + // Bob's shared half remains in the dividend pool. The half taken from + // Alice is credited exclusively to Bob's owning coldkey. assert_abs_diff_eq!( AlphaDividendsPerSubnet::::get(alpha, bob), + pending_alpha / 2.into(), + epsilon = 1.into() + ); + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + alpha, + ) + .saturating_sub(alice_stake_before), + AlphaBalance::ZERO, + epsilon = 1.into() + ); + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha) + .saturating_sub(bob_stake_before), pending_alpha, epsilon = 1.into() ); @@ -1783,6 +1804,11 @@ fn test_get_root_children_drain_with_take() { Delegates::::insert(alice, PerU16::zero()); Delegates::::insert(bob, PerU16::zero()); + let alice_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&alice, &cold_alice, alpha); + let bob_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha); + // Lets drain! let pending_alpha = AlphaBalance::from(1_000_000_000); SubtensorModule::distribute_emission( @@ -1793,7 +1819,8 @@ fn test_get_root_children_drain_with_take() { AlphaBalance::ZERO, ); - // Bob makes it all. + // Alice's inherited share is taken entirely by Bob. Bob's own half + // remains a shared dividend while the childkey take is owner-only. close( AlphaDividendsPerSubnet::::get(alpha, alice).into(), 0, @@ -1801,6 +1828,20 @@ fn test_get_root_children_drain_with_take() { ); close( AlphaDividendsPerSubnet::::get(alpha, bob).into(), + (pending_alpha / 2.into()).into(), + 10, + ); + close( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&alice, &cold_alice, alpha) + .saturating_sub(alice_stake_before) + .into(), + 0, + 10, + ); + close( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha) + .saturating_sub(bob_stake_before) + .into(), pending_alpha.into(), 10, ); @@ -1871,6 +1912,11 @@ fn test_get_root_children_drain_with_half_take() { Delegates::::insert(alice, PerU16::zero()); Delegates::::insert(bob, PerU16::zero()); + let alice_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&alice, &cold_alice, alpha); + let bob_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha); + // Lets drain! let pending_alpha = AlphaBalance::from(1_000_000_000); SubtensorModule::distribute_emission( @@ -1881,7 +1927,8 @@ fn test_get_root_children_drain_with_half_take() { AlphaBalance::ZERO, ); - // Alice and Bob make the same amount. + // Half of Alice's inherited share remains with Alice and half becomes + // owner-only childkey take for Bob. Bob's shared dividends exclude it. close( AlphaDividendsPerSubnet::::get(alpha, alice).into(), (pending_alpha / 4.into()).into(), @@ -1889,12 +1936,105 @@ fn test_get_root_children_drain_with_half_take() { ); close( AlphaDividendsPerSubnet::::get(alpha, bob).into(), - 3 * u64::from(pending_alpha / 4.into()), + (pending_alpha / 2.into()).into(), + 10000, + ); + close( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&alice, &cold_alice, alpha) + .saturating_sub(alice_stake_before) + .into(), + (pending_alpha / 4.into()).into(), + 10000, + ); + close( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha) + .saturating_sub(bob_stake_before) + .into(), + (pending_alpha * 3.into() / 4.into()).into(), 10000, ); }); } +#[test] +fn test_root_childkey_take_is_owner_only_not_root_claimable() { + fn run_case(childkey_take: PerU16) -> (AlphaBalance, AlphaBalance, AlphaBalance) { + new_test_ext(1).execute_with(move || { + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + SubtensorModule::set_tao_weight(u64::MAX); + SubtensorModule::set_ck_burn(0); + + let cold_alice = U256::from(0); + let cold_bob = U256::from(1); + let alice = U256::from(2); + let bob = U256::from(3); + register_ok_neuron(alpha, alice, cold_alice, 0); + register_ok_neuron(alpha, bob, cold_bob, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_alice), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_bob), + bob, + )); + + let root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + NetUid::ROOT, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + NetUid::ROOT, + root_stake, + ); + + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + ChildkeyTake::::insert(bob, alpha, childkey_take); + Delegates::::insert(alice, PerU16::zero()); + Delegates::::insert(bob, PerU16::zero()); + + let bob_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha); + let pending_root_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha, + AlphaBalance::ZERO, + ); + + ( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob, &cold_bob, alpha) + .saturating_sub(bob_owner_stake_before), + RootAlphaDividendsPerSubnet::::get(alpha, alice), + RootAlphaDividendsPerSubnet::::get(alpha, bob), + ) + }) + } + + let without_child_take = run_case(PerU16::zero()); + let with_child_take = run_case(PerU16::from_parts(u16::MAX)); + + let owner_increase = with_child_take.0.saturating_sub(without_child_take.0); + let parent_reduction = without_child_take.1.saturating_sub(with_child_take.1); + let total_without = without_child_take.0 + without_child_take.1 + without_child_take.2; + let total_with = with_child_take.0 + with_child_take.1 + with_child_take.2; + + assert_eq!(without_child_take.0, AlphaBalance::ZERO); + assert!(owner_increase > AlphaBalance::ZERO); + assert_abs_diff_eq!(owner_increase, parent_reduction, epsilon = 10.into()); + assert_abs_diff_eq!(with_child_take.2, without_child_take.2, epsilon = 10.into()); + assert_abs_diff_eq!(total_with, total_without, epsilon = 10.into()); +} + // // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_with_weights --exact --show-output --nocapture // #[test] // fn test_get_root_children_with_weights() {