diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 818235a955..21f7866459 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -18,13 +18,18 @@ impl Pallet { .to_u64(), ); log::debug!("Block emission: {block_emission:?}"); - // --- 3. Run emission through network. + + // --- 3. Reveal matured weights. + Self::reveal_crv3_commits(); + // --- 4. Run emission through network. Self::run_coinbase(block_emission); - // --- 4. Set pending children on the epoch; but only after the coinbase has been run. + // --- 5. Update moving prices AFTER using them for emissions. + Self::update_moving_prices(); + // --- 6. Set pending children on the epoch; but only after the coinbase has been run. Self::try_set_pending_children(block_number); - // --- 5. Run auto-claim root divs. + // --- 7. Run auto-claim root divs. Self::run_auto_claim_root_divs(last_block_hash); - // --- 6. Populate root coldkey maps. + // --- 8. Populate root coldkey maps. Self::populate_root_coldkey_staking_maps(); // Return ok. @@ -261,4 +266,24 @@ impl Pallet { return next_value.saturating_to_num::().into(); } } + + pub fn update_moving_prices() { + let subnets_to_emit_to: Vec = + Self::get_subnets_to_emit_to(&Self::get_all_subnet_netuids()); + // Only update price EMA for subnets that we emit to. + for netuid_i in subnets_to_emit_to.iter() { + // Update moving prices after using them above. + Self::update_moving_price(*netuid_i); + } + } + + pub fn reveal_crv3_commits() { + let netuids: Vec = Self::get_all_subnet_netuids(); + for netuid in netuids.into_iter().filter(|netuid| *netuid != NetUid::ROOT) { + // Reveal matured weights. + if let Err(e) = Self::reveal_crv3_commits_for_subnet(netuid) { + log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); + }; + } + } } diff --git a/pallets/subtensor/src/coinbase/reveal_commits.rs b/pallets/subtensor/src/coinbase/reveal_commits.rs index 889c41d96a..6fdafa76da 100644 --- a/pallets/subtensor/src/coinbase/reveal_commits.rs +++ b/pallets/subtensor/src/coinbase/reveal_commits.rs @@ -36,7 +36,7 @@ pub struct LegacyWeightsTlockPayload { impl Pallet { /// The `reveal_crv3_commits` function is run at the very beginning of epoch `n`, - pub fn reveal_crv3_commits(netuid: NetUid) -> dispatch::DispatchResult { + pub fn reveal_crv3_commits_for_subnet(netuid: NetUid) -> dispatch::DispatchResult { let reveal_period = Self::get_reveal_period(netuid); let cur_block = Self::get_current_block_as_u64(); let cur_epoch = Self::get_epoch_index(netuid, cur_block); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 45eec7ddec..5cb2ea45f8 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -22,114 +22,66 @@ impl Pallet { pub fn run_coinbase(block_emission: U96F32) { // --- 0. Get current block. let current_block: u64 = Self::get_current_block_as_u64(); - log::debug!("Current block: {current_block:?}"); - - // --- 1. Get all netuids (filter out root) + log::debug!( + "Running coinbase for block {current_block:?} with block emission: {block_emission:?}" + ); + // --- 1. Get all subnets (excluding root). let subnets: Vec = Self::get_all_subnet_netuids() .into_iter() .filter(|netuid| *netuid != NetUid::ROOT) .collect(); - log::debug!("All subnet netuids: {subnets:?}"); + log::debug!("All subnets: {subnets:?}"); + // --- 2. Get subnets to emit to let subnets_to_emit_to: Vec = Self::get_subnets_to_emit_to(&subnets); log::debug!("Subnets to emit to: {subnets_to_emit_to:?}"); - // 2. Get subnets to emit to and emissions + // --- 3. Get emissions for subnets to emit to let subnet_emissions = Self::get_subnet_block_emissions(&subnets_to_emit_to, block_emission); + log::debug!("Subnet emissions: {subnet_emissions:?}"); let root_sell_flag = Self::get_network_root_sell_flag(&subnets_to_emit_to); + log::debug!("Root sell flag: {root_sell_flag:?}"); - // --- 3. Get subnet terms (tao_in, alpha_in, and alpha_out) - // Computation is described in detail in the dtao whitepaper. - let mut tao_in: BTreeMap = BTreeMap::new(); - let mut alpha_in: BTreeMap = BTreeMap::new(); - let mut alpha_out: BTreeMap = BTreeMap::new(); - let mut excess_tao: BTreeMap = BTreeMap::new(); - - // Only calculate for subnets that we are emitting to. - for netuid_i in subnets_to_emit_to.iter() { - // Get subnet price. - let price_i = T::SwapInterface::current_alpha_price((*netuid_i).into()); - log::debug!("price_i: {price_i:?}"); - // Emission is price over total. - let default_tao_in_i: U96F32 = subnet_emissions - .get(netuid_i) - .copied() - .unwrap_or(asfloat!(0)); - log::debug!("default_tao_in_i: {default_tao_in_i:?}"); - let default_alpha_in_i: U96F32 = - default_tao_in_i.safe_div_or(price_i, U96F32::saturating_from_num(0.0)); - log::debug!("default_alpha_in_i: {default_alpha_in_i:?}"); - // Get alpha_emission total - let alpha_emission_i: U96F32 = asfloat!( - Self::get_block_emission_for_issuance(Self::get_alpha_issuance(*netuid_i).into()) - .unwrap_or(0) - ); - log::debug!("alpha_emission_i: {alpha_emission_i:?}"); - - // Get initial alpha_in - let mut alpha_in_i: U96F32; - let mut tao_in_i: U96F32; + // --- 4. Emit to subnets for this block. + Self::emit_to_subnets(&subnets_to_emit_to, &subnet_emissions, root_sell_flag); - if default_alpha_in_i > alpha_emission_i { - alpha_in_i = alpha_emission_i; - tao_in_i = alpha_in_i.saturating_mul(price_i); - let difference_tao: U96F32 = default_tao_in_i.saturating_sub(tao_in_i); - excess_tao.insert(*netuid_i, difference_tao); - } else { - tao_in_i = default_tao_in_i; - alpha_in_i = default_alpha_in_i; - excess_tao.insert(*netuid_i, U96F32::from_num(0.0)); - } - log::debug!("alpha_in_i: {alpha_in_i:?}"); + // --- 5. Drain pending emissions. + let emissions_to_distribute = Self::drain_pending(&subnets, current_block); - // Get alpha_out. - let mut alpha_out_i = alpha_emission_i; - // Only emit TAO if the subnetwork allows registration. - if !Self::get_network_registration_allowed(*netuid_i) - && !Self::get_network_pow_registration_allowed(*netuid_i) - { - tao_in_i = asfloat!(0.0); - alpha_in_i = asfloat!(0.0); - alpha_out_i = asfloat!(0.0); - } - // Insert values into maps - tao_in.insert(*netuid_i, tao_in_i); - alpha_in.insert(*netuid_i, alpha_in_i); - alpha_out.insert(*netuid_i, alpha_out_i); - } - log::debug!("tao_in: {tao_in:?}"); - log::debug!("alpha_in: {alpha_in:?}"); - log::debug!("alpha_out: {alpha_out:?}"); - log::debug!("excess_tao: {excess_tao:?}"); - log::debug!("root_sell_flag: {root_sell_flag:?}"); + // --- 6. Distribute the emissions to the subnets. + Self::distribute_emissions_to_subnets(&emissions_to_distribute); + } - // --- 4. Inject and buy Alpha with any excess TAO. + pub fn inject_and_maybe_swap( + subnets_to_emit_to: &[NetUid], + tao_in: &BTreeMap, + alpha_in: &BTreeMap, + excess_tao: &BTreeMap, + ) { for netuid_i in subnets_to_emit_to.iter() { let tao_in_i: TaoCurrency = tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); let alpha_in_i: AlphaCurrency = - AlphaCurrency::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0)))); - let difference_tao: U96F32 = *excess_tao.get(netuid_i).unwrap_or(&asfloat!(0)); + tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); + let tao_to_swap_with: TaoCurrency = + tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); - if difference_tao > asfloat!(0) { + if tao_to_swap_with > TaoCurrency::ZERO { let buy_swap_result = Self::swap_tao_for_alpha( *netuid_i, - tou64!(difference_tao).into(), + tao_to_swap_with, T::SwapInterface::max_price(), true, ); if let Ok(buy_swap_result_ok) = buy_swap_result { - let bought_alpha = AlphaCurrency::from(buy_swap_result_ok.amount_paid_out); + let bought_alpha: AlphaCurrency = buy_swap_result_ok.amount_paid_out.into(); Self::recycle_subnet_alpha(*netuid_i, bought_alpha); } } - } - // --- 5. Update counters - for netuid_i in subnets_to_emit_to.iter() { // Inject Alpha in. let alpha_in_i = AlphaCurrency::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0)))); @@ -137,52 +89,94 @@ impl Pallet { SubnetAlphaIn::::mutate(*netuid_i, |total| { *total = total.saturating_add(alpha_in_i); }); - // Injection Alpha out. - let alpha_out_i = - AlphaCurrency::from(tou64!(*alpha_out.get(netuid_i).unwrap_or(&asfloat!(0)))); - SubnetAlphaOutEmission::::insert(*netuid_i, alpha_out_i); - SubnetAlphaOut::::mutate(*netuid_i, |total| { - *total = total.saturating_add(alpha_out_i); - }); + // Inject TAO in. - let tao_in_i: TaoCurrency = + let injected_tao: TaoCurrency = tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - SubnetTaoInEmission::::insert(*netuid_i, TaoCurrency::from(tao_in_i)); + SubnetTaoInEmission::::insert(*netuid_i, injected_tao); SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tao_in_i.into()); + *total = total.saturating_add(injected_tao); }); TotalStake::::mutate(|total| { - *total = total.saturating_add(tao_in_i.into()); + *total = total.saturating_add(injected_tao); }); - let difference_tao: U96F32 = *excess_tao.get(netuid_i).unwrap_or(&asfloat!(0)); + // Update total TAO issuance. + let difference_tao = tou64!(*excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))); TotalIssuance::::mutate(|total| { - *total = total.saturating_add(tao_in_i.into()); - *total = total.saturating_add(tou64!(difference_tao).into()); + *total = total + .saturating_add(injected_tao.into()) + .saturating_add(difference_tao.into()); }); } + } - // --- 6. Compute owner cuts and remove them from alpha_out remaining. - // Remove owner cuts here so that we can properly seperate root dividends in the next step. - // Owner cuts are accumulated and then fed to the drain at the end of this func. - let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); - let mut owner_cuts: BTreeMap = BTreeMap::new(); - for netuid_i in subnets_to_emit_to.iter() { - // Get alpha out. - let alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0)); - log::debug!("alpha_out_i: {alpha_out_i:?}"); - // Calculate the owner cut. - let owner_cut_i: U96F32 = alpha_out_i.saturating_mul(cut_percent); - log::debug!("owner_cut_i: {owner_cut_i:?}"); - // Save owner cut. - *owner_cuts.entry(*netuid_i).or_insert(asfloat!(0)) = owner_cut_i; - // Save new alpha_out. - alpha_out.insert(*netuid_i, alpha_out_i.saturating_sub(owner_cut_i)); - // Accumulate the owner cut in pending. - PendingOwnerCut::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(owner_cut_i).into()); - }); + pub fn get_subnet_terms( + subnet_emissions: &BTreeMap, + ) -> ( + BTreeMap, + BTreeMap, + BTreeMap, + BTreeMap, + ) { + // Computation is described in detail in the dtao whitepaper. + let mut tao_in: BTreeMap = BTreeMap::new(); + let mut alpha_in: BTreeMap = BTreeMap::new(); + let mut alpha_out: BTreeMap = BTreeMap::new(); + let mut excess_tao: BTreeMap = BTreeMap::new(); + + // Only calculate for subnets that we are emitting to. + for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() { + // Get alpha_emission this block. + let alpha_emission_i: U96F32 = asfloat!( + Self::get_block_emission_for_issuance(Self::get_alpha_issuance(netuid_i).into()) + .unwrap_or(0) + ); + log::debug!("alpha_emission_i: {alpha_emission_i:?}"); + + // Get subnet price. + let price_i: U96F32 = T::SwapInterface::current_alpha_price(netuid_i.into()); + log::debug!("price_i: {price_i:?}"); + + let mut tao_in_i: U96F32 = tao_emission_i; + let alpha_out_i: U96F32 = alpha_emission_i; + let mut alpha_in_i: U96F32 = tao_emission_i.safe_div_or(price_i, U96F32::from_num(0.0)); + + if alpha_in_i > alpha_emission_i { + alpha_in_i = alpha_emission_i; + tao_in_i = alpha_in_i.saturating_mul(price_i); + } + + let excess_amount: U96F32 = tao_emission_i.saturating_sub(tao_in_i); + excess_tao.insert(netuid_i, excess_amount); + + // Insert values into maps + tao_in.insert(netuid_i, tao_in_i); + alpha_in.insert(netuid_i, alpha_in_i); + alpha_out.insert(netuid_i, alpha_out_i); } + (tao_in, alpha_in, alpha_out, excess_tao) + } + + pub fn emit_to_subnets( + subnets_to_emit_to: &[NetUid], + subnet_emissions: &BTreeMap, + root_sell_flag: bool, + ) { + // --- 1. Get subnet terms (tao_in, alpha_in, and alpha_out) + // and excess_tao amounts. + let (tao_in, alpha_in, alpha_out, excess_amount) = Self::get_subnet_terms(subnet_emissions); + + log::debug!("tao_in: {tao_in:?}"); + log::debug!("alpha_in: {alpha_in:?}"); + log::debug!("alpha_out: {alpha_out:?}"); + log::debug!("excess_amount: {excess_amount:?}"); + + // --- 2. Inject TAO and ALPHA to pool and swap with excess TAO. + Self::inject_and_maybe_swap(subnets_to_emit_to, &tao_in, &alpha_in, &excess_amount); + + // --- 3. Inject ALPHA for participants. + let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); // Get total TAO on root. let root_tao: U96F32 = asfloat!(SubnetTAO::::get(NetUid::ROOT)); @@ -191,12 +185,25 @@ impl Pallet { let tao_weight: U96F32 = root_tao.saturating_mul(Self::get_tao_weight()); log::debug!("tao_weight: {tao_weight:?}"); - // --- 7. Seperate out root dividends in alpha and keep them. - // Then accumulate those dividends for later. for netuid_i in subnets_to_emit_to.iter() { - // Get remaining alpha out. - let alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0.0)); - log::debug!("alpha_out_i: {alpha_out_i:?}"); + // Get alpha_out for this block. + let mut alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0)); + + let alpha_created: AlphaCurrency = AlphaCurrency::from(tou64!(alpha_out_i)); + SubnetAlphaOutEmission::::insert(*netuid_i, alpha_created); + SubnetAlphaOut::::mutate(*netuid_i, |total| { + *total = total.saturating_add(alpha_created); + }); + + // Calculate the owner cut. + let owner_cut_i: U96F32 = alpha_out_i.saturating_mul(cut_percent); + log::debug!("owner_cut_i: {owner_cut_i:?}"); + // Deduct owner cut from alpha_out. + alpha_out_i = alpha_out_i.saturating_sub(owner_cut_i); + // Accumulate the owner cut in pending. + PendingOwnerCut::::mutate(*netuid_i, |total| { + *total = total.saturating_add(tou64!(owner_cut_i).into()); + }); // Get ALPHA issuance. let alpha_issuance: U96F32 = asfloat!(Self::get_alpha_issuance(*netuid_i)); @@ -214,24 +221,16 @@ impl Pallet { .saturating_mul(asfloat!(0.5)); // 50% to validators. log::debug!("root_alpha: {root_alpha:?}"); - if root_sell_flag { - // Only accumulate root alpha divs if root sell is allowed. - PendingRootAlphaDivs::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(root_alpha).into()); - }); - } else { - // If we are not selling the root alpha, we should recycle it. - Self::recycle_subnet_alpha(*netuid_i, AlphaCurrency::from(tou64!(root_alpha))); - } - // Get pending server alpha, which is the miner cut of the alpha out. // Currently miner cut is 50% of the alpha out. let pending_server_alpha = alpha_out_i.saturating_mul(asfloat!(0.5)); + log::debug!("pending_server_alpha: {pending_server_alpha:?}"); // The total validator alpha is the remaining alpha out minus the server alpha. let total_validator_alpha = alpha_out_i.saturating_sub(pending_server_alpha); - + log::debug!("total_validator_alpha: {total_validator_alpha:?}"); // The alpha validators don't get the root alpha. let pending_validator_alpha = total_validator_alpha.saturating_sub(root_alpha); + log::debug!("pending_validator_alpha: {pending_validator_alpha:?}"); // Accumulate the server alpha emission. PendingServerEmission::::mutate(*netuid_i, |total| { @@ -241,23 +240,35 @@ impl Pallet { PendingValidatorEmission::::mutate(*netuid_i, |total| { *total = total.saturating_add(tou64!(pending_validator_alpha).into()); }); - } - // --- 8. Update moving prices after using them in the emission calculation. - // Only update price EMA for subnets that we emit to. - for netuid_i in subnets_to_emit_to.iter() { - // Update moving prices after using them above. - Self::update_moving_price(*netuid_i); + if root_sell_flag { + // Only accumulate root alpha divs if root sell is allowed. + PendingRootAlphaDivs::::mutate(*netuid_i, |total| { + *total = total.saturating_add(tou64!(root_alpha).into()); + }); + } else { + // If we are not selling the root alpha, we should recycle it. + Self::recycle_subnet_alpha(*netuid_i, AlphaCurrency::from(tou64!(root_alpha))); + } } + } - // --- 9. Drain pending emission through the subnet based on tempo. + pub fn drain_pending( + subnets: &[NetUid], + current_block: u64, + ) -> BTreeMap { + // Map of netuid to (pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut). + let mut emissions_to_distribute: BTreeMap< + NetUid, + (AlphaCurrency, AlphaCurrency, AlphaCurrency, AlphaCurrency), + > = BTreeMap::new(); + // --- Drain pending emissions for all subnets hat are at their tempo. // Run the epoch for *all* subnets, even if we don't emit anything. for &netuid in subnets.iter() { - // Reveal matured weights. - if let Err(e) = Self::reveal_crv3_commits(netuid) { - log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); - }; - // Pass on subnets that have not reached their tempo. + // Increment blocks since last step. + BlocksSinceLastStep::::mutate(netuid, |total| *total = total.saturating_add(1)); + + // Run the epoch if applicable. if Self::should_run_epoch(netuid, current_block) && Self::is_epoch_input_state_consistent(netuid) { @@ -272,27 +283,49 @@ impl Pallet { let pending_validator_alpha = PendingValidatorEmission::::get(netuid); PendingValidatorEmission::::insert(netuid, AlphaCurrency::ZERO); - // Get and drain the subnet pending root alpha divs. + // Get and drain the pending Alpha for root divs. let pending_root_alpha = PendingRootAlphaDivs::::get(netuid); PendingRootAlphaDivs::::insert(netuid, AlphaCurrency::ZERO); - // Get owner cut and drain. + // Get and drain the pending owner cut. let owner_cut = PendingOwnerCut::::get(netuid); PendingOwnerCut::::insert(netuid, AlphaCurrency::ZERO); - // Distribute the emission. - Self::distribute_emission( + // Save the emissions to distribute. + emissions_to_distribute.insert( netuid, - pending_server_alpha, - pending_validator_alpha, - pending_root_alpha, - owner_cut, + ( + pending_server_alpha, + pending_validator_alpha, + pending_root_alpha, + owner_cut, + ), ); - } else { - // Increment - BlocksSinceLastStep::::mutate(netuid, |total| *total = total.saturating_add(1)); } } + emissions_to_distribute + } + + pub fn distribute_emissions_to_subnets( + emissions_to_distribute: &BTreeMap< + NetUid, + (AlphaCurrency, AlphaCurrency, AlphaCurrency, AlphaCurrency), + >, + ) { + for ( + &netuid, + &(pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut), + ) in emissions_to_distribute.iter() + { + // Distribute the emission to the subnet. + Self::distribute_emission( + netuid, + pending_server_alpha, + pending_validator_alpha, + pending_root_alpha, + pending_owner_cut, + ); + } } pub fn get_network_root_sell_flag(subnets_to_emit_to: &[NetUid]) -> bool { diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 895a908ba8..1a5a8ec402 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -7,10 +7,18 @@ use subtensor_swap_interface::SwapHandler; impl Pallet { pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec { + // Filter out root subnet. // Filter out subnets with no first emission block number. subnets .iter() + .filter(|netuid| !netuid.is_root()) .filter(|netuid| FirstEmissionBlockNumber::::get(*netuid).is_some()) + .filter(|netuid| SubtokenEnabled::::get(*netuid)) + .filter(|&netuid| { + // Only emit TAO if the subnetwork allows registration. + Self::get_network_registration_allowed(*netuid) + || Self::get_network_pow_registration_allowed(*netuid) + }) .copied() .collect() } diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 596933b898..2ba9f93556 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -14,7 +14,10 @@ use approx::assert_abs_diff_eq; use frame_support::assert_ok; use pallet_subtensor_swap::position::PositionId; use sp_core::U256; -use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; +use substrate_fixed::{ + transcendental::sqrt, + types::{I64F64, I96F32, U64F64, U96F32}, +}; use subtensor_runtime_common::{AlphaCurrency, NetUidStorageIndex}; use subtensor_swap_interface::{SwapEngine, SwapHandler}; @@ -3328,6 +3331,501 @@ fn test_mining_emission_distribution_with_root_sell() { }); } +#[test] +fn test_coinbase_subnets_with_no_reg_get_no_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + let netuid1 = add_dynamic_network(&U256::from(3), &U256::from(4)); + + // Setup initial state + SubtokenEnabled::::insert(netuid0, true); + SubtokenEnabled::::insert(netuid1, true); + FirstEmissionBlockNumber::::insert(netuid0, 0); + FirstEmissionBlockNumber::::insert(netuid1, 0); + // Explicitly allow registration for both subnets + NetworkRegistrationAllowed::::insert(netuid0, true); + NetworkRegistrationAllowed::::insert(netuid1, true); + NetworkPowRegistrationAllowed::::insert(netuid0, false); + NetworkPowRegistrationAllowed::::insert(netuid1, true); + + // Note that netuid0 has only one method allowed + // And, netuid1 has *both* methods allowed + // Both should be in the list. + let subnets_to_emit_to_0 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + // Check that both subnets are in the list + assert_eq!(subnets_to_emit_to_0.len(), 2); + assert!(subnets_to_emit_to_0.contains(&netuid0)); + assert!(subnets_to_emit_to_0.contains(&netuid1)); + + // Disabled registration of both methods on ONLY netuid0 + NetworkRegistrationAllowed::::insert(netuid0, false); + NetworkPowRegistrationAllowed::::insert(netuid0, false); + + // Check that netuid0 is not in the list + let subnets_to_emit_to_1 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + assert_eq!(subnets_to_emit_to_1.len(), 1); + assert!(!subnets_to_emit_to_1.contains(&netuid0)); + // Netuid1 still in the list + assert!(subnets_to_emit_to_1.contains(&netuid1)); + }); +} + +// Tests for the excess TAO condition +#[test] +fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoCurrency::from(1_000_000_000_000_000), + AlphaCurrency::from(1_000_000_000_000_000), + ); + // Initialize swap v3 + Swap::maybe_initialize_v3(netuid0); + + // Set netuid0 to have price tao_emission / price > alpha_emission + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); + let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); + let sqrt_price_to_set: U64F64 = sqrt(price_to_set).unwrap(); + + let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) + .saturating_mul(price_to_set_fixed) + .saturating_add(U96F32::saturating_from_num(0.01)); + + // Set the price + pallet_subtensor_swap::AlphaSqrtPrice::::insert(netuid0, sqrt_price_to_set); + // Check the price is set + assert_abs_diff_eq!( + pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), + price_to_set.to_num::(), + epsilon = 0.001 + ); + + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + + // Check our condition is met + assert!(tao_emission / price_to_set_fixed > alpha_emission); + + // alpha_out should be the alpha_emission, always + assert_abs_diff_eq!( + alpha_out[&netuid0].to_num::(), + alpha_emission.to_num::(), + epsilon = 0.01 + ); + + // alpha_in should equal the alpha_emission + assert_abs_diff_eq!( + alpha_in[&netuid0].to_num::(), + alpha_emission.to_num::(), + epsilon = 0.01 + ); + // tao_in should be the alpha_in at the ratio of the price + assert_abs_diff_eq!( + tao_in[&netuid0].to_num::(), + alpha_in[&netuid0] + .saturating_mul(price_to_set_fixed) + .to_num::(), + epsilon = 0.01 + ); + + // excess_tao should be the difference between the tao_emission and the tao_in + assert_abs_diff_eq!( + excess_tao[&netuid0].to_num::(), + tao_emission.to_num::() - tao_in[&netuid0].to_num::(), + epsilon = 0.01 + ); + }); +} + +#[test] +fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoCurrency::from(1_000_000_000_000_000), + AlphaCurrency::from(1_000_000_000_000_000), + ); + // Initialize swap v3 + Swap::maybe_initialize_v3(netuid0); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let tao_emission = U96F32::saturating_from_num(34566756_u64); + + let price: U96F32 = Swap::current_alpha_price(netuid0); + + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + + // Check our condition is met + assert!(tao_emission / price <= alpha_emission); + + // alpha_out should be the alpha_emission, always + assert_abs_diff_eq!( + alpha_out[&netuid0].to_num::(), + alpha_emission.to_num::(), + epsilon = 0.1 + ); + + // assuming alpha_in < alpha_emission + // Then alpha_in should be tao_emission / price + assert_abs_diff_eq!( + alpha_in[&netuid0].to_num::(), + tao_emission.to_num::() / price.to_num::(), + epsilon = 0.01 + ); + + // tao_in should be the tao_emission + assert_abs_diff_eq!( + tao_in[&netuid0].to_num::(), + tao_emission.to_num::(), + epsilon = 0.01 + ); + + // excess_tao should be 0 + assert_abs_diff_eq!( + excess_tao[&netuid0].to_num::(), + tao_emission.to_num::() - tao_in[&netuid0].to_num::(), + epsilon = 0.01 + ); + }); +} + +// Tests for the inject and swap are in the right order. +#[test] +fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoCurrency::from(1_000_000_000_000_000), + AlphaCurrency::from(1_000_000_000_000_000), + ); + // Initialize swap v3 + Swap::maybe_initialize_v3(netuid0); + + let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); + let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); + // We have excess TAO, so we will be swapping with it. + let excess_tao = BTreeMap::from([(netuid0, U96F32::saturating_from_num(789100))]); + + // Run the inject and maybe swap + SubtensorModule::inject_and_maybe_swap(&[netuid0], &tao_in, &alpha_in, &excess_tao); + + let tao_in_after = SubnetTAO::::get(netuid0); + let alpha_in_after = SubnetAlphaIn::::get(netuid0); + + // Make sure that when we inject and swap, we do it in the right order. + // Thereby not skewing the ratio away from the price. + let ratio_after: U96F32 = U96F32::saturating_from_num(alpha_in_after.to_u64()) + .saturating_div(U96F32::saturating_from_num(tao_in_after.to_u64())); + let price_after: U96F32 = U96F32::saturating_from_num( + pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), + ); + assert_abs_diff_eq!( + ratio_after.to_num::(), + price_after.to_num::(), + epsilon = 1.0 + ); + }); +} + +#[test] +fn test_coinbase_drain_pending_increments_blockssincelaststep() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + + let blocks_since_last_step_before = BlocksSinceLastStep::::get(netuid0); + + // Check that blockssincelaststep is incremented + SubtensorModule::drain_pending(&[netuid0], 1); + + let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); + assert!(blocks_since_last_step_after > blocks_since_last_step_before); + assert_eq!( + blocks_since_last_step_after, + blocks_since_last_step_before + 1 + ); + }); +} + +#[test] +fn test_coinbase_drain_pending_resets_blockssincelaststep() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + Tempo::::insert(netuid0, 100); + // Ensure the block number we use is the tempo block + let block_number = 98; + assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); + + let blocks_since_last_step_before = 12345678; + BlocksSinceLastStep::::insert(netuid0, blocks_since_last_step_before); + LastMechansimStepBlock::::insert(netuid0, 12345); // garbage value + + // Check that blockssincelaststep is reset to 0 on tempo + SubtensorModule::drain_pending(&[netuid0], block_number); + + let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); + assert_eq!(blocks_since_last_step_after, 0); + // Also check LastMechansimStepBlock is set to the block number we ran on + assert_eq!(LastMechansimStepBlock::::get(netuid0), block_number); + }); +} + +#[test] +fn test_coinbase_drain_pending_gets_counters_and_resets_them() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + Tempo::::insert(netuid0, 100); + // Ensure the block number we use is the tempo block + let block_number = 98; + assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); + + let pending_server_em = AlphaCurrency::from(123434534); + let pending_validator_em = AlphaCurrency::from(111111); + let pending_root = AlphaCurrency::from(12222222); + let pending_owner_cut = AlphaCurrency::from(12345678); + + PendingServerEmission::::insert(netuid0, pending_server_em); + PendingValidatorEmission::::insert(netuid0, pending_validator_em); + PendingRootAlphaDivs::::insert(netuid0, pending_root); + PendingOwnerCut::::insert(netuid0, pending_owner_cut); + + let emissions_to_distribute = SubtensorModule::drain_pending(&[netuid0], block_number); + assert_eq!(emissions_to_distribute.len(), 1); + assert_eq!( + emissions_to_distribute[&netuid0], + ( + pending_server_em, + pending_validator_em, + pending_root, + pending_owner_cut + ) + ); + + // Check that the pending emissions are reset + assert_eq!( + PendingServerEmission::::get(netuid0), + AlphaCurrency::ZERO + ); + assert_eq!( + PendingValidatorEmission::::get(netuid0), + AlphaCurrency::ZERO + ); + assert_eq!( + PendingRootAlphaDivs::::get(netuid0), + AlphaCurrency::ZERO + ); + assert_eq!(PendingOwnerCut::::get(netuid0), AlphaCurrency::ZERO); + }); +} + +#[test] +fn test_coinbase_emit_to_subnets_with_no_root_sell() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + // Set owner cut to ~10% + SubnetOwnerCut::::set(u16::MAX / 10); + mock::setup_reserves( + netuid0, + TaoCurrency::from(1_000_000_000_000_000), + AlphaCurrency::from(1_000_000_000_000_000), + ); + // Initialize swap v3 + Swap::maybe_initialize_v3(netuid0); + + let tao_emission = U96F32::saturating_from_num(12345678); + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // NO root sell + let root_sell_flag = false; + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price: U96F32 = Swap::current_alpha_price(netuid0); + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + // Based on the price, we should have NO excess TAO + assert!(tao_emission / price <= alpha_emission); + + // ==== Run the emit to subnets ===== + SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, root_sell_flag); + + // Find the owner cut expected + let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); + log::info!("owner_cut_expected: {owner_cut_expected:?}"); + log::info!("alpha_emission: {alpha_emission:?}"); + log::info!("owner_cut: {owner_cut:?}"); + + let alpha_issuance: U96F32 = + U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); + let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); + let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); + let root_prop: U96F32 = tao_weight + .checked_div(tao_weight.saturating_add(alpha_issuance)) + .unwrap_or(U96F32::min_value()); + // Expect root alpha divs to be root prop * alpha emission + let expected_root_alpha_divs: AlphaCurrency = AlphaCurrency::from( + root_prop + .saturating_mul(alpha_emission) + .saturating_to_num::(), + ); + + // ===== Check that the pending emissions are set correctly ===== + // Owner cut is as expected + assert_abs_diff_eq!( + PendingOwnerCut::::get(netuid0).to_u64(), + owner_cut_expected.saturating_to_num::(), + epsilon = 200_u64 + ); + // NO root sell, so no root alpha divs + assert_eq!( + PendingRootAlphaDivs::::get(netuid0), + AlphaCurrency::ZERO + ); + // Should be alpha_emission minus the owner cut, + assert_abs_diff_eq!( + PendingServerEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_to_num::(), + epsilon = 200_u64 + ); + // We ALWAYS deduct the root alpha divs + assert_abs_diff_eq!( + PendingValidatorEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_sub(expected_root_alpha_divs.to_u64().into()) + .saturating_to_num::(), + epsilon = 200_u64 + ); + }); +} + +#[test] +fn test_coinbase_emit_to_subnets_with_root_sell() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + // Set owner cut to ~10% + SubnetOwnerCut::::set(u16::MAX / 10); + mock::setup_reserves( + netuid0, + TaoCurrency::from(1_000_000_000_000_000), + AlphaCurrency::from(1_000_000_000_000_000), + ); + // Initialize swap v3 + Swap::maybe_initialize_v3(netuid0); + + let tao_emission = U96F32::saturating_from_num(12345678); + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // NO root sell + let root_sell_flag = true; + + let alpha_emission: U96F32 = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price: U96F32 = Swap::current_alpha_price(netuid0); + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + // Based on the price, we should have NO excess TAO + assert!(tao_emission / price <= alpha_emission); + + // ==== Run the emit to subnets ===== + SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, root_sell_flag); + + // Find the owner cut expected + let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); + log::info!("owner_cut_expected: {owner_cut_expected:?}"); + log::info!("alpha_emission: {alpha_emission:?}"); + log::info!("owner_cut: {owner_cut:?}"); + + let alpha_issuance: U96F32 = + U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); + let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); + let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); + let root_prop: U96F32 = tao_weight + .checked_div(tao_weight.saturating_add(alpha_issuance)) + .unwrap_or(U96F32::min_value()); + // Expect root alpha divs to be root prop * alpha emission + let expected_root_alpha_divs: AlphaCurrency = AlphaCurrency::from( + root_prop + .saturating_mul(alpha_emission) + .saturating_to_num::(), + ); + + // ===== Check that the pending emissions are set correctly ===== + // Owner cut is as expected + assert_abs_diff_eq!( + PendingOwnerCut::::get(netuid0).to_u64(), + owner_cut_expected.saturating_to_num::(), + epsilon = 200_u64 + ); + // YES root sell, so we have root alpha divs + assert_abs_diff_eq!( + PendingRootAlphaDivs::::get(netuid0).to_u64(), + expected_root_alpha_divs.to_u64(), + epsilon = 200_u64 + ); + // Should be alpha_emission minus the owner cut + assert_abs_diff_eq!( + PendingServerEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_to_num::(), + epsilon = 200_u64 + ); + // Validator emission is also minus root alpha divs + assert_abs_diff_eq!( + PendingValidatorEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_sub(expected_root_alpha_divs.to_u64().into()) + .saturating_to_num::(), + epsilon = 200_u64 + ); + }); +} + // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_pending_emission_start_call_not_done --exact --show-output --nocapture #[test] fn test_pending_emission_start_call_not_done() { diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index a71a225dc7..21d37984e4 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -5016,7 +5016,7 @@ fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { step_epochs(1, netuid); // Attempt to reveal commits after the reveal epoch has passed - assert_ok!(SubtensorModule::reveal_crv3_commits(netuid)); + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); // Verify that the weights for the neuron have not been set let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); @@ -5353,7 +5353,7 @@ fn test_reveal_crv3_commits_decryption_failure() { }, ); - assert_ok!(SubtensorModule::reveal_crv3_commits(netuid)); + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) .expect("Failed to get neuron UID for hotkey") as usize; @@ -5973,7 +5973,7 @@ fn test_reveal_crv3_commits_removes_past_epoch_commits() { // --------------------------------------------------------------------- // Run the reveal pass WITHOUT a pulse – only expiry housekeeping runs. // --------------------------------------------------------------------- - assert_ok!(SubtensorModule::reveal_crv3_commits(netuid)); + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); // past_epoch (< reveal_epoch) must be gone assert!( diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 34b5e624e6..de08022060 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -68,7 +68,7 @@ impl Pallet { } // initializes V3 swap for a subnet if needed - pub(super) fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { + pub fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { if SwapV3Initialized::::get(netuid) { return Ok(()); }