Skip to main content
SUBMIT A PRSUBMIT AN ISSUElast edit: May 26, 2026

Conviction and locked stake

The locked stake feature lets coldkey holders lock alpha stake to a specific hotkey on a subnet. Locked stake builds conviction, a score that grows over time toward the locked amount. Conviction provides a public, on-chain signal of long-term commitment that cannot be silently reversed.

Conviction provides information about subnet owners and other large investors in a subnet. A subnet owner whose alpha is locked has made a cryptographic commitment: unwinding a large position requires switching the lock to decaying mode and then waiting through an exponential decay period before the stake is gone. This gives other stakers advance warning before any large exit completes.

Locking stake binds a specific amount of a coldkey's staked alpha on a subnet to a specific delegate (stake recipient) hotkey.

The lock code ensures that total alpha staked by the coldkey on that subnet cannot decrease below the locked amount. Everything above the locked amount is freely unstakable.

The coldkey can also continue to stake additional alpha at any time: the lock only blocks the staked balance from dropping below the locked amount.

Conviction increases over time toward the amount of locked stake, following an exponential curve so it slows as it approaches the limit value of the locked amount.

Decaying and perpetual modes

By default, the locked amount decreases or 'decays' over time along an exponential curve, freeing up more of the originally locked amount to potentially be unstaked.

Because conviction will rise toward the locked amount, while the locked amount itself falls, over time, conviction will peak somewhere in the middle and then start to fall again.

The locked amount reaches zero (freeing all stake) with no explicit action needed.

A locked amount can also be set to perpetual so that it will never decreas.

The mode, decaying or perpetual, is per-coldkey per-subnet and can be changed at any time. Switching from perpetual to decaying initiates the decay process immediately from the current locked mass.

One lock per coldkey per subnet is enforced. If a lock already exists for a coldkey on a subnet, additional lock_stake calls top up the locked amount (provided the hotkey matches the existing lock).

The conviction score

The conviction score grows over time, from zero toward the locked amount. In perpetual mode it follows an exponential curve:

c1=m(mc0)eΔt/τc_1 = m - (m - c_0) \cdot e^{-\Delta t / \tau}

where:

  • c0c_0: conviction at last update
  • c1c_1: conviction now
  • mm: locked mass (alpha units)
  • Δt\Delta t: blocks elapsed since last update
  • τ\tau: maturity time constant (MaturityRate, a governance-settable on-chain value; query the chain for the current value)

In decaying mode, both the locked mass and conviction decay toward zero, but they follow different curves. Starting from a fresh lock (c0=0c_0 = 0), conviction first rises as the lock accumulates maturation time, then falls as the mass erodes. The formula (when UnlockRate = MaturityRate = τ, the default) is:

c1=eΔt/τ(c0+mΔtτ)c_1 = e^{-\Delta t / \tau} \left( c_0 + m \cdot \frac{\Delta t}{\tau} \right)

m1=meΔt/τm_1 = m \cdot e^{-\Delta t / \tau}

Switching to perpetual mode stops the mass decay and allows conviction to grow toward the full locked amount.

90% conviction (perpetual mode) is reached at approximately 2.3τ2.3\tau blocks. At one time constant τ\tau, conviction is at 63.2% of locked mass.

Query for current time constants

MaturityRate and UnlockRate are governance-settable on-chain storage values. The specific block counts and day estimates depend on the current on-chain values. Query api.query.subtensorModule.maturityRate() and api.query.subtensorModule.unlockRate() on the live chain before relying on any specific number.

Perpetual mode (fresh lock of 100 alpha, c0=0c_0 = 0):

ElapsedLocked massConviction
01000
0.5τ10039.3
10063.2
10086.5
2.3τ100~90
10095.0

Conviction closes in on the locked mass; maximum conviction equals the locked mass.

See how it's calculated

Closing a gap between current conviction and the target (locked mass):

gap  = m - c0
c1 = m - gap × exp(-dt/τ)

exp(-dt/τ) is the fraction of the gap that remains after dt blocks.

  • dt = 0exp(0) = 1 → gap unchanged → c1 = c0 ✓
  • dt = τexp(-1) ≈ 0.368 → 36.8% of gap remains → 63.2% closed
  • dt → ∞exp(-∞) = 0 → gap gone → c1 = m ✓

Starting from c0 = 0 (fresh lock of 100 alpha, perpetual mode):

gap = 100
at τ: c1 = 100 - 100 × 0.368 = 63.2
at 2τ: c1 = 100 - 100 × 0.135 = 86.5
at 3τ: c1 = 100 - 100 × 0.050 = 95.0

Conviction is always closing in on m, getting closer every block, never quite arriving.

Perpetual mode conviction diagram

Decaying mode (fresh lock of 100 alpha, c0=0c_0 = 0, UnlockRate = MaturityRate = τ):

ElapsedLocked massConviction
01000
0.5τ60.730.3
36.836.8 (peak)
13.527.1
5.014.9

Conviction peaks at ~36.8% of the original locked mass at elapsed time = τ. After that both values fall toward zero. Note that once elapsed time exceeds τ, conviction exceeds the remaining locked mass; it reflects accumulated commitment, not just current holdings. Topping up an existing lock adds to locked mass immediately, conviction continuing from its current value.

Decaying mode conviction diagram

See how it's calculated

When UnlockRate = MaturityRate = τ, conviction is the accumulated area under the decaying lock curve:

c1 = exp(-dt/τ) × (c0 + m × dt/τ)
m1 = m × exp(-dt/τ)

Starting from c0 = 0 (fresh lock of 100 alpha, decaying mode):

at 0.5τ:  m1 = 60.7,  c1 = 100 × 0.5 × exp(-0.5) = 30.3
at τ: m1 = 36.8, c1 = 100 × 1.0 × exp(-1) = 36.8 ← peak
at 2τ: m1 = 13.5, c1 = 100 × 2.0 × exp(-2) = 27.1
at 3τ: m1 = 5.0, c1 = 100 × 3.0 × exp(-3) = 14.9

The term (dt/τ) × exp(-dt/τ) is maximized at dt = τ (value = 1/e ≈ 0.368). Conviction represents accumulated commitment, not current holdings; after τ has elapsed, conviction exceeds the remaining locked mass.

Subnet owner auto-locking

When a subnet owner receives their distribution cut each epoch, it is automatically locked to the subnet owner's hotkey by default. If the owner already has a lock, the auto-lock tops it up using the existing lock's hotkey. If no lock exists, the auto-lock targets the subnet owner's hotkey.

Auto-locking is enabled per-subnet by default and can be disabled by the subnet owner or root via sudo_set_owner_cut_auto_lock_enabled (admin-utils pallet).

Any lock targeting the subnet owner's hotkey instantly matures conviction to the locked amount. This applies to any coldkey locking to the subnet owner's hotkey, not just the owner locking to themselves. The trigger is the target hotkey, not the locking coldkey.

Key swap behavior

Hotkey swap (system-level): When a hotkey is swapped via btcli wallet swap-hotkey, all locks targeting the old hotkey are transferred to the new hotkey. Conviction is not reset, because the same coldkey owns both hotkeys.

Coldkey swap: A coldkey swap fails if the destination coldkey already has active locked mass on any subnet. The swap succeeds if the destination coldkey only has expired or zero-mass locks.

Transferring locked stake

When stake is moved to another coldkey within the same subnet, lock obligations follow the alpha proportionally. The runtime resolves how much of the transfer carries lock state:

  1. Freely available alpha transfers first: alpha above the locked amount moves with no lock implications.
  2. Locked alpha is drawn next: if the transfer exceeds freely available alpha, the remainder comes from locked mass. Conviction transfers proportionally with the locked amount. This step fails with LockHotkeyMismatch if the destination coldkey already has a lock pointing at a different hotkey.

Cross-subnet moves are different: moving stake between subnets goes through unstake → TAO transfer → restake, which must satisfy the lock constraint. You cannot move locked alpha across subnets directly.

Querying conviction

In Polkadot.js, go to Developer → RPC calls and select the stakeInfo module to call getColdkeyLock. For the other two methods, go to Developer → Runtime calls and select the stakeInfoRuntimeApi module.

Polkadot.js moduleMethodReturns
RPC calls → stakeInfogetColdkeyLock(coldkey, netuid)The current LockState for this coldkey on netuid, rolled forward to the current block, or None if no lock exists
Runtime calls → stakeInfoRuntimeApigetHotkeyConviction(hotkey, netuid)Current total conviction for hotkey on netuid, summed over all coldkeys that have locked to it
Runtime calls → stakeInfoRuntimeApigetMostConvictedHotkeyOnSubnet(netuid)The hotkey with the highest conviction on netuid, or None if no locks exist

Conviction is a rolling value: querying at different blocks yields different results as time passes and the exponential evolves.

Storage

All six storage items live under Developer → Chain state → subtensorModule in Polkadot.js.

Storage itemKeysContents
lock(coldkey, netuid, hotkey)coldkey, netuid, hotkeyIndividual per-coldkey lock record (LockState)
hotkeyLock(netuid, hotkey)netuid, hotkeyAggregate perpetual lock totals for non-owner hotkeys
decayingHotkeyLock(netuid, hotkey)netuid, hotkeyAggregate decaying lock totals for non-owner hotkeys
ownerLock(netuid)netuidAggregate perpetual lock total for the subnet owner hotkey
decayingOwnerLock(netuid)netuidAggregate decaying lock total for the subnet owner hotkey
decayingLock(coldkey, netuid)coldkey, netuidfalse = perpetual mode; absent = decaying (default)

Two governance-settable parameters control the time constants:

  • MaturityRate: time constant τ (in blocks) for conviction growth in perpetual mode. Query on-chain for the current value.
  • UnlockRate: time constant τ (in blocks) for locked mass decay in decaying mode. Query on-chain for the current value.

Both are adjustable by governance. Query api.query.subtensorModule.maturityRate() and api.query.subtensorModule.unlockRate() for current values before computing time estimates.

Subnet ownership changes

Not yet active

The ownership transfer function (change_subnet_owner_if_needed) is implemented in Subtensor codebase, but is currently commented out, so it is not active and enabling it will require a runtime upgrade.

When activated, ownership transfers automatically at the end of each block's coinbase run if two conditions hold simultaneously:

  1. The subnet is at least one year old (≥ 7,200 × 365 + 1,800 blocks from networkRegisteredAt)
  2. Total aggregate conviction across all locks on the subnet ≥ 10% of SubnetAlphaOut

The hotkey with the highest aggregate conviction (subnet_king) then becomes the subnet owner hotkey, and that hotkey's owning coldkey becomes the subnet owner.

To monitor readiness via Polkadot.js (Developer → Chain state → subtensorModule):

QueryWhat it tells you
networkRegisteredAt(netuid)Block the subnet was created; add 2,629,800 to get the one-year threshold
subnetAlphaOut(netuid)Total outstanding alpha; 10% of this is the conviction threshold
Developer → Runtime calls → stakeInfoRuntimeApigetMostConvictedHotkeyOnSubnet(netuid)The hotkey that would currently win ownership
Developer → Runtime calls → stakeInfoRuntimeApigetHotkeyConviction(hotkey, netuid)Any hotkey's current aggregate conviction score

Extrinsics

lock_stake

api.tx.subtensorModule.lockStake(hotkey, netuid, amount)

Locks amount alpha from the coldkey's stake on netuid to hotkey.

  • If no lock exists for this coldkey on netuid, a new lock is created with conviction 0.
  • If a lock already exists, amount is added to the locked mass. The hotkey must match the existing lock. Use move_lock first if switching hotkeys.
  • amount must not exceed the coldkey's total alpha staked on the subnet.
  • Locked alpha continues to earn staking rewards normally.
  • New locks are decaying by default. Call set_perpetual_lock(true) after locking to opt into perpetual mode.

Errors:

  • InsufficientStakeForLock: available alpha is less than amount
  • LockHotkeyMismatch: a lock exists for a different hotkey on this subnet
  • AmountTooLow: amount is zero

Event emitted: StakeLocked { coldkey, hotkey, netuid, amount }

set_perpetual_lock

api.tx.subtensorModule.setPerpetualLock(netuid, enabled)

Sets or clears perpetual lock mode for the coldkey's lock on netuid.

  • enabled = true: the coldkey's locked mass no longer decays. Conviction can grow toward the full locked amount.
  • enabled = false: the coldkey's locked mass resumes decaying. This is how you initiate an exit from a lock; the mass decays exponentially over time according to UnlockRate.

Switching modes rolls the lock forward to the current block first, so no mass or conviction is lost in the transition.

Switching to decaying mode is public

Calling set_perpetual_lock(false) emits the PerpetualLockUpdated event on-chain immediately. This is by design: the decay period exists specifically so that other stakers can observe the signal and act accordingly. A switch to decaying mode by a subnet owner should be interpreted as a potential intent to reduce their position.

Event emitted: PerpetualLockUpdated { coldkey, netuid, enabled }

move_lock

api.tx.subtensorModule.moveLock(destination_hotkey, netuid)

Reassigns the coldkey's existing lock on netuid from its current hotkey to destination_hotkey.

  • Conviction resets to zero when the old and new hotkeys are owned by different coldkeys.
  • Conviction is preserved when both hotkeys are owned by the same coldkey (moving between your own hotkeys).
  • The locked mass is preserved in both cases.

When moving to a different-coldkey hotkey, conviction resets to zero, giving the previous hotkey's stakers a window to react before conviction rebuilds.

Errors:

  • NoExistingLock: no lock exists for this coldkey on the subnet

Event emitted: LockMoved { coldkey, origin_hotkey, destination_hotkey, netuid }

Locking does not affect emissions

Locking stake does not change the amount of emissions you receive. Emissions are determined by stake weight and consensus participation. Conviction is a governance/signaling mechanism only.

Appendix: implementation

The conviction formula is closed-form with no iteration or history. The runtime stores only a checkpoint at the last mutation and evaluates forward on demand.

What's stored (LockState, lock.rs):

pub struct LockState {
pub locked_mass: AlphaBalance, // constant in perpetual mode; decays in decaying mode
pub conviction: U64F64, // c0: conviction at last_update
pub last_update: u64, // block number of last write
}

No history. Just a snapshot at a single block. The three fields are sufficient to reconstruct lock state at any future block.

The formula (calculate_decayed_mass_and_conviction, lock.rs):

In perpetual mode (perpetual_lock = true):

let maturity_decay = Self::exp_decay(dt, maturity_rate);  // exp(-dt/τ)
let new_locked_mass = locked_mass; // unchanged
let new_conviction =
maturity_decay.saturating_mul(conviction) // c0 × exp(-dt/τ)
.saturating_add(
mass_fixed.saturating_mul( // + m × (1 - exp(-dt/τ))
U64F64::from(1).saturating_sub(maturity_decay)
)
);
// = m - (m - c0) × exp(-dt/τ)

In decaying mode (perpetual_lock = false), when unlock_rate == maturity_rate (the default, both are the same on-chain value):

let unlock_decay = Self::exp_decay(dt, unlock_rate);    // exp(-dt/τ)
let maturity_decay = Self::exp_decay(dt, maturity_rate); // exp(-dt/τ) [same τ]
let new_locked_mass = unlock_decay.saturating_mul(mass_fixed); // m × exp(-dt/τ)
let conviction_from_existing = maturity_decay.saturating_mul(conviction); // c0 × exp(-dt/τ)
let dt_fixed = U64F64::from(dt);
let tau_fixed = U64F64::from(maturity_rate);
let conviction_from_mass = mass_fixed.saturating_mul(
dt_fixed.safe_div(tau_fixed).saturating_mul(maturity_decay) // m × (dt/τ) × exp(-dt/τ)
);
let new_conviction = conviction_from_existing + conviction_from_mass;
// = exp(-dt/τ) × (c0 + m × dt/τ)

When the two rates differ, the conviction from mass uses the closed-form integral:

// γ = τ_unlock × (exp(-dt/τ_unlock) - exp(-dt/τ_maturity)) / (τ_unlock - τ_maturity)
let gamma = tau_x.saturating_mul(decay_delta).checked_div(tau_delta);
let conviction_from_mass = mass_fixed.saturating_mul(gamma.max(0));

This is the analytic solution to the convolution of the decaying mass with the maturity kernel exp(-t/τ_maturity)/τ_maturity.

Owner lock special case (roll_forward_lock, lock.rs):

if owner_lock {
rolled.conviction = U64F64::from(rolled.locked_mass); // instant full conviction
}

Owner locks targeting the subnet owner's own hotkey always have conviction == locked_mass, regardless of elapsed time.

On-demand evaluation (roll_forward_lock, lock.rs):

Every mutation (lock_stake, set_perpetual_lock, move_lock) calls roll_forward_lock first, advancing all values to the current block and writing them as the new checkpoint. From that point, (locked_mass, conviction, last_update) is sufficient to evaluate state at any future block without history.