Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ jobs:
wasm-pack --version
- name: wasm smoke test (node)
run: |
wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node
wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node --no-opt
node sdk/bittensor-core-wasm/tests/smoke.mjs
- name: Report wasm compiler cache
if: always()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ sdk/python/dist/
.github/scripts/deploy/proxy_proxy_blob.hex
.github/scripts/deploy/pending-release.json
.github/scripts/deploy/deployment-multisig-proposal.hex

# Local historical alpha-accounting scanner and PostgreSQL data
.alpha-accounting-scanner/
232 changes: 232 additions & 0 deletions docs/plans/alpha-imbalance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
---
title: Alpha imbalance accounting
description: Plan for linear alpha imbalance accounting across issuance and supply-moving paths.
---

# Alpha imbalance accounting

## Goal

Use linear alpha imbalances across issuance and supply-moving paths in the same
way TAO uses credits. Minted alpha must exist as a short-lived credit until it
is resolved into an accounting bucket or explicitly recycled. This makes
unallocated issuance visible in the type system and prevents silent supply
drift.

This work does not change root stake accounting. Root stake is TAO-backed and
is outside the per-subnet alpha-assets system.

## Accounting contract

At transaction and block boundaries, every live non-root subnet should satisfy:

```text
TotalAlphaIssuance[netuid]
= SubnetAlphaIn[netuid]
+ SubnetAlphaOut[netuid]
+ BalancerAlphaReservoir[netuid]
```

While an imbalance is alive, it represents the temporary difference:

```text
TotalAlphaIssuance
= stored alpha + outstanding credits - outstanding debts
```

The existing burn semantics are preserved:

- `AlphaBurned` is cumulative telemetry and a logical subdivision of
`SubnetAlphaOut`; burning does not reduce issuance.
- `AlphaRecycled` is cumulative telemetry for alpha removed from issuance.
- Recycling reduces `TotalAlphaIssuance`.

All new accounting operations should use checked arithmetic. Saturation must
not conceal an accounting underflow or overflow.

## Current gaps

The current alpha imbalance types are not suitable as conservation resources:

- They implement `Clone` and `Copy`, so a value can be duplicated.
- They can be dropped without finalizing their issuance.
- Their constructors are public.
- They implement a single-currency imbalance trait even though alpha is a
multi-asset system keyed by `netuid`.
- Infallible `merge` and `offset` operations log a netuid mismatch and discard
one side.
- Coinbase currently ignores the pool-side mint record.
- Subnet genesis and registration directly initialize alpha buckets.
- The alpha-assets issuance map is not yet the canonical value used by
`get_alpha_issuance`.

## Phase 1: Linear imbalance primitive

Replace the copyable records with a FRAME-style, multi-asset imbalance:

- Mark it `#[must_use]`.
- Remove `Clone`, `Copy`, codec, metadata, and frozen-structure derives.
- Keep constructors private to alpha-assets.
- Include `netuid` and amount in every imbalance.
- Add drop handlers. Dropping an unresolved credit decreases issuance and
records the amount as recycled; dropping a debt increases issuance.
- Make `merge`, `subsume`, and `offset` fallible when netuids differ.
- Return both original values on a failed combination.
- Add an associated credit type to `AlphaAssetsInterface`.
- Add an explicit settlement operation that consumes an imbalance without
invoking its drop handler after a destination has accepted the alpha.

For compatibility during the staged rollout, raw `burn_alpha(netuid, amount)`
and `recycle_alpha(netuid, amount)` remain temporarily available. They are
removed after source balances can be withdrawn into credits in later phases.

No imbalance may be stored on chain or exposed through runtime metadata.

## Phase 2: Central resolution layer

Add the alpha equivalent of `spend_tao` in the subtensor pallet:

```text
resolve_to_alpha_in(credit, amount)
resolve_to_alpha_out(credit, amount)
resolve_to_alpha_reservoir(credit, amount)

withdraw_from_alpha_in(netuid, amount) -> credit
withdraw_from_alpha_out(netuid, amount) -> credit
withdraw_from_alpha_reservoir(netuid, amount) -> credit
```

Each resolver must:

1. Verify the credit's netuid.
2. Split only the requested amount.
3. update the destination using checked arithmetic.
4. Return the remainder on success.
5. Return the original credit on failure.

Production code should stop directly mutating `SubnetAlphaIn` and
`SubnetAlphaOut` after this layer is available.

## Phase 3: Coinbase and balancer reservoir

Convert the two alpha emission streams:

- Issue the participant emission as a credit and resolve it into
`SubnetAlphaOut`.
- Issue the pool emission as a credit and divide it between active
`SubnetAlphaIn` and `BalancerAlphaReservoir`.

Previously deferred reservoir alpha may become active alongside a new
emission. The correct flow is:

1. Withdraw the old reservoir into a credit.
2. Merge it with the newly issued credit for the same netuid.
3. Resolve the price-active portion into `SubnetAlphaIn`.
4. Resolve the remainder back into the reservoir.

Every error path must either return the credit to its caller or explicitly
recycle it.

## Phase 4: Supply-moving operations

Convert the remaining supply paths:

- TAO-to-alpha swaps withdraw from `AlphaIn` and resolve into `AlphaOut`.
- Alpha-to-TAO swaps withdraw from `AlphaOut` and resolve into `AlphaIn`.
- Stake transfers withdraw and resolve within `AlphaOut`.
- Recycling withdraws a credit and drops or explicitly recycles it.
- Burning withdraws from an individual stake and resolves into the burned
subdivision of `AlphaOut`, preserving issuance.
- Collateral purchase splitting divides one alpha credit between locked stake
and burn.
- Protocol-owned alpha resolves into `AlphaOut` and its
`SubnetProtocolAlpha` subdivision.
- Basket escrow operations use the same withdrawal and resolution helpers.

The main implementation sites are:

- `pallets/subtensor/src/staking/stake_utils.rs`
- `pallets/subtensor/src/staking/recycle_alpha.rs`
- `pallets/subtensor/src/subnets/collateral.rs`
- `pallets/subtensor/src/staking/basket_flush.rs`
- `pallets/subtensor/src/staking/claim_root.rs`

## Phase 5: Genesis, registration, and dissolution

New subnet pool alpha must be issued and resolved into `SubnetAlphaIn`, not
inserted independently of alpha-assets.

Subnet dissolution must:

1. Withdraw `SubnetAlphaIn`, `SubnetAlphaOut`, and reservoir alpha.
2. Merge and recycle the aggregate credits exactly once.
3. Clear subordinate stake, basket, collateral, and protocol records without
recycling them again.
4. Reset alpha-assets state for the dissolved netuid according to the netuid
reuse policy.

Genesis builders, benchmark helpers, and test setup utilities must initialize
alpha-assets issuance whenever they seed alpha buckets.

## Phase 6: Migration and canonical issuance

Before making the alpha-assets map authoritative, run a storage-versioned
migration over live non-root subnets:

```text
issuance = AlphaIn + AlphaOut + BalancerAlphaReservoir
```

The migration must overwrite the alpha-assets value rather than increment it,
because existing reserve and genesis alpha was not necessarily created through
alpha-assets.

The migration requires:

- `pre_upgrade` snapshots of the three accounting buckets.
- A benchmarked bound based on the maximum/live subnet count.
- `post_upgrade` checks for every migrated netuid.
- An explicit policy for stale alpha-assets entries belonging to dissolved
netuids.

After the migration and all production mutations use the resolution layer,
`get_alpha_issuance` can read
`AlphaAssets::total_alpha_issuance(netuid)` directly.

## Tests and rollout gates

### Primitive tests

- Credits and debts cannot be copied or cloned.
- Dropping an unresolved issued credit reverses issuance.
- Settling a credit preserves issuance.
- Split, extract, merge, subsume, and offset conserve the amount.
- Cross-netuid combinations fail and return both values unchanged.
- Zero imbalances do not touch storage.
- Overflow and underflow cannot be hidden by saturation.

### Integration tests

- Coinbase resolves every issued rao.
- Deferred reservoir alpha can become active later without duplication.
- Stake and unstake round trips preserve issuance.
- Burn preserves issuance and recycle reduces it.
- Subnet registration initializes issuance correctly.
- Dissolution leaves zero issuance without double recycling.
- Basket, collateral, protocol-owned alpha, and transaction rollback paths
preserve the boundary invariant.

### Runtime rollout

1. Ship the hardened primitive without changing economic calculations.
2. Run the alpha-assets tracker in shadow mode and compare it with the derived
bucket total.
3. Convert coinbase and reservoir handling.
4. Convert swaps, staking, burn, recycle, collateral, and basket paths.
5. Run the reconciliation migration.
6. Make alpha-assets issuance canonical.
7. Remove legacy raw amount APIs and add a CI guard against direct production
mutations of alpha accounting buckets.

Weights must be regenerated for any extrinsic or hook whose storage accesses
change.
Empty file added docs/plans/locks.md
Empty file.
71 changes: 71 additions & 0 deletions pallets/alpha-assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ impl Imbalance<AlphaBalance> for NegativeAlphaImbalance {
pub trait AlphaAssetsInterface {
fn total_alpha_issuance(netuid: NetUid) -> AlphaBalance;

fn alpha_burned(netuid: NetUid) -> AlphaBalance;

fn clear_alpha_counters(netuid: NetUid);

fn rebase_alpha_counters(
netuid: NetUid,
issuance_offset: AlphaBalance,
burned_offset: AlphaBalance,
recycled_offset: AlphaBalance,
);

fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance;

fn burn_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance;
Expand All @@ -257,6 +268,20 @@ impl AlphaAssetsInterface for () {
AlphaBalance::ZERO
}

fn alpha_burned(_netuid: NetUid) -> AlphaBalance {
AlphaBalance::ZERO
}

fn clear_alpha_counters(_netuid: NetUid) {}

fn rebase_alpha_counters(
_netuid: NetUid,
_issuance_offset: AlphaBalance,
_burned_offset: AlphaBalance,
_recycled_offset: AlphaBalance,
) {
}

fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance {
PositiveAlphaImbalance::new(netuid, amount)
}
Expand Down Expand Up @@ -300,6 +325,35 @@ pub mod pallet {
}

impl<T: pallet::Config> Pallet<T> {
pub fn clear_alpha_counters(netuid: NetUid) {
TotalAlphaIssuance::<T>::remove(netuid);
AlphaBurned::<T>::remove(netuid);
AlphaRecycled::<T>::remove(netuid);
}

pub fn rebase_alpha_counters(
netuid: NetUid,
issuance_offset: AlphaBalance,
burned_offset: AlphaBalance,
recycled_offset: AlphaBalance,
) {
if !issuance_offset.is_zero() {
TotalAlphaIssuance::<T>::mutate(netuid, |issuance| {
*issuance = issuance.saturating_sub(issuance_offset);
});
}
if !burned_offset.is_zero() {
AlphaBurned::<T>::mutate(netuid, |burned| {
*burned = burned.saturating_sub(burned_offset);
});
}
if !recycled_offset.is_zero() {
AlphaRecycled::<T>::mutate(netuid, |recycled| {
*recycled = recycled.saturating_sub(recycled_offset);
});
}
}

pub fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance {
if !amount.is_zero() {
TotalAlphaIssuance::<T>::mutate(netuid, |issuance| {
Expand Down Expand Up @@ -339,6 +393,23 @@ impl<T: pallet::Config> AlphaAssetsInterface for Pallet<T> {
TotalAlphaIssuance::<T>::get(netuid)
}

fn alpha_burned(netuid: NetUid) -> AlphaBalance {
AlphaBurned::<T>::get(netuid)
}

fn clear_alpha_counters(netuid: NetUid) {
Self::clear_alpha_counters(netuid)
}

fn rebase_alpha_counters(
netuid: NetUid,
issuance_offset: AlphaBalance,
burned_offset: AlphaBalance,
recycled_offset: AlphaBalance,
) {
Self::rebase_alpha_counters(netuid, issuance_offset, burned_offset, recycled_offset)
}

fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance {
Self::mint_alpha(netuid, amount)
}
Expand Down
33 changes: 33 additions & 0 deletions pallets/alpha-assets/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ fn recycle_alpha_reduces_total_issuance_saturating_at_zero() {
});
}

#[test]
fn clear_alpha_counters_removes_previous_generation_state() {
new_test_ext().execute_with(|| {
let netuid = NetUid::from(6u16);

AlphaAssets::mint_alpha(netuid, 100u64.into());
AlphaAssets::burn_alpha(netuid, 30u64.into());
AlphaAssets::recycle_alpha(netuid, 20u64.into());
AlphaAssets::clear_alpha_counters(netuid);

assert!(!TotalAlphaIssuance::<Test>::contains_key(netuid));
assert!(!AlphaBurned::<Test>::contains_key(netuid));
assert!(!AlphaRecycled::<Test>::contains_key(netuid));
});
}

#[test]
fn rebase_alpha_counters_preserves_current_generation_deltas() {
new_test_ext().execute_with(|| {
let netuid = NetUid::from(7u16);

TotalAlphaIssuance::<Test>::insert(netuid, AlphaBalance::from(125u64));
AlphaBurned::<Test>::insert(netuid, AlphaBalance::from(47u64));
AlphaRecycled::<Test>::insert(netuid, AlphaBalance::from(31u64));

AlphaAssets::rebase_alpha_counters(netuid, 100u64.into(), 40u64.into(), 30u64.into());

assert_eq!(TotalAlphaIssuance::<Test>::get(netuid), 25u64.into());
assert_eq!(AlphaBurned::<Test>::get(netuid), 7u64.into());
assert_eq!(AlphaRecycled::<Test>::get(netuid), 1u64.into());
});
}

#[test]
fn positive_imbalance_only_merges_with_same_netuid() {
new_test_ext().execute_with(|| {
Expand Down
5 changes: 5 additions & 0 deletions pallets/commitments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,11 @@ impl<T: Config> Pallet<T> {
continue;
};

if commit.header.v.len() != 32 || commit.header.w.len() != 32 {
log::warn!("Invalid TLECiphertext header for {who:?}");
continue;
}

let decrypted_bytes: Vec<u8> =
tld::<TinyBLS381, AESGCMStreamCipherProvider>(commit, sig)
.map_err(|e| {
Expand Down
Loading
Loading