From 4d5725e527eaf3781ca7a1c2f46326d8189600f8 Mon Sep 17 00:00:00 2001 From: igoraxz Date: Mon, 1 Jun 2026 10:36:10 +0100 Subject: [PATCH] feat: add matured TaoFlow EMA so emission tracks durable demand The user-flow EMA driving emission allocation reacts symmetrically to inflows and outflows, letting transient capital inflate a subnet's emission share before it is withdrawn. This adds a second EMA on top of the user-flow EMA and uses the lower of the two as the signal: raw = flow EMA of (buys - sells) [existing] slow = flow EMA of raw [new] matured = min(raw, slow) [used in the share calc] Because slow lags raw, the clamp is asymmetric: Inflow spike: raw rises above slow -> matured = slow (credit accrues slowly) Outflow: raw drops below slow -> matured = raw (applies immediately) Steady flow: raw ~= slow -> matured ~= raw (no penalty) Transient inflows are credited only at the slow EMA's pace while withdrawals take effect immediately; durable sustained inflow still converges to the full signal. Design notes: - No new parameter: slow reuses FlowEmaSmoothingFactor. A sweep of the maturity half-life against both manipulation resistance and honest-subnet bootstrap time put the optimum at the same half-life as the main flow EMA (the knee of the trade-off); equal factors make slow a clean double-EMA of the flow. - SubnetEmaSlowTaoFlow stores the unclamped EMA(raw), not min(raw, slow), so the slow EMA tracks the true long-run signal and can recover; the clamp is applied only at read time. - On first access the slow EMA seeds at the current raw EMA (no emission cliff at deployment) with last_block = 0, so the update branch runs and persists; the first update is a no-op (slow = raw). - Protocol EMA is not matured -- only user demand is delayed. Builds on the normalized protocol cost change (#2675). Co-Authored-By: Claude Opus 4.6 (1M context) --- pallets/subtensor/src/coinbase/root.rs | 1 + .../src/coinbase/subnet_emissions.rs | 64 +++++++++++++++++-- pallets/subtensor/src/lib.rs | 14 +++- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b64043a4f5..aadb582c18 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -298,6 +298,7 @@ impl Pallet { SubnetMovingPrice::::remove(netuid); SubnetTaoFlow::::remove(netuid); SubnetEmaTaoFlow::::remove(netuid); + SubnetEmaSlowTaoFlow::::remove(netuid); SubnetProtocolFlow::::remove(netuid); SubnetEmaProtocolFlow::::remove(netuid); SubnetExcessTao::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 63dbf36e5a..a73ba7657d 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -145,6 +145,49 @@ impl Pallet { } } + /// Compute the slow EMA of the raw user-flow EMA (second smoothing layer). + /// + /// Reuses the main `FlowEmaSmoothingFactor` rather than introducing a separate + /// maturity factor. A parameter sweep of the maturity half-life against both + /// manipulation resistance and honest-subnet bootstrap time found the best + /// balance at the same half-life as the main flow EMA: it sits at the knee of + /// the trade-off (shorter weakens the clamp; longer barely improves resistance + /// while slowing new-subnet onboarding). Equal factors also make the slow layer + /// a clean double-EMA of the flow, which is the simplest behaviour to reason + /// about and govern. + /// + /// This stores EMA(raw), NOT the clamped min(raw, slow). The clamp is applied + /// at read time in `get_shares_flow`. Storing the unclamped slow EMA ensures it + /// tracks the true long-run raw signal rather than the clamped value. + /// + /// On first access for a subnet, the slow EMA initializes to the current raw EMA, + /// so existing subnets do not face an emission cliff at deployment. + fn get_slow_ema_flow(netuid: NetUid, raw_ema: I64F64) -> I64F64 { + let current_block: u64 = Self::get_current_block_as_u64(); + + // First access: seed the slow EMA at the current raw EMA (so no emission + // cliff at deployment) with last_block = 0. On any normal block + // (current_block != 0) the update branch then runs and persists the value; + // the first update is a no-op (slow = raw) and subsequent blocks smooth + // normally. (At genuine block 0 this would skip persistence, but subnets do + // not emit at genesis.) + let (last_block, last_slow_ema) = + SubnetEmaSlowTaoFlow::::get(netuid).unwrap_or((0, raw_ema)); + + if last_block != current_block { + let flow_alpha = I64F64::saturating_from_num(FlowEmaSmoothingFactor::::get()) + .safe_div(I64F64::saturating_from_num(i64::MAX)); + let one = I64F64::saturating_from_num(1); + let slow_ema = (one.saturating_sub(flow_alpha)) + .saturating_mul(last_slow_ema) + .saturating_add(flow_alpha.saturating_mul(raw_ema)); + SubnetEmaSlowTaoFlow::::insert(netuid, (current_block, slow_ema)); + slow_ema + } else { + last_slow_ema + } + } + // Either the minimal EMA flow L = min{Si}, or an artificial // cut off at some higher value A (TaoFlowCutoff) // L = max {A, min{min{S[i], 0}}} @@ -245,20 +288,27 @@ impl Pallet { let net_flow_enabled = NetTaoFlowEnabled::::get(); let zero = I64F64::saturating_from_num(0); - // Always update both EMAs (keeps protocol EMA warm for when toggled on). + // Always update all EMAs (keeps protocol/slow EMAs warm for when toggled on). // Fixes #2667: protocol EMA accumulator was only drained when enabled, // causing a shock on toggle. + // + // matured = min(raw, slow): a second EMA smoothing layer (slow EMA of the raw + // flow EMA) that delays emission credit from inflow spikes (raw rises before + // slow) while applying outflows immediately (raw falls below slow). This makes + // emission share track durable demand rather than transient flow. let subnet_emas: Vec<(NetUid, I64F64, I64F64)> = subnets_to_emit_to .iter() .map(|netuid| { - let user_ema = Self::get_ema_flow(*netuid); + let raw_user_ema = Self::get_ema_flow(*netuid); + let slow_user_ema = Self::get_slow_ema_flow(*netuid, raw_user_ema); + let matured_user_ema = raw_user_ema.min(slow_user_ema); let protocol_ema = Self::update_ema_protocol_flow(*netuid); - (*netuid, user_ema, protocol_ema) + (*netuid, matured_user_ema, protocol_ema) }) .collect(); // When net flow is enabled, normalize protocol EMA so that its - // positive total matches the user EMA positive total. This prevents + // positive total matches the matured user EMA positive total. This prevents // subsidy concentration: as emissions concentrate on fewer subnets, // their protocol EMA grows, but the normalization factor shrinks to // compensate, keeping the deduction proportional to user demand. @@ -287,7 +337,7 @@ impl Pallet { let ema_flows: BTreeMap = subnet_emas .into_iter() - .map(|(netuid, user_ema, protocol_ema)| { + .map(|(netuid, matured_user_ema, protocol_ema)| { let net = if net_flow_enabled { // Only scale positive protocol cost by norm_factor. Negative // protocol cost (root drain > emissions) is a benefit, kept as-is. @@ -296,9 +346,9 @@ impl Pallet { } else { protocol_ema }; - user_ema.saturating_sub(scaled_protocol) + matured_user_ema.saturating_sub(scaled_protocol) } else { - user_ema + matured_user_ema }; (netuid, net) }) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 5b670713bc..ee1d52a9f7 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1622,11 +1622,18 @@ pub mod pallet { pub type SubnetTaoFlow = StorageMap<_, Identity, NetUid, i64, ValueQuery, DefaultZeroI64>; - /// --- MAP ( netuid ) --> subnet_ema_tao_flow | Returns the EMA of TAO inflow-outflow balance. + /// --- MAP ( netuid ) --> subnet_ema_tao_flow | Returns the EMA of TAO inflow-outflow balance (raw user flow). #[pallet::storage] pub type SubnetEmaTaoFlow = StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + /// --- MAP ( netuid ) --> subnet_ema_slow_tao_flow | Slow EMA of the raw user flow EMA (second + /// smoothing layer). Used for the maturity clamp matured = min(raw, slow). Stores EMA(raw), + /// NOT min(raw, slow). + #[pallet::storage] + pub type SubnetEmaSlowTaoFlow = + StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + /// --- ITEM --> net_tao_flow_enabled | When true, emission shares use net flow (user - protocol). When false, uses gross user flow only. #[pallet::type_value] pub fn DefaultNetTaoFlowEnabled() -> bool { @@ -1678,7 +1685,10 @@ pub mod pallet { 216_000 } #[pallet::storage] - /// --- ITEM --> Flow EMA smoothing factor (flow alpha), u64 normalized + /// --- ITEM --> Flow EMA smoothing factor (flow alpha), u64 normalized. + /// Used for both the raw flow EMA and the maturity (slow) EMA on top of it + /// (see `get_slow_ema_flow`): evaluation showed the optimal maturity half-life + /// equals the main flow half-life, so a single factor is reused. pub type FlowEmaSmoothingFactor = StorageValue<_, u64, ValueQuery, DefaultFlowEmaSmoothingFactor>;