From b256fc4f14f11ecfaa043c472a57bce6663881de Mon Sep 17 00:00:00 2001 From: kyron <266216617+kyron1112567@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:55:02 +0000 Subject: [PATCH] fix: sum emission across mechanisms instead of weighting it Emission is an absolute alpha amount, not a normalized ratio, so aggregating it as a weighted sum reported roughly 1/N of what a hotkey on an N-mechanism subnet was actually paid. Sum it like server_emission and validator_emission, which come from the same per-mechanism split. Emission also orders pruning candidates, so with an uneven split this could rank a higher earner below a lower one. Bump spec_version to 443. --- pallets/subtensor/src/subnets/mechanism.rs | 31 +++------- pallets/subtensor/src/tests/mechanism.rs | 68 ++++++++++++++++++++-- runtime/src/lib.rs | 2 +- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/pallets/subtensor/src/subnets/mechanism.rs b/pallets/subtensor/src/subnets/mechanism.rs index 30c4e90086..d5b8c2ba48 100644 --- a/pallets/subtensor/src/subnets/mechanism.rs +++ b/pallets/subtensor/src/subnets/mechanism.rs @@ -264,17 +264,6 @@ impl Pallet { .saturating_to_num::() } - fn weighted_acc_alpha( - existing: AlphaBalance, - added: AlphaBalance, - weight: U64F64, - ) -> AlphaBalance { - U64F64::saturating_from_num(existing) - .saturating_add(U64F64::saturating_from_num(added).saturating_mul(weight)) - .saturating_to_num::() - .into() - } - /// Splits rao_emission between different sub-subnets using `split_emissions` function. /// /// Runs the epoch function for each sub-subnet and consolidates hotkey_emission @@ -323,7 +312,14 @@ impl Pallet { .server_emission .saturating_add(terms.server_emission); - // The rest of the terms need to be aggregated as weighted sum + // Combined emission is an absolute alpha amount, not a ratio: + // per mechanism it equals server_emission + validator_emission, so + // it has to be summed like those two. Weighting it would report a + // fraction of what the hotkey was actually paid. + acc_terms.emission = acc_terms.emission.saturating_add(terms.emission); + + // The rest of the terms are normalized ratios and are aggregated + // as a weighted sum acc_terms.dividend = Self::weighted_acc_u16( acc_terms.dividend, terms.dividend, @@ -335,11 +331,6 @@ impl Pallet { sub_weight, ); acc_terms.active |= terms.active; - acc_terms.emission = Self::weighted_acc_alpha( - acc_terms.emission, - terms.emission, - sub_weight, - ); acc_terms.consensus = Self::weighted_acc_u16( acc_terms.consensus, terms.consensus, @@ -367,11 +358,7 @@ impl Pallet { sub_weight, ), active: terms.active, // booleans are ORed across subs - emission: Self::weighted_acc_alpha( - 0u64.into(), - terms.emission, - sub_weight, - ), + emission: terms.emission, // absolute amount, summed across subs consensus: Self::weighted_acc_u16(0, terms.consensus, sub_weight), validator_trust: Self::weighted_acc_u16( 0, diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index 764e67617e..737b800da6 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -29,6 +29,7 @@ // - [x] Mechanism limit can be set up to 8 (with admin pallet) // - [x] When reduction of mechanism limit occurs, Weights, Incentive, LastUpdate, Bonds, and WeightCommits are cleared // - [x] Epoch terms of subnet are weighted sum (or logical OR) of all mechanism epoch terms +// - [x] Emission is summed (not weighted) across mechanisms, matching paid emission // - [x] Subnet epoch terms persist in state // - [x] Mechanism epoch terms persist in state // - [x] "Yuma Emergency Mode" (consensus sum is 0 for a mechanism), emission distributed by stake @@ -803,10 +804,6 @@ fn epoch_with_mechanisms_persists_and_aggregates_all_terms() { (U64F64::saturating_from_num(a) * w0 + U64F64::saturating_from_num(b) * w1) .saturating_to_num::() }; - let wu64 = |a: u64, b: u64| -> u64 { - (U64F64::saturating_from_num(a) * w0 + U64F64::saturating_from_num(b) * w1) - .saturating_to_num::() - }; // For each UID, compute expected aggregate from out0/out1 terms let check_uid = |uid: usize, hk: &U256| { @@ -820,8 +817,9 @@ fn epoch_with_mechanisms_persists_and_aggregates_all_terms() { t0.new_validator_permit || t1.new_validator_permit ); - // Emission (u64) - let exp_em = wu64(u64::from(t0.emission), u64::from(t1.emission)); + // Emission is an absolute alpha amount and is summed across mechanisms, + // not weighted, so it matches what the hotkey is actually paid. + let exp_em = u64::from(t0.emission).saturating_add(u64::from(t1.emission)); assert_abs_diff_eq!(u64::from(emission_v[uid]), exp_em, epsilon = 1); // u16 terms @@ -887,6 +885,64 @@ fn epoch_with_mechanisms_no_weight_no_incentive() { }); } +// Persisted `Emission` is what the metagraph reports per UID. It has to equal the +// alpha the hotkey is actually paid (server + validator emission summed over every +// mechanism). Aggregating it as a weighted sum instead under-reported a multi-mechanism +// subnet by roughly the mechanism count, and — because `Emission` also orders pruning +// candidates — an uneven split could rank a higher earner below a lower one. +#[test] +fn epoch_with_mechanisms_persisted_emission_matches_paid_emission() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let idx0 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(0)); + let idx1 = SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(1)); + + let ck0 = U256::from(1); + let hk0 = U256::from(2); + let ck1 = U256::from(3); + let hk1 = U256::from(4); + let hk2 = U256::from(6); + let emission = AlphaBalance::from(1_000_000_000u64); + + mock_epoch_state(netuid, ck0, hk0, ck1, hk1); + mock_3_neurons(netuid, hk2); + + // Two mechanisms on an uneven 25/75 split, with each miner favored by a + // different mechanism so the weighting cannot cancel out. + MechanismCountCurrent::::insert(netuid, MechId::from(2u8)); + let split0 = u16::MAX / 4; + MechanismEmissionSplit::::insert(netuid, vec![split0, u16::MAX - split0]); + + ValidatorPermit::::insert(netuid, vec![true, false, false]); + Weights::::insert( + idx0, + 0, + vec![(1u16, 0xFFFF / 5 * 3), (2u16, 0xFFFF / 5 * 2)], + ); + Weights::::insert(idx1, 0, vec![(1u16, 0xFFFF / 5), (2u16, 0xFFFF / 5 * 4)]); + + let agg = SubtensorModule::epoch_with_mechanisms(netuid, emission); + + // What each hotkey is actually credited this epoch. + let paid: BTreeMap = agg + .into_iter() + .map(|(hk, se, ve)| (hk, u64::from(se).saturating_add(u64::from(ve)))) + .collect(); + + let emission_v = Emission::::get(netuid); + + for (uid, hk) in [(0_usize, hk0), (1_usize, hk1), (2_usize, hk2)] { + let reported = u64::from(emission_v[uid]); + let credited = paid.get(&hk).copied().expect("hotkey present in aggregate"); + assert_abs_diff_eq!(reported, credited, epsilon = 8); + } + + // The whole epoch emission is accounted for, not a weighted fraction of it. + let total_reported: u64 = emission_v.iter().map(|e| u64::from(*e)).sum(); + assert_abs_diff_eq!(total_reported, u64::from(emission), epsilon = 16); + }); +} + #[test] fn neuron_dereg_cleans_weights_across_subids() { new_test_ext(1).execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f4ad13bf81..27fe5f0c2c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,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: 442, + spec_version: 443, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,