From 4394f7d418bdd05d75dab052bb1fb56573807c95 Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Thu, 4 Jun 2026 21:56:19 +0100 Subject: [PATCH] update autolock hyperparam --- .../conviction-staking.md | 117 +++++++++--------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/docs/staking-and-delegation/conviction-staking.md b/docs/staking-and-delegation/conviction-staking.md index aee52c4e..b0756567 100644 --- a/docs/staking-and-delegation/conviction-staking.md +++ b/docs/staking-and-delegation/conviction-staking.md @@ -49,7 +49,6 @@ where: In decaying mode, both the locked mass and conviction decay toward zero, but they follow different curves. Starting from a fresh lock ($c_0 = 0$), conviction first rises as the lock accumulates maturation time, then falls as the mass erodes. The formula (when `UnlockRate` = `MaturityRate` = τ) is: - $$c_1 = e^{-\Delta t / \tau} \left( c_0 + m \cdot \frac{\Delta t}{\tau} \right)$$ $$m_1 = m \cdot e^{-\Delta t / \tau}$$ @@ -65,13 +64,13 @@ Switching to perpetual mode stops the mass decay and allows conviction to grow t **Perpetual mode** (fresh lock of 100 alpha, $c_0 = 0$): | Elapsed | Locked mass | Conviction | -|---|---|---| -| 0 | 100 | 0 | -| 0.5τ | 100 | 39.3 | -| 1τ | 100 | 63.2 | -| 2τ | 100 | 86.5 | -| 2.3τ | 100 | ~90 | -| 3τ | 100 | 95.0 | +| ------- | ----------- | ---------- | +| 0 | 100 | 0 | +| 0.5τ | 100 | 39.3 | +| 1τ | 100 | 63.2 | +| 2τ | 100 | 86.5 | +| 2.3τ | 100 | ~90 | +| 3τ | 100 | 95.0 | Conviction closes in on the locked mass; maximum conviction equals the locked mass. @@ -108,13 +107,13 @@ Conviction is always closing in on `m`, getting closer every block, never quite **Decaying mode** (fresh lock of 100 alpha, $c_0 = 0$, `UnlockRate` = `MaturityRate` = τ): -| Elapsed | Locked mass | Conviction | -|---|---|---| -| 0 | 100 | 0 | -| 0.5τ | 60.7 | 30.3 | -| 1τ | 36.8 | **36.8 (peak)** | -| 2τ | 13.5 | 27.1 | -| 3τ | 5.0 | 14.9 | +| Elapsed | Locked mass | Conviction | +| ------- | ----------- | --------------- | +| 0 | 100 | 0 | +| 0.5τ | 60.7 | 30.3 | +| 1τ | 36.8 | **36.8 (peak)** | +| 2τ | 13.5 | 27.1 | +| 3τ | 5.0 | 14.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. @@ -143,12 +142,19 @@ The term `(dt/τ) × exp(-dt/τ)` is maximized at `dt = τ` (value = `1/e ≈ 0. - ## Subnet owner auto-locking -When a subnet owner receives their distribution cut each epoch, it is **not automatically locked** by default. The owner can opt in to auto-locking, which locks the distribution cut to the subnet owner's hotkey each epoch. 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. +When a subnet owner receives their distribution cut each epoch, it is **not automatically locked** by default. The owner can opt in to auto-locking by modifying the `owner_cut_auto_lock_enabled` hyperparameter on the subnet. To do this, run the following command in your terminal: + +```sh +btcli sudo set --param owner_cut_auto_lock_enabled --value true --netuid NETUID +``` + +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. -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. +:::info +Once enabled, 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. +::: ## Key swap behavior @@ -171,7 +177,6 @@ Conviction locks provide no protection against deregistration. The deregistratio If a subnet is deregistered, conviction lock records for that subnet are deleted before the standard subnet dissolution process runs. The underlying staked alpha is then handled the same way as any other stake on a deregistered subnet: it is converted to TAO pro-rata via the subnet's AMM pool and returned to each coldkey's free balance. Accumulated conviction is gone. - ## Querying conviction @@ -197,20 +202,20 @@ The following methods on `bittensor.Subtensor` read chain state and do not submi **`LockState`** is a TypedDict returned by the lock query methods: -| Field | Type | Description | -|---|---|---| -| `locked_mass` | `Balance` | Locked amount in subnet alpha units | -| `conviction` | `float` | Matured conviction score | -| `last_update` | `int` | Block number of the last stored checkpoint | - -| Method | Returns | Description | -|---|---|---| -| `get_coldkey_lock(coldkey_ss58, netuid)` | `Optional[LockState]` | Current lock state with decay applied, or `None` if no lock exists | -| `get_stake_lock(coldkey_ss58, netuid, hotkey_ss58)` | `Optional[LockState]` | Raw stored lock checkpoint without decay applied | -| `get_stake_locks(coldkey_ss58, netuid)` | `list[tuple[str, LockState]]` | All locks for a coldkey on a subnet as (hotkey_ss58, LockState) pairs | -| `is_perpetual_lock(coldkey_ss58, netuid)` | `bool` | `True` if the lock does not decay; `False` if it is in decaying mode | -| `get_hotkey_conviction(hotkey_ss58, netuid)` | `float` | Total conviction for a hotkey, summed over all coldkeys that have locked to it | -| `get_most_convicted_hotkey_on_subnet(netuid)` | `Optional[str]` | SS58 address of the hotkey with the highest conviction, or `None` if no locks exist | +| Field | Type | Description | +| ------------- | --------- | ------------------------------------------ | +| `locked_mass` | `Balance` | Locked amount in subnet alpha units | +| `conviction` | `float` | Matured conviction score | +| `last_update` | `int` | Block number of the last stored checkpoint | + +| Method | Returns | Description | +| --------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------- | +| `get_coldkey_lock(coldkey_ss58, netuid)` | `Optional[LockState]` | Current lock state with decay applied, or `None` if no lock exists | +| `get_stake_lock(coldkey_ss58, netuid, hotkey_ss58)` | `Optional[LockState]` | Raw stored lock checkpoint without decay applied | +| `get_stake_locks(coldkey_ss58, netuid)` | `list[tuple[str, LockState]]` | All locks for a coldkey on a subnet as (hotkey_ss58, LockState) pairs | +| `is_perpetual_lock(coldkey_ss58, netuid)` | `bool` | `True` if the lock does not decay; `False` if it is in decaying mode | +| `get_hotkey_conviction(hotkey_ss58, netuid)` | `float` | Total conviction for a hotkey, summed over all coldkeys that have locked to it | +| `get_most_convicted_hotkey_on_subnet(netuid)` | `Optional[str]` | SS58 address of the hotkey with the highest conviction, or `None` if no locks exist | ```python import bittensor as bt @@ -236,18 +241,17 @@ king = st.get_most_convicted_hotkey_on_subnet(netuid=1) In [Polkadot.js](https://polkadot.js.org/apps/), 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 module | Method | Returns | -|---|---|---| -| RPC calls → `stakeInfo` | `getColdkeyLock(coldkey, netuid)` | The current `LockState` for this coldkey on `netuid`, rolled forward to the current block, or `None` if no lock exists | -| Runtime calls → `stakeInfoRuntimeApi` | `getHotkeyConviction(hotkey, netuid)` | Current total conviction for `hotkey` on `netuid`, summed over all coldkeys that have locked to it | -| Runtime calls → `stakeInfoRuntimeApi` | `getMostConvictedHotkeyOnSubnet(netuid)` | The hotkey with the highest conviction on `netuid`, or `None` if no locks exist | +| Polkadot.js module | Method | Returns | +| ------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| RPC calls → `stakeInfo` | `getColdkeyLock(coldkey, netuid)` | The current `LockState` for this coldkey on `netuid`, rolled forward to the current block, or `None` if no lock exists | +| Runtime calls → `stakeInfoRuntimeApi` | `getHotkeyConviction(hotkey, netuid)` | Current total conviction for `hotkey` on `netuid`, summed over all coldkeys that have locked to it | +| Runtime calls → `stakeInfoRuntimeApi` | `getMostConvictedHotkeyOnSubnet(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. - ## Extrinsics Extrinsics are signed transactions submitted to the Subtensor blockchain. The `api.tx.subtensorModule.*` form below is the raw Polkadot.js encoding used for direct chain interaction. The Python SDK (`bittensor.Subtensor`) provides a wrapper method for each extrinsic that handles wallet signing, submission, and optional MEV Shield encryption. @@ -440,7 +444,6 @@ Reassigns the coldkey's existing lock on `netuid` from its current hotkey to `de - Conviction is **preserved** when both hotkeys are owned by the same coldkey (moving between your own hotkeys). - The locked mass of alpha within the subnet is conserved across the move from one hotkey to another. - **Errors:** - `NoExistingLock`: no lock exists for this coldkey on the subnet @@ -466,26 +469,25 @@ The hotkey with the highest aggregate conviction (`subnet_king`) then becomes th **To monitor readiness via Polkadot.js (Developer → Chain state → `subtensorModule`):** -| Query | What 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 → `stakeInfoRuntimeApi` → `getMostConvictedHotkeyOnSubnet(netuid)` | The hotkey that would currently win ownership | -| Developer → Runtime calls → `stakeInfoRuntimeApi` → `getHotkeyConviction(hotkey, netuid)` | Any hotkey's current aggregate conviction score | - +| Query | What 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 → `stakeInfoRuntimeApi` → `getMostConvictedHotkeyOnSubnet(netuid)` | The hotkey that would currently win ownership | +| Developer → Runtime calls → `stakeInfoRuntimeApi` → `getHotkeyConviction(hotkey, netuid)` | Any hotkey's current aggregate conviction score | ## Storage All six storage items live under **Developer → Chain state → `subtensorModule`** in Polkadot.js. -| Storage item | Keys | Contents | -|---|---|---| -| `lock(coldkey, netuid, hotkey)` | coldkey, netuid, hotkey | Individual per-coldkey lock record (`LockState`) | -| `hotkeyLock(netuid, hotkey)` | netuid, hotkey | Aggregate perpetual lock totals for non-owner hotkeys | -| `decayingHotkeyLock(netuid, hotkey)` | netuid, hotkey | Aggregate decaying lock totals for non-owner hotkeys | -| `ownerLock(netuid)` | netuid | Aggregate perpetual lock total for the subnet owner hotkey | -| `decayingOwnerLock(netuid)` | netuid | Aggregate decaying lock total for the subnet owner hotkey | -| `decayingLock(coldkey, netuid)` | coldkey, netuid | `false` = perpetual mode; absent = decaying (default) | +| Storage item | Keys | Contents | +| ------------------------------------ | ----------------------- | ---------------------------------------------------------- | +| `lock(coldkey, netuid, hotkey)` | coldkey, netuid, hotkey | Individual per-coldkey lock record (`LockState`) | +| `hotkeyLock(netuid, hotkey)` | netuid, hotkey | Aggregate perpetual lock totals for non-owner hotkeys | +| `decayingHotkeyLock(netuid, hotkey)` | netuid, hotkey | Aggregate decaying lock totals for non-owner hotkeys | +| `ownerLock(netuid)` | netuid | Aggregate perpetual lock total for the subnet owner hotkey | +| `decayingOwnerLock(netuid)` | netuid | Aggregate decaying lock total for the subnet owner hotkey | +| `decayingLock(coldkey, netuid)` | coldkey, netuid | `false` = perpetual mode; absent = decaying (default) | Two governance-settable parameters control the time constants: @@ -494,7 +496,6 @@ Two governance-settable parameters control the time constants: Both are adjustable by governance. Query `api.query.subtensorModule.maturityRate()` and `api.query.subtensorModule.unlockRate()` for current values before computing time estimates. - ## 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. @@ -514,6 +515,7 @@ No history. Just a snapshot at a single block. The three fields are sufficient t **The formula** (`calculate_decayed_mass_and_conviction`, `lock.rs`): In perpetual mode (`perpetual_lock = true`): + ```rust let maturity_decay = Self::exp_decay(dt, maturity_rate); // exp(-dt/τ) let new_locked_mass = locked_mass; // unchanged @@ -528,6 +530,7 @@ let new_conviction = ``` In decaying mode (`perpetual_lock = false`), when `unlock_rate == maturity_rate`: + ```rust let unlock_decay = Self::exp_decay(dt, unlock_rate); // exp(-dt/τ) let maturity_decay = Self::exp_decay(dt, maturity_rate); // exp(-dt/τ) [same τ] @@ -543,11 +546,13 @@ let new_conviction = conviction_from_existing + conviction_from_mass; ``` When the two rates differ, the conviction from mass uses the closed-form integral: + ```rust // γ = τ_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`):