Skip to content

Conviction v2#2658

Open
gztensor wants to merge 20 commits into
devnet-readyfrom
feat/conviction-v2
Open

Conviction v2#2658
gztensor wants to merge 20 commits into
devnet-readyfrom
feat/conviction-v2

Conversation

@gztensor
Copy link
Copy Markdown
Contributor

@gztensor gztensor commented May 12, 2026

Description

This PR implements the 2nd iteration of stake locks (conviction).

Implements an exponential stake lock mechanism where a coldkey can lock alpha stake to one hotkey per subnet, building conviction for that hotkey over time. A coldkey may have only one active lock per subnet; top-ups must target the same hotkey.

Lock state is stored as (locked_mass, conviction, last_update) for (coldkey, netuid, hotkey). Aggregate conviction is tracked separately through:

  • HotkeyLock(netuid, hotkey) for non-owner perpetual coldkey aggregate locks.
  • DecayingLock(netuid, hotkey) for non-owner decaying coldkey aggregate locks.
  • OwnerLock(netuid) for the subnet owner coldkey’s aggregate lock.
  • DecayingLock(netuid) as an optional flag controlling whether subnet owner locked mass decays.

Lock state uses lazy evaluation: stored values are rolled forward only when lock-aware code touches them.

Roll-forward uses two independent exponential timescales:

  • UnlockRate: controls decay of locked mass.
  • MaturityRate: controls conviction maturation/decay.

Defaults:

  • UnlockRate is set so 90% of locked mass unlocks in 365.25 days at 12s blocks.
  • MaturityRate is 20% slower than UnlockRate.

Current non-owner roll-forward formula follows the closed-form exponential model:

decay_x = exp(-dt / UnlockRate)
decay_z = exp(-dt / MaturityRate)

locked_mass_1 = locked_mass_0 * decay_x

if UnlockRate == MaturityRate:
    gamma = (dt / MaturityRate) * decay_z
else:
    gamma = UnlockRate * (decay_x - decay_z) / (UnlockRate - MaturityRate)

conviction_1 = decay_z * conviction_0 + gamma * locked_mass_0

For subnet owner coldkey locks, conviction is immediate: after roll-forward, conviction is set to current locked_mass. If PerpetualLock(netuid) is enabled, subnet owner locked mass does not decay, while conviction still matures to locked mass.

The new extrinsic lock_stake creates or tops up a lock. If a coldkey already has a lock on the subnet, the hotkey must match. Top-up rolls the existing lock forward first, adds to locked_mass, and preserves rolled conviction. Fresh non-owner locked mass starts with zero added conviction; owner coldkey locks receive immediate owner conviction.

The new extrinsic set_perpetual_lock lets the lock owner coldkey toggle DecayingLock for their lock, enabling or clearing decaying owner locked-mass behavior. The default value is false, so the locks are perpetual by default for everyone.

There is no separate unlock queue/state. Locked mass decays over time. When locked mass and conviction decay to zero, cleanup removes the lock row and the matching aggregate row when applicable.

Unstake protection is enforced through available_to_unstake: a coldkey can unstake only alpha above the rolled locked amount on that subnet. Lock-aware stake transitions check this so locked alpha cannot leave the locked subnet through move/swap-style operations.

Same-subnet transfer between coldkeys is lock-aware: if transferred alpha exceeds the sender’s unlocked portion, the locked portion follows the stake to the destination coldkey. The hotkey remains the same. Conviction is moved proportionally to the moved locked alpha using fixed-point proportional math.

Small nomination cleanup can force-reduce a lock. The removed alpha reduces locked mass and reduces conviction proportionally. The aggregate lock is reduced by the same locked mass and conviction delta, using OwnerLock for subnet owner coldkey locks and HotkeyLock otherwise.

Subnet owner reassignment is conviction-aware. Once the subnet is old enough (1 year old) and enough alpha is locked (at least 10% of SubnetAlphaOut), the subnet king is selected from aggregate conviction. If ownership changes, aggregate lock state is moved between OwnerLock and HotkeyLock so the new owner’s aggregate is tracked as owner conviction and the old owner’s aggregate becomes a normal hotkey aggregate.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Other (please describe):

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have run ./scripts/fix_rust.sh to ensure my code is formatted and linted correctly
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@gztensor gztensor marked this pull request as draft May 12, 2026 22:12
@gztensor gztensor added the skip-cargo-audit This PR fails cargo audit but needs to be merged anyway label May 12, 2026
@gztensor gztensor marked this pull request as ready for review May 14, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-cargo-audit This PR fails cargo audit but needs to be merged anyway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants